UNPKG

@canboat/canboatjs

Version:

Native javascript version of canboat

381 lines (335 loc) 11.4 kB
/** * 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. */ const debug = require('debug')('canboatjs:n2kdevice') const EventEmitter = require('events') const _ = require('lodash') const Uint64LE = require('int64-buffer').Uint64LE const { defaultTransmitPGNs, getIndustryCode, getManufacturerCode, getDeviceClassCode } = require('./codes') const { toPgn } = require('./toPgn') let packageJson try { packageJson = require('../' + 'package.json') } catch (ex) { } const deviceTransmitPGNs = [ 60928, 59904, 126996, 126464 ] class N2kDevice extends EventEmitter { constructor (options) { super() 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, "Unique Number": 1263, "Manufacturer Code": 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"] = options.uniqueNumber || Math.floor(Math.random() * Math.floor(2097151)) } let version = packageJson ? packageJson.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": "canboatjs", "Model Serial Code": options.uniqueNumber ? options.uniqueNumber.toString() : "000001", "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 } } this.options = _.isUndefined(options) ? {} : options this.address = _.isUndefined(options.preferredAddress) ? 100 : options.preferredAddress 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 = _.union(deviceTransmitPGNs, defaultTransmitPGNs) } else { this.transmitPGNs = [...deviceTransmitPGNs] } if ( this.options.transmitPGNs ) { this.transmitPGNs = _.union(this.transmitPGNs, this.options.transmitPGNs) } } start() { sendISORequest(this, 60928, 254) setTimeout(() => { sendAddressClaim(this) }, 1000) } setStatus(msg) { if ( this.options.app && this.options.app.setPluginStatus ) { this.options.app.setProviderStatus(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) console.error(err.stack) } /* 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) { } } function handleISORequest(device, n2kMsg) { debug('handleISORequest %j', n2kMsg) switch (n2kMsg.fields.PGN) { case 126996: // Product Information request sendProductInformation(device) break; case 126998: // Config Information request sendConfigInformation(device) break; case 60928: // ISO address claim request device.sendPGN(device.addressClaim) break; case 126464: sendPGNList(device) break; default: if ( !device.options.disableNAKs ) { debug(`Got unsupported ISO request for PGN ${n2kMsg.fields.PGN}. Sending NAK.`) sendNAKAcknowledgement(device, n2kMsg.src, n2kMsg.fields.PGN) } } } function handleGroupFunction(device, n2kMsg) { debug('handleGroupFunction %j', n2kMsg) if(n2kMsg.fields["Function Code"] === 'Request') { handleRequestGroupFunction(device, n2kMsg) } else if(n2kMsg.fields["Function Code"] === 'Command') { handleCommandGroupFunction(device, n2kMsg) } else { 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" debug("Sending 'PGN Not Supported' Group Function response for requested PGN", n2kMsg.fields.PGN) const acknowledgement = { pgn: 126208, dst: n2kMsg.src, "Function Code": 2, "PGN": n2kMsg.fields.PGN, "PGN error code": 4, "Transmission interval/Priority error code": 0, "# of Parameters": 0 } 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" debug("Sending 'PGN Not Supported' Group Function response for commanded PGN", n2kMsg.fields.PGN) const acknowledgement = { pgn: 126208, dst: n2kMsg.src, "Function Code": 2, "PGN": n2kMsg.fields.PGN, "PGN error code": 4, "Transmission interval/Priority error code": 0, "# of Parameters": 0 } device.sendPGN(acknowledgement) } } } function handleISOAddressClaim(device, n2kMsg) { if ( n2kMsg.src != device.address ) { if ( !device.devices[n2kMsg.src] ) { debug(`registering device ${n2kMsg.src}`) device.devices[n2kMsg.src] = { addressClaim: n2kMsg } if ( device.cansend ) { //sendISORequest(device, 126996, undefined, n2kMsg.src) } } return } debug('Checking ISO address claim. %j', n2kMsg) const uint64ValueFromReceivedClaim = getISOAddressClaimAsUint64(n2kMsg) const uint64ValueFromOurOwnClaim = getISOAddressClaimAsUint64(device.addressClaim) if(uint64ValueFromOurOwnClaim < uint64ValueFromReceivedClaim) { 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) { this.foundConflict = true increaseOwnAddress(device) // We have bigger address claim data -> we have to change our address debug(`Address conflict detected! trying address ${device.address}.`) sendAddressClaim(device) } } function increaseOwnAddress(device) { var 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] = {} } 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 } device.sendPGN({ pgn: 126993, dst: 255, prio:7, "Data transmit offset": "00:01:00", "Sequence Counter": device.heartbeatCounter, "Controller 1 State":"Error Active" }) } function sendAddressClaim(device) { if ( device.devices[device.address] ) { //someone already has this address, so find a free one increaseOwnAddress(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 debug('claimed address %d', 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, dst=255) { debug(`Sending iso request for ${pgn} to ${dst}`) const isoRequest = { pgn: 59904, dst: dst, "PGN": pgn } device.sendPGN(isoRequest, src) } function sendProductInformation(device) { debug("Sending product info..") device.sendPGN(device.productInfo) } function sendConfigInformation(device) { if ( device.configurationInfo ) { debug("Sending config info..") device.sendPGN(device.configurationInfo) } } function sendNAKAcknowledgement(device, src, requestedPGN) { const acknowledgement = { pgn: 59392, dst: src, Control: 1, "Group Function": 255, PGN: requestedPGN } device.sendPGN(acknowledgement) } function sendPGNList(device, src) { //FIXME: for now, adding everything that signalk-to-nmea2000 supports //need a way for plugins, etc. to register the pgns they provide const pgnList = { pgn: 126464, dst: src, "Function Code": 0, list: device.transmitPGNs } device.sendPGN(pgnList) } function getISOAddressClaimAsUint64(pgn) { return new Uint64LE(toPgn(pgn)) } module.exports = N2kDevice