diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 63572b9089..f3282c64d3 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -242,8 +242,8 @@ const DEFAULT_STACK_CONFIG: Readonly = { END_DEVICE_POLL_TIMEOUT: 8,// zigpc: 8 TRANSIENT_KEY_TIMEOUT_S: 300,// zigpc: 65535 }; -/** Default behavior is to disable app key requests, so reduce key table to zero as well */ -const DEFAULT_KEY_TABLE_SIZE = 0; +/** Default behavior is to disable app key requests */ +const ALLOW_APP_KEY_REQUESTS = false; /** * NOTE: This from SDK is currently ignored here because of issues in below links: @@ -320,15 +320,13 @@ export class EmberAdapter extends Adapter { this.ezsp = new Ezsp(serialPortOptions); - this.ezsp.on(EzspEvents.STACK_STATUS, this.onStackStatus.bind(this)); - this.ezsp.on(EzspEvents.MESSAGE_SENT, this.onMessageSent.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.STACK_STATUS, this.onStackStatus.bind(this)); this.ezsp.on(EzspEvents.TRUST_CENTER_JOIN, this.onTrustCenterJoin.bind(this)); + this.ezsp.on(EzspEvents.MESSAGE_SENT, this.onMessageSent.bind(this)); + this.ezsp.on(EzspEvents.GREENPOWER_MESSAGE, this.onGreenpowerMessage.bind(this)); } private loadStackConfig(): StackConfig { @@ -535,29 +533,17 @@ export class EmberAdapter extends Adapter { networkAddress: (payload as ZdoTypes.NetworkAddressResponse).nwkAddress, ieeeAddr: (payload as ZdoTypes.NetworkAddressResponse).eui64 } as NetworkAddressPayload); + } else if (apsFrame.clusterId === Zdo.ClusterId.END_DEVICE_ANNOUNCE) { + this.emit(Events.deviceAnnounce, { + networkAddress: (payload as ZdoTypes.EndDeviceAnnounce).nwkAddress, + ieeeAddr: (payload as ZdoTypes.EndDeviceAnnounce).eui64 + } as DeviceAnnouncePayload); } } catch (error) { this.oneWaitress.resolveZDO(sender, apsFrame, error); } } - /** - * Emitted from @see Ezsp.ezspIncomingMessageHandler - * - * @param sender - * @param nodeId - * @param eui64 - * @param macCapFlags - */ - private async onEndDeviceAnnounce(sender: NodeId, apsFrame: EmberApsFrame, payload: ZdoTypes.EndDeviceAnnounce): Promise { - // reduced function device - // if ((payload.capabilities.deviceType === 0)) { - - // } - - this.emit(Events.deviceAnnounce, {networkAddress: payload.nwkAddress, ieeeAddr: payload.eui64} as DeviceAnnouncePayload); - } - /** * Emitted from @see Ezsp.ezspIncomingMessageHandler * @@ -801,8 +787,6 @@ export class EmberAdapter extends Adapter { // 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()); @@ -927,11 +911,11 @@ export class EmberAdapter extends Adapter { + `with status=${SLStatus[status]}.`); } - const appKeyPolicy = DEFAULT_KEY_TABLE_SIZE > 0 ? EzspDecisionId.ALLOW_APP_KEY_REQUESTS : EzspDecisionId.DENY_APP_KEY_REQUESTS; - status = (await this.emberSetEzspPolicy(EzspPolicyId.APP_KEY_REQUEST_POLICY, appKeyPolicy)); + const appKeyRequestsPolicy = ALLOW_APP_KEY_REQUESTS ? EzspDecisionId.ALLOW_APP_KEY_REQUESTS : EzspDecisionId.DENY_APP_KEY_REQUESTS; + status = await this.emberSetEzspPolicy(EzspPolicyId.APP_KEY_REQUEST_POLICY, appKeyRequestsPolicy); if (status !== SLStatus.OK) { - throw new Error(`[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to ${EzspDecisionId[appKeyPolicy]} ` + throw new Error(`[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to ${EzspDecisionId[appKeyRequestsPolicy]} ` + `with status=${SLStatus[status]}.`); } @@ -966,7 +950,6 @@ export class EmberAdapter extends Adapter { const [npStatus, nodeType, netParams] = (await this.ezsp.ezspGetNetworkParameters()); - logger.debug(`[INIT TC] Current network config=${JSON.stringify(this.networkOptions)}`, NS); logger.debug(`[INIT TC] Current adapter network: nodeType=${EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`, NS); if ((npStatus === SLStatus.OK) && (nodeType === EmberNodeType.COORDINATOR) && (this.networkOptions.panID === netParams.panId) @@ -981,8 +964,6 @@ export class EmberAdapter extends Adapter { throw new Error(`[BACKUP] Failed to export Network Key with status=${SLStatus[nkStatus]}.`); } - logger.debug(`[INIT TC] Current adapter network: networkKey=${networkKey.contents.toString('hex')}`, NS); - // config doesn't match adapter anymore if (!networkKey.contents.equals(configNetworkKey)) { action = NetworkInitAction.LEAVE; @@ -1044,13 +1025,13 @@ export class EmberAdapter extends Adapter { logger.info(`[INIT TC] Forming from backup.`, NS); const keyList: LinkKeyBackupData[] = backup.devices.map((device) => { const octets = Array.from(device.ieeeAddress.reverse()); - const key: LinkKeyBackupData = { + + return { deviceEui64: `0x${octets.map(octet => octet.toString(16).padStart(2, '0')).join('')}`, key: {contents: device.linkKey.key}, outgoingFrameCounter: device.linkKey.txCounter, incomingFrameCounter: device.linkKey.rxCounter, }; - return key; }); // before forming @@ -1152,11 +1133,11 @@ export class EmberAdapter extends Adapter { throw new Error(`[INIT FORM] Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`); } - if (!fromBackup && DEFAULT_KEY_TABLE_SIZE > 0) { + if (!fromBackup) { status = await this.ezsp.ezspClearKeyTable(); if (status !== SLStatus.OK) { - throw new Error(`[INIT FORM] Failed to clear key table with status=${SLStatus[status]}.`); + logger.error(`[INIT FORM] Failed to clear key table with status=${SLStatus[status]}.`, NS); } } @@ -1305,12 +1286,9 @@ export class EmberAdapter extends Adapter { let status: SLStatus; 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 { - status = (await this.ezsp.ezspImportLinkKey(i, backupData[i].deviceEui64, backupData[i].key)); - } + // erase any key index not present in backup but available on the NCP + status = (i >= backupData.length) ? await this.ezsp.ezspEraseKeyTableEntry(i) : + await this.ezsp.ezspImportLinkKey(i, backupData[i].deviceEui64, backupData[i].key); if (status !== SLStatus.OK) { throw new Error(`[BACKUP] Failed to ${((i >= backupData.length) ? "erase" : "set")} key table entry at index ${i} ` @@ -1976,17 +1954,7 @@ export class EmberAdapter extends Adapter { throw new Error(`[BACKUP] No network key set.`); } - let keyList: LinkKeyBackupData[] = []; - - if (DEFAULT_KEY_TABLE_SIZE > 0) { - 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() - // )); + const keyList: LinkKeyBackupData[] = ALLOW_APP_KEY_REQUESTS ? await this.exportLinkKeys() : []; let context: SecManContext = initSecurityManagerContext(); context.coreKeyType = SecManKeyType.TC_LINK; diff --git a/src/adapter/ember/ezsp/buffalo.ts b/src/adapter/ember/ezsp/buffalo.ts index 74c1cf2c3e..bdc1b2e718 100644 --- a/src/adapter/ember/ezsp/buffalo.ts +++ b/src/adapter/ember/ezsp/buffalo.ts @@ -499,8 +499,8 @@ export class EzspBuffalo extends Buffalo { } public readSecManNetworkKeyInfo(): SecManNetworkKeyInfo { - const networkKeySet = this.readUInt8() === 1 ? true : false; - const alternateNetworkKeySet = this.readUInt8() === 1 ? true : false; + const networkKeySet = this.readUInt8() !== 0; + const alternateNetworkKeySet = this.readUInt8() !== 0; const networkKeySequenceNumber = this.readUInt8(); const altNetworkKeySequenceNumber = this.readUInt8(); const networkKeyFrameCounter = this.readUInt32(); @@ -632,7 +632,7 @@ export class EzspBuffalo extends Buffalo { const channel = this.readUInt8(); const panId = this.readUInt16(); const extendedPanId = this.readListUInt8(EXTENDED_PAN_ID_SIZE); - const allowingJoin = this.readUInt8(); + const allowingJoin = this.readUInt8() !== 0; const stackProfile = this.readUInt8(); const nwkUpdateId = this.readUInt8(); @@ -650,7 +650,7 @@ export class EzspBuffalo extends Buffalo { this.writeUInt8(value.channel); this.writeUInt16(value.panId); this.writeListUInt8(value.extendedPanId); - this.writeUInt8(value.allowingJoin); + this.writeUInt8(value.allowingJoin ? 1 : 0); this.writeUInt8(value.stackProfile); this.writeUInt8(value.nwkUpdateId); } @@ -1186,9 +1186,9 @@ export class EzspBuffalo extends Buffalo { 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 enhanced = this.readUInt8() !== 0; + const permitJoin = this.readUInt8() !== 0; + const hasCapacity = this.readUInt8() !== 0; const panId = this.readUInt16(); const sender = this.readUInt16(); const extendedPanId = this.readListUInt8(EXTENDED_PAN_ID_SIZE); @@ -1245,9 +1245,9 @@ export class EzspBuffalo extends Buffalo { 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 enhanced = this.readUInt8() !== 0; + const permitJoin = this.readUInt8() !== 0; + const hasCapacity = this.readUInt8() !== 0; const panId = this.readUInt16(); const sender = this.readUInt16(); const extendedPanId = this.readListUInt8(EXTENDED_PAN_ID_SIZE); @@ -1290,8 +1290,8 @@ export class EzspBuffalo extends Buffalo { public readEmberTokenInfo(): EmberTokenInfo { const nvm3Key = this.readUInt32(); - const isCnt = this.readUInt8() === 1 ? true : false; - const isIdx = this.readUInt8() === 1 ? true : false; + const isCnt = this.readUInt8() !== 0; + const isIdx = this.readUInt8() !== 0; const size = this.readUInt8(); const arraySize = this.readUInt8(); diff --git a/src/adapter/ember/ezsp/ezsp.ts b/src/adapter/ember/ezsp/ezsp.ts index 8cd9c482a6..de407429c6 100644 --- a/src/adapter/ember/ezsp/ezsp.ts +++ b/src/adapter/ember/ezsp/ezsp.ts @@ -181,38 +181,61 @@ const ZA_MAX_HOPS = 12; */ const MESSAGE_TAG_MASK = 0x7F; -/* eslint-disable max-len */ export enum EzspEvents { - //-- App logic + //-- An error was detected that requires resetting the NCP. NCP_NEEDS_RESET_AND_INIT = 'NCP_NEEDS_RESET_AND_INIT', //-- ezspIncomingMessageHandler - /** params => apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer */ ZDO_RESPONSE = 'ZDO_RESPONSE', - /** params => type: EmberIncomingMessageType, apsFrame: EmberApsFrame, lastHopLqi: number, sender: NodeId, messageContents: Buffer */ INCOMING_MESSAGE = 'INCOMING_MESSAGE', - /** params => sourcePanId: PanId, sourceAddress: EUI64, groupId: number | null, lastHopLqi: number, messageContents: Buffer */ + //-- ezspMacFilterMatchMessageHandler TOUCHLINK_MESSAGE = 'TOUCHLINK_MESSAGE', - /** params => sender: NodeId, apsFrame: EmberApsFrame, payload: EndDeviceAnnouncePayload */ - END_DEVICE_ANNOUNCE = 'END_DEVICE_ANNOUNCE', - //-- ezspStackStatusHandler - /** params => status: SLStatus */ STACK_STATUS = 'STACK_STATUS', - //-- ezspTrustCenterJoinHandler - /** params => newNodeId: NodeId, newNodeEui64: EUI64, status: EmberDeviceUpdate, policyDecision: EmberJoinDecision, parentOfNewNodeId: NodeId */ TRUST_CENTER_JOIN = 'TRUST_CENTER_JOIN', - //-- ezspMessageSentHandler - /** params => status: SLStatus, type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number */ MESSAGE_SENT = 'MESSAGE_SENT', - //-- ezspGpepIncomingMessageHandler - /** params => sequenceNumber: number, commandIdentifier: number, sourceId: number, frameCounter: number, gpdCommandId: number, gpdCommandPayload: Buffer, gpdLink: number */ GREENPOWER_MESSAGE = 'GREENPOWER_MESSAGE', } -/* eslint-enable max-len */ + +interface EzspEventMap { + [EzspEvents.NCP_NEEDS_RESET_AND_INIT]: [status: EzspStatus], + [EzspEvents.ZDO_RESPONSE]: [apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer], + [EzspEvents.INCOMING_MESSAGE]: [ + type: EmberIncomingMessageType, + apsFrame: EmberApsFrame, + lastHopLqi: number, + sender: NodeId, + messageContents: Buffer + ], + [EzspEvents.TOUCHLINK_MESSAGE]: [sourcePanId: PanId, sourceAddress: EUI64, groupId: number | null, lastHopLqi: number, messageContents: Buffer], + [EzspEvents.STACK_STATUS]: [status: SLStatus], + [EzspEvents.TRUST_CENTER_JOIN]: [ + newNodeId: NodeId, + newNodeEui64: EUI64, + status: EmberDeviceUpdate, + policyDecision: EmberJoinDecision, + parentOfNewNodeId: NodeId + ], + [EzspEvents.MESSAGE_SENT]: [ + status: SLStatus, + type: EmberOutgoingMessageType, + indexOrDestination: number, + apsFrame: EmberApsFrame, + messageTag: number + ], + [EzspEvents.GREENPOWER_MESSAGE]: [ + sequenceNumber: number, + commandIdentifier: number, + sourceId: number, + frameCounter: number, + gpdCommandId: number, + gpdCommandPayload: Buffer, + gpdLink: number + ], +} /** * Host EZSP layer. @@ -224,10 +247,8 @@ export enum EzspEvents { * 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 'NCP_NEEDS_RESET_AND_INIT(EzspStatus)' An error was detected that requires resetting the NCP. */ -export class Ezsp extends EventEmitter { +export class Ezsp extends EventEmitter { private version: number; private readonly tickInterval: number; public readonly ash: UartAsh; @@ -249,10 +270,8 @@ export class Ezsp extends EventEmitter { 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; + /** Awaiting response resolve/timer struct. Null if not waiting for response. */ + private responseWaiter: EzspWaiter | null; /** Counter for Queue Full errors */ public counterErrQueueFull: number; @@ -294,7 +313,7 @@ export class Ezsp extends EventEmitter { } private initVariables(): void { - if (this.waitingForResponse) { + if (this.responseWaiter != null) { clearTimeout(this.responseWaiter.timer); } @@ -311,7 +330,6 @@ export class Ezsp extends EventEmitter { this.sendingCommand = false; this.frameSequence = -1;// start at 0 this.sendSequence = 0;// start at 1 - this.waitingForResponse = false; this.responseWaiter = null; this.counterErrQueueFull = 0; } @@ -402,7 +420,8 @@ export class Ezsp extends EventEmitter { // logger.debug(`<<<< ${buffer.data.subarray(0, buffer.len).toString('hex')}`, NS); if (buffer.data[EZSP_FRAME_CONTROL_INDEX] & EZSP_FRAME_CONTROL_ASYNCH_CB) { - buffer.data.copy(this.callbackFrameContents, 0, 0, buffer.len);// take only what len tells us is actual content + // take only what len tells us is actual content + buffer.data.copy(this.callbackFrameContents, 0, 0, buffer.len); this.callbackFrameLength = buffer.len; @@ -418,18 +437,25 @@ export class Ezsp extends EventEmitter { logger.debug(`<=x= ${this.callbackFrameToString} Invalid, status=${EzspStatus[status]}.`, NS); } } else { - buffer.data.copy(this.frameContents, 0, 0, buffer.len);// take only what len tells us is actual content + // 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); + this.ash.rxFree.freeBuffer(buffer);// always - const status = this.validateReceivedFrame(this.buffalo); + if (this.responseWaiter != null) { + const status = this.validateReceivedFrame(this.buffalo); - clearTimeout(this.responseWaiter.timer); - this.responseWaiter.resolve(status); + clearTimeout(this.responseWaiter.timer); + this.responseWaiter.resolve(status); + + this.responseWaiter = null;// done, gc + } else { + logger.debug(`Received response while not expecting one. Ignoring.`, NS); + } } } @@ -552,7 +578,6 @@ export class Ezsp extends EventEmitter { } this.frameLength = length; - let status: EzspStatus; logger.debug(`===> ${this.frameToString}`, NS); @@ -562,13 +587,11 @@ export class Ezsp extends EventEmitter { const sendStatus = (this.ash.send(this.frameLength, this.frameContents)); if (sendStatus !== EzspStatus.SUCCESS) { - reject(new EzspError(sendStatus)); + return reject(new EzspError(sendStatus)); } this.responseWaiter = { - timer: setTimeout(() => { - reject(new EzspError(EzspStatus.ASH_ERROR_TIMEOUTS)); - }, this.ash.responseTimeout), + timer: setTimeout(() => reject(new EzspError(EzspStatus.ASH_ERROR_TIMEOUTS)), this.ash.responseTimeout), resolve, }; }); @@ -577,6 +600,8 @@ export class Ezsp extends EventEmitter { throw new EzspError(status); } } catch (error) { + this.responseWaiter = null; + logger.debug(`=x=> ${this.frameToString} ${error}`, NS); this.ezspErrorHandler(status); diff --git a/src/adapter/ember/types.ts b/src/adapter/ember/types.ts index c40c07120e..fcc255f959 100644 --- a/src/adapter/ember/types.ts +++ b/src/adapter/ember/types.ts @@ -559,7 +559,7 @@ export type EmberZigbeeNetwork = { /** uint8_t */ channel: number, /** bool */ - allowingJoin: number, + allowingJoin: boolean, /** uint8_t[EXTENDED_PAN_ID_SIZE] */ extendedPanId: ExtendedPanId, /** uint8_t */ diff --git a/src/adapter/ember/uart/ash.ts b/src/adapter/ember/uart/ash.ts index 0058d671a5..a48b07b328 100644 --- a/src/adapter/ember/uart/ash.ts +++ b/src/adapter/ember/uart/ash.ts @@ -59,14 +59,6 @@ const ashGetFrmNum = (ctrl: number): number => ((ctrl & ASH_FRMNUM_MASK) >> ASH_ /** ASH get acknum in control byte */ const ashGetACKNum = (ctrl: number): number => ((ctrl & ASH_ACKNUM_MASK) >> ASH_ACKNUM_BIT); - -export enum AshEvents { - /** When the ASH protocol detects a fatal error (bubbles up to restart adapter). */ - FATAL_ERROR = 'fatalError', - /** When a frame has been parsed and queued in the rxQueue. */ - FRAME = 'frame', -} - type UartAshCounters = { /** DATA frame data fields bytes transmitted */ txData: number, @@ -159,7 +151,6 @@ enum Flag { NRTX = 0x200, } - /** max frames sent without being ACKed (1-7) */ const CONFIG_TX_K = 3; /** enables randomizing DATA frame payloads */ @@ -183,10 +174,22 @@ const CONFIG_NR_TIME = 480; /** Read/write max bytes count at stream level */ const CONFIG_HIGHWATER_MARK = 256; +export enum AshEvents { + /** When the ASH protocol detects a fatal error (bubbles up to restart adapter). */ + FATAL_ERROR = 'fatalError', + /** When a frame has been parsed and queued in the rxQueue. */ + FRAME = 'frame', +} + +interface UartAshEventMap { + [AshEvents.FATAL_ERROR]: [status: EzspStatus]; + [AshEvents.FRAME]: []; +} + /** * ASH Protocol handler. */ -export class UartAsh extends EventEmitter { +export class UartAsh extends EventEmitter { private readonly portOptions: SerialPortOptions; private serialPort: SerialPort; private socketPort: Socket; @@ -1179,7 +1182,7 @@ export class UartAsh extends EventEmitter { this.counters.rxData += this.rxDataBuffer.len; - setImmediate(this.emit.bind(this, AshEvents.FRAME)); + setImmediate(() => this.emit(AshEvents.FRAME)); return EzspStatus.SUCCESS; } else { // frame is out of sequence