ember-zli
Version:
Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver
261 lines (260 loc) • 13.9 kB
JavaScript
import { ZSpec } from "zigbee-herdsman";
import { FIXED_ENDPOINTS } from "zigbee-herdsman/dist/adapter/ember/adapter/endpoints.js";
import { EMBER_HIGH_RAM_CONCENTRATOR, EMBER_LOW_RAM_CONCENTRATOR, SECURITY_LEVEL_Z3, STACK_PROFILE_ZIGBEE_PRO, } from "zigbee-herdsman/dist/adapter/ember/consts.js";
import { EmberKeyStructBitmask, EmberLibraryId, EmberLibraryStatus, EmberNetworkInitBitmask, EmberSourceRouteDiscoveryMode, EmberVersionType, EzspStatus, IEEE802154CcaMode, SLStatus, } from "zigbee-herdsman/dist/adapter/ember/enums.js";
import { EZSP_MIN_PROTOCOL_VERSION, EZSP_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH } from "zigbee-herdsman/dist/adapter/ember/ezsp/consts.js";
import { EzspConfigId, EzspDecisionId, EzspPolicyId, EzspValueId } from "zigbee-herdsman/dist/adapter/ember/ezsp/enums.js";
import { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js";
import { lowHighBytes } from "zigbee-herdsman/dist/adapter/ember/utils/math.js";
import { logger } from "../index.js";
import { NVM3ObjectKey } from "./enums.js";
import { ROUTER_FIXED_ENDPOINTS } from "./router-endpoints.js";
const NS = { namespace: "ember" };
export let emberFullVersion = {
ezsp: -1,
revision: "unknown",
build: -1,
major: -1,
minor: -1,
patch: -1,
special: -1,
type: EmberVersionType.PRE_RELEASE,
};
export const waitForStackStatus = async (ezsp, status, timeout = 10000) => await new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
ezsp.removeListener("stackStatus", onStackStatus);
return reject(new Error(`Timed out waiting for stack status '${SLStatus[status]}'.`));
}, timeout);
const onStackStatus = (receivedStatus) => {
logger.debug(`Received stack status ${receivedStatus} while waiting for ${status}.`, NS);
if (status === receivedStatus) {
clearTimeout(timeoutHandle);
ezsp.removeListener("stackStatus", onStackStatus);
resolve();
}
};
ezsp.on("stackStatus", onStackStatus);
});
export const emberStart = async (portConf) => {
const ezsp = new Ezsp({ adapter: "ember", ...portConf });
// NOTE: something deep in this call can throw too
const startResult = await ezsp.start();
if (startResult !== 0) {
throw new Error(`Failed to start EZSP layer with status=${EzspStatus[startResult]}.`);
}
// call before any other command, else fails
emberFullVersion = await emberVersion(ezsp);
return ezsp;
};
export const emberStop = async (ezsp) => {
// workaround to remove ASH COUNTERS logged on stop
// @ts-expect-error workaround (overriding private)
ezsp.ash.logCounters = () => { };
await ezsp.stop();
};
export const emberVersion = async (ezsp) => {
// send the Host version number to the NCP.
// The NCP returns the EZSP version that the NCP is running along with the stackType and stackVersion
let [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(EZSP_PROTOCOL_VERSION);
// verify that the stack type is what is expected
if (ncpStackType !== EZSP_STACK_TYPE_MESH) {
throw new Error(`Stack type ${ncpStackType} is not expected!`);
}
if (ncpEzspProtocolVer === EZSP_PROTOCOL_VERSION) {
logger.debug(`NCP EZSP protocol version (${ncpEzspProtocolVer}) matches Host.`, NS);
}
else if (ncpEzspProtocolVer < EZSP_PROTOCOL_VERSION && ncpEzspProtocolVer >= EZSP_MIN_PROTOCOL_VERSION) {
[ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(ncpEzspProtocolVer);
logger.info(`NCP EZSP protocol version (${ncpEzspProtocolVer}) lower than Host. Switched.`, NS);
}
else {
throw new Error(`NCP EZSP protocol version (${ncpEzspProtocolVer}) is not supported by Host [${EZSP_MIN_PROTOCOL_VERSION}-${EZSP_PROTOCOL_VERSION}].`);
}
ezsp.setProtocolVersion(ncpEzspProtocolVer);
logger.debug(`NCP info: EZSPVersion=${ncpEzspProtocolVer} StackType=${ncpStackType} StackVersion=${ncpStackVer}`, NS);
const [status, versionStruct] = await ezsp.ezspGetVersionStruct();
if (status !== SLStatus.OK) {
// Should never happen with support of only EZSP v13+
throw new Error("NCP has old-style version number. Not supported.");
}
const version = {
ezsp: ncpEzspProtocolVer,
revision: `${versionStruct.major}.${versionStruct.minor}.${versionStruct.patch} [${EmberVersionType[versionStruct.type]}]`,
...versionStruct,
};
if (versionStruct.type !== EmberVersionType.GA) {
logger.warning(`NCP is running a non-GA version (${EmberVersionType[versionStruct.type]}).`, NS);
}
logger.info(`NCP version: ${JSON.stringify(version)}`, NS);
return version;
};
export const emberNetworkInit = async (ezsp, wasConfigured = false) => {
if (!wasConfigured) {
// minimum required for proper network init
const status = await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO);
if (status !== SLStatus.OK) {
throw new Error(`Failed to set stack profile with status=${SLStatus[status]}.`);
}
}
const networkInitStruct = {
bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT,
};
return await ezsp.ezspNetworkInit(networkInitStruct);
};
export const emberNetworkConfig = async (ezsp, stackConf, manufacturerCode) => {
/** The address cache needs to be initialized and used with the source routing code for the trust center to operate properly. */
await ezsp.ezspSetConfigurationValue(EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE, 2);
/** MAC indirect timeout should be 7.68 secs (STACK_PROFILE_ZIGBEE_PRO) */
await ezsp.ezspSetConfigurationValue(EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680);
/** Max hops should be 2 * nwkMaxDepth, where nwkMaxDepth is 15 (STACK_PROFILE_ZIGBEE_PRO) */
await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_HOPS, 30);
await ezsp.ezspSetConfigurationValue(EzspConfigId.SUPPORTED_NETWORKS, 1);
// allow other devices to modify the binding table
await ezsp.ezspSetPolicy(EzspPolicyId.BINDING_MODIFICATION_POLICY, EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS);
// return message tag only in ezspMessageSentHandler()
await ezsp.ezspSetPolicy(EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY, EzspDecisionId.MESSAGE_TAG_ONLY_IN_CALLBACK);
await ezsp.ezspSetValue(EzspValueId.TRANSIENT_DEVICE_TIMEOUT, 2, lowHighBytes(stackConf.TRANSIENT_DEVICE_TIMEOUT));
await ezsp.ezspSetManufacturerCode(manufacturerCode);
// network security init
await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO);
await ezsp.ezspSetConfigurationValue(EzspConfigId.SECURITY_LEVEL, SECURITY_LEVEL_Z3);
// common configs
await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, stackConf.MAX_END_DEVICE_CHILDREN);
await ezsp.ezspSetConfigurationValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, stackConf.END_DEVICE_POLL_TIMEOUT);
await ezsp.ezspSetConfigurationValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, stackConf.TRANSIENT_KEY_TIMEOUT_S);
// XXX: temp-fix: forces a side-effect in the firmware that prevents broadcast issues in environments with unusual interferences
await ezsp.ezspSetValue(EzspValueId.CCA_THRESHOLD, 1, [0]);
if (stackConf.CCA_MODE) {
// validated in `loadStackConfig`
await ezsp.ezspSetRadioIeee802154CcaMode(IEEE802154CcaMode[stackConf.CCA_MODE]);
}
};
export const emberRegisterFixedEndpoints = async (ezsp, multicastTable, router = false) => {
for (const ep of router ? ROUTER_FIXED_ENDPOINTS : FIXED_ENDPOINTS) {
if (ep.networkIndex !== 0x00) {
logger.debug(`Multi-network not currently supported. Skipping endpoint ${JSON.stringify(ep)}.`, NS);
continue;
}
const [epStatus] = await ezsp.ezspGetEndpointFlags(ep.endpoint);
// endpoint already registered
if (epStatus === SLStatus.OK) {
logger.debug(`Endpoint '${ep.endpoint}' already registered.`, NS);
}
else {
// check to see if ezspAddEndpoint needs to be called
// if ezspInit is called without NCP reset, ezspAddEndpoint is not necessary and will return an error
const status = await ezsp.ezspAddEndpoint(ep.endpoint, ep.profileId, ep.deviceId, ep.deviceVersion, [...ep.inClusterList], // copy
[...ep.outClusterList]);
if (status === SLStatus.OK) {
logger.debug(`Registered endpoint '${ep.endpoint}'.`, NS);
}
else {
throw new Error(`Failed to register endpoint '${ep.endpoint}' with status=${SLStatus[status]}.`);
}
}
for (const multicastId of ep.multicastIds) {
const multicastEntry = {
multicastId,
endpoint: ep.endpoint,
networkIndex: ep.networkIndex,
};
const status = await ezsp.ezspSetMulticastTableEntry(multicastTable.length, multicastEntry);
if (status !== SLStatus.OK) {
throw new Error(`Failed to register group '${multicastId}' in multicast table with status=${SLStatus[status]}.`);
}
logger.debug(`Registered multicast table entry (${multicastTable.length}): ${JSON.stringify(multicastEntry)}.`, NS);
multicastTable.push(multicastEntry.multicastId);
}
}
};
export const emberSetConcentrator = async (ezsp, stackConf) => {
const status = await ezsp.ezspSetConcentrator(true, stackConf.CONCENTRATOR_RAM_TYPE === "low" ? EMBER_LOW_RAM_CONCENTRATOR : EMBER_HIGH_RAM_CONCENTRATOR, stackConf.CONCENTRATOR_MIN_TIME, stackConf.CONCENTRATOR_MAX_TIME, stackConf.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, stackConf.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, stackConf.CONCENTRATOR_MAX_HOPS);
if (status !== SLStatus.OK) {
throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${SLStatus[status]}.`);
}
const remainTilMTORR = await ezsp.ezspSetSourceRouteDiscoveryMode(EmberSourceRouteDiscoveryMode.RESCHEDULE);
logger.info(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`, NS);
};
// -- Utils
export const getLibraryStatus = (id, status) => {
if (status === EmberLibraryStatus.LIBRARY_ERROR) {
return "ERROR";
}
let statusStr = "NOT_PRESENT";
const present = Boolean(status & EmberLibraryStatus.LIBRARY_PRESENT_MASK);
if (present) {
statusStr = "PRESENT";
if (id === EmberLibraryId.ZIGBEE_PRO) {
statusStr += status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_HAVE_ROUTER_CAPABILITY ? " / ROUTER_CAPABILITY" : " / END_DEVICE_ONLY";
if (status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_ZLL_SUPPORT) {
statusStr += " / ZLL_SUPPORT";
}
}
if (id === EmberLibraryId.SECURITY_CORE) {
statusStr += status & EmberLibraryStatus.SECURITY_LIBRARY_HAVE_ROUTER_SUPPORT ? " / ROUTER_SUPPORT" : " / END_DEVICE_ONLY";
}
if (id === EmberLibraryId.PACKET_VALIDATE) {
statusStr += status & EmberLibraryStatus.PACKET_VALIDATE_LIBRARY_ENABLED ? " / ENABLED" : " / DISABLED";
}
}
return statusStr;
};
export const getKeyStructBitmask = (bitmask) => {
const bitmaskValues = [];
for (const key in EmberKeyStructBitmask) {
const val = EmberKeyStructBitmask[key];
if (typeof val !== "number") {
continue;
}
if (bitmask & val) {
bitmaskValues.push(key);
}
}
return bitmaskValues.join("|");
};
export const parseTokenData = (nvm3Key, data) => {
switch (nvm3Key) {
case NVM3ObjectKey.STACK_BOOT_COUNTER:
case NVM3ObjectKey.STACK_NONCE_COUNTER:
case NVM3ObjectKey.STACK_ANALYSIS_REBOOT:
case NVM3ObjectKey.MULTI_NETWORK_STACK_NONCE_COUNTER:
case NVM3ObjectKey.STACK_APS_FRAME_COUNTER:
case NVM3ObjectKey.STACK_GP_INCOMING_FC:
case NVM3ObjectKey.STACK_GP_INCOMING_FC_IN_SINK: {
return `${data.readUIntLE(0, data.length)}`;
}
case NVM3ObjectKey.STACK_MIN_RECEIVED_RSSI: {
return `${data.readIntLE(0, data.length)}`;
}
case NVM3ObjectKey.STACK_CHILD_TABLE: {
// TODO
return `EUI64: ${data.subarray(0, 8).toString("hex")} | ${data.subarray(8).toString("hex")}`;
}
// TODO:
// case NVM3ObjectKey.STACK_BINDING_TABLE: {}
// TODO:
// case NVM3ObjectKey.STACK_KEY_TABLE: {}
case NVM3ObjectKey.STACK_TRUST_CENTER: {
// TODO
return `${data.subarray(0, 2).toString("hex")} | EUI64: ${data.subarray(2, 10).toString("hex")} | Link Key: ${data.subarray(10).toString("hex")}`;
}
case NVM3ObjectKey.STACK_KEYS:
case NVM3ObjectKey.STACK_ALTERNATE_KEY: {
// TODO
return `Network Key: ${data.subarray(0, -1).toString("hex")} | Sequence Number: ${data.readUInt8(16)}`;
}
case NVM3ObjectKey.STACK_NODE_DATA: {
// TODO
// [4-5] === network join status?
return (`PAN ID: ${data.subarray(0, 2).toString("hex")} | Radio TX Power ${data.readUInt8(2)} | Radio Channel ${data.readUInt8(3)} ` +
`| ${data.subarray(4, 8).toString("hex")} | Ext PAN ID: ${data.subarray(8, 16).toString("hex")}`);
}
case NVM3ObjectKey.STACK_NETWORK_MANAGEMENT: {
// TODO
return `Channels: ${ZSpec.Utils.uint32MaskToChannels(data.readUInt32LE(0))} | ${data.subarray(4).toString("hex")}`;
}
default: {
return data.toString("hex");
}
}
};