@canboat/canboatjs
Version:
Native javascript version of canboat
471 lines • 17.1 kB
JavaScript
"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