UNPKG

zigbee-herdsman

Version:

An open source Zigbee gateway solution with node.js.

1,102 lines 60.2 kB
"use strict"; /* v8 ignore start */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.apsBusyQueue = exports.apsQueue = exports.busyQueue = void 0; const node_events_1 = __importDefault(require("node:events")); const node_net_1 = __importDefault(require("node:net")); const slip_1 = __importDefault(require("slip")); const buffalo_1 = require("../../../buffalo"); const logger_1 = require("../../../utils/logger"); const serialPort_1 = require("../../serialPort"); const utils_1 = require("../../utils"); const constants_1 = __importStar(require("./constants")); const frameParser_1 = require("./frameParser"); const parser_1 = __importDefault(require("./parser")); const writer_1 = __importDefault(require("./writer")); const NS = "zh:deconz:driver"; const queue = []; exports.busyQueue = []; exports.apsQueue = []; exports.apsBusyQueue = []; const DRIVER_EVENT = Symbol("drv_ev"); const DEV_STATUS_NET_STATE_MASK = 0x03; const DEV_STATUS_APS_CONFIRM = 0x04; const DEV_STATUS_APS_INDICATION = 0x08; const DEV_STATUS_APS_FREE_SLOTS = 0x20; //const DEV_STATUS_CONFIG_CHANGED = 0x10; var DriverState; (function (DriverState) { DriverState[DriverState["Init"] = 0] = "Init"; DriverState[DriverState["Connected"] = 1] = "Connected"; DriverState[DriverState["Connecting"] = 2] = "Connecting"; DriverState[DriverState["ReadConfiguration"] = 3] = "ReadConfiguration"; DriverState[DriverState["WaitToReconnect"] = 4] = "WaitToReconnect"; DriverState[DriverState["Reconfigure"] = 5] = "Reconfigure"; DriverState[DriverState["CloseAndRestart"] = 6] = "CloseAndRestart"; })(DriverState || (DriverState = {})); var TxState; (function (TxState) { TxState[TxState["Idle"] = 0] = "Idle"; TxState[TxState["WaitResponse"] = 1] = "WaitResponse"; })(TxState || (TxState = {})); var DriverEvent; (function (DriverEvent) { DriverEvent[DriverEvent["Action"] = 0] = "Action"; DriverEvent[DriverEvent["Connected"] = 1] = "Connected"; DriverEvent[DriverEvent["Disconnected"] = 2] = "Disconnected"; DriverEvent[DriverEvent["DeviceStateUpdated"] = 3] = "DeviceStateUpdated"; DriverEvent[DriverEvent["ConnectError"] = 4] = "ConnectError"; DriverEvent[DriverEvent["CloseError"] = 5] = "CloseError"; DriverEvent[DriverEvent["EnqueuedApsDataRequest"] = 6] = "EnqueuedApsDataRequest"; DriverEvent[DriverEvent["Tick"] = 7] = "Tick"; DriverEvent[DriverEvent["FirmwareCommandSend"] = 8] = "FirmwareCommandSend"; DriverEvent[DriverEvent["FirmwareCommandReceived"] = 9] = "FirmwareCommandReceived"; DriverEvent[DriverEvent["FirmwareCommandTimeout"] = 10] = "FirmwareCommandTimeout"; })(DriverEvent || (DriverEvent = {})); class Driver extends node_events_1.default.EventEmitter { serialPort; serialPortOptions; writer; parser; frameParserEvent = frameParser_1.frameParserEvents; seqNumber; deviceStatus = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore configChanged; socketPort; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore timeoutCounter = 0; watchdogTriggeredTime = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore lastFirmwareRxTime = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore tickTimer; driverStateStart = 0; driverState = DriverState.Init; firmwareLog; transactionID = 0; // for APS and ZDO // in flight lockstep sending commands txState = TxState.Idle; txCommand = 0; txSeq = 0; txTime = 0; networkOptions; backup; configMatchesBackup = false; configIsNewNetwork = false; restoredFromBackup = false; paramMacAddress = 0n; paramTcAddress = 0n; paramFirmwareVersion = 0; paramCurrentChannel = 0; paramNwkPanid = 0; paramNwkKey = Buffer.alloc(16); paramEndpoint0; paramEndpoint1; fixParamEndpoint0; fixParamEndpoint1; paramNwkUpdateId = 0; paramChannelMask = 0; paramProtocolVersion = 0; paramFrameCounter = 0; paramApsUseExtPanid = 0n; constructor(serialPortOptions, networkOptions, backup, firmwareLog) { super(); this.seqNumber = 0; this.configChanged = 0; this.networkOptions = networkOptions; this.serialPortOptions = serialPortOptions; this.backup = backup; this.firmwareLog = firmwareLog; this.writer = new writer_1.default(); this.parser = new parser_1.default(); this.fixParamEndpoint0 = Buffer.from([ 0x00, // index 0x01, // endpoint, 0x04, // profileId 0x01, 0x05, // deviceId 0x00, 0x01, // deviceVersion 0x05, // in cluster count 0x00, // basic 0x00, 0x06, // on/off 0x00, 0x0a, // time 0x00, 0x19, // ota 0x00, 0x01, // ias ace 0x05, 0x04, // out cluster count 0x01, // power configuration 0x00, 0x20, // poll control 0x00, 0x00, // ias zone 0x05, 0x02, // ias wd 0x05, ]); this.fixParamEndpoint1 = Buffer.from([ 0x01, // index 0xf2, // endpoint, 0xe0, // profileId 0xa1, 0x64, // deviceId 0x00, 0x01, // deviceVersion 0x00, // in cluster count 0x01, // out cluster count 0x21, // green power 0x00, ]); this.tickTimer = setInterval(() => { this.tick(); }, 100); this.onParsed = this.onParsed.bind(this); this.frameParserEvent.on("deviceStateUpdated", (data) => { this.checkDeviceStatus(data); }); this.on("close", () => { for (const interval of this.intervals) { clearInterval(interval); } this.timeoutCounter = 0; this.cleanupAllQueues(); }); this.on(DRIVER_EVENT, (event, data) => { this.handleStateEvent(event, data); }); } cleanupAllQueues() { const msg = `Cleanup in state: ${DriverState[this.driverState]}`; for (let i = 0; i < queue.length; i++) { queue[i].reject(new Error(msg)); } queue.length = 0; for (let i = 0; i < exports.busyQueue.length; i++) { exports.busyQueue[i].reject(new Error(msg)); } exports.busyQueue.length = 0; for (let i = 0; i < exports.apsQueue.length; i++) { exports.apsQueue[i].reject(new Error(msg)); } exports.apsQueue.length = 0; for (let i = 0; i < exports.apsBusyQueue.length; i++) { exports.apsBusyQueue[i].reject(new Error(msg)); } exports.apsBusyQueue.length = 0; } started() { return this.driverState === DriverState.Connected; } intervals = []; registerInterval(interval) { this.intervals.push(interval); } async catchPromise(val) { return (await Promise.resolve(val).catch((err) => logger_1.logger.debug(`Promise was caught with reason: ${err}`, NS))); } nextTransactionID() { this.transactionID++; if (this.transactionID > 255) { this.transactionID = 1; } return this.transactionID; } tick() { this.emitStateEvent(DriverEvent.Tick); } emitStateEvent(event, data) { this.emit(DRIVER_EVENT, event, data); } needWatchdogReset() { const now = Date.now(); if (300 * 1000 < now - this.watchdogTriggeredTime) { return true; } return false; } async resetWatchdog() { const lastTime = this.watchdogTriggeredTime; try { logger_1.logger.debug("Reset firmware watchdog", NS); // Set timestamp before command to let needWatchdogReset() no trigger multiple times. this.watchdogTriggeredTime = Date.now(); await this.writeParameterRequest(constants_1.ParamId.DEV_WATCHDOG_TTL, 600); logger_1.logger.debug("Reset firmware watchdog success", NS); } catch (_err) { this.watchdogTriggeredTime = lastTime; logger_1.logger.debug("Reset firmware watchdog failed", NS); } } handleFirmwareEvent(event, data) { if (event === DriverEvent.FirmwareCommandSend) { if (this.txState !== TxState.Idle) { throw new Error("Unexpected TX state not idle"); } const d = data; this.txState = TxState.WaitResponse; this.txCommand = d.cmd; this.txSeq = d.seq; this.txTime = Date.now(); //logger.debug(`tx wait for cmd: ${d.cmd.toString(16).padStart(2, "0")}, seq: ${d.seq}`, NS); } else if (event === DriverEvent.FirmwareCommandReceived) { if (this.txState !== TxState.WaitResponse) { return; } const d = data; if (this.txCommand === d.cmd && this.txSeq === d.seq) { this.txState = TxState.Idle; //logger.debug(`tx released for cmd: ${d.cmd.toString(16).padStart(2, "0")}, seq: ${d.seq}`, NS); } } else if (event === DriverEvent.FirmwareCommandTimeout) { if (this.txState === TxState.WaitResponse) { this.txState = TxState.Idle; logger_1.logger.debug(`tx timeout for cmd: ${this.txCommand.toString(16).padStart(2, "0")}, seq: ${this.txSeq}`, NS); } } else if (event === DriverEvent.Tick) { if (this.txState === TxState.WaitResponse) { if (Date.now() - this.txTime > 2000) { this.emitStateEvent(DriverEvent.FirmwareCommandTimeout); } } } } handleConnectedStateEvent(event, _data) { if (event === DriverEvent.DeviceStateUpdated) { this.handleApsQueueOnDeviceState(); } else if (event === DriverEvent.Tick) { if (this.needWatchdogReset()) { this.resetWatchdog().catch(() => { }); } this.processQueue(); if (this.txState === TxState.Idle) { this.deviceStatus = 0; // force refresh in response this.sendReadDeviceStateRequest(this.nextSeqNumber()); } } else if (event === DriverEvent.Disconnected) { logger_1.logger.debug("Disconnected wait and reconnect", NS); this.driverStateStart = Date.now(); this.driverState = DriverState.WaitToReconnect; } } handleConnectingStateEvent(event, _data) { if (event === DriverEvent.Action) { this.watchdogTriggeredTime = 0; // force reset watchdog this.cleanupAllQueues(); // start with fresh queues // TODO(mpi): In future we should simply try which baudrate may work (in a state machine). // E.g. connect with baudrate XY, query firmware, on timeout try other baudrate. // Most units out there are ConBee2/3 which support 115200. // The 38400 default is outdated now and only works for a few units. const baudrate = this.serialPortOptions.baudRate || 38400; if (!this.serialPortOptions.path) { // unlikely but handle it anyway this.driverStateStart = Date.now(); this.driverState = DriverState.WaitToReconnect; return; } let prom; if ((0, utils_1.isTcpPath)(this.serialPortOptions.path)) { prom = this.openSocketPort(); } else if (baudrate) { prom = this.openSerialPort(baudrate); } else { // unlikely but handle it anyway this.driverStateStart = Date.now(); this.driverState = DriverState.WaitToReconnect; } if (prom) { prom.catch((err) => { logger_1.logger.debug(`${err}`, NS); this.driverStateStart = Date.now(); this.driverState = DriverState.WaitToReconnect; }); } } else if (event === DriverEvent.Connected) { this.driverStateStart = Date.now(); this.driverState = DriverState.ReadConfiguration; this.emitStateEvent(DriverEvent.Action); } } isNetworkConfigurationValid() { const opts = this.networkOptions; let configExtPanID = 0n; const configNetworkKey = Buffer.from(opts.networkKey || []); if (opts.extendedPanID) { // NOTE(mpi): U64 values in buffer are big endian! configExtPanID = Buffer.from(opts.extendedPanID).readBigUInt64BE(); } if (this.backup) { // NOTE(mpi): U64 values in buffer are big endian! const backupExtPanID = Buffer.from(this.backup.networkOptions.extendedPanId).readBigUInt64BE(); if (opts.panID === this.backup.networkOptions.panId && configExtPanID === backupExtPanID && opts.channelList.includes(this.backup.logicalChannel) && configNetworkKey.equals(this.backup.networkOptions.networkKey)) { logger_1.logger.debug("Configuration matches backup", NS); this.configMatchesBackup = true; } else { logger_1.logger.debug("Configuration doesn't match backup (ignore backup)", NS); this.configMatchesBackup = false; // ignore Backup } } if (this.paramMacAddress !== this.paramTcAddress) { return false; } if (!this.paramEndpoint0 || this.fixParamEndpoint0.compare(this.paramEndpoint0) !== 0) { logger_1.logger.debug("Endpoint[0] doesn't match configuration", NS); return false; } if (!this.paramEndpoint1 || this.fixParamEndpoint1.compare(this.paramEndpoint1) !== 0) { logger_1.logger.debug("Endpoint[1] doesn't match configuration", NS); return false; } if ((this.deviceStatus & DEV_STATUS_NET_STATE_MASK) !== constants_1.NetworkState.Connected) { return false; } if (opts.channelList.find((ch) => ch === this.paramCurrentChannel) === undefined) { return false; } if (configExtPanID !== 0n) { if (configExtPanID !== this.paramApsUseExtPanid) { this.configIsNewNetwork = true; return false; } } if (opts.panID !== this.paramNwkPanid) { return false; } if (opts.networkKey) { if (!configNetworkKey.equals(this.paramNwkKey)) { // this.configIsNewNetwork = true; // maybe, but we need to consider key rotation return false; } } if (this.backup && this.configMatchesBackup) { // The backup might be from another unit, if the mac doesn't match clone it! // NOTE(mpi): U64 values in buffer are big endian! const backupMacAddress = this.backup.coordinatorIeeeAddress.readBigUInt64BE(); if (backupMacAddress !== this.paramMacAddress) { this.configIsNewNetwork = true; return false; } if (this.paramNwkUpdateId < this.backup.networkUpdateId) { return false; } // NOTE(mpi): Ignore the frame counter for now and only handle in case of this.configIsNewNetwork == true. // TODO(mpi): We might also check Trust Center Link Key and key sequence number (unlikely but possible case). } // TODO(mpi): Check endpoint configuration // const ep1 = = await this.driver.readParameterRequest(PARAM.PARAM.STK.Endpoint,); return true; } async reconfigureNetwork() { const opts = this.networkOptions; // if the configuration has a different channel, broadcast a channel change to the network first if (this.networkOptions.channelList.length !== 0) { if (opts.channelList[0] !== this.paramCurrentChannel) { logger_1.logger.debug(`change channel from ${this.paramCurrentChannel} to ${opts.channelList[0]}`, NS); // increase the NWK Update ID so devices which search for the network know this is an update this.paramNwkUpdateId = (this.paramNwkUpdateId + 1) % 255; this.paramCurrentChannel = opts.channelList[0]; if ((this.deviceStatus & DEV_STATUS_NET_STATE_MASK) === constants_1.NetworkState.Connected) { await this.sendChangeChannelRequest(); } } } // first disconnect the network await this.changeNetworkStateRequest(constants_1.NetworkState.Disconnected); // check if a backup needs to be applied // Ember check if backup is needed: // - panId, extPanId, network key different -> leave network // - left or not joined -> consider using backup // backup is only used when matching the z2m config: panId, extPanId, channel, network key // parameters restored from backup: // - networkKey, // - networkKeyInfo.sequenceNumber NOTE(mpi): not a reason for using backup!? // - networkKeyInfo.frameCounter // - networkOptions.panId // - extendedPanId // - logicalChannel // - backup!.ezsp!.hashed_tclk! NOTE(mpi): not a reason for using backup!? // - backup!.networkUpdateId NOTE(mpi): not a reason for using backup!? let frameCounter = 0; if (this.backup && this.configMatchesBackup) { // NOTE(mpi): U64 values in buffer are big endian! const backupMacAddress = this.backup.coordinatorIeeeAddress.readBigUInt64BE(); if (backupMacAddress !== this.paramMacAddress) { logger_1.logger.debug(`Use mac address from backup 0x${backupMacAddress.toString(16).padStart(16, "0")}, replaces 0x${this.paramMacAddress.toString(16).padStart(16, "0")}`, NS); this.paramMacAddress = backupMacAddress; this.restoredFromBackup = true; await this.writeParameterRequest(constants_1.ParamId.MAC_ADDRESS, backupMacAddress); } if (this.configIsNewNetwork && this.paramFrameCounter < this.backup.networkKeyInfo.frameCounter) { // delicate situation, only update frame counter if: // - backup counter is higher // - this is in fact a new network // - configIsNewNetwork guards also from mistreating counter overflow logger_1.logger.debug(`Use higher frame counter from backup ${this.backup.networkKeyInfo.frameCounter}`, NS); // Additionally increase frame counter. Note this might still be too low! frameCounter = this.backup.networkKeyInfo.frameCounter + 1000; this.restoredFromBackup = true; } if (this.paramNwkUpdateId < this.backup.networkUpdateId) { logger_1.logger.debug(`Use network update ID from backup ${this.backup.networkUpdateId}`, NS); this.paramNwkUpdateId = this.backup.networkUpdateId; this.restoredFromBackup = true; } // TODO(mpi): Later on also check key sequence number. } if (this.paramMacAddress !== this.paramTcAddress) { this.paramTcAddress = this.paramMacAddress; await this.writeParameterRequest(constants_1.ParamId.APS_TRUST_CENTER_ADDRESS, this.paramTcAddress); } if (this.configIsNewNetwork && this.paramFrameCounter < frameCounter) { this.paramFrameCounter = frameCounter; try { await this.writeParameterRequest(constants_1.ParamId.STK_FRAME_COUNTER, this.paramFrameCounter); } catch (_err) { // on older firmware versions this fails as unsuppored } } await this.writeParameterRequest(constants_1.ParamId.STK_NWK_UPDATE_ID, this.paramNwkUpdateId); if (this.networkOptions.channelList.length !== 0) { await this.writeParameterRequest(constants_1.ParamId.APS_CHANNEL_MASK, 1 << this.networkOptions.channelList[0]); } this.paramNwkPanid = this.networkOptions.panID; await this.writeParameterRequest(constants_1.ParamId.NWK_PANID, this.networkOptions.panID); await this.writeParameterRequest(constants_1.ParamId.STK_PREDEFINED_PANID, 1); if (this.networkOptions.extendedPanID) { // NOTE(mpi): U64 values in buffer are big endian! this.paramApsUseExtPanid = Buffer.from(this.networkOptions.extendedPanID).readBigUInt64BE(); await this.writeParameterRequest(constants_1.ParamId.APS_USE_EXTENDED_PANID, this.paramApsUseExtPanid); } // check current network key against configuration.yaml if (this.networkOptions.networkKey) { this.paramNwkKey = Buffer.from(this.networkOptions.networkKey); await this.writeParameterRequest(constants_1.ParamId.STK_NETWORK_KEY, Buffer.from([0x0, ...this.networkOptions.networkKey])); } // check current endpoint configuration if (!this.paramEndpoint0 || this.fixParamEndpoint0.compare(this.paramEndpoint0) !== 0) { this.paramEndpoint0 = this.fixParamEndpoint0; await this.writeParameterRequest(constants_1.ParamId.STK_ENDPOINT, this.paramEndpoint0); } if (!this.paramEndpoint1 || this.fixParamEndpoint1.compare(this.paramEndpoint1) !== 0) { this.paramEndpoint1 = this.fixParamEndpoint1; await this.writeParameterRequest(constants_1.ParamId.STK_ENDPOINT, this.paramEndpoint1); } // now reconnect, this will also store configuration in nvram await this.changeNetworkStateRequest(constants_1.NetworkState.Connected); return; } handleReadConfigurationStateEvent(event, _data) { if (event === DriverEvent.Action) { logger_1.logger.debug("Query firmware parameters", NS); this.deviceStatus = 0; // need fresh value Promise.all([ this.resetWatchdog(), this.readFirmwareVersionRequest(), this.readDeviceStatusRequest(), this.readParameterRequest(constants_1.ParamId.MAC_ADDRESS), this.readParameterRequest(constants_1.ParamId.APS_TRUST_CENTER_ADDRESS), this.readParameterRequest(constants_1.ParamId.NWK_PANID), this.readParameterRequest(constants_1.ParamId.APS_USE_EXTENDED_PANID), this.readParameterRequest(constants_1.ParamId.STK_CURRENT_CHANNEL), this.readParameterRequest(constants_1.ParamId.STK_NETWORK_KEY, Buffer.from([0])), this.readParameterRequest(constants_1.ParamId.STK_NWK_UPDATE_ID), this.readParameterRequest(constants_1.ParamId.APS_CHANNEL_MASK), this.readParameterRequest(constants_1.ParamId.STK_PROTOCOL_VERSION), this.readParameterRequest(constants_1.ParamId.STK_FRAME_COUNTER), this.readParameterRequest(constants_1.ParamId.STK_ENDPOINT, Buffer.from([0])), this.readParameterRequest(constants_1.ParamId.STK_ENDPOINT, Buffer.from([1])), ]) .then(([_watchdog, fwVersion, _deviceState, mac, tcAddress, panid, apsUseExtPanid, currentChannel, nwkKey, nwkUpdateId, channelMask, protocolVersion, frameCounter, ep0, ep1,]) => { this.paramFirmwareVersion = fwVersion; this.paramCurrentChannel = currentChannel; this.paramApsUseExtPanid = apsUseExtPanid; this.paramNwkPanid = panid; this.paramNwkKey = nwkKey; this.paramNwkUpdateId = nwkUpdateId; this.paramMacAddress = mac; this.paramTcAddress = tcAddress; this.paramChannelMask = channelMask; this.paramProtocolVersion = protocolVersion; if (frameCounter !== null) { this.paramFrameCounter = frameCounter; } if (ep0 !== null) { this.paramEndpoint0 = ep0; } if (ep1 !== null) { this.paramEndpoint1 = ep1; } // console.log({fwVersion, mac, panid, apsUseExtPanid, currentChannel, nwkKey, nwkUpdateId, channelMask, protocolVersion, frameCounter}); if (this.isNetworkConfigurationValid()) { logger_1.logger.debug("Zigbee configuration valid", NS); this.driverStateStart = Date.now(); this.driverState = DriverState.Connected; // enable optional firmware debug messages let logLevel = 0; for (const level of this.firmwareLog) { if (level === "APS") logLevel |= 0x00000100; else if (level === "APS_L2") logLevel |= 0x00010000; } if (logLevel !== 0) { this.writeParameterRequest(constants_1.ParamId.STK_DEBUG_LOG_LEVEL, logLevel) .then((_x) => { logger_1.logger.debug("Enabled firmware logging", NS); }) .catch((_err) => { logger_1.logger.debug("Firmware logging unsupported by firmware", NS); }); } } else { this.driverStateStart = Date.now(); this.driverState = DriverState.Reconfigure; this.emitStateEvent(DriverEvent.Action); } }) .catch((_err) => { this.driverStateStart = Date.now(); this.driverState = DriverState.CloseAndRestart; logger_1.logger.debug("Failed to query firmware parameters", NS); }); } else if (event === DriverEvent.Tick) { this.processQueue(); } } handleReconfigureStateEvent(event, _data) { if (event === DriverEvent.Action) { logger_1.logger.debug("Reconfigure Zigbee network to match configuration", NS); this.reconfigureNetwork() .then(() => { this.driverStateStart = Date.now(); this.driverState = DriverState.Connected; }) .catch((err) => { logger_1.logger.debug(`Failed to reconfigure Zigbee network, error: ${err}, wait 15 seconds to retry`, NS); this.driverStateStart = Date.now(); }); } else if (event === DriverEvent.Tick) { this.processQueue(); // if we run into this timeout assume some error and retry after waiting a bit if (15000 < Date.now() - this.driverStateStart) { this.driverStateStart = Date.now(); this.driverState = DriverState.CloseAndRestart; } if (this.txState === TxState.Idle) { // needed to process channel change ZDP request this.deviceStatus = 0; // force refresh in response this.sendReadDeviceStateRequest(this.nextSeqNumber()); } } else if (event === DriverEvent.DeviceStateUpdated) { this.handleApsQueueOnDeviceState(); } } handleWaitToReconnectStateEvent(event, _data) { if (event === DriverEvent.Tick) { if (5000 < Date.now() - this.driverStateStart) { this.driverState = DriverState.Connecting; this.emitStateEvent(DriverEvent.Action); } } } handleCloseAndRestartStateEvent(event, _data) { if (event === DriverEvent.Tick) { if (1000 < Date.now() - this.driverStateStart) { // if the connection is open try to close it every second. this.driverStateStart = Date.now(); if (this.isOpen()) { this.close().catch(() => { }); } else { this.driverState = DriverState.WaitToReconnect; } } } } handleApsQueueOnDeviceState() { // logger.debug(`Updated device status: ${data.toString(2)}`, NS); const netState = this.deviceStatus & DEV_STATUS_NET_STATE_MASK; if (this.txState === TxState.Idle) { if (netState === constants_1.NetworkState.Connected) { const status = this.deviceStatus; if (status & DEV_STATUS_APS_CONFIRM) { this.deviceStatus = 0; // force refresh in response this.sendReadApsConfirmRequest(this.nextSeqNumber()); } else if (status & DEV_STATUS_APS_INDICATION) { this.deviceStatus = 0; // force refresh in response this.sendReadApsIndicationRequest(this.nextSeqNumber()); } else if (status & DEV_STATUS_APS_FREE_SLOTS) { this.deviceStatus = 0; // force refresh in response this.processApsQueue(); } } } } handleStateEvent(event, data) { try { // all states if (event === DriverEvent.Tick || event === DriverEvent.FirmwareCommandReceived || event === DriverEvent.FirmwareCommandSend || event === DriverEvent.FirmwareCommandTimeout) { this.handleFirmwareEvent(event, data); this.processBusyQueueTimeouts(); this.processApsBusyQueueTimeouts(); } if (this.driverState === DriverState.Init) { this.driverState = DriverState.WaitToReconnect; this.driverStateStart = 0; // force fast initial connect } else if (this.driverState === DriverState.Connected) { this.handleConnectedStateEvent(event, data); } else if (this.driverState === DriverState.Connecting) { this.handleConnectingStateEvent(event, data); } else if (this.driverState === DriverState.WaitToReconnect) { this.handleWaitToReconnectStateEvent(event, data); } else if (this.driverState === DriverState.ReadConfiguration) { this.handleReadConfigurationStateEvent(event, data); } else if (this.driverState === DriverState.Reconfigure) { this.handleReconfigureStateEvent(event, data); } else if (this.driverState === DriverState.CloseAndRestart) { this.handleCloseAndRestartStateEvent(event, data); } else { if (event !== DriverEvent.Tick) { logger_1.logger.debug(`handle state: ${DriverState[this.driverState]}, event: ${DriverEvent[event]}`, NS); } } } catch (_err) { // console.error(err); } } onPortClose(error) { if (error) { logger_1.logger.info(`Port close: state: ${DriverState[this.driverState]}, reason: ${error}`, NS); } else { logger_1.logger.debug(`Port closed in state: ${DriverState[this.driverState]}`, NS); } this.emitStateEvent(DriverEvent.Disconnected); this.emit("close"); } onPortError(error) { logger_1.logger.error(`Port error: ${error}`, NS); this.emitStateEvent(DriverEvent.Disconnected); this.emit("close"); } isOpen() { if (this.serialPort) return this.serialPort.isOpen; if (this.socketPort) return this.socketPort.readyState !== "closed"; return false; } openSerialPort(baudrate) { return new Promise((resolve, reject) => { if (!this.serialPortOptions.path) { reject(new Error("Failed to open serial port, path is undefined")); } logger_1.logger.debug(`Opening serial port: ${this.serialPortOptions.path}`, NS); const path = this.serialPortOptions.path || ""; if (!this.serialPort) { this.serialPort = new serialPort_1.SerialPort({ path, baudRate: baudrate, autoOpen: false }); this.writer.pipe(this.serialPort); this.serialPort.pipe(this.parser); this.parser.on("parsed", this.onParsed); this.serialPort.on("close", this.onPortClose.bind(this)); this.serialPort.on("error", this.onPortError.bind(this)); } if (!this.serialPort) { reject(new Error("Failed to create SerialPort instance")); return; } if (this.serialPort.isOpen) { resolve(); return; } this.serialPort.open((error) => { if (error) { reject(new Error(`Error while opening serialport '${error}'`)); if (this.serialPort) { if (this.serialPort.isOpen) { this.emitStateEvent(DriverEvent.ConnectError); //this.serialPort!.close(); } } } else { logger_1.logger.debug("Serialport opened", NS); this.emitStateEvent(DriverEvent.Connected); resolve(); } }); }); } async openSocketPort() { if (!this.serialPortOptions.path) { throw new Error("No serial port TCP path specified"); } const info = (0, utils_1.parseTcpPath)(this.serialPortOptions.path); logger_1.logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS); this.socketPort = new node_net_1.default.Socket(); this.socketPort.setNoDelay(true); this.socketPort.setKeepAlive(true, 15000); this.writer = new writer_1.default(); this.writer.pipe(this.socketPort); this.parser = new parser_1.default(); this.socketPort.pipe(this.parser); this.parser.on("parsed", this.onParsed); return await new Promise((resolve, reject) => { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.on("connect", () => { logger_1.logger.debug("Socket connected", NS); }); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.on("ready", () => { logger_1.logger.debug("Socket ready", NS); this.emitStateEvent(DriverEvent.Connected); resolve(); }); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.once("close", this.onPortClose); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.on("error", (error) => { logger_1.logger.error(`Socket error ${error}`, NS); reject(new Error("Error while opening socket")); }); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.connect(info.port, info.host); }); } close() { return new Promise((resolve, reject) => { if (this.serialPort) { if (this.serialPort.isOpen) { // wait until remaining data is written this.serialPort.flush(); this.serialPort.close((error) => { if (error) { // TODO(mpi): monitor, this must not happen after drain // close() failes if there is pending data to write! this.emitStateEvent(DriverEvent.CloseError); reject(new Error(`Error while closing serialport '${error}'`)); return; } }); } this.emitStateEvent(DriverEvent.Disconnected); this.emit("close"); resolve(); } else if (this.socketPort) { this.socketPort.destroy(); this.socketPort = undefined; this.emitStateEvent(DriverEvent.Disconnected); resolve(); } else { resolve(); this.emit("close"); } }); } readParameterRequest(parameterId, parameter) { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject) => { //logger.debug(`push read parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId}`, NS); const ts = 0; const commandId = constants_1.FirmwareCommand.ReadParameter; const networkState = constants_1.NetworkState.Ignore; const req = { commandId, networkState, parameterId, parameter, seqNumber, resolve, reject, ts }; queue.push(req); }); } writeParameterRequest(parameterId, parameter) { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject) => { //logger.debug(`push write parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId} parameter: ${parameter}`, NS); const ts = 0; const commandId = constants_1.FirmwareCommand.WriteParameter; const networkState = constants_1.NetworkState.Ignore; const req = { commandId, networkState, parameterId, parameter, seqNumber, resolve, reject, ts }; queue.push(req); }); } sendChangeChannelRequest() { const zdpSeq = this.nextTransactionID(); const scanChannels = 1 << this.networkOptions.channelList[0]; const scanDuration = 0xfe; // special value = channel change const payload = Buffer.alloc(7); let pos = 0; pos = payload.writeUInt8(zdpSeq, pos); pos = payload.writeUInt32LE(scanChannels, pos); pos = payload.writeUInt8(scanDuration, pos); pos = payload.writeUInt8(this.paramNwkUpdateId, pos); const req = { requestId: this.nextTransactionID(), destAddrMode: constants_1.ApsAddressMode.Nwk, destAddr16: constants_1.NwkBroadcastAddress.BroadcastRxOnWhenIdle, destEndpoint: 0, profileId: 0, clusterId: 0x0038, // ZDP_MGMT_NWK_UPDATE_REQ_CLID srcEndpoint: 0, asduLength: payload.length, asduPayload: payload, txOptions: 0, radius: constants_1.default.PARAM.txRadius.DEFAULT_RADIUS, timeout: constants_1.default.PARAM.APS.MAX_SEND_TIMEOUT, }; return this.enqueueApsDataRequest(req); } async writeLinkKey(ieeeAddress, hashedKey) { const buf = Buffer.alloc(8 + 16); if (ieeeAddress[1] !== "x") { ieeeAddress = `0x${ieeeAddress}`; } buf.writeBigUint64LE(BigInt(ieeeAddress)); for (let i = 0; i < 16; i++) { buf.writeUint8(hashedKey[i], 8 + i); } await this.writeParameterRequest(constants_1.ParamId.STK_LINK_KEY, buf); } readFirmwareVersionRequest() { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject) => { //logger.debug(`push read firmware version request to queue. seqNr: ${seqNumber}`, NS); const ts = 0; const commandId = constants_1.FirmwareCommand.FirmwareVersion; const networkState = constants_1.NetworkState.Ignore; const parameterId = constants_1.ParamId.NONE; const req = { commandId, networkState, parameterId, seqNumber, resolve, reject, ts }; queue.push(req); }); } readDeviceStatusRequest() { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject) => { //logger.debug(`push read firmware version request to queue. seqNr: ${seqNumber}`, NS); const ts = 0; const commandId = constants_1.FirmwareCommand.Status; const networkState = constants_1.NetworkState.Ignore; const parameterId = constants_1.ParamId.NONE; const req = { commandId, networkState, parameterId, seqNumber, resolve, reject, ts }; queue.push(req); }); } sendReadParameterRequest(parameterId, seqNumber, arg) { let frameLength = 8; // starts with min. frame length let payloadLength = 1; // min. parameterId if (arg instanceof Buffer) { payloadLength += arg.byteLength; frameLength += arg.byteLength; } const buf = new buffalo_1.Buffalo(Buffer.alloc(frameLength)); buf.writeUInt8(constants_1.FirmwareCommand.ReadParameter); buf.writeUInt8(seqNumber); buf.writeUInt8(0); // reserved, shall be 0 buf.writeUInt16(frameLength); buf.writeUInt16(payloadLength); buf.writeUInt8(parameterId); if (arg instanceof Buffer) { buf.writeBuffer(arg, arg.byteLength); } return this.sendRequest(buf.getBuffer()); } sendWriteParameterRequest(parameterId, value, seqNumber) { // command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id, parameter const param = constants_1.stackParameters.find((x) => x.id === parameterId); if (!param) { throw new Error("tried to write unknown stack parameter"); } const buf = Buffer.alloc(128); let pos = 0; pos = buf.writeUInt8(constants_1.FirmwareCommand.WriteParameter, pos); pos = buf.writeUInt8(seqNumber, pos); pos = buf.writeUInt8(0, pos); // status: not used const posFrameLength = pos; // remember pos = buf.writeUInt16LE(0, pos); // dummy frame length // -------------- actual data --------------------------------------- const posPayloadLength = pos; // remember pos = buf.writeUInt16LE(0, pos); // dummy payload length pos = buf.writeUInt8(parameterId, pos); if (value instanceof Buffer) { for (let i = 0; i < value.length; i++) { pos = buf.writeUInt8(value[i], pos); } } else if (typeof value === "number") { if (param.type === constants_1.DataType.U8) { pos = buf.writeUInt8(value, pos); } else if (param.type === constants_1.DataType.U16) { pos = buf.writeUInt16LE(value, pos); } else if (param.type === constants_1.DataType.U32) { pos = buf.writeUInt32LE(value, pos); } else { throw new Error("tried to write unknown parameter number type"); } } else if (typeof value === "bigint") { if (param.type === constants_1.DataType.U64) { pos = buf.writeBigUInt64LE(value, pos); } else { throw new Error("tried to write unknown parameter number type"); } } else { throw new Error("tried to write unknown parameter type"); } const payloadLength = pos - (posPayloadLength + 2); buf.writeUInt16LE(payloadLength, posPayloadLength); // actual payload length buf.writeUInt16LE(pos, posFrameLength); // actual frame length const out = buf.subarray(0, pos); return this.sendRequest(out); } sendReadFirmwareVersionRequest(seqNumber) { /* command id, sequence number, 0, framelength(U16) */ return this.sendRequest(Buffer.from([constants_1.FirmwareCommand.FirmwareVersion, seqNumber, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])); } sendReadDeviceStateRequest(seqNumber) { /* command id, sequence number, 0, framelength(U16) */ return this.sendRequest(Buffer.from([constants_1.FirmwareCommand.Status, seqNumber, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00])); } sendRequest(buffer) { const frame = Buffer.concat([buffer, this.calcCrc(buffer)]); const slipframe = slip_1.default.encode(frame); if (frame[0] === 0x00) { throw new Error(`send unexpected frame with invalid command ID: 0x${frame[0].toString(16).padStart(2, "0")}`); } if (slipframe.length >= 256) { throw new Error("send unexpected long slip frame"); } let written = false; if (this.serialPort) { if (!this.serialPort.isOpen) { throw new Error("Can't write to serial port while it isn't open"); } for (let retry = 0; retry < 3 && !written; retry++) { written = this.serialPort.write(slipframe, (err) => { if (err) { throw new Error(`Failed to write to serial port: ${err.message}`); } }); // if written is false, we also need to wait for drain() this.serialPort.drain(); // flush } } else if (this.socketPort) { written = this.socketPort.write(slipframe, (err) => { if (err) { throw new Error(`Failed to write to serial port: ${err.message}`); } written = true; }); // handle in upper functions // if (!written) { // await this.sleep(1000); // } } if (!written) { throw new Error(`Failed to send request cmd: ${frame[0]}, seq: ${frame[1]}`); } const result = { cmd: frame[0], seq: frame[1] }; this.emitStateEvent(DriverEvent.FirmwareCommandSend, result); return result; } processQueue() { if (queue.length === 0) { return; } if (exports.busyQueue.length > 0) { return; } if (this.txState !== TxState.Idle) { return; } const req = queue.shift(); if (req) { req.ts = Date.now(); try { switch (req.commandId) { case constants_1.FirmwareCommand.ReadParameter: logger_1.logger.debug(`send read parameter request from queue. parameter: ${constants_1.ParamId[req.parameterId]} seq: ${req.seqNumber}`, NS); this.sendReadParameterRequest(req.parameterId, req.seqNumber, req.parameter); break; case constants_1.FirmwareCommand.WriteParameter: if (req.parameter === undefined) { throw new Error(`Write parameter request without parameter: ${constants_1.ParamId[req.parameterId]}`); } logger_1.logger.debug(`Send write parameter request from queue. seq: ${req.seqNumber} parameter: ${constants_1.ParamId[req.parameterId]}`, NS); this.sendWriteParameterRequest(req.parameterId, req.parameter, req.seqNumber); break; case constants_1.FirmwareCommand.FirmwareVersion: logger_1.logger.debug(`