UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,115 lines (1,027 loc) 375 kB
/* v8 ignore start */ import EventEmitter from "node:events"; import {Queue} from "../../../utils"; import {logger} from "../../../utils/logger"; import * as ZSpec from "../../../zspec"; import type {Eui64, ExtendedPanId, NodeId, PanId} from "../../../zspec/tstypes"; import * as Zcl from "../../../zspec/zcl"; import {Clusters} from "../../../zspec/zcl/definition/cluster"; import * as Zdo from "../../../zspec/zdo"; import type {SerialPortOptions} from "../../tstype"; import {FIXED_ENDPOINTS} from "../adapter/endpoints"; import { 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, } from "../consts"; import { EmberApsOption, EmberCounterType, EmberDeviceUpdate, type EmberDutyCycleState, type EmberEntropySource, type EmberEventUnits, type EmberExtendedSecurityBitmask, EmberGPStatus, EmberGpApplicationId, EmberGpKeyType, EmberGpSecurityLevel, EmberIncomingMessageType, EmberInterpanMessageType, EmberJoinDecision, EmberKeyStatus, EmberLeaveNetworkOption, type EmberLibraryId, type EmberLibraryStatus, type EmberMacPassthroughType, type EmberMultiPhyNwkConfig, type EmberNetworkStatus, type EmberNodeType, EmberOutgoingMessageType, type EmberSourceRouteDiscoveryMode, EmberStackError, type EmberTXPowerMode, type EmberTransmitPriority, type EzspNetworkScanType, EzspStatus, type EzspZllNetworkOperation, type IEEE802154CcaMode, SLStatus, SecManFlag, SecManKeyType, } from "../enums"; import {EzspError} from "../ezspError"; import type { Ember802154RadioPriorities, EmberAesMmoHashContext, EmberApsFrame, EmberBeaconClassificationParams, EmberBeaconData, EmberBindingTableEntry, EmberCertificate283k1Data, EmberCertificateData, EmberChildData, EmberCurrentSecurityState, EmberDutyCycleLimits, EmberEndpointDescription, EmberGpAddress, EmberGpProxyTableEntry, EmberGpSinkTableEntry, EmberInitialSecurityState, EmberKeyData, EmberMessageDigest, EmberMultiPhyRadioParameters, EmberMulticastTableEntry, EmberMultiprotocolPriorities, EmberNeighborTableEntry, EmberNetworkInitStruct, EmberNetworkParameters, EmberPerDeviceDutyCycle, EmberPrivateKeyData, EmberPublicKey283k1Data, EmberPublicKeyData, EmberRouteTableEntry, EmberRxPacketInfo, EmberSignature283k1Data, EmberSignatureData, EmberSmacData, EmberTokTypeStackZllData, EmberTokTypeStackZllSecurity, EmberTokenData, EmberTokenInfo, EmberVersion, EmberZigbeeNetwork, EmberZllAddressAssignment, EmberZllDeviceInfoRecord, EmberZllInitialSecurityState, EmberZllNetwork, SecManAPSKeyMetadata, SecManContext, SecManKey, SecManNetworkKeyInfo, } from "../types"; import {UartAsh} from "../uart/ash"; import {initSecurityManagerContext} from "../utils/initters"; import {highByte, highLowToInt, lowByte} from "../utils/math"; import {EzspBuffalo} from "./buffalo"; import { EMBER_ENCRYPTION_KEY_SIZE, 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_ASYNCH_CB, EZSP_FRAME_CONTROL_COMMAND, EZSP_FRAME_CONTROL_DIRECTION_MASK, EZSP_FRAME_CONTROL_INDEX, EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK, EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET, EZSP_FRAME_CONTROL_OVERFLOW, EZSP_FRAME_CONTROL_OVERFLOW_MASK, EZSP_FRAME_CONTROL_PENDING_CB, EZSP_FRAME_CONTROL_PENDING_CB_MASK, EZSP_FRAME_CONTROL_RESPONSE, EZSP_FRAME_CONTROL_SLEEP_MODE_MASK, EZSP_FRAME_CONTROL_TRUNCATED, EZSP_FRAME_CONTROL_TRUNCATED_MASK, EZSP_FRAME_ID_INDEX, EZSP_MAX_FRAME_LENGTH, EZSP_PARAMETERS_INDEX, EZSP_SEQUENCE_INDEX, } from "./consts"; import { type EmberLeaveReason, type EmberRejoinReason, type EzspConfigId, type EzspEndpointFlag, EzspExtendedValueId, EzspFrameID, EzspMfgTokenId, type EzspPolicyId, EzspSleepMode, EzspValueId, } from "./enums"; const NS = "zh:ember:ezsp"; /** * Simple object to resolve/timeout on command waiting for response. */ type EzspWaiter = { timer: NodeJS.Timeout; resolve: (value: EzspStatus | PromiseLike<EzspStatus>) => 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; export interface EmberEzspEventMap { /** An error was detected that requires resetting the NCP. */ ncpNeedsResetAndInit: [status: EzspStatus]; /** @see Ezsp.ezspIncomingMessageHandler */ zdoResponse: [apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer]; /** ezspIncomingMessageHandler */ incomingMessage: [type: EmberIncomingMessageType, apsFrame: EmberApsFrame, lastHopLqi: number, sender: NodeId, messageContents: Buffer]; /** @see Ezsp.ezspMacFilterMatchMessageHandler */ touchlinkMessage: [sourcePanId: PanId, sourceAddress: Eui64, groupId: number, lastHopLqi: number, messageContents: Buffer]; /** @see Ezsp.ezspStackStatusHandler */ stackStatus: [status: SLStatus]; /** @see Ezsp.ezspTrustCenterJoinHandler */ trustCenterJoin: [ newNodeId: NodeId, newNodeEui64: Eui64, status: EmberDeviceUpdate, policyDecision: EmberJoinDecision, parentOfNewNodeId: NodeId, ]; /** @see Ezsp.ezspMessageSentHandler */ messageSent: [status: SLStatus, type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number]; } /** * 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`...). */ export class Ezsp extends EventEmitter<EmberEzspEventMap> { private version: 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 readonly callbackBuffalo: EzspBuffalo; /** The contents of the current EZSP frame. CAREFUL using this guy, it's pre-allocated. */ private readonly callbackFrameContents: Buffer; /** The total Length of the incoming frame */ private callbackFrameLength: number; private initialVersionSent: 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; private readonly queue: Queue; /** Awaiting response resolve/timer struct. undefined if not waiting for response. */ private responseWaiter?: EzspWaiter; /** Counter for Queue Full errors */ public counterErrQueueFull: number; constructor(options: SerialPortOptions) { super(); this.frameContents = Buffer.alloc(EZSP_MAX_FRAME_LENGTH); this.buffalo = new EzspBuffalo(this.frameContents, 0); this.callbackFrameContents = Buffer.alloc(EZSP_MAX_FRAME_LENGTH); this.callbackBuffalo = new EzspBuffalo(this.callbackFrameContents, 0); this.queue = new Queue(1); this.ash = new UartAsh(options); this.version = 0; this.frameLength = 0; this.callbackFrameLength = 0; this.initialVersionSent = false; this.frameSequence = -1; // start at 0 this.sendSequence = 0; // start at 1 this.counterErrQueueFull = 0; } /** * 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 received frame in storage. */ get frameToString(): string { const id = this.buffalo.getFrameId(); return `[FRAME: ID=${id}:"${EzspFrameID[id]}" Seq=${this.frameContents[EZSP_SEQUENCE_INDEX]} Len=${this.frameLength}]`; } /** * Create a string representation of the last received callback frame in storage. */ get callbackFrameToString(): string { const id = this.callbackBuffalo.getFrameId(); return `[CBFRAME: ID=${id}:"${EzspFrameID[id]}" Seq=${this.callbackFrameContents[EZSP_SEQUENCE_INDEX]} Len=${this.callbackFrameLength}]`; } public async start(): Promise<EzspStatus> { logger.info("======== EZSP starting ========", NS); let status: EzspStatus = EzspStatus.HOST_FATAL_ERROR; for (let i = 0; i < MAX_INIT_ATTEMPTS; i++) { status = await this.ash.resetNcp(); // fail early if we couldn't even get the port set up if (status !== EzspStatus.SUCCESS) { return status; } status = await this.ash.start(); if (status === EzspStatus.SUCCESS) { logger.info("======== EZSP started ========", NS); // registered after reset sequence this.ash.on("frame", this.onAshFrame.bind(this)); this.ash.on("fatalError", this.onAshFatalError.bind(this)); 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<void> { await this.ash.stop(); this.ash.removeAllListeners(); logger.info("======== EZSP stopped ========", NS); } /** * Must be called immediately after `ezspVersion` to sync the Host protocol version. * @param version Version for the Host to use. */ public setProtocolVersion(version: number): void { this.version = version; } /** * Check if connected. * If not, attempt to restore the connection. * * @returns */ public checkConnection(): boolean { return this.ash.connected; } /** * Triggered by @see 'FATAL_ERROR' */ private onAshFatalError(status: EzspStatus): void { this.emit("ncpNeedsResetAndInit", status); } /** * Triggered by @see 'FRAME' */ private onAshFrame(): void { const buffer = this.ash.rxQueue.getPrecedingEntry(); if (buffer == null) { // something is seriously wrong logger.error("Found no buffer in queue but ASH layer sent signal that one was available.", NS); return; } this.ash.rxQueue.removeEntry(buffer); // logger.debug(`<<<< ${buffer.data.subarray(0, buffer.len).toString('hex')}`, NS); if (buffer.data[EZSP_FRAME_CONTROL_INDEX] & EZSP_FRAME_CONTROL_ASYNCH_CB) { // take only what len tells us is actual content buffer.data.copy(this.callbackFrameContents, 0, 0, buffer.len); this.callbackFrameLength = buffer.len; logger.debug(`<=== ${this.callbackFrameToString}`, NS); this.ash.rxFree.freeBuffer(buffer); const status = this.validateReceivedFrame(this.callbackBuffalo); if (status === EzspStatus.SUCCESS) { this.callbackDispatch(); } else { logger.debug(`<=x= ${this.callbackFrameToString} Invalid, status=${EzspStatus[status]}.`, NS); } } else { // take only what len tells us is actual content buffer.data.copy(this.frameContents, 0, 0, buffer.len); this.frameLength = buffer.len; logger.debug(`<=== ${this.frameToString}`, NS); this.ash.rxFree.freeBuffer(buffer); // always if (this.responseWaiter !== undefined) { const status = this.validateReceivedFrame(this.buffalo); clearTimeout(this.responseWaiter.timer); this.responseWaiter.resolve(status); this.responseWaiter = undefined; // done, gc } else { logger.debug("Received response while not expecting one. Ignoring.", NS); } } } /** * 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 Frame: ${this.frameToString}.`; if (status === EzspStatus.ERROR_QUEUE_FULL) { this.counterErrQueueFull += 1; logger.error(`Adapter queue full (counter: ${this.counterErrQueueFull}). ${lastFrameStr}`, NS); } else if (status === EzspStatus.ERROR_OVERFLOW) { logger.error( `The adapter has run out of buffers, causing general malfunction. Remediate network congestion, if present. ${lastFrameStr}`, NS, ); } else { logger.error(`ERROR Transaction failure; status=${EzspStatus[status]}. ${lastFrameStr}`, NS); } // Do not reset if improper frame control direction flag (XXX: certain devices appear to trigger this somehow, without reason?) // 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 && status !== EzspStatus.ERROR_WRONG_DIRECTION ) { this.emit("ncpNeedsResetAndInit", status); } } private nextFrameSequence(): number { this.frameSequence = ++this.frameSequence & 0xff; return this.frameSequence; } private startCommand(command: number): EzspBuffalo { const sendBuffalo = new EzspBuffalo(Buffer.alloc(EZSP_MAX_FRAME_LENGTH)); // Send initial EZSP_VERSION command with old packet format for old Hosts/NCPs if (command === EzspFrameID.VERSION && !this.initialVersionSent) { sendBuffalo.setPosition(EZSP_PARAMETERS_INDEX); sendBuffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(command)); } else { // convert to extended frame format sendBuffalo.setPosition(EZSP_EXTENDED_PARAMETERS_INDEX); sendBuffalo.setCommandByte(EZSP_EXTENDED_FRAME_ID_LB_INDEX, lowByte(command)); sendBuffalo.setCommandByte(EZSP_EXTENDED_FRAME_ID_HB_INDEX, highByte(command)); } return sendBuffalo; } /** * 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_ERROR_TIMEOUTS * * 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(sendBuffalo: EzspBuffalo): Promise<EzspStatus> { if (!this.checkConnection()) { logger.debug("[SEND COMMAND] NOT CONNECTED", NS); return EzspStatus.NOT_CONNECTED; } const sequence = this.nextFrameSequence(); sendBuffalo.setCommandByte(EZSP_SEQUENCE_INDEX, sequence); // we always set the network index in the ezsp frame control. sendBuffalo.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 && sendBuffalo.getCommandByte(EZSP_FRAME_ID_INDEX) === EzspFrameID.VERSION) { this.initialVersionSent = true; } else { sendBuffalo.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 = sendBuffalo.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; } return await this.queue.execute<EzspStatus>(async () => { let status: EzspStatus = EzspStatus.ASH_ERROR_TIMEOUTS; // will be overwritten below as necessary const frameId = sendBuffalo.getFrameId(); const frameString = `[FRAME: ID=${frameId}:"${EzspFrameID[frameId]}" Seq=${sequence} Len=${length}]`; logger.debug(`===> ${frameString}`, NS); try { status = await new Promise<EzspStatus>((resolve, reject: (reason: EzspError) => void): void => { const sendStatus = this.ash.send(length, sendBuffalo.getBuffer()); if (sendStatus !== EzspStatus.SUCCESS) { reject(new EzspError(sendStatus)); return; } this.responseWaiter = { timer: setTimeout(() => reject(new EzspError(EzspStatus.ASH_ERROR_TIMEOUTS)), this.ash.responseTimeout), resolve, }; }); if (status !== EzspStatus.SUCCESS) { throw new EzspError(status); } } catch (error) { this.responseWaiter = undefined; logger.debug(`=x=> ${frameString} ${error}`, NS); if (error instanceof EzspError) { status = error.code; this.ezspErrorHandler(status); } } return status; }); } /** * Sets the stage for parsing if valid (indexes buffalo to params index). * @returns */ public validateReceivedFrame(buffalo: EzspBuffalo): EzspStatus { const [retStatus, frameControl, frameId, parametersIndex] = buffalo.getResponseMetadata(); let status = retStatus; if (frameId === EzspFrameID.INVALID_COMMAND) { status = 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) { this.ezspErrorHandler(status); } 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) { status = EzspStatus.SUCCESS; } return status; } /** * Dispatches callback frames handlers. */ public callbackDispatch(): void { switch (this.callbackBuffalo.getExtFrameId()) { case EzspFrameID.NO_CALLBACKS: { this.ezspNoCallbacks(); break; } case EzspFrameID.STACK_TOKEN_CHANGED_HANDLER: { const tokenAddress = this.callbackBuffalo.readUInt16(); this.ezspStackTokenChangedHandler(tokenAddress); break; } case EzspFrameID.TIMER_HANDLER: { const timerId = this.callbackBuffalo.readUInt8(); this.ezspTimerHandler(timerId); break; } case EzspFrameID.COUNTER_ROLLOVER_HANDLER: { const type: EmberCounterType = this.callbackBuffalo.readUInt8(); this.ezspCounterRolloverHandler(type); break; } case EzspFrameID.CUSTOM_FRAME_HANDLER: { const payload = this.callbackBuffalo.readPayload(); this.ezspCustomFrameHandler(payload); break; } case EzspFrameID.STACK_STATUS_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); this.ezspStackStatusHandler(status); break; } case EzspFrameID.ENERGY_SCAN_RESULT_HANDLER: { const channel = this.callbackBuffalo.readUInt8(); const maxRssiValue = this.callbackBuffalo.readInt8(); this.ezspEnergyScanResultHandler(channel, maxRssiValue); break; } case EzspFrameID.NETWORK_FOUND_HANDLER: { const networkFound: EmberZigbeeNetwork = this.callbackBuffalo.readEmberZigbeeNetwork(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); this.ezspNetworkFoundHandler(networkFound, lastHopLqi, lastHopRssi); break; } case EzspFrameID.SCAN_COMPLETE_HANDLER: { const channel = this.callbackBuffalo.readUInt8(); const status = this.callbackBuffalo.readStatus(this.version); this.ezspScanCompleteHandler(channel, status); break; } case EzspFrameID.UNUSED_PAN_ID_FOUND_HANDLER: { const panId: PanId = this.callbackBuffalo.readUInt16(); const channel = this.callbackBuffalo.readUInt8(); this.ezspUnusedPanIdFoundHandler(panId, channel); break; } case EzspFrameID.CHILD_JOIN_HANDLER: { const index = this.callbackBuffalo.readUInt8(); const joining = this.callbackBuffalo.readUInt8() !== 0; const childId: NodeId = this.callbackBuffalo.readUInt16(); const childEui64 = this.callbackBuffalo.readIeeeAddr(); const childType: EmberNodeType = this.callbackBuffalo.readUInt8(); this.ezspChildJoinHandler(index, joining, childId, childEui64, childType); break; } case EzspFrameID.DUTY_CYCLE_HANDLER: { const channelPage = this.callbackBuffalo.readUInt8(); const channel = this.callbackBuffalo.readUInt8(); const state: EmberDutyCycleState = this.callbackBuffalo.readUInt8(); const totalDevices = this.callbackBuffalo.readUInt8(); const arrayOfDeviceDutyCycles: EmberPerDeviceDutyCycle[] = this.callbackBuffalo.readEmberPerDeviceDutyCycle(); this.ezspDutyCycleHandler(channelPage, channel, state, totalDevices, arrayOfDeviceDutyCycles); break; } case EzspFrameID.REMOTE_SET_BINDING_HANDLER: { const entry: EmberBindingTableEntry = this.callbackBuffalo.readEmberBindingTableEntry(); const index = this.callbackBuffalo.readUInt8(); const policyDecision = this.callbackBuffalo.readStatus(this.version); this.ezspRemoteSetBindingHandler(entry, index, policyDecision); break; } case EzspFrameID.REMOTE_DELETE_BINDING_HANDLER: { const index = this.callbackBuffalo.readUInt8(); const policyDecision = this.callbackBuffalo.readStatus(this.version); this.ezspRemoteDeleteBindingHandler(index, policyDecision); break; } case EzspFrameID.MESSAGE_SENT_HANDLER: { if (this.version < 0x0e) { const type: EmberOutgoingMessageType = this.callbackBuffalo.readUInt8(); const indexOrDestination = this.callbackBuffalo.readUInt16(); const apsFrame: EmberApsFrame = this.callbackBuffalo.readEmberApsFrame(); const messageTag = this.callbackBuffalo.readUInt8(); const status = this.callbackBuffalo.readStatus(this.version); // EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY set to messageTag only, so skip parsing entirely // const messageContents = this.callbackBuffalo.readPayload(); this.ezspMessageSentHandler(status, type, indexOrDestination, apsFrame, messageTag); } else { const status = this.callbackBuffalo.readUInt32(); const type: EmberOutgoingMessageType = this.callbackBuffalo.readUInt8(); const indexOrDestination = this.callbackBuffalo.readUInt16(); const apsFrame: EmberApsFrame = this.callbackBuffalo.readEmberApsFrame(); const messageTag = this.callbackBuffalo.readUInt16(); // EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY set to messageTag only, so skip parsing entirely // const messageContents = this.callbackBuffalo.readPayload(); this.ezspMessageSentHandler(status, type, indexOrDestination, apsFrame, messageTag); } break; } case EzspFrameID.POLL_COMPLETE_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); this.ezspPollCompleteHandler(status); break; } case EzspFrameID.POLL_HANDLER: { const childId: NodeId = this.callbackBuffalo.readUInt16(); const transmitExpected = this.callbackBuffalo.readUInt8() !== 0; this.ezspPollHandler(childId, transmitExpected); break; } case EzspFrameID.INCOMING_MESSAGE_HANDLER: { if (this.version < 0x0e) { const type: EmberIncomingMessageType = this.callbackBuffalo.readUInt8(); const apsFrame = this.callbackBuffalo.readEmberApsFrame(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const senderShortId = this.callbackBuffalo.readUInt16(); const bindingIndex = this.callbackBuffalo.readUInt8(); const addressIndex = this.callbackBuffalo.readUInt8(); const packetInfo: EmberRxPacketInfo = { senderShortId, senderLongId: ZSpec.BLANK_EUI64, bindingIndex, addressIndex, lastHopLqi, lastHopRssi, lastHopTimestamp: 0, }; const messageContents = this.callbackBuffalo.readPayload(); this.ezspIncomingMessageHandler(type, apsFrame, packetInfo, messageContents); } else { const type: EmberIncomingMessageType = this.callbackBuffalo.readUInt8(); const apsFrame = this.callbackBuffalo.readEmberApsFrame(); const packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); const messageContents = this.callbackBuffalo.readPayload(); this.ezspIncomingMessageHandler(type, apsFrame, packetInfo, messageContents); } break; } case EzspFrameID.INCOMING_MANY_TO_ONE_ROUTE_REQUEST_HANDLER: { const source: NodeId = this.callbackBuffalo.readUInt16(); const longId = this.callbackBuffalo.readIeeeAddr(); const cost = this.callbackBuffalo.readUInt8(); this.ezspIncomingManyToOneRouteRequestHandler(source, longId, cost); break; } case EzspFrameID.INCOMING_ROUTE_ERROR_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); const target: NodeId = this.callbackBuffalo.readUInt16(); this.ezspIncomingRouteErrorHandler(status, target); break; } case EzspFrameID.INCOMING_NETWORK_STATUS_HANDLER: { const errorCode = this.callbackBuffalo.readUInt8(); const target: NodeId = this.callbackBuffalo.readUInt16(); this.ezspIncomingNetworkStatusHandler(errorCode, target); break; } case EzspFrameID.INCOMING_ROUTE_RECORD_HANDLER: { const source: NodeId = this.callbackBuffalo.readUInt16(); const sourceEui = this.callbackBuffalo.readIeeeAddr(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const relayCount = this.callbackBuffalo.readUInt8(); const relayList = this.callbackBuffalo.readListUInt16(relayCount); //this.callbackBuffalo.readListUInt8(relayCount * 2); this.ezspIncomingRouteRecordHandler(source, sourceEui, lastHopLqi, lastHopRssi, relayCount, relayList); break; } case EzspFrameID.ID_CONFLICT_HANDLER: { const id: NodeId = this.callbackBuffalo.readUInt16(); this.ezspIdConflictHandler(id); break; } case EzspFrameID.MAC_PASSTHROUGH_MESSAGE_HANDLER: { if (this.version < 0x0e) { const messageType: EmberMacPassthroughType = this.callbackBuffalo.readUInt8(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const packetInfo: EmberRxPacketInfo = { senderShortId: ZSpec.NULL_NODE_ID, senderLongId: ZSpec.BLANK_EUI64, bindingIndex: ZSpec.NULL_BINDING, addressIndex: 0xff, lastHopLqi, lastHopRssi, lastHopTimestamp: 0, }; const messageContents = this.callbackBuffalo.readPayload(); this.ezspMacPassthroughMessageHandler(messageType, packetInfo, messageContents); } else { const messageType: EmberMacPassthroughType = this.callbackBuffalo.readUInt8(); const packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); const messageContents = this.callbackBuffalo.readPayload(); this.ezspMacPassthroughMessageHandler(messageType, packetInfo, messageContents); } break; } case EzspFrameID.MAC_FILTER_MATCH_MESSAGE_HANDLER: { if (this.version < 0x0e) { const filterIndexMatch = this.callbackBuffalo.readUInt8(); const legacyPassthroughType: EmberMacPassthroughType = this.callbackBuffalo.readUInt8(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const packetInfo: EmberRxPacketInfo = { senderShortId: ZSpec.NULL_NODE_ID, senderLongId: ZSpec.BLANK_EUI64, bindingIndex: ZSpec.NULL_BINDING, addressIndex: 0xff, lastHopLqi, lastHopRssi, lastHopTimestamp: 0, }; const messageContents = this.callbackBuffalo.readPayload(); this.ezspMacFilterMatchMessageHandler(filterIndexMatch, legacyPassthroughType, packetInfo, messageContents); } else { const filterIndexMatch = this.callbackBuffalo.readUInt8(); const legacyPassthroughType: EmberMacPassthroughType = this.callbackBuffalo.readUInt8(); const packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); const messageContents = this.callbackBuffalo.readPayload(); this.ezspMacFilterMatchMessageHandler(filterIndexMatch, legacyPassthroughType, packetInfo, messageContents); } break; } case EzspFrameID.RAW_TRANSMIT_COMPLETE_HANDLER: { if (this.version < 0x0e) { const status = this.callbackBuffalo.readStatus(this.version); this.ezspRawTransmitCompleteHandler(Buffer.alloc(0), status); } else { const messageContents = this.callbackBuffalo.readPayload(); const status = this.callbackBuffalo.readUInt32(); this.ezspRawTransmitCompleteHandler(messageContents, status); } break; } case EzspFrameID.SWITCH_NETWORK_KEY_HANDLER: { const sequenceNumber = this.callbackBuffalo.readUInt8(); this.ezspSwitchNetworkKeyHandler(sequenceNumber); break; } case EzspFrameID.ZIGBEE_KEY_ESTABLISHMENT_HANDLER: { const partner = this.callbackBuffalo.readIeeeAddr(); const status: EmberKeyStatus = this.callbackBuffalo.readUInt8(); this.ezspZigbeeKeyEstablishmentHandler(partner, status); break; } case EzspFrameID.TRUST_CENTER_JOIN_HANDLER: { const newNodeId: NodeId = this.callbackBuffalo.readUInt16(); const newNodeEui64 = this.callbackBuffalo.readIeeeAddr(); const status: EmberDeviceUpdate = this.callbackBuffalo.readUInt8(); const policyDecision: EmberJoinDecision = this.callbackBuffalo.readUInt8(); const parentOfNewNodeId: NodeId = this.callbackBuffalo.readUInt16(); this.ezspTrustCenterJoinHandler(newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId); break; } case EzspFrameID.GENERATE_CBKE_KEYS_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); const ephemeralPublicKey: EmberPublicKeyData = this.callbackBuffalo.readEmberPublicKeyData(); this.ezspGenerateCbkeKeysHandler(status, ephemeralPublicKey); break; } case EzspFrameID.CALCULATE_SMACS_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); const initiatorSmac: EmberSmacData = this.callbackBuffalo.readEmberSmacData(); const responderSmac: EmberSmacData = this.callbackBuffalo.readEmberSmacData(); this.ezspCalculateSmacsHandler(status, initiatorSmac, responderSmac); break; } case EzspFrameID.GENERATE_CBKE_KEYS_HANDLER283K1: { const status = this.callbackBuffalo.readStatus(this.version); const ephemeralPublicKey: EmberPublicKey283k1Data = this.callbackBuffalo.readEmberPublicKey283k1Data(); this.ezspGenerateCbkeKeysHandler283k1(status, ephemeralPublicKey); break; } case EzspFrameID.CALCULATE_SMACS_HANDLER283K1: { const status = this.callbackBuffalo.readStatus(this.version); const initiatorSmac: EmberSmacData = this.callbackBuffalo.readEmberSmacData(); const responderSmac: EmberSmacData = this.callbackBuffalo.readEmberSmacData(); this.ezspCalculateSmacsHandler283k1(status, initiatorSmac, responderSmac); break; } case EzspFrameID.DSA_SIGN_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); const messageContents = this.callbackBuffalo.readPayload(); this.ezspDsaSignHandler(status, messageContents); break; } case EzspFrameID.DSA_VERIFY_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); this.ezspDsaVerifyHandler(status); break; } case EzspFrameID.MFGLIB_RX_HANDLER: { const linkQuality = this.callbackBuffalo.readUInt8(); const rssi = this.callbackBuffalo.readInt8(); const packetContents = this.callbackBuffalo.readPayload(); this.ezspMfglibRxHandler(linkQuality, rssi, packetContents); break; } case EzspFrameID.INCOMING_BOOTLOAD_MESSAGE_HANDLER: { if (this.version < 0x0e) { const longId = this.callbackBuffalo.readIeeeAddr(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const packetInfo: EmberRxPacketInfo = { senderShortId: ZSpec.NULL_NODE_ID, senderLongId: ZSpec.BLANK_EUI64, bindingIndex: ZSpec.NULL_BINDING, addressIndex: 0xff, lastHopLqi, lastHopRssi, lastHopTimestamp: 0, }; const messageContents = this.callbackBuffalo.readPayload(); this.ezspIncomingBootloadMessageHandler(longId, packetInfo, messageContents); } else { const longId = this.callbackBuffalo.readIeeeAddr(); const packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); const messageContents = this.callbackBuffalo.readPayload(); this.ezspIncomingBootloadMessageHandler(longId, packetInfo, messageContents); } break; } case EzspFrameID.BOOTLOAD_TRANSMIT_COMPLETE_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); const messageContents = this.callbackBuffalo.readPayload(); this.ezspBootloadTransmitCompleteHandler(status, messageContents); break; } case EzspFrameID.INCOMING_MFG_TEST_MESSAGE_HANDLER: { const messageType = this.callbackBuffalo.readUInt8(); const messageContents = this.callbackBuffalo.readPayload(); this.ezspIncomingMfgTestMessageHandler(messageType, messageContents); break; } case EzspFrameID.ZLL_NETWORK_FOUND_HANDLER: { if (this.version < 0x0e) { const networkInfo = this.callbackBuffalo.readEmberZllNetwork(); const isDeviceInfoNull = this.callbackBuffalo.readUInt8() !== 0; const deviceInfo = this.callbackBuffalo.readEmberZllDeviceInfoRecord(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const packetInfo: EmberRxPacketInfo = { senderShortId: ZSpec.NULL_NODE_ID, senderLongId: ZSpec.BLANK_EUI64, bindingIndex: ZSpec.NULL_BINDING, addressIndex: 0xff, lastHopLqi, lastHopRssi, lastHopTimestamp: 0, }; this.ezspZllNetworkFoundHandler(networkInfo, isDeviceInfoNull, deviceInfo, packetInfo); } else { const networkInfo = this.callbackBuffalo.readEmberZllNetwork(); const isDeviceInfoNull = this.callbackBuffalo.readUInt8() !== 0; const deviceInfo = this.callbackBuffalo.readEmberZllDeviceInfoRecord(); const packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); this.ezspZllNetworkFoundHandler(networkInfo, isDeviceInfoNull, deviceInfo, packetInfo); } break; } case EzspFrameID.ZLL_SCAN_COMPLETE_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); this.ezspZllScanCompleteHandler(status); break; } case EzspFrameID.ZLL_ADDRESS_ASSIGNMENT_HANDLER: { if (this.version < 0x0e) { const addressInfo = this.callbackBuffalo.readEmberZllAddressAssignment(); const lastHopLqi = this.callbackBuffalo.readUInt8(); const lastHopRssi = this.callbackBuffalo.readInt8(); const packetInfo: EmberRxPacketInfo = { senderShortId: ZSpec.NULL_NODE_ID, senderLongId: ZSpec.BLANK_EUI64, bindingIndex: ZSpec.NULL_BINDING, addressIndex: 0xff, lastHopLqi, lastHopRssi, lastHopTimestamp: 0, }; this.ezspZllAddressAssignmentHandler(addressInfo, packetInfo); } else { const addressInfo = this.callbackBuffalo.readEmberZllAddressAssignment(); const packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); this.ezspZllAddressAssignmentHandler(addressInfo, packetInfo); } break; } case EzspFrameID.ZLL_TOUCH_LINK_TARGET_HANDLER: { const networkInfo = this.callbackBuffalo.readEmberZllNetwork(); this.ezspZllTouchLinkTargetHandler(networkInfo); break; } case EzspFrameID.D_GP_SENT_HANDLER: { const status = this.callbackBuffalo.readStatus(this.version); const gpepHandle = this.callbackBuffalo.readUInt8(); this.ezspDGpSentHandler(status, gpepHandle); break; } case EzspFrameID.GPEP_INCOMING_MESSAGE_HANDLER: { const gpStatus = this.callbackBuffalo.readUInt8(); const gpdLink = this.callbackBuffalo.readUInt8(); const sequenceNumber = this.callbackBuffalo.readUInt8(); const addr = this.callbackBuffalo.readEmberGpAddress(); const gpdfSecurityLevel: EmberGpSecurityLevel = this.callbackBuffalo.readUInt8(); const gpdfSecurityKeyType: EmberGpKeyType = this.callbackBuffalo.readUInt8(); const autoCommissioning = this.callbackBuffalo.readUInt8() !== 0; const bidirectionalInfo = this.callbackBuffalo.readUInt8(); const gpdSecurityFrameCounter = this.callbackBuffalo.readUInt32(); const gpdCommandId = this.callbackBuffalo.readUInt8(); const mic = this.callbackBuffalo.readUInt32(); const proxyTableIndex = this.callbackBuffalo.readUInt8(); const gpdCommandPayload = this.callbackBuffalo.readPayload(); let packetInfo: EmberRxPacketInfo; if (this.version < 0x10) { packetInfo = { senderShortId: ZSpec.NULL_NODE_ID, senderLongId: ZSpec.BLANK_EUI64, bindingIndex: ZSpec.NULL_BINDING, addressIndex: 0xff, lastHopLqi: gpdLink, lastHopRssi: 0, lastHopTimestamp: 0, }; } else { packetInfo = this.callbackBuffalo.readEmberRxPacketInfo(); } this.ezspGpepIncomingMessageHandler( gpStatus, gpdLink, sequenceNumber, addr, gpdfSecurityLevel, gpdfSecurityKeyType, autoCommissioning, bidirectionalInfo, gpdSecurityFrameCounter, gpdCommandId, mic, proxyTableIndex, gpdCommandPayload, packetInfo, ); break; } default: logger.debug(`<=x= Ignored unused/unknown ${this.callbackFrameToString}`, NS); } } /** * * @returns uint8_t */ private nextSendSequence(): number { this.sendSequence = ++this.sendSequence & MESSAGE_TAG_MASK; return this.sendSequence; } /** * 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 NodeId 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. * Sequence set in OUT as returned by ezspSend${x} command * @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} cal