UNPKG

@signalk/n2k-signalk

Version:

NMEA 2000 to SignalK conversion library

393 lines (364 loc) 11.2 kB
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)) }) }