zigbee-on-host
Version:
ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP)
991 lines • 207 kB
JavaScript
"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