UNPKG

@canboat/canboatjs

Version:

Native javascript version of canboat

569 lines (562 loc) 19.9 kB
"use strict"; /** * 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. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ActisenseStream = ActisenseStream; const utilities_1 = require("./utilities"); const util_1 = __importDefault(require("util")); const stream_1 = require("stream"); const bit_buffer_1 = require("bit-buffer"); const toPgn_1 = require("./toPgn"); const stringMsg_1 = require("./stringMsg"); const codes_1 = require("./codes"); const lodash_1 = __importDefault(require("lodash")); const fromPgn_1 = require("./fromPgn"); /* 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 ActisenseStream(options) { if (this === undefined) { return new ActisenseStream(options); } this.debugOut = (0, utilities_1.createDebug)('canboatjs:actisense-out', options); this.debug = (0, utilities_1.createDebug)('canboatjs:actisense-serial', options); stream_1.Transform.call(this, { objectMode: true }); this.debug('options: %j', options); this.reconnect = options.reconnect || true; this.serial = null; this.options = options; this.transmitPGNRetries = 2; this.transmitPGNs = codes_1.defaultTransmitPGNs; if (this.options.transmitPGNs) { this.transmitPGNs = lodash_1.default.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(); } util_1.default.inherits(ActisenseStream, stream_1.Transform); ActisenseStream.prototype.start = function () { if (this.serial !== null) { this.serial.unpipe(this); this.serial.removeAllListeners(); this.serial = null; } if (this.reconnect === false) { return; } 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; 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) { try { // eslint-disable-next-line @typescript-eslint/no-require-imports const { SerialPort } = require('serialport'); this.serial = new SerialPort({ path: this.options.device, baudRate: this.options.baudrate || 115200 }); } catch (err) { setProviderError('serialport module not available'); console.error(err); return; } this.serial.on('data', (data) => { try { readData(this, data); } catch (err) { setProviderError(err.message); console.error(err); } }); if (this.options.app) { const writeString = (msg) => { this.debugOut(`sending ${msg}`); let buf = parseInput(msg); buf = composeMessage(N2K_MSG_SEND, buf, buf.length); this.debugOut(buf); this.serial.write(buf); this.options.app.emit('connectionwrite', { providerId: this.options.providerId }); }; const writeObject = (msg) => { const data = (0, toPgn_1.toPgn)(msg); const actisense = (0, stringMsg_1.encodeActisense)({ pgn: msg.pgn, data, dst: msg.dst }); this.debugOut(`sending ${actisense}`); let buf = parseInput(actisense); buf = composeMessage(N2K_MSG_SEND, buf, buf.length); this.debugOut(buf); this.serial.write(buf); this.options.app.emit('connectionwrite', { providerId: this.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', (err) => { setProviderError(err.message); console.log(err); this.scheduleReconnect(); }); this.serial.on('close', () => { setProviderError('Closed, reconnecting...'); //this.start.bind(this) this.scheduleReconnect(); }); this.serial.on('open', () => { try { this.reconnectDelay = 1000; setProviderStatus(`Connected to ${this.options.device}`); const buf = composeMessage(NGT_MSG_SEND, Buffer.from(NGT_STARTUP_MSG), NGT_STARTUP_MSG.length); this.debugOut(buf); this.serial.write(buf); this.debug('sent startup message'); this.gotStartupResponse = false; if (this.options.disableSetTransmitPGNs) { enableOutput(this); } else { setTimeout(() => { if (this.gotStartupResponse === false) { this.debug('retry startup message...'); this.debugOut(buf); this.serial.write(buf); } }, 5000); } } catch (err) { setProviderError(err.message); console.error(err); console.error(err.stack); } }); } }; ActisenseStream.prototype.scheduleReconnect = function () { this.reconnectDelay *= this.reconnectDelay < 60 * 1000 ? 1.5 : 1; const msg = `Not connected (retry delay ${(this.reconnectDelay / 1000).toFixed(0)} s)`; this.debug(msg); this.setProviderStatus(msg); setTimeout(this.start.bind(this), this.reconnectDelay); }; function readData(that, data) { for (let i = 0; i < data.length; i++) { //console.log(data[i]) read1Byte(that, data[i]); } } function read1Byte(that, c) { let 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(that, pgn) { that.debug('enabling pgn %d', pgn); const msg = composeEnablePGN(pgn); that.debugOut(msg); that.serial.write(msg); } function enableOutput(that) { that.debug('outputEnabled'); that.outAvailable = true; if (that.options.app) { that.options.app.emit('nmea2000OutAvailable'); } } function requestTransmitPGNList(that) { that.debug('request tx pgns...'); const requestMsg = composeRequestTXPGNList(); that.debugOut(requestMsg); that.serial.write(requestMsg); setTimeout(() => { if (!that.gotTXPGNList) { if (that.transmitPGNRetries-- > 0) { that.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) { let checksum = 0; for (let i = 0; i < len; i++) { checksum = addUInt8(checksum, buffer[i]); } const command = buffer[2]; if (checksum != 0) { that.debug('received message with invalid checksum (%d,%d)', command, len); return; } if (that.options.sendNetworkStats || that.debug.enabled) { const newbuf = Buffer.alloc(len + 7); const bs = new bit_buffer_1.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 (that.debug.enabled && command != 0xf2) { //don't log system status if (!that.parser) { that.parser = new fromPgn_1.Parser({}); } const js = that.parser.parseBuffer(bs.view.buffer); if (js) { that.debug('got ntg message: %j', js); } } } if (command === 0x11) { //confirm startup that.gotStartupResponse = true; that.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]; const bv = new bit_buffer_1.BitView(buffer.slice(15, that.bufferOffset)); const bs = new bit_buffer_1.BitStream(bv); const pgns = []; for (let i = 0; i < pgnCount; i++) { pgns.push(bs.readUint32()); } that.debug('tx pgns: %j', pgns); that.neededTransmitPGNs = that.transmitPGNs.filter((pgn) => { return pgns.indexOf(pgn) == -1; }); that.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, that.neededTransmitPGNs[0]); } else { enableOutput(that); } } } else if (command === 0x47) { //response from enable a pgn if (buffer[3] === 1) { that.debug('enabled %d', that.neededTransmitPGNs[0]); that.neededTransmitPGNs = that.neededTransmitPGNs.slice(1); if (that.neededTransmitPGNs.length === 0) { const commitMsg = composeCommitTXPGN(); that.debugOut(commitMsg); that.serial.write(commitMsg); } else { enableTXPGN(that, that.neededTransmitPGNs[0]); } } else { that.debug('bad response from Enable TX: %d', buffer[3]); } } else if (command === 0x01) { that.debug('commited tx list'); const activateMsg = composeActivateTXPGN(); that.debugOut(activateMsg); that.serial.write(activateMsg); } else if (command === 0x4b) { that.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) { let checksum = 0; for (let i = 0; i < len; i++) { checksum = addUInt8(checksum, buffer[i]); } if (checksum != 0) { that.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) { const bv = new bit_buffer_1.BitView(buffer); const bs = new bit_buffer_1.BitStream(bv); const pgn = { prio: bs.readUint8(), pgn: bs.readUint8() + 256 * (bs.readUint8() + 256 * bs.readUint8()), dst: bs.readUint8(), src: bs.readUint8(), timestamp: bs.readUint32() }; const len = bs.readUint8(); const arr = []; 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; }, arr) .map((x) => (x.length === 1 ? '0' + x : x)) .join(',')); } function composeMessage(command, buffer, len) { const outBuf = Buffer.alloc(500); const out = new bit_buffer_1.BitStream(outBuf); out.writeUint8(DLE); out.writeUint8(STX); out.writeUint8(command); const lenPos = out.byteIndex; out.writeUint8(0); //length. will update later let crc = command; for (let i = 0; i < len; i++) { const 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); //that.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) { const split = msg.split(','); const buffer = Buffer.alloc(500); const bs = new bit_buffer_1.BitStream(buffer); const prio = Number(split[1]); const pgn = Number(split[2]); const dst = Number(split[4]); const 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 (let i = 6; i < bytes + 6; i++) { bs.writeUint8(parseInt('0x' + split[i], 16)); } return bs.view.buffer.slice(0, bs.byteIndex); } function composeCommitTXPGN() { const msg = new Uint32Array([0x01]); return composeMessage(NGT_MSG_SEND, Buffer.from(msg), msg.length); } function composeActivateTXPGN() { const msg = new Uint32Array([0x4b]); return composeMessage(NGT_MSG_SEND, Buffer.from(msg), msg.length); } function composeRequestTXPGNList() { const msg = new Uint32Array([0x49]); return composeMessage(NGT_MSG_SEND, Buffer.from(msg), msg.length); } function composeEnablePGN(pgn) { const outBuf = Buffer.alloc(14); const out = new bit_buffer_1.BitStream(outBuf); out.writeUint8(0x47); out.writeUint32(pgn); out.writeUint8(1); //enabled out.writeUint32(0xfffffffe); out.writeUint32(0xfffffffe); const res = composeMessage(NGT_MSG_SEND, out.view.buffer.slice(0, out.byteIndex), out.byteIndex); //that.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) that.debug('composeDisablePGN: %o', res) return res; } */ ActisenseStream.prototype.end = function () { if (this.serial) { this.serial.close(); } }; ActisenseStream.prototype._transform = function (chunk, encoding, done) { this.debug(`got data ${typeof chunk}`); readData(this, chunk); done(); }; //# sourceMappingURL=actisense-serial.js.map