zigbee-on-host
Version:
Zigbee stack designed to run on a host and communicate with a radio co-processor (RCP)
700 lines • 33.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OTRCPDriver = void 0;
const node_fs_1 = require("node:fs");
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 zigbee_js_1 = require("../zigbee/zigbee.js");
const aps_handler_js_1 = require("../zigbee-stack/aps-handler.js");
const frame_js_1 = require("../zigbee-stack/frame.js");
const mac_handler_js_1 = require("../zigbee-stack/mac-handler.js");
const nwk_gp_handler_js_1 = require("../zigbee-stack/nwk-gp-handler.js");
const nwk_handler_js_1 = require("../zigbee-stack/nwk-handler.js");
const stack_context_js_1 = require("../zigbee-stack/stack-context.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";
// 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;
class OTRCPDriver {
#onMACFrame;
#streamRawConfig;
writer = new ot_rcp_writer_js_1.OTRCPWriter({ highWaterMark: CONFIG_HIGHWATER_MARK });
parser = new ot_rcp_parser_js_1.OTRCPParser({ readableHighWaterMark: CONFIG_HIGHWATER_MARK });
#protocolVersionMajor = 0;
#protocolVersionMinor = 0;
#ncpVersion = "";
#interfaceType = 0;
#rcpAPIVersion = 0;
#rcpMinHostAPIVersion = 0;
/** Centralized stack context holding all shared state */
context;
/** MAC layer handler */
macHandler;
/** NWK layer handler */
nwkHandler;
/** APS layer handler */
apsHandler;
/** NWK GP layer handler */
nwkGPHandler;
/**
* 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 = -1; // start at 0 but effectively 1 returned by first `nextSpinelTID` call
/** If defined, indicates we're waiting for the property with the specific payload to come in */
#resetWaiter;
/** TID currently being awaited */
#tidWaiters = new Map();
#networkUp = false;
#pendingChangeChannel;
constructor(callbacks, streamRawConfig, netParams, saveDir, emitMACFrames = false) {
/* v8 ignore else -- @preserve */
if (!(0, node_fs_1.existsSync)(saveDir)) {
(0, node_fs_1.mkdirSync)(saveDir);
}
this.#onMACFrame = callbacks.onMACFrame;
this.#streamRawConfig = streamRawConfig;
const contextCallbacks = {
onDeviceLeft: callbacks.onDeviceLeft,
};
this.context = new stack_context_js_1.StackContext(contextCallbacks, (0, node_path_1.join)(saveDir, "zoh.save"), netParams);
const macCallbacks = {
onFrame: callbacks.onMACFrame,
onSendFrame: this.sendStreamRaw.bind(this),
onAPSSendTransportKeyNWK: async (address16, key, keySeqNum, destination64) => {
await this.apsHandler.sendTransportKeyNWK(address16, key, keySeqNum, destination64);
},
onMarkRouteSuccess: (destination16) => {
this.nwkHandler.markRouteSuccess(destination16);
},
onMarkRouteFailure: (destination16) => {
this.nwkHandler.markRouteFailure(destination16);
},
};
this.macHandler = new mac_handler_js_1.MACHandler(this.context, macCallbacks, statuses_js_1.SpinelStatus.NO_ACK, emitMACFrames);
const nwkCallbacks = {
onAPSSendTransportKeyNWK: async (address16, key, keySeqNum, destination64) => {
await this.apsHandler.sendTransportKeyNWK(address16, key, keySeqNum, destination64);
},
};
this.nwkHandler = new nwk_handler_js_1.NWKHandler(this.context, this.macHandler, nwkCallbacks);
const apsCallbacks = {
onFrame: callbacks.onFrame,
onDeviceJoined: callbacks.onDeviceJoined,
onDeviceRejoined: callbacks.onDeviceRejoined,
onDeviceAuthorized: callbacks.onDeviceAuthorized,
};
this.apsHandler = new aps_handler_js_1.APSHandler(this.context, this.macHandler, this.nwkHandler, apsCallbacks);
// Setup NWK GP handler callbacks
const nwkGPCallbacks = {
onGPFrame: callbacks.onGPFrame,
};
this.nwkGPHandler = new nwk_gp_handler_js_1.NWKGPHandler(nwkGPCallbacks);
}
// #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;
}
// #endregion
// #region HDLC/Spinel
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`)), timeout),
resolve,
reject,
});
});
}
/**
* Logic optimizes code paths to try to avoid more parsing when frames will eventually get ignored by detecting as early as possible.
* HOT PATH: This method is called for every incoming frame. Optimizations:
* - Early bail-outs to minimize processing
* - Inline-able operations
* - Minimal allocations in critical paths
*/
async onStreamRawFrame(payload, metadata) {
if (!this.#networkUp) {
return;
}
// Emit MAC frames if listeners registered (not in hot path for normal operation)
if (this.macHandler.emitFrames) {
setImmediate(() => {
this.#onMACFrame(payload, metadata?.rssi);
});
}
// Metadata logging
if (metadata) {
logger_js_1.logger.debug(() => `<--- SPINEL STREAM_RAW METADATA[rssi=${metadata.rssi} noiseFloor=${metadata.noiseFloor} flags=${metadata.flags}]`, NS);
}
try {
await (0, frame_js_1.processFrame)(payload, this.context, this.macHandler, this.nwkHandler, this.nwkGPHandler, this.apsHandler, metadata?.rssi);
}
catch (error) {
// TODO log or throw depending on error
logger_js_1.logger.error(error.stack, NS);
}
}
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 if -- @preserve */
if (spinelFrame.header.flg !== spinel_js_1.SPINEL_HEADER_FLG_SPINEL) {
// non-Spinel frame (likely BLE HCI)
return;
}
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);
}
/* v8 ignore else -- @preserve */
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);
}
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);
// only send what is recorded as "data" (by length)
this.writer.writeBuffer(hdlcFrame.data.subarray(0, hdlcFrame.length));
if (waitForResponse) {
return await this.waitForTID(spinelFrame.header.tid, timeout);
}
}
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);
}
async sendStreamRaw(payload) {
await this.setProperty((0, spinel_js_1.writePropertyStreamRaw)(payload, this.#streamRawConfig));
}
/**
* @returns [SPINEL_PROTOCOL_VERSION_THREAD_MAJOR, SPINEL_PROTOCOL_VERSION_THREAD_MINOR]
*/
async getProtocolVersion() {
const response = await this.getProperty(1 /* SpinelPropertyId.PROTOCOL_VERSION */);
return (0, spinel_js_1.readPropertyii)(1 /* SpinelPropertyId.PROTOCOL_VERSION */, response.payload);
}
/**
* Recommended format: STACK-NAME/STACK-VERSION[BUILD_INFO][; OTHER_INFO]; BUILD_DATE_AND_TIME
* Encoded as a zero-terminated UTF-8 string.
*/
async getNCPVersion() {
const response = await this.getProperty(2 /* SpinelPropertyId.NCP_VERSION */);
return (0, spinel_js_1.readPropertyU)(2 /* SpinelPropertyId.NCP_VERSION */, response.payload).replaceAll("\u0000", "");
}
/**
* @returns SPINEL_PROTOCOL_TYPE_*
*/
async getInterfaceType() {
const response = await this.getProperty(3 /* SpinelPropertyId.INTERFACE_TYPE */);
return (0, spinel_js_1.readPropertyi)(3 /* SpinelPropertyId.INTERFACE_TYPE */, response.payload);
}
async getRCPAPIVersion() {
const response = await this.getProperty(176 /* SpinelPropertyId.RCP_API_VERSION */);
return (0, spinel_js_1.readPropertyi)(176 /* SpinelPropertyId.RCP_API_VERSION */, response.payload);
}
async getRCPMinHostAPIVersion() {
const response = await this.getProperty(177 /* SpinelPropertyId.RCP_MIN_HOST_API_VERSION */);
return (0, spinel_js_1.readPropertyi)(177 /* SpinelPropertyId.RCP_MIN_HOST_API_VERSION */, response.payload);
}
/**
* 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);
}
/**
* 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.context.loaded || 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.context.loaded || 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.#onMACFrame(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));
}
// #endregion
// #region Network Management
//---- 05-3474-23 #2.5.4.6
// Network Discovery, Get, and Set attributes (both requests and confirms) are mandatory
// Zigbee Coordinator:
// - The NWK Formation request and confirm, the NWK Leave request, NWK Leave indication, NWK Leave confirm, NWK Join indication,
// NWK Permit Joining request, NWK Permit Joining confirm, NWK Route Discovery request, and NWK Route Discovery confirm SHALL be supported.
// - The NWK Direct Join request and NWK Direct Join confirm MAY be supported.
// - The NWK Join request and the NWK Join confirm SHALL NOT be supported.
// NWK Sync request, indication and confirm plus NWK reset request and confirm plus NWK route discovery request and confirm SHALL be optional
// reception of the NWK Network Status indication SHALL be supported, but no action is required
get isNetworkUp() {
return this.#networkUp;
}
/**
* Set the Spinel properties required to start a 802.15.4 MAC network.
*
* Should be called after `start`.
*/
async formNetwork() {
logger_js_1.logger.info("======== Network starting ========", NS);
if (!this.context.loaded) {
throw new Error("Cannot form network before state is loaded");
}
// TODO: sanity checks?
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 */, this.context.netParams.channel));
// TODO: ?
// try { await this.setPHYCCAThreshold(10); } catch (error) {}
await this.setPHYTXPower(this.context.netParams.txPower);
await this.setProperty((0, spinel_js_1.writePropertyE)(52 /* SpinelPropertyId.MAC_15_4_LADDR */, this.context.netParams.eui64));
await this.setProperty((0, spinel_js_1.writePropertyS)(53 /* SpinelPropertyId.MAC_15_4_SADDR */, 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */));
await this.setProperty((0, spinel_js_1.writePropertyS)(54 /* SpinelPropertyId.MAC_15_4_PANID */, this.context.netParams.panId));
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));
const txPower = await this.getPHYTXPower();
const radioRSSI = await this.getPHYRSSI();
this.context.rssiMin = await this.getPHYRXSensitivity();
let ccaThreshold;
try {
ccaThreshold = await this.getPHYCCAThreshold();
}
catch (error) {
logger_js_1.logger.debug(() => `PHY_CCA_THRESHOLD: ${error}`, NS);
}
logger_js_1.logger.info(`======== Network started (PHY: txPower=${txPower}dBm rssi=${radioRSSI}dBm rxSensitivity=${this.context.rssiMin}dBm ccaThreshold=${ccaThreshold}dBm) ========`, NS);
await this.startStack();
this.#networkUp = true;
}
/**
* Remove the current state file and clear all related tables.
*
* Will throw if state already loaded (should be called before `start`).
*/
async resetNetwork() {
logger_js_1.logger.info("======== Network resetting ========", NS);
if (this.context.loaded) {
throw new Error("Cannot reset network after state already loaded");
}
await this.context.clear();
logger_js_1.logger.info("======== Network reset ========", NS);
}
/**
* Start the components of the Zigbee stack
*/
async startStack() {
await this.context.start();
await this.macHandler.start();
await this.nwkHandler.start();
await this.nwkGPHandler.start();
await this.apsHandler.start();
}
/**
* Stop the components of the Zigbee stack
*/
stopStack() {
this.apsHandler.stop();
this.nwkGPHandler.stop();
this.nwkHandler.stop();
this.macHandler.stop();
this.context.stop();
}
// TODO: interference detection (& optionally auto channel changing)
// #endregion
// #region Driver
async waitForReset() {
await new Promise((resolve, reject) => {
this.#resetWaiter = {
timer: setTimeout(reject.bind(this, new Error("Reset timeout after 5000ms")), 5000),
resolve,
};
});
}
/**
* 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.context.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
[this.#protocolVersionMajor, this.#protocolVersionMinor] = await this.getProtocolVersion();
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
this.#ncpVersion = await this.getNCPVersion();
logger_js_1.logger.info(`NCP version: ${this.#ncpVersion}`, NS);
// check interface type to make sure that it is what we expect
this.#interfaceType = await this.getInterfaceType();
logger_js_1.logger.info(`Interface type: ${this.#interfaceType}`, NS);
this.#rcpAPIVersion = await this.getRCPAPIVersion();
logger_js_1.logger.info(`RCP API version: ${this.#rcpAPIVersion}`, NS);
this.#rcpMinHostAPIVersion = await this.getRCPMinHostAPIVersion();
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);
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;
}
this.stopStack();
clearTimeout(this.#pendingChangeChannel);
this.#pendingChangeChannel = undefined;
for (const [, waiter] of this.#tidWaiters) {
clearTimeout(waiter.timer);
waiter.timer = undefined;
waiter.reject(new Error("Driver stopping"));
}
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.context.saveState();
logger_js_1.logger.info("======== Driver stopped ========", NS);
}
/**
* 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);
}
// #endregion
// #region Wrappers
/**
* Wraps Zigbee APS DATA sending for ZDO.
* Throws if could not send.
* @param payload
* @param nwkDest16
* @param nwkDest64
* @param clusterId
* @returns
* - The APS counter of the sent frame.
* - The ZDO counter of the sent frame.
*/
async sendZDO(payload, nwkDest16, nwkDest64, clusterId) {
if (nwkDest16 === 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */ || nwkDest64 === this.context.netParams.eui64) {
throw new Error("Cannot send ZDO to coordinator");
}
// increment and set the ZDO sequence number in outgoing payload
const zdoCounter = this.apsHandler.nextZDOSeqNum();
payload[0] = zdoCounter;
logger_js_1.logger.debug(() => `===> ZDO[seqNum=${payload[0]} clusterId=${clusterId} nwkDst=${nwkDest16}:${nwkDest64}]`, NS);
if (clusterId === 56 /* ZigbeeConsts.NWK_UPDATE_REQUEST */ && nwkDest16 >= 65532 /* ZigbeeConsts.BCAST_DEFAULT */ && payload[5] === 0xfe) {
// TODO: needs testing
this.context.netParams.channel = (0, zigbee_js_1.convertMaskToChannels)(payload.readUInt32LE(1))[0];
this.context.netParams.nwkUpdateId = payload[6];
// force saving after net params change
await this.context.savePeriodicState();
this.#pendingChangeChannel = setTimeout(this.setProperty.bind(this, (0, spinel_js_1.writePropertyC)(33 /* SpinelPropertyId.PHY_CHAN */, this.context.netParams.channel)), 9000 /* ZigbeeConsts.BCAST_TIME_WINDOW */);
}
const apsCounter = await this.apsHandler.sendData(payload, 0 /* ZigbeeNWKRouteDiscovery.SUPPRESS */, // nwkDiscoverRoute
nwkDest16, // nwkDest16
nwkDest64, // nwkDest64
nwkDest16 < 65528 /* ZigbeeConsts.BCAST_MIN */ ? 0 /* ZigbeeAPSDeliveryMode.UNICAST */ : 2 /* ZigbeeAPSDeliveryMode.BCAST */, // apsDeliveryMode
clusterId, // clusterId
0 /* ZigbeeConsts.ZDO_PROFILE_ID */, // profileId
0 /* ZigbeeConsts.ZDO_ENDPOINT */, // destEndpoint
0 /* ZigbeeConsts.ZDO_ENDPOINT */, // sourceEndpoint
undefined);
return [apsCounter, zdoCounter];
}
/**
* Wraps Zigbee APS DATA sending for unicast.
* Throws if could not send.
* @param payload
* @param profileId
* @param clusterId
* @param dest16
* @param dest64
* @param destEp
* @param sourceEp
* @returns The APS counter of the sent frame.
*/
async sendUnicast(payload, profileId, clusterId, dest16, dest64, destEp, sourceEp) {
if (dest16 === 0 /* ZigbeeConsts.COORDINATOR_ADDRESS */ || dest64 === this.context.netParams.eui64) {
throw new Error("Cannot send unicast to coordinator");
}
return await this.apsHandler.sendData(payload, 0 /* ZigbeeNWKRouteDiscovery.SUPPRESS */, // nwkDiscoverRoute
dest16, // nwkDest16
dest64, // nwkDest64
0 /* ZigbeeAPSDeliveryMode.UNICAST */, // apsDeliveryMode
clusterId, // clusterId
profileId, // profileId
destEp, // destEndpoint
sourceEp, // sourceEndpoint
undefined);
}
/**
* Wraps Zigbee APS DATA sending for groupcast.
* Throws if could not send.
* @param payload
* @param profileId
* @param clusterId
* @param group The group to send to
* @param destEp
* @param sourceEp
* @returns The APS counter of the sent frame.
*/
async sendGroupcast(payload, profileId, clusterId, group, sourceEp) {
return await this.apsHandler.sendData(payload, 0 /* ZigbeeNWKRouteDiscovery.SUPPRESS */, // nwkDiscoverRoute
65533 /* ZigbeeConsts.BCAST_RX_ON_WHEN_IDLE */, // nwkDest16
undefined, // nwkDest64
3 /* ZigbeeAPSDeliveryMode.GROUP */, // apsDeliveryMode
clusterId, // clusterId
profileId, // profileId
undefined, // destEndpoint
sourceEp, // sourceEndpoint
group);
}
/**
* Wraps Zigbee APS DATA sending for broadcast.
* Throws if could not send.
* @param payload
* @param profileId
* @param clusterId
* @param dest16 The broadcast address to send to [0xfff8..0xffff]
* @param destEp
* @param sourceEp
* @returns The APS counter of the sent frame.
*/
async sendBroadcast(payload, profileId, clusterId, dest16, destEp, sourceEp) {
if (dest16 < 65528 /* ZigbeeConsts.BCAST_MIN */ || dest16 > 65535 /* ZigbeeConsts.BCAST_SLEEPY */) {
throw new Error("Invalid parameters");
}
return await this.apsHandler.sendData(payload, 0 /* ZigbeeNWKRouteDiscovery.SUPPRESS */, // nwkDiscoverRoute
dest16, // nwkDest16
undefined, // nwkDest64
2 /* ZigbeeAPSDeliveryMode.BCAST */, // apsDeliveryMode
clusterId, // clusterId
profileId, // profileId
destEp, // destEndpoint
sourceEp, // sourceEndpoint
undefined);
}
}
exports.OTRCPDriver = OTRCPDriver;
//# sourceMappingURL=ot-rcp-driver.js.map