From c36d051e0a0e20654ff716b17cde783e5b3a4989 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:38:12 +0100 Subject: [PATCH] feat: Add new `ember` adapter implementation, targeting EZSP 13 and above (#918) * New ember adapter implementation targeting EZSP 13 and above * Fix CI. * Tentative CI test fix. --- src/adapter/adapter.ts | 5 +- src/adapter/ember/adapter/emberAdapter.ts | 3982 ++++++++++ src/adapter/ember/adapter/endpoints.ts | 80 + src/adapter/ember/adapter/index.ts | 3 + src/adapter/ember/adapter/oneWaitress.ts | 299 + src/adapter/ember/adapter/requestQueue.ts | 160 + src/adapter/ember/adapter/tokensManager.ts | 780 ++ src/adapter/ember/consts.ts | 290 + src/adapter/ember/enums.ts | 2423 ++++++ src/adapter/ember/ezsp/buffalo.ts | 1269 ++++ src/adapter/ember/ezsp/consts.ts | 148 + src/adapter/ember/ezsp/enums.ts | 958 +++ src/adapter/ember/ezsp/ezsp.ts | 7969 ++++++++++++++++++++ src/adapter/ember/types.ts | 812 ++ src/adapter/ember/uart/ash.ts | 1882 +++++ src/adapter/ember/uart/consts.ts | 115 + src/adapter/ember/uart/enums.ts | 192 + src/adapter/ember/uart/parser.ts | 46 + src/adapter/ember/uart/queues.ts | 243 + src/adapter/ember/uart/writer.ts | 52 + src/adapter/ember/utils/initters.ts | 73 + src/adapter/ember/utils/math.ts | 96 + src/adapter/ember/zdo.ts | 1034 +++ src/adapter/tstype.ts | 2 +- src/utils/backup.ts | 4 + test/adapter/ember/ash.test.ts | 347 + test/adapter/ember/consts.ts | 46 + test/adapter/ember/ezspBuffalo.test.ts | 48 + test/adapter/ember/math.test.ts | 183 + test/adapter/ember/requestQueue.test.ts | 613 ++ test/controller.test.ts | 2 +- 31 files changed, 24152 insertions(+), 4 deletions(-) create mode 100644 src/adapter/ember/adapter/emberAdapter.ts create mode 100644 src/adapter/ember/adapter/endpoints.ts create mode 100644 src/adapter/ember/adapter/index.ts create mode 100644 src/adapter/ember/adapter/oneWaitress.ts create mode 100644 src/adapter/ember/adapter/requestQueue.ts create mode 100644 src/adapter/ember/adapter/tokensManager.ts create mode 100644 src/adapter/ember/consts.ts create mode 100644 src/adapter/ember/enums.ts create mode 100644 src/adapter/ember/ezsp/buffalo.ts create mode 100644 src/adapter/ember/ezsp/consts.ts create mode 100644 src/adapter/ember/ezsp/enums.ts create mode 100644 src/adapter/ember/ezsp/ezsp.ts create mode 100644 src/adapter/ember/types.ts create mode 100644 src/adapter/ember/uart/ash.ts create mode 100644 src/adapter/ember/uart/consts.ts create mode 100644 src/adapter/ember/uart/enums.ts create mode 100644 src/adapter/ember/uart/parser.ts create mode 100644 src/adapter/ember/uart/queues.ts create mode 100644 src/adapter/ember/uart/writer.ts create mode 100644 src/adapter/ember/utils/initters.ts create mode 100644 src/adapter/ember/utils/math.ts create mode 100644 src/adapter/ember/zdo.ts create mode 100644 test/adapter/ember/ash.test.ts create mode 100644 test/adapter/ember/consts.ts create mode 100644 test/adapter/ember/ezspBuffalo.test.ts create mode 100644 test/adapter/ember/math.test.ts create mode 100644 test/adapter/ember/requestQueue.test.ts diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 3444de0978..d918c08e53 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -44,12 +44,13 @@ abstract class Adapter extends events.EventEmitter { const {DeconzAdapter} = await import('./deconz/adapter'); const {ZiGateAdapter} = await import('./zigate/adapter'); const {EZSPAdapter} = await import('./ezsp/adapter'); + const {EmberAdapter} = await import('./ember/adapter'); type AdapterImplementation = (typeof ZStackAdapter | typeof DeconzAdapter | typeof ZiGateAdapter - | typeof EZSPAdapter); + | typeof EZSPAdapter | typeof EmberAdapter); let adapters: AdapterImplementation[]; const adapterLookup = {zstack: ZStackAdapter, deconz: DeconzAdapter, zigate: ZiGateAdapter, - ezsp: EZSPAdapter}; + ezsp: EZSPAdapter, ember: EmberAdapter}; if (serialPortOptions.adapter && serialPortOptions.adapter !== 'auto') { if (adapterLookup.hasOwnProperty(serialPortOptions.adapter)) { adapters = [adapterLookup[serialPortOptions.adapter]]; diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts new file mode 100644 index 0000000000..6d5a97748a --- /dev/null +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -0,0 +1,3982 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import equals from 'fast-deep-equal/es6'; +import {fs} from "mz"; +import SerialPortUtils from '../../serialPortUtils'; +import SocketPortUtils from '../../socketPortUtils'; +import {BackupUtils, RealpathSync, Wait} from "../../../utils"; +import {Adapter, TsType} from "../.."; +import {LoggerStub} from "../../../controller/logger-stub"; +import {Backup, UnifiedBackupStorage} from "../../../models"; +import {FrameType, Direction, ZclFrame, Foundation} from "../../../zcl"; +import Cluster from "../../../zcl/definition/cluster"; +import { + DeviceAnnouncePayload, + DeviceJoinedPayload, + DeviceLeavePayload, + Events, + RawDataPayload, + ZclDataPayload +} from "../../events"; +import {halCommonCrc16, highByte, highLowToInt, lowByte, lowHighBytes} from "../utils/math"; +import {Ezsp, EzspEvents} from "../ezsp/ezsp"; +import { + EMBER_ENCRYPTION_KEY_SIZE, + EUI64_SIZE, + EZSP_MAX_FRAME_LENGTH, + EZSP_PROTOCOL_VERSION, + EZSP_STACK_TYPE_MESH +} from "../ezsp/consts"; +import { + EzspConfigId, + EzspDecisionBitmask, + EzspDecisionId, + EzspPolicyId, + EzspValueId +} from "../ezsp/enums"; +import {EzspBuffalo} from "../ezsp/buffalo"; +import { + EmberApsOption, + EmberOutgoingMessageType, + EmberStatus, + EzspStatus, + EmberVersionType, + SLStatus, + SecManFlag, + EmberNodeType, + EmberNetworkStatus, + SecManKeyType, + EmberLeaveRequestFlags, + EmberInterpanMessageType, + EmberSourceRouteDiscoveryMode, + EmberTXPowerMode, + EmberKeepAliveMode, + EmberJoinDecision, + EmberExtendedSecurityBitmask, + EmberInitialSecurityBitmask, + EmberJoinMethod, + EmberNetworkInitBitmask, + EmberDeviceUpdate, + EzspNetworkScanType, + EmberIncomingMessageType, + EmberCounterType, +} from "../enums"; +import { + EmberAesMmoHashContext, + EmberApsFrame, + EmberEUI64, + EmberExtendedPanId, + EmberInitialSecurityState, + EmberKeyData, + EmberMulticastId, + EmberMulticastTableEntry, + EmberNetworkInitStruct, + EmberNetworkParameters, + EmberNodeId, + EmberPanId, + EmberVersion, + SecManAPSKeyMetadata, + SecManContext, + SecManKey, +} from "../types"; +import { + EmberZdoStatus, + EndDeviceAnnouncePayload, + LQITableResponsePayload, + SimpleDescriptorResponsePayload, + NodeDescriptorResponsePayload, + ActiveEndpointsResponsePayload, + RoutingTableResponsePayload, + ACTIVE_ENDPOINTS_REQUEST, + BINDING_TABLE_REQUEST, + BIND_REQUEST, + IEEE_ADDRESS_REQUEST, + LEAVE_REQUEST, + LQI_TABLE_REQUEST, + MATCH_DESCRIPTORS_REQUEST, + MULTICAST_BINDING, + NETWORK_ADDRESS_REQUEST, + NODE_DESCRIPTOR_REQUEST, + PERMIT_JOINING_REQUEST, + POWER_DESCRIPTOR_REQUEST, + ROUTING_TABLE_REQUEST, + SIMPLE_DESCRIPTOR_REQUEST, + UNBIND_REQUEST, + UNICAST_BINDING, + ZDO_ENDPOINT, + ZDO_MESSAGE_OVERHEAD, + ZDO_PROFILE_ID, + PERMIT_JOINING_RESPONSE, + NODE_DESCRIPTOR_RESPONSE, + LQI_TABLE_RESPONSE, + ROUTING_TABLE_RESPONSE, + ACTIVE_ENDPOINTS_RESPONSE, + SIMPLE_DESCRIPTOR_RESPONSE, + BIND_RESPONSE, + UNBIND_RESPONSE, + LEAVE_RESPONSE +} from "../zdo"; +import { + EMBER_BROADCAST_ADDRESS, + EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS, + EMBER_SLEEPY_BROADCAST_ADDRESS, + EMBER_INSTALL_CODE_CRC_SIZE, + EMBER_INSTALL_CODE_SIZES, + MANUFACTURER_CODE, + EMBER_NUM_802_15_4_CHANNELS, + EMBER_MIN_802_15_4_CHANNEL_NUMBER, + ZIGBEE_COORDINATOR_ADDRESS, + UNKNOWN_NETWORK_STATE, + EMBER_UNKNOWN_NODE_ID, + MAXIMUM_APS_PAYLOAD_LENGTH, + APS_ENCRYPTION_OVERHEAD, + APS_FRAGMENTATION_OVERHEAD, + INVALID_PAN_ID, + LONG_DEST_FRAME_CONTROL, + MAC_ACK_REQUIRED, + MAXIMUM_INTERPAN_LENGTH, + STUB_NWK_FRAME_CONTROL, + TOUCHLINK_PROFILE_ID, + INTERPAN_APS_FRAME_TYPE, + SHORT_DEST_FRAME_CONTROL, + EMBER_HIGH_RAM_CONCENTRATOR, + BLANK_EUI64, + STACK_PROFILE_ZIGBEE_PRO, + SECURITY_LEVEL_Z3, + INVALID_RADIO_CHANNEL, + BLANK_EXTENDED_PAN_ID, + GP_ENDPOINT, + EMBER_ALL_802_15_4_CHANNELS_MASK, + ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY, + HA_PROFILE_ID, +} from "../consts"; +import {EmberRequestQueue} from "./requestQueue"; +import {FIXED_ENDPOINTS} from "./endpoints"; +import {aesMmoHashInit, initNetworkCache, initSecurityManagerContext} from "../utils/initters"; +import {randomBytes} from "crypto"; +import {EmberOneWaitress} from "./oneWaitress"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import {EmberTokensManager} from "./tokensManager"; + +const debug = Debug('zigbee-herdsman:adapter:ember:adapter'); + +export type NetworkCache = { + //-- basic network info + eui64: EmberEUI64, + parameters: EmberNetworkParameters, + status: EmberNetworkStatus, + /** uint8_t */ +}; + +/** + * + */ +type ConcentratorConfig = { + /** + * Minimum Time between broadcasts (in seconds) <1-60> + * Default: 10 + * The minimum amount of time that must pass between MTORR broadcasts. + */ + minTime: number, + /** + * Maximum Time between broadcasts (in seconds) <30-300> + * Default: 60 + * The maximum amount of time that can pass between MTORR broadcasts. + */ + maxTime: number, + /** + * Route Error Threshold <1-100> + * Default: 3 + * The number of route errors that will trigger a re-broadcast of the MTORR. + */ + routeErrorThreshold: number, + /** + * Delivery Failure Threshold <1-100> + * Default: 1 + * The number of APS delivery failures that will trigger a re-broadcast of the MTORR. + */ + deliveryFailureThreshold: number, + /** + * Maximum number of hops for Broadcast <0-30> + * Default: 0 + * The maximum number of hops that the MTORR broadcast will be allowed to have. + * A value of 0 will be converted to the EMBER_MAX_HOPS value set by the stack. + */ + mapHops: number, +}; + +/** + * Use for a link key backup. + * + * Each entry notes the EUI64 of the device it is paired to and the key data. + * This key may be hashed and not the actual link key currently in use. + */ +type LinkKeyBackupData = { + deviceEui64: EmberEUI64, + key: EmberKeyData, + outgoingFrameCounter: number, + incomingFrameCounter: number, +}; + +/** Enum to pass strings from numbers up to Z2M. */ +enum RoutingTableStatus { + ACTIVE = 0x0, + DISCOVERY_UNDERWAY = 0x1, + DISCOVERY_FAILED = 0x2, + INACTIVE = 0x3, + VALIDATION_UNDERWAY = 0x4, + RESERVED1 = 0x5, + RESERVED2 = 0x6, + RESERVED3 = 0x7, +}; + +/** Events specific to OneWaitress usage. */ +enum OneWaitressEvents { + STACK_STATUS_NETWORK_UP = 'STACK_STATUS_NETWORK_UP', + STACK_STATUS_NETWORK_DOWN = 'STACK_STATUS_NETWORK_DOWN', + STACK_STATUS_NETWORK_OPENED = 'STACK_STATUS_NETWORK_OPENED', + STACK_STATUS_NETWORK_CLOSED = 'STACK_STATUS_NETWORK_CLOSED', +}; + +enum NetworkInitAction { + /** Ain't that nice! */ + DONE, + /** Config mismatch, must leave network. */ + LEAVE, + /** Config mismatched, left network. Will evaluate forming from backup or config next. */ + LEFT, + /** Form the network using config. No backup, or backup mismatch. */ + FORM_CONFIG, + /** Re-form the network using full backed-up data. */ + FORM_BACKUP, +}; + +/** NOTE: Drivers can override `manufacturer`. Verify logic doesn't work in most cases anyway. */ +const autoDetectDefinitions = [ + /** NOTE: Manuf code "0x1321" for "Shenzhen Sonoff Technologies Co., Ltd." */ + {manufacturer: 'ITEAD', vendorId: '1a86', productId: '55d4'},// Sonoff ZBDongle-E + /** NOTE: Manuf code "0x134B" for "Nabu Casa, Inc." */ + {manufacturer: 'Nabu Casa', vendorId: '10c4', productId: 'ea60'},// Home Assistant SkyConnect +]; + +/** + * Config for EMBER_LOW_RAM_CONCENTRATOR type concentrator. + * + * Based on ZigbeeMinimalHost/zigpc + */ +const LOW_RAM_CONCENTRATOR_CONFIG: ConcentratorConfig = { + minTime: 5,// zigpc: 10 + maxTime: 60,// zigpc: 60 + routeErrorThreshold: 3,// zigpc: 3 + deliveryFailureThreshold: 1,// zigpc: 1, ZigbeeMinimalHost: 3 + mapHops: 0,// zigpc: 0 +}; +/** + * Config for EMBER_HIGH_RAM_CONCENTRATOR type concentrator. + * + * XXX: For now, same as low, until proper values can be determined. + */ +const HIGH_RAM_CONCENTRATOR_CONFIG: ConcentratorConfig = { + minTime: 5, + maxTime: 60, + routeErrorThreshold: 3, + deliveryFailureThreshold: 1, + mapHops: 0, +}; + +/** + * Application generated ZDO messages use sequence numbers 0-127, and the stack + * uses sequence numbers 128-255. This simplifies life by eliminating the need + * for coordination between the two entities, and allows both to send ZDO + * messages with non-conflicting sequence numbers. + */ +const APPLICATION_ZDO_SEQUENCE_MASK = 0x7F; +/** Current revision of the spec by zigbee alliance. XXX: what are `Zigbee Pro 2023` devices reporting?? */ +const CURRENT_ZIGBEE_SPEC_REVISION = 23; +/** Each scan period is 15.36ms. Scan for at least 200ms (2^4 + 1 periods) to pick up WiFi beacon frames. */ +const ENERGY_SCAN_DURATION = 4; +/** Oldest supported EZSP version for backups. Don't take the risk to restore a broken network until older backup versions can be investigated. */ +const BACKUP_OLDEST_SUPPORTED_EZSP_VERSION = 12; +/** + * 9sec is minimum recommended for `ezspBroadcastNextNetworkKey` to have propagated throughout network. + * NOTE: This is blocking the request queue, so we shouldn't go crazy high. + */ +const BROADCAST_NETWORK_KEY_SWITCH_WAIT_TIME = 15000; + +/** + * Stack configuration values for various supported stacks. + */ +const STACK_CONFIGS = { + "default": { + /** <1-250> (Default: 2) @see EzspConfigId.ADDRESS_TABLE_SIZE */ + ADDRESS_TABLE_SIZE: 16,// zigpc: 32, darkxst: 16 + /** <0-4> (Default: 2) @see EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE */ + TRUST_CENTER_ADDRESS_CACHE_SIZE: 2, + /** (Default: USE_TOKEN) @see EzspConfigId.TX_POWER_MODE */ + TX_POWER_MODE: EmberTXPowerMode.USE_TOKEN, + /** <-> (Default: 1) @see EzspConfigId.SUPPORTED_NETWORKS */ + SUPPORTED_NETWORKS: 1, + /** <-> (Default: ) @see EzspConfigId.STACK_PROFILE */ + STACK_PROFILE: STACK_PROFILE_ZIGBEE_PRO, + /** <-> (Default: ) @see EzspConfigId.SECURITY_LEVEL */ + SECURITY_LEVEL: SECURITY_LEVEL_Z3, + /** (Default: KEEP_ALIVE_SUPPORT_ALL) @see EzspValueId.END_DEVICE_KEEP_ALIVE_SUPPORT_MODE */ + END_DEVICE_KEEP_ALIVE_SUPPORT_MODE: EmberKeepAliveMode.KEEP_ALIVE_SUPPORT_ALL,// zigpc: KEEP_ALIVE_SUPPORT_ALL + /** <-> (Default: MAXIMUM_APS_PAYLOAD_LENGTH) @see EzspValueId.MAXIMUM_INCOMING_TRANSFER_SIZE */ + MAXIMUM_INCOMING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH, + /** <-> (Default: MAXIMUM_APS_PAYLOAD_LENGTH) @see EzspValueId.MAXIMUM_OUTGOING_TRANSFER_SIZE */ + MAXIMUM_OUTGOING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH, + /** <-> (Default: 10000) @see EzspValueId.TRANSIENT_DEVICE_TIMEOUT */ + TRANSIENT_DEVICE_TIMEOUT: 10000, + /** <0-127> (Default: 2) @see EzspConfigId.BINDING_TABLE_SIZE */ + BINDING_TABLE_SIZE: 5,// zigpc: 2, Z3GatewayGPCombo: 5 + /** <0-127> (Default: 0) @see EzspConfigId.KEY_TABLE_SIZE */ + KEY_TABLE_SIZE: 0,// zigpc: 4 + /** <6-64> (Default: 6) @see EzspConfigId.MAX_END_DEVICE_CHILDREN */ + MAX_END_DEVICE_CHILDREN: 6,// zigpc: 6 + /** <1-255> (Default: 10) @see EzspConfigId.APS_UNICAST_MESSAGE_COUNT */ + APS_UNICAST_MESSAGE_COUNT: 20,// zigpc: 10, darkxst: 20 + /** <15-254> (Default: 15) @see EzspConfigId.BROADCAST_TABLE_SIZE */ + BROADCAST_TABLE_SIZE: 15,// zigpc: 15, Z3GatewayGPCombo: 35 - NOTE: Sonoff Dongle-E fails at 35 + /** [1, 16, 26] (Default: 16). @see EzspConfigId.NEIGHBOR_TABLE_SIZE */ + NEIGHBOR_TABLE_SIZE: 26,// zigpc: 16, darkxst: 26 + /** (Default: 8) @see EzspConfigId.END_DEVICE_POLL_TIMEOUT */ + END_DEVICE_POLL_TIMEOUT: 8,// zigpc: 8 + /** <0-65535> (Default: 300) @see EzspConfigId.TRANSIENT_KEY_TIMEOUT_S */ + TRANSIENT_KEY_TIMEOUT_S: 300,// zigpc: 65535 + /** <-> (Default: 16) @see EzspConfigId.RETRY_QUEUE_SIZE */ + RETRY_QUEUE_SIZE: 16, + /** <0-255> (Default: 0) @see EzspConfigId.SOURCE_ROUTE_TABLE_SIZE */ + SOURCE_ROUTE_TABLE_SIZE: 200,// Z3GatewayGPCombo: 100, darkxst: 200 + /** <1-250> (Default: 8) @see EzspConfigId.MULTICAST_TABLE_SIZE */ + MULTICAST_TABLE_SIZE: 16,// darkxst: 16 + }, + "zigbeed": { + ADDRESS_TABLE_SIZE: 128, + TRUST_CENTER_ADDRESS_CACHE_SIZE: 2, + TX_POWER_MODE: EmberTXPowerMode.USE_TOKEN, + SUPPORTED_NETWORKS: 1, + STACK_PROFILE: STACK_PROFILE_ZIGBEE_PRO, + SECURITY_LEVEL: SECURITY_LEVEL_Z3, + END_DEVICE_KEEP_ALIVE_SUPPORT_MODE: EmberKeepAliveMode.KEEP_ALIVE_SUPPORT_ALL, + MAXIMUM_INCOMING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH, + MAXIMUM_OUTGOING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH, + TRANSIENT_DEVICE_TIMEOUT: 10000, + BINDING_TABLE_SIZE: 128, + KEY_TABLE_SIZE: 0,// zigbeed 128 + MAX_END_DEVICE_CHILDREN: 64, + APS_UNICAST_MESSAGE_COUNT: 32, + BROADCAST_TABLE_SIZE: 15, + NEIGHBOR_TABLE_SIZE: 26, + END_DEVICE_POLL_TIMEOUT: 8, + TRANSIENT_KEY_TIMEOUT_S: 300, + RETRY_QUEUE_SIZE: 16, + SOURCE_ROUTE_TABLE_SIZE: 254, + MULTICAST_TABLE_SIZE: 128, + /* + ROUTE_TABLE_SIZE: 254, + DISCOVERY_TABLE_SIZE: 64, + PACKET_BUFFER_COUNT: 255, + CUSTOM_MAC_FILTER_TABLE_SIZE: 64, + MAC_FILTER_TABLE_SIZE: 32, + CHILD_TABLE_SIZE: 64, + PLUGIN_ZIGBEE_PRO_STACK_CHILD_TABLE_SIZE: 64, + APS_MESSAGE_COUNT: 64, + */ + }, +}; + +/** + * Enabling this allows to immediately reject requests that won't be able to get to their destination. + * However, it causes more NCP calls, notably to get the source route overhead. + * XXX: Needs further testing before enabling + */ +const CHECK_APS_PAYLOAD_LENGTH = false; +/** Time for a ZDO request to get a callback response. ASH is 2400*6 for ACK timeout. */ +const DEFAULT_ZDO_REQUEST_TIMEOUT = 15000;// msec +/** Time for a ZCL request to get a callback response. ASH is 2400*6 for ACK timeout. */ +const DEFAULT_ZCL_REQUEST_TIMEOUT = 15000;//msec +/** Time for a network-related request to get a response (usually via event). */ +const DEFAULT_NETWORK_REQUEST_TIMEOUT = 10000;// nothing on the network to bother requests, should be much faster than this +/** Time between watchdog counters reading/clearing */ +const WATCHDOG_COUNTERS_FEED_INTERVAL = 3600000;// every hour... + +/** + * Relay calls between Z2M and EZSP-layer and handle any error that might occur via queue & waitress. + * + * Anything post `start` that requests anything from the EZSP layer must run through the request queue for proper execution flow. + */ +export class EmberAdapter extends Adapter { + /** Key in STACK_CONFIGS */ + public readonly stackConfig: 'default' | 'zigbeed'; + /** EMBER_LOW_RAM_CONCENTRATOR or EMBER_HIGH_RAM_CONCENTRATOR. */ + private concentratorType: number; + + private readonly ezsp: Ezsp; + private version: {ezsp: number, revision: string} & EmberVersion; + + private requestQueue: EmberRequestQueue; + private oneWaitress: EmberOneWaitress; + /** Periodically retrieve counters then clear them. */ + private watchdogCountersHandle: NodeJS.Timeout; + + /** Hold ZDO request in process. */ + private zdoRequestBuffalo: EzspBuffalo; + /** Sequence number used for ZDO requests. static uint8_t */ + private zdoRequestSequence: number; + /** Default radius used for broadcast ZDO requests. uint8_t */ + private zdoRequestRadius: number; + + private interpanLock: boolean; + + /** + * Cached network params to avoid NCP calls. Prevents frequent EZSP transactions. + * NOTE: Do not use directly, use getter functions for it that check if valid or need retrieval from NCP. + */ + private networkCache: NetworkCache; + + private defaultApsOptions: EmberApsOption; + + /** + * Mirrors the NCP multicast table. null === not in use. + * Index 0 is Green Power and must always remain there. + */ + private multicastTable: EmberMulticastTableEntry[]; + + constructor(networkOptions: TsType.NetworkOptions, serialPortOptions: TsType.SerialPortOptions, backupPath: string, + adapterOptions: TsType.AdapterOptions, logger?: LoggerStub) { + super(networkOptions, serialPortOptions, backupPath, adapterOptions, logger); + + // TODO config, should be fine like this for now? + this.stackConfig = SocketPortUtils.isTcpPath(serialPortOptions.path) ? 'zigbeed' : 'default'; + // TODO config + this.concentratorType = EMBER_HIGH_RAM_CONCENTRATOR; + + // TODO: config dispatch interval, tested at 100, 80, 60 + this.requestQueue = new EmberRequestQueue(60); + this.oneWaitress = new EmberOneWaitress(); + this.zdoRequestBuffalo = new EzspBuffalo(Buffer.alloc(EZSP_MAX_FRAME_LENGTH)); + + // TODO: config tick interval, tested at 500, 300, 100, 60, 30, all work fine and only really noticeable with interviews + this.ezsp = new Ezsp(60, serialPortOptions); + + this.ezsp.on(EzspEvents.STACK_STATUS, this.onStackStatus.bind(this)); + + this.ezsp.on(EzspEvents.MESSAGE_SENT_DELIVERY_FAILED, this.onMessageSentDeliveryFailed.bind(this)); + + this.ezsp.on(EzspEvents.ZDO_RESPONSE, this.onZDOResponse.bind(this)); + this.ezsp.on(EzspEvents.END_DEVICE_ANNOUNCE, this.onEndDeviceAnnounce.bind(this)); + this.ezsp.on(EzspEvents.INCOMING_MESSAGE, this.onIncomingMessage.bind(this)); + this.ezsp.on(EzspEvents.TOUCHLINK_MESSAGE, this.onTouchlinkMessage.bind(this)); + this.ezsp.on(EzspEvents.GREENPOWER_MESSAGE, this.onGreenpowerMessage.bind(this)); + + this.ezsp.on(EzspEvents.TRUST_CENTER_JOIN, this.onTrustCenterJoin.bind(this)); + } + + /** + * Emitted from @see Ezsp.ezspStackStatusHandler + * @param status + */ + private async onStackStatus(status: EmberStatus): Promise { + // to be extra careful, should clear network cache upon receiving this. + this.clearNetworkCache(); + + switch (status) { + case EmberStatus.NETWORK_UP: { + this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_UP); + console.log(`[STACK STATUS] Network up.`); + break; + } + case EmberStatus.NETWORK_DOWN: { + this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_DOWN); + console.log(`[STACK STATUS] Network down.`); + break; + } + case EmberStatus.NETWORK_OPENED: { + this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_OPENED); + this.requestQueue.enqueue( + async (): Promise => { + const setJPstatus = (await this.emberSetJoinPolicy(EmberJoinDecision.USE_PRECONFIGURED_KEY)); + + if (setJPstatus !== EzspStatus.SUCCESS) { + console.error(`[ZDO] Failed set join policy for with status=${EzspStatus[setJPstatus]}.`); + return EmberStatus.ERR_FATAL; + } + + return EmberStatus.SUCCESS; + }, + console.error,// no reject, just log error if any + true,// prioritize just to avoid delays if queue is busy + ); + console.log(`[STACK STATUS] Network opened.`); + break; + } + case EmberStatus.NETWORK_CLOSED: { + this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_CLOSED); + console.log(`[STACK STATUS] Network closed.`); + break; + } + default: { + debug(`[STACK STATUS] ${EmberStatus[status]}.`); + break; + } + } + } + + /** + * Emitted from @see Ezsp.ezspMessageSentHandler + * WARNING: Cannot rely on `ezspMessageSentHandler` > `ezspIncomingMessageHandler` order, some devices mix it up! + * + * @param type + * @param indexOrDestination + * @param apsFrame + * @param messageTag + */ + private async onMessageSentDeliveryFailed(type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number) + : Promise { + switch (type) { + case EmberOutgoingMessageType.BROADCAST: + case EmberOutgoingMessageType.BROADCAST_WITH_ALIAS: + case EmberOutgoingMessageType.MULTICAST: + case EmberOutgoingMessageType.MULTICAST_WITH_ALIAS: { + // BC/MC not checking for message sent, avoid unnecessary waitress lookups + console.error(`Delivery of ${EmberOutgoingMessageType[type]} failed for "${indexOrDestination}" ` + + `[apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`); + break; + } + default: { + // reject any waitress early (don't wait for timeout if we know we're gonna get there eventually) + this.oneWaitress.deliveryFailedFor(indexOrDestination, apsFrame); + break; + } + } + } + + /** + * Emitted from @see Ezsp.ezspIncomingMessageHandler + * + * @param clusterId The ZDO response cluster ID. + * @param sender The sender of the response. Should match `payload.nodeId` in many responses. + * @param payload If null, the response indicated a failure. + */ + private async onZDOResponse(status: EmberZdoStatus, sender: EmberNodeId, apsFrame: EmberApsFrame, payload: unknown) + : Promise { + this.oneWaitress.resolveZDO(status, sender, apsFrame, payload); + } + + /** + * Emitted from @see Ezsp.ezspIncomingMessageHandler + * + * @param sender + * @param nodeId + * @param eui64 + * @param macCapFlags + */ + private async onEndDeviceAnnounce(sender: EmberNodeId, apsFrame: EmberApsFrame, payload: EndDeviceAnnouncePayload): Promise { + // reduced function device + // if ((payload.capabilities.deviceType === 0)) { + + // } + + this.emit(Events.deviceAnnounce, {networkAddress: payload.nodeId, ieeeAddr: payload.eui64} as DeviceAnnouncePayload); + } + + /** + * Emitted from @see Ezsp.ezspIncomingMessageHandler + * + * @param type + * @param apsFrame + * @param lastHopLqi + * @param sender + * @param messageContents + */ + private async onIncomingMessage(type: EmberIncomingMessageType, apsFrame: EmberApsFrame, lastHopLqi: number, sender: EmberNodeId, + messageContents: Buffer): Promise { + try { + const payload: ZclDataPayload = { + address: sender, + frame: ZclFrame.fromBuffer(apsFrame.clusterId, messageContents), + endpoint: apsFrame.sourceEndpoint, + linkquality: lastHopLqi, + groupID: apsFrame.groupId, + wasBroadcast: ((type === EmberIncomingMessageType.BROADCAST) || (type === EmberIncomingMessageType.BROADCAST_LOOPBACK)), + destinationEndpoint: apsFrame.destinationEndpoint, + }; + + this.oneWaitress.resolveZCL(payload); + this.emit(Events.zclData, payload); + } catch (error) { + const payload: RawDataPayload = { + clusterID: apsFrame.clusterId, + address: sender, + data: messageContents, + endpoint: apsFrame.sourceEndpoint, + linkquality: lastHopLqi, + groupID: apsFrame.groupId, + wasBroadcast: ((type === EmberIncomingMessageType.BROADCAST) || (type === EmberIncomingMessageType.BROADCAST_LOOPBACK)), + destinationEndpoint: apsFrame.destinationEndpoint, + }; + + this.emit(Events.rawData, payload); + } + } + + /** + * Emitted from @see Ezsp.ezspMacFilterMatchMessageHandler when the message is a valid InterPAN touchlink message. + * + * @param sourcePanId + * @param sourceAddress + * @param groupId + * @param lastHopLqi + * @param messageContents + */ + private async onTouchlinkMessage(sourcePanId: EmberPanId, sourceAddress: EmberEUI64, groupId: number | null, lastHopLqi: number, + messageContents: Buffer): Promise { + const payload: ZclDataPayload = { + frame: ZclFrame.fromBuffer(Cluster.touchlink.ID, messageContents), + address: sourceAddress, + endpoint: 1,// arbitrary since not sent over-the-air + linkquality: lastHopLqi, + groupID: groupId, + wasBroadcast: true,// XXX: since always sent broadcast atm... + destinationEndpoint: FIXED_ENDPOINTS[0].endpoint, + }; + + this.oneWaitress.resolveZCL(payload); + this.emit(Events.zclData, payload); + } + + /** + * Emitted from @see Ezsp.ezspGpepIncomingMessageHandler + * + * @param sender uint32_t or EmberEUI64 depending on `EmberGpApplicationId`. See emitter + * @param gpdCommandId + * @param gpdLink + * @param sequenceNumber + * @param deviceId + * @param options + * @param key + * @param counter + */ + private async onGreenpowerMessage(sender: number | EmberEUI64, gpdCommandId: number, gpdLink: number, sequenceNumber: number, deviceId?: number, + options?: number, key?: EmberKeyData, counter?: number): Promise { + // TODO: all this stuff needs triple-checking, also with upstream (not really multi-adapter at first glance?) + // more params avail in EZSP handler + switch (gpdCommandId) { + case 0xE0: { + if (!key) { + return; + } + + // commissioning notification + const gpdMessage = { + // gppNwkAddr: ?,// XXX + commandID: gpdCommandId, + commandFrame: {options: options, securityKey: key.contents, deviceID: deviceId, outgoingCounter: counter}, + // XXX: Z2M seems to want only sourceId, but it isn't always present..? @see ezspGpepIncomingMessageHandler + srcID: sender, + }; + const zclFrame = ZclFrame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, true, null, sequenceNumber, 'commissioningNotification', + Cluster.greenPower.ID, gpdMessage); + const payload: ZclDataPayload = { + frame: zclFrame, + address: sender, + endpoint: GP_ENDPOINT, + linkquality: gpdLink, + groupID: null, + wasBroadcast: true, + destinationEndpoint: GP_ENDPOINT, + }; + + this.emit(Events.zclData, payload); + } + default:{// XXX: all the rest in one basket? + const gpdMessage = {commandID: gpdCommandId, srcID: sender};// same as above about `srcID` + const zclFrame = ZclFrame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, true, null, sequenceNumber, 'notification', + Cluster.greenPower.ID, gpdMessage); + + const payload: ZclDataPayload = { + frame: zclFrame, + address: sender, + endpoint: GP_ENDPOINT, + linkquality: gpdLink, + groupID: null, + wasBroadcast: true, + destinationEndpoint: GP_ENDPOINT, + }; + + this.emit(Events.zclData, payload); + } + } + } + + /** + * Emitted from @see Ezsp.ezspTrustCenterJoinHandler + * + * @param newNodeId + * @param newNodeEui64 + * @param status + * @param policyDecision + * @param parentOfNewNodeId + */ + private async onTrustCenterJoin(newNodeId: EmberNodeId, newNodeEui64: EmberEUI64, status: EmberDeviceUpdate, + policyDecision: EmberJoinDecision, parentOfNewNodeId: EmberNodeId): Promise { + if (status === EmberDeviceUpdate.DEVICE_LEFT) { + // NOTE: `policyDecision` here is NO_ACTION and `parentOfNewNodeId` is 65535 + const payload: DeviceLeavePayload = { + networkAddress: newNodeId, + ieeeAddr: newNodeEui64, + }; + + this.emit(Events.deviceLeave, payload); + } else { + if (policyDecision !== EmberJoinDecision.DENY_JOIN) { + const payload: DeviceJoinedPayload = { + networkAddress: newNodeId, + ieeeAddr: newNodeEui64, + }; + + this.emit(Events.deviceJoined, payload); + } else { + console.log(`[TRUST CENTER] Device ${newNodeId}:${newNodeEui64} was denied joining via ${parentOfNewNodeId}.`); + } + } + } + + private async watchdogCounters(): Promise { + this.requestQueue.enqueue( + async (): Promise => { + // listed as per EmberCounterType + const counters = (await this.ezsp.ezspReadAndClearCounters()); + + let countersLogString = "[NCP COUNTERS] "; + + for (let i = 0; i < EmberCounterType.COUNT; i++) { + countersLogString += `${EmberCounterType[i]}: ${counters[i]} | `; + } + + console.log(countersLogString); + + return EmberStatus.SUCCESS; + }, + console.error,// no reject, just log error if any + ); + } + + private initVariables(): void { + this.ezsp.removeAllListeners(EzspEvents.ncpNeedsResetAndInit); + + clearInterval(this.watchdogCountersHandle); + + this.zdoRequestBuffalo.setPosition(0); + this.zdoRequestSequence = 0;// start at 1 + this.zdoRequestRadius = 255; + + this.interpanLock = false; + + this.networkCache = initNetworkCache(); + + this.defaultApsOptions = (EmberApsOption.RETRY | EmberApsOption.ENABLE_ROUTE_DISCOVERY | EmberApsOption.ENABLE_ADDRESS_DISCOVERY); + + // always at least length==1 because of allowed MULTICAST_TABLE_SIZE range + this.multicastTable = new Array(STACK_CONFIGS[this.stackConfig].MULTICAST_TABLE_SIZE).fill(null); + + this.ezsp.once(EzspEvents.ncpNeedsResetAndInit, this.onNcpNeedsResetAndInit.bind(this)); + } + + /** + * Proceed to execute the long list of commands required to setup comms between Host<>NCP. + * This is called by start and on internal reset. + */ + private async initEzsp(): Promise { + let result: TsType.StartResult = "resumed"; + + await this.onNCPPreReset(); + + try { + // NOTE: something deep in this call can throw too + const result = (await this.ezsp.start()); + + if (result !== EzspStatus.SUCCESS) { + throw new Error(`Failed to start EZSP layer with status=${EzspStatus[result]}.`); + } + } catch (err) { + throw err; + } + + // call before any other command, else fails + await this.emberVersion(); + + await this.initNCPPreConfiguration(); + await this.initNCPAddressTable(); + await this.initNCPConfiguration(); + + // WARNING: From here on EZSP commands that affect memory allocation on the NCP should no longer be called (like resizing tables) + + await this.onNCPPostReset(); + await this.registerFixedEndpoints(); + this.clearNetworkCache(); + + result = (await this.initTrustCenter()); + + // after network UP, as per SDK, ensures clean slate + await this.initNCPConcentrator(); + + // await (this.emberStartEnergyScan());// TODO: via config of some kind, better off waiting for UI supports though + + // populate network cache info + const [status, , parameters] = (await this.ezsp.ezspGetNetworkParameters()); + + if (status !== EmberStatus.SUCCESS) { + throw new Error(`Failed to get network parameters with status=${EmberStatus[status]}.`); + } + + this.networkCache.parameters = parameters; + this.networkCache.status = (await this.ezsp.ezspNetworkState()); + this.networkCache.eui64 = (await this.ezsp.ezspGetEui64()); + + debug(`[INIT] Network Ready! ${JSON.stringify(this.networkCache)}`); + + return result; + } + + /** + * NCP Config init. Should always be called first in the init stack (after version cmd). + * @returns + */ + private async initNCPPreConfiguration(): Promise { + // this can only decrease, not increase, NCP-side value + await this.emberSetEzspConfigValue(EzspConfigId.ADDRESS_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].ADDRESS_TABLE_SIZE); + await this.emberSetEzspConfigValue( + EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE, + STACK_CONFIGS[this.stackConfig].TRUST_CENTER_ADDRESS_CACHE_SIZE + ); + + if (STACK_CONFIGS[this.stackConfig].STACK_PROFILE === STACK_PROFILE_ZIGBEE_PRO) { + // BUG 14222: If stack profile is 2 (ZigBee Pro), we need to enforce + // the standard stack configuration values for that feature set. + /** MAC indirect timeout should be 7.68 secs */ + await this.emberSetEzspConfigValue(EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680); + /** Max hops should be 2 * nwkMaxDepth, where nwkMaxDepth is 15 */ + await this.emberSetEzspConfigValue(EzspConfigId.MAX_HOPS, 30); + } + + await this.emberSetEzspConfigValue(EzspConfigId.TX_POWER_MODE, STACK_CONFIGS[this.stackConfig].TX_POWER_MODE); + await this.emberSetEzspConfigValue(EzspConfigId.SUPPORTED_NETWORKS, STACK_CONFIGS[this.stackConfig].SUPPORTED_NETWORKS); + + await this.emberSetEzspValue( + EzspValueId.END_DEVICE_KEEP_ALIVE_SUPPORT_MODE, + 1, + [STACK_CONFIGS[this.stackConfig].END_DEVICE_KEEP_ALIVE_SUPPORT_MODE] + ); + + // allow other devices to modify the binding table + await this.emberSetEzspPolicy( + EzspPolicyId.BINDING_MODIFICATION_POLICY, + EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS + ); + // return message tag and message contents in ezspMessageSentHandler() + await this.emberSetEzspPolicy( + EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY, + EzspDecisionId.MESSAGE_TAG_AND_CONTENTS_IN_CALLBACK + ); + + await this.emberSetEzspValue( + EzspValueId.MAXIMUM_INCOMING_TRANSFER_SIZE, + 2, + lowHighBytes(STACK_CONFIGS[this.stackConfig].MAXIMUM_INCOMING_TRANSFER_SIZE) + ); + await this.emberSetEzspValue( + EzspValueId.MAXIMUM_OUTGOING_TRANSFER_SIZE, + 2, + lowHighBytes(STACK_CONFIGS[this.stackConfig].MAXIMUM_OUTGOING_TRANSFER_SIZE) + ); + await this.emberSetEzspValue( + EzspValueId.TRANSIENT_DEVICE_TIMEOUT, + 2, + lowHighBytes(STACK_CONFIGS[this.stackConfig].TRANSIENT_DEVICE_TIMEOUT) + ); + + // Set the manufacturing code. This is defined by ZigBee document 053874r10 + // Ember's ID is 0x1002 and is the default, but this can be overridden in App Builder. + await this.ezsp.ezspSetManufacturerCode(MANUFACTURER_CODE); + + // network security init + await this.emberSetEzspConfigValue(EzspConfigId.STACK_PROFILE, STACK_CONFIGS[this.stackConfig].STACK_PROFILE); + await this.emberSetEzspConfigValue(EzspConfigId.SECURITY_LEVEL, STACK_CONFIGS[this.stackConfig].SECURITY_LEVEL); + } + + /** + * NCP Address table init. + * @returns + */ + private async initNCPAddressTable(): Promise { + const desiredTableSize = STACK_CONFIGS[this.stackConfig].ADDRESS_TABLE_SIZE; + // If the host and the ncp disagree on the address table size, explode. + const [status, addressTableSize] = (await this.ezsp.ezspGetConfigurationValue(EzspConfigId.ADDRESS_TABLE_SIZE)); + // After the change of ncp memory model in UC, we can not increase the default NCP table sizes anymore. + // Therefore, checking for desiredTableSize == (ncp)addressTableSize might not be always true anymore + // assert(desiredTableSize <= addressTableSize); + if ((status !== EzspStatus.SUCCESS) || (addressTableSize > desiredTableSize)) { + throw new Error( + `[INIT] NCP (${addressTableSize}) disagrees with Host (min ${desiredTableSize}) on table size. status=${EzspStatus[status]}` + ); + } + } + + /** + * NCP configuration init + */ + private async initNCPConfiguration(): Promise { + await this.emberSetEzspConfigValue(EzspConfigId.BINDING_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].BINDING_TABLE_SIZE); + await this.emberSetEzspConfigValue(EzspConfigId.KEY_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].KEY_TABLE_SIZE); + await this.emberSetEzspConfigValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, STACK_CONFIGS[this.stackConfig].MAX_END_DEVICE_CHILDREN); + await this.emberSetEzspConfigValue(EzspConfigId.APS_UNICAST_MESSAGE_COUNT, STACK_CONFIGS[this.stackConfig].APS_UNICAST_MESSAGE_COUNT); + await this.emberSetEzspConfigValue(EzspConfigId.BROADCAST_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].BROADCAST_TABLE_SIZE); + await this.emberSetEzspConfigValue(EzspConfigId.NEIGHBOR_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].NEIGHBOR_TABLE_SIZE); + await this.emberSetEzspConfigValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, STACK_CONFIGS[this.stackConfig].END_DEVICE_POLL_TIMEOUT); + await this.emberSetEzspConfigValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, STACK_CONFIGS[this.stackConfig].TRANSIENT_KEY_TIMEOUT_S); + await this.emberSetEzspConfigValue(EzspConfigId.RETRY_QUEUE_SIZE, STACK_CONFIGS[this.stackConfig].RETRY_QUEUE_SIZE); + await this.emberSetEzspConfigValue(EzspConfigId.SOURCE_ROUTE_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].SOURCE_ROUTE_TABLE_SIZE); + await this.emberSetEzspConfigValue(EzspConfigId.MULTICAST_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].MULTICAST_TABLE_SIZE); + } + + /** + * NCP concentrator init. Also enables source route discovery mode with RESCHEDULE. + * + * From AN1233: + * To function correctly in a Zigbee PRO network, a trust center also requires that: + * + * 1. The trust center application must act as a concentrator (either high or low RAM). + * 2. The trust center application must have support for source routing. + * It must record the source routes and properly handle requests by the stack for a particular source route. + * 3. The trust center application must use an address cache for security, in order to maintain a mapping of IEEE address to short ID. + * + * Failure to satisfy all of the above requirements may result in failures when joining/rejoining devices to the network across multiple hops + * (through a target node that is neither the trust center nor one of its neighboring routers.) + */ + private async initNCPConcentrator(): Promise { + const config = (this.concentratorType === EMBER_HIGH_RAM_CONCENTRATOR) ? HIGH_RAM_CONCENTRATOR_CONFIG : LOW_RAM_CONCENTRATOR_CONFIG; + const status = (await this.ezsp.ezspSetConcentrator( + true, + this.concentratorType, + config.minTime, + config.maxTime, + config.routeErrorThreshold, + config.deliveryFailureThreshold, + config.mapHops, + )); + + if (status !== EmberStatus.SUCCESS) { + throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${status}.`); + } + + const remainTilMTORR = (await this.ezsp.ezspSetSourceRouteDiscoveryMode(EmberSourceRouteDiscoveryMode.RESCHEDULE)); + + console.log(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`); + } + + /** + * Register fixed endpoints and set any related multicast entries that need to be. + */ + private async registerFixedEndpoints(): Promise { + for (const ep of FIXED_ENDPOINTS) { + if (ep.networkIndex !== 0x00) { + debug(`Multi-network not currently supported. Skipping endpoint ${JSON.stringify(ep)}.`); + continue; + } + + const [epStatus,] = (await this.ezsp.ezspGetEndpointFlags(ep.endpoint)); + + // endpoint not already registered + if (epStatus !== EzspStatus.SUCCESS) { + // 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 this.ezsp.ezspAddEndpoint( + ep.endpoint, + ep.profileId, + ep.deviceId, + ep.deviceVersion, + ep.inClusterList, + ep.outClusterList, + )); + + if (status === EzspStatus.SUCCESS) { + debug(`Registered endpoint "${ep.endpoint}" with status=${EzspStatus[status]}.`); + } else { + throw new Error(`Failed to register endpoint "${ep.endpoint}" with status=${EzspStatus[status]}.`); + } + } else { + debug(`Endpoint "${ep.endpoint}" already registered.`); + } + + if (ep.endpoint === GP_ENDPOINT) { + const gpMulticastEntry: EmberMulticastTableEntry = { + multicastId: this.greenPowerGroup, + endpoint: ep.endpoint, + networkIndex: ep.networkIndex, + }; + + const status = (await this.ezsp.ezspSetMulticastTableEntry(0, gpMulticastEntry)); + + if (status !== EmberStatus.SUCCESS) { + throw new Error(`Failed to register group "Green Power" in multicast table with status=${EmberStatus[status]}.`); + } + + // NOTE: ensure GP is always added first in the table + this.multicastTable[0] = gpMulticastEntry; + debug(`Registered multicast table entry: ${JSON.stringify(gpMulticastEntry)}.`); + } + } + } + + /** + * + * @returns True if the network needed to be formed. + */ + private async initTrustCenter(): Promise { + // init TC policies + { + let status = (await this.emberSetEzspPolicy( + EzspPolicyId.TC_KEY_REQUEST_POLICY, + EzspDecisionId.ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY, + )); + + if (status !== EzspStatus.SUCCESS) { + throw new Error(`[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY ` + + `with status=${EzspStatus[status]}.`); + } + + status = (await this.emberSetEzspPolicy( + EzspPolicyId.APP_KEY_REQUEST_POLICY, + STACK_CONFIGS[this.stackConfig].KEY_TABLE_SIZE ? EzspDecisionId.ALLOW_APP_KEY_REQUESTS : EzspDecisionId.DENY_APP_KEY_REQUESTS, + )); + + if (status !== EzspStatus.SUCCESS) { + throw new Error(`[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to DENY_APP_KEY_REQUESTS ` + + `with status=${EzspStatus[status]}.`); + } + + status = (await this.emberSetJoinPolicy(EmberJoinDecision.USE_PRECONFIGURED_KEY)); + + if (status !== EzspStatus.SUCCESS) { + throw new Error(`[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=${EzspStatus[status]}.`); + } + } + + const configNetworkKey = Buffer.from(this.networkOptions.networkKey); + const networkInitStruct: EmberNetworkInitStruct = { + bitmask: (EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT) + }; + const initStatus = (await this.ezsp.ezspNetworkInit(networkInitStruct)); + + debug(`[INIT TC] Network init status=${EmberStatus[initStatus]}.`); + + if ((initStatus !== EmberStatus.SUCCESS) && (initStatus !== EmberStatus.NOT_JOINED)) { + throw new Error(`[INIT TC] Failed network init request with status=${EmberStatus[initStatus]}.`); + } + + let action: NetworkInitAction = NetworkInitAction.DONE; + + if (initStatus === EmberStatus.SUCCESS) { + // network + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_UP}, + DEFAULT_NETWORK_REQUEST_TIMEOUT, + '[INIT TC] Network init', + ); + + const [npStatus, nodeType, netParams] = (await this.ezsp.ezspGetNetworkParameters()); + + debug(`[INIT TC] Current network config=${JSON.stringify(this.networkOptions)}`); + debug(`[INIT TC] Current NCP network: nodeType=${EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`); + + if ((npStatus === EmberStatus.SUCCESS) && (nodeType === EmberNodeType.COORDINATOR) && (this.networkOptions.panID === netParams.panId) + && (equals(this.networkOptions.extendedPanID, netParams.extendedPanId)) + && (this.networkOptions.channelList.includes(netParams.radioChannel))) { + // config matches adapter so far, no error, we can check the network key + const context = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + const [networkKey, nkStatus] = (await this.ezsp.ezspExportKey(context)); + + if (nkStatus !== SLStatus.OK) { + throw new Error(`[BACKUP] Failed to export Network Key with status=${SLStatus[nkStatus]}.`); + } + + debug(`[INIT TC] Current NCP network: networkKey=${networkKey.contents.toString('hex')}`); + + // config doesn't match adapter anymore + if (!networkKey.contents.equals(configNetworkKey)) { + action = NetworkInitAction.LEAVE; + } + } else { + // config doesn't match adapter + action = NetworkInitAction.LEAVE; + } + + if (action === NetworkInitAction.LEAVE) { + console.log(`[INIT TC] NCP network does not match config. Leaving network...`); + const leaveStatus = (await this.ezsp.ezspLeaveNetwork()); + + if (leaveStatus !== EmberStatus.SUCCESS) { + throw new Error(`[INIT TC] Failed leave network request with status=${EmberStatus[leaveStatus]}.`); + } + + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_DOWN}, + DEFAULT_NETWORK_REQUEST_TIMEOUT, + '[INIT TC] Leave network', + ); + + await Wait(200);// settle down + + action = NetworkInitAction.LEFT; + } + } + + const backup: Backup = (await this.getStoredBackup()); + + if ((initStatus === EmberStatus.NOT_JOINED) || (action === NetworkInitAction.LEFT)) { + // no network + if (backup != null) { + if ((this.networkOptions.panID === backup.networkOptions.panId) + && (Buffer.from(this.networkOptions.extendedPanID).equals(backup.networkOptions.extendedPanId)) + && (this.networkOptions.channelList.includes(backup.logicalChannel)) + && (configNetworkKey.equals(backup.networkOptions.networkKey))) { + // config matches backup + action = NetworkInitAction.FORM_BACKUP; + } else { + // config doesn't match backup + console.log(`[INIT TC] Config does not match backup.`); + action = NetworkInitAction.FORM_CONFIG; + } + } else { + // no backup + console.log(`[INIT TC] No valid backup found.`); + action = NetworkInitAction.FORM_CONFIG; + } + } else { + action = NetworkInitAction.DONE;// just to be clear + } + + //---- from here on, we assume everything is in place for whatever decision was taken above + + let result: TsType.StartResult = 'resumed'; + + switch (action) { + case NetworkInitAction.FORM_BACKUP: { + console.log(`[INIT TC] Forming from backup.`); + const keyList: LinkKeyBackupData[] = backup.devices.map((device) => { + const octets = Array.from(device.ieeeAddress.reverse()); + const deviceEui64 = '0x' + octets.map(octet => octet.toString(16).padStart(2, '0')).join(""); + const key: LinkKeyBackupData = { + deviceEui64, + key: {contents: device.linkKey.key}, + outgoingFrameCounter: device.linkKey.txCounter, + incomingFrameCounter: device.linkKey.rxCounter, + }; + return key; + }); + + // before forming + await this.importLinkKeys(keyList); + + await this.formNetwork( + true,/*from backup*/ + backup.networkOptions.networkKey, + backup.networkKeyInfo.sequenceNumber, + backup.networkOptions.panId, + Array.from(backup.networkOptions.extendedPanId), + backup.logicalChannel, + backup.ezsp.hashed_tclk, + ); + + result = 'restored'; + break; + } + case NetworkInitAction.FORM_CONFIG: { + console.log(`[INIT TC] Forming from config.`); + await this.formNetwork( + false,/*from config*/ + configNetworkKey, + 0, + this.networkOptions.panID, + this.networkOptions.extendedPanID, + this.networkOptions.channelList[0], + randomBytes(EMBER_ENCRYPTION_KEY_SIZE),// rnd TC link key + ); + + result = 'reset'; + break; + } + case NetworkInitAction.DONE: { + console.log(`[INIT TC] NCP network matches config.`); + break; + } + default: { + throw new Error(`[INIT TC] Invalid action "${NetworkInitAction[action]}" for final stage.`); + } + } + + // can't let frame counter wrap to zero (uint32_t), will force a broadcast after init if getting too close + if (backup != null && (backup.networkKeyInfo.frameCounter > 0xFEEEEEEE)) { + // XXX: while this remains a pretty low occurrence in most (small) networks, + // currently Z2M won't support the key update because of one-way config... + // need to investigate handling this properly + + // console.warn(`[INIT TC] Network key frame counter is reaching its limit. Scheduling broadcast to update network key. ` + // + `This may result in some devices (especially battery-powered) temporarily losing connection.`); + // // XXX: no idea here on the proper timer value, but this will block the network for several seconds on exec + // // (probably have to take the behavior of sleepy-end devices into account to improve chances of reaching everyone right away?) + // setTimeout(async () => { + // this.requestQueue.enqueue(async (): Promise => { + // await this.broadcastNetworkKeyUpdate(); + + // return EmberStatus.SUCCESS; + // }, console.error, true);// no reject just log error if any, will retry next start, & prioritize so we know it'll run when expected + // }, 300000); + console.warn(`[INIT TC] Network key frame counter is reaching its limit. A new network key will have to be instaured soon.`); + } + + return result; + } + + /** + * Form a network using given parameters. + */ + private async formNetwork(fromBackup: boolean, networkKey: Buffer, networkKeySequenceNumber: number, panId: EmberPanId, + extendedPanId: EmberExtendedPanId, radioChannel: number, tcLinkKey: Buffer): Promise { + const state: EmberInitialSecurityState = { + bitmask: ( + EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY | EmberInitialSecurityBitmask.HAVE_PRECONFIGURED_KEY + | EmberInitialSecurityBitmask.HAVE_NETWORK_KEY | EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + | EmberInitialSecurityBitmask.REQUIRE_ENCRYPTED_KEY + ), + preconfiguredKey: {contents: tcLinkKey}, + networkKey: {contents: networkKey}, + networkKeySequenceNumber: networkKeySequenceNumber, + preconfiguredTrustCenterEui64: BLANK_EUI64, + }; + + if (fromBackup) { + state.bitmask |= EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET; + } + + let emberStatus = (await this.ezsp.ezspSetInitialSecurityState(state)); + + if (emberStatus !== EmberStatus.SUCCESS) { + throw new Error(`[INIT FORM] Failed to set initial security state with status=${EmberStatus[emberStatus]}.`); + } + + const extended: EmberExtendedSecurityBitmask = ( + EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.NWK_LEAVE_REQUEST_NOT_ALLOWED + ); + const extSecStatus = (await this.ezsp.ezspSetExtendedSecurityBitmask(extended)); + + if (extSecStatus !== EzspStatus.SUCCESS) { + throw new Error(`[INIT FORM] Failed to set extended security bitmask to ${extended} with status=${EzspStatus[extSecStatus]}.`); + } + + if (!fromBackup && STACK_CONFIGS[this.stackConfig].KEY_TABLE_SIZE) { + emberStatus = await this.ezsp.ezspClearKeyTable(); + + if (emberStatus !== EmberStatus.SUCCESS) { + throw new Error(`[INIT FORM] Failed to clear key table with status=${EmberStatus[emberStatus]}.`); + } + } + + const netParams: EmberNetworkParameters = { + panId, + extendedPanId, + radioTxPower: 5, + radioChannel, + joinMethod: EmberJoinMethod.MAC_ASSOCIATION, + nwkManagerId: ZIGBEE_COORDINATOR_ADDRESS, + nwkUpdateId: 0, + channels: EMBER_ALL_802_15_4_CHANNELS_MASK, + }; + + console.log(`[INIT FORM] Forming new network with: ${JSON.stringify(netParams)}`); + + emberStatus = (await this.ezsp.ezspFormNetwork(netParams)); + + if (emberStatus !== EmberStatus.SUCCESS) { + throw new Error(`[INIT FORM] Failed form network request with status=${EmberStatus[emberStatus]}.`); + } + + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_UP}, + DEFAULT_NETWORK_REQUEST_TIMEOUT, + '[INIT FORM] Form network', + ); + + const stStatus = await this.ezsp.ezspStartWritingStackTokens(); + + debug(`[INIT FORM] Start writing stack tokens status=${EzspStatus[stStatus]}.`); + + console.log(`[INIT FORM] New network formed!`); + } + + /** + * Loads currently stored backup and returns it in internal backup model. + */ + public async getStoredBackup(): Promise { + try { + await fs.access(this.backupPath); + } catch (error) { + return null; + } + + let data: UnifiedBackupStorage; + + try { + data = JSON.parse((await fs.readFile(this.backupPath)).toString()); + } catch (error) { + throw new Error(`[BACKUP] Coordinator backup is corrupted.`); + } + + if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) { + if (data.metadata?.version !== 1) { + throw new Error(`[BACKUP] Unsupported open coordinator backup version (version=${data.metadata?.version}).`); + } + + if (!data.stack_specific?.ezsp || !data.metadata.internal.ezspVersion) { + throw new Error(`[BACKUP] Specified backup is not EZSP.`); + } + + if (data.metadata.internal.ezspVersion < BACKUP_OLDEST_SUPPORTED_EZSP_VERSION) { + throw new Error(`[BACKUP] Specified backup is not a supported EZSP version (min: ${BACKUP_OLDEST_SUPPORTED_EZSP_VERSION}).`); + } + + return BackupUtils.fromUnifiedBackup(data); + } else { + throw new Error(`[BACKUP] Unknown backup format.`); + } + } + + /** + * Export link keys for backup. + * + * @return List of keys data with AES hashed keys + */ + public async exportLinkKeys(): Promise { + const [confStatus, keyTableSize] = (await this.ezsp.ezspGetConfigurationValue(EzspConfigId.KEY_TABLE_SIZE)); + + if (confStatus !== EzspStatus.SUCCESS) { + throw new Error(`[BACKUP] Failed to retrieve key table size from NCP with status=${EzspStatus[confStatus]}.`); + } + + let deviceEui64: EmberEUI64; + let plaintextKey: SecManKey; + let apsKeyMeta: SecManAPSKeyMetadata; + let status: SLStatus; + const keyList: LinkKeyBackupData[] = []; + + for (let i = 0; i < keyTableSize; i++) { + [deviceEui64, plaintextKey, apsKeyMeta, status] = (await this.ezsp.ezspExportLinkKeyByIndex(i)); + debug(`[BACKUP] Export link key at index ${i}, status=${SLStatus[status]}.`); + + // only include key if we could retrieve one at index and hash it properly + if (status === SLStatus.OK) { + // Rather than give the real link key, the backup contains a hashed version of the key. + // This is done to prevent a compromise of the backup data from compromising the current link keys. + // This is per the Smart Energy spec. + const [hashStatus, hashedKey] = (await this.emberAesHashSimple(plaintextKey.contents)); + + if (hashStatus === EmberStatus.SUCCESS) { + keyList.push({ + deviceEui64, + key: {contents: hashedKey}, + outgoingFrameCounter: apsKeyMeta.outgoingFrameCounter, + incomingFrameCounter: apsKeyMeta.incomingFrameCounter, + }); + } else { + // this should never happen? + console.error(`[BACKUP] Failed to hash link key at index ${i} with status=${EmberStatus[hashStatus]}. Omitting from backup.`); + } + } + } + + console.log(`[BACKUP] Retrieved ${keyList.length} link keys.`); + + return keyList; + } + + /** + * Import link keys from backup. + * + * @param backupData + */ + public async importLinkKeys(backupData: LinkKeyBackupData[]): Promise { + if (!backupData?.length) { + return; + } + + const [confStatus, keyTableSize] = (await this.ezsp.ezspGetConfigurationValue(EzspConfigId.KEY_TABLE_SIZE)); + + if (confStatus !== EzspStatus.SUCCESS) { + throw new Error(`[BACKUP] Failed to retrieve key table size from NCP with status=${EzspStatus[confStatus]}.`); + } + + if (backupData.length > keyTableSize) { + throw new Error(`[BACKUP] Current key table of ${keyTableSize} is too small to import backup of ${backupData.length}!`); + } + + const networkStatus = (await this.emberNetworkState()); + + if (networkStatus !== EmberNetworkStatus.NO_NETWORK) { + throw new Error(`[BACKUP] Cannot import TC data while network is up, networkStatus=${EmberNetworkStatus[networkStatus]}.`); + } + + let status: EmberStatus; + + for (let i = 0; i < keyTableSize; i++) { + if (i >= backupData.length) { + // erase any key index not present in backup but available on the NCP + status = (await this.ezsp.ezspEraseKeyTableEntry(i)); + } else { + const importStatus = (await this.ezsp.ezspImportLinkKey(i, backupData[i].deviceEui64, backupData[i].key)); + status = ((importStatus === SLStatus.OK) ? EmberStatus.SUCCESS : EmberStatus.KEY_TABLE_INVALID_ADDRESS); + } + + if (status !== EmberStatus.SUCCESS) { + throw new Error(`[BACKUP] Failed to ${((i >= backupData.length) ? "erase" : "set")} key table entry at index ${i} ` + + `with status=${EmberStatus[status]}`); + } + } + + debug(`[BACKUP] Imported ${backupData.length} keys.`); + } + + /** + * Routine to update the network key and broadcast the update to the network after a set time. + * NOTE: This should run at a large interval, but before the uint32_t of the frame counter is able to reach all Fs (can't wrap to 0). + * This may disrupt sleepy end devices that miss the update, but they should be able to TC rejoin (in most cases...). + * On the other hand, the more often this runs, the more secure the network is... + */ + private async broadcastNetworkKeyUpdate(): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + console.warn(`[TRUST CENTER] Performing a network key update. This might take a while and disrupt normal operation.`); + + // zero-filled = let stack generate new random network key + let status = await this.ezsp.ezspBroadcastNextNetworkKey({contents: Buffer.alloc(EMBER_ENCRYPTION_KEY_SIZE)}); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[TRUST CENTER] Failed to broadcast next network key with status=${EmberStatus[status]}.`); + return status; + } + + // XXX: this will block other requests for a while, but should ensure the key propagates without interference? + // could also stop dispatching entirely and do this outside the queue if necessary/better + await Wait(BROADCAST_NETWORK_KEY_SWITCH_WAIT_TIME); + + status = (await this.ezsp.ezspBroadcastNetworkKeySwitch()); + + if (status !== EmberStatus.SUCCESS) { + // XXX: Not sure how likely this is, but this is bad, probably should hard fail? + console.error(`[TRUST CENTER] Failed to broadcast network key switch with status=${EmberStatus[status]}.`); + return status; + } + + resolve(); + return status; + }, + reject, + ); + }); + } + + /** + * Received when EZSP layer alerts of a problem that needs the NCP to be reset. + * @param status + */ + private async onNcpNeedsResetAndInit(status: EzspStatus): Promise { + console.error(`!!! NCP FATAL ERROR reason=${EzspStatus[status]}. ATTEMPTING RESET... !!!`); + + try { + await this.stop(); + await Wait(500);// just because + await this.start(); + } catch (err) { + console.error(`Failed to reset and init NCP. ${err}`); + this.emit(Events.disconnected); + } + } + + /** + * Called right before a NCP reset. + */ + private async onNCPPreReset(): Promise { + this.requestQueue.stopDispatching(); + } + + /** + * Called right after a NCP reset, right before the creation of endpoints. + */ + private async onNCPPostReset(): Promise { + this.requestQueue.startDispatching(); + + this.watchdogCountersHandle = setInterval(this.watchdogCounters.bind(this), WATCHDOG_COUNTERS_FEED_INTERVAL); + } + + /** + * Handle changes in groups that needs to be propagated to the NCP multicast table. + * + * XXX: Since Z2M doesn't explicitly check-in downstream when groups are created/removed, we look at outgoing genGroups commands. + * If the NCP doesn't know about groups, it can miss messages from some devices (remotes for example), so we add it... + * + * @param commandId + * @param groupId + */ + private async onGroupChange(commandId: number, groupId?: number): Promise { + switch (commandId) { + case Cluster.genGroups.commands.add.ID: { + // check if group already in multicast table, should not happen... + const existingIndex = this.multicastTable.findIndex((e) => ((e != null) && (e.multicastId === groupId))); + + if (existingIndex == -1) { + // find first unused index + const newEntryIndex = this.multicastTable.findIndex((e) => (!e)); + + if (newEntryIndex != -1) { + const newEntry: EmberMulticastTableEntry = { + multicastId: groupId, + endpoint: FIXED_ENDPOINTS[0].endpoint, + networkIndex: FIXED_ENDPOINTS[0].networkIndex, + }; + const status = (await this.ezsp.ezspSetMulticastTableEntry(newEntryIndex, newEntry)); + + if (status !== EmberStatus.SUCCESS) { + console.error( + `Failed to register group "${groupId}" in multicast table at index "${newEntryIndex}" with status=${EmberStatus[status]}.` + ); + } else { + debug(`Registered multicast table entry: ${JSON.stringify(newEntry)}.`); + } + + // always assume "it worked" to keep sync with Z2M first, NCP second, otherwise trouble might arise... should always work anyway + this.multicastTable[newEntryIndex] = newEntry; + } else { + console.warn(`Coordinator multicast table is full (max: ${STACK_CONFIGS[this.stackConfig].MULTICAST_TABLE_SIZE}). ` + + `Some devices in new groups may not work properly, including in group "${groupId}". ` + + `If that happens, please remove groups to be below the limit. ` + + `Removed groups are only removed from coordinator after a Zigbee2MQTT restart.`); + } + } else { + debug(`Added group "${groupId}", but local table says it is already registered at index "${existingIndex}". Skipping.`); + } + break; + } + // NOTE: Can't remove groups, since we watch from command exec to group members, that would trigger from any removed member, + // even though the group might still exist... + // Leaving this here (since it's done...), just in case we get better notifications for groups from upstream. + // case Cluster.genGroups.commands.remove.ID: { + // const entryIndex = this.multicastTable.findIndex((e) => ((e != null) && (e.multicastId === groupId))); + + // // just in case, never remove GP at i zero, should never be the case... + // if (entryIndex > 0) { + // const entry = this.multicastTable[entryIndex]; + // entry.endpoint = 0;// signals "not in use" in the stack + // const status = (await this.ezsp.ezspSetMulticastTableEntry(entryIndex, entry)); + + // if (status !== EmberStatus.SUCCESS) { + // console.error(`Failed to remove multicast table entry at index "${entryIndex}" for group "${groupId}".`); + // } else { + // debug(`Removed multicast table entry at index "${entryIndex}".`); + // } + + // // always assume "it worked" to keep sync with Z2M first, NCP second, otherwise trouble might arise... should always work anyway + // this.multicastTable[entryIndex] = null; + // } else { + // debug(`Removed group "${groupId}", but local table did not have a reference to it.`); + // } + // break; + // } + // case Cluster.genGroups.commands.removeAll.ID: { + // // this can create quite a few NCP calls, but hopefully shouldn't happen often + // // always skip green power at i==0 + // for (let i = 1; i < this.multicastTable.length; i++) { + // const entry = this.multicastTable[i]; + + // if (entry != null) { + // entry.endpoint = 0;// signals "not in use" in the stack + // const status = (await this.ezsp.ezspSetMulticastTableEntry(i, entry)); + + // if (status !== EmberStatus.SUCCESS) { + // console.error(`Failed to remove multicast entry at index "${i}" with status=${EmberStatus[status]}.`); + // } else { + // debug(`Removed multicast table entry at index "${i}".`); + // } + // } + + // this.multicastTable[i] = null; + // } + + // break; + // } + } + } + + //---- START Events + + //---- END Events + + //---- START Cache-enabled EZSP wrappers + + /** + * Clear the cached network values (set to invalid values). + */ + private clearNetworkCache(): void { + this.networkCache = initNetworkCache(); + } + + /** + * Return the current network state. + * This call caches the results on the host to prevent frequent EZSP transactions. + * Check against UNKNOWN_NETWORK_STATE for validity. + */ + public async emberNetworkState(): Promise { + if (this.networkCache.status === (UNKNOWN_NETWORK_STATE as EmberNetworkStatus)) { + const networkStatus = (await this.ezsp.ezspNetworkState()); + + this.networkCache.status = networkStatus; + } + + return this.networkCache.status; + } + + /** + * Return the EUI 64 of the local node + * This call caches the results on the host to prevent frequent EZSP transactions. + * Check against BLANK_EUI64 for validity. + */ + public async emberGetEui64(): Promise { + if (this.networkCache.eui64 === BLANK_EUI64) { + this.networkCache.eui64 = (await this.ezsp.ezspGetEui64()); + } + + return this.networkCache.eui64; + } + + /** + * Return the PAN ID of the local node. + * This call caches the results on the host to prevent frequent EZSP transactions. + * Check against INVALID_PAN_ID for validity. + */ + public async emberGetPanId(): Promise { + if (this.networkCache.parameters.panId === INVALID_PAN_ID) { + const [status, , parameters] = (await this.ezsp.ezspGetNetworkParameters()); + + if (status === EmberStatus.SUCCESS) { + this.networkCache.parameters = parameters; + } else { + console.error(`Failed to get PAN ID (via network parameters) with status=${EmberStatus[status]}.`); + } + } + + return this.networkCache.parameters.panId; + } + + /** + * Return the Extended PAN ID of the local node. + * This call caches the results on the host to prevent frequent EZSP transactions. + * Check against BLANK_EXTENDED_PAN_ID for validity. + */ + public async emberGetExtendedPanId(): Promise { + if (equals(this.networkCache.parameters.extendedPanId, BLANK_EXTENDED_PAN_ID)) { + const [status, , parameters] = (await this.ezsp.ezspGetNetworkParameters()); + + if (status === EmberStatus.SUCCESS) { + this.networkCache.parameters = parameters; + } else { + console.error(`Failed to get Extended PAN ID (via network parameters) with status=${EmberStatus[status]}.`); + } + } + + return this.networkCache.parameters.extendedPanId; + } + + /** + * Return the radio channel (uint8_t) of the current network. + * This call caches the results on the host to prevent frequent EZSP transactions. + * Check against INVALID_RADIO_CHANNEL for validity. + */ + public async emberGetRadioChannel(): Promise { + if (this.networkCache.parameters.radioChannel === INVALID_RADIO_CHANNEL) { + const [status, , parameters] = (await this.ezsp.ezspGetNetworkParameters()); + + if (status === EmberStatus.SUCCESS) { + this.networkCache.parameters = parameters; + } else { + console.error(`Failed to get radio channel (via network parameters) with status=${EmberStatus[status]}.`); + } + } + + return this.networkCache.parameters.radioChannel; + } + + // queued + public async emberStartEnergyScan(): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + const status = (await this.ezsp.ezspStartScan( + EzspNetworkScanType.ENERGY_SCAN, + EMBER_ALL_802_15_4_CHANNELS_MASK, + ENERGY_SCAN_DURATION, + )); + + if (status !== SLStatus.OK) { + console.error(`Failed energy scan request with status=${SLStatus[status]}.`); + return EmberStatus.ERR_FATAL; + } + + // TODO: result in logs only atm, since UI doesn't support it + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + //---- END Cache-enabled EZSP wrappers + + //---- START EZSP wrappers + + /** + * Ensure the Host & NCP are aligned on protocols using version. + * Cache the retrieved information. + * + * NOTE: currently throws on mismatch until support for lower versions is implemented (not planned atm) + * + * Does nothing if ncpNeedsResetAndInit == true. + */ + private async emberVersion(): Promise { + // Note that NCP == Network Co-Processor + // the EZSP protocol version that the Host is running, we are the host so we set this value + const hostEzspProtocolVer = EZSP_PROTOCOL_VERSION; + // 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 + const [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = (await this.ezsp.ezspVersion(hostEzspProtocolVer)); + + // verify that the stack type is what is expected + if (ncpStackType !== EZSP_STACK_TYPE_MESH) { + throw new Error(`Stack type ${ncpStackType} is not expected!`); + } + + // verify that the NCP EZSP Protocol version is what is expected + if (ncpEzspProtocolVer !== EZSP_PROTOCOL_VERSION) { + throw new Error(`NCP EZSP protocol version of ${ncpEzspProtocolVer} does not match Host version ${hostEzspProtocolVer}`); + } + + debug(`NCP info: EZSPVersion=${ncpEzspProtocolVer} StackType=${ncpStackType} StackVersion=${ncpStackVer}`); + + const [status, versionStruct] = (await this.ezsp.ezspGetVersionStruct()); + + if (status !== EzspStatus.SUCCESS) { + // NCP has old style version number + debug(`NCP has old-style version number.`); + this.version = { + ezsp: ncpEzspProtocolVer, + revision: `${ncpStackVer}`, + major: ncpStackVer, + minor: 0, + patch: 0, + special: 0, + build: 0, + type: EmberVersionType.GA,// default... + }; + } else { + // NCP has new style version number + this.version = { + ezsp: ncpEzspProtocolVer, + revision: `${versionStruct.major}.${versionStruct.minor}.${versionStruct.patch} [${EmberVersionType[versionStruct.type]}]`, + ...versionStruct, + }; + + if (versionStruct.type !== EmberVersionType.GA) { + console.warn(`NCP is running a non-GA version (${EmberVersionType[versionStruct.type]}).`); + } + } + + debug(`NCP version info: ${JSON.stringify(this.version)}`); + } + + /** + * This function sets an EZSP config value. + * WARNING: Do not call for values that cannot be set after init without first resetting NCP (like table sizes). + * To avoid an extra NCP call, this does not check for it. + * @param configId + * @param value uint16_t + * @returns + */ + private async emberSetEzspConfigValue(configId: EzspConfigId, value: number): Promise { + const status = (await this.ezsp.ezspSetConfigurationValue(configId, value)); + + debug(`[EzspConfigId] SET "${EzspConfigId[configId]}" TO "${value}" with status=${EzspStatus[status]}.`); + + if (status === EzspStatus.ERROR_INVALID_ID) { + // can be ZLL where not all NCPs need or support it. + console.warn(`[EzspConfigId] Unsupported configuration ID ${EzspConfigId[configId]} by NCP.`); + } else if (status !== EzspStatus.SUCCESS) { + // don't fail in case a set value gets called "out of time" + console.error(`[EzspConfigId] Failed to SET "${EzspConfigId[configId]}" TO "${value}" with status=${EzspStatus[status]}.`); + } + + return status; + } + + /** + * This function sets an EZSP value. + * @param valueId + * @param valueLength uint8_t + * @param value uint8_t * + * @returns + */ + private async emberSetEzspValue(valueId: EzspValueId, valueLength: number, value: number[]): Promise { + const status = (await this.ezsp.ezspSetValue(valueId, valueLength, value)); + + debug(`[EzspValueId] SET "${EzspValueId[valueId]}" TO "${value}" with status=${EzspStatus[status]}.`); + + return status; + } + + /** + * This function sets an EZSP policy. + * @param policyId + * @param decisionId Can be bitop + * @returns + */ + private async emberSetEzspPolicy(policyId: EzspPolicyId, decisionId: number): Promise { + const status = (await this.ezsp.ezspSetPolicy(policyId, decisionId)); + + debug(`[EzspPolicyId] SET "${EzspPolicyId[policyId]}" TO "${decisionId}" with status=${EzspStatus[status]}.`); + + return status; + } + + /** + * Here we convert the normal Ember AES hash call to the specialized EZSP call. + * This came about because we cannot pass a block of data that is + * both input and output into EZSP. The block must be broken up into two + * elements. We unify the two pieces here to make it invisible to the users. + * @param context EmberAesMmoHashContext * + * @param finalize + * @param data uint8_t * Expected of valid length (as in, not larger alloc) + * @returns status + * @returns result context or null + */ + private async aesMmoHash(context: EmberAesMmoHashContext, finalize: boolean, data: Buffer): + Promise<[EmberStatus, reContext: EmberAesMmoHashContext]> { + if (data.length > 255) { + throw new Error(EzspStatus[EzspStatus.ERROR_INVALID_CALL]); + } + + const [status, reContext] = (await this.ezsp.ezspAesMmoHash(context, finalize, data)); + + return [status, reContext]; + } + + /** + * This routine processes the passed chunk of data and updates + * the hash calculation based on it. The data passed in MUST + * have a length that is a multiple of 16. + * + * @param context EmberAesMmoHashContext* A pointer to the location of the hash context to update. + * @param data const uint8_t* A pointer to the location of the data to hash. + * + * @returns An ::EmberStatus value indicating EMBER_SUCCESS if the hash was + * calculated successfully. EMBER_INVALID_CALL if the block size is not a + * multiple of 16 bytes, and EMBER_INDEX_OUT_OF_RANGE is returned when the + * data exceeds the maximum limits of the hash function. + * @returns result context or null + */ + private async emberAesMmoHashUpdate(context: EmberAesMmoHashContext, data: Buffer): Promise<[EmberStatus, reContext: EmberAesMmoHashContext]> { + return this.aesMmoHash(context, false/*finalize?*/, data); + } + + /** + * This routine processes the passed chunk of data (if non-NULL) + * and update the hash context that is passed in. In then performs + * the final calculations on the hash and returns the final answer + * in the result parameter of the ::EmberAesMmoHashContext structure. + * The length of the data passed in may be any value, it does not have + * to be a multiple of 16. + * + * @param context EmberAesMmoHashContext * A pointer to the location of the hash context to finalize. + * @param data uint8_t * A pointer to the location of data to hash. May be NULL. + * + * @returns An ::EmberStatus value indicating EMBER_SUCCESS if the hash was + * calculated successfully. EMBER_INVALID_CALL if the block size is not a + * multiple of 16 bytes, and EMBER_INDEX_OUT_OF_RANGE is returned when the + * data exceeds the maximum limits of the hash function. + * @returns result context or null + */ + private async emberAesMmoHashFinal(context: EmberAesMmoHashContext, data: Buffer): Promise<[EmberStatus, reContext: EmberAesMmoHashContext]> { + return this.aesMmoHash(context, true/*finalize?*/, data); + } + + /** + * This is a convenience method when the hash data is less than 255 + * bytes. It inits, updates, and finalizes the hash in one function call. + * + * @param data const uint8_t* The data to hash. Expected of valid length (as in, not larger alloc) + * + * @returns An ::EmberStatus value indicating EMBER_SUCCESS if the hash was + * calculated successfully. EMBER_INVALID_CALL if the block size is not a + * multiple of 16 bytes, and EMBER_INDEX_OUT_OF_RANGE is returned when the + * data exceeds the maximum limits of the hash function. + * @returns result uint8_t* The location where the result of the hash will be written. + */ + private async emberAesHashSimple(data: Buffer): Promise<[EmberStatus, result: Buffer]> { + const context = aesMmoHashInit(); + + const [status, reContext] = (await this.emberAesMmoHashFinal(context, data)); + + return [status, reContext?.result]; + } + + /** + * Enable local permit join and optionally broadcast the ZDO Mgmt_Permit_Join_req message. + * This API can be called from any device type and still return EMBER_SUCCESS. + * If the API is called from an end device, the permit association bit will just be left off. + * + * @param duration uint8_t The duration that the permit join bit will remain on + * and other devices will be able to join the current network. + * @param broadcastMgmtPermitJoin whether or not to broadcast the ZDO Mgmt_Permit_Join_req message. + * + * @returns status of whether or not permit join was enabled. + * @returns apsFrame Will be null if not broadcasting. + * @returns messageTag The tag passed to ezspSend${x} function. + */ + private async emberPermitJoining(duration: number, broadcastMgmtPermitJoin: boolean) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + let status = (await this.ezsp.ezspPermitJoining(duration)); + let apsFrame: EmberApsFrame = null; + let messageTag: number = null; + + debug(`Permit joining for ${duration} sec. status=${[status]}`); + + if (broadcastMgmtPermitJoin) { + // `authentication`: TC significance always 1 (zb specs) + [status, apsFrame, messageTag] = (await this.emberPermitJoiningRequest(EMBER_BROADCAST_ADDRESS, duration, 1, this.defaultApsOptions)); + } + + return [status, apsFrame, messageTag]; + } + + /** + * Set the trust center policy bitmask using decision. + * @param decision + * @returns + */ + private async emberSetJoinPolicy(decision: EmberJoinDecision): Promise { + let policy: number = EzspDecisionBitmask.DEFAULT_CONFIGURATION; + + if (decision == EmberJoinDecision.USE_PRECONFIGURED_KEY) { + policy = (EzspDecisionBitmask.ALLOW_JOINS | EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS); + } else if (decision == EmberJoinDecision.SEND_KEY_IN_THE_CLEAR) { + policy = (EzspDecisionBitmask.ALLOW_JOINS | EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS | EzspDecisionBitmask.SEND_KEY_IN_CLEAR); + } else if (decision == EmberJoinDecision.ALLOW_REJOINS_ONLY) { + policy = EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS; + } + + return this.emberSetEzspPolicy(EzspPolicyId.TRUST_CENTER_POLICY, policy); + } + + /** + * Get Source Route Overhead + * + * Returns the number of bytes needed in a packet for source routing. + * Since each hop consumes 2 bytes in the packet, this routine calculates the + * total number of bytes needed based on number of hops to reach the destination. + * + * This function is called by the framework to determine the overhead required + * in the network frame for source routing to a particular destination. + * + * @param destination The node id of the destination Ver.: always + * @returns int8u The number of bytes needed for source routing in a packet. + */ + public async emberGetSourceRouteOverhead(destination: EmberNodeId): Promise { + const [status, value] = (await this.ezsp.ezspGetSourceRouteOverhead(destination)); + + if (status === EzspStatus.SUCCESS) { + return value; + } else { + debug(`Failed to get source route overhead (via extended value), status=${EzspStatus[status]}.`); + } + + return 0; + } + + /** + * Return the maximum size of the payload that the Application Support sub-layer will accept for + * the given message type, destination, and APS frame. + * + * The size depends on multiple factors, including the security level in use and additional information + * added to the message to support the various options. + * + * @param type The outgoing message type. + * @param indexOrDestination uint16_t Depending on the message type, this is either the + * EmberNodeId of the destination, an index into the address table, an index + * into the binding table, the multicast identifier, or a broadcast address. + * @param apsFrame EmberApsFrame *The APS frame for the message. + * @return uint8_t The maximum APS payload length for the given message. + */ + private async maximumApsPayloadLength(type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame): Promise { + let destination: EmberNodeId = EMBER_UNKNOWN_NODE_ID; + let max: number = MAXIMUM_APS_PAYLOAD_LENGTH;// uint8_t + + if ((apsFrame.options & EmberApsOption.ENCRYPTION) !== 0) { + max -= APS_ENCRYPTION_OVERHEAD; + } + + if ((apsFrame.options & EmberApsOption.SOURCE_EUI64) !== 0) { + max -= EUI64_SIZE; + } + + if ((apsFrame.options & EmberApsOption.DESTINATION_EUI64) !== 0) { + max -= EUI64_SIZE; + } + + if ((apsFrame.options & EmberApsOption.FRAGMENT) !== 0) { + max -= APS_FRAGMENTATION_OVERHEAD; + } + + switch (type) { + case EmberOutgoingMessageType.DIRECT: + destination = indexOrDestination; + break; + case EmberOutgoingMessageType.VIA_ADDRESS_TABLE: + destination = (await this.ezsp.ezspGetAddressTableRemoteNodeId(indexOrDestination)); + break; + case EmberOutgoingMessageType.VIA_BINDING: + destination = (await this.ezsp.ezspGetBindingRemoteNodeId(indexOrDestination)); + break; + case EmberOutgoingMessageType.MULTICAST: + // APS multicast messages include the two-byte group id and exclude the one-byte destination endpoint, + // for a net loss of an extra byte. + max--; + break; + case EmberOutgoingMessageType.BROADCAST: + break; + default: + break; + } + + max -= (await this.emberGetSourceRouteOverhead(destination)); + + return max; + } + + //---- END EZSP wrappers + + //---- START Ember ZDO + + /** + * ZDO + * Change the default radius for broadcast ZDO requests + * + * @param radius uint8_t The radius to be used for future ZDO request broadcasts. + */ + private setZDORequestRadius(radius: number): void { + this.zdoRequestRadius = radius; + } + + /** + * ZDO + * Retrieve the default radius for broadcast ZDO requests + * + * @return uint8_t The radius to be used for future ZDO request broadcasts. + */ + private getZDORequestRadius(): number { + return this.zdoRequestRadius; + } + + /** + * ZDO + * Get the next device request sequence number. + * + * Requests have sequence numbers so that they can be matched up with the + * responses. To avoid complexities, the library uses numbers with the high + * bit clear and the stack uses numbers with the high bit set. + * + * @return uint8_t The next device request sequence number + */ + private nextZDORequestSequence(): number { + return (this.zdoRequestSequence = ((++this.zdoRequestSequence) & APPLICATION_ZDO_SEQUENCE_MASK)); + } + + /** + * ZDO + * + * @param destination + * @param clusterId uint16_t + * @param options + * @param length uint8_t + * @returns status Indicates success or failure (with reason) of send + * @returns apsFrame The APS Frame resulting of the request being built and sent (`sequence` set from stack-given value). + * @returns messageTag The tag passed to ezspSend${x} function. + */ + private async sendZDORequestBuffer(destination: EmberNodeId, clusterId: number, options: EmberApsOption): + Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + if (this.zdoRequestBuffalo.getPosition() > EZSP_MAX_FRAME_LENGTH) { + return [EmberStatus.MESSAGE_TOO_LONG, null, null]; + } + + const messageTag = this.nextZDORequestSequence(); + + this.zdoRequestBuffalo.setCommandByte(0, messageTag); + + const apsFrame: EmberApsFrame = { + profileId: ZDO_PROFILE_ID, + clusterId: clusterId, + sourceEndpoint: ZDO_ENDPOINT, + destinationEndpoint: ZDO_ENDPOINT, + options: options, + groupId: 0, + sequence: 0,// set by stack + }; + const messageContents = this.zdoRequestBuffalo.getWritten(); + + if (destination === EMBER_BROADCAST_ADDRESS || destination === EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS + || destination === EMBER_SLEEPY_BROADCAST_ADDRESS) { + debug(`~~~> [ZDO BROADCAST apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`); + const [status, apsSequence] = (await this.ezsp.ezspSendBroadcast( + destination, + apsFrame, + this.getZDORequestRadius(), + messageTag, + messageContents, + )); + apsFrame.sequence = apsSequence; + + debug(`~~~> [SENT ZDO type=BROADCAST apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag} status=${EmberStatus[status]}]`); + return [status, apsFrame, messageTag]; + } else { + debug(`~~~> [ZDO UNICAST apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`); + const [status, apsSequence] = (await this.ezsp.ezspSendUnicast( + EmberOutgoingMessageType.DIRECT, + destination, + apsFrame, + messageTag, + messageContents, + )); + apsFrame.sequence = apsSequence; + + debug(`~~~> [SENT ZDO type=DIRECT apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag} status=${EmberStatus[status]}]`); + return [status, apsFrame, messageTag]; + } + } + + /** + * ZDO + * Service Discovery Functions + * Request the specified node to send a list of its endpoints that + * match the specified application profile and, optionally, lists of input + * and/or output clusters. + * @param target The node whose matching endpoints are desired. The request can + * be sent unicast or broadcast ONLY to the "RX-on-when-idle-address" (0xFFFD) + * If sent as a broadcast, any node that has matching endpoints will send a + * response. + * @param profile uint16_t The application profile to match. + * @param inCount uint8_t The number of input clusters. To not match any input + * clusters, set this value to 0. + * @param outCount uint8_t The number of output clusters. To not match any output + * clusters, set this value to 0. + * @param inClusters uint16_t * The list of input clusters. + * @param outClusters uint16_t * The list of output clusters. + * @param options The options to use when sending the unicast request. See + * emberSendUnicast() for a description. This parameter is ignored if the target + * is a broadcast address. + * @returns An EmberStatus value. EMBER_SUCCESS, MESSAGE_TOO_LONG, + * EMBER_NETWORK_DOWN or EMBER_NETWORK_BUSY. + */ + private async emberMatchDescriptorsRequest(target: EmberNodeId, profile: number, inClusters: number[], outClusters: number[], + options: EmberApsOption): Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + // 2 bytes for NWK Address + 2 bytes for Profile Id + 1 byte for in Cluster Count + // + in times 2 for 2 byte Clusters + out Cluster Count + out times 2 for 2 byte Clusters + const length = (ZDO_MESSAGE_OVERHEAD + 2 + 2 + 1 + (inClusters.length * 2) + 1 + (outClusters.length * 2)); + + // sanity check + if (length > EZSP_MAX_FRAME_LENGTH) { + return [EmberStatus.MESSAGE_TOO_LONG, null, null]; + } + + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt16(target); + this.zdoRequestBuffalo.writeUInt16(profile); + this.zdoRequestBuffalo.writeUInt8(inClusters.length); + this.zdoRequestBuffalo.writeListUInt16(inClusters); + this.zdoRequestBuffalo.writeUInt8(outClusters.length); + this.zdoRequestBuffalo.writeListUInt16(outClusters); + + debug(`~~~> [ZDO MATCH DESCRIPTOR target=${target} profile=${profile} inClusters=${inClusters} outClusters=${outClusters}]`); + return this.sendZDORequestBuffer(target, MATCH_DESCRIPTORS_REQUEST, options); + } + + /** + * ZDO + * Device Discovery Functions + * Request the 16 bit network address of a node whose EUI64 is known. + * + * @param target The EUI64 of the node. + * @param reportKids true to request that the target list their children + * in the response. + * @param childStartIndex uint8_t The index of the first child to list in the response. + * Ignored if @c reportKids is false. + * + * @return An ::EmberStatus value. + * - ::EMBER_SUCCESS - The request was transmitted successfully. + * - ::EMBER_NO_BUFFERS - Insufficient message buffers were available to construct the request. + * - ::EMBER_NETWORK_DOWN - The node is not part of a network. + * - ::EMBER_NETWORK_BUSY - Transmission of the request failed. + */ + private async emberNetworkAddressRequest(target: EmberEUI64, reportKids: boolean, childStartIndex: number) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeIeeeAddr(target); + this.zdoRequestBuffalo.writeUInt8(reportKids ? 1 : 0); + this.zdoRequestBuffalo.writeUInt8(childStartIndex); + + debug(`~~~> [ZDO NETWORK ADDRESS target=${target} reportKids=${reportKids} childStartIndex=${childStartIndex}]`); + return this.sendZDORequestBuffer(EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS, NETWORK_ADDRESS_REQUEST, EmberApsOption.SOURCE_EUI64); + } + + /** + * ZDO + * Device Discovery Functions + * @brief Request the EUI64 of a node whose 16 bit network address is known. + * + * @param target uint16_t The network address of the node. + * @param reportKids uint8_t true to request that the target list their children + * in the response. + * @param childStartIndex uint8_t The index of the first child to list in the response. + * Ignored if reportKids is false. + * @param options The options to use when sending the request. See ::emberSendUnicast() for a description. + * + * @return An ::EmberStatus value. + * - ::EMBER_SUCCESS + * - ::EMBER_NO_BUFFERS + * - ::EMBER_NETWORK_DOWN + * - ::EMBER_NETWORK_BUSY + */ + private async emberIeeeAddressRequest(target: EmberNodeId, reportKids: boolean, childStartIndex: number, + options: EmberApsOption): Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt16(target); + this.zdoRequestBuffalo.writeUInt8(reportKids ? 1 : 0); + this.zdoRequestBuffalo.writeUInt8(childStartIndex); + + debug(`~~~> [ZDO IEEE ADDRESS target=${target} reportKids=${reportKids} childStartIndex=${childStartIndex}]`); + return this.sendZDORequestBuffer(target, IEEE_ADDRESS_REQUEST, options); + } + + /** + * ZDO + * @param discoveryNodeId uint16_t + * @param reportKids uint8_t + * @param childStartIndex uint8_t + * @param options + * @param targetNodeIdOfRequest + */ + private async emberIeeeAddressRequestToTarget(discoveryNodeId: EmberNodeId, reportKids: boolean, childStartIndex: number, + options: EmberApsOption, targetNodeIdOfRequest: EmberNodeId): Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt16(discoveryNodeId); + this.zdoRequestBuffalo.writeUInt8(reportKids ? 1 : 0); + this.zdoRequestBuffalo.writeUInt8(childStartIndex); + + debug(`~~~> [ZDO IEEE ADDRESS targetNodeIdOfRequest=${targetNodeIdOfRequest} discoveryNodeId=${discoveryNodeId} ` + + `reportKids=${reportKids} childStartIndex=${childStartIndex}]`); + return this.sendZDORequestBuffer(targetNodeIdOfRequest, IEEE_ADDRESS_REQUEST, options); + } + + /** + * ZDO + * + * @param target uint16_t + * @param clusterId uint16_t + * @param options + * @returns + */ + private async emberSendZigDevRequestTarget(target: EmberNodeId, clusterId: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt16(target); + + return this.sendZDORequestBuffer(target, clusterId, options); + } + + /** + * ZDO + * @brief Request the specified node to send the simple descriptor for + * the specified endpoint. + * The simple descriptor contains information specific + * to a single endpoint. It describes the application profile identifier, + * application device identifier, application device version, application flags, + * application input clusters and application output clusters. It is defined in + * the ZigBee Application Framework Specification. + * + * @param target uint16_t The node of interest. + * @param targetEndpoint uint8_t The endpoint on the target node whose simple + * descriptor is desired. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberSimpleDescriptorRequest(target: EmberNodeId, targetEndpoint: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt16(target); + this.zdoRequestBuffalo.writeUInt8(targetEndpoint); + + debug(`~~~> [ZDO SIMPLE DESCRIPTOR target=${target} targetEndpoint=${targetEndpoint}]`); + return this.sendZDORequestBuffer(target, SIMPLE_DESCRIPTOR_REQUEST, options); + } + + /** + * ZDO + * @brief Send a request to remove a binding entry with the specified + * contents from the specified node. + * + * @param target The node on which the binding will be removed. + * @param source The source EUI64 in the binding entry. + * @param sourceEndpoint The source endpoint in the binding entry. + * @param clusterId The cluster ID in the binding entry. + * @param type The type of binding, either ::UNICAST_BINDING, + * ::MULTICAST_BINDING, or ::UNICAST_MANY_TO_ONE_BINDING. + * ::UNICAST_MANY_TO_ONE_BINDING is an Ember-specific extension + * and should be used only when the target is an Ember device. + * @param destination The destination EUI64 in the binding entry for the + * ::UNICAST_BINDING or ::UNICAST_MANY_TO_ONE_BINDING. + * @param groupAddress The group address for the ::MULTICAST_BINDING. + * @param destinationEndpoint The destination endpoint in the binding entry for + * the ::UNICAST_BINDING or ::UNICAST_MANY_TO_ONE_BINDING. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An ::EmberStatus value. + * - ::EMBER_SUCCESS + * - ::EMBER_NO_BUFFERS + * _ ::EMBER_NETWORK_DOWN + * - ::EMBER_NETWORK_BUSY + * @param target + * @param bindClusterId + * @param source + * @param sourceEndpoint uint8_t + * @param clusterId uint16_t + * @param type uint8_t + * @param destination + * @param groupAddress uint16_t + * @param destinationEndpoint uint8_t + * @param options + */ + private async emberSendZigDevBindRequest(target: EmberNodeId, bindClusterId: number, source: EmberEUI64, sourceEndpoint: number, + clusterId: number, type: number, destination: EmberEUI64, groupAddress: EmberMulticastId, destinationEndpoint: number, + options: EmberApsOption): Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeIeeeAddr(source); + this.zdoRequestBuffalo.writeUInt8(sourceEndpoint); + this.zdoRequestBuffalo.writeUInt16(clusterId); + this.zdoRequestBuffalo.writeUInt8(type); + + switch (type) { + case UNICAST_BINDING: + this.zdoRequestBuffalo.writeIeeeAddr(destination); + this.zdoRequestBuffalo.writeUInt8(destinationEndpoint); + break; + case MULTICAST_BINDING: + this.zdoRequestBuffalo.writeUInt16(groupAddress); + break; + default: + return [EmberStatus.ERR_FATAL, null, null]; + } + + return this.sendZDORequestBuffer(target, bindClusterId, options); + } + + /** + * ZDO + * Send a request to create a binding entry with the specified + * contents on the specified node. + * + * @param target The node on which the binding will be created. + * @param source The source EUI64 in the binding entry. + * @param sourceEndpoint The source endpoint in the binding entry. + * @param clusterId The cluster ID in the binding entry. + * @param type The type of binding, either ::UNICAST_BINDING, + * ::MULTICAST_BINDING, or ::UNICAST_MANY_TO_ONE_BINDING. + * ::UNICAST_MANY_TO_ONE_BINDING is an Ember-specific extension + * and should be used only when the target is an Ember device. + * @param destination The destination EUI64 in the binding entry for + * ::UNICAST_BINDING or ::UNICAST_MANY_TO_ONE_BINDING. + * @param groupAddress The group address for the ::MULTICAST_BINDING. + * @param destinationEndpoint The destination endpoint in the binding entry for + * the ::UNICAST_BINDING or ::UNICAST_MANY_TO_ONE_BINDING. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberBindRequest(target: EmberNodeId, source: EmberEUI64, sourceEndpoint: number, clusterId: number, type: number, + destination: EmberEUI64, groupAddress: EmberMulticastId, destinationEndpoint: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO BIND target=${target} source=${source} sourceEndpoint=${sourceEndpoint} clusterId=${clusterId} type=${type} ` + + `destination=${destination} groupAddress=${groupAddress} destinationEndpoint=${destinationEndpoint}]`); + return this.emberSendZigDevBindRequest( + target, + BIND_REQUEST, + source, + sourceEndpoint, + clusterId, + type, + destination, + groupAddress, + destinationEndpoint, + options + ); + } + + /** + * ZDO + * Send a request to remove a binding entry with the specified + * contents from the specified node. + * + * @param target The node on which the binding will be removed. + * @param source The source EUI64 in the binding entry. + * @param sourceEndpoint uint8_t The source endpoint in the binding entry. + * @param clusterId uint16_t The cluster ID in the binding entry. + * @param type uint8_t The type of binding, either ::UNICAST_BINDING, + * ::MULTICAST_BINDING, or ::UNICAST_MANY_TO_ONE_BINDING. + * ::UNICAST_MANY_TO_ONE_BINDING is an Ember-specific extension + * and should be used only when the target is an Ember device. + * @param destination The destination EUI64 in the binding entry for the + * ::UNICAST_BINDING or ::UNICAST_MANY_TO_ONE_BINDING. + * @param groupAddress The group address for the ::MULTICAST_BINDING. + * @param destinationEndpoint uint8_t The destination endpoint in the binding entry for + * the ::UNICAST_BINDING or ::UNICAST_MANY_TO_ONE_BINDING. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An ::EmberStatus value. + * - ::EMBER_SUCCESS + * - ::EMBER_NO_BUFFERS + * _ ::EMBER_NETWORK_DOWN + * - ::EMBER_NETWORK_BUSY + */ + private async emberUnbindRequest(target: EmberNodeId, source: EmberEUI64, sourceEndpoint: number, clusterId: number, type: number, + destination: EmberEUI64, groupAddress: EmberMulticastId, destinationEndpoint: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO UNBIND target=${target} source=${source} sourceEndpoint=${sourceEndpoint} clusterId=${clusterId} type=${type} ` + + `destination=${destination} groupAddress=${groupAddress} destinationEndpoint=${destinationEndpoint}]`); + return this.emberSendZigDevBindRequest( + target, + UNBIND_REQUEST, + source, + sourceEndpoint, + clusterId, + type, + destination, + groupAddress, + destinationEndpoint, + options + ); + } + + /** + * ZDO + * Request the specified node to send a list of its active + * endpoints. An active endpoint is one for which a simple descriptor is + * available. + * + * @param target The node whose active endpoints are desired. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberActiveEndpointsRequest(target: EmberNodeId, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO ACTIVE ENDPOINTS target=${target}]`); + return this.emberSendZigDevRequestTarget(target, ACTIVE_ENDPOINTS_REQUEST, options); + } + + /** + * ZDO + * Request the specified node to send its power descriptor. + * The power descriptor gives a dynamic indication of the power + * status of the node. It describes current power mode, + * available power sources, current power source and + * current power source level. It is defined in the ZigBee + * Application Framework Specification. + * + * @param target The node whose power descriptor is desired. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberPowerDescriptorRequest(target: EmberNodeId, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO POWER DESCRIPTOR target=${target}]`); + return this.emberSendZigDevRequestTarget(target, POWER_DESCRIPTOR_REQUEST, options); + } + + /** + * ZDO + * Request the specified node to send its node descriptor. + * The node descriptor contains information about the capabilities of the ZigBee + * node. It describes logical type, APS flags, frequency band, MAC capabilities + * flags, manufacturer code and maximum buffer size. It is defined in the ZigBee + * Application Framework Specification. + * + * @param target The node whose node descriptor is desired. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An ::EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberNodeDescriptorRequest(target: EmberNodeId, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO NODE DESCRIPTOR target=${target}]`); + return this.emberSendZigDevRequestTarget(target, NODE_DESCRIPTOR_REQUEST, options); + } + + /** + * ZDO + * Request the specified node to send its LQI (neighbor) table. + * The response gives PAN ID, EUI64, node ID and cost for each neighbor. The + * EUI64 is only available if security is enabled. The other fields in the + * response are set to zero. The response format is defined in the ZigBee Device + * Profile Specification. + * + * @param target The node whose LQI table is desired. + * @param startIndex uint8_t The index of the first neighbor to include in the + * response. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberLqiTableRequest(target: EmberNodeId, startIndex: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO LQI TABLE target=${target} startIndex=${startIndex}]`); + return this.emberTableRequest(LQI_TABLE_REQUEST, target, startIndex, options); + } + + /** + * ZDO + * Request the specified node to send its routing table. + * The response gives destination node ID, status and many-to-one flags, + * and the next hop node ID. + * The response format is defined in the ZigBee Device + * Profile Specification. + * + * @param target The node whose routing table is desired. + * @param startIndex uint8_t The index of the first route entry to include in the + * response. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberRoutingTableRequest(target: EmberNodeId, startIndex: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO ROUTING TABLE target=${target} startIndex=${startIndex}]`); + return this.emberTableRequest(ROUTING_TABLE_REQUEST, target, startIndex, options); + } + + /** + * ZDO + * Request the specified node to send its nonvolatile bindings. + * The response gives source address, source endpoint, cluster ID, destination + * address and destination endpoint for each binding entry. The response format + * is defined in the ZigBee Device Profile Specification. + * Note that bindings that have the Ember-specific ::UNICAST_MANY_TO_ONE_BINDING + * type are reported as having the standard ::UNICAST_BINDING type. + * + * @param target The node whose binding table is desired. + * @param startIndex uint8_t The index of the first binding entry to include in the + * response. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberBindingTableRequest(target: EmberNodeId, startIndex: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + debug(`~~~> [ZDO BINDING TABLE target=${target} startIndex=${startIndex}]`); + return this.emberTableRequest(BINDING_TABLE_REQUEST, target, startIndex, options); + } + + /** + * ZDO + * + * @param clusterId uint16_t + * @param target + * @param startIndex uint8_t + * @param options + * @returns + */ + private async emberTableRequest(clusterId: number, target: EmberNodeId, startIndex: number, options: EmberApsOption) + : Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt8(startIndex); + + return this.sendZDORequestBuffer(target, clusterId, options); + } + + /** + * ZDO + * Request the specified node to remove the specified device from + * the network. The device to be removed must be the node to which the request + * is sent or one of its children. + * + * @param target The node which will remove the device. + * @param deviceAddress All zeros if the target is to remove itself from + * the network or the EUI64 of a child of the target device to remove + * that child. + * @param leaveRequestFlags uint8_t A bitmask of leave options. + * Include ::AND_REJOIN if the target is to rejoin the network immediately after leaving. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberLeaveRequest(target: EmberNodeId, deviceAddress: EmberEUI64, leaveRequestFlags: number, options: EmberApsOption): + Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeIeeeAddr(deviceAddress); + this.zdoRequestBuffalo.writeUInt8(leaveRequestFlags); + + debug(`~~~> [ZDO LEAVE target=${target} deviceAddress=${deviceAddress} leaveRequestFlags=${leaveRequestFlags}]`); + return this.sendZDORequestBuffer(target, LEAVE_REQUEST, options); + } + + /** + * ZDO + * Request the specified node to allow or disallow association. + * + * @param target The node which will allow or disallow association. The request + * can be broadcast by using a broadcast address (0xFFFC/0xFFFD/0xFFFF). No + * response is sent if the request is broadcast. + * @param duration uint8_t A value of 0x00 disables joining. A value of 0xFF enables + * joining. Any other value enables joining for that number of seconds. + * @param authentication uint8_t Controls Trust Center authentication behavior. + * @param options The options to use when sending the request. See + * emberSendUnicast() for a description. This parameter is ignored if the target + * is a broadcast address. + * + * @return An EmberStatus value. ::EMBER_SUCCESS, ::EMBER_NO_BUFFERS, + * ::EMBER_NETWORK_DOWN or ::EMBER_NETWORK_BUSY. + */ + private async emberPermitJoiningRequest(target: EmberNodeId, duration: number, authentication: number, options: EmberApsOption): + Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> { + this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD); + + this.zdoRequestBuffalo.writeUInt8(duration); + this.zdoRequestBuffalo.writeUInt8(authentication); + + debug(`~~~> [ZDO PERMIT JOINING target=${target} duration=${duration} authentication=${authentication}]`); + return this.sendZDORequestBuffer(target, PERMIT_JOINING_REQUEST, options); + } + + //---- END Ember ZDO + + //-- START Adapter implementation + + public static async isValidPath(path: string): Promise { + // For TCP paths we cannot get device information, therefore we cannot validate it. + if (SocketPortUtils.isTcpPath(path)) { + return false; + } + + try { + return SerialPortUtils.is(RealpathSync(path), autoDetectDefinitions); + } catch (error) { + debug(`Failed to determine if path is valid: '${error}'`); + return false; + } + } + + public static async autoDetectPath(): Promise { + const paths = await SerialPortUtils.find(autoDetectDefinitions); + paths.sort((a, b) => (a < b) ? -1 : 1); + return paths.length > 0 ? paths[0] : null; + } + + public async start(): Promise { + console.log(`======== Ember Adapter Starting ========`); + this.initVariables(); + + debug(`Starting EZSP with stack configuration: "${this.stackConfig}".`); + const result = await this.initEzsp(); + + return result; + } + + public async stop(): Promise { + await this.ezsp.stop(); + + this.initVariables(); + console.log(`======== Ember Adapter Stopped ========`); + } + + // queued, non-InterPAN + public async getCoordinator(): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // in all likelihood this will be retrieved from cache + const ieeeAddr = (await this.emberGetEui64()); + + resolve({ + ieeeAddr, + networkAddress: ZIGBEE_COORDINATOR_ADDRESS, + manufacturerID: MANUFACTURER_CODE, + endpoints: FIXED_ENDPOINTS.map((ep) => { + return { + profileID: ep.profileId, + ID: ep.endpoint, + deviceID: ep.deviceId, + inputClusters: ep.inClusterList, + outputClusters: ep.outClusterList, + }; + }), + }); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + public async getCoordinatorVersion(): Promise { + return {type: `EZSP v${this.version.ezsp}`, meta: this.version}; + } + + // queued + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async reset(type: "soft" | "hard"): Promise { + return Promise.reject(new Error("Not supported")); + // NOTE: although this function is legacy atm, a couple of new untested EZSP functions that could also prove useful: + // this.ezsp.ezspTokenFactoryReset(true/*excludeOutgoingFC*/, true/*excludeBootCounter*/); + // this.ezsp.ezspResetNode() + } + + public async supportsBackup(): Promise { + return true; + } + + // queued + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async backup(ieeeAddressesInDatabase: string[]): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + // grab fresh version here, bypass cache + const [netStatus, , netParams] = (await this.ezsp.ezspGetNetworkParameters()); + + if (netStatus !== EmberStatus.SUCCESS) { + console.error(`[BACKUP] Failed to get network parameters.`); + return netStatus; + } + + // update cache + this.networkCache.parameters = netParams; + this.networkCache.eui64 = (await this.ezsp.ezspGetEui64()); + + const [netKeyStatus, netKeyInfo] = (await this.ezsp.ezspGetNetworkKeyInfo()); + + if (netKeyStatus !== SLStatus.OK) { + console.error(`[BACKUP] Failed to get network keys info.`); + return ((netKeyStatus === SLStatus.BUSY) || (netKeyStatus === SLStatus.NOT_READY)) + ? EmberStatus.NETWORK_BUSY : EmberStatus.ERR_FATAL;// allow retry on statuses that should be temporary + } + + if (!netKeyInfo.networkKeySet) { + throw new Error(`[BACKUP] No network key set.`); + } + + let keyList: LinkKeyBackupData[] = []; + + if (STACK_CONFIGS[this.stackConfig].KEY_TABLE_SIZE) { + keyList = (await this.exportLinkKeys()); + } + + // XXX: this only makes sense on stop (if that), not hourly/on start, plus network needs to be at near-standstill @see AN1387 + // const tokensBuf = (await EmberTokensManager.saveTokens( + // this.ezsp, + // Buffer.from(this.networkCache.eui64.substring(2/*0x*/), 'hex').reverse() + // )); + // console.log(tokensBuf.toString('hex')); + + let context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.TC_LINK; + const [tcLinkKey, tclkStatus] = (await this.ezsp.ezspExportKey(context)); + + if (tclkStatus !== SLStatus.OK) { + throw new Error(`[BACKUP] Failed to export TC Link Key with status=${SLStatus[tclkStatus]}.`); + } + + context = initSecurityManagerContext();// make sure it's back to zeroes + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + const [networkKey, nkStatus] = (await this.ezsp.ezspExportKey(context)); + + if (nkStatus !== SLStatus.OK) { + throw new Error(`[BACKUP] Failed to export Network Key with status=${SLStatus[nkStatus]}.`); + } + + const zbChannels = Array.from(Array(EMBER_NUM_802_15_4_CHANNELS), (e, i)=> i + EMBER_MIN_802_15_4_CHANNEL_NUMBER); + + resolve({ + networkOptions: { + panId: netParams.panId,// uint16_t + extendedPanId: Buffer.from(netParams.extendedPanId), + channelList: zbChannels.map((c: number) => ((2 ** c) & netParams.channels) ? c : null).filter((x) => x), + networkKey: networkKey.contents, + networkKeyDistribute: false, + }, + logicalChannel: netParams.radioChannel, + networkKeyInfo: { + sequenceNumber: netKeyInfo.networkKeySequenceNumber, + frameCounter: netKeyInfo.networkKeyFrameCounter, + }, + securityLevel: STACK_CONFIGS[this.stackConfig].SECURITY_LEVEL, + networkUpdateId: netParams.nwkUpdateId, + coordinatorIeeeAddress: Buffer.from(this.networkCache.eui64.substring(2)/*take out 0x*/, 'hex').reverse(), + devices: keyList.map((key) => ({ + networkAddress: null,// not used for restore, no reason to make NCP calls for nothing + ieeeAddress: Buffer.from(key.deviceEui64.substring(2)/*take out 0x*/, 'hex').reverse(), + isDirectChild: false,// not used + linkKey: { + key: key.key.contents, + rxCounter: key.incomingFrameCounter, + txCounter: key.outgoingFrameCounter, + }, + })), + ezsp: { + version: this.version.ezsp, + hashed_tclk: tcLinkKey.contents, + // tokens: tokensBuf.toString('hex'), + // altNetworkKey: altNetworkKey.contents, + } + }); + + return EmberStatus.SUCCESS; + }, + reject, + true,// takes prio + ); + }); + + } + + // queued, non-InterPAN + public async getNetworkParameters(): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // first call will cache for the others, but in all likelihood, it will all be from freshly cached after init + // since Controller caches this also. + const panID = (await this.emberGetPanId()); + const extendedPanID = (await this.emberGetExtendedPanId()); + const channel = (await this.emberGetRadioChannel()); + + resolve({ + panID: panID, + extendedPanID: parseInt(Buffer.from(extendedPanID).toString('hex'), 16), + channel: channel, + }); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued + public async setTransmitPower(value: number): Promise { + if (typeof value !== 'number') { + console.error(`Tried to set transmit power to non-number. Value ${value} of type ${typeof value}.`); + return; + } + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + const status = await this.ezsp.ezspSetRadioPower(value); + + if (status !== EmberStatus.SUCCESS) { + console.error(`Failed to set transmit power to ${value} status=${EmberStatus[status]}.`); + return status; + } + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued + public async addInstallCode(ieeeAddress: string, key: Buffer): Promise { + if (!key) { + throw new Error(`[ADD INSTALL CODE] Failed for "${ieeeAddress}"; no code given.`); + } + + let validInstallCodeSize = false; + + for (const validCodeSize of EMBER_INSTALL_CODE_SIZES) { + if (key.length === validCodeSize) { + validInstallCodeSize = true; + break; + } + } + + if (!validInstallCodeSize) { + throw new Error(`[ADD INSTALL CODE] Failed for "${ieeeAddress}"; invalid code size.`); + } + + // Reverse the bits in a byte + const reverse = (b: number): number => { + return ((b * 0x0802 & 0x22110) | (b * 0x8020 & 0x88440)) * 0x10101 >> 16; + }; + let crc = 0xFFFF;// uint16_t + + // Compute the CRC and verify that it matches. + // The bit reversals, byte swap, and ones' complement are due to differences between halCommonCrc16 and the Smart Energy version. + for (let index = 0; index < (key.length - EMBER_INSTALL_CODE_CRC_SIZE); index++) { + crc = halCommonCrc16(reverse(key[index]), crc); + } + + crc = ~highLowToInt(reverse(lowByte(crc)), reverse(highByte(crc))); + + if (key[key.length - EMBER_INSTALL_CODE_CRC_SIZE] !== lowByte(crc) || key[key.length - EMBER_INSTALL_CODE_CRC_SIZE + 1] !== highByte(crc)) { + throw new Error(`[ADD INSTALL CODE] Failed for "${ieeeAddress}"; invalid code CRC.`); + } + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + // Compute the key from the install code and CRC. + const [aesStatus, keyContents] = (await this.emberAesHashSimple(key)); + + if (aesStatus !== EmberStatus.SUCCESS) { + console.error(`[ADD INSTALL CODE] Failed AES hash for "${ieeeAddress}" with status=${EmberStatus[aesStatus]}.`); + return aesStatus; + } + + // Add the key to the transient key table. + // This will be used while the DUT joins. + const impStatus = (await this.ezsp.ezspImportTransientKey(ieeeAddress, {contents: keyContents}, SecManFlag.NONE)); + + if (impStatus == SLStatus.OK) { + debug(`[ADD INSTALL CODE] Success for "${ieeeAddress}".`); + } else { + console.error(`[ADD INSTALL CODE] Failed for "${ieeeAddress}" with status=${SLStatus[impStatus]}.`); + return EmberStatus.ERR_FATAL; + } + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + /** WARNING: Adapter impl. Starts timer immediately upon returning */ + public waitFor(networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, transactionSequenceNumber: number, + clusterID: number, commandIdentifier: number, timeout: number): {promise: Promise; cancel: () => void;} { + const waiter = this.oneWaitress.waitFor({ + target: networkAddress, + apsFrame: { + clusterId: clusterID, + profileId: HA_PROFILE_ID,// XXX: ok? only used by OTA upstream + sequence: 0,// set by stack + sourceEndpoint: endpoint, + destinationEndpoint: 0, + groupId: 0, + options: EmberApsOption.NONE, + }, + zclSequence: transactionSequenceNumber, + }, timeout || DEFAULT_ZCL_REQUEST_TIMEOUT * 3);// XXX: since this is used by OTA..? + + return { + cancel: (): void => this.oneWaitress.remove(waiter.id), + promise: waiter.start().promise, + }; + } + + //---- ZDO + + // queued, non-InterPAN + public async permitJoin(seconds: number, networkAddress: number): Promise { + const preJoining = async (): Promise => { + if (seconds) { + const plaintextKey: SecManKey = {contents: Buffer.from(ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY)}; + const impKeyStatus = (await this.ezsp.ezspImportTransientKey(BLANK_EUI64, plaintextKey, SecManFlag.NONE)); + + debug(`[ZDO] Pre joining import transient key status=${SLStatus[impKeyStatus]}.`); + return impKeyStatus === SLStatus.OK ? EmberStatus.SUCCESS : EmberStatus.ERR_FATAL; + } else { + await this.ezsp.ezspClearTransientLinkKeys(); + + const setJPstatus = (await this.emberSetJoinPolicy(EmberJoinDecision.ALLOW_REJOINS_ONLY)); + + if (setJPstatus !== EzspStatus.SUCCESS) { + console.error(`[ZDO] Failed set join policy for with status=${EzspStatus[setJPstatus]}.`); + return EmberStatus.ERR_FATAL; + } + + return EmberStatus.SUCCESS; + } + }; + + // NOTE: can't ZDO PJ on coordinator, so if network address is null or zero (coordinator), using local permit join + if (networkAddress) { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + const pjStatus = (await preJoining()); + + if (pjStatus !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed pre joining request for "${networkAddress}" with status=${EmberStatus[pjStatus]}.`); + return pjStatus; + } + + // `authentication`: TC significance always 1 (zb specs) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberPermitJoiningRequest(networkAddress, seconds, 1, 0)); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed permit joining request for "${networkAddress}" with status=${EmberStatus[status]}.`); + return status; + } + + (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: PERMIT_JOINING_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT)); + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } else { + // no device specified to open, open coordinator + broadcast + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + const pjStatus = (await preJoining()); + + if (pjStatus !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed pre joining request for "${networkAddress}" with status=${EmberStatus[pjStatus]}.`); + return pjStatus; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberPermitJoining(seconds, true/*broadcast*/)); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed permit joining request with status=${EmberStatus[status]}.`); + return status; + } + + // NOTE: because Z2M is refreshing the permit join duration early to prevent it from closing + // (every 200sec, even if only opened for 254sec), we can't wait for the stack opened status, + // as it won't trigger again if already opened... so instead we assume it worked + // NOTE2: with EZSP, 255=forever, and 254=max, but since upstream logic uses fixed 254 with interval refresh, + // we can't simply bypass upstream calls if called for "forever" to prevent useless NCP calls (3-4 each time), + // until called with 0 (disable), since we don't know if it was requested for forever or not... + // TLDR: upstream logic change required to allow this + // if (seconds) { + // await this.oneWaitress.startWaitingForEvent( + // {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_OPENED}, + // DEFAULT_ZCL_REQUEST_TIMEOUT, + // '[ZDO] Permit Joining', + // ); + // } else { + // // NOTE: CLOSED stack status is not triggered if the network was not OPENED in the first place, so don't wait for it + // // same kind of problem as described above (upstream always tries to close after start, but EZSP already is) + // } + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + } + + // queued, non-InterPAN + public async lqi(networkAddress: number): Promise { + const neighbors: TsType.LQINeighbor[] = []; + + const request = async (startIndex: number): Promise<[EmberStatus, tableEntries: number, entryCount: number]> => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [reqStatus, apsFrame, messageTag] = (await this.emberLqiTableRequest(networkAddress, startIndex, this.defaultApsOptions)); + + if (reqStatus !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed LQI request for "${networkAddress}" (index "${startIndex}") with status=${EmberStatus[reqStatus]}.`); + return [reqStatus, null, null]; + } + + const result = (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: LQI_TABLE_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT)); + + for (const entry of result.entryList) { + neighbors.push({ + ieeeAddr: entry.eui64, + networkAddress: entry.nodeId, + linkquality: entry.lqi, + relationship: entry.relationship, + depth: entry.depth, + }); + } + + return [EmberStatus.SUCCESS, result.neighborTableEntries, result.entryList.length]; + }; + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + let [status, tableEntries, entryCount] = (await request(0)); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (neighbors.length < size) { + [status, tableEntries, entryCount] = (await request(nextStartIndex)); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + nextStartIndex += entryCount; + } + + resolve({neighbors}); + return status; + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async routingTable(networkAddress: number): Promise { + const table: TsType.RoutingTableEntry[] = []; + + const request = async (startIndex: number): Promise<[EmberStatus, tableEntries: number, entryCount: number]> => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [reqStatus, apsFrame, messageTag] = (await this.emberRoutingTableRequest(networkAddress, startIndex, this.defaultApsOptions)); + + if (reqStatus !== EmberStatus.SUCCESS) { + console.error( + `[ZDO] Failed routing table request for "${networkAddress}" (index "${startIndex}") with status=${EmberStatus[reqStatus]}.` + ); + return [reqStatus, null, null]; + } + + const result = (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: ROUTING_TABLE_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT)); + + for (const entry of result.entryList) { + table.push({ + destinationAddress: entry.destinationAddress, + status: RoutingTableStatus[entry.status],// get str value from enum to satisfy upstream's needs + nextHop: entry.nextHopAddress, + }); + } + + return [EmberStatus.SUCCESS, result.routingTableEntries, result.entryList.length]; + }; + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + let [status, tableEntries, entryCount] = (await request(0)); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (table.length < size) { + [status, tableEntries, entryCount] = (await request(nextStartIndex)); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + nextStartIndex += entryCount; + } + + resolve({table}); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async nodeDescriptor(networkAddress: number): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + /* eslint-disable @typescript-eslint/no-unused-vars */ + const [status, apsFrame, messageTag] = (await this.emberNodeDescriptorRequest(networkAddress, this.defaultApsOptions)); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed node descriptor for "${networkAddress}" with status=${EmberStatus[status]}.`); + return status; + } + + const result = (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: NODE_DESCRIPTOR_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT)); + + let type: TsType.DeviceType = 'Unknown'; + + switch (result.logicalType) { + case 0x0: + type = 'Coordinator'; + break; + case 0x1: + type = 'Router'; + break; + case 0x2: + type = 'EndDevice'; + break; + } + + // always 0 before rev. 21 where field was added + if (result.stackRevision < CURRENT_ZIGBEE_SPEC_REVISION) { + console.warn(`[ZDO] Node descriptor for "${networkAddress}" reports device is only compliant to revision ` + + `"${(result.stackRevision < 21) ? 'pre-21' : result.stackRevision}" of the ZigBee specification ` + + `(current revision: ${CURRENT_ZIGBEE_SPEC_REVISION}).`); + } + + resolve({type, manufacturerCode: result.manufacturerCode}); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async activeEndpoints(networkAddress: number): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberActiveEndpointsRequest(networkAddress, this.defaultApsOptions)); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed active endpoints request for "${networkAddress}" with status=${EmberStatus[status]}.`); + return status; + } + + const result = (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: ACTIVE_ENDPOINTS_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT)); + + resolve({endpoints: result.endpointList}); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberSimpleDescriptorRequest( + networkAddress, + endpointID, + this.defaultApsOptions + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed simple descriptor request for "${networkAddress}" endpoint "${endpointID}" ` + + `with status=${EmberStatus[status]}.`); + return status; + } + + const result = (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: SIMPLE_DESCRIPTOR_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT)); + + resolve({ + profileID: result.profileId, + endpointID: result.endpoint, + deviceID: result.deviceId, + inputClusters: result.inClusterList, + outputClusters: result.outClusterList, + }); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async bind(destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number, clusterID: number, + destinationAddressOrGroup: string | number, type: "endpoint" | "group", destinationEndpoint?: number): Promise { + if (typeof destinationAddressOrGroup === 'string' && type === 'endpoint') { + // dest address is EUI64 (str), so type should always be endpoint (unicast) + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberBindRequest( + destinationNetworkAddress, + sourceIeeeAddress, + sourceEndpoint, + clusterID, + UNICAST_BINDING, + destinationAddressOrGroup, + null,// doesn't matter + destinationEndpoint, + this.defaultApsOptions, + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed bind request for "${destinationNetworkAddress}" destination "${destinationAddressOrGroup}" ` + + `endpoint "${destinationEndpoint}" with status=${EmberStatus[status]}.`); + return status; + } + + await this.oneWaitress.startWaitingFor({ + target: destinationNetworkAddress, + apsFrame, + responseClusterId: BIND_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT); + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } else if (typeof destinationAddressOrGroup === 'number' && type === 'group') { + // dest is group num, so type should always be group (multicast) + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberBindRequest( + destinationNetworkAddress, + sourceIeeeAddress, + sourceEndpoint, + clusterID, + MULTICAST_BINDING, + null,// doesn't matter + destinationAddressOrGroup, + destinationEndpoint,// doesn't matter + this.defaultApsOptions, + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed bind request for "${destinationNetworkAddress}" group "${destinationAddressOrGroup}" ` + + `with status=${EmberStatus[status]}.`); + return status; + } + + await this.oneWaitress.startWaitingFor({ + target: destinationNetworkAddress, + apsFrame, + responseClusterId: BIND_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT); + + resolve(); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + } + + // queued, non-InterPAN + public async unbind(destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number, clusterID: number, + destinationAddressOrGroup: string | number, type: "endpoint" | "group", destinationEndpoint: number): Promise { + if (typeof destinationAddressOrGroup === 'string' && type === 'endpoint') { + // dest address is EUI64 (str), so type should always be endpoint (unicast) + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberUnbindRequest( + destinationNetworkAddress, + sourceIeeeAddress, + sourceEndpoint, + clusterID, + UNICAST_BINDING, + destinationAddressOrGroup, + null,// doesn't matter + destinationEndpoint, + this.defaultApsOptions, + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed unbind request for "${destinationNetworkAddress}" destination "${destinationAddressOrGroup}" ` + + `endpoint "${destinationEndpoint}" with status=${EmberStatus[status]}.`); + return status; + } + + await this.oneWaitress.startWaitingFor({ + target: destinationNetworkAddress, + apsFrame, + responseClusterId: UNBIND_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT); + + resolve(); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } else if (typeof destinationAddressOrGroup === 'number' && type === 'group') { + // dest is group num, so type should always be group (multicast) + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberUnbindRequest( + destinationNetworkAddress, + sourceIeeeAddress, + sourceEndpoint, + clusterID, + MULTICAST_BINDING, + null,// doesn't matter + destinationAddressOrGroup, + destinationEndpoint,// doesn't matter + this.defaultApsOptions, + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed unbind request for "${destinationNetworkAddress}" group "${destinationAddressOrGroup}" ` + + `with status=${EmberStatus[status]}.`); + return status; + } + + await this.oneWaitress.startWaitingFor({ + target: destinationNetworkAddress, + apsFrame, + responseClusterId: UNBIND_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT); + + resolve(); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + } + + // queued, non-InterPAN + public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, apsFrame, messageTag] = (await this.emberLeaveRequest( + networkAddress, + ieeeAddr, + EmberLeaveRequestFlags.WITHOUT_REJOIN, + this.defaultApsOptions + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`[ZDO] Failed remove device request for "${networkAddress}" target "${ieeeAddr}" ` + + `with status=${EmberStatus[status]}.`); + return status; + } + + await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + responseClusterId: LEAVE_RESPONSE, + }, DEFAULT_ZDO_REQUEST_TIMEOUT); + + resolve(); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + //---- ZCL + + // queued, non-InterPAN + public async sendZclFrameToEndpoint(ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number): Promise { + const sourceEndpointInfo = typeof sourceEndpoint === 'number' ? + FIXED_ENDPOINTS.find((epi) => (epi.endpoint === sourceEndpoint)) : FIXED_ENDPOINTS[0]; + const command = zclFrame.getCommand(); + let commandResponseId: number = null; + + if (command.hasOwnProperty('response') && disableResponse === false) { + commandResponseId = command.response; + } else if (!zclFrame.Header.frameControl.disableDefaultResponse) { + commandResponseId = Foundation.defaultRsp.ID; + } + + const apsFrame: EmberApsFrame = { + profileId: sourceEndpointInfo.profileId, + clusterId: zclFrame.Cluster.ID, + sourceEndpoint: sourceEndpointInfo.endpoint, + destinationEndpoint: (typeof endpoint === 'number') ? endpoint : FIXED_ENDPOINTS[0].endpoint, + options: this.defaultApsOptions, + groupId: 0, + sequence: 0,// set by stack + }; + + // don't RETRY if no response expected + if (commandResponseId == null) { + apsFrame.options &= ~EmberApsOption.RETRY; + } + + const data = zclFrame.toBuffer(); + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + if (CHECK_APS_PAYLOAD_LENGTH) { + const maxPayloadLength = ( + await this.maximumApsPayloadLength(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame) + ); + + if (data.length > maxPayloadLength) { + return EmberStatus.MESSAGE_TOO_LONG;// queue will reject + } + } + + // track group changes in NCP multicast table + if (apsFrame.clusterId === Cluster.genGroups.ID) { + await this.onGroupChange(command.ID, zclFrame.Payload.groupid); + } + + debug(`~~~> [ZCL to=${networkAddress} apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.Header)}]`); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, messageTag] = (await this.ezsp.send( + EmberOutgoingMessageType.DIRECT, + networkAddress, + apsFrame, + data, + 0,// alias + 0,// alias seq + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${EmberStatus[status]}.`); + return status;// let queue handle retry based on status + } + + if (commandResponseId != null) { + // NOTE: aps sequence number will have been set by send function + const result = (await this.oneWaitress.startWaitingFor({ + target: networkAddress, + apsFrame, + zclSequence: zclFrame.Header.transactionSequenceNumber, + }, timeout || DEFAULT_ZCL_REQUEST_TIMEOUT)); + + resolve(result); + } else { + resolve(null);// don't expect a response + return EmberStatus.SUCCESS; + } + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame, sourceEndpoint?: number): Promise { + const sourceEndpointInfo = typeof sourceEndpoint === 'number' ? + FIXED_ENDPOINTS.find((epi) => (epi.endpoint === sourceEndpoint)) : FIXED_ENDPOINTS[0]; + const apsFrame: EmberApsFrame = { + profileId: sourceEndpointInfo.profileId, + clusterId: zclFrame.Cluster.ID, + sourceEndpoint: sourceEndpointInfo.endpoint, + destinationEndpoint: FIXED_ENDPOINTS[0].endpoint, + options: this.defaultApsOptions, + groupId: groupID, + sequence: 0,// set by stack + }; + const data = zclFrame.toBuffer(); + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + if (CHECK_APS_PAYLOAD_LENGTH) { + const maxPayloadLength = ( + await this.maximumApsPayloadLength(EmberOutgoingMessageType.MULTICAST, groupID, apsFrame) + ); + + if (data.length > maxPayloadLength) { + return EmberStatus.MESSAGE_TOO_LONG;// queue will reject + } + } + + debug(`~~~> [ZCL GROUP apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.Header)}]`); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, messageTag] = (await this.ezsp.send( + EmberOutgoingMessageType.MULTICAST, + apsFrame.groupId,// not used for MC + apsFrame, + data, + 0,// alias + 0,// alias seq + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`~x~> [ZCL GROUP] Failed to send with status=${EmberStatus[status]}.`); + return status;// let queue handle retry based on status + } + + // NOTE: since ezspMessageSentHandler could take a while here, we don't block, it'll just be logged if the delivery failed + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued, non-InterPAN + public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number): Promise { + const sourceEndpointInfo = typeof sourceEndpoint === 'number' ? + FIXED_ENDPOINTS.find((epi) => (epi.endpoint === sourceEndpoint)) : FIXED_ENDPOINTS[0]; + const apsFrame: EmberApsFrame = { + profileId: sourceEndpointInfo.profileId, + clusterId: zclFrame.Cluster.ID, + sourceEndpoint: sourceEndpointInfo.endpoint, + destinationEndpoint: (typeof endpoint === 'number') ? endpoint : FIXED_ENDPOINTS[0].endpoint, + options: this.defaultApsOptions, + groupId: EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS, + sequence: 0,// set by stack + }; + const data = zclFrame.toBuffer(); + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.checkInterpanLock(); + + if (CHECK_APS_PAYLOAD_LENGTH) { + const maxPayloadLength = ( + await this.maximumApsPayloadLength(EmberOutgoingMessageType.BROADCAST, EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS, apsFrame) + ); + + if (data.length > maxPayloadLength) { + return EmberStatus.MESSAGE_TOO_LONG;// queue will reject + } + } + + debug(`~~~> [ZCL BROADCAST apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.Header)}]`); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [status, messageTag] = (await this.ezsp.send( + EmberOutgoingMessageType.BROADCAST, + EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS, + apsFrame, + data, + 0,// alias + 0,// alias seq + )); + + if (status !== EmberStatus.SUCCESS) { + console.error(`~x~> [ZCL BROADCAST] Failed to send with status=${EmberStatus[status]}.`); + return status;// let queue handle retry based on status + } + + // NOTE: since ezspMessageSentHandler could take a while here, we don't block, it'll just be logged if the delivery failed + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + //---- InterPAN for Touchlink + // XXX: There might be a better way to handle touchlink with ZLL ezsp functions, but I don't have any device to test so, didn't look into it... + // TODO: check all this touchlink/interpan stuff + + // queued + public async setChannelInterPAN(channel: number): Promise { + if (typeof channel !== 'number') { + console.error(`Tried to set channel InterPAN to non-number. Channel ${channel} of type ${typeof channel}.`); + return; + } + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + this.interpanLock = true; + const status = (await this.ezsp.ezspSetLogicalAndRadioChannel(channel)); + + if (status !== EmberStatus.SUCCESS) { + this.interpanLock = false;// XXX: ok? + console.error(`Failed to set InterPAN channel to ${channel} with status=${EmberStatus[status]}.`); + return status; + } + + resolve(); + return status; + }, + reject, + ); + }); + } + + // queued + public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddress: string): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + const msgBuffalo = new EzspBuffalo(Buffer.alloc(MAXIMUM_INTERPAN_LENGTH)); + + // cache-enabled getters + const sourcePanId = (await this.emberGetPanId()); + const sourceEui64 = (await this.emberGetEui64()); + + msgBuffalo.writeUInt16((LONG_DEST_FRAME_CONTROL | MAC_ACK_REQUIRED));// macFrameControl + msgBuffalo.writeUInt8(0);// sequence Skip Sequence number, stack sets the sequence number. + msgBuffalo.writeUInt16(INVALID_PAN_ID);// destPanId + msgBuffalo.writeIeeeAddr(ieeeAddress);// destAddress (longAddress) + msgBuffalo.writeUInt16(sourcePanId);// sourcePanId + msgBuffalo.writeIeeeAddr(sourceEui64);// sourceAddress + msgBuffalo.writeUInt16(STUB_NWK_FRAME_CONTROL);// nwkFrameControl + msgBuffalo.writeUInt8((EmberInterpanMessageType.UNICAST | INTERPAN_APS_FRAME_TYPE));// apsFrameControl + msgBuffalo.writeUInt16(zclFrame.Cluster.ID); + msgBuffalo.writeUInt16(TOUCHLINK_PROFILE_ID); + + debug(`~~~> [ZCL TOUCHLINK to=${ieeeAddress} header=${JSON.stringify(zclFrame.Header)}]`); + const status = (await this.ezsp.ezspSendRawMessage(Buffer.concat([msgBuffalo.getWritten(), zclFrame.toBuffer()]))); + + if (status !== EmberStatus.SUCCESS) { + console.error(`~x~> [ZCL TOUCHLINK to=${ieeeAddress}] Failed to send with status=${EmberStatus[status]}.`); + return status; + } + + // NOTE: can use ezspRawTransmitCompleteHandler if needed here + + resolve(); + return status; + }, + reject, + ); + }); + } + + // queued + public async sendZclFrameInterPANBroadcast(zclFrame: ZclFrame, timeout: number): Promise { + const command = zclFrame.getCommand(); + + if (!command.hasOwnProperty('response')) { + throw new Error(`Command '${command.name}' has no response, cannot wait for response.`); + } + + // just for waitress + const apsFrame: EmberApsFrame = { + profileId: TOUCHLINK_PROFILE_ID, + clusterId: zclFrame.Cluster.ID, + sourceEndpoint: 0, + destinationEndpoint: 0, + options: EmberApsOption.NONE, + groupId: EMBER_SLEEPY_BROADCAST_ADDRESS, + sequence: 0,// set by stack + }; + + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + const msgBuffalo = new EzspBuffalo(Buffer.alloc(MAXIMUM_INTERPAN_LENGTH)); + + // cache-enabled getters + const sourcePanId = (await this.emberGetPanId()); + const sourceEui64 = (await this.emberGetEui64()); + + msgBuffalo.writeUInt16(SHORT_DEST_FRAME_CONTROL);// macFrameControl + msgBuffalo.writeUInt8(0);// sequence Skip Sequence number, stack sets the sequence number. + msgBuffalo.writeUInt16(INVALID_PAN_ID);// destPanId + msgBuffalo.writeUInt16(apsFrame.groupId);// destAddress (longAddress) + msgBuffalo.writeUInt16(sourcePanId);// sourcePanId + msgBuffalo.writeIeeeAddr(sourceEui64);// sourceAddress + msgBuffalo.writeUInt16(STUB_NWK_FRAME_CONTROL);// nwkFrameControl + msgBuffalo.writeUInt8((EmberInterpanMessageType.BROADCAST | INTERPAN_APS_FRAME_TYPE));// apsFrameControl + msgBuffalo.writeUInt16(apsFrame.clusterId); + msgBuffalo.writeUInt16(apsFrame.profileId); + + const data = Buffer.concat([msgBuffalo.getWritten(), zclFrame.toBuffer()]); + + debug(`~~~> [ZCL TOUCHLINK BROADCAST header=${JSON.stringify(zclFrame.Header)}]`); + const status = (await this.ezsp.ezspSendRawMessage(data)); + + if (status !== EmberStatus.SUCCESS) { + console.error(`~x~> [ZCL TOUCHLINK BROADCAST] Failed to send with status=${EmberStatus[status]}.`); + return status; + } + + // NOTE: can use ezspRawTransmitCompleteHandler if needed here + + const result = (await this.oneWaitress.startWaitingFor({ + target: null, + apsFrame: apsFrame, + zclSequence: zclFrame.Header.transactionSequenceNumber, + }, timeout || DEFAULT_ZCL_REQUEST_TIMEOUT * 2));// XXX: touchlink timeout? + + resolve(result); + + return EmberStatus.SUCCESS; + }, + reject, + ); + }); + } + + // queued + public async restoreChannelInterPAN(): Promise { + return new Promise((resolve, reject): void => { + this.requestQueue.enqueue( + async (): Promise => { + const status = (await this.ezsp.ezspSetLogicalAndRadioChannel(this.networkOptions.channelList[0])); + + if (status !== EmberStatus.SUCCESS) { + console.error( + `Failed to restore InterPAN channel to ${this.networkOptions.channelList[0]} with status=${EmberStatus[status]}.` + ); + return status; + } + + // let adapter settle down + await Wait(3000); + + this.interpanLock = false; + + resolve(); + return status; + }, + reject, + ); + }); + } + + //-- END Adapter implementation + + private checkInterpanLock(): void { + if (this.interpanLock) { + console.error(`[INTERPAN MODE] Cannot execute non-InterPAN commands.`); + + // will be caught by request queue and rejected internally. + throw new Error(EzspStatus[EzspStatus.ERROR_INVALID_CALL]); + } + } + +} diff --git a/src/adapter/ember/adapter/endpoints.ts b/src/adapter/ember/adapter/endpoints.ts new file mode 100644 index 0000000000..4e9cc76bb7 --- /dev/null +++ b/src/adapter/ember/adapter/endpoints.ts @@ -0,0 +1,80 @@ +import Cluster from '../../../zcl/definition/cluster'; +import {GP_ENDPOINT, GP_PROFILE_ID, HA_PROFILE_ID} from '../consts'; +import {ClusterId, ProfileId} from '../types'; + + +type FixedEndpointInfo = { + /** Actual Zigbee endpoint number. uint8_t */ + endpoint: number, + /** Profile ID of the device on this endpoint. */ + profileId: ProfileId, + /** Device ID of the device on this endpoint. uint16_t*/ + deviceId: number, + /** Version of the device. uint8_t */ + deviceVersion: number, + /** List of server clusters. */ + inClusterList: ClusterId[], + /** List of client clusters. */ + outClusterList: ClusterId[], + /** Network index for this endpoint. uint8_t */ + networkIndex: number, +}; + + +/** + * List of endpoints to register. + * + * Index 0 is used as default and expected to be the primary network. + */ +export const FIXED_ENDPOINTS: readonly FixedEndpointInfo[] = [ + {// primary network + endpoint: 1, + profileId: HA_PROFILE_ID, + deviceId: 0x65,// ? + deviceVersion: 1, + inClusterList: [ + Cluster.genBasic.ID,// 0x0000,// Basic + Cluster.genIdentify.ID,// 0x0003,// Identify + Cluster.genOnOff.ID,// 0x0006,// On/off + Cluster.genLevelCtrl.ID,// 0x0008,// Level Control + Cluster.genTime.ID,// 0x000A,// Time + Cluster.genOta.ID,// 0x0019,// Over the Air Bootloading + // Cluster.genPowerProfile.ID,// 0x001A,// Power Profile XXX: missing ZCL cluster def in Z2M? + Cluster.lightingColorCtrl.ID,// 0x0300,// Color Control + ], + outClusterList: [ + Cluster.genBasic.ID,// 0x0000,// Basic + Cluster.genIdentify.ID,// 0x0003,// Identify + Cluster.genGroups.ID,// 0x0004,// Groups + Cluster.genScenes.ID,// 0x0005,// Scenes + Cluster.genOnOff.ID,// 0x0006,// On/off + Cluster.genLevelCtrl.ID,// 0x0008,// Level Control + Cluster.genPollCtrl.ID,// 0x0020,// Poll Control + Cluster.lightingColorCtrl.ID,// 0x0300,// Color Control + Cluster.msIlluminanceMeasurement.ID,// 0x0400,// Illuminance Measurement + Cluster.msTemperatureMeasurement.ID,// 0x0402,// Temperature Measurement + Cluster.msRelativeHumidity.ID,// 0x0405,// Relative Humidity Measurement + Cluster.msOccupancySensing.ID,// 0x0406,// Occupancy Sensing + Cluster.ssIasZone.ID,// 0x0500,// IAS Zone + Cluster.seMetering.ID,// 0x0702,// Simple Metering + Cluster.haMeterIdentification.ID,// 0x0B01,// Meter Identification + Cluster.haApplianceStatistics.ID,// 0x0B03,// Appliance Statistics + Cluster.haElectricalMeasurement.ID,// 0x0B04,// Electrical Measurement + Cluster.touchlink.ID,// 0x1000, // touchlink + ], + networkIndex: 0x00, + }, + {// green power + endpoint: GP_ENDPOINT, + profileId: GP_PROFILE_ID, + deviceId: 0x66, + deviceVersion: 1, + inClusterList: [ + Cluster.greenPower.ID,// 0x0021,// Green Power + ], + outClusterList: [ + Cluster.greenPower.ID,// 0x0021,// Green Power + ], + networkIndex: 0x00, + }, +]; diff --git a/src/adapter/ember/adapter/index.ts b/src/adapter/ember/adapter/index.ts new file mode 100644 index 0000000000..581b5bed29 --- /dev/null +++ b/src/adapter/ember/adapter/index.ts @@ -0,0 +1,3 @@ +import {EmberAdapter} from './emberAdapter'; + +export {EmberAdapter}; diff --git a/src/adapter/ember/adapter/oneWaitress.ts b/src/adapter/ember/adapter/oneWaitress.ts new file mode 100644 index 0000000000..f95f2d7ed5 --- /dev/null +++ b/src/adapter/ember/adapter/oneWaitress.ts @@ -0,0 +1,299 @@ +/* istanbul ignore file */ +import equals from 'fast-deep-equal/es6'; +import {ZclDataPayload} from "../../events"; +import {TOUCHLINK_PROFILE_ID} from "../consts"; +import {EmberApsFrame, EmberNodeId} from "../types"; +import {EmberZdoStatus} from "../zdo"; + + +type OneWaitressMatcher = { + /** + * Matches `indexOrDestination` in `ezspMessageSentHandler` or `sender` in `ezspIncomingMessageHandler` + * Except for InterPAN touchlink, it should always be present. + */ + target?: EmberNodeId, + apsFrame: EmberApsFrame, + /** Cluster ID for when the response doesn't match the request. Takes priority over apsFrame.clusterId. */ + responseClusterId?: number, + zclSequence?: number; +}; + +type OneWaitressEventMatcher = { + eventName: string, + /** If supplied, keys/values are expected to match with resolve payload. */ + payload?: {[k: string]: unknown}, +}; + +interface Waiter { + id: number; + matcher: A; + timer?: NodeJS.Timeout; + resolve: (payload: B) => void; + reject: (error: Error) => void; + resolved: boolean; + timedout: boolean; +}; + +/** + * The one waitress to rule them all. Hopefully. + * Careful, she'll burn you if you're late on delivery! + * + * NOTE: `messageTag` is unreliable, so not used... + */ +export class EmberOneWaitress { + private waiters: Map>; + // NOTE: for now, this could be much simpler (array-like), but more complex events might come into play + private eventWaiters: Map>; + private currentId: number; + private currentEventId: number; + + public constructor() { + this.waiters = new Map(); + this.eventWaiters = new Map(); + this.currentId = 0; + this.currentEventId = 0; + } + + /** + * Reject because of failed delivery notified by `ezspMessageSentHandler`. + * NOTE: This checks for APS sequence, which is only valid in `ezspMessageSentHandler`, not `ezspIncomingMessageHandler` (sequence from stack) + * + * @param target + * @param apsFrame + * @returns + */ + public deliveryFailedFor(target: number, apsFrame: EmberApsFrame): boolean { + for (const [index, waiter] of this.waiters.entries()) { + if (waiter.timedout) { + this.waiters.delete(index); + continue; + } + + // no target in touchlink + // in `ezspMessageSentHandler`, the clusterId for ZDO is still the request one, so check against apsFrame, not override + if (((waiter.matcher.apsFrame.profileId === TOUCHLINK_PROFILE_ID) || (target === waiter.matcher.target)) + && (apsFrame.sequence === waiter.matcher.apsFrame.sequence) && (apsFrame.profileId === waiter.matcher.apsFrame.profileId) + && (apsFrame.clusterId === waiter.matcher.apsFrame.clusterId)) { + clearTimeout(waiter.timer); + + waiter.resolved = true; + + this.waiters.delete(index); + waiter.reject(new Error(`Delivery failed for ${JSON.stringify(apsFrame)}`)); + + return true; + } + } + + return false; + } + + /** + * Resolve or reject ZDO response based on given status. + * @param status + * @param sender + * @param apsFrame + * @param payload + * @returns + */ + public resolveZDO(status: EmberZdoStatus, sender: EmberNodeId, apsFrame: EmberApsFrame, payload: unknown): boolean { + for (const [index, waiter] of this.waiters.entries()) { + if (waiter.timedout) { + this.waiters.delete(index); + continue; + } + + // always a sender expected in ZDO, profileId is a bit redundant here, but... + if ((sender === waiter.matcher.target) && (apsFrame.profileId === waiter.matcher.apsFrame.profileId) + && (apsFrame.clusterId === (waiter.matcher.responseClusterId != null ? + waiter.matcher.responseClusterId : waiter.matcher.apsFrame.clusterId))) { + clearTimeout(waiter.timer); + + waiter.resolved = true; + + this.waiters.delete(index); + + if (status === EmberZdoStatus.ZDP_SUCCESS) { + waiter.resolve(payload); + } else if (status === EmberZdoStatus.ZDP_NO_ENTRY) { + // XXX: bypassing fail here since Z2M seems to trigger ZDO remove-type commands without checking current state + // Z2M also fails with ZCL payload NOT_FOUND though. This should be removed once upstream fixes that. + console.log(`[ZDO] Received status ZDP_NO_ENTRY for "${sender}" cluster "${apsFrame.clusterId}". Ignoring.`); + waiter.resolve(payload); + } else { + waiter.reject(new Error(`[ZDO] Failed response by NCP for "${sender}" cluster "${apsFrame.clusterId}" ` + + `with status=${EmberZdoStatus[status]}.`)); + } + + return true; + } + } + + return false; + } + + public resolveZCL(payload: ZclDataPayload): boolean { + for (const [index, waiter] of this.waiters.entries()) { + if (waiter.timedout) { + this.waiters.delete(index); + continue; + } + + // no target in touchlink, also no APS sequence, but use the ZCL one instead + if (((waiter.matcher.apsFrame.profileId === TOUCHLINK_PROFILE_ID) || (payload.address === waiter.matcher.target)) + && (!waiter.matcher.zclSequence || (payload.frame.Header.transactionSequenceNumber === waiter.matcher.zclSequence)) + && (payload.frame.Cluster.ID === waiter.matcher.apsFrame.clusterId) + && (payload.endpoint === waiter.matcher.apsFrame.destinationEndpoint)) { + clearTimeout(waiter.timer); + + waiter.resolved = true; + + this.waiters.delete(index); + waiter.resolve(payload); + + return true; + } + } + + return false; + } + + public waitFor(matcher: OneWaitressMatcher, timeout: number): {id: number; start: () => {promise: Promise; id: number}} { + const id = this.currentId++; + this.currentId &= 0xFFFF;// roll-over every so often - 65535 should be enough not to create conflicts ;-) + + const promise: Promise = new Promise((resolve, reject): void => { + const object: Waiter = {matcher, resolve, reject, timedout: false, resolved: false, id}; + + this.waiters.set(id, object); + }); + + const start = (): {promise: Promise; id: number} => { + const waiter = this.waiters.get(id); + + if (waiter && !waiter.resolved && !waiter.timer) { + // Capture the stack trace from the caller of start() + const error = new Error(); + Error.captureStackTrace(error); + + waiter.timer = setTimeout((): void => { + error.message = `${JSON.stringify(matcher)} timed out after ${timeout}ms`; + waiter.timedout = true; + + waiter.reject(error); + }, timeout); + } + + return {promise, id}; + }; + + return {id, start}; + } + + /** + * Shortcut that starts the timer immediately and returns the promise. + * No access to `id`, so no easy cancel. + * @param matcher + * @param timeout + * @returns + */ + public startWaitingFor(matcher: OneWaitressMatcher, timeout: number): Promise { + return this.waitFor(matcher, timeout).start().promise; + } + + public remove(id: number): void { + const waiter = this.waiters.get(id); + + if (waiter) { + if (!waiter.timedout && waiter.timer) { + clearTimeout(waiter.timer); + } + + this.waiters.delete(id); + } + } + + /** + * Matches event name with matcher's, and payload (if any in matcher) using `fast-deep-equal/es6` (all keys & values must match) + * @param eventName + * @param payload + * @returns + */ + public resolveEvent(eventName: string, payload?: {[k: string]: unknown}): boolean { + for (const [index, waiter] of this.eventWaiters.entries()) { + if (waiter.timedout) { + this.eventWaiters.delete(index); + continue; + } + + if (eventName === waiter.matcher.eventName && (!waiter.matcher.payload || (equals(payload, waiter.matcher.payload)))) { + clearTimeout(waiter.timer); + + waiter.resolved = true; + + this.eventWaiters.delete(index); + waiter.resolve(payload); + + return true; + } + } + } + + public waitForEvent(matcher: OneWaitressEventMatcher, timeout: number, reason: string = null) + : {id: number; start: () => {promise: Promise; id: number}} { + // NOTE: logic is very much the same as `waitFor`, just different matcher + const id = this.currentEventId++; + this.currentEventId &= 0xFFFF;// roll-over every so often - 65535 should be enough not to create conflicts ;-) + + const promise: Promise = new Promise((resolve, reject): void => { + const object: Waiter = {matcher, resolve, reject, timedout: false, resolved: false, id}; + + this.eventWaiters.set(id, object); + }); + + const start = (): {promise: Promise; id: number} => { + const waiter = this.eventWaiters.get(id); + + if (waiter && !waiter.resolved && !waiter.timer) { + // Capture the stack trace from the caller of start() + const error = new Error(); + Error.captureStackTrace(error); + + waiter.timer = setTimeout((): void => { + error.message = `${reason ? reason : JSON.stringify(matcher)} timed out after ${timeout}ms`; + waiter.timedout = true; + + waiter.reject(error); + }, timeout); + } + + return {promise, id}; + }; + + return {id, start}; + } + + /** + * Shortcut that starts the timer immediately and returns the promise. + * No access to `id`, so no easy cancel. + * @param matcher + * @param timeout + * @param reason If supplied, will be used as timeout label, otherwise stringified matcher is. + * @returns + */ + public startWaitingForEvent(matcher: OneWaitressEventMatcher, timeout: number, reason: string = null): Promise { + return this.waitForEvent(matcher, timeout, reason).start().promise; + } + + public removeEvent(id: number): void { + const waiter = this.eventWaiters.get(id); + + if (waiter) { + if (!waiter.timedout && waiter.timer) { + clearTimeout(waiter.timer); + } + + this.eventWaiters.delete(id); + } + } +} diff --git a/src/adapter/ember/adapter/requestQueue.ts b/src/adapter/ember/adapter/requestQueue.ts new file mode 100644 index 0000000000..860f1430a6 --- /dev/null +++ b/src/adapter/ember/adapter/requestQueue.ts @@ -0,0 +1,160 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import {EmberStatus, EzspStatus} from "../enums"; + +const debug = Debug('zigbee-herdsman:adapter:ember:queue'); + +interface EmberRequestQueueEntry { + /** + * Times tried to successfully send the call. + * This has no maximum, but since it is only for temporary issues, it will either succeed after a couple of tries, or hard fail. + */ + sendAttempts: number; + /** The function the entry is supposed to execute. */ + func: () => Promise; + /** The wrapping promise's reject to reject if necessary. */ + reject: (reason: Error) => void; +}; + +export const NETWORK_BUSY_DEFER_MSEC = 500; +export const NETWORK_DOWN_DEFER_MSEC = 1500; + +export class EmberRequestQueue { + private readonly dispatchInterval: number; + /** Interval handler that manages `dispatch()` */ + private dispatchHandler: NodeJS.Timeout; + /** If true, the queue is currently busy dispatching. */ + private dispatching: boolean; + /** The queue holding requests to be sent. */ + private queue: EmberRequestQueueEntry[]; + /** Queue with requests that should take priority over the above queue. */ + private priorityQueue: EmberRequestQueueEntry[]; + + constructor(dispatchInterval: number) { + this.dispatchInterval = dispatchInterval || 60; + this.dispatching = false; + this.queue = []; + this.priorityQueue = []; + } + + /** + * Empty each queue. + */ + public clear(): void { + this.queue = []; + this.priorityQueue = []; + } + + /** + * Prevent sending requests (usually due to NCP being reset). + */ + public stopDispatching(): void { + clearInterval(this.dispatchHandler); + + debug(`Dispatching stopped; queue=${this.queue.length} priorityQueue=${this.priorityQueue.length}`); + } + + /** + * Allow sending requests. + * Must be called after init. + */ + public startDispatching(): void { + this.dispatchHandler = setInterval(this.dispatch.bind(this), this.dispatchInterval); + + debug(`Dispatching started.`); + } + + /** + * Store a function in the queue to be resolved when appropriate. + * @param function The function to enqueue. Upon dispatch: + * - if its return value is one of MAX_MESSAGE_LIMIT_REACHED, NETWORK_BUSY, NETWORK_DOWN, + * queue will defer dispatching and keep the function in the queue; reject otherwise. + * - if it throws, it is expected to throw `EzspStatus`, and will act same as above if one of NOT_CONNECTED, NO_TX_SPACE; reject otherwise. + * - any other value will result in the function being removed from the queue. + * @param reject The `reject` of the Promise wrapping the `enqueue` call + * (`resolve` is done in `func` directly to have typing on results & control on exec). + * @param prioritize If true, function will be enqueued in the priority queue. Defaults to false. + * @returns new length of the queue. + */ + public enqueue(func: () => Promise, reject: (reason: Error) => void, prioritize: boolean = false): number { + debug(`Status queue=${this.queue.length} priorityQueue=${this.priorityQueue.length}.`); + return (prioritize ? this.priorityQueue : this.queue).push({ + sendAttempts: 0, + func, + reject, + }); + } + + /** + * Dispatch the head of the queue. + * + * If request `func` throws, catch error and reject the request. `ezsp${x}` functions throw `EzspStatus` as error. + * + * If request `func` resolves but has an error, look at what error, and determine if should retry or remove the request from queue. + * + * If request `func` resolves without error, remove request from queue. + * + * WARNING: Because of this logic for "internal retries", any error thrown by `func` will not immediatedly bubble back to Adapter/Controller + */ + public async dispatch(): Promise { + if (this.dispatching) { + return; + } + + let fromPriorityQueue = true; + let entry = this.priorityQueue[0];// head of queue if any, priority first + + if (!entry) { + fromPriorityQueue = false; + entry = this.queue[0]; + } + + if (entry) { + this.dispatching = true; + entry.sendAttempts++; + + // NOTE: refer to `enqueue()` comment to keep logic in sync with expectations, adjust comment on change. + try { + const status: EmberStatus = (await entry.func()); + + if ((status === EmberStatus.MAX_MESSAGE_LIMIT_REACHED) || (status === EmberStatus.NETWORK_BUSY)) { + debug(`Dispatching deferred: NCP busy.`); + this.defer(NETWORK_BUSY_DEFER_MSEC); + } else if (status === EmberStatus.NETWORK_DOWN) { + debug(`Dispatching deferred: Network not ready`); + this.defer(NETWORK_DOWN_DEFER_MSEC); + } else { + // success + (fromPriorityQueue ? this.priorityQueue : this.queue).shift(); + + if (status !== EmberStatus.SUCCESS) { + entry.reject(new Error(EmberStatus[status])); + } + } + } catch (err) {// message is EzspStatus string from ezsp${x} commands, except for stuff rejected by OneWaitress, but that's never "retry" + if (err.message === EzspStatus[EzspStatus.NO_TX_SPACE]) { + debug(`Dispatching deferred: Host busy.`); + this.defer(NETWORK_BUSY_DEFER_MSEC); + } else if (err.message === EzspStatus[EzspStatus.NOT_CONNECTED]) { + debug(`Dispatching deferred: Network not ready`); + this.defer(NETWORK_DOWN_DEFER_MSEC); + } else { + (fromPriorityQueue ? this.priorityQueue : this.queue).shift(); + entry.reject(err); + } + } finally { + this.dispatching = false; + } + } + } + + /** + * Defer dispatching for the specified duration (in msec). + * @param msec + */ + public defer(msec: number): void { + this.stopDispatching(); + + setTimeout(this.startDispatching.bind(this), msec); + } +} diff --git a/src/adapter/ember/adapter/tokensManager.ts b/src/adapter/ember/adapter/tokensManager.ts new file mode 100644 index 0000000000..ed6fbe6ba5 --- /dev/null +++ b/src/adapter/ember/adapter/tokensManager.ts @@ -0,0 +1,780 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import {initSecurityManagerContext} from "../utils/initters"; +import {BLANK_EUI64} from "../consts"; +import {EMBER_ENCRYPTION_KEY_SIZE, EUI64_SIZE} from "../ezsp/consts"; +import {EmberStatus, EzspStatus, SLStatus, SecManFlag, SecManKeyType} from "../enums"; +import {EzspValueId} from "../ezsp/enums"; +import {EmberTokenData, SecManKey} from "../types"; +import {Ezsp} from "../ezsp/ezsp"; + +const debug = Debug('zigbee-herdsman:adapter:ember:adapter:tokens'); + +/* eslint-disable @typescript-eslint/no-unused-vars */ +//------------------------------------------------------------------------------ +// Definitions for stack tokens. +// protocol\zigbee\stack\config\token-stack.h + +/** + * Creator Codes + * + * The CREATOR is used as a distinct identifier tag for the token. + * + * The CREATOR is necessary because the token name is defined differently depending on the hardware platform. + * Therefore, the CREATOR ensures that token definitions and data stay tagged and known. + * The only requirement is that each creator definition must be unique. + * See hal/micro/token.h for a more complete explanation. + * + */ +// STACK CREATORS +const CREATOR_STACK_NVDATA_VERSION = 0xFF01; +const CREATOR_STACK_BOOT_COUNTER = 0xE263; +const CREATOR_STACK_NONCE_COUNTER = 0xE563; +const CREATOR_STACK_ANALYSIS_REBOOT = 0xE162; +const CREATOR_STACK_KEYS = 0xEB79; +const CREATOR_STACK_NODE_DATA = 0xEE64; +const CREATOR_STACK_CLASSIC_DATA = 0xE364; +const CREATOR_STACK_ALTERNATE_KEY = 0xE475; +const CREATOR_STACK_APS_FRAME_COUNTER = 0xE123; +const CREATOR_STACK_TRUST_CENTER = 0xE124; +const CREATOR_STACK_NETWORK_MANAGEMENT = 0xE125; +const CREATOR_STACK_PARENT_INFO = 0xE126; +const CREATOR_STACK_PARENT_ADDITIONAL_INFO = 0xE127; +const CREATOR_STACK_MULTI_PHY_NWK_INFO = 0xE128; +const CREATOR_STACK_MIN_RECEIVED_RSSI = 0xE129; +// Restored EUI64 +const CREATOR_STACK_RESTORED_EUI64 = 0xE12A; + +// MULTI-NETWORK STACK CREATORS +const CREATOR_MULTI_NETWORK_STACK_KEYS = 0xE210; +const CREATOR_MULTI_NETWORK_STACK_NODE_DATA = 0xE211; +const CREATOR_MULTI_NETWORK_STACK_ALTERNATE_KEY = 0xE212; +const CREATOR_MULTI_NETWORK_STACK_TRUST_CENTER = 0xE213; +const CREATOR_MULTI_NETWORK_STACK_NETWORK_MANAGEMENT = 0xE214; +const CREATOR_MULTI_NETWORK_STACK_PARENT_INFO = 0xE215; + +// A temporary solution for multi-network nwk counters: +// This counter will be used on the network with index 1. +const CREATOR_MULTI_NETWORK_STACK_NONCE_COUNTER = 0xE220; +const CREATOR_MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO = 0xE221; + +// GP stack tokens. +const CREATOR_STACK_GP_DATA = 0xE258; +const CREATOR_STACK_GP_PROXY_TABLE = 0xE259; +const CREATOR_STACK_GP_SINK_TABLE = 0xE25A; +const CREATOR_STACK_GP_INCOMING_FC = 0xE25B; +const CREATOR_STACK_GP_INCOMING_FC_IN_SINK = 0xE25C; +// APP CREATORS +const CREATOR_STACK_BINDING_TABLE = 0xE274; +const CREATOR_STACK_CHILD_TABLE = 0xFF0D; +const CREATOR_STACK_KEY_TABLE = 0xE456; +const CREATOR_STACK_CERTIFICATE_TABLE = 0xE500; +const CREATOR_STACK_ZLL_DATA = 0xE501; +const CREATOR_STACK_ZLL_SECURITY = 0xE502; +const CREATOR_STACK_ADDITIONAL_CHILD_DATA = 0xE503; + + +/** + * NVM3 Object Keys + * + * The NVM3 object key is used as a distinct identifier tag for a token stored in NVM3. + * + * Every token must have a defined NVM3 object key and the object key must be unique. + * The object key defined must be in the following format: + * + * NVM3KEY_tokenname where tokenname is the name of the token without NVM3KEY_ or TOKEN_ prefix. + * + */ +// NVM3KEY domain base keys +const NVM3KEY_DOMAIN_USER = 0x00000; +const NVM3KEY_DOMAIN_ZIGBEE = 0x10000; +const NVM3KEY_DOMAIN_COMMON = 0x80000; + +// STACK KEYS +const NVM3KEY_STACK_NVDATA_VERSION = (NVM3KEY_DOMAIN_ZIGBEE | 0xFF01); +const NVM3KEY_STACK_BOOT_COUNTER = (NVM3KEY_DOMAIN_ZIGBEE | 0xE263); +const NVM3KEY_STACK_NONCE_COUNTER = (NVM3KEY_DOMAIN_ZIGBEE | 0xE563); +const NVM3KEY_STACK_ANALYSIS_REBOOT = (NVM3KEY_DOMAIN_ZIGBEE | 0xE162); +const NVM3KEY_STACK_KEYS = (NVM3KEY_DOMAIN_ZIGBEE | 0xEB79); +const NVM3KEY_STACK_NODE_DATA = (NVM3KEY_DOMAIN_ZIGBEE | 0xEE64); +const NVM3KEY_STACK_CLASSIC_DATA = (NVM3KEY_DOMAIN_ZIGBEE | 0xE364); +const NVM3KEY_STACK_ALTERNATE_KEY = (NVM3KEY_DOMAIN_ZIGBEE | 0xE475); +const NVM3KEY_STACK_APS_FRAME_COUNTER = (NVM3KEY_DOMAIN_ZIGBEE | 0xE123); +const NVM3KEY_STACK_TRUST_CENTER = (NVM3KEY_DOMAIN_ZIGBEE | 0xE124); +const NVM3KEY_STACK_NETWORK_MANAGEMENT = (NVM3KEY_DOMAIN_ZIGBEE | 0xE125); +const NVM3KEY_STACK_PARENT_INFO = (NVM3KEY_DOMAIN_ZIGBEE | 0xE126); +const NVM3KEY_STACK_PARENT_ADDITIONAL_INFO = (NVM3KEY_DOMAIN_ZIGBEE | 0xE127); +const NVM3KEY_STACK_MULTI_PHY_NWK_INFO = (NVM3KEY_DOMAIN_ZIGBEE | 0xE128); +const NVM3KEY_STACK_MIN_RECEIVED_RSSI = (NVM3KEY_DOMAIN_ZIGBEE | 0xE129); +// Restored EUI64 +const NVM3KEY_STACK_RESTORED_EUI64 = (NVM3KEY_DOMAIN_ZIGBEE | 0xE12A); + +// MULTI-NETWORK STACK KEYS +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_MULTI_NETWORK_STACK_KEYS = (NVM3KEY_DOMAIN_ZIGBEE | 0x0000); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_MULTI_NETWORK_STACK_NODE_DATA = (NVM3KEY_DOMAIN_ZIGBEE | 0x0080); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_MULTI_NETWORK_STACK_ALTERNATE_KEY = (NVM3KEY_DOMAIN_ZIGBEE | 0x0100); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_MULTI_NETWORK_STACK_TRUST_CENTER = (NVM3KEY_DOMAIN_ZIGBEE | 0x0180); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_MULTI_NETWORK_STACK_NETWORK_MANAGEMENT = (NVM3KEY_DOMAIN_ZIGBEE | 0x0200); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_MULTI_NETWORK_STACK_PARENT_INFO = (NVM3KEY_DOMAIN_ZIGBEE | 0x0280); + +// Temporary solution for multi-network nwk counters: +// This counter will be used on the network with index 1. +const NVM3KEY_MULTI_NETWORK_STACK_NONCE_COUNTER = (NVM3KEY_DOMAIN_ZIGBEE | 0xE220); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved +const NVM3KEY_MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO = (NVM3KEY_DOMAIN_ZIGBEE | 0x0300); + +// GP stack tokens. +const NVM3KEY_STACK_GP_DATA = (NVM3KEY_DOMAIN_ZIGBEE | 0xE258); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_GP_PROXY_TABLE = (NVM3KEY_DOMAIN_ZIGBEE | 0x0380); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_GP_SINK_TABLE = (NVM3KEY_DOMAIN_ZIGBEE | 0x0400); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved +const NVM3KEY_STACK_GP_INCOMING_FC = (NVM3KEY_DOMAIN_ZIGBEE | 0x0480); + +// APP KEYS +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_BINDING_TABLE = (NVM3KEY_DOMAIN_ZIGBEE | 0x0500); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_CHILD_TABLE = (NVM3KEY_DOMAIN_ZIGBEE | 0x0580); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_KEY_TABLE = (NVM3KEY_DOMAIN_ZIGBEE | 0x0600); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_CERTIFICATE_TABLE = (NVM3KEY_DOMAIN_ZIGBEE | 0x0680); +const NVM3KEY_STACK_ZLL_DATA = (NVM3KEY_DOMAIN_ZIGBEE | 0xE501); +const NVM3KEY_STACK_ZLL_SECURITY = (NVM3KEY_DOMAIN_ZIGBEE | 0xE502); +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved. +const NVM3KEY_STACK_ADDITIONAL_CHILD_DATA = (NVM3KEY_DOMAIN_ZIGBEE | 0x0700); + +// This key is used for an indexed token and the subsequent 0x7F keys are also reserved +const NVM3KEY_STACK_GP_INCOMING_FC_IN_SINK = (NVM3KEY_DOMAIN_ZIGBEE | 0x0780); + +// XXX: comment out in prod, along with debug token prints +// const DEBUG_TOKEN_STRINGS = { +// [NVM3KEY_STACK_NVDATA_VERSION]: 'NVM3KEY_STACK_NVDATA_VERSION', +// [NVM3KEY_STACK_BOOT_COUNTER]: 'NVM3KEY_STACK_BOOT_COUNTER', +// [NVM3KEY_STACK_NONCE_COUNTER]: 'NVM3KEY_STACK_NONCE_COUNTER', +// [NVM3KEY_STACK_ANALYSIS_REBOOT]: 'NVM3KEY_STACK_ANALYSIS_REBOOT', +// [NVM3KEY_STACK_KEYS]: 'NVM3KEY_STACK_KEYS', +// [NVM3KEY_STACK_NODE_DATA]: 'NVM3KEY_STACK_NODE_DATA', +// [NVM3KEY_STACK_CLASSIC_DATA]: 'NVM3KEY_STACK_CLASSIC_DATA', +// [NVM3KEY_STACK_ALTERNATE_KEY]: 'NVM3KEY_STACK_ALTERNATE_KEY', +// [NVM3KEY_STACK_APS_FRAME_COUNTER]: 'NVM3KEY_STACK_APS_FRAME_COUNTER', +// [NVM3KEY_STACK_TRUST_CENTER]: 'NVM3KEY_STACK_TRUST_CENTER', +// [NVM3KEY_STACK_NETWORK_MANAGEMENT]: 'NVM3KEY_STACK_NETWORK_MANAGEMENT', +// [NVM3KEY_STACK_PARENT_INFO]: 'NVM3KEY_STACK_PARENT_INFO', +// [NVM3KEY_STACK_PARENT_ADDITIONAL_INFO]: 'NVM3KEY_STACK_PARENT_ADDITIONAL_INFO', +// [NVM3KEY_STACK_MULTI_PHY_NWK_INFO]: 'NVM3KEY_STACK_MULTI_PHY_NWK_INFO', +// [NVM3KEY_STACK_MIN_RECEIVED_RSSI]: 'NVM3KEY_STACK_MIN_RECEIVED_RSSI', +// [NVM3KEY_STACK_RESTORED_EUI64]: 'NVM3KEY_STACK_RESTORED_EUI64', +// [NVM3KEY_MULTI_NETWORK_STACK_KEYS]: 'NVM3KEY_MULTI_NETWORK_STACK_KEYS', +// [NVM3KEY_MULTI_NETWORK_STACK_NODE_DATA]: 'NVM3KEY_MULTI_NETWORK_STACK_NODE_DATA', +// [NVM3KEY_MULTI_NETWORK_STACK_ALTERNATE_KEY]: 'NVM3KEY_MULTI_NETWORK_STACK_ALTERNATE_KEY', +// [NVM3KEY_MULTI_NETWORK_STACK_TRUST_CENTER]: 'NVM3KEY_MULTI_NETWORK_STACK_TRUST_CENTER', +// [NVM3KEY_MULTI_NETWORK_STACK_NETWORK_MANAGEMENT]: 'NVM3KEY_MULTI_NETWORK_STACK_NETWORK_MANAGEMENT', +// [NVM3KEY_MULTI_NETWORK_STACK_PARENT_INFO]: 'NVM3KEY_MULTI_NETWORK_STACK_PARENT_INFO', +// [NVM3KEY_MULTI_NETWORK_STACK_NONCE_COUNTER]: 'NVM3KEY_MULTI_NETWORK_STACK_NONCE_COUNTER', +// [NVM3KEY_MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO]: 'NVM3KEY_MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO', +// [NVM3KEY_STACK_GP_DATA]: 'NVM3KEY_STACK_GP_DATA', +// [NVM3KEY_STACK_GP_PROXY_TABLE]: 'NVM3KEY_STACK_GP_PROXY_TABLE', +// [NVM3KEY_STACK_GP_SINK_TABLE]: 'NVM3KEY_STACK_GP_SINK_TABLE', +// [NVM3KEY_STACK_GP_INCOMING_FC]: 'NVM3KEY_STACK_GP_INCOMING_FC', +// [NVM3KEY_STACK_BINDING_TABLE]: 'NVM3KEY_STACK_BINDING_TABLE', +// [NVM3KEY_STACK_CHILD_TABLE]: 'NVM3KEY_STACK_CHILD_TABLE', +// [NVM3KEY_STACK_KEY_TABLE]: 'NVM3KEY_STACK_KEY_TABLE', +// [NVM3KEY_STACK_CERTIFICATE_TABLE]: 'NVM3KEY_STACK_CERTIFICATE_TABLE', +// [NVM3KEY_STACK_ZLL_DATA]: 'NVM3KEY_STACK_ZLL_DATA', +// [NVM3KEY_STACK_ZLL_SECURITY]: 'NVM3KEY_STACK_ZLL_SECURITY', +// [NVM3KEY_STACK_ADDITIONAL_CHILD_DATA]: 'NVM3KEY_STACK_ADDITIONAL_CHILD_DATA', +// [NVM3KEY_STACK_GP_INCOMING_FC_IN_SINK]: 'NVM3KEY_STACK_GP_INCOMING_FC_IN_SINK', +// }; + +/** + * The current version number of the stack tokens. + * MSB is the version, LSB is a complement. + * + * See hal/micro/token.h for a more complete explanation. + */ +const CURRENT_STACK_TOKEN_VERSION = 0x03FC; + +/** 8-byte IEEE + 16-byte Key + 1-byte info */ +const KEY_TABLE_ENTRY_SIZE = 25; +const KEY_ENTRY_IEEE_OFFSET = 0; +/** first 4 bytes may point to PSA ID if data[KEY_ENTRY_INFO_OFFSET] & KEY_TABLE_ENTRY_HAS_PSA_ID */ +const KEY_ENTRY_KEY_DATA_OFFSET = 8; +const KEY_ENTRY_INFO_OFFSET = 24; +/* eslint-enable @typescript-eslint/no-unused-vars */ + +/** uint16_t */ +const CREATORS: number[] = [ + CREATOR_STACK_NVDATA_VERSION, + CREATOR_STACK_BOOT_COUNTER, + CREATOR_STACK_NONCE_COUNTER, + CREATOR_STACK_ANALYSIS_REBOOT, + CREATOR_STACK_KEYS, + CREATOR_STACK_NODE_DATA, + CREATOR_STACK_CLASSIC_DATA, + CREATOR_STACK_ALTERNATE_KEY, + CREATOR_STACK_APS_FRAME_COUNTER, + CREATOR_STACK_TRUST_CENTER, + CREATOR_STACK_NETWORK_MANAGEMENT, + CREATOR_STACK_PARENT_INFO, + CREATOR_STACK_PARENT_ADDITIONAL_INFO, + CREATOR_STACK_MULTI_PHY_NWK_INFO, + CREATOR_STACK_MIN_RECEIVED_RSSI, + CREATOR_STACK_RESTORED_EUI64, + CREATOR_MULTI_NETWORK_STACK_KEYS, + CREATOR_MULTI_NETWORK_STACK_NODE_DATA, + CREATOR_MULTI_NETWORK_STACK_ALTERNATE_KEY, + CREATOR_MULTI_NETWORK_STACK_TRUST_CENTER, + CREATOR_MULTI_NETWORK_STACK_NETWORK_MANAGEMENT, + CREATOR_MULTI_NETWORK_STACK_PARENT_INFO, + CREATOR_MULTI_NETWORK_STACK_NONCE_COUNTER, + CREATOR_MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO, + CREATOR_STACK_GP_DATA, + CREATOR_STACK_GP_PROXY_TABLE, + CREATOR_STACK_GP_SINK_TABLE, + CREATOR_STACK_GP_INCOMING_FC, + CREATOR_STACK_GP_INCOMING_FC_IN_SINK, + CREATOR_STACK_BINDING_TABLE, + CREATOR_STACK_CHILD_TABLE, + CREATOR_STACK_KEY_TABLE, + CREATOR_STACK_CERTIFICATE_TABLE, + CREATOR_STACK_ZLL_DATA, + CREATOR_STACK_ZLL_SECURITY, + CREATOR_STACK_ADDITIONAL_CHILD_DATA, +]; + +/** uint32_t */ +const NVM3KEYS: number[] = [ + NVM3KEY_STACK_NVDATA_VERSION, + NVM3KEY_STACK_BOOT_COUNTER, + NVM3KEY_STACK_NONCE_COUNTER, + NVM3KEY_STACK_ANALYSIS_REBOOT, + NVM3KEY_STACK_KEYS, + NVM3KEY_STACK_NODE_DATA, + NVM3KEY_STACK_CLASSIC_DATA, + NVM3KEY_STACK_ALTERNATE_KEY, + NVM3KEY_STACK_APS_FRAME_COUNTER, + NVM3KEY_STACK_TRUST_CENTER, + NVM3KEY_STACK_NETWORK_MANAGEMENT, + NVM3KEY_STACK_PARENT_INFO, + NVM3KEY_STACK_PARENT_ADDITIONAL_INFO, + NVM3KEY_STACK_MULTI_PHY_NWK_INFO, + NVM3KEY_STACK_MIN_RECEIVED_RSSI, + NVM3KEY_STACK_RESTORED_EUI64, + NVM3KEY_MULTI_NETWORK_STACK_KEYS, + NVM3KEY_MULTI_NETWORK_STACK_NODE_DATA, + NVM3KEY_MULTI_NETWORK_STACK_ALTERNATE_KEY, + NVM3KEY_MULTI_NETWORK_STACK_TRUST_CENTER, + NVM3KEY_MULTI_NETWORK_STACK_NETWORK_MANAGEMENT, + NVM3KEY_MULTI_NETWORK_STACK_PARENT_INFO, + NVM3KEY_MULTI_NETWORK_STACK_NONCE_COUNTER, + NVM3KEY_MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO, + NVM3KEY_STACK_GP_DATA, + NVM3KEY_STACK_GP_PROXY_TABLE, + NVM3KEY_STACK_GP_SINK_TABLE, + NVM3KEY_STACK_GP_INCOMING_FC, + NVM3KEY_STACK_BINDING_TABLE, + NVM3KEY_STACK_CHILD_TABLE, + NVM3KEY_STACK_KEY_TABLE, + NVM3KEY_STACK_CERTIFICATE_TABLE, + NVM3KEY_STACK_ZLL_DATA, + NVM3KEY_STACK_ZLL_SECURITY, + NVM3KEY_STACK_ADDITIONAL_CHILD_DATA, + NVM3KEY_STACK_GP_INCOMING_FC_IN_SINK, +]; + +const BLANK_EUI64_BUF = Buffer.from(BLANK_EUI64.substring(2)/*take out 0x*/, 'hex'); + +export class EmberTokensManager { + + /** + * Host-only API to check whether the NCP uses key storage. + * + * @returns false if keys are in classic key storage, and true if they are located in PSA key storage. + */ + private static async ncpUsesPSAKeyStorage(ezsp: Ezsp): Promise { + const [status, valueLength, value] = (await ezsp.ezspGetValue(EzspValueId.KEY_STORAGE_VERSION, 1)); + + if ((status !== EzspStatus.SUCCESS) || (valueLength < 1)) { + throw new Error(`[TOKENS] Error retrieving key storage version, status=${EzspStatus[status]}.`); + } + + return (value[0] === 1); + } + + /** + * Matcher for Zigbeed tokens. + * @param nvm3Key + * @returns + */ + private static getCreatorFromNvm3Key(nvm3Key: number): number { + for (let i = 0; i < NVM3KEYS.length; i++) { + if (NVM3KEYS[i] === nvm3Key) { + return CREATORS[i]; + } + } + + return 0xFFFF; + } + + /** + * Saves tokens. Only for NVM3-based NCP. + * + * The binary file format to save the tokens are + * + * Number of Tokens (1 byte) + * Token0 (4 bytes) Token0Size(1 byte) Token0ArraySize(1 byte) Token0Data(Token0Size * Token0ArraySize) + * : + * : + * TokenM (4 bytes) TokenMSize(1 byte) TokenMArraySize(1 byte) TokenMData(TokenMSize * TokenMArraySize) + * + * @param localEui64 Used in place of blank `restoredEui64` keys + * + * @return Saved tokens buffer or null. + */ + public static async saveTokens(ezsp: Ezsp, localEui64: Buffer): Promise { + console.log(`[TOKENS] Saving tokens...`); + const tokenCount = (await ezsp.ezspGetTokenCount()); + + if (tokenCount) { + const chunks: Buffer[] = [Buffer.from([tokenCount])];// 1 byte + // returns 1 if NCP has secure key storage (where these tokens do not store the key data). + // Don't compile for scripted test or any non-host code due to linker issues. + const hasSecureStorage: boolean = (await EmberTokensManager.ncpUsesPSAKeyStorage(ezsp)); + + debug(`[TOKENS] Saving ${tokenCount} tokens, ${hasSecureStorage ? "with" : "without"} secure storage.`); + + for (let i = 0; i < tokenCount; i++) { + const [tiStatus, tokenInfo] = (await ezsp.ezspGetTokenInfo(i)); + let writeOffset: number = 0; + + if (tiStatus === EmberStatus.SUCCESS) { + const outputToken = Buffer.alloc(4 + 1 + 1 + (tokenInfo.size * tokenInfo.arraySize)); + outputToken.writeUInt32LE(tokenInfo.nvm3Key, writeOffset);// 4 bytes + writeOffset += 4; + outputToken.writeUInt8(tokenInfo.size, writeOffset++);// 1 byte + outputToken.writeUInt8(tokenInfo.arraySize, writeOffset++);// 1 byte + + for (let arrayIndex = 0; arrayIndex < tokenInfo.arraySize; arrayIndex++) { + const [tdStatus, tokenData] = (await ezsp.ezspGetTokenData(tokenInfo.nvm3Key, arrayIndex)); + + if (tdStatus === EmberStatus.SUCCESS) { + if (hasSecureStorage) { + // Populate keys into tokenData because tokens do not contain them with secure key storage + await EmberTokensManager.saveKeysToData(ezsp, tokenData, tokenInfo.nvm3Key, arrayIndex); + + // ensure the token data was retrieved properly, length should match the size announced by the token info + console.assert( + tokenData.data.length === tokenInfo.size, + `[TOKENS] Mismatch in token data size; got ${tokenData.data.length}, expected ${tokenInfo.size}.` + ); + } + // debug(`[TOKENS] TOKEN nvm3Key=${DEBUG_TOKEN_STRINGS[tokenInfo.nvm3Key]} size=${tokenInfo.size} ` + // + `arraySize=${tokenInfo.arraySize} token=${tokenData.data.toString('hex')}`); + + // Check the Key to see if the token to save is restoredEui64, in that case + // check if it is blank, then save the node EUI64 in its place, else save the value + // received from the API. Once it saves, during restore process the set token will + // simply write the restoredEUI64 and the node will start to use that. + if (tokenInfo.nvm3Key === NVM3KEY_STACK_RESTORED_EUI64 && tokenData.size === EUI64_SIZE + && (tokenData.data === BLANK_EUI64_BUF)) { + // Special case : Save the node EUI64 on the restoredEui64 token while saving. + tokenData.data.set(localEui64); + debug(`[TOKENS] Saved node EUI64 in place of blank RESTORED EUI64.`); + } + + outputToken.set(tokenData.data, writeOffset); + writeOffset += tokenData.size; + } else { + console.error(`[TOKENS] Failed to get token data at index ${arrayIndex} with status=${EmberStatus[tdStatus]}.`); + } + } + + chunks.push(outputToken); + } else { + console.error(`[TOKENS] Failed to get token info at index ${i} with status=${EmberStatus[tiStatus]}.`); + } + } + + return Buffer.concat(chunks); + } else { + // ezspGetTokenCount == 0 OR (ezspGetTokenInfo|ezspGetTokenData|ezspSetTokenData return LIBRARY_NOT_PRESENT) + // ezspTokenFactoryReset will do nothing. + console.error(`[TOKENS] Saving tokens not supported by NCP (not NVM3-based).`); + } + + return null; + } + + /** + * Restores tokens. Only for NVM3-based NCP. + * XXX: If a previous backup from an NVM3 NCP is attempted on a non-NVM3 NCP, + * it should just fail (LIBRARY_NOT_PRESENT all on token-related functions). + * + * @see EmberTokensManager.saveTokens() for format + * + * @return EmberStatus status code + */ + public static async restoreTokens(ezsp: Ezsp, inBuffer: Buffer): Promise { + if (!inBuffer?.length) { + throw new Error(`[TOKENS] Restore tokens buffer empty.`); + } + + console.log(`[TOKENS] Restoring tokens...`); + + let readOffset: number = 0; + const inTokenCount = inBuffer.readUInt8(readOffset++); + const hasSecureStorage: boolean = (await EmberTokensManager.ncpUsesPSAKeyStorage(ezsp)); + + debug(`[TOKENS] Restoring ${inTokenCount} tokens, ${hasSecureStorage ? "with" : "without"} secure storage.`); + + for (let i = 0; i < inTokenCount; i++) { + const [tiStatus, tokenInfo] = (await ezsp.ezspGetTokenInfo(i)); + + if (tiStatus === EmberStatus.SUCCESS) { + const nvm3Key = inBuffer.readUInt32LE(readOffset);// 4 bytes Token Key/Creator + readOffset += 4; + const size = inBuffer.readUInt8(readOffset++);// 1 byte token size + const arraySize = inBuffer.readUInt8(readOffset++);// 1 byte array size. + + for (let arrayIndex = 0; arrayIndex < arraySize; arrayIndex++) { + const tokenData: EmberTokenData = { + data: inBuffer.subarray(readOffset, readOffset + size), + size, + }; + + if (hasSecureStorage) { + // do not keep keys in classic key storage upon restoration + await EmberTokensManager.restoreKeysFromData(ezsp, tokenData, tokenInfo.nvm3Key, arrayIndex); + } + + const status = (await ezsp.ezspSetTokenData(nvm3Key, arrayIndex, tokenData)) as EmberStatus; + + console.assert( + status === EmberStatus.SUCCESS, + `[TOKENS] Failed to set token data for key "${nvm3Key}" with status=${EmberStatus[status]}.` + ); + + readOffset += tokenData.size; + } + } else { + console.error(`[TOKENS] Failed to get token info at index ${i} with status=${EmberStatus[tiStatus]}.`); + } + } + + return EmberStatus.SUCCESS; + } + + /** + * Secure key storage needs to export the keys first so backup file has them. + * + * @param tokenData EmberTokenData* [IN/OUT] + * @param nvm3Key uint32_t + * @param index uint8_t + * @returns + */ + private static async saveKeysToData(ezsp: Ezsp, tokenData: EmberTokenData, nvm3Key: number, index: number): Promise { + let status: SLStatus = SLStatus.OK; + const context = initSecurityManagerContext(); + let plaintextKey: SecManKey; + + if (nvm3Key === NVM3KEY_STACK_KEYS) { + // typedef struct { + // uint8_t networkKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // uint8_t activeKeySeqNum; + // } tokTypeStackKeys; + + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 0);// at beginning + } else if (nvm3Key === NVM3KEY_STACK_ALTERNATE_KEY) { + // typedef struct { + // uint8_t networkKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // uint8_t activeKeySeqNum; + // } tokTypeStackKeys; + + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 1; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 0);// at beginning + } else if (nvm3Key === NVM3KEY_STACK_TRUST_CENTER) { + // typedef struct { + // uint16_t mode; + // uint8_t eui64[8]; + // uint8_t key[16]; // ignored if (mode & TRUST_CENTER_KEY_LIVES_IN_PSA) + // } tokTypeStackTrustCenter; + + context.coreKeyType = SecManKeyType.TC_LINK; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 2 + EUI64_SIZE);// uint16_t+uint8_t[8] + } else if (nvm3Key === NVM3KEY_STACK_KEY_TABLE) { + // typedef uint8_t tokTypeStackKeyTable[25]; + + context.coreKeyType = SecManKeyType.APP_LINK; + context.keyIndex = index; + //this must be set to export a specific link key from table + context.flags |= SecManFlag.KEY_INDEX_IS_VALID; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, KEY_ENTRY_KEY_DATA_OFFSET);// end part of uint8_t[25] + } else if (nvm3Key === NVM3KEY_STACK_GP_PROXY_TABLE) { + // typedef struct { + // uint8_t status; + // uint32_t options; + // //EmberGpAddress gpd; + // uint8_t gpAddress[8]; + // uint8_t endpoint; + // //uint16_t assignedAlias; + // uint8_t securityOptions; + // uint8_t gpdKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // //EmberGpSinkListEntry sinkList[2]; + // uint8_t sinkType[2]; + // uint8_t sinkEUI[2][8]; + // //uint16_t sinkNodeId[2]; + // } tokTypeStackGpProxyTableEntry; + + context.coreKeyType = SecManKeyType.GREEN_POWER_PROXY_TABLE_KEY; + context.keyIndex = index; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 1 + 4 + 8 + 1 + 1);// uint8_t+uint32_t+uint8_t[8]+uint8_t+uint8_t + } else if (nvm3Key === NVM3KEY_STACK_GP_SINK_TABLE) { + // typedef struct { + // uint8_t status; + // uint16_t options; + // //EmberGpAddress gpd; + // uint8_t gpAddress[8]; + // uint8_t endpoint; + // uint8_t securityOptions; + // uint8_t gpdKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // uint8_t sinkType[2]; + // uint16_t groupList[2][2]; + // uint32_t securityFrameCounter; // This is no more used, Incoming FC for gpd in a separate Token to control its update. + // uint16_t assignedAlias; + // uint8_t deviceId; + // uint8_t groupcastRadius; + // } tokTypeStackGpSinkTableEntry; + + context.coreKeyType = SecManKeyType.GREEN_POWER_SINK_TABLE_KEY; + context.keyIndex = index; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 1 + 2 + 8 + 1 + 1);// uint8_t+uint16_t+uint8_t[8]+uint8_t+uint8_t + } else if (nvm3Key === NVM3KEY_STACK_ZLL_SECURITY) { + // typedef struct { + // uint32_t bitmask; + // uint8_t keyIndex; + // uint8_t encryptionKey[EMBER_ENCRYPTION_KEY_SIZE]; + // uint8_t preconfiguredKey[EMBER_ENCRYPTION_KEY_SIZE]; + // } EmberTokTypeStackZllSecurity; + + context.coreKeyType = SecManKeyType.ZLL_ENCRYPTION_KEY; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 4 + 1);// uint32_t+uint8_t + + context.coreKeyType = SecManKeyType.ZLL_PRECONFIGURED_KEY; + + [plaintextKey, status] = (await ezsp.ezspExportKey(context)); + + tokenData.data.set(plaintextKey.contents, 4 + 1 + EMBER_ENCRYPTION_KEY_SIZE);// uint32_t+uint8_t+uint8_t[EMBER_ENCRYPTION_KEY_SIZE] + } else { + //nothing needs to be done for non-key tokens + } + + return status; + } + + /** + * + * @param data_s EmberTokenData* + * @param nvm3Key uint32_t + * @param index uint8_t + * @returns + * + * @from sli_zigbee_af_trust_center_backup_restore_keys_from_data + */ + private static async restoreKeysFromData(ezsp: Ezsp, tokenData: EmberTokenData, nvm3Key: number, index: number): Promise { + let status: SLStatus = SLStatus.OK; + const context = initSecurityManagerContext(); + + const plaintextKey: SecManKey = {contents: null}; + + if (nvm3Key === NVM3KEY_STACK_KEYS) { + // typedef struct { + // uint8_t networkKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // uint8_t activeKeySeqNum; + // } tokTypeStackKeys; + + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + plaintextKey.contents = tokenData.data.subarray(0, EMBER_ENCRYPTION_KEY_SIZE);// at beginning + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else if (nvm3Key === NVM3KEY_STACK_ALTERNATE_KEY) { + // typedef struct { + // uint8_t networkKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // uint8_t activeKeySeqNum; + // } tokTypeStackKeys; + + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 1; + plaintextKey.contents = tokenData.data.subarray(0, EMBER_ENCRYPTION_KEY_SIZE);// at beginning + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else if (nvm3Key === NVM3KEY_STACK_TRUST_CENTER) { + // typedef struct { + // uint16_t mode; + // uint8_t eui64[8]; + // uint8_t key[16]; // ignored if (mode & TRUST_CENTER_KEY_LIVES_IN_PSA) + // } tokTypeStackTrustCenter; + + context.coreKeyType = SecManKeyType.TC_LINK; + const s = 2 + EUI64_SIZE; + plaintextKey.contents = tokenData.data.subarray(s, s + EMBER_ENCRYPTION_KEY_SIZE);// uint16_t+uint8_t[8] + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else if (nvm3Key === NVM3KEY_STACK_KEY_TABLE) { + // typedef uint8_t tokTypeStackKeyTable[25]; + + context.coreKeyType = SecManKeyType.APP_LINK; + context.keyIndex = index; + context.flags |= SecManFlag.KEY_INDEX_IS_VALID; + plaintextKey.contents = tokenData.data.subarray( + KEY_ENTRY_KEY_DATA_OFFSET, + KEY_ENTRY_KEY_DATA_OFFSET + EMBER_ENCRYPTION_KEY_SIZE + );// end part of uint8_t[25] + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else if (nvm3Key === NVM3KEY_STACK_GP_PROXY_TABLE) { + // typedef struct { + // uint8_t status; + // uint32_t options; + // //EmberGpAddress gpd; + // uint8_t gpAddress[8]; + // uint8_t endpoint; + // //uint16_t assignedAlias; + // uint8_t securityOptions; + // uint8_t gpdKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // //EmberGpSinkListEntry sinkList[2]; + // uint8_t sinkType[2]; + // uint8_t sinkEUI[2][8]; + // //uint16_t sinkNodeId[2]; + // } tokTypeStackGpProxyTableEntry; + + context.coreKeyType = SecManKeyType.GREEN_POWER_PROXY_TABLE_KEY; + context.keyIndex = index; + const s = 1 + 4 + 8 + 1 + 1; + plaintextKey.contents = tokenData.data.subarray(s, s + EMBER_ENCRYPTION_KEY_SIZE);// uint8_t+uint32_t+uint8_t[8]+uint8_t+uint8_t + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else if (nvm3Key === NVM3KEY_STACK_GP_SINK_TABLE) { + // typedef struct { + // uint8_t status; + // uint16_t options; + // //EmberGpAddress gpd; + // uint8_t gpAddress[8]; + // uint8_t endpoint; + // uint8_t securityOptions; + // uint8_t gpdKey[16]; // ignored if using Secure Key Storage (but moved to PSA and cleared if upgrade code is run) + // uint8_t sinkType[2]; + // uint16_t groupList[2][2]; + // uint32_t securityFrameCounter; // This is no more used, Incoming FC for gpd in a separate Token to control its update. + // uint16_t assignedAlias; + // uint8_t deviceId; + // uint8_t groupcastRadius; + // } tokTypeStackGpSinkTableEntry; + + context.coreKeyType = SecManKeyType.GREEN_POWER_SINK_TABLE_KEY; + context.keyIndex = index; + const s = 1 + 2 + 8 + 1 + 1; + plaintextKey.contents = tokenData.data.subarray(s, s + EMBER_ENCRYPTION_KEY_SIZE);// uint8_t+uint16_t+uint8_t[8]+uint8_t+uint8_t + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else if (nvm3Key === NVM3KEY_STACK_ZLL_SECURITY) { + // typedef struct { + // uint32_t bitmask; + // uint8_t keyIndex; + // uint8_t encryptionKey[EMBER_ENCRYPTION_KEY_SIZE]; + // uint8_t preconfiguredKey[EMBER_ENCRYPTION_KEY_SIZE]; + // } EmberTokTypeStackZllSecurity; + + context.coreKeyType = SecManKeyType.ZLL_ENCRYPTION_KEY; + let s = 4 + 1; + plaintextKey.contents = tokenData.data.subarray(s, s + EMBER_ENCRYPTION_KEY_SIZE);// uint32_t+uint8_t + + status = await ezsp.ezspImportKey(context, plaintextKey); + + context.coreKeyType = SecManKeyType.ZLL_PRECONFIGURED_KEY; + s += EMBER_ENCRYPTION_KEY_SIZE;// after `encryptionKey` + plaintextKey.contents = tokenData.data.subarray(s, s + EMBER_ENCRYPTION_KEY_SIZE);// uint32_t+uint8_t+uint8_t[EMBER_ENCRYPTION_KEY_SIZE] + + status = await ezsp.ezspImportKey(context, plaintextKey); + } else { + // unknown key + } + + return status; + } + + /** + * Updates zigbeed tokens from a backup of NCP tokens. + * + * @return EmberStatus status code + */ + public static async writeNcpTokensToZigbeedTokens(ezsp: Ezsp, inBuffer: Buffer): Promise { + if (!inBuffer?.length) { + throw new Error(`[TOKENS] Restore tokens buffer empty.`); + } + + console.log(`[TOKENS] Restoring tokens to Zigbeed...`); + + let readOffset: number = 0; + const inTokenCount = inBuffer.readUInt8(readOffset++); + + for (let i = 0; i < inTokenCount; i++) { + const nvm3Key = inBuffer.readUInt32LE(readOffset);// 4 bytes Token Key/Creator + readOffset += 4; + const size = inBuffer.readUInt8(readOffset++);// 1 byte token size + const arraySize = inBuffer.readUInt8(readOffset++);// 1 byte array size. + + for (let arrayIndex = 0; arrayIndex < arraySize; arrayIndex++) { + const tokenData: EmberTokenData = { + data: inBuffer.subarray(readOffset, readOffset + size), + size, + }; + + const creator = EmberTokensManager.getCreatorFromNvm3Key(nvm3Key);// uint16_t + const status = (await ezsp.ezspSetTokenData(creator, arrayIndex, tokenData)); + + console.assert( + status === EmberStatus.SUCCESS, + `[TOKENS] Failed to set Zigbeed token data for key "${nvm3Key}" creator "${creator}" with status=${EmberStatus[status]}.` + ); + + readOffset += tokenData.size; + } + } + + return EmberStatus.SUCCESS; + } +} diff --git a/src/adapter/ember/consts.ts b/src/adapter/ember/consts.ts new file mode 100644 index 0000000000..443859c286 --- /dev/null +++ b/src/adapter/ember/consts.ts @@ -0,0 +1,290 @@ +//------------------------------------------------------------------------------------------------- +// General + +/** Endpoint profile ID */ +export const CBA_PROFILE_ID = 0x0105; +/** Endpoint profile ID for Zigbee 3.0. "Home Automation" */ +export const HA_PROFILE_ID = 0x0104; +/** Endpoint profile ID for Smart Energy */ +export const SE_PROFILE_ID = 0x0109; +/** Endpoint profile ID for Green Power */ +export const GP_PROFILE_ID = 0xA1E0; +/** The touchlink (ZigBee Light Link/ZLL) Profile ID. */ +export const TOUCHLINK_PROFILE_ID = 0xC05E; +/** The profile ID used to address all the public profiles. */ +export const WILDCARD_PROFILE_ID = 0xFFFF; + +/** Ember Corporation's Manufacturer ID allocated by the Zigbee alliance. This shall not change. */ +export const EMBER_COMPANY_MANUFACTURER_CODE = 0x1002; + +/** + * The MFG code set by AppBuilder for use in the App Framework (AF). + * If not set by AppBuilder we default it to 0x0000. The customer should be setting this value. + */ +export const MANUFACTURER_CODE = 0x1049; + +/** The network ID of the coordinator in a ZigBee network is 0x0000. */ +export const ZIGBEE_COORDINATOR_ADDRESS = 0x0000; + +/** A blank (also used as "wildcard") EUI64 hex string prefixed with 0x */ +export const BLANK_EUI64 = "0xFFFFFFFFFFFFFFFF"; +/** A blank extended PAN ID. (null/not present) */ +export const BLANK_EXTENDED_PAN_ID: readonly number[] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; +/** An invalid profile ID. This is a reserved profileId. */ +export const INVALID_PROFILE_ID = 0xFFFF; +/** An invalid cluster ID. */ +export const INVALID_CLUSTER_ID = 0xFFFF; +/** An invalid PAN ID. */ +export const INVALID_PAN_ID = 0xFFFF; +/** Serves to initialize cache */ +export const INVALID_NODE_TYPE = 0xFF; +/** Serves to initialize cache for config IDs */ +export const INVALID_CONFIG_VALUE = 0xFFFF; +/** Serves to initialize cache */ +export const INVALID_RADIO_CHANNEL = 0xFF; +/** A distinguished network ID that will never be assigned to any node. It is used to indicate the absence of a node ID. */ +export const NULL_NODE_ID = 0xFFFF; +export const UNKNOWN_NETWORK_STATE = 0xFF; +/** A distinguished binding index used to indicate the absence of a binding. */ +export const NULL_BINDING = 0xFF; +/** + * A distinguished network ID that will never be assigned to any node. + * This value is returned when getting the remote node ID from the binding table and the given binding table index refers + * to a multicast binding entry. + */ +export const EMBER_MULTICAST_NODE_ID = 0xFFFE; +/** + * A distinguished network ID that will never be assigned + * to any node. This value is used when getting the remote node ID + * from the address or binding tables. It indicates that the address + * or binding table entry is currently in use but the node ID + * corresponding to the EUI64 in the table is currently unknown. + */ +export const EMBER_UNKNOWN_NODE_ID = 0xFFFD; +/** + * A distinguished network ID that will never be assigned + * to any node. This value is used when getting the remote node ID + * from the address or binding tables. It indicates that the address + * or binding table entry is currently in use and network address + * discovery is underway. + */ +export const EMBER_DISCOVERY_ACTIVE_NODE_ID = 0xFFFC; +/** A distinguished address table index used to indicate the absence of an address table entry. */ +export const EMBER_NULL_ADDRESS_TABLE_INDEX = 0xFF; +/** Invalidates cached information */ +export const SOURCE_ROUTE_OVERHEAD_UNKNOWN = 0xFF; + +// Permit join times. +export const PERMIT_JOIN_FOREVER = 0xFF; +export const PERMIT_JOIN_MAX_TIMEOUT = 0xFE; + +//------------------------------------------------------------------------------------------------- +// Network + +/** + * ZigBee Broadcast Addresses + * + * ZigBee specifies three different broadcast addresses that + * reach different collections of nodes. Broadcasts are normally sent only + * to routers. Broadcasts can also be forwarded to end devices, either + * all of them or only those that do not sleep. Broadcasting to end + * devices is both significantly more resource-intensive and significantly + * less reliable than broadcasting to routers. + */ +/** Broadcast to all routers. */ +export const EMBER_BROADCAST_ADDRESS = 0xFFFC; +/** Broadcast to all non-sleepy devices. */ +export const EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS = 0xFFFD; +/** Broadcast to all devices, including sleepy end devices. */ +export const EMBER_SLEEPY_BROADCAST_ADDRESS = 0xFFFF; +// From table 3.51 of 053474r14 +// When sending many-to-one route requests, the following +// addresses are used +// 0xFFF9 indicates a non-memory-constrained many-to-one route request +// 0xFFF8 indicates a memory-constrained many-to-one route request +export const EMBER_MIN_BROADCAST_ADDRESS = 0xFFF8; + +/** The maximum 802.15.4 channel number is 26. */ +export const EMBER_MAX_802_15_4_CHANNEL_NUMBER = 26; +/** The minimum 2.4GHz 802.15.4 channel number is 11. */ +export const EMBER_MIN_802_15_4_CHANNEL_NUMBER = 11; +/** The minimum SubGhz channel number is 0. */ +export const EMBER_MIN_SUBGHZ_CHANNEL_NUMBER = 0; + +/** + * ZigBee protocol specifies that active scans have a duration of 3 (138 msec). + * See documentation for emberStartScan in include/network-formation.h + * for more info on duration values. + */ +export const EMBER_ACTIVE_SCAN_DURATION = 3; +/** The SubGhz scan duration is 5. */ +export const EMBER_SUB_GHZ_SCAN_DURATION = 5; +/** There are sixteen 802.15.4 channels. */ +export const EMBER_NUM_802_15_4_CHANNELS = (EMBER_MAX_802_15_4_CHANNEL_NUMBER - EMBER_MIN_802_15_4_CHANNEL_NUMBER + 1); +/** A bitmask to scan all 2.4 GHz 802.15.4 channels. */ +export const EMBER_ALL_802_15_4_CHANNELS_MASK = 0x07FFF800; +/** The channels that the plugin will preferentially scan when forming and joining. */ +export const NETWORK_FIND_CHANNEL_MASK = 0x0318C800; +/** + * Cut-off value (dBm) <-128..127> + * The maximum noise allowed on a channel to consider for forming a network. + * If the noise on all preferred channels is above this limit and "Enable scanning all channels" is ticked, the scan continues on all channels. + * Use emberAfPluginNetworkFindGetEnergyThresholdForChannelCallback() to override this value. + */ +export const NETWORK_FIND_CUT_OFF_VALUE = -48; + +/** + * The additional overhead required for network source routing (relay count = 1, relay index = 1). + * This does not include the size of the relay list itself. + */ +export const NWK_SOURCE_ROUTE_OVERHEAD = 2; +export const SOURCE_ROUTING_RESERVED_PAYLOAD_LENGTH = 0; +/** + * The maximum APS payload, not including any APS options. + * This value is also available from emberMaximumApsPayloadLength() or ezspMaximumPayloadLength(). + * See http://portal.ember.com/faq/payload for more information. + */ +export const MAXIMUM_APS_PAYLOAD_LENGTH = (82 - SOURCE_ROUTING_RESERVED_PAYLOAD_LENGTH); +// export const MAXIMUM_APS_PAYLOAD_LENGTH_SECURITY_NONE = (100 - SOURCE_ROUTING_RESERVED_PAYLOAD_LENGTH); +/** The additional overhead required for APS encryption (security = 5, MIC = 4). */ +export const APS_ENCRYPTION_OVERHEAD = 9; +/** The additional overhead required for APS fragmentation. */ +export const APS_FRAGMENTATION_OVERHEAD = 2; + +/** + * A concentrator with insufficient memory to store source routes for the entire network. + * Route records are sent to the concentrator prior to every inbound APS unicast. + */ +export const EMBER_LOW_RAM_CONCENTRATOR = 0xFFF8; +/** + * A concentrator with sufficient memory to store source routes for the entire network. + * Remote nodes stop sending route records once the concentrator has successfully received one. + */ +export const EMBER_HIGH_RAM_CONCENTRATOR = 0xFFF9; + + +//------------------------------------------------------------------------------------------------- +// Security + +/** The short address of the trust center. This address never changes dynamically. */ +export const EMBER_TRUST_CENTER_NODE_ID = 0x0000; + + +/** The size of the CRC that is appended to an installation code. */ +export const EMBER_INSTALL_CODE_CRC_SIZE = 2; + +/** The number of sizes of acceptable installation codes used in Certificate Based Key Establishment (CBKE). */ +export const EMBER_NUM_INSTALL_CODE_SIZES = 4; + +/** + * Various sizes of valid installation codes that are stored in the manufacturing tokens. + * Note that each size includes 2 bytes of CRC appended to the end of the installation code. + */ +export const EMBER_INSTALL_CODE_SIZES = [ + 6 + EMBER_INSTALL_CODE_CRC_SIZE, + 8 + EMBER_INSTALL_CODE_CRC_SIZE, + 12 + EMBER_INSTALL_CODE_CRC_SIZE, + 16 + EMBER_INSTALL_CODE_CRC_SIZE +]; + +/** + * Default value for context's PSA algorithm permission (CCM* with 4 byte tag). + * Only used by NCPs with secure key storage; define is mirrored here to allow + * host code to initialize the context itself rather than needing a new EZSP frame. + */ +export const ZB_PSA_ALG = 0x05440100; + +export const STACK_PROFILE_ZIGBEE_PRO = 0x02; +export const SECURITY_LEVEL_Z3 = 0x05; + +/** This key is "ZigBeeAlliance09" */ +export const ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY: readonly number[] = [ + 0x5A, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6C, 0x6C, 0x69, 0x61, 0x6E, 0x63, 0x65, 0x30, 0x39 +]; + + + +//------------------------------------------------------------------------------------------------- +// Zigbee Green Power types and defines. + +/** The GP endpoint, as defined in the ZigBee spec. */ +export const GP_ENDPOINT = 0xF2; + +/** Number of GP sink list entries. Minimum is 2 sink list entries. */ +export const GP_SINK_LIST_ENTRIES = 2; +/** The size of the SinkList entries in sink table in format of octet string that has a format of {<1 byte length>, } */ +export const GP_SIZE_OF_SINK_LIST_ENTRIES_OCTET_STRING = (1 + (GP_SINK_LIST_ENTRIES * 4));// sizeof(EmberGpSinkGroup) === uint16_t * 2 + + +//------------------------------------------------------------------------------------------------- +//-- InterPAN + +// Max PHY size = 128 +// -1 byte for PHY length +// -2 bytes for MAC CRC +export const MAXIMUM_INTERPAN_LENGTH = 125; + +// MAC frame control +// Bits: +// | 0-2 | 3 | 4 | 5 | 6 | 7-9 | 10-11 | 12-13 | 14-15 | +// | Frame | Security | Frame | Ack | Intra | Reserved | Dest. | Reserved | Src | +// | Type | Enabled | Pending | Req | PAN | | Addr. | | Adrr. | +// | | | | | | | Mode | | Mode | + +// Frame Type +// 000 = Beacon +// 001 = Data +// 010 = Acknwoledgement +// 011 = MAC Command +// 100 - 111 = Reserved + +// Addressing Mode +// 00 - PAN ID and address field are not present +// 01 - Reserved +// 10 - Address field contains a 16-bit short address +// 11 - Address field contains a 64-bit extended address + +const MAC_FRAME_TYPE_DATA = 0x0001; +// const MAC_FRAME_SOURCE_MODE_SHORT = 0x8000; +const MAC_FRAME_SOURCE_MODE_LONG = 0xC000; +const MAC_FRAME_DESTINATION_MODE_SHORT = 0x0800; +const MAC_FRAME_DESTINATION_MODE_LONG = 0x0C00; + +// The two possible incoming MAC frame controls. +// Using short source address is not allowed. +export const SHORT_DEST_FRAME_CONTROL = (MAC_FRAME_TYPE_DATA | MAC_FRAME_DESTINATION_MODE_SHORT | MAC_FRAME_SOURCE_MODE_LONG); +export const LONG_DEST_FRAME_CONTROL = (MAC_FRAME_TYPE_DATA | MAC_FRAME_DESTINATION_MODE_LONG | MAC_FRAME_SOURCE_MODE_LONG); + +export const MAC_ACK_REQUIRED = 0x0020; + +/** NWK stub frame has two control bytes. */ +export const STUB_NWK_SIZE = 2; +export const STUB_NWK_FRAME_CONTROL = 0x000B; + +/** + * Interpan APS Unicast, same for Broadcast. + * - Frame Control (1-byte) + * - Cluster ID (2-bytes) + * - Profile ID (2-bytes) + */ +export const INTERPAN_APS_UNICAST_BROADCAST_SIZE = 5; +/** + * Interpan APS Multicast + * - Frame Control (1-byte) + * - Group ID (2-bytes) + * - Cluster ID (2-bytes) + * - Profile ID (2-bytes) + */ +export const INTERPAN_APS_MULTICAST_SIZE = 7; + +export const MAX_STUB_APS_SIZE = (INTERPAN_APS_MULTICAST_SIZE); +export const MIN_STUB_APS_SIZE = (INTERPAN_APS_UNICAST_BROADCAST_SIZE); + +export const INTERPAN_APS_FRAME_TYPE = 0x03; +export const INTERPAN_APS_FRAME_TYPE_MASK = 0x03; + +/** The only allowed APS FC value (without the delivery mode subfield) */ +export const INTERPAN_APS_FRAME_CONTROL_NO_DELIVERY_MODE = (INTERPAN_APS_FRAME_TYPE); + +export const INTERPAN_APS_FRAME_DELIVERY_MODE_MASK = 0x0C; +export const INTERPAN_APS_FRAME_SECURITY = 0x20; diff --git a/src/adapter/ember/enums.ts b/src/adapter/ember/enums.ts new file mode 100644 index 0000000000..6de4aac0c8 --- /dev/null +++ b/src/adapter/ember/enums.ts @@ -0,0 +1,2423 @@ + +/** Status Defines */ +export enum SLStatus { + // ----------------------------------------------------------------------------- + // Generic Errors + + /** No error. */ + OK = 0x0000, + /** Generic error. */ + FAIL = 0x0001, + + // ----------------------------------------------------------------------------- + // State Errors + + /** Generic invalid state error. */ + INVALID_STATE = 0x0002, + /** Module is not ready for requested operation. */ + NOT_READY = 0x0003, + /** Module is busy and cannot carry out requested operation. */ + BUSY = 0x0004, + /** Operation is in progress and not yet complete (pass or fail). */ + IN_PROGRESS = 0x0005, + /** Operation aborted. */ + ABORT = 0x0006, + /** Operation timed out. */ + TIMEOUT = 0x0007, + /** Operation not allowed per permissions. */ + PERMISSION = 0x0008, + /** Non-blocking operation would block. */ + WOULD_BLOCK = 0x0009, + /** Operation/module is Idle, cannot carry requested operation. */ + IDLE = 0x000A, + /** Operation cannot be done while construct is waiting. */ + IS_WAITING = 0x000B, + /** No task/construct waiting/pending for that action/event. */ + NONE_WAITING = 0x000C, + /** Operation cannot be done while construct is suspended. */ + SUSPENDED = 0x000D, + /** Feature not available due to software configuration. */ + NOT_AVAILABLE = 0x000E, + /** Feature not supported. */ + NOT_SUPPORTED = 0x000F, + /** Initialization failed. */ + INITIALIZATION = 0x0010, + /** Module has not been initialized. */ + NOT_INITIALIZED = 0x0011, + /** Module has already been initialized. */ + ALREADY_INITIALIZED = 0x0012, + /** Object/construct has been deleted. */ + DELETED = 0x0013, + /** Illegal call from ISR. */ + ISR = 0x0014, + /** Illegal call because network is up. */ + NETWORK_UP = 0x0015, + /** Illegal call because network is down. */ + NETWORK_DOWN = 0x0016, + /** Failure due to not being joined in a network. */ + NOT_JOINED = 0x0017, + /** Invalid operation as there are no beacons. */ + NO_BEACONS = 0x0018, + + // ----------------------------------------------------------------------------- + // Allocation/ownership Errors + + /** Generic allocation error. */ + ALLOCATION_FAILED = 0x0019, + /** No more resource available to perform the operation. */ + NO_MORE_RESOURCE = 0x001A, + /** Item/list/queue is empty. */ + EMPTY = 0x001B, + /** Item/list/queue is full. */ + FULL = 0x001C, + /** Item would overflow. */ + WOULD_OVERFLOW = 0x001D, + /** Item/list/queue has been overflowed. */ + HAS_OVERFLOWED = 0x001E, + /** Generic ownership error. */ + OWNERSHIP = 0x001F, + /** Already/still owning resource. */ + IS_OWNER = 0x0020, + + // ----------------------------------------------------------------------------- + // Invalid Parameters Errors + + /** Generic invalid argument or consequence of invalid argument. */ + INVALID_PARAMETER = 0x0021, + /** Invalid null pointer received as argument. */ + NULL_POINTER = 0x0022, + /** Invalid configuration provided. */ + INVALID_CONFIGURATION = 0x0023, + /** Invalid mode. */ + INVALID_MODE = 0x0024, + /** Invalid handle. */ + INVALID_HANDLE = 0x0025, + /** Invalid type for operation. */ + INVALID_TYPE = 0x0026, + /** Invalid index. */ + INVALID_INDEX = 0x0027, + /** Invalid range. */ + INVALID_RANGE = 0x0028, + /** Invalid key. */ + INVALID_KEY = 0x0029, + /** Invalid credentials. */ + INVALID_CREDENTIALS = 0x002A, + /** Invalid count. */ + INVALID_COUNT = 0x002B, + /** Invalid signature / verification failed. */ + INVALID_SIGNATURE = 0x002C, + /** Item could not be found. */ + NOT_FOUND = 0x002D, + /** Item already exists. */ + ALREADY_EXISTS = 0x002E, + + // ----------------------------------------------------------------------------- + // IO/Communication Errors + + /** Generic I/O failure. */ + IO = 0x002F, + /** I/O failure due to timeout. */ + IO_TIMEOUT = 0x0030, + /** Generic transmission error. */ + TRANSMIT = 0x0031, + /** Transmit underflowed. */ + TRANSMIT_UNDERFLOW = 0x0032, + /** Transmit is incomplete. */ + TRANSMIT_INCOMPLETE = 0x0033, + /** Transmit is busy. */ + TRANSMIT_BUSY = 0x0034, + /** Generic reception error. */ + RECEIVE = 0x0035, + /** Failed to read on/via given object. */ + OBJECT_READ = 0x0036, + /** Failed to write on/via given object. */ + OBJECT_WRITE = 0x0037, + /** Message is too long. */ + MESSAGE_TOO_LONG = 0x0038, + + // ----------------------------------------------------------------------------- + // EEPROM/Flash Errors + + /** EEPROM MFG version mismatch. */ + EEPROM_MFG_VERSION_MISMATCH = 0x0039, + /** EEPROM Stack version mismatch. */ + EEPROM_STACK_VERSION_MISMATCH = 0x003A, + /** Flash write is inhibited. */ + FLASH_WRITE_INHIBITED = 0x003B, + /** Flash verification failed. */ + FLASH_VERIFY_FAILED = 0x003C, + /** Flash programming failed. */ + FLASH_PROGRAM_FAILED = 0x003D, + /** Flash erase failed. */ + FLASH_ERASE_FAILED = 0x003E, + + // ----------------------------------------------------------------------------- + // MAC Errors + + /** MAC no data. */ + MAC_NO_DATA = 0x003F, + /** MAC no ACK received. */ + MAC_NO_ACK_RECEIVED = 0x0040, + /** MAC indirect timeout. */ + MAC_INDIRECT_TIMEOUT = 0x0041, + /** MAC unknown header type. */ + MAC_UNKNOWN_HEADER_TYPE = 0x0042, + /** MAC ACK unknown header type. */ + MAC_ACK_HEADER_TYPE = 0x0043, + /** MAC command transmit failure. */ + MAC_COMMAND_TRANSMIT_FAILURE = 0x0044, + + // ----------------------------------------------------------------------------- + // CLI_STORAGE Errors + + /** Error in open NVM */ + CLI_STORAGE_NVM_OPEN_ERROR = 0x0045, + + // ----------------------------------------------------------------------------- + // Security status codes + + /** Image checksum is not valid. */ + SECURITY_IMAGE_CHECKSUM_ERROR = 0x0046, + /** Decryption failed */ + SECURITY_DECRYPT_ERROR = 0x0047, + + // ----------------------------------------------------------------------------- + // Command status codes + + /** Command was not recognized */ + COMMAND_IS_INVALID = 0x0048, + /** Command or parameter maximum length exceeded */ + COMMAND_TOO_LONG = 0x0049, + /** Data received does not form a complete command */ + COMMAND_INCOMPLETE = 0x004A, + + // ----------------------------------------------------------------------------- + // Misc Errors + + /** Bus error, e.g. invalid DMA address */ + BUS_ERROR = 0x004B, + + // ----------------------------------------------------------------------------- + // Unified MAC Errors + + /** CCA failure. */ + CCA_FAILURE = 0x004C, + + // ----------------------------------------------------------------------------- + // Scan errors + + /** MAC scanning. */ + MAC_SCANNING = 0x004D, + /** MAC incorrect scan type. */ + MAC_INCORRECT_SCAN_TYPE = 0x004E, + /** Invalid channel mask. */ + INVALID_CHANNEL_MASK = 0x004F, + /** Bad scan duration. */ + BAD_SCAN_DURATION = 0x0050, + + // ----------------------------------------------------------------------------- + // Bluetooth status codes + + /** Bonding procedure can't be started because device has no space left for bond. */ + BT_OUT_OF_BONDS = 0x0402, + /** Unspecified error */ + BT_UNSPECIFIED = 0x0403, + /** Hardware failure */ + BT_HARDWARE = 0x0404, + /** The bonding does not exist. */ + BT_NO_BONDING = 0x0406, + /** Error using crypto functions */ + BT_CRYPTO = 0x0407, + /** Data was corrupted. */ + BT_DATA_CORRUPTED = 0x0408, + /** Invalid periodic advertising sync handle */ + BT_INVALID_SYNC_HANDLE = 0x040A, + /** Bluetooth cannot be used on this hardware */ + BT_INVALID_MODULE_ACTION = 0x040B, + /** Error received from radio */ + BT_RADIO = 0x040C, + /** Returned when remote disconnects the connection-oriented channel by sending disconnection request. */ + BT_L2CAP_REMOTE_DISCONNECTED = 0x040D, + /** Returned when local host disconnect the connection-oriented channel by sending disconnection request. */ + BT_L2CAP_LOCAL_DISCONNECTED = 0x040E, + /** Returned when local host did not find a connection-oriented channel with given destination CID. */ + BT_L2CAP_CID_NOT_EXIST = 0x040F, + /** Returned when connection-oriented channel disconnected due to LE connection is dropped. */ + BT_L2CAP_LE_DISCONNECTED = 0x0410, + /** Returned when connection-oriented channel disconnected due to remote end send data even without credit. */ + BT_L2CAP_FLOW_CONTROL_VIOLATED = 0x0412, + /** Returned when connection-oriented channel disconnected due to remote end send flow control credits exceed 65535. */ + BT_L2CAP_FLOW_CONTROL_CREDIT_OVERFLOWED = 0x0413, + /** Returned when connection-oriented channel has run out of flow control credit and local application still trying to send data. */ + BT_L2CAP_NO_FLOW_CONTROL_CREDIT = 0x0414, + /** Returned when connection-oriented channel has not received connection response message within maximum timeout. */ + BT_L2CAP_CONNECTION_REQUEST_TIMEOUT = 0x0415, + /** Returned when local host received a connection-oriented channel connection response with an invalid destination CID. */ + BT_L2CAP_INVALID_CID = 0x0416, + /** Returned when local host application tries to send a command which is not suitable for L2CAP channel's current state. */ + BT_L2CAP_WRONG_STATE = 0x0417, + /** Flash reserved for PS store is full */ + BT_PS_STORE_FULL = 0x041B, + /** PS key not found */ + BT_PS_KEY_NOT_FOUND = 0x041C, + /** Mismatched or insufficient security level */ + BT_APPLICATION_MISMATCHED_OR_INSUFFICIENT_SECURITY = 0x041D, + /** Encryption/decryption operation failed. */ + BT_APPLICATION_ENCRYPTION_DECRYPTION_ERROR = 0x041E, + + // ----------------------------------------------------------------------------- + // Bluetooth controller status codes + + /** Connection does not exist, or connection open request was cancelled. */ + BT_CTRL_UNKNOWN_CONNECTION_IDENTIFIER = 0x1002, + /** + * Pairing or authentication failed due to incorrect results in the pairing or authentication procedure. + * This could be due to an incorrect PIN or Link Key + */ + BT_CTRL_AUTHENTICATION_FAILURE = 0x1005, + /** Pairing failed because of missing PIN, or authentication failed because of missing Key */ + BT_CTRL_PIN_OR_KEY_MISSING = 0x1006, + /** Controller is out of memory. */ + BT_CTRL_MEMORY_CAPACITY_EXCEEDED = 0x1007, + /** Link supervision timeout has expired. */ + BT_CTRL_CONNECTION_TIMEOUT = 0x1008, + /** Controller is at limit of connections it can support. */ + BT_CTRL_CONNECTION_LIMIT_EXCEEDED = 0x1009, + /** + * The Synchronous Connection Limit to a Device Exceeded error code indicates that the Controller has reached + * the limit to the number of synchronous connections that can be achieved to a device. + */ + BT_CTRL_SYNCHRONOUS_CONNECTION_LIMIT_EXCEEDED = 0x100A, + /** + * The ACL Connection Already Exists error code indicates that an attempt to create a new ACL Connection + * to a device when there is already a connection to this device. + */ + BT_CTRL_ACL_CONNECTION_ALREADY_EXISTS = 0x100B, + /** Command requested cannot be executed because the Controller is in a state where it cannot process this command at this time. */ + BT_CTRL_COMMAND_DISALLOWED = 0x100C, + /** The Connection Rejected Due To Limited Resources error code indicates that an incoming connection was rejected due to limited resources. */ + BT_CTRL_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES = 0x100D, + /** + * The Connection Rejected Due To Security Reasons error code indicates that a connection was rejected due + * to security requirements not being fulfilled, like authentication or pairing. + */ + BT_CTRL_CONNECTION_REJECTED_DUE_TO_SECURITY_REASONS = 0x100E, + /** + * The Connection was rejected because this device does not accept the BD_ADDR. + * This may be because the device will only accept connections from specific BD_ADDRs. + */ + BT_CTRL_CONNECTION_REJECTED_DUE_TO_UNACCEPTABLE_BD_ADDR = 0x100F, + /** The Connection Accept Timeout has been exceeded for this connection attempt. */ + BT_CTRL_CONNECTION_ACCEPT_TIMEOUT_EXCEEDED = 0x1010, + /** A feature or parameter value in the HCI command is not supported. */ + BT_CTRL_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE = 0x1011, + /** Command contained invalid parameters. */ + BT_CTRL_INVALID_COMMAND_PARAMETERS = 0x1012, + /** User on the remote device terminated the connection. */ + BT_CTRL_REMOTE_USER_TERMINATED = 0x1013, + /** The remote device terminated the connection because of low resources */ + BT_CTRL_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES = 0x1014, + /** Remote Device Terminated Connection due to Power Off */ + BT_CTRL_REMOTE_POWERING_OFF = 0x1015, + /** Local device terminated the connection. */ + BT_CTRL_CONNECTION_TERMINATED_BY_LOCAL_HOST = 0x1016, + /** + * The Controller is disallowing an authentication or pairing procedure because too little time has elapsed + * since the last authentication or pairing attempt failed. + */ + BT_CTRL_REPEATED_ATTEMPTS = 0x1017, + /** + * The device does not allow pairing. This can be for example, when a device only allows pairing during + * a certain time window after some user input allows pairing + */ + BT_CTRL_PAIRING_NOT_ALLOWED = 0x1018, + /** The remote device does not support the feature associated with the issued command. */ + BT_CTRL_UNSUPPORTED_REMOTE_FEATURE = 0x101A, + /** No other error code specified is appropriate to use. */ + BT_CTRL_UNSPECIFIED_ERROR = 0x101F, + /** Connection terminated due to link-layer procedure timeout. */ + BT_CTRL_LL_RESPONSE_TIMEOUT = 0x1022, + /** LL procedure has collided with the same transaction or procedure that is already in progress. */ + BT_CTRL_LL_PROCEDURE_COLLISION = 0x1023, + /** The requested encryption mode is not acceptable at this time. */ + BT_CTRL_ENCRYPTION_MODE_NOT_ACCEPTABLE = 0x1025, + /** Link key cannot be changed because a fixed unit key is being used. */ + BT_CTRL_LINK_KEY_CANNOT_BE_CHANGED = 0x1026, + /** LMP PDU or LL PDU that includes an instant cannot be performed because the instant when this would have occurred has passed. */ + BT_CTRL_INSTANT_PASSED = 0x1028, + /** It was not possible to pair as a unit key was requested and it is not supported. */ + BT_CTRL_PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED = 0x1029, + /** LMP transaction was started that collides with an ongoing transaction. */ + BT_CTRL_DIFFERENT_TRANSACTION_COLLISION = 0x102A, + /** The Controller cannot perform channel assessment because it is not supported. */ + BT_CTRL_CHANNEL_ASSESSMENT_NOT_SUPPORTED = 0x102E, + /** The HCI command or LMP PDU sent is only possible on an encrypted link. */ + BT_CTRL_INSUFFICIENT_SECURITY = 0x102F, + /** A parameter value requested is outside the mandatory range of parameters for the given HCI command or LMP PDU. */ + BT_CTRL_PARAMETER_OUT_OF_MANDATORY_RANGE = 0x1030, + /** + * The IO capabilities request or response was rejected because the sending Host does not support + * Secure Simple Pairing even though the receiving Link Manager does. + */ + BT_CTRL_SIMPLE_PAIRING_NOT_SUPPORTED_BY_HOST = 0x1037, + /** + * The Host is busy with another pairing operation and unable to support the requested pairing. + * The receiving device should retry pairing again later. + */ + BT_CTRL_HOST_BUSY_PAIRING = 0x1038, + /** The Controller could not calculate an appropriate value for the Channel selection operation. */ + BT_CTRL_CONNECTION_REJECTED_DUE_TO_NO_SUITABLE_CHANNEL_FOUND = 0x1039, + /** Operation was rejected because the controller is busy and unable to process the request. */ + BT_CTRL_CONTROLLER_BUSY = 0x103A, + /** Remote device terminated the connection because of an unacceptable connection interval. */ + BT_CTRL_UNACCEPTABLE_CONNECTION_INTERVAL = 0x103B, + /** Advertising for a fixed duration completed or, for directed advertising, that advertising completed without a connection being created. */ + BT_CTRL_ADVERTISING_TIMEOUT = 0x103C, + /** Connection was terminated because the Message Integrity Check (MIC) failed on a received packet. */ + BT_CTRL_CONNECTION_TERMINATED_DUE_TO_MIC_FAILURE = 0x103D, + /** LL initiated a connection but the connection has failed to be established. Controller did not receive any packets from remote end. */ + BT_CTRL_CONNECTION_FAILED_TO_BE_ESTABLISHED = 0x103E, + /** The MAC of the 802.11 AMP was requested to connect to a peer, but the connection failed. */ + BT_CTRL_MAC_CONNECTION_FAILED = 0x103F, + /** + * The master, at this time, is unable to make a coarse adjustment to the piconet clock, using the supplied parameters. + * Instead the master will attempt to move the clock using clock dragging. + */ + BT_CTRL_COARSE_CLOCK_ADJUSTMENT_REJECTED_BUT_WILL_TRY_TO_ADJUST_USING_CLOCK_DRAGGING = 0x1040, + /** A command was sent from the Host that should identify an Advertising or Sync handle, but the Advertising or Sync handle does not exist. */ + BT_CTRL_UNKNOWN_ADVERTISING_IDENTIFIER = 0x1042, + /** Number of operations requested has been reached and has indicated the completion of the activity (e.g., advertising or scanning). */ + BT_CTRL_LIMIT_REACHED = 0x1043, + /** A request to the Controller issued by the Host and still pending was successfully canceled. */ + BT_CTRL_OPERATION_CANCELLED_BY_HOST = 0x1044, + /** An attempt was made to send or receive a packet that exceeds the maximum allowed packet l */ + BT_CTRL_PACKET_TOO_LONG = 0x1045, + + // ----------------------------------------------------------------------------- + // Bluetooth attribute status codes + + /** The attribute handle given was not valid on this server */ + BT_ATT_INVALID_HANDLE = 0x1101, + /** The attribute cannot be read */ + BT_ATT_READ_NOT_PERMITTED = 0x1102, + /** The attribute cannot be written */ + BT_ATT_WRITE_NOT_PERMITTED = 0x1103, + /** The attribute PDU was invalid */ + BT_ATT_INVALID_PDU = 0x1104, + /** The attribute requires authentication before it can be read or written. */ + BT_ATT_INSUFFICIENT_AUTHENTICATION = 0x1105, + /** Attribute Server does not support the request received from the client. */ + BT_ATT_REQUEST_NOT_SUPPORTED = 0x1106, + /** Offset specified was past the end of the attribute */ + BT_ATT_INVALID_OFFSET = 0x1107, + /** The attribute requires authorization before it can be read or written. */ + BT_ATT_INSUFFICIENT_AUTHORIZATION = 0x1108, + /** Too many prepare writes have been queued */ + BT_ATT_PREPARE_QUEUE_FULL = 0x1109, + /** No attribute found within the given attribute handle range. */ + BT_ATT_ATT_NOT_FOUND = 0x110A, + /** The attribute cannot be read or written using the Read Blob Request */ + BT_ATT_ATT_NOT_LONG = 0x110B, + /** The Encryption Key Size used for encrypting this link is insufficient. */ + BT_ATT_INSUFFICIENT_ENC_KEY_SIZE = 0x110C, + /** The attribute value length is invalid for the operation */ + BT_ATT_INVALID_ATT_LENGTH = 0x110D, + /** The attribute request that was requested has encountered an error that was unlikely, and therefore could not be completed as requested. */ + BT_ATT_UNLIKELY_ERROR = 0x110E, + /** The attribute requires encryption before it can be read or written. */ + BT_ATT_INSUFFICIENT_ENCRYPTION = 0x110F, + /** The attribute type is not a supported grouping attribute as defined by a higher layer specification. */ + BT_ATT_UNSUPPORTED_GROUP_TYPE = 0x1110, + /** Insufficient Resources to complete the request */ + BT_ATT_INSUFFICIENT_RESOURCES = 0x1111, + /** The server requests the client to rediscover the database. */ + BT_ATT_OUT_OF_SYNC = 0x1112, + /** The attribute parameter value was not allowed. */ + BT_ATT_VALUE_NOT_ALLOWED = 0x1113, + /** When this is returned in a BGAPI response, the application tried to read or write the value of a user attribute from the GATT database. */ + BT_ATT_APPLICATION = 0x1180, + /** The requested write operation cannot be fulfilled for reasons other than permissions. */ + BT_ATT_WRITE_REQUEST_REJECTED = 0x11FC, + /** The Client Characteristic Configuration descriptor is not configured according to the requirements of the profile or service. */ + BT_ATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_IMPROPERLY_CONFIGURED = 0x11FD, + /** The profile or service request cannot be serviced because an operation that has been previously triggered is still in progress. */ + BT_ATT_PROCEDURE_ALREADY_IN_PROGRESS = 0x11FE, + /** The attribute value is out of range as defined by a profile or service specification. */ + BT_ATT_OUT_OF_RANGE = 0x11FF, + + // ----------------------------------------------------------------------------- + // Bluetooth Security Manager Protocol status codes + + /** The user input of passkey failed, for example, the user cancelled the operation */ + BT_SMP_PASSKEY_ENTRY_FAILED = 0x1201, + /** Out of Band data is not available for authentication */ + BT_SMP_OOB_NOT_AVAILABLE = 0x1202, + /** The pairing procedure cannot be performed as authentication requirements cannot be met due to IO capabilities of one or both devices */ + BT_SMP_AUTHENTICATION_REQUIREMENTS = 0x1203, + /** The confirm value does not match the calculated compare value */ + BT_SMP_CONFIRM_VALUE_FAILED = 0x1204, + /** Pairing is not supported by the device */ + BT_SMP_PAIRING_NOT_SUPPORTED = 0x1205, + /** The resultant encryption key size is insufficient for the security requirements of this device */ + BT_SMP_ENCRYPTION_KEY_SIZE = 0x1206, + /** The SMP command received is not supported on this device */ + BT_SMP_COMMAND_NOT_SUPPORTED = 0x1207, + /** Pairing failed due to an unspecified reason */ + BT_SMP_UNSPECIFIED_REASON = 0x1208, + /** Pairing or authentication procedure is disallowed because too little time has elapsed since last pairing request or security request */ + BT_SMP_REPEATED_ATTEMPTS = 0x1209, + /** The Invalid Parameters error code indicates: the command length is invalid or a parameter is outside of the specified range. */ + BT_SMP_INVALID_PARAMETERS = 0x120A, + /** Indicates to the remote device that the DHKey Check value received doesn't match the one calculated by the local device. */ + BT_SMP_DHKEY_CHECK_FAILED = 0x120B, + /** Indicates that the confirm values in the numeric comparison protocol do not match. */ + BT_SMP_NUMERIC_COMPARISON_FAILED = 0x120C, + /** Indicates that the pairing over the LE transport failed due to a Pairing Request sent over the BR/EDR transport in process. */ + BT_SMP_BREDR_PAIRING_IN_PROGRESS = 0x120D, + /** Indicates that the BR/EDR Link Key generated on the BR/EDR transport cannot be used to derive and distribute keys for the LE transport. */ + BT_SMP_CROSS_TRANSPORT_KEY_DERIVATION_GENERATION_NOT_ALLOWED = 0x120E, + /** Indicates that the device chose not to accept a distributed key. */ + BT_SMP_KEY_REJECTED = 0x120F, + + // ----------------------------------------------------------------------------- + // Bluetooth Mesh status codes + + /** Returned when trying to add a key or some other unique resource with an ID which already exists */ + BT_MESH_ALREADY_EXISTS = 0x0501, + /** Returned when trying to manipulate a key or some other resource with an ID which does not exist */ + BT_MESH_DOES_NOT_EXIST = 0x0502, + /** + * Returned when an operation cannot be executed because a pre-configured limit for keys, key bindings, + * elements, models, virtual addresses, provisioned devices, or provisioning sessions is reached + */ + BT_MESH_LIMIT_REACHED = 0x0503, + /** Returned when trying to use a reserved address or add a "pre-provisioned" device using an address already used by some other device */ + BT_MESH_INVALID_ADDRESS = 0x0504, + /** In a BGAPI response, the user supplied malformed data; in a BGAPI event, the remote end responded with malformed or unrecognized data */ + BT_MESH_MALFORMED_DATA = 0x0505, + /** An attempt was made to initialize a subsystem that was already initialized. */ + BT_MESH_ALREADY_INITIALIZED = 0x0506, + /** An attempt was made to use a subsystem that wasn't initialized yet. Call the subsystem's init function first. */ + BT_MESH_NOT_INITIALIZED = 0x0507, + /** Returned when trying to establish a friendship as a Low Power Node, but no acceptable friend offer message was received. */ + BT_MESH_NO_FRIEND_OFFER = 0x0508, + /** Provisioning link was unexpectedly closed before provisioning was complete. */ + BT_MESH_PROV_LINK_CLOSED = 0x0509, + /**An unrecognized provisioning PDU was received. */ + BT_MESH_PROV_INVALID_PDU = 0x050A, + /**A provisioning PDU with wrong length or containing field values that are out of bounds was received. */ + BT_MESH_PROV_INVALID_PDU_FORMAT = 0x050B, + /**An unexpected (out of sequence) provisioning PDU was received. */ + BT_MESH_PROV_UNEXPECTED_PDU = 0x050C, + /**The computed confirmation value did not match the expected value. */ + BT_MESH_PROV_CONFIRMATION_FAILED = 0x050D, + /**Provisioning could not be continued due to insufficient resources. */ + BT_MESH_PROV_OUT_OF_RESOURCES = 0x050E, + /**The provisioning data block could not be decrypted. */ + BT_MESH_PROV_DECRYPTION_FAILED = 0x050F, + /**An unexpected error happened during provisioning. */ + BT_MESH_PROV_UNEXPECTED_ERROR = 0x0510, + /**Device could not assign unicast addresses to all of its elements. */ + BT_MESH_PROV_CANNOT_ASSIGN_ADDR = 0x0511, + /**Returned when trying to reuse an address of a previously deleted device before an IV Index Update has been executed. */ + BT_MESH_ADDRESS_TEMPORARILY_UNAVAILABLE = 0x0512, + /**Returned when trying to assign an address that is used by one of the devices in the Device Database, or by the Provisioner itself. */ + BT_MESH_ADDRESS_ALREADY_USED = 0x0513, + /**Application key or publish address are not set */ + BT_MESH_PUBLISH_NOT_CONFIGURED = 0x0514, + /**Application key is not bound to a model */ + BT_MESH_APP_KEY_NOT_BOUND = 0x0515, + + // ----------------------------------------------------------------------------- + // Bluetooth Mesh foundation status codes + + /** Returned when address in request was not valid */ + BT_MESH_FOUNDATION_INVALID_ADDRESS = 0x1301, + /** Returned when model identified is not found for a given element */ + BT_MESH_FOUNDATION_INVALID_MODEL = 0x1302, + /** Returned when the key identified by AppKeyIndex is not stored in the node */ + BT_MESH_FOUNDATION_INVALID_APP_KEY = 0x1303, + /** Returned when the key identified by NetKeyIndex is not stored in the node */ + BT_MESH_FOUNDATION_INVALID_NET_KEY = 0x1304, + /** Returned when The node cannot serve the request due to insufficient resources */ + BT_MESH_FOUNDATION_INSUFFICIENT_RESOURCES = 0x1305, + /** Returned when the key identified is already stored in the node and the new NetKey value is different */ + BT_MESH_FOUNDATION_KEY_INDEX_EXISTS = 0x1306, + /** Returned when the model does not support the publish mechanism */ + BT_MESH_FOUNDATION_INVALID_PUBLISH_PARAMS = 0x1307, + /** Returned when the model does not support the subscribe mechanism */ + BT_MESH_FOUNDATION_NOT_SUBSCRIBE_MODEL = 0x1308, + /** Returned when storing of the requested parameters failed */ + BT_MESH_FOUNDATION_STORAGE_FAILURE = 0x1309, + /**Returned when requested setting is not supported */ + BT_MESH_FOUNDATION_NOT_SUPPORTED = 0x130A, + /**Returned when the requested update operation cannot be performed due to general constraints */ + BT_MESH_FOUNDATION_CANNOT_UPDATE = 0x130B, + /**Returned when the requested delete operation cannot be performed due to general constraints */ + BT_MESH_FOUNDATION_CANNOT_REMOVE = 0x130C, + /**Returned when the requested bind operation cannot be performed due to general constraints */ + BT_MESH_FOUNDATION_CANNOT_BIND = 0x130D, + /**Returned when The node cannot start advertising with Node Identity or Proxy since the maximum number of parallel advertising is reached */ + BT_MESH_FOUNDATION_TEMPORARILY_UNABLE = 0x130E, + /**Returned when the requested state cannot be set */ + BT_MESH_FOUNDATION_CANNOT_SET = 0x130F, + /**Returned when an unspecified error took place */ + BT_MESH_FOUNDATION_UNSPECIFIED = 0x1310, + /**Returned when the NetKeyIndex and AppKeyIndex combination is not valid for a Config AppKey Update */ + BT_MESH_FOUNDATION_INVALID_BINDING = 0x1311, + + // ----------------------------------------------------------------------------- + // Wi-Fi Errors + + /** Invalid firmware keyset */ + WIFI_INVALID_KEY = 0x0B01, + /** The firmware download took too long */ + WIFI_FIRMWARE_DOWNLOAD_TIMEOUT = 0x0B02, + /** Unknown request ID or wrong interface ID used */ + WIFI_UNSUPPORTED_MESSAGE_ID = 0x0B03, + /** The request is successful but some parameters have been ignored */ + WIFI_WARNING = 0x0B04, + /** No Packets waiting to be received */ + WIFI_NO_PACKET_TO_RECEIVE = 0x0B05, + /** The sleep mode is granted */ + WIFI_SLEEP_GRANTED = 0x0B08, + /** The WFx does not go back to sleep */ + WIFI_SLEEP_NOT_GRANTED = 0x0B09, + /** The SecureLink MAC key was not found */ + WIFI_SECURE_LINK_MAC_KEY_ERROR = 0x0B10, + /** The SecureLink MAC key is already installed in OTP */ + WIFI_SECURE_LINK_MAC_KEY_ALREADY_BURNED = 0x0B11, + /** The SecureLink MAC key cannot be installed in RAM */ + WIFI_SECURE_LINK_RAM_MODE_NOT_ALLOWED = 0x0B12, + /** The SecureLink MAC key installation failed */ + WIFI_SECURE_LINK_FAILED_UNKNOWN_MODE = 0x0B13, + /** SecureLink key (re)negotiation failed */ + WIFI_SECURE_LINK_EXCHANGE_FAILED = 0x0B14, + /** The device is in an inappropriate state to perform the request */ + WIFI_WRONG_STATE = 0x0B18, + /** The request failed due to regulatory limitations */ + WIFI_CHANNEL_NOT_ALLOWED = 0x0B19, + /** The connection request failed because no suitable AP was found */ + WIFI_NO_MATCHING_AP = 0x0B1A, + /** The connection request was aborted by host */ + WIFI_CONNECTION_ABORTED = 0x0B1B, + /** The connection request failed because of a timeout */ + WIFI_CONNECTION_TIMEOUT = 0x0B1C, + /** The connection request failed because the AP rejected the device */ + WIFI_CONNECTION_REJECTED_BY_AP = 0x0B1D, + /** The connection request failed because the WPA handshake did not complete successfully */ + WIFI_CONNECTION_AUTH_FAILURE = 0x0B1E, + /** The request failed because the retry limit was exceeded */ + WIFI_RETRY_EXCEEDED = 0x0B1F, + /** The request failed because the MSDU life time was exceeded */ + WIFI_TX_LIFETIME_EXCEEDED = 0x0B20, + + // ----------------------------------------------------------------------------- + // MVP Driver and MVP Math status codes + + /** Critical fault */ + COMPUTE_DRIVER_FAULT = 0x1501, + /** ALU operation output NaN */ + COMPUTE_DRIVER_ALU_NAN = 0x1502, + /** ALU numeric overflow */ + COMPUTE_DRIVER_ALU_OVERFLOW = 0x1503, + /** ALU numeric underflow */ + COMPUTE_DRIVER_ALU_UNDERFLOW = 0x1504, + /** Overflow during array store */ + COMPUTE_DRIVER_STORE_CONVERSION_OVERFLOW = 0x1505, + /** Underflow during array store conversion */ + COMPUTE_DRIVER_STORE_CONVERSION_UNDERFLOW = 0x1506, + /** Infinity encountered during array store conversion */ + COMPUTE_DRIVER_STORE_CONVERSION_INFINITY = 0x1507, + /** NaN encountered during array store conversion */ + COMPUTE_DRIVER_STORE_CONVERSION_NAN = 0x1508, + + /** MATH NaN encountered */ + COMPUTE_MATH_NAN = 0x1512, + /** MATH Infinity encountered */ + COMPUTE_MATH_INFINITY = 0x1513, + /** MATH numeric overflow */ + COMPUTE_MATH_OVERFLOW = 0x1514, + /** MATH numeric underflow */ + COMPUTE_MATH_UNDERFLOW = 0x1515, +}; + +/** + * Many EmberZNet API functions return an ::EmberStatus value to indicate the success or failure of the call. + * Return codes are one byte long. + */ +export enum EmberStatus { + // Generic Messages. These messages are system wide. + /** The generic "no error" message. */ + SUCCESS = 0x00, + /** The generic "fatal error" message. */ + ERR_FATAL = 0x01, + /** An invalid value was passed as an argument to a function. */ + BAD_ARGUMENT = 0x02, + /** The requested information was not found. */ + NOT_FOUND = 0x03, + /** The manufacturing and stack token format in non-volatile memory is different than what the stack expects (returned at initialization). */ + EEPROM_MFG_STACK_VERSION_MISMATCH = 0x04, + /** The manufacturing token format in non-volatile memory is different than what the stack expects (returned at initialization). */ + EEPROM_MFG_VERSION_MISMATCH = 0x06, + /** The stack token format in non-volatile memory is different than what the stack expects (returned at initialization). */ + EEPROM_STACK_VERSION_MISMATCH = 0x07, + + // Packet Buffer Module Errors + /** There are no more buffers. */ + NO_BUFFERS = 0x18, + /** Packet is dropped by packet-handoff callbacks. */ + PACKET_HANDOFF_DROP_PACKET = 0x19, + + // Serial Manager Errors + /** Specifies an invalid baud rate. */ + SERIAL_INVALID_BAUD_RATE = 0x20, + /** Specifies an invalid serial port. */ + SERIAL_INVALID_PORT = 0x21, + /** Tried to send too much data. */ + SERIAL_TX_OVERFLOW = 0x22, + /** There wasn't enough space to store a received character and the character was dropped. */ + SERIAL_RX_OVERFLOW = 0x23, + /** Detected a UART framing error. */ + SERIAL_RX_FRAME_ERROR = 0x24, + /** Detected a UART parity error. */ + SERIAL_RX_PARITY_ERROR = 0x25, + /** There is no received data to process. */ + SERIAL_RX_EMPTY = 0x26, + /** The receive interrupt was not handled in time and a character was dropped. */ + SERIAL_RX_OVERRUN_ERROR = 0x27, + + // MAC Errors + /** The MAC transmit queue is full. */ + MAC_TRANSMIT_QUEUE_FULL = 0x39, + // Internal + /** MAC header FCF error on receive. */ + MAC_UNKNOWN_HEADER_TYPE = 0x3A, + /** MAC ACK header received. */ + MAC_ACK_HEADER_TYPE = 0x3B, + /** The MAC can't complete this task because it is scanning. */ + MAC_SCANNING = 0x3D, + /** No pending data exists for a data poll. */ + MAC_NO_DATA = 0x31, + /** Attempts to scan when joined to a network. */ + MAC_JOINED_NETWORK = 0x32, + /** Scan duration must be 0 to 14 inclusive. Tried to scan with an incorrect duration value. */ + MAC_BAD_SCAN_DURATION = 0x33, + /** emberStartScan was called with an incorrect scan type. */ + MAC_INCORRECT_SCAN_TYPE = 0x34, + /** emberStartScan was called with an invalid channel mask. */ + MAC_INVALID_CHANNEL_MASK = 0x35, + /** Failed to scan the current channel because the relevant MAC command could not be transmitted. */ + MAC_COMMAND_TRANSMIT_FAILURE = 0x36, + /** An ACK was expected following the transmission but the MAC level ACK was never received. */ + MAC_NO_ACK_RECEIVED = 0x40, + /** MAC failed to transmit a message because it could not successfully perform a radio network switch. */ + MAC_RADIO_NETWORK_SWITCH_FAILED = 0x41, + /** An indirect data message timed out before a poll requested it. */ + MAC_INDIRECT_TIMEOUT = 0x42, + + // Simulated EEPROM Errors + /** + * The Simulated EEPROM is telling the application that at least one flash page to be erased. + * The GREEN status means the current page has not filled above the ::ERASE_CRITICAL_THRESHOLD. + * + * The application should call the function ::halSimEepromErasePage() when it can to erase a page. + */ + SIM_EEPROM_ERASE_PAGE_GREEN = 0x43, + /** + * The Simulated EEPROM is telling the application that at least one flash page must be erased. + * The RED status means the current page has filled above the ::ERASE_CRITICAL_THRESHOLD. + * + * Due to the shrinking availability of write space, data could be lost. + * The application must call the function ::halSimEepromErasePage() as soon as possible to erase a page. + */ + SIM_EEPROM_ERASE_PAGE_RED = 0x44, + /** + * The Simulated EEPROM has run out of room to write new data and the data trying to be set has been lost. + * This error code is the result of ignoring the ::SIM_EEPROM_ERASE_PAGE_RED error code. + * + * The application must call the function ::halSimEepromErasePage() to make room for any further calls to set a token. + */ + SIM_EEPROM_FULL = 0x45, + // Errors 46 and 47 are now defined below in the flash error block (was attempting to prevent renumbering). + /** + * Attempt 1 to initialize the Simulated EEPROM has failed. + * + * This failure means the information already stored in the Flash (or a lack thereof), + * is fatally incompatible with the token information compiled into the code image being run. + */ + SIM_EEPROM_INIT_1_FAILED = 0x48, + /** + * Attempt 2 to initialize the Simulated EEPROM has failed. + * + * This failure means Attempt 1 failed, and the token system failed to properly reload default tokens and reset the Simulated EEPROM. + */ + SIM_EEPROM_INIT_2_FAILED = 0x49, + /** + * Attempt 3 to initialize the Simulated EEPROM has failed. + * + * This failure means one or both of the tokens ::TOKEN_MFG_NVDATA_VERSION or ::TOKEN_STACK_NVDATA_VERSION + * were incorrect and the token system failed to properly reload default tokens and reset the Simulated EEPROM. + */ + SIM_EEPROM_INIT_3_FAILED = 0x4A, + /** + * The Simulated EEPROM is repairing itself. + * + * While there's nothing for an app to do when the SimEE is going to + * repair itself (SimEE has to be fully functional for the rest of the + * system to work), alert the application to the fact that repair + * is occurring. There are debugging scenarios where an app might want + * to know that repair is happening, such as monitoring frequency. + * @note Common situations will trigger an expected repair, such as + * using an erased chip or changing token definitions. + */ + SIM_EEPROM_REPAIRING = 0x4D, + + // Flash Errors + /** + * A fatal error has occurred while trying to write data to the Flash. + * The target memory attempting to be programmed is already programmed. + * The flash write routines were asked to flip a bit from a 0 to 1, + * which is physically impossible and the write was therefore inhibited. + * The data in the Flash cannot be trusted after this error. + */ + ERR_FLASH_WRITE_INHIBITED = 0x46, + /** + * A fatal error has occurred while trying to write data to the Flash and the write verification has failed. + * Data in the Flash cannot be trusted after this error and it is possible this error is the result of exceeding the life cycles of the Flash. + */ + ERR_FLASH_VERIFY_FAILED = 0x47, + /** + * A fatal error has occurred while trying to write data to the Flash possibly due to write protection or an invalid address. + * Data in the Flash cannot be trusted after this error and it is possible this error is the result of exceeding the life cycles of the Flash. + */ + ERR_FLASH_PROG_FAIL = 0x4B, + /** + * A fatal error has occurred while trying to erase the Flash possibly due to write protection. + * Data in the Flash cannot be trusted after this error and it is possible this error is the result of exceeding the life cycles of the Flash. + */ + ERR_FLASH_ERASE_FAIL = 0x4C, + + // Bootloader Errors + /** The bootloader received an invalid message (failed attempt to go into bootloader). */ + ERR_BOOTLOADER_TRAP_TABLE_BAD = 0x58, + /** The bootloader received an invalid message (failed attempt to go into the bootloader). */ + ERR_BOOTLOADER_TRAP_UNKNOWN = 0x59, + /** The bootloader cannot complete the bootload operation because either an image was not found or the image exceeded memory bounds. */ + ERR_BOOTLOADER_NO_IMAGE = 0x05A, + + // Transport Errors + /** The APS layer attempted to send or deliver a message and failed. */ + DELIVERY_FAILED = 0x66, + /** This binding index is out of range for the current binding table. */ + BINDING_INDEX_OUT_OF_RANGE = 0x69, + /** This address table index is out of range for the current address table. */ + ADDRESS_TABLE_INDEX_OUT_OF_RANGE = 0x6A, + /** An invalid binding table index was given to a function. */ + INVALID_BINDING_INDEX = 0x6C, + /** The API call is not allowed given the current state of the stack. */ + INVALID_CALL = 0x70, + /** The link cost to a node is not known. */ + COST_NOT_KNOWN = 0x71, + /** The maximum number of in-flight messages = i.e., ::EMBER_APS_UNICAST_MESSAGE_COUNT, has been reached. */ + MAX_MESSAGE_LIMIT_REACHED = 0x72, + /** The message to be transmitted is too big to fit into a single over-the-air packet. */ + MESSAGE_TOO_LONG = 0x74, + /** The application is trying to delete or overwrite a binding that is in use. */ + BINDING_IS_ACTIVE = 0x75, + /** The application is trying to overwrite an address table entry that is in use. */ + ADDRESS_TABLE_ENTRY_IS_ACTIVE = 0x76, + /** An attempt was made to transmit during the suspend period. */ + TRANSMISSION_SUSPENDED = 0x77, + + // Green Power status codes + /** Security match. */ + MATCH = 0x78, + /** Drop frame. */ + DROP_FRAME = 0x79, + /** */ + PASS_UNPROCESSED = 0x7A, + /** */ + TX_THEN_DROP = 0x7B, + /** */ + NO_SECURITY = 0x7C, + /** */ + COUNTER_FAILURE = 0x7D, + /** */ + AUTH_FAILURE = 0x7E, + /** */ + UNPROCESSED = 0x7F, + + // HAL Module Errors + /** The conversion is complete. */ + ADC_CONVERSION_DONE = 0x80, + /** The conversion cannot be done because a request is being processed. */ + ADC_CONVERSION_BUSY = 0x81, + /** The conversion is deferred until the current request has been processed. */ + ADC_CONVERSION_DEFERRED = 0x82, + /** No results are pending. */ + ADC_NO_CONVERSION_PENDING = 0x84, + /** Sleeping (for a duration) has been abnormally interrupted and exited prematurely. */ + SLEEP_INTERRUPTED = 0x85, + + // PHY Errors + /** + * The transmit attempt failed because the radio scheduler could not find a slot + * to transmit this packet in or a higher priority event interrupted it. + */ + PHY_TX_SCHED_FAIL = 0x87, + /** The transmit hardware buffer underflowed. */ + PHY_TX_UNDERFLOW = 0x88, + /** The transmit hardware did not finish transmitting a packet. */ + PHY_TX_INCOMPLETE = 0x89, + /** An unsupported channel setting was specified. */ + PHY_INVALID_CHANNEL = 0x8A, + /** An unsupported power setting was specified. */ + PHY_INVALID_POWER = 0x8B, + /** The requested operation cannot be completed because the radio is currently busy, either transmitting a packet or performing calibration. */ + PHY_TX_BUSY = 0x8C, + /** The transmit attempt failed because all CCA attempts indicated that the channel was busy. */ + PHY_TX_CCA_FAIL = 0x8D, + /** + * The transmit attempt was blocked from going over the air. + * Typically this is due to the Radio Hold Off (RHO) or Coexistence plugins as they can prevent transmits based on external signals. + */ + PHY_TX_BLOCKED = 0x8E, + /** The expected ACK was received after the last transmission. */ + PHY_ACK_RECEIVED = 0x8F, + + // Return Codes Passed to emberStackStatusHandler() See also ::emberStackStatusHandler = ,. + /** The stack software has completed initialization and is ready to send and receive packets over the air. */ + NETWORK_UP = 0x90, + /** The network is not operating. */ + NETWORK_DOWN = 0x91, + /** An attempt to join a network failed. */ + JOIN_FAILED = 0x94, + /** After moving, a mobile node's attempt to re-establish contact with the network failed. */ + MOVE_FAILED = 0x96, + /** + * An attempt to join as a router failed due to a Zigbee versus Zigbee Pro incompatibility. + * Zigbee devices joining Zigbee Pro networks (or vice versa) must join as End Devices, not Routers. + */ + CANNOT_JOIN_AS_ROUTER = 0x98, + /** The local node ID has changed. The application can get the new node ID by calling ::emberGetNodeId(). */ + NODE_ID_CHANGED = 0x99, + /** The local PAN ID has changed. The application can get the new PAN ID by calling ::emberGetPanId(). */ + PAN_ID_CHANGED = 0x9A, + /** The channel has changed. */ + CHANNEL_CHANGED = 0x9B, + /** The network has been opened for joining. */ + NETWORK_OPENED = 0x9C, + /** The network has been closed for joining. */ + NETWORK_CLOSED = 0x9D, + /** An attempt to join or rejoin the network failed because no router beacons could be heard by the joining node. */ + NO_BEACONS = 0xAB, + /** + * An attempt was made to join a Secured Network using a pre-configured key, but the Trust Center sent back a + * Network Key in-the-clear when an encrypted Network Key was required. (::EMBER_REQUIRE_ENCRYPTED_KEY). + */ + RECEIVED_KEY_IN_THE_CLEAR = 0xAC, + /** An attempt was made to join a Secured Network, but the device did not receive a Network Key. */ + NO_NETWORK_KEY_RECEIVED = 0xAD, + /** After a device joined a Secured Network, a Link Key was requested (::EMBER_GET_LINK_KEY_WHEN_JOINING) but no response was ever received. */ + NO_LINK_KEY_RECEIVED = 0xAE, + /** + * An attempt was made to join a Secured Network without a pre-configured key, + * but the Trust Center sent encrypted data using a pre-configured key. + */ + PRECONFIGURED_KEY_REQUIRED = 0xAF, + + // Security Errors + /** The passed key data is not valid. A key of all zeros or all F's are reserved values and cannot be used. */ + KEY_INVALID = 0xB2, + /** The chosen security level (the value of ::EMBER_SECURITY_LEVEL) is not supported by the stack. */ + INVALID_SECURITY_LEVEL = 0x95, + /** + * An error occurred when trying to encrypt at the APS Level. + * + * To APS encrypt an outgoing packet, the sender + * needs to know the EUI64 of the destination. This error occurs because + * the EUI64 of the destination can't be determined from + * the short address (no entry in the neighbor, child, binding + * or address tables). + * + * Every time this error code is seen, note that the stack initiates an + * IEEE address discovery request behind the scenes. Responses + * to the request are stored in the trust center cache portion of the + * address table. Note that you need at least 1 entry allocated for + * TC cache in the address table plugin. Depending on the available rows in + * the table, newly discovered addresses may replace old ones. The address + * table plugin is enabled by default on the host. If you are using an SoC + * platform, please be sure to add the address table plugin. + * + * When customers choose to send APS messages by using short addresses, + * they should incorporate a retry mechanism and try again, no sooner than + * 2 seconds later, to resend the APS message. If the app always + * receives 0xBE (IEEE_ADDRESS_DISCOVERY_IN_PROGRESS) after + * multiple retries, that might indicate that: + * a) destination node is not on the network + * b) there are problems with the health of the network + * c) there may not be any space set aside in the address table for + * the newly discovered address - this can be rectified by reserving + * more entries for the trust center cache in the address table plugin + */ + IEEE_ADDRESS_DISCOVERY_IN_PROGRESS = 0xBE, + /** + * An error occurred when trying to encrypt at the APS Level. + * + * This error occurs either because the long address of the recipient can't be + * determined from the short address (no entry in the binding table) + * or there is no link key entry in the table associated with the destination, + * or there was a failure to load the correct key into the encryption core. + */ + APS_ENCRYPTION_ERROR = 0xA6, + /** There was an attempt to form or join a network with security without calling ::emberSetInitialSecurityState() first. */ + SECURITY_STATE_NOT_SET = 0xA8, + /** + * There was an attempt to set an entry in the key table using an invalid long address. Invalid addresses include: + * - The local device's IEEE address + * - Trust Center's IEEE address + * - An existing table entry's IEEE address + * - An address consisting of all zeros or all F's + */ + KEY_TABLE_INVALID_ADDRESS = 0xB3, + /** There was an attempt to set a security configuration that is not valid given the other security settings. */ + SECURITY_CONFIGURATION_INVALID = 0xB7, + /** + * There was an attempt to broadcast a key switch too quickly after broadcasting the next network key. + * The Trust Center must wait at least a period equal to the broadcast timeout so that all routers have a chance + * to receive the broadcast of the new network key. + */ + TOO_SOON_FOR_SWITCH_KEY = 0xB8, + /** The received signature corresponding to the message that was passed to the CBKE Library failed verification and is not valid. */ + SIGNATURE_VERIFY_FAILURE = 0xB9, + /** + * The message could not be sent because the link key corresponding to the destination is not authorized for use in APS data messages. + * APS Commands (sent by the stack) are allowed. + * To use it for encryption of APS data messages it must be authorized using a key agreement protocol (such as CBKE). + */ + KEY_NOT_AUTHORIZED = 0xBB, + /** The security data provided was not valid, or an integrity check failed. */ + SECURITY_DATA_INVALID = 0xBD, + + // Miscellaneous Network Errors + /** The node has not joined a network. */ + NOT_JOINED = 0x93, + /** A message cannot be sent because the network is currently overloaded. */ + NETWORK_BUSY = 0xA1, + /** The application tried to send a message using an endpoint that it has not defined. */ + INVALID_ENDPOINT = 0xA3, + /** The application tried to use a binding that has been remotely modified and the change has not yet been reported to the application. */ + BINDING_HAS_CHANGED = 0xA4, + /** An attempt to generate random bytes failed because of insufficient random data from the radio. */ + INSUFFICIENT_RANDOM_DATA = 0xA5, + /** A Zigbee route error command frame was received indicating that a source routed message from this node failed en route. */ + SOURCE_ROUTE_FAILURE = 0xA9, + /** + * A Zigbee route error command frame was received indicating that a message sent to this node along a many-to-one route failed en route. + * The route error frame was delivered by an ad-hoc search for a functioning route. + */ + MANY_TO_ONE_ROUTE_FAILURE = 0xAA, + + // Miscellaneous Utility Errors + /** + * A critical and fatal error indicating that the version of the + * stack trying to run does not match with the chip it's running on. The + * software (stack) on the chip must be replaced with software + * compatible with the chip. + */ + STACK_AND_HARDWARE_MISMATCH = 0xB0, + /** An index was passed into the function that was larger than the valid range. */ + INDEX_OUT_OF_RANGE = 0xB1, + /** There are no empty entries left in the table. */ + TABLE_FULL = 0xB4, + /** The requested table entry has been erased and contains no valid data. */ + TABLE_ENTRY_ERASED = 0xB6, + /** The requested function cannot be executed because the library that contains the necessary functionality is not present. */ + LIBRARY_NOT_PRESENT = 0xB5, + /** The stack accepted the command and is currently processing the request. The results will be returned via an appropriate handler. */ + OPERATION_IN_PROGRESS = 0xBA, + /** + * The EUI of the Trust center has changed due to a successful rejoin. + * The device may need to perform other authentication to verify the new TC is authorized to take over. + */ + TRUST_CENTER_EUI_HAS_CHANGED = 0xBC, + /** Trust center swapped out. The EUI has changed. */ + TRUST_CENTER_SWAPPED_OUT_EUI_HAS_CHANGED = TRUST_CENTER_EUI_HAS_CHANGED, + /** Trust center swapped out. The EUI has not changed. */ + TRUST_CENTER_SWAPPED_OUT_EUI_HAS_NOT_CHANGED = 0xBF, + + // NVM3 Token Errors + /** NVM3 is telling the application that the initialization was aborted as no valid NVM3 page was found. */ + NVM3_TOKEN_NO_VALID_PAGES = 0xC0, + /** NVM3 is telling the application that the initialization was aborted as the NVM3 instance was already opened with other parameters. */ + NVM3_ERR_OPENED_WITH_OTHER_PARAMETERS = 0xC1, + /** NVM3 is telling the application that the initialization was aborted as the NVM3 instance is not aligned properly in memory. */ + NVM3_ERR_ALIGNMENT_INVALID = 0xC2, + /** NVM3 is telling the application that the initialization was aborted as the size of the NVM3 instance is too small. */ + NVM3_ERR_SIZE_TOO_SMALL = 0xC3, + /** NVM3 is telling the application that the initialization was aborted as the NVM3 page size is not supported. */ + NVM3_ERR_PAGE_SIZE_NOT_SUPPORTED = 0xC4, + /** NVM3 is telling the application that there was an error initializing some of the tokens. */ + NVM3_ERR_TOKEN_INIT = 0xC5, + /** NVM3 is telling the application there has been an error when attempting to upgrade SimEE tokens. */ + NVM3_ERR_UPGRADE = 0xC6, + /** NVM3 is telling the application that there has been an unknown error. */ + NVM3_ERR_UNKNOWN = 0xC7, + + // Application Errors. These error codes are available for application use. + /** + * This error is reserved for customer application use. + * This will never be returned from any portion of the network stack or HAL. + */ + APPLICATION_ERROR_0 = 0xF0, + APPLICATION_ERROR_1 = 0xF1, + APPLICATION_ERROR_2 = 0xF2, + APPLICATION_ERROR_3 = 0xF3, + APPLICATION_ERROR_4 = 0xF4, + APPLICATION_ERROR_5 = 0xF5, + APPLICATION_ERROR_6 = 0xF6, + APPLICATION_ERROR_7 = 0xF7, + APPLICATION_ERROR_8 = 0xF8, + APPLICATION_ERROR_9 = 0xF9, + APPLICATION_ERROR_10 = 0xFA, + APPLICATION_ERROR_11 = 0xFB, + APPLICATION_ERROR_12 = 0xFC, + APPLICATION_ERROR_13 = 0xFD, + APPLICATION_ERROR_14 = 0xFE, + APPLICATION_ERROR_15 = 0xFF, +}; + +/** Status values used by EZSP. */ +export enum EzspStatus { + /** Success. */ + SUCCESS = 0x00, + /** Fatal error. */ + SPI_ERR_FATAL = 0x10, + /** The Response frame of the current transaction indicates the NCP has reset. */ + SPI_ERR_NCP_RESET = 0x11, + /** The NCP is reporting that the Command frame of the current transaction is oversized (the length byte is too large). */ + SPI_ERR_OVERSIZED_EZSP_FRAME = 0x12, + /** The Response frame of the current transaction indicates the previous transaction was aborted (nSSEL deasserted too soon). */ + SPI_ERR_ABORTED_TRANSACTION = 0x13, + /** The Response frame of the current transaction indicates the frame terminator is missing from the Command frame. */ + SPI_ERR_MISSING_FRAME_TERMINATOR = 0x14, + /** The NCP has not provided a Response within the time limit defined by WAIT_SECTION_TIMEOUT. */ + SPI_ERR_WAIT_SECTION_TIMEOUT = 0x15, + /** The Response frame from the NCP is missing the frame terminator. */ + SPI_ERR_NO_FRAME_TERMINATOR = 0x16, + /** The Host attempted to send an oversized Command (the length byte is too large) and the AVR's spi-protocol.c blocked the transmission. */ + SPI_ERR_EZSP_COMMAND_OVERSIZED = 0x17, + /** The NCP attempted to send an oversized Response (the length byte is too large) and the AVR's spi-protocol.c blocked the reception. */ + SPI_ERR_EZSP_RESPONSE_OVERSIZED = 0x18, + /** The Host has sent the Command and is still waiting for the NCP to send a Response. */ + SPI_WAITING_FOR_RESPONSE = 0x19, + /** The NCP has not asserted nHOST_INT within the time limit defined by WAKE_HANDSHAKE_TIMEOUT. */ + SPI_ERR_HANDSHAKE_TIMEOUT = 0x1A, + /** The NCP has not asserted nHOST_INT after an NCP reset within the time limit defined by STARTUP_TIMEOUT. */ + SPI_ERR_STARTUP_TIMEOUT = 0x1B, + /** The Host attempted to verify the SPI Protocol activity and version number, and the verification failed. */ + SPI_ERR_STARTUP_FAIL = 0x1C, + /** The Host has sent a command with a SPI Byte that is unsupported by the current mode the NCP is operating in. */ + SPI_ERR_UNSUPPORTED_SPI_COMMAND = 0x1D, + /** Operation not yet complete. */ + ASH_IN_PROGRESS = 0x20, + /** Fatal error detected by host. */ + HOST_FATAL_ERROR = 0x21, + /** Fatal error detected by NCP. */ + ASH_NCP_FATAL_ERROR = 0x22, + /** Tried to send DATA frame too long. */ + DATA_FRAME_TOO_LONG = 0x23, + /** Tried to send DATA frame too short. */ + DATA_FRAME_TOO_SHORT = 0x24, + /** No space for tx'ed DATA frame. */ + NO_TX_SPACE = 0x25, + /** No space for rec'd DATA frame. */ + NO_RX_SPACE = 0x26, + /** No receive data available. */ + NO_RX_DATA = 0x27, + /** Not in Connected state. */ + NOT_CONNECTED = 0x28, + /** The NCP received a command before the EZSP version had been set. */ + ERROR_VERSION_NOT_SET = 0x30, + /** The NCP received a command containing an unsupported frame ID. */ + ERROR_INVALID_FRAME_ID = 0x31, + /** The direction flag in the frame control field was incorrect. */ + ERROR_WRONG_DIRECTION = 0x32, + /** + * The truncated flag in the frame control field was set, indicating there was not enough memory available to + * complete the response or that the response would have exceeded the maximum EZSP frame length. + */ + ERROR_TRUNCATED = 0x33, + /** + * The overflow flag in the frame control field was set, indicating one or more callbacks occurred since the previous + * response and there was not enough memory available to report them to the Host. + */ + ERROR_OVERFLOW = 0x34, + /** Insufficient memory was available. */ + ERROR_OUT_OF_MEMORY = 0x35, + /** The value was out of bounds. */ + ERROR_INVALID_VALUE = 0x36, + /** The configuration id was not recognized. */ + ERROR_INVALID_ID = 0x37, + /** Configuration values can no longer be modified. */ + ERROR_INVALID_CALL = 0x38, + /** The NCP failed to respond to a command. */ + ERROR_NO_RESPONSE = 0x39, + /** The length of the command exceeded the maximum EZSP frame length. */ + ERROR_COMMAND_TOO_LONG = 0x40, + /** The UART receive queue was full causing a callback response to be dropped. */ + ERROR_QUEUE_FULL = 0x41, + /** The command has been filtered out by NCP. */ + ERROR_COMMAND_FILTERED = 0x42, + /** EZSP Security Key is already set */ + ERROR_SECURITY_KEY_ALREADY_SET = 0x43, + /** EZSP Security Type is invalid */ + ERROR_SECURITY_TYPE_INVALID = 0x44, + /** EZSP Security Parameters are invalid */ + ERROR_SECURITY_PARAMETERS_INVALID = 0x45, + /** EZSP Security Parameters are already set */ + ERROR_SECURITY_PARAMETERS_ALREADY_SET = 0x46, + /** EZSP Security Key is not set */ + ERROR_SECURITY_KEY_NOT_SET = 0x47, + /** EZSP Security Parameters are not set */ + ERROR_SECURITY_PARAMETERS_NOT_SET = 0x48, + /** Received frame with unsupported control byte */ + ERROR_UNSUPPORTED_CONTROL = 0x49, + /** Received frame is unsecure, when security is established */ + ERROR_UNSECURE_FRAME = 0x4A, + /** Incompatible ASH version */ + ASH_ERROR_VERSION = 0x50, + /** Exceeded max ACK timeouts */ + ASH_ERROR_TIMEOUTS = 0x51, + /** Timed out waiting for RSTACK */ + ASH_ERROR_RESET_FAIL = 0x52, + /** Unexpected ncp reset */ + ASH_ERROR_NCP_RESET = 0x53, + /** Serial port initialization failed */ + ERROR_SERIAL_INIT = 0x54, + /** Invalid ncp processor type */ + ASH_ERROR_NCP_TYPE = 0x55, + /** Invalid ncp reset method */ + ASH_ERROR_RESET_METHOD = 0x56, + /** XON/XOFF not supported by host driver */ + ASH_ERROR_XON_XOFF = 0x57, + /** ASH protocol started */ + ASH_STARTED = 0x70, + /** ASH protocol connected */ + ASH_CONNECTED = 0x71, + /** ASH protocol disconnected */ + ASH_DISCONNECTED = 0x72, + /** Timer expired waiting for ack */ + ASH_ACK_TIMEOUT = 0x73, + /** Frame in progress cancelled */ + ASH_CANCELLED = 0x74, + /** Received frame out of sequence */ + ASH_OUT_OF_SEQUENCE = 0x75, + /** Received frame with CRC error */ + ASH_BAD_CRC = 0x76, + /** Received frame with comm error */ + ASH_COMM_ERROR = 0x77, + /** Received frame with bad ackNum */ + ASH_BAD_ACKNUM = 0x78, + /** Received frame shorter than minimum */ + ASH_TOO_SHORT = 0x79, + /** Received frame longer than maximum */ + ASH_TOO_LONG = 0x7A, + /** Received frame with illegal control byte */ + ASH_BAD_CONTROL = 0x7B, + /** Received frame with illegal length for its type */ + ASH_BAD_LENGTH = 0x7C, + /** Received ASH Ack */ + ASH_ACK_RECEIVED = 0x7D, + /** Sent ASH Ack */ + ASH_ACK_SENT = 0x7E, + /** Received ASH Nak */ + ASH_NAK_RECEIVED = 0x7F, + /** Sent ASH Nak */ + ASH_NAK_SENT = 0x80, + /** Received ASH RST */ + ASH_RST_RECEIVED = 0x81, + /** Sent ASH RST */ + ASH_RST_SENT = 0x82, + /** ASH Status */ + ASH_STATUS = 0x83, + /** ASH TX */ + ASH_TX = 0x84, + /** ASH RX */ + ASH_RX = 0x85, + /** Failed to connect to CPC daemon or failed to open CPC endpoint */ + CPC_ERROR_INIT = 0x86, + /** No reset or error */ + NO_ERROR = 0xFF +}; + +export enum EmberStackError { + // Error codes that a router uses to notify the message initiator about a broken route. + ROUTE_ERROR_NO_ROUTE_AVAILABLE = 0x00, + ROUTE_ERROR_TREE_LINK_FAILURE = 0x01, + ROUTE_ERROR_NON_TREE_LINK_FAILURE = 0x02, + ROUTE_ERROR_LOW_BATTERY_LEVEL = 0x03, + ROUTE_ERROR_NO_ROUTING_CAPACITY = 0x04, + ROUTE_ERROR_NO_INDIRECT_CAPACITY = 0x05, + ROUTE_ERROR_INDIRECT_TRANSACTION_EXPIRY = 0x06, + ROUTE_ERROR_TARGET_DEVICE_UNAVAILABLE = 0x07, + ROUTE_ERROR_TARGET_ADDRESS_UNALLOCATED = 0x08, + ROUTE_ERROR_PARENT_LINK_FAILURE = 0x09, + ROUTE_ERROR_VALIDATE_ROUTE = 0x0A, + ROUTE_ERROR_SOURCE_ROUTE_FAILURE = 0x0B, + ROUTE_ERROR_MANY_TO_ONE_ROUTE_FAILURE = 0x0C, + ROUTE_ERROR_ADDRESS_CONFLICT = 0x0D, + ROUTE_ERROR_VERIFY_ADDRESSES = 0x0E, + ROUTE_ERROR_PAN_IDENTIFIER_UPDATE = 0x0F, + + NETWORK_STATUS_NETWORK_ADDRESS_UPDATE = 0x10, + NETWORK_STATUS_BAD_FRAME_COUNTER = 0x11, + NETWORK_STATUS_BAD_KEY_SEQUENCE_NUMBER = 0x12, + NETWORK_STATUS_UNKNOWN_COMMAND = 0x13 +} + +/** Type of Ember software version */ +export enum EmberVersionType { + PRE_RELEASE = 0x00, + + // Alpha, should be used rarely + ALPHA_1 = 0x11, + ALPHA_2 = 0x12, + ALPHA_3 = 0x13, + // Leave space in case we decide to add other types in the future. + BETA_1 = 0x21, + BETA_2 = 0x22, + BETA_3 = 0x23, + + // Anything other than 0xAA is considered pre-release + // Silicon Labs may define other types in the future (e.g. beta, alpha) + // Silicon Labs chose an arbitrary number (0xAA) to allow for expansion, but + // to prevent ambiguity in case 0x00 or 0xFF is accidentally retrieved + // as the version type. + GA = 0xAA, +}; + +export enum EmberLeaveRequestFlags { + /** Leave and rejoin. */ + AND_REJOIN = 0x80, + // Note: removeChildren is treated to be deprecated and should not be used! + // CCB 2047 + // - CCB makes the first step to deprecate the 'leave and remove children' functionality. + // - We were proactive here and deprecated it right away. + // AND_REMOVE_CHILDREN = 0x40, + /** Leave. */ + WITHOUT_REJOIN = 0x00, +}; + +/** + * For emberSetTxPowerMode and mfglibSetPower. + * uint16_t + */ +export enum EmberTXPowerMode { + /** + * The application should call ::emberSetTxPowerMode() with the + * txPowerMode parameter set to this value to disable all power mode options, + * resulting in normal power mode and bi-directional RF transmitter output. + */ + DEFAULT = 0x0000, + /** + * The application should call ::emberSetTxPowerMode() with the + * txPowerMode parameter set to this value to enable boost power mode. + */ + BOOST = 0x0001, + /** + * The application should call ::emberSetTxPowerMode() with the + * txPowerMode parameter set to this value to enable the alternate transmitter + * output. + */ + ALTERNATE = 0x0002, + /** + * The application should call ::emberSetTxPowerMode() with the + * txPowerMode parameter set to this value to enable both boost mode and the + * alternate transmitter output. + */ + BOOST_AND_ALTERNATE = 0x0003,// (BOOST | ALTERNATE) + // The application does not ever need to call emberSetTxPowerMode() with the + // txPowerMode parameter set to this value. This value is used internally by + // the stack to indicate that the default token configuration has not been + // overridden by a prior call to emberSetTxPowerMode(). + USE_TOKEN = 0x8000, +} + +/** uint8_t */ +export enum EmberKeepAliveMode { + KEEP_ALIVE_SUPPORT_UNKNOWN = 0x00, + MAC_DATA_POLL_KEEP_ALIVE = 0x01, + END_DEVICE_TIMEOUT_KEEP_ALIVE = 0x02, + KEEP_ALIVE_SUPPORT_ALL = 0x03, +}; + +/** This is the Extended Security Bitmask that controls the use of various extended security features. */ +export enum EmberExtendedSecurityBitmask { + /** + * If this bit is set, the 'key token data' field is set in the Initial Security Bitmask to 0 (No Preconfig Key token). + * Otherwise, the field is left as is. + */ + PRECONFIG_KEY_NOT_VALID = 0x0001, + // bits 2-3 are unused. + /** + * This denotes that the network key update can only happen if the network key update request is unicast and encrypted + * i.e. broadcast network key update requests will not be processed if bit 1 is set + */ + SECURE_NETWORK_KEY_ROTATION = 0x0002, + /** This denotes whether a joiner node (router or end-device) uses a Global Link Key or a Unique Link Key. */ + JOINER_GLOBAL_LINK_KEY = 0x0010, + /** + * This denotes whether the device's outgoing frame counter is allowed to be reset during forming or joining. + * If the flag is set, the outgoing frame counter is not allowed to be reset. + * If the flag is not set, the frame counter is allowed to be reset. + */ + EXT_NO_FRAME_COUNTER_RESET = 0x0020, + /** This denotes whether a device should discard or accept network leave without rejoin commands. */ + NWK_LEAVE_WITHOUT_REJOIN_NOT_ALLOWED = 0x0040, + // Bit 7 reserved for future use (stored in TOKEN). + /** This denotes whether a router node should discard or accept network Leave Commands. */ + NWK_LEAVE_REQUEST_NOT_ALLOWED = 0x0100, + /** + * This denotes whether a node is running the latest stack specification or is emulating R18 specs behavior. + * If this flag is enabled, a router node should only send encrypted Update Device messages while the TC + * should only accept encrypted Updated Device messages. + */ + R18_STACK_BEHAVIOR = 0x0200, + // Bit 10 is reserved for future use (stored in TOKEN). + // Bit 11 is reserved for future use(stored in RAM). + // Bit 12 - This denotes whether an end device should discard or accept ZDO Leave + // from a network node other than its parent. + ZDO_LEAVE_FROM_NON_PARENT_NOT_ALLOWED = 0x1000, + // Bits 13-15 are unused. +}; + +/** This is the Initial Security Bitmask that controls the use of various security features. */ +export enum EmberInitialSecurityBitmask { + /** Enables Distributed Trust Center Mode for the device forming the network. (Previously known as ::EMBER_NO_TRUST_CENTER_MODE) */ + DISTRIBUTED_TRUST_CENTER_MODE = 0x0002, + /** Enables a Global Link Key for the Trust Center. All nodes will share the same Trust Center Link Key. */ + TRUST_CENTER_GLOBAL_LINK_KEY = 0x0004, + /** Enables devices that perform MAC Association with a pre-configured Network Key to join the network. It is only set on the Trust Center. */ + PRECONFIGURED_NETWORK_KEY_MODE = 0x0008, + // Hidden field used internally. + HAVE_TRUST_CENTER_UNKNOWN_KEY_TOKEN = 0x0010, + // Hidden field used internally. + HAVE_TRUST_CENTER_LINK_KEY_TOKEN = 0x0020, + /** + * This denotes that the ::EmberInitialSecurityState::preconfiguredTrustCenterEui64 has a value in it containing the trust center EUI64. + * The device will only join a network and accept commands from a trust center with that EUI64. + * Normally this bit is NOT set and the EUI64 of the trust center is learned during the join process. + * When commissioning a device to join onto an existing network that is using a trust center and without sending any messages, + * this bit must be set and the field ::EmberInitialSecurityState::preconfiguredTrustCenterEui64 must be populated with the appropriate EUI64. + */ + HAVE_TRUST_CENTER_EUI64 = 0x0040, + /** + * This denotes that the ::EmberInitialSecurityState::preconfiguredKey is not the actual Link Key but a Root Key known only to the Trust Center. + * It is hashed with the IEEE Address of the destination device to create the actual Link Key used in encryption. + * This is bit is only used by the Trust Center. The joining device need not set this. + */ + TRUST_CENTER_USES_HASHED_LINK_KEY = 0x0084, + /** + * This denotes that the ::EmberInitialSecurityState::preconfiguredKey element has valid data that should be used to configure + * the initial security state. + */ + HAVE_PRECONFIGURED_KEY = 0x0100, + /** + * This denotes that the ::EmberInitialSecurityState::networkKey element has valid data that should be used to configure + * the initial security state. + */ + HAVE_NETWORK_KEY = 0x0200, + /** + * This denotes to a joining node that it should attempt to acquire a Trust Center Link Key during joining. + * This is necessary if the device does not have a pre-configured key, or wants to obtain a new one + * (since it may be using a well-known key during joining). + */ + GET_LINK_KEY_WHEN_JOINING = 0x0400, + /** + * This denotes that a joining device should only accept an encrypted network key from the Trust Center (using its pre-configured key). + * A key sent in-the-clear by the Trust Center will be rejected and the join will fail. + * This option is only valid when using a pre-configured key. + */ + REQUIRE_ENCRYPTED_KEY = 0x0800, + /** + * This denotes whether the device should NOT reset its outgoing frame counters (both NWK and APS) when + * ::emberSetInitialSecurityState() is called. + * Normally it is advised to reset the frame counter before joining a new network. + * However, when a device is joining to the same network again (but not using ::emberRejoinNetwork()), + * it should keep the NWK and APS frame counters stored in its tokens. + * + * NOTE: The application is allowed to dynamically change the behavior via EMBER_EXT_NO_FRAME_COUNTER_RESET field. + */ + NO_FRAME_COUNTER_RESET = 0x1000, + /** + * This denotes that the device should obtain its pre-configured key from an installation code stored in the manufacturing token. + * The token contains a value that will be hashed to obtain the actual pre-configured key. + * If that token is not valid, the call to ::emberSetInitialSecurityState() will fail. + */ + GET_PRECONFIGURED_KEY_FROM_INSTALL_CODE = 0x2000, + // Internal data + EM_SAVED_IN_TOKEN = 0x4000, + /* All other bits are reserved and must be zero. */ +} + +/** Either marks an event as inactive or specifies the units for the event execution time. uint8_t */ +export enum EmberEventUnits { + /** The event is not scheduled to run. */ + INACTIVE = 0, + /** The execution time is in approximate milliseconds. */ + MS_TIME = 1, + /** The execution time is in 'binary' quarter seconds (256 approximate milliseconds each). */ + QS_TIME = 2, + /** The execution time is in 'binary' minutes (65536 approximate milliseconds each). */ + MINUTE_TIME = 3, + /** The event is scheduled to run at the earliest opportunity. */ + ZERO_DELAY = 4, +}; + +/** + * Defines the events reported to the application + * by the ::emberCounterHandler(). Usage of the destinationNodeId + * or data fields found in the EmberCounterInfo or EmberExtraCounterInfo + * structs is denoted for counter types that use them. (See comments + * accompanying enum definitions in this source file for details.) + */ +export enum EmberCounterType { + /** The MAC received a broadcast Data frame, Command frame, or Beacon + * destinationNodeId: BROADCAST_ADDRESS or Data frames or + * sender node ID for Beacon frames + * data: not used + */ + MAC_RX_BROADCAST = 0, + /** The MAC transmitted a broadcast Data frame, Command frame or Beacon. + * destinationNodeId: BROADCAST_ADDRESS + * data: not used + */ + MAC_TX_BROADCAST = 1, + /** The MAC received a unicast Data or Command frame + * destinationNodeId: MAC layer source or EMBER_UNKNOWN_NODE_ID + * if no 16-bit source node ID is present in the frame + * data: not used + */ + MAC_RX_UNICAST = 2, + /** The MAC successfully transmitted a unicast Data or Command frame + * Note: Only frames with a 16-bit destination node ID are counted. + * destinationNodeId: MAC layer destination address + * data: not used + */ + MAC_TX_UNICAST_SUCCESS = 3, + /** The MAC retried a unicast Data or Command frame after initial Tx attempt. + * Note: CSMA-related failures are tracked separately via + * PHY_CCA_FAIL_COUNT. + * destinationNodeId: MAC layer destination or EMBER_UNKNOWN_NODE_ID + * if no 16-bit destination node ID is present in the frame + * data: number of retries (after initial Tx attempt) accumulated so far + * for this packet. (Should always be >0.) + */ + MAC_TX_UNICAST_RETRY = 4, + /** The MAC unsuccessfully transmitted a unicast Data or Command frame. + * Note: Only frames with a 16-bit destination node ID are counted. + * destinationNodeId: MAC layer destination address + * data: not used + */ + MAC_TX_UNICAST_FAILED = 5, + /** The APS layer received a data broadcast. + * destinationNodeId: sender's node ID + * data: not used + */ + APS_DATA_RX_BROADCAST = 6, + /** The APS layer transmitted a data broadcast. */ + APS_DATA_TX_BROADCAST = 7, + /** The APS layer received a data unicast. + * destinationNodeId: sender's node ID + * data: not used + */ + APS_DATA_RX_UNICAST = 8, + /** The APS layer successfully transmitted a data unicast. + * destinationNodeId: NWK destination address + * data: number of APS retries (>=0) consumed for this unicast. + */ + APS_DATA_TX_UNICAST_SUCCESS = 9, + /** The APS layer retried a unicast Data frame. This is a placeholder + * and is not used by the @c ::emberCounterHandler() callback. Instead, + * the number of APS retries are returned in the data parameter + * of the callback for the @c ::APS_DATA_TX_UNICAST_SUCCESS + * and @c ::APS_DATA_TX_UNICAST_FAILED types. + * However, our supplied Counters component code will attempt to collect this + * information from the aforementioned counters and populate this counter. + * Note that this counter's behavior differs from that of + * @c ::MAC_TX_UNICAST_RETRY . + */ + APS_DATA_TX_UNICAST_RETRY = 10, + /* The APS layer unsuccessfully transmitted a data unicast. + * destinationNodeId: NWK destination address + * data: number of APS retries (>=0) consumed for this unicast. + */ + APS_DATA_TX_UNICAST_FAILED = 11, + /** The network layer successfully submitted a new route discovery + * to the MAC. */ + ROUTE_DISCOVERY_INITIATED = 12, + /** An entry was added to the neighbor table. */ + NEIGHBOR_ADDED = 13, + /** An entry was removed from the neighbor table. */ + NEIGHBOR_REMOVED = 14, + /** A neighbor table entry became stale because it had not been heard from. */ + NEIGHBOR_STALE = 15, + /** A node joined or rejoined to the network via this node. + * destinationNodeId: node ID of child + * data: not used + */ + JOIN_INDICATION = 16, + /** An entry was removed from the child table. + * destinationNodeId: node ID of child + * data: not used + */ + CHILD_REMOVED = 17, + /** EZSP-UART only. An overflow error occurred in the UART. */ + ASH_OVERFLOW_ERROR = 18, + /** EZSP-UART only. A framing error occurred in the UART. */ + ASH_FRAMING_ERROR = 19, + /** EZSP-UART only. An overrun error occurred in the UART. */ + ASH_OVERRUN_ERROR = 20, + /** A message was dropped at the Network layer because the NWK frame + counter was not higher than the last message seen from that source. */ + NWK_FRAME_COUNTER_FAILURE = 21, + /** A message was dropped at the APS layer because the APS frame counter + was not higher than the last message seen from that source. + destinationNodeId: node ID of MAC source that relayed the message + data: not used + */ + APS_FRAME_COUNTER_FAILURE = 22, + /** EZSP-UART only. An XOFF was transmitted by the UART. */ + ASH_XOFF = 23, + /** An encrypted message was dropped by the APS layer because + * the sender's key has not been authenticated. + * As a result, the key is not authorized for use in APS data messages. + * destinationNodeId: EMBER_NULL_NODE_ID + * data: APS key table index related to the sender + */ + APS_LINK_KEY_NOT_AUTHORIZED = 24, + /** A NWK encrypted message was received but dropped because decryption + * failed. + * destinationNodeId: sender of the dropped packet + * data: not used + */ + NWK_DECRYPTION_FAILURE = 25, + /** An APS encrypted message was received but dropped because decryption + * failed. + * destinationNodeId: sender of the dropped packet + * data: not used + */ + APS_DECRYPTION_FAILURE = 26, + /** The number of failures to allocate a set of linked packet buffers. + This doesn't necessarily mean that the packet buffer count was + 0 at the time, but that the number requested was greater than the + number free. */ + ALLOCATE_PACKET_BUFFER_FAILURE = 27, + /** The number of relayed unicast packets. + destinationId: NWK layer destination address of relayed packet + data: not used + */ + RELAYED_UNICAST = 28, + /** The number of times a packet was dropped due to reaching the preset + * PHY-to-MAC queue limit (sli_mac_phy_to_mac_queue_length). The limit will + * determine how many messages are accepted by the PHY between calls to + * emberTick(). After that limit is reached, packets will be dropped. + * The counter records the number of dropped packets. + * + * NOTE: For each call to emberCounterHandler() there may be more + * than 1 packet that was dropped due to the limit reached. The + * actual number of packets dropped will be returned in the 'data' + * parameter passed to that function. + * + * destinationNodeId: not used + * data: number of dropped packets represented by this counter event + * phyIndex: present + */ + PHY_TO_MAC_QUEUE_LIMIT_REACHED = 29, + /** The number of times a packet was dropped due to the packet-validate + library checking a packet and rejecting it due to length or + other formatting problems. + destinationNodeId: not used + data: type of validation condition that failed + */ + PACKET_VALIDATE_LIBRARY_DROPPED_COUNT = 30, + /** The number of times the NWK retry queue is full and a new + message failed to be added. + destinationNodeId; not used + data: NWK retry queue size that has been exceeded + */ + TYPE_NWK_RETRY_OVERFLOW = 31, + /** The number of times the PHY layer was unable to transmit due to + * a failed CCA (Clear Channel Assessment) attempt. See also: + * MAC_TX_UNICAST_RETRY. + * destinationNodeId: MAC layer destination or EMBER_UNKNOWN_NODE_ID + * if no 16-bit destination node ID is present in the frame + * data: not used + */ + PHY_CCA_FAIL_COUNT = 32, + /** The number of times a NWK broadcast was dropped because + the broadcast table was full. + */ + BROADCAST_TABLE_FULL = 33, + /** The number of times a low-priority packet traffic arbitration + request has been made. + */ + PTA_LO_PRI_REQUESTED = 34, + /** The number of times a high-priority packet traffic arbitration + request has been made. + */ + PTA_HI_PRI_REQUESTED = 35, + /** The number of times a low-priority packet traffic arbitration + request has been denied. + */ + PTA_LO_PRI_DENIED = 36, + /** The number of times a high-priority packet traffic arbitration + request has been denied. + */ + PTA_HI_PRI_DENIED = 37, + /** The number of times a low-priority packet traffic arbitration + transmission has been aborted. + */ + PTA_LO_PRI_TX_ABORTED = 38, + /** The number of times a high-priority packet traffic arbitration + transmission has been aborted. + */ + PTA_HI_PRI_TX_ABORTED = 39, + /** The number of times an address conflict has caused node_id change, and an address conflict error is sent + */ + ADDRESS_CONFLICT_SENT = 40, + /** A placeholder giving the number of Ember counter types. */ + COUNT = 41, +}; + +/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ +/** An enumerated list of library identifiers. */ +export enum EmberLibraryId { + FIRST = 0x00, + + ZIGBEE_PRO = 0x00, + BINDING = 0x01, + END_DEVICE_BIND = 0x02, + SECURITY_CORE = 0x03, + SECURITY_LINK_KEYS = 0x04, + ALARM = 0x05, + CBKE = 0x06, + CBKE_DSA_SIGN = 0x07, + ECC = 0x08, + CBKE_DSA_VERIFY = 0x09, + PACKET_VALIDATE = 0x0A, + INSTALL_CODE = 0x0B, + ZLL = 0x0C, + CBKE_283K1 = 0x0D, + ECC_283K1 = 0x0E, + CBKE_CORE = 0x0F, + NCP = 0x10, + MULTI_NETWORK = 0x11, + ENHANCED_BEACON_REQUEST = 0x12, + CBKE_283K1_DSA_VERIFY = 0x13, + MULTI_PAN = 0x14, + + NUMBER_OF_LIBRARIES = 0x15, + NULL = 0xFF +}; + +/** This indicates the presence, absence, or status of an Ember stack library. */ +export enum EmberLibraryStatus { + // Base return codes. These may be ORed with statuses further below. + LIBRARY_PRESENT_MASK = 0x80, + LIBRARY_IS_STUB = 0x00, + LIBRARY_ERROR = 0xFF, + + // The ZigBee Pro library uses the following to indicate additional functionality: + /** no router capability */ + ZIGBEE_PRO_LIBRARY_END_DEVICE_ONLY = 0x00, + ZIGBEE_PRO_LIBRARY_HAVE_ROUTER_CAPABILITY = 0x01, + ZIGBEE_PRO_LIBRARY_ZLL_SUPPORT = 0x02, + + // The Security library uses the following to indicate additional functionality: + SECURITY_LIBRARY_END_DEVICE_ONLY = 0x00, + /** router or trust center support */ + SECURITY_LIBRARY_HAVE_ROUTER_SUPPORT = 0x01, + + // The Packet Validate library may be globally turned on/off. Bit 0 indicates whether the library is enabled/disabled. + PACKET_VALIDATE_LIBRARY_DISABLED = 0x00, + PACKET_VALIDATE_LIBRARY_ENABLED = 0x01, + PACKET_VALIDATE_LIBRARY_ENABLE_MASK = 0x01 +}; +/* eslint-enable @typescript-eslint/no-duplicate-enum-values */ + +/** Defines the entropy source used by the stack. */ +export enum EmberEntropySource { + /** Error in identifying the entropy source. */ + ERROR = 0x00, + /** The default radio entropy source. */ + RADIO = 0x01, + /** TRNG with mbed TLS support. */ + MBEDTLS_TRNG = 0x02, + /** Other mbed TLS entropy source. */ + MBEDTLS = 0x03, +}; + +/** Defines the options that should be used when initializing the node's network configuration. */ +export enum EmberNetworkInitBitmask { + NO_OPTIONS = 0x0000, + /** The Parent Node ID and EUI64 are stored in a token. This prevents the need to perform an Orphan scan on startup. */ + PARENT_INFO_IN_TOKEN = 0x0001, + /** Z3 compliant end devices on a network must send a rejoin request on reboot. */ + END_DEVICE_REJOIN_ON_REBOOT = 0x0002, +}; + +/** Defines the possible join states for a node. uint8_t */ +export enum EmberNetworkStatus { + /** The node is not associated with a network in any way. */ + NO_NETWORK, + /** The node is currently attempting to join a network. */ + JOINING_NETWORK, + /** The node is joined to a network. */ + JOINED_NETWORK, + /** The node is an end device joined to a network but its parent is not responding. */ + JOINED_NETWORK_NO_PARENT, + /** The node is a Sleepy-to-Sleepy initiator */ + JOINED_NETWORK_S2S_INITIATOR, + /** The node is a Sleepy-to-Sleepy target */ + JOINED_NETWORK_S2S_TARGET, + /** The node is in the process of leaving its current network. */ + LEAVING_NETWORK +}; + +/** Network scan types. */ +export enum EzspNetworkScanType { + /** An energy scan scans each channel for its RSSI value. */ + ENERGY_SCAN = 0x00, + /** An active scan scans each channel for available networks. */ + ACTIVE_SCAN = 0x01 +}; + +/** The type of method used for joining. uint8_t */ +export enum EmberJoinMethod { + /** Devices normally use MAC association to join a network, which respects + * the "permit joining" flag in the MAC beacon. + * This value should be used by default. + */ + MAC_ASSOCIATION = 0, + /** For networks where the "permit joining" flag is never turned + * on, devices will need to use a ZigBee NWK Rejoin. This value causes the + * rejoin to be sent withOUT NWK security and the Trust Center will be + * asked to send the NWK key to the device. The NWK key sent to the device + * can be encrypted with the device's corresponding Trust Center link key. + * That is determined by the ::EmberJoinDecision on the Trust Center + * returned by the ::emberTrustCenterJoinHandler(). + */ + NWK_REJOIN = 1, + /* For networks where the "permit joining" flag is never turned + * on, devices will need to use a NWK Rejoin. If those devices have been + * preconfigured with the NWK key (including sequence number), they can use + * a secured rejoin. This is only necessary for end devices since they need + * a parent. Routers can simply use the ::CONFIGURED_NWK_STATE + * join method below. + */ + NWK_REJOIN_HAVE_NWK_KEY = 2, + /** For networks where all network and security information is known + ahead of time, a router device may be commissioned such that it does + not need to send any messages to begin communicating on the network. + */ + CONFIGURED_NWK_STATE = 3, + /** This enumeration causes an unencrypted Network Commissioning Request to be + sent out with joinType set to initial join. The trust center may respond + by establishing a new dynamic link key and then sending the network key. + Network Commissioning Requests should only be sent to parents that support + processing of the command. + */ + NWK_COMMISSIONING_JOIN = 4, + /** This enumeration causes an unencrypted Network Commissioning Request to be + sent out with joinType set to rejoin. The trust center may respond + by establishing a new dynamic link key and then sending the network key. + Network Commissioning Requests should only be sent to parents that support + processing of the command. + */ + NWK_COMMISSIONING_REJOIN = 5, + /** This enumeration causes an encrypted Network Commissioning Request to be + sent out with joinType set to rejoin. This enumeration is used by devices + that already have the network key and wish to recover connection to a + parent or the network in general. + Network Commissioning Requests should only be sent to parents that support + processing of the command. + */ + NWK_COMMISSIONING_REJOIN_HAVE_NWK_KEY = 6, +}; + +/** Defines the possible types of nodes and the roles that a node might play in a network. */ +export enum EmberNodeType { + /** The device is not joined. */ + UNKNOWN_DEVICE = 0, + /** Will relay messages and can act as a parent to other nodes. */ + COORDINATOR = 1, + /** Will relay messages and can act as a parent to other nodes. */ + ROUTER = 2, + /** Communicates only with its parent and will not relay messages. */ + END_DEVICE = 3, + /** An end device whose radio can be turned off to save power. The application must call ::emberPollForData() to receive messages. */ + SLEEPY_END_DEVICE = 4, + /** Sleepy end device which transmits with wake up frames (CSL). */ + S2S_INITIATOR_DEVICE = 5, + /** Sleepy end device which duty cycles the radio Rx (CSL). */ + S2S_TARGET_DEVICE = 6, +}; + +/** */ +export enum EmberMultiPhyNwkConfig { + ROUTERS_ALLOWED = 0x01, + BROADCASTS_ENABLED = 0x02, + DISABLED = 0x80 +}; + +/** + * Duty cycle states + * + * Applications have no control over the state but the callback exposes + * state changes to the application. + */ +export enum EmberDutyCycleState { + /** No duty cycle tracking or metrics are taking place. */ + TRACKING_OFF = 0, + /** Duty Cycle is tracked and has not exceeded any thresholds. */ + LBT_NORMAL = 1, + /** The limited threshold of the total duty cycle allotment was exceeded. */ + LBT_LIMITED_THRESHOLD_REACHED = 2, + /** The critical threshold of the total duty cycle allotment was exceeded. */ + LBT_CRITICAL_THRESHOLD_REACHED = 3, + /** The suspend limit was reached and all outbound transmissions are blocked. */ + LBT_SUSPEND_LIMIT_REACHED = 4, +}; + +/** Defines binding types. uint8_t */ +export enum EmberBindingType { + /** A binding that is currently not in use. */ + UNUSED_BINDING = 0, + /** A unicast binding whose 64-bit identifier is the destination EUI64. */ + UNICAST_BINDING = 1, + /** A unicast binding whose 64-bit identifier is the many-to-one + * destination EUI64. Route discovery should be disabled when sending + * unicasts via many-to-one bindings. */ + MANY_TO_ONE_BINDING = 2, + /** A multicast binding whose 64-bit identifier is the group address. This + * binding can be used to send messages to the group and to receive + * messages sent to the group. */ + MULTICAST_BINDING = 3, +}; + +/** Defines the possible outgoing message types. uint8_t */ +export enum EmberOutgoingMessageType { + /** Unicast sent directly to an EmberNodeId. */ + DIRECT, + /** Unicast sent using an entry in the address table. */ + VIA_ADDRESS_TABLE, + /** Unicast sent using an entry in the binding table. */ + VIA_BINDING, + /** Multicast message. This value is passed to emberMessageSentHandler() only. + * It may not be passed to emberSendUnicast(). */ + MULTICAST, + /** An aliased multicast message. This value is passed to emberMessageSentHandler() only. + * It may not be passed to emberSendUnicast(). */ + MULTICAST_WITH_ALIAS, + /** An aliased Broadcast message. This value is passed to emberMessageSentHandler() only. + * It may not be passed to emberSendUnicast(). */ + BROADCAST_WITH_ALIAS, + /** A broadcast message. This value is passed to emberMessageSentHandler() only. + * It may not be passed to emberSendUnicast(). */ + BROADCAST +}; + +/** Defines the possible incoming message types. uint8_t */ +export enum EmberIncomingMessageType { + /** Unicast. */ + UNICAST, + /** Unicast reply. */ + UNICAST_REPLY, + /** Multicast. */ + MULTICAST, + /** Multicast sent by the local device. */ + MULTICAST_LOOPBACK, + /** Broadcast. */ + BROADCAST, + /** Broadcast sent by the local device. */ + BROADCAST_LOOPBACK +}; + +/** + * Options to use when sending a message. + * + * The discover-route, APS-retry, and APS-indirect options may be used together. + * Poll response cannot be combined with any other options. + * uint16_t + */ +export enum EmberApsOption { + /** No options. */ + NONE = 0x0000, + ENCRYPT_WITH_TRANSIENT_KEY = 0x0001, + USE_ALIAS_SEQUENCE_NUMBER = 0x0002, + /** + * This signs the application layer message body (APS Frame not included) and appends the ECDSA signature to the end of the message, + * which is needed by Smart Energy applications and requires the CBKE and ECC libraries. + * The ::emberDsaSignHandler() function is called after DSA signing is complete but before the message has been sent by the APS layer. + * Note that when passing a buffer to the stack for DSA signing, the final byte in the buffer has a special significance as an indicator + * of how many leading bytes should be ignored for signature purposes. See the API documentation of emberDsaSign() + * or the dsaSign EZSP command for more details about this requirement. + */ + DSA_SIGN = 0x0010, + /** Send the message using APS Encryption using the Link Key shared with the destination node to encrypt the data at the APS Level. */ + ENCRYPTION = 0x0020, + /** + * Resend the message using the APS retry mechanism. + * This option and the enable route discovery option must be enabled for an existing route to be repaired automatically. + */ + RETRY = 0x0040, + /** + * Send the message with the NWK 'enable route discovery' flag, which causes a route discovery to be initiated if no route to the + * destination is known. Note that in the mesh stack, this option and the APS retry option must be enabled an existing route to be + * repaired automatically. + */ + ENABLE_ROUTE_DISCOVERY = 0x0100, + /** Send the message with the NWK 'force route discovery' flag, which causes a route discovery to be initiated even if one is known. */ + FORCE_ROUTE_DISCOVERY = 0x0200, + /** Include the source EUI64 in the network frame. */ + SOURCE_EUI64 = 0x0400, + /** Include the destination EUI64 in the network frame. */ + DESTINATION_EUI64 = 0x0800, + /** Send a ZDO request to discover the node ID of the destination if it is not already known. */ + ENABLE_ADDRESS_DISCOVERY = 0x1000, + /** + * This message is being sent in response to a call to ::emberPollHandler(). + * It causes the message to be sent immediately instead of being queued up until the next poll from the (end device) destination. + */ + POLL_RESPONSE = 0x2000, + /** + * This incoming message is a valid ZDO request and the application is responsible for sending a ZDO response. + * This flag is used only within emberIncomingMessageHandler() when EMBER_APPLICATION_RECEIVES_UNSUPPORTED_ZDO_REQUESTS is defined. */ + ZDO_RESPONSE_REQUIRED = 0x4000, + /** + * This message is part of a fragmented message. This option may only be set for unicasts. + * The groupId field gives the index of this fragment in the low-order byte. + * If the low-order byte is zero this is the first fragment and the high-order byte contains the number of fragments in the message. + */ + FRAGMENT = 0x8000,// SIGNED_ENUM 0x8000 +}; + +/** + * Types of source route discovery modes used by the concentrator. + * + * OFF no source route discovery is scheduled + * + * ON source routes discovery is scheduled, and it is triggered periodically + * + * RESCHEDULE source routes discoveries are re-scheduled to be sent once immediately and then triggered periodically + */ +export enum EmberSourceRouteDiscoveryMode { + /** off */ + OFF = 0x00, + /** on */ + ON = 0x01, + /** reschedule */ + RESCHEDULE = 0x02, +}; + +/** The types of MAC passthrough messages that an application may receive. This is a bitmask. */ +export enum EmberMacPassthroughType { + /** No MAC passthrough messages. */ + NONE = 0x00, + /** SE InterPAN messages. */ + SE_INTERPAN = 0x01, + /** EmberNet and first generation (v1) standalone bootloader messages. */ + EMBERNET = 0x02, + /** EmberNet messages filtered by their source address. */ + EMBERNET_SOURCE = 0x04, + /** Application-specific passthrough messages. */ + APPLICATION = 0x08, + /** Custom inter-pan filter. */ + CUSTOM = 0x10, + + /** Internal Stack passthrough. */ + INTERNAL_ZLL = 0x80, + INTERNAL_GP = 0x40 +}; + +/** + * Interpan Message type: unicast, broadcast, or multicast. + * uint8_t + */ +export enum EmberInterpanMessageType { + UNICAST = 0x00, + BROADCAST = 0x08, + MULTICAST = 0x0C, +} + +/** This is the Current Security Bitmask that details the use of various security features. */ +export enum EmberCurrentSecurityBitmask { + // These options are the same for Initial and Current Security state. + + /** This denotes that the device is running in a network with ZigBee + * Standard Security. */ + STANDARD_SECURITY_MODE_ = 0x0000, + /** This denotes that the device is running in a network without + * a centralized Trust Center. */ + DISTRIBUTED_TRUST_CENTER_MODE_ = 0x0002, + /** This denotes that the device has a Global Link Key. The Trust Center + * Link Key is the same across multiple nodes. */ + TRUST_CENTER_GLOBAL_LINK_KEY_ = 0x0004, + + // Bit 3 reserved + + /** This denotes that the node has a Trust Center Link Key. */ + HAVE_TRUST_CENTER_LINK_KEY = 0x0010, + + /** This denotes that the Trust Center is using a Hashed Link Key. */ + TRUST_CENTER_USES_HASHED_LINK_KEY_ = 0x0084, + + // Bits 1, 5, 6, and 8-15 reserved. +}; + +/** + * The list of supported key types used by Zigbee Security Manager. + * uint8_t + */ +export enum SecManKeyType { + NONE, + /** + * This is the network key, used for encrypting and decrypting network payloads. + * There is only one of these keys in storage. + */ + NETWORK, + /** + * This is the Trust Center Link Key. On the joining device, this is the APS + * key used to communicate with the trust center. On the trust center, this + * key can be used as a root key for APS encryption and decryption when + * communicating with joining devices (if the security policy has the + * EMBER_TRUST_CENTER_USES_HASHED_LINK_KEY bit set). + * There is only one of these keys in storage. + */ + TC_LINK, + /** + * This is a Trust Center Link Key, but it times out after either + * ::EMBER_TRANSIENT_KEY_TIMEOUT_S or + * ::EMBER_AF_PLUGIN_NETWORK_CREATOR_SECURITY_NETWORK_OPEN_TIME_S (if + * defined), whichever is longer. This type of key is set on trust centers + * who wish to open joining with a temporary, or transient, APS key for + * devices to join with. Joiners who wish to try several keys when joining a + * network may set several of these types of keys before attempting to join. + * This is an indexed key, and local storage can fit as many keys as + * available RAM allows. + */ + TC_LINK_WITH_TIMEOUT, + /** + * This is an Application link key. On both joining devices and the trust + * center, this key is used in APS encryption and decryption when + * communicating to a joining device. + * This is an indexed key table of size EMBER_KEY_TABLE_SIZE, so long as there + * is sufficient nonvolatile memory to store keys. + */ + APP_LINK, + /** This is the ZLL encryption key for use by algorithms that require it. */ + ZLL_ENCRYPTION_KEY, + /** For ZLL, this is the pre-configured link key used during classical ZigBee commissioning. */ + ZLL_PRECONFIGURED_KEY, + /** This is a Green Power Device (GPD) key used on a Proxy device. */ + GREEN_POWER_PROXY_TABLE_KEY, + /** This is a Green Power Device (GPD) key used on a Sink device. */ + GREEN_POWER_SINK_TABLE_KEY, + /** + * This is a generic key type intended to be loaded for one-time hashing or crypto operations. + * This key is not persisted. Intended for use by the Zigbee stack. + */ + INTERNAL, +}; + +/** + * Derived keys are calculated when performing Zigbee crypto operations. The stack makes use of these derivations. + * Compounding derivations can be specified by using an or-equals on two derived types if applicable; + * this is limited to performing the key-transport, key-load, or verify-key hashes on either the TC Swap Out or TC Hashed Link keys. + * uint16_t + */ +export enum SecManDerivedKeyType { + /** Perform no derivation; use the key as is. */ + NONE = 0x0000, + /** Perform the Key-Transport-Key hash. */ + KEY_TRANSPORT_KEY = 0x0001, + /** Perform the Key-Load-Key hash. */ + KEY_LOAD_KEY = 0x0002, + /** Perform the Verify Key hash. */ + VERIFY_KEY = 0x0004, + /** Perform a simple AES hash of the key for TC backup. */ + TC_SWAP_OUT_KEY = 0x0008, + /** For a TC using hashed link keys, hashed the root key against the supplied EUI in context. */ + TC_HASHED_LINK_KEY = 0x0010, +}; + +/** + * Security Manager context flags. + * uint8_t + */ +export enum SecManFlag { + NONE = 0x00, + /** + * For export APIs, this flag indicates the key_index parameter is valid in + * the ::sl_zb_sec_man_context_t structure. This bit is set by the caller + * when intending to search for a key by key_index. This flag has no + * significance for import APIs. */ + KEY_INDEX_IS_VALID = 0x01, + /** + * For export APIs, this flag indicates the eui64 parameter is valid in the + * ::sl_zb_sec_man_context_t structure. This bit is set by the caller when + * intending to search for a key by eui64. It is also set when searching by + * key_index and an entry is found. This flag has no significance for import + * APIs. */ + EUI_IS_VALID = 0x02, + /** + * Internal use only. This indicates that the transient key being added is an + * unconfirmed, updated key. This bit is set when we add a transient key and + * the ::EmberTcLinkKeyRequestPolicy policy + * is ::EMBER_ALLOW_TC_LINK_KEY_REQUEST_AND_GENERATE_NEW_KEY, whose behavior + * dictates that we generate a new, unconfirmed key, send it to the requester, + * and await for a Verify Key Confirm message. */ + UNCONFIRMED_TRANSIENT_KEY = 0x04, + /** + * Internal use only. This indicates that the key being added was derived via + * dynamic link key negotiation. This may be used in conjunction with the above + * ::UNCONFIRMED_TRANSIENT_KEY while the derived link key awaits + * confirmation + */ + AUTHENTICATED_DYNAMIC_LINK_KEY = 0x08, + /** + * Internal use only. This indicates that the "key" being added is instead the + * symmetric passphrase to be stored in the link key table. This flag will trigger the + * addition of the KEY_TABLE_SYMMETRIC_PASSPHRASE bitmask when storing the symmetric + * passphrase so that it can be differentiated from other keys with the same EUI64. + */ + SYMMETRIC_PASSPHRASE = 0x10, +}; + +/** This denotes the status of an attempt to establish a key with another device. */ +export enum EmberKeyStatus { + STATUS_NONE = 0x00, + APP_LINK_KEY_ESTABLISHED = 0x01, + TRUST_CENTER_LINK_KEY_ESTABLISHED = 0x03, + + ESTABLISHMENT_TIMEOUT = 0x04, + TABLE_FULL = 0x05, + + // These are success status values applying only to the Trust Center answering key requests. + TC_RESPONDED_TO_KEY_REQUEST = 0x06, + TC_APP_KEY_SENT_TO_REQUESTER = 0x07, + + // These are failure status values applying only to the + // Trust Center answering key requests. + TC_RESPONSE_TO_KEY_REQUEST_FAILED = 0x08, + TC_REQUEST_KEY_TYPE_NOT_SUPPORTED = 0x09, + TC_NO_LINK_KEY_FOR_REQUESTER = 0x0A, + TC_REQUESTER_EUI64_UNKNOWN = 0x0B, + TC_RECEIVED_FIRST_APP_KEY_REQUEST = 0x0C, + TC_TIMEOUT_WAITING_FOR_SECOND_APP_KEY_REQUEST = 0x0D, + TC_NON_MATCHING_APP_KEY_REQUEST_RECEIVED = 0x0E, + TC_FAILED_TO_SEND_APP_KEYS = 0x0F, + TC_FAILED_TO_STORE_APP_KEY_REQUEST = 0x10, + TC_REJECTED_APP_KEY_REQUEST = 0x11, + TC_FAILED_TO_GENERATE_NEW_KEY = 0x12, + TC_FAILED_TO_SEND_TC_KEY = 0x13, + + // These are generic status values for a key requester. + TRUST_CENTER_IS_PRE_R21 = 0x1E, + + // These are status values applying only to the Trust Center verifying link keys. + TC_REQUESTER_VERIFY_KEY_TIMEOUT = 0x32, + TC_REQUESTER_VERIFY_KEY_FAILURE = 0x33, + TC_REQUESTER_VERIFY_KEY_SUCCESS = 0x34, + + // These are status values applying only to the key requester + // verifying link keys. + VERIFY_LINK_KEY_FAILURE = 0x64, + VERIFY_LINK_KEY_SUCCESS = 0x65, +}; + +/** This bitmask describes the presence of fields within the ::EmberKeyStruct. uint16_t */ +export enum EmberKeyStructBitmask { + /** This indicates that the key has a sequence number associated with it. (i.e., a Network Key). */ + HAS_SEQUENCE_NUMBER = 0x0001, + /** This indicates that the key has an outgoing frame counter and the corresponding value within the ::EmberKeyStruct has been populated.*/ + HAS_OUTGOING_FRAME_COUNTER = 0x0002, + /** This indicates that the key has an incoming frame counter and the corresponding value within the ::EmberKeyStruct has been populated.*/ + HAS_INCOMING_FRAME_COUNTER = 0x0004, + /** + * This indicates that the key has an associated Partner EUI64 address and the corresponding value + * within the ::EmberKeyStruct has been populated. + */ + HAS_PARTNER_EUI64 = 0x0008, + /** + * This indicates the key is authorized for use in APS data messages. + * If the key is not authorized for use in APS data messages it has not yet gone through a key agreement protocol, such as CBKE (i.e., ECC). + */ + IS_AUTHORIZED = 0x0010, + /** + * This indicates that the partner associated with the link is a sleepy end device. + * This bit is set automatically if the local device hears a device announce from the partner indicating it is not an 'RX on when idle' device. + */ + PARTNER_IS_SLEEPY = 0x0020, + /** + * This indicates that the transient key which is being added is unconfirmed. + * This bit is set when we add a transient key while the EmberTcLinkKeyRequestPolicy is EMBER_ALLOW_TC_LINK_KEY_REQUEST_AND_GENERATE_NEW_KEY + */ + UNCONFIRMED_TRANSIENT = 0x0040, + /** This indicates that the actual key data is stored in PSA, and the respective PSA ID is recorded in the psa_id field. */ + HAS_PSA_ID = 0x0080, + /** + * This indicates that the keyData field has valid data. On certain parts and depending on the security configuration, + * keys may live in secure storage and are not exportable. In such cases, keyData will not house the actual key contents. + */ + HAS_KEY_DATA = 0x0100, + /** + * This indicates that the key represents a Device Authentication Token and is not an encryption key. + * The Authentication token is persisted for the lifetime of the device on the network and used to validate and update the device connection. + * It is only removed when the device leaves or is decommissioned from the network + */ + IS_AUTHENTICATION_TOKEN = 0x0200, + /** This indicates that the key has been derived by the Dynamic Link Key feature. */ + DLK_DERIVED = 0x0400, + /** This indicates that the device this key is being used to communicate with supports the APS frame counter synchronization procedure. */ + FC_SYNC_SUPPORTED = 0x0800, +}; + +/** + * The Status of the Update Device message sent to the Trust Center. + * The device may have joined or rejoined insecurely, rejoined securely, or left. + * MAC Security has been deprecated and therefore there is no secure join. + * These map to the actual values within the APS Command frame so they cannot be arbitrarily changed. + * uint8_t + */ +export enum EmberDeviceUpdate { + STANDARD_SECURITY_SECURED_REJOIN = 0, + STANDARD_SECURITY_UNSECURED_JOIN = 1, + DEVICE_LEFT = 2, + STANDARD_SECURITY_UNSECURED_REJOIN = 3, +}; + +/** The decision made by the Trust Center when a node attempts to join. uint8_t */ +export enum EmberJoinDecision { + /** Allow the node to join. The node has the key. */ + USE_PRECONFIGURED_KEY = 0, + /** Allow the node to join. Send the key to the node. */ + SEND_KEY_IN_THE_CLEAR, + /** Deny join. */ + DENY_JOIN, + /** Take no action. */ + NO_ACTION, + /** Allow rejoins only.*/ + ALLOW_REJOINS_ONLY +}; + +/** A bitmask indicating the state of the ZLL device. This maps directly to the ZLL information field in the scan response. uint16_t */ +export enum EmberZllState { + /** No state. */ + NONE = 0x0000, + /** The device is factory new. */ + FACTORY_NEW = 0x0001, + /** The device is capable of assigning addresses to other devices. */ + ADDRESS_ASSIGNMENT_CAPABLE = 0x0002, + /** The device is initiating a link operation. */ + LINK_INITIATOR = 0x0010, + /** The device is requesting link priority. */ + LINK_PRIORITY_REQUEST = 0x0020, + /** The device is a ZigBee 3.0 device. */ + PROFILE_INTEROP = 0x0080, + /** The device is on a non-ZLL network. */ + NON_ZLL_NETWORK = 0x0100, + /** Internal use: the ZLL token's key values point to a PSA key identifier */ + TOKEN_POINTS_TO_PSA_ID = 0x0200, +}; + +/** Differentiates among ZLL network operations. */ +export enum EzspZllNetworkOperation { + /** ZLL form network command. */ + FORM_NETWORK = 0x00, + /** ZLL join target command. */ + JOIN_TARGET = 0x01 +}; + +/** The key encryption algorithms supported by the stack. */ +export enum EmberZllKeyIndex { + /** The key encryption algorithm for use during development. */ + DEVELOPMENT = 0x00, + /** The key encryption algorithm shared by all certified devices. */ + MASTER = 0x04, + /** The key encryption algorithm for use during development and certification. */ + CERTIFICATION = 0x0F, +}; + +/** uint8_t */ +export enum EmberGpApplicationId { + /** Source identifier. */ + SOURCE_ID = 0x00, + /** IEEE address. */ + IEEE_ADDRESS = 0x02, +}; + +/** Green Power Security Level. uint8_t */ +export enum EmberGpSecurityLevel { + /** No Security */ + NONE = 0x00, + /** Reserved */ + RESERVED = 0x01, + /** 4 Byte Frame Counter and 4 Byte MIC */ + FC_MIC = 0x02, + /** 4 Byte Frame Counter and 4 Byte MIC with encryption */ + FC_MIC_ENCRYPTED = 0x03, +}; + +/** Green Power Security Security Key Type. uint8_t */ +export enum EmberGpKeyType { + /** No Key */ + NONE = 0x00, + /** GP Security Key Type is Zigbee Network Key */ + NWK = 0x01, + /** GP Security Key Type is Group Key */ + GPD_GROUP = 0x02, + /** GP Security Key Type is Derived Network Key */ + NWK_DERIVED = 0x03, + /** GP Security Key Type is Out Of Box Key */ + GPD_OOB = 0x04, + /** GP Security Key Type is GPD Derived Key */ + GPD_DERIVED = 0x07, +}; + +/** uint8_t */ +export enum EmberGpProxyTableEntryStatus { + /** The GP table entry is in use for a Proxy Table Entry. */ + ACTIVE = 0x01, + /** The proxy table entry is not in use. */ + UNUSED = 0xFF, +}; + +/** GP Sink Type. */ +export enum EmberGpSinkType { + /** Sink Type is Full Unicast */ + FULL_UNICAST, + /** Sink Type is Derived groupcast, the group ID is derived from the GpdId during commissioning. + * The sink is added to the APS group with that groupId. + */ + D_GROUPCAST, + /** Sink type GROUPCAST, the groupId can be obtained from the APS group table + * or from the sink table. + */ + GROUPCAST, + /** Sink Type is Light Weight Unicast. */ + LW_UNICAST, + /** Unused sink type */ + UNUSED = 0xFF +}; + +/** uint8_t */ +export enum EmberGpSinkTableEntryStatus { + /** The GP table entry is in use for a Sink Table Entry. */ + ACTIVE = 0x01, + /** The proxy table entry is not in use. */ + UNUSED = 0xFF, +}; diff --git a/src/adapter/ember/ezsp/buffalo.ts b/src/adapter/ember/ezsp/buffalo.ts new file mode 100644 index 0000000000..46634c6a1e --- /dev/null +++ b/src/adapter/ember/ezsp/buffalo.ts @@ -0,0 +1,1269 @@ +/* istanbul ignore file */ +import Buffalo from "../../../buffalo/buffalo"; +import {GP_SINK_LIST_ENTRIES} from "../consts"; +import {EmberGpApplicationId, EmberGpSinkType, EzspStatus} from "../enums"; +import { + EmberAesMmoHashContext, + EmberApsFrame, + EmberBeaconClassificationParams, + EmberBeaconData, + EmberBeaconIterator, + EmberBindingTableEntry, + EmberCertificate283k1Data, + EmberCertificateData, + EmberChildData, + EmberCurrentSecurityState, + EmberDutyCycleLimits, + EmberGpAddress, + EmberGpProxyTableEntry, + EmberGpSinkListEntry, + EmberGpSinkTableEntry, + EmberInitialSecurityState, + EmberKeyData, + EmberMessageDigest, + EmberMultiPhyRadioParameters, + EmberMulticastTableEntry, + EmberNeighborTableEntry, + EmberNetworkInitStruct, + EmberNetworkParameters, + EmberPerDeviceDutyCycle, + EmberPrivateKey283k1Data, + EmberPrivateKeyData, + EmberPublicKey283k1Data, + EmberPublicKeyData, + EmberRouteTableEntry, + EmberSignature283k1Data, + EmberSignatureData, + EmberSmacData, + EmberTokTypeStackZllData, + EmberTokTypeStackZllSecurity, + EmberTokenData, + EmberTokenInfo, + EmberTransientKeyData, + EmberZigbeeNetwork, + EmberZllAddressAssignment, + EmberZllDeviceInfoRecord, + EmberZllInitialSecurityState, + EmberZllNetwork, + EmberZllSecurityAlgorithmData, + SecManAPSKeyMetadata, + SecManContext, + SecManKey, + SecManNetworkKeyInfo +} from "../types"; +import {highByte} from "../utils/math"; +import { + EMBER_ENCRYPTION_KEY_SIZE, + EMBER_CERTIFICATE_SIZE, + EMBER_PUBLIC_KEY_SIZE, + EMBER_PRIVATE_KEY_SIZE, + EMBER_SMAC_SIZE, + EMBER_SIGNATURE_SIZE, + EMBER_AES_HASH_BLOCK_SIZE, + EMBER_CERTIFICATE_283K1_SIZE, + EMBER_PUBLIC_KEY_283K1_SIZE, + EMBER_PRIVATE_KEY_283K1_SIZE, + EMBER_SIGNATURE_283K1_SIZE, + EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX, + EZSP_EXTENDED_FRAME_FORMAT_VERSION_MASK, + EZSP_EXTENDED_FRAME_FORMAT_VERSION, + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + EZSP_EXTENDED_PARAMETERS_INDEX, + EZSP_EXTENDED_FRAME_CONTROL_RESERVED_MASK, + EZSP_FRAME_CONTROL_INDEX, + EZSP_FRAME_ID_INDEX, + EZSP_PARAMETERS_INDEX, + EZSP_EXTENDED_FRAME_ID_HB_INDEX, + EZSP_EXTENDED_FRAME_ID_LB_INDEX, + EXTENDED_PAN_ID_SIZE, +} from "./consts"; +import {EzspFrameID} from "./enums"; + + +export class EzspBuffalo extends Buffalo { + + public getBufferLength(): number { + return this.buffer.length; + } + + /** Set the position of the internal position tracker. */ + public setPosition(position: number): void { + this.position = position; + } + + /** + * Set the byte at given position without affecting the internal position tracker. + * @param position + * @param value + */ + public setCommandByte(position: number, value: number): void { + this.buffer.writeUInt8(value, position); + } + + /** + * Get the byte at given position without affecting the internal position tracker. + * @param position + * @returns + */ + public getCommandByte(position: number): number { + return this.buffer.readUInt8(position); + } + + /** + * Get the byte at given position without affecting the internal position tracker. + * @param position + * @returns + */ + public getResponseByte(position: number): number { + return this.buffer.readUInt8(position); + } + + public getExtFrameControl(): number { + return (this.getResponseByte(EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX) << 8 | this.getResponseByte(EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX)); + } + + public getExtFrameId(): EzspFrameID { + return (this.getResponseByte(EZSP_EXTENDED_FRAME_ID_HB_INDEX) << 8 | this.getResponseByte(EZSP_EXTENDED_FRAME_ID_LB_INDEX)); + } + + public getFrameId(): EzspFrameID { + if ((this.getResponseByte(EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX) & EZSP_EXTENDED_FRAME_FORMAT_VERSION_MASK) + === EZSP_EXTENDED_FRAME_FORMAT_VERSION) { + return this.getExtFrameId(); + } else { + return this.getResponseByte(EZSP_FRAME_ID_INDEX) as EzspFrameID; + } + } + + /** + * Get the frame control, ID and params index according to format version. + * Throws if frame control is unsupported (using reserved). + * @returns Anything but SUCCESS should stop further processing. + */ + public getResponseMetadata(): [status: EzspStatus, frameControl: number, frameId: EzspFrameID, parametersIndex: number] { + let status: EzspStatus = EzspStatus.SUCCESS; + let frameControl: number; + let frameId: EzspFrameID; + let parametersIndex: number; + + if ((this.getResponseByte(EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX) & EZSP_EXTENDED_FRAME_FORMAT_VERSION_MASK) + === EZSP_EXTENDED_FRAME_FORMAT_VERSION) { + // use extended ezsp frame format + frameControl = this.getExtFrameControl(); + frameId = this.getExtFrameId(); + parametersIndex = EZSP_EXTENDED_PARAMETERS_INDEX; + + if (highByte(frameControl) & EZSP_EXTENDED_FRAME_CONTROL_RESERVED_MASK) { + // reject if unsupported frame + status = EzspStatus.ERROR_UNSUPPORTED_CONTROL; + } + } else { + // use legacy ezsp frame format + frameControl = this.getResponseByte(EZSP_FRAME_CONTROL_INDEX); + frameId = this.getResponseByte(EZSP_FRAME_ID_INDEX) as EzspFrameID; + parametersIndex = EZSP_PARAMETERS_INDEX; + } + + return [status, frameControl, frameId, parametersIndex]; + } + + /** + * Get a copy of the rest of the buffer (from current position to end). + * WARNING: Make sure the length is appropriate, if alloc'ed longer, it will return everything until the end. + * @returns + */ + public readRest(): Buffer { + return Buffer.from(this.buffer.subarray(this.position)); + } + + /** + * This is mostly used for payload/encryption stuff. + * Copies the buffer to avoid memory referencing issues since Ezsp has a single buffer allocated. + * @param length + * @returns + */ + protected readBufferCopy(length: number): Buffer { + return Buffer.from(this.readBuffer(length)); + } + + /** + * Write a uint8_t for payload length, followed by payload buffer (copied at post-length position). + * + * WARNING: `payload` must have a valid length (as in, not a Buffer allocated to longer length). + * Should be passed with getWritten() in most cases. + * @param payload + */ + public writePayload(payload: Buffer): void { + this.writeUInt8(payload.length); + + this.position += payload.copy(this.buffer, this.position); + } + + /** + * Read a uint8_t for payload length, followed by payload buffer (using post-length position). + * @returns + */ + public readPayload(): Buffer { + const messageLength = this.readUInt8(); + + return this.readBufferCopy(messageLength); + } + + public writeEmberNetworkParameters(value: EmberNetworkParameters): void { + this.writeListUInt8(value.extendedPanId); + this.writeUInt16(value.panId); + this.writeUInt8(value.radioTxPower); + this.writeUInt8(value.radioChannel); + this.writeUInt8(value.joinMethod); + this.writeUInt16(value.nwkManagerId); + this.writeUInt8(value.nwkUpdateId); + this.writeUInt32(value.channels); + } + + public readEmberNetworkParameters(): EmberNetworkParameters { + const extendedPanId = this.readListUInt8({length: EXTENDED_PAN_ID_SIZE}); + const panId = this.readUInt16(); + const radioTxPower = this.readUInt8(); + const radioChannel = this.readUInt8(); + const joinMethod = this.readUInt8(); + const nwkManagerId = this.readUInt16(); + const nwkUpdateId = this.readUInt8(); + const channels = this.readUInt32(); + + return { + extendedPanId, + panId, + radioTxPower, + radioChannel, + joinMethod, + nwkManagerId, + nwkUpdateId, + channels, + }; + } + + public writeEmberMultiPhyRadioParameters(value: EmberMultiPhyRadioParameters): void { + this.writeUInt8(value.radioTxPower); + this.writeUInt8(value.radioPage); + this.writeUInt8(value.radioChannel); + } + + public readEmberMultiPhyRadioParameters(): EmberMultiPhyRadioParameters { + const radioTxPower = this.readUInt8(); + const radioPage = this.readUInt8(); + const radioChannel = this.readUInt8(); + + return {radioTxPower, radioPage, radioChannel}; + } + + public writeEmberApsFrame(value: EmberApsFrame): void { + this.writeUInt16(value.profileId); + this.writeUInt16(value.clusterId); + this.writeUInt8(value.sourceEndpoint); + this.writeUInt8(value.destinationEndpoint); + this.writeUInt16(value.options); + this.writeUInt16(value.groupId); + this.writeUInt8(value.sequence); + // this.writeUInt8(value.radius);// XXX: not in gecko_sdk, appended with separate param + } + + public readEmberApsFrame(): EmberApsFrame { + const profileId = this.readUInt16(); + const clusterId = this.readUInt16(); + const sourceEndpoint = this.readUInt8(); + const destinationEndpoint = this.readUInt8(); + const options = this.readUInt16(); + const groupId = this.readUInt16(); + const sequence = this.readUInt8(); + // const radius = this.readUInt8();// XXX: not in gecko_sdk, appended with separate param + + return { + profileId, + clusterId, + sourceEndpoint, + destinationEndpoint, + options, + groupId, + sequence, + // radius, + }; + } + + public writeEmberBindingTableEntry(value: EmberBindingTableEntry): void { + this.writeUInt8(value.type); + this.writeUInt8(value.local); + this.writeUInt16(value.clusterId); + this.writeUInt8(value.remote); + this.writeIeeeAddr(value.identifier); + this.writeUInt8(value.networkIndex); + } + + public readEmberBindingTableEntry(): EmberBindingTableEntry { + const type = this.readUInt8(); + const local = this.readUInt8(); + const clusterId = this.readUInt16(); + const remote = this.readUInt8(); + const identifier = this.readIeeeAddr(); + const networkIndex = this.readUInt8(); + + return { + type, + local, + clusterId, + remote, + identifier, + networkIndex, + }; + } + + public writeEmberMulticastTableEntry(value: EmberMulticastTableEntry): void { + this.writeUInt16(value.multicastId); + this.writeUInt8(value.endpoint); + this.writeUInt8(value.networkIndex); + } + + public readEmberMulticastTableEntry(): EmberMulticastTableEntry { + const multicastId = this.readUInt16(); + const endpoint = this.readUInt8(); + // XXX: not in gecko_sdk? as workaround check length for now since used at end in just one place + const networkIndex = this.isMore() ? this.readUInt8() : 0x00; + + return {multicastId, endpoint, networkIndex}; + } + + public writeEmberBeaconClassificationParams(value: EmberBeaconClassificationParams): void { + this.writeUInt8(value.minRssiForReceivingPkts); + this.writeUInt16(value.beaconClassificationMask); + } + + public readEmberBeaconClassificationParams(): EmberBeaconClassificationParams { + const minRssiForReceivingPkts = this.readUInt8();// Int8... + const beaconClassificationMask = this.readUInt16(); + + return {minRssiForReceivingPkts, beaconClassificationMask}; + } + + public writeEmberNeighborTableEntry(value: EmberNeighborTableEntry): void { + this.writeUInt16(value.shortId); + this.writeUInt8(value.averageLqi); + this.writeUInt8(value.inCost); + this.writeUInt8(value.outCost); + this.writeUInt8(value.age); + this.writeIeeeAddr(value.longId); + } + + public readEmberNeighborTableEntry(): EmberNeighborTableEntry { + const shortId = this.readUInt16(); + const averageLqi = this.readUInt8(); + const inCost = this.readUInt8(); + const outCost = this.readUInt8(); + const age = this.readUInt8(); + const longId = this.readIeeeAddr(); + + return { + shortId, + averageLqi, + inCost, + outCost, + age, + longId, + }; + } + + public writeEmberRouteTableEntry(value: EmberRouteTableEntry): void { + this.writeUInt16(value.destination); + this.writeUInt16(value.nextHop); + this.writeUInt8(value.status); + this.writeUInt8(value.age); + this.writeUInt8(value.concentratorType); + this.writeUInt8(value.routeRecordState); + } + + public readEmberRouteTableEntry(): EmberRouteTableEntry { + const destination = this.readUInt16(); + const nextHop = this.readUInt16(); + const status = this.readUInt8(); + const age = this.readUInt8(); + const concentratorType = this.readUInt8(); + const routeRecordState = this.readUInt8(); + + return { + destination, + nextHop, + status, + age, + concentratorType, + routeRecordState, + }; + } + + public writeEmberKeyData(value: EmberKeyData): void { + this.writeBuffer(value.contents, EMBER_ENCRYPTION_KEY_SIZE); + } + + public readEmberKeyData(): EmberKeyData { + const contents = this.readBufferCopy(EMBER_ENCRYPTION_KEY_SIZE); + + return {contents}; + } + + public writeSecManKey(value: SecManKey): void { + this.writeEmberKeyData(value); + } + + public readSecManKey(): SecManKey { + return this.readEmberKeyData(); + } + + public writeSecManContext(value: SecManContext): void { + this.writeUInt8(value.coreKeyType); + this.writeUInt8(value.keyIndex); + this.writeUInt16(value.derivedType); + this.writeIeeeAddr(value.eui64); + this.writeUInt8(value.multiNetworkIndex); + this.writeUInt8(value.flags); + this.writeUInt32(value.psaKeyAlgPermission); + } + + public readSecManContext(): SecManContext { + const core_key_type = this.readUInt8(); + const key_index = this.readUInt8(); + const derived_type = this.readUInt16(); + const eui64 = this.readIeeeAddr(); + const multi_network_index = this.readUInt8(); + const flags = this.readUInt8(); + const psa_key_alg_permission = this.readUInt32(); + + return { + coreKeyType: core_key_type, + keyIndex: key_index, + derivedType: derived_type, + eui64, + multiNetworkIndex: multi_network_index, + flags, + psaKeyAlgPermission: psa_key_alg_permission, + }; + } + + public writeSecManNetworkKeyInfo(value: SecManNetworkKeyInfo): void { + this.writeUInt8(value.networkKeySet ? 1 : 0); + this.writeUInt8(value.alternateNetworkKeySet ? 1 : 0); + this.writeUInt8(value.networkKeySequenceNumber); + this.writeUInt8(value.altNetworkKeySequenceNumber); + this.writeUInt32(value.networkKeyFrameCounter); + } + + public readSecManNetworkKeyInfo(): SecManNetworkKeyInfo { + const networkKeySet = this.readUInt8() === 1 ? true : false; + const alternateNetworkKeySet = this.readUInt8() === 1 ? true : false; + const networkKeySequenceNumber = this.readUInt8(); + const altNetworkKeySequenceNumber = this.readUInt8(); + const networkKeyFrameCounter = this.readUInt32(); + + return { + networkKeySet: networkKeySet, + alternateNetworkKeySet: alternateNetworkKeySet, + networkKeySequenceNumber: networkKeySequenceNumber, + altNetworkKeySequenceNumber: altNetworkKeySequenceNumber, + networkKeyFrameCounter: networkKeyFrameCounter, + }; + } + + public writeSecManAPSKeyMetadata(value: SecManAPSKeyMetadata): void { + this.writeUInt16(value.bitmask); + this.writeUInt32(value.outgoingFrameCounter); + this.writeUInt32(value.incomingFrameCounter); + this.writeUInt16(value.ttlInSeconds); + } + + public readSecManAPSKeyMetadata(): SecManAPSKeyMetadata { + const bitmask = this.readUInt16(); + const outgoing_frame_counter = this.readUInt32(); + const incoming_frame_counter = this.readUInt32(); + const ttl_in_seconds = this.readUInt16(); + + return { + bitmask, + outgoingFrameCounter: outgoing_frame_counter, + incomingFrameCounter: incoming_frame_counter, + ttlInSeconds: ttl_in_seconds, + }; + } + + public writeEmberTransientKeyData(value: EmberTransientKeyData): void { + this.writeIeeeAddr(value.eui64); + this.writeEmberKeyData(value.keyData); + this.writeUInt32(value.incomingFrameCounter); + this.writeUInt16(value.bitmask); + this.writeUInt16(value.remainingTimeSeconds); + this.writeUInt8(value.networkIndex); + } + + public readEmberTransientKeyData(): EmberTransientKeyData { + const eui64 = this.readIeeeAddr(); + const keyData = this.readEmberKeyData(); + const incomingFrameCounter = this.readUInt32(); + const bitmask = this.readUInt16(); + const remainingTimeSeconds = this.readUInt16(); + const networkIndex = this.readUInt8(); + + return { + eui64, + keyData, + incomingFrameCounter, + bitmask, + remainingTimeSeconds, + networkIndex, + }; + } + + public writeEmberInitialSecurityState(value: EmberInitialSecurityState): void { + this.writeUInt16(value.bitmask); + this.writeEmberKeyData(value.preconfiguredKey); + this.writeEmberKeyData(value.networkKey); + this.writeUInt8(value.networkKeySequenceNumber); + this.writeIeeeAddr(value.preconfiguredTrustCenterEui64); + } + + public readEmberInitialSecurityState(): EmberInitialSecurityState { + const bitmask = this.readUInt16(); + const preconfiguredKey = this.readEmberKeyData(); + const networkKey = this.readEmberKeyData(); + const networkKeySequenceNumber = this.readUInt8(); + const preconfiguredTrustCenterEui64 = this.readIeeeAddr(); + + return { + bitmask, + preconfiguredKey, + networkKey, + networkKeySequenceNumber, + preconfiguredTrustCenterEui64, + }; + } + + public writeEmberCurrentSecurityState(value: EmberCurrentSecurityState): void { + this.writeUInt16(value.bitmask); + this.writeIeeeAddr(value.trustCenterLongAddress); + } + + public readEmberCurrentSecurityState(): EmberCurrentSecurityState { + const bitmask = this.readUInt16(); + const trustCenterLongAddress = this.readIeeeAddr(); + + return {bitmask, trustCenterLongAddress}; + } + + public writeEmberChildData(value: EmberChildData): void { + this.writeIeeeAddr(value.eui64); + this.writeUInt8(value.type); + this.writeUInt16(value.id); + this.writeUInt8(value.phy); + this.writeUInt8(value.power); + this.writeUInt8(value.timeout); + this.writeUInt32(value.remainingTimeout); + } + + public readEmberChildData(): EmberChildData { + const eui64 = this.readIeeeAddr(); + const type = this.readUInt8(); + const id = this.readUInt16(); + const phy = this.readUInt8(); + const power = this.readUInt8(); + const timeout = this.readUInt8(); + const remainingTimeout = this.readUInt32(); + + return { + eui64, + type, + id, + phy, + power, + timeout, + remainingTimeout, + }; + } + + public readEmberZigbeeNetwork(): EmberZigbeeNetwork { + const channel = this.readUInt8(); + const panId = this.readUInt16(); + const extendedPanId = this.readListUInt8({length: EXTENDED_PAN_ID_SIZE}); + const allowingJoin = this.readUInt8(); + const stackProfile = this.readUInt8(); + const nwkUpdateId = this.readUInt8(); + + return { + channel, + panId, + extendedPanId, + allowingJoin, + stackProfile, + nwkUpdateId, + }; + } + + public writeEmberZigbeeNetwork(value: EmberZigbeeNetwork): void { + this.writeUInt8(value.channel); + this.writeUInt16(value.panId); + this.writeListUInt8(value.extendedPanId); + this.writeUInt8(value.allowingJoin); + this.writeUInt8(value.stackProfile); + this.writeUInt8(value.nwkUpdateId); + } + + public writeEmberCertificateData(value: EmberCertificateData): void { + this.writeBuffer(value.contents, EMBER_CERTIFICATE_SIZE); + } + + public readEmberCertificateData(): EmberCertificateData { + const contents = this.readBufferCopy(EMBER_CERTIFICATE_SIZE); + + return {contents}; + } + + public writeEmberPublicKeyData(value: EmberPublicKeyData): void { + this.writeBuffer(value.contents, EMBER_PUBLIC_KEY_SIZE); + } + + public readEmberPublicKeyData(): EmberPublicKeyData { + const contents = this.readBufferCopy(EMBER_PUBLIC_KEY_SIZE); + + return {contents}; + } + + public writeEmberPrivateKeyData(value: EmberPrivateKeyData): void { + this.writeBuffer(value.contents, EMBER_PRIVATE_KEY_SIZE); + } + + public readEmberPrivateKeyData(): EmberPrivateKeyData { + const contents = this.readBufferCopy(EMBER_PRIVATE_KEY_SIZE); + + return {contents}; + } + + public writeEmberSmacData(value: EmberSmacData): void { + this.writeBuffer(value.contents, EMBER_SMAC_SIZE); + } + + public readEmberSmacData(): EmberSmacData { + const contents = this.readBufferCopy(EMBER_SMAC_SIZE); + + return {contents}; + } + + public writeEmberSignatureData(value: EmberSignatureData): void { + this.writeBuffer(value.contents, EMBER_SIGNATURE_SIZE); + } + + public readEmberSignatureData(): EmberSignatureData { + const contents = this.readBufferCopy(EMBER_SIGNATURE_SIZE); + + return {contents}; + } + + public writeEmberCertificate283k1Data(value: EmberCertificate283k1Data): void { + this.writeBuffer(value.contents, EMBER_CERTIFICATE_283K1_SIZE); + } + + public readEmberCertificate283k1Data(): EmberCertificate283k1Data { + const contents = this.readBufferCopy(EMBER_CERTIFICATE_283K1_SIZE); + + return {contents}; + } + + public writeEmberPublicKey283k1Data(value: EmberPublicKey283k1Data): void { + this.writeBuffer(value.contents, EMBER_PUBLIC_KEY_283K1_SIZE); + } + + public readEmberPublicKey283k1Data(): EmberPublicKey283k1Data { + const contents = this.readBufferCopy(EMBER_PUBLIC_KEY_283K1_SIZE); + + return {contents}; + } + + public writeEmberPrivateKey283k1Data(value: EmberPrivateKey283k1Data): void { + this.writeBuffer(value.contents, EMBER_PRIVATE_KEY_283K1_SIZE); + } + + public readEmberPrivateKey283k1Data(): EmberPrivateKey283k1Data { + const contents = this.readBufferCopy(EMBER_PRIVATE_KEY_283K1_SIZE); + + return {contents}; + } + + public writeEmberSignature283k1Data(value: EmberSignature283k1Data): void { + this.writeBuffer(value.contents, EMBER_SIGNATURE_283K1_SIZE); + } + + public readEmberSignature283k1Data(): EmberSignature283k1Data { + const contents = this.readBufferCopy(EMBER_SIGNATURE_283K1_SIZE); + + return {contents}; + } + + public writeEmberAesMmoHashContext(context: EmberAesMmoHashContext): void { + this.writeBuffer(context.result, EMBER_AES_HASH_BLOCK_SIZE); + this.writeUInt32(context.length); + } + + public readEmberAesMmoHashContext(): EmberAesMmoHashContext { + const result = this.readBufferCopy(EMBER_AES_HASH_BLOCK_SIZE); + const length = this.readUInt32(); + + return {result, length}; + } + + public writeEmberMessageDigest(value: EmberMessageDigest): void { + this.writeBuffer(value.contents, EMBER_AES_HASH_BLOCK_SIZE); + } + + public readEmberMessageDigest(): EmberMessageDigest { + const contents = this.readBufferCopy(EMBER_AES_HASH_BLOCK_SIZE); + + return {contents}; + } + + public writeEmberNetworkInitStruct(networkInitStruct: EmberNetworkInitStruct): void { + this.writeUInt16(networkInitStruct.bitmask); + } + + public readEmberNetworkInitStruct(): EmberNetworkInitStruct { + const bitmask = this.readUInt16(); + + return {bitmask}; + } + + public writeEmberZllNetwork(network: EmberZllNetwork): void { + this.writeEmberZigbeeNetwork(network.zigbeeNetwork); + this.writeEmberZllSecurityAlgorithmData(network.securityAlgorithm); + this.writeIeeeAddr(network.eui64); + this.writeUInt16(network.nodeId); + this.writeUInt16(network.state); + this.writeUInt8(network.nodeType); + this.writeUInt8(network.numberSubDevices); + this.writeUInt8(network.totalGroupIdentifiers); + this.writeUInt8(network.rssiCorrection); + } + + public readEmberZllNetwork(): EmberZllNetwork { + const zigbeeNetwork = this.readEmberZigbeeNetwork(); + const securityAlgorithm = this.readEmberZllSecurityAlgorithmData(); + const eui64 = this.readIeeeAddr(); + const nodeId = this.readUInt16(); + const state = this.readUInt16(); + const nodeType = this.readUInt8(); + const numberSubDevices = this.readUInt8(); + const totalGroupIdentifiers = this.readUInt8(); + const rssiCorrection = this.readUInt8(); + + return { + zigbeeNetwork, + securityAlgorithm, + eui64, + nodeId, + state, + nodeType, + numberSubDevices, + totalGroupIdentifiers, + rssiCorrection, + }; + } + + public writeEmberZllSecurityAlgorithmData(data: EmberZllSecurityAlgorithmData): void { + this.writeUInt32(data.transactionId); + this.writeUInt32(data.responseId); + this.writeUInt16(data.bitmask); + } + + public readEmberZllSecurityAlgorithmData(): EmberZllSecurityAlgorithmData { + const transactionId = this.readUInt32(); + const responseId = this.readUInt32(); + const bitmask = this.readUInt16(); + + return {transactionId, responseId, bitmask}; + } + + public writeEmberZllInitialSecurityState(state: EmberZllInitialSecurityState): void { + this.writeUInt32(state.bitmask); + this.writeUInt8(state.keyIndex); + this.writeEmberKeyData(state.encryptionKey); + this.writeEmberKeyData(state.preconfiguredKey); + } + + public writeEmberTokTypeStackZllData(data: EmberTokTypeStackZllData): void { + this.writeUInt32(data.bitmask); + this.writeUInt16(data.freeNodeIdMin); + this.writeUInt16(data.freeNodeIdMax); + this.writeUInt16(data.myGroupIdMin); + this.writeUInt16(data.freeGroupIdMin); + this.writeUInt16(data.freeGroupIdMax); + this.writeUInt8(data.rssiCorrection); + } + + public readEmberTokTypeStackZllData(): EmberTokTypeStackZllData { + const bitmask = this.readUInt32(); + const freeNodeIdMin = this.readUInt16(); + const freeNodeIdMax = this.readUInt16(); + const myGroupIdMin = this.readUInt16(); + const freeGroupIdMin = this.readUInt16(); + const freeGroupIdMax = this.readUInt16(); + const rssiCorrection = this.readUInt8(); + + return { + bitmask, + freeNodeIdMin, + freeNodeIdMax, + myGroupIdMin, + freeGroupIdMin, + freeGroupIdMax, + rssiCorrection, + }; + } + + public writeEmberTokTypeStackZllSecurity(security: EmberTokTypeStackZllSecurity): void { + this.writeUInt32(security.bitmask); + this.writeUInt8(security.keyIndex); + this.writeBuffer(security.encryptionKey, EMBER_ENCRYPTION_KEY_SIZE); + this.writeBuffer(security.preconfiguredKey, EMBER_ENCRYPTION_KEY_SIZE); + } + + public readEmberTokTypeStackZllSecurity(): EmberTokTypeStackZllSecurity { + const bitmask = this.readUInt32(); + const keyIndex = this.readUInt8(); + const encryptionKey = this.readBufferCopy(EMBER_ENCRYPTION_KEY_SIZE); + const preconfiguredKey = this.readBufferCopy(EMBER_ENCRYPTION_KEY_SIZE); + + return { + bitmask, + keyIndex, + encryptionKey, + preconfiguredKey, + }; + } + + public writeEmberGpAddress(value: EmberGpAddress): void { + this.writeUInt8(value.applicationId); + + if (value.applicationId === EmberGpApplicationId.SOURCE_ID) { + this.writeUInt32(value.sourceId); + this.writeUInt32(value.sourceId);// filler + } else if (value.applicationId === EmberGpApplicationId.IEEE_ADDRESS) { + this.writeIeeeAddr(value.gpdIeeeAddress); + } + + this.writeUInt8(value.endpoint); + } + + public readEmberGpAddress(): EmberGpAddress { + const applicationId = this.readUInt8(); + + if (applicationId === EmberGpApplicationId.SOURCE_ID) { + const sourceId = this.readUInt32(); + this.readUInt32();// filler + const endpoint = this.readUInt8(); + + return {applicationId, sourceId, endpoint}; + } else if (applicationId === EmberGpApplicationId.IEEE_ADDRESS) { + const gpdIeeeAddress = this.readIeeeAddr(); + const endpoint = this.readUInt8(); + + return {applicationId, gpdIeeeAddress, endpoint}; + } + + return null; + } + + public readEmberGpSinkList(): EmberGpSinkListEntry[] { + const list: EmberGpSinkListEntry[] = []; + + for (let i = 0; i < GP_SINK_LIST_ENTRIES; i++) { + const type: EmberGpSinkType = this.readUInt8(); + + switch (type) { + case EmberGpSinkType.FULL_UNICAST: + case EmberGpSinkType.LW_UNICAST: + case EmberGpSinkType.UNUSED: + default: + const sinkNodeId = this.readUInt16(); + const sinkEUI = this.readIeeeAddr(); + + list.push({ + type, + unicast: { + sinkNodeId, + sinkEUI, + } + }); + break; + case EmberGpSinkType.D_GROUPCAST: + case EmberGpSinkType.GROUPCAST: + const alias = this.readUInt16(); + const groupID = this.readUInt16(); + + // fillers + this.readUInt16(); + this.readUInt16(); + this.readUInt16(); + + list.push({ + type, + groupcast: { + alias, + groupID, + } + }); + break; + } + } + + return list; + } + + public writeEmberGpSinkList(value: EmberGpSinkListEntry[]): void { + for (let i = 0; i < GP_SINK_LIST_ENTRIES; i++) { + this.writeUInt8(value[i].type); + + switch (value[i].type) { + case EmberGpSinkType.FULL_UNICAST: + case EmberGpSinkType.LW_UNICAST: + case EmberGpSinkType.UNUSED: + default: + this.writeUInt16(value[i].unicast.sinkNodeId); + this.writeIeeeAddr(value[i].unicast.sinkEUI);// changed 8 to const var + + break; + + case EmberGpSinkType.D_GROUPCAST: + case EmberGpSinkType.GROUPCAST: + this.writeUInt16(value[i].groupcast.alias); + this.writeUInt16(value[i].groupcast.groupID); + //fillers + this.writeUInt16(value[i].groupcast.alias); + this.writeUInt16(value[i].groupcast.groupID); + this.writeUInt16(value[i].groupcast.alias); + break; + } + } + } + + public readEmberGpProxyTableEntry(): EmberGpProxyTableEntry { + const status = this.readUInt8(); + const options = this.readUInt32(); + const gpd = this.readEmberGpAddress(); + const assignedAlias = this.readUInt16(); + const securityOptions = this.readUInt8(); + const gpdSecurityFrameCounter = this.readUInt32(); + const gpdKey = this.readEmberKeyData(); + const sinkList = this.readEmberGpSinkList(); + const groupcastRadius = this.readUInt8(); + const searchCounter = this.readUInt8(); + + return { + status, + options, + gpd, + assignedAlias, + securityOptions, + gpdSecurityFrameCounter, + gpdKey, + sinkList, + groupcastRadius, + searchCounter, + }; + } + + public writeEmberGpProxyTableEntry(value: EmberGpProxyTableEntry): void { + this.writeUInt8(value.status); + this.writeUInt32(value.options); + this.writeEmberGpAddress(value.gpd); + this.writeUInt16(value.assignedAlias); + this.writeUInt8(value.securityOptions); + this.writeUInt32(value.gpdSecurityFrameCounter); + this.writeEmberKeyData(value.gpdKey); + this.writeEmberGpSinkList(value.sinkList); + this.writeUInt8(value.groupcastRadius); + this.writeUInt8(value.searchCounter); + } + + public readEmberGpSinkTableEntry(): EmberGpSinkTableEntry { + const status = this.readUInt8(); + const options = this.readUInt16(); + const gpd = this.readEmberGpAddress(); + const deviceId = this.readUInt8(); + const sinkList = this.readEmberGpSinkList(); + const assignedAlias = this.readUInt16(); + const groupcastRadius = this.readUInt8(); + const securityOptions = this.readUInt8(); + const gpdSecurityFrameCounter = this.readUInt32(); + const gpdKey = this.readEmberKeyData(); + + return { + status, + options, + gpd, + deviceId, + sinkList, + assignedAlias, + groupcastRadius, + securityOptions, + gpdSecurityFrameCounter, + gpdKey, + }; + } + + public writeEmberGpSinkTableEntry(value: EmberGpSinkTableEntry): void { + this.writeUInt8(value.status); + this.writeUInt16(value.options); + this.writeEmberGpAddress(value.gpd); + this.writeUInt8(value.deviceId); + this.writeEmberGpSinkList(value.sinkList); + this.writeUInt16(value.assignedAlias); + this.writeUInt8(value.groupcastRadius); + this.writeUInt8(value.securityOptions); + this.writeUInt32(value.gpdSecurityFrameCounter); + this.writeEmberKeyData(value.gpdKey); + } + + public writeEmberDutyCycleLimits(limits: EmberDutyCycleLimits): void { + this.writeUInt16(limits.limitThresh); + this.writeUInt16(limits.critThresh); + this.writeUInt16(limits.suspLimit); + } + + public readEmberDutyCycleLimits(): EmberDutyCycleLimits { + const limitThresh = this.readUInt16(); + const critThresh = this.readUInt16(); + const suspLimit = this.readUInt16(); + + return { + limitThresh, + critThresh, + suspLimit, + }; + } + + public writeEmberPerDeviceDutyCycle(maxDevices: number, arrayOfDeviceDutyCycles: EmberPerDeviceDutyCycle[]): void { + this.writeUInt16(maxDevices); + + for (let i = 0; i < maxDevices; i++) { + this.writeUInt16(arrayOfDeviceDutyCycles[i].nodeId); + this.writeUInt16(arrayOfDeviceDutyCycles[i].dutyCycleConsumed); + } + } + + public readEmberPerDeviceDutyCycle(): EmberPerDeviceDutyCycle[] { + const maxDevices = this.readUInt8(); + const arrayOfDeviceDutyCycles: EmberPerDeviceDutyCycle[] = []; + + for (let i = 0; i < maxDevices; i++) { + const nodeId = this.readUInt16(); + const dutyCycleConsumed = this.readUInt16(); + + arrayOfDeviceDutyCycles.push({nodeId, dutyCycleConsumed}); + } + + return arrayOfDeviceDutyCycles; + } + + public readEmberZllDeviceInfoRecord(): EmberZllDeviceInfoRecord { + const ieeeAddress = this.readIeeeAddr(); + const endpointId = this.readUInt8(); + const profileId = this.readUInt16(); + const deviceId = this.readUInt16(); + const version = this.readUInt8(); + const groupIdCount = this.readUInt8(); + + return { + ieeeAddress, + endpointId, + profileId, + deviceId, + version, + groupIdCount, + }; + } + + public readEmberZllInitialSecurityState(): EmberZllInitialSecurityState { + const bitmask = this.readUInt32(); + const keyIndex = this.readUInt8(); + const encryptionKey = this.readEmberKeyData(); + const preconfiguredKey = this.readEmberKeyData(); + + return { + bitmask, + keyIndex, + encryptionKey, + preconfiguredKey, + }; + } + + public readEmberZllAddressAssignment(): EmberZllAddressAssignment { + const nodeId = this.readUInt16(); + const freeNodeIdMin = this.readUInt16(); + const freeNodeIdMax = this.readUInt16(); + const groupIdMin = this.readUInt16(); + const groupIdMax = this.readUInt16(); + const freeGroupIdMin = this.readUInt16(); + const freeGroupIdMax = this.readUInt16(); + + return { + nodeId, + freeNodeIdMin, + freeNodeIdMax, + groupIdMin, + groupIdMax, + freeGroupIdMin, + freeGroupIdMax, + }; + } + + public writeEmberBeaconIterator(value: EmberBeaconIterator): void { + this.writeUInt8(value.beacon.channel); + this.writeUInt8(value.beacon.lqi); + this.writeUInt8(value.beacon.rssi); + this.writeUInt8(value.beacon.depth); + this.writeUInt8(value.beacon.nwkUpdateId); + this.writeUInt8(value.beacon.power); + this.writeUInt8(value.beacon.parentPriority); + this.writeUInt8(value.beacon.enhanced ? 1 : 0); + this.writeUInt8(value.beacon.permitJoin ? 1 : 0); + this.writeUInt8(value.beacon.hasCapacity ? 1 : 0); + this.writeUInt16(value.beacon.panId); + this.writeUInt16(value.beacon.sender); + this.writeListUInt8(value.beacon.extendedPanId); + this.writeUInt8(value.index); + } + + public readEmberBeaconIterator(): EmberBeaconIterator { + const channel = this.readUInt8(); + const lqi = this.readUInt8(); + const rssi = this.readUInt8(); + const depth = this.readUInt8(); + const nwkUpdateId = this.readUInt8(); + const power = this.readUInt8(); + const parentPriority = this.readUInt8(); + const enhanced = this.readUInt8() === 1 ? true : false; + const permitJoin = this.readUInt8() === 1 ? true : false; + const hasCapacity = this.readUInt8() === 1 ? true : false; + const panId = this.readUInt16(); + const sender = this.readUInt16(); + const extendedPanId = this.readListUInt8({length: EXTENDED_PAN_ID_SIZE}); + const index = this.readUInt8(); + + return { + beacon: { + channel, + lqi, + rssi, + depth, + nwkUpdateId, + power, + parentPriority, + enhanced, + permitJoin, + hasCapacity, + panId, + sender, + extendedPanId, + supportedKeyNegotiationMethods: 0, + extended_beacon: false, + tcConnectivity: true, + longUptime: true, + preferParent: true, + macDataPollKeepalive: true, + endDeviceKeepalive: true + }, + index, + }; + } + + public writeEmberBeaconData(value: EmberBeaconData): void { + this.writeUInt8(value.channel); + this.writeUInt8(value.lqi); + this.writeUInt8(value.rssi); + this.writeUInt8(value.depth); + this.writeUInt8(value.nwkUpdateId); + this.writeUInt8(value.power); + this.writeUInt8(value.parentPriority); + this.writeUInt8(value.enhanced ? 1 : 0); + this.writeUInt8(value.permitJoin ? 1 : 0); + this.writeUInt8(value.hasCapacity ? 1 : 0); + this.writeUInt16(value.panId); + this.writeUInt16(value.sender); + this.writeListUInt8(value.extendedPanId); + } + + public readEmberBeaconData(): EmberBeaconData { + const channel = this.readUInt8(); + const lqi = this.readUInt8(); + const rssi = this.readUInt8(); + const depth = this.readUInt8(); + const nwkUpdateId = this.readUInt8(); + const power = this.readUInt8(); + const parentPriority = this.readUInt8(); + const enhanced = this.readUInt8() === 1 ? true : false; + const permitJoin = this.readUInt8() === 1 ? true : false; + const hasCapacity = this.readUInt8() === 1 ? true : false; + const panId = this.readUInt16(); + const sender = this.readUInt16(); + const extendedPanId = this.readListUInt8({length: EXTENDED_PAN_ID_SIZE}); + + return { + channel, + lqi, + rssi, + depth, + nwkUpdateId, + power, + parentPriority, + enhanced, + permitJoin, + hasCapacity, + panId, + sender, + extendedPanId, + supportedKeyNegotiationMethods: 0, + extended_beacon: false, + tcConnectivity: true, + longUptime: true, + preferParent: true, + macDataPollKeepalive: true, + endDeviceKeepalive: true + }; + } + + public writeEmberTokenData(tokenData: EmberTokenData): void { + this.writeUInt32(tokenData.size); + this.writeBuffer(tokenData.data, tokenData.size); + } + + public readEmberTokenData(): EmberTokenData { + const size = this.readUInt32(); + const data = this.readBufferCopy(size); + + return {size, data}; + } + + public readEmberTokenInfo(): EmberTokenInfo { + const nvm3Key = this.readUInt32(); + const isCnt = this.readUInt8() === 1 ? true : false; + const isIdx = this.readUInt8() === 1 ? true : false; + const size = this.readUInt8(); + const arraySize = this.readUInt8(); + + return { + nvm3Key, + isCnt, + isIdx, + size, + arraySize, + }; + } + + public writeEmberTokenInfo(tokenInfo: EmberTokenInfo): void { + this.writeUInt32(tokenInfo.nvm3Key); + this.writeUInt8(tokenInfo.isCnt ? 1 : 0); + this.writeUInt8(tokenInfo.isIdx ? 1 : 0); + this.writeUInt8(tokenInfo.size); + this.writeUInt8(tokenInfo.arraySize); + } +} diff --git a/src/adapter/ember/ezsp/consts.ts b/src/adapter/ember/ezsp/consts.ts new file mode 100644 index 0000000000..ba22cb11b0 --- /dev/null +++ b/src/adapter/ember/ezsp/consts.ts @@ -0,0 +1,148 @@ +//------------------------------------------------------------------------------------------------- +// EZSP Protocol + +/** Latest EZSP protocol version */ +export const EZSP_PROTOCOL_VERSION = 0x0D; + +/** EZSP max length + Frame Control extra byte + Frame ID extra byte */ +export const EZSP_MAX_FRAME_LENGTH = (200 + 1 + 1); + +/** EZSP Sequence Index for both legacy and extended frame format */ +export const EZSP_SEQUENCE_INDEX = 0; + +/** Legacy EZSP Frame Format */ +export const EZSP_MIN_FRAME_LENGTH = 3; +/** Legacy EZSP Frame Format */ +export const EZSP_FRAME_CONTROL_INDEX = 1; +/** Legacy EZSP Frame Format */ +export const EZSP_FRAME_ID_INDEX = 2; +/** Legacy EZSP Frame Format */ +export const EZSP_PARAMETERS_INDEX = 3; + +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_MIN_FRAME_LENGTH = 5; +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_FRAME_ID_LENGTH = 2; +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX = 1; +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX = 2; +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_FRAME_ID_LB_INDEX = 3; +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_FRAME_ID_HB_INDEX = 4; +/** Extended EZSP Frame Format */ +export const EZSP_EXTENDED_PARAMETERS_INDEX = 5; + +export const EZSP_STACK_TYPE_MESH = 0x02; + + +//---- Frame Control Lower Byte (LB) Definitions + +/** + * The high bit of the frame control lower byte indicates the direction of the message. + * Commands are sent from the Host to the EM260. Responses are sent from the EM260 to the Host. + */ +export const EZSP_FRAME_CONTROL_DIRECTION_MASK = 0x80; +export const EZSP_FRAME_CONTROL_COMMAND = 0x00; +export const EZSP_FRAME_CONTROL_RESPONSE = 0x80; + +/** Bits 5 and 6 of the frame control lower byte carry the network index the ezsp message is related to. + * The NCP upon processing an incoming EZSP command, temporary switches the current network to the one indicated in the EZSP frame control. + */ +export const EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK = 0x60; +export const EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET = 5; + +// Command Frame Control Fields + +/** The EM260 enters the sleep mode specified by the command frame control once it has sent its response. */ +export const EZSP_FRAME_CONTROL_SLEEP_MODE_MASK = 0x03; + +// Response Frame Control Fields + +/** + * The overflow flag in the response frame control indicates to the Host that one or more callbacks occurred since the previous response + * and there was not enough memory available to report them to the Host. + */ +export const EZSP_FRAME_CONTROL_OVERFLOW_MASK = 0x01; +export const EZSP_FRAME_CONTROL_NO_OVERFLOW = 0x00; +export const EZSP_FRAME_CONTROL_OVERFLOW = 0x01; + +/** + * The truncated flag in the response frame control indicates to the Host that the response has been truncated. + * This will happen if there is not enough memory available to complete the response or if the response + * would have exceeded the maximum EZSP frame length. + */ +export const EZSP_FRAME_CONTROL_TRUNCATED_MASK = 0x02; +export const EZSP_FRAME_CONTROL_NOT_TRUNCATED = 0x00; +export const EZSP_FRAME_CONTROL_TRUNCATED = 0x02; + +/** + * The pending callbacks flag in the response frame control lower byte indicates to the Host that there is at least one callback ready to be read. + * This flag is clear if the response to a callback command read the last pending callback. + */ +export const EZSP_FRAME_CONTROL_PENDING_CB_MASK = 0x04; +export const EZSP_FRAME_CONTROL_PENDING_CB = 0x04; +export const EZSP_FRAME_CONTROL_NO_PENDING_CB = 0x00; + +/** The synchronous callback flag in the response frame control lower byte indicates this ezsp frame is the response to an ezspCallback(). */ +export const EZSP_FRAME_CONTROL_SYNCH_CB_MASK = 0x08; +export const EZSP_FRAME_CONTROL_SYNCH_CB = 0x08; +export const EZSP_FRAME_CONTROL_NOT_SYNCH_CB = 0x00; + +/** + * The asynchronous callback flag in the response frame control lower byte indicates this ezsp frame is a callback sent asynchronously by the ncp. + * This flag may be set only in the uart version when EZSP_VALUE_UART_SYNCH_CALLBACKS is 0. + */ +export const EZSP_FRAME_CONTROL_ASYNCH_CB_MASK = 0x10; +export const EZSP_FRAME_CONTROL_ASYNCH_CB = 0x10; +export const EZSP_FRAME_CONTROL_NOT_ASYNCH_CB = 0x00; + +//---- Frame Control Higher Byte (HB) Definitions + +/** Bit 7 of the frame control higher byte indicates whether security is enabled or not. */ +export const EZSP_EXTENDED_FRAME_CONTROL_SECURITY_MASK = 0x80; +export const EZSP_EXTENDED_FRAME_CONTROL_SECURE = 0x80; +export const EZSP_EXTENDED_FRAME_CONTROL_UNSECURE = 0x00; + +/** Bit 6 of the frame control higher byte indicates whether padding is enabled or not. */ +export const EZSP_EXTENDED_FRAME_CONTROL_PADDING_MASK = 0x40; +export const EZSP_EXTENDED_FRAME_CONTROL_PADDED = 0x40; +export const EZSP_EXTENDED_FRAME_CONTROL_UNPADDED = 0x00; + +/** Bits 0 and 1 of the frame control higher byte indicates the frame format version. */ +export const EZSP_EXTENDED_FRAME_FORMAT_VERSION_MASK = 0x03; +export const EZSP_EXTENDED_FRAME_FORMAT_VERSION = 0x01; + +/** Reserved bits 2-5 */ +export const EZSP_EXTENDED_FRAME_CONTROL_RESERVED_MASK = 0x3C; + +//------------------------------------------------------------------------------------------------- +// EZSP Data types + +/** Size of EUI64 (an IEEE address) in bytes (8). */ +export const EUI64_SIZE = 8; +/** Size of an extended PAN identifier in bytes (8). */ +export const EXTENDED_PAN_ID_SIZE = 8; +/** Size of an encryption key in bytes (16). */ +export const EMBER_ENCRYPTION_KEY_SIZE = 16; +/** Size of Implicit Certificates used for Certificate-based Key Exchange(CBKE). */ +export const EMBER_CERTIFICATE_SIZE = 48; +/** Size of Public Keys used in Elliptical Cryptography ECMQV algorithms. */ +export const EMBER_PUBLIC_KEY_SIZE = 22; +/** Size of Private Keys used in Elliptical Cryptography ECMQV algorithms. */ +export const EMBER_PRIVATE_KEY_SIZE = 21; +/** Size of the SMAC used in Elliptical Cryptography ECMQV algorithms. */ +export const EMBER_SMAC_SIZE = 16; +/** Size of the DSA signature used in Elliptical Cryptography Digital Signature Algorithms. */ +export const EMBER_SIGNATURE_SIZE = 42; +/** The size of AES-128 MMO hash is 16-bytes. This is defined in the core. ZigBee specification. */ +export const EMBER_AES_HASH_BLOCK_SIZE = 16; +/** Size of Implicit Certificates used for Certificate Based Key Exchange using the ECC283K1 curve in bytes. */ +export const EMBER_CERTIFICATE_283K1_SIZE = 74; +/** Size of Public Keys used in SECT283k1 Elliptical Cryptography ECMQV algorithms */ +export const EMBER_PUBLIC_KEY_283K1_SIZE = 37; +/** Size of Private Keys used SECT283k1 in Elliptical Cryptography ECMQV algorithms*/ +export const EMBER_PRIVATE_KEY_283K1_SIZE = 36; +/** Size of the DSA signature used in SECT283k1 Elliptical Cryptography Digital Signature Algorithms. */ +export const EMBER_SIGNATURE_283K1_SIZE = 72; diff --git a/src/adapter/ember/ezsp/enums.ts b/src/adapter/ember/ezsp/enums.ts new file mode 100644 index 0000000000..62221847c1 --- /dev/null +++ b/src/adapter/ember/ezsp/enums.ts @@ -0,0 +1,958 @@ +/** EZSP Frame IDs */ +export enum EzspFrameID { + // Configuration Frames + VERSION = 0x0000, + GET_CONFIGURATION_VALUE = 0x0052, + SET_CONFIGURATION_VALUE = 0x0053, + READ_ATTRIBUTE = 0x0108, + WRITE_ATTRIBUTE = 0x0109, + ADD_ENDPOINT = 0x0002, + SET_POLICY = 0x0055, + GET_POLICY = 0x0056, + SEND_PAN_ID_UPDATE = 0x0057, + GET_VALUE = 0x00AA, + GET_EXTENDED_VALUE = 0x0003, + SET_VALUE = 0x00AB, + SET_PASSIVE_ACK_CONFIG = 0x0105, + + // Utilities Frames + NOP = 0x0005, + ECHO = 0x0081, + INVALID_COMMAND = 0x0058, + CALLBACK = 0x0006, + NO_CALLBACKS = 0x0007, + SET_TOKEN = 0x0009, + GET_TOKEN = 0x000A, + GET_MFG_TOKEN = 0x000B, + SET_MFG_TOKEN = 0x000C, + STACK_TOKEN_CHANGED_HANDLER = 0x000D, + GET_RANDOM_NUMBER = 0x0049, + SET_TIMER = 0x000E, + GET_TIMER = 0x004E, + TIMER_HANDLER = 0x000F, + DEBUG_WRITE = 0x0012, + READ_AND_CLEAR_COUNTERS = 0x0065, + READ_COUNTERS = 0x00F1, + COUNTER_ROLLOVER_HANDLER = 0x00F2, + DELAY_TEST = 0x009D, + GET_LIBRARY_STATUS = 0x0001, + GET_XNCP_INFO = 0x0013, + CUSTOM_FRAME = 0x0047, + CUSTOM_FRAME_HANDLER = 0x0054, + GET_EUI64 = 0x0026, + GET_NODE_ID = 0x0027, + GET_PHY_INTERFACE_COUNT = 0x00FC, + GET_TRUE_RANDOM_ENTROPY_SOURCE = 0x004F, + + // Networking Frames + SET_MANUFACTURER_CODE = 0x0015, + SET_POWER_DESCRIPTOR = 0x0016, + NETWORK_INIT = 0x0017, + NETWORK_STATE = 0x0018, + STACK_STATUS_HANDLER = 0x0019, + START_SCAN = 0x001A, + ENERGY_SCAN_RESULT_HANDLER = 0x0048, + NETWORK_FOUND_HANDLER = 0x001B, + SCAN_COMPLETE_HANDLER = 0x001C, + UNUSED_PAN_ID_FOUND_HANDLER = 0x00D2, + FIND_UNUSED_PAN_ID = 0x00D3, + STOP_SCAN = 0x001D, + FORM_NETWORK = 0x001E, + JOIN_NETWORK = 0x001F, + JOIN_NETWORK_DIRECTLY = 0x003B, + LEAVE_NETWORK = 0x0020, + FIND_AND_REJOIN_NETWORK = 0x0021, + PERMIT_JOINING = 0x0022, + CHILD_JOIN_HANDLER = 0x0023, + ENERGY_SCAN_REQUEST = 0x009C, + GET_NETWORK_PARAMETERS = 0x0028, + GET_RADIO_PARAMETERS = 0x00FD, + GET_PARENT_CHILD_PARAMETERS = 0x0029, + GET_CHILD_DATA = 0x004A, + SET_CHILD_DATA = 0x00AC, + CHILD_ID = 0x0106, + CHILD_INDEX = 0x0107, + GET_SOURCE_ROUTE_TABLE_TOTAL_SIZE = 0x00C3, + GET_SOURCE_ROUTE_TABLE_FILLED_SIZE = 0x00C2, + GET_SOURCE_ROUTE_TABLE_ENTRY = 0x00C1, + GET_NEIGHBOR = 0x0079, + GET_NEIGHBOR_FRAME_COUNTER = 0x003E, + SET_NEIGHBOR_FRAME_COUNTER = 0x00AD, + SET_ROUTING_SHORTCUT_THRESHOLD = 0x00D0, + GET_ROUTING_SHORTCUT_THRESHOLD = 0x00D1, + NEIGHBOR_COUNT = 0x007A, + GET_ROUTE_TABLE_ENTRY = 0x007B, + SET_RADIO_POWER = 0x0099, + SET_RADIO_CHANNEL = 0x009A, + GET_RADIO_CHANNEL = 0x00FF, + SET_RADIO_IEEE802154_CCA_MODE = 0x0095, + SET_CONCENTRATOR = 0x0010, + SET_BROKEN_ROUTE_ERROR_CODE = 0x0011, + MULTI_PHY_START = 0x00F8, + MULTI_PHY_STOP = 0x00F9, + MULTI_PHY_SET_RADIO_POWER = 0x00FA, + SEND_LINK_POWER_DELTA_REQUEST = 0x00F7, + MULTI_PHY_SET_RADIO_CHANNEL = 0x00FB, + GET_DUTY_CYCLE_STATE = 0x0035, + SET_DUTY_CYCLE_LIMITS_IN_STACK = 0x0040, + GET_DUTY_CYCLE_LIMITS = 0x004B, + GET_CURRENT_DUTY_CYCLE = 0x004C, + DUTY_CYCLE_HANDLER = 0x004D, + GET_FIRST_BEACON = 0x003D, + GET_NEXT_BEACON = 0x0004, + GET_NUM_STORED_BEACONS = 0x0008, + CLEAR_STORED_BEACONS = 0x003C, + SET_LOGICAL_AND_RADIO_CHANNEL = 0x00B9, + + // Binding Frames + CLEAR_BINDING_TABLE = 0x002A, + SET_BINDING = 0x002B, + GET_BINDING = 0x002C, + DELETE_BINDING = 0x002D, + BINDING_IS_ACTIVE = 0x002E, + GET_BINDING_REMOTE_NODE_ID = 0x002F, + SET_BINDING_REMOTE_NODE_ID = 0x0030, + REMOTE_SET_BINDING_HANDLER = 0x0031, + REMOTE_DELETE_BINDING_HANDLER = 0x0032, + + // Messaging Frames + MAXIMUM_PAYLOAD_LENGTH = 0x0033, + SEND_UNICAST = 0x0034, + SEND_BROADCAST = 0x0036, + PROXY_BROADCAST = 0x0037, + SEND_MULTICAST = 0x0038, + SEND_MULTICAST_WITH_ALIAS = 0x003A, + SEND_REPLY = 0x0039, + MESSAGE_SENT_HANDLER = 0x003F, + SEND_MANY_TO_ONE_ROUTE_REQUEST = 0x0041, + POLL_FOR_DATA = 0x0042, + POLL_COMPLETE_HANDLER = 0x0043, + POLL_HANDLER = 0x0044, + INCOMING_SENDER_EUI64_HANDLER = 0x0062, + INCOMING_MESSAGE_HANDLER = 0x0045, + SET_SOURCE_ROUTE_DISCOVERY_MODE = 0x005A, + INCOMING_MANY_TO_ONE_ROUTE_REQUEST_HANDLER = 0x007D, + INCOMING_ROUTE_ERROR_HANDLER = 0x0080, + INCOMING_NETWORK_STATUS_HANDLER = 0x00C4, + INCOMING_ROUTE_RECORD_HANDLER = 0x0059, + SET_SOURCE_ROUTE = 0x00AE, + UNICAST_CURRENT_NETWORK_KEY = 0x0050, + ADDRESS_TABLE_ENTRY_IS_ACTIVE = 0x005B, + SET_ADDRESS_TABLE_REMOTE_EUI64 = 0x005C, + SET_ADDRESS_TABLE_REMOTE_NODE_ID = 0x005D, + GET_ADDRESS_TABLE_REMOTE_EUI64 = 0x005E, + GET_ADDRESS_TABLE_REMOTE_NODE_ID = 0x005F, + SET_EXTENDED_TIMEOUT = 0x007E, + GET_EXTENDED_TIMEOUT = 0x007F, + REPLACE_ADDRESS_TABLE_ENTRY = 0x0082, + LOOKUP_NODE_ID_BY_EUI64 = 0x0060, + LOOKUP_EUI64_BY_NODE_ID = 0x0061, + GET_MULTICAST_TABLE_ENTRY = 0x0063, + SET_MULTICAST_TABLE_ENTRY = 0x0064, + ID_CONFLICT_HANDLER = 0x007C, + WRITE_NODE_DATA = 0x00FE, + SEND_RAW_MESSAGE = 0x0096, + SEND_RAW_MESSAGE_EXTENDED = 0x0051, + MAC_PASSTHROUGH_MESSAGE_HANDLER = 0x0097, + MAC_FILTER_MATCH_MESSAGE_HANDLER = 0x0046, + RAW_TRANSMIT_COMPLETE_HANDLER = 0x0098, + SET_MAC_POLL_FAILURE_WAIT_TIME = 0x00F4, + SET_BEACON_CLASSIFICATION_PARAMS = 0x00EF, + GET_BEACON_CLASSIFICATION_PARAMS = 0x00F3, + + // Security Frames + SET_INITIAL_SECURITY_STATE = 0x0068, + GET_CURRENT_SECURITY_STATE = 0x0069, + EXPORT_KEY = 0x0114, + IMPORT_KEY = 0x0115, + SWITCH_NETWORK_KEY_HANDLER = 0x006e, + FIND_KEY_TABLE_ENTRY = 0x0075, + SEND_TRUST_CENTER_LINK_KEY = 0x0067, + ERASE_KEY_TABLE_ENTRY = 0x0076, + CLEAR_KEY_TABLE = 0x00B1, + REQUEST_LINK_KEY = 0x0014, + UPDATE_TC_LINK_KEY = 0x006C, + ZIGBEE_KEY_ESTABLISHMENT_HANDLER = 0x009B, + CLEAR_TRANSIENT_LINK_KEYS = 0x006B, + GET_NETWORK_KEY_INFO = 0x0116, + GET_APS_KEY_INFO = 0x010C, + IMPORT_LINK_KEY = 0x010E, + EXPORT_LINK_KEY_BY_INDEX = 0x010F, + EXPORT_LINK_KEY_BY_EUI = 0x010D, + CHECK_KEY_CONTEXT = 0x0110, + IMPORT_TRANSIENT_KEY = 0x0111, + EXPORT_TRANSIENT_KEY_BY_INDEX = 0x0112, + EXPORT_TRANSIENT_KEY_BY_EUI = 0x0113, + + // Trust Center Frames + TRUST_CENTER_JOIN_HANDLER = 0x0024, + BROADCAST_NEXT_NETWORK_KEY = 0x0073, + BROADCAST_NETWORK_KEY_SWITCH = 0x0074, + AES_MMO_HASH = 0x006F, + REMOVE_DEVICE = 0x00A8, + UNICAST_NWK_KEY_UPDATE = 0x00A9, + + // Certificate Based Key Exchange (CBKE) Frames + GENERATE_CBKE_KEYS = 0x00A4, + GENERATE_CBKE_KEYS_HANDLER = 0x009E, + CALCULATE_SMACS = 0x009F, + CALCULATE_SMACS_HANDLER = 0x00A0, + GENERATE_CBKE_KEYS283K1 = 0x00E8, + GENERATE_CBKE_KEYS_HANDLER283K1 = 0x00E9, + CALCULATE_SMACS283K1 = 0x00EA, + CALCULATE_SMACS_HANDLER283K1 = 0x00EB, + CLEAR_TEMPORARY_DATA_MAYBE_STORE_LINK_KEY = 0x00A1, + CLEAR_TEMPORARY_DATA_MAYBE_STORE_LINK_KEY283K1 = 0x00EE, + GET_CERTIFICATE = 0x00A5, + GET_CERTIFICATE283K1 = 0x00EC, + DSA_SIGN = 0x00A6, + DSA_SIGN_HANDLER = 0x00A7, + DSA_VERIFY = 0x00A3, + DSA_VERIFY_HANDLER = 0x0078, + DSA_VERIFY283K1 = 0x00B0, + SET_PREINSTALLED_CBKE_DATA = 0x00A2, + SAVE_PREINSTALLED_CBKE_DATA283K1 = 0x00ED, + + // Mfglib Frames + MFGLIB_START = 0x0083, + MFGLIB_END = 0x0084, + MFGLIB_START_TONE = 0x0085, + MFGLIB_STOP_TONE = 0x0086, + MFGLIB_START_STREAM = 0x0087, + MFGLIB_STOP_STREAM = 0x0088, + MFGLIB_SEND_PACKET = 0x0089, + MFGLIB_SET_CHANNEL = 0x008a, + MFGLIB_GET_CHANNEL = 0x008b, + MFGLIB_SET_POWER = 0x008c, + MFGLIB_GET_POWER = 0x008d, + MFGLIB_RX_HANDLER = 0x008e, + + // Bootloader Frames + LAUNCH_STANDALONE_BOOTLOADER = 0x008f, + SEND_BOOTLOAD_MESSAGE = 0x0090, + GET_STANDALONE_BOOTLOADER_VERSION_PLAT_MICRO_PHY = 0x0091, + INCOMING_BOOTLOAD_MESSAGE_HANDLER = 0x0092, + BOOTLOAD_TRANSMIT_COMPLETE_HANDLER = 0x0093, + AES_ENCRYPT = 0x0094, + + // ZLL Frames + ZLL_NETWORK_OPS = 0x00B2, + ZLL_SET_INITIAL_SECURITY_STATE = 0x00B3, + ZLL_SET_SECURITY_STATE_WITHOUT_KEY = 0x00CF, + ZLL_START_SCAN = 0x00B4, + ZLL_SET_RX_ON_WHEN_IDLE = 0x00B5, + ZLL_NETWORK_FOUND_HANDLER = 0x00B6, + ZLL_SCAN_COMPLETE_HANDLER = 0x00B7, + ZLL_ADDRESS_ASSIGNMENT_HANDLER = 0x00B8, + ZLL_TOUCH_LINK_TARGET_HANDLER = 0x00BB, + ZLL_GET_TOKENS = 0x00BC, + ZLL_SET_DATA_TOKEN = 0x00BD, + ZLL_SET_NON_ZLL_NETWORK = 0x00BF, + IS_ZLL_NETWORK = 0x00BE, + ZLL_SET_RADIO_IDLE_MODE = 0x00D4, + ZLL_GET_RADIO_IDLE_MODE = 0x00BA, + SET_ZLL_NODE_TYPE = 0x00D5, + SET_ZLL_ADDITIONAL_STATE = 0x00D6, + ZLL_OPERATION_IN_PROGRESS = 0x00D7, + ZLL_RX_ON_WHEN_IDLE_GET_ACTIVE = 0x00D8, + ZLL_SCANNING_COMPLETE = 0x00F6, + GET_ZLL_PRIMARY_CHANNEL_MASK = 0x00D9, + GET_ZLL_SECONDARY_CHANNEL_MASK = 0x00DA, + SET_ZLL_PRIMARY_CHANNEL_MASK = 0x00DB, + SET_ZLL_SECONDARY_CHANNEL_MASK = 0x00DC, + ZLL_CLEAR_TOKENS = 0x0025, + + // WWAH Frames + SET_PARENT_CLASSIFICATION_ENABLED = 0x00E7, + GET_PARENT_CLASSIFICATION_ENABLED = 0x00F0, + SET_LONG_UP_TIME = 0x00E3, + SET_HUB_CONNECTIVITY = 0x00E4, + IS_UP_TIME_LONG = 0x00E5, + IS_HUB_CONNECTED = 0x00E6, + + // Green Power Frames + GP_PROXY_TABLE_PROCESS_GP_PAIRING = 0x00C9, + D_GP_SEND = 0x00C6, + D_GP_SENT_HANDLER = 0x00C7, + GPEP_INCOMING_MESSAGE_HANDLER = 0x00C5, + GP_PROXY_TABLE_GET_ENTRY = 0x00C8, + GP_PROXY_TABLE_LOOKUP = 0x00C0, + GP_SINK_TABLE_GET_ENTRY = 0x00DD, + GP_SINK_TABLE_LOOKUP = 0x00DE, + GP_SINK_TABLE_SET_ENTRY = 0x00DF, + GP_SINK_TABLE_REMOVE_ENTRY = 0x00E0, + GP_SINK_TABLE_FIND_OR_ALLOCATE_ENTRY = 0x00E1, + GP_SINK_TABLE_CLEAR_ALL = 0x00E2, + GP_SINK_TABLE_INIT = 0x0070, + GP_SINK_TABLE_SET_SECURITY_FRAME_COUNTER = 0x00F5, + GP_SINK_COMMISSION = 0x010A, + GP_TRANSLATION_TABLE_CLEAR = 0x010B, + GP_SINK_TABLE_GET_NUMBER_OF_ACTIVE_ENTRIES = 0x0118, + + // Token Interface Frames + GET_TOKEN_COUNT = 0x0100, + GET_TOKEN_INFO = 0x0101, + GET_TOKEN_DATA = 0x0102, + SET_TOKEN_DATA = 0x0103, + RESET_NODE = 0x0104, + GP_SECURITY_TEST_VECTORS = 0x0117, + TOKEN_FACTORY_RESET = 0x0077 +} + +/** Identifies a configuration value. uint8_t */ +export enum EzspConfigId { + // 0x00? + /** + * The NCP no longer supports configuration of packet buffer count at runtime + * using this parameter. Packet buffers must be configured using the + * EMBER_PACKET_BUFFER_COUNT macro when building the NCP project. + */ + PACKET_BUFFER_COUNT = 0x01, + /** + * The maximum number of router neighbors the stack can keep track of. A + * neighbor is a node within radio range. + */ + NEIGHBOR_TABLE_SIZE = 0x02, + /** + * The maximum number of APS retried messages the stack can be transmitting at + * any time. + */ + APS_UNICAST_MESSAGE_COUNT = 0x03, + /** + * The maximum number of non-volatile bindings supported by the stack. + */ + BINDING_TABLE_SIZE = 0x04, + /** + * The maximum number of EUI64 to network address associations that the stack + * can maintain for the application. (Note, the total number of such address + * associations maintained by the NCP is the sum of the value of this setting + * and the value of ::TRUST_CENTER_ADDRESS_CACHE_SIZE. + */ + ADDRESS_TABLE_SIZE = 0x05, + /** + * The maximum number of multicast groups that the device may be a member of. + */ + MULTICAST_TABLE_SIZE = 0x06, + /** + * The maximum number of destinations to which a node can route messages. This + * includes both messages originating at this node and those relayed for + * others. + */ + ROUTE_TABLE_SIZE = 0x07, + /** + * The number of simultaneous route discoveries that a node will support. + */ + DISCOVERY_TABLE_SIZE = 0x08, + // 0x0A? + // 0x0B? + /** + * Specifies the stack profile. + */ + STACK_PROFILE = 0x0C, + /** + * The security level used for security at the MAC and network layers. The + * supported values are 0 (no security) and 5 (payload is encrypted and a + * four-byte MIC is used for authentication). + */ + SECURITY_LEVEL = 0x0D, + // 0x0E? + // 0x0F? + /** + * The maximum number of hops for a message. + */ + MAX_HOPS = 0x10, + /** + * The maximum number of end device children that a router will support. + */ + MAX_END_DEVICE_CHILDREN = 0x11, + /** + * The maximum amount of time that the MAC will hold a message for indirect + * transmission to a child. + */ + INDIRECT_TRANSMISSION_TIMEOUT = 0x12, + /** + * The maximum amount of time that an end device child can wait between polls. + * If no poll is heard within this timeout, then the parent removes the end + * device from its tables. Value range 0-14. The timeout corresponding to a + * value of zero is 10 seconds. The timeout corresponding to a nonzero value N + * is 2^N minutes, ranging from 2^1 = 2 minutes to 2^14 = 16384 minutes. + */ + END_DEVICE_POLL_TIMEOUT = 0x13, + // 0x14? + // 0x15? + // 0x16? + /** + * Enables boost power mode and/or the alternate transmitter output. + */ + TX_POWER_MODE = 0x17, + /** + * 0: Allow this node to relay messages. 1: Prevent this node from relaying + * messages. + */ + DISABLE_RELAY = 0x18, + /** + * The maximum number of EUI64 to network address associations that the Trust + * Center can maintain. These address cache entries are reserved for and + * reused by the Trust Center when processing device join/rejoin + * authentications. This cache size limits the number of overlapping joins the + * Trust Center can process within a narrow time window (e.g. two seconds), + * and thus should be set to the maximum number of near simultaneous joins the + * Trust Center is expected to accommodate. (Note, the total number of such + * address associations maintained by the NCP is the sum of the value of this + * setting and the value of ::ADDRESS_TABLE_SIZE.) + */ + TRUST_CENTER_ADDRESS_CACHE_SIZE = 0x19, + /** + * The size of the source route table. + */ + SOURCE_ROUTE_TABLE_SIZE = 0x1A, + // 0x1B? + /** The number of blocks of a fragmented message that can be sent in a single window. */ + FRAGMENT_WINDOW_SIZE = 0x1C, + /** The time the stack will wait (in milliseconds) between sending blocks of a fragmented message. */ + FRAGMENT_DELAY_MS = 0x1D, + /** + * The size of the Key Table used for storing individual link keys (if the + * device is a Trust Center) or Application Link Keys (if the device is a normal node). + */ + KEY_TABLE_SIZE = 0x1E, + /** The APS ACK timeout value. The stack waits this amount of time between resends of APS retried messages. */ + APS_ACK_TIMEOUT = 0x1F, + /** + * The duration of a beacon jitter, in the units used by the 15.4 scan + * parameter (((1 << duration) + 1) * 15ms), when responding to a beacon request. + */ + BEACON_JITTER_DURATION = 0x20, + // 0x21? + /** The number of PAN id conflict reports that must be received by the network manager within one minute to trigger a PAN id change. */ + PAN_ID_CONFLICT_REPORT_THRESHOLD = 0x22, + /** + * The timeout value in minutes for how long the Trust Center or a normal node + * waits for the ZigBee Request Key to complete. On the Trust Center this + * controls whether or not the device buffers the request, waiting for a + * matching pair of ZigBee Request Key. If the value is non-zero, the Trust + * Center buffers and waits for that amount of time. If the value is zero, the + * Trust Center does not buffer the request and immediately responds to the + * request. Zero is the most compliant behavior. + */ + REQUEST_KEY_TIMEOUT = 0x24, + // 0x25? + // 0x26? + // 0x27? + // 0x28? + /** + * This value indicates the size of the runtime modifiable certificate table. + * Normally certificates are stored in MFG tokens but this table can be used + * to field upgrade devices with new Smart Energy certificates. This value + * cannot be set, it can only be queried. + */ + CERTIFICATE_TABLE_SIZE = 0x29, + /** + * This is a bitmask that controls which incoming ZDO request messages are + * passed to the application. The bits are defined in the + * EmberZdoConfigurationFlags enumeration. To see if the application is + * required to send a ZDO response in reply to an incoming message, the + * application must check the APS options bitfield within the + * incomingMessageHandler callback to see if the + * EMBER_APS_OPTION_ZDO_RESPONSE_REQUIRED flag is set. + */ + APPLICATION_ZDO_FLAGS = 0x2A, + /** The maximum number of broadcasts during a single broadcast timeout period. */ + BROADCAST_TABLE_SIZE = 0x2B, + /** The size of the MAC filter list table. */ + MAC_FILTER_TABLE_SIZE = 0x2C, + /** The number of supported networks. */ + SUPPORTED_NETWORKS = 0x2D, + /** + * Whether multicasts are sent to the RxOnWhenIdle=true address (0xFFFD) or + * the sleepy broadcast address (0xFFFF). The RxOnWhenIdle=true address is the + * ZigBee compliant destination for multicasts. + */ + SEND_MULTICASTS_TO_SLEEPY_ADDRESS = 0x2E, + /** ZLL group address initial configuration. */ + ZLL_GROUP_ADDRESSES = 0x2F, + /** ZLL rssi threshold initial configuration. */ + ZLL_RSSI_THRESHOLD = 0x30, + // 0x31? + // 0x32? + /** Toggles the MTORR flow control in the stack. */ + MTORR_FLOW_CONTROL = 0x33, + /** Setting the retry queue size. Applies to all queues. Default value in the sample applications is 16. */ + RETRY_QUEUE_SIZE = 0x34, + /** + * Setting the new broadcast entry threshold. The number (BROADCAST_TABLE_SIZE + * - NEW_BROADCAST_ENTRY_THRESHOLD) of broadcast table entries are reserved + * for relaying the broadcast messages originated on other devices. The local + * device will fail to originate a broadcast message after this threshold is + * reached. Setting this value to BROADCAST_TABLE_SIZE and greater will + * effectively kill this limitation. + */ + NEW_BROADCAST_ENTRY_THRESHOLD = 0x35, + /** + * The length of time, in seconds, that a trust center will store a transient + * link key that a device can use to join its network. A transient key is + * added with a call to emberAddTransientLinkKey. After the transient key is + * added, it will be removed once this amount of time has passed. A joining + * device will not be able to use that key to join until it is added again on + * the trust center. The default value is 300 seconds, i.e., 5 minutes. + */ + TRANSIENT_KEY_TIMEOUT_S = 0x36, + /** The number of passive acknowledgements to record from neighbors before we stop re-transmitting broadcasts */ + BROADCAST_MIN_ACKS_NEEDED = 0x37, + /** + * The length of time, in seconds, that a trust center will allow a Trust + * Center (insecure) rejoin for a device that is using the well-known link + * key. This timeout takes effect once rejoins using the well-known key has + * been allowed. This command updates the + * sli_zigbee_allow_tc_rejoins_using_well_known_key_timeout_sec value. + */ + TC_REJOINS_USING_WELL_KNOWN_KEY_TIMEOUT_S = 0x38, + /** Valid range of a CTUNE value is 0x0000-0x01FF. Higher order bits (0xFE00) of the 16-bit value are ignored. */ + CTUNE_VALUE = 0x39, + // 0x3A? + // 0x3B? + // 0x3C? + // 0x3D? + // 0x3E? + // 0x3F? + /** + * To configure non trust center node to assume a concentrator type of the + * trust center it join to, until it receive many-to-one route request from + * the trust center. For the trust center node, concentrator type is + * configured from the concentrator plugin. The stack by default assumes trust + * center be a low RAM concentrator that make other devices send route record + * to the trust center even without receiving a many-to-one route request. The + * default concentrator type can be changed by setting appropriate + * EmberAssumeTrustCenterConcentratorType config value. + */ + ASSUME_TC_CONCENTRATOR_TYPE = 0x40, + /** This is green power proxy table size. This value is read-only and cannot be set at runtime */ + GP_PROXY_TABLE_SIZE = 0x41, + /** This is green power sink table size. This value is read-only and cannot be set at runtime */ + GP_SINK_TABLE_SIZE = 0x42 +} + +/** Identifies a policy decision. */ +export enum EzspDecisionId { + /** + * BINDING_MODIFICATION_POLICY default decision. + * + * Do not allow the local binding table to be changed by remote nodes. + */ + DISALLOW_BINDING_MODIFICATION = 0x10, + /** + * BINDING_MODIFICATION_POLICY decision. + * + * Allow remote nodes to change the local binding table. + */ + ALLOW_BINDING_MODIFICATION = 0x11, + /** + * BINDING_MODIFICATION_POLICY decision. + * + * Allows remote nodes to set local binding entries only if the entries correspond to endpoints + * defined on the device, and for output clusters bound to those endpoints. + */ + CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS = 0x12, + /** + * UNICAST_REPLIES_POLICY default decision. + * + * The NCP will automatically send an empty reply (containing no payload) for every unicast received. + * */ + HOST_WILL_NOT_SUPPLY_REPLY = 0x20, + /** + * UNICAST_REPLIES_POLICY decision. + * + * The NCP will only send a reply if it receives a sendReply command from the Host. + */ + HOST_WILL_SUPPLY_REPLY = 0x21, + /** + * POLL_HANDLER_POLICY default decision. + * + * Do not inform the Host when a child polls. + */ + POLL_HANDLER_IGNORE = 0x30, + /** + * POLL_HANDLER_POLICY decision. + * + * Generate a pollHandler callback when a child polls. + */ + POLL_HANDLER_CALLBACK = 0x31, + /** + * MESSAGE_CONTENTS_IN_CALLBACK_POLICY default decision. + * + * Include only the message tag in the messageSentHandler callback. + */ + MESSAGE_TAG_ONLY_IN_CALLBACK = 0x40, + /** + * MESSAGE_CONTENTS_IN_CALLBACK_POLICY decision. + * + * Include both the message tag and the message contents in the messageSentHandler callback. + */ + MESSAGE_TAG_AND_CONTENTS_IN_CALLBACK = 0x41, + /** + * TC_KEY_REQUEST_POLICY decision. + * + * When the Trust Center receives a request for a Trust Center link key, it will be ignored. + */ + DENY_TC_KEY_REQUESTS = 0x50, + /** + * TC_KEY_REQUEST_POLICY decision. + * + * When the Trust Center receives a request for a Trust Center link key, it will reply to it with the corresponding key. + */ + ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY = 0x51, + /** + * TC_KEY_REQUEST_POLICY decision. + * + * When the Trust Center receives a request for a Trust Center link key, it will generate a key to send to the joiner. + * After generation, the key will be added to the transient key tabe and After verification, this key will be added into the link key table. + */ + ALLOW_TC_KEY_REQUEST_AND_GENERATE_NEW_KEY = 0x52, + /** + * APP_KEY_REQUEST_POLICY decision. + * When the Trust Center receives a request for an application link key, it will be ignored. + * */ + DENY_APP_KEY_REQUESTS = 0x60, + /** + * APP_KEY_REQUEST_POLICY decision. + * + * When the Trust Center receives a request for an application link key, it will randomly generate a key and send it to both partners. + */ + ALLOW_APP_KEY_REQUESTS = 0x61, + /** Indicates that packet validate library checks are enabled on the NCP. */ + PACKET_VALIDATE_LIBRARY_CHECKS_ENABLED = 0x62, + /** Indicates that packet validate library checks are NOT enabled on the NCP. */ + PACKET_VALIDATE_LIBRARY_CHECKS_DISABLED = 0x63 +} + +/** + * This is the policy decision bitmask that controls the trust center decision strategies. + * The bitmask is modified and extracted from the EzspDecisionId for supporting bitmask operations. + * uint16_t + */ +export enum EzspDecisionBitmask { + /** Disallow joins and rejoins. */ + DEFAULT_CONFIGURATION = 0x0000, + /** Send the network key to all joining devices. */ + ALLOW_JOINS = 0x0001, + /** Send the network key to all rejoining devices. */ + ALLOW_UNSECURED_REJOINS = 0x0002, + /** Send the network key in the clear. */ + SEND_KEY_IN_CLEAR = 0x0004, + /** Do nothing for unsecured rejoins. */ + IGNORE_UNSECURED_REJOINS = 0x0008, + /** Allow joins if there is an entry in the transient key table. */ + JOINS_USE_INSTALL_CODE_KEY = 0x0010, + /** Delay sending the network key to a new joining device. */ + DEFER_JOINS = 0x0020, +} + +/** Identifies a policy. */ +export enum EzspPolicyId { + /** Controls trust center behavior. */ + TRUST_CENTER_POLICY = 0x00, + /** Controls how external binding modification requests are handled. */ + BINDING_MODIFICATION_POLICY = 0x01, + /** Controls whether the Host supplies unicast replies. */ + UNICAST_REPLIES_POLICY = 0x02, + /** Controls whether pollHandler callbacks are generated. */ + POLL_HANDLER_POLICY = 0x03, + /** Controls whether the message contents are included in the messageSentHandler callback. */ + MESSAGE_CONTENTS_IN_CALLBACK_POLICY = 0x04, + /** Controls whether the Trust Center will respond to Trust Center link key requests. */ + TC_KEY_REQUEST_POLICY = 0x05, + /** Controls whether the Trust Center will respond to application link key requests. */ + APP_KEY_REQUEST_POLICY = 0x06, + /** + * Controls whether ZigBee packets that appear invalid are automatically dropped by the stack. + * A counter will be incremented when this occurs. + */ + PACKET_VALIDATE_LIBRARY_POLICY = 0x07, + /** Controls whether the stack will process ZLL messages. */ + ZLL_POLICY = 0x08, + /** + * Controls whether Trust Center (insecure) rejoins for devices using the well-known link key are accepted. + * If rejoining using the well-known key is allowed, + * it is disabled again after sli_zigbee_allow_tc_rejoins_using_well_known_key_timeout_sec seconds. + */ + TC_REJOINS_USING_WELL_KNOWN_KEY_POLICY = 0x09 +} + +/** Identifies a value. */ +export enum EzspValueId { + /** The contents of the node data stack token. */ + TOKEN_STACK_NODE_DATA = 0x00, + /** The types of MAC passthrough messages that the host wishes to receive. */ + MAC_PASSTHROUGH_FLAGS = 0x01, + /** + * The source address used to filter legacy EmberNet messages when the + * EMBER_MAC_PASSTHROUGH_EMBERNET_SOURCE flag is set in MAC_PASSTHROUGH_FLAGS. + */ + EMBERNET_PASSTHROUGH_SOURCE_ADDRESS = 0x02, + /** The number of available internal RAM general purpose buffers. Read only. */ + FREE_BUFFERS = 0x03, + /** Selects sending synchronous callbacks in ezsp-uart. */ + UART_SYNCH_CALLBACKS = 0x04, + /** + * The maximum incoming transfer size for the local node. + * Default value is set to 82 and does not use fragmentation. Sets the value in Node Descriptor. + * To set, this takes the input of a uint8 array of length 2 where you pass the lower byte at index 0 and upper byte at index 1. + */ + MAXIMUM_INCOMING_TRANSFER_SIZE = 0x05, + /** + * The maximum outgoing transfer size for the local node. + * Default value is set to 82 and does not use fragmentation. Sets the value in Node Descriptor. + * To set, this takes the input of a uint8 array of length 2 where you pass the lower byte at index 0 and upper byte at index 1. + */ + MAXIMUM_OUTGOING_TRANSFER_SIZE = 0x06, + /** A bool indicating whether stack tokens are written to persistent storage as they change. */ + STACK_TOKEN_WRITING = 0x07, + /** A read-only value indicating whether the stack is currently performing a rejoin. */ + STACK_IS_PERFORMING_REJOIN = 0x08, + /** A list of EmberMacFilterMatchData values. */ + MAC_FILTER_LIST = 0x09, + /** The Ember Extended Security Bitmask. */ + EXTENDED_SECURITY_BITMASK = 0x0A, + /** The node short ID. */ + NODE_SHORT_ID = 0x0B, + /** The descriptor capability of the local node. Write only. */ + DESCRIPTOR_CAPABILITY = 0x0C, + /** The stack device request sequence number of the local node. */ + STACK_DEVICE_REQUEST_SEQUENCE_NUMBER = 0x0D, + /** Enable or disable radio hold-off. */ + RADIO_HOLD_OFF = 0x0E, + /** The flags field associated with the endpoint data. */ + ENDPOINT_FLAGS = 0x0F, + /** Enable/disable the Mfg security config key settings. */ + MFG_SECURITY_CONFIG = 0x10, + /** Retrieves the version information from the stack on the NCP. */ + VERSION_INFO = 0x11, + /** + * This will get/set the rejoin reason noted by the host for a subsequent call to emberFindAndRejoinNetwork(). + * After a call to emberFindAndRejoinNetwork() the host's rejoin reason will be set to EMBER_REJOIN_REASON_NONE. + * The NCP will store the rejoin reason used by the call to emberFindAndRejoinNetwork(). + * Application is not required to do anything with this value. + * The App Framework sets this for cases of emberFindAndRejoinNetwork that it initiates, but if the app is invoking a rejoin directly, + * it should/can set this value to aid in debugging of any rejoin state machine issues over EZSP logs after the fact. + * The NCP doesn't do anything with this value other than cache it so you can read it later. + */ + NEXT_HOST_REJOIN_REASON = 0x12, + /** + * This is the reason that the last rejoin took place. This value may only be retrieved, not set. + * The rejoin may have been initiated by the stack (NCP) or the application (host). + * If a host initiated a rejoin the reason will be set by default to EMBER_REJOIN_DUE_TO_APP_EVENT_1. + * If the application wishes to denote its own rejoin reasons it can do so by calling + * ezspSetValue(EMBER_VALUE_HOST_REJOIN_REASON, EMBER_REJOIN_DUE_TO_APP_EVENT_X). + * X is a number corresponding to one of the app events defined. + * If the NCP initiated a rejoin it will record this value internally for retrieval by ezspGetValue(REAL_REJOIN_REASON). + */ + LAST_REJOIN_REASON = 0x13, + /** The next ZigBee sequence number. */ + NEXT_ZIGBEE_SEQUENCE_NUMBER = 0x14, + /** CCA energy detect threshold for radio. */ + CCA_THRESHOLD = 0x15, + /** The threshold value for a counter */ + SET_COUNTER_THRESHOLD = 0x17, + /** Resets all counters thresholds to 0xFF */ + RESET_COUNTER_THRESHOLDS = 0x18, + /** Clears all the counters */ + CLEAR_COUNTERS = 0x19, + /** The node's new certificate signed by the CA. */ + CERTIFICATE_283K1 = 0x1A, + /** The Certificate Authority's public key. */ + PUBLIC_KEY_283K1 = 0x1B, + /** The node's new static private key. */ + PRIVATE_KEY_283K1 = 0x1C, + // 0x1D? + // 0x1E? + // 0x1F? + // 0x20? + // 0x21? + // 0x22? + /** The NWK layer security frame counter value */ + NWK_FRAME_COUNTER = 0x23, + /** The APS layer security frame counter value. Managed by the stack. Users should not set these unless doing backup and restore. */ + APS_FRAME_COUNTER = 0x24, + /** Sets the device type to use on the next rejoin using device type */ + RETRY_DEVICE_TYPE = 0x25, + // 0x26? + // 0x27? + // 0x28? + /** Setting this byte enables R21 behavior on the NCP. */ + ENABLE_R21_BEHAVIOR = 0x29, + /** Configure the antenna mode(0-don't switch,1-primary,2-secondary,3-TX antenna diversity). */ + ANTENNA_MODE = 0x30, + /** Enable or disable packet traffic arbitration. */ + ENABLE_PTA = 0x31, + /** Set packet traffic arbitration configuration options. */ + PTA_OPTIONS = 0x32, + /** Configure manufacturing library options (0-non-CSMA transmits,1-CSMA transmits). To be used with Manufacturing Library. */ + MFGLIB_OPTIONS = 0x33, + /** + * Sets the flag to use either negotiated power by link power delta (LPD) or fixed power value provided by user + * while forming/joining a network for packet transmissions on sub-ghz interface. This is mainly for testing purposes. + */ + USE_NEGOTIATED_POWER_BY_LPD = 0x34, + /** Set packet traffic arbitration PWM options. */ + PTA_PWM_OPTIONS = 0x35, + /** Set packet traffic arbitration directional priority pulse width in microseconds. */ + PTA_DIRECTIONAL_PRIORITY_PULSE_WIDTH = 0x36, + /** Set packet traffic arbitration phy select timeout(ms). */ + PTA_PHY_SELECT_TIMEOUT = 0x37, + /** Configure the RX antenna mode: (0-do not switch; 1-primary; 2-secondary; 3-RX antenna diversity). */ + ANTENNA_RX_MODE = 0x38, + /** Configure the timeout to wait for the network key before failing a join. Acceptable timeout range [3,255]. Value is in seconds. */ + NWK_KEY_TIMEOUT = 0x39, + /** + * The number of failed CSMA attempts due to failed CCA made by the MAC before continuing transmission with CCA disabled. + * This is the same as calling the emberForceTxAfterFailedCca(uint8_t csmaAttempts) API. A value of 0 disables the feature. + */ + FORCE_TX_AFTER_FAILED_CCA_ATTEMPTS = 0x3A, + /** + * The length of time, in seconds, that a trust center will store a transient link key that a device can use to join its network. + * A transient key is added with a call to sl_zb_sec_man_import_transient_key. After the transient key is added, + * it will be removed once this amount of time has passed. A joining device will not be able to use that key to join + * until it is added again on the trust center. + * The default value is 300 seconds (5 minutes). + */ + TRANSIENT_KEY_TIMEOUT_S = 0x3B, + /** Cumulative energy usage metric since the last value reset of the coulomb counter plugin. Setting this value will reset the coulomb counter. */ + COULOMB_COUNTER_USAGE = 0x3C, + /** When scanning, configure the maximum number of beacons to store in cache. Each beacon consumes one packet buffer in RAM. */ + MAX_BEACONS_TO_STORE = 0x3D, + /** Set the mask to filter out unacceptable child timeout options on a router. */ + END_DEVICE_TIMEOUT_OPTIONS_MASK = 0x3E, + /** The end device keep-alive mode supported by the parent. */ + END_DEVICE_KEEP_ALIVE_SUPPORT_MODE = 0x3F, + /** + * Return the active radio config. Read only. + * Values are 0: Default, 1: Antenna Diversity, 2: Co-Existence, 3: Antenna Diversity and Co-Existence. + */ + ACTIVE_RADIO_CONFIG = 0x41, + /** Return the number of seconds the network will remain open. A return value of 0 indicates that the network is closed. Read only. */ + NWK_OPEN_DURATION = 0x42, + /** + * Timeout in milliseconds to store entries in the transient device table. + * If the devices are not authenticated before the timeout, the entry shall be purged + */ + TRANSIENT_DEVICE_TIMEOUT = 0x43, + /** + * Return information about the key storage on an NCP. + * Returns 0 if keys are in classic key storage, and 1 if they are located in PSA key storage. Read only. + */ + KEY_STORAGE_VERSION = 0x44, + /** Return activation state about TC Delayed Join on an NCP. A return value of 0 indicates that the feature is not activated. */ + DELAYED_JOIN_ACTIVATION = 0x45 +} + +/** + * Identifies a value based on specified characteristics. + * Each set of characteristics is unique to that value and is specified during the call to get the extended value. + * + * uint16_t + */ +export enum EzspExtendedValueId { + /** The flags field associated with the specified endpoint. Value is uint16_t */ + ENDPOINT_FLAGS = 0x0000, + /** + * This is the reason for the node to leave the network as well as the device that told it to leave. + * The leave reason is the 1st byte of the value while the node ID is the 2nd and 3rd byte. + * If the leave was caused due to an API call rather than an over the air message, the node ID will be EMBER_UNKNOWN_NODE_ID (0xFFFD). + */ + LAST_LEAVE_REASON = 0x0001, + /** This number of bytes of overhead required in the network frame for source routing to a particular destination. */ + GET_SOURCE_ROUTE_OVERHEAD = 0x0002 +} + +/** Flags associated with the endpoint data configured on the NCP. */ +export enum EzspEndpointFlag { + /** Indicates that the endpoint is disabled and NOT discoverable via ZDO. */ + DISABLED = 0x00, + /** Indicates that the endpoint is enabled and discoverable via ZDO. */ + ENABLED = 0x01 +} + +/** Notes the last leave reason. uint8_t */ +export enum EmberLeaveReason { + REASON_NONE = 0, + DUE_TO_NWK_LEAVE_MESSAGE = 1, + DUE_TO_APS_REMOVE_MESSAGE = 2, + // Currently, the stack does not process the ZDO leave message since it is optional. + DUE_TO_ZDO_LEAVE_MESSAGE = 3, + DUE_TO_ZLL_TOUCHLINK = 4, + + DUE_TO_APP_EVENT_1 = 0xFF, +}; + +/** Notes the last rejoin reason. uint8_t */ +export enum EmberRejoinReason { + REASON_NONE = 0, + DUE_TO_NWK_KEY_UPDATE = 1, + DUE_TO_LEAVE_MESSAGE = 2, + DUE_TO_NO_PARENT = 3, + DUE_TO_ZLL_TOUCHLINK = 4, + DUE_TO_END_DEVICE_REBOOT = 5, + + // App. Framework events + // 0xA0 - 0xE0 + // See af.h for a subset of defined rejoin reasons + + // Customer-defined Events + // These are numbered down from 0xFF so their assigned values + // need not change if more application events are needed. + DUE_TO_APP_EVENT_5 = 0xFB, + DUE_TO_APP_EVENT_4 = 0xFC, + DUE_TO_APP_EVENT_3 = 0xFD, + DUE_TO_APP_EVENT_2 = 0xFE, + DUE_TO_APP_EVENT_1 = 0xFF, +}; + +/** Manufacturing token IDs used by ezspGetMfgToken(). */ +export enum EzspMfgTokenId { + /** Custom version (2 bytes). */ + CUSTOM_VERSION = 0x00, + /** Manufacturing string (16 bytes). */ + STRING = 0x01, + /** Board name (16 bytes). */ + BOARD_NAME = 0x02, + /** Manufacturing ID (2 bytes). */ + MANUF_ID = 0x03, + /** Radio configuration (2 bytes). */ + PHY_CONFIG = 0x04, + /** Bootload AES key (16 bytes). */ + BOOTLOAD_AES_KEY = 0x05, + /** ASH configuration (40 bytes). */ + ASH_CONFIG = 0x06, + /** EZSP storage (8 bytes). */ + EZSP_STORAGE = 0x07, + /** + * Radio calibration data (64 bytes). 4 bytes are stored for each of the 16 channels. + * This token is not stored in the Flash Information Area. It is updated by the stack each time a calibration is performed. + */ + STACK_CAL_DATA = 0x08, + /** Certificate Based Key Exchange (CBKE) data (92 bytes). */ + CBKE_DATA = 0x09, + /** Installation code (20 bytes). */ + INSTALLATION_CODE = 0x0A, + /** + * Radio channel filter calibration data (1 byte). + * This token is not stored in the Flash Information Area. It is updated by the stack each time a calibration is performed. + */ + STACK_CAL_FILTER = 0x0B, + /** Custom EUI64 MAC address (8 bytes). */ + CUSTOM_EUI_64 = 0x0C, + /** CTUNE value (2 byte). */ + CTUNE = 0x0D +} + + +export enum EzspSleepMode { + /** Processor idle. */ + IDLE = 0x00, + /** Wake on interrupt or timer. */ + DEEP_SLEEP = 0x01, + /** Wake on interrupt only. */ + POWER_DOWN = 0x02, + /** Reserved */ + RESERVED_SLEEP = 0x03, +} \ No newline at end of file diff --git a/src/adapter/ember/ezsp/ezsp.ts b/src/adapter/ember/ezsp/ezsp.ts new file mode 100644 index 0000000000..f462e7352b --- /dev/null +++ b/src/adapter/ember/ezsp/ezsp.ts @@ -0,0 +1,7969 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import EventEmitter from "events"; +import {SerialPortOptions} from "../../tstype"; +import Cluster from "../../../zcl/definition/cluster"; +import {byteToBits, getMacCapFlags, highByte, highLowToInt, lowByte, lowHighBits} from "../utils/math"; +import { + EmberOutgoingMessageType, + EmberCounterType, + EmberDutyCycleState, + EmberEntropySource, + EmberEventUnits, + EmberLibraryId, + EmberLibraryStatus, + EmberMultiPhyNwkConfig, + EmberNetworkStatus, + EmberNodeType, + EmberStatus, + EzspNetworkScanType, + EzspStatus, + SLStatus, + EmberIncomingMessageType, + EmberSourceRouteDiscoveryMode, + EmberMacPassthroughType, + SecManKeyType, + EmberKeyStatus, + SecManFlag, + EmberDeviceUpdate, + EmberJoinDecision, + EzspZllNetworkOperation, + EmberGpSecurityLevel, + EmberGpKeyType, + EmberTXPowerMode, + EmberExtendedSecurityBitmask, + EmberStackError, + EmberInterpanMessageType, + EmberGpApplicationId, +} from "../enums"; +import { + EmberVersion, + EmberEUI64, + EmberPanId, + EmberBeaconData, + EmberBeaconIterator, + EmberBindingTableEntry, + EmberChildData, + EmberDutyCycleLimits, + EmberMultiPhyRadioParameters, + EmberNeighborTableEntry, + EmberNetworkInitStruct, + EmberNetworkParameters, + EmberNodeId, + EmberPerDeviceDutyCycle, + EmberRouteTableEntry, + EmberApsFrame, + EmberMulticastTableEntry, + EmberBeaconClassificationParams, + EmberInitialSecurityState, + EmberCurrentSecurityState, + SecManContext, + SecManKey, + SecManNetworkKeyInfo, + SecManAPSKeyMetadata, + EmberKeyData, + EmberAesMmoHashContext, + EmberPublicKeyData, + EmberCertificateData, + EmberSmacData, + EmberPublicKey283k1Data, + EmberCertificate283k1Data, + EmberMessageDigest, + EmberSignatureData, + EmberSignature283k1Data, + EmberPrivateKeyData, + EmberZllNetwork, + EmberZllInitialSecurityState, + EmberZllDeviceInfoRecord, + EmberZllAddressAssignment, + EmberTokTypeStackZllData, + EmberTokTypeStackZllSecurity, + EmberGpAddress, + EmberGpProxyTableEntry, + EmberGpSinkTableEntry, + EmberTokenInfo, + EmberTokenData, + EmberZigbeeNetwork +} from "../types"; +import { + EmberZdoStatus, + ZDOLQITableEntry, + ACTIVE_ENDPOINTS_RESPONSE, + BINDING_TABLE_RESPONSE, + BIND_RESPONSE, + IEEE_ADDRESS_RESPONSE, + LEAVE_RESPONSE, + LQI_TABLE_RESPONSE, + MATCH_DESCRIPTORS_RESPONSE, + NETWORK_ADDRESS_RESPONSE, + NODE_DESCRIPTOR_RESPONSE, + PERMIT_JOINING_RESPONSE, + POWER_DESCRIPTOR_RESPONSE, + ROUTING_TABLE_RESPONSE, + SIMPLE_DESCRIPTOR_RESPONSE, + UNBIND_RESPONSE, + ZDORoutingTableEntry, + ZDO_MESSAGE_OVERHEAD, + ZDO_PROFILE_ID, + ZDOBindingTableEntry, + END_DEVICE_ANNOUNCE, + IEEEAddressResponsePayload, + NetworkAddressResponsePayload, + MatchDescriptorsResponsePayload, + SimpleDescriptorResponsePayload, + NodeDescriptorResponsePayload, + PowerDescriptorResponsePayload, + ActiveEndpointsResponsePayload, + LQITableResponsePayload, + RoutingTableResponsePayload, + BindingTableResponsePayload, + EndDeviceAnnouncePayload +} from "../zdo"; +import { + EZSP_FRAME_CONTROL_ASYNCH_CB, + EZSP_FRAME_CONTROL_INDEX, + EZSP_MAX_FRAME_LENGTH, + EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX, + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + EZSP_EXTENDED_FRAME_FORMAT_VERSION, + EZSP_EXTENDED_FRAME_ID_HB_INDEX, + EZSP_EXTENDED_FRAME_ID_LB_INDEX, + EZSP_EXTENDED_PARAMETERS_INDEX, + EZSP_FRAME_CONTROL_COMMAND, + EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK, + EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET, + EZSP_FRAME_CONTROL_SLEEP_MODE_MASK, + EZSP_FRAME_ID_INDEX, + EZSP_PARAMETERS_INDEX, + EZSP_SEQUENCE_INDEX, + EZSP_FRAME_CONTROL_DIRECTION_MASK, + EZSP_FRAME_CONTROL_RESPONSE, + EZSP_FRAME_CONTROL_TRUNCATED_MASK, + EZSP_FRAME_CONTROL_TRUNCATED, + EZSP_FRAME_CONTROL_OVERFLOW_MASK, + EZSP_FRAME_CONTROL_OVERFLOW, + EZSP_FRAME_CONTROL_PENDING_CB_MASK, + EZSP_FRAME_CONTROL_PENDING_CB, + EXTENDED_PAN_ID_SIZE, +} from "./consts"; +import { + EmberLeaveReason, + EmberRejoinReason, + EzspConfigId, + EzspEndpointFlag, + EzspExtendedValueId, + EzspFrameID, + EzspMfgTokenId, + EzspPolicyId, + EzspSleepMode, + EzspValueId +} from "./enums"; +import {AshEvents, UartAsh} from "../uart/ash"; +import {EzspBuffer} from "../uart/queues"; +import {EzspBuffalo} from "./buffalo"; +import { + GP_PROFILE_ID, + HA_PROFILE_ID, + INTERPAN_APS_FRAME_CONTROL_NO_DELIVERY_MODE, + INTERPAN_APS_FRAME_DELIVERY_MODE_MASK, + INTERPAN_APS_FRAME_SECURITY, + INTERPAN_APS_MULTICAST_SIZE, + INTERPAN_APS_UNICAST_BROADCAST_SIZE, + LONG_DEST_FRAME_CONTROL, + MAC_ACK_REQUIRED, + MIN_STUB_APS_SIZE, + SHORT_DEST_FRAME_CONTROL, + STUB_NWK_FRAME_CONTROL, + STUB_NWK_SIZE, + TOUCHLINK_PROFILE_ID, + WILDCARD_PROFILE_ID +} from "../consts"; +import {FIXED_ENDPOINTS} from "../adapter/endpoints"; + +const debug = Debug('zigbee-herdsman:adapter:ember:ezsp'); + + +/** + * Simple object to resolve/timeout on command waiting for response. + */ +type EzspWaiter = { + timer: NodeJS.Timeout, + resolve: (value: EzspStatus | PromiseLike) => void; +}; + +/** no multi-network atm, so just use const */ +const DEFAULT_NETWORK_INDEX = FIXED_ENDPOINTS[0].networkIndex; +/** other values not supported atm */ +const DEFAULT_SLEEP_MODE = EzspSleepMode.IDLE; +/** Maximum number of times we attempt to reset the NCP and start the ASH protocol. */ +const MAX_INIT_ATTEMPTS = 5; +/** + * This is the max hops that the network can support - used to determine the max source route overhead + * and broadcast radius if we havent defined MAX_HOPS then define based on profile ID + */ +// #ifdef HAS_SECURITY_PROFILE_SE +// export const ZA_MAX_HOPS = 6; +// #else +const ZA_MAX_HOPS = 12; +// #endif +/** + * The mask applied to generated message tags used by the framework when sending messages via EZSP. + * Customers who call ezspSend functions directly must use message tags outside this mask. + */ +const MESSAGE_TAG_MASK = 0x7F; + +/* eslint-disable max-len */ +export enum EzspEvents { + //-- App logic + ncpNeedsResetAndInit = 'ncpNeedsResetAndInit', + + //-- ezspIncomingMessageHandler + /** params => status: EmberZdoStatus, sender: EmberNodeId, apsFrame: EmberApsFrame, payload: { cluster-dependent @see zdo.ts } */ + ZDO_RESPONSE = 'ZDO_RESPONSE', + /** params => type: EmberIncomingMessageType, apsFrame: EmberApsFrame, lastHopLqi: number, sender: EmberNodeId, messageContents: Buffer */ + INCOMING_MESSAGE = 'INCOMING_MESSAGE', + /** params => sourcePanId: EmberPanId, sourceAddress: EmberEUI64, groupId: number | null, lastHopLqi: number, messageContents: Buffer */ + TOUCHLINK_MESSAGE = 'TOUCHLINK_MESSAGE', + /** params => sender: EmberNodeId, apsFrame: EmberApsFrame, payload: EndDeviceAnnouncePayload */ + END_DEVICE_ANNOUNCE = 'END_DEVICE_ANNOUNCE', + + //-- ezspStackStatusHandler + /** params => status: EmberStatus */ + STACK_STATUS = 'STACK_STATUS', + + //-- ezspTrustCenterJoinHandler + /** params => newNodeId: EmberNodeId, newNodeEui64: EmberEUI64, status: EmberDeviceUpdate, policyDecision: EmberJoinDecision, parentOfNewNodeId: EmberNodeId */ + TRUST_CENTER_JOIN = 'TRUST_CENTER_JOIN', + + //-- ezspMessageSentHandler + /** params => type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number */ + // MESSAGE_SENT_SUCCESS = 'MESSAGE_SENT_SUCCESS', + /** params => type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number */ + MESSAGE_SENT_DELIVERY_FAILED = 'MESSAGE_SENT_DELIVERY_FAILED', + + //-- ezspGpepIncomingMessageHandler + /** params => sender: number | EmberEUI64, gpdCommandId: number, gpdLink: number, sequenceNumber: number, deviceId?: number, options?: number, key?: EmberKeyData, counter?: number */ + GREENPOWER_MESSAGE = 'GREENPOWER_MESSAGE', +} +/* eslint-enable max-len */ + +/** + * Host EZSP layer. + * + * Provides functions that allow the Host application to send every EZSP command to the NCP. + * + * Commands to send to the serial>ASH layers all are named `ezsp${CommandName}`. + * They do nothing but build the command, send it and return the value(s). + * Callers are expected to handle errors appropriately. + * - They will throw `EzspStatus` if `sendCommand` fails or the returned value(s) by NCP are invalid (wrong length, etc). + * - Most will return `EmberStatus` given by NCP (some `EzspStatus`, some `SLStatus`...). + * + * @event 'ncpNeedsResetAndInit(EzspStatus)' An error was detected that requires resetting the NCP. + */ +export class Ezsp extends EventEmitter { + private readonly tickInterval: number; + public readonly ash: UartAsh; + private readonly buffalo: EzspBuffalo; + /** The contents of the current EZSP frame. CAREFUL using this guy, it's pre-allocated. */ + private readonly frameContents: Buffer; + /** The total Length of the incoming frame */ + private frameLength: number; + + private initialVersionSent: boolean; + /** True if a command is in the process of being sent. */ + private sendingCommand: boolean; + /** EZSP frame sequence number. Used in EZSP_SEQUENCE_INDEX byte. */ + private frameSequence: number; + /** Sequence used for EZSP send() tagging. static uint8_t */ + private sendSequence: number; + /** If if a command is currently waiting for a response. Used to manage async CBs vs command responses */ + private waitingForResponse: boolean; + /** Awaiting response resolve/timer struct. If waitingForResponse is not true, this should not be used. */ + private responseWaiter: EzspWaiter; + + /** Counter for Queue Full errors */ + public counterErrQueueFull: number; + + /** Handle used to tick for possible received callbacks */ + private tickHandle: NodeJS.Timeout; + + constructor(tickInterval: number, options: SerialPortOptions) { + super(); + + this.tickInterval = tickInterval || 60; + this.frameContents = Buffer.alloc(EZSP_MAX_FRAME_LENGTH); + this.buffalo = new EzspBuffalo(this.frameContents); + + this.ash = new UartAsh(options); + this.ash.on(AshEvents.hostError, this.onAshHostError.bind(this)); + this.ash.on(AshEvents.ncpError, this.onAshNCPError.bind(this)); + this.ash.on(AshEvents.frame, this.onAshFrame.bind(this)); + } + + /** + * Returns the number of EZSP responses that have been received by the serial + * protocol and are ready to be collected by the EZSP layer via + * responseReceived(). + */ + get pendingResponseCount(): number { + return this.ash.rxQueue.length; + } + + /** + * Create a string representation of the last frame in storage (sent or received). + */ + get frameToString(): string { + const id = this.buffalo.getFrameId(); + return `[FRAME: ID=${id}:"${EzspFrameID[id]}" Seq=${this.frameContents[EZSP_SEQUENCE_INDEX]} Len=${this.frameLength}]`; + } + + private initVariables(): void { + clearInterval(this.tickHandle); + + this.frameContents.fill(0); + this.frameLength = 0; + this.buffalo.setPosition(0); + this.initialVersionSent = false; + this.sendingCommand = false; + this.frameSequence = -1;// start at 0 + this.sendSequence = 0;// start at 1 + this.waitingForResponse = false; + this.responseWaiter = null; + this.counterErrQueueFull = 0; + this.tickHandle = null; + } + + public async start(): Promise { + console.log(`======== EZSP starting ========`); + + this.initVariables(); + + let status: EzspStatus; + + for (let i = 0; i < MAX_INIT_ATTEMPTS; i++) { + status = await this.ash.resetNcp(); + + if (status !== EzspStatus.SUCCESS) { + return status; + } + + status = await this.ash.start(); + + if (status === EzspStatus.SUCCESS) { + console.log(`======== EZSP started ========`); + this.registerHandlers(); + return status; + } + } + + return status; + } + + /** + * Cleanly close down the serial protocol (UART). + * After this function has been called, init() must be called to resume communication with the NCP. + */ + public async stop(): Promise { + await this.ash.stop(); + + if (this.waitingForResponse) { + clearTimeout(this.responseWaiter.timer); + } + + clearInterval(this.tickHandle); + + this.initVariables(); + console.log(`======== EZSP stopped ========`); + } + + /** + * Check if connected. + * If not, attempt to restore the connection. + * + * @returns + */ + public checkConnection(): boolean { + return this.ash.connected; + } + + private onAshHostError(status: EzspStatus): void { + this.ezspErrorHandler(status); + } + + private onAshNCPError(status: EzspStatus): void { + this.ezspErrorHandler(status); + } + + private onAshFrame(): void { + // let tick handle if not waiting for response (CBs) + if (this.waitingForResponse) { + const status = this.responseReceived(); + + if (status !== EzspStatus.NO_RX_DATA) { + // we've got a non-CB frame, must be it! + clearTimeout(this.responseWaiter.timer); + this.responseWaiter.resolve(status); + } + } + } + + /** + * Event from the EZSP layer indicating that the transaction with the NCP could not be completed due to a + * serial protocol error or that the response received from the NCP reported an error. + * The status parameter provides more information about the error. + * + * @param status + */ + public ezspErrorHandler(status: EzspStatus): void { + const lastFrameStr = `Last: ${this.frameToString}.`; + + if (status === EzspStatus.ERROR_QUEUE_FULL) { + this.counterErrQueueFull += 1; + + console.error(`NCP Queue full (counter: ${this.counterErrQueueFull}). ${lastFrameStr}`); + } else if (status === EzspStatus.ERROR_OVERFLOW) { + console.error( + `The NCP has run out of buffers, causing general malfunction. Remediate network congestion, if present. ` + + lastFrameStr + ); + } else { + console.error(`ERROR Transaction failure; status=${EzspStatus[status]}. ${lastFrameStr}`); + } + + // Do not reset if this is a decryption failure, as we ignored the packet + // Do not reset for a callback overflow or error queue, as we don't want the device to reboot under stress; + // Resetting under these conditions does not solve the problem as the problem is external to the NCP. + // Throttling the additional traffic and staggering things might make it better instead. + // For all other errors, we reset the NCP + if ((status !== EzspStatus.ERROR_SECURITY_PARAMETERS_INVALID) && (status !== EzspStatus.ERROR_OVERFLOW) + && (status !== EzspStatus.ERROR_QUEUE_FULL)) { + this.emit(EzspEvents.ncpNeedsResetAndInit, status); + } + } + + private registerHandlers(): void { + this.tickHandle = setInterval(this.tick.bind(this), this.tickInterval); + } + + /** + * The Host application must call this function periodically to allow the EZSP layer to handle asynchronous events. + */ + private tick(): void { + if (this.sendingCommand) { + // don't process any callbacks while expecting a command's response + return; + } + + // nothing in the rx queue, nothing to receive + if (this.ash.rxQueue.empty) { + return; + } + + if (this.responseReceived() === EzspStatus.SUCCESS) { + this.callbackDispatch(); + } + } + + private nextFrameSequence(): number { + return (this.frameSequence = ((++this.frameSequence) & 0xFF)); + } + + private startCommand(command: number): void { + if (this.sendingCommand) { + console.error(`[SEND COMMAND] Cannot send second one before processing response from first one.`); + throw new Error(EzspStatus[EzspStatus.ERROR_INVALID_CALL]); + } + + this.sendingCommand = true; + + // Send initial EZSP_VERSION command with old packet format for old Hosts/NCPs + if (command === EzspFrameID.VERSION && !this.initialVersionSent) { + this.buffalo.setPosition(EZSP_PARAMETERS_INDEX); + + this.buffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(command)); + } else { + // convert to extended frame format + this.buffalo.setPosition(EZSP_EXTENDED_PARAMETERS_INDEX); + + this.buffalo.setCommandByte(EZSP_EXTENDED_FRAME_ID_LB_INDEX, lowByte(command)); + this.buffalo.setCommandByte(EZSP_EXTENDED_FRAME_ID_HB_INDEX, highByte(command)); + } + } + + /** + * Sends the current EZSP command frame. Returns EZSP_SUCCESS if the command was sent successfully. + * Any other return value means that an error has been detected by the serial protocol layer. + * + * if ezsp.sendCommand fails early, this will be: + * - EzspStatus.ERROR_INVALID_CALL + * - EzspStatus.NOT_CONNECTED + * - EzspStatus.ERROR_COMMAND_TOO_LONG + * + * if ezsp.sendCommand fails, this will be whatever ash.send returns: + * - EzspStatus.SUCCESS + * - EzspStatus.NO_TX_SPACE + * - EzspStatus.DATA_FRAME_TOO_SHORT + * - EzspStatus.DATA_FRAME_TOO_LONG + * - EzspStatus.NOT_CONNECTED + * + * if ezsp.sendCommand times out, this will be EzspStatus.ASH_ACK_TIMEOUT (XXX: for now) + * + * if ezsp.sendCommand resolves, this will be whatever ezsp.responseReceived returns: + * - EzspStatus.NO_RX_DATA (should not happen if command was sent (since we subscribe to frame event to trigger function)) + * - status from EzspFrameID.INVALID_COMMAND status byte + * - EzspStatus.ERROR_UNSUPPORTED_CONTROL + * - EzspStatus.ERROR_WRONG_DIRECTION + * - EzspStatus.ERROR_TRUNCATED + * - EzspStatus.SUCCESS + */ + private async sendCommand(): Promise { + if (!this.checkConnection()) { + debug("[SEND COMMAND] NOT CONNECTED"); + return EzspStatus.NOT_CONNECTED; + } + + this.buffalo.setCommandByte(EZSP_SEQUENCE_INDEX, this.nextFrameSequence()); + // we always set the network index in the ezsp frame control. + this.buffalo.setCommandByte( + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + (EZSP_FRAME_CONTROL_COMMAND | (DEFAULT_SLEEP_MODE & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) + | ((DEFAULT_NETWORK_INDEX << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK)) + ); + + // Send initial EZSP_VERSION command with old packet format for old Hosts/NCPs + if (!this.initialVersionSent && (this.buffalo.getCommandByte(EZSP_FRAME_ID_INDEX) === EzspFrameID.VERSION)) { + this.initialVersionSent = true; + } else { + this.buffalo.setCommandByte(EZSP_EXTENDED_FRAME_CONTROL_HB_INDEX, EZSP_EXTENDED_FRAME_FORMAT_VERSION); + } + + // might have tried to write more than allocated EZSP_MAX_FRAME_LENGTH for frameContents + // use write index to detect broken frames cases (inc'ed every time a byte is supposed to have been written) + // since index is always inc'ed on setCommandByte, this should always end at 202 max + const length: number = this.buffalo.getPosition(); + + if (length > EZSP_MAX_FRAME_LENGTH) { + // this.ezspErrorHandler(EzspStatus.ERROR_COMMAND_TOO_LONG);// XXX: this forces a NCP reset?? + return EzspStatus.ERROR_COMMAND_TOO_LONG; + } + + this.frameLength = length; + + let status: EzspStatus; + + debug(`===> ${this.frameToString}`); + + try { + status = await (new Promise((resolve, reject: (reason: Error) => void): void => { + const sendStatus = (this.ash.send(this.frameLength, this.frameContents)); + + if (sendStatus !== EzspStatus.SUCCESS) { + reject(new Error(EzspStatus[sendStatus])); + } + + const error = new Error(); + Error.captureStackTrace(error); + + this.waitingForResponse = true; + this.responseWaiter = { + timer: setTimeout(() => { + this.waitingForResponse = false; + error.message = `timed out after ${this.ash.responseTimeout}ms`; + + reject(error); + }, this.ash.responseTimeout), + resolve, + }; + })); + + if (status !== EzspStatus.SUCCESS) { + throw status; + } + } catch (err) { + debug(`=x=> ${this.frameToString} Error: ${err}`); + + this.ezspErrorHandler(status); + } + + this.sendingCommand = false; + + return status; + } + + /** + * Checks whether a new EZSP response frame has been received. + * If any, the response payload is stored in frameContents/frameLength. + * Any other return value means that an error has been detected by the serial protocol layer. + * @returns NO_RX_DATA if no new response has been received. + * @returns SUCCESS if a new response has been received. + */ + public checkResponseReceived(): EzspStatus { + // trigger housekeeping in ASH layer + this.ash.sendExec(); + + let status: EzspStatus = EzspStatus.NO_RX_DATA; + let dropBuffer: EzspBuffer = null; + let buffer: EzspBuffer = this.ash.rxQueue.getPrecedingEntry(null); + + while (buffer != null) { + // While we are waiting for a response to a command, we use the asynch callback flag to ignore asynchronous callbacks. + // This allows our caller to assume that no callbacks will appear between sending a command and receiving its response. + if (this.waitingForResponse && (buffer.data[EZSP_FRAME_CONTROL_INDEX] & EZSP_FRAME_CONTROL_ASYNCH_CB)) { + debug(`Skipping async callback while waiting for response to command.`); + + if (this.ash.rxFree.length === 0) { + dropBuffer = buffer; + } + + buffer = this.ash.rxQueue.getPrecedingEntry(buffer); + } else { + this.ash.rxQueue.removeEntry(buffer); + buffer.data.copy(this.frameContents, 0, 0, buffer.len);// take only what len tells us is actual content + + this.frameLength = buffer.len; + + debug(`<=== ${this.frameToString}`);// raw=${this.frameContents.subarray(0, this.frameLength).toString('hex')}`); + + this.ash.rxFree.freeBuffer(buffer); + + buffer = null; + status = EzspStatus.SUCCESS; + this.waitingForResponse = false; + } + } + + if (dropBuffer != null) { + this.ash.rxQueue.removeEntry(dropBuffer); + this.ash.rxFree.freeBuffer(dropBuffer); + + debug(`ERROR Host receive queue full. Dropping received callback: ${dropBuffer.data.toString('hex')}`); + + this.ezspErrorHandler(EzspStatus.ERROR_QUEUE_FULL); + } + + return status; + } + + /** + * Check if a response was received and sets the stage for parsing if valid (indexes buffalo to params index). + * @returns + */ + public responseReceived(): EzspStatus { + let status: EzspStatus; + + status = this.checkResponseReceived(); + + if (status === EzspStatus.NO_RX_DATA) { + return status; + } + + let frameControl: number, frameId: number, parametersIndex: number; + // eslint-disable-next-line prefer-const + [status, frameControl, frameId, parametersIndex] = this.buffalo.getResponseMetadata(); + + if (status === EzspStatus.SUCCESS) { + if (frameId === EzspFrameID.INVALID_COMMAND) { + status = this.buffalo.getResponseByte(parametersIndex); + } + + if ((frameControl & EZSP_FRAME_CONTROL_DIRECTION_MASK) !== EZSP_FRAME_CONTROL_RESPONSE) { + status = EzspStatus.ERROR_WRONG_DIRECTION; + } + + if ((frameControl & EZSP_FRAME_CONTROL_TRUNCATED_MASK) === EZSP_FRAME_CONTROL_TRUNCATED) { + status = EzspStatus.ERROR_TRUNCATED; + } + + if ((frameControl & EZSP_FRAME_CONTROL_OVERFLOW_MASK) === EZSP_FRAME_CONTROL_OVERFLOW) { + status = EzspStatus.ERROR_OVERFLOW; + } + + if ((frameControl & EZSP_FRAME_CONTROL_PENDING_CB_MASK) === EZSP_FRAME_CONTROL_PENDING_CB) { + this.ash.ncpHasCallbacks = true; + } else { + this.ash.ncpHasCallbacks = false; + } + + // Set the callback network + //this.callbackNetworkIndex = (frameControl & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK) >> EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET; + } + + if (status !== EzspStatus.SUCCESS) { + debug(`[RESPONSE RECEIVED] ERROR ${EzspStatus[status]}`); + this.ezspErrorHandler(status); + } + + this.buffalo.setPosition(parametersIndex); + + // An overflow does not indicate a comms failure; + // The system can still communicate but buffers are running critically low. + // This is almost always due to network congestion and goes away when the network becomes quieter. + if (status === EzspStatus.ERROR_OVERFLOW) { + return EzspStatus.SUCCESS; + } + + return status; + } + + /** + * Dispatches callback frames handlers. + */ + public callbackDispatch(): void { + switch (this.buffalo.getExtFrameId()) { + case EzspFrameID.NO_CALLBACKS: { + this.ezspNoCallbacks(); + break; + } + case EzspFrameID.STACK_TOKEN_CHANGED_HANDLER: { + const tokenAddress = this.buffalo.readUInt16(); + this.ezspStackTokenChangedHandler(tokenAddress); + break; + } + case EzspFrameID.TIMER_HANDLER: { + const timerId = this.buffalo.readUInt8(); + this.ezspTimerHandler(timerId); + break; + } + case EzspFrameID.COUNTER_ROLLOVER_HANDLER: { + const type: EmberCounterType = this.buffalo.readUInt8(); + this.ezspCounterRolloverHandler(type); + break; + } + case EzspFrameID.CUSTOM_FRAME_HANDLER: { + const payloadLength = this.buffalo.readUInt8(); + const payload = this.buffalo.readListUInt8({length: payloadLength}); + this.ezspCustomFrameHandler(payloadLength, payload); + break; + } + case EzspFrameID.STACK_STATUS_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + this.ezspStackStatusHandler(status); + break; + } + case EzspFrameID.ENERGY_SCAN_RESULT_HANDLER: { + const channel = this.buffalo.readUInt8(); + const maxRssiValue = this.buffalo.readUInt8(); + this.ezspEnergyScanResultHandler(channel, maxRssiValue); + break; + } + case EzspFrameID.NETWORK_FOUND_HANDLER: { + const networkFound: EmberZigbeeNetwork = this.buffalo.readEmberZigbeeNetwork(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + this.ezspNetworkFoundHandler(networkFound, lastHopLqi, lastHopRssi); + break; + } + case EzspFrameID.SCAN_COMPLETE_HANDLER: { + const channel = this.buffalo.readUInt8(); + const status: EmberStatus = this.buffalo.readUInt8(); + this.ezspScanCompleteHandler(channel, status); + break; + } + case EzspFrameID.UNUSED_PAN_ID_FOUND_HANDLER: { + const panId: EmberPanId = this.buffalo.readUInt16(); + const channel = this.buffalo.readUInt8(); + this.ezspUnusedPanIdFoundHandler(panId, channel); + break; + } + case EzspFrameID.CHILD_JOIN_HANDLER: { + const index = this.buffalo.readUInt8(); + const joining: boolean = this.buffalo.readUInt8() === 1 ? true : false; + const childId: EmberNodeId = this.buffalo.readUInt16(); + const childEui64: EmberEUI64 = this.buffalo.readIeeeAddr(); + const childType: EmberNodeType = this.buffalo.readUInt8(); + this.ezspChildJoinHandler(index, joining, childId, childEui64, childType); + break; + } + case EzspFrameID.DUTY_CYCLE_HANDLER: { + const channelPage = this.buffalo.readUInt8(); + const channel = this.buffalo.readUInt8(); + const state: EmberDutyCycleState = this.buffalo.readUInt8(); + const totalDevices = this.buffalo.readUInt8(); + const arrayOfDeviceDutyCycles: EmberPerDeviceDutyCycle[] = this.buffalo.readEmberPerDeviceDutyCycle(); + this.ezspDutyCycleHandler(channelPage, channel, state, totalDevices, arrayOfDeviceDutyCycles); + break; + } + case EzspFrameID.REMOTE_SET_BINDING_HANDLER: { + const entry : EmberBindingTableEntry = this.buffalo.readEmberBindingTableEntry(); + const index = this.buffalo.readUInt8(); + const policyDecision: EmberStatus = this.buffalo.readUInt8(); + this.ezspRemoteSetBindingHandler(entry, index, policyDecision); + break; + } + case EzspFrameID.REMOTE_DELETE_BINDING_HANDLER: { + const index = this.buffalo.readUInt8(); + const policyDecision: EmberStatus = this.buffalo.readUInt8(); + this.ezspRemoteDeleteBindingHandler(index, policyDecision); + break; + } + case EzspFrameID.MESSAGE_SENT_HANDLER: { + const type: EmberOutgoingMessageType = this.buffalo.readUInt8(); + const indexOrDestination = this.buffalo.readUInt16(); + const apsFrame: EmberApsFrame = this.buffalo.readEmberApsFrame(); + const messageTag = this.buffalo.readUInt8(); + const status: EmberStatus = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspMessageSentHandler(type, indexOrDestination, apsFrame, messageTag, status, messageContents); + break; + } + case EzspFrameID.POLL_COMPLETE_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + this.ezspPollCompleteHandler(status); + break; + } + case EzspFrameID.POLL_HANDLER: { + const childId: EmberNodeId = this.buffalo.readUInt16(); + const transmitExpected: boolean = this.buffalo.readUInt8() === 1 ? true : false; + this.ezspPollHandler(childId, transmitExpected); + break; + } + case EzspFrameID.INCOMING_SENDER_EUI64_HANDLER: { + const senderEui64: EmberEUI64 = this.buffalo.readIeeeAddr(); + this.ezspIncomingSenderEui64Handler(senderEui64); + break; + } + case EzspFrameID.INCOMING_MESSAGE_HANDLER: { + const type: EmberIncomingMessageType = this.buffalo.readUInt8(); + const apsFrame: EmberApsFrame = this.buffalo.readEmberApsFrame(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + const sender: EmberNodeId = this.buffalo.readUInt16(); + const bindingIndex = this.buffalo.readUInt8(); + const addressIndex = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspIncomingMessageHandler( + type, + apsFrame, + lastHopLqi, + lastHopRssi, + sender, + bindingIndex, + addressIndex, + messageContents + ); + break; + } + case EzspFrameID.INCOMING_MANY_TO_ONE_ROUTE_REQUEST_HANDLER: { + const source: EmberNodeId = this.buffalo.readUInt16(); + const longId: EmberEUI64 = this.buffalo.readIeeeAddr(); + const cost = this.buffalo.readUInt8(); + this.ezspIncomingManyToOneRouteRequestHandler(source, longId, cost); + break; + } + case EzspFrameID.INCOMING_ROUTE_ERROR_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const target: EmberNodeId = this.buffalo.readUInt16(); + this.ezspIncomingRouteErrorHandler(status, target); + break; + } + case EzspFrameID.INCOMING_NETWORK_STATUS_HANDLER: { + const errorCode = this.buffalo.readUInt8(); + const target: EmberNodeId = this.buffalo.readUInt16(); + this.ezspIncomingNetworkStatusHandler(errorCode, target); + break; + } + case EzspFrameID.INCOMING_ROUTE_RECORD_HANDLER: { + const source: EmberNodeId = this.buffalo.readUInt16(); + const sourceEui: EmberEUI64 = this.buffalo.readIeeeAddr(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + const relayCount = this.buffalo.readUInt8(); + const relayList = this.buffalo.readListUInt16({length: relayCount});//this.buffalo.readListUInt8({length: (relayCount * 2)}); + this.ezspIncomingRouteRecordHandler(source, sourceEui, lastHopLqi, lastHopRssi, relayCount, relayList); + break; + } + case EzspFrameID.ID_CONFLICT_HANDLER: { + const id: EmberNodeId = this.buffalo.readUInt16(); + this.ezspIdConflictHandler(id); + break; + } + case EzspFrameID.MAC_PASSTHROUGH_MESSAGE_HANDLER: { + const messageType: EmberMacPassthroughType = this.buffalo.readUInt8(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspMacPassthroughMessageHandler(messageType, lastHopLqi, lastHopRssi, messageContents); + break; + } + case EzspFrameID.MAC_FILTER_MATCH_MESSAGE_HANDLER: { + const filterIndexMatch = this.buffalo.readUInt8(); + const legacyPassthroughType: EmberMacPassthroughType = this.buffalo.readUInt8(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspMacFilterMatchMessageHandler(filterIndexMatch, legacyPassthroughType, lastHopLqi, lastHopRssi, messageContents); + break; + } + case EzspFrameID.RAW_TRANSMIT_COMPLETE_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + this.ezspRawTransmitCompleteHandler(status); + break; + } + case EzspFrameID.SWITCH_NETWORK_KEY_HANDLER: { + const sequenceNumber = this.buffalo.readUInt8(); + this.ezspSwitchNetworkKeyHandler(sequenceNumber); + break; + } + case EzspFrameID.ZIGBEE_KEY_ESTABLISHMENT_HANDLER: { + const partner: EmberEUI64 = this.buffalo.readIeeeAddr(); + const status: EmberKeyStatus = this.buffalo.readUInt8(); + this.ezspZigbeeKeyEstablishmentHandler(partner, status); + break; + } + case EzspFrameID.TRUST_CENTER_JOIN_HANDLER: { + const newNodeId: EmberNodeId = this.buffalo.readUInt16(); + const newNodeEui64: EmberEUI64 = this.buffalo.readIeeeAddr(); + const status: EmberDeviceUpdate = this.buffalo.readUInt8(); + const policyDecision: EmberJoinDecision = this.buffalo.readUInt8(); + const parentOfNewNodeId: EmberNodeId = this.buffalo.readUInt16(); + this.ezspTrustCenterJoinHandler(newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId); + break; + } + case EzspFrameID.GENERATE_CBKE_KEYS_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const ephemeralPublicKey: EmberPublicKeyData = this.buffalo.readEmberPublicKeyData(); + this.ezspGenerateCbkeKeysHandler(status, ephemeralPublicKey); + break; + } + case EzspFrameID.CALCULATE_SMACS_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const initiatorSmac: EmberSmacData = this.buffalo.readEmberSmacData(); + const responderSmac: EmberSmacData = this.buffalo.readEmberSmacData(); + this.ezspCalculateSmacsHandler(status, initiatorSmac, responderSmac); + break; + } + case EzspFrameID.GENERATE_CBKE_KEYS_HANDLER283K1: { + const status: EmberStatus = this.buffalo.readUInt8(); + const ephemeralPublicKey: EmberPublicKey283k1Data = this.buffalo.readEmberPublicKey283k1Data(); + this.ezspGenerateCbkeKeysHandler283k1(status, ephemeralPublicKey); + break; + } + case EzspFrameID.CALCULATE_SMACS_HANDLER283K1: { + const status: EmberStatus = this.buffalo.readUInt8(); + const initiatorSmac: EmberSmacData = this.buffalo.readEmberSmacData(); + const responderSmac: EmberSmacData = this.buffalo.readEmberSmacData(); + this.ezspCalculateSmacsHandler283k1(status, initiatorSmac, responderSmac); + break; + } + case EzspFrameID.DSA_SIGN_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspDsaSignHandler(status, messageContents); + break; + } + case EzspFrameID.DSA_VERIFY_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + this.ezspDsaVerifyHandler(status); + break; + } + case EzspFrameID.MFGLIB_RX_HANDLER: { + const linkQuality = this.buffalo.readUInt8(); + const rssi = this.buffalo.readUInt8(); + const packetLength = this.buffalo.readUInt8(); + const packetContents = this.buffalo.readListUInt8({length: packetLength}); + this.ezspMfglibRxHandler(linkQuality, rssi, packetLength, packetContents); + break; + } + case EzspFrameID.INCOMING_BOOTLOAD_MESSAGE_HANDLER: { + const longId: EmberEUI64 = this.buffalo.readIeeeAddr(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspIncomingBootloadMessageHandler(longId, lastHopLqi, lastHopRssi, messageContents); + break; + } + case EzspFrameID.BOOTLOAD_TRANSMIT_COMPLETE_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const messageContents = this.buffalo.readPayload(); + this.ezspBootloadTransmitCompleteHandler(status, messageContents); + break; + } + case EzspFrameID.ZLL_NETWORK_FOUND_HANDLER: { + const networkInfo: EmberZllNetwork = this.buffalo.readEmberZllNetwork(); + const isDeviceInfoNull: boolean = this.buffalo.readUInt8() === 1 ? true : false; + const deviceInfo: EmberZllDeviceInfoRecord = this.buffalo.readEmberZllDeviceInfoRecord(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + this.ezspZllNetworkFoundHandler(networkInfo, isDeviceInfoNull, deviceInfo, lastHopLqi, lastHopRssi); + break; + } + case EzspFrameID.ZLL_SCAN_COMPLETE_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + this.ezspZllScanCompleteHandler(status); + break; + } + case EzspFrameID.ZLL_ADDRESS_ASSIGNMENT_HANDLER: { + const addressInfo: EmberZllAddressAssignment = this.buffalo.readEmberZllAddressAssignment(); + const lastHopLqi = this.buffalo.readUInt8(); + const lastHopRssi = this.buffalo.readUInt8(); + this.ezspZllAddressAssignmentHandler(addressInfo, lastHopLqi, lastHopRssi); + break; + } + case EzspFrameID.ZLL_TOUCH_LINK_TARGET_HANDLER: { + const networkInfo: EmberZllNetwork = this.buffalo.readEmberZllNetwork(); + this.ezspZllTouchLinkTargetHandler(networkInfo); + break; + } + case EzspFrameID.D_GP_SENT_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const gpepHandle = this.buffalo.readUInt8(); + this.ezspDGpSentHandler(status, gpepHandle); + break; + } + case EzspFrameID.GPEP_INCOMING_MESSAGE_HANDLER: { + const status: EmberStatus = this.buffalo.readUInt8(); + const gpdLink = this.buffalo.readUInt8(); + const sequenceNumber = this.buffalo.readUInt8(); + const addr: EmberGpAddress = this.buffalo.readEmberGpAddress(); + const gpdfSecurityLevel: EmberGpSecurityLevel = this.buffalo.readUInt8(); + const gpdfSecurityKeyType: EmberGpKeyType = this.buffalo.readUInt8(); + const autoCommissioning: boolean = this.buffalo.readUInt8() === 1 ? true : false; + const bidirectionalInfo = this.buffalo.readUInt8(); + const gpdSecurityFrameCounter = this.buffalo.readUInt32(); + const gpdCommandId = this.buffalo.readUInt8(); + const mic = this.buffalo.readUInt32(); + const proxyTableIndex = this.buffalo.readUInt8(); + const gpdCommandPayload = this.buffalo.readPayload(); + this.ezspGpepIncomingMessageHandler( + status, + gpdLink, + sequenceNumber, + addr, + gpdfSecurityLevel, + gpdfSecurityKeyType, + autoCommissioning, + bidirectionalInfo, + gpdSecurityFrameCounter, + gpdCommandId, + mic, + proxyTableIndex, + gpdCommandPayload + ); + break; + } + default: + this.ezspErrorHandler(EzspStatus.ERROR_INVALID_FRAME_ID); + } + } + + /** + * + * @returns uint8_t + */ + private nextSendSequence(): number { + return (this.sendSequence = ((++this.sendSequence) & MESSAGE_TAG_MASK)); + } + + /** + * Calls ezspSend${x} based on type and takes care of tagging message. + * + * Alias types expect `alias` & `sequence` params, along with `apsFrame.radius`. + * + * @param type Specifies the outgoing message type. + * @param indexOrDestination uint16_t Depending on the type of addressing used, this is either the EmberNodeId of the destination, + * an index into the address table, or an index into the binding table. + * Unused for multicast types. + * This must be one of the three ZigBee broadcast addresses for broadcast. + * @param apsFrame [IN/OUT] EmberApsFrame * The APS frame which is to be added to the message. + * @param message uint8_t * Content of the message. + * @param alias The alias source address + * @param sequence uint8_t The alias sequence number + * @returns Result of the ezspSend${x} call or EmberStatus.BAD_ARGUMENT if type not supported. + * @returns apsSequence as returned by ezspSend${x} command + * @returns messageTag Tag used for ezspSend${x} command + */ + public async send(type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, message: Buffer, + alias: EmberNodeId, sequence: number): Promise<[EmberStatus, messageTag: number]> { + let status: EmberStatus = EmberStatus.BAD_ARGUMENT; + let apsSequence: number; + const messageTag = this.nextSendSequence(); + + switch (type) { + case EmberOutgoingMessageType.VIA_BINDING: + case EmberOutgoingMessageType.VIA_ADDRESS_TABLE: + case EmberOutgoingMessageType.DIRECT: { + [status, apsSequence] = (await this.ezspSendUnicast(type, indexOrDestination, apsFrame, messageTag, message)); + break; + } + case EmberOutgoingMessageType.MULTICAST: { + [status, apsSequence] = (await this.ezspSendMulticast( + apsFrame, + ZA_MAX_HOPS/* hops */, + ZA_MAX_HOPS/* nonmember radius */, + messageTag, + message + )); + break; + } + case EmberOutgoingMessageType.MULTICAST_WITH_ALIAS: { + [status, apsSequence] = (await this.ezspSendMulticastWithAlias( + apsFrame, + apsFrame.radius/*radius*/, + apsFrame.radius/*nonmember radius*/, + alias, + sequence, + messageTag, + message + )); + break; + } + case EmberOutgoingMessageType.BROADCAST: { + [status, apsSequence] = (await this.ezspSendBroadcast( + indexOrDestination, + apsFrame, + ZA_MAX_HOPS/*radius*/, + messageTag, + message + )); + break; + } + case EmberOutgoingMessageType.BROADCAST_WITH_ALIAS: { + [status, apsSequence] = (await this.ezspProxyBroadcast( + alias, + indexOrDestination, + sequence, + apsFrame, + apsFrame.radius, + messageTag, + message + )); + break; + } + default: + break; + } + + apsFrame.sequence = apsSequence; + + // NOTE: match `~~~>` from adapter since this is just a wrapper for it + debug(`~~~> [SENT type=${EmberOutgoingMessageType[type]} apsSequence=${apsSequence} messageTag=${messageTag} status=${EmberStatus[status]}]`); + return [status, messageTag]; + } + + /** + * Retrieving the new version info. + * Wrapper for `ezspGetValue`. + * @returns Send status + * @returns EmberVersion*, null if status not SUCCESS. + */ + public async ezspGetVersionStruct(): Promise<[EzspStatus, version: EmberVersion]> { + const [status, outValueLength, outValue] = (await this.ezspGetValue(EzspValueId.VERSION_INFO, 7));// sizeof(EmberVersion) + + if (outValueLength !== 7) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return [status, { + build : outValue[0] + ((outValue[1]) << 8), + major : outValue[2], + minor : outValue[3], + patch : outValue[4], + special: outValue[5], + type : outValue[6], + }]; + } + + /** + * Function for manipulating the endpoints flags on the NCP. + * Wrapper for `ezspGetExtendedValue` + * @param endpoint uint8_t + * @param flags EzspEndpointFlags + * @returns EzspStatus + */ + public async ezspSetEndpointFlags(endpoint: number, flags: EzspEndpointFlag): Promise { + return this.ezspSetValue(EzspValueId.ENDPOINT_FLAGS, 3, [endpoint, lowByte(flags), highByte(flags)]); + } + + /** + * Function for manipulating the endpoints flags on the NCP. + * Wrapper for `ezspGetExtendedValue`. + * @param endpoint uint8_t + * @returns EzspStatus + * @returns flags + */ + public async ezspGetEndpointFlags(endpoint: number): Promise<[EzspStatus, flags: EzspEndpointFlag]> { + const [status, outValLen, outVal] = (await this.ezspGetExtendedValue(EzspExtendedValueId.ENDPOINT_FLAGS, endpoint, 2)); + + if (outValLen < 2) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + const returnFlags = highLowToInt(outVal[1], outVal[0]); + + return [status, returnFlags]; + } + + /** + * Wrapper for `ezspGetExtendedValue`. + * @param EmberNodeId + * @param destination + * @returns EzspStatus + * @returns overhead uint8_t + */ + public async ezspGetSourceRouteOverhead(destination: EmberNodeId): Promise<[EzspStatus, overhead: number]> { + const [status, outValLen, outVal] = (await this.ezspGetExtendedValue(EzspExtendedValueId.GET_SOURCE_ROUTE_OVERHEAD, destination, 1)); + + if (outValLen < 1) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return [status, outVal[0]]; + } + + /** + * Wrapper for `ezspGetExtendedValue`. + * @returns EzspStatus + * @returns reason + * @returns nodeId EmberNodeId* + */ + public async ezspGetLastLeaveReason(): Promise<[EzspStatus, reason: EmberLeaveReason, nodeId: EmberNodeId]> { + const [status, outValLen, outVal] = (await this.ezspGetExtendedValue(EzspExtendedValueId.LAST_LEAVE_REASON, 0, 3)); + + if (outValLen < 3) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return [status, outVal[0], highLowToInt(outVal[2], outVal[1])]; + } + + /** + * Wrapper for `ezspGetValue`. + * @returns EzspStatus + * @returns reason + */ + public async ezspGetLastRejoinReason(): Promise<[EzspStatus, reason: EmberRejoinReason]> { + const [status, outValLen, outVal] = (await this.ezspGetValue(EzspValueId.LAST_REJOIN_REASON, 1)); + + if (outValLen < 1) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return [status, outVal[0]]; + } + + /** + * Wrapper for `ezspSetValue`. + * @param mask + * @returns + */ + public async ezspSetExtendedSecurityBitmask(mask: EmberExtendedSecurityBitmask): Promise { + return this.ezspSetValue(EzspValueId.EXTENDED_SECURITY_BITMASK, 2, [lowByte(mask), highByte(mask)]); + } + + /** + * Wrapper for `ezspGetValue`. + * @returns + */ + public async ezspGetExtendedSecurityBitmask(): Promise<[EzspStatus, mask: EmberExtendedSecurityBitmask]> { + const [status, outValLen, outVal] = (await this.ezspGetValue(EzspValueId.EXTENDED_SECURITY_BITMASK, 2)); + + if (outValLen < 2) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return [status, highLowToInt(outVal[1], outVal[0])]; + } + + /** + * Wrapper for `ezspSetValue`. + * @returns + */ + public async ezspStartWritingStackTokens(): Promise { + return this.ezspSetValue(EzspValueId.STACK_TOKEN_WRITING, 1, [1]); + } + + /** + * Wrapper for `ezspSetValue`. + * @returns + */ + public async ezspStopWritingStackTokens(): Promise { + return this.ezspSetValue(EzspValueId.STACK_TOKEN_WRITING, 1, [0]); + } + + //-----------------------------------------------------------------------------// + //---------------------------- START EZSP COMMANDS ----------------------------// + //-----------------------------------------------------------------------------// + + //----------------------------------------------------------------------------- + // Configuration Frames + //----------------------------------------------------------------------------- + + /** + * The command allows the Host to specify the desired EZSP version and must be + * sent before any other command. The response provides information about the + * firmware running on the NCP. + * + * @param desiredProtocolVersion uint8_t The EZSP version the Host wishes to use. + * To successfully set the version and allow other commands, this must be same as EZSP_PROTOCOL_VERSION. + * @return + * - uint8_t The EZSP version the NCP is using. + * - uint8_t * The type of stack running on the NCP (2). + * - uint16_t * The version number of the stack. + */ + async ezspVersion(desiredProtocolVersion: number): Promise<[protocolVersion: number, stackType: number, stackVersion: number]> { + this.startCommand(EzspFrameID.VERSION); + this.buffalo.writeUInt8(desiredProtocolVersion); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const protocolVersion: number = this.buffalo.readUInt8(); + const stackType: number = this.buffalo.readUInt8(); + const stackVersion: number = this.buffalo.readUInt16(); + + return [protocolVersion, stackType, stackVersion]; + } + + /** + * Reads a configuration value from the NCP. + * + * @param configId Identifies which configuration value to read. + * @returns + * - EzspStatus.SUCCESS if the value was read successfully, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize configId. + * - uint16_t * The configuration value. + */ + async ezspGetConfigurationValue(configId: EzspConfigId): Promise<[EzspStatus, value: number]> { + this.startCommand(EzspFrameID.GET_CONFIGURATION_VALUE); + this.buffalo.writeUInt8(configId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EzspStatus = this.buffalo.readUInt8(); + const value: number = this.buffalo.readUInt16(); + + return [status, value]; + } + + /** + * Writes a configuration value to the NCP. Configuration values can be modified + * by the Host after the NCP has reset. Once the status of the stack changes to + * EMBER_NETWORK_UP, configuration values can no longer be modified and this + * command will respond with EzspStatus.ERROR_INVALID_CALL. + * + * @param configId Identifies which configuration value to change. + * @param value uint16_t The new configuration value. + * @returns EzspStatus + * - EzspStatus.SUCCESS if the configuration value was changed, + * - EzspStatus.ERROR_OUT_OF_MEMORY if the new value exceeded the available memory, + * - EzspStatus.ERROR_INVALID_VALUE if the new value was out of bounds, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize configId, + * - EzspStatus.ERROR_INVALID_CALL if configuration values can no longer be modified. + */ + async ezspSetConfigurationValue(configId: EzspConfigId, value: number): Promise { + this.startCommand(EzspFrameID.SET_CONFIGURATION_VALUE); + this.buffalo.writeUInt8(configId); + this.buffalo.writeUInt16(value); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EzspStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Read attribute data on NCP endpoints. + * @param endpoint uint8_t Endpoint + * @param cluster uint16_t Cluster. + * @param attributeId uint16_t Attribute ID. + * @param mask uint8_t Mask. + * @param manufacturerCode uint16_t Manufacturer code. + * @returns + * - An EmberStatus value indicating success or the reason for failure. + * - uint8_t * Attribute data type. + * - uint8_t * Length of attribute data. + * - uint8_t * Attribute data. + */ + async ezspReadAttribute(endpoint: number, cluster: number, attributeId: number, mask: number, manufacturerCode: number, readLength: number): + Promise<[EmberStatus, dataType: number, outReadLength: number, data: number[]]> { + this.startCommand(EzspFrameID.READ_ATTRIBUTE); + this.buffalo.writeUInt8(endpoint); + this.buffalo.writeUInt16(cluster); + this.buffalo.writeUInt16(attributeId); + this.buffalo.writeUInt8(mask); + this.buffalo.writeUInt16(manufacturerCode); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const maxReadLength = readLength; + const status: EmberStatus = this.buffalo.readUInt8(); + const dataType = this.buffalo.readUInt8(); + readLength = this.buffalo.readUInt8(); + + if (readLength > maxReadLength) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + const data = this.buffalo.readListUInt8({length: readLength}); + + return [status, dataType, readLength, data]; + } + + /** + * Write attribute data on NCP endpoints. + * @param endpoint uint8_t Endpoint + * @param cluster uint16_t Cluster. + * @param attributeId uint16_t Attribute ID. + * @param mask uint8_t Mask. + * @param manufacturerCode uint16_t Manufacturer code. + * @param overrideReadOnlyAndDataType Override read only and data type. + * @param justTest Override read only and data type. + * @param dataType uint8_t Attribute data type. + * @param data uint8_t * Attribute data. + * @returns EmberStatus An EmberStatus value indicating success or the reason for failure. + */ + async ezspWriteAttribute(endpoint: number, cluster: number, attributeId: number, mask: number, manufacturerCode: number, + overrideReadOnlyAndDataType: boolean, justTest: boolean, dataType: number, data: Buffer): Promise { + this.startCommand(EzspFrameID.WRITE_ATTRIBUTE); + this.buffalo.writeUInt8(endpoint); + this.buffalo.writeUInt16(cluster); + this.buffalo.writeUInt16(attributeId); + this.buffalo.writeUInt8(mask); + this.buffalo.writeUInt16(manufacturerCode); + this.buffalo.writeUInt8(overrideReadOnlyAndDataType ? 1 : 0); + this.buffalo.writeUInt8(justTest ? 1 : 0); + this.buffalo.writeUInt8(dataType); + this.buffalo.writePayload(data); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Configures endpoint information on the NCP. The NCP does not remember these + * settings after a reset. Endpoints can be added by the Host after the NCP has + * reset. Once the status of the stack changes to EMBER_NETWORK_UP, endpoints + * can no longer be added and this command will respond with EzspStatus.ERROR_INVALID_CALL. + * @param endpoint uint8_t The application endpoint to be added. + * @param profileId uint16_t The endpoint's application profile. + * @param deviceId uint16_t The endpoint's device ID within the application profile. + * @param deviceVersion uint8_t The endpoint's device version. + * @param inputClusterList uint16_t * Input cluster IDs the endpoint will accept. + * @param outputClusterList uint16_t * Output cluster IDs the endpoint may send. + * @returns EzspStatus + * - EzspStatus.SUCCESS if the endpoint was added, + * - EzspStatus.ERROR_OUT_OF_MEMORY if there is not enough memory available to add the endpoint, + * - EzspStatus.ERROR_INVALID_VALUE if the endpoint already exists, + * - EzspStatus.ERROR_INVALID_CALL if endpoints can no longer be added. + */ + async ezspAddEndpoint(endpoint: number, profileId: number, deviceId: number, deviceVersion: number, + inputClusterList: number[], outputClusterList: number[]): Promise { + this.startCommand(EzspFrameID.ADD_ENDPOINT); + this.buffalo.writeUInt8(endpoint); + this.buffalo.writeUInt16(profileId); + this.buffalo.writeUInt16(deviceId); + this.buffalo.writeUInt8(deviceVersion); + this.buffalo.writeUInt8(inputClusterList.length); + this.buffalo.writeUInt8(outputClusterList.length); + this.buffalo.writeListUInt16(inputClusterList); + this.buffalo.writeListUInt16(outputClusterList); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EzspStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Allows the Host to change the policies used by the NCP to make fast + * decisions. + * @param policyId Identifies which policy to modify. + * @param decisionId The new decision for the specified policy. + * @returns + * - EzspStatus.SUCCESS if the policy was changed, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize policyId. + */ + async ezspSetPolicy(policyId: EzspPolicyId, decisionId: number): Promise { + this.startCommand(EzspFrameID.SET_POLICY); + this.buffalo.writeUInt8(policyId); + this.buffalo.writeUInt8(decisionId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EzspStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Allows the Host to read the policies used by the NCP to make fast decisions. + * @param policyId Identifies which policy to read. + * @returns + * - EzspStatus.SUCCESS if the policy was read successfully, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize policyId. + * - EzspDecisionId * The current decision for the specified policy. + */ + async ezspGetPolicy(policyId: EzspPolicyId): Promise<[EzspStatus, number]> { + this.startCommand(EzspFrameID.GET_POLICY); + this.buffalo.writeUInt8(policyId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EzspStatus = this.buffalo.readUInt8(); + const decisionId: number = this.buffalo.readUInt8(); + + return [status, decisionId]; + } + + /** + * Triggers a pan id update message. + * @param The new Pan Id + * @returns true if the request was successfully handed to the stack, false otherwise + */ + async ezspSendPanIdUpdate(newPan: EmberPanId): Promise { + this.startCommand(EzspFrameID.SEND_PAN_ID_UPDATE); + this.buffalo.writeUInt16(newPan); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: boolean = this.buffalo.readUInt8() === 1 ? true : false; + + return status; + } + + /** + * Reads a value from the NCP. + * @param valueId Identifies which value to read. + * @returns + * - EzspStatus.SUCCESS if the value was read successfully, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize valueId, + * - EzspStatus.ERROR_INVALID_VALUE if the length of the returned value exceeds the size of local storage allocated to receive it. + * - uint8_t * Both a command and response parameter. + * On command, the maximum in bytes of local storage allocated to receive the returned value. + * On response, the actual length in bytes of the returned value. + * - uint8_t * The value. + */ + async ezspGetValue(valueId: EzspValueId, valueLength: number): + Promise<[EzspStatus, outValueLength: number, outValue: number[]]> { + this.startCommand(EzspFrameID.GET_VALUE); + this.buffalo.writeUInt8(valueId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + // let value: number[] = null; + + const maxValueLength = valueLength; + const status: EzspStatus = this.buffalo.readUInt8(); + valueLength = this.buffalo.readUInt8(); + + if (valueLength > maxValueLength) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + const value = this.buffalo.readListUInt8({length: valueLength}); + + return [status, valueLength, value]; + } + + /** + * Reads a value from the NCP but passes an extra argument specific to the value + * being retrieved. + * @param valueId Identifies which extended value ID to read. + * @param characteristics uint32_t Identifies which characteristics of the extended value ID to read. These are specific to the value being read. + * @returns + * - EzspStatus.SUCCESS if the value was read successfully, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize valueId, + * - EzspStatus.ERROR_INVALID_VALUE if the length of the returned value exceeds the size of local storage allocated to receive it. + * - uint8_t * Both a command and response parameter. + * On command, the maximum in bytes of local storage allocated to receive the returned value. + * On response, the actual length in bytes of the returned value. + * - uint8_t * The value. + */ + async ezspGetExtendedValue(valueId: EzspExtendedValueId, characteristics: number, valueLength: number): + Promise<[EzspStatus, outValueLength: number, outValue: number[]]> { + this.startCommand(EzspFrameID.GET_EXTENDED_VALUE); + this.buffalo.writeUInt8(valueId); + this.buffalo.writeUInt32(characteristics); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + // let value: number[] = null; + + const maxValueLength = valueLength; + const status: EzspStatus = this.buffalo.readUInt8(); + valueLength = this.buffalo.readUInt8(); + + if (valueLength > maxValueLength) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + const value = this.buffalo.readListUInt8({length: valueLength}); + + return [status, valueLength, value]; + } + + /** + * Writes a value to the NCP. + * @param valueId Identifies which value to change. + * @param valueLength uint8_t The length of the value parameter in bytes. + * @param value uint8_t * The new value. + * @returns EzspStatus + * - EzspStatus.SUCCESS if the value was changed, + * - EzspStatus.ERROR_INVALID_VALUE if the new value was out of bounds, + * - EzspStatus.ERROR_INVALID_ID if the NCP does not recognize valueId, + * - EzspStatus.ERROR_INVALID_CALL if the value could not be modified. + */ + async ezspSetValue(valueId: EzspValueId, valueLength: number, value: number[]): Promise { + this.startCommand(EzspFrameID.SET_VALUE); + this.buffalo.writeUInt8(valueId); + this.buffalo.writeUInt8(valueLength); + this.buffalo.writeListUInt8(value); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EzspStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Allows the Host to control the broadcast behaviour of a routing device used + * by the NCP. + * @param config uint8_t Passive ack config enum. + * @param minAcksNeeded uint8_t The minimum number of acknowledgments (re-broadcasts) to wait for until + * deeming the broadcast transmission complete. + * @returns EmberStatus An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetPassiveAckConfig(config: number, minAcksNeeded: number): Promise { + this.startCommand(EzspFrameID.SET_PASSIVE_ACK_CONFIG); + this.buffalo.writeUInt8(config); + this.buffalo.writeUInt8(minAcksNeeded); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + //----------------------------------------------------------------------------- + // Utilities Frames + //----------------------------------------------------------------------------- + /** + * A command which does nothing. The Host can use this to set the sleep mode or to check the status of the NCP. + */ + async ezspNop(): Promise { + this.startCommand(EzspFrameID.NOP); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Variable length data from the Host is echoed back by the NCP. This command + * has no other effects and is designed for testing the link between the Host and NCP. + * @param data uint8_t * The data to be echoed back. + * @returns + * - The length of the echo parameter in bytes. + * - echo uint8_t * The echo of the data. + */ + async ezspEcho(data: Buffer): Promise { + this.startCommand(EzspFrameID.ECHO); + this.buffalo.writePayload(data); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const echo = this.buffalo.readPayload(); + + if (echo.length > data.length) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return echo; + } + + /** + * Allows the NCP to respond with a pending callback. + */ + async ezspCallback(): Promise { + this.startCommand(EzspFrameID.CALLBACK); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + this.callbackDispatch(); + } + + /** + * Callback + * Indicates that there are currently no pending callbacks. + */ + ezspNoCallbacks(): void { + debug(`ezspNoCallbacks(): callback called`); + } + + /** + * Sets a token (8 bytes of non-volatile storage) in the Simulated EEPROM of the NCP. + * @param tokenId uint8_t Which token to set + * @param tokenData uint8_t * The data to write to the token. + * @returns EmberStatus An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetToken(tokenId: number, tokenData: number[]): Promise { + this.startCommand(EzspFrameID.SET_TOKEN); + this.buffalo.writeUInt8(tokenId); + this.buffalo.writeListUInt8(tokenData); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Retrieves a token (8 bytes of non-volatile storage) from the Simulated EEPROM of the NCP. + * @param tokenId uint8_t Which token to read + * @returns + * - An EmberStatus value indicating success or the reason for failure. + * - uint8_t * The contents of the token. + */ + async ezspGetToken(tokenId: number): Promise<[EmberStatus, tokenData: number[]]> { + this.startCommand(EzspFrameID.GET_TOKEN); + this.buffalo.writeUInt8(tokenId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const tokenData = this.buffalo.readListUInt8({length: 8}); + + return [status, tokenData]; + } + + /** + * Retrieves a manufacturing token from the Flash Information Area of the NCP + * (except for EZSP_STACK_CAL_DATA which is managed by the stack). + * @param Which manufacturing token to read. + * @returns + * - uint8_t The length of the tokenData parameter in bytes. + * - uint8_t * The manufacturing token data. + */ + async ezspGetMfgToken(tokenId: EzspMfgTokenId): Promise<[number, tokenData: number[]]> { + this.startCommand(EzspFrameID.GET_MFG_TOKEN); + this.buffalo.writeUInt8(tokenId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const tokenDataLength = this.buffalo.readUInt8(); + let expectedTokenDataLength: number = 0; + + // the size of corresponding the EZSP Mfg token, please refer to app/util/ezsp/ezsp-enum.h + switch (tokenId) { + // 2 bytes + case EzspMfgTokenId.CUSTOM_VERSION: + case EzspMfgTokenId.MANUF_ID: + case EzspMfgTokenId.PHY_CONFIG: + case EzspMfgTokenId.CTUNE: + expectedTokenDataLength = 2; + break; + // 8 bytes + case EzspMfgTokenId.EZSP_STORAGE: + case EzspMfgTokenId.CUSTOM_EUI_64: + expectedTokenDataLength = 8; + break; + // 16 bytes + case EzspMfgTokenId.STRING: + case EzspMfgTokenId.BOARD_NAME: + case EzspMfgTokenId.BOOTLOAD_AES_KEY: + expectedTokenDataLength = 16; + break; + // 20 bytes + case EzspMfgTokenId.INSTALLATION_CODE: + expectedTokenDataLength = 20; + break; + // 40 bytes + case EzspMfgTokenId.ASH_CONFIG: + expectedTokenDataLength = 40; + break; + // 92 bytes + case EzspMfgTokenId.CBKE_DATA: + expectedTokenDataLength = 92; + break; + default: + break; + } + + if (tokenDataLength != expectedTokenDataLength) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + const tokenData = this.buffalo.readListUInt8({length: tokenDataLength}); + + return [tokenDataLength, tokenData]; + } + + /** + * Sets a manufacturing token in the Customer Information Block (CIB) area of + * the NCP if that token currently unset (fully erased). Cannot be used with + * EZSP_STACK_CAL_DATA, EZSP_STACK_CAL_FILTER, EZSP_MFG_ASH_CONFIG, or + * EZSP_MFG_CBKE_DATA token. + * @param tokenId Which manufacturing token to set. + * @param tokenData uint8_t * The manufacturing token data. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetMfgToken(tokenId: EzspMfgTokenId, tokenData: Buffer): Promise { + this.startCommand(EzspFrameID.SET_MFG_TOKEN); + this.buffalo.writeUInt8(tokenId); + this.buffalo.writePayload(tokenData); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Callback + * A callback invoked to inform the application that a stack token has changed. + * @param tokenAddress uint16_t The address of the stack token that has changed. + */ + ezspStackTokenChangedHandler(tokenAddress: number): void { + debug(`ezspStackTokenChangedHandler(): callback called with: [tokenAddress=${tokenAddress}]`); + } + + /** + * Returns a pseudorandom number. + * @returns + * - Always returns EMBER_SUCCESS. + * - uint16_t * A pseudorandom number. + */ + async ezspGetRandomNumber(): Promise<[EmberStatus, value: number]> { + this.startCommand(EzspFrameID.GET_RANDOM_NUMBER); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const value = this.buffalo.readUInt16(); + + return [status, value]; + } + + /** + * Sets a timer on the NCP. There are 2 independent timers available for use by the Host. + * A timer can be cancelled by setting time to 0 or units to EMBER_EVENT_INACTIVE. + * @param timerId uint8_t Which timer to set (0 or 1). + * @param time uint16_t The delay before the timerHandler callback will be generated. + * Note that the timer clock is free running and is not synchronized with this command. + * This means that the actual delay will be between time and (time - 1). The maximum delay is 32767. + * @param units The units for time. + * @param repeat If true, a timerHandler callback will be generated repeatedly. If false, only a single timerHandler callback will be generated. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetTimer(timerId: number, time: number, units: EmberEventUnits, repeat: boolean): Promise { + this.startCommand(EzspFrameID.SET_TIMER); + this.buffalo.writeUInt8(timerId); + this.buffalo.writeUInt16(time); + this.buffalo.writeUInt8(units); + this.buffalo.writeUInt8(repeat ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Gets information about a timer. The Host can use this command to find out how + * much longer it will be before a previously set timer will generate a + * callback. + * @param timerId uint8_t Which timer to get information about (0 or 1). + * @returns + * - uint16_t The delay before the timerHandler callback will be generated. + * - EmberEventUnits * The units for time. + * - bool * True if a timerHandler callback will be generated repeatedly. False if only a single timerHandler callback will be generated. + */ + async ezspGetTimer(timerId: number): Promise<[number, units: EmberEventUnits, repeat: boolean]> { + this.startCommand(EzspFrameID.GET_TIMER); + this.buffalo.writeUInt8(timerId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const time = this.buffalo.readUInt16(); + const units = this.buffalo.readUInt8(); + const repeat = this.buffalo.readUInt8() === 1 ? true : false; + + return [time, units, repeat]; + } + + /** + * Callback + * A callback from the timer. + * @param timerId uint8_t Which timer generated the callback (0 or 1). + */ + ezspTimerHandler(timerId: number): void { + debug(`ezspTimerHandler(): callback called with: [timerId=${timerId}]`); + } + + /** + * Sends a debug message from the Host to the Network Analyzer utility via the NCP. + * @param binaryMessage true if the message should be interpreted as binary data, false if the message should be interpreted as ASCII text. + * @param messageContents uint8_t * The binary message. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspDebugWrite(binaryMessage: boolean, messageContents: Buffer): Promise { + this.startCommand(EzspFrameID.DEBUG_WRITE); + this.buffalo.writeUInt8(binaryMessage ? 1 : 0); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Retrieves and clears Ember counters. See the EmberCounterType enumeration for the counter types. + * @returns uint16_t * A list of all counter values ordered according to the EmberCounterType enumeration. + */ + async ezspReadAndClearCounters(): Promise { + this.startCommand(EzspFrameID.READ_AND_CLEAR_COUNTERS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const values = this.buffalo.readListUInt16({length: EmberCounterType.COUNT}); + + return values; + } + + /** + * Retrieves Ember counters. See the EmberCounterType enumeration for the counter types. + * @returns uint16_t * A list of all counter values ordered according to the EmberCounterType enumeration. + */ + async ezspReadCounters(): Promise { + this.startCommand(EzspFrameID.READ_COUNTERS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const values = this.buffalo.readListUInt16({length: EmberCounterType.COUNT}); + + return values; + } + + /** + * Callback + * This call is fired when a counter exceeds its threshold + * @param type Type of Counter + */ + ezspCounterRolloverHandler(type: EmberCounterType): void { + debug(`ezspCounterRolloverHandler(): callback called with: [type=${EmberCounterType[type]}]`); + } + + /** + * Used to test that UART flow control is working correctly. + * @param delay uint16_t Data will not be read from the host for this many milliseconds. + */ + async ezspDelayTest(delay: number): Promise { + this.startCommand(EzspFrameID.DELAY_TEST); + this.buffalo.writeUInt16(delay); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * This retrieves the status of the passed library ID to determine if it is compiled into the stack. + * @param libraryId The ID of the library being queried. + * @returns The status of the library being queried. + */ + async ezspGetLibraryStatus(libraryId: EmberLibraryId): Promise { + this.startCommand(EzspFrameID.GET_LIBRARY_STATUS); + this.buffalo.writeUInt8(libraryId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberLibraryStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Allows the HOST to know whether the NCP is running the XNCP library. If so, + * the response contains also the manufacturer ID and the version number of the + * XNCP application that is running on the NCP. + * @returns + * - EMBER_SUCCESS if the NCP is running the XNCP library, + * - EMBER_INVALID_CALL otherwise. + * - manufacturerId uint16_t * The manufactured ID the user has defined in the XNCP application. + * - versionNumber uint16_t * The version number of the XNCP application. + */ + async ezspGetXncpInfo(): Promise<[EmberStatus, manufacturerId: number, versionNumber: number]> { + this.startCommand(EzspFrameID.GET_XNCP_INFO); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const manufacturerId = this.buffalo.readUInt16(); + const versionNumber = this.buffalo.readUInt16(); + + return [status, manufacturerId, versionNumber]; + } + + /** + * Provides the customer a custom EZSP frame. On the NCP, these frames are only + * handled if the XNCP library is included. On the NCP side these frames are + * handled in the emberXNcpIncomingCustomEzspMessageCallback() callback + * function. + * @param uint8_t * The payload of the custom frame (maximum 119 bytes). + * @param uint8_t The expected length of the response. + * @returns + * - The status returned by the custom command. + * - uint8_t *The response. + */ + async ezspCustomFrame(payload: Buffer, replyLength: number): + Promise<[EmberStatus, outReply: Buffer]> { + this.startCommand(EzspFrameID.CUSTOM_FRAME); + this.buffalo.writePayload(payload); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const reply = this.buffalo.readPayload(); + + if (reply.length > replyLength) { + throw EzspStatus.ERROR_INVALID_VALUE; + } + + return [status, reply]; + } + + /** + * Callback + * A callback indicating a custom EZSP message has been received. + * @param payloadLength uint8_t The length of the custom frame payload. + * @param payload uint8_t * The payload of the custom frame. + */ + ezspCustomFrameHandler(payloadLength: number, payload: number[]): void { + debug(`ezspCustomFrameHandler(): callback called with: [payloadLength=${payloadLength}], [payload=${payload}]`); + } + + /** + * Returns the EUI64 ID of the local node. + * @returns The 64-bit ID. + */ + async ezspGetEui64(): Promise { + this.startCommand(EzspFrameID.GET_EUI64); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const eui64 = this.buffalo.readIeeeAddr(); + + return eui64; + } + + /** + * Returns the 16-bit node ID of the local node. + * @returns The 16-bit ID. + */ + async ezspGetNodeId(): Promise { + this.startCommand(EzspFrameID.GET_NODE_ID); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const nodeId: EmberNodeId = this.buffalo.readUInt16(); + + return nodeId; + } + + /** + * Returns number of phy interfaces present. + * @returns uint8_t Value indicate how many phy interfaces present. + */ + async ezspGetPhyInterfaceCount(): Promise { + this.startCommand(EzspFrameID.GET_PHY_INTERFACE_COUNT); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const interfaceCount = this.buffalo.readUInt8(); + + return interfaceCount; + } + + /** + * Returns the entropy source used for true random number generation. + * @returns Value indicates the used entropy source. + */ + async ezspGetTrueRandomEntropySource(): Promise { + this.startCommand(EzspFrameID.GET_TRUE_RANDOM_ENTROPY_SOURCE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const entropySource: EmberEntropySource = this.buffalo.readUInt8(); + + return entropySource; + } + + //----------------------------------------------------------------------------- + // Networking Frames + //----------------------------------------------------------------------------- + + /** + * Sets the manufacturer code to the specified value. + * The manufacturer code is one of the fields of the node descriptor. + * @param code uint16_t The manufacturer code for the local node. + */ + async ezspSetManufacturerCode(code: number): Promise { + this.startCommand(EzspFrameID.SET_MANUFACTURER_CODE); + this.buffalo.writeUInt16(code); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Sets the power descriptor to the specified value. The power descriptor is a + * dynamic value. Therefore, you should call this function whenever the value + * changes. + * @param descriptor uint16_t The new power descriptor for the local node. + */ + async ezspSetPowerDescriptor(descriptor: number): Promise { + this.startCommand(EzspFrameID.SET_POWER_DESCRIPTOR); + this.buffalo.writeUInt16(descriptor); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Resume network operation after a reboot. The node retains its original type. + * This should be called on startup whether or not the node was previously part + * of a network. EMBER_NOT_JOINED is returned if the node is not part of a + * network. This command accepts options to control the network initialization. + * @param networkInitStruct EmberNetworkInitStruct * An EmberNetworkInitStruct containing the options for initialization. + * @returns An EmberStatus value that indicates one of the following: successful + * initialization, EMBER_NOT_JOINED if the node is not part of a network, or the + * reason for failure. + */ + async ezspNetworkInit(networkInitStruct: EmberNetworkInitStruct): Promise { + this.startCommand(EzspFrameID.NETWORK_INIT); + this.buffalo.writeEmberNetworkInitStruct(networkInitStruct); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Returns a value indicating whether the node is joining, joined to, or leaving a network. + * @returns Command send status. + * @returns An EmberNetworkStatus value indicating the current join status. + */ + async ezspNetworkState(): Promise { + this.startCommand(EzspFrameID.NETWORK_STATE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberNetworkStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Callback + * A callback invoked when the status of the stack changes. If the status + * parameter equals EMBER_NETWORK_UP, then the getNetworkParameters command can + * be called to obtain the new network parameters. If any of the parameters are + * being stored in nonvolatile memory by the Host, the stored values should be + * updated. + * @param status Stack status + */ + ezspStackStatusHandler(status: EmberStatus): void { + debug(`ezspStackStatusHandler(): callback called with: [status=${EmberStatus[status]}]`); + + this.emit(EzspEvents.STACK_STATUS, status); + } + + /** + * This function will start a scan. + * @param scanType Indicates the type of scan to be performed. Possible values are: EZSP_ENERGY_SCAN and EZSP_ACTIVE_SCAN. + * For each type, the respective callback for reporting results is: energyScanResultHandler and networkFoundHandler. + * The energy scan and active scan report errors and completion via the scanCompleteHandler. + * @param channelMask uint32_t Bits set as 1 indicate that this particular channel should be scanned. + * Bits set to 0 indicate that this particular channel should not be scanned. For example, a channelMask value of 0x00000001 + * would indicate that only channel 0 should be scanned. Valid channels range from 11 to 26 inclusive. + * This translates to a channel mask value of 0x07FFF800. + * As a convenience, a value of 0 is reinterpreted as the mask for the current channel. + * @param duration uint8_t Sets the exponent of the number of scan periods, where a scan period is 960 symbols. + * The scan will occur for ((2^duration) + 1) scan periods. + * @returns + * - SL_STATUS_OK signals that the scan successfully started. Possible error responses and their meanings: + * - SL_STATUS_MAC_SCANNING, we are already scanning; + * - SL_STATUS_BAD_SCAN_DURATION, we have set a duration value that is not 0..14 inclusive; + * - SL_STATUS_MAC_INCORRECT_SCAN_TYPE, we have requested an undefined scanning type; + * - SL_STATUS_INVALID_CHANNEL_MASK, our channel mask did not specify any valid channels. + */ + async ezspStartScan(scanType: EzspNetworkScanType, channelMask: number, duration: number): Promise { + this.startCommand(EzspFrameID.START_SCAN); + this.buffalo.writeUInt8(scanType); + this.buffalo.writeUInt32(channelMask); + this.buffalo.writeUInt8(duration); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: SLStatus = this.buffalo.readUInt32(); + + return status; + } + + /** + * Callback + * Reports the result of an energy scan for a single channel. The scan is not + * complete until the scanCompleteHandler callback is called. + * @param channel uint8_t The 802.15.4 channel number that was scanned. + * @param maxRssiValue int8_t The maximum RSSI value found on the channel. + */ + ezspEnergyScanResultHandler(channel: number, maxRssiValue: number): void { + debug(`ezspEnergyScanResultHandler(): callback called with: [channel=${channel}], [maxRssiValue=${maxRssiValue}]`); + console.log(`Energy scan for channel ${channel} reports max RSSI value at ${maxRssiValue}.`); + } + + /** + * Callback + * Reports that a network was found as a result of a prior call to startScan. + * Gives the network parameters useful for deciding which network to join. + * @param networkFound EmberZigbeeNetwork * The parameters associated with the network found. + * @param lastHopLqi uint8_t The link quality from the node that generated this beacon. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during the reception. + */ + ezspNetworkFoundHandler(networkFound: EmberZigbeeNetwork, lastHopLqi: number, lastHopRssi: number): void { + debug(`ezspNetworkFoundHandler(): callback called with: [networkFound=${networkFound}], ` + + `[lastHopLqi=${lastHopLqi}], [lastHopRssi=${lastHopRssi}]`); + } + + /** + * Callback + * @param channel uint8_t The channel on which the current error occurred. Undefined for the case of EMBER_SUCCESS. + * @param status The error condition that occurred on the current channel. Value will be EMBER_SUCCESS when the scan has completed. + * Returns the status of the current scan of type EZSP_ENERGY_SCAN or + * EZSP_ACTIVE_SCAN. EMBER_SUCCESS signals that the scan has completed. Other + * error conditions signify a failure to scan on the channel specified. + */ + ezspScanCompleteHandler(channel: number, status: EmberStatus): void { + debug(`ezspScanCompleteHandler(): callback called with: [channel=${channel}], [status=${EmberStatus[status]}]`); + } + + /** + * Callback + * This function returns an unused panID and channel pair found via the find + * unused panId scan procedure. + * @param The unused panID which has been found. + * @param channel uint8_t The channel that the unused panID was found on. + */ + ezspUnusedPanIdFoundHandler(panId: EmberPanId, channel: number): void { + debug(`ezspUnusedPanIdFoundHandler(): callback called with: [panId=${panId}], [channel=${channel}]`); + } + + /** + * This function starts a series of scans which will return an available panId. + * @param channelMask uint32_t The channels that will be scanned for available panIds. + * @param duration uint8_t The duration of the procedure. + * @returns The error condition that occurred during the scan. Value will be + * EMBER_SUCCESS if there are no errors. + */ + async ezspFindUnusedPanId(channelMask: number, duration: number): Promise { + this.startCommand(EzspFrameID.FIND_UNUSED_PAN_ID); + this.buffalo.writeUInt32(channelMask); + this.buffalo.writeUInt8(duration); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Terminates a scan in progress. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspStopScan(): Promise { + this.startCommand(EzspFrameID.STOP_SCAN); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Forms a new network by becoming the coordinator. + * @param parameters EmberNetworkParameters * Specification of the new network. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspFormNetwork(parameters: EmberNetworkParameters): Promise { + this.startCommand(EzspFrameID.FORM_NETWORK); + this.buffalo.writeEmberNetworkParameters(parameters); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Causes the stack to associate with the network using the specified network + * parameters. It can take several seconds for the stack to associate with the + * local network. Do not send messages until the stackStatusHandler callback + * informs you that the stack is up. + * @param nodeType Specification of the role that this node will have in the network. + * This role must not be EMBER_COORDINATOR. To be a coordinator, use the formNetwork command. + * @param parameters EmberNetworkParameters * Specification of the network with which the node should associate. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspJoinNetwork(nodeType: EmberNodeType, parameters: EmberNetworkParameters): Promise { + this.startCommand(EzspFrameID.JOIN_NETWORK); + this.buffalo.writeUInt8(nodeType); + this.buffalo.writeEmberNetworkParameters(parameters); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Causes the stack to associate with the network using the specified network + * parameters in the beacon parameter. It can take several seconds for the stack + * to associate with the local network. Do not send messages until the + * stackStatusHandler callback informs you that the stack is up. Unlike + * ::emberJoinNetwork(), this function does not issue an active scan before + * joining. Instead, it will cause the local node to issue a MAC Association + * Request directly to the specified target node. It is assumed that the beacon + * parameter is an artifact after issuing an active scan. (For more information, + * see emberGetBestBeacon and emberGetNextBeacon.) + * @param localNodeType Specifies the role that this node will have in the network. This role must not be EMBER_COORDINATOR. + * To be a coordinator, use the formNetwork command. + * @param beacon EmberBeaconData * Specifies the network with which the node should associate. + * @param radioTxPower int8_t The radio transmit power to use, specified in dBm. + * @param clearBeaconsAfterNetworkUp If true, clear beacons in cache upon join success. If join fail, do nothing. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspJoinNetworkDirectly(localNodeType: EmberNodeType, beacon: EmberBeaconData, radioTxPower: number, clearBeaconsAfterNetworkUp: boolean) + : Promise { + this.startCommand(EzspFrameID.JOIN_NETWORK_DIRECTLY); + this.buffalo.writeUInt8(localNodeType); + this.buffalo.writeEmberBeaconData(beacon); + this.buffalo.writeUInt8(radioTxPower); + this.buffalo.writeUInt8(clearBeaconsAfterNetworkUp ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Causes the stack to leave the current network. This generates a + * stackStatusHandler callback to indicate that the network is down. The radio + * will not be used until after sending a formNetwork or joinNetwork command. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspLeaveNetwork(): Promise { + this.startCommand(EzspFrameID.LEAVE_NETWORK); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * The application may call this function when contact with the network has been + * lost. The most common usage case is when an end device can no longer + * communicate with its parent and wishes to find a new one. Another case is + * when a device has missed a Network Key update and no longer has the current + * Network Key. The stack will call ezspStackStatusHandler to indicate that the + * network is down, then try to re-establish contact with the network by + * performing an active scan, choosing a network with matching extended pan id, + * and sending a ZigBee network rejoin request. A second call to the + * ezspStackStatusHandler callback indicates either the success or the failure + * of the attempt. The process takes approximately 150 milliseconds per channel + * to complete. + * @param haveCurrentNetworkKey This parameter tells the stack whether to try to use the current network key. + * If it has the current network key it will perform a secure rejoin (encrypted). If this fails the device should try an unsecure rejoin. + * If the Trust Center allows the rejoin then the current Network Key will be sent encrypted using the device's Link Key. + * @param channelMask uint32_t A mask indicating the channels to be scanned. See emberStartScan for format details. + * A value of 0 is reinterpreted as the mask for the current channel. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspFindAndRejoinNetwork(haveCurrentNetworkKey: boolean, channelMask: number): Promise { + this.startCommand(EzspFrameID.FIND_AND_REJOIN_NETWORK); + this.buffalo.writeUInt8(haveCurrentNetworkKey ? 1 : 0); + this.buffalo.writeUInt32(channelMask); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Tells the stack to allow other nodes to join the network with this node as + * their parent. Joining is initially disabled by default. + * @param duration uint8_t A value of 0x00 disables joining. A value of 0xFF enables joining. + * Any other value enables joining for that number of seconds. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspPermitJoining(duration: number): Promise { + this.startCommand(EzspFrameID.PERMIT_JOINING); + this.buffalo.writeUInt8(duration); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Callback + * Indicates that a child has joined or left. + * @param index uint8_t The index of the child of interest. + * @param joining True if the child is joining. False the child is leaving. + * @param childId The node ID of the child. + * @param childEui64 The EUI64 of the child. + * @param childType The node type of the child. + */ + ezspChildJoinHandler(index: number, joining: boolean, childId: EmberNodeId, childEui64: EmberEUI64, childType: EmberNodeType): void { + debug(`ezspChildJoinHandler(): callback called with: [index=${index}], [joining=${joining}], ` + + `[childId=${childId}], [childEui64=${childEui64}], [childType=${childType}]`); + } + + /** + * Sends a ZDO energy scan request. This request may only be sent by the current + * network manager and must be unicast, not broadcast. See ezsp-utils.h for + * related macros emberSetNetworkManagerRequest() and + * emberChangeChannelRequest(). + * @param target The network address of the node to perform the scan. + * @param scanChannels uint32_t A mask of the channels to be scanned + * @param scanDuration uint8_t How long to scan on each channel. + * Allowed values are 0..5, with the scan times as specified by 802.15.4 (0 = 31ms, 1 = 46ms, 2 = 77ms, 3 = 138ms, 4 = 261ms, 5 = 507ms). + * @param scanCount uint16_t The number of scans to be performed on each channel (1..8). + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspEnergyScanRequest(target: EmberNodeId, scanChannels: number, scanDuration: number, scanCount: number): Promise { + this.startCommand(EzspFrameID.ENERGY_SCAN_REQUEST); + this.buffalo.writeUInt16(target); + this.buffalo.writeUInt32(scanChannels); + this.buffalo.writeUInt8(scanDuration); + this.buffalo.writeUInt16(scanCount); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Returns the current network parameters. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberNodeType * An EmberNodeType value indicating the current node type. + * @returns EmberNetworkParameters * The current network parameters. + */ + async ezspGetNetworkParameters(): Promise<[EmberStatus, nodeType: EmberNodeType, parameters: EmberNetworkParameters]> { + this.startCommand(EzspFrameID.GET_NETWORK_PARAMETERS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const nodeType = this.buffalo.readUInt8(); + const parameters = this.buffalo.readEmberNetworkParameters(); + + return [status, nodeType, parameters]; + } + + /** + * Returns the current radio parameters based on phy index. + * @param phyIndex uint8_t Desired index of phy interface for radio parameters. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberMultiPhyRadioParameters * The current radio parameters based on provided phy index. + */ + async ezspGetRadioParameters(phyIndex: number): Promise<[EmberStatus, parameters: EmberMultiPhyRadioParameters]> { + this.startCommand(EzspFrameID.GET_RADIO_PARAMETERS); + this.buffalo.writeUInt8(phyIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const parameters = this.buffalo.readEmberMultiPhyRadioParameters(); + + return [status, parameters]; + } + + /** + * Returns information about the children of the local node and the parent of + * the local node. + * @returns uint8_t The number of children the node currently has. + * @returns The parent's EUI64. The value is undefined for nodes without parents (coordinators and nodes that are not joined to a network). + * @returns EmberNodeId * The parent's node ID. The value is undefined for nodes without parents + * (coordinators and nodes that are not joined to a network). + */ + async ezspGetParentChildParameters(): Promise<[number, parentEui64: EmberEUI64, parentNodeId: EmberNodeId]> { + this.startCommand(EzspFrameID.GET_PARENT_CHILD_PARAMETERS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const childCount = this.buffalo.readUInt8(); + const parentEui64 = this.buffalo.readIeeeAddr(); + const parentNodeId = this.buffalo.readUInt16(); + + return [childCount, parentEui64, parentNodeId]; + } + + /** + * Returns information about a child of the local node. + * @param uint8_t The index of the child of interest in the child table. Possible indexes range from zero to EMBER_CHILD_TABLE_SIZE. + * @returns EMBER_SUCCESS if there is a child at index. EMBER_NOT_JOINED if there is no child at index. + * @returns EmberChildData * The data of the child. + */ + async ezspGetChildData(index: number): Promise<[EmberStatus, childData: EmberChildData]> { + this.startCommand(EzspFrameID.GET_CHILD_DATA); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const childData = this.buffalo.readEmberChildData(); + + return [status, childData]; + } + + /** + * Sets child data to the child table token. + * @param index uint8_t The index of the child of interest in the child table. Possible indexes range from zero to (EMBER_CHILD_TABLE_SIZE - 1). + * @param childData EmberChildData * The data of the child. + * @returns EMBER_SUCCESS if the child data is set successfully at index. EMBER_INDEX_OUT_OF_RANGE if provided index is out of range. + */ + async ezspSetChildData(index: number, childData: EmberChildData): Promise { + this.startCommand(EzspFrameID.SET_CHILD_DATA); + this.buffalo.writeUInt8(index); + this.buffalo.writeEmberChildData(childData); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Convert a child index to a node ID + * @param childIndex uint8_t The index of the child of interest in the child table. Possible indexes range from zero to EMBER_CHILD_TABLE_SIZE. + * @returns The node ID of the child or EMBER_NULL_NODE_ID if there isn't a child at the childIndex specified + */ + async ezspChildId(childIndex: number): Promise { + this.startCommand(EzspFrameID.CHILD_ID); + this.buffalo.writeUInt8(childIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const childId: EmberNodeId = this.buffalo.readUInt16(); + + return childId; + } + + /** + * Convert a node ID to a child index + * @param childId The node ID of the child + * @returns uint8_t The child index or 0xFF if the node ID doesn't belong to a child + */ + async ezspChildIndex(childId: EmberNodeId): Promise { + this.startCommand(EzspFrameID.CHILD_INDEX); + this.buffalo.writeUInt16(childId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const childIndex = this.buffalo.readUInt8(); + + return childIndex; + } + + /** + * Returns the source route table total size. + * @returns uint8_t Total size of source route table. + */ + async ezspGetSourceRouteTableTotalSize(): Promise { + this.startCommand(EzspFrameID.GET_SOURCE_ROUTE_TABLE_TOTAL_SIZE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const sourceRouteTableTotalSize = this.buffalo.readUInt8(); + + return sourceRouteTableTotalSize; + } + + /** + * Returns the number of filled entries in source route table. + * @returns uint8_t The number of filled entries in source route table. + */ + async ezspGetSourceRouteTableFilledSize(): Promise { + this.startCommand(EzspFrameID.GET_SOURCE_ROUTE_TABLE_FILLED_SIZE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const sourceRouteTableFilledSize = this.buffalo.readUInt8(); + + return sourceRouteTableFilledSize; + } + + /** + * Returns information about a source route table entry + * @param index uint8_t The index of the entry of interest in the source route table. + * Possible indexes range from zero to SOURCE_ROUTE_TABLE_FILLED_SIZE. + * @returns EMBER_SUCCESS if there is source route entry at index. EMBER_NOT_FOUND if there is no source route at index. + * @returns EmberNodeId * The node ID of the destination in that entry. + * @returns uint8_t * The closer node index for this source route table entry + */ + async ezspGetSourceRouteTableEntry(index: number): Promise<[EmberStatus, destination: EmberNodeId, closerIndex: number]> { + this.startCommand(EzspFrameID.GET_SOURCE_ROUTE_TABLE_ENTRY); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const destination = this.buffalo.readUInt16(); + const closerIndex = this.buffalo.readUInt8(); + + return [status, destination, closerIndex]; + } + + /** + * Returns the neighbor table entry at the given index. The number of active + * neighbors can be obtained using the neighborCount command. + * @param index uint8_t The index of the neighbor of interest. Neighbors are stored in ascending order by node id, + * with all unused entries at the end of the table. + * @returns EMBER_ERR_FATAL if the index is greater or equal to the number of active neighbors, or if the device is an end device. + * Returns EMBER_SUCCESS otherwise. + * @returns EmberNeighborTableEntry * The contents of the neighbor table entry. + */ + async ezspGetNeighbor(index: number): Promise<[EmberStatus, value: EmberNeighborTableEntry]> { + this.startCommand(EzspFrameID.GET_NEIGHBOR); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const value = this.buffalo.readEmberNeighborTableEntry(); + + return [status, value]; + } + + /** + * Return EmberStatus depending on whether the frame counter of the node is + * found in the neighbor or child table. This function gets the last received + * frame counter as found in the Network Auxiliary header for the specified + * neighbor or child + * @param eui64 eui64 of the node + * @returns Return EMBER_NOT_FOUND if the node is not found in the neighbor or child table. Returns EMBER_SUCCESS otherwise + * @returns uint32_t * Return the frame counter of the node from the neighbor or child table + */ + async ezspGetNeighborFrameCounter(eui64: EmberEUI64): Promise<[EmberStatus, returnFrameCounter: number]> { + this.startCommand(EzspFrameID.GET_NEIGHBOR_FRAME_COUNTER); + this.buffalo.writeIeeeAddr(eui64); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const returnFrameCounter = this.buffalo.readUInt32(); + + return [status, returnFrameCounter]; + } + + /** + * Sets the frame counter for the neighbour or child. + * @param eui64 eui64 of the node + * @param frameCounter uint32_t Return the frame counter of the node from the neighbor or child table + * @returns + * - EMBER_NOT_FOUND if the node is not found in the neighbor or child table. + * - EMBER_SUCCESS otherwise + */ + async ezspSetNeighborFrameCounter(eui64: EmberEUI64, frameCounter: number): Promise { + this.startCommand(EzspFrameID.SET_NEIGHBOR_FRAME_COUNTER); + this.buffalo.writeIeeeAddr(eui64); + this.buffalo.writeUInt32(frameCounter); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Sets the routing shortcut threshold to directly use a neighbor instead of + * performing routing. + * @param costThresh uint8_t The routing shortcut threshold to configure. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetRoutingShortcutThreshold(costThresh: number): Promise { + this.startCommand(EzspFrameID.SET_ROUTING_SHORTCUT_THRESHOLD); + this.buffalo.writeUInt8(costThresh); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + + return status; + } + + /** + * Gets the routing shortcut threshold used to differentiate between directly + * using a neighbor vs. performing routing. + * @returns uint8_t The routing shortcut threshold + */ + async ezspGetRoutingShortcutThreshold(): Promise { + this.startCommand(EzspFrameID.GET_ROUTING_SHORTCUT_THRESHOLD); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const routingShortcutThresh = this.buffalo.readUInt8(); + return routingShortcutThresh; + } + + /** + * Returns the number of active entries in the neighbor table. + * @returns uint8_t The number of active entries in the neighbor table. + */ + async ezspNeighborCount(): Promise { + this.startCommand(EzspFrameID.NEIGHBOR_COUNT); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const value = this.buffalo.readUInt8(); + return value; + } + + /** + * Returns the route table entry at the given index. The route table size can be + * obtained using the getConfigurationValue command. + * @param index uint8_t The index of the route table entry of interest. + * @returns + * - EMBER_ERR_FATAL if the index is out of range or the device is an end + * - EMBER_SUCCESS otherwise. + * @returns EmberRouteTableEntry * The contents of the route table entry. + */ + async ezspGetRouteTableEntry(index: number): Promise<[EmberStatus, value: EmberRouteTableEntry]> { + this.startCommand(EzspFrameID.GET_ROUTE_TABLE_ENTRY); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const value = this.buffalo.readEmberRouteTableEntry(); + + return [status, value]; + } + + /** + * Sets the radio output power at which a node is operating. Ember radios have + * discrete power settings. For a list of available power settings, see the + * technical specification for the RF communication module in your Developer + * Kit. Note: Care should be taken when using this API on a running network, as + * it will directly impact the established link qualities neighboring nodes have + * with the node on which it is called. This can lead to disruption of existing + * routes and erratic network behavior. + * @param power int8_t Desired radio output power, in dBm. + * @returns An EmberStatus value indicating the success or failure of the command. + */ + async ezspSetRadioPower(power: number): Promise { + this.startCommand(EzspFrameID.SET_RADIO_POWER); + this.buffalo.writeUInt8(power); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the channel to use for sending and receiving messages. For a list of + * available radio channels, see the technical specification for the RF + * communication module in your Developer Kit. Note: Care should be taken when + * using this API, as all devices on a network must use the same channel. + * @param channel uint8_t Desired radio channel. + * @returns An EmberStatus value indicating the success or failure of the command. + */ + async ezspSetRadioChannel(channel: number): Promise { + this.startCommand(EzspFrameID.SET_RADIO_CHANNEL); + this.buffalo.writeUInt8(channel); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Gets the channel in use for sending and receiving messages. + * @returns uint8_t Current radio channel. + */ + async ezspGetRadioChannel(): Promise { + this.startCommand(EzspFrameID.GET_RADIO_CHANNEL); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const channel = this.buffalo.readUInt8(); + + return channel; + } + + /** + * Set the configured 802.15.4 CCA mode in the radio. + * @param ccaMode uint8_t A RAIL_IEEE802154_CcaMode_t value. + * @returns An EmberStatus value indicating the success or failure of the + * command. + */ + async ezspSetRadioIeee802154CcaMode(ccaMode: number): Promise { + this.startCommand(EzspFrameID.SET_RADIO_IEEE802154_CCA_MODE); + this.buffalo.writeUInt8(ccaMode); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Enable/disable concentrator support. + * @param on If this bool is true the concentrator support is enabled. Otherwise is disabled. + * If this bool is false all the other arguments are ignored. + * @param concentratorType uint16_t Must be either EMBER_HIGH_RAM_CONCENTRATOR or EMBER_LOW_RAM_CONCENTRATOR. + * The former is used when the caller has enough memory to store source routes for the whole network. + * In that case, remote nodes stop sending route records once the concentrator has successfully received one. + * The latter is used when the concentrator has insufficient RAM to store all outbound source routes. + * In that case, route records are sent to the concentrator prior to every inbound APS unicast. + * @param minTime uint16_t The minimum amount of time that must pass between MTORR broadcasts. + * @param maxTime uint16_t The maximum amount of time that can pass between MTORR broadcasts. + * @param routeErrorThreshold uint8_t The number of route errors that will trigger a re-broadcast of the MTORR. + * @param deliveryFailureThreshold uint8_t The number of APS delivery failures that will trigger a re-broadcast of the MTORR. + * @param maxHops uint8_t The maximum number of hops that the MTORR broadcast will be allowed to have. + * A value of 0 will be converted to the EMBER_MAX_HOPS value set by the stack. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetConcentrator(on: boolean, concentratorType: number, minTime: number, maxTime: number, routeErrorThreshold: number, + deliveryFailureThreshold: number, maxHops: number): Promise { + this.startCommand(EzspFrameID.SET_CONCENTRATOR); + this.buffalo.writeUInt8(on ? 1 : 0); + this.buffalo.writeUInt16(concentratorType); + this.buffalo.writeUInt16(minTime); + this.buffalo.writeUInt16(maxTime); + this.buffalo.writeUInt8(routeErrorThreshold); + this.buffalo.writeUInt8(deliveryFailureThreshold); + this.buffalo.writeUInt8(maxHops); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the error code that is sent back from a router with a broken route. + * @param errorCode uint8_t Desired error code. + * @returns An EmberStatus value indicating the success or failure of the + * command. + */ + async ezspSetBrokenRouteErrorCode(errorCode: number): Promise { + this.startCommand(EzspFrameID.SET_BROKEN_ROUTE_ERROR_CODE); + this.buffalo.writeUInt8(errorCode); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This causes to initialize the desired radio interface other than native and + * form a new network by becoming the coordinator with same panId as native + * radio network. + * @param phyIndex uint8_t Index of phy interface. The native phy index would be always zero hence valid phy index starts from one. + * @param page uint8_t Desired radio channel page. + * @param channel uint8_t Desired radio channel. + * @param power int8_t Desired radio output power, in dBm. + * @param bitmask Network configuration bitmask. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspMultiPhyStart(phyIndex: number, page: number, channel: number, power: number, bitmask: EmberMultiPhyNwkConfig): Promise { + this.startCommand(EzspFrameID.MULTI_PHY_START); + this.buffalo.writeUInt8(phyIndex); + this.buffalo.writeUInt8(page); + this.buffalo.writeUInt8(channel); + this.buffalo.writeUInt8(power); + this.buffalo.writeUInt8(bitmask); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This causes to bring down the radio interface other than native. + * @param phyIndex uint8_t Index of phy interface. The native phy index would be always zero hence valid phy index starts from one. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspMultiPhyStop(phyIndex: number): Promise { + this.startCommand(EzspFrameID.MULTI_PHY_STOP); + this.buffalo.writeUInt8(phyIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the radio output power for desired phy interface at which a node is + * operating. Ember radios have discrete power settings. For a list of available + * power settings, see the technical specification for the RF communication + * module in your Developer Kit. Note: Care should be taken when using this api + * on a running network, as it will directly impact the established link + * qualities neighboring nodes have with the node on which it is called. This + * can lead to disruption of existing routes and erratic network behavior. + * @param phyIndex uint8_t Index of phy interface. The native phy index would be always zero hence valid phy index starts from one. + * @param power int8_t Desired radio output power, in dBm. + * @returns An EmberStatus value indicating the success or failure of the + * command. + */ + async ezspMultiPhySetRadioPower(phyIndex: number, power: number): Promise { + this.startCommand(EzspFrameID.MULTI_PHY_SET_RADIO_POWER); + this.buffalo.writeUInt8(phyIndex); + this.buffalo.writeUInt8(power); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Send Link Power Delta Request from a child to its parent + * @returns An EmberStatus value indicating the success or failure of sending the request. + */ + async ezspSendLinkPowerDeltaRequest(): Promise { + this.startCommand(EzspFrameID.SEND_LINK_POWER_DELTA_REQUEST); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the channel for desired phy interface to use for sending and receiving + * messages. For a list of available radio pages and channels, see the technical + * specification for the RF communication module in your Developer Kit. Note: + * Care should be taken when using this API, as all devices on a network must + * use the same page and channel. + * @param phyIndex uint8_t Index of phy interface. The native phy index would be always zero hence valid phy index starts from one. + * @param page uint8_t Desired radio channel page. + * @param channel uint8_t Desired radio channel. + * @returns An EmberStatus value indicating the success or failure of the command. + */ + async ezspMultiPhySetRadioChannel(phyIndex: number, page: number, channel: number): Promise { + this.startCommand(EzspFrameID.MULTI_PHY_SET_RADIO_CHANNEL); + this.buffalo.writeUInt8(phyIndex); + this.buffalo.writeUInt8(page); + this.buffalo.writeUInt8(channel); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Obtains the current duty cycle state. + * @returns An EmberStatus value indicating the success or failure of the command. + * @returns EmberDutyCycleState * The current duty cycle state in effect. + */ + async ezspGetDutyCycleState(): Promise<[EmberStatus, returnedState: EmberDutyCycleState]> { + this.startCommand(EzspFrameID.GET_DUTY_CYCLE_STATE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const returnedState = this.buffalo.readUInt8(); + + return [status, returnedState]; + } + + /** + * Set the current duty cycle limits configuration. The Default limits set by + * stack if this call is not made. + * @param limits EmberDutyCycleLimits * The duty cycle limits configuration to utilize. + * @returns EMBER_SUCCESS if the duty cycle limit configurations set + * successfully, EMBER_BAD_ARGUMENT if set illegal value such as setting only + * one of the limits to default or violates constraints Susp > Crit > Limi, + * EMBER_INVALID_CALL if device is operating on 2.4Ghz + */ + async ezspSetDutyCycleLimitsInStack(limits: EmberDutyCycleLimits): Promise { + this.startCommand(EzspFrameID.SET_DUTY_CYCLE_LIMITS_IN_STACK); + this.buffalo.writeEmberDutyCycleLimits(limits); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Obtains the current duty cycle limits that were previously set by a call to + * emberSetDutyCycleLimitsInStack(), or the defaults set by the stack if no set + * call was made. + * @returns An EmberStatus value indicating the success or failure of the command. + * @returns EmberDutyCycleLimits * Return current duty cycle limits if returnedLimits is not NULL + */ + async ezspGetDutyCycleLimits(): Promise<[EmberStatus, returnedLimits: EmberDutyCycleLimits]> { + this.startCommand(EzspFrameID.GET_DUTY_CYCLE_LIMITS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const returnedLimits = this.buffalo.readEmberDutyCycleLimits(); + + return [status, returnedLimits]; + } + + /** + * Returns the duty cycle of the stack's connected children that are being + * monitored, up to maxDevices. It indicates the amount of overall duty cycle + * they have consumed (up to the suspend limit). The first entry is always the + * local stack's nodeId, and thus the total aggregate duty cycle for the device. + * The passed pointer arrayOfDeviceDutyCycles MUST have space for maxDevices. + * @param maxDevices uint8_t Number of devices to retrieve consumed duty cycle. + * @returns + * - EMBER_SUCCESS if the duty cycles were read successfully, + * - EMBER_BAD_ARGUMENT maxDevices is greater than EMBER_MAX_END_DEVICE_CHILDREN + 1. + * @returns uint8_t * Consumed duty cycles up to maxDevices. When the number of children that are being monitored is less than maxDevices, + * the EmberNodeId element in the EmberPerDeviceDutyCycle will be 0xFFFF. + */ + async ezspGetCurrentDutyCycle(maxDevices: number): Promise<[EmberStatus, arrayOfDeviceDutyCycles: number[]]> { + this.startCommand(EzspFrameID.GET_CURRENT_DUTY_CYCLE); + this.buffalo.writeUInt8(maxDevices); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const arrayOfDeviceDutyCycles = this.buffalo.readListUInt8({length: 134}); + + return [status, arrayOfDeviceDutyCycles]; + } + + /** + * Callback + * Callback fires when the duty cycle state has changed + * @param channelPage uint8_t The channel page whose duty cycle state has changed. + * @param channel uint8_t The channel number whose duty cycle state has changed. + * @param state The current duty cycle state. + * @param totalDevices uint8_t The total number of connected end devices that are being monitored for duty cycle. + * @param arrayOfDeviceDutyCycles EmberPerDeviceDutyCycle * Consumed duty cycles of end devices that are being monitored. + * The first entry always be the local stack's nodeId, and thus the total aggregate duty cycle for the device. + */ + ezspDutyCycleHandler(channelPage: number, channel: number, state: EmberDutyCycleState, totalDevices: number, + arrayOfDeviceDutyCycles: EmberPerDeviceDutyCycle[]): void { + debug(`ezspDutyCycleHandler(): callback called with: [channelPage=${channelPage}], [channel=${channel}], ` + + `[state=${state}], [totalDevices=${totalDevices}], [arrayOfDeviceDutyCycles=${arrayOfDeviceDutyCycles}]`); + } + + /** + * Returns the first beacon in the cache. Beacons are stored in cache after + * issuing an active scan. + * @returns + * - EMBER_SUCCESS if first beacon found, + * - EMBER_BAD_ARGUMENT if input parameters are invalid, EMBER_INVALID_CALL if no beacons stored, + * - EMBER_ERR_FATAL if no first beacon found. + * @returns EmberBeaconIterator * The iterator to use when returning the first beacon. This argument must not be NULL. + */ + async ezspGetFirstBeacon(): Promise<[EmberStatus, beaconIterator: EmberBeaconIterator]> { + this.startCommand(EzspFrameID.GET_FIRST_BEACON); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const beaconIterator = this.buffalo.readEmberBeaconIterator(); + + return [status, beaconIterator]; + } + + /** + * Returns the next beacon in the cache. Beacons are stored in cache after + * issuing an active scan. + * @returns + * - EMBER_SUCCESS if next beacon found, + * - EMBER_BAD_ARGUMENT if input parameters are invalid, + * - EMBER_ERR_FATAL if no next beacon found. + * @returns EmberBeaconData * The next beacon retrieved. It is assumed that emberGetFirstBeacon has been called first. + * This argument must not be NULL. + */ + async ezspGetNextBeacon(): Promise<[EmberStatus, beacon: EmberBeaconData]> { + this.startCommand(EzspFrameID.GET_NEXT_BEACON); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const beacon = this.buffalo.readEmberBeaconData(); + + return [status, beacon]; + } + + /** + * Returns the number of cached beacons that have been collected from a scan. + * @returns uint8_t The number of cached beacons that have been collected from a scan. + */ + async ezspGetNumStoredBeacons(): Promise { + this.startCommand(EzspFrameID.GET_NUM_STORED_BEACONS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const numBeacons = this.buffalo.readUInt8(); + + return numBeacons; + } + + /** + * Clears all cached beacons that have been collected from a scan. + */ + async ezspClearStoredBeacons(): Promise { + this.startCommand(EzspFrameID.CLEAR_STORED_BEACONS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * This call sets the radio channel in the stack and propagates the information + * to the hardware. + * @param radioChannel uint8_t The radio channel to be set. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetLogicalAndRadioChannel(radioChannel: number): Promise { + this.startCommand(EzspFrameID.SET_LOGICAL_AND_RADIO_CHANNEL); + this.buffalo.writeUInt8(radioChannel); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + //----------------------------------------------------------------------------- + // Binding Frames + //----------------------------------------------------------------------------- + + /** + * Deletes all binding table entries. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspClearBindingTable(): Promise { + this.startCommand(EzspFrameID.CLEAR_BINDING_TABLE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets an entry in the binding table. + * @param index uint8_t The index of a binding table entry. + * @param value EmberBindingTableEntry * The contents of the binding entry. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetBinding(index: number, value: EmberBindingTableEntry): Promise { + this.startCommand(EzspFrameID.SET_BINDING); + this.buffalo.writeUInt8(index); + this.buffalo.writeEmberBindingTableEntry(value); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Gets an entry from the binding table. + * @param index uint8_t The index of a binding table entry. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberBindingTableEntry * The contents of the binding entry. + */ + async ezspGetBinding(index: number): Promise<[EmberStatus, value: EmberBindingTableEntry]> { + this.startCommand(EzspFrameID.GET_BINDING); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const value = this.buffalo.readEmberBindingTableEntry(); + + return [status, value]; + } + + /** + * Deletes a binding table entry. + * @param index uint8_t The index of a binding table entry. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspDeleteBinding(index: number): Promise { + this.startCommand(EzspFrameID.DELETE_BINDING); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Indicates whether any messages are currently being sent using this binding + * table entry. Note that this command does not indicate whether a binding is + * clear. To determine whether a binding is clear, check whether the type field + * of the EmberBindingTableEntry has the value EMBER_UNUSED_BINDING. + * @param index uint8_t The index of a binding table entry. + * @returns True if the binding table entry is active, false otherwise. + */ + async ezspBindingIsActive(index: number): Promise { + this.startCommand(EzspFrameID.BINDING_IS_ACTIVE); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const active = this.buffalo.readUInt8() === 1 ? true : false; + + return active; + } + + /** + * Returns the node ID for the binding's destination, if the ID is known. If a + * message is sent using the binding and the destination's ID is not known, the + * stack will discover the ID by broadcasting a ZDO address request. The + * application can avoid the need for this discovery by using + * setBindingRemoteNodeId when it knows the correct ID via some other means. The + * destination's node ID is forgotten when the binding is changed, when the + * local node reboots or, much more rarely, when the destination node changes + * its ID in response to an ID conflict. + * @param index uint8_t The index of a binding table entry. + * @returns The short ID of the destination node or EMBER_NULL_NODE_ID if no destination is known. + */ + async ezspGetBindingRemoteNodeId(index: number): Promise { + this.startCommand(EzspFrameID.GET_BINDING_REMOTE_NODE_ID); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const nodeId: EmberNodeId = this.buffalo.readUInt16(); + + return nodeId; + } + + /** + * Set the node ID for the binding's destination. See getBindingRemoteNodeId for + * a description. + * @param index uint8_t The index of a binding table entry. + * @param The short ID of the destination node. + */ + async ezspSetBindingRemoteNodeId(index: number, nodeId: EmberNodeId): Promise { + this.startCommand(EzspFrameID.SET_BINDING_REMOTE_NODE_ID); + this.buffalo.writeUInt8(index); + this.buffalo.writeUInt16(nodeId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Callback + * The NCP used the external binding modification policy to decide how to handle + * a remote set binding request. The Host cannot change the current decision, + * but it can change the policy for future decisions using the setPolicy + * command. + * @param entry EmberBindingTableEntry * The requested binding. + * @param index uint8_t The index at which the binding was added. + * @param policyDecision EMBER_SUCCESS if the binding was added to the table and any other status if not. + */ + ezspRemoteSetBindingHandler(entry: EmberBindingTableEntry, index: number, policyDecision: EmberStatus): void { + debug(`ezspRemoteSetBindingHandler(): callback called with: [entry=${entry}], [index=${index}], ` + + `[policyDecision=${EmberStatus[policyDecision]}]`); + } + + /** + * Callback + * The NCP used the external binding modification policy to decide how to handle + * a remote delete binding request. The Host cannot change the current decision, + * but it can change the policy for future decisions using the setPolicy + * command. + * @param index uint8_t The index of the binding whose deletion was requested. + * @param policyDecision EMBER_SUCCESS if the binding was removed from the table and any other status if not. + */ + ezspRemoteDeleteBindingHandler(index: number, policyDecision: EmberStatus): void { + debug(`ezspRemoteDeleteBindingHandler(): callback called with: [index=${index}], [policyDecision=${EmberStatus[policyDecision]}]`); + } + + //----------------------------------------------------------------------------- + // Messaging Frames + //----------------------------------------------------------------------------- + + /** + * Returns the maximum size of the payload. The size depends on the security level in use. + * @returns uint8_t The maximum APS payload length. + */ + async ezspMaximumPayloadLength(): Promise { + this.startCommand(EzspFrameID.MAXIMUM_PAYLOAD_LENGTH); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const apsLength = this.buffalo.readUInt8(); + + return apsLength; + } + + /** + * Sends a unicast message as per the ZigBee specification. The message will + * arrive at its destination only if there is a known route to the destination + * node. Setting the ENABLE_ROUTE_DISCOVERY option will cause a route to be + * discovered if none is known. Setting the FORCE_ROUTE_DISCOVERY option will + * force route discovery. Routes to end-device children of the local node are + * always known. Setting the APS_RETRY option will cause the message to be + * retransmitted until either a matching acknowledgement is received or three + * transmissions have been made. Note: Using the FORCE_ROUTE_DISCOVERY option + * will cause the first transmission to be consumed by a route request as part + * of discovery, so the application payload of this packet will not reach its + * destination on the first attempt. If you want the packet to reach its + * destination, the APS_RETRY option must be set so that another attempt is made + * to transmit the message with its application payload after the route has been + * constructed. Note: When sending fragmented messages, the stack will only + * assign a new APS sequence number for the first fragment of the message (i.e., + * EMBER_APS_OPTION_FRAGMENT is set and the low-order byte of the groupId field + * in the APS frame is zero). For all subsequent fragments of the same message, + * the application must set the sequence number field in the APS frame to the + * sequence number assigned by the stack to the first fragment. + * @param type Specifies the outgoing message type. + * Must be one of EMBER_OUTGOING_DIRECT, EMBER_OUTGOING_VIA_ADDRESS_TABLE, or EMBER_OUTGOING_VIA_BINDING. + * @param indexOrDestination Depending on the type of addressing used, this is either the EmberNodeId of the destination, + * an index into the address table, or an index into the binding table. + * @param apsFrame EmberApsFrame * The APS frame which is to be added to the message. + * @param messageTag uint8_t A value chosen by the Host. This value is used in the ezspMessageSentHandler response to refer to this message. + * @param messageContents uint8_t * Content of the message. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns uint8_t * The sequence number that will be used when this message is transmitted. + */ + async ezspSendUnicast(type: EmberOutgoingMessageType, indexOrDestination: EmberNodeId, apsFrame: EmberApsFrame, messageTag: number, + messageContents: Buffer): Promise<[EmberStatus, apsSequence: number]> { + this.startCommand(EzspFrameID.SEND_UNICAST); + this.buffalo.writeUInt8(type); + this.buffalo.writeUInt16(indexOrDestination); + this.buffalo.writeEmberApsFrame(apsFrame); + this.buffalo.writeUInt8(messageTag); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const apsSequence = this.buffalo.readUInt8(); + + return [status, apsSequence]; + } + + /** + * Sends a broadcast message as per the ZigBee specification. + * @param destination The destination to which to send the broadcast. This must be one of the three ZigBee broadcast addresses. + * @param apsFrame EmberApsFrame * The APS frame for the message. + * @param radius uint8_t The message will be delivered to all nodes within radius hops of the sender. + * A radius of zero is converted to EMBER_MAX_HOPS. + * @param uint8_t A value chosen by the Host. This value is used in the ezspMessageSentHandler response to refer to this message. + * @param uint8_t * The broadcast message. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns uint8_t * The sequence number that will be used when this message is transmitted. + */ + async ezspSendBroadcast(destination: EmberNodeId, apsFrame: EmberApsFrame, radius: number, messageTag: number, messageContents: Buffer): + Promise<[EmberStatus, apsSequence: number]> { + this.startCommand(EzspFrameID.SEND_BROADCAST); + this.buffalo.writeUInt16(destination); + this.buffalo.writeEmberApsFrame(apsFrame); + this.buffalo.writeUInt8(radius); + this.buffalo.writeUInt8(messageTag); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const apsSequence = this.buffalo.readUInt8(); + + return [status, apsSequence]; + } + + /** + * Sends a proxied broadcast message as per the ZigBee specification. + * @param source The source from which to send the broadcast. + * @param destination The destination to which to send the broadcast. This must be one of the three ZigBee broadcast addresses. + * @param nwkSequence uint8_t The network sequence number for the broadcast. + * @param apsFrame EmberApsFrame * The APS frame for the message. + * @param radius uint8_t The message will be delivered to all nodes within radius hops of the sender. + * A radius of zero is converted to EMBER_MAX_HOPS. + * @param messageTag uint8_t A value chosen by the Host. This value is used in the ezspMessageSentHandler response to refer to this message. + * @param messageContents uint8_t * The broadcast message. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns uint8_t * The APS sequence number that will be used when this message is transmitted. + */ + async ezspProxyBroadcast(source: EmberNodeId, destination: EmberNodeId, nwkSequence: number, apsFrame: EmberApsFrame, radius: number, + messageTag: number, messageContents: Buffer): Promise<[EmberStatus, apsSequence: number]> { + this.startCommand(EzspFrameID.PROXY_BROADCAST); + this.buffalo.writeUInt16(source); + this.buffalo.writeUInt16(destination); + this.buffalo.writeUInt8(nwkSequence); + this.buffalo.writeEmberApsFrame(apsFrame); + this.buffalo.writeUInt8(radius); + this.buffalo.writeUInt8(messageTag); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const apsSequence = this.buffalo.readUInt8(); + + return [status, apsSequence]; + } + + /** + * Sends a multicast message to all endpoints that share a specific multicast ID + * and are within a specified number of hops of the sender. + * @param apsFrame EmberApsFrame * The APS frame for the message. The multicast will be sent to the groupId in this frame. + * @param hops uint8_t The message will be delivered to all nodes within this number of hops of the sender. + * A value of zero is converted to EMBER_MAX_HOPS. + * @param nonmemberRadius uint8_t The number of hops that the message will be forwarded by devices that are not members of the group. + * A value of 7 or greater is treated as infinite. + * @param messageTag uint8_t A value chosen by the Host. This value is used in the ezspMessageSentHandler response to refer to this message. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The multicast message. + * @returns An EmberStatus value. For any result other than EMBER_SUCCESS, the message will not be sent. + * - EMBER_SUCCESS - The message has been submitted for transmission. + * - EMBER_INVALID_BINDING_INDEX - The bindingTableIndex refers to a non-multicast binding. + * - EMBER_NETWORK_DOWN - The node is not part of a network. + * - EMBER_MESSAGE_TOO_LONG - The message is too large to fit in a MAC layer frame. + * - EMBER_NO_BUFFERS - The free packet buffer pool is empty. + * - EMBER_NETWORK_BUSY - Insufficient resources available in Network or MAC layers to send message. + * @returns uint8_t * The sequence number that will be used when this message is transmitted. + */ + async ezspSendMulticast(apsFrame: EmberApsFrame, hops: number, nonmemberRadius: number, messageTag: number, messageContents: Buffer): + Promise<[EmberStatus, apsSequence: number]> { + this.startCommand(EzspFrameID.SEND_MULTICAST); + this.buffalo.writeEmberApsFrame(apsFrame); + this.buffalo.writeUInt8(hops); + this.buffalo.writeUInt8(nonmemberRadius); + this.buffalo.writeUInt8(messageTag); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const apsSequence = this.buffalo.readUInt8(); + return [status, apsSequence]; + } + + /** + * Sends a multicast message to all endpoints that share a specific multicast ID + * and are within a specified number of hops of the sender. + * @param apsFrame EmberApsFrame * The APS frame for the message. The multicast will be sent to the groupId in this frame. + * @param hops uint8_t The message will be delivered to all nodes within this number of hops of the sender. + * A value of zero is converted to EMBER_MAX_HOPS. + * @param nonmemberRadius uint8_t The number of hops that the message will be forwarded by devices that are not members of the group. + * A value of 7 or greater is treated as infinite. + * @param alias uint16_t The alias source address + * @param nwkSequence uint8_t the alias sequence number + * @param messageTag uint8_t A value chosen by the Host. This value is used in the ezspMessageSentHandler response to refer to this message. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The multicast message. + * @returns An EmberStatus value. For any result other than EMBER_SUCCESS, the + * message will not be sent. EMBER_SUCCESS - The message has been submitted for + * transmission. EMBER_INVALID_BINDING_INDEX - The bindingTableIndex refers to a + * non-multicast binding. EMBER_NETWORK_DOWN - The node is not part of a + * network. EMBER_MESSAGE_TOO_LONG - The message is too large to fit in a MAC + * layer frame. EMBER_NO_BUFFERS - The free packet buffer pool is empty. + * EMBER_NETWORK_BUSY - Insufficient resources available in Network or MAC + * layers to send message. + * @returns The sequence number that will be used when this message is transmitted. + */ + async ezspSendMulticastWithAlias(apsFrame: EmberApsFrame, hops: number, nonmemberRadius: number, alias: number, nwkSequence: number, + messageTag: number, messageContents: Buffer): Promise<[EmberStatus, apsSequence: number]> { + this.startCommand(EzspFrameID.SEND_MULTICAST_WITH_ALIAS); + this.buffalo.writeEmberApsFrame(apsFrame); + this.buffalo.writeUInt8(hops); + this.buffalo.writeUInt8(nonmemberRadius); + this.buffalo.writeUInt16(alias); + this.buffalo.writeUInt8(nwkSequence); + this.buffalo.writeUInt8(messageTag); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const apsSequence = this.buffalo.readUInt8(); + return [status, apsSequence]; + } + + /** + * Sends a reply to a received unicast message. The incomingMessageHandler + * callback for the unicast being replied to supplies the values for all the + * parameters except the reply itself. + * @param sender Value supplied by incoming unicast. + * @param apsFrame EmberApsFrame * Value supplied by incoming unicast. + * @param uint8_t The length of the messageContents parameter in bytes. + * @param uint8_t * The reply message. + * @returns An EmberStatus value. + * - EMBER_INVALID_CALL - The EZSP_UNICAST_REPLIES_POLICY is set to EZSP_HOST_WILL_NOT_SUPPLY_REPLY. + * This means the NCP will automatically send an empty reply. The Host must change + * the policy to EZSP_HOST_WILL_SUPPLY_REPLY before it can supply the reply. + * There is one exception to this rule: In the case of responses to message + * fragments, the host must call sendReply when a message fragment is received. + * In this case, the policy set on the NCP does not matter. The NCP expects a + * sendReply call from the Host for message fragments regardless of the current + * policy settings. + * - EMBER_NO_BUFFERS - Not enough memory was available to send the reply. + * - EMBER_NETWORK_BUSY - Either no route or insufficient resources available. + * - EMBER_SUCCESS - The reply was successfully queued for transmission. + */ + async ezspSendReply(sender: EmberNodeId, apsFrame: EmberApsFrame, messageContents: Buffer): + Promise { + this.startCommand(EzspFrameID.SEND_REPLY); + this.buffalo.writeUInt16(sender); + this.buffalo.writeEmberApsFrame(apsFrame); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback indicating the stack has completed sending a message. + * @param type The type of message sent. + * @param indexOrDestination uint16_t The destination to which the message was sent, for direct unicasts, + * or the address table or binding index for other unicasts. The value is unspecified for multicasts and broadcasts. + * @param apsFrame EmberApsFrame * The APS frame for the message. + * @param messageTag uint8_t The value supplied by the Host in the ezspSendUnicast, ezspSendBroadcast or ezspSendMulticast command. + * @param status An EmberStatus value of EMBER_SUCCESS if an ACK was received from the destination + * or EMBER_DELIVERY_FAILED if no ACK was received. + * @param messageContents uint8_t * The unicast message supplied by the Host. The message contents are only included here if the decision + * for the messageContentsInCallback policy is messageTagAndContentsInCallback. + */ + ezspMessageSentHandler(type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number, + status: EmberStatus, messageContents: Buffer): void { + debug(`ezspMessageSentHandler(): callback called with: [type=${EmberOutgoingMessageType[type]}], [indexOrDestination=${indexOrDestination}], ` + + `[apsFrame=${JSON.stringify(apsFrame)}], [messageTag=${messageTag}], [status=${EmberStatus[status]}], ` + + `[messageContents=${messageContents.toString('hex')}]`); + + if (status === EmberStatus.DELIVERY_FAILED) { + // no ACK was received from the destination + this.emit(EzspEvents.MESSAGE_SENT_DELIVERY_FAILED, type, indexOrDestination, apsFrame, messageTag); + } + // shouldn't be any other status except SUCCESS... no use for it atm + } + + /** + * Sends a route request packet that creates routes from every node in the + * network back to this node. This function should be called by an application + * that wishes to communicate with many nodes, for example, a gateway, central + * monitor, or controller. A device using this function was referred to as an + * 'aggregator' in EmberZNet 2.x and earlier, and is referred to as a + * 'concentrator' in the ZigBee specification and EmberZNet 3. This function + * enables large scale networks, because the other devices do not have to + * individually perform bandwidth-intensive route discoveries. Instead, when a + * remote node sends an APS unicast to a concentrator, its network layer + * automatically delivers a special route record packet first, which lists the + * network ids of all the intermediate relays. The concentrator can then use + * source routing to send outbound APS unicasts. (A source routed message is one + * in which the entire route is listed in the network layer header.) This allows + * the concentrator to communicate with thousands of devices without requiring + * large route tables on neighboring nodes. This function is only available in + * ZigBee Pro (stack profile 2), and cannot be called on end devices. Any router + * can be a concentrator (not just the coordinator), and there can be multiple + * concentrators on a network. Note that a concentrator does not automatically + * obtain routes to all network nodes after calling this function. Remote + * applications must first initiate an inbound APS unicast. Many-to-one routes + * are not repaired automatically. Instead, the concentrator application must + * call this function to rediscover the routes as necessary, for example, upon + * failure of a retried APS message. The reason for this is that there is no + * scalable one-size-fits-all route repair strategy. A common and recommended + * strategy is for the concentrator application to refresh the routes by calling + * this function periodically. + * @param concentratorType uint16_t Must be either EMBER_HIGH_RAM_CONCENTRATOR or EMBER_LOW_RAM_CONCENTRATOR. + * The former is used when the caller has enough memory to store source routes for the whole network. + * In that case, remote nodes stop sending route records once the concentrator has successfully received one. + * The latter is used when the concentrator has insufficient RAM to store all outbound source routes. + * In that case, route records are sent to the concentrator prior to every inbound APS unicast. + * @param radius uint8_t The maximum number of hops the route request will be relayed. A radius of zero is converted to EMBER_MAX_HOPS + * @returns EMBER_SUCCESS if the route request was successfully submitted to the + * transmit queue, and EMBER_ERR_FATAL otherwise. + */ + async ezspSendManyToOneRouteRequest(concentratorType: number, radius: number): Promise { + this.startCommand(EzspFrameID.SEND_MANY_TO_ONE_ROUTE_REQUEST); + this.buffalo.writeUInt16(concentratorType); + this.buffalo.writeUInt8(radius); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Periodically request any pending data from our parent. Setting interval to 0 + * or units to EMBER_EVENT_INACTIVE will generate a single poll. + * @param interval uint16_t The time between polls. Note that the timer clock is free running and is not synchronized with this command. + * This means that the time will be between interval and (interval - 1). The maximum interval is 32767. + * @param units The units for interval. + * @param failureLimit uint8_t The number of poll failures that will be tolerated before a pollCompleteHandler callback is generated. + * A value of zero will result in a callback for every poll. Any status value apart from EMBER_SUCCESS + * and EMBER_MAC_NO_DATA is counted as a failure. + * @returns The result of sending the first poll. + */ + async ezspPollForData(interval: number, units: EmberEventUnits, failureLimit: number): Promise { + this.startCommand(EzspFrameID.POLL_FOR_DATA); + this.buffalo.writeUInt16(interval); + this.buffalo.writeUInt8(units); + this.buffalo.writeUInt8(failureLimit); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * Indicates the result of a data poll to the parent of the local node. + * @param status An EmberStatus value: + * - EMBER_SUCCESS - Data was received in response to the poll. + * - EMBER_MAC_NO_DATA - No data was pending. + * - EMBER_DELIVERY_FAILED - The poll message could not be sent. + * - EMBER_MAC_NO_ACK_RECEIVED - The poll message was sent but not acknowledged by the parent. + */ + ezspPollCompleteHandler(status: EmberStatus): void { + debug(`ezspPollCompleteHandler(): callback called with: [status=${EmberStatus[status]}]`); + } + + /** + * Callback + * Indicates that the local node received a data poll from a child. + * @param childId The node ID of the child that is requesting data. + * @param transmitExpected True if transmit is expected, false otherwise. + */ + ezspPollHandler(childId: EmberNodeId, transmitExpected: boolean): void { + debug(`ezspPollHandler(): callback called with: [childId=${childId}], [transmitExpected=${transmitExpected}]`); + } + + /** + * Callback + * A callback indicating a message has been received containing the EUI64 of the + * sender. This callback is called immediately before the incomingMessageHandler + * callback. It is not called if the incoming message did not contain the EUI64 + * of the sender. + * @param senderEui64 The EUI64 of the sender + */ + ezspIncomingSenderEui64Handler(senderEui64: EmberEUI64): void { + debug(`ezspIncomingSenderEui64Handler(): callback called with: [senderEui64=${senderEui64}]`); + } + + /** + * Callback + * A callback indicating a message has been received. + * @param type The type of the incoming message. One of the following: EMBER_INCOMING_UNICAST, EMBER_INCOMING_UNICAST_REPLY, + * EMBER_INCOMING_MULTICAST, EMBER_INCOMING_MULTICAST_LOOPBACK, EMBER_INCOMING_BROADCAST, EMBER_INCOMING_BROADCAST_LOOPBACK + * @param apsFrame EmberApsFrame * The APS frame from the incoming message. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the message. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during the reception. + * @param sender The sender of the message. + * @param bindingIndex uint8_t The index of a binding that matches the message or 0xFF if there is no matching binding. + * @param addressIndex uint8_t The index of the entry in the address table that matches the sender of the message + * or 0xFF if there is no matching entry. + * @param messageContents uint8_t * The incoming message. + */ + ezspIncomingMessageHandler(type: EmberIncomingMessageType, apsFrame: EmberApsFrame, lastHopLqi: number, lastHopRssi: number, + sender: EmberNodeId, bindingIndex: number, addressIndex: number, messageContents: Buffer): void { + debug(`ezspIncomingMessageHandler(): callback called with: [type=${EmberIncomingMessageType[type]}], [apsFrame=${JSON.stringify(apsFrame)}], ` + + `[lastHopLqi=${lastHopLqi}], [lastHopRssi=${lastHopRssi}], [sender=${sender}], [bindingIndex=${bindingIndex}], ` + + `[addressIndex=${addressIndex}], [messageContents=${messageContents.toString('hex')}]`); + // from protocol\zigbee\app\util\zigbee-framework\zigbee-device-host.h + if (apsFrame.profileId === ZDO_PROFILE_ID) { + const zdoBuffalo = new EzspBuffalo(messageContents, ZDO_MESSAGE_OVERHEAD);// set pos to skip `transaction sequence number` + + switch (apsFrame.clusterId) { + case IEEE_ADDRESS_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO IEEE_ADDRESS_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + // 64-bit address for the remote device + const eui64 = zdoBuffalo.readIeeeAddr(); + // 16-bit address for the remote device + const nodeId = zdoBuffalo.readUInt16(); + // valid range 0x00-0xFF, count of the number of 16-bit shot addresses to follow. + // if the RequestType in the request is Extended Response, and there are no assoc. devices on the remote device, should be 0 + // if an error occurs or the RequestType in the request is for a Single Device Response, + // this fiel is not included in the frame. + let assocDevCount: number = 0; + // 0x00-0xFF, starting index into the list of assoc. devices for this report. + // if the RequestType in the request is Extended Response, and there are no assoc. devices on the remote device, + // this field is not included in the frame, same if error, or RequestType is for Single Device Response + let startIndex: number = 0; + // list of 0x0000-0xFFFF, one corresponds to each assoc. device to the remote device. + // if the RequestType in the request is Extended Response, and there are no assoc. devices on the remote device, + // this field is not included in the frame, same if error, or RequestType is for Single Device Response + let assocDevList: number[] = []; + + if (zdoBuffalo.isMore()) { + assocDevCount = zdoBuffalo.readUInt8(); + startIndex = zdoBuffalo.readUInt8(); + + assocDevList = zdoBuffalo.readListUInt16({length: assocDevCount}); + } + + debug(`<=== [ZDO IEEE_ADDRESS_RESPONSE status=${status} eui64=${eui64} nodeId=${nodeId} startIndex=${startIndex} ` + + `assocDevList=${assocDevList}]`); + debug(`<=== [ZDO IEEE_ADDRESS_RESPONSE] Support not implemented upstream`); + + const payload: IEEEAddressResponsePayload = {eui64, nodeId, assocDevList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case NETWORK_ADDRESS_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO NETWORK_ADDRESS_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + // 64-bit address for the remote device + const eui64 = zdoBuffalo.readIeeeAddr(); + // 16-bit address for the remote device + const nodeId = zdoBuffalo.readUInt16(); + // valid range 0x00-0xFF, count of the number of 16-bit shot addresses to follow. + // if the RequestType in the request is Extended Response, and there are no assoc. devices on the remote device, should be 0 + // if an error occurs or the RequestType in the request is for a Single Device Response, + // this fiel is not included in the frame. + let assocDevCount: number = 0; + // 0x00-0xFF, starting index into the list of assoc. devices for this report. + // if the RequestType in the request is Extended Response, and there are no assoc. devices on the remote device, + // this field is not included in the frame, same if error, or RequestType is for Single Device Response + let startIndex: number = 0; + // list of 0x0000-0xFFFF, one corresponds to each assoc. device to the remote device. + // if the RequestType in the request is Extended Response, and there are no assoc. devices on the remote device, + // this field is not included in the frame, same if error, or RequestType is for Single Device Response + let assocDevList: number[] = []; + + if (zdoBuffalo.isMore()) { + assocDevCount = zdoBuffalo.readUInt8(); + startIndex = zdoBuffalo.readUInt8(); + + assocDevList = zdoBuffalo.readListUInt16({length: assocDevCount}); + } + + debug(`<=== [ZDO NETWORK_ADDRESS_RESPONSE status=${status} eui64=${eui64} nodeId=${nodeId} startIndex=${startIndex} ` + + `assocDevList=${assocDevList}]`); + + const payload: NetworkAddressResponsePayload = {eui64, nodeId, assocDevList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case MATCH_DESCRIPTORS_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO MATCH_DESCRIPTORS_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + const nodeId = zdoBuffalo.readUInt16(); + const endpointCount = zdoBuffalo.readUInt8(); + const endpointList = zdoBuffalo.readListUInt8({length: endpointCount}); + + debug(`<=== [ZDO MATCH_DESCRIPTORS_RESPONSE status=${status} nodeId=${nodeId} endpointList=${endpointList}]`); + debug(`<=== [ZDO MATCH_DESCRIPTORS_RESPONSE] Support not implemented upstream`); + + const payload: MatchDescriptorsResponsePayload = {nodeId, endpointList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case SIMPLE_DESCRIPTOR_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO SIMPLE_DESCRIPTOR_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + const nodeId = zdoBuffalo.readUInt16(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const length = zdoBuffalo.readUInt8(); + const endpoint = zdoBuffalo.readUInt8(); + const profileId = zdoBuffalo.readUInt16(); + const deviceId = zdoBuffalo.readUInt16(); + // values 0000-1111, others reserved + const deviceVersion = zdoBuffalo.readUInt8(); + const inClusterCount = zdoBuffalo.readUInt8(); + const inClusterList = zdoBuffalo.readListUInt16({length: inClusterCount}); + const outClusterCount = zdoBuffalo.readUInt8(); + const outClusterList = zdoBuffalo.readListUInt16({length: outClusterCount}); + + debug(`<=== [ZDO SIMPLE_DESCRIPTOR_RESPONSE status=${status} nodeId=${nodeId} endpoint=${endpoint} profileId=${profileId} ` + + `deviceId=${deviceId} deviceVersion=${deviceVersion} inClusterList=${inClusterList} outClusterList=${outClusterList}]`); + + const payload: SimpleDescriptorResponsePayload = { + nodeId, + endpoint, + profileId, + deviceId, + inClusterList, + outClusterList, + }; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case NODE_DESCRIPTOR_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO NODE_DESCRIPTOR_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + const nodeId = zdoBuffalo.readUInt16(); + // in bits: [logical type: 3] [complex description available: 1] [user descriptor available: 1] [reserved/unused: 3] + const nodeDescByte1 = zdoBuffalo.readUInt8(); + // 000 == Zigbee Coordinator, 001 == Zigbee Router, 010 === Zigbee End Device, 011-111 === Reserved + const logicalType = (nodeDescByte1 & 0x07); + // 0 == not avail + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const complexDescAvail = (nodeDescByte1 & 0x08) >> 3; + // 0 == not avai + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const userDescAvail = (nodeDescByte1 & 0x10) >> 4; + // in bits: [aps flags: 3] [frequency band: 5] + const nodeDescByte2 = zdoBuffalo.readUInt8(); + // currently not supported, should be zero + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const apsFlags = (nodeDescByte2 & 0x07); + // 0 == 868 – 868.6 MHz BPSK, 1 == Reserved, 2 == 902 – 928 MHz BPSK, + // 3 == 2400 – 2483.5 MHz, 4 == European FSK sub-GHz bands: (863-876MHz and 915-921MHz) + const freqBand = (nodeDescByte2 & 0xF8) >> 3; + /** @see MACCapabilityFlags */ + const macCapFlags = zdoBuffalo.readUInt8(); + // allocated by Zigbee Alliance + const manufacturerCode = zdoBuffalo.readUInt16(); + // valid range 0x00-0x7F, max size in octets of the network sub-layer data unit (NSDU) for node. + // max size of data or commands passed to or from the app by the app support sub-layer before any fragmentation or re-assembly. + // can be used as a high-level indication for network management + const maxBufSize = zdoBuffalo.readUInt8(); + // valid range 0x0000-0x7FFF, max size in octets of the application sub-layer data unit (ASDU) + // that can be transferred to this node in one single message transfer. + // can exceed max buf size through use of fragmentation. + const maxIncTxSize = zdoBuffalo.readUInt16(); + // in bits: + // [primary trust center: 1] + // [backup trust center: 1] + // [primary binding table cache: 1] + // [backup binding table cache: 1] + // [primary discovery cache: 1] + // [backup discovery cache: 1] + // [network manager: 1] + // [reserved: 2] + // [stack compliance revision: 7] + const serverMask = zdoBuffalo.readUInt16(); + // revision of the Zigbee Pro Core specs implemented (always zeroed out prior to revision 21 that added these fields to the spec) + const stackRevision = (serverMask & 0xFE00) >> 9; + // valid range 0x0000-0x7FFF, max size in octets of the application sub-layer data uni (ASDU) + // that can be transferred from this node in one single message transfer. + // can exceed max buf size through use of fragmentation. + const maxOutTxSize = zdoBuffalo.readUInt16(); + // in bits: [extended active endpoint list available: 1] [extended simple descriptor list available: 1] [reserved: 6] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const descCapFlags = zdoBuffalo.readUInt8(); + + debug(`<=== [ZDO NODE_DESCRIPTOR_RESPONSE status=${status} nodeId=${nodeId} logicalType=${logicalType} ` + + `freqBand=${freqBand} macCapFlags=${byteToBits(macCapFlags)} manufacturerCode=${manufacturerCode} maxBufSize=${maxBufSize} ` + + `maxIncTxSize=${maxIncTxSize} stackRevision=${stackRevision} maxOutTxSize=${maxOutTxSize}]`); + + const payload: NodeDescriptorResponsePayload = { + nodeId, + logicalType, + macCapFlags: getMacCapFlags(macCapFlags), + manufacturerCode, + stackRevision + }; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case POWER_DESCRIPTOR_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO POWER_DESCRIPTOR_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + const nodeId = zdoBuffalo.readUInt16(); + // mode: 0000 == receiver sync'ed with receiver on when idle subfield of the node descriptor + // 0001 == receiver comes on periodically as defined by the node power descriptor + // 0010 == receiver comes on when stimulated, for example, by a user pressing a button + // 0011-1111 reserved + // source (bits): 0 == constants (mains) power + // 1 == rechargeable battery + // 2 == disposable battery + // 3 == reserved + const [currentPowerMode, availPowerSources] = lowHighBits(zdoBuffalo.readUInt8()); + // source (bits): 0 == constants (mains) power + // 1 == rechargeable battery + // 2 == disposable battery + // 3 == reserved + // level: 0000 == critical + // 0100 == 33% + // 1000 == 66% + // 1100 == 100% + // All other values reserved + const [currentPowerSource, currentPowerSourceLevel] = lowHighBits(zdoBuffalo.readUInt8()); + + debug(`<=== [ZDO POWER_DESCRIPTOR_RESPONSE status=${status} nodeId=${nodeId} currentPowerMode=${currentPowerMode} ` + + `availPowerSources=${availPowerSources} currentPowerSource=${currentPowerSource} ` + + `currentPowerSourceLevel=${currentPowerSourceLevel}]`); + debug(`<=== [ZDO POWER_DESCRIPTOR_RESPONSE] Support not implemented upstream`); + + const payload: PowerDescriptorResponsePayload = { + nodeId, + currentPowerMode, + availPowerSources, + currentPowerSource, + currentPowerSourceLevel + }; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case ACTIVE_ENDPOINTS_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO ACTIVE_ENDPOINTS_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + const nodeId = zdoBuffalo.readUInt16(); + const endpointCount = zdoBuffalo.readUInt8(); + const endpointList = zdoBuffalo.readListUInt8({length: endpointCount}); + + debug(`<=== [ZDO ACTIVE_ENDPOINTS_RESPONSE status=${status} nodeId=${nodeId} endpointList=${endpointList}]`); + + const payload: ActiveEndpointsResponsePayload = {nodeId, endpointList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case LQI_TABLE_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO LQI_TABLE_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + // 0x00-0xFF, total number of neighbor table entries within the remote device + const neighborTableEntries = zdoBuffalo.readUInt8(); + // 0x00-0xFF, starting index within the neighbor table to begin reporting for the NeighborTableList + const startIndex = zdoBuffalo.readUInt8(); + // 0x00-0x02, number of neighbor table entries included within NeighborTableList + const entryCount = zdoBuffalo.readUInt8(); + // list of descriptors, beginning with the {startIndex} element and continuing for {entryCount} + // of the elements in the remote device's neighbor table, including the device address and assoc. LQI + const entryList: ZDOLQITableEntry[] = []; + + for (let i = 0; i < entryCount; i++) { + const extendedPanId = zdoBuffalo.readListUInt8({length: EXTENDED_PAN_ID_SIZE}); + const eui64 = zdoBuffalo.readIeeeAddr(); + const nodeId = zdoBuffalo.readUInt16(); + const deviceTypeByte = zdoBuffalo.readUInt8(); + const permitJoiningByte = zdoBuffalo.readUInt8(); + const depth = zdoBuffalo.readUInt8(); + const lqi = zdoBuffalo.readUInt8(); + + entryList.push({ + extendedPanId, + eui64, + nodeId, + deviceType: deviceTypeByte & 0x03, + rxOnWhenIdle: (deviceTypeByte & 0x0C) >> 2, + relationship: (deviceTypeByte & 0x70) >> 4, + reserved1: (deviceTypeByte & 0x10) >> 7, + permitJoining: permitJoiningByte & 0x03, + reserved2: (permitJoiningByte & 0xFC) >> 2, + depth, + lqi, + }); + } + + debug(`<=== [ZDO LQI_TABLE_RESPONSE status=${status} neighborTableEntries=${neighborTableEntries} startIndex=${startIndex} ` + + `entryCount=${entryCount} entryList=${JSON.stringify(entryList)}]`); + + const payload: LQITableResponsePayload = {neighborTableEntries, entryList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case ROUTING_TABLE_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO ROUTING_TABLE_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + // 0x00-0xFF, total number of routing table entries within the remote device + const routingTableEntries = zdoBuffalo.readUInt8(); + // 0x00-0xFF, starting index within the routing table to begin reporting for the RoutingTableList + const startIndex = zdoBuffalo.readUInt8(); + // 0x00-0xFF, number of routing table entries included within RoutingTableList + const entryCount = zdoBuffalo.readUInt8(); + // list of descriptors, beginning with the {startIndex} element and continuing for {entryCount} + // of the elements in the remote device's routing table + const entryList: ZDORoutingTableEntry[] = []; + + for (let i = 0; i < entryCount; i++) { + const destinationAddress = zdoBuffalo.readUInt16(); + const statusByte = zdoBuffalo.readUInt8(); + const nextHopAddress = zdoBuffalo.readUInt16(); + + entryList.push({ + destinationAddress, + status: statusByte & 0x07, + memoryConstrained: (statusByte & 0x08) >> 3, + manyToOne: (statusByte & 0x10) >> 4, + routeRecordRequired: (statusByte & 0x20) >> 5, + reserved: (statusByte & 0xC0) >> 6, + nextHopAddress, + }); + } + + debug(`<=== [ZDO ROUTING_TABLE_RESPONSE status=${status} routingTableEntries=${routingTableEntries} startIndex=${startIndex} ` + + `entryCount=${entryCount} entryList=${JSON.stringify(entryList)}]`); + + const payload: RoutingTableResponsePayload = {routingTableEntries, entryList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case BINDING_TABLE_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + if (status !== EmberZdoStatus.ZDP_SUCCESS) { + debug(`<=== [ZDO BINDING_TABLE_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, null); + } else { + const bindingTableEntries = zdoBuffalo.readUInt8(); + const startIndex = zdoBuffalo.readUInt8(); + const entryCount = zdoBuffalo.readUInt8(); + const entryList: ZDOBindingTableEntry[] = []; + + for (let i = 0; i < entryCount; i++) { + const sourceEui64 = zdoBuffalo.readIeeeAddr(); + const sourceEndpoint = zdoBuffalo.readUInt8(); + const clusterId = zdoBuffalo.readUInt16(); + const destAddrMode = zdoBuffalo.readUInt8(); + const dest = (destAddrMode === 0x01) ? zdoBuffalo.readUInt16() : ((destAddrMode === 0x03) ? zdoBuffalo.readIeeeAddr() : null); + const destEndpoint = (destAddrMode === 0x03) ? zdoBuffalo.readUInt8() : null; + + entryList.push({ + sourceEui64, + sourceEndpoint, + clusterId, + destAddrMode, + dest, + destEndpoint, + }); + } + + debug(`<=== [ZDO BINDING_TABLE_RESPONSE status=${status} bindingTableEntries=${bindingTableEntries} startIndex=${startIndex} ` + + `entryCount=${entryCount} entryList=${JSON.stringify(entryList)}]`); + debug(`<=== [ZDO BINDING_TABLE_RESPONSE] Support not implemented upstream`); + + const payload: BindingTableResponsePayload = {bindingTableEntries, entryList}; + + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame, payload); + } + break; + } + case BIND_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + debug(`<=== [ZDO BIND_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame); + break; + } + case UNBIND_RESPONSE:{ + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + debug(`<=== [ZDO UNBIND_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame); + break; + } + case LEAVE_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + debug(`<=== [ZDO LEAVE_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame); + break; + } + case PERMIT_JOINING_RESPONSE: { + const status: EmberZdoStatus = zdoBuffalo.readUInt8(); + + debug(`<=== [ZDO PERMIT_JOINING_RESPONSE status=${status}]`); + this.emit(EzspEvents.ZDO_RESPONSE, status, sender, apsFrame); + break; + } + case END_DEVICE_ANNOUNCE: { + const nodeId = zdoBuffalo.readUInt16(); + const eui64 = zdoBuffalo.readIeeeAddr(); + /** @see MACCapabilityFlags */ + const capabilities = zdoBuffalo.readUInt8(); + + debug(`<=== [ZDO END_DEVICE_ANNOUNCE nodeId=${nodeId} eui64=${eui64} capabilities=${byteToBits(capabilities)}]`); + + const payload: EndDeviceAnnouncePayload = {nodeId, eui64, capabilities: getMacCapFlags(capabilities)}; + + // this one gets its own event since its purpose is to pass an event up to Z2M + this.emit(EzspEvents.END_DEVICE_ANNOUNCE, sender, apsFrame, payload); + break; + } + default: { + console.log(`<=== [ZDO clusterId=${apsFrame.clusterId}] Support not implemented`); + break; + } + } + } else if (apsFrame.profileId === HA_PROFILE_ID || apsFrame.profileId === WILDCARD_PROFILE_ID) { + this.emit(EzspEvents.INCOMING_MESSAGE, type, apsFrame, lastHopLqi, sender, messageContents); + } else if (apsFrame.profileId === GP_PROFILE_ID) { + // only broadcast loopback in here + } + } + + /** + * Sets source route discovery(MTORR) mode to on, off, reschedule + * @param mode uint8_t Source route discovery mode: off:0, on:1, reschedule:2 + * @returns uint32_t Remaining time(ms) until next MTORR broadcast if the mode is on, MAX_INT32U_VALUE if the mode is off + */ + async ezspSetSourceRouteDiscoveryMode(mode: EmberSourceRouteDiscoveryMode): Promise { + this.startCommand(EzspFrameID.SET_SOURCE_ROUTE_DISCOVERY_MODE); + this.buffalo.writeUInt8(mode); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const remainingTime = this.buffalo.readUInt32(); + + return remainingTime; + } + + /** + * Callback + * A callback indicating that a many-to-one route to the concentrator with the given short and long id is available for use. + * @param EmberNodeId The short id of the concentrator. + * @param longId The EUI64 of the concentrator. + * @param cost uint8_t The path cost to the concentrator. The cost may decrease as additional route request packets + * for this discovery arrive, but the callback is made only once. + */ + ezspIncomingManyToOneRouteRequestHandler(source: EmberNodeId, longId: EmberEUI64, cost: number): void { + debug(`ezspIncomingManyToOneRouteRequestHandler(): callback called with: [source=${source}], [longId=${longId}], [cost=${cost}]`); + } + + /** + * Callback + * A callback invoked when a route error message is received. + * The error indicates that a problem routing to or from the target node was encountered. + * + * A status of ::EMBER_SOURCE_ROUTE_FAILURE indicates that a source-routed unicast sent from this node encountered a broken link. + * Note that this case occurs only if this node is a concentrator using many-to-one routing for inbound messages and source-routing for + * outbound messages. The node prior to the broken link generated the route error message and returned it to us along the many-to-one route. + * + * A status of ::EMBER_MANY_TO_ONE_ROUTE_FAILURE also occurs only if the local device is a concentrator, and indicates that a unicast sent + * to the local device along a many-to-one route encountered a broken link. The node prior to the broken link generated the route error + * message and forwarded it to the local device via a randomly chosen neighbor, taking advantage of the many-to-one nature of the route. + * + * A status of ::EMBER_MAC_INDIRECT_TIMEOUT indicates that a message sent to the target end device could not be delivered by the parent + * because the indirect transaction timer expired. Upon receipt of the route error, the stack sets the extended timeout for the target node + * in the address table, if present. It then calls this handler to indicate receipt of the error. + * + * Note that if the original unicast data message is sent using the ::EMBER_APS_OPTION_RETRY option, a new route error message is generated + * for each failed retry. Therefore, it is not unusual to receive three route error messages in succession for a single failed retried APS + * unicast. On the other hand, it is also not guaranteed that any route error messages will be delivered successfully at all. + * The only sure way to detect a route failure is to use retried APS messages and to check the status of the ::emberMessageSentHandler(). + * + * @param status ::EMBER_SOURCE_ROUTE_FAILURE, ::EMBER_MANY_TO_ONE_ROUTE_FAILURE, ::EMBER_MAC_INDIRECT_TIMEOUT + * @param target The short id of the remote node. + */ + ezspIncomingRouteErrorHandler(status: EmberStatus, target: EmberNodeId): void { + debug(`ezspIncomingRouteErrorHandler(): callback called with: [status=${EmberStatus[status]}], [target=${target}]`); + // NOTE: This can trigger immediately after removal of a device with status MAC_INDIRECT_TIMEOUT + } + + /** + * Callback + * A callback invoked when a network status/route error message is received. + * The error indicates that there was a problem sending/receiving messages from the target node. + * + * Note: Network analyzer may flag this message as "route error" which is the old name for the "network status" command. + * + * This handler is a superset of ezspIncomingRouteErrorHandler. The old API was only invoking the handler for a couple of the possible + * error codes and these were being translated into EmberStatus. + * + * @param errorCode uint8_t One byte over-the-air error code from network status message + * @param target The short ID of the remote node + */ + ezspIncomingNetworkStatusHandler(errorCode: EmberStackError, target: EmberNodeId): void { + debug(`ezspIncomingNetworkStatusHandler(): callback called with: [errorCode=${EmberStackError[errorCode]}], [target=${target}]`); + console.log(`Received network/route error ${EmberStackError[errorCode]} for "${target}".`); + } + + /** + * Callback + * Reports the arrival of a route record command frame. + * @param EmberNodeId The source of the route record. + * @param EmberEUI64 The EUI64 of the source. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the route record. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during the reception. + * @param uint8_t The number of relays in relayList. + * @param relayList uint8_t * The route record. Each relay in the list is an uint16_t node ID. + * The list is passed as uint8_t * to avoid alignment problems. + */ + ezspIncomingRouteRecordHandler(source: EmberNodeId, sourceEui: EmberEUI64, lastHopLqi: number, + lastHopRssi: number, relayCount: number, relayList: number[]): void { + debug(`ezspIncomingRouteRecordHandler(): callback called with: [source=${source}], [sourceEui=${sourceEui}], ` + + `[lastHopLqi=${lastHopLqi}], [lastHopRssi=${lastHopRssi}], [relayCount=${relayCount}], [relayList=${relayList}]`); + // XXX: could at least trigger a `Events.lastSeenChanged` but this is not currently being listened to at the adapter level + } + + /** + * Supply a source route for the next outgoing message. + * @param destination The destination of the source route. + * @param relayList uint16_t * The source route. + * @returns EMBER_SUCCESS if the source route was successfully stored, and + * EMBER_NO_BUFFERS otherwise. + */ + async ezspSetSourceRoute(destination: EmberNodeId, relayList: number[]): Promise { + this.startCommand(EzspFrameID.SET_SOURCE_ROUTE); + this.buffalo.writeUInt16(destination); + this.buffalo.writeUInt8(relayList.length); + this.buffalo.writeListUInt16(relayList); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Send the network key to a destination. + * @param targetShort The destination node of the key. + * @param targetLong The long address of the destination node. + * @param parentShortId The parent node of the destination node. + * @returns EMBER_SUCCESS if send was successful + */ + async ezspUnicastCurrentNetworkKey(targetShort: EmberNodeId, targetLong: EmberEUI64, parentShortId: EmberNodeId): Promise { + this.startCommand(EzspFrameID.UNICAST_CURRENT_NETWORK_KEY); + this.buffalo.writeUInt16(targetShort); + this.buffalo.writeIeeeAddr(targetLong); + this.buffalo.writeUInt16(parentShortId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Indicates whether any messages are currently being sent using this address + * table entry. Note that this function does not indicate whether the address + * table entry is unused. To determine whether an address table entry is unused, + * check the remote node ID. The remote node ID will have the value + * EMBER_TABLE_ENTRY_UNUSED_NODE_ID when the address table entry is not in use. + * @param uint8_tThe index of an address table entry. + * @returns True if the address table entry is active, false otherwise. + */ + async ezspAddressTableEntryIsActive(addressTableIndex: number): Promise { + this.startCommand(EzspFrameID.ADDRESS_TABLE_ENTRY_IS_ACTIVE); + this.buffalo.writeUInt8(addressTableIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const active = this.buffalo.readUInt8() === 1 ? true : false; + + return active; + } + + /** + * Sets the EUI64 of an address table entry. This function will also check other + * address table entries, the child table and the neighbor table to see if the + * node ID for the given EUI64 is already known. If known then this function + * will also set node ID. If not known it will set the node ID to + * EMBER_UNKNOWN_NODE_ID. + * @param addressTableIndex uint8_t The index of an address table entry. + * @param eui64 The EUI64 to use for the address table entry. + * @returns + * - EMBER_SUCCESS if the EUI64 was successfully set, + * - EMBER_ADDRESS_TABLE_ENTRY_IS_ACTIVE otherwise. + */ + async ezspSetAddressTableRemoteEui64(addressTableIndex: number, eui64: EmberEUI64): Promise { + this.startCommand(EzspFrameID.SET_ADDRESS_TABLE_REMOTE_EUI64); + this.buffalo.writeUInt8(addressTableIndex); + this.buffalo.writeIeeeAddr(eui64); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the short ID of an address table entry. Usually the application will not + * need to set the short ID in the address table. Once the remote EUI64 is set + * the stack is capable of figuring out the short ID on its own. However, in + * cases where the application does set the short ID, the application must set + * the remote EUI64 prior to setting the short ID. + * @param addressTableIndex uint8_t The index of an address table entry. + * @param id The short ID corresponding to the remote node whose EUI64 is stored in the address table at the given index + * or EMBER_TABLE_ENTRY_UNUSED_NODE_ID which indicates that the entry stored in the address table at the given index is not in use. + */ + async ezspSetAddressTableRemoteNodeId(addressTableIndex: number, id: EmberNodeId): Promise { + this.startCommand(EzspFrameID.SET_ADDRESS_TABLE_REMOTE_NODE_ID); + this.buffalo.writeUInt8(addressTableIndex); + this.buffalo.writeUInt16(id); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Gets the EUI64 of an address table entry. + * @param addressTableIndex uint8_t The index of an address table entry. + * @returns The EUI64 of the address table entry is copied to this location. + */ + async ezspGetAddressTableRemoteEui64(addressTableIndex: number): Promise { + this.startCommand(EzspFrameID.GET_ADDRESS_TABLE_REMOTE_EUI64); + this.buffalo.writeUInt8(addressTableIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const eui64 = this.buffalo.readIeeeAddr(); + + return eui64; + } + + /** + * Gets the short ID of an address table entry. + * @param addressTableIndex uint8_t The index of an address table entry. + * @returns One of the following: The short ID corresponding to the remote node + * whose EUI64 is stored in the address table at the given index. + * - EMBER_UNKNOWN_NODE_ID - Indicates that the EUI64 stored in the address table + * at the given index is valid but the short ID is currently unknown. + * - EMBER_DISCOVERY_ACTIVE_NODE_ID - Indicates that the EUI64 stored in the + * address table at the given location is valid and network address discovery is + * underway. + * - EMBER_TABLE_ENTRY_UNUSED_NODE_ID - Indicates that the entry stored + * in the address table at the given index is not in use. + */ + async ezspGetAddressTableRemoteNodeId(addressTableIndex: number): Promise { + this.startCommand(EzspFrameID.GET_ADDRESS_TABLE_REMOTE_NODE_ID); + this.buffalo.writeUInt8(addressTableIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const nodeId: EmberNodeId = this.buffalo.readUInt16(); + return nodeId; + } + + /** + * Tells the stack whether or not the normal interval between retransmissions of a retried unicast message should + * be increased by EMBER_INDIRECT_TRANSMISSION_TIMEOUT. + * The interval needs to be increased when sending to a sleepy node so that the message is not retransmitted until the destination + * has had time to wake up and poll its parent. + * The stack will automatically extend the timeout: + * - For our own sleepy children. + * - When an address response is received from a parent on behalf of its child. + * - When an indirect transaction expiry route error is received. + * - When an end device announcement is received from a sleepy node. + * @param remoteEui64 The address of the node for which the timeout is to be set. + * @param extendedTimeout true if the retry interval should be increased by EMBER_INDIRECT_TRANSMISSION_TIMEOUT. + * false if the normal retry interval should be used. + */ + async ezspSetExtendedTimeout(remoteEui64: EmberEUI64, extendedTimeout: boolean): Promise { + this.startCommand(EzspFrameID.SET_EXTENDED_TIMEOUT); + this.buffalo.writeIeeeAddr(remoteEui64); + this.buffalo.writeUInt8(extendedTimeout ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Indicates whether or not the stack will extend the normal interval between + * retransmissions of a retried unicast message by + * EMBER_INDIRECT_TRANSMISSION_TIMEOUT. + * @param remoteEui64 The address of the node for which the timeout is to be returned. + * @returns true if the retry interval will be increased by EMBER_INDIRECT_TRANSMISSION_TIMEOUT + * and false if the normal retry interval will be used. + */ + async ezspGetExtendedTimeout(remoteEui64: EmberEUI64): Promise { + this.startCommand(EzspFrameID.GET_EXTENDED_TIMEOUT); + this.buffalo.writeIeeeAddr(remoteEui64); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const extendedTimeout = this.buffalo.readUInt8() === 1 ? true : false; + + return extendedTimeout; + } + + /** + * Replaces the EUI64, short ID and extended timeout setting of an address table + * entry. The previous EUI64, short ID and extended timeout setting are + * returned. + * @param addressTableIndex uint8_t The index of the address table entry that will be modified. + * @param newEui64 The EUI64 to be written to the address table entry. + * @param newId One of the following: The short ID corresponding to the new EUI64. + * EMBER_UNKNOWN_NODE_ID if the new EUI64 is valid but the short ID is unknown and should be discovered by the stack. + * EMBER_TABLE_ENTRY_UNUSED_NODE_ID if the address table entry is now unused. + * @param newExtendedTimeout true if the retry interval should be increased by EMBER_INDIRECT_TRANSMISSION_TIMEOUT. + * false if the normal retry interval should be used. + * @returns EMBER_SUCCESS if the EUI64, short ID and extended timeout setting + * were successfully modified, and EMBER_ADDRESS_TABLE_ENTRY_IS_ACTIVE + * otherwise. + * @returns oldEui64 The EUI64 of the address table entry before it was modified. + * @returns oldId EmberNodeId * One of the following: The short ID corresponding to the EUI64 before it was modified. + * EMBER_UNKNOWN_NODE_ID if the short ID was unknown. EMBER_DISCOVERY_ACTIVE_NODE_ID if discovery of the short ID was underway. + * EMBER_TABLE_ENTRY_UNUSED_NODE_ID if the address table entry was unused. + * @returns oldExtendedTimeouttrue bool * if the retry interval was being increased by EMBER_INDIRECT_TRANSMISSION_TIMEOUT. + * false if the normal retry interval was being used. + */ + async ezspReplaceAddressTableEntry(addressTableIndex: number, newEui64: EmberEUI64, newId: EmberNodeId, newExtendedTimeout: boolean): + Promise<[EmberStatus, oldEui64: EmberEUI64, oldId: EmberNodeId, oldExtendedTimeout: boolean]> { + this.startCommand(EzspFrameID.REPLACE_ADDRESS_TABLE_ENTRY); + this.buffalo.writeUInt8(addressTableIndex); + this.buffalo.writeIeeeAddr(newEui64); + this.buffalo.writeUInt16(newId); + this.buffalo.writeUInt8(newExtendedTimeout ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const oldEui64 = this.buffalo.readIeeeAddr(); + const oldId = this.buffalo.readUInt16(); + const oldExtendedTimeout = this.buffalo.readUInt8() === 1 ? true : false; + + return [status, oldEui64, oldId, oldExtendedTimeout]; + } + + /** + * Returns the node ID that corresponds to the specified EUI64. The node ID is + * found by searching through all stack tables for the specified EUI64. + * @param eui64 The EUI64 of the node to look up. + * @returns The short ID of the node or EMBER_NULL_NODE_ID if the short ID is not + * known. + */ + async ezspLookupNodeIdByEui64(eui64: EmberEUI64): Promise { + this.startCommand(EzspFrameID.LOOKUP_NODE_ID_BY_EUI64); + this.buffalo.writeIeeeAddr(eui64); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const nodeId: EmberNodeId = this.buffalo.readUInt16(); + + return nodeId; + } + + /** + * Returns the EUI64 that corresponds to the specified node ID. The EUI64 is + * found by searching through all stack tables for the specified node ID. + * @param nodeId The short ID of the node to look up. + * @returns EMBER_SUCCESS if the EUI64 was found, EMBER_ERR_FATAL if the EUI64 is + * not known. + * @returns eui64 The EUI64 of the node. + */ + async ezspLookupEui64ByNodeId(nodeId: EmberNodeId): Promise<[EmberStatus, eui64: EmberEUI64]> { + this.startCommand(EzspFrameID.LOOKUP_EUI64_BY_NODE_ID); + this.buffalo.writeUInt16(nodeId); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const eui64 = this.buffalo.readIeeeAddr(); + + return [status, eui64]; + } + + /** + * Gets an entry from the multicast table. + * @param uint8_t The index of a multicast table entry. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberMulticastTableEntry * The contents of the multicast entry. + */ + async ezspGetMulticastTableEntry(index: number): Promise<[EmberStatus, value: EmberMulticastTableEntry]> { + this.startCommand(EzspFrameID.GET_MULTICAST_TABLE_ENTRY); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const value = this.buffalo.readEmberMulticastTableEntry(); + + return [status, value]; + } + + /** + * Sets an entry in the multicast table. + * @param index uint8_t The index of a multicast table entry + * @param EmberMulticastTableEntry * The contents of the multicast entry. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetMulticastTableEntry(index: number, value: EmberMulticastTableEntry): Promise { + this.startCommand(EzspFrameID.SET_MULTICAST_TABLE_ENTRY); + this.buffalo.writeUInt8(index); + this.buffalo.writeEmberMulticastTableEntry(value); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback invoked by the EmberZNet stack when an id conflict is discovered, + * that is, two different nodes in the network were found to be using the same + * short id. The stack automatically removes the conflicting short id from its + * internal tables (address, binding, route, neighbor, and child tables). The + * application should discontinue any other use of the id. + * @param id The short id for which a conflict was detected + */ + ezspIdConflictHandler(id: EmberNodeId): void { + debug(`ezspIdConflictHandler(): callback called with: [id=${id}]`); + console.warn(`An ID conflict was detected for device "${id}".`); + } + + /** + * Write the current node Id, PAN ID, or Node type to the tokens + * @param erase Erase the node type or not + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspWriteNodeData(erase: boolean): Promise { + this.startCommand(EzspFrameID.WRITE_NODE_DATA); + this.buffalo.writeUInt8(erase ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Transmits the given message without modification. The MAC header is assumed + * to be configured in the message at the time this function is called. + * @param messageContents uint8_t * The raw message. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSendRawMessage(messageContents: Buffer): Promise { + this.startCommand(EzspFrameID.SEND_RAW_MESSAGE); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Transmits the given message without modification. The MAC header is assumed + * to be configured in the message at the time this function is called. + * @param messageContents uint8_t * The raw message. + * @param priority uint8_t transmit priority. + * @param useCca Should we enable CCA or not. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSendRawMessageExtended(messageContents: Buffer, priority: number, useCca: boolean): Promise { + this.startCommand(EzspFrameID.SEND_RAW_MESSAGE_EXTENDED); + this.buffalo.writePayload(messageContents); + this.buffalo.writeUInt8(priority); + this.buffalo.writeUInt8(useCca ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback invoked by the EmberZNet stack when a MAC passthrough message is + * received. + * @param messageType The type of MAC passthrough message received. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the message. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during reception. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The raw message that was received. + */ + ezspMacPassthroughMessageHandler(messageType: EmberMacPassthroughType, lastHopLqi: number, lastHopRssi: number, messageContents: Buffer): void { + debug(`ezspMacPassthroughMessageHandler(): callback called with: [messageType=${messageType}], [lastHopLqi=${lastHopLqi}], ` + + `[lastHopRssi=${lastHopRssi}], [messageContents=${messageContents.toString('hex')}]`); + } + + /** + * Callback + * A callback invoked by the EmberZNet stack when a raw MAC message that has + * matched one of the application's configured MAC filters. + * @param filterIndexMatch uint8_t The index of the filter that was matched. + * @param legacyPassthroughType The type of MAC passthrough message received. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the message. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during reception. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The raw message that was received. + */ + ezspMacFilterMatchMessageHandler(filterIndexMatch: number, legacyPassthroughType: EmberMacPassthroughType, lastHopLqi: number, + lastHopRssi: number, messageContents: Buffer): void { + debug(`ezspMacFilterMatchMessageHandler(): callback called with: [filterIndexMatch=${filterIndexMatch}], ` + + `[legacyPassthroughType=${legacyPassthroughType}], [lastHopLqi=${lastHopLqi}], [lastHopRssi=${lastHopRssi}], ` + + `[messageContents=${messageContents.toString('hex')}]`); + + // TODO: needs triple-checking, this is only valid for InterPAN messages + const msgBuffalo = new EzspBuffalo(messageContents, 0); + + const macFrameControl = msgBuffalo.readUInt16() & ~(MAC_ACK_REQUIRED); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const sequence = msgBuffalo.readUInt8(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const destPanId: EmberPanId = msgBuffalo.readUInt16(); + let destAddress: EmberEUI64 | EmberNodeId; + + if (macFrameControl === LONG_DEST_FRAME_CONTROL) { + destAddress = msgBuffalo.readIeeeAddr(); + } else if (macFrameControl === SHORT_DEST_FRAME_CONTROL) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + destAddress = msgBuffalo.readUInt16(); + } else { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN macFrameControl "${macFrameControl}".`); + return; + } + + const sourcePanId: EmberPanId = msgBuffalo.readUInt16(); + const sourceAddress: EmberEUI64 = msgBuffalo.readIeeeAddr(); + + // Now that we know the correct MAC length, verify the interpan frame is the correct length. + let remainingLength = msgBuffalo.getBufferLength() - msgBuffalo.getPosition(); + + if (remainingLength < (STUB_NWK_SIZE + MIN_STUB_APS_SIZE)) { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN length "${remainingLength}".`); + return; + } + + const nwkFrameControl = msgBuffalo.readUInt16(); + remainingLength -= 2;// read 2 more bytes before APS stuff + + if (nwkFrameControl !== STUB_NWK_FRAME_CONTROL) { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN nwkFrameControl "${nwkFrameControl}".`); + return; + } + + const apsFrameControl = msgBuffalo.readUInt8(); + + if ((apsFrameControl & ~(INTERPAN_APS_FRAME_DELIVERY_MODE_MASK) & ~(INTERPAN_APS_FRAME_SECURITY)) + !== INTERPAN_APS_FRAME_CONTROL_NO_DELIVERY_MODE) { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN apsFrameControl "${apsFrameControl}".`); + return; + } + + const messageType = (apsFrameControl & INTERPAN_APS_FRAME_DELIVERY_MODE_MASK); + let groupId: number = null; + + switch (messageType) { + case EmberInterpanMessageType.UNICAST: + case EmberInterpanMessageType.BROADCAST: { + if (remainingLength < INTERPAN_APS_UNICAST_BROADCAST_SIZE) { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN length "${remainingLength}".`); + return; + } + break; + } + case EmberInterpanMessageType.MULTICAST: { + if (remainingLength < INTERPAN_APS_MULTICAST_SIZE) { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN length "${remainingLength}".`); + return; + } + + groupId = msgBuffalo.readUInt16(); + break; + } + default: { + debug(`ezspMacFilterMatchMessageHandler INVALID InterPAN messageType "${messageType}".`); + return; + } + } + + const clusterId = msgBuffalo.readUInt16(); + const profileId = msgBuffalo.readUInt16(); + const payload = msgBuffalo.readRest(); + + if (profileId === TOUCHLINK_PROFILE_ID && clusterId === Cluster.touchlink.ID) { + this.emit(EzspEvents.TOUCHLINK_MESSAGE, sourcePanId, sourceAddress, groupId, lastHopLqi, payload); + } + } + + /** + * Callback + * A callback invoked by the EmberZNet stack when the MAC has finished + * transmitting a raw message. + * @param status EMBER_SUCCESS if the transmission was successful, or EMBER_DELIVERY_FAILED if not + */ + ezspRawTransmitCompleteHandler(status: EmberStatus): void { + debug(`ezspRawTransmitCompleteHandler(): callback called with: [status=${EmberStatus[status]}]`); + } + + /** + * This function is useful to sleepy end devices. This function will set the + * retry interval (in milliseconds) for mac data poll. This interval is the time + * in milliseconds the device waits before retrying a data poll when a MAC level + * data poll fails for any reason. + * @param waitBeforeRetryIntervalMs uint32_t Time in milliseconds the device waits before retrying + * a data poll when a MAC level data poll fails for any reason. + */ + async ezspSetMacPollFailureWaitTime(waitBeforeRetryIntervalMs: number): Promise { + this.startCommand(EzspFrameID.SET_MAC_POLL_FAILURE_WAIT_TIME); + this.buffalo.writeUInt32(waitBeforeRetryIntervalMs); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Sets the priority masks and related variables for choosing the best beacon. + * @param param EmberBeaconClassificationParams * The beacon prioritization related variable + * @returns The attempt to set the pramaters returns EMBER_SUCCESS + */ + async ezspSetBeaconClassificationParams(param: EmberBeaconClassificationParams): Promise { + this.startCommand(EzspFrameID.SET_BEACON_CLASSIFICATION_PARAMS); + this.buffalo.writeEmberBeaconClassificationParams(param); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Gets the priority masks and related variables for choosing the best beacon. + * @returns The attempt to get the pramaters returns EMBER_SUCCESS + * @returns EmberBeaconClassificationParams * Gets the beacon prioritization related variable + */ + async ezspGetBeaconClassificationParams(): Promise<[EmberStatus, param: EmberBeaconClassificationParams]> { + this.startCommand(EzspFrameID.GET_BEACON_CLASSIFICATION_PARAMS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const param = this.buffalo.readEmberBeaconClassificationParams(); + + return [status, param]; + } + + //----------------------------------------------------------------------------- + // Security Frames + //----------------------------------------------------------------------------- + + /** + * Sets the security state that will be used by the device when it forms or + * joins the network. This call should not be used when restoring saved network + * state via networkInit as this will result in a loss of security data and will + * cause communication problems when the device re-enters the network. + * @param state EmberInitialSecurityState * The security configuration to be set. + * @returns The success or failure code of the operation. + */ + async ezspSetInitialSecurityState(state: EmberInitialSecurityState): Promise { + this.startCommand(EzspFrameID.SET_INITIAL_SECURITY_STATE); + this.buffalo.writeEmberInitialSecurityState(state); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Gets the current security state that is being used by a device that is joined + * in the network. + * @returns The success or failure code of the operation. + * @returns EmberCurrentSecurityState * The security configuration in use by the stack. + */ + async ezspGetCurrentSecurityState(): Promise<[EmberStatus, state: EmberCurrentSecurityState]> { + this.startCommand(EzspFrameID.GET_CURRENT_SECURITY_STATE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const state = this.buffalo.readEmberCurrentSecurityState(); + + return [status, state]; + } + + /** + * Exports a key from security manager based on passed context. + * @param context sl_zb_sec_man_context_t * Metadata to identify the requested key. + * @returns sl_zb_sec_man_key_t * Data to store the exported key in. + * @returns sl_status_t * The success or failure code of the operation. + */ + async ezspExportKey(context: SecManContext): Promise<[key: SecManKey, status: SLStatus]> { + /** + * Export a key from storage. Certain keys are indexed, while others are not, as described here. + * + * If context->core_key_type is.. + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_NETWORK, then context->key_index dictates whether to + * export the current (active) network key (index 0) or the alternate network + * key (index 1). + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_TC_LINK, then context->eui64 is checked if + * context->flags is set to ZB_SEC_MAN_FLAG_EUI_IS_VALID. If the EUI supplied + * does not match the TC EUI stored on the local device (if it is known), then + * an error is thrown. + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_TC_LINK_WITH_TIMEOUT, then keys may be searched by + * context->eui64 or context->key_index. context->flags determines how to search + * (see ::sl_zigbee_sec_man_flags_t). + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_APP_LINK, then keys may be searched by + * context->eui64 or context->key_index. context->flags determines how to search + * (see ::sl_zigbee_sec_man_flags_t). + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_GREEN_POWER_PROXY_TABLE_KEY or + * SL_ZB_SEC_MAN_KEY_TYPE_GREEN_POWER_SINK_TABLE_KEY, then context->key_index + * dictates which key entry to export. These Green Power keys are indexed keys, + * and there are EMBER_GP_PROXY_TABLE_SIZE/EMBER_GP_SINK_TABLE_SIZE many of them. + * + * For all other key types, both context->key_index and context->eui64 are not used. + * + * @param context sl_zb_sec_man_context_t* [IN/OUT] The context to set. The context dictates which key + * type to export, which key_index (if applicable) into the relevant key + * storage, which eui64 (if applicable), etc. + * @param plaintext_key sl_zb_sec_man_key_t* [OUT] The key to export. + * + * @note The context->derived_type must be SL_ZB_SEC_MAN_DERIVED_KEY_TYPE_NONE. + * Other values are ignored. + * + * @return SL_STATUS_OK upon success, a valid error code otherwise. + */ + // NOTE: added for good measure + if (context.coreKeyType === SecManKeyType.INTERNAL) { + console.assert(false, `ezspImportKey cannot use INTERNAL key type.`); + return [null, SLStatus.INVALID_PARAMETER]; + } + + this.startCommand(EzspFrameID.EXPORT_KEY); + this.buffalo.writeSecManContext(context); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const key = this.buffalo.readSecManKey(); + const status: SLStatus = this.buffalo.readUInt32(); + + return [key, status]; + } + + /** + * Imports a key into security manager based on passed context. + * @param context sl_zb_sec_man_context_t * Metadata to identify where the imported key should be stored. + * @param key sl_zb_sec_man_key_t * The key to be imported. + * @returns The success or failure code of the operation. + */ + async ezspImportKey(context: SecManContext, key: SecManKey): Promise { + /** + * Import a key into storage. Certain keys are + * indexed, while others are not, as described here. + * + * If context->core_key_type is.. + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_NETWORK, then context->key_index dictates whether to + * import the current (active) network key (index 0) or the alternate network + * key (index 1). + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_TC_LINK_WITH_TIMEOUT, then context->eui64 must be + * set. context->key_index is unused. + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_APP_LINK, then context->key_index determines which + * index in the persisted key table that the entry should be stored to. + * context->eui64 must also be set. + * If context->key_index is 0xFF, a suitable key index will be found (either one + * storing an existing key with address of context->eui64, or an open entry), + * and context->key_index will be updated with where the entry was stored. + * + * ..SL_ZB_SEC_MAN_KEY_TYPE_GREEN_POWER_PROXY_TABLE_KEY or + * SL_ZB_SEC_MAN_KEY_TYPE_GREEN_POWER_SINK_TABLE_KEY, then context->key_index + * dictates which key entry to import. These Green Power keys are indexed keys, + * and there are EMBER_GP_PROXY_TABLE_SIZE/EMBER_GP_SINK_TABLE_SIZE many of them. + * + * For all other key types, both context->key_index and context->eui64 are not + * used. + * + * @param context sl_zb_sec_man_context_t* [IN] The context to set. The context dictates which key type + * to save, key_index (if applicable) into the relevant key storage, eui64 (if + * applicable), etc. + * @param plaintext_key sl_zb_sec_man_key_t* [IN] The key to import. + * @note The context->derived_type must be SL_ZB_SEC_MAN_DERIVED_KEY_TYPE_NONE, + * else, an error will be thrown. Key derivations, which are used in crypto + * operations, are performed using the ::sl_zb_sec_man_load_key_context routine. + * @return SL_STATUS_OK upon success, a valid error code otherwise. + */ + // NOTE: added for good measure + if (context.coreKeyType === SecManKeyType.INTERNAL) { + console.assert(false, `ezspImportKey cannot use INTERNAL key type.`); + return SLStatus.INVALID_PARAMETER; + } + + this.startCommand(EzspFrameID.IMPORT_KEY); + this.buffalo.writeSecManContext(context); + this.buffalo.writeSecManKey(key); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: SLStatus = this.buffalo.readUInt32(); + + return status; + } + + /** + * Callback + * A callback to inform the application that the Network Key has been updated + * and the node has been switched over to use the new key. The actual key being + * used is not passed up, but the sequence number is. + * @param sequenceNumber uint8_t The sequence number of the new network key. + */ + ezspSwitchNetworkKeyHandler(sequenceNumber: number): void { + debug(`ezspSwitchNetworkKeyHandler(): callback called with: [sequenceNumber=${sequenceNumber}]`); + } + + /** + * This function searches through the Key Table and tries to find the entry that + * matches the passed search criteria. + * @param address The address to search for. Alternatively, all zeros may be passed in to search for the first empty entry. + * @param linkKey This indicates whether to search for an entry that contains a link key or a master key. + * true means to search for an entry with a Link Key. + * @returns uint8_t This indicates the index of the entry that matches the search + * criteria. A value of 0xFF is returned if not matching entry is found. + */ + async ezspFindKeyTableEntry(address: EmberEUI64, linkKey: boolean): Promise { + this.startCommand(EzspFrameID.FIND_KEY_TABLE_ENTRY); + this.buffalo.writeIeeeAddr(address); + this.buffalo.writeUInt8(linkKey ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const index = this.buffalo.readUInt8(); + + return index; + } + + /** + * This function sends an APS TransportKey command containing the current trust + * center link key. The node to which the command is sent is specified via the + * short and long address arguments. + * @param destinationNodeId The short address of the node to which this command will be sent + * @param destinationEui64 The long address of the node to which this command will be sent + * @returns An EmberStatus value indicating success of failure of the operation + */ + async ezspSendTrustCenterLinkKey(destinationNodeId: EmberNodeId, destinationEui64: EmberEUI64): Promise { + this.startCommand(EzspFrameID.SEND_TRUST_CENTER_LINK_KEY); + this.buffalo.writeUInt16(destinationNodeId); + this.buffalo.writeIeeeAddr(destinationEui64); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This function erases the data in the key table entry at the specified index. + * If the index is invalid, false is returned. + * @param index uint8_t This indicates the index of entry to erase. + * @returns ::EMBER_SUCCESS if the index is valid and the key data was erased. + * ::EMBER_KEY_INVALID if the index is out of range for the size of the key table. + */ + async ezspEraseKeyTableEntry(index: number): Promise { + this.startCommand(EzspFrameID.ERASE_KEY_TABLE_ENTRY); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This function clears the key table of the current network. + * @returns ::EMBER_SUCCESS if the key table was successfully cleared. + * ::EMBER_INVALID_CALL otherwise. + */ + async ezspClearKeyTable(): Promise { + this.startCommand(EzspFrameID.CLEAR_KEY_TABLE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * A function to request a Link Key from the Trust Center with another device on + * the Network (which could be the Trust Center). A Link Key with the Trust + * Center is possible but the requesting device cannot be the Trust Center. Link + * Keys are optional in ZigBee Standard Security and thus the stack cannot know + * whether the other device supports them. If EMBER_REQUEST_KEY_TIMEOUT is + * non-zero on the Trust Center and the partner device is not the Trust Center, + * both devices must request keys with their partner device within the time + * period. The Trust Center only supports one outstanding key request at a time + * and therefore will ignore other requests. If the timeout is zero then the + * Trust Center will immediately respond and not wait for the second request. + * The Trust Center will always immediately respond to requests for a Link Key + * with it. Sleepy devices should poll at a higher rate until a response is + * received or the request times out. The success or failure of the request is + * returned via ezspZigbeeKeyEstablishmentHandler(...) + * @param partner This is the IEEE address of the partner device that will share the link key. + * @returns The success or failure of sending the request. + * This is not the final result of the attempt. ezspZigbeeKeyEstablishmentHandler(...) will return that. + */ + async ezspRequestLinkKey(partner: EmberEUI64): Promise { + this.startCommand(EzspFrameID.REQUEST_LINK_KEY); + this.buffalo.writeIeeeAddr(partner); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Requests a new link key from the Trust Center. This function starts by + * sending a Node Descriptor request to the Trust Center to verify its R21+ + * stack version compliance. A Request Key message will then be sent, followed + * by a Verify Key Confirm message. + * @param maxAttempts uint8_t The maximum number of attempts a node should make when sending the Node Descriptor, + * Request Key, and Verify Key Confirm messages. The number of attempts resets for each message type sent + * (e.g., if maxAttempts is 3, up to 3 Node Descriptors are sent, up to 3 Request Keys, and up to 3 Verify Key Confirm messages are sent). + * @returns The success or failure of sending the request. + * If the Node Descriptor is successfully transmitted, ezspZigbeeKeyEstablishmentHandler(...) + * will be called at a later time with a final status result. + */ + async ezspUpdateTcLinkKey(maxAttempts: number): Promise { + this.startCommand(EzspFrameID.UPDATE_TC_LINK_KEY); + this.buffalo.writeUInt8(maxAttempts); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * This is a callback that indicates the success or failure of an attempt to establish a key with a partner device. + * @param partner This is the IEEE address of the partner that the device successfully established a key with. + * This value is all zeros on a failure. + * @param status This is the status indicating what was established or why the key establishment failed. + */ + ezspZigbeeKeyEstablishmentHandler(partner: EmberEUI64, status: EmberKeyStatus): void { + debug(`ezspZigbeeKeyEstablishmentHandler(): callback called with: [partner=${partner}], [status=${EmberKeyStatus[status]}]`); + // NOTE: For security reasons, any valid `partner` (not wildcard) that return with a status=TC_REQUESTER_VERIFY_KEY_TIMEOUT + // are kicked off the network for posing a risk, unless HA devices allowed (as opposed to Z3) + // and always if status=TC_REQUESTER_VERIFY_KEY_FAILURE + } + + /** + * Clear all of the transient link keys from RAM. + */ + async ezspClearTransientLinkKeys(): Promise { + this.startCommand(EzspFrameID.CLEAR_TRANSIENT_LINK_KEYS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Retrieve information about the current and alternate network key, excluding their contents. + * @returns Success or failure of retrieving network key info. + * @returns sl_zb_sec_man_network_key_info_t * Information about current and alternate network keys. + */ + async ezspGetNetworkKeyInfo(): Promise<[SLStatus, networkKeyInfo: SecManNetworkKeyInfo]> { + /** + * Retrieve information about the network key and alternate network key. + * It will not retrieve the actual network key contents. + * + * @param network_key_info sl_zb_sec_man_network_key_info_t* [OUT] The network key info struct used to store network key metadata, + * containing information about whether the current and next network keys are set, and the + * sequence numbers associated with each key. + * + * @return sl_status_t SL_STATUS_OK + * + */ + this.startCommand(EzspFrameID.GET_NETWORK_KEY_INFO); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: SLStatus = this.buffalo.readUInt32(); + const networkKeyInfo = this.buffalo.readSecManNetworkKeyInfo(); + + return [status, networkKeyInfo]; + } + + /** + * Retrieve metadata about an APS link key. Does not retrieve contents. + * @param context_in sl_zb_sec_man_context_t * Context used to input information about key. + * @returns EUI64 associated with this APS link key + * @returns sl_zb_sec_man_aps_key_metadata_t * Metadata about the referenced key. + * @returns sl_status_t * Status of metadata retrieval operation. + */ + async ezspGetApsKeyInfo(context_in: SecManContext): Promise<[eui: EmberEUI64, key_data: SecManAPSKeyMetadata, status: SLStatus ]> { + /** + * Retrieve metadata about an APS key. + * It does not retrieve the actual key contents. + * + * @param context sl_zb_sec_man_context_t* [IN/OUT] The context to use to look up a key entry. If the + * user calls this function with the ::ZB_SEC_MAN_FLAG_KEY_INDEX_IS_VALID bit + * set in the context->flag field, then the key_index field in the context + * argument dictates which entry to retrieve. For keys with timeout and + * application link keys, the key_index retrieves the indexed entry into the + * respective table. Upon success, the eui64 field in the context is updated. + * If the user calls this function with the + * ::ZB_SEC_MAN_FLAG_EUI_IS_VALID bit set in the + * context->flag field, then the eui64 field in the context argument + * dictates which entry to retrieve. If the context->core_key_type argument is + * set to SL_ZB_SEC_MAN_KEY_TYPE_NETWORK, an error is returned as network keys + * are not tied to any specific EUI. + * If neither the ::ZB_SEC_MAN_FLAG_KEY_INDEX_IS_VALID bit nor the + * ::ZB_SEC_MAN_FLAG_EUI_IS_VALID bit is set in context->flags, then an error + * will be returned by this function. + * Upon success in fetching a key, the other fields in this argument are + * updated (e.g. a successful search by key_index will update the euii64 + * field). + * + * @param key_data sl_zb_sec_man_aps_key_metadata_t* [OUT] Metadata to fill in. + * + * @return SL_STATUS_OK if successful, SL_STATUS_NOT_FOUND if + * the key_index or eui64 does not result in a found entry, + * SL_STATUS_INVALID_TYPE if the core key type is not an APS layer key (e.g. + * SL_ZB_SEC_MAN_KEY_TYPE_NETWORK), or SL_STATUS_INVALID_MODE if core_key_type + * is SL_ZB_SEC_MAN_KEY_TYPE_TC_LINK and the initial security state does not + * indicate the a preconfigured key has been set (that is, both + * EMBER_HAVE_PRECONFIGURED_KEY and + * EMBER_GET_PRECONFIGURED_KEY_FROM_INSTALL_CODE have not been set in the + * initial security state). + */ + this.startCommand(EzspFrameID.GET_APS_KEY_INFO); + this.buffalo.writeSecManContext(context_in); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const eui: EmberEUI64 = this.buffalo.readIeeeAddr(); + const keyData = this.buffalo.readSecManAPSKeyMetadata(); + const status: SLStatus = this.buffalo.readUInt32(); + + return [eui, keyData, status]; + } + + /** + * Import an application link key into the key table. + * @param index uint8_t Index where this key is to be imported to. + * @param address EUI64 this key is associated with. + * @param plaintextKey sl_zb_sec_man_key_t * The key data to be imported. + * @returns Status of key import operation. + */ + async ezspImportLinkKey(index: number, address: EmberEUI64, plaintextKey: SecManKey): Promise { + /** + * Import a link key, or SL_ZB_SEC_MAN_KEY_TYPE_APP_LINK key, into storage. + * + * @param index uint8_t [IN] The index to set or overwrite in the key table for keys of + * type SL_ZB_SEC_MAN_KEY_TYPE_APP_LINK. If index is set to 0xFF (255), then + * the key will either overwrite whichever key table entry has an EUI of address + * (if one exists) or write to the first available key table entry. The index + * that the key was placed into will not be returned by this API. + * @param address EmberEUI64 [IN] The EUI belonging to the key. + * @param plaintext_key sl_zb_sec_man_key_t* [IN] A pointer to the key to import. + * + * @return SL_STATUS_OK upon success, a valid error code otherwise. + * + */ + this.startCommand(EzspFrameID.IMPORT_LINK_KEY); + this.buffalo.writeUInt8(index); + this.buffalo.writeIeeeAddr(address); + this.buffalo.writeSecManKey(plaintextKey); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: SLStatus = this.buffalo.readUInt32(); + return status; + } + + /** + * Export the link key at given index from the key table. + * @param uint8_t Index of key to export. + * @returns EUI64 associated with the exported key. + * @returns sl_zb_sec_man_key_t * The exported key. + * @returns sl_zb_sec_man_aps_key_metadata_t * Metadata about the key. + * @returns sl_status_t * Status of key export operation. + */ + async ezspExportLinkKeyByIndex(index: number): + Promise<[eui: EmberEUI64, plaintextKey: SecManKey, keyData: SecManAPSKeyMetadata, status: SLStatus]> { + /** + * Export an APS link key by index. + * + * @param index uint8_t + * @param address EmberEUI64 + * @param plaintext_key sl_zb_sec_man_key_t* + * @param key_data sl_zb_sec_man_aps_key_metadata_t* + */ + this.startCommand(EzspFrameID.EXPORT_LINK_KEY_BY_INDEX); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const eui: EmberEUI64 = this.buffalo.readIeeeAddr(); + const plaintextKey: SecManKey = this.buffalo.readSecManKey(); + const keyData: SecManAPSKeyMetadata = this.buffalo.readSecManAPSKeyMetadata(); + const status: SLStatus = this.buffalo.readUInt32(); + + return [eui, plaintextKey, keyData, status]; + } + + /** + * Export the link key associated with the given EUI from the key table. + * @param eui EUI64 associated with the key to export. + * @returns sl_zb_sec_man_key_t * The exported key. + * @returns uint8_t * Key index of the exported key. + * @returns sl_zb_sec_man_aps_key_metadata_t * Metadata about the key. + * @returns sl_status_t * Status of key export operation. + */ + async ezspExportLinkKeyByEui(eui: EmberEUI64): + Promise<[plaintextKey: SecManKey, index: number, keyData: SecManAPSKeyMetadata, status: SLStatus]> { + /** + * Search through the Key table to find an entry that has the same EUI address as the passed value. + * If NULL is passed in for the address then it finds the first unused entry and sets the index in the context. + * It is valid to pass in NULL to plaintext_key or key_data in case the index of the referenced key is desired + * but not its value or other metadata. + * @param eui EmberEUI64 + * @param context sl_zb_sec_man_context_t* + * @param plaintext_key sl_zb_sec_man_key_t* + * @param key_data sl_zb_sec_man_aps_key_metadata_t* + */ + this.startCommand(EzspFrameID.EXPORT_LINK_KEY_BY_EUI); + this.buffalo.writeIeeeAddr(eui); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const plaintextKey: SecManKey = this.buffalo.readSecManKey(); + const index = this.buffalo.readUInt8(); + const keyData: SecManAPSKeyMetadata = this.buffalo.readSecManAPSKeyMetadata(); + const status: SLStatus = this.buffalo.readUInt32(); + + return [plaintextKey, index, keyData, status]; + } + + /** + * Check whether a key context can be used to load a valid key. + * @param context sl_zb_sec_man_context_t * Context struct to check the validity of. + * @returns Validity of the checked context. + */ + async ezspCheckKeyContext(context: SecManContext): Promise { + /** + * Check that the passed key exists and can be successfully loaded. + * This function does not actually load the context, but only checks that it can be loaded. + * + * @param context sl_zb_sec_man_context_t* [IN] The context to check for validity. The fields that must be set depend + * on the key type set in the context, as enough information is needed to identify the key. + * + * @return sl_status_t SL_STATUS_OK upon success, SL_STATUS_NOT_FOUND otherwise. + */ + this.startCommand(EzspFrameID.CHECK_KEY_CONTEXT); + this.buffalo.writeSecManContext(context); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: SLStatus = this.buffalo.readUInt32(); + return status; + } + + /** + * Import a transient link key. + * @param eui64 EUI64 associated with this transient key. + * @param plaintextKey sl_zb_sec_man_key_t * The key to import. + * @param sl_zigbee_sec_man_flags_t Flags associated with this transient key. + * @returns Status of key import operation. + */ + async ezspImportTransientKey(eui64: EmberEUI64, plaintextKey: SecManKey, flags: SecManFlag): Promise { + /** + * @brief Add a transient or temporary key entry to key storage. + * A key entry added with this API is timed out after + * ::EMBER_TRANSIENT_KEY_TIMEOUT_S seconds, unless the key entry was added using + * the Network Creator Security component, in which case the key will time out + * after the longer between + * ::EMBER_AF_PLUGIN_NETWORK_CREATOR_SECURITY_NETWORK_OPEN_TIME_S seconds and + * ::EMBER_TRANSIENT_KEY_TIMEOUT_S seconds. + * + * @param eui64 [IN] An EmberEUI64 to import. + * @param plaintext_key [IN] A sl_zb_sec_man_key_t* to import. + * @param flags [IN] + * + * @return See ::zb_sec_man_import_transient_key for return information. + */ + this.startCommand(EzspFrameID.IMPORT_TRANSIENT_KEY); + this.buffalo.writeIeeeAddr(eui64); + this.buffalo.writeSecManKey(plaintextKey); + this.buffalo.writeUInt8(flags); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: SLStatus = this.buffalo.readUInt32(); + + return status; + } + + /** + * Export a transient link key from a given table index. + * @param uint8_t Index to export from. + * @returns sl_zb_sec_man_context_t * Context struct for export operation. + * @returns sl_zb_sec_man_key_t * The exported key. + * @returns sl_zb_sec_man_aps_key_metadata_t * Metadata about the key. + * @returns sl_status_t * Status of key export operation. + */ + async ezspExportTransientKeyByIndex(index: number): + Promise<[context: SecManContext, plaintextKey: SecManKey, key_data: SecManAPSKeyMetadata, status: SLStatus]> { + /** + * Search for a transient, or temporary, key + * entry from key storage by key index. + * + * @param index [IN] The key_index to fetch. + * @param context sl_zb_sec_man_context_t* [OUT] The context about the key, filled in upon success. + * @param plaintext_key sl_zb_sec_man_key_t* [OUT] If the security configuration allows for it, filled in + * with the key contents upon success. + * @param key_data sl_zb_sec_man_aps_key_metadata_t* [OUT] Filled in with metadata about the key upon success. + * + * @return sl_status_t SL_STATUS_OK upon success, SL_STATUS_NOT_FOUND otherwise. + */ + this.startCommand(EzspFrameID.EXPORT_TRANSIENT_KEY_BY_INDEX); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const context = this.buffalo.readSecManContext(); + const plaintextKey = this.buffalo.readSecManKey(); + const keyData = this.buffalo.readSecManAPSKeyMetadata(); + const status: SLStatus = this.buffalo.readUInt32(); + + return [context, plaintextKey, keyData, status]; + } + + /** + * Export a transient link key associated with a given EUI64 + * @param eui Index to export from. + * @returns sl_zb_sec_man_context_t * Context struct for export operation. + * @returns sl_zb_sec_man_key_t * The exported key. + * @returns sl_zb_sec_man_aps_key_metadata_t * Metadata about the key. + * @returns sl_status_t * Status of key export operation. + */ + async ezspExportTransientKeyByEui(eui: EmberEUI64): + Promise<[context: SecManContext, plaintextKey: SecManKey, key_data: SecManAPSKeyMetadata, status: SLStatus]> { + /** + * Search for a transient, or temporary, key + * entry from key storage by EUI. + * + * @param eui64 [IN] The EUI to search for. + * @param context sl_zb_sec_man_context_t* [OUT] The context about the key, filled in upon success. + * @param plaintext_key sl_zb_sec_man_key_t* [OUT] If the security configuration allows for it, filled in + * with the key contents upon success. + * @param key_data sl_zb_sec_man_aps_key_metadata_t* [OUT] Filled in with metadata about the key upon success. + * + * @return sl_status_t SL_STATUS_OK upon success, SL_STATUS_NOT_FOUND otherwise. + */ + this.startCommand(EzspFrameID.EXPORT_TRANSIENT_KEY_BY_EUI); + this.buffalo.writeIeeeAddr(eui); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const context = this.buffalo.readSecManContext(); + const plaintextKey = this.buffalo.readSecManKey(); + const keyData = this.buffalo.readSecManAPSKeyMetadata(); + const status: SLStatus = this.buffalo.readUInt32(); + + return [context, plaintextKey, keyData, status]; + } + + //----------------------------------------------------------------------------- + // Trust Center Frames + //----------------------------------------------------------------------------- + + /** + * Callback + * The NCP used the trust center behavior policy to decide whether to allow a + * new node to join the network. The Host cannot change the current decision, + * but it can change the policy for future decisions using the setPolicy + * command. + * @param newNodeId The Node Id of the node whose status changed + * @param newNodeEui64 The EUI64 of the node whose status changed. + * @param status The status of the node: Secure Join/Rejoin, Unsecure Join/Rejoin, Device left. + * @param policyDecision An EmberJoinDecision reflecting the decision made. + * @param parentOfNewNodeId The parent of the node whose status has changed. + */ + ezspTrustCenterJoinHandler(newNodeId: EmberNodeId, newNodeEui64: EmberEUI64, status: EmberDeviceUpdate, + policyDecision: EmberJoinDecision, parentOfNewNodeId: EmberNodeId): void { + debug(`ezspTrustCenterJoinHandler(): callback called with: [newNodeId=${newNodeId}], [newNodeEui64=${newNodeEui64}], ` + + `[status=${EmberDeviceUpdate[status]}], [policyDecision=${EmberJoinDecision[policyDecision]}], ` + + `[parentOfNewNodeId=${parentOfNewNodeId}]`); + // NOTE: this is mostly just passing stuff up to Z2M, so use only one emit for all, let adapter do the rest, no parsing needed + this.emit(EzspEvents.TRUST_CENTER_JOIN, newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId); + } + + /** + * This function broadcasts a new encryption key, but does not tell the nodes in + * the network to start using it. To tell nodes to switch to the new key, use + * ezspBroadcastNetworkKeySwitch(). This is only valid for the Trust + * Center/Coordinator. It is up to the application to determine how quickly to + * send the Switch Key after sending the alternate encryption key. + * @param key EmberKeyData * An optional pointer to a 16-byte encryption key (EMBER_ENCRYPTION_KEY_SIZE). + * An all zero key may be passed in, which will cause the stack to randomly generate a new key. + * @returns EmberStatus value that indicates the success or failure of the command. + */ + async ezspBroadcastNextNetworkKey(key: EmberKeyData): Promise { + this.startCommand(EzspFrameID.BROADCAST_NEXT_NETWORK_KEY); + this.buffalo.writeEmberKeyData(key); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This function broadcasts a switch key message to tell all nodes to change to + * the sequence number of the previously sent Alternate Encryption Key. + * @returns EmberStatus value that indicates the success or failure of the + * command. + */ + async ezspBroadcastNetworkKeySwitch(): Promise { + this.startCommand(EzspFrameID.BROADCAST_NETWORK_KEY_SWITCH); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This routine processes the passed chunk of data and updates the hash context + * based on it. If the 'finalize' parameter is not set, then the length of the + * data passed in must be a multiple of 16. If the 'finalize' parameter is set + * then the length can be any value up 1-16, and the final hash value will be + * calculated. + * @param context EmberAesMmoHashContext * The hash context to update. + * @param finalize This indicates whether the final hash value should be calculated + * @param data uint8_t * The data to hash. + * @returns The result of the operation + * @returns EmberAesMmoHashContext * The updated hash context. + */ + async ezspAesMmoHash(context: EmberAesMmoHashContext, finalize: boolean, data: Buffer): + Promise<[EmberStatus, returnContext: EmberAesMmoHashContext]> { + this.startCommand(EzspFrameID.AES_MMO_HASH); + this.buffalo.writeEmberAesMmoHashContext(context); + this.buffalo.writeUInt8(finalize ? 1 : 0); + this.buffalo.writePayload(data); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const returnContext = this.buffalo.readEmberAesMmoHashContext(); + + return [status, returnContext]; + } + + /** + * This command sends an APS remove device using APS encryption to the + * destination indicating either to remove itself from the network, or one of + * its children. + * @param destShort The node ID of the device that will receive the message + * @param destLong The long address (EUI64) of the device that will receive the message. + * @param targetLong The long address (EUI64) of the device to be removed. + * @returns An EmberStatus value indicating success, or the reason for failure + */ + async ezspRemoveDevice(destShort: EmberNodeId, destLong: EmberEUI64, targetLong: EmberEUI64): Promise { + this.startCommand(EzspFrameID.REMOVE_DEVICE); + this.buffalo.writeUInt16(destShort); + this.buffalo.writeIeeeAddr(destLong); + this.buffalo.writeIeeeAddr(targetLong); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This command will send a unicast transport key message with a new NWK key to + * the specified device. APS encryption using the device's existing link key + * will be used. + * @param destShort The node ID of the device that will receive the message + * @param destLong The long address (EUI64) of the device that will receive the message. + * @param key EmberKeyData * The NWK key to send to the new device. + * @returns An EmberStatus value indicating success, or the reason for failure + */ + async ezspUnicastNwkKeyUpdate(destShort: EmberNodeId, destLong: EmberEUI64, key: EmberKeyData): Promise { + this.startCommand(EzspFrameID.UNICAST_NWK_KEY_UPDATE); + this.buffalo.writeUInt16(destShort); + this.buffalo.writeIeeeAddr(destLong); + this.buffalo.writeEmberKeyData(key); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + //----------------------------------------------------------------------------- + // Certificate Based Key Exchange (CBKE) Frames + //----------------------------------------------------------------------------- + + /** + * This call starts the generation of the ECC Ephemeral Public/Private key pair. + * When complete it stores the private key. The results are returned via + * ezspGenerateCbkeKeysHandler(). + */ + async ezspGenerateCbkeKeys(): Promise { + this.startCommand(EzspFrameID.GENERATE_CBKE_KEYS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback by the Crypto Engine indicating that a new ephemeral + * public/private key pair has been generated. The public/private key pair is + * stored on the NCP, but only the associated public key is returned to the + * host. The node's associated certificate is also returned. + * @param status The result of the CBKE operation. + * @param ephemeralPublicKey EmberPublicKeyData * The generated ephemeral public key. + */ + ezspGenerateCbkeKeysHandler(status: EmberStatus, ephemeralPublicKey: EmberPublicKeyData): void { + debug(`ezspGenerateCbkeKeysHandler(): callback called with: [status=${EmberStatus[status]}], [ephemeralPublicKey=${ephemeralPublicKey}]`); + } + + /** + * Calculates the SMAC verification keys for both the initiator and responder + * roles of CBKE using the passed parameters and the stored public/private key + * pair previously generated with ezspGenerateKeysRetrieveCert(). It also stores + * the unverified link key data in temporary storage on the NCP until the key + * establishment is complete. + * @param amInitiator The role of this device in the Key Establishment protocol. + * @param partnerCertificate EmberCertificateData * The key establishment partner's implicit certificate. + * @param partnerEphemeralPublicKey EmberPublicKeyData * The key establishment partner's ephemeral public key + */ + async ezspCalculateSmacs(amInitiator: boolean, partnerCertificate: EmberCertificateData, partnerEphemeralPublicKey: EmberPublicKeyData) + : Promise { + this.startCommand(EzspFrameID.CALCULATE_SMACS); + this.buffalo.writeUInt8(amInitiator ? 1 : 0); + this.buffalo.writeEmberCertificateData(partnerCertificate); + this.buffalo.writeEmberPublicKeyData(partnerEphemeralPublicKey); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback to indicate that the NCP has finished calculating the Secure + * Message Authentication Codes (SMAC) for both the initiator and responder. The + * associated link key is kept in temporary storage until the host tells the NCP + * to store or discard the key via emberClearTemporaryDataMaybeStoreLinkKey(). + * @param status The Result of the CBKE operation. + * @param initiatorSmac EmberSmacData * The calculated value of the initiator's SMAC + * @param responderSmac EmberSmacData * The calculated value of the responder's SMAC + */ + ezspCalculateSmacsHandler(status: EmberStatus, initiatorSmac: EmberSmacData, responderSmac: EmberSmacData): void { + debug(`ezspCalculateSmacsHandler(): callback called with: [status=${EmberStatus[status]}], [initiatorSmac=${initiatorSmac}], ` + + `[responderSmac=${responderSmac}]`); + } + + /** + * This call starts the generation of the ECC 283k1 curve Ephemeral + * Public/Private key pair. When complete it stores the private key. The results + * are returned via ezspGenerateCbkeKeysHandler283k1(). + */ + async ezspGenerateCbkeKeys283k1(): Promise { + this.startCommand(EzspFrameID.GENERATE_CBKE_KEYS283K1); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback by the Crypto Engine indicating that a new 283k1 ephemeral + * public/private key pair has been generated. The public/private key pair is + * stored on the NCP, but only the associated public key is returned to the + * host. The node's associated certificate is also returned. + * @param status The result of the CBKE operation. + * @param ephemeralPublicKey EmberPublicKey283k1Data * The generated ephemeral public key. + */ + ezspGenerateCbkeKeysHandler283k1(status: EmberStatus, ephemeralPublicKey: EmberPublicKey283k1Data): void { + debug(`ezspGenerateCbkeKeysHandler283k1(): callback called with: [status=${EmberStatus[status]}], ` + + `[ephemeralPublicKey=${ephemeralPublicKey}]`); + } + + /** + * Calculates the SMAC verification keys for both the initiator and responder + * roles of CBKE for the 283k1 ECC curve using the passed parameters and the + * stored public/private key pair previously generated with + * ezspGenerateKeysRetrieveCert283k1(). It also stores the unverified link key + * data in temporary storage on the NCP until the key establishment is complete. + * @param amInitiator The role of this device in the Key Establishment protocol. + * @param partnerCertificate EmberCertificate283k1Data * The key establishment partner's implicit certificate. + * @param partnerEphemeralPublicKey EmberPublicKey283k1Data * The key establishment partner's ephemeral public key + */ + async ezspCalculateSmacs283k1(amInitiator: boolean, partnerCertificate: EmberCertificate283k1Data, + partnerEphemeralPublicKey: EmberPublicKey283k1Data): Promise { + this.startCommand(EzspFrameID.CALCULATE_SMACS283K1); + this.buffalo.writeUInt8(amInitiator ? 1 : 0); + this.buffalo.writeEmberCertificate283k1Data(partnerCertificate); + this.buffalo.writeEmberPublicKey283k1Data(partnerEphemeralPublicKey); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback to indicate that the NCP has finished calculating the Secure + * Message Authentication Codes (SMAC) for both the initiator and responder for + * the CBKE 283k1 Library. The associated link key is kept in temporary storage + * until the host tells the NCP to store or discard the key via + * emberClearTemporaryDataMaybeStoreLinkKey(). + * @param status The Result of the CBKE operation. + * @param initiatorSmac EmberSmacData * The calculated value of the initiator's SMAC + * @param responderSmac EmberSmacData * The calculated value of the responder's SMAC + */ + ezspCalculateSmacsHandler283k1(status: EmberStatus, initiatorSmac: EmberSmacData, responderSmac: EmberSmacData): void { + debug(`ezspCalculateSmacsHandler283k1(): callback called with: [status=${EmberStatus[status]}], [initiatorSmac=${initiatorSmac}], ` + + `[responderSmac=${responderSmac}]`); + } + + /** + * Clears the temporary data associated with CBKE and the key establishment, + * most notably the ephemeral public/private key pair. If storeLinKey is true it + * moves the unverified link key stored in temporary storage into the link key + * table. Otherwise it discards the key. + * @param storeLinkKey A bool indicating whether to store (true) or discard (false) the unverified link + * key derived when ezspCalculateSmacs() was previously called. + */ + async ezspClearTemporaryDataMaybeStoreLinkKey(storeLinkKey: boolean): Promise { + this.startCommand(EzspFrameID.CLEAR_TEMPORARY_DATA_MAYBE_STORE_LINK_KEY); + this.buffalo.writeUInt8(storeLinkKey ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Clears the temporary data associated with CBKE and the key establishment, + * most notably the ephemeral public/private key pair. If storeLinKey is true it + * moves the unverified link key stored in temporary storage into the link key + * table. Otherwise it discards the key. + * @param storeLinkKey A bool indicating whether to store (true) or discard (false) the unverified link + * key derived when ezspCalculateSmacs() was previously called. + */ + async ezspClearTemporaryDataMaybeStoreLinkKey283k1(storeLinkKey: boolean): Promise { + this.startCommand(EzspFrameID.CLEAR_TEMPORARY_DATA_MAYBE_STORE_LINK_KEY283K1); + this.buffalo.writeUInt8(storeLinkKey ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Retrieves the certificate installed on the NCP. + * @returns EmberCertificateData * The locally installed certificate. + */ + async ezspGetCertificate(): Promise<[EmberStatus, localCert: EmberCertificateData]> { + this.startCommand(EzspFrameID.GET_CERTIFICATE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const localCert = this.buffalo.readEmberCertificateData(); + + return [status, localCert]; + } + + /** + * Retrieves the 283k certificate installed on the NCP. + * @returns EmberCertificate283k1Data * The locally installed certificate. + */ + async ezspGetCertificate283k1(): Promise<[EmberStatus, localCert: EmberCertificate283k1Data]> { + this.startCommand(EzspFrameID.GET_CERTIFICATE283K1); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const localCert = this.buffalo.readEmberCertificate283k1Data(); + + return [status, localCert]; + } + + /** + * LEGACY FUNCTION: This functionality has been replaced by a single bit in the + * EmberApsFrame, EMBER_APS_OPTION_DSA_SIGN. Devices wishing to send signed + * messages should use that as it requires fewer function calls and message + * buffering. The dsaSignHandler response is still called when + * EMBER_APS_OPTION_DSA_SIGN is used. However, this function is still supported. + * This function begins the process of signing the passed message contained + * within the messageContents array. If no other ECC operation is going on, it + * will immediately return with EMBER_OPERATION_IN_PROGRESS to indicate the + * start of ECC operation. It will delay a period of time to let APS retries + * take place, but then it will shut down the radio and consume the CPU + * processing until the signing is complete. This may take up to 1 second. The + * signed message will be returned in the dsaSignHandler response. Note that the + * last byte of the messageContents passed to this function has special + * significance. As the typical use case for DSA signing is to sign the ZCL + * payload of a DRLC Report Event Status message in SE 1.0, there is often both + * a signed portion (ZCL payload) and an unsigned portion (ZCL header). The last + * byte in the content of messageToSign is therefore used as a special indicator + * to signify how many bytes of leading data in the array should be excluded + * from consideration during the signing process. If the signature needs to + * cover the entire array (all bytes except last one), the caller should ensure + * that the last byte of messageContents is 0x00. When the signature operation + * is complete, this final byte will be replaced by the signature type indicator + * (0x01 for ECDSA signatures), and the actual signature will be appended to the + * original contents after this byte. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The message contents for which to create a signature. + * Per above notes, this may include a leading portion of data not included in the signature, + * in which case the last byte of this array should be set to the index of the first byte + * to be considered for signing. Otherwise, the last byte of messageContents should be 0x00 + * to indicate that a signature should occur across the entire contents. + * @returns EMBER_OPERATION_IN_PROGRESS if the stack has queued up the operation + * for execution. EMBER_INVALID_CALL if the operation can't be performed in this + * context, possibly because another ECC operation is pending. + */ + async ezspDsaSign(messageContents: Buffer): Promise { + this.startCommand(EzspFrameID.DSA_SIGN); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * The handler that returns the results of the signing operation. On success, + * the signature will be appended to the original message (including the + * signature type indicator that replaced the startIndex field for the signing) + * and both are returned via this callback. + * @param status The result of the DSA signing operation. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t *The message and attached which includes the original message and the appended signature. + */ + ezspDsaSignHandler(status: EmberStatus, messageContents: Buffer): void { + debug(`ezspDsaSignHandler(): callback called with: [status=${EmberStatus[status]}], [messageContents=${messageContents.toString('hex')}]`); + } + + /** + * Verify that signature of the associated message digest was signed by the + * private key of the associated certificate. + * @param digest EmberMessageDigest * The AES-MMO message digest of the signed data. + * If dsaSign command was used to generate the signature for this data, the final byte (replaced by signature type of 0x01) + * in the messageContents array passed to dsaSign is included in the hash context used for the digest calculation. + * @param signerCertificate EmberCertificateData * The certificate of the signer. Note that the signer's certificate and the verifier's + * certificate must both be issued by the same Certificate Authority, so they should share the same CA Public Key. + * @param receivedSig EmberSignatureData * The signature of the signed data. + */ + async ezspDsaVerify(digest: EmberMessageDigest, signerCertificate: EmberCertificateData, receivedSig: EmberSignatureData): Promise { + this.startCommand(EzspFrameID.DSA_VERIFY); + this.buffalo.writeEmberMessageDigest(digest); + this.buffalo.writeEmberCertificateData(signerCertificate); + this.buffalo.writeEmberSignatureData(receivedSig); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * This callback is executed by the stack when the DSA verification has + * completed and has a result. If the result is EMBER_SUCCESS, the signature is + * valid. If the result is EMBER_SIGNATURE_VERIFY_FAILURE then the signature is + * invalid. If the result is anything else then the signature verify operation + * failed and the validity is unknown. + * @param status The result of the DSA verification operation. + */ + ezspDsaVerifyHandler(status: EmberStatus): void { + debug(`ezspDsaVerifyHandler(): callback called with: [status=${EmberStatus[status]}]`); + } + + /** + * Verify that signature of the associated message digest was signed by the + * private key of the associated certificate. + * @param digest EmberMessageDigest * The AES-MMO message digest of the signed data. + * If dsaSign command was used to generate the signature for this data, the final byte (replaced by signature type of 0x01) + * in the messageContents array passed to dsaSign is included in the hash context used for the digest calculation. + * @param signerCertificate EmberCertificate283k1Data * The certificate of the signer. Note that the signer's certificate and the verifier's + * certificate must both be issued by the same Certificate Authority, so they should share the same CA Public Key. + * @param receivedSig EmberSignature283k1Data * The signature of the signed data. + */ + async ezspDsaVerify283k1(digest: EmberMessageDigest, signerCertificate: EmberCertificate283k1Data, receivedSig: EmberSignature283k1Data) + : Promise { + this.startCommand(EzspFrameID.DSA_VERIFY283K1); + this.buffalo.writeEmberMessageDigest(digest); + this.buffalo.writeEmberCertificate283k1Data(signerCertificate); + this.buffalo.writeEmberSignature283k1Data(receivedSig); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the device's CA public key, local certificate, and static private key on + * the NCP associated with this node. + * @param caPublic EmberPublicKeyData * The Certificate Authority's public key. + * @param myCert EmberCertificateData * The node's new certificate signed by the CA. + * @param myKey EmberPrivateKeyData *The node's new static private key. + */ + async ezspSetPreinstalledCbkeData(caPublic: EmberPublicKeyData, myCert: EmberCertificateData, myKey: EmberPrivateKeyData): + Promise { + this.startCommand(EzspFrameID.SET_PREINSTALLED_CBKE_DATA); + this.buffalo.writeEmberPublicKeyData(caPublic); + this.buffalo.writeEmberCertificateData(myCert); + this.buffalo.writeEmberPrivateKeyData(myKey); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the device's 283k1 curve CA public key, local certificate, and static + * private key on the NCP associated with this node. + * @returns Status of operation + */ + async ezspSavePreinstalledCbkeData283k1(): Promise { + this.startCommand(EzspFrameID.SAVE_PREINSTALLED_CBKE_DATA283K1); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + //----------------------------------------------------------------------------- + // Mfglib Frames + //----------------------------------------------------------------------------- + + /** + * Activate use of mfglib test routines and enables the radio receiver to report + * packets it receives to the mfgLibRxHandler() callback. These packets will not + * be passed up with a CRC failure. All other mfglib functions will return an + * error until the mfglibStart() has been called + * @param rxCallback true to generate a mfglibRxHandler callback when a packet is received. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspMfglibStart(rxCallback: boolean): Promise { + this.startCommand(EzspFrameID.MFGLIB_START); + this.buffalo.writeUInt8(rxCallback ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Deactivate use of mfglib test routines; restores the hardware to the state it + * was in prior to mfglibStart() and stops receiving packets started by + * mfglibStart() at the same time. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibEnd(): Promise { + this.startCommand(EzspFrameID.MFGLIB_END); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Starts transmitting an unmodulated tone on the currently set channel and + * power level. Upon successful return, the tone will be transmitting. To stop + * transmitting tone, application must call mfglibStopTone(), allowing it the + * flexibility to determine its own criteria for tone duration (time, event, + * etc.) + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibStartTone(): Promise { + this.startCommand(EzspFrameID.MFGLIB_START_TONE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Stops transmitting tone started by mfglibStartTone(). + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibStopTone(): Promise { + this.startCommand(EzspFrameID.MFGLIB_STOP_TONE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Starts transmitting a random stream of characters. This is so that the radio + * modulation can be measured. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibStartStream(): Promise { + this.startCommand(EzspFrameID.MFGLIB_START_STREAM); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Stops transmitting a random stream of characters started by + * mfglibStartStream(). + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibStopStream(): Promise { + this.startCommand(EzspFrameID.MFGLIB_STOP_STREAM); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sends a single packet consisting of the following bytes: packetLength, + * packetContents[0], ... , packetContents[packetLength - 3], CRC[0], CRC[1]. + * The total number of bytes sent is packetLength + 1. The radio replaces the + * last two bytes of packetContents[] with the 16-bit CRC for the packet. + * @param packetLength uint8_t The length of the packetContents parameter in bytes. Must be greater than 3 and less than 123. + * @param packetContents uint8_t * The packet to send. The last two bytes will be replaced with the 16-bit CRC. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibSendPacket(packetContents: Buffer): Promise { + this.startCommand(EzspFrameID.MFGLIB_SEND_PACKET); + this.buffalo.writePayload(packetContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Sets the radio channel. Calibration occurs if this is the first time the + * channel has been used. + * @param channel uint8_t The channel to switch to. Valid values are 11 - 26. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibSetChannel(channel: number): Promise { + this.startCommand(EzspFrameID.MFGLIB_SET_CHANNEL); + this.buffalo.writeUInt8(channel); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Returns the current radio channel, as previously set via mfglibSetChannel(). + * @returns uint8_t The current channel. + */ + async mfglibGetChannel(): Promise { + this.startCommand(EzspFrameID.MFGLIB_GET_CHANNEL); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const channel = this.buffalo.readUInt8(); + return channel; + } + + /** + * First select the transmit power mode, and then include a method for selecting + * the radio transmit power. The valid power settings depend upon the specific + * radio in use. Ember radios have discrete power settings, and then requested + * power is rounded to a valid power setting; the actual power output is + * available to the caller via mfglibGetPower(). + * @param txPowerMode uint16_t Power mode. Refer to txPowerModes in stack/include/ember-types.h for possible values. + * @param power int8_t Power in units of dBm. Refer to radio data sheet for valid range. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async mfglibSetPower(txPowerMode: EmberTXPowerMode, power: number): Promise { + this.startCommand(EzspFrameID.MFGLIB_SET_POWER); + this.buffalo.writeUInt16(txPowerMode); + this.buffalo.writeUInt8(power); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Returns the current radio power setting, as previously set via mfglibSetPower(). + * @returns int8_t Power in units of dBm. Refer to radio data sheet for valid range. + */ + async mfglibGetPower(): Promise { + this.startCommand(EzspFrameID.MFGLIB_GET_POWER); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const power = this.buffalo.readUInt8(); + return power; + } + + /** + * Callback + * A callback indicating a packet with a valid CRC has been received. + * @param linkQuality uint8_t The link quality observed during the reception + * @param rssi int8_t The energy level (in units of dBm) observed during the reception. + * @param packetLength uint8_t The length of the packetContents parameter in bytes. Will be greater than 3 and less than 123. + * @param packetContents uint8_t * The received packet (last 2 bytes are not FCS / CRC and may be discarded) + */ + ezspMfglibRxHandler(linkQuality: number, rssi: number, packetLength: number, packetContents: number[]): void { + debug(`ezspMfglibRxHandler(): callback called with: [linkQuality=${linkQuality}], [rssi=${rssi}], ` + + `[packetLength=${packetLength}], [packetContents=${packetContents}]`); + // gecko_sdk_4.4.0\protocol\zigbee\app\framework\plugin\manufacturing-library-cli\manufacturing-library-cli-host.c + } + + //----------------------------------------------------------------------------- + // Bootloader Frames + //----------------------------------------------------------------------------- + + /** + * Quits the current application and launches the standalone bootloader (if + * installed) The function returns an error if the standalone bootloader is not + * present + * @param mode uint8_t Controls the mode in which the standalone bootloader will run. See the app. note for full details. + * Options are: STANDALONE_BOOTLOADER_NORMAL_MODE: Will listen for an over-the-air image transfer on the current + * channel with current power settings. STANDALONE_BOOTLOADER_RECOVERY_MODE: Will listen for an over-the-air image + * transfer on the default channel with default power settings. Both modes also allow an image transfer to begin + * with XMODEM over the serial protocol's Bootloader Frame. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspLaunchStandaloneBootloader(mode: number): Promise { + this.startCommand(EzspFrameID.LAUNCH_STANDALONE_BOOTLOADER); + this.buffalo.writeUInt8(mode); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Transmits the given bootload message to a neighboring node using a specific + * 802.15.4 header that allows the EmberZNet stack as well as the bootloader to + * recognize the message, but will not interfere with other ZigBee stacks. + * @param broadcast If true, the destination address and pan id are both set to the broadcast address. + * @param destEui64 The EUI64 of the target node. Ignored if the broadcast field is set to true. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The multicast message. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSendBootloadMessage(broadcast: boolean, destEui64: EmberEUI64, messageContents: Buffer): + Promise { + this.startCommand(EzspFrameID.SEND_BOOTLOAD_MESSAGE); + this.buffalo.writeUInt8(broadcast ? 1 : 0); + this.buffalo.writeIeeeAddr(destEui64); + this.buffalo.writePayload(messageContents); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Detects if the standalone bootloader is installed, and if so returns the + * installed version. If not return 0xffff. A returned version of 0x1234 would + * indicate version 1.2 build 34. Also return the node's version of PLAT, MICRO + * and PHY. + * @returns uint16_t BOOTLOADER_INVALID_VERSION if the standalone bootloader is not present, + * or the version of the installed standalone bootloader. + * @returns uint8_t * The value of PLAT on the node + * @returns uint8_t * The value of MICRO on the node + * @returns uint8_t * The value of PHY on the node + */ + async ezspGetStandaloneBootloaderVersionPlatMicroPhy(): Promise<[number, nodePlat: number, nodeMicro: number, nodePhy: number]> { + this.startCommand(EzspFrameID.GET_STANDALONE_BOOTLOADER_VERSION_PLAT_MICRO_PHY); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const bootloader_version = this.buffalo.readUInt16(); + const nodePlat = this.buffalo.readUInt8(); + const nodeMicro = this.buffalo.readUInt8(); + const nodePhy = this.buffalo.readUInt8(); + + return [bootloader_version, nodePlat, nodeMicro, nodePhy]; + } + + /** + * Callback + * A callback invoked by the EmberZNet stack when a bootload message is + * received. + * @param longId The EUI64 of the sending node. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the message. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during the reception. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t *The bootload message that was sent. + */ + ezspIncomingBootloadMessageHandler(longId: EmberEUI64, lastHopLqi: number, lastHopRssi: number, messageContents: Buffer): void { + debug(`ezspIncomingBootloadMessageHandler(): callback called with: [longId=${longId}], [lastHopLqi=${lastHopLqi}], ` + + `[lastHopRssi=${lastHopRssi}], [messageContents=${messageContents.toString('hex')}]`); + } + + /** + * Callback + * A callback invoked by the EmberZNet stack when the MAC has finished + * transmitting a bootload message. + * @param status An EmberStatus value of EMBER_SUCCESS if an ACK was received from the destination + * or EMBER_DELIVERY_FAILED if no ACK was received. + * @param messageLength uint8_t The length of the messageContents parameter in bytes. + * @param messageContents uint8_t * The message that was sent. + */ + ezspBootloadTransmitCompleteHandler(status: EmberStatus, messageContents: Buffer): void { + debug(`ezspBootloadTransmitCompleteHandler(): callback called with: [status=${EmberStatus[status]}], ` + + `[messageContents=${messageContents.toString('hex')}]`); + } + + /** + * Perform AES encryption on plaintext using key. + * @param uint8_t * 16 bytes of plaintext. + * @param uint8_t * The 16-byte encryption key to use. + * @returns uint8_t * 16 bytes of ciphertext. + */ + async ezspAesEncrypt(plaintext: number[], key: number[]): Promise { + this.startCommand(EzspFrameID.AES_ENCRYPT); + this.buffalo.writeListUInt8(plaintext); + this.buffalo.writeListUInt8(key); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const ciphertext = this.buffalo.readListUInt8({length: 16}); + + return ciphertext; + } + + //----------------------------------------------------------------------------- + // ZLL Frames + //----------------------------------------------------------------------------- + + /** + * A consolidation of ZLL network operations with similar signatures; + * specifically, forming and joining networks or touch-linking. + * @param networkInfo EmberZllNetwork * Information about the network. + * @param op Operation indicator. + * @param radioTxPower int8_t Radio transmission power. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspZllNetworkOps(networkInfo: EmberZllNetwork, op: EzspZllNetworkOperation, radioTxPower: number): Promise { + this.startCommand(EzspFrameID.ZLL_NETWORK_OPS); + this.buffalo.writeEmberZllNetwork(networkInfo); + this.buffalo.writeUInt8(op); + this.buffalo.writeUInt8(radioTxPower); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This call will cause the device to setup the security information used in its + * network. It must be called prior to forming, starting, or joining a network. + * @param networkKey EmberKeyData * ZLL Network key. + * @param securityState EmberZllInitialSecurityState * Initial security state of the network. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspZllSetInitialSecurityState(networkKey: EmberKeyData, securityState: EmberZllInitialSecurityState): Promise { + this.startCommand(EzspFrameID.ZLL_SET_INITIAL_SECURITY_STATE); + this.buffalo.writeEmberKeyData(networkKey); + this.buffalo.writeEmberZllInitialSecurityState(securityState); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This call will update ZLL security token information. Unlike + * emberZllSetInitialSecurityState, this can be called while a network is + * already established. + * @param securityState EmberZllInitialSecurityState * Security state of the network. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspZllSetSecurityStateWithoutKey(securityState: EmberZllInitialSecurityState): Promise { + this.startCommand(EzspFrameID.ZLL_SET_SECURITY_STATE_WITHOUT_KEY); + this.buffalo.writeEmberZllInitialSecurityState(securityState); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This call will initiate a ZLL network scan on all the specified channels. + * @param channelMask uint32_t The range of channels to scan. + * @param radioPowerForScan int8_t The radio output power used for the scan requests. + * @param nodeType The node type of the local device. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspZllStartScan(channelMask: number, radioPowerForScan: number, nodeType: EmberNodeType): Promise { + this.startCommand(EzspFrameID.ZLL_START_SCAN); + this.buffalo.writeUInt32(channelMask); + this.buffalo.writeUInt8(radioPowerForScan); + this.buffalo.writeUInt8(nodeType); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * This call will change the mode of the radio so that the receiver is on for a + * specified amount of time when the device is idle. + * @param durationMs uint32_t The duration in milliseconds to leave the radio on. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspZllSetRxOnWhenIdle(durationMs: number): Promise { + this.startCommand(EzspFrameID.ZLL_SET_RX_ON_WHEN_IDLE); + this.buffalo.writeUInt32(durationMs); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * This call is fired when a ZLL network scan finds a ZLL network. + * @param networkInfo EmberZllNetwork * Information about the network. + * @param isDeviceInfoNull Used to interpret deviceInfo field. + * @param deviceInfo EmberZllDeviceInfoRecord * Device specific information. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the message. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during reception. + */ + ezspZllNetworkFoundHandler(networkInfo: EmberZllNetwork, isDeviceInfoNull: boolean, deviceInfo: EmberZllDeviceInfoRecord, + lastHopLqi: number, lastHopRssi: number): void { + debug(`ezspZllNetworkFoundHandler(): callback called with: [networkInfo=${networkInfo}], [isDeviceInfoNull=${isDeviceInfoNull}], ` + + `[deviceInfo=${deviceInfo}], [lastHopLqi=${lastHopLqi}], [lastHopRssi=${lastHopRssi}]`); + } + + /** + * Callback + * This call is fired when a ZLL network scan is complete. + * @param status Status of the operation. + */ + ezspZllScanCompleteHandler(status: EmberStatus): void { + debug(`ezspZllScanCompleteHandler(): callback called with: [status=${EmberStatus[status]}]`); + } + + /** + * Callback + * This call is fired when network and group addresses are assigned to a remote + * mode in a network start or network join request. + * @param addressInfo EmberZllAddressAssignment * Address assignment information. + * @param lastHopLqi uint8_t The link quality from the node that last relayed the message. + * @param lastHopRssi int8_t The energy level (in units of dBm) observed during reception. + */ + ezspZllAddressAssignmentHandler(addressInfo: EmberZllAddressAssignment, lastHopLqi: number, lastHopRssi: number): void { + debug(`ezspZllAddressAssignmentHandler(): callback called with: [addressInfo=${addressInfo}], [lastHopLqi=${lastHopLqi}], ` + + `[lastHopRssi=${lastHopRssi}]`); + } + + /** + * Callback + * This call is fired when the device is a target of a touch link. + * @param networkInfo EmberZllNetwork * Information about the network. + */ + ezspZllTouchLinkTargetHandler(networkInfo: EmberZllNetwork): void { + debug(`ezspZllTouchLinkTargetHandler(): callback called with: [networkInfo=${networkInfo}]`); + } + + /** + * Get the ZLL tokens. + * @returns EmberTokTypeStackZllData * Data token return value. + * @returns EmberTokTypeStackZllSecurity * Security token return value. + */ + async ezspZllGetTokens(): Promise<[data: EmberTokTypeStackZllData, security: EmberTokTypeStackZllSecurity]> { + this.startCommand(EzspFrameID.ZLL_GET_TOKENS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const data = this.buffalo.readEmberTokTypeStackZllData(); + const security = this.buffalo.readEmberTokTypeStackZllSecurity(); + + return [data, security]; + } + + /** + * Set the ZLL data token. + * @param data EmberTokTypeStackZllData * Data token to be set. + */ + async ezspZllSetDataToken(data: EmberTokTypeStackZllData): Promise { + this.startCommand(EzspFrameID.ZLL_SET_DATA_TOKEN); + this.buffalo.writeEmberTokTypeStackZllData(data); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Set the ZLL data token bitmask to reflect the ZLL network state. + */ + async ezspZllSetNonZllNetwork(): Promise { + this.startCommand(EzspFrameID.ZLL_SET_NON_ZLL_NETWORK); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Is this a ZLL network? + * @returns ZLL network? + */ + async ezspIsZllNetwork(): Promise { + this.startCommand(EzspFrameID.IS_ZLL_NETWORK); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const isZllNetwork = this.buffalo.readUInt8() === 1 ? true : false; + + return isZllNetwork; + } + + /** + * This call sets the radio's default idle power mode. + * @param mode The power mode to be set. + */ + async ezspZllSetRadioIdleMode(mode: number): Promise { + this.startCommand(EzspFrameID.ZLL_SET_RADIO_IDLE_MODE); + this.buffalo.writeUInt8(mode); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * This call gets the radio's default idle power mode. + * @returns uint8_t The current power mode. + */ + async ezspZllGetRadioIdleMode(): Promise { + this.startCommand(EzspFrameID.ZLL_GET_RADIO_IDLE_MODE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const radioIdleMode = this.buffalo.readUInt8(); + + return radioIdleMode; + } + + /** + * This call sets the default node type for a factory new ZLL device. + * @param nodeType The node type to be set. + */ + async ezspSetZllNodeType(nodeType: EmberNodeType): Promise { + this.startCommand(EzspFrameID.SET_ZLL_NODE_TYPE); + this.buffalo.writeUInt8(nodeType); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * This call sets additional capability bits in the ZLL state. + * @param uint16_t A mask with the bits to be set or cleared. + */ + async ezspSetZllAdditionalState(state: number): Promise { + this.startCommand(EzspFrameID.SET_ZLL_ADDITIONAL_STATE); + this.buffalo.writeUInt16(state); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Is there a ZLL (Touchlink) operation in progress? + * @returns ZLL operation in progress? false on error + */ + async ezspZllOperationInProgress(): Promise { + this.startCommand(EzspFrameID.ZLL_OPERATION_IN_PROGRESS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const zllOperationInProgress = this.buffalo.readUInt8() === 1 ? true : false; + return zllOperationInProgress; + } + + /** + * Is the ZLL radio on when idle mode is active? + * @returns ZLL radio on when idle mode is active? false on error + */ + async ezspZllRxOnWhenIdleGetActive(): Promise { + this.startCommand(EzspFrameID.ZLL_RX_ON_WHEN_IDLE_GET_ACTIVE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const zllRxOnWhenIdleGetActive = this.buffalo.readUInt8() === 1 ? true : false; + + return zllRxOnWhenIdleGetActive; + } + + /** + * Informs the ZLL API that application scanning is complete + */ + async ezspZllScanningComplete(): Promise { + this.startCommand(EzspFrameID.ZLL_SCANNING_COMPLETE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Get the primary ZLL (touchlink) channel mask. + * @returns uint32_t The primary ZLL channel mask + */ + async ezspGetZllPrimaryChannelMask(): Promise { + this.startCommand(EzspFrameID.GET_ZLL_PRIMARY_CHANNEL_MASK); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const zllPrimaryChannelMask = this.buffalo.readUInt32(); + + return zllPrimaryChannelMask; + } + + /** + * Get the secondary ZLL (touchlink) channel mask. + * @returns uint32_t The secondary ZLL channel mask + */ + async ezspGetZllSecondaryChannelMask(): Promise { + this.startCommand(EzspFrameID.GET_ZLL_SECONDARY_CHANNEL_MASK); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const zllSecondaryChannelMask = this.buffalo.readUInt32(); + + return zllSecondaryChannelMask; + } + + /** + * Set the primary ZLL (touchlink) channel mask + * @param uint32_t The primary ZLL channel mask + */ + async ezspSetZllPrimaryChannelMask(zllPrimaryChannelMask: number): Promise { + + this.startCommand(EzspFrameID.SET_ZLL_PRIMARY_CHANNEL_MASK); + this.buffalo.writeUInt32(zllPrimaryChannelMask); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Set the secondary ZLL (touchlink) channel mask. + * @param uint32_t The secondary ZLL channel mask + */ + async ezspSetZllSecondaryChannelMask(zllSecondaryChannelMask: number): Promise { + + this.startCommand(EzspFrameID.SET_ZLL_SECONDARY_CHANNEL_MASK); + this.buffalo.writeUInt32(zllSecondaryChannelMask); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Clear ZLL stack tokens. + */ + async ezspZllClearTokens(): Promise { + this.startCommand(EzspFrameID.ZLL_CLEAR_TOKENS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + //----------------------------------------------------------------------------- + // WWAH Frames + //----------------------------------------------------------------------------- + + /** + * Sets whether to use parent classification when processing beacons during a + * join or rejoin. Parent classification considers whether a received beacon + * indicates trust center connectivity and long uptime on the network + * @param enabled Enable or disable parent classification + */ + async ezspSetParentClassificationEnabled(enabled: boolean): Promise { + this.startCommand(EzspFrameID.SET_PARENT_CLASSIFICATION_ENABLED); + this.buffalo.writeUInt8(enabled ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Gets whether to use parent classification when processing beacons during a + * join or rejoin. Parent classification considers whether a received beacon + * indicates trust center connectivity and long uptime on the network + * @returns Enable or disable parent classification + */ + async ezspGetParentClassificationEnabled(): Promise { + this.startCommand(EzspFrameID.GET_PARENT_CLASSIFICATION_ENABLED); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const enabled = this.buffalo.readUInt8() === 1 ? true : false; + + return enabled; + } + + /** + * sets the device uptime to be long or short + * @param hasLongUpTime if the uptime is long or not + */ + async ezspSetLongUpTime(hasLongUpTime: boolean): Promise { + this.startCommand(EzspFrameID.SET_LONG_UP_TIME); + this.buffalo.writeUInt8(hasLongUpTime ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * sets the hub connectivity to be true or false + * @param connected if the hub is connected or not + */ + async ezspSetHubConnectivity(connected: boolean): Promise { + this.startCommand(EzspFrameID.SET_HUB_CONNECTIVITY); + this.buffalo.writeUInt8(connected ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * checks if the device uptime is long or short + * @returns if the uptime is long or not + */ + async ezspIsUpTimeLong(): Promise { + this.startCommand(EzspFrameID.IS_UP_TIME_LONG); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const hasLongUpTime = this.buffalo.readUInt8() === 1 ? true : false; + + return hasLongUpTime; + } + + /** + * checks if the hub is connected or not + * @returns if the hub is connected or not + */ + async ezspIsHubConnected(): Promise { + this.startCommand(EzspFrameID.IS_HUB_CONNECTED); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const isHubConnected = this.buffalo.readUInt8() === 1 ? true : false; + + return isHubConnected; + } + + //----------------------------------------------------------------------------- + // Green Power Frames + //----------------------------------------------------------------------------- + + /** + * Update the GP Proxy table based on a GP pairing. + * @param options uint32_t The options field of the GP Pairing command. + * @param addr EmberGpAddress * The target GPD. + * @param commMode uint8_t The communication mode of the GP Sink. + * @param sinkNetworkAddress uint16_t The network address of the GP Sink. + * @param sinkGroupId uint16_t The group ID of the GP Sink. + * @param assignedAlias uint16_t The alias assigned to the GPD. + * @param sinkIeeeAddress uint8_t * The IEEE address of the GP Sink. + * @param gpdKey EmberKeyData * The key to use for the target GPD. + * @param gpdSecurityFrameCounter uint32_t The GPD security frame counter. + * @param forwardingRadius uint8_t The forwarding radius. + * @returns Whether a GP Pairing has been created or not. + */ + async ezspGpProxyTableProcessGpPairing(options: number, addr: EmberGpAddress, commMode: number, sinkNetworkAddress: number, + sinkGroupId: number, assignedAlias: number, sinkIeeeAddress: EmberEUI64, gpdKey: EmberKeyData, gpdSecurityFrameCounter: number, + forwardingRadius: number): Promise { + this.startCommand(EzspFrameID.GP_PROXY_TABLE_PROCESS_GP_PAIRING); + this.buffalo.writeUInt32(options); + this.buffalo.writeEmberGpAddress(addr); + this.buffalo.writeUInt8(commMode); + this.buffalo.writeUInt16(sinkNetworkAddress); + this.buffalo.writeUInt16(sinkGroupId); + this.buffalo.writeUInt16(assignedAlias); + this.buffalo.writeIeeeAddr(sinkIeeeAddress); + this.buffalo.writeEmberKeyData(gpdKey); + this.buffalo.writeUInt32(gpdSecurityFrameCounter); + this.buffalo.writeUInt8(forwardingRadius); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const gpPairingAdded = this.buffalo.readUInt8() === 1 ? true : false; + + return gpPairingAdded; + } + + /** + * Adds/removes an entry from the GP Tx Queue. + * @param action The action to perform on the GP TX queue (true to add, false to remove). + * @param useCca Whether to use ClearChannelAssessment when transmitting the GPDF. + * @param addr EmberGpAddress * The Address of the destination GPD. + * @param gpdCommandId uint8_t The GPD command ID to send. + * @param gpdAsdu uint8_t * The GP command payload. + * @param gpepHandle uint8_t The handle to refer to the GPDF. + * @param gpTxQueueEntryLifetimeMs uint16_t How long to keep the GPDF in the TX Queue. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspDGpSend(action: boolean, useCca: boolean, addr: EmberGpAddress, gpdCommandId: number, gpdAsdu: Buffer, + gpepHandle: number, gpTxQueueEntryLifetimeMs: number): Promise { + this.startCommand(EzspFrameID.D_GP_SEND); + this.buffalo.writeUInt8(action ? 1 : 0); + this.buffalo.writeUInt8(useCca ? 1 : 0); + this.buffalo.writeEmberGpAddress(addr); + this.buffalo.writeUInt8(gpdCommandId); + this.buffalo.writePayload(gpdAsdu); + this.buffalo.writeUInt8(gpepHandle); + this.buffalo.writeUInt16(gpTxQueueEntryLifetimeMs); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Callback + * A callback to the GP endpoint to indicate the result of the GPDF + * transmission. + * @param status An EmberStatus value indicating success or the reason for failure. + * @param gpepHandle uint8_t The handle of the GPDF. + */ + ezspDGpSentHandler(status: EmberStatus, gpepHandle: number): void { + debug(`ezspDGpSentHandler(): callback called with: [status=${EmberStatus[status]}], [gpepHandle=${gpepHandle}]`); + } + + /** + * Callback + * A callback invoked by the ZigBee GP stack when a GPDF is received. + * @param status The status of the GPDF receive. + * @param gpdLink uint8_t The gpdLink value of the received GPDF. + * @param sequenceNumber uint8_t The GPDF sequence number. + * @param addr EmberGpAddress *The address of the source GPD. + * @param gpdfSecurityLevel The security level of the received GPDF. + * @param gpdfSecurityKeyType The securityKeyType used to decrypt/authenticate the incoming GPDF. + * @param autoCommissioning Whether the incoming GPDF had the auto-commissioning bit set. + * @param bidirectionalInfo uint8_t Bidirectional information represented in bitfields, + * where bit0 holds the rxAfterTx of incoming gpdf and bit1 holds if tx queue is available for outgoing gpdf. + * @param gpdSecurityFrameCounter uint32_t The security frame counter of the incoming GDPF. + * @param gpdCommandId uint8_t The gpdCommandId of the incoming GPDF. + * @param mic uint32_t The received MIC of the GPDF. + * @param proxyTableIndex uint8_tThe proxy table index of the corresponding proxy table entry to the incoming GPDF. + * @param gpdCommandPayload uint8_t * The GPD command payload. + */ + ezspGpepIncomingMessageHandler(status: EmberStatus, gpdLink: number, sequenceNumber: number, addr: EmberGpAddress, + gpdfSecurityLevel: EmberGpSecurityLevel, gpdfSecurityKeyType: EmberGpKeyType, autoCommissioning: boolean, bidirectionalInfo: number, + gpdSecurityFrameCounter: number, gpdCommandId: number, mic: number, proxyTableIndex: number, gpdCommandPayload: Buffer): void { + debug(`ezspGpepIncomingMessageHandler(): callback called with: [status=${EmberStatus[status]}], [gpdLink=${gpdLink}], ` + + `[sequenceNumber=${sequenceNumber}], [addr=${addr}], [gpdfSecurityLevel=${gpdfSecurityLevel}], ` + + `[gpdfSecurityKeyType=${gpdfSecurityKeyType}], [autoCommissioning=${autoCommissioning}], [bidirectionalInfo=${bidirectionalInfo}], ` + + `[gpdSecurityFrameCounter=${gpdSecurityFrameCounter}], [gpdCommandId=${gpdCommandId}], [mic=${mic}], ` + + `[proxyTableIndex=${proxyTableIndex}], [gpdCommandPayload=${gpdCommandPayload}]`); + + // TODO: triple-checking required here + if (gpdCommandPayload.length) { + const gpdBuffalo = new EzspBuffalo(gpdCommandPayload, 0); + + switch (gpdCommandId) { + case 0xE0: { + // commissioning notification + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const st = gpdBuffalo.readUInt8(); + const deviceId = gpdBuffalo.readUInt8(); + const options = gpdBuffalo.readUInt8(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const extOptions = gpdBuffalo.readUInt8(); + const key = gpdBuffalo.readEmberKeyData(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const mic = gpdBuffalo.readUInt32(); + const counter = gpdBuffalo.readUInt32(); + + this.emit( + EzspEvents.GREENPOWER_MESSAGE, + (addr.applicationId === EmberGpApplicationId.SOURCE_ID) ? addr.sourceId : addr.gpdIeeeAddress, + gpdCommandId, + gpdLink, + sequenceNumber, + deviceId, + options, + key, + counter + ); + break; + } + default: { + // notification + this.emit( + EzspEvents.GREENPOWER_MESSAGE, + (addr.applicationId === EmberGpApplicationId.SOURCE_ID) ? addr.sourceId : addr.gpdIeeeAddress, + gpdCommandId, + gpdLink, + sequenceNumber, + null, + null, + null, + null + ); + break; + } + } + } + } + + /** + * Retrieves the proxy table entry stored at the passed index. + * @param proxyIndex uint8_t The index of the requested proxy table entry. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberGpProxyTableEntry * An EmberGpProxyTableEntry struct containing a copy of the requested proxy entry. + */ + async ezspGpProxyTableGetEntry(proxyIndex: number): Promise<[EmberStatus, entry: EmberGpProxyTableEntry]> { + this.startCommand(EzspFrameID.GP_PROXY_TABLE_GET_ENTRY); + this.buffalo.writeUInt8(proxyIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const entry = this.buffalo.readEmberGpProxyTableEntry(); + + return [status, entry]; + } + + /** + * Finds the index of the passed address in the gp table. + * @param addr EmberGpAddress * The address to search for + * @returns uint8_t The index, or 0xFF for not found + */ + async ezspGpProxyTableLookup(addr: EmberGpAddress): Promise { + this.startCommand(EzspFrameID.GP_PROXY_TABLE_LOOKUP); + this.buffalo.writeEmberGpAddress(addr); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const index = this.buffalo.readUInt8(); + + return index; + } + + /** + * Retrieves the sink table entry stored at the passed index. + * @param sinkIndex uint8_t The index of the requested sink table entry. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberGpSinkTableEntry * An EmberGpSinkTableEntry struct containing a copy of the requested sink entry. + */ + async ezspGpSinkTableGetEntry(sinkIndex: number): Promise<[EmberStatus, entry: EmberGpSinkTableEntry]> { + this.startCommand(EzspFrameID.GP_SINK_TABLE_GET_ENTRY); + this.buffalo.writeUInt8(sinkIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const entry = this.buffalo.readEmberGpSinkTableEntry(); + + return [status, entry]; + } + + /** + * Finds the index of the passed address in the gp table. + * @param addr EmberGpAddress *The address to search for. + * @returns uint8_t The index, or 0xFF for not found + */ + async ezspGpSinkTableLookup(addr: EmberGpAddress): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_LOOKUP); + this.buffalo.writeEmberGpAddress(addr); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const index = this.buffalo.readUInt8(); + + return index; + } + + /** + * Retrieves the sink table entry stored at the passed index. + * @param sinkIndex uint8_t The index of the requested sink table entry. + * @param entry EmberGpSinkTableEntry * An EmberGpSinkTableEntry struct containing a copy of the sink entry to be updated. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspGpSinkTableSetEntry(sinkIndex: number, entry: EmberGpSinkTableEntry): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_SET_ENTRY); + this.buffalo.writeUInt8(sinkIndex); + this.buffalo.writeEmberGpSinkTableEntry(entry); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Removes the sink table entry stored at the passed index. + * @param uint8_t The index of the requested sink table entry. + */ + async ezspGpSinkTableRemoveEntry(sinkIndex: number): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_REMOVE_ENTRY); + this.buffalo.writeUInt8(sinkIndex); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Finds or allocates a sink entry + * @param addr EmberGpAddress * An EmberGpAddress struct containing a copy of the gpd address to be found. + * @returns uint8_t An index of found or allocated sink or 0xFF if failed. + */ + async ezspGpSinkTableFindOrAllocateEntry(addr: EmberGpAddress): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_FIND_OR_ALLOCATE_ENTRY); + this.buffalo.writeEmberGpAddress(addr); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const index = this.buffalo.readUInt8(); + + return index; + } + + /** + * Clear the entire sink table + */ + async ezspGpSinkTableClearAll(): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_CLEAR_ALL); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Iniitializes Sink Table + */ + async ezspGpSinkTableInit(): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_INIT); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Sets security framecounter in the sink table + * @param index uint8_t Index to the Sink table + * @param sfc uint32_t Security Frame Counter + */ + async ezspGpSinkTableSetSecurityFrameCounter(index: number, sfc: number): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_SET_SECURITY_FRAME_COUNTER); + this.buffalo.writeUInt8(index); + this.buffalo.writeUInt32(sfc); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Puts the GPS in commissioning mode. + * @param uint8_t commissioning options + * @param uint16_t gpm address for security. + * @param uint16_t gpm address for pairing. + * @param uint8_t sink endpoint. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspGpSinkCommission(options: number, gpmAddrForSecurity: number, gpmAddrForPairing: number, sinkEndpoint: number): Promise { + this.startCommand(EzspFrameID.GP_SINK_COMMISSION); + this.buffalo.writeUInt8(options); + this.buffalo.writeUInt16(gpmAddrForSecurity); + this.buffalo.writeUInt16(gpmAddrForPairing); + this.buffalo.writeUInt8(sinkEndpoint); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Clears all entries within the translation table. + */ + async ezspGpTranslationTableClear(): Promise { + this.startCommand(EzspFrameID.GP_TRANSLATION_TABLE_CLEAR); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Return number of active entries in sink table. + * @returns uint8_t Number of active entries in sink table. 0 if error. + */ + async ezspGpSinkTableGetNumberOfActiveEntries(): Promise { + this.startCommand(EzspFrameID.GP_SINK_TABLE_GET_NUMBER_OF_ACTIVE_ENTRIES); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const number_of_entries = this.buffalo.readUInt8(); + + return number_of_entries; + } + + //----------------------------------------------------------------------------- + // Token Interface Frames + //----------------------------------------------------------------------------- + + /** + * Gets the total number of tokens. + * @returns uint8_t Total number of tokens. + */ + async ezspGetTokenCount(): Promise { + this.startCommand(EzspFrameID.GET_TOKEN_COUNT); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const count = this.buffalo.readUInt8(); + + return count; + } + + /** + * Gets the token information for a single token at provided index + * @param index uint8_t Index of the token in the token table for which information is needed. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberTokenInfo * Token information. + */ + async ezspGetTokenInfo(index: number): Promise<[EmberStatus, tokenInfo: EmberTokenInfo]> { + this.startCommand(EzspFrameID.GET_TOKEN_INFO); + this.buffalo.writeUInt8(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const tokenInfo = this.buffalo.readEmberTokenInfo(); + + return [status, tokenInfo]; + } + + /** + * Gets the token data for a single token with provided key + * @param token uint32_t Key of the token in the token table for which data is needed. + * @param index uint32_t Index in case of the indexed token. + * @returns An EmberStatus value indicating success or the reason for failure. + * @returns EmberTokenData * Token Data + */ + async ezspGetTokenData(token: number, index: number): Promise<[EmberStatus, tokenData: EmberTokenData]> { + this.startCommand(EzspFrameID.GET_TOKEN_DATA); + this.buffalo.writeUInt32(token); + this.buffalo.writeUInt32(index); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + const tokenData = this.buffalo.readEmberTokenData(); + + return [status, tokenData]; + } + + /** + * Sets the token data for a single token with provided key + * @param token uint32_t Key of the token in the token table for which data is to be set. + * @param index uint32_t Index in case of the indexed token. + * @param EmberTokenData * Token Data + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspSetTokenData(token: number, index: number, tokenData: EmberTokenData): Promise { + this.startCommand(EzspFrameID.SET_TOKEN_DATA); + this.buffalo.writeUInt32(token); + this.buffalo.writeUInt32(index); + this.buffalo.writeEmberTokenData(tokenData); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Reset the node by calling halReboot. + */ + async ezspResetNode(): Promise { + this.startCommand(EzspFrameID.RESET_NODE); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } + + /** + * Run GP security test vectors. + * @returns An EmberStatus value indicating success or the reason for failure. + */ + async ezspGpSecurityTestVectors(): Promise { + this.startCommand(EzspFrameID.GP_SECURITY_TEST_VECTORS); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + + const status: EmberStatus = this.buffalo.readUInt8(); + return status; + } + + /** + * Factory reset all configured zigbee tokens + * @param excludeOutgoingFC Exclude network and APS outgoing frame counter tokens. + * @param excludeBootCounter Exclude stack boot counter token. + */ + async ezspTokenFactoryReset(excludeOutgoingFC: boolean, excludeBootCounter: boolean): Promise { + this.startCommand(EzspFrameID.TOKEN_FACTORY_RESET); + this.buffalo.writeUInt8(excludeOutgoingFC ? 1 : 0); + this.buffalo.writeUInt8(excludeBootCounter ? 1 : 0); + + const sendStatus: EzspStatus = await this.sendCommand(); + + if (sendStatus !== EzspStatus.SUCCESS) { + throw new Error(EzspStatus[sendStatus]); + } + } +} diff --git a/src/adapter/ember/types.ts b/src/adapter/ember/types.ts new file mode 100644 index 0000000000..03126b944a --- /dev/null +++ b/src/adapter/ember/types.ts @@ -0,0 +1,812 @@ +import { + EmberApsOption, + EmberBindingType, + EmberCurrentSecurityBitmask, + EmberGpApplicationId, + EmberGpProxyTableEntryStatus, + EmberGpSinkTableEntryStatus, + EmberGpSinkType, + EmberJoinMethod, + EmberKeyStructBitmask, + EmberNetworkInitBitmask, + EmberNodeType, + EmberVersionType, + EmberZllKeyIndex, + EmberZllState, + SecManDerivedKeyType, + SecManFlag, + SecManKeyType +} from "./enums"; + +/** + * EUI 64-bit ID (IEEE 802.15.4 long address). uint8_t[EUI64_SIZE] + * + * EXPECTED WITH 0x PREFIX + */ +export type EmberEUI64 = string; +/** IEEE 802.15.4 node ID. Also known as short address. uint16_t */ +export type EmberNodeId = number; +/** IEEE 802.15.4 PAN ID. uint16_t */ +export type EmberPanId = number; +/** PAN 64-bit ID (IEEE 802.15.4 long address). uint8_t[EXTENDED_PAN_ID_SIZE] */ +export type EmberExtendedPanId = number[]; +/** 16-bit ZigBee multicast group identifier. uint16_t */ +export type EmberMulticastId = number; +/** + * The percent of duty cycle for a limit. + * + * Duty cycle, limits, and thresholds are reported in units of + * percent * 100 (i.e., 10000 = 100.00%, 1 = 0.01%). + * uint16_t + */ +export type EmberDutyCycleHectoPct = number; +/** Refer to the Zigbee application profile ID. uint16_t */ +export type ProfileId = number; +/** Refer to the ZCL cluster ID. uint16_t */ +export type ClusterId = number; + + +/** A version structure containing all version information. */ +export type EmberVersion = { + /** + * A unique build number generated by Silicon Labs' internal build engineering process + * + * uint16_t + */ + build: number, + /** + * Major version number + * (used to indicate major architectural changes or significant supported platform changes). + * + * A.b.c.d + * uint8_t + */ + major: number, + /** + * Minor version number + * (used to indicate significant new features, API changes; not always code-compatible with previous minor versions). + * + * a.B.c.d + * uint8_t + */ + minor: number, + /** + * Patch (sub-minor) version number + * (used to indicate bug fixes or minor features that don't affect code-compatibility with previous application code). + * + * a.b.C.d + * uint8_t + */ + patch: number, + /** + * Special version number + * (used to indicate superficial changes that don't require re-certification of the stack as a ZigBee-Compliant Platform, + * such as changes that only affect installer packaging, documentation, or comments in the code) + * + * a.b.c.D + * uint8_t + */ + special: number, + /** + * Corresponding to an enum value from EmberVersionType. + * + * Pre-release, Alpha, Beta, GA + */ + type: EmberVersionType, +}; + +/** Defines the network initialization configuration that should be used when ::emberNetworkInit() is called by the application. */ +export type EmberNetworkInitStruct = { + bitmask: EmberNetworkInitBitmask, +}; + +/** + * Holds network parameters. + * + * For information about power settings and radio channels, see the technical specification for the RF communication module in your Developer Kit. + */ +export type EmberNetworkParameters = { + /** The network's extended PAN identifier. int8_t[EXTENDED_PAN_ID_SIZE] */ + extendedPanId: EmberExtendedPanId, + /** The network's PAN identifier. uint16_t*/ + panId: EmberPanId, + /** A power setting, in dBm. int8_t*/ + radioTxPower: number, + /** A radio channel. Be sure to specify a channel supported by the radio. uint8_t */ + radioChannel: number, + /** + * Join method: The protocol messages used to establish an initial parent. + * It is ignored when forming a ZigBee network, or when querying the stack for its network parameters. + */ + joinMethod: EmberJoinMethod, + /** + * NWK Manager ID. The ID of the network manager in the current network. + * This may only be set at joining when using EMBER_USE_CONFIGURED_NWK_STATE as the join method. + */ + nwkManagerId: EmberNodeId, + /** + * An NWK Update ID. The value of the ZigBee nwkUpdateId known by the stack. + * It is used to determine the newest instance of the network after a PAN + * ID or channel change. This may only be set at joining when using + * EMBER_USE_CONFIGURED_NWK_STATE as the join method. + * uint8_t + */ + nwkUpdateId: number, + /** + * The NWK channel mask. The list of preferred channels that the NWK manager + * has told this device to use when searching for the network. + * This may only be set at joining when using EMBER_USE_CONFIGURED_NWK_STATE as the join method. + * uint32_t + */ + channels: number, +}; + +/** Defines a beacon entry that is processed when scanning, joining, or rejoining. */ +export type EmberBeaconData = { + panId: EmberPanId, + sender: EmberNodeId, + /** uint8_t */ + channel: number, + /** uint8_t */ + lqi: number, + /** int8_t */ + rssi: number, + /** uint8_t */ + depth: number, + /** uint8_t */ + nwkUpdateId: number, + /** Only valid if enhanced beacon. int8_t */ + power: number, + /** TC connectivity and long uptime from capacity field. int8_t */ + parentPriority: number, + /** uint8_t */ + supportedKeyNegotiationMethods: number, + extended_beacon: boolean, + /** Enhanced or regular beacon. default true */ + enhanced: boolean, + /** default true */ + permitJoin: boolean, + /** default true */ + hasCapacity: boolean, + /** default true */ + tcConnectivity: boolean, + /** default true */ + longUptime: boolean, + /** default true */ + preferParent: boolean, + /** default true */ + macDataPollKeepalive: boolean + /** default true */, + endDeviceKeepalive : boolean, + /** uint8_t[EXTENDED_PAN_ID_SIZE] */ + extendedPanId: EmberExtendedPanId, +}; + +/** + * Holds radio parameters. + * + * This is mainly useful for dual PHY and switched radio device (2.4 GHz or SubGHz) to retrieve radio parameters. + */ +export type EmberMultiPhyRadioParameters = { + /** int8_t */ + radioTxPower: number, + /** uint8_t */ + radioPage: number, + /** uint8_t */ + radioChannel: number, +}; + +/** This structure contains information about child nodes. */ +export type EmberChildData = { + /** */ + eui64: EmberEUI64, + /** */ + type: EmberNodeType, + /** */ + id: EmberNodeId, + /** uint8_t */ + phy: number, + /** uint8_t */ + power: number, + /** uint8_t */ + timeout: number, + /** uint32_t */ + remainingTimeout: number, +}; + +/** + * Defines an entry in the neighbor table. + * + * A neighbor table entry stores information about the + * reliability of RF links to and from neighboring nodes. + */ +export type EmberNeighborTableEntry = { + /** The neighbor's two-byte network ID. uint16_t */ + shortId: number, + /** Filtered Link Quality indicator. uint8_t */ + averageLqi: number, + /** + * The incoming cost for this neighbor, computed from the average LQI. + * Values range from 1 for a good link to 7 for a bad link. + * uint8_t + */ + inCost: number, + /** The outgoing cost for this neighbor, obtained from the most recently + * received neighbor exchange message from the neighbor. A value of zero + * means that a neighbor exchange message from the neighbor has not been + * received recently enough, or that our ID was not present in the most + * recently received one. EmberZNet Pro only. + * uint8_t + */ + outCost: number, + /** In EmberZNet Pro, the number of aging periods elapsed since a neighbor + * exchange message was last received from this neighbor. In stack profile 1, + * the number of aging periods since any packet was received. + * An entry with an age greater than 6 is considered stale and may be + * reclaimed. In case the entry is used by a routing table entry it is + * considered stale with an age of 8. The aging period is 16 seconds. + * On receiving an incoming packet from the neighbor, the age is set to 3. + * uint8_t + * */ + age: number, + /** The 8 byte EUI64 of the neighbor. */ + longId: EmberEUI64, +}; + +/** + * Defines an entry in the route table. + * + * A route table entry stores information about the next + * hop along the route to the destination. + */ +export type EmberRouteTableEntry = { + /** The short ID of the destination. uint16_t */ + destination: number, + /** The short ID of the next hop to this destination. uint16_t */ + nextHop: number, + /** Indicates whether this entry is active (0), being discovered (1), or unused (3). uint8_t */ + status: number, + /** The number of seconds since this route entry was last used to send a packet. uint8_t */ + age: number, + /** Indicates whether this destination is a High-RAM Concentrator (2), a Low-RAM Concentrator (1), or not a concentrator (0). uint8_t */ + concentratorType: number, + /** + * For a High-RAM Concentrator, indicates whether a route record + * is needed (2), has been sent (1), or is no long needed (0) because + * a source routed message from the concentrator has been received. + * uint8_t + */ + routeRecordState: number, +}; + +/** + * A structure containing duty cycle limit configurations. + * + * All limits are absolute and are required to be as follows: suspLimit > critThresh > limitThresh + * For example: suspLimit = 250 (2.5%), critThresh = 180 (1.8%), limitThresh 100 (1.00%). + */ +export type EmberDutyCycleLimits = { + /** The Limited Threshold in % * 100. */ + limitThresh: EmberDutyCycleHectoPct, + /** The Critical Threshold in % * 100. */ + critThresh: EmberDutyCycleHectoPct, + /** The Suspended Limit (LBT) in % * 100. */ + suspLimit: EmberDutyCycleHectoPct, +}; + +/** A structure containing, per device, overall duty cycle consumed (up to the suspend limit). */ +export type EmberPerDeviceDutyCycle = { + /** Node ID of the device whose duty cycle is reported. */ + nodeId: EmberNodeId, + /** The amount of overall duty cycle consumed (up to suspend limit). */ + dutyCycleConsumed: EmberDutyCycleHectoPct, +}; + +/** Defines a iterator used to loop over cached beacons. Fields denoted with a private comment should not be written to. */ +export type EmberBeaconIterator = { + /** Public fields */ + beacon: EmberBeaconData, + /** Private fields - Do not write to these variables. uint8_t */ + index: number, +}; + +/** + * Defines an entry in the binding table. + * + * A binding entry specifies a local endpoint, a remote endpoint, a + * cluster ID and either the destination EUI64 (for unicast bindings) or the + * 64-bit group address (for multicast bindings). + */ +export type EmberBindingTableEntry = { + /** The type of binding. */ + type: EmberBindingType , + /** The endpoint on the local node. uint8_t */ + local: number, + /** A cluster ID that matches one from the local endpoint's simple descriptor. + * This cluster ID is set by the provisioning application to indicate which + * part an endpoint's functionality is bound to this particular remote node + * and is used to distinguish between unicast and multicast bindings. Note + * that a binding can be used to to send messages with any cluster ID, not + * just that listed in the binding. + * uint16_t + */ + clusterId: number, + /** The endpoint on the remote node (specified by \c identifier). uint8_t */ + remote: number, + /** A 64-bit identifier. This is either: + * - The destination EUI64, for unicasts. + * - A 16-bit multicast group address, for multicasts. + */ + identifier: EmberEUI64, + /** The index of the network the binding belongs to. uint8_t */ + networkIndex: number, +}; + +/** An in-memory representation of a ZigBee APS frame of an incoming or outgoing message. */ +export type EmberApsFrame = { + /** The application profile ID that describes the format of the message. uint16_t */ + profileId: number, + /** The cluster ID for this message. uint16_t */ + clusterId: number, + /** The source endpoint. uint8_t */ + sourceEndpoint: number, + /** The destination endpoint. uint8_t */ + destinationEndpoint: number, + /** A bitmask of options from the enumeration above. */ + options: EmberApsOption, + /** The group ID for this message, if it is multicast mode. uint16_t */ + groupId: number, + /** The sequence number. uint8_t */ + sequence: number, + /** uint8_t */ + radius?: number,// XXX: marked optional since doesn't appear to be used +}; + +/** + * Defines an entry in the multicast table. + * + * A multicast table entry indicates that a particular endpoint is a member of a particular multicast group. + * Only devices with an endpoint in a multicast group will receive messages sent to that multicast group. + */ +export type EmberMulticastTableEntry = { + /** The multicast group ID. */ + multicastId: EmberMulticastId, + /** The endpoint that is a member, or 0 if this entry is not in use (the ZDO is not a member of any multicast groups). uint8_t */ + endpoint: number, + /** The network index of the network the entry is related to. uint8_t */ + networkIndex: number, +}; + +export type EmberBeaconClassificationParams = { + /** int8_t */ + minRssiForReceivingPkts: number, + /** uint16_t */ + beaconClassificationMask: number, +}; + +/** This data structure contains the key data that is passed into various other functions. */ +export type EmberKeyData = { + /** This is the key byte data. uint8_t[EMBER_ENCRYPTION_KEY_SIZE] */ + contents: Buffer; +}; + +/** This describes the Initial Security features and requirements that will be used when forming or joining the network. */ +export type EmberInitialSecurityState = { + /** + * This bitmask enumerates which security features should be used and the presence of valid data within other elements of the + * ::EmberInitialSecurityState data structure. For more details, see the ::EmberInitialSecurityBitmask. + * uint16_t + */ + bitmask: number, + /** + * This is the pre-configured key that can be used by devices when joining the network if the Trust Center does not send + * the initial security data in-the-clear. + * For the Trust Center, it will be the global link key and must be set regardless of whether joining devices are + * expected to have a pre-configured Link Key. This parameter will only be used if the EmberInitialSecurityState::bitmask + * sets the bit indicating ::EMBER_HAVE_PRECONFIGURED_KEY. + */ + preconfiguredKey: EmberKeyData, + /** + * This is the Network Key used when initially forming the network. + * It must be set on the Trust Center and is not needed for devices joining the network. + * This parameter will only be used if the EmberInitialSecurityState::bitmask sets the bit indicating ::EMBER_HAVE_NETWORK_KEY. + */ + networkKey: EmberKeyData, + /** + * This is the sequence number associated with the network key. It must be set if the Network Key is set and is used to indicate + * a particular of the network key for updating and switching. + * This parameter will only be used if the ::EMBER_HAVE_NETWORK_KEY is set. + * Generally, it should be set to 0 when forming the network; joining devices can ignore this value. + * uint8_t + * */ + networkKeySequenceNumber: number, + /** + * This is the long address of the trust center on the network that will be joined. + * It is usually NOT set prior to joining the network and is learned during the joining message exchange. + * This field is only examined if ::EMBER_HAVE_TRUST_CENTER_EUI64 is set in the EmberInitialSecurityState::bitmask. + * Most devices should clear that bit and leave this field alone. + * This field must be set when using commissioning mode. + * It is required to be in little-endian format. + */ + preconfiguredTrustCenterEui64: EmberEUI64, +}; + +/** This describes the security features used by the stack for a joined device. */ +export type EmberCurrentSecurityState = { + /** This bitmask indicates the security features currently in use on this node. */ + bitmask: EmberCurrentSecurityBitmask, + /** + * This indicates the EUI64 of the Trust Center. + * It will be all zeroes if the Trust Center Address is not known (i.e., the device is in a Distributed Trust Center network). + */ + trustCenterLongAddress: EmberEUI64, +}; + +/** + * This data structure houses the context when interacting with the Zigbee + * Security Manager APIs. For example, when importing a key into storage, the various + * fields of this structure are used to determine which type of key is being stored. + * */ +export type SecManContext = { + coreKeyType: SecManKeyType, + /** uint8_t */ + keyIndex: number, + derivedType: SecManDerivedKeyType, + eui64: EmberEUI64, + /** uint8_t */ + multiNetworkIndex: number, + flags: SecManFlag, + /** + * Unused for classic key storage. + * The algorithm type should be brought in by psa/crypto_types.h. + * Zigbee Security Manager uses PSA_ALG_ECB_NO_PADDING for keys with AES-ECB encryption, + * and defines ZB_PSA_ALG as AES-CCM with a 4-byte tag, used as this field's default value otherwise. + * uint32_t + */ + psaKeyAlgPermission: number, +}; + +/** This data structure contains the metadata pertaining to an network key */ +export type SecManNetworkKeyInfo = { + networkKeySet: boolean, + alternateNetworkKeySet: boolean, + /** uint8_t */ + networkKeySequenceNumber: number, + /** uint8_t */ + altNetworkKeySequenceNumber: number, + /** uint32_t */ + networkKeyFrameCounter: number, +}; + +/** This data structure contains the metadata pertaining to an APS key */ +export type SecManAPSKeyMetadata = { + bitmask: EmberKeyStructBitmask, + /** valid only if bitmask & EMBER_KEY_HAS_OUTGOING_FRAME_COUNTER uint32_t */ + outgoingFrameCounter: number, + /** valid only if bitmask & EMBER_KEY_HAS_INCOMING_FRAME_COUNTER uint32_t */ + incomingFrameCounter: number, + /** valid only if core_key_type == SL_ZB_SEC_MAN_KEY_TYPE_TC_LINK_WITH_TIMEOUT uint16_t */ + ttlInSeconds: number,// +}; + +/** This data structure contains the key data that is passed into various other functions. */ +export type SecManKey = EmberKeyData; + +/** This data structure contains the context data when calculating an AES MMO hash (message digest). */ +export type EmberAesMmoHashContext = { + /** uint8_t[EMBER_AES_HASH_BLOCK_SIZE] */ + result: Buffer, + /** uint32_t */ + length: number, +}; + +/** This data structure contains the public key data that is used for Certificate Based Key Exchange (CBKE). */ +export type EmberPublicKeyData = { + /** uint8_t[EMBER_PUBLIC_KEY_SIZE] */ + contents: Buffer, +}; + +/** This data structure contains the certificate data that is used for Certificate Based Key Exchange (CBKE). */ +export type EmberCertificateData = { + /** uint8_t[EMBER_CERTIFICATE_SIZE] */ + contents: Buffer; +}; + +/** This data structure contains the Shared Message Authentication Code SMAC) data that is used for Certificate Based Key Exchange (CBKE). */ +export type EmberSmacData = { + /** uint8_t[EMBER_SMAC_SIZE] */ + contents: Buffer; +}; + +/** This data structure contains the public key data that is used for Certificate Based Key Exchange (CBKE) in SECT283k1 Elliptical Cryptography. */ +export type EmberPublicKey283k1Data = { + /** uint8_t[EMBER_PUBLIC_KEY_283K1_SIZE] */ + contents: Buffer, +}; + +/** This data structure contains the private key data that is used for Certificate Based Key Exchange (CBKE) in SECT283k1 Elliptical Cryptography. */ +export type EmberPrivateKey283k1Data = { + /** uint8_t[EMBER_PRIVATE_KEY_283K1_SIZE] */ + contents: Buffer, +}; + +/** This data structure contains the certificate data that is used for Certificate Based Key Exchange (CBKE) in SECT283k1 Elliptical Cryptography. */ +export type EmberCertificate283k1Data = { + /* This is the certificate byte data. uint8_t[EMBER_CERTIFICATE_283K1_SIZE] */ + contents: Buffer, +}; + +/** This data structure contains an AES-MMO Hash (the message digest). */ +export type EmberMessageDigest = { + /** uint8_t[EMBER_AES_HASH_BLOCK_SIZE] */ + contents: Buffer, +}; + +/** This data structure contains a DSA signature. It is the bit concatenation of the 'r' and 's' components of the signature. */ +export type EmberSignatureData = { + /** uint8_t[EMBER_SIGNATURE_SIZE] */ + contents: Buffer, +}; + +/** + * This data structure contains a DSA signature used in SECT283k1 Elliptical Cryptography. + * It is the bit concatenation of the 'r' and 's' components of the signature. + */ +export type EmberSignature283k1Data = { + /** uint8_t[EMBER_SIGNATURE_283K1_SIZE] */ + contents: Buffer; +}; + +/** This data structure contains the private key data that is used for Certificate Based Key Exchange (CBKE). */ +export type EmberPrivateKeyData = { + /** uint8_t[EMBER_PRIVATE_KEY_SIZE] */ + contents: Buffer, +}; + +/** Defines a ZigBee network and the associated parameters. */ +export type EmberZigbeeNetwork = { + /** uint16_t */ + panId: EmberPanId, + /** uint8_t */ + channel: number, + /** bool */ + allowingJoin: number, + /** uint8_t[EXTENDED_PAN_ID_SIZE] */ + extendedPanId: EmberExtendedPanId, + /** uint8_t */ + stackProfile: number, + /** uint8_t */ + nwkUpdateId: number, +}; + +/** Information about the ZLL security state and how to transmit the network key to the device securely. */ +export type EmberZllSecurityAlgorithmData = { + /** uint32_t */ + transactionId: number, + /** uint32_t */ + responseId: number, + /** uint16_t */ + bitmask: number, +}; + +/** Information about the ZLL network and specific device that responded to a ZLL scan request. */ +export type EmberZllNetwork = { + zigbeeNetwork: EmberZigbeeNetwork, + securityAlgorithm: EmberZllSecurityAlgorithmData, + eui64: EmberEUI64, + nodeId: EmberNodeId, + state: EmberZllState, + nodeType: EmberNodeType, + /** uint8_t */ + numberSubDevices: number, + /** uint8_t */ + totalGroupIdentifiers: number, + /** uint8_t */ + rssiCorrection: number, +}; + +/** Describe the Initial Security features and requirements that will be used when forming or joining ZigBee Light Link networks. */ +export type EmberZllInitialSecurityState = { + /** This bitmask is unused. All values are reserved for future use. uint32_t */ + bitmask: number, + /** The key encryption algorithm advertised by the application. */ + keyIndex: EmberZllKeyIndex, + /** The encryption key for use by algorithms that require it. */ + encryptionKey: EmberKeyData, + /** The pre-configured link key used during classical ZigBee commissioning. */ + preconfiguredKey: EmberKeyData, +}; + +/** Information discovered during a ZLL scan about the ZLL device's endpoint information. */ +export type EmberZllDeviceInfoRecord = { + ieeeAddress: EmberEUI64, + /** uint8_t */ + endpointId: number, + /** uint16_t */ + profileId: number, + /** uint16_t */ + deviceId: number, + /** uint8_t */ + version: number, + /** uint8_t */ + groupIdCount: number, +}; + +/** Network and group address assignment information. */ +export type EmberZllAddressAssignment = { + nodeId: EmberNodeId, + freeNodeIdMin: EmberNodeId, + freeNodeIdMax: EmberNodeId, + groupIdMin: EmberMulticastId, + groupIdMax: EmberMulticastId, + freeGroupIdMin: EmberMulticastId, + freeGroupIdMax: EmberMulticastId, +}; + +export type EmberTokTypeStackZllData = { + /** uint32_t */ + bitmask: number, + /** uint16_t */ + freeNodeIdMin: number, + /** uint16_t */ + freeNodeIdMax: number, + /** uint16_t */ + myGroupIdMin: number, + /** uint16_t */ + freeGroupIdMin: number, + /** uint16_t */ + freeGroupIdMax: number, + /** uint8_t */ + rssiCorrection: number, +}; + +export type EmberTokTypeStackZllSecurity = { + /** uint32_t */ + bitmask: number, + /** uint8_t */ + keyIndex: number, + /** uint8_t[EMBER_ENCRYPTION_KEY_SIZE] */ + encryptionKey: Buffer, + /** uint8_t[EMBER_ENCRYPTION_KEY_SIZE] */ + preconfiguredKey: Buffer, +}; + +/** 32-bit GPD source identifier uint32_t */ +export type EmberGpSourceId = number; + +/** + * GPD Address for sending and receiving a GPDF. + * EmberGpAddress_gpdIeeeAddress | EmberGpAddress_sourceId; + */ +export type EmberGpAddress = { + // union { + /** The IEEE address is used when the application identifier is ::EMBER_GP_APPLICATION_IEEE_ADDRESS. */ + gpdIeeeAddress?: EmberEUI64, + /** The 32-bit source identifier is used when the application identifier is ::EMBER_GP_APPLICATION_SOURCE_ID. */ + sourceId?: EmberGpSourceId, + // } id; + /** Application identifier of the GPD. */ + applicationId: EmberGpApplicationId, + /** Application endpoint , only used when application identifier is ::EMBER_GP_APPLICATION_IEEE_ADDRESS. uint8_t */ + endpoint: number, +}; + +/** 32-bit security frame counter uint32_t */ +export type EmberGpSecurityFrameCounter = number; + +/** The internal representation of a proxy table entry. */ +export type EmberGpProxyTableEntry = { + /** Internal status. Defines if the entry is unused or used as a proxy entry */ + status: EmberGpProxyTableEntryStatus, + /** The tunneling options (this contains both options and extendedOptions from the spec). uint32_t */ + options: number, + /** The addressing info of the GPD */ + gpd: EmberGpAddress, + /** The assigned alias for the GPD */ + assignedAlias: EmberNodeId, + /** The security options field. uint8_t */ + securityOptions: number, + /** The SFC of the GPD */ + gpdSecurityFrameCounter: EmberGpSecurityFrameCounter, + /** The key for the GPD. */ + gpdKey: EmberKeyData, + /** The list of sinks; hardcoded to 2, which is the spec minimum. EmberGpSinkListEntry[GP_SINK_LIST_ENTRIES] */ + sinkList: EmberGpSinkListEntry[], + /** The groupcast radius. uint8_t */ + groupcastRadius: number, + /** The search counter. uint8_t */ + searchCounter: number, +}; + +/** GP Sink Address. */ +export type EmberGpSinkAddress = { + /** EUI64 or long address of the sink */ + sinkEUI: EmberEUI64, + /** Node ID or network address of the sink */ + sinkNodeId: EmberNodeId, +}; + +/** GP Sink Group. */ +export type EmberGpSinkGroup = { + /** Group ID of the sink. uint16_t */ + groupID: number, + /** Alias ID of the sink. uint16_t */ + alias: number, +}; + +/** GP Sink List Entry. */ +export type EmberGpSinkListEntry = { + /** Sink Type */ + type: EmberGpSinkType, + // union { + unicast?: EmberGpSinkAddress, + groupcast?: EmberGpSinkGroup, + /** Entry for Sink Group List */ + groupList?: EmberGpSinkGroup, + // } target; +}; + + +/** The internal representation of a sink table entry. */ +export type EmberGpSinkTableEntry = { + /** Internal status. Defines if the entry is unused or used as a sink table entry */ + status: EmberGpSinkTableEntryStatus, + /** The tunneling options (this contains both options and extendedOptions from the spec). uint16_t */ + options: number, + /** The addressing info of the GPD */ + gpd: EmberGpAddress, + /** The device ID for the GPD. uint8_t */ + deviceId: number, + /** The list of sinks; hardcoded to 2, which is the spec minimum. EmberGpSinkListEntry[GP_SINK_LIST_ENTRIES] */ + sinkList: EmberGpSinkListEntry[], + /** The assigned alias for the GPD */ + assignedAlias: EmberNodeId, + /** The groupcast radius. uint8_t */ + groupcastRadius: number, + /** The security options field. uint8_t */ + securityOptions: number, + /** The SFC of the GPD */ + gpdSecurityFrameCounter: EmberGpSecurityFrameCounter, + /** The GPD key associated with this entry. */ + gpdKey: EmberKeyData, +}; + +/** A structure containing the information of a token. */ +export type EmberTokenInfo = { + /** NVM3 token key. uint32_t */ + nvm3Key: number, + /** The token is a counter token type. */ + isCnt: boolean, + /** The token is an indexed token type. */ + isIdx: boolean, + /** Size of the object of the token. uint8_t */ + size: number, + /** The array size for the token when it is an indexed token. uint8_t */ + arraySize: number, +}; + +/** A structure containing the information of a token data. */ +export type EmberTokenData = { + /** The size of the token data in number of bytes. uint32_t */ + size: number, + /** A data pointer pointing to the storage for the token data of above size. void * */ + data: Buffer, +}; + +/** This data structure contains the transient key data that is used during Zigbee 3.0 joining. */ +export type EmberTransientKeyData = { + eui64: EmberEUI64, + /** uint32_t */ + incomingFrameCounter: number, + bitmask: EmberKeyStructBitmask, + /** uint16_t */ + remainingTimeSeconds: number, + /** uint8_t */ + networkIndex: number, + // union { + /** valid only if bitmask & EMBER_KEY_HAS_KEY_DATA (on some parts, keys are stored in secure storage and not RAM) */ + keyData?: EmberKeyData, + /** valid only if bitmask & EMBER_KEY_HAS_PSA_ID (on some parts, keys are stored in secure storage and not RAM). uint32_t */ + psa_id?: number, + // }, +}; diff --git a/src/adapter/ember/uart/ash.ts b/src/adapter/ember/uart/ash.ts new file mode 100644 index 0000000000..46194a8329 --- /dev/null +++ b/src/adapter/ember/uart/ash.ts @@ -0,0 +1,1882 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import {EventEmitter} from "stream"; +import {Socket} from "net"; +import SocketPortUtils from "../../socketPortUtils"; +import {SerialPort} from "../../serialPort"; +import {SerialPortOptions} from "../../tstype"; +import { + ASH_ACKNUM_BIT, + ASH_ACKNUM_MASK, + ASH_CRC_LEN, + ASH_DFRAME_MASK, + ASH_FLIP, + ASH_FRAME_LEN_ACK, + ASH_FRAME_LEN_DATA_MIN, + ASH_FRAME_LEN_ERROR, + ASH_FRAME_LEN_NAK, + ASH_FRAME_LEN_RSTACK, + ASH_FRMNUM_BIT, + ASH_FRMNUM_MASK, + ASH_MAX_DATA_FIELD_LEN, + ASH_MAX_FRAME_WITH_CRC_LEN, + ASH_MAX_TIMEOUTS, + ASH_MIN_DATA_FIELD_LEN, + ASH_MIN_FRAME_WITH_CRC_LEN, + ASH_NFLAG_BIT, + ASH_NFLAG_MASK, + ASH_RFLAG_BIT, + ASH_RFLAG_MASK, + ASH_SHFRAME_MASK, + ASH_VERSION, + ASH_WAKE, + EZSP_HOST_RX_POOL_SIZE, + LFSR_POLY, + LFSR_SEED, + SH_RX_BUFFER_LEN, + SH_TX_BUFFER_LEN, + TX_POOL_BUFFERS, +} from "./consts"; +import {inc8, mod8, withinRange, halCommonCrc16} from "../utils/math"; +import {EzspStatus} from "../enums"; +import {AshFrameType, AshReservedByte, NcpFailedCode} from "./enums"; +import {EzspBuffer, EzspFreeList, EzspQueue} from "./queues"; +import {AshWriter} from "./writer"; +import {AshParser} from "./parser"; +import {Wait} from "../../../utils"; + +const debug = Debug('zigbee-herdsman:adapter:ember:uart:ash'); + + +/** ASH get rflag in control byte */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ashGetRFlag = (ctrl: number): number => ((ctrl & ASH_RFLAG_MASK) >> ASH_RFLAG_BIT); +/** ASH get nflag in control byte */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ashGetNFlag = (ctrl: number): number => ((ctrl & ASH_NFLAG_MASK) >> ASH_NFLAG_BIT); +/** ASH get frmnum in control byte */ +const ashGetFrmNum = (ctrl: number): number => ((ctrl & ASH_FRMNUM_MASK) >> ASH_FRMNUM_BIT); +/** ASH get acknum in control byte */ +const ashGetACKNum = (ctrl: number): number => ((ctrl & ASH_ACKNUM_MASK) >> ASH_ACKNUM_BIT); + + +export enum AshEvents { + /** When the host detects a fatal error */ + hostError = 'hostError', + /** When the NCP reports a fatal error */ + ncpError = 'ncpError', + /** When a frame has been parsed and queued in the rxQueue. */ + frame = 'frame', +} + +type UartAshCounters = { + /** DATA frame data fields bytes transmitted */ + txData: number, + /** frames of all types transmitted */ + txAllFrames: number, + /** DATA frames transmitted */ + txDataFrames: number, + /** ACK frames transmitted */ + txAckFrames: number, + /** NAK frames transmitted */ + txNakFrames: number, + /** DATA frames retransmitted */ + txReDataFrames: number, + /** ACK and NAK frames with nFlag 0 transmitted */ + // txN0Frames: number, + /** ACK and NAK frames with nFlag 1 transmitted */ + txN1Frames: number, + /** frames cancelled (with ASH_CAN byte) */ + txCancelled: number, + + /** DATA frame data fields bytes received */ + rxData: number, + /** frames of all types received */ + rxAllFrames: number, + /** DATA frames received */ + rxDataFrames: number, + /** ACK frames received */ + rxAckFrames: number, + /** NAK frames received */ + rxNakFrames: number, + /** retransmitted DATA frames received */ + rxReDataFrames: number, + /** ACK and NAK frames with nFlag 0 received */ + // rxN0Frames: number, + /** ACK and NAK frames with nFlag 1 received */ + rxN1Frames: number, + /** frames cancelled (with ASH_CAN byte) */ + rxCancelled: number, + + /** frames with CRC errors */ + rxCrcErrors: number, + /** frames with comm errors (with ASH_SUB byte) */ + rxCommErrors: number, + /** frames shorter than minimum */ + rxTooShort: number, + /** frames longer than maximum */ + rxTooLong: number, + /** frames with illegal control byte */ + rxBadControl: number, + /** frames with illegal length for type of frame */ + rxBadLength: number, + /** frames with bad ACK numbers */ + rxBadAckNumber: number, + /** DATA frames discarded due to lack of buffers */ + rxNoBuffer: number, + /** duplicate retransmitted DATA frames */ + rxDuplicates: number, + /** DATA frames received out of sequence */ + rxOutOfSequence: number, + /** received ACK timeouts */ + rxAckTimeouts: number, +}; + +enum SendState { + IDLE = 0, + SHFRAME = 1, + TX_DATA = 2, + RETX_DATA = 3, +} + +// Bits in ashFlags +enum Flag { + /** Reject Condition */ + REJ = 0x01, + /** Retransmit Condition */ + RETX = 0x02, + /** send NAK */ + NAK = 0x04, + /** send ACK */ + ACK = 0x08, + /** send RST */ + RST = 0x10, + /** send immediate CAN */ + CAN = 0x20, + /** in CONNECTED state, else ERROR */ + CONNECTED = 0x40, + /** not ready to receive DATA frames */ + NR = 0x100, + /** last transmitted NR status */ + NRTX = 0x200, +} + + +/** max frames sent without being ACKed (1-7) */ +const CONFIG_TX_K = 3; +/** enables randomizing DATA frame payloads */ +const CONFIG_RANDOMIZE = true; +/** adaptive rec'd ACK timeout initial value */ +const CONFIG_ACK_TIME_INIT = 800; +/** " " " " " minimum value */ +const CONFIG_ACK_TIME_MIN = 400; +/** " " " " " maximum value */ +const CONFIG_ACK_TIME_MAX = 2400; +/** time allowed to receive RSTACK after ncp is reset */ +const CONFIG_TIME_RST = 2500; +/** time between checks for received RSTACK (CONNECTED status) */ +const CONFIG_TIME_RST_CHECK = 100; +/** if free buffers < limit, host receiver isn't ready, will hold off the ncp from sending normal priority frames */ +const CONFIG_NR_LOW_LIMIT = 8;// RX_FREE_LW +/** if free buffers > limit, host receiver is ready */ +const CONFIG_NR_HIGH_LIMIT = 12;// RX_FREE_HW +/** time until a set nFlag must be resent (max 2032) */ +const CONFIG_NR_TIME = 480; +/** Read/write max bytes count at stream level */ +const CONFIG_HIGHWATER_MARK = 256; + +/** + * ASH Protocol handler. + */ +export class UartAsh extends EventEmitter { + private readonly portOptions: SerialPortOptions; + private serialPort: SerialPort; + private socketPort: Socket; + private writer: AshWriter; + private parser: AshParser; + + /** True when serial/socket is currently closing. */ + private closing: boolean; + + /** time ackTimer started: 0 means not ready uint16_t */ + private ackTimer: number; + /** time used to check ackTimer expiry (msecs) uint16_t */ + private ackPeriod: number; + /** not ready timer (16 msec units). Set to (now + config.nrTime) when started. uint8_t */ + private nrTimer: number; + /** frame decode in progress */ + private decodeInProgress: boolean; + + // Variables used in encoding frames + /** true when preceding byte was escaped */ + private encodeEscFlag: boolean; + /** byte to send after ASH_ESC uint8_t */ + private encodeFlip: number; + /** uint16_t */ + private encodeCrc: number; + /** encoder state: 0 = control/data bytes, 1 = crc low byte, 2 = crc high byte, 3 = flag. uint8_t */ + private encodeState: number; + /** bytes remaining to encode. uint8_t */ + private encodeCount: number; + + // Variables used in decoding frames + /** bytes in frame, plus CRC, clamped to limit +1: high values also used to record certain errors. uint8_t */ + private decodeLen: number; + /** ASH_FLIP if previous byte was ASH_ESC. uint8_t */ + private decodeFlip: number; + /** a 2 byte queue to avoid outputting crc bytes. uint8_t */ + private decodeByte1: number; + /** at frame end, they contain the received crc. uint8_t */ + private decodeByte2: number; + /** uint16_t */ + private decodeCrc: number; + + /** outgoing short frames */ + private txSHBuffer: Buffer; + /** incoming short frames */ + private rxSHBuffer: Buffer; + + /** bit flags for top-level logic. uint16_t */ + private flags: number; + /** frame ack'ed from remote peer. uint8_t */ + private ackRx: number; + /** frame ack'ed to remote peer. uint8_t */ + private ackTx: number; + /** next frame to be transmitted. uint8_t */ + private frmTx: number; + /** next frame to be retransmitted. uint8_t */ + private frmReTx: number; + /** next frame expected to be rec'd. uint8_t */ + private frmRx: number; + /** frame at retx queue's head. uint8_t */ + private frmReTxHead: number; + /** consecutive timeout counter. uint8_t */ + private timeouts: number; + /** rec'd DATA frame buffer. uint8_t */ + private rxDataBuffer: EzspBuffer; + /** rec'd frame length. uint8_t */ + private rxLen: number; + /** tx frame offset. uint8_t */ + private txOffset: number; + + public counters: UartAshCounters; + + private ncpError: EzspStatus; + private hostError: EzspStatus; + /** sendExec() state variable */ + private sendState: SendState; + + /** NCP is enabled to sleep, set by EZSP, not supported atm, always false */ + public ncpSleepEnabled: boolean; + /** + * Set when the ncp has indicated it has a pending callback by seting the callback flag in the frame control byte + * or (uart version only) by sending an an ASH_WAKE byte between frames. + */ + public ncpHasCallbacks: boolean; + + /** Transmit buffers */ + private txPool: EzspBuffer[]; + public txQueue: EzspQueue; + public reTxQueue: EzspQueue; + public txFree: EzspFreeList; + + /** Receive buffers */ + private rxPool: EzspBuffer[]; + public rxQueue: EzspQueue; + public rxFree: EzspFreeList; + + constructor(options: SerialPortOptions) { + super(); + + this.portOptions = options; + this.serialPort = null; + this.socketPort = null; + this.writer = null; + this.parser = null; + + this.txPool = new Array(TX_POOL_BUFFERS); + this.txQueue = new EzspQueue(); + this.reTxQueue = new EzspQueue(); + this.txFree = new EzspFreeList(); + + this.rxPool = new Array(EZSP_HOST_RX_POOL_SIZE); + this.rxQueue = new EzspQueue(); + this.rxFree = new EzspFreeList(); + } + + /** + * Check if port is valid, open, and not closing. + */ + get portOpen(): boolean { + if (this.closing) { + return false; + } + + return this.serialPort != null ? this.serialPort.isOpen : !this.socketPort?.closed; + } + + /** + * Get max wait time before response is considered timed out. + */ + get responseTimeout(): number { + return ASH_MAX_TIMEOUTS * CONFIG_ACK_TIME_MAX; + } + + /** + * Indicates if the host is in the Connected state. + * If not, the host and NCP cannot exchange DATA frames. + * Note that this function does not actively confirm that communication with NCP is healthy, but simply returns its last known status. + * + * @returns + * - true - host and NCP can exchange DATA frames + * - false - host and NCP cannot now exchange DATA frames + */ + get connected(): boolean { + return ((this.flags & Flag.CONNECTED) !== 0); + } + + /** + * Has nothing to do... + */ + get idle(): boolean { + return ( + !this.decodeInProgress // don't have a partial frame + // && (this.serial.readAvailable() === EzspStatus.NO_RX_DATA) // no rx data + && this.rxQueue.empty // no rx frames to process + && !this.ncpHasCallbacks // no pending callbacks + && (this.flags === Flag.CONNECTED) // no pending ACKs, NAKs, etc. + && (this.ackTx === this.frmRx) // do not need to send an ACK + && (this.ackRx === this.frmTx) // not waiting to receive an ACK + && (this.sendState === SendState.IDLE) // nothing being transmitted now + && this.txQueue.empty // nothing waiting to transmit + // && this.serial.outputIsIdle() // nothing in OS buffers or UART FIFO + ); + } + + /** + * Initialize ASH variables, timers and queues, but not the serial port + */ + private initVariables(): void { + this.closing = false; + + this.serialPort = null; + this.socketPort = null; + this.writer = null; + this.parser = null; + this.txSHBuffer = Buffer.alloc(SH_TX_BUFFER_LEN); + this.rxSHBuffer = Buffer.alloc(SH_RX_BUFFER_LEN); + this.ackTimer = 0; + this.ackPeriod = 0; + this.nrTimer = 0; + + this.flags = 0; + this.decodeInProgress = false; + this.ackRx = 0; + this.ackTx = 0; + this.frmTx = 0; + this.frmReTx = 0; + this.frmRx = 0; + this.frmReTxHead = 0; + this.timeouts = 0; + this.rxDataBuffer = null; + this.rxLen = 0; + + // init to "start of frame" default + this.encodeCount = 0; + this.encodeState = 0; + this.encodeEscFlag = false; + this.encodeCrc = 0xFFFF; + this.txOffset = 0; + + // init to "start of frame" default + this.decodeLen = 0; + this.decodeByte1 = 0; + this.decodeByte2 = 0; + this.decodeFlip = 0; + this.decodeCrc = 0xFFFF; + + this.ncpError = EzspStatus.NO_ERROR; + this.hostError = EzspStatus.NO_ERROR; + this.sendState = SendState.IDLE; + + this.ncpSleepEnabled = false; + this.ncpHasCallbacks = false; + + this.stopAckTimer(); + this.stopNrTimer(); + this.initQueues(); + + this.counters = { + txData: 0, + txAllFrames: 0, + txDataFrames: 0, + txAckFrames: 0, + txNakFrames: 0, + txReDataFrames: 0, + // txN0Frames: 0, + txN1Frames: 0, + txCancelled: 0, + + rxData: 0, + rxAllFrames: 0, + rxDataFrames: 0, + rxAckFrames: 0, + rxNakFrames: 0, + rxReDataFrames: 0, + // rxN0Frames: 0, + rxN1Frames: 0, + rxCancelled: 0, + + rxCrcErrors: 0, + rxCommErrors: 0, + rxTooShort: 0, + rxTooLong: 0, + rxBadControl: 0, + rxBadLength: 0, + rxBadAckNumber: 0, + rxNoBuffer: 0, + rxDuplicates: 0, + rxOutOfSequence: 0, + rxAckTimeouts: 0, + }; + } + + /** + * Initializes all queues and free lists. + * All receive buffers are put into rxFree, and rxQueue is empty. + * All transmit buffers are put into txFree, and txQueue and reTxQueue are empty. + */ + private initQueues(): void { + this.txQueue.tail = null; + this.reTxQueue.tail = null; + this.txFree.link = null; + + for (let i = 0; i < TX_POOL_BUFFERS; i++) { + this.txFree.freeBuffer(this.txPool[i] = new EzspBuffer()); + } + + this.rxQueue.tail = null; + this.rxFree.link = null; + + for (let i = 0; i < EZSP_HOST_RX_POOL_SIZE; i++) { + this.rxFree.freeBuffer(this.rxPool[i] = new EzspBuffer()); + } + } + + private async initPort(): Promise { + if (!SocketPortUtils.isTcpPath(this.portOptions.path)) { + if (this.serialPort != null) { + this.serialPort.close(); + } + + const serialOpts = { + path: this.portOptions.path, + baudRate: typeof this.portOptions.baudRate === 'number' ? this.portOptions.baudRate : 115200, + rtscts: typeof this.portOptions.rtscts === 'boolean' ? this.portOptions.rtscts : false, + autoOpen: false, + parity: 'none' as const, + stopBits: 1 as const, + xon: false, + xoff: false, + }; + + // enable software flow control if RTS/CTS not enabled in config + if (!serialOpts.rtscts) { + debug(`RTS/CTS config is off, enabling software flow control.`); + serialOpts.xon = true; + serialOpts.xoff = true; + } + + //@ts-expect-error Jest testing + if (this.portOptions.binding != null) { + //@ts-expect-error Jest testing + serialOpts.binding = this.portOptions.binding; + } + + debug(`Opening SerialPort with ${JSON.stringify(serialOpts)}`); + this.serialPort = new SerialPort(serialOpts); + + this.writer = new AshWriter({highWaterMark: CONFIG_HIGHWATER_MARK}); + this.writer.pipe(this.serialPort); + + this.parser = new AshParser({readableHighWaterMark: CONFIG_HIGHWATER_MARK}); + this.serialPort.pipe(this.parser); + this.parser.on('data', this.onFrame.bind(this)); + + try { + await this.serialPort.asyncOpen(); + debug('Serialport opened'); + + this.serialPort.once('close', this.onPortClose.bind(this)); + this.serialPort.on('error', this.onPortError.bind(this)); + } catch (error) { + await this.stop(); + + throw error; + } + } else { + if (this.socketPort != null) { + this.socketPort.destroy(); + } + + const info = SocketPortUtils.parseTcpPath(this.portOptions.path); + debug(`Opening TCP socket with ${info.host}:${info.port}`); + + this.socketPort = new Socket(); + this.socketPort.setNoDelay(true); + this.socketPort.setKeepAlive(true, 15000); + + this.writer = new AshWriter({highWaterMark: CONFIG_HIGHWATER_MARK}); + this.writer.pipe(this.socketPort); + + this.parser = new AshParser({readableHighWaterMark: CONFIG_HIGHWATER_MARK}); + this.socketPort.pipe(this.parser); + this.parser.on('data', this.onFrame.bind(this)); + + return new Promise((resolve, reject): void => { + const openError = async (err: Error): Promise => { + await this.stop(); + + reject(err); + }; + + this.socketPort.on('connect', () => { + debug('Socket connected'); + }); + this.socketPort.on('ready', async (): Promise => { + debug('Socket ready'); + this.socketPort.removeListener('error', openError); + this.socketPort.once('close', this.onPortClose.bind(this)); + this.socketPort.on('error', this.onPortError.bind(this)); + + resolve(); + }); + this.socketPort.once('error', openError); + + this.socketPort.connect(info.port, info.host); + }); + } + } + + /** + * Handle port closing + * @param err A boolean for Socket, an Error for serialport + */ + private async onPortClose(err: boolean | Error): Promise { + console.log(`Port closed. Error? ${err ?? 'no'}`); + } + + /** + * Handle port error + * @param error + */ + private async onPortError(error: Error): Promise { + console.log(`Port error: ${error}`); + this.hostDisconnect(EzspStatus.ERROR_SERIAL_INIT); + await this.stop(); + } + + /** + * Handle received frame from AshParser. + * @param buf + */ + private onFrame(buffer: Buffer): void { + const iCAN = buffer.lastIndexOf(AshReservedByte.CANCEL);// should only be one, but just in case... + + if (iCAN !== -1) { + // ignore the cancel before RSTACK + if (this.flags & Flag.CONNECTED) { + this.counters.rxCancelled += 1; + + console.warn(`Frame(s) in progress cancelled. ${buffer.subarray(0, iCAN).toString('hex')}`); + } + + // get rid of everything up to the CAN flag and start reading frame from there, no need to loop through bytes in vain + buffer = buffer.subarray(iCAN + 1); + } + + const status = this.receiveFrame(buffer); + + if (status === EzspStatus.SUCCESS) { + // ? + } else if ((status !== EzspStatus.ASH_IN_PROGRESS) && (status !== EzspStatus.NO_RX_DATA)) { + throw new Error(EzspStatus[status]); + } + } + + /** + * Initializes the ASH protocol, and waits until the NCP finishes rebooting, or a non-recoverable error occurs. + * + * @returns + * - EzspStatus.SUCCESS + * - EzspStatus.HOST_FATAL_ERROR + * - EzspStatus.ASH_NCP_FATAL_ERROR) + */ + public async start(): Promise { + if (this.closing || (this.flags & Flag.CONNECTED)) { + return EzspStatus.ERROR_INVALID_CALL; + } + + console.log(`======== ASH starting ========`); + + if (this.serialPort != null) { + this.serialPort.flush();// clear read/write buffers + } else { + // XXX: Socket equiv? + } + + this.sendExec(); + + // block til RSTACK or timeout + for (let i = 0; i < CONFIG_TIME_RST; i += CONFIG_TIME_RST_CHECK) { + if ((this.flags & Flag.CONNECTED)) { + console.log(`======== ASH started ========`); + + return EzspStatus.SUCCESS; + } + + debug(`Waiting for RSTACK... ${i}/${CONFIG_TIME_RST}`); + await Wait(CONFIG_TIME_RST_CHECK); + } + + return EzspStatus.HOST_FATAL_ERROR; + } + + /** + * Stops the ASH protocol - flushes and closes the serial port, clears all queues, stops timers, etc. + */ + public async stop(): Promise { + this.closing = true; + + this.printCounters(); + + if (this.serialPort?.isOpen) { + try { + await this.serialPort.asyncFlushAndClose(); + + debug(`Serial port closed.`); + } catch (err) { + debug(`Failed to close serial port ${err}.`); + } + + this.serialPort.removeAllListeners(); + } else if (this.socketPort != null && !this.socketPort.closed) { + this.socketPort.destroy(); + this.socketPort.removeAllListeners(); + + debug(`Socket port closed.`); + } + + this.initVariables(); + console.log(`======== ASH stopped ========`); + } + + /** + * Initializes the ASH serial port and (if enabled) resets the NCP. + * The method used to do the reset is specified by the the host configuration parameter resetMethod. + * + * When the reset method is sending a RST frame, the caller should retry NCP resets a few times if it fails. + * + * @returns + * - EzspStatus.SUCCESS + * - EzspStatus.HOST_FATAL_ERROR + */ + public async resetNcp(): Promise { + if (this.closing) { + return EzspStatus.ERROR_INVALID_CALL; + } + + console.log(`======== ASH NCP reset ========`); + + this.initVariables(); + + let status: EzspStatus; + + // ask ncp to reset itself using RST frame + try { + await this.initPort(); + + this.flags = Flag.RST | Flag.CAN; + + return EzspStatus.SUCCESS; + } catch (err) { + this.hostError = status; + + return EzspStatus.HOST_FATAL_ERROR; + } + } + + /** + * Adds a DATA frame to the transmit queue to send to the NCP. + * Frames that are too long or too short will not be sent, and frames will not be added to the queue + * if the host is not in the Connected state, or the NCP is not ready to receive a DATA frame or if there + * is no room in the queue; + * + * @param len length of data field + * @param inBuf array containing the data to be sent + * + * @returns + * - EzspStatus.SUCCESS + * - EzspStatus.NO_TX_SPACE + * - EzspStatus.DATA_FRAME_TOO_SHORT + * - EzspStatus.DATA_FRAME_TOO_LONG + * - EzspStatus.NOT_CONNECTED + */ + public send(len: number, inBuf: Buffer): EzspStatus { + // Check for errors that might have been detected + if (this.hostError !== EzspStatus.NO_ERROR) { + return EzspStatus.HOST_FATAL_ERROR; + } + + if (this.ncpError !== EzspStatus.NO_ERROR) { + return EzspStatus.ASH_NCP_FATAL_ERROR; + } + + // After verifying that the data field length is within bounds, + // copies data frame to a buffer and appends it to the transmit queue. + if (len < ASH_MIN_DATA_FIELD_LEN) { + return EzspStatus.DATA_FRAME_TOO_SHORT; + } else if (len > ASH_MAX_DATA_FIELD_LEN) { + return EzspStatus.DATA_FRAME_TOO_LONG; + } + + if (!(this.flags & Flag.CONNECTED)) { + return EzspStatus.NOT_CONNECTED; + } + + const buffer: EzspBuffer = this.txFree.allocBuffer(); + + if (buffer == null) { + return EzspStatus.NO_TX_SPACE; + } + + inBuf.copy(buffer.data, 0, 0, len); + + buffer.len = len; + + this.randomizeBuffer(buffer.data, buffer.len);// IN/OUT data + this.txQueue.addTail(buffer); + this.sendExec(); + + return EzspStatus.SUCCESS; + } + + /** + * Manages outgoing communication to the NCP, including DATA frames as well as the frames used for + * initialization and error detection and recovery. + */ + public sendExec(): void { + let outByte: number = 0x00; + let inByte: number = 0x00; + let len: number = 0; + let buffer: EzspBuffer = null; + + // Check for received acknowledgement timer expiry + if (this.ackTimerHasExpired()) { + if (this.flags & Flag.CONNECTED) { + const expectedFrm = ((this.flags & Flag.RETX) ? this.frmReTx : this.frmTx); + + if (this.ackRx !== expectedFrm) { + this.counters.rxAckTimeouts += 1; + + this.adjustAckPeriod(true); + + debug(`Timer expired waiting for ACK for ${expectedFrm}, current=${this.ackRx}`); + + if (++this.timeouts >= ASH_MAX_TIMEOUTS) { + this.hostDisconnect(EzspStatus.ASH_ERROR_TIMEOUTS); + + return; + } + + this.startRetransmission(); + } else { + this.stopAckTimer(); + } + } else { + this.hostDisconnect(EzspStatus.ASH_ERROR_RESET_FAIL); + } + } + + while (this.writer.writeAvailable()) { + // Send ASH_CAN character immediately, ahead of any other transmit data + if (this.flags & Flag.CAN) { + if (this.sendState === SendState.IDLE) { + // sending RST or just woke NCP + this.writer.writeByte(AshReservedByte.CANCEL); + } else if (this.sendState === SendState.TX_DATA) { + // cancel frame in progress + this.counters.txCancelled += 1; + + this.writer.writeByte(AshReservedByte.CANCEL); + + this.stopAckTimer(); + + this.sendState = SendState.IDLE; + } + + this.flags &= ~Flag.CAN; + + continue; + } + + switch (this.sendState) { + case SendState.IDLE: + // In between frames - do some housekeeping and decide what to send next + // If retransmitting, set the next frame to send to the last ackNum + // received, then check to see if retransmission is now complete. + if (this.flags & Flag.RETX) { + if (withinRange(this.frmReTx, this.ackRx, this.frmTx)) { + this.frmReTx = this.ackRx; + } + + if (this.frmReTx === this.frmTx) { + this.flags &= ~Flag.RETX; + + this.scrubReTxQueue(); + } + } + + // restrain ncp if needed + this.dataFrameFlowControl(); + + // See if a short frame is flagged to be sent + // The order of the tests below - RST, NAK and ACK - + // sets the relative priority of sending these frame types. + if (this.flags & Flag.RST) { + this.txSHBuffer[0] = AshFrameType.RST; + + this.setAndStartAckTimer(CONFIG_TIME_RST); + + len = 1; + this.flags &= ~(Flag.RST | Flag.NAK | Flag.ACK); + this.sendState = SendState.SHFRAME; + debug(`---> [FRAME type=RST]`); + } else if (this.flags & (Flag.NAK | Flag.ACK)) { + if (this.flags & Flag.NAK) { + this.txSHBuffer[0] = AshFrameType.NAK + (this.frmRx << ASH_ACKNUM_BIT); + this.flags &= ~(Flag.NRTX | Flag.NAK | Flag.ACK); + debug(`---> [FRAME type=NAK frmRx=${this.frmRx}]`); + } else { + this.txSHBuffer[0] = AshFrameType.ACK + (this.frmRx << ASH_ACKNUM_BIT); + this.flags &= ~(Flag.NRTX | Flag.ACK); + debug(`---> [FRAME type=ACK frmRx=${this.frmRx}]`); + } + + if (this.flags & Flag.NR) { + this.txSHBuffer[0] |= ASH_NFLAG_MASK; + this.flags |= Flag.NRTX; + + this.startNrTimer(); + } + + this.ackTx = this.frmRx; + len = 1; + this.sendState = SendState.SHFRAME; + } else if (this.flags & Flag.RETX) { + // Retransmitting DATA frames for error recovery + buffer = this.reTxQueue.getNthEntry(mod8(this.frmTx - this.frmReTx)); + len = buffer.len + 1; + this.txSHBuffer[0] = AshFrameType.DATA | (this.frmReTx << ASH_FRMNUM_BIT) | (this.frmRx << ASH_ACKNUM_BIT) | ASH_RFLAG_MASK; + this.sendState = SendState.RETX_DATA; + debug(`---> [FRAME type=DATA_RETX frmReTx=${this.frmReTx} frmRx=${this.frmRx}]`); + } else if (this.ackTx != this.frmRx) { + // An ACK should be generated + this.flags |= Flag.ACK; + break; + } else if (!this.txQueue.empty && withinRange(this.ackRx, this.frmTx, (this.ackRx + CONFIG_TX_K - 1)) ) { + // Send a DATA frame if ready + buffer = this.txQueue.head; + len = buffer.len + 1; + + this.counters.txData += (len - 1); + + this.txSHBuffer[0] = AshFrameType.DATA | (this.frmTx << ASH_FRMNUM_BIT) | (this.frmRx << ASH_ACKNUM_BIT); + this.sendState = SendState.TX_DATA; + debug(`---> [FRAME type=DATA frmTx=${this.frmTx} frmRx=${this.frmRx}]`); + } else { + // Otherwise there's nothing to send + this.writer.writeFlush(); + + return; + } + + this.countFrame(true); + + // Start frame - encodeByte() is inited by a non-zero length argument + outByte = this.encodeByte(len, this.txSHBuffer[0]); + + this.writer.writeByte(outByte); + break; + case SendState.SHFRAME: + // sending short frame + if (this.txOffset !== 0xFF) { + inByte = this.txSHBuffer[this.txOffset]; + outByte = this.encodeByte(0, inByte); + + this.writer.writeByte(outByte); + } else { + this.sendState = SendState.IDLE; + } + break; + case SendState.TX_DATA: + case SendState.RETX_DATA: + // sending OR resending data frame + if (this.txOffset !== 0xFF) { + inByte = this.txOffset ? buffer.data[this.txOffset - 1] : this.txSHBuffer[0]; + outByte = this.encodeByte(0, inByte); + + this.writer.writeByte(outByte); + } else { + if (this.sendState === SendState.TX_DATA) { + this.frmTx = inc8(this.frmTx); + buffer = this.txQueue.removeHead(); + + this.reTxQueue.addTail(buffer); + } else { + this.frmReTx = inc8(this.frmReTx); + } + + if (this.ackTimerIsNotRunning()) { + this.startAckTimer(); + } + + this.ackTx = this.frmRx; + this.sendState = SendState.IDLE; + } + break; + } + } + + this.writer.writeFlush(); + } + + /** + * Retrieve a frame and accept, reTx, reject, fail based on type & validity in current state. + * @returns + * - EzspStatus.SUCCESS On valid RSTACK or valid DATA frame. + * - EzspStatus.ASH_IN_PROGRESS + * - EzspStatus.NO_RX_DATA + * - EzspStatus.NO_RX_SPACE + * - EzspStatus.HOST_FATAL_ERROR + * - EzspStatus.ASH_NCP_FATAL_ERROR + */ + private receiveFrame(buffer: Buffer): EzspStatus { + // Check for errors that might have been detected + if (this.hostError !== EzspStatus.NO_ERROR) { + return EzspStatus.HOST_FATAL_ERROR; + } + + if (this.ncpError !== EzspStatus.NO_ERROR) { + return EzspStatus.ASH_NCP_FATAL_ERROR; + } + + let ackNum: number = 0; + let frmNum: number = 0; + let frameType: AshFrameType = AshFrameType.INVALID; + + // Read data from serial port and assemble a frame until complete, aborted + // due to an error, cancelled, or there is no more serial data available. + const status = this.readFrame(buffer); + + switch (status) { + case EzspStatus.SUCCESS: + break; + case EzspStatus.ASH_IN_PROGRESS: + // should have a complete frame by now, if not, don't process further + return EzspStatus.NO_RX_DATA; + case EzspStatus.ASH_CANCELLED: + // should have been taken out in onFrame + return this.hostDisconnect(status); + case EzspStatus.ASH_BAD_CRC: + this.counters.rxCrcErrors += 1; + + this.rejectFrame(); + console.error(`Received frame with CRC error`); + return EzspStatus.NO_RX_DATA; + case EzspStatus.ASH_COMM_ERROR: + this.counters.rxCommErrors += 1; + + this.rejectFrame(); + console.error(`Received frame with comm error`); + return EzspStatus.NO_RX_DATA; + case EzspStatus.ASH_TOO_SHORT: + this.counters.rxTooShort += 1; + + this.rejectFrame(); + console.error(`Received frame shorter than minimum`); + return EzspStatus.NO_RX_DATA; + case EzspStatus.ASH_TOO_LONG: + this.counters.rxTooLong += 1; + + this.rejectFrame(); + console.error(`Received frame longer than maximum`); + return EzspStatus.NO_RX_DATA; + case EzspStatus.ASH_ERROR_XON_XOFF: + return this.hostDisconnect(status); + default: + throw new Error(`Unhandled error while receiving frame.`); + } + + // Got a complete frame - validate its control and length. + // On an error the type returned will be TYPE_INVALID. + frameType = this.getFrameType(this.rxSHBuffer[0], this.rxLen); + + // Free buffer allocated for a received frame if: + // DATA frame, and out of order + // DATA frame, and not in the CONNECTED state + // not a DATA frame + if (frameType === AshFrameType.DATA) { + if (!(this.flags & Flag.CONNECTED) || (ashGetFrmNum(this.rxSHBuffer[0]) !== this.frmRx)) { + this.freeNonNullRxBuffer(); + } + } else { + this.freeNonNullRxBuffer(); + } + + const frameTypeStr = AshFrameType[frameType]; + + debug(`<--- [FRAME type=${frameTypeStr}]`); + this.countFrame(false); + + // Process frames received while not in the connected state - + // ignore everything except RSTACK and ERROR frames + if (!(this.flags & Flag.CONNECTED)) { + if (frameType === AshFrameType.RSTACK) { + // RSTACK frames have the ncp ASH version in the first data field byte, + // and the reset reason in the second byte + if (this.rxSHBuffer[1] !== ASH_VERSION) { + return this.hostDisconnect(EzspStatus.ASH_ERROR_VERSION); + } + + // Ignore a RSTACK if the reset reason doesn't match our reset method + if (this.rxSHBuffer[2] !== NcpFailedCode.RESET_SOFTWARE) { + return EzspStatus.ASH_IN_PROGRESS; + } + + this.ncpError = EzspStatus.NO_ERROR; + + this.stopAckTimer(); + + this.timeouts = 0; + + this.setAckPeriod(CONFIG_ACK_TIME_INIT); + + this.flags = Flag.CONNECTED | Flag.ACK; + + console.log(`======== ASH connected ========`); + + return EzspStatus.SUCCESS; + } else if (frameType === AshFrameType.ERROR) { + return this.ncpDisconnect(this.rxSHBuffer[2]); + } + + return EzspStatus.ASH_IN_PROGRESS; + } + + // Connected - process the ackNum in ACK, NAK and DATA frames + if ((frameType === AshFrameType.DATA) || (frameType === AshFrameType.ACK) || (frameType === AshFrameType.NAK) ) { + ackNum = ashGetACKNum(this.rxSHBuffer[0]); + + debug(`<--- [FRAME type=${frameTypeStr} ackNum=${ackNum}]`); + + if (!withinRange(this.ackRx, ackNum, this.frmTx)) { + this.counters.rxBadAckNumber += 1; + + debug(`<-x- [FRAME type=${frameTypeStr} ackNum=${ackNum}] Invalid ACK num; not within <${this.ackRx}-${this.frmTx}>`); + + frameType = AshFrameType.INVALID; + } else if (ackNum !== this.ackRx) { + // new frame(s) ACK'ed? + this.ackRx = ackNum; + this.timeouts = 0; + + if (this.flags & Flag.RETX) { + // start timer if unACK'ed frames + this.stopAckTimer(); + + if (ackNum !== this.frmReTx) { + this.startAckTimer(); + } + } else { + this.adjustAckPeriod(false);// factor ACK time into period + + if (ackNum !== this.frmTx) { + // if more unACK'ed frames, + this.startAckTimer();// then restart ACK timer + } + + this.scrubReTxQueue();// free buffer(s) in ReTx queue + } + } + } + + // Process frames received while connected + switch (frameType) { + case AshFrameType.DATA: + frmNum = ashGetFrmNum(this.rxSHBuffer[0]); + const frameStr = `[FRAME type=${frameTypeStr} ackNum=${ackNum} frmNum=${frmNum}]`; + + if (frmNum === this.frmRx) { + // is frame in sequence? + if (this.rxDataBuffer == null) { + // valid frame but no memory? + this.counters.rxNoBuffer += 1; + + debug(`<-x- ${frameStr} No buffer available`); + + this.rejectFrame(); + + return EzspStatus.NO_RX_SPACE; + } + + if (this.rxSHBuffer[0] & ASH_RFLAG_MASK) { + // if retransmitted, force ACK + this.flags |= Flag.ACK; + } + + this.flags &= ~(Flag.REJ | Flag.NAK);// clear the REJ condition + this.frmRx = inc8(this.frmRx); + + this.randomizeBuffer(this.rxDataBuffer.data, this.rxDataBuffer.len);// IN/OUT data + this.rxQueue.addTail(this.rxDataBuffer);// add frame to receive queue + + debug(`<--- ${frameStr} Added to rxQueue`); + + this.counters.rxData += this.rxDataBuffer.len; + + setImmediate(() => { + this.emit(AshEvents.frame); + }); + return EzspStatus.SUCCESS; + } else { + // frame is out of sequence + if (this.rxSHBuffer[0] & ASH_RFLAG_MASK) { + // if retransmitted, force ACK + this.counters.rxDuplicates += 1; + this.flags |= Flag.ACK; + } else { + // 1st OOS? then set REJ, send NAK + if ((this.flags & Flag.REJ) === 0) { + this.counters.rxOutOfSequence += 1; + + debug(`<-x- ${frameStr} Out of sequence: expected ${this.frmRx}; got ${frmNum}.`); + } + + this.rejectFrame(); + } + } + break; + case AshFrameType.ACK: + // already fully processed + break; + case AshFrameType.NAK: + // start retransmission if needed + this.startRetransmission(); + + break; + case AshFrameType.RSTACK: + // unexpected ncp reset + this.ncpError = this.rxSHBuffer[2]; + + return this.hostDisconnect(EzspStatus.ASH_ERROR_NCP_RESET); + case AshFrameType.ERROR: + // ncp error + return this.ncpDisconnect(this.rxSHBuffer[2]); + case AshFrameType.INVALID: + // reject invalid frames + debug(`<-x- [FRAME type=${frameTypeStr}] Rejecting. ${this.rxSHBuffer.toString('hex')}`); + + this.rejectFrame(); + break; + } + + return EzspStatus.ASH_IN_PROGRESS; + } + + /** + * If the last control byte received was a DATA control, and we are connected and not already in the reject condition, + * then send a NAK and set the reject condition. + */ + private rejectFrame(): void { + if (((this.rxSHBuffer[0] & ASH_DFRAME_MASK) === AshFrameType.DATA) + && ((this.flags & (Flag.REJ | Flag.CONNECTED)) === Flag.CONNECTED) ) { + this.flags |= (Flag.REJ | Flag.NAK); + } + } + + /** + * Retrieve and process serial bytes. + * @returns + */ + private readFrame(buffer: Buffer): EzspStatus { + let status: EzspStatus; + let index: number = 0; + // let inByte: number = 0x00; + let outByte: number = 0x00; + + if (!this.decodeInProgress) { + this.rxLen = 0; + this.rxDataBuffer = null; + } + + for (const inByte of buffer) { + // 0xFF byte signals a callback is pending when between frames in synchronous (polled) callback mode. + if (!this.decodeInProgress && (inByte === ASH_WAKE)) { + if (this.ncpSleepEnabled) { + this.ncpHasCallbacks = true; + } + + status = EzspStatus.ASH_IN_PROGRESS; + continue; + } + + // Decode next input byte - note that many input bytes do not produce + // an output byte. Return on any error in decoding. + index = this.rxLen; + [status, outByte, this.rxLen] = this.decodeByte(inByte, outByte, this.rxLen); + + // discard an invalid frame + if ((status !== EzspStatus.ASH_IN_PROGRESS) && (status !== EzspStatus.SUCCESS)) { + this.freeNonNullRxBuffer(); + + break; + } + + // if input byte produced an output byte + if (this.rxLen !== index) { + if (this.rxLen <= SH_RX_BUFFER_LEN) { + // if a short frame, return in rxBuffer + this.rxSHBuffer[index] = outByte; + } else { + // if a longer DATA frame, allocate an EzspBuffer for it. + // (Note the control byte is always returned in shRxBuffer[0]. + // Even if no buffer can be allocated, the control's ackNum must be processed.) + if (this.rxLen === (SH_RX_BUFFER_LEN + 1)) { + // alloc buffer, copy prior data + this.rxDataBuffer = this.rxFree.allocBuffer(); + + if (this.rxDataBuffer !== null) { + // const len = SH_RX_BUFFER_LEN - 1; + + // (void) memcpy(this.rxDataBuffer.data, this.shRxBuffer + 1, SH_RX_BUFFER_LEN - 1); + this.rxSHBuffer.copy(this.rxDataBuffer.data, 0, 1, SH_RX_BUFFER_LEN); + + this.rxDataBuffer.len = SH_RX_BUFFER_LEN - 1; + } + } + + if (this.rxDataBuffer !== null) { + // copy next byte to buffer + this.rxDataBuffer.data[index - 1] = outByte;// -1 since control is omitted + this.rxDataBuffer.len = index; + } + } + } + + if (status !== EzspStatus.ASH_IN_PROGRESS) { + break; + } + } + + return status; + } + + /** + * + */ + private freeNonNullRxBuffer(): void { + if (this.rxDataBuffer !== null) { + this.rxFree.freeBuffer(this.rxDataBuffer); + + this.rxDataBuffer = null; + } + } + + /** + * + */ + private scrubReTxQueue(): void { + let buffer: EzspBuffer; + + while (this.ackRx !== this.frmReTxHead) { + buffer = this.reTxQueue.removeHead(); + + this.txFree.freeBuffer(buffer); + + this.frmReTxHead = inc8(this.frmReTxHead); + } + } + + /** + * If not already retransmitting, and there are unacked frames, start retransmitting after the last frame that was acked. + */ + private startRetransmission(): void { + if (!(this.flags & Flag.RETX) && (this.ackRx != this.frmTx) ) { + this.stopAckTimer(); + + this.frmReTx = this.ackRx; + this.flags |= (Flag.RETX | Flag.CAN); + } + } + + /** + * Check free rx buffers to see whether able to receive DATA frames: set or clear NR flag appropriately. + * Inform ncp of our status using the nFlag in ACKs and NAKs. + * Note that not ready status must be refreshed if it persists beyond a maximum time limit. + */ + private dataFrameFlowControl(): void { + if (this.flags & Flag.CONNECTED) { + // Set/clear NR flag based on the number of buffers free + if (this.rxFree.length < CONFIG_NR_LOW_LIMIT) { + this.flags |= Flag.NR; + + debug(`NOT READY - Signaling NCP`); + } else if (this.rxFree.length > CONFIG_NR_HIGH_LIMIT) { + this.flags &= ~Flag.NR; + + this.stopNrTimer(); // needed?? + // debug(`READY - Signaling NCP`);// spams-a-lot + } + + // Force an ACK (or possibly NAK) if we need to send an updated nFlag + // due to either a changed NR status or to refresh a set nFlag + if (this.flags & Flag.NR) { + if (!(this.flags & Flag.NRTX) || this.nrTimerHasExpired()) { + this.flags |= Flag.ACK; + + this.startNrTimer(); + } + } else { + this.nrTimerHasExpired();// ensure timer checked often + + if (this.flags & Flag.NRTX) { + this.flags |= Flag.ACK; + + this.stopNrTimer();// needed??? + } + } + } else { + this.stopNrTimer(); + + this.flags &= ~(Flag.NRTX | Flag.NR); + } + } + + /** + * Sets a fatal error state at the Host level. + * @param error + * @returns EzspStatus.HOST_FATAL_ERROR + */ + private hostDisconnect(error: EzspStatus): EzspStatus { + this.flags = 0; + this.hostError = error; + + console.error(`ASH disconnected: ${EzspStatus[error]} | NCP status: ${EzspStatus[this.ncpError]}`); + + this.emit(AshEvents.hostError, error); + + return EzspStatus.HOST_FATAL_ERROR; + } + + /** + * Sets a fatal error state at the NCP level. Will require a reset. + * @param error + * @returns EzspStatus.ASH_NCP_FATAL_ERROR + */ + private ncpDisconnect(error: EzspStatus): EzspStatus { + this.flags = 0; + this.ncpError = error; + + console.error(`ASH disconnected: ${EzspStatus[error]} | NCP status: ${EzspStatus[this.ncpError]}`); + + this.emit(AshEvents.ncpError, error); + + return EzspStatus.ASH_NCP_FATAL_ERROR; + } + + /** + * Same as randomizeArray(0, buffer, len). + * Returns buffer as-is if randomize is OFF. + * @param buffer IN/OUT + * @param len + */ + public randomizeBuffer(buffer: Buffer, len: number): void { + // If enabled, exclusive-OR buffer data with a pseudo-random sequence + if (CONFIG_RANDOMIZE) { + this.randomizeArray(0, buffer, len);// zero inits the random sequence + } + } + + /** + * Randomizes array contents by XORing with an 8-bit pseudo random sequence. + * This reduces the likelihood that byte-stuffing will greatly increase the size of the payload. + * (This could happen if a DATA frame contained repeated instances of the same reserved byte value.) + * + * @param seed zero initializes the random sequence a non-zero value continues from a previous invocation + * @param buf IN/OUT pointer to the array whose contents will be randomized + * @param len number of bytes in the array to modify + * @returns last value of the sequence. + * If a buffer is processed in two or more chunks, as with linked buffers, + * this value should be passed back as the value of the seed argument + */ + public randomizeArray(seed: number, buf: Buffer, len: number): number { + let outIdx = 0; + + if (seed === 0) { + seed = LFSR_SEED; + } + + while (len--) { + // *buf++ ^= seed; + buf[outIdx++] ^= seed; + + seed = (seed & 1) ? ((seed >> 1) ^ LFSR_POLY) : (seed >> 1); + } + + return seed; + } + + /** + * Get the frame type from the control byte and validate it against the frame length. + * @param control + * @param len Frame length + * @returns AshFrameType.INVALID if bad control/length otherwise the frame type. + */ + public getFrameType(control: number, len: number): AshFrameType { + if (control === AshFrameType.RSTACK) { + if (len === ASH_FRAME_LEN_RSTACK) { + return AshFrameType.RSTACK; + } + } else if (control === AshFrameType.ERROR) { + if (len === ASH_FRAME_LEN_ERROR) { + return AshFrameType.ERROR; + } + } else if ( (control & ASH_DFRAME_MASK) === AshFrameType.DATA) { + if (len >= ASH_FRAME_LEN_DATA_MIN) { + return AshFrameType.DATA; + } + } else if ( (control & ASH_SHFRAME_MASK) === AshFrameType.ACK) { + if (len === ASH_FRAME_LEN_ACK) { + return AshFrameType.ACK; + } + } else if ( (control & ASH_SHFRAME_MASK) === AshFrameType.NAK) { + if (len === ASH_FRAME_LEN_NAK) { + return AshFrameType.NAK; + } + } else { + this.counters.rxBadControl += 1; + debug(`Frame illegal control ${control}.`);// EzspStatus.ASH_BAD_CONTROL + + return AshFrameType.INVALID; + } + + this.counters.rxBadLength += 1; + debug(`Frame illegal length ${len} for control ${control}.`);// EzspStatus.ASH_BAD_LENGTH + + return AshFrameType.INVALID; + } + + /** + * Encode byte for sending. + * @param len Start a new frame if non-zero + * @param byte + * @returns outByte + */ + private encodeByte(len: number, byte: number): number { + // start a new frame if len is non-zero + if (len) { + this.encodeCount = len; + this.txOffset = 0; + this.encodeState = 0; + this.encodeEscFlag = false; + this.encodeCrc = 0xFFFF; + } + + // was an escape last time? + if (this.encodeEscFlag) { + this.encodeEscFlag = false; + + // send data byte with bit flipped + return this.encodeFlip; + } + + // control and data field bytes + if (this.encodeState === 0) { + this.encodeCrc = halCommonCrc16(byte, this.encodeCrc); + + if (--this.encodeCount === 0) { + this.encodeState = 1; + } else { + ++this.txOffset; + } + + return this.encodeStuffByte(byte); + } else if (this.encodeState === 1) { + // CRC high byte + this.encodeState = 2; + + return this.encodeStuffByte(this.encodeCrc >> 8); + } else if (this.encodeState === 2) { + // CRC low byte + this.encodeState = 3; + + return this.encodeStuffByte(this.encodeCrc & 0xFF); + } + + this.txOffset = 0xFF; + + return AshReservedByte.FLAG; + } + + /** + * Stuff byte as defined by ASH protocol. + * @param byte + * @returns + */ + private encodeStuffByte(byte: number): number { + if (AshReservedByte[byte] != null) { + // is special byte + this.encodeEscFlag = true; + this.encodeFlip = byte ^ ASH_FLIP; + + return AshReservedByte.ESCAPE; + } else { + return byte; + } + } + + /** + * Decode received byte. + * @param byte + * @param inByte IN/OUT + * @param inLen IN/OUT + * @returns [EzspStatus, outByte, outLen] + * - EzspStatus.ASH_IN_PROGRESS + * - EzspStatus.ASH_COMM_ERROR + * - EzspStatus.ASH_BAD_CRC + * - EzspStatus.ASH_TOO_SHORT + * - EzspStatus.ASH_TOO_LONG + * - EzspStatus.SUCCESS + * - EzspStatus.ASH_CANCELLED + * - EzspStatus.ASH_ERROR_XON_XOFF + */ + private decodeByte(byte: number, inByte: number, inLen: number): [EzspStatus, outByte: number, outLen: number] { + let status: EzspStatus = EzspStatus.ASH_IN_PROGRESS; + + if (!this.decodeInProgress) { + this.decodeLen = 0; + this.decodeByte1 = 0; + this.decodeByte2 = 0; + this.decodeFlip = 0; + this.decodeCrc = 0xFFFF; + } + + switch (byte) { + case AshReservedByte.FLAG: + // flag byte (frame delimiter) + if (this.decodeLen === 0) { + // if no frame data, not end flag, so ignore it + this.decodeFlip = 0;// ignore isolated data escape between flags + break; + } else if (this.decodeLen === 0xFF) { + status = EzspStatus.ASH_COMM_ERROR; + } else if (this.decodeCrc !== ((this.decodeByte2 << 8) + this.decodeByte1)) { + status = EzspStatus.ASH_BAD_CRC; + } else if (this.decodeLen < ASH_MIN_FRAME_WITH_CRC_LEN) { + status = EzspStatus.ASH_TOO_SHORT; + } else if (this.decodeLen > ASH_MAX_FRAME_WITH_CRC_LEN) { + status = EzspStatus.ASH_TOO_LONG; + } else { + status = EzspStatus.SUCCESS; + } + break; + case AshReservedByte.ESCAPE: + // byte stuffing escape byte + this.decodeFlip = ASH_FLIP; + break; + case AshReservedByte.CANCEL: + // cancel frame without an error + status = EzspStatus.ASH_CANCELLED; + break; + case AshReservedByte.SUBSTITUTE: + // discard remainder of frame + this.decodeLen = 0xFF;// special value flags low level comm error + break; + case AshReservedByte.XON: + case AshReservedByte.XOFF: + // If host is using RTS/CTS, ignore any XON/XOFFs received from the NCP. + // If using XON/XOFF, the host driver must remove them from the input stream. + // If it doesn't, it probably means the driver isn't setup for XON/XOFF, + // so issue an error to flag the serial port driver problem. + if (this.serialPort != null && !this.serialPort.settings.rtscts) { + status = EzspStatus.ASH_ERROR_XON_XOFF; + } + break; + default: + // a normal byte + byte ^= this.decodeFlip; + this.decodeFlip = 0; + + if (this.decodeLen <= ASH_MAX_FRAME_WITH_CRC_LEN) { + // limit length to max + 1 + ++this.decodeLen; + } + + if (this.decodeLen > ASH_CRC_LEN) { + // compute frame CRC even if too long + this.decodeCrc = halCommonCrc16(this.decodeByte2, this.decodeCrc); + + if (this.decodeLen <= ASH_MAX_FRAME_WITH_CRC_LEN) { + // store to only max len + inByte = this.decodeByte2; + inLen = this.decodeLen - ASH_CRC_LEN;// CRC is not output, reduce length + } + } + + this.decodeByte2 = this.decodeByte1; + this.decodeByte1 = byte; + break; + } + + this.decodeInProgress = (status === EzspStatus.ASH_IN_PROGRESS); + + return [status, inByte, inLen]; + } + + /** + * Starts the Not Ready timer + * + * On the host, this times nFlag refreshing when the host doesn't have room for callbacks for a prolonged period. + * + * On the NCP, if this times out the NCP resumes sending callbacks. + */ + private startNrTimer(): void { + this.nrTimer = (Date.now() + CONFIG_NR_TIME); + } + + /** + * Stop Not Ready timer (set to 0). + */ + private stopNrTimer(): void { + this.nrTimer = 0; + } + + /** + * Tests whether the Not Ready timer has expired or has stopped. If expired, it is stopped. + * + * @returns true if the Not Ready timer has expired or stopped + */ + private nrTimerHasExpired(): boolean { + if (this.nrTimer) { + if ((Date.now() - this.nrTimer) >= 0) { + this.nrTimer = 0; + } + } + + return (!this.nrTimer); + } + + /** + * Indicates whether or not Not Ready timer is currently running. + * + * @return True if nrTime == 0 + */ + private nrTimerIsNotRunning(): boolean { + return this.nrTimer === 0; + } + + /** + * Sets the acknowledgement timer period (in msec) and stops the timer. + */ + private setAckPeriod(msec: number): void { + this.ackPeriod = msec; + this.ackTimer = 0; + } + + /** + * Sets the acknowledgement timer period (in msec), and starts the timer running. + */ + private setAndStartAckTimer(msec: number): void { + this.setAckPeriod(msec); + this.startAckTimer(); + } + + /** + * Adapts the acknowledgement timer period to the observed ACK delay. + * If the timer is not running, it does nothing. + * If the timer has expired, the timeout period is doubled. + * If the timer has not expired, the elapsed time is fed into simple + * + * IIR filter: + * T[n+1] = (7*T[n] + elapsedTime) / 8 + * + * The timeout period, ackPeriod, is limited such that: + * config.ackTimeMin <= ackPeriod <= config.ackTimeMax. + * + * The acknowledgement timer is always stopped by this function. + * + * @param expired true if timer has expired + */ + private adjustAckPeriod(expired: boolean): void { + if (expired) { + // if expired, double the period + this.ackPeriod += this.ackPeriod; + } else if (this.ackTimer) { + // adjust period only if running + // time elapsed since timer was started + let temp: number = this.ackPeriod; + // compute time to receive acknowledgement, then stop timer + const lastAckTime: number = Date.now() - this.ackTimer; + temp = (temp << 3) - temp; + temp += lastAckTime << 2; + temp >>= 3; + this.ackPeriod = (temp & 0xFFFF); + } + + // keep ackPeriod within limits + if (this.ackPeriod > CONFIG_ACK_TIME_MAX) { + this.ackPeriod = CONFIG_ACK_TIME_MAX; + } else if (this.ackPeriod < CONFIG_ACK_TIME_MIN) { + this.ackPeriod = CONFIG_ACK_TIME_MIN; + } + + this.ackTimer = 0;// always stop the timer + } + + /** + * Sets ACK Timer to the specified period and starts it running. + */ + private startAckTimer(): void { + this.ackTimer = Date.now(); + } + + /** + * Stops and clears ACK Timer. + */ + private stopAckTimer(): void { + this.ackTimer = 0; + } + + /** + * Indicates whether or not ACK Timer has expired. + * If the timer is stopped (0) then it is not expired. + * + * @returns + */ + private ackTimerHasExpired(): boolean { + if (this.ackTimer === 0) { + // if timer is not running, return false + return false; + } + + // return ((halCommonGetInt16uMillisecondTick() - this.ackTimer) >= this.ackPeriod); + return ((Date.now() - this.ackTimer) >= this.ackPeriod); + } + + /** + * Indicates whether or not ACK Timer is currently running (!= 0). + * The timer may be running even if expired. + */ + private ackTimerIsNotRunning(): boolean { + return this.ackTimer === 0; + } + + /** + * Increase counters based on frame type and direction. + * @param sent True if frame being sent, false if being received. + */ + private countFrame(sent: boolean): void { + let control: number; + + if (sent) { + control = this.txSHBuffer[0]; + this.counters.txAllFrames += 1; + } else { + control = this.rxSHBuffer[0]; + this.counters.rxAllFrames += 1; + } + + if ((control & ASH_DFRAME_MASK) === AshFrameType.DATA) { + if (sent) { + if (control & ASH_RFLAG_MASK) { + this.counters.txReDataFrames += 1; + } else { + this.counters.txDataFrames += 1; + } + } else { + if (control & ASH_RFLAG_MASK) { + this.counters.rxReDataFrames += 1; + } else { + this.counters.rxDataFrames += 1; + } + } + } else if ((control & ASH_SHFRAME_MASK) === AshFrameType.ACK) { + if (sent) { + this.counters.txAckFrames += 1; + + if (control & ASH_NFLAG_MASK) { + this.counters.txN1Frames += 1; + }/* else { + this.counters.txN0Frames += 1; + }*/ + } else { + this.counters.rxAckFrames += 1; + + if (control & ASH_NFLAG_MASK) { + this.counters.rxN1Frames += 1; + }/* else { + this.counters.rxN0Frames += 1; + }*/ + } + } else if ((control & ASH_SHFRAME_MASK) === AshFrameType.NAK) { + if (sent) { + this.counters.txNakFrames += 1; + + if (control & ASH_NFLAG_MASK) { + this.counters.txN1Frames += 1; + }/* else { + this.counters.txN0Frames += 1; + }*/ + } else { + this.counters.rxNakFrames += 1; + + if (control & ASH_NFLAG_MASK) { + this.counters.rxN1Frames += 1; + }/* else { + this.counters.rxN0Frames += 1; + }*/ + } + } + } + + /** + * Prints counters in a nicely formatted table. + */ + private printCounters(): void { + console.table({ + "Total frames": {"Received": this.counters.rxAllFrames, "Transmitted": this.counters.txAllFrames}, + "Cancelled": {"Received": this.counters.rxCancelled, "Transmitted": this.counters.txCancelled}, + "DATA frames": {"Received": this.counters.rxDataFrames, "Transmitted": this.counters.txDataFrames}, + "DATA bytes": {"Received": this.counters.rxData, "Transmitted": this.counters.txData}, + "Retry frames": {"Received": this.counters.rxReDataFrames, "Transmitted": this.counters.txReDataFrames}, + "ACK frames": {"Received": this.counters.rxAckFrames, "Transmitted": this.counters.txAckFrames}, + "NAK frames": {"Received": this.counters.rxNakFrames, "Transmitted": this.counters.txNakFrames}, + "nRdy frames": {"Received": this.counters.rxN1Frames, "Transmitted": this.counters.txN1Frames}, + }); + + console.table({ + "CRC errors": {"Received": this.counters.rxCrcErrors}, + "Comm errors": {"Received": this.counters.rxCommErrors}, + "Length < minimum": {"Received": this.counters.rxTooShort}, + "Length > maximum": {"Received": this.counters.rxTooLong}, + "Bad controls": {"Received": this.counters.rxBadControl}, + "Bad lengths": {"Received": this.counters.rxBadLength}, + "Bad ACK numbers": {"Received": this.counters.rxBadAckNumber}, + "Out of buffers": {"Received": this.counters.rxNoBuffer}, + "Retry dupes": {"Received": this.counters.rxDuplicates}, + "Out of sequence": {"Received": this.counters.rxOutOfSequence}, + "ACK timeouts": {"Received": this.counters.rxAckTimeouts}, + }); + } +} diff --git a/src/adapter/ember/uart/consts.ts b/src/adapter/ember/uart/consts.ts new file mode 100644 index 0000000000..ffd32a44fa --- /dev/null +++ b/src/adapter/ember/uart/consts.ts @@ -0,0 +1,115 @@ +import {EZSP_MAX_FRAME_LENGTH, EZSP_MIN_FRAME_LENGTH} from "../ezsp/consts"; + +/** + * Define the size of the receive buffer pool on the EZSP host. + * + * The number of receive buffers does not need to be greater than the number of packet buffers available on the NCP, + * because this in turn is the maximum number of callbacks that could be received between commands. + * In reality a value of 20 is a generous allocation. + */ +export const EZSP_HOST_RX_POOL_SIZE = 20; +/** + * The number of transmit buffers must be set to the number of receive buffers + * -- to hold the immediate ACKs sent for each callabck frame received -- + * plus 3 buffers for the retransmit queue and one each for an automatic ACK + * (due to data flow control) and a command. + */ +export const TX_POOL_BUFFERS = (EZSP_HOST_RX_POOL_SIZE + 5); + + +/** protocol version */ +export const ASH_VERSION = 2; + +/** + * Timeouts before link is judged down. + * + * Consecutive ACK timeouts (minus 1) needed to enter the ERROR state. + * + * Is 3 in ash-ncp.h + */ +export const ASH_MAX_TIMEOUTS = 6; +/** max time in msecs for ncp to wake */ +export const ASH_MAX_WAKE_TIME = 150; + +/** + * Define the units used by the Not Ready timer as 2**n msecs + * log2 of msecs per NR timer unit + */ +export const ASH_NR_TIMER_BIT = 4; + +/** Control byte mask for DATA frame */ +export const ASH_DFRAME_MASK = 0x80; +/** Control byte mask for short frames (ACK/NAK) */ +export const ASH_SHFRAME_MASK = 0xE0; + +/** Acknowledge frame number */ +export const ASH_ACKNUM_MASK = 0x07; +export const ASH_ACKNUM_BIT = 0; +/** Retransmitted frame flag */ +export const ASH_RFLAG_MASK = 0x08; +export const ASH_RFLAG_BIT = 3; +/** Receiver not ready flag */ +export const ASH_NFLAG_MASK = 0x08; +export const ASH_NFLAG_BIT = 3; +/** Reserved for future use */ +export const ASH_PFLAG_MASK = 0x10; +export const ASH_PFLAG_BIT = 4; +/** DATA frame number */ +export const ASH_FRMNUM_MASK = 0x70; +export const ASH_FRMNUM_BIT = 4; + + +/** + * The wake byte special function applies only when in between frames, + * so it does not need to be escaped within a frame. + * (also means NCP data pending) + */ +export const ASH_WAKE = 0xFF; /*!< */ + +/** Constant used in byte-stuffing (XOR mask used in byte stuffing) */ +export const ASH_FLIP = 0x20; + + +// Field and frame lengths, excluding flag byte and any byte stuffing overhead +// All limits are inclusive +export const ASH_MIN_DATA_FIELD_LEN = EZSP_MIN_FRAME_LENGTH; +export const ASH_MAX_DATA_FIELD_LEN = EZSP_MAX_FRAME_LENGTH; +/** with control */ +export const ASH_MIN_DATA_FRAME_LEN = (ASH_MIN_DATA_FIELD_LEN + 1); +/** control plus data field, but not CRC */ +export const ASH_MIN_FRAME_LEN = 1; +export const ASH_MAX_FRAME_LEN = (ASH_MAX_DATA_FIELD_LEN + 1); +export const ASH_CRC_LEN = 2; +export const ASH_MIN_FRAME_WITH_CRC_LEN = (ASH_MIN_FRAME_LEN + ASH_CRC_LEN); +export const ASH_MAX_FRAME_WITH_CRC_LEN = (ASH_MAX_FRAME_LEN + ASH_CRC_LEN); + + +// Lengths for each frame type: includes control and data field (if any), excludes the CRC and flag bytes +/** ash frame len data min */ +export const ASH_FRAME_LEN_DATA_MIN = (ASH_MIN_DATA_FIELD_LEN + 1); +/** [control] */ +export const ASH_FRAME_LEN_ACK = 1; +/** [control] */ +export const ASH_FRAME_LEN_NAK = 1; +/** [control] */ +export const ASH_FRAME_LEN_RST = 1; +/** [control, version, reset reason] */ +export const ASH_FRAME_LEN_RSTACK = 3; +/** [control, version, error] */ +export const ASH_FRAME_LEN_ERROR = 3; + + +// Define lengths of short frames - includes control byte and data field +/** longest non-data frame sent */ +export const SH_TX_BUFFER_LEN = 2; +/** longest non-data frame received */ +export const SH_RX_BUFFER_LEN = 3; + + +// Define constants for the LFSR in randomizeBuffer() +/** polynomial */ +export const LFSR_POLY = 0xB8; +/** initial value (seed) */ +export const LFSR_SEED = 0x42; + +export const VALID_BAUDRATES = [600,1200,2400,4800,9600,19200,38400,57600,115200,230400,460800]; diff --git a/src/adapter/ember/uart/enums.ts b/src/adapter/ember/uart/enums.ts new file mode 100644 index 0000000000..a9c9fb06f1 --- /dev/null +++ b/src/adapter/ember/uart/enums.ts @@ -0,0 +1,192 @@ +/** + * Identify the type of frame from control byte. + * + * Control byte formats + * +---------+----+----+----+----+----+----+----+----++---------+ + * | | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 || Range | + * +---------+----+----+----+----+----+----+----+----++---------+ + * | DATA | 0 | frameNum | rF | ackNum ||0x00-0x7F| + * +---------+----+----+----+----+----+----+----+----++---------+ + * | ACK | 1 | 0 | 0 | pF | nF | ackNum ||0x80-0x9F| + * | NAK | 1 | 0 | 1 | pF | nF | ackNum ||0xA0-0xBF| + * +---------+----+----+----+----+----+----+----+----++---------+ + * | RST | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 || 0xC0 | + * | RSTACK | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 || 0xC1 | + * | ERROR | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 || 0xC2 | + * +---------+----+----+----+----+----+----+----+----++---------+ + * rF = rFlag (retransmission flag) + * nF = nFlag (receiver not ready flag, always 0 in frames sent by the NCP) + * pF = flag reserved for future use + * frameNum = DATA frame’s 3-bit sequence number + * ackNum = acknowledges receipt of DATA frames up to, but not including, ackNum + * Control byte values 0xC3-0xFE are unused, 0xFF is reserved. + */ +export enum AshFrameType { + INVALID = -1, + /** + * Carries all EZSP frames. + * + * [CONTROL, EZSP 0, EZSP 1, EZSP 2, EZSP n, CRC high, CRC low, FLAG] + * + * Notation used in documentation: DATA(F, A, R) + * - F: frame number (frmNum) + * - A: acknowledge number (ackNum) + * - R: retransmit flag (reTx) + * + * Example without pseudo-random sequence applied to Data Field: + * - EZSP “version” command: 00 00 00 02 + * - DATA(2, 5, 0) = 25 00 00 00 02 1A AD 7E + * - EZSP “version” response: 00 80 00 02 02 11 30 + * - DATA(5, 3, 0) = 53 00 80 00 02 02 11 30 63 16 7E + * + * Example with pseudo-random sequence applied to Data Field: + * - EZSP “version” command: 00 00 00 02 + * - DATA(2, 5, 0) = 25 42 21 A8 56 A6 09 7E + * - EZSP “version” response: 00 80 00 02 02 11 30 + * - DATA(5, 3, 0) = 53 42 A1 A8 56 28 04 82 96 23 7E + * + * Sent by: NCP, Host + */ + DATA = 0, + /** + * Acknowledges receipt of a valid DATA frame. + * + * [CONTROL, CRC high, CRC low, FLAG] + * + * Notation used in documentation: ACK(A)+/- + * - A: acknowledge number (ackNum) + * - +/-: not ready flag (nRdy); “+” = “0” = “ready”; “-” = “1” = “not ready” + * + * Examples: + * - ACK(1)+ :81 60 59 7E + * - ACK(6)– : 8E 91 B6 7E + * + * Sent by: NCP, Host + */ + ACK = 0x80,// 0b10000000 + /** + * Indicates receipt of a DATA frame with an error or that was discarded due to lack of memory. + * + * [CONTROL, CRC high, CRC low, FLAG] + * + * Notation used in documentation: NAK(A)+/- + * - A: acknowledge number (ackNum) + * - +/-: not ready flag (nRdy); “+” = “0” = “ready”; “-” = “1” = “not ready” + * + * Examples: + * - NAK(6)+ : A6 34 DC 7E + * - NAK(5)- : AD 85 B7 7E + * + * Sent by: NCP, Host + */ + NAK = 0xA0,// 0b10100000 + /** + * Requests the NCP to perform a software reset (valid even if the NCP is in the FAILED state). + * + * [CONTROL, CRC high, CRC low, FLAG] + * + * Notation used in documentation: RST() + * + * Example: C0 38 BC 7E + * + * Sent by: Host + */ + RST = 0xC0,// 0b11000000 + /** + * Informs the Host that the NCP has reset and the reason for the reset. + * + * [CONTROL, version, reset code, CRC high, CRC low, FLAG] + * + * Notation used in documentation: RSTACK(V, C) + * - V: version + * - C: reset code + * + * Example: C1 02 02 9B 7B 7E + * + * Sent by: NCP + */ + RSTACK = 0xC1,// 0b11000001 + /** + * Informs the Host that the NCP detected a fatal error and is in the FAILED state. + * + * [CONTROL, version, error code, CRC high, CRC low, FLAG] + * + * Notation used in documentation: ERROR(V, C ) + * - V: version + * - C: reset code + * + * Example: C2 01 52 FA BD 7E + * + * Sent by: NCP + */ + ERROR = 0xC2,// 0b11000010 +} + +export enum AshReservedByte { + /** + * Marks the end of a frame. + * + * When a Flag Byte is received, the data received since the last Flag Byte or Cancel Byte + * is tested to see whether it is a valid frame. */ + FLAG = 0x7E, + /** + * Indicates that the following byte is escaped. + * + * If the byte after the Escape Byte is not a reserved byte, + * bit 5 of the byte is complemented to restore its original value. + * If the byte after the Escape Byte is a reserved value, the Escape Byte has no effect. */ + ESCAPE = 0x7D, + /** + * Resume transmission. + * + * Used in XON/XOFF flow control. Always ignored if received by the NCP. + */ + XON = 0x11, + /** + * Stop transmission. + * + * Used in XON/XOFF flow control. Always ignored if received by the NCP + */ + XOFF = 0x13, + /** + * Replaces a byte received with a low-level communication error (e.g., framing error) from the UART. + * + * When a Substitute Byte is processed, the data between the previous and the next Flag Bytes is ignored. + */ + SUBSTITUTE = 0x18, + /** + * Terminates a frame in progress. + * + * A Cancel Byte causes all data received since the previous Flag Byte to be ignored. + * Note that as a special case, RST and RSTACK frames are preceded by Cancel Bytes to ignore any link startup noise. + */ + CANCEL = 0x1A, +} + +/** + * The NCP enters the FAILED state if it detects one of the following errors: + * - An abnormal internal reset due to an error, failed assertion, or fault. + * - Exceeding the maximum number of consecutive acknowledgement timeouts. + * + * When the NCP enters the FAILED state, the NCP sends an ERROR frame containing a reset or error code + * and will reply to all subsequent frames received, except RST, with an ERROR frame. + * To reinitialize the ASH protocol, the Host must reset the NCP by either asserting the nRESET pin or sending the RST frame. + * + * The codes are returned by the NCP in the: + * - Reset Code byte of a RSTACK frame + * - Error Code byte of an ERROR frame. + * + * Silicon Labs wireless mesh chips can detect numerous reset fault causes beyond those in the table. + * When sent to the host, these new reset codes have 0x80 added to the value returned by their HAL’s reset code. + */ +export enum NcpFailedCode { + RESET_UNKNOWN_REASON = 0, + RESET_EXTERNAL = 1, + RESET_POWERON = 2, + RESET_WATCHDOG = 3, + RESET_ASSERT = 6, + RESET_BOOTLOADER = 9, + RESET_SOFTWARE = 11, + ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT = 0x51, + CHIP_SPECIFIC_ERROR_RESET_CODE = 0x80, +} diff --git a/src/adapter/ember/uart/parser.ts b/src/adapter/ember/uart/parser.ts new file mode 100644 index 0000000000..f470fae900 --- /dev/null +++ b/src/adapter/ember/uart/parser.ts @@ -0,0 +1,46 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import {Transform, TransformCallback, TransformOptions} from "stream"; +import {AshReservedByte} from "./enums"; + +const debug = Debug('zigbee-herdsman:adapter:ember:uart:ash:parser'); + +export class AshParser extends Transform { + private buffer: Buffer; + + public constructor(opts?: TransformOptions) { + super(opts); + + this.buffer = Buffer.alloc(0); + } + + _transform(chunk: Buffer, encoding: BufferEncoding, cb: TransformCallback): void { + let data = Buffer.concat([this.buffer, chunk]); + let position: number; + + while ((position = data.indexOf(AshReservedByte.FLAG)) !== -1) { + // emit the frame via 'data' event + const frame = data.subarray(0, position + 1); + + setImmediate((): void => { + debug(`<<<< [FRAME raw=${frame.toString('hex')}]`); + this.push(frame); + }); + + // remove the frame from internal buffer (set below) + data = data.subarray(position + 1); + } + + this.buffer = data; + + cb(); + } + + _flush(cb: TransformCallback): void { + this.push(this.buffer); + + this.buffer = Buffer.alloc(0); + + cb(); + } +} diff --git a/src/adapter/ember/uart/queues.ts b/src/adapter/ember/uart/queues.ts new file mode 100644 index 0000000000..7acd382f0f --- /dev/null +++ b/src/adapter/ember/uart/queues.ts @@ -0,0 +1,243 @@ +/* istanbul ignore file */ +import {EZSP_MAX_FRAME_LENGTH} from "../ezsp/consts"; + + +/** + * Buffer to hold a DATA frame. + * Allocates `data` to `EZSP_MAX_FRAME_LENGTH` filled with zeroes. + */ +export class EzspBuffer { + /** uint8_t[EZSP_MAX_FRAME_LENGTH] */ + public data: Buffer; + public link: EzspBuffer; + /** uint8_t */ + public len: number; + + constructor() { + this.data = Buffer.alloc(EZSP_MAX_FRAME_LENGTH);// inits to all-zeroes + this.link = null; + this.len = 0; + } +} + +/** + * Simple queue (singly-linked list) + */ +export class EzspQueue { + public tail: EzspBuffer; + + constructor() { + this.tail = null; + } + + /** + * Get the number of buffers in the queue. + * @returns + */ + get length(): number { + let head: EzspBuffer = this.tail; + let count: number = 0; + + for (count; head != null; count++) { + head = head.link; + } + + return count; + } + + get empty(): boolean { + return (this.tail == null); + } + + /** + * Get a pointer to the buffer at the head of the queue. + * @returns + */ + get head(): EzspBuffer { + let head: EzspBuffer = this.tail; + + if (head == null) { + throw new Error(`Tried to get head from an empty queue.`); + } + + if (head != null) { + while (head.link != null) { + head = head.link; + } + + } + + return head; + } + + /** + * Get a pointer to the Nth entry in the queue (the tail corresponds to N = 1). + * + * @param n + * @returns + */ + public getNthEntry(n: number): EzspBuffer { + if (n === 0) { + throw new Error(`Asked for 0th element in queue.`); + } + + let buf: EzspBuffer = this.tail; + + while (--n) { + if (buf == null) { + throw new Error(`Less than N entries in queue.`); + } + + buf = buf.link; + } + + return buf; + } + + /** + * Get a pointer to the entry before the specified entry (closer to the tail). + * If the buffer specified is null, the head entry is returned. + * If the buffer specified is the tail, null is returned. + * @param entry The buffer to look before. + * @returns + */ + public getPrecedingEntry(entry: EzspBuffer): EzspBuffer { + let buf: EzspBuffer = this.tail; + + if (buf === entry) { + return null; + } else { + do { + if (buf.link === entry) { + return buf; + } + + buf = buf.link; + } while (buf != null); + + throw new Error(`Buffer not in queue.`); + } + } + + /** + * Add a buffer to the tail of the queue. + * @param buf + */ + public addTail(buf: EzspBuffer): void { + if (buf) { + buf.link = this.tail; + this.tail = buf; + } else { + throw new Error(`Called addTail with null buffer`); + } + } + + /** + * Remove the buffer at the head of the queue. + * @returns The removed buffer. + */ + public removeHead(): EzspBuffer { + let head: EzspBuffer = this.tail; + + if (head == null) { + throw new Error(`Tried to remove head from an empty queue.`); + } + + if (head.link == null) { + this.tail = null; + } else { + let prev: EzspBuffer; + + do { + prev = head; + head = head.link; + } while (head.link != null); + + prev.link = null; + } + + return head; + } + + /** + * Remove the specified entry from the queue. + * @param entry + * @returns A pointer to the preceding entry (if any). + */ + public removeEntry(entry: EzspBuffer): EzspBuffer { + const buf: EzspBuffer = this.getPrecedingEntry(entry); + + if (buf != null) { + buf.link = entry.link; + } else { + this.tail = entry.link; + } + + return buf; + } +} + +/** + * Simple free list (singly-linked list) + */ +export class EzspFreeList { + public link: EzspBuffer; + + constructor() { + this.link = null; + } + + /** + * Get the number of buffers in the free list. + * @returns + */ + get length(): number { + let next: EzspBuffer = this.link; + let count: number = 0; + + for (count; next != null; count++) { + next = next.link; + } + + return count; + } + + /** + * Add a buffer to the free list. + * @param buf + */ + public freeBuffer(buf: EzspBuffer): void { + if (buf) { + buf.link = this.link; + this.link = buf; + } else { + throw new Error(`Called freeBuffer with null buffer`); + } + } + + /** + * Get a buffer from the free list. + * @returns + */ + public allocBuffer(): EzspBuffer { + const buf: EzspBuffer = this.link; + + if (buf != null) { + this.link = buf.link; + buf.len = 0; + + if (buf.data.length !== EZSP_MAX_FRAME_LENGTH) { + // should never happen if buffers are handled properly, but just in case, re-alloc to max length + buf.data = Buffer.alloc(EZSP_MAX_FRAME_LENGTH); + + const e = new Error(); + console.assert(false, `Pre-allocated buffer had improper size and had to be re-allocated. ${e.stack}`); + } else { + // (void) memset(buffer->data, 0, EZSP_MAX_FRAME_LENGTH); + buf.data.fill(0); + } + } + + return buf; + } +} diff --git a/src/adapter/ember/uart/writer.ts b/src/adapter/ember/uart/writer.ts new file mode 100644 index 0000000000..c577f94127 --- /dev/null +++ b/src/adapter/ember/uart/writer.ts @@ -0,0 +1,52 @@ +/* istanbul ignore file */ +import Debug from "debug"; +import {Readable, ReadableOptions} from "stream"; + +const debug = Debug('zigbee-herdsman:adapter:ember:uart:ash:writer'); + + +export class AshWriter extends Readable { + private bytesToWrite: number[]; + + constructor(opts?: ReadableOptions) { + super(opts); + + this.bytesToWrite = []; + } + + private writeBytes(): void { + const buffer = Buffer.from(this.bytesToWrite); + this.bytesToWrite = []; + + debug(`>>>> [FRAME raw=${buffer.toString('hex')}]`); + + // this.push(buffer); + this.emit('data', buffer); + } + + public writeByte(byte: number): void { + this.bytesToWrite.push(byte); + } + + public writeAvailable(): boolean { + if (this.readableLength < this.readableHighWaterMark) { + return true; + } else { + this.writeFlush(); + + return false; + } + } + + /** + * If there is anything to send, send to the port. + */ + public writeFlush(): void { + if (this.bytesToWrite.length) { + this.writeBytes(); + } + } + + public _read(): void { + } +} diff --git a/src/adapter/ember/utils/initters.ts b/src/adapter/ember/utils/initters.ts new file mode 100644 index 0000000000..2291c0361e --- /dev/null +++ b/src/adapter/ember/utils/initters.ts @@ -0,0 +1,73 @@ +/* istanbul ignore file */ +import {NetworkCache} from "../adapter/emberAdapter"; +import { + BLANK_EUI64, + UNKNOWN_NETWORK_STATE, + ZB_PSA_ALG, + INVALID_PAN_ID, + INVALID_RADIO_CHANNEL, + NULL_NODE_ID, + EMBER_ALL_802_15_4_CHANNELS_MASK, + BLANK_EXTENDED_PAN_ID +} from "../consts"; +import { + EmberJoinMethod, + EmberNetworkStatus, + SecManDerivedKeyType, + SecManFlag, + SecManKeyType +} from "../enums"; +import {EMBER_AES_HASH_BLOCK_SIZE} from "../ezsp/consts"; +import {EmberAesMmoHashContext, SecManContext} from "../types"; + + +/** + * Initialize a network cache index with proper "invalid" values. + * @returns + */ +export const initNetworkCache = (): NetworkCache => { + return { + eui64: BLANK_EUI64, + parameters: { + extendedPanId: BLANK_EXTENDED_PAN_ID.slice(),// copy + panId: INVALID_PAN_ID, + radioTxPower: 0, + radioChannel: INVALID_RADIO_CHANNEL, + joinMethod: EmberJoinMethod.MAC_ASSOCIATION, + nwkManagerId: NULL_NODE_ID, + nwkUpdateId: 0, + channels: EMBER_ALL_802_15_4_CHANNELS_MASK, + }, + status: UNKNOWN_NETWORK_STATE as EmberNetworkStatus, + }; +}; + +/** + * This routine will initialize a Security Manager context correctly for use in subsequent function calls. + * @returns + */ +export const initSecurityManagerContext = (): SecManContext => { + return { + coreKeyType: SecManKeyType.NONE, + keyIndex: 0, + derivedType: SecManDerivedKeyType.NONE, + eui64: `0x0000000000000000`, + multiNetworkIndex: 0, + flags: SecManFlag.NONE, + psaKeyAlgPermission: ZB_PSA_ALG,// unused for classic key storage + }; +}; + +/** + * This routine clears the passed context so that a new hash calculation + * can be performed. + * + * @returns context A pointer to the location of hash context to clear. + */ +export const aesMmoHashInit = (): EmberAesMmoHashContext => { + // MEMSET(context, 0, sizeof(EmberAesMmoHashContext)); + return { + result: Buffer.alloc(EMBER_AES_HASH_BLOCK_SIZE),// uint8_t[EMBER_AES_HASH_BLOCK_SIZE] + length: 0x00000000,// uint32_t + }; +}; diff --git a/src/adapter/ember/utils/math.ts b/src/adapter/ember/utils/math.ts new file mode 100644 index 0000000000..98c3e6f9b7 --- /dev/null +++ b/src/adapter/ember/utils/math.ts @@ -0,0 +1,96 @@ +/* istanbul ignore file */ +//-------------------------------------------------------------- +// Define macros for handling 3-bit frame numbers modulo 8 + +import {MACCapabilityFlags} from "../zdo"; + +/** mask to frame number modulus */ +export const mod8 = (n: number): number => n & 7; +/** increment in frame number modulus */ +export const inc8 = (n: number): number => mod8(n + 1); +/** Return true if n is within the range lo through hi, computed (mod 8) */ +export const withinRange = (lo: number, n: number, hi: number): boolean => mod8(n - lo) <= mod8(hi - lo); + + +//-------------------------------------------------------------- +// CRC + +/** + * Calculates 16-bit cyclic redundancy code (CITT CRC 16). + * + * Applies the standard CITT CRC 16 polynomial to a + * single byte. It should support being called first with an initial + * value, then repeatedly until all data is processed. + * + * @param newByte The new byte to be run through CRC. + * @param prevResult The previous CRC result. + * @returns The new CRC result. + */ +export const halCommonCrc16 = (newByte: number, prevResult: number): number => { + /* + * 16bit CRC notes: + * "CRC-CCITT" + * poly is g(X) = X^16 + X^12 + X^5 + 1 (0x1021) + * used in the FPGA (green boards and 15.4) + * initial remainder should be 0xFFFF + */ + prevResult = ((prevResult >> 8) & 0xFFFF) | ((prevResult << 8) & 0xFFFF); + prevResult ^= newByte; + prevResult ^= (prevResult & 0xFF) >> 4; + prevResult ^= (((prevResult << 8) & 0xFFFF) << 4) & 0xFFFF; + + prevResult ^= (((prevResult & 0xFF) << 5) & 0xFF) | (((((prevResult & 0xFF) >> 3) & 0xFFFF) << 8) & 0xFFFF); + + return prevResult; +}; + + +//-------------------------------------------------------------- +// Byte manipulation + +/** Returns the low bits of the 8-bit value 'n' as uint8_t. */ +export const lowBits = (n: number): number => (n & 0xF); +/** Returns the high bits of the 8-bit value 'n' as uint8_t. */ +export const highBits = (n: number): number => (lowBits(n >> 4) & 0xF); +/** Returns the low byte of the 16-bit value 'n' as uint8_t. */ +export const lowByte = (n: number): number => (n & 0xFF); +/** Returns the high byte of the 16-bit value 'n' as uint8_t. */ +export const highByte = (n: number): number => (lowByte(n >> 8) & 0xFF); +/** Returns the value built from the two uint8_t values high and low. */ +export const highLowToInt = (high: number, low: number): number => (((high & 0xFFFF) << 8) + ((low & 0xFFFF) & 0xFF)); +/** Useful to reference a single bit of a byte. */ +export const bit = (x: number): number => (1 << x); +/** Useful to reference a single bit of an uint32_t type. */ +export const bit32 = (x: number): number => (1 << x); +/** Returns both the low and high bytes (in that order) of the same 16-bit value 'n' as uint8_t. */ +export const lowHighBytes = (n: number): [number, highByte: number] => [lowByte(n), highByte(n)]; +/** Returns both the low and high bits (in that order) of the same 8-bit value 'n' as uint8_t. */ +export const lowHighBits = (n: number): [number, highBits: number] => [lowBits(n), highBits(n)]; + +/** + * Get byte as an 8-bit string (`n` assumed of proper range). + * @param n + * @returns + */ +export const byteToBits = (n: number): string => { + return (n >>> 0).toString(2).padStart(8, '0'); +}; + +/** + * Get the values for the bitmap `Mac Capability Flags Field` as per spec. + * Given value is assumed to be a proper byte. + * @param capabilities + * @returns + */ +export const getMacCapFlags = (capabilities: number): MACCapabilityFlags => { + return { + alternatePANCoordinator: (capabilities & 0x01), + deviceType: (capabilities & 0x02) >> 1, + powerSource: (capabilities & 0x04) >> 2, + rxOnWhenIdle: (capabilities & 0x08) >> 3, + reserved1: (capabilities & 0x10) >> 4, + reserved2: (capabilities & 0x20) >> 5, + securityCapability: (capabilities & 0x40) >> 6, + allocateAddress: (capabilities & 0x80) >> 7, + }; +}; diff --git a/src/adapter/ember/zdo.ts b/src/adapter/ember/zdo.ts new file mode 100644 index 0000000000..405d164b8b --- /dev/null +++ b/src/adapter/ember/zdo.ts @@ -0,0 +1,1034 @@ +//------------------------------------------------------------------------------ +// ZigBee Device Object (ZDO) + +import {EmberEUI64, EmberExtendedPanId, EmberNodeId} from "./types"; + +/** The endpoint where the ZigBee Device Object (ZDO) resides. */ +export const ZDO_ENDPOINT = 0; + +/** The profile ID used by the ZigBee Device Object (ZDO). */ +export const ZDO_PROFILE_ID = 0x0000; + +/** ZDO messages start with a sequence number. */ +export const ZDO_MESSAGE_OVERHEAD = 1; + +/** + * ZDO response status. + * + * Most responses to ZDO commands contain a status byte. + * The meaning of this byte is defined by the ZigBee Device Profile. + * uint8_t + */ +export enum EmberZdoStatus { + // These values are taken from Table 48 of ZDP Errata 043238r003 and Table 2 + // of NWK 02130r10. + ZDP_SUCCESS = 0x00, + // 0x01 to 0x7F are reserved + ZDP_INVALID_REQUEST_TYPE = 0x80, + ZDP_DEVICE_NOT_FOUND = 0x81, + ZDP_INVALID_ENDPOINT = 0x82, + ZDP_NOT_ACTIVE = 0x83, + ZDP_NOT_SUPPORTED = 0x84, + ZDP_TIMEOUT = 0x85, + ZDP_NO_MATCH = 0x86, + // 0x87 is reserved = 0x87, + ZDP_NO_ENTRY = 0x88, + ZDP_NO_DESCRIPTOR = 0x89, + ZDP_INSUFFICIENT_SPACE = 0x8a, + ZDP_NOT_PERMITTED = 0x8b, + ZDP_TABLE_FULL = 0x8c, + ZDP_NOT_AUTHORIZED = 0x8d, + ZDP_DEVICE_BINDING_TABLE_FULL = 0x8e, + ZDP_INVALID_INDEX = 0x8f, + ZDP_FRAME_TOO_LARGE = 0x90, + ZDP_BAD_KEY_NEGOTIATION_METHOD = 0x91, + ZDP_TEMPORARY_FAILURE = 0x92, + + APS_SECURITY_FAIL = 0xad, + + NWK_ALREADY_PRESENT = 0xc5, + NWK_TABLE_FULL = 0xc7, + NWK_UNKNOWN_DEVICE = 0xc8, + + NWK_MISSING_TLV = 0xd6, + NWK_INVALID_TLV = 0xd7, +}; + +export type MACCapabilityFlags = { + /** + * The alternate PAN coordinator sub-field is one bit in length and shall be set to 1 if this node is capable of becoming a PAN coordinator. + * Otherwise, the alternative PAN coordinator sub-field shall be set to 0. + */ + alternatePANCoordinator: number, + /** + * The device type sub-field is one bit in length and shall be set to 1 if this node is a full function device (FFD). + * Otherwise, the device type sub-field shall be set to 0, indicating a reduced function device (RFD). + */ + deviceType: number, + /** + * The power source sub-field is one bit in length and shall be set to 1 if the current power source is mains power. + * Otherwise, the power source sub-field shall be set to 0. + * This information is derived from the node current power source field of the node power descriptor. + */ + powerSource: number, + /** + * The receiver on when idle sub-field is one bit in length and shall be set to 1 if the device does not disable its receiver to + * conserve power during idle periods. + * Otherwise, the receiver on when idle sub-field shall be set to 0 (see also section 2.3.2.4.) + */ + rxOnWhenIdle: number, + /** reserved */ + reserved1: number, + /** reserved */ + reserved2: number, + /** + * The security capability sub-field is one bit in length and shall be set to 1 if the device is capable of sending and receiving + * frames secured using the security suite specified in [B1]. + * Otherwise, the security capability sub-field shall be set to 0. + */ + securityCapability: number, + /** The allocate address sub-field is one bit in length and shall be set to 0 or 1. */ + allocateAddress: number, +}; + +export type ZDOLQITableEntry = { + /** + * The 64-bit extended PAN identifier of the neighboring device. + * + * 64-bit + */ + extendedPanId: EmberExtendedPanId, + /** + * 64-bit IEEE address that is unique to every device. + * If this value is unknown at the time of the request, this field shall be set to 0xffffffffffffffff. + * + * 64-bit + */ + eui64: EmberEUI64, + /** The 16-bit network address of the neighboring device. 16-bit */ + nodeId: EmberNodeId, + /** + * The type of the neighbor device: + * 0x00 = ZigBee coordinator + * 0x01 = ZigBee router + * 0x02 = ZigBee end device + * 0x03 = Unknown + * + * 2-bit + */ + deviceType: number, + /** + * Indicates if neighbor's receiver is enabled during idle portions of the CAP: + * 0x00 = Receiver is off + * 0x01 = Receiver is on + * 0x02 = unknown + * + * 2-bit + */ + rxOnWhenIdle: number, + /** + * The relationship between the neighbor and the current device: + * 0x00 = neighbor is the parent + * 0x01 = neighbor is a child + * 0x02 = neighbor is a sibling + * 0x03 = None of the above + * 0x04 = previous child + * + * 3-bit + */ + relationship: number, + /** This reserved bit shall be set to 0. 1-bit */ + reserved1: number, + /** + * An indication of whether the neighbor device is accepting join requests: + * 0x00 = neighbor is not accepting join requests + * 0x01 = neighbor is accepting join requests + * 0x02 = unknown + * + * 2-bit + */ + permitJoining: number, + /** Each of these reserved bits shall be set to 0. 6-bit */ + reserved2: number, + /** + * The tree depth of the neighbor device. + * A value of 0x00 indicates that the device is the ZigBee coordinator for the network + * + * 8-bit + */ + depth: number, + /** + * The estimated link quality for RF transmissions from this device. + * See [B1] for discussion of how this is calculated. + * + * 8-bit + */ + lqi: number, +}; + +export type ZDORoutingTableEntry = { + /** 16-bit network address of this route */ + destinationAddress: EmberNodeId, + /** + * Status of the route + * 0x0=ACTIVE. + * 0x1=DISCOVERY_UNDERWAY. + * 0x2=DISCOVERY_FAILED. + * 0x3=INACTIVE. + * 0x4=VALIDATION_UNDERWAY + * 0x5-0x7=RESERVED + * + * 3-bit + */ + status: number, + /** + * A flag indicating whether the device is a memory constrained concentrator + * + * 1-bit + */ + memoryConstrained: number, + /** + * A flag indicating that the destination is a concentrator that issued a many-to-one request + * + * 1-bit + */ + manyToOne: number, + /** + * A flag indicating that a route record command frame should be sent to the destination prior to the next data packet. + * + * 1-bit + */ + routeRecordRequired: number, + /** 2-bit */ + reserved: number, + /** 16-bit network address of the next hop on the way to the destination. */ + nextHopAddress: EmberNodeId, +}; + +export type ZDOBindingTableEntry = { + /** The source IEEE address for the binding entry. */ + sourceEui64: EmberEUI64, + /** The source endpoint for the binding entry. uint8_t */ + sourceEndpoint: number, + /** The identifier of the cluster on the source device that is bound to the destination device. uint16_t */ + clusterId: number, + /** + * The addressing mode for the destination address. This field can take one of the non-reserved values from the following list: + * - 0x00 = reserved + * - 0x01 = 16-bit group address for DstAddr and DstEndpoint not present + * - 0x02 = reserved + * - 0x03 = 64-bit extended address for DstAddr and DstEndp present + * - 0x04 – 0xff = reserved + * + * uint8_t + */ + destAddrMode: number, + /** The destination address for the binding entry. uint16_t or uint8_t[EUI64_SIZE] */ + dest: EmberNodeId | EmberEUI64, + /** + * This field shall be present only if the DstAddrMode field has a value of 0x03 and, if present, + * shall be the destination endpoint for the binding entry. + * uint8_t or not present + */ + destEndpoint?: number, +}; + +/** @see IEEE_ADDRESS_RESPONSE */ +export type IEEEAddressResponsePayload = { + eui64: EmberEUI64, + nodeId: EmberNodeId, + assocDevList: number[], +}; + +/** @see NETWORK_ADDRESS_RESPONSE */ +export type NetworkAddressResponsePayload = { + eui64: EmberEUI64, + nodeId: EmberNodeId, + assocDevList: number[], +}; + +/** @see MATCH_DESCRIPTORS_RESPONSE */ +export type MatchDescriptorsResponsePayload = { + nodeId: EmberNodeId, + endpointList: number[], +}; + +/** @see SIMPLE_DESCRIPTOR_RESPONSE */ +export type SimpleDescriptorResponsePayload = { + nodeId: EmberNodeId, + /** uint8_t */ + // inClusterCount: number, + /** const uint16_t* */ + inClusterList: number[], + /** uint8_t */ + // outClusterCount: number, + /** const uint16_t* */ + outClusterList: number[], + /** uint18_t */ + profileId: number, + /** uint16_t */ + deviceId: number, + /** uint8_t */ + endpoint: number, +}; + +/** @see NODE_DESCRIPTOR_RESPONSE */ +export type NodeDescriptorResponsePayload = { + nodeId: EmberNodeId, + logicalType: number, + macCapFlags: MACCapabilityFlags, + manufacturerCode: number, + stackRevision: number, +}; + +/** @see POWER_DESCRIPTOR_RESPONSE */ +export type PowerDescriptorResponsePayload = { + nodeId: EmberNodeId, + currentPowerMode: number, + availPowerSources: number, + currentPowerSource: number, + currentPowerSourceLevel: number, +}; + +/** @see ACTIVE_ENDPOINTS_RESPONSE */ +export type ActiveEndpointsResponsePayload = { + nodeId: EmberNodeId, + endpointList: number[], +}; + +/** @see LQI_TABLE_RESPONSE */ +export type LQITableResponsePayload = { + neighborTableEntries: number, + entryList: ZDOLQITableEntry[], +}; + +/** @see ROUTING_TABLE_RESPONSE */ +export type RoutingTableResponsePayload = { + routingTableEntries: number, + entryList: ZDORoutingTableEntry[], +}; + +/** @see BINDING_TABLE_RESPONSE */ +export type BindingTableResponsePayload = { + bindingTableEntries: number, + entryList: ZDOBindingTableEntry[], +}; + +/** @see END_DEVICE_ANNOUNCE */ +export type EndDeviceAnnouncePayload = { + nodeId: EmberNodeId, + eui64: EmberEUI64, + capabilities: MACCapabilityFlags, +}; + +/** + * Defines for ZigBee device profile cluster IDs follow. These + * include descriptions of the formats of the messages. + * + * Note that each message starts with a 1-byte transaction sequence + * number. This sequence number is used to match a response command frame + * to the request frame that it is replying to. The application shall + * maintain a 1-byte counter that is copied into this field and incremented + * by one for each command sent. When a value of 0xff is reached, the next + * command shall re-start the counter with a value of 0x00. + */ + +// Network and IEEE Address Request/Response +/** + * Network request: [transaction sequence number: 1] + * [EUI64:8] [type:1] [start index:1] + */ +export const NETWORK_ADDRESS_REQUEST = 0x0000; +/** + * Response: [transaction sequence number: 1] + * [status:1] [EUI64:8] [node ID:2] + * [ID count:1] [start index:1] [child ID:2]* + */ +export const NETWORK_ADDRESS_RESPONSE = 0x8000; +/** + * IEEE request: [transaction sequence number: 1] + * [node ID:2] [type:1] [start index:1] + * [type] = 0x00 single address response, ignore the start index + * = 0x01 extended response -] sends kid's IDs as well + */ +export const IEEE_ADDRESS_REQUEST = 0x0001; +/** + * Response: [transaction sequence number: 1] + * [status:1] [EUI64:8] [node ID:2] + * [ID count:1] [start index:1] [child ID:2]* + */ +export const IEEE_ADDRESS_RESPONSE = 0x8001; + +// Node Descriptor Request/Response +/** + * Request: [transaction sequence number: 1] [node ID:2] [tlvs: varies] + */ +export const NODE_DESCRIPTOR_REQUEST = 0x0002; +/** + * Response: [transaction sequence number: 1] [status:1] [node ID:2] + * [node descriptor: 13] [tlvs: varies] + * + * Node Descriptor field is divided into subfields of bitmasks as follows: + * (Note: All lengths below are given in bits rather than bytes.) + * Logical Type: 3 + * Complex Descriptor Available: 1 + * User Descriptor Available: 1 + * (reserved/unused): 3 + * APS Flags: 3 + * Frequency Band: 5 + * MAC capability flags: 8 + * Manufacturer Code: 16 + * Maximum buffer size: 8 + * Maximum incoming transfer size: 16 + * Server mask: 16 + * Maximum outgoing transfer size: 16 + * Descriptor Capability Flags: 8 + * See ZigBee document 053474, Section 2.3.2.3 for more details. + */ +export const NODE_DESCRIPTOR_RESPONSE = 0x8002; + +// Power Descriptor Request / Response +/** + * + * Request: [transaction sequence number: 1] [node ID:2] + */ +export const POWER_DESCRIPTOR_REQUEST = 0x0003; +/** + * Response: [transaction sequence number: 1] [status:1] [node ID:2] + * [current power mode, available power sources:1] + * [current power source, current power source level:1] + * See ZigBee document 053474, Section 2.3.2.4 for more details. + */ +export const POWER_DESCRIPTOR_RESPONSE = 0x8003; + +// Simple Descriptor Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [node ID:2] [endpoint:1] + */ +export const SIMPLE_DESCRIPTOR_REQUEST = 0x0004; +/** + * Response: [transaction sequence number: 1] + * [status:1] [node ID:2] [length:1] [endpoint:1] + * [app profile ID:2] [app device ID:2] + * [app device version, app flags:1] + * [input cluster count:1] [input cluster:2]* + * [output cluster count:1] [output cluster:2]* + */ +export const SIMPLE_DESCRIPTOR_RESPONSE = 0x8004; + +// Active Endpoints Request / Response +/** + * + * Request: [transaction sequence number: 1] [node ID:2] + */ +export const ACTIVE_ENDPOINTS_REQUEST = 0x0005; +/** + * Response: [transaction sequence number: 1] + * [status:1] [node ID:2] [endpoint count:1] [endpoint:1]* + */ +export const ACTIVE_ENDPOINTS_RESPONSE = 0x8005; + +// Match Descriptors Request / Response +/** + * Request: [transaction sequence number: 1] + * [node ID:2] [app profile ID:2] + * [input cluster count:1] [input cluster:2]* + * [output cluster count:1] [output cluster:2]* + */ +export const MATCH_DESCRIPTORS_REQUEST = 0x0006; +/** + * Response: [transaction sequence number: 1] + * [status:1] [node ID:2] [endpoint count:1] [endpoint:1]* + */ +export const MATCH_DESCRIPTORS_RESPONSE = 0x8006; + +// End Device Announce and End Device Announce Response +/** + * Request: [transaction sequence number: 1] + * [node ID:2] [EUI64:8] [capabilities:1] + */ +export const END_DEVICE_ANNOUNCE = 0x0013; +/** + * No response is sent. + */ +export const END_DEVICE_ANNOUNCE_RESPONSE = 0x8013; + +// System Server Discovery Request / Response +// This is broadcast and only servers which have matching services respond. +// The response contains the request services that the recipient provides. +/** + * Request: [transaction sequence number: 1] [server mask:2] + */ +export const SYSTEM_SERVER_DISCOVERY_REQUEST = 0x0015; +/** + * Response: [transaction sequence number: 1] + * [status (== EMBER_ZDP_SUCCESS):1] [server mask:2] + */ +export const SYSTEM_SERVER_DISCOVERY_RESPONSE = 0x8015; + +// Parent Announce and Parent Announce Response +// This is broadcast and only servers which have matching children respond. +// The response contains the list of children that the recipient now holds. +/** + * Request: [transaction sequence number: 1] + * [number of children:1] [child EUI64:8] [child Age:4]* + */ +export const PARENT_ANNOUNCE = 0x001F; +/** + * Response: [transaction sequence number: 1] + * [number of children:1] [child EUI64:8] [child Age:4]* + */ +export const PARENT_ANNOUNCE_RESPONSE = 0x801F; + +// Find Node Cache Request / Response +// This is broadcast and only discovery servers which have the information for the device of interest, or the device of interest itself, respond. +// The requesting device can then direct any service discovery requests to the responder. +/** + * Request: [transaction sequence number: 1] + * [device of interest ID:2] [d-of-i EUI64:8] + */ +export const FIND_NODE_CACHE_REQUEST = 0x001C; +/** + * Response: [transaction sequence number: 1] + * [responder ID:2] [device of interest ID:2] [d-of-i EUI64:8] + */ +export const FIND_NODE_CACHE_RESPONSE = 0x801C; + +// End Device Bind Request / Response +/** + * Request: [transaction sequence number: 1] + * [node ID:2] [EUI64:8] [endpoint:1] [app profile ID:2] + * [input cluster count:1] [input cluster:2]* + * [output cluster count:1] [output cluster:2]* + */ +export const END_DEVICE_BIND_REQUEST = 0x0020; +/** + * Response: [transaction sequence number: 1] [status:1] + */ +export const END_DEVICE_BIND_RESPONSE = 0x8020; + +// Clear All Bindings Request / Response +/** + * Request: [transaction sequence number: 1] + * [clear all bindings request EUI64 TLV:Variable] + * Clear all bindings request EUI64 TLV: + * [Count N:1][EUI64 1:8]...[EUI64 N:8] + */ +export const CLEAR_ALL_BINDINGS_REQUEST = 0x002B; +/** + * Response: [transaction sequence number: 1] [status:1] + */ +export const CLEAR_ALL_BINDINGS_RESPONSE = 0x802B; + + +// Binding types and Request / Response +// Bind and unbind have the same formats. +// There are two possible formats, depending on whether the destination is a group address or a device address. +// Device addresses include an endpoint, groups don't. +/** + * + */ +export const UNICAST_BINDING = 0x03; +/** + * + */ +export const UNICAST_MANY_TO_ONE_BINDING = 0x83; +/** + * + */ +export const MULTICAST_BINDING = 0x01; + +/** + * Request: [transaction sequence number: 1] + * [source EUI64:8] [source endpoint:1] + * [cluster ID:2] [destination address:3 or 10] + * Destination address: + * [0x01:1] [destination group:2] + * Or: + * [0x03:1] [destination EUI64:8] [destination endpoint:1] + * + */ +export const BIND_REQUEST = 0x0021; +/** + * Response: [transaction sequence number: 1] [status:1] + */ +export const BIND_RESPONSE = 0x8021; +/** + * Request: [transaction sequence number: 1] + * [source EUI64:8] [source endpoint:1] + * [cluster ID:2] [destination address:3 or 10] + * Destination address: + * [0x01:1] [destination group:2] + * Or: + * [0x03:1] [destination EUI64:8] [destination endpoint:1] + * + */ +export const UNBIND_REQUEST = 0x0022; +/** + * Response: [transaction sequence number: 1] [status:1] + */ +export const UNBIND_RESPONSE = 0x8022; + + +// LQI Table Request / Response +/** + * Request: [transaction sequence number: 1] [start index:1] + */ +export const LQI_TABLE_REQUEST = 0x0031; +/** + * Response: [transaction sequence number: 1] [status:1] + * [neighbor table entries:1] [start index:1] + * [entry count:1] [entry:22]* + * [entry] = [extended PAN ID:8] [EUI64:8] [node ID:2] + * [device type, RX on when idle, relationship:1] + * [permit joining:1] [depth:1] [LQI:1] + * + * The device-type byte has the following fields: + * + * Name Mask Values + * + * device type 0x03 0x00 coordinator + * 0x01 router + * 0x02 end device + * 0x03 unknown + * + * rx mode 0x0C 0x00 off when idle + * 0x04 on when idle + * 0x08 unknown + * + * relationship 0x70 0x00 parent + * 0x10 child + * 0x20 sibling + * 0x30 other + * 0x40 previous child + * reserved 0x10 + * + * The permit-joining byte has the following fields + * + * Name Mask Values + * + * permit joining 0x03 0x00 not accepting join requests + * 0x01 accepting join requests + * 0x02 unknown + * reserved 0xFC + * + */ +export const LQI_TABLE_RESPONSE = 0x8031; + +// Routing Table Request / Response +/** + * Request: [transaction sequence number: 1] [start index:1] + */ +export const ROUTING_TABLE_REQUEST = 0x0032; +/** + * Response: [transaction sequence number: 1] [status:1] + * [routing table entries:1] [start index:1] + * [entry count:1] [entry:5]* + * [entry] = [destination address:2] + * [status:1] + * [next hop:2] + * + * + * The status byte has the following fields: + * Name Mask Values + * + * status 0x07 0x00 active + * 0x01 discovery underway + * 0x02 discovery failed + * 0x03 inactive + * 0x04 validation underway + * + * flags 0x38 + * 0x08 memory constrained + * 0x10 many-to-one + * 0x20 route record required + * + * reserved 0xC0 + */ +export const ROUTING_TABLE_RESPONSE = 0x8032; + +// Binding Table Request / Response +/** + * Request: [transaction sequence number: 1] [start index:1] + */ +export const BINDING_TABLE_REQUEST = 0x0033; +/** + * Response: [transaction sequence number: 1] + * [status:1] [binding table entries:1] [start index:1] + * [entry count:1] [entry:14/21]* + * [entry] = [source EUI64:8] [source endpoint:1] [cluster ID:2] + * [dest addr mode:1] [dest:2/8] [dest endpoint:0/1] + * [br] + * @note If Dest. Address Mode = 0x03, then the Long Dest. Address will be + * used and Dest. endpoint will be included. If Dest. Address Mode = 0x01, + * then the Short Dest. Address will be used and there will be no Dest. + * endpoint. + */ +export const BINDING_TABLE_RESPONSE = 0x8033; + +// Leave Request / Response +/** + * Request: [transaction sequence number: 1] [EUI64:8] [flags:1] + * The flag bits are: + * 0x40 remove children + * 0x80 rejoin + */ +export const LEAVE_REQUEST = 0x0034; +/** + * Response: [transaction sequence number: 1] [status:1] + */ +export const LEAVE_RESPONSE = 0x8034; + +// Permit Joining Request / Response +/** + * Request: [transaction sequence number: 1] + * [duration:1] [permit authentication:1] + */ +export const PERMIT_JOINING_REQUEST = 0x0036; +/** + * Response: [transaction sequence number: 1] [status:1] + */ +export const PERMIT_JOINING_RESPONSE = 0x8036; + +// Network Update Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [scan channels:4] [duration:1] [count:0/1] [manager:0/2] + * + * If the duration is in 0x00 ... 0x05, 'count' is present but + * not 'manager'. Perform 'count' scans of the given duration on the + * given channels. + * + * If duration is 0xFE, 'channels' should have a single channel + * and 'count' and 'manager' are not present. Switch to the indicated + * channel. + * + * If duration is 0xFF, 'count' is not present. Set the active + * channels and the network manager ID to the values given. + * + * Unicast requests always get a response, which is INVALID_REQUEST if the + * duration is not a legal value. + */ +export const NWK_UPDATE_REQUEST = 0x0038; +/** + * + * Response: [transaction sequence number: 1] [status:1] + * [scanned channels:4] [transmissions:2] [failures:2] + * [energy count:1] [energy:1]* + */ +export const NWK_UPDATE_RESPONSE = 0x8038; + +/** + * + */ +export const NWK_UPDATE_ENHANCED_REQUEST = 0x0039; +/** + * + */ +export const NWK_UPDATE_ENHANCED_RESPONSE = 0x8039; + +/** + * + */ +export const NWK_UPDATE_IEEE_JOINING_LIST_REQUEST = 0x003A; +/** + * + */ +export const NWK_UPDATE_IEEE_JOINING_LIST_REPONSE = 0x803A; + +/** + * + */ +export const NWK_UNSOLICITED_ENHANCED_UPDATE_NOTIFY = 0x803B; + + +// Beacon Survey Request / Response +// This command can be used by a remote device to survey the end devices to determine how many potential parents they have access to. +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one Beacon Survey Configuration TLV (variable octets), + * which contain the ScanChannelListStructure (variable length) + * and the ConfigurationBitmask (1 octet). This information provides + * the configuration for the end device's beacon survey. + * See R23 spec section 2.4.3.3.12 for the request and 3.2.2.2.1 + * for the ChannelListStructure. + */ +export const BEACON_SURVEY_REQUEST = 0x003C; +/** + * + * Response: [transaction sequence number: 1] + * [status: 1] + * [TLVs: varies] + * + * Contains one Beacon Survey Results TLV (4 octets), which contain + * the number of on-network, off-network, potential parent and total + * beacons recorded. If the device that received the request is not a + * router, a Potential Parent TLV (variable octects) will be found. This + * will contain information on the device's current parent, as well as + * any potential parents found via beacons (up to a maximum of 5). A + * Pan ID Conflict TLV can also found in the response. + * See R23 spec section 2.4.4.3.13 for the response. + */ +export const BEACON_SURVEY_RESPONSE = 0x803C; + +// Security Start Key Negotiation Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Curve25519 Public Point TLVs (40 octets), + * which contain an EUI64 and the 32-byte Curve public point. + * See R23 spec section 2.4.3.4.1 + * + * @note This command SHALL NOT be APS encrypted regardless of + * whether sent before or after the device joins the network. + * This command SHALL be network encrypted if the device has a + * network key, i.e. it has joined the network earlier and wants + * to negotiate or renegotiate a new link key; otherwise, if it + * is used prior to joining the network, it SHALL NOT be network + * encrypted. + */ +export const KEY_NEGOTIATION_REQUEST = 0x0040; +/** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more Curve25519 Public Point TLVs (40 octets), + * which contain an EUI64 and the 32-byte Curve public point, or + * Local TLVs. + * See R23 spec section 2.4.4.4.1 + * + * @note This command SHALL NOT be APS encrypted. When performing + * Key Negotiation with an unauthenticated neighbor that is not + * yet on the network, network layer encryption SHALL NOT be used + * on the message. If the message is being sent to unauthenticated + * device that is not on the network and is not a neighbor, it + * SHALL be relayed as described in section 4.6.3.7.7. Otherwise + * the message SHALL have network layer encryption. + */ +export const KEY_NEGOTIATION_RESPONSE = 0x8040; + +// Retrieve Authentication Token Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Authentication Token ID TLVs (1 octet), + * which contain the TLV Type Tag ID of the source of the + * authentication token. See R23 spec section 2.4.3.4.2 + */ +export const AUTHENTICATION_TOKEN_REQUEST = 0x0041; +/** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more 128-bit Symmetric Passphrase Global TLVs + * (16 octets), which contain the symmetric passphrase authentication + * token. See R23 spec section 2.4.4.4.2 + */ +export const AUTHENTICATION_TOKEN_RESPONSE = 0x8041; + +// Retrieve Authentication Level Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Target IEEE Address TLVs (8 octets), + * which contain the EUI64 of the device of interest. + * See R23 spec section 2.4.3.4.3 + */ +export const AUTHENTICATION_LEVEL_REQUEST = 0x0042; +/** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more Device Authentication Level TLVs + * (10 octets), which contain the EUI64 of the inquired device, + * along with the its initial join method and its active link + * key update method. + * See R23 spec section 2.4.4.4.3 + */ +export const AUTHENTICATION_LEVEL_RESPONSE = 0x8042; + +// Set Configuration Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Global TLVs (1 octet), + * which contain the TLV Type Tag ID, and their + * value. + */ +export const SET_CONFIGURATION_REQUEST = 0x0043; +/** + * + * Response: [transaction sequence number: 1] [status:1] + */ +export const SET_CONFIGURATION_RESPONSE = 0x8043; + +// Get Configuration Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more TLVs (1 octet), + * which the sender wants to get information + */ +export const GET_CONFIGURATION_REQUEST = 0x0044; +/** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more TLV tag Ids and their values + * in response to the request + */ +export const GET_CONFIGURATION_RESPONSE = 0x8044; + +// Security Start Key Update Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more TLVs. These TLVs can be Selected Key + * Negotiation Method TLVs (10 octets), Fragmentation Parameters + * Global TLVs (5 octets), or other TLVs. + * See R23 spec section 2.4.3.4.6 + * + * @note This SHALL NOT be APS encrypted or NWK encrypted if the + * link key update mechanism is done as part of the initial join + * and before the receiving device has been issued a network + * key. This SHALL be both APS encrypted and NWK encrypted if + * the link key update mechanism is performed to refresh the + * link key when the receiving device has the network key and + * has previously successfully joined the network. + */ +export const KEY_UPDATE_REQUEST = 0x0045; +/** + * + * Response: [transaction sequence number: 1] [status:1] + * + * See R23 spec section 2.4.4.4.6 + * + * @note This command SHALL be APS encrypted. + */ +export const KEY_UPDATE_RESPONSE = 0x8045; + +// Security Decommission Request / Response +/** + * + * Request: [transaction sequence number: 1] + * [security decommission request EUI64 TLV:Variable] + * Security Decommission request EUI64 TLV: + * [Count N:1][EUI64 1:8]...[EUI64 N:8] + */ +export const SECURITY_DECOMMISSION_REQUEST = 0x0046; +/** + * + * Response: [transaction sequence number: 1] [status:1] + */ +export const SECURITY_DECOMMISSION_RESPONSE = 0x8046; + +// Challenge for APS frame counter synchronization +/** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains at least the APS Frame Counter Challenge TLV, which holds the + * sender EUI and the 64 bit challenge value. + */ +export const SECURITY_CHALLENGE_REQUEST = 0x0047; +/** + * + * Response: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains at least the APS Frame Counter Response TLV, which holds the + * sender EUI, received challenge value, APS frame counter, challenge + * security frame counter, and 8-byte MIC. + */ +export const SECURITY_CHALLENGE_RESPONSE = 0x8047; + +// Unsupported Not mandatory and not supported. +/** + * + */ +export const COMPLEX_DESCRIPTOR_REQUEST = 0x0010; +/** + * + */ +export const COMPLEX_DESCRIPTOR_RESPONSE = 0x8010; +/** + * + */ +export const USER_DESCRIPTOR_REQUEST = 0x0011; +/** + * + */ +export const USER_DESCRIPTOR_RESPONSE = 0x8011; +/** + * + */ +export const DISCOVERY_REGISTER_REQUEST = 0x0012; +/** + * + */ +export const DISCOVERY_REGISTER_RESPONSE = 0x8012; +/** + * + */ +export const USER_DESCRIPTOR_SET = 0x0014; +/** + * + */ +export const USER_DESCRIPTOR_CONFIRM = 0x8014; +/** + * + */ +export const NETWORK_DISCOVERY_REQUEST = 0x0030; +/** + * + */ +export const NETWORK_DISCOVERY_RESPONSE = 0x8030; +/** + * + */ +export const DIRECT_JOIN_REQUEST = 0x0035; +/** + * + */ +export const DIRECT_JOIN_RESPONSE = 0x8035; + + +// Discovery Cache Request / Response +// DEPRECATED +/** + * Response: [transaction sequence number: 1] + * [status (== EMBER_ZDP_SUCCESS):1] + */ +export const DISCOVERY_CACHE_REQUEST = 0x0012; +/** + * Request: [transaction sequence number: 1] + * [source node ID:2] [source EUI64:8] + */ +export const DISCOVERY_CACHE_RESPONSE = 0x8012; + + +export const CLUSTER_ID_RESPONSE_MINIMUM = 0x8000; + diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index 42105a91e3..e66700b892 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -10,7 +10,7 @@ interface SerialPortOptions { baudRate?: number; rtscts?: boolean; path?: string; - adapter?: 'zstack' | 'deconz' | 'zigate' | 'ezsp' | 'auto'; + adapter?: 'zstack' | 'deconz' | 'zigate' | 'ezsp' | 'ember' | 'auto'; } interface AdapterOptions { diff --git a/src/utils/backup.ts b/src/utils/backup.ts index 34c259422c..973e02177b 100644 --- a/src/utils/backup.ts +++ b/src/utils/backup.ts @@ -111,6 +111,10 @@ export const fromUnifiedBackup = (backup: Models.UnifiedBackupStorage): Models.B znp: { version: backup.metadata.internal?.znpVersion || undefined, trustCenterLinkKeySeed: tclkSeedString ? Buffer.from(tclkSeedString, "hex") : undefined, + }, + ezsp: { + version: backup.metadata.internal?.ezspVersion || undefined, + hashed_tclk: backup.stack_specific?.ezsp?.hashed_tclk ? Buffer.from(backup.stack_specific.ezsp.hashed_tclk, "hex") : undefined, } }; }; diff --git a/test/adapter/ember/ash.test.ts b/test/adapter/ember/ash.test.ts new file mode 100644 index 0000000000..3f2f9380fb --- /dev/null +++ b/test/adapter/ember/ash.test.ts @@ -0,0 +1,347 @@ +import {OpenOptions} from '@serialport/stream' +import {MockBinding, MockBindingInterface} from '@serialport/binding-mock' +import { + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + EZSP_FRAME_CONTROL_COMMAND, + EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK, + EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET, + EZSP_FRAME_CONTROL_SLEEP_MODE_MASK, + EZSP_FRAME_ID_INDEX, + EZSP_MAX_FRAME_LENGTH, + EZSP_PARAMETERS_INDEX, EZSP_SEQUENCE_INDEX +} from "../../../src/adapter/ember/ezsp/consts"; +import {EzspStatus} from "../../../src/adapter/ember/enums"; +import {EzspBuffer} from "../../../src/adapter/ember/uart/queues"; +import {UartAsh} from "../../../src/adapter/ember/uart/ash"; +import {EZSP_HOST_RX_POOL_SIZE, TX_POOL_BUFFERS} from "../../../src/adapter/ember/uart/consts"; +import {RECD_RSTACK_BYTES, SEND_RST_BYTES, NCP_ACK_FIRST, adapterSONOFFDongleE} from "./consts"; +import {EzspBuffalo} from "../../../src/adapter/ember/ezsp/buffalo.ts"; +import {lowByte} from '../../../src/adapter/ember/utils/math'; +import {EzspFrameID} from '../../../src/adapter/ember/ezsp/enums.ts'; +import {Wait} from '../../../src/utils/'; + +// XXX: Below are copies from uart>ash.ts, should be kept in sync (avoids export) +/** max frames sent without being ACKed (1-7) */ +const CONFIG_TX_K = 3; +/** enables randomizing DATA frame payloads */ +const CONFIG_RANDOMIZE = true; +/** adaptive rec'd ACK timeout initial value */ +const CONFIG_ACK_TIME_INIT = 800; +/** " " " " " minimum value */ +const CONFIG_ACK_TIME_MIN = 400; +/** " " " " " maximum value */ +const CONFIG_ACK_TIME_MAX = 2400; +/** time allowed to receive RSTACK after ncp is reset */ +const CONFIG_TIME_RST = 2500; +/** time between checks for received RSTACK (CONNECTED status) */ +const CONFIG_TIME_RST_CHECK = 100; +/** if free buffers < limit, host receiver isn't ready, will hold off the ncp from sending normal priority frames */ +const CONFIG_NR_LOW_LIMIT = 8;// RX_FREE_LW +/** if free buffers > limit, host receiver is ready */ +const CONFIG_NR_HIGH_LIMIT = 12;// RX_FREE_HW +/** time until a set nFlag must be resent (max 2032) */ +const CONFIG_NR_TIME = 480; +/** Read/write max bytes count at stream level */ +const CONFIG_HIGHWATER_MARK = 256; + +const mockSerialPortCloseEvent = jest.fn(); +const mockSerialPortErrorEvent = jest.fn(); + +// todo doesnt reset if closing +// todo doesnt start if closing or connected +// todo doesnt close port if already closed on stop +// todo port error triggers stop +// todo emit `reset` only on port error +// todo emit `close` only when ASH layer stopped +// todo emit `frame` only on valid DATA frame + +const mocks = [mockSerialPortCloseEvent, mockSerialPortErrorEvent]; + +describe('Ember UART ASH Protocol', () => { + const openOpts: OpenOptions = {path: '/dev/ttyACM0', baudRate: 115200, binding: MockBinding}; + /** + * Mock binding provides: + * + * uartAsh.serialPort.port.recording => Buffer of all data written if record==true + * + * uartAsh.serialPort.port.lastWrite => Buffer of last write + */ + let uartAsh: UartAsh; + let buffalo: EzspBuffalo; + let frameSequence: number; + + beforeAll(async () => { + jest.useRealTimers();// messes with serialport promise handling otherwise? + }); + afterAll(async () => { + jest.useRealTimers(); + }); + beforeEach(() => { + for (const mock of mocks) { + mock.mockClear(); + } + + frameSequence = 0; + uartAsh = new UartAsh(openOpts); + buffalo = new EzspBuffalo(Buffer.alloc(EZSP_MAX_FRAME_LENGTH)); + MockBinding.createPort('/dev/ttyACM0', {/*echo: true,*/ record: true, /*readyData: emitRSTACK,*/ ...adapterSONOFFDongleE}); + + buffalo.setPosition(0); + }); + afterEach(async () => { + await uartAsh.stop(); + MockBinding.reset(); + }); + + it('Inits properly and allocates buffers as needed', () => { + expect(uartAsh.connected).toStrictEqual(false); + expect(uartAsh.txQueue).toBeDefined(); + expect(uartAsh.reTxQueue).toBeDefined(); + expect(uartAsh.txFree).toBeDefined(); + expect(uartAsh.rxQueue).toBeDefined(); + expect(uartAsh.rxFree).toBeDefined(); + + //@ts-expect-error private + uartAsh.initVariables(); + + expect(uartAsh.ncpSleepEnabled).toStrictEqual(false); + expect(uartAsh.ncpHasCallbacks).toStrictEqual(false); + expect(uartAsh.txQueue.length).toStrictEqual(0); + expect(uartAsh.reTxQueue.length).toStrictEqual(0); + expect(uartAsh.txFree.length).toStrictEqual(TX_POOL_BUFFERS); + expect(uartAsh.rxQueue.length).toStrictEqual(0); + expect(uartAsh.rxFree.length).toStrictEqual(EZSP_HOST_RX_POOL_SIZE); + expect(uartAsh.txQueue.tail).toStrictEqual(null); + expect(uartAsh.reTxQueue.tail).toStrictEqual(null); + expect(uartAsh.txFree.link).toBeInstanceOf(EzspBuffer); + expect(uartAsh.txFree.link.data.length).toStrictEqual(EZSP_MAX_FRAME_LENGTH); + expect(uartAsh.rxQueue.tail).toStrictEqual(null); + expect(uartAsh.rxFree.link).toBeInstanceOf(EzspBuffer); + expect(uartAsh.rxFree.link.data.length).toStrictEqual(EZSP_MAX_FRAME_LENGTH); + + for (const c in uartAsh.counters) { + expect(uartAsh.counters[c]).toStrictEqual(0); + } + + // this is mostly Queues testing, but make sure it works in "real" context + const link = uartAsh.txFree.link; + const buffer: EzspBuffer = uartAsh.txFree.allocBuffer(); + + expect(buffer).toStrictEqual(link); + expect(uartAsh.txFree.link).toStrictEqual(buffer.link); + expect(uartAsh.txFree.length).toStrictEqual(TX_POOL_BUFFERS - 1); + + uartAsh.txQueue.addTail(buffer); + + expect(buffer.link).toStrictEqual(null); + expect(uartAsh.txQueue.tail).toStrictEqual(buffer); + expect(uartAsh.txQueue.length).toStrictEqual(1); + + const head = uartAsh.txQueue.removeHead(); + + expect(head).toStrictEqual(buffer); + expect(head).toStrictEqual(link); + expect(uartAsh.txQueue.tail).toStrictEqual(null); + expect(uartAsh.txQueue.length).toStrictEqual(0); + + uartAsh.txFree.freeBuffer(head); + + uartAsh.txQueue.addTail(uartAsh.txFree.allocBuffer()); + uartAsh.txQueue.addTail(uartAsh.txFree.allocBuffer()); + + expect(uartAsh.txQueue.length).toStrictEqual(2); + expect(uartAsh.txFree.length).toStrictEqual(TX_POOL_BUFFERS - 2); + }); + it('Reaches CONNECTED state', async () => { + //@ts-expect-error private + const initVariablesSpy = jest.spyOn(uartAsh, 'initVariables'); + //@ts-expect-error private + const initPortSpy = jest.spyOn(uartAsh, 'initPort'); + const resetNcpSpy = jest.spyOn(uartAsh, 'resetNcp'); + const sendExecSpy = jest.spyOn(uartAsh, 'sendExec'); + //@ts-expect-error private + const onPortCloseSpy = jest.spyOn(uartAsh, 'onPortClose'); + //@ts-expect-error private + const onPortErrorSpy = jest.spyOn(uartAsh, 'onPortError'); + + const resetResult = (await uartAsh.resetNcp()); + + //@ts-expect-error private + expect(uartAsh.serialPort.settings.binding).toBe(MockBinding);// just making sure mock was registered + expect(resetResult).toStrictEqual(EzspStatus.SUCCESS); + expect(resetNcpSpy).toHaveBeenCalledTimes(1); + expect(initVariablesSpy).toHaveBeenCalledTimes(1); + expect(initPortSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(uartAsh.flags).toStrictEqual(48);// RST|CAN + //@ts-expect-error private + expect(uartAsh.serialPort).toBeDefined(); + //@ts-expect-error private + expect(uartAsh.writer).toBeDefined(); + //@ts-expect-error private + expect(uartAsh.parser).toBeDefined(); + expect(uartAsh.portOpen).toBeTruthy(); + + //@ts-expect-error private + uartAsh.serialPort.port.emitData(Buffer.from(RECD_RSTACK_BYTES)); + const startResult = (await uartAsh.start()); + + expect(startResult).toStrictEqual(EzspStatus.SUCCESS); + expect(sendExecSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(uartAsh.serialPort.port.lastWrite).toStrictEqual(Buffer.from(SEND_RST_BYTES)); + expect(uartAsh.connected).toBeTruthy(); + expect(uartAsh.counters.txAllFrames).toStrictEqual(1);// RST + expect(uartAsh.counters.rxAllFrames).toStrictEqual(1);// RSTACK + + Object.keys(uartAsh.counters).forEach((key) => { + if (key !== 'txAllFrames' && key !== 'rxAllFrames') { + expect(uartAsh.counters[key]).toStrictEqual(0); + } + }); + + await uartAsh.stop(); + + expect(initVariablesSpy).toHaveBeenCalledTimes(2);// always called on stop + expect(onPortErrorSpy).toHaveBeenCalledTimes(0); + expect(onPortCloseSpy).toHaveBeenCalledTimes(1); + }); + it.skip('Resets but failed to start b/c error in RSTACK frame returned by NCP', async () => { + //@ts-expect-error private + const rejectFrameSpy = jest.spyOn(uartAsh, 'rejectFrame'); + //@ts-expect-error private + const receiveFrameSpy = jest.spyOn(uartAsh, 'receiveFrame'); + //@ts-expect-error private + const decodeByteSpy = jest.spyOn(uartAsh, 'decodeByte'); + + const resetResult = (await uartAsh.resetNcp()); + + expect(resetResult).toStrictEqual(EzspStatus.SUCCESS); + + const badCrcRSTACK = Buffer.from(RECD_RSTACK_BYTES); + badCrcRSTACK[badCrcRSTACK.length - 2] = 0;// throw CRC low + + //@ts-expect-error private + uartAsh.serialPort.port.emitData(badCrcRSTACK); + const startResult = (await uartAsh.start()); + + await Wait(10); + + expect(startResult).toStrictEqual(EzspStatus.HOST_FATAL_ERROR); + expect(uartAsh.counters.txAllFrames).toStrictEqual(1); + expect(uartAsh.counters.rxAllFrames).toStrictEqual(0); + expect(uartAsh.counters.rxCrcErrors).toStrictEqual(1); + expect(rejectFrameSpy).toHaveBeenCalledTimes(1);// received bad RSTACK + expect(decodeByteSpy.mock.results[decodeByteSpy.mock.results.length - 1].value[0]).toStrictEqual(EzspStatus.ASH_BAD_CRC); + expect(receiveFrameSpy).toHaveLastReturnedWith(EzspStatus.NO_RX_DATA); + expect(uartAsh.connected).toBeFalsy(); + }); + describe('In CONNECTED state...', () => { + beforeEach(async () => { + const resetResult = (await uartAsh.resetNcp()); + //@ts-expect-error private + uartAsh.serialPort.port.emitData(Buffer.from(RECD_RSTACK_BYTES)); + const startResult = (await uartAsh.start()); + + expect(resetResult).toStrictEqual(EzspStatus.SUCCESS); + expect(startResult).toStrictEqual(EzspStatus.SUCCESS); + expect(uartAsh.connected).toBeTruthy(); + + uartAsh.sendExec();// ACK for RSTACK == 8070787e + expect(uartAsh.idle).toBeTruthy(); + expect(uartAsh.counters.txAckFrames).toStrictEqual(1);// ACK for RSTACK + }); + afterEach(async () => { + }); + + it('Sends DATA frame to NCP', async () => { + buffalo.setPosition(EZSP_PARAMETERS_INDEX); + buffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(EzspFrameID.VERSION)); + buffalo.setCommandByte(EZSP_SEQUENCE_INDEX, frameSequence++); + buffalo.setCommandByte( + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + (EZSP_FRAME_CONTROL_COMMAND | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) + | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK)) + ); + buffalo.writeUInt8(13);// desiredProtocolVersion + + let sendBuf = buffalo.getWritten(); + + uartAsh.send(sendBuf.length, sendBuf); + + await Wait(10); + + expect(uartAsh.counters.txDataFrames).toStrictEqual(1); + //@ts-expect-error private + expect(uartAsh.serialPort.port.recording).toStrictEqual(Buffer.concat([ + Buffer.from('1ac038bc7e', 'hex'),// RST + Buffer.from('8070787e', 'hex'),// RSTACK ACK + Buffer.from('004221a8597c057e', 'hex'),// DATA + ])); + }) + + it('Sends DATA frame and receives response from NCP', async () => { + buffalo.setPosition(EZSP_PARAMETERS_INDEX); + buffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(EzspFrameID.VERSION)); + buffalo.setCommandByte(EZSP_SEQUENCE_INDEX, frameSequence++); + buffalo.setCommandByte( + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + (EZSP_FRAME_CONTROL_COMMAND | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) + | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK)) + ); + buffalo.writeUInt8(2);// desiredProtocolVersion + + let sendBuf = buffalo.getWritten(); + + uartAsh.send(sendBuf.length, sendBuf); + + await Wait(10); + + //@ts-expect-error private + uartAsh.serialPort.port.emitData(Buffer.from(NCP_ACK_FIRST));// just an ACK, doesn't matter what it is + + await Wait(10);// force wait new frame + + expect(uartAsh.counters.txAckFrames).toStrictEqual(1); + expect(uartAsh.counters.rxAckFrames).toStrictEqual(1); + }); + + it('TODO: Sends DATA frame with NR flags when buffers are low on host', async () => {}); + + it('TODO: Sends DATA frame but times out waiting for response', async () => {}); + + it('TODO: Resends DATA frame', async () => {}); + + it('Allows sending up to TX_K frames before receiving ACK', async () => { + buffalo.setPosition(EZSP_PARAMETERS_INDEX); + buffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(EzspFrameID.VERSION)); + buffalo.setCommandByte(EZSP_SEQUENCE_INDEX, frameSequence++); + buffalo.setCommandByte( + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + (EZSP_FRAME_CONTROL_COMMAND | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) + | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK)) + ); + buffalo.writeUInt8(13);// desiredProtocolVersion + + let sendBuf = buffalo.getWritten(); + + for (let i = 0; i <= CONFIG_TX_K; i++) { + uartAsh.send(sendBuf.length, sendBuf); + } + + await Wait(10); + + expect(uartAsh.counters.txDataFrames).toStrictEqual(3); + expect(uartAsh.txQueue.length).toStrictEqual(1); + + //@ts-expect-error private + expect(uartAsh.serialPort.port.recording).toStrictEqual(Buffer.concat([ + Buffer.from('1ac038bc7e', 'hex'),// RST + Buffer.from('8070787e', 'hex'),// RSTACK ACK + Buffer.from('004221a8597c057e', 'hex'),// DATA 1 + Buffer.from('104221a859785f7e', 'hex'),// DATA 2 + Buffer.from('204221a85974b17e', 'hex'),// DATA 3 + ])); + }); + }); +}); diff --git a/test/adapter/ember/consts.ts b/test/adapter/ember/consts.ts new file mode 100644 index 0000000000..a5686dc0c9 --- /dev/null +++ b/test/adapter/ember/consts.ts @@ -0,0 +1,46 @@ +import {ASH_VERSION} from "../../../src/adapter/ember/uart/consts"; +import {AshFrameType, AshReservedByte, NcpFailedCode} from "../../../src/adapter/ember/uart/enums"; + + +export const adapterSONOFFDongleE = {manufacturer: 'ITEAD', vendorId: '1a86', productId: '55d4'}; +export const adapterHASkyConnect = {manufacturer: 'Nabu Casa', vendorId: '10c4', productId: 'ea60'}; + +/** + * Bytes sent to NCP on init + * + * 1ac038bc7e + */ +export const SEND_RST_BYTES = [ + AshReservedByte.CANCEL,// 26 - 0x1a + AshFrameType.RST,// 192 - 0xc0 + 56,// CRC high - 0x38 + 188,// CRC low - 0xbc + AshReservedByte.FLAG,// 126 - 0x7e +]; +/** + * Pre-decoding values. + * + * [193, 2, 2, 155, 123, 126] + * + * 1ac1020b0a527e + */ +export const RECD_RSTACK_BYTES = [ + AshReservedByte.CANCEL,// 26 - 0x1a + AshFrameType.RSTACK,// 193 - 0xc1 + ASH_VERSION,// 2 - 0x02 + NcpFailedCode.RESET_SOFTWARE,// 11 - 0x0b + 10,// CRC high - 0x0a + 82,// CRC low - 0x52 + AshReservedByte.FLAG,// 126 - 0x7e +]; +/** + * ACK sent by NCP after first DATA frame received. + * + * ACK(1)+ + */ +export const NCP_ACK_FIRST = [ + AshFrameType.ACK + 1, + 0x60,// CRC High + 0x59,// CRC Low + AshReservedByte.FLAG +]; diff --git a/test/adapter/ember/ezspBuffalo.test.ts b/test/adapter/ember/ezspBuffalo.test.ts new file mode 100644 index 0000000000..29cd22abf9 --- /dev/null +++ b/test/adapter/ember/ezspBuffalo.test.ts @@ -0,0 +1,48 @@ +import {EzspBuffalo} from '../../../src/adapter/ember/ezsp/buffalo'; +import {EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, EZSP_FRAME_CONTROL_COMMAND, EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK, EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET, EZSP_FRAME_CONTROL_SLEEP_MODE_MASK, EZSP_FRAME_ID_INDEX, EZSP_MAX_FRAME_LENGTH, EZSP_PARAMETERS_INDEX, EZSP_SEQUENCE_INDEX} from '../../../src/adapter/ember/ezsp/consts'; +import {EzspFrameID} from '../../../src/adapter/ember/ezsp/enums'; +import {lowByte} from '../../../src/adapter/ember/utils/math'; + + +describe('Ember EZSP Buffalo', () => { + let buffalo: EzspBuffalo; + + beforeAll(async () => { + }); + + afterAll(async () => { + }); + + beforeEach(() => { + buffalo = new EzspBuffalo(Buffer.alloc(EZSP_MAX_FRAME_LENGTH)); + }); + + afterEach(() => { + }); + + it('Is empty after init', () => { + expect(buffalo.getWritten()).toStrictEqual(Buffer.from([])); + }); + + it('Writes & read at position without altering internal position tracker', () => { + // mock send `version` command logic flow + buffalo.setPosition(EZSP_PARAMETERS_INDEX); + buffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(EzspFrameID.VERSION)); + buffalo.setCommandByte(EZSP_SEQUENCE_INDEX, 0); + buffalo.setCommandByte( + EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, + (EZSP_FRAME_CONTROL_COMMAND | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) + | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK)) + ); + buffalo.writeUInt8(12);// desiredProtocolVersion + + expect(buffalo.getWritten()).toStrictEqual(Buffer.from([0x00, 0x00, 0x00, 0x0c])); + + expect(buffalo.getCommandByte(EZSP_FRAME_ID_INDEX)).toStrictEqual(lowByte(EzspFrameID.VERSION)); + expect(buffalo.getCommandByte(EZSP_SEQUENCE_INDEX)).toStrictEqual(0); + expect(buffalo.getCommandByte(EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX)).toStrictEqual( + (EZSP_FRAME_CONTROL_COMMAND | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) + | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK)) + ); + }); +}); diff --git a/test/adapter/ember/math.test.ts b/test/adapter/ember/math.test.ts new file mode 100644 index 0000000000..dc68268e84 --- /dev/null +++ b/test/adapter/ember/math.test.ts @@ -0,0 +1,183 @@ +import * as m from "../../../src/adapter/ember/utils/math"; + +const ASH_CRC_INIT = 0xFFFF; +const B32 = 0xBEEFFACE; + +describe('Ember Math utils', () => { + it('mod8', () => { + let t = m.mod8(0x00); + expect(t).toStrictEqual(0); + t = m.mod8(0x03); + expect(t).toStrictEqual(3); + t = m.mod8(0x07); + expect(t).toStrictEqual(7); + t = m.mod8(0x08); + expect(t).toStrictEqual(0); + t = m.mod8(0x10); + expect(t).toStrictEqual(0); + t = m.mod8(0xFE); + expect(t).toStrictEqual(6); + t = m.mod8(0xFF); + expect(t).toStrictEqual(7); + }); + it('inc8', () => { + let t = m.inc8(0x00); + expect(t).toStrictEqual(1); + t = m.inc8(0x03); + expect(t).toStrictEqual(4); + t = m.inc8(0x07); + expect(t).toStrictEqual(0); + t = m.inc8(0x08); + expect(t).toStrictEqual(1); + t = m.inc8(0x10); + expect(t).toStrictEqual(1); + t = m.inc8(0xFE); + expect(t).toStrictEqual(7); + t = m.inc8(0xFF); + expect(t).toStrictEqual(0); + }); + it('withinRange', () => { + let t = m.withinRange(0x00, 0x04, 0x07); + expect(t).toStrictEqual(true); + t = m.withinRange(0x00, 0x04, 0x08); + expect(t).toStrictEqual(false); + t = m.withinRange(0xAA, 0xAC, 0xFE); + expect(t).toStrictEqual(true); + t = m.withinRange(0x00, 0x04, 0xF8); + expect(t).toStrictEqual(false); + }); + it('halCommonCrc16', () => { + let t = m.halCommonCrc16(0x00, ASH_CRC_INIT); + expect(t).toStrictEqual(57840); + t = m.halCommonCrc16(0x03, t); + expect(t).toStrictEqual(11628); + t = m.halCommonCrc16(0xFE, t); + expect(t).toStrictEqual(38686); + t = m.halCommonCrc16(0xA5, t); + expect(t).toStrictEqual(2065); + }); + it('lowBits', () => { + let t = m.lowBits(10); + expect(t).toStrictEqual(10); + t = m.lowBits(100); + expect(t).toStrictEqual(4); + }); + it('highBits', () => { + let t = m.highBits(10); + expect(t).toStrictEqual(0); + t = m.highBits(100); + expect(t).toStrictEqual(6); + }); + it('lowByte', () => { + let t = m.lowByte(10); + expect(t).toStrictEqual(10); + t = m.lowByte(100); + expect(t).toStrictEqual(100); + t = m.lowByte(1000); + expect(t).toStrictEqual(232); + t = m.lowByte(255); + expect(t).toStrictEqual(255); + t = m.lowByte(1024); + expect(t).toStrictEqual(0); + t = m.lowByte(B32); + expect(t).toStrictEqual(206); + }); + it('highByte', () => { + let t = m.highByte(10); + expect(t).toStrictEqual(0); + t = m.highByte(100); + expect(t).toStrictEqual(0); + t = m.highByte(1000); + expect(t).toStrictEqual(3); + t = m.highByte(255); + expect(t).toStrictEqual(0); + t = m.highByte(1024); + expect(t).toStrictEqual(4); + t = m.highByte(B32); + expect(t).toStrictEqual(250); + }); + it('highLowToInt', () => { + let t = m.highLowToInt(254, 10); + expect(t).toStrictEqual(65034); + t = m.highLowToInt(10, 100); + expect(t).toStrictEqual(2660); + t = m.highLowToInt(1000, 2000); + expect(t).toStrictEqual(256208); + t = m.highLowToInt(355, 255); + expect(t).toStrictEqual(91135); + t = m.highLowToInt(123, 1024); + expect(t).toStrictEqual(31488); + t = m.highLowToInt(1, B32); + expect(t).toStrictEqual(462); + t = m.highLowToInt(B32, 1); + expect(t).toStrictEqual(16436737); + }); + it('bit', () => { + let t = m.bit(11); + expect(t).toStrictEqual(2048); + t = m.bit(15); + expect(t).toStrictEqual(32768); + t = m.bit(26); + expect(t).toStrictEqual(67108864); + t = (m.bit(11) | m.bit(15) | m.bit(20) | m.bit(25)); + expect(t).toStrictEqual(34637824); + t = ((m.bit(12) | m.bit(13) | m.bit(14) | m.bit(16) | m.bit(17) | m.bit(18) | + m.bit(19) | m.bit(21) | m.bit(22) | m.bit(23) | m.bit(24) | m.bit(26))); + expect(t).toStrictEqual(99577856); + t = 53 & m.bit(0); + expect(t).toStrictEqual(1); + t = 53 & m.bit(3); + expect(t).toStrictEqual(0); + t = 53 | m.bit(0); + expect(t).toStrictEqual(53); + t = 53 | m.bit(3); + expect(t).toStrictEqual(61); + }); + it('bit32', () => { + let t = m.bit32(11); + expect(t).toStrictEqual(2048); + t = m.bit32(15); + expect(t).toStrictEqual(32768); + t = m.bit32(26); + expect(t).toStrictEqual(67108864); + t = (m.bit32(11) | m.bit32(15) | m.bit32(20) | m.bit32(25)); + expect(t).toStrictEqual(34637824); + t = ((m.bit32(12) | m.bit32(13) | m.bit32(14) | m.bit32(16) | m.bit32(17) | m.bit32(18) | + m.bit32(19) | m.bit32(21) | m.bit32(22) | m.bit32(23) | m.bit32(24) | m.bit32(26))); + expect(t).toStrictEqual(99577856); + t = B32 & m.bit32(0); + expect(t).toStrictEqual(0); + t = B32 & m.bit32(3); + expect(t).toStrictEqual(8); + t = B32 | m.bit32(0); + expect(t).toStrictEqual(-1091568945); + t = B32 | m.bit32(3); + expect(t).toStrictEqual(-1091568946); + }); + it('lowHighBytes', () => { + let [l, h] = m.lowHighBytes(1024); + expect(l).toStrictEqual(0); + expect(h).toStrictEqual(4); + expect(l).toStrictEqual(m.lowByte(1024)); + expect(h).toStrictEqual(m.highByte(1024)); + [l, h] = m.lowHighBytes(255); + expect(l).toStrictEqual(255); + expect(h).toStrictEqual(0); + }); + it('lowHighBits', () => { + let [l, h] = m.lowHighBits(10); + expect(l).toStrictEqual(10); + expect(h).toStrictEqual(0); + expect(l).toStrictEqual(m.lowBits(10)); + expect(h).toStrictEqual(m.highBits(0)); + [l, h] = m.lowHighBits(100); + expect(l).toStrictEqual(4); + expect(h).toStrictEqual(6); + }); + it('byteToBits', () => { + let t = m.byteToBits(2); + expect(t).toStrictEqual('00000010'); + t = m.byteToBits(4); + expect(t).toStrictEqual('00000100'); + }) +}); diff --git a/test/adapter/ember/requestQueue.test.ts b/test/adapter/ember/requestQueue.test.ts new file mode 100644 index 0000000000..2017968b95 --- /dev/null +++ b/test/adapter/ember/requestQueue.test.ts @@ -0,0 +1,613 @@ +import {EmberRequestQueue, NETWORK_BUSY_DEFER_MSEC, NETWORK_DOWN_DEFER_MSEC} from '../../../src/adapter/ember/adapter/requestQueue'; +import {EmberStatus, EzspStatus} from '../../../src/adapter/ember/enums'; +import {Wait} from '../../../src/utils'; + +let fakeWaitTime = 1000; +let varyingReturn: EmberStatus = EmberStatus.SUCCESS; +const getVaryingReturn = async (): Promise => { + await Wait(fakeWaitTime); + return varyingReturn; +}; +const getThrownError = async (): Promise => { + await Wait(fakeWaitTime); + throw new Error(EzspStatus[EzspStatus.ASH_ACK_TIMEOUT]); +} +const getThrowNetworkBusy = async (): Promise => { + await Wait(fakeWaitTime); + throw new Error(EzspStatus[EzspStatus.NO_TX_SPACE]); +}; +const getThrowNetworkDown = async (): Promise => { + await Wait(fakeWaitTime); + throw new Error(EzspStatus[EzspStatus.NOT_CONNECTED]); +}; + +class TestThis { + public bs: boolean; + public q: EmberRequestQueue; + + constructor() { + this.bs = false; + this.q = new EmberRequestQueue(60); + } + + public async getNewBS(): Promise { + await new Promise((resolve, reject): void => { + this.q.enqueue( + async (): Promise => { + await Wait(fakeWaitTime); + + this.bs = true; + + resolve(); + return EmberStatus.SUCCESS; + }, + reject, + ) + }) + + return this.bs; + } +} + +let deferSpy; + +describe('Ember Request Queue', () => { + let requestQueue: EmberRequestQueue; + + beforeAll(async () => { + jest.useFakeTimers(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + beforeEach(() => { + requestQueue = new EmberRequestQueue(60); + // don't let defer (dispatching tick mgmt) interfere with "manual" flow of tests unless wanted + deferSpy = jest.spyOn(requestQueue, 'defer').mockImplementation(jest.fn()); + }); + + afterEach(() => { + fakeWaitTime = 1000; + varyingReturn = EmberStatus.SUCCESS; + }); + + it('Queues request and resolves it', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.SUCCESS; + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + jest.advanceTimersByTime(fakeWaitTime + 100); + + await expect(p).resolves.toBe(123);// gives result of resolve + + expect(funcSpy).toHaveBeenCalledTimes(1);// enqueued func was called + expect(funcRejectSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request, rejects it on error, and removes it from queue', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.ERR_FATAL; + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + jest.advanceTimersByTime(fakeWaitTime + 100); + + await expect(p).rejects.toStrictEqual(new Error(EmberStatus[varyingReturn])); + + expect(funcSpy).toHaveBeenCalledTimes(1); + expect(funcRejectSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request, rejects it on throw, and removes it from queue', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getThrownError(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + jest.advanceTimersByTime(fakeWaitTime + 100); + + await expect(p).rejects.toStrictEqual(new Error(EzspStatus[EzspStatus.ASH_ACK_TIMEOUT])); + + expect(funcSpy).toHaveBeenCalledTimes(1); + expect(funcRejectSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request, defers on NETWORK_BUSY and defers again on NETWORK_DOWN', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.NETWORK_BUSY; + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// enqueued + + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100 + (NETWORK_BUSY_DEFER_MSEC * 0.25)); + + expect(deferSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_BUSY_DEFER_MSEC + 100); + + varyingReturn = EmberStatus.NETWORK_DOWN; + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100 + (NETWORK_DOWN_DEFER_MSEC * 0.25)); + + expect(deferSpy).toHaveBeenCalledTimes(2); + expect(funcSpy).toHaveBeenCalledTimes(2);// dispatch x2, func called x2 + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_DOWN_DEFER_MSEC + 100); + }); + it('Queues request, defers on NETWORK_BUSY and then resolves it', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.NETWORK_BUSY; + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// enqueued + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100 + (NETWORK_BUSY_DEFER_MSEC * 0.25)); + + expect(deferSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_BUSY_DEFER_MSEC + 100); + + varyingReturn = EmberStatus.SUCCESS; + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + await expect(p).resolves.toBe(123);// gives result of resolve + + expect(funcSpy).toHaveBeenCalledTimes(2);// enqueued func was called + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request, defers on NETWORK_BUSY and only retries once after internal change', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.NETWORK_BUSY; + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + // internally changes external parameter that changes the queue's next run + varyingReturn = EmberStatus.SUCCESS; + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// enqueued + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100 + (NETWORK_BUSY_DEFER_MSEC * 0.25)); + + expect(deferSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_BUSY_DEFER_MSEC + 100); + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + await expect(p).resolves.toBe(123);// gives result of resolve + + expect(funcSpy).toHaveBeenCalledTimes(2);// enqueued func was called + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request, defers on thrown NETWORK_BUSY', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getThrowNetworkBusy(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// enqueued + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100 + (NETWORK_BUSY_DEFER_MSEC * 0.25)); + + expect(deferSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_BUSY_DEFER_MSEC + 100); + }); + it('Queues request and resolves by priority', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.SUCCESS; + const p = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(123); + return status; + }, + reject, + ); + }); + const pPrio = new Promise((resolve, reject) => { + requestQueue.enqueue( + async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + resolve(456); + return status; + }, + reject, + true, + ); + }); + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcPrioSpy = jest.spyOn(requestQueue.priorityQueue[0], 'func'); + + expect(enqueueSpy).toHaveBeenCalledTimes(2); + expect(funcSpy).toHaveBeenCalledTimes(0); + expect(funcPrioSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + //@ts-expect-error private + expect(requestQueue.priorityQueue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + jest.advanceTimersByTime(fakeWaitTime + 100); + + await expect(pPrio).resolves.toBe(456);// gives result of resolve + + expect(funcSpy).toHaveBeenCalledTimes(0);// enqueued func was not called + expect(funcPrioSpy).toHaveBeenCalledTimes(1);// enqueued func was called + //@ts-expect-error private + expect(requestQueue.priorityQueue).toHaveLength(0);// no longer in queue + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + requestQueue.dispatch();// don't await so we can advance timer + + jest.advanceTimersByTime(fakeWaitTime + 100); + + await expect(p).resolves.toBe(123);// gives result of resolve + + expect(funcSpy).toHaveBeenCalledTimes(1);// enqueued func was called + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request send & forget style', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.SUCCESS; + + requestQueue.enqueue(async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + return status; + }, () => {}); + + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + expect(funcSpy).toHaveBeenCalledTimes(1);// enqueued func was called + expect(funcRejectSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request send & forget style that errors out silently', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + varyingReturn = EmberStatus.ERR_FATAL; + + requestQueue.enqueue(async (): Promise => { + const status: EmberStatus = await getVaryingReturn(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + return status; + }, () => {}); + + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + expect(funcSpy).toHaveBeenCalledTimes(1);// enqueued func was called + expect(funcRejectSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request send & forget style that throws silently', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + requestQueue.enqueue(async (): Promise => { + const status: EmberStatus = await getThrownError(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + return status; + }, () => {}); + + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + expect(funcSpy).toHaveBeenCalledTimes(1);// enqueued func was called + expect(funcRejectSpy).toHaveBeenCalledTimes(1); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(0);// no longer in queue + }); + it('Queues request send & forget style that throws NETWORK_DOWN silently and retries', async () => { + const enqueueSpy = jest.spyOn(requestQueue, 'enqueue'); + + requestQueue.enqueue(async (): Promise => { + const status: EmberStatus = await getThrowNetworkDown(); + + if (status !== EmberStatus.SUCCESS) { + return status; + } + + return status; + }, () => {}); + + //@ts-expect-error private + const funcSpy = jest.spyOn(requestQueue.queue[0], 'func'); + //@ts-expect-error private + const funcRejectSpy = jest.spyOn(requestQueue.queue[0], 'reject'); + + expect(enqueueSpy).toHaveBeenCalledTimes(1); + expect(funcSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1); + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + expect(funcSpy).toHaveBeenCalledTimes(1);// enqueued func was called + expect(funcRejectSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_DOWN_DEFER_MSEC * 2); + + requestQueue.dispatch();// don't await so we can advance timer + + await jest.advanceTimersByTimeAsync(fakeWaitTime + 100); + + expect(funcSpy).toHaveBeenCalledTimes(2);// enqueued func was called + expect(funcRejectSpy).toHaveBeenCalledTimes(0); + //@ts-expect-error private + expect(requestQueue.queue).toHaveLength(1);// still in queue + + await jest.advanceTimersByTimeAsync(NETWORK_DOWN_DEFER_MSEC * 2); + }); + + it('In-class queues also work, just for kicks...', async () => { + const t = new TestThis(); + + expect(t.bs).toBeFalsy(); + + const tBS = t.getNewBS(); + + t.q.dispatch();// don't await so we can advance timer + jest.advanceTimersByTime(fakeWaitTime + 100); + + await expect(tBS).resolves.toBeTruthy(); + }) +}); diff --git a/test/controller.test.ts b/test/controller.test.ts index ed76acdea7..e0ace036aa 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -3717,7 +3717,7 @@ describe('Controller', () => { mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); let error; try {await Adapter.create(null, {path: null, baudRate: 100, rtscts: false, adapter: 'efr'}, null, null)} catch (e) {error = e;} - expect(error).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate, ezsp`)); + expect(error).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate, ezsp, ember`)); }); it('Emit read from device', async () => {