UNPKG

@canboat/canboatjs

Version:

Native javascript version of canboat

648 lines (552 loc) 16.9 kB
/** * Copyright 2018 Scott Bender (scott@scottbender.net) * * 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. */ const debug = require('debug')('signalk:actisense-serial') const debugOut = require('debug')('signalk:actisense-out') const Transform = require('stream').Transform const isArray = require('lodash').isArray const BitStream = require('bit-buffer').BitStream const BitView = require('bit-buffer').BitView const { toPgn } = require('./toPgn') const { encodeActisense } = require('./stringMsg') const { defaultTransmitPGNs } = require('./codes') const _ = require('lodash') const FromPgn = require('./fromPgn').Parser /* ASCII characters used to mark packet start/stop */ const STX = 0x02 /* Start packet */ const ETX = 0x03 /* End packet */ const DLE = 0x10 /* Start pto encode a STX or ETX send DLE+STX or DLE+ETX */ const ESC = 0x1B /* Escape */ /* Actisense message structure is: DLE STX <command> <len> [<data> ...] <checksum> DLE ETX <command> is a byte from the list below. In <data> any DLE characters are double escaped (DLE DLE). <len> encodes the unescaped length. <checksum> is such that the sum of all unescaped data bytes plus the command byte plus the length adds up to zero, modulo 256. */ const N2K_MSG_RECEIVED = 0x93 /* Receive standard N2K message */ const N2K_MSG_SEND = 0x94 /* Send N2K message */ const NGT_MSG_RECEIVED = 0xA0 /* Receive NGT specific message */ const NGT_MSG_SEND = 0xA1 /* Send NGT message */ const MSG_START = 1 const MSG_ESCAPE = 2 const MSG_MESSAGE = 3 const NGT_STARTUP_MSG = new Uint8Array([0x11, 0x02, 0x00]) function SerialStream (options) { if (!(this instanceof SerialStream)) { return new SerialStream(options) } Transform.call(this, { objectMode: true }) debug('options: %j', options) this.reconnect = options.reconnect || true this.serial = null this.options = options this.transmitPGNRetries = 2 this.transmitPGNs = defaultTransmitPGNs if ( this.options.transmitPGNs ) { this.transmitPGNs = _.union(this.transmitPGNs, this.options.transmitPGNs) } this.options.disableSetTransmitPGNs = true if ( process.env.DISABLESETTRANSMITPGNS ) { this.options.disableSetTransmitPGNs = true } if ( process.env.ENABLESETTRANSMITPGNS ) { this.options.disableSetTransmitPGNs = false } this.start() } require('util').inherits(SerialStream, Transform) SerialStream.prototype.start = function () { if (this.serial !== null) { this.serial.unpipe(this) this.serial.removeAllListeners() this.serial = null } if (this.reconnect === false) { return } this.buffer = Buffer.alloc(500) this.bufferOffset = 0 this.isFile = false this.state = MSG_START if ( typeof this.reconnectDelay === 'undefined' ) { this.reconnectDelay = 1000 } if ( !this.options.fromFile ) { const { SerialPort } = require('serialport') this.serial = new SerialPort({ path: this.options.device, baudRate: this.options.baudrate || 115200 }) const setProviderStatus = this.options.app && this.options.app.setProviderStatus ? (msg) => { this.options.app.setProviderStatus(this.options.providerId, msg) } : () => {} const setProviderError = this.options.app && this.options.app.setProviderError ? (msg) => { this.options.app.setProviderError(this.options.providerId, msg) } : () => {} this.setProviderStatus = setProviderStatus var that = this this.serial.on('data', (data) => { try { readData(this, data) } catch ( err ) { setProviderError(err.message) console.error(err) } }) if ( this.options.app ) { function writeString(msg) { debugOut(`sending ${msg}`) var buf = parseInput(msg) buf = composeMessage(N2K_MSG_SEND, buf, buf.length) debugOut(buf) that.serial.write(buf) that.options.app.emit('connectionwrite', { providerId: that.options.providerId }) } function writeObject(msg) { var data = toPgn(msg) var actisense = encodeActisense({ pgn: msg.pgn, data, dst: msg.dst}) debugOut(`sending ${actisense}`) var buf = parseInput(actisense) buf = composeMessage(N2K_MSG_SEND, buf, buf.length) debugOut(buf) that.serial.write(buf) that.options.app.emit('connectionwrite', { providerId: that.options.providerId }) } this.options.app.on(this.options.outEevent || 'nmea2000out', msg => { if ( this.outAvailable ) { if ( typeof msg === 'string' ) { writeString(msg) } else { writeObject(msg) } } }) this.options.app.on(this.options.jsonOutEvent || 'nmea2000JsonOut', msg => { if ( this.outAvailable ) { writeObject(msg) } }) } this.outAvailable = false this.serial.on('error', function (x) { setProviderError(x.message) console.log(x) that.scheduleReconnect() }) this.serial.on('close', () => { setProviderError('Closed, reconnecting...') //this.start.bind(this) that.scheduleReconnect() }) this.serial.on( 'open', function () { try { this.reconnectDelay = 1000 setProviderStatus(`Connected to ${that.options.device}`) var buf = composeMessage(NGT_MSG_SEND, Buffer.from(NGT_STARTUP_MSG), NGT_STARTUP_MSG.length) debugOut(buf) that.serial.write(buf) debug('sent startup message') that.gotStartupResponse = false if ( that.options.disableSetTransmitPGNs ) { enableOutput(that) } else { setTimeout(() => { if ( that.gotStartupResponse === false ) { debug('retry startup message...') debugOut(bug) that.serial.write(buf) } }, 5000) } } catch ( err ) { setProviderError(err.message) console.error(err) console.error(err.stack) } } ) } } SerialStream.prototype.scheduleReconnect = function () { this.reconnectDelay *= this.reconnectDelay < 60 * 1000 ? 1.5 : 1 const msg = `Not connected (retry delay ${( this.reconnectDelay / 1000 ).toFixed(0)} s)` debug(msg) this.setProviderStatus(msg) setTimeout(this.start.bind(this), this.reconnectDelay) } function readData(that, data) { for ( var i = 0; i < data.length; i++ ) { //console.log(data[i]) read1Byte(that, data[i]) } } function read1Byte(that, c) { var startEscape = false; var noEscape = false; //debug("received byte %02x state=%d offset=%d\n", c, state, head - buf); if (that.stat == MSG_START) { if ((c == ESC) && that.isFile) { noEscape = true; } } if (that.stat == MSG_ESCAPE) { if (c == ETX) { if ( !that.options.outputOnly ) { if ( that.buffer[0] == N2K_MSG_RECEIVED ) { processN2KMessage(that, that.buffer, that.bufferOffset) } else if ( that.buffer[0] == NGT_MSG_RECEIVED) { processNTGMessage(that, that.buffer, that.bufferOffset) } } that.bufferOffset = 0 that.stat = MSG_START; } else if (c == STX) { that.bufferOffset = 0 that.stat = MSG_MESSAGE; } else if ((c == DLE) || ((c == ESC) && that.isFile) || that.noEscape) { that.buffer.writeUInt8(c, that.bufferOffset) that.bufferOffset++ that.stat = MSG_MESSAGE; } else { console.error("DLE followed by unexpected char , ignore message"); that.stat = MSG_START; } } else if (that.stat == MSG_MESSAGE) { if (c == DLE) { that.stat = MSG_ESCAPE; } else if (that.isFile && (c == ESC) && !noEscape) { that.stat = MSG_ESCAPE; } else { that.buffer.writeUInt8(c, that.bufferOffset) that.bufferOffset++ } } else { if (c == DLE) { that.stat = MSG_ESCAPE; } } } function enableTXPGN(serial, pgn) { debug('enabling pgn %d', pgn) const msg = composeEnablePGN(pgn) debugOut(msg) serial.write(msg) } function enableOutput(that) { debug('outputEnabled') that.outAvailable = true if ( that.options.app ) { that.options.app.emit('nmea2000OutAvailable') } } function requestTransmitPGNList(that) { debug('request tx pgns...') const requestMsg = composeRequestTXPGNList() debugOut(requestMsg) that.serial.write(requestMsg) setTimeout(() => { if ( !that.gotTXPGNList ) { if ( that.transmitPGNRetries-- > 0 ) { debug('did not get tx pgn list, retrying...') requestTransmitPGNList(that) } else { const msg = 'could not set transmit pgn list' that.options.app.setProviderStatus(msg) console.warn(msg) enableOutput(that) } } }, 10000) } function processNTGMessage(that, buffer, len) { var checksum = 0 for ( var i = 0; i < len; i++ ) { checksum = addUInt8(checksum, buffer[i]) } const command = buffer[2] if ( checksum != 0 ) { debug('received message with invalid checksum (%d,%d)', command, len) return } if ( that.options.sendNetworkStats || debug.enabled ) { let newbuf = new Buffer.alloc(len + 7 ) var bs = new BitStream(newbuf) const pgn = 0x40000 + buffer[2] bs.writeUint8(0) //prio bs.writeUint8(pgn) bs.writeUint8(pgn >> 8) bs.writeUint8(pgn >> 16) bs.writeUint8(0) //dst bs.writeUint8(0) //src bs.writeUint32(0) //timestamp bs.writeUint8(len-4) buffer.copy(bs.view.buffer, bs.byteIndex, 3) if ( that.options.plainText ) { that.push(binToActisense(bs.view.buffer, len+7)) } else { that.push(bs.view.buffer, len+7) } if ( debug.enabled && command != 0xf2 ) { //don't log system status if ( !that.parser ) { that.parser = new FromPgn() } const js = that.parser.parseBuffer(bs.view.buffer) if ( js ) { debug('got ntg message: %j', js) } } } if ( command === 0x11 ) { //confirm startup that.gotStartupResponse = true debug('got startup response') } if ( !that.outAvailable ) { if ( command === 0x11 ) { that.gotTXPGNList = false setTimeout(() => { requestTransmitPGNList(that) }, 2000) } else if ( command === 0x49 && buffer[3] === 1 ) { that.gotTXPGNList = true const pgnCount = buffer[14]; let bv = new BitView(buffer.slice(15, that.bufferOffset)); let bs = new BitStream(bv) let pgns = [] for ( let i = 0; i < pgnCount; i++ ) { pgns.push(bs.readUint32()) } debug('tx pgns: %j', pgns) that.neededTransmitPGNs = that.transmitPGNs.filter(pgn => { return pgns.indexOf(pgn) == -1 }) debug('needed pgns: %j', that.neededTransmitPGNs) } else if ( command === 0x49 && buffer[3] === 4 ) { //I think this means done receiving the pgns list if ( that.neededTransmitPGNs ) { if ( that.neededTransmitPGNs.length ) { enableTXPGN(that.serial, that.neededTransmitPGNs[0]) } else { enableOutput(that) } } } else if ( command === 0x47 ) { //response from enable a pgn if ( buffer[3] === 1 ) { debug('enabled %d', that.neededTransmitPGNs[0]) that.neededTransmitPGNs = that.neededTransmitPGNs.slice(1) if ( that.neededTransmitPGNs.length === 0 ) { const commitMsg = composeCommitTXPGN() debugOut(commitMsg) that.serial.write(commitMsg) } else { enableTXPGN(that.serial, that.neededTransmitPGNs[0]) } } else { debug('bad response from Enable TX: %d', buffer[3]) } } else if ( command === 0x01 ) { debug('commited tx list') const activateMsg = composeActivateTXPGN() debugOut(activateMsg) that.serial.write(activateMsg) } else if ( command === 0x4b ) { debug('activated tx list') enableOutput(that) } } } function addUInt8(num, add) { if ( num + add > 255 ) { num = add - (256-num) } else { num += add } return num } function processN2KMessage(that, buffer, len) { var checksum = 0 for ( var i = 0; i < len; i++ ) { checksum = addUInt8(checksum, buffer[i]) } if ( checksum != 0 ) { debug('received message with invalid checksum') return } if ( that.options.plainText ) { that.push(binToActisense(buffer.slice(2, len))) } else { that.push(buffer.slice(2, len)) } } function binToActisense(buffer) { var bv = new BitView(buffer); var bs = new BitStream(bv) var pgn = {} pgn.prio = bs.readUint8() pgn.pgn = bs.readUint8() + 256 * (bs.readUint8() + 256 * bs.readUint8()); pgn.dst = bs.readUint8() pgn.src = bs.readUint8() pgn.timestamp = bs.readUint32() var len = bs.readUint8() return ( new Date().toISOString() + `,${pgn.prio},${pgn.pgn},${pgn.src},${pgn.dst},${len},` + new Uint32Array(buffer.slice(11, 11+len)) .reduce(function(acc, i) { acc.push(i.toString(16)); return acc; }, []) .map(x => (x.length === 1 ? "0" + x : x)) .join(",") ); } function composeMessage(command, buffer, len) { var outBuf = Buffer.alloc(500); var out = new BitStream(outBuf) out.writeUint8(DLE) out.writeUint8(STX) out.writeUint8(command) var lenPos = out.byteIndex out.writeUint8(0) //length. will update later var crc = command; for (var i = 0; i < len; i++) { var c = buffer.readUInt8(i) if (c == DLE) { out.writeUint8(DLE); } out.writeUint8(c) crc = addUInt8(crc, c) } crc = addUInt8(crc, len) out.writeUint8(256-crc) out.writeUint8(DLE) out.writeUint8(ETX) out.view.buffer.writeUInt8(len, lenPos) //debug(`command ${out.view.buffer[2]} ${lenPos} ${len} ${out.view.buffer[lenPos]} ${out.view.buffer.length} ${out.byteIndex}`) return out.view.buffer.slice(0, out.byteIndex) } function parseInput(msg) { var split = msg.split(',') var buffer = Buffer.alloc(500) var bs = new BitStream(buffer) var prio = Number(split[1]) var pgn = Number(split[2]) var dst = Number(split[4]) var bytes = Number(split[5]) bs.writeUint8(prio) bs.writeUint8(pgn) bs.writeUint8(pgn >> 8) bs.writeUint8(pgn >> 16) bs.writeUint8(dst) /* bs.writeUint8(split[3]) bs.writeUint32(0) */ bs.writeUint8(bytes) for ( var i = 6; i < (bytes+6); i++ ) { bs.writeUint8(parseInt('0x' + split[i], 16)) } return bs.view.buffer.slice(0, bs.byteIndex) } function composeCommitTXPGN() { let msg = new Uint32Array([0x01]) return composeMessage(NGT_MSG_SEND, Buffer.from(msg), msg.length) } function composeActivateTXPGN() { let msg = new Uint32Array([0x4b]) return composeMessage(NGT_MSG_SEND, Buffer.from(msg), msg.length) } function composeRequestTXPGNList() { let msg = new Uint32Array([0x49]) return composeMessage(NGT_MSG_SEND, Buffer.from(msg), msg.length) } function composeEnablePGN(pgn) { var outBuf = Buffer.alloc(14); let out = new BitStream(outBuf) out.writeUint8(0x47) out.writeUint32(pgn) out.writeUint8(1) //enabled out.writeUint32(0xfffffffe) out.writeUint32(0xfffffffe) let res = composeMessage(NGT_MSG_SEND, out.view.buffer.slice(0, out.byteIndex), out.byteIndex) //debug('composeEnablePGN: %o', res) return res; } function composeDisablePGN(pgn) { var outBuf = Buffer.alloc(14); let out = new BitStream(outBuf) out.writeUint8(0x47) out.writeUint32(pgn) out.writeUint8(0) //disabled //disbale system time //10 02 a1 0e 47 10 10 f0 01 00 00 e8 03 00 00 00 00 00 00 1e 10 03 out.writeUint32(0x000003e8) //??? out.writeUint32(0x00) let res = composeMessage(NGT_MSG_SEND, out.view.buffer.slice(0, out.byteIndex), out.byteIndex) debug('composeDisablePGN: %o', res) return res; } SerialStream.prototype.end = function () { if ( this.serial ) { this.serial.close() } } SerialStream.prototype._transform = function (chunk, encoding, done) { debug(`got data ${typeof chunk}`) readData(this, chunk) done() } module.exports = SerialStream