@canboat/canboatjs
Version:
Native javascript version of canboat
411 lines • 15.8 kB
JavaScript
/**
* Copyright 2025 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.N2kDevice = void 0;
const ts_pgns_1 = require("@canboat/ts-pgns");
const node_events_1 = require("node:events");
const lodash_1 = __importDefault(require("lodash"));
const int64_buffer_1 = require("int64-buffer");
const codes_1 = require("./codes");
const toPgn_1 = require("./toPgn");
const package_json_1 = __importDefault(require("../package.json"));
const persist_1 = require("./persist");
const utilities_1 = require("./utilities");
const deviceTransmitPGNs = [60928, 59904, 126996, 126464];
class N2kDevice extends node_events_1.EventEmitter {
addressClaim;
productInfo;
configurationInfo;
options;
address;
cansend;
foundConflict;
heartbeatCounter;
devices;
sentAvailable;
addressClaimDetectionTime;
transmitPGNs;
addressClaimSentAt;
addressClaimChecker;
heartbeatInterval;
debug;
constructor(options, debugName) {
super();
this.options = options === undefined ? {} : options;
this.debug = (0, utilities_1.createDebug)(debugName, options);
let uniqueNumber;
if (options.uniqueNumber !== undefined) {
uniqueNumber = options.uniqueNumber;
}
else {
uniqueNumber = this.getPersistedData('uniqueNumber');
if (uniqueNumber === undefined) {
uniqueNumber = Math.floor(Math.random() * Math.floor(2097151));
this.savePersistedData('uniqueNumber', uniqueNumber);
}
}
if (options.addressClaim) {
this.addressClaim = options.addressClaim;
this.addressClaim.pgn = 60928;
this.addressClaim.dst = 255;
this.addressClaim.prio = 6;
}
else {
this.addressClaim = {
pgn: 60928,
dst: 255,
prio: 6,
'Manufacturer Code': options.manufacturerCode != undefined
? options.manufacturerCode
: 999,
'Device Function': 130, // PC gateway
'Device Class': 25, // Inter/Intranetwork Device
'Device Instance Lower': 0,
'Device Instance Upper': 0,
'System Instance': 0,
'Industry Group': 4, // Marine
Reserved1: 1,
Reserved2: 2
};
this.addressClaim['Unique Number'] = uniqueNumber;
}
const version = package_json_1.default ? package_json_1.default.version : '1.0';
if (options.productInfo) {
this.productInfo = options.productInfo;
this.productInfo.pgn = 126996;
this.productInfo.dst = 255;
}
else {
this.productInfo = {
pgn: 126996,
dst: 255,
'NMEA 2000 Version': 1300,
'Product Code': 667, // Just made up..
'Model ID': 'Signal K',
'Model Version': getModelVersion(options),
'Model Serial Code': uniqueNumber.toString(),
'Certification Level': 0,
'Load Equivalency': 1
};
}
this.productInfo['Software Version Code'] = version;
if (options.serverVersion && options.serverUrl) {
this.configurationInfo = {
pgn: 126998,
dst: 255,
'Installation Description #1': options.serverUrl,
'Installation Description #2': options.serverDescription,
'Manufacturer Information': options.serverVersion
};
}
let address = undefined;
address = this.getPersistedData('lastAddress');
if (address === undefined) {
address = lodash_1.default.isUndefined(options.preferredAddress)
? 100
: options.preferredAddress;
}
this.address = address;
this.cansend = false;
this.foundConflict = false;
this.heartbeatCounter = 0;
this.devices = {};
this.sentAvailable = false;
this.addressClaimDetectionTime =
options.addressClaimDetectionTime !== undefined
? options.addressClaimDetectionTime
: 5000;
if (!options.disableDefaultTransmitPGNs) {
this.transmitPGNs = lodash_1.default.union(deviceTransmitPGNs, codes_1.defaultTransmitPGNs);
}
else {
this.transmitPGNs = [...deviceTransmitPGNs];
}
if (this.options.transmitPGNs) {
this.transmitPGNs = lodash_1.default.union(this.transmitPGNs, this.options.transmitPGNs);
}
}
start() {
sendISORequest(this, 60928, 254);
setTimeout(() => {
sendAddressClaim(this);
}, 1000);
}
getPersistedData(key) {
try {
return (0, persist_1.getPersistedData)(this.options, this.options.providerId, key);
}
catch (err) {
this.debug('reading persisted data %o', err);
if (err.code !== 'ENOENT') {
console.error(err);
this.setError(err.message);
}
}
}
savePersistedData(key, value) {
try {
(0, persist_1.savePersistedData)(this.options, this.options.providerId, key, value);
}
catch (err) {
console.error(err);
this.setError(err.message);
}
}
setStatus(msg) {
if (this.options.app && this.options.app.setPluginStatus) {
this.options.app.setProviderStatus(this.options.providerId, msg);
}
}
setError(msg) {
if (this.options.app && this.options.app.setPluginStatus) {
this.options.app.setProviderError(this.options.providerId, msg);
}
}
n2kMessage(pgn) {
if (pgn.dst == 255 || pgn.dst == this.address) {
try {
if (pgn.pgn == 59904) {
handleISORequest(this, pgn);
}
else if (pgn.pgn == 126208) {
handleGroupFunction(this, pgn);
}
else if (pgn.pgn == 60928) {
handleISOAddressClaim(this, pgn);
}
else if (pgn.pgn == 126996) {
handleProductInformation(this, pgn);
}
}
catch (err) {
console.error(err);
}
/*
var handler = this.handlers[pgn.pgn.toString()]
if ( pgn.dst == this.address )
debug(`handler ${handler}`)
if ( _.isFunction(handler) ) {
debug(`got handled PGN %j ${handled}`, pgn)
handler(pgn)
}
*/
}
}
sendPGN(_pgn, _src = undefined) { }
}
exports.N2kDevice = N2kDevice;
function getModelVersion(options) {
if (options.app?.config?.getExternalHostname !== undefined) {
return `${options.app.config.ssl ? 'https' : 'http'}://${options.app.config.getExternalHostname()}:${options.app.config.getExternalPort()}`;
}
else {
return 'canboatjs';
}
}
function handleISORequest(device, n2kMsg) {
device.debug('handleISORequest %j', n2kMsg);
const PGN = Number(n2kMsg.fields.pgn);
switch (PGN) {
case 126996: // Product Information request
sendProductInformation(device);
break;
case 126998: // Config Information request
sendConfigInformation(device);
break;
case 60928: // ISO address claim request
device.debug('sending address claim %j', device.addressClaim);
device.sendPGN(device.addressClaim);
break;
case 126464:
sendPGNList(device, n2kMsg.src);
break;
default:
if (!device.options.disableNAKs) {
device.debug(`Got unsupported ISO request for PGN ${PGN}. Sending NAK.`);
sendNAKAcknowledgement(device, n2kMsg.src, PGN);
}
}
}
function handleGroupFunction(device, n2kMsg) {
device.debug('handleGroupFunction %j', n2kMsg);
const functionCode = n2kMsg.fields.functionCode;
if (functionCode === 'Request') {
handleRequestGroupFunction(device, n2kMsg);
}
else if (functionCode === 'Command') {
handleCommandGroupFunction(device, n2kMsg);
}
else {
device.debug('Got unsupported Group Function PGN: %j', n2kMsg);
}
function handleRequestGroupFunction(device, n2kMsg) {
if (!device.options.disableNAKs) {
// We really don't support group function requests for any PGNs yet -> always respond with pgnErrorCode 1 = "PGN not supported"
const PGN = n2kMsg.fields.pgn;
device.debug("Sending 'PGN Not Supported' Group Function response for requested PGN", PGN);
const acknowledgement = new ts_pgns_1.PGN_126208_NmeaAcknowledgeGroupFunction({
pgn: PGN,
pgnErrorCode: ts_pgns_1.PgnErrorCode.NotSupported,
transmissionIntervalPriorityErrorCode: ts_pgns_1.TransmissionInterval.Acknowledge,
numberOfParameters: 0,
list: []
}, n2kMsg.src);
device.sendPGN(acknowledgement);
}
}
function handleCommandGroupFunction(device, n2kMsg) {
if (!device.options.disableNAKs) {
// We really don't support group function commands for any PGNs yet -> always respond with pgnErrorCode 1 = "PGN not supported"
const PGN = n2kMsg.fields.pgn;
device.debug("Sending 'PGN Not Supported' Group Function response for commanded PGN", PGN);
const acknowledgement = new ts_pgns_1.PGN_126208_NmeaAcknowledgeGroupFunction({
pgn: PGN,
pgnErrorCode: ts_pgns_1.PgnErrorCode.NotSupported,
transmissionIntervalPriorityErrorCode: ts_pgns_1.TransmissionInterval.Acknowledge,
numberOfParameters: 0,
list: []
}, n2kMsg.src);
device.sendPGN(acknowledgement);
}
}
}
function handleISOAddressClaim(device, n2kMsg) {
if (n2kMsg.src != device.address) {
if (!device.devices[n2kMsg.src]) {
device.debug(`registering device ${n2kMsg.src}`);
device.devices[n2kMsg.src] = { addressClaim: n2kMsg };
if (device.cansend) {
//sendISORequest(device, 126996, undefined, n2kMsg.src)
}
}
return;
}
device.debug('Checking ISO address claim. %j', n2kMsg);
const uint64ValueFromReceivedClaim = getISOAddressClaimAsUint64(n2kMsg);
const uint64ValueFromOurOwnClaim = getISOAddressClaimAsUint64(device.addressClaim);
if (uint64ValueFromOurOwnClaim < uint64ValueFromReceivedClaim) {
device.debug(`Address conflict detected! Kept our address as ${device.address}.`);
sendAddressClaim(device); // We have smaller address claim data -> we can keep our address -> re-claim it
}
else if (uint64ValueFromOurOwnClaim > uint64ValueFromReceivedClaim) {
device.foundConflict = true;
increaseOwnAddress(device); // We have bigger address claim data -> we have to change our address
device.debug(`Address conflict detected! trying address ${device.address}.`);
sendAddressClaim(device);
}
}
function increaseOwnAddress(device) {
const start = device.address;
do {
device.address = (device.address + 1) % 253;
} while (device.address != start && device.devices[device.address]);
}
function handleProductInformation(device, n2kMsg) {
if (!device.devices[n2kMsg.src]) {
device.devices[n2kMsg.src] = {};
}
device.debug('got product information %j', n2kMsg);
device.devices[n2kMsg.src].productInformation = n2kMsg;
}
function sendHeartbeat(device) {
device.heartbeatCounter = device.heartbeatCounter + 1;
if (device.heartbeatCounter > 252) {
device.heartbeatCounter = 0;
}
const hb = new ts_pgns_1.PGN_126993({
dataTransmitOffset: 60,
sequenceCounter: device.heartbeatCounter,
controller1State: ts_pgns_1.ControllerState.ErrorActive
});
device.sendPGN(hb);
}
function sendAddressClaim(device) {
if (device.devices[device.address]) {
//someone already has this address, so find a free one
increaseOwnAddress(device);
}
device.debug(`Sending address claim ${device.address}`);
device.sendPGN(device.addressClaim);
device.setStatus(`Claimed address ${device.address}`);
device.addressClaimSentAt = Date.now();
if (device.addressClaimChecker) {
clearTimeout(device.addressClaimChecker);
}
device.addressClaimChecker = setTimeout(() => {
//if ( Date.now() - device.addressClaimSentAt > 1000 ) {
//device.addressClaimChecker = null
device.debug('claimed address %d', device.address);
device.savePersistedData('lastAddress', device.address);
device.cansend = true;
if (!device.sentAvailable) {
if (device.options.app) {
device.options.app.emit('nmea2000OutAvailable');
}
device.emit('nmea2000OutAvailable');
device.sentAvailable = true;
}
sendISORequest(device, 126996);
if (!device.heartbeatInterval) {
device.heartbeatInterval = setInterval(() => {
sendHeartbeat(device);
}, 60 * 1000);
}
//}
}, device.addressClaimDetectionTime);
}
function sendISORequest(device, pgn, src = undefined, dst = 255) {
device.debug(`Sending iso request for ${pgn} to ${dst}`);
const isoRequest = new ts_pgns_1.PGN_59904({ pgn });
device.sendPGN(isoRequest, src);
}
function sendProductInformation(device) {
device.debug('Sending product info %j', device.productInfo);
device.sendPGN(device.productInfo);
}
function sendConfigInformation(device) {
if (device.configurationInfo) {
device.debug('Sending config info..');
device.sendPGN(device.configurationInfo);
}
}
function sendNAKAcknowledgement(device, src, requestedPGN) {
const acknowledgement = new ts_pgns_1.PGN_59392({
control: ts_pgns_1.IsoControl.Ack,
groupFunction: 255,
pgn: requestedPGN
}, src);
device.sendPGN(acknowledgement);
}
function sendPGNList(device, dst) {
//FIXME: for now, adding everything that signalk-to-nmea2000 supports
//need a way for plugins, etc. to register the pgns they provide
const pgnList = new ts_pgns_1.PGN_126464({
functionCode: ts_pgns_1.PgnListFunction.TransmitPgnList,
list: device.transmitPGNs.map((num) => {
return { pgn: num };
})
}, dst);
device.sendPGN(pgnList);
}
function getISOAddressClaimAsUint64(pgn) {
return new int64_buffer_1.Uint64LE((0, toPgn_1.toPgn)(pgn));
}
//# sourceMappingURL=n2kDevice.js.map
;