UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,258 lines (1,084 loc) 59.9 kB
/* v8 ignore start */ import events from "node:events"; import net from "node:net"; import slip from "slip"; import {Buffalo} from "../../../buffalo"; import type {Backup} from "../../../models"; import {logger} from "../../../utils/logger"; import {SerialPort} from "../../serialPort"; import SocketPortUtils from "../../socketPortUtils"; import type {NetworkOptions, SerialPortOptions} from "../../tstype"; import PARAM, { ApsAddressMode, type ApsDataRequest, type ApsRequest, DataType, FirmwareCommand, NetworkState, NwkBroadcastAddress, ParamId, type ReceivedDataResponse, type Request, stackParameters, } from "./constants"; import {frameParserEvents} from "./frameParser"; import Parser from "./parser"; import Writer from "./writer"; const NS = "zh:deconz:driver"; const queue: Array<Request> = []; export const busyQueue: Array<Request> = []; const apsQueue: Array<ApsRequest> = []; export const apsBusyQueue: Array<ApsRequest> = []; 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; enum DriverState { Init = 0, Connected = 1, Connecting = 2, ReadConfiguration = 3, WaitToReconnect = 4, Reconfigure = 5, CloseAndRestart = 6, } enum TxState { Idle = 0, WaitResponse = 1, } enum DriverEvent { Action = 0, Connected = 1, Disconnected = 2, DeviceStateUpdated = 3, ConnectError = 4, CloseError = 5, EnqueuedApsDataRequest = 6, Tick = 7, FirmwareCommandSend = 8, FirmwareCommandReceived = 9, FirmwareCommandTimeout = 10, } interface CommandResult { cmd: number; seq: number; } type DriverEventData = number | CommandResult; class Driver extends events.EventEmitter { private serialPort?: SerialPort; private serialPortOptions: SerialPortOptions; private writer: Writer; private parser: Parser; private frameParserEvent = frameParserEvents; private seqNumber: number; private deviceStatus = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore private configChanged: number; private socketPort?: net.Socket; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore private timeoutCounter = 0; private watchdogTriggeredTime = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore private lastFirmwareRxTime = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore private tickTimer: NodeJS.Timeout; private driverStateStart = 0; private driverState: DriverState = DriverState.Init; private firmwareLog: string[]; private transactionID = 0; // for APS and ZDO // in flight lockstep sending commands private txState: TxState = TxState.Idle; private txCommand = 0; private txSeq = 0; private txTime = 0; private networkOptions: NetworkOptions; private backup: Backup | undefined; private configMatchesBackup = false; private configIsNewNetwork = false; public restoredFromBackup = false; public paramMacAddress = 0n; public paramTcAddress = 0n; public paramFirmwareVersion = 0; public paramCurrentChannel = 0; public paramNwkPanid = 0; public paramNwkKey = Buffer.alloc(16); public paramEndpoint0: Buffer | undefined; public paramEndpoint1: Buffer | undefined; public fixParamEndpoint0: Buffer; public fixParamEndpoint1: Buffer; public paramNwkUpdateId = 0; public paramChannelMask = 0; public paramProtocolVersion = 0; public paramFrameCounter = 0; public paramApsUseExtPanid = 0n; public constructor(serialPortOptions: SerialPortOptions, networkOptions: NetworkOptions, backup: Backup | undefined, firmwareLog: string[]) { super(); this.seqNumber = 0; this.configChanged = 0; this.networkOptions = networkOptions; this.serialPortOptions = serialPortOptions; this.backup = backup; this.firmwareLog = firmwareLog; this.writer = new Writer(); this.parser = new Parser(); 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: number) => { 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); }); } public 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 < busyQueue.length; i++) { busyQueue[i].reject(new Error(msg)); } busyQueue.length = 0; for (let i = 0; i < apsQueue.length; i++) { apsQueue[i].reject(new Error(msg)); } apsQueue.length = 0; for (let i = 0; i < apsBusyQueue.length; i++) { apsBusyQueue[i].reject(new Error(msg)); } apsBusyQueue.length = 0; } public started(): boolean { return this.driverState === DriverState.Connected; } protected intervals: NodeJS.Timeout[] = []; protected registerInterval(interval: NodeJS.Timeout): void { this.intervals.push(interval); } protected async catchPromise<T>(val: Promise<T>): Promise<undefined | Awaited<T>> { return (await Promise.resolve(val).catch((err) => logger.debug(`Promise was caught with reason: ${err}`, NS))) as undefined | Awaited<T>; } public nextTransactionID(): number { this.transactionID++; if (this.transactionID > 255) { this.transactionID = 1; } return this.transactionID; } private tick(): void { this.emitStateEvent(DriverEvent.Tick); } private emitStateEvent(event: DriverEvent, data?: DriverEventData) { this.emit(DRIVER_EVENT, event, data); } private needWatchdogReset(): boolean { const now = Date.now(); if (300 * 1000 < now - this.watchdogTriggeredTime) { return true; } return false; } private async resetWatchdog(): Promise<void> { const lastTime = this.watchdogTriggeredTime; try { logger.debug("Reset firmware watchdog", NS); // Set timestamp before command to let needWatchdogReset() no trigger multiple times. this.watchdogTriggeredTime = Date.now(); await this.writeParameterRequest(ParamId.DEV_WATCHDOG_TTL, 600); logger.debug("Reset firmware watchdog success", NS); } catch (_err) { this.watchdogTriggeredTime = lastTime; logger.debug("Reset firmware watchdog failed", NS); } } private handleFirmwareEvent(event: DriverEvent, data?: DriverEventData): void { if (event === DriverEvent.FirmwareCommandSend) { if (this.txState !== TxState.Idle) { throw new Error("Unexpected TX state not idle"); } const d = data as CommandResult; 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 as CommandResult; 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.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); } } } } private handleConnectedStateEvent(event: DriverEvent, _data?: DriverEventData): void { if (event === DriverEvent.DeviceStateUpdated) { this.handleApsQueueOnDeviceState(); } else if (event === DriverEvent.Tick) { if (this.needWatchdogReset()) { this.resetWatchdog(); } this.processQueue(); if (this.txState === TxState.Idle) { this.deviceStatus = 0; // force refresh in response this.sendReadDeviceStateRequest(this.nextSeqNumber()); } } else if (event === DriverEvent.Disconnected) { logger.debug("Disconnected wait and reconnect", NS); this.driverStateStart = Date.now(); this.driverState = DriverState.WaitToReconnect; } } private handleConnectingStateEvent(event: DriverEvent, _data?: DriverEventData): void { 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: Promise<void> | undefined; if (SocketPortUtils.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.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); } } private isNetworkConfigurationValid(): boolean { 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.debug("Configuration matches backup", NS); this.configMatchesBackup = true; } else { 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.debug("Endpoint[0] doesn't match configuration", NS); return false; } if (!this.paramEndpoint1 || this.fixParamEndpoint1.compare(this.paramEndpoint1) !== 0) { logger.debug("Endpoint[1] doesn't match configuration", NS); return false; } if ((this.deviceStatus & DEV_STATUS_NET_STATE_MASK) !== 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; } private async reconfigureNetwork(): Promise<void> { 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.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) === NetworkState.Connected) { await this.sendChangeChannelRequest(); } } } // first disconnect the network await this.changeNetworkStateRequest(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.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(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.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.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(ParamId.APS_TRUST_CENTER_ADDRESS, this.paramTcAddress); } if (this.configIsNewNetwork && this.paramFrameCounter < frameCounter) { this.paramFrameCounter = frameCounter; try { await this.writeParameterRequest(ParamId.STK_FRAME_COUNTER, this.paramFrameCounter); } catch (_err) { // on older firmware versions this fails as unsuppored } } await this.writeParameterRequest(ParamId.STK_NWK_UPDATE_ID, this.paramNwkUpdateId); if (this.networkOptions.channelList.length !== 0) { await this.writeParameterRequest(ParamId.APS_CHANNEL_MASK, 1 << this.networkOptions.channelList[0]); } this.paramNwkPanid = this.networkOptions.panID; await this.writeParameterRequest(ParamId.NWK_PANID, this.networkOptions.panID); await this.writeParameterRequest(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(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(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(ParamId.STK_ENDPOINT, this.paramEndpoint0); } if (!this.paramEndpoint1 || this.fixParamEndpoint1.compare(this.paramEndpoint1) !== 0) { this.paramEndpoint1 = this.fixParamEndpoint1; await this.writeParameterRequest(ParamId.STK_ENDPOINT, this.paramEndpoint1); } // now reconnect, this will also store configuration in nvram await this.changeNetworkStateRequest(NetworkState.Connected); return; } private handleReadConfigurationStateEvent(event: DriverEvent, _data?: DriverEventData): void { if (event === DriverEvent.Action) { logger.debug("Query firmware parameters", NS); this.deviceStatus = 0; // need fresh value Promise.all([ this.resetWatchdog(), this.readFirmwareVersionRequest(), this.readDeviceStatusRequest(), this.readParameterRequest(ParamId.MAC_ADDRESS), this.readParameterRequest(ParamId.APS_TRUST_CENTER_ADDRESS), this.readParameterRequest(ParamId.NWK_PANID), this.readParameterRequest(ParamId.APS_USE_EXTENDED_PANID), this.readParameterRequest(ParamId.STK_CURRENT_CHANNEL), this.readParameterRequest(ParamId.STK_NETWORK_KEY, Buffer.from([0])), this.readParameterRequest(ParamId.STK_NWK_UPDATE_ID), this.readParameterRequest(ParamId.APS_CHANNEL_MASK), this.readParameterRequest(ParamId.STK_PROTOCOL_VERSION), this.readParameterRequest(ParamId.STK_FRAME_COUNTER), this.readParameterRequest(ParamId.STK_ENDPOINT, Buffer.from([0])), this.readParameterRequest(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 as number; this.paramApsUseExtPanid = apsUseExtPanid as bigint; this.paramNwkPanid = panid as number; this.paramNwkKey = nwkKey as Buffer; this.paramNwkUpdateId = nwkUpdateId as number; this.paramMacAddress = mac as bigint; this.paramTcAddress = tcAddress as bigint; this.paramChannelMask = channelMask as number; this.paramProtocolVersion = protocolVersion as number; if (frameCounter !== null) { this.paramFrameCounter = frameCounter as number; } if (ep0 !== null) { this.paramEndpoint0 = ep0 as Buffer; } if (ep1 !== null) { this.paramEndpoint1 = ep1 as Buffer; } // console.log({fwVersion, mac, panid, apsUseExtPanid, currentChannel, nwkKey, nwkUpdateId, channelMask, protocolVersion, frameCounter}); if (this.isNetworkConfigurationValid()) { 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(ParamId.STK_DEBUG_LOG_LEVEL, logLevel) .then((_x) => { logger.debug("Enabled firmware logging", NS); }) .catch((_err) => { 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.debug("Failed to query firmware parameters", NS); }); } else if (event === DriverEvent.Tick) { this.processQueue(); } } private handleReconfigureStateEvent(event: DriverEvent, _data?: DriverEventData): void { if (event === DriverEvent.Action) { logger.debug("Reconfigure Zigbee network to match configuration", NS); this.reconfigureNetwork() .then(() => { this.driverStateStart = Date.now(); this.driverState = DriverState.Connected; }) .catch((err) => { 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(); } } private handleWaitToReconnectStateEvent(event: DriverEvent, _data?: DriverEventData): void { if (event === DriverEvent.Tick) { if (5000 < Date.now() - this.driverStateStart) { this.driverState = DriverState.Connecting; this.emitStateEvent(DriverEvent.Action); } } } private handleCloseAndRestartStateEvent(event: DriverEvent, _data?: DriverEventData): void { 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(); } else { this.driverState = DriverState.WaitToReconnect; } } } } private 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 === 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(); } } } } private handleStateEvent(event: DriverEvent, data?: DriverEventData): void { 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.debug(`handle state: ${DriverState[this.driverState]}, event: ${DriverEvent[event]}`, NS); } } } catch (_err) { // console.error(err); } } private onPortClose(error: boolean | Error): void { if (error) { logger.info(`Port close: state: ${DriverState[this.driverState]}, reason: ${error}`, NS); } else { logger.debug(`Port closed in state: ${DriverState[this.driverState]}`, NS); } this.emitStateEvent(DriverEvent.Disconnected); this.emit("close"); } private onPortError(error: Error): void { logger.error(`Port error: ${error}`, NS); this.emitStateEvent(DriverEvent.Disconnected); this.emit("close"); } private isOpen(): boolean { if (this.serialPort) return this.serialPort.isOpen; if (this.socketPort) return this.socketPort.readyState !== "closed"; return false; } public openSerialPort(baudrate: number): Promise<void> { return new Promise((resolve, reject): void => { if (!this.serialPortOptions.path) { reject(new Error("Failed to open serial port, path is undefined")); } logger.debug(`Opening serial port: ${this.serialPortOptions.path}`, NS); const path = this.serialPortOptions.path || ""; if (!this.serialPort) { this.serialPort = new 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.debug("Serialport opened", NS); this.emitStateEvent(DriverEvent.Connected); resolve(); } }); }); } private async openSocketPort(): Promise<void> { if (!this.serialPortOptions.path) { throw new Error("No serial port TCP path specified"); } const info = SocketPortUtils.parseTcpPath(this.serialPortOptions.path); logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS); this.socketPort = new net.Socket(); this.socketPort.setNoDelay(true); this.socketPort.setKeepAlive(true, 15000); this.writer = new Writer(); this.writer.pipe(this.socketPort); this.parser = new Parser(); this.socketPort.pipe(this.parser); this.parser.on("parsed", this.onParsed); return await new Promise((resolve, reject): void => { // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort!.on("connect", () => { logger.debug("Socket connected", NS); }); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort!.on("ready", () => { 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.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); }); } public close(): Promise<void> { return new Promise((resolve, reject): void => { if (this.serialPort) { if (this.serialPort.isOpen) { // wait until remaining data is written this.serialPort.flush(); this.serialPort.close((error): void => { 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"); } }); } public readParameterRequest(parameterId: ParamId, parameter?: Buffer | number | bigint): Promise<unknown> { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject): void => { //logger.debug(`push read parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId}`, NS); const ts = 0; const commandId = FirmwareCommand.ReadParameter; const networkState = NetworkState.Ignore; const req: Request = {commandId, networkState, parameterId, parameter, seqNumber, resolve, reject, ts}; queue.push(req); }); } public writeParameterRequest(parameterId: ParamId, parameter: Buffer | number | bigint): Promise<void> { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject): void => { //logger.debug(`push write parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId} parameter: ${parameter}`, NS); const ts = 0; const commandId = FirmwareCommand.WriteParameter; const networkState = NetworkState.Ignore; const req: Request = {commandId, networkState, parameterId, parameter, seqNumber, resolve, reject, ts}; queue.push(req); }); } private sendChangeChannelRequest(): Promise<undefined | ReceivedDataResponse> { 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; payload.writeUInt8(zdpSeq, pos); pos += 1; payload.writeUInt32LE(scanChannels, pos); pos += 4; payload.writeUInt8(scanDuration, pos); pos += 1; payload.writeUInt8(this.paramNwkUpdateId, pos); pos += 1; const req: ApsDataRequest = { requestId: this.nextTransactionID(), destAddrMode: ApsAddressMode.Nwk, destAddr16: NwkBroadcastAddress.BroadcastRxOnWhenIdle, destEndpoint: 0, profileId: 0, clusterId: 0x0038, // ZDP_MGMT_NWK_UPDATE_REQ_CLID srcEndpoint: 0, asduLength: payload.length, asduPayload: payload, txOptions: 0, radius: PARAM.PARAM.txRadius.DEFAULT_RADIUS, timeout: PARAM.PARAM.APS.MAX_SEND_TIMEOUT, }; return this.enqueueApsDataRequest(req); } public async writeLinkKey(ieeeAddress: string, hashedKey: Buffer): Promise<void> { 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(ParamId.STK_LINK_KEY, buf); } public readFirmwareVersionRequest(): Promise<number> { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject): void => { //logger.debug(`push read firmware version request to queue. seqNr: ${seqNumber}`, NS); const ts = 0; const commandId = FirmwareCommand.FirmwareVersion; const networkState = NetworkState.Ignore; const parameterId = ParamId.NONE; const req: Request = {commandId, networkState, parameterId, seqNumber, resolve, reject, ts}; queue.push(req); }); } public readDeviceStatusRequest(): Promise<number> { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject): void => { //logger.debug(`push read firmware version request to queue. seqNr: ${seqNumber}`, NS); const ts = 0; const commandId = FirmwareCommand.Status; const networkState = NetworkState.Ignore; const parameterId = ParamId.NONE; const req: Request = {commandId, networkState, parameterId, seqNumber, resolve, reject, ts}; queue.push(req); }); } private sendReadParameterRequest(parameterId: ParamId, seqNumber: number, arg?: Buffer | number | bigint): CommandResult { 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(Buffer.alloc(frameLength)); buf.writeUInt8(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()); } private sendWriteParameterRequest(parameterId: ParamId, value: Buffer | number | bigint, seqNumber: number): CommandResult { // command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id, parameter const param = 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; buf.writeUInt8(FirmwareCommand.WriteParameter, pos); pos += 1; buf.writeUInt8(seqNumber, pos); pos += 1; buf.writeUInt8(0, pos); // status: not used pos += 1; const posFrameLength = pos; // remember buf.writeUInt16LE(0, pos); // dummy frame length pos += 2; // -------------- actual data --------------------------------------- const posPayloadLength = pos; // remember buf.writeUInt16LE(0, pos); // dummy payload length pos += 2; buf.writeUInt8(parameterId, pos); pos += 1; if (value instanceof Buffer) { for (let i = 0; i < value.length; i++) { buf.writeUInt8(value[i], pos); pos += 1; } } else if (typeof value === "number") { if (param.type === DataType.U8) { buf.writeUInt8(value, pos); pos += 1; } else if (param.type === DataType.U16) { buf.writeUInt16LE(value, pos); pos += 2; } else if (param.type === DataType.U32) { buf.writeUInt32LE(value, pos); pos += 4; } else { throw new Error("tried to write unknown parameter number type"); } } else if (typeof value === "bigint") { if (param.type === DataType.U64) { buf.writeBigUInt64LE(value, pos); pos += 8; } 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); } private sendReadFirmwareVersionRequest(seqNumber: number): CommandResult { /* command id, sequence number, 0, framelength(U16) */ return this.sendRequest(Buffer.from([FirmwareCommand.FirmwareVersion, seqNumber, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])); } private sendReadDeviceStateRequest(seqNumber: number): CommandResult { /* command id, sequence number, 0, framelength(U16) */ return this.sendRequest(Buffer.from([FirmwareCommand.Status, seqNumber, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00])); } private sendRequest(buffer: Buffer): CommandResult { const frame = Buffer.concat([buffer, this.calcCrc(buffer)]); const slipframe = slip.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; } private processQueue(): void { if (queue.length === 0) { return; } if (busyQueue.length > 0) { return; } if (this.txState !== TxState.Idle) { return; } const req: Request | undefined = queue.shift(); if (req) { req.ts = Date.now(); try { switch (req.commandId) { case FirmwareCommand.ReadParameter: logger.debug(`send read parameter request from queue. parameter: ${ParamId[req.parameterId]} seq: ${req.seqNumber}`, NS); this.sendReadParameterRequest(req.parameterId, req.seqNumber, req.parameter); break; case FirmwareCommand.WriteParameter: if (req.parameter === undefined) { throw new Error(`Write parameter request without parameter: ${ParamId[req.parameterId]}`); } logger.debug(`Send write parameter request from queue. seq: ${req.seqNumber} parameter: ${ParamId[req.parameterId]}`, NS); this.sendWriteParameterRequest(req.parameterId, req.parameter, req.seqNumber); break; case FirmwareCommand.FirmwareVersion: logger.debug(`Send read firmware version request from queue. seq: ${req.seqNumber}`, NS); this.sendReadFirmwareVersionRequest(req.seqNumber); break; case FirmwareCommand.Status: //logger.debug(`Send read device state from queue. seqNr: ${req.seqNumber}`, NS); this.sendReadDeviceStateRequest(req.seqNumber); break; case FirmwareCommand.ChangeNetworkState: