UNPKG

@canboat/canboatjs

Version:

Native javascript version of canboat

471 lines 17.1 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.CanbusStream = CanbusStream; const utilities_1 = require("./utilities"); const stream_1 = require("stream"); const toPgn_1 = require("./toPgn"); const lodash_1 = __importDefault(require("lodash")); const candevice_1 = require("./candevice"); const utilities_2 = require("./utilities"); const canId_1 = require("./canId"); const stringMsg_1 = require("./stringMsg"); const canSocket_1 = require("./canSocket"); const util_1 = __importDefault(require("util")); function CanbusStream(options) { if (this === undefined) { return new CanbusStream(options); } this.debug = (0, utilities_1.createDebug)('canboatjs:n2k-out', options); stream_1.Transform.call(this, { objectMode: true }); this.supportsDeviceCreation = true; this.sentUtils = false; this.plainText = false; this.devices = {}; this.options = options; this.reconnecting = false; // Guard flag to prevent concurrent reconnections this.stoppingChannel = false; // Flag to track intentional stops this.start(); this.setProviderStatus = options.app && options.app.setProviderStatus ? (msg) => { options.app.setProviderStatus(options.providerId, msg); } : () => { }; this.setProviderError = options.app && options.app.setProviderError ? (msg) => { options.app.setProviderError(options.providerId, msg); } : () => { }; if (options.fromStdIn) { return; } // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; if (options.app) { const outEvents = (options.outEvent || 'nmea2000out') .split(',') .map((event) => event.trim()); outEvents.forEach((event) => { options.app.on(event, (msg) => { that.sendPGN(msg); }); }); const jsonOutEvents = (options.jsonOutEvent || 'nmea2000JsonOut') .split(',') .map((event) => event.trim()); jsonOutEvents.forEach((event) => { options.app.on(event, (msg) => { that.sendPGN(msg); }); }); } // Store timeout value for timer recreation during reconnects this.noDataReceivedTimeout = typeof options.noDataReceivedTimeout !== 'undefined' ? options.noDataReceivedTimeout : -1; if (this.connect() == false) { return; } // Initial timer setup (will be recreated on each reconnect in connect()) this._setupNoDataTimer(); } // Setup or recreate the no-data monitoring timer CanbusStream.prototype._setupNoDataTimer = function () { // Clear existing timer if present if (this.noDataInterval) { clearInterval(this.noDataInterval); this.noDataInterval = null; } if (this.noDataReceivedTimeout > 0) { this.noDataInterval = setInterval(() => { if (this.channel && this.lastDataReceived && Date.now() - this.lastDataReceived > this.noDataReceivedTimeout * 1000) { if (this.options.app) { console.error(`No data received for ${this.noDataReceivedTimeout}s, retrying...`); } this.setProviderError('No data received, retrying...'); // Mark as intentional stop before stopping channel this.stoppingChannel = true; const channel = this.channel; delete this.channel; try { channel.stop(); } catch (_error) { console.error('Error stopping channel:', _error); } // Attempt reconnection this.connect(); } }, this.noDataReceivedTimeout * 1000); } }; CanbusStream.prototype.connect = function () { // Prevent concurrent reconnection attempts if (this.reconnecting) { if (this.options.app) { console.log('Reconnection already in progress, skipping...'); } return false; } this.reconnecting = true; const canDevice = this.options.canDevice || 'can0'; try { // Clean up old candevice if it exists if (this.candevice) { try { this.candevice.stop(); } catch (e) { console.error('Error stopping candevice:', e); } delete this.candevice; } // Clean up old channel if it exists if (this.channel) { try { this.channel.removeAllListeners('onStopped'); this.channel.removeAllListeners('onMessage'); if (!this.stoppingChannel) { this.channel.stop(); } } catch (e) { console.error('Error cleaning up old channel:', e); } delete this.channel; } // Reset stopping flag this.stoppingChannel = false; // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; this.channel = new canSocket_1.CanChannel(canDevice); this.channel.addListener('onStopped', () => { // Check if we still have a channel reference (not already handled) if (!this.channel) { return; // Already handled by timeout or manual stop } // Check if this was an intentional stop by us const wasOurStop = this.stoppingChannel; if (wasOurStop) { // We stopped it intentionally (e.g., from timeout handler), don't auto-reconnect if (this.options.app) { console.log('Channel stopped intentionally, reconnection handled elsewhere'); } return; } // External/unexpected stop - need to reconnect delete this.channel; this.setProviderError('Stopped unexpectedly, Retrying...'); if (this.options.app) { console.error('CAN channel stopped unexpectedly, retrying...'); } setTimeout(() => { this.setProviderError('Reconnecting...'); this.connect(); }, 2000); }); this.channel.addListener('onMessage', (msg) => { const pgn = (0, canId_1.parseCanId)(msg.id); if (this.noDataInterval) { this.lastDataReceived = Date.now(); } //always send address claims through if (pgn.pgn != 60928 && that.candevice && that.candevice.cansend && pgn.src == that.candevice.address) { return; } let data; if (that.plainText) { const timestamp = new Date().toISOString(); data = (0, utilities_2.binToActisense)(pgn, timestamp, msg.data, msg.data.length); this.push(data); if (this.options.app.listenerCount('canboatjs:rawoutput') > 0) { this.options.app.emit('canboatjs:rawoutput', data); } } else { data = { pgn, length: msg.data.length, data: msg.data }; if (this.options.app.listenerCount('canboatjs:rawoutput') > 0) { that.options.app.emit('canboatjs:rawoutput', { pgn, length: msg.data.length, data: (0, utilities_1.byteStringArray)(msg.data) }); } that.push(data); } }); this.channel.start(); this.setProviderStatus('Connected to CAN bus'); this.candevice = new candevice_1.CanDevice(this, this.options); this.candevice.start(); // Recreate the no-data monitoring timer for this connection this._setupNoDataTimer(); // Clear reconnecting flag on success this.reconnecting = false; if (this.options.app) { console.log(`Successfully connected to ${canDevice}`); } if (this.sentUtils === false && this.options.app.emitPropertyValue) { this.options.app.emitPropertyValue('canboatjsUtils', { id: this.options.id, utils: this }); this.sentUtils = true; } return true; } catch (e) { console.error(`unable to open canbus ${canDevice}: ${e}`); console.error(e.stack); this.setProviderError(e.message); // Clear reconnecting flag on failure this.reconnecting = false; // Schedule retry after failure if (this.options.app) { console.error('Will retry connection in 5 seconds...'); } setTimeout(() => { this.connect(); }, 5000); return false; } }; util_1.default.inherits(CanbusStream, stream_1.Transform); CanbusStream.prototype.start = function () { }; CanbusStream.prototype.sendPGN = function (msg, force, candeviceArg = undefined) { if (this.candevice) { if (!this.channel) { return; } const candevice = candeviceArg || this.candevice; if (!candevice.cansend && force !== true) { //we have not completed address claim yet return; } this.debug('sending %j', msg); if (this.options.app) { this.options.app.emit('connectionwrite', { providerId: this.options.providerId }); } const src = lodash_1.default.isString(msg) === false && msg.forceSrc ? msg.src : candevice.address; if (lodash_1.default.isString(msg)) { const split = msg.split(','); split[3] = src; msg = split.join(','); } else { msg.src = src; if (lodash_1.default.isUndefined(msg.prio)) { msg.prio = 3; } if (lodash_1.default.isUndefined(msg.dst)) { msg.dst = 255; } } let canid; let buffer; let pgn; if (lodash_1.default.isObject(msg)) { canid = (0, canId_1.encodeCanId)(msg); buffer = (0, toPgn_1.toPgn)(msg); pgn = msg; } else { pgn = (0, stringMsg_1.parseActisense)(msg); canid = (0, canId_1.encodeCanId)(pgn); buffer = pgn.data; } if (this.debug.enabled) { const str = (0, stringMsg_1.toActisenseSerialFormat)(pgn.pgn, buffer, pgn.dst, pgn.src); this.debug(str); } if (buffer === undefined) { this.debug("can't convert %j", msg); return; } //seems as though 126720 should always be encoded this way if (buffer.length > 8 || pgn.pgn == 126720) { const pgns = (0, utilities_2.getPlainPGNs)(buffer); pgns.forEach((pbuffer) => { this.channel.send({ id: canid, ext: true, data: pbuffer }); if (msg.pgn === 126996 || msg.pgn === 126998 || msg.pgn === 60928) { // forward on so these are seen by the server this.push({ pgn, length: pbuffer.length, data: pbuffer }); } if (this.options.app.listenerCount('canboatjs:rawsend') > 0) { this.options.app.emit('canboatjs:rawsend', { knownSrc: true, data: { pgn, length: pbuffer.length, data: (0, utilities_1.byteStringArray)(pbuffer) } }); } }); } else { this.channel.send({ id: canid, ext: true, data: buffer }); if (msg.pgn === 126996 || msg.pgn === 126998 || msg.pgn === 60928) { // forward on so these are seen by the server this.push({ pgn, length: buffer.length, data: buffer }); } if (this.options.app.listenerCount('canboatjs:rawsend') > 0) { this.options.app.emit('canboatjs:rawsend', { knownSrc: true, data: { pgn, length: buffer.length, data: (0, utilities_1.byteStringArray)(buffer) } }); } } if (pgn.pgn == 59904 && pgn.src !== 254 && (pgn.dst == 255 || pgn.dst == this.address)) { if (pgn.PGN !== undefined) { pgn.fields = { pgn: pgn.PGN, ...pgn.fields }; } this.candevice.n2kMessage(pgn); } } }; CanbusStream.prototype._transform = function (chunk, encoding, done) { done(); }; CanbusStream.prototype.end = function () { if (this.candevice) { try { this.candevice.stop(); } catch (e) { console.error('Error stopping candevice in end():', e); } delete this.candevice; } if (this.channel) { // Mark as intentional stop to prevent reconnection this.stoppingChannel = true; const channel = this.channel; delete this.channel; try { channel.stop(); } catch (e) { console.error('Error stopping channel in end():', e); } } if (this.noDataInterval) { clearInterval(this.noDataInterval); this.noDataInterval = null; } }; CanbusStream.prototype.pipe = function (pipeTo) { if (!pipeTo.fromPgn) { this.plainText = true; } return CanbusStream.super_.prototype.pipe.call(this, pipeTo); }; CanbusStream.prototype.createEmulator = function (id, options, addressClaim, productInfo, configInfo) { const device = new CanbusDeviceEmulator(this, id, options, addressClaim, productInfo, configInfo); this.devices[id] = device; return device; }; CanbusStream.prototype.removeEmulator = function (id) { const device = this.devices[id]; if (device) { device.stop(); delete this.devices[id]; } }; class CanbusDeviceEmulator extends stream_1.EventEmitter { stream; device; config; boundListener; id; constructor(stream, id, options, addressClaim, productInfo, configInfo) { super(); this.stream = stream; this.id = id; this.config = { configPath: stream.options.app?.config?.configPath }; this.device = new candevice_1.CanDevice(this, { app: this, providerId: 'emulator-' + id, addressClaim, productInfo, configurationInfo: configInfo }); this.device.start(); this.boundListener = this.pgnReceived.bind(this); stream.options.app.on(stream.options.analyzerOutEvent || 'N2KAnalyzerOut', this.boundListener); } stop() { this.device.stop(); if (this.boundListener) { this.stream.options.app.removeListener(this.stream.options.analyzerOutEvent || 'N2KAnalyzerOut', this.boundListener); } this.boundListener = undefined; this.removeAllListeners(); } pgnReceived(pgn) { this.emit('N2KAnalyzerOut', pgn); } sendPGN(pgn, force) { this.stream.sendPGN(pgn, force, this.device); } send(pgn) { this.stream.sendPGN(pgn, false, this.device); } onPGN(cb) { this.on('N2KAnalyzerOut', cb); } setProviderError(id, error) { console.error(`${id}:${this.id} ${error}`); } setProviderStatus(id, status) { console.log(`${id}:${this.id} ${status}`); } } //# sourceMappingURL=canbus.js.map