@signalk/n2k-signalk
Version:
NMEA 2000 to SignalK conversion library
348 lines • 12.7 kB
JavaScript
;
const EventEmitter = require('events').EventEmitter;
var through = require('through');
var debug = require('debug')('signalk:n2k-signalk');
const toPgn = require('@canboat/canboatjs').toPgn;
const Uint64LE = require('int64-buffer').Uint64LE;
const PGN = require('@canboat/ts-pgns').PGN;
require('util').inherits(N2kMapper, EventEmitter);
var n2kMappings = {};
Object.assign(n2kMappings, require('./pgns'));
Object.assign(n2kMappings, require('./fusion'));
Object.assign(n2kMappings, require('./lowrance'));
Object.assign(n2kMappings, require('./raymarine'));
Object.assign(n2kMappings, require('./maretron'));
Object.assign(n2kMappings, require('./actisense'));
Object.assign(n2kMappings, require('./digitalyacht'));
Object.assign(n2kMappings, require('./simrad'));
function N2kMapper(options) {
this.state = {};
this.unknownPGNs = {};
this.customPgns = {};
this.options = options || {};
if (this.options.onPropertyValues) {
this.options.onPropertyValues('pgn-to-signalk', values => {
values
.filter(v => v !== undefined)
.forEach(pv => {
Object.entries(pv.value).forEach(([pgnNumber, mappings]) => {
if (n2kMappings[pgnNumber] &&
!this.options.allowCustomPGNOverride) {
console.error(`pgn ${pgnNumber} can't be overwritten`);
}
else {
this.customPgns[pgnNumber] = mappings;
debug('registered custom pgn %d by %s', pgnNumber, pv.setter);
}
});
});
});
}
}
N2kMapper.prototype.n2kOutIsAvailable = function (listener, event) {
this.n2kOutEvent = event;
this.n2kListener = listener;
this.requestAllMeta();
};
N2kMapper.prototype.requestMetaData = function (dst, pgn) {
if (!this.n2kListener) {
debug(`skipping request for pgn ${pgn} from src ${dst}: n2k output not available yet`);
return Promise.resolve();
}
const reqPgn = {
pgn: 59904,
dst: dst,
PGN: pgn
};
debug(`requesting pgn ${pgn} from src ${dst}`);
return new Promise((resolve, reject) => {
this.n2kListener.emit(this.n2kOutEvent, reqPgn);
setTimeout(() => {
resolve();
}, 5000);
});
};
N2kMapper.prototype.requestMetaPGNs = async function (dst, pgns) {
for (let i = 0; i < pgns.length; i++) {
await this.requestMetaData(dst, pgns[i]);
}
};
N2kMapper.prototype.checkSrcMetasAndRetry = function (src) {
if (src !== '255') {
const neededPGNs = Object.keys(metaPGNs).filter(pgn => {
return (!this.state[src].metaPGNsReceived ||
!this.state[src].metaPGNsReceived[pgn]);
});
if (neededPGNs.length > 0) {
debug('did not get meta pgns %j for src %d', neededPGNs, src);
this.requestMetaPGNs(src, neededPGNs).then(() => {
neededPGNs.forEach(pgn => {
if (!this.state[src].metaPGNsReceived ||
!this.state[src].metaPGNsReceived[pgn]) {
debug(`did not get meta pgn ${pgn} for src ${src}`);
this.emit('n2kSourceMetadataTimeout', pgn, src);
}
});
});
}
}
};
N2kMapper.prototype.requestAllMeta = function () {
this.requestMetaPGNs(255, Object.keys(metaPGNs)).then(() => {
Object.keys(this.state).forEach(src => this.checkSrcMetasAndRetry(src));
});
};
N2kMapper.prototype.toDelta = function (n2k) {
if (metaPGNs[n2k.pgn]) {
const meta = metaPGNs[n2k.pgn](n2k);
if (!this.state[n2k.src]) {
this.state[n2k.src] = {};
}
if (n2k.pgn === 60928) {
const canName = new Uint64LE(toPgn(n2k)).toString(16);
if (this.state[n2k.src].canName &&
this.state[n2k.src].canName != canName) {
// clear out any existing state since the src addresses have changed
this.emit('n2kSourceChanged', n2k.src, this.state[n2k.src].canName, canName);
this.state[n2k.src] = {};
this.requestMetaData(n2k.src, 126996).then(() => {
return this.requestMetaData(n2k.src, 126998);
});
}
this.state[n2k.src].deviceInstance = meta.deviceInstance;
meta.canName = canName;
this.state[n2k.src].canName = canName;
}
if (!this.state[n2k.src].metaPGNsReceived) {
this.state[n2k.src].metaPGNsReceived = {};
}
this.state[n2k.src].metaPGNsReceived[n2k.pgn] = Date.now();
this.emit('n2kSourceMetadata', n2k, meta);
}
else {
if (!n2kMappings[n2k.pgn]) {
if (!this.unknownPGNs[n2k.src]) {
this.unknownPGNs[n2k.src] = {};
}
if (!this.unknownPGNs[n2k.src][n2k.pgn]) {
this.unknownPGNs[n2k.src][n2k.pgn] = n2k;
this.emit('n2kSourceMetadata', n2k, {
unknownPGNs: this.unknownPGNs[n2k.src]
});
}
}
return toDelta(n2k, this.state, this.customPgns);
}
};
var toDelta = function (n2k, state, customPgns = {}) {
try {
var theMappings, customMappings;
theMappings = [
...(customPgns[n2k.pgn] || []),
...(n2kMappings[n2k.pgn] || [])
];
var src_state;
if (state) {
var n2k_src = n2k.src.toString();
if (!state[n2k_src]) {
state[n2k_src] = {};
}
src_state = state[n2k_src];
}
var result = {
updates: [
{
source: {
label: '',
type: 'NMEA2000',
pgn: Number(n2k.pgn),
src: n2k.src.toString()
},
timestamp: n2k.timestamp.substring(0, 10) +
'T' +
n2k.timestamp.substring(11, n2k.timestamp.length),
values: toValuesArray(theMappings, n2k, src_state)
}
]
};
if (src_state && src_state.canName) {
result.updates[0].source.canName = src_state.canName;
}
if (src_state && typeof src_state.deviceInstance !== 'undefined') {
result.updates[0].source.deviceInstance = src_state.deviceInstance;
}
if (typeof theMappings !== 'undefined' &&
typeof theMappings !== 'function') {
theMappings.forEach(function (mapping) {
if (typeof mapping.context === 'function') {
result.context = mapping.context(n2k, src_state);
}
});
if (result.context) {
//filter out invalid mmsi
let last = result.context.lastIndexOf(':');
if (last != -1 && result.context.slice(last + 1).length < 9) {
console.error('bad MMSI: ' + result.context);
return;
}
}
if (theMappings.length === 1 && theMappings[0].instance) {
result.updates[0].source.instance = theMappings[0].instance(n2k);
}
}
return result;
}
catch (ex) {
console.error(ex);
console.error('Unable to convert:' + ex.message + ':' + JSON.stringify(n2k));
return { updates: [] };
}
};
function getValue(n2k, theMapping, state) {
if (typeof theMapping.source !== 'undefined') {
var stringValue = n2k.fields[theMapping.source];
if (!stringValue && stringValue != '') {
return undefined;
}
var numberValue = Number(stringValue);
return isNaN(numberValue) ? stringValue : numberValue;
}
else {
if (theMapping.value) {
return theMapping.value(n2k, state);
}
}
}
function reduceMapping(updates, theMapping) {
try {
if (typeof theMapping === 'function') {
updates.push.apply(updates, theMapping(n2k, state));
}
else {
var path = typeof theMapping.node === 'function'
? theMapping.node(n2k, state)
: theMapping.node;
var value = typeof theMapping.source === 'function'
? theMapping.source(n2k, state)
: getValue(n2k, theMapping, state);
var allowNull = typeof theMapping.allowNull !== 'undefined' && theMapping.allowNull;
if (!(value == null) || allowNull) {
// null or undefined
updates.push({
path: path,
value: value
});
}
}
}
catch (ex) {
process.stderr.write(ex + ' ' + JSON.stringify(n2k));
}
return updates;
}
var toValuesArray = function (theMappings, n2k, state) {
if (n2k.fields && typeof theMappings !== 'undefined') {
return theMappings
.filter(function (theMapping) {
try {
if (theMapping.pgnClass) {
return (theMapping.pgnClass.isMatch(n2k) &&
(theMapping.filter === undefined || theMapping.filter(n2k, state)));
}
else {
return (typeof theMapping.filter === 'undefined' ||
theMapping.filter(n2k, state));
}
}
catch (ex) {
process.stderr.write(ex + ' ' + n2k);
return false;
}
})
.reduce((updates, theMapping) => {
try {
if (typeof theMapping === 'function') {
Array.prototype.push.apply(updates, theMapping(n2k, state));
}
else {
var path = typeof theMapping.node === 'function'
? theMapping.node(n2k, state)
: theMapping.node;
var value = typeof theMapping.source === 'function'
? theMapping.source(n2k, state)
: getValue(n2k, theMapping, state);
var allowNull = typeof theMapping.allowNull !== 'undefined' &&
theMapping.allowNull;
if (!(value == null) || allowNull) {
// null or undefined
updates.push({
path: path,
value: value
});
}
}
}
catch (ex) {
process.stderr.write(ex + ' ' + JSON.stringify(n2k));
console.error(ex);
}
return updates;
}, [])
.filter(function (x) {
return x != undefined;
});
}
return [];
};
var addToTree = function (pathValue, source, tree) {
var result = {};
var temp = tree;
var parts = msg.path.split('.');
for (var i = 0; i < parts.length - 1; i++) {
temp[parts[i]] = {};
temp = temp[parts[i]];
}
temp[parts[parts.length - 1]] = msg;
return result;
};
function addAsNested(pathValue, source, timestamp, result) {
var temp = result;
var parts = pathValue.path.split('.');
for (var i = 0; i < parts.length - 1; i++) {
if (typeof temp[parts[i]] === 'undefined') {
temp[parts[i]] = {};
}
temp = temp[parts[i]];
}
// mapping produced an object like {latitude:...,longitude:...}
if (typeof pathValue.value === 'object') {
temp[parts[parts.length - 1]] = pathValue.value;
temp[parts[parts.length - 1]].source = source;
temp[parts[parts.length - 1]].timestamp = timestamp + '';
}
else {
temp[parts[parts.length - 1]] = {
value: pathValue.value,
source: source,
timestamp: timestamp + ''
};
}
}
const metaPGNs = {
60928: n2k => {
return {
...n2k.fields,
deviceInstance: (n2k.fields.deviceInstanceUpper << 3) | n2k.fields.deviceInstanceLower
};
},
126998: n2k => n2k.fields,
126996: n2k => n2k.fields
};
exports.N2kMapper = N2kMapper;
exports.toDelta = toDelta;
exports.toDeltaTransformer = function (options, state) {
return through(function (data) {
this.queue(exports.toDelta(data, state));
});
};
//# sourceMappingURL=n2kMapper.js.map