UNPKG

zigbee-on-host

Version:

ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP)

991 lines 207 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OTRCPDriver = exports.NetworkKeyUpdateMethod = exports.ApplicationKeyRequestPolicy = exports.TrustCenterKeyRequestPolicy = exports.InstallCodePolicy = void 0; const node_events_1 = __importDefault(require("node:events")); const node_fs_1 = require("node:fs"); const promises_1 = require("node:fs/promises"); const node_path_1 = require("node:path"); const hdlc_js_1 = require("../spinel/hdlc.js"); const spinel_js_1 = require("../spinel/spinel.js"); const statuses_js_1 = require("../spinel/statuses.js"); const logger_js_1 = require("../utils/logger.js"); const mac_js_1 = require("../zigbee/mac.js"); const zigbee_js_1 = require("../zigbee/zigbee.js"); const zigbee_aps_js_1 = require("../zigbee/zigbee-aps.js"); const zigbee_nwk_js_1 = require("../zigbee/zigbee-nwk.js"); const zigbee_nwkgp_js_1 = require("../zigbee/zigbee-nwkgp.js"); const descriptors_js_1 = require("./descriptors.js"); const ot_rcp_parser_js_1 = require("./ot-rcp-parser.js"); const ot_rcp_writer_js_1 = require("./ot-rcp-writer.js"); const NS = "ot-rcp-driver"; var InstallCodePolicy; (function (InstallCodePolicy) { /** Do not support Install Codes */ InstallCodePolicy[InstallCodePolicy["NOT_SUPPORTED"] = 0] = "NOT_SUPPORTED"; /** Support but do not require use of Install Codes or preset passphrases */ InstallCodePolicy[InstallCodePolicy["NOT_REQUIRED"] = 1] = "NOT_REQUIRED"; /** Require the use of Install Codes by joining devices or preset Passphrases */ InstallCodePolicy[InstallCodePolicy["REQUIRED"] = 2] = "REQUIRED"; })(InstallCodePolicy || (exports.InstallCodePolicy = InstallCodePolicy = {})); var TrustCenterKeyRequestPolicy; (function (TrustCenterKeyRequestPolicy) { TrustCenterKeyRequestPolicy[TrustCenterKeyRequestPolicy["DISALLOWED"] = 0] = "DISALLOWED"; /** Any device MAY request */ TrustCenterKeyRequestPolicy[TrustCenterKeyRequestPolicy["ALLOWED"] = 1] = "ALLOWED"; /** Only devices in the apsDeviceKeyPairSet with a KeyAttribute value of PROVISIONAL_KEY MAY request. */ TrustCenterKeyRequestPolicy[TrustCenterKeyRequestPolicy["ONLY_PROVISIONAL"] = 2] = "ONLY_PROVISIONAL"; })(TrustCenterKeyRequestPolicy || (exports.TrustCenterKeyRequestPolicy = TrustCenterKeyRequestPolicy = {})); var ApplicationKeyRequestPolicy; (function (ApplicationKeyRequestPolicy) { ApplicationKeyRequestPolicy[ApplicationKeyRequestPolicy["DISALLOWED"] = 0] = "DISALLOWED"; /** Any device MAY request an application link key with any device (except the Trust Center) */ ApplicationKeyRequestPolicy[ApplicationKeyRequestPolicy["ALLOWED"] = 1] = "ALLOWED"; /** Only those devices listed in applicationKeyRequestList MAY request and receive application link keys. */ ApplicationKeyRequestPolicy[ApplicationKeyRequestPolicy["ONLY_APPROVED"] = 2] = "ONLY_APPROVED"; })(ApplicationKeyRequestPolicy || (exports.ApplicationKeyRequestPolicy = ApplicationKeyRequestPolicy = {})); var NetworkKeyUpdateMethod; (function (NetworkKeyUpdateMethod) { /** Broadcast using only network encryption */ NetworkKeyUpdateMethod[NetworkKeyUpdateMethod["BROADCAST"] = 0] = "BROADCAST"; /** Unicast using network encryption and APS encryption with a device’s link key. */ NetworkKeyUpdateMethod[NetworkKeyUpdateMethod["UNICAST"] = 1] = "UNICAST"; })(NetworkKeyUpdateMethod || (exports.NetworkKeyUpdateMethod = NetworkKeyUpdateMethod = {})); // const SPINEL_FRAME_MAX_SIZE = 1300; // const SPINEL_FRAME_MAX_COMMAND_HEADER_SIZE = 4; // const SPINEL_FRAME_MAX_COMMAND_PAYLOAD_SIZE = SPINEL_FRAME_MAX_SIZE - SPINEL_FRAME_MAX_COMMAND_HEADER_SIZE; // const SPINEL_ENCRYPTER_EXTRA_DATA_SIZE = 0; // const SPINEL_FRAME_BUFFER_SIZE = SPINEL_FRAME_MAX_SIZE + SPINEL_ENCRYPTER_EXTRA_DATA_SIZE; const CONFIG_TID_MASK = 0x0e; const CONFIG_HIGHWATER_MARK = hdlc_js_1.HDLC_TX_CHUNK_SIZE * 4; /** The number of OctetDurations until a route discovery expires. */ // const CONFIG_NWK_ROUTE_DISCOVERY_TIME = 0x4c4b4; // 0x2710 msec on 2.4GHz /** The maximum depth of the network (number of hops) used for various calculations of network timing and limitations. */ const CONFIG_NWK_MAX_DEPTH = 15; const CONFIG_NWK_MAX_HOPS = CONFIG_NWK_MAX_DEPTH * 2; /** The number of network layer retries on unicast messages that are attempted before reporting the result to the higher layer. */ // const CONFIG_NWK_UNICAST_RETRIES = 3; /** The delay between network layer retries. (ms) */ // const CONFIG_NWK_UNICAST_RETRY_DELAY = 50; /** The total delivery time for a broadcast transmission to be delivered to all RxOnWhenIdle=TRUE devices in the network. (sec) */ // const CONFIG_NWK_BCAST_DELIVERY_TIME = 9; /** The time between link status command frames (msec) */ const CONFIG_NWK_LINK_STATUS_PERIOD = 15000; /** Avoid synchronization with other nodes by randomizing `CONFIG_NWK_LINK_STATUS_PERIOD` with this (msec) */ const CONFIG_NWK_LINK_STATUS_JITTER = 1000; /** The number of missed link status command frames before resetting the link costs to zero. */ // const CONFIG_NWK_ROUTER_AGE_LIMIT = 3; /** This is an index into Table 3-54. It indicates the default timeout in minutes for any end device that does not negotiate a different timeout value. */ // const CONFIG_NWK_END_DEVICE_TIMEOUT_DEFAULT = 8; /** The time between concentrator route discoveries. (msec) */ const CONFIG_NWK_CONCENTRATOR_DISCOVERY_TIME = 60000; /** The hop count radius for concentrator route discoveries. */ const CONFIG_NWK_CONCENTRATOR_RADIUS = CONFIG_NWK_MAX_HOPS; /** The number of delivery failures that trigger an immediate concentrator route discoveries. */ const CONFIG_NWK_CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD = 1; /** The number of route failures that trigger an immediate concentrator route discoveries. */ const CONFIG_NWK_CONCENTRATOR_ROUTE_FAILURE_THRESHOLD = 3; /** Minimum Time between MTORR broadcasts (msec) */ const CONFIG_NWK_CONCENTRATOR_MIN_TIME = 10000; /** The time between state saving to disk. (msec) */ const CONFIG_SAVE_STATE_TIME = 60000; class OTRCPDriver extends node_events_1.default { writer; parser; streamRawConfig; savePath; #emitMACFrames; #protocolVersionMajor = 0; #protocolVersionMinor = 0; #ncpVersion = ""; #interfaceType = 0; #rcpAPIVersion = 0; #rcpMinHostAPIVersion = 0; /** The minimum observed RSSI */ rssiMin = -100; /** The maximum observed RSSI */ rssiMax = -25; /** The minimum observed LQI */ lqiMin = 15; /** The maximum observed LQI */ lqiMax = 250; /** * Transaction ID used in Spinel frame * * NOTE: 0 is used for "no response expected/needed" (e.g. unsolicited update commands from NCP to host) */ #spinelTID; /** Sequence number used in outgoing MAC frames */ #macSeqNum; /** Sequence number used in outgoing NWK frames */ #nwkSeqNum; /** Counter used in outgoing APS frames */ #apsCounter; /** Sequence number used in outgoing ZDO frames */ #zdoSeqNum; /** * 8-bit sequence number for route requests. Incremented by 1 every time the NWK layer on a particular device issues a route request. */ #routeRequestId; /** If defined, indicates we're waiting for the property with the specific payload to come in */ #resetWaiter; /** TID currently being awaited */ #tidWaiters; #stateLoaded; #networkUp; #saveStateTimeout; #pendingChangeChannel; #nwkLinkStatusTimeout; #manyToOneRouteRequestTimeout; /** Associations pending DATA_RQ from device. Mapping by network64 */ pendingAssociations; /** Indirect transmission for devices with rxOnWhenIdle set to false. Mapping by network64 */ indirectTransmissions; /** Count of MAC NO_ACK reported by Spinel for each device (only present if any). Mapping by network16 */ macNoACKs; /** Count of route failures reported by the network for each device (only present if any). Mapping by network16 */ routeFailures; //---- Trust Center (see 05-3474-R #4.7.1) #trustCenterPolicies; #macAssociationPermit; #allowJoinTimeout; //----- Green Power (see 14-0563-18) #gpCommissioningMode; #gpCommissioningWindowTimeout; #gpLastMACSequenceNumber; #gpLastSecurityFrameCounter; //---- NWK netParams; /** pre-computed hash of default TC link key for VERIFY_KEY. set by `loadState` */ #tcVerifyKeyHash; /** Time of last many-to-one route request */ #lastMTORRTime; /** Master table of all known devices on the network. mapping by network64 */ deviceTable; /** Lookup synced with deviceTable, maps network address to IEEE address */ address16ToAddress64; /** mapping by network16 */ sourceRouteTable; // TODO: possibility of a route/sourceRoute blacklist? //---- APS /** mapping by network16 */ // public readonly apsDeviceKeyPairSet: Map<number, APSDeviceKeyPairSet>; /** mapping by network16 */ // public readonly apsBindingTable: Map<number, APSBindingTable>; //---- Attribute /** Several attributes are set by `loadState` */ configAttributes; constructor(streamRawConfig, netParams, saveDir, emitMACFrames = false) { super(); if (!(0, node_fs_1.existsSync)(saveDir)) { (0, node_fs_1.mkdirSync)(saveDir); } this.savePath = (0, node_path_1.join)(saveDir, "zoh.save"); this.#emitMACFrames = emitMACFrames; this.streamRawConfig = streamRawConfig; this.writer = new ot_rcp_writer_js_1.OTRCPWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); this.parser = new ot_rcp_parser_js_1.OTRCPParser({ readableHighWaterMark: CONFIG_HIGHWATER_MARK }); this.#spinelTID = -1; // start at 0 but effectively 1 returned by first nextTID() call this.#resetWaiter = undefined; this.#tidWaiters = new Map(); this.#macSeqNum = 0; // start at 1 this.#nwkSeqNum = 0; // start at 1 this.#apsCounter = 0; // start at 1 this.#zdoSeqNum = 0; // start at 1 this.#routeRequestId = 0; // start at 1 this.#stateLoaded = false; this.#networkUp = false; this.pendingAssociations = new Map(); this.indirectTransmissions = new Map(); this.macNoACKs = new Map(); this.routeFailures = new Map(); //---- Trust Center this.#trustCenterPolicies = { allowJoins: false, installCode: InstallCodePolicy.NOT_REQUIRED, allowRejoinsWithWellKnownKey: true, allowTCKeyRequest: TrustCenterKeyRequestPolicy.ALLOWED, networkKeyUpdatePeriod: 0, // disable networkKeyUpdateMethod: NetworkKeyUpdateMethod.BROADCAST, allowAppKeyRequest: ApplicationKeyRequestPolicy.DISALLOWED, // appKeyRequestList: undefined, allowRemoteTCPolicyChange: false, allowVirtualDevices: false, }; this.#macAssociationPermit = false; //---- Green Power this.#gpCommissioningMode = false; this.#gpLastMACSequenceNumber = -1; this.#gpLastSecurityFrameCounter = -1; //---- NWK this.netParams = netParams; this.#tcVerifyKeyHash = Buffer.alloc(0); // set by `loadState` this.#lastMTORRTime = 0; this.deviceTable = new Map(); this.address16ToAddress64 = new Map(); this.sourceRouteTable = new Map(); //---- APS // this.apsDeviceKeyPairSet = new Map(); // this.apsBindingTable = new Map(); //---- Attributes this.configAttributes = { address: Buffer.alloc(0), // set by `loadState` nodeDescriptor: Buffer.alloc(0), // set by `loadState` powerDescriptor: Buffer.alloc(0), // set by `loadState` simpleDescriptors: Buffer.alloc(0), // set by `loadState` activeEndpoints: Buffer.alloc(0), // set by `loadState` }; } // #region Getters/Setters get protocolVersionMajor() { return this.#protocolVersionMajor; } get protocolVersionMinor() { return this.#protocolVersionMinor; } get ncpVersion() { return this.#ncpVersion; } get interfaceType() { return this.#interfaceType; } get rcpAPIVersion() { return this.#rcpAPIVersion; } get rcpMinHostAPIVersion() { return this.#rcpMinHostAPIVersion; } get currentSpinelTID() { return this.#spinelTID + 1; } // #endregion // #region TIDs/counters /** * @returns increased TID offsetted by +1. [1-14] range for the "actually-used" value (0 is reserved) */ nextSpinelTID() { this.#spinelTID = (this.#spinelTID + 1) % CONFIG_TID_MASK; return this.#spinelTID + 1; } nextMACSeqNum() { this.#macSeqNum = (this.#macSeqNum + 1) & 0xff; return this.#macSeqNum; } nextNWKSeqNum() { this.#nwkSeqNum = (this.#nwkSeqNum + 1) & 0xff; return this.#nwkSeqNum; } nextAPSCounter() { this.#apsCounter = (this.#apsCounter + 1) & 0xff; return this.#apsCounter; } nextZDOSeqNum() { this.#zdoSeqNum = (this.#zdoSeqNum + 1) & 0xff; return this.#zdoSeqNum; } nextTCKeyFrameCounter() { this.netParams.tcKeyFrameCounter = (this.netParams.tcKeyFrameCounter + 1) & 0xffffffff; return this.netParams.tcKeyFrameCounter; } nextNWKKeyFrameCounter() { this.netParams.networkKeyFrameCounter = (this.netParams.networkKeyFrameCounter + 1) & 0xffffffff; return this.netParams.networkKeyFrameCounter; } nextRouteRequestId() { this.#routeRequestId = (this.#routeRequestId + 1) & 0xff; return this.#routeRequestId; } decrementRadius(radius) { // XXX: init at 29 when passed CONFIG_NWK_MAX_HOPS? return radius - 1 || 1; } // #endregion /** * Get the basic info from the RCP firmware and reset it. * @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#appendix-C.1 * * Should be called before `formNetwork` but after `resetNetwork` (if needed) */ async start() { logger_js_1.logger.info("======== Driver starting ========", NS); await this.loadState(); // flush this.writer.writeBuffer(Buffer.from([126 /* HdlcReservedByte.FLAG */])); // Example output: // Protocol version: 4.3 // NCP version: SL-OPENTHREAD/2.5.2.0_GitHub-1fceb225b; EFR32; Mar 19 2025 13:45:44 // Interface type: 3 // RCP API version: 10 // RCP min host API version: 4 // check the protocol version to see if it is supported let response = await this.getProperty(1 /* SpinelPropertyId.PROTOCOL_VERSION */); [this.#protocolVersionMajor, this.#protocolVersionMinor] = (0, spinel_js_1.readPropertyii)(1 /* SpinelPropertyId.PROTOCOL_VERSION */, response.payload); logger_js_1.logger.info(`Protocol version: ${this.#protocolVersionMajor}.${this.#protocolVersionMinor}`, NS); // check the NCP version to see if a firmware update may be necessary response = await this.getProperty(2 /* SpinelPropertyId.NCP_VERSION */); // recommended format: STACK-NAME/STACK-VERSION[BUILD_INFO][; OTHER_INFO]; BUILD_DATE_AND_TIME this.#ncpVersion = (0, spinel_js_1.readPropertyU)(2 /* SpinelPropertyId.NCP_VERSION */, response.payload).replaceAll("\u0000", ""); logger_js_1.logger.info(`NCP version: ${this.#ncpVersion}`, NS); // check interface type to make sure that it is what we expect response = await this.getProperty(3 /* SpinelPropertyId.INTERFACE_TYPE */); this.#interfaceType = (0, spinel_js_1.readPropertyi)(3 /* SpinelPropertyId.INTERFACE_TYPE */, response.payload); logger_js_1.logger.info(`Interface type: ${this.#interfaceType}`, NS); response = await this.getProperty(176 /* SpinelPropertyId.RCP_API_VERSION */); this.#rcpAPIVersion = (0, spinel_js_1.readPropertyi)(176 /* SpinelPropertyId.RCP_API_VERSION */, response.payload); logger_js_1.logger.info(`RCP API version: ${this.#rcpAPIVersion}`, NS); response = await this.getProperty(177 /* SpinelPropertyId.RCP_MIN_HOST_API_VERSION */); this.#rcpMinHostAPIVersion = (0, spinel_js_1.readPropertyi)(177 /* SpinelPropertyId.RCP_MIN_HOST_API_VERSION */, response.payload); logger_js_1.logger.info(`RCP min host API version: ${this.#rcpMinHostAPIVersion}`, NS); await this.sendCommand(1 /* SpinelCommandId.RESET */, Buffer.from([2 /* SpinelResetReason.STACK */]), false); await this.waitForReset(); logger_js_1.logger.info("======== Driver started ========", NS); } async stop() { logger_js_1.logger.info("======== Driver stopping ========", NS); this.disallowJoins(); this.gpExitCommissioningMode(); const networkWasUp = this.#networkUp; // pre-emptive this.#networkUp = false; // TODO: clear all timeouts/intervals if (this.#resetWaiter?.timer) { clearTimeout(this.#resetWaiter.timer); this.#resetWaiter.timer = undefined; this.#resetWaiter = undefined; } clearTimeout(this.#saveStateTimeout); this.#saveStateTimeout = undefined; clearTimeout(this.#nwkLinkStatusTimeout); this.#nwkLinkStatusTimeout = undefined; clearTimeout(this.#manyToOneRouteRequestTimeout); this.#manyToOneRouteRequestTimeout = undefined; clearTimeout(this.#pendingChangeChannel); this.#pendingChangeChannel = undefined; for (const [, waiter] of this.#tidWaiters) { clearTimeout(waiter.timer); waiter.timer = undefined; waiter.reject(new Error("Driver stopping", { cause: statuses_js_1.SpinelStatus.INVALID_STATE })); } this.#tidWaiters.clear(); if (networkWasUp) { // TODO: proper spinel/radio shutdown? await this.setProperty((0, spinel_js_1.writePropertyb)(32 /* SpinelPropertyId.PHY_ENABLED */, false)); await this.setProperty((0, spinel_js_1.writePropertyb)(55 /* SpinelPropertyId.MAC_RAW_STREAM_ENABLED */, false)); } await this.saveState(); logger_js_1.logger.info("======== Driver stopped ========", NS); } async waitForReset() { await new Promise((resolve, reject) => { this.#resetWaiter = { timer: setTimeout(reject.bind(this, new Error("Reset timeout after 5000ms", { cause: statuses_js_1.SpinelStatus.RESPONSE_TIMEOUT })), 5000), resolve, }; }); } /** * Performs a STACK reset after resetting a few PHY/MAC properties to default. * If up, will stop network before. */ async resetStack() { await this.setProperty((0, spinel_js_1.writePropertyC)(48 /* SpinelPropertyId.MAC_SCAN_STATE */, 0 /* SCAN_STATE_IDLE */)); // await this.setProperty(writePropertyC(SpinelPropertyId.MAC_PROMISCUOUS_MODE, 0 /* MAC_PROMISCUOUS_MODE_OFF */)); await this.setProperty((0, spinel_js_1.writePropertyb)(32 /* SpinelPropertyId.PHY_ENABLED */, false)); await this.setProperty((0, spinel_js_1.writePropertyb)(55 /* SpinelPropertyId.MAC_RAW_STREAM_ENABLED */, false)); if (this.#networkUp) { await this.stop(); } await this.sendCommand(1 /* SpinelCommandId.RESET */, Buffer.from([2 /* SpinelResetReason.STACK */]), false); await this.waitForReset(); } /** * Performs a software reset into bootloader. * If up, will stop network before. */ async resetIntoBootloader() { if (this.#networkUp) { await this.stop(); } await this.sendCommand(1 /* SpinelCommandId.RESET */, Buffer.from([3 /* SpinelResetReason.BOOTLOADER */]), false); } // #region HDLC/Spinel async onFrame(buffer) { const hdlcFrame = (0, hdlc_js_1.decodeHdlcFrame)(buffer); // logger.debug(() => `<--- HDLC[length=${hdlcFrame.length}]`, NS); const spinelFrame = (0, spinel_js_1.decodeSpinelFrame)(hdlcFrame); /* v8 ignore start */ if (spinelFrame.header.flg !== spinel_js_1.SPINEL_HEADER_FLG_SPINEL) { // non-Spinel frame (likely BLE HCI) return; } /* v8 ignore stop */ logger_js_1.logger.debug(() => `<--- SPINEL[tid=${spinelFrame.header.tid} cmdId=${spinelFrame.commandId} len=${spinelFrame.payload.byteLength}]`, NS); // resolve waiter if any (never for tid===0 since unsolicited frames) const waiter = spinelFrame.header.tid > 0 ? this.#tidWaiters.get(spinelFrame.header.tid) : undefined; let status = statuses_js_1.SpinelStatus.OK; if (waiter) { clearTimeout(waiter.timer); } if (spinelFrame.commandId === 6 /* SpinelCommandId.PROP_VALUE_IS */) { const [propId, pOffset] = (0, spinel_js_1.getPackedUInt)(spinelFrame.payload, 0); switch (propId) { case 113 /* SpinelPropertyId.STREAM_RAW */: { const [macData, metadata] = (0, spinel_js_1.readStreamRaw)(spinelFrame.payload, pOffset); await this.onStreamRawFrame(macData, metadata); break; } case 0 /* SpinelPropertyId.LAST_STATUS */: { [status] = (0, spinel_js_1.getPackedUInt)(spinelFrame.payload, pOffset); // verbose, waiter will provide feedback // logger.debug(() => `<--- SPINEL LAST_STATUS[${SpinelStatus[status]}]`, NS); // TODO: getting RESET_POWER_ON after RESET instead of RESET_SOFTWARE?? if (this.#resetWaiter && (status === statuses_js_1.SpinelStatus.RESET_SOFTWARE || status === statuses_js_1.SpinelStatus.RESET_POWER_ON)) { clearTimeout(this.#resetWaiter.timer); this.#resetWaiter.resolve(spinelFrame); this.#resetWaiter = undefined; } break; } case 57 /* SpinelPropertyId.MAC_ENERGY_SCAN_RESULT */: { // https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#section-5.8.10 let resultOffset = pOffset; const channel = spinelFrame.payload.readUInt8(resultOffset); resultOffset += 1; const rssi = spinelFrame.payload.readInt8(resultOffset); resultOffset += 1; logger_js_1.logger.info(`<=== ENERGY_SCAN[channel=${channel} rssi=${rssi}]`, NS); break; } } } if (waiter) { if (status === statuses_js_1.SpinelStatus.OK) { waiter.resolve(spinelFrame); } else { waiter.reject(new Error(`Failed with status=${statuses_js_1.SpinelStatus[status]}`, { cause: status })); } } this.#tidWaiters.delete(spinelFrame.header.tid); } /** * Logic optimizes code paths to try to avoid more parsing when frames will eventually get ignored by detecting as early as possible. */ async onStreamRawFrame(payload, metadata) { // discard MAC frames before network is started if (!this.#networkUp) { return; } if (this.#emitMACFrames) { setImmediate(() => { this.emit("macFrame", payload, metadata?.rssi); }); } try { const [macFCF, macFCFOutOffset] = (0, mac_js_1.decodeMACFrameControl)(payload, 0); // TODO: process BEACON for PAN ID conflict detection? if (macFCF.frameType !== 3 /* MACFrameType.CMD */ && macFCF.frameType !== 1 /* MACFrameType.DATA */) { logger_js_1.logger.debug(() => `<-~- MAC Ignoring frame with type not CMD/DATA (${macFCF.frameType})`, NS); return; } const [macHeader, macHOutOffset] = (0, mac_js_1.decodeMACHeader)(payload, macFCFOutOffset, macFCF); if (metadata) { logger_js_1.logger.debug(() => `<--- SPINEL STREAM_RAW METADATA[rssi=${metadata.rssi} noiseFloor=${metadata.noiseFloor} flags=${metadata.flags}]`, NS); } const macPayload = (0, mac_js_1.decodeMACPayload)(payload, macHOutOffset, macFCF, macHeader); if (macFCF.frameType === 3 /* MACFrameType.CMD */) { await this.processMACCommand(macPayload, macHeader); // done return; } if (macHeader.destinationPANId !== 65535 /* ZigbeeMACConsts.BCAST_PAN */ && macHeader.destinationPANId !== this.netParams.panId) { logger_js_1.logger.debug(() => `<-~- MAC Ignoring frame with mismatching PAN Id ${macHeader.destinationPANId}`, NS); return; } if (macFCF.destAddrMode === 2 /* MACFrameAddressMode.SHORT */ && macHeader.destination16 !== 65535 /* ZigbeeMACConsts.BCAST_ADDR */ && macHeader.destination16 !== 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */) { logger_js_1.logger.debug(() => `<-~- MAC Ignoring frame intended for device ${macHeader.destination16}`, NS); return; } if (macPayload.byteLength > 0) { const protocolVersion = (macPayload.readUInt8(0) & 60 /* ZigbeeNWKConsts.FCF_VERSION */) >> 2; if (protocolVersion === 3 /* ZigbeeNWKConsts.VERSION_GREEN_POWER */) { if ((macFCF.destAddrMode === 2 /* MACFrameAddressMode.SHORT */ && macHeader.destination16 === 65535 /* ZigbeeMACConsts.BCAST_ADDR */) || macFCF.destAddrMode === 3 /* MACFrameAddressMode.EXT */) { const [nwkGPFCF, nwkGPFCFOutOffset] = (0, zigbee_nwkgp_js_1.decodeZigbeeNWKGPFrameControl)(macPayload, 0); const [nwkGPHeader, nwkGPHOutOffset] = (0, zigbee_nwkgp_js_1.decodeZigbeeNWKGPHeader)(macPayload, nwkGPFCFOutOffset, nwkGPFCF); if (nwkGPHeader.frameControl.frameType !== 0 /* ZigbeeNWKGPFrameType.DATA */ && nwkGPHeader.frameControl.frameType !== 1 /* ZigbeeNWKGPFrameType.MAINTENANCE */) { logger_js_1.logger.debug(() => `<-~- NWKGP Ignoring frame with type ${nwkGPHeader.frameControl.frameType}`, NS); return; } if (this.checkZigbeeNWKGPDuplicate(macHeader, nwkGPHeader)) { logger_js_1.logger.debug(() => `<-~- NWKGP Ignoring duplicate frame macSeqNum=${macHeader.sequenceNumber} nwkGPFC=${nwkGPHeader.securityFrameCounter}`, NS); return; } const nwkGPPayload = (0, zigbee_nwkgp_js_1.decodeZigbeeNWKGPPayload)(macPayload, nwkGPHOutOffset, this.netParams.networkKey, macHeader.source64, nwkGPFCF, nwkGPHeader); this.processZigbeeNWKGPFrame(nwkGPPayload, macHeader, nwkGPHeader, this.computeLQA(metadata?.rssi ?? this.rssiMin)); } else { logger_js_1.logger.debug(() => `<-x- NWKGP Invalid frame addressing ${macFCF.destAddrMode} (${macHeader.destination16})`, NS); return; } } else { const [nwkFCF, nwkFCFOutOffset] = (0, zigbee_nwk_js_1.decodeZigbeeNWKFrameControl)(macPayload, 0); const [nwkHeader, nwkHOutOffset] = (0, zigbee_nwk_js_1.decodeZigbeeNWKHeader)(macPayload, nwkFCFOutOffset, nwkFCF); if (macHeader.destination16 !== undefined && macHeader.destination16 >= 65528 /* ZigbeeConsts.BCAST_MIN */ && nwkHeader.source16 === 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */) { logger_js_1.logger.debug(() => "<-~- NWK Ignoring frame from coordinator (broadcast loopback)", NS); return; } const sourceLQA = this.computeDeviceLQA(nwkHeader.source16, nwkHeader.source64, metadata?.rssi ?? this.rssiMin); const nwkPayload = (0, zigbee_nwk_js_1.decodeZigbeeNWKPayload)(macPayload, nwkHOutOffset, undefined, // use pre-hashed this.netParams.networkKey, /* nwkHeader.frameControl.extendedSource ? nwkHeader.source64 : this.address16ToAddress64.get(nwkHeader.source16!) */ nwkHeader.source64 ?? this.address16ToAddress64.get(nwkHeader.source16), nwkFCF, nwkHeader); if (nwkFCF.frameType === 0 /* ZigbeeNWKFrameType.DATA */) { const [apsFCF, apsFCFOutOffset] = (0, zigbee_aps_js_1.decodeZigbeeAPSFrameControl)(nwkPayload, 0); const [apsHeader, apsHOutOffset] = (0, zigbee_aps_js_1.decodeZigbeeAPSHeader)(nwkPayload, apsFCFOutOffset, apsFCF); if (apsHeader.frameControl.ackRequest && nwkHeader.source16 !== 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */) { await this.sendZigbeeAPSACK(macHeader, nwkHeader, apsHeader); } const apsPayload = (0, zigbee_aps_js_1.decodeZigbeeAPSPayload)(nwkPayload, apsHOutOffset, undefined, // use pre-hashed this.netParams.tcKey, /* nwkHeader.frameControl.extendedSource ? nwkHeader.source64 : this.address16ToAddress64.get(nwkHeader.source16!) */ nwkHeader.source64 ?? this.address16ToAddress64.get(nwkHeader.source16), apsFCF, apsHeader); await this.onZigbeeAPSFrame(apsPayload, macHeader, nwkHeader, apsHeader, sourceLQA); } else if (nwkFCF.frameType === 1 /* ZigbeeNWKFrameType.CMD */) { await this.processZigbeeNWKCommand(nwkPayload, macHeader, nwkHeader); } else if (nwkFCF.frameType === 3 /* ZigbeeNWKFrameType.INTERPAN */) { throw new Error("INTERPAN not supported", { cause: statuses_js_1.SpinelStatus.UNIMPLEMENTED }); } } } } catch (error) { // TODO log or throw depending on error logger_js_1.logger.error(error.stack, NS); } } sendFrame(hdlcFrame) { // only send what is recorded as "data" (by length) this.writer.writeBuffer(hdlcFrame.data.subarray(0, hdlcFrame.length)); } async sendCommand(commandId, buffer, waitForResponse = true, timeout = 10000) { const tid = this.nextSpinelTID(); logger_js_1.logger.debug(() => `---> SPINEL[tid=${tid} cmdId=${commandId} len=${buffer.byteLength} wait=${waitForResponse} timeout=${timeout}]`, NS); const spinelFrame = { header: { tid, nli: 0, flg: spinel_js_1.SPINEL_HEADER_FLG_SPINEL, }, commandId, payload: buffer, }; const hdlcFrame = (0, spinel_js_1.encodeSpinelFrame)(spinelFrame); this.sendFrame(hdlcFrame); if (waitForResponse) { return await this.waitForTID(spinelFrame.header.tid, timeout); } } async waitForTID(tid, timeout) { return await new Promise((resolve, reject) => { // TODO reject if tid already present? (shouldn't happen as long as concurrency is fine...) this.#tidWaiters.set(tid, { timer: setTimeout(reject.bind(this, new Error(`-x-> SPINEL[tid=${tid}] Timeout after ${timeout}ms`, { cause: statuses_js_1.SpinelStatus.RESPONSE_TIMEOUT })), timeout), resolve, reject, }); }); } async getProperty(propertyId, timeout = 10000) { const [data] = (0, spinel_js_1.writePropertyId)(propertyId, 0); return await this.sendCommand(2 /* SpinelCommandId.PROP_VALUE_GET */, data, true, timeout); } async setProperty(payload, timeout = 10000) { // LAST_STATUS checked in `onFrame` await this.sendCommand(3 /* SpinelCommandId.PROP_VALUE_SET */, payload, true, timeout); } /** * The CCA (clear-channel assessment) threshold. * NOTE: Currently not implemented in: ot-ti * @returns dBm (int8) */ async getPHYCCAThreshold() { const response = await this.getProperty(36 /* SpinelPropertyId.PHY_CCA_THRESHOLD */); return (0, spinel_js_1.readPropertyc)(36 /* SpinelPropertyId.PHY_CCA_THRESHOLD */, response.payload); } /** * The CCA (clear-channel assessment) threshold. * Set to -128 to disable. * The value will be rounded down to a value that is supported by the underlying radio hardware. * NOTE: Currently not implemented in: ot-ti * @param ccaThreshold dBm (>= -128 and <= 127) */ async setPHYCCAThreshold(ccaThreshold) { await this.setProperty((0, spinel_js_1.writePropertyc)(36 /* SpinelPropertyId.PHY_CCA_THRESHOLD */, Math.min(Math.max(ccaThreshold, -128), 127))); } /** * The transmit power of the radio. * @returns dBm (int8) */ async getPHYTXPower() { const response = await this.getProperty(37 /* SpinelPropertyId.PHY_TX_POWER */); return (0, spinel_js_1.readPropertyc)(37 /* SpinelPropertyId.PHY_TX_POWER */, response.payload); } /** * The transmit power of the radio. * The value will be rounded down to a value that is supported by the underlying radio hardware. * @param txPower dBm (>= -128 and <= 127) */ async setPHYTXPower(txPower) { await this.setProperty((0, spinel_js_1.writePropertyc)(37 /* SpinelPropertyId.PHY_TX_POWER */, Math.min(Math.max(txPower, -128), 127))); } /** * The current RSSI (Received signal strength indication) from the radio. * This value can be used in energy scans and for determining the ambient noise floor for the operating environment. * @returns dBm (int8) */ async getPHYRSSI() { const response = await this.getProperty(38 /* SpinelPropertyId.PHY_RSSI */); return (0, spinel_js_1.readPropertyc)(38 /* SpinelPropertyId.PHY_RSSI */, response.payload); } /** * The radio receive sensitivity. * This value can be used as lower bound noise floor for link metrics computation. * @returns dBm (int8) */ async getPHYRXSensitivity() { const response = await this.getProperty(39 /* SpinelPropertyId.PHY_RX_SENSITIVITY */); return (0, spinel_js_1.readPropertyc)(39 /* SpinelPropertyId.PHY_RX_SENSITIVITY */, response.payload); } /* v8 ignore start */ /** * Start an energy scan. * Cannot be used after state is loaded or network is up. * @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#section-5.8.1 * @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#section-5.8.10 * @param channels List of channels to scan * @param period milliseconds per channel * @param txPower */ async startEnergyScan(channels, period, txPower) { if (this.#stateLoaded || this.#networkUp) { return; } const radioRSSI = await this.getPHYRSSI(); const rxSensitivity = await this.getPHYRXSensitivity(); logger_js_1.logger.info(`PHY state: rssi=${radioRSSI} rxSensitivity=${rxSensitivity}`, NS); await this.setProperty((0, spinel_js_1.writePropertyb)(32 /* SpinelPropertyId.PHY_ENABLED */, true)); await this.setPHYTXPower(txPower); await this.setProperty((0, spinel_js_1.writePropertyb)(59 /* SpinelPropertyId.MAC_RX_ON_WHEN_IDLE_MODE */, true)); await this.setProperty((0, spinel_js_1.writePropertyAC)(49 /* SpinelPropertyId.MAC_SCAN_MASK */, channels)); await this.setProperty((0, spinel_js_1.writePropertyS)(50 /* SpinelPropertyId.MAC_SCAN_PERIOD */, period)); await this.setProperty((0, spinel_js_1.writePropertyC)(48 /* SpinelPropertyId.MAC_SCAN_STATE */, 2 /* SCAN_STATE_ENERGY */)); } async stopEnergyScan() { await this.setProperty((0, spinel_js_1.writePropertyS)(50 /* SpinelPropertyId.MAC_SCAN_PERIOD */, 100)); await this.setProperty((0, spinel_js_1.writePropertyC)(48 /* SpinelPropertyId.MAC_SCAN_STATE */, 0 /* SCAN_STATE_IDLE */)); await this.setProperty((0, spinel_js_1.writePropertyb)(32 /* SpinelPropertyId.PHY_ENABLED */, false)); } /** * Start sniffing. * Cannot be used after state is loaded or network is up. * WARNING: This is expected to run in the "run-and-quit" pattern as it overrides the `onStreamRawFrame` function. * @param channel The channel to sniff on */ async startSniffer(channel) { if (this.#stateLoaded || this.#networkUp) { return; } await this.setProperty((0, spinel_js_1.writePropertyb)(32 /* SpinelPropertyId.PHY_ENABLED */, true)); await this.setProperty((0, spinel_js_1.writePropertyC)(33 /* SpinelPropertyId.PHY_CHAN */, channel)); // 0 => MAC_PROMISCUOUS_MODE_OFF" => Normal MAC filtering is in place. // 1 => MAC_PROMISCUOUS_MODE_NETWORK" => All MAC packets matching network are passed up the stack. // 2 => MAC_PROMISCUOUS_MODE_FULL" => All decoded MAC packets are passed up the stack. await this.setProperty((0, spinel_js_1.writePropertyC)(56 /* SpinelPropertyId.MAC_PROMISCUOUS_MODE */, 2)); await this.setProperty((0, spinel_js_1.writePropertyb)(59 /* SpinelPropertyId.MAC_RX_ON_WHEN_IDLE_MODE */, true)); await this.setProperty((0, spinel_js_1.writePropertyb)(55 /* SpinelPropertyId.MAC_RAW_STREAM_ENABLED */, true)); // override `onStreamRawFrame` behavior for sniff this.onStreamRawFrame = async (payload, metadata) => { this.emit("macFrame", payload, metadata?.rssi); await Promise.resolve(); }; } async stopSniffer() { await this.setProperty((0, spinel_js_1.writePropertyC)(56 /* SpinelPropertyId.MAC_PROMISCUOUS_MODE */, 0)); await this.setProperty((0, spinel_js_1.writePropertyb)(32 /* SpinelPropertyId.PHY_ENABLED */, false)); // first, avoids BUSY signal await this.setProperty((0, spinel_js_1.writePropertyb)(55 /* SpinelPropertyId.MAC_RAW_STREAM_ENABLED */, false)); } /* v8 ignore stop */ // #endregion // #region MAC Layer /** * Send 802.15.4 MAC frame without checking for need to use indirect transmission. * @param seqNum * @param payload * @param dest16 * @param dest64 * @returns True if success sending */ async sendMACFrameDirect(seqNum, payload, dest16, dest64) { if (dest16 === undefined && dest64 !== undefined) { dest16 = this.deviceTable.get(dest64)?.address16; } try { logger_js_1.logger.debug(() => `===> MAC[seqNum=${seqNum} dst=${dest16}:${dest64}]`, NS); await this.setProperty((0, spinel_js_1.writePropertyStreamRaw)(payload, this.streamRawConfig)); if (this.#emitMACFrames) { setImmediate(() => { this.emit("macFrame", payload); }); } if (dest16 !== undefined) { this.macNoACKs.delete(dest16); this.routeFailures.delete(dest16); } return true; } catch (error) { logger_js_1.logger.debug(() => `=x=> MAC[seqNum=${seqNum} dst=${dest16}:${dest64}] ${error.message}`, NS); if (error.cause === statuses_js_1.SpinelStatus.NO_ACK && dest16 !== undefined) { this.macNoACKs.set(dest16, (this.macNoACKs.get(dest16) ?? 0) + 1); } // TODO: ? // - NOMEM // - BUSY // - DROPPED // - CCA_FAILURE return false; } } /** * Send 802.15.4 MAC frame. * @param seqNum * @param payload * @param dest16 * @param dest64 * @returns True if success sending. Undefined if set for indirect transmission. */ async sendMACFrame(seqNum, payload, dest16, dest64) { if (dest16 !== undefined || dest64 !== undefined) { if (dest64 === undefined && dest16 !== undefined) { dest64 = this.address16ToAddress64.get(dest16); } if (dest64 !== undefined) { const addrTXs = this.indirectTransmissions.get(dest64); if (addrTXs) { addrTXs.push({ sendFrame: this.sendMACFrameDirect.bind(this, seqNum, payload, dest16, dest64), timestamp: Date.now(), }); logger_js_1.logger.debug(() => `=|=> MAC[seqNum=${seqNum} dst=${dest16}:${dest64}] set for indirect transmission (count=${addrTXs.length})`, NS); return; // done } } } // just send the packet when: // - RX on when idle // - can't determine radio state // - no dest info return await this.sendMACFrameDirect(seqNum, payload, dest16, dest64); } /** * Send 802.15.4 MAC command * @param cmdId * @param dest16 * @param dest64 * @param extSource * @param payload * @returns True if success sending */ async sendMACCommand(cmdId, dest16, dest64, extSource, payload) { const macSeqNum = this.nextMACSeqNum(); logger_js_1.logger.debug(() => `===> MAC CMD[seqNum=${macSeqNum} cmdId=${cmdId} dst=${dest16}:${dest64} extSrc=${extSource}]`, NS); const macFrame = (0, mac_js_1.encodeMACFrame)({ frameControl: { frameType: 3 /* MACFrameType.CMD */, securityEnabled: false, framePending: false, ackRequest: dest16 !== 65535 /* ZigbeeMACConsts.BCAST_ADDR */, panIdCompression: true, seqNumSuppress: false, iePresent: false, destAddrMode: dest64 !== undefined ? 3 /* MACFrameAddressMode.EXT */ : 2 /* MACFrameAddressMode.SHORT */, frameVersion: 0 /* MACFrameVersion.V2003 */, sourceAddrMode: extSource ? 3 /* MACFrameAddressMode.EXT */ : 2 /* MACFrameAddressMode.SHORT */, }, sequenceNumber: macSeqNum, destinationPANId: this.netParams.panId, destination16: dest16, // depends on `destAddrMode` above destination64: dest64, // depends on `destAddrMode` above // sourcePANId: undefined, // panIdCompression=true source16: 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */, // depends on `sourceAddrMode` above source64: this.netParams.eui64, // depends on `sourceAddrMode` above commandId: cmdId, fcs: 0, }, payload); return await this.sendMACFrameDirect(macSeqNum, macFrame, dest16, dest64); } /** * Process 802.15.4 MAC command. * @param data * @param macHeader * @returns */ async processMACCommand(data, macHeader) { let offset = 0; switch (macHeader.commandId) { case 1 /* MACCommandId.ASSOC_REQ */: { offset = await this.processMACAssocReq(data, offset, macHeader); break; } case 2 /* MACCommandId.ASSOC_RSP */: { offset = this.processMACAssocRsp(data, offset, macHeader); break; } case 7 /* MACCommandId.BEACON_REQ */: { offset = await this.processMACBeaconReq(data, offset, macHeader); break; } case 4 /* MACCommandId.DATA_RQ */: { offset = await this.processMACDataReq(data, offset, macHeader); break; } // TODO: other cases? // DISASSOC_NOTIFY // PANID_CONFLICT // ORPHAN_NOTIFY // COORD_REALIGN // GTS_REQ default: { logger_js_1.logger.error(`<=x= MAC CMD[cmdId=${macHeader.commandId} macSrc=${macHeader.source16}:${macHeader.source64}] Unsupported`, NS); return; } } // excess data in packet // if (offset < data.byteLength) { // logger.debug(() => `<=== MAC CMD contained more data: ${data.toString('hex')}`, NS); // } } /** * Process 802.15.4 MAC association request. * @param data * @param offset * @param macHeader * @returns */ async processMACAssocReq(data, offset, macHeader) { const capabilities = data.readUInt8(offset); offset += 1; logger_js_1.logger.debug(() => `<=== MAC ASSOC_REQ[macSrc=${macHeader.source16}:${macHeader.source64} cap=${capabilities}]`, NS); if (macHeader.source64 === undefined) { logger_js_1.logger.debug(() => `<=x= MAC ASSOC_REQ[macSrc=${macHeader.source16}:${macHeader.source64} cap=${capabilities}] Invalid source64`, NS); } else { const address16 = this.deviceTable.get(macHeader.source64)?.address16; const decodedCap = (0, mac_js_1.decodeMACCapabilities)(capabilities); const [status, newAddress16] = await this.associate(address16, macHeader.source64, address16 === undefined /* initial join if unknown device, else rejoin */, decodedCap, true /* neighbor */); this.pendingAssociations.set(macHeader.source64, { sendResp: async () => { await this.sendMACAssocRsp(macHeader.source64, newAddress16, status); if (status === mac_js_1.MACAssociationStatus.SUCCESS) { await this.sendZigbeeAPSTransportKeyNWK(newAddress16, this.netParams.networkKey, this.netParams.networkKeySequenceNumber, macHeader.source64); } }, timestamp: Date.now(), }); } return offset; } /** * Process 802.15.4 MAC association response. * @param data * @param offset * @param macHeader * @returns */ processMACAssocRsp(data, offset, macHeader) { const address = data.readUInt16LE(offset); offset += 2; const status = data.readUInt8(offset); offset += 1; logger_js_1.logger.debug(() => `<=== MAC ASSOC_RSP[macSrc=${macHeader.source16}:${macHeader.source64} addr16=${address} status=${mac_js_1.MACAssociationStatus[status]}]`, NS); return offset; } /** * Send 802.15.4 MAC association response * @param dest64 * @param newAddress16 * @param status * @returns */ async sendMACAssocRsp(dest64, newAddress16, status) { logger_js_1.logger.debug(() => `===> MAC ASSOC_RSP[dst64=${dest64} newAddr16=${newAddress16} status=${status}]`, NS); const finalPayload = Buffer.alloc(3); let offset = 0; finalPayload.writeUInt16LE(newAddress16, offset); offset += 2; finalPayload.writeUInt8(status, offset); offset += 1; return await this.sendMACCommand(2 /* MACCommandId.ASSOC_RSP */, undefined, // dest16 dest64, // dest64 true, // sourceExt finalPayload); } /** * Process 802.15.4 MAC beacon request. * @param _data * @param offset * @param _macHeader * @returns */ async processMACBeaconReq(_data, offset, _macHeader) { logger_js_1.logger.debug(() => "<=== MAC BEACON_REQ[]", NS); const macSeqNum = this.nextMACSeqNum(); const macFrame = (0, mac_js_1.encodeMACFrame)({ frameControl: { frameType: 0 /* MACFrameType.BEACON */, securityEnabled: false, framePending: false, ackRequest: false, panIdCompression: false, seqNumSuppress: false, iePresent: false, destAddrMode: 0 /* MACFrameAddressMode.NONE */, frameVersion: 0 /* MACFrameVersion.V2003 */, sourceAddrMode: 2 /* MACFrameAddressMode.SHORT */, }, sequenceNumber: macSeqNum, sourcePANId: this.netParams.panId, source16: 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */, superframeSpec: { beaconOrder: 0x0f, // value from spec superframeOrder: 0x0f, // value from spec finalCAPSlot: 0x0f, // XXX: value from sniff, matches above... batteryExtension: false, panCoordinator: true, associationPermit: this.#macAssociationPermit, }, gtsInfo: { permit: false }, pendAddr: {}, fcs: 0, }, (0, mac_js_1.encodeMACZigbeeBeacon)({ protocolId: 0 /* ZigbeeMACConsts.ZIGBEE_BEACON_PROTOCOL_ID */, profile: 0x2, // ZigBee PRO version: 2 /* ZigbeeNWKConsts.VERSION_2007 */, routerCapacity: true, deviceDepth: 0, // coordinator endDeviceCapacity: true, extendedPANId: this.netParams.extendedPANId, txOffset: 0xffffff, // XXX: value from sniffed frames updateId: this.netParams.nwkUpdateId, // XXX: correct? })); logger_js_1.logger.debug(() => `===> MAC BEACON[seqNum=${macSeqNum}]`, NS); awa