@signalk/nmea0183-signalk
Version:
A node.js/javascript parser for NMEA0183 sentences. Sentences are parsed to Signal K format.
484 lines • 15.8 kB
JavaScript
"use strict";
/**
* Copyright 2016 Signal K and Fabian Tollenaar <fabian@signalk.org>.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const utils = __importStar(require("@signalk/nmea0183-utilities"));
const schema = __importStar(require("@signalk/signalk-schema"));
const ggencoder_1 = require("ggencoder");
const knotsToMs = (v) => parseFloat(utils.transform(v, 'knots', 'ms').toFixed(2));
const degToRad = (v) => utils.transform(v, 'deg', 'rad');
const cToK = (v) => parseFloat(utils.transform(v, 'c', 'k').toFixed(2));
const percentToRatio = (v) => v / 100;
const stateMapping = {
0: 'motoring',
1: 'anchored',
2: 'not under command',
3: 'restricted manouverability',
4: 'constrained by draft',
5: 'moored',
6: 'aground',
7: 'fishing',
8: 'sailing',
9: 'hazardous material high speed',
10: 'hazardous material wing in ground',
14: 'ais-sart',
15: 'default'
};
const msgTypeToTransmitterClass = {
1: 'A',
2: 'A',
3: 'A',
4: 'BASE',
5: 'A',
18: 'B',
19: 'B',
21: 'ATON'
};
// AIS Base Stations (type 4) aren't vessels — they are fixed shore-based
// stations that broadcast position + UTC time. Route them to `atons.` so
// clients don't display them as vessels. There isn't a dedicated context in
// the Signal K spec for base stations, and atons is consistent with how other
// non-vessel AIS targets are handled.
const msgTypeToPrefix = {
1: 'vessels.',
2: 'vessels.',
3: 'vessels.',
4: 'atons.',
5: 'vessels.',
9: 'aircraft.',
18: 'vessels.',
19: 'vessels.',
21: 'atons.',
24: 'vessels.'
};
const specialManeuverMapping = {
0: 'not available',
1: 'not engaged',
2: 'engaged',
3: 'reserved'
};
const beaufortScale = {
0: 'calm, 0-0.2 m/s',
1: 'light air, 0.3-1.5 m/s',
2: 'light breeze, 1.6-3.3 m/s',
3: 'gentle breeze, 3.4-5.4 m/s',
4: 'moderate breeze, 5.5-7.9 m/s',
5: 'fresh breeze, 8-10.7 m/s',
6: 'strong breeze, 10.8-13.8 m/s',
7: 'high wind, 13.9-17.1 m/s',
8: 'gale, 17.2-20.7 m/s',
9: 'strong gale, 20.8-24.4 m/s',
10: 'storm, 24.5-28.4 m/s',
11: 'violent storm, 28.5-32.6 m/s',
12: 'hurricane-force, ≥ 32.7 m/s',
13: 'not available',
14: 'reserved',
15: 'reserved'
};
const statusTable = {
0: 'steady',
1: 'decreasing',
2: 'increasing',
3: 'not available'
};
const precipitationType = {
0: 'reserved',
1: 'rain',
2: 'thunderstorm',
3: 'freezing rain',
4: 'mixed/ice',
5: 'snow',
6: 'reserved',
7: 'not available'
};
const iceTable = {
0: 'no',
1: 'yes',
2: 'reserved',
3: 'not available'
};
// AIS type 5 ETA has only month/day/hour/minute (no year). Returns an ISO string
// using the current UTC year, rolling to next year when the date has already
// passed. Returns undefined when any field is missing (non-type-5 messages) or
// set to the AIS "not available" sentinel (month=0, day=0, hour=24, minute=60).
function toDestinationEtaIso(etaMo, etaDay, etaHr, etaMin) {
const month = Number(etaMo);
const day = Number(etaDay);
const hour = Number(etaHr);
const minute = Number(etaMin);
if (!Number.isInteger(month) ||
!Number.isInteger(day) ||
!Number.isInteger(hour) ||
!Number.isInteger(minute)) {
return undefined;
}
if (month < 1 ||
month > 12 ||
day < 1 ||
day > 31 ||
hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59) {
return undefined;
}
const now = new Date();
const year = now.getUTCFullYear();
const currentYearEta = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0));
if (currentYearEta.getTime() < now.getTime()) {
return new Date(Date.UTC(year + 1, month - 1, day, hour, minute, 0, 0)).toISOString();
}
return currentYearEta.toISOString();
}
const VDM = function (input, session) {
const { sentence, tags } = input;
// AisDecode exposes a large number of optional fields whose types vary per
// AIS message type; cast to a looser bag so downstream bit-math reads as
// naturally as it did in JS. Runtime behaviour is identical.
const data = new ggencoder_1.AisDecode(sentence, session);
const values = [];
if (data.valid === false) {
return null;
}
if (data.mmsi) {
values.push({
path: '',
value: {
mmsi: data.mmsi
}
});
}
if (data.shipname) {
values.push({
path: '',
value: {
name: data.shipname
}
});
}
if (typeof data.sog != 'undefined' && data.sog != 102.3) {
values.push({
path: 'navigation.speedOverGround',
value: utils.transform(data.sog, 'knots', 'ms')
});
}
if (typeof data.cog != 'undefined' && data.cog != 360) {
values.push({
path: 'navigation.courseOverGroundTrue',
value: utils.transform(data.cog, 'deg', 'rad')
});
}
if (typeof data.hdg != 'undefined' && data.hdg != 511) {
values.push({
path: 'navigation.headingTrue',
value: utils.transform(data.hdg, 'deg', 'rad')
});
}
// AIS rate-of-turn (ITU-R M.1371-5, msg types 1/2/3):
// 0 : not turning
// 1..126 / -1..-126 : turning right/left, ROT_deg_per_min = sign(rot) * (|rot|/4.733)^2
// 127 / -127 : turning right/left at >5 deg/30s, no rate available
// -128 (0x80) : no turn information
// ggencoder exposes the raw signed byte in `data.rot`; skip it when the value
// doesn't carry a usable rate.
if (typeof data.rot !== 'undefined' &&
data.rot !== -128 &&
data.rot !== 127 &&
data.rot !== -127) {
const rotDegPerMin = Math.sign(data.rot) * Math.pow(Math.abs(data.rot) / 4.733, 2);
values.push({
path: 'navigation.rateOfTurn',
value: utils.transform(rotDegPerMin, 'deg', 'rad') / 60
});
}
if (data.length) {
values.push({
path: 'design.length',
value: { overall: data.length }
});
}
if (data.width) {
values.push({
path: 'design.beam',
value: data.width
});
}
if (data.draught) {
values.push({
path: 'design.draft',
value: { current: data.draught }
});
}
if (data.dimA) {
values.push({
path: 'sensors.ais.fromBow',
value: data.dimA
});
}
if (data.dimD && data.width) {
let fromCenter;
if (data.dimD > data.width / 2) {
fromCenter = (data.dimD - data.width / 2) * -1;
}
else {
fromCenter = data.width / 2 - data.dimD;
}
values.push({
path: 'sensors.ais.fromCenter',
value: fromCenter
});
}
if (typeof data.navstatus !== 'undefined') {
const state = stateMapping[data.navstatus];
if (typeof state !== 'undefined') {
values.push({
path: 'navigation.state',
value: state
});
}
}
if (data.destination) {
values.push({
path: 'navigation.destination.commonName',
value: data.destination
});
}
const etaIso = toDestinationEtaIso(data.etaMo, data.etaDay, data.etaHr, data.etaMin);
if (etaIso) {
values.push({
path: 'navigation.destination.eta',
value: etaIso
});
}
if (data.callsign) {
values.push({
path: '',
value: { communication: { callsignVhf: data.callsign } }
});
}
if (data.aistype) {
const aisClass = msgTypeToTransmitterClass[data.aistype];
if (aisClass) {
values.push({
path: 'sensors.ais.class',
value: aisClass
});
}
}
if (data.imo) {
values.push({
path: '',
value: {
registrations: {
imo: `IMO ${data.imo}`
}
}
});
}
let contextPrefix = msgTypeToPrefix[data.aistype] || 'vessels.';
if (data.aidtype) {
contextPrefix = 'atons.';
const atonType = schema.getAtonTypeName(data.aidtype);
if (typeof atonType !== 'undefined') {
values.push({
path: 'atonType',
value: { id: data.aidtype, name: atonType }
});
}
if (typeof data.offpos !== 'undefined') {
values.push({
path: 'offPosition',
value: data.offpos == 1
});
}
if (typeof data.virtual !== 'undefined') {
values.push({
path: 'virtual',
value: data.virtual == 1
});
}
}
if (data.cargo) {
const typeName = schema.getAISShipTypeName(data.cargo);
if (typeof typeName !== 'undefined') {
values.push({
path: 'design.aisShipType',
value: { id: data.cargo, name: typeName }
});
}
}
if (typeof data.smi !== 'undefined') {
values.push({
path: 'navigation.specialManeuver',
value: specialManeuverMapping[data.smi]
});
}
if (typeof data.dac !== 'undefined') {
values.push({
path: 'sensors.ais.designatedAreaCode',
value: data.dac
});
}
if (typeof data.fid !== 'undefined') {
if (data.fid == 31 || data.fid == 11 || data.fid == 33) {
contextPrefix = 'meteo.';
}
values.push({
path: 'sensors.ais.functionalId',
value: data.fid
});
}
if (data.lon && data.lat) {
values.push({
path: 'navigation.position',
value: {
longitude: data.lon,
latitude: data.lat
}
});
}
;
[
['avgwindspd', 'wind.averageSpeed', knotsToMs],
['windgust', 'wind.gust', knotsToMs],
['winddir', 'wind.directionTrue', degToRad],
['windgustdir', 'wind.gustDirectionTrue', degToRad],
['airtemp', 'outside.temperature', cToK],
['relhumid', 'outside.relativeHumidity', percentToRatio],
['dewpoint', 'outside.dewPointTemperature', cToK],
['airpress', 'outside.pressure', (v) => v * 100],
['waterlevel', 'water.level', (v) => v],
['signwavewhgt', 'water.waves.significantHeight', (v) => v],
['waveperiod', 'water.waves.period', (v) => v],
['wavedir', 'water.waves.directionTrue', degToRad],
['swellhgt', 'water.swell.height', (v) => v],
['swellperiod', 'water.swell.period', (v) => v],
['swelldir', 'water.swell.directionTrue', degToRad],
['watertemp', 'water.temperature', cToK],
['salinity', 'water.salinity', percentToRatio],
['surfcurrspd', 'water.current.drift', knotsToMs],
['surfcurrdir', 'water.current.set', degToRad]
].forEach(
// `as const`-free tuples - widen the tuple entry to a cast triple so the
// destructure is safe under noUncheckedIndexedAccess.
(entry) => {
const [propName, path, f] = entry;
if (data[propName] !== undefined) {
contextPrefix = 'meteo.';
values.push({
path: `environment.` + path,
value: f(data[propName])
});
}
});
[
['ice', 'water.ice', iceTable],
['precipitation', 'outside.precipitation', precipitationType],
['seastate', 'water.seaState', beaufortScale],
['waterlevelten', 'water.levelTendency', statusTable],
['airpressten', 'outside.pressureTendency', statusTable]
].forEach((entry) => {
const [propName, path, f] = entry;
if (data[propName] !== undefined) {
contextPrefix = 'meteo.';
values.push({
path: `environment.` + path,
value: f[data[propName]]
}, {
path: `environment.` + path + `Value`,
value: data[propName]
});
}
});
if (data.horvisib !== undefined && data.horvisibrange !== undefined) {
contextPrefix = 'meteo.';
values.push({
path: 'environment.outside.horizontalVisibility',
value: utils.transform(data.horvisib, 'nm', 'm')
}, {
path: 'environment.outside.horizontalVisibility.overRange',
value: data.horvisibrange
});
}
if (data.utcday !== undefined &&
data.utchour !== undefined &&
data.utcminute !== undefined) {
contextPrefix = 'meteo.';
const y = new Date().getUTCFullYear();
const m = new Date().getUTCMonth() + 1;
const d = data.utcday;
const h = data.utchour;
const min = data.utcminute;
const date = `${y}-${m.toString().padStart(2, '0')}-${d
.toString()
.padStart(2, '0')}T${h.toString().padStart(2, '0')}:${min
.toString()
.padStart(2, '0')}:00.000Z`;
values.push({
path: 'environment.date',
value: date
});
}
if (values.length === 0) {
return null;
}
const delta = {
context: contextPrefix + `urn:mrn:imo:mmsi:${data.mmsikey || data.mmsi}`,
updates: [
{
source: tags.source,
timestamp: tags.timestamp,
values: values
}
]
};
return delta;
};
exports.default = VDM;
//# sourceMappingURL=VDM.js.map