From bef46559a93a5218a50868cd0bb1692ae2f76383 Mon Sep 17 00:00:00 2001 From: Ilya Kirov Date: Tue, 27 Aug 2024 21:37:16 +0300 Subject: [PATCH] feat: ZBOSS adapter for nRF ZBOSS NCP (#1165) * zboss step 1 * step 2 first parsing * calc crc * ... commands * ... commands * ... commands * ... commands * ... commands * check crc and ack * reset * step 2 * step 3 * step 4 * reset on start * tc policy * step 5 * Some change parsing * code style for lint * getNetworkParameters * Forming network * active endpoints * working pairing interview * remove, bind, unbind * lint fixes * lqi * cleanup * fix handle empty package * Autodetect path * Adapt code * fix * prettier * lint * fix test * fix coverage --- src/adapter/adapter.ts | 18 +- src/adapter/tstype.ts | 2 +- src/adapter/zboss/adapter/index.ts | 3 + src/adapter/zboss/adapter/zbossAdapter.ts | 553 +++++++++++ src/adapter/zboss/commands.ts | 1104 +++++++++++++++++++++ src/adapter/zboss/consts.ts | 9 + src/adapter/zboss/driver.ts | 436 ++++++++ src/adapter/zboss/enums.ts | 328 ++++++ src/adapter/zboss/frame.ts | 175 ++++ src/adapter/zboss/reader.ts | 64 ++ src/adapter/zboss/types.ts | 0 src/adapter/zboss/uart.ts | 417 ++++++++ src/adapter/zboss/utils.ts | 58 ++ src/adapter/zboss/writer.ts | 49 + test/controller.test.ts | 2 +- 15 files changed, 3214 insertions(+), 4 deletions(-) create mode 100644 src/adapter/zboss/adapter/index.ts create mode 100644 src/adapter/zboss/adapter/zbossAdapter.ts create mode 100644 src/adapter/zboss/commands.ts create mode 100644 src/adapter/zboss/consts.ts create mode 100644 src/adapter/zboss/driver.ts create mode 100644 src/adapter/zboss/enums.ts create mode 100644 src/adapter/zboss/frame.ts create mode 100644 src/adapter/zboss/reader.ts create mode 100644 src/adapter/zboss/types.ts create mode 100644 src/adapter/zboss/uart.ts create mode 100644 src/adapter/zboss/utils.ts create mode 100644 src/adapter/zboss/writer.ts diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 090d61ef03..1c9bfdc253 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -55,10 +55,24 @@ abstract class Adapter extends events.EventEmitter { 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 EmberAdapter; + const {ZBOSSAdapter} = await import('./zboss/adapter'); + type AdapterImplementation = + | typeof ZStackAdapter + | typeof DeconzAdapter + | typeof ZiGateAdapter + | typeof EZSPAdapter + | typeof EmberAdapter + | typeof ZBOSSAdapter; let adapters: AdapterImplementation[]; - const adapterLookup = {zstack: ZStackAdapter, deconz: DeconzAdapter, zigate: ZiGateAdapter, ezsp: EZSPAdapter, ember: EmberAdapter}; + const adapterLookup = { + zstack: ZStackAdapter, + deconz: DeconzAdapter, + zigate: ZiGateAdapter, + ezsp: EZSPAdapter, + ember: EmberAdapter, + zboss: ZBOSSAdapter, + }; if (serialPortOptions.adapter && serialPortOptions.adapter !== 'auto') { if (adapterLookup[serialPortOptions.adapter]) { diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index bdddbb4bca..1363ca1d5a 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' | 'ember' | 'auto'; + adapter?: 'zstack' | 'deconz' | 'zigate' | 'ezsp' | 'ember' | 'zboss' | 'auto'; } interface AdapterOptions { diff --git a/src/adapter/zboss/adapter/index.ts b/src/adapter/zboss/adapter/index.ts new file mode 100644 index 0000000000..c61df0e2e7 --- /dev/null +++ b/src/adapter/zboss/adapter/index.ts @@ -0,0 +1,3 @@ +import {ZBOSSAdapter} from './zbossAdapter'; + +export {ZBOSSAdapter}; diff --git a/src/adapter/zboss/adapter/zbossAdapter.ts b/src/adapter/zboss/adapter/zbossAdapter.ts new file mode 100644 index 0000000000..74e647593d --- /dev/null +++ b/src/adapter/zboss/adapter/zbossAdapter.ts @@ -0,0 +1,553 @@ +/* istanbul ignore file */ + +import {Adapter, TsType} from '../..'; +import {Backup} from '../../../models'; +import {Queue, RealpathSync, Waitress} from '../../../utils'; +import {logger} from '../../../utils/logger'; +import {BroadcastAddress} from '../../../zspec/enums'; +import * as Zcl from '../../../zspec/zcl'; +import {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from '../../events'; +import SerialPortUtils from '../../serialPortUtils'; +import SocketPortUtils from '../../socketPortUtils'; +import {Coordinator, LQI, LQINeighbor} from '../../tstype'; +import {ZBOSSDriver} from '../driver'; +import {CommandId, DeviceUpdateStatus} from '../enums'; +import {FrameType, ZBOSSFrame} from '../frame'; + +const NS = 'zh:zboss'; + +const autoDetectDefinitions = [ + // Nordic Zigbee NCP + {manufacturer: 'ZEPHYR', vendorId: '2fe3', productId: '0100'}, +]; + +interface WaitressMatcher { + address: number | string; + endpoint: number; + transactionSequenceNumber?: number; + clusterID: number; + commandIdentifier: number; +} + +export class ZBOSSAdapter extends Adapter { + private queue: Queue; + private readonly driver: ZBOSSDriver; + private waitress: Waitress; + public coordinator?: Coordinator; + + constructor( + networkOptions: TsType.NetworkOptions, + serialPortOptions: TsType.SerialPortOptions, + backupPath: string, + adapterOptions: TsType.AdapterOptions, + ) { + super(networkOptions, serialPortOptions, backupPath, adapterOptions); + const concurrent = adapterOptions && adapterOptions.concurrent ? adapterOptions.concurrent : 8; + logger.debug(`Adapter concurrent: ${concurrent}`, NS); + this.queue = new Queue(concurrent); + + this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); + this.driver = new ZBOSSDriver(serialPortOptions, networkOptions); + this.driver.on('frame', this.processMessage.bind(this)); + } + + private async processMessage(frame: ZBOSSFrame): Promise { + logger.debug(`processMessage: ${JSON.stringify(frame)}`, NS); + if ( + frame.type == FrameType.INDICATION && + frame.commandId == CommandId.ZDO_DEV_UPDATE_IND && + frame.payload.status == DeviceUpdateStatus.LEFT + ) { + logger.debug(`Device left network request received: ${frame.payload.nwk} ${frame.payload.ieee}`, NS); + const payload: DeviceLeavePayload = { + networkAddress: frame.payload.nwk, + ieeeAddr: frame.payload.ieee, + }; + + this.emit('deviceLeave', payload); + } + if (frame.type == FrameType.INDICATION && frame.commandId == CommandId.NWK_LEAVE_IND) { + logger.debug(`Device left network request received from ${frame.payload.ieee}`, NS); + const payload: DeviceLeavePayload = { + networkAddress: frame.payload.nwk, + ieeeAddr: frame.payload.ieee, + }; + + this.emit('deviceLeave', payload); + } + if (frame.type == FrameType.INDICATION && frame.commandId == CommandId.ZDO_DEV_ANNCE_IND) { + logger.debug(`Device join request received: ${frame.payload.nwk} ${frame.payload.ieee}`, NS); + const payload: DeviceJoinedPayload = { + networkAddress: frame.payload.nwk, + ieeeAddr: frame.payload.ieee, + }; + + this.emit('deviceJoined', payload); + } + + if (frame.type == FrameType.INDICATION && frame.commandId == CommandId.APSDE_DATA_IND) { + logger.debug(`ZCL frame received from ${frame.payload.srcNwk} ${frame.payload.srcEndpoint}`, NS); + const payload: ZclPayload = { + clusterID: frame.payload.clusterID, + header: Zcl.Header.fromBuffer(frame.payload.data), + data: frame.payload.data, + address: frame.payload.srcNwk, + endpoint: frame.payload.srcEndpoint, + linkquality: frame.payload.lqi, + groupID: frame.payload.grpNwk, + wasBroadcast: false, + destinationEndpoint: frame.payload.dstEndpoint, + }; + + this.waitress.resolve(payload); + this.emit('zclPayload', payload); + } + //this.emit('event', frame); + } + + 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) { + logger.debug(`Failed to determine if path is valid: '${error}'`, NS); + 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 { + logger.info(`ZBOSS Adapter starting`, NS); + + await this.driver.connect(); + + return this.driver.startup(); + } + + public async stop(): Promise { + await this.driver.stop(); + + logger.info(`ZBOSS Adapter stopped`, NS); + } + + public async getCoordinator(): Promise { + return this.queue.execute(async () => { + const info = await this.driver.getCoordinator(); + logger.debug(`ZBOSS Adapter Coordinator description:\n${JSON.stringify(info)}`, NS); + this.coordinator = { + networkAddress: info.networkAddress, + manufacturerID: 0, + ieeeAddr: info.ieeeAddr, + endpoints: info.endpoints, + }; + + return this.coordinator; + }); + } + + public async getCoordinatorVersion(): Promise { + return this.driver.getCoordinatorVersion(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async reset(type: 'soft' | 'hard'): Promise { + return Promise.reject(new Error('Not supported')); + } + + public async supportsBackup(): Promise { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async backup(ieeeAddressesInDatabase: string[]): Promise { + throw new Error('This adapter does not support backup'); + } + + public async getNetworkParameters(): Promise { + return this.queue.execute(async () => { + const channel = this.driver.netInfo!.network.channel; + const panID = this.driver.netInfo!.network.panID!; + const extendedPanID = this.driver.netInfo!.network.extendedPanID; + + return { + panID, + extendedPanID: parseInt(Buffer.from(extendedPanID).toString('hex'), 16), + channel, + }; + }); + } + + public async supportsChangeChannel(): Promise { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async changeChannel(newChannel: number): Promise { + throw new Error(`Channel change is not supported for 'zboss' yet`); + } + + public async setTransmitPower(value: number): Promise { + if (this.driver.isInitialized()) { + return this.queue.execute(async () => { + await this.driver.setTXPower(value); + }); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async addInstallCode(ieeeAddress: string, key: Buffer): Promise { + throw new Error(`Install code is not supported for 'zboss' yet`); + } + + public async permitJoin(seconds: number, networkAddress: number): Promise { + if (this.driver.isInitialized()) { + return this.queue.execute(async () => { + await this.driver.permitJoin(networkAddress, seconds); + if (!networkAddress) { + // send broadcast permit + await this.driver.permitJoin(0xfffc, seconds); + } + }); + } + } + + public async lqi(networkAddress: number): Promise { + return this.queue.execute(async (): Promise => { + const neighbors: LQINeighbor[] = []; + + const request = async (startIndex: number): Promise => { + try { + const result = await this.driver.lqi(networkAddress, startIndex); + + return result; + } catch (error) { + throw new Error(`LQI for '${networkAddress}' failed: ${error}`); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const add = (list: any): void => { + for (const entry of list) { + neighbors.push({ + linkquality: entry.lqi, + networkAddress: entry.nwk, + ieeeAddr: entry.ieee, + relationship: (entry.relationship >> 4) & 0x7, + depth: entry.depth, + }); + } + }; + + let response = (await request(0)).payload; + add(response.neighbors); + const size = response.entries; + let nextStartIndex = response.neighbors.length; + + while (neighbors.length < size) { + response = await request(nextStartIndex); + add(response.neighbors); + nextStartIndex += response.neighbors.length; + } + + return {neighbors}; + }, networkAddress); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async routingTable(networkAddress: number): Promise { + throw new Error(`Routing table is not supported for 'zboss' yet`); + } + + public async nodeDescriptor(networkAddress: number): Promise { + return this.queue.execute(async () => { + try { + logger.debug(`Requesting 'Node Descriptor' for '${networkAddress}'`, NS); + const descriptor = await this.driver.nodeDescriptor(networkAddress); + const logicaltype = descriptor.payload.flags & 0x07; + return { + manufacturerCode: descriptor.payload.manufacturerCode, + type: logicaltype == 0 ? 'Coordinator' : logicaltype == 1 ? 'Router' : 'EndDevice', + }; + } catch (error) { + logger.debug(`Node descriptor request for '${networkAddress}' failed (${error}), retry`, NS); + throw error; + } + }); + } + + public async activeEndpoints(networkAddress: number): Promise { + logger.debug(`Requesting 'Active endpoints' for '${networkAddress}'`, NS); + return this.queue.execute(async () => { + const endpoints = await this.driver.activeEndpoints(networkAddress); + return {endpoints: [...endpoints.payload.endpoints]}; + }, networkAddress); + } + + public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { + logger.debug(`Requesting 'Simple Descriptor' for '${networkAddress}' endpoint ${endpointID}`, NS); + return this.queue.execute(async () => { + const sd = await this.driver.simpleDescriptor(networkAddress, endpointID); + return { + profileID: sd.payload.profileID, + endpointID: sd.payload.endpoint, + deviceID: sd.payload.deviceID, + inputClusters: sd.payload.inputClusters, + outputClusters: sd.payload.outputClusters, + }; + }, networkAddress); + } + + public async bind( + destinationNetworkAddress: number, + sourceIeeeAddress: string, + sourceEndpoint: number, + clusterID: number, + destinationAddressOrGroup: string | number, + type: 'endpoint' | 'group', + destinationEndpoint?: number, + ): Promise { + return this.queue.execute(async () => { + await this.driver.bind( + destinationNetworkAddress, + sourceIeeeAddress, + sourceEndpoint, + clusterID, + destinationAddressOrGroup, + type, + destinationEndpoint, + ); + }, destinationNetworkAddress); + } + + public async unbind( + destinationNetworkAddress: number, + sourceIeeeAddress: string, + sourceEndpoint: number, + clusterID: number, + destinationAddressOrGroup: string | number, + type: 'endpoint' | 'group', + destinationEndpoint: number, + ): Promise { + return this.queue.execute(async () => { + await this.driver.unbind( + destinationNetworkAddress, + sourceIeeeAddress, + sourceEndpoint, + clusterID, + destinationAddressOrGroup, + type, + destinationEndpoint, + ); + }, destinationNetworkAddress); + } + + public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { + return this.queue.execute(async () => { + await this.driver.removeDevice(networkAddress, ieeeAddr); + }, networkAddress); + } + + public async sendZclFrameToEndpoint( + ieeeAddr: string, + networkAddress: number, + endpoint: number, + zclFrame: Zcl.Frame, + timeout: number, + disableResponse: boolean, + disableRecovery: boolean, + sourceEndpoint?: number, + ): Promise { + return this.queue.execute(async () => { + return this.sendZclFrameToEndpointInternal( + ieeeAddr, + networkAddress, + endpoint, + sourceEndpoint || 1, + zclFrame, + timeout, + disableResponse, + disableRecovery, + 0, + 0, + false, + false, + false, + null, + ); + }, networkAddress); + } + + private async sendZclFrameToEndpointInternal( + ieeeAddr: string, + networkAddress: number, + endpoint: number, + sourceEndpoint: number, + zclFrame: Zcl.Frame, + timeout: number, + disableResponse: boolean, + disableRecovery: boolean, + responseAttempt: number, + dataRequestAttempt: number, + checkedNetworkAddress: boolean, + discoveredRoute: boolean, + assocRemove: boolean, + assocRestore: {ieeeadr: string; nwkaddr: number; noderelation: number} | null, + ): Promise { + if (ieeeAddr == null) { + ieeeAddr = this.coordinator!.ieeeAddr; + } + logger.debug( + `sendZclFrameToEndpointInternal ${ieeeAddr}:${networkAddress}/${endpoint} ` + + `(${responseAttempt},${dataRequestAttempt},${this.queue.count()}), timeout=${timeout}`, + NS, + ); + let response = null; + const command = zclFrame.command; + if (command.response && disableResponse === false) { + response = this.waitFor( + networkAddress, + endpoint, + zclFrame.header.transactionSequenceNumber, + zclFrame.cluster.ID, + command.response!, + timeout, + ); + } else if (!zclFrame.header.frameControl.disableDefaultResponse) { + response = this.waitFor( + networkAddress, + endpoint, + zclFrame.header.transactionSequenceNumber, + zclFrame.cluster.ID, + Zcl.Foundation.defaultRsp.ID, + timeout, + ); + } + + const dataConfirmResult = await this.driver.request( + ieeeAddr, + 0x0104, + zclFrame.cluster.ID, + endpoint, + sourceEndpoint || 0x01, + zclFrame.toBuffer(), + ); + if (!dataConfirmResult) { + if (response != null) { + response.cancel(); + } + throw Error('sendZclFrameToEndpointInternal error'); + } + if (response !== null) { + try { + const result = await response.start().promise; + return result; + } catch (error) { + logger.debug(`Response timeout (${ieeeAddr}:${networkAddress},${responseAttempt})`, NS); + if (responseAttempt < 1 && !disableRecovery) { + return this.sendZclFrameToEndpointInternal( + ieeeAddr, + networkAddress, + endpoint, + sourceEndpoint, + zclFrame, + timeout, + disableResponse, + disableRecovery, + responseAttempt + 1, + dataRequestAttempt, + checkedNetworkAddress, + discoveredRoute, + assocRemove, + assocRestore, + ); + } else { + throw error; + } + } + } else { + return; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame, sourceEndpoint?: number): Promise { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async setChannelInterPAN(channel: number): Promise { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddress: string): Promise { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { + throw new Error(`Is not supported for 'zboss' yet`); + } + + public async restoreChannelInterPAN(): Promise { + return; + } + + public waitFor( + networkAddress: number, + endpoint: number, + // frameType: Zcl.FrameType, + // direction: Zcl.Direction, + transactionSequenceNumber: number, + clusterID: number, + commandIdentifier: number, + timeout: number, + ): {promise: Promise; cancel: () => void; start: () => {promise: Promise}} { + const payload = { + address: networkAddress, + endpoint, + clusterID, + commandIdentifier, + transactionSequenceNumber, + }; + + const waiter = this.waitress.waitFor(payload, timeout); + const cancel = (): void => this.waitress.remove(waiter.ID); + + return {cancel: cancel, promise: waiter.start().promise, start: waiter.start}; + } + + private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string { + return ( + `Timeout - ${matcher.address} - ${matcher.endpoint}` + + ` - ${matcher.transactionSequenceNumber} - ${matcher.clusterID}` + + ` - ${matcher.commandIdentifier} after ${timeout}ms` + ); + } + + private waitressValidator(payload: ZclPayload, matcher: WaitressMatcher): boolean { + return ( + (payload.header && + (!matcher.address || payload.address === matcher.address) && + payload.endpoint === matcher.endpoint && + (!matcher.transactionSequenceNumber || payload.header.transactionSequenceNumber === matcher.transactionSequenceNumber) && + payload.clusterID === matcher.clusterID && + matcher.commandIdentifier === payload.header.commandIdentifier) || + false + ); + } +} diff --git a/src/adapter/zboss/commands.ts b/src/adapter/zboss/commands.ts new file mode 100644 index 0000000000..9c780b18c5 --- /dev/null +++ b/src/adapter/zboss/commands.ts @@ -0,0 +1,1104 @@ +/* istanbul ignore file */ + +import {BuffaloZclDataType, DataType} from '../../zspec/zcl/definition/enums'; +import { + BuffaloZBOSSDataType, + CommandId, + DeviceType, + DeviceUpdateStatus, + PolicyType, + ResetOptions, + ResetSource, + StatusCategory, + StatusCodeAPS, + StatusCodeCBKE, + StatusCodeGeneric, +} from './enums'; + +export interface ParamsDesc { + name: string; + type: DataType | BuffaloZclDataType | BuffaloZBOSSDataType; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ + condition?: (payload: any, buffalo: any) => boolean; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ + typed?: any; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ + options?: (payload: any, options: any) => void; +} + +interface ZBOSSFrameDesc { + request: ParamsDesc[]; + response: ParamsDesc[]; + indication?: ParamsDesc[]; +} + +const commonResponse = [ + {name: 'category', type: DataType.UINT8, typed: StatusCategory}, + {name: 'status', type: DataType.UINT8, typed: [StatusCodeGeneric, StatusCodeAPS, StatusCodeCBKE]}, +]; + +export const FRAMES: {[key in CommandId]?: ZBOSSFrameDesc} = { + // ------------------------------------------ + // NCP config + // ------------------------------------------ + + // Requests firmware, stack and protocol versions from NCP + [CommandId.GET_MODULE_VERSION]: { + request: [], + response: [ + ...commonResponse, + {name: 'fwVersion', type: DataType.UINT32}, + {name: 'stackVersion', type: DataType.UINT32}, + {name: 'protocolVersion', type: DataType.UINT32}, + ], + }, + // Force NCP module reboot + [CommandId.NCP_RESET]: { + request: [{name: 'options', type: DataType.UINT8, typed: ResetOptions}], + response: [...commonResponse], + }, + // Requests current Zigbee role of the local device + [CommandId.GET_ZIGBEE_ROLE]: { + request: [], + response: [...commonResponse, {name: 'role', type: DataType.UINT8, typed: DeviceType}], + }, + // Set Zigbee role of the local device + [CommandId.SET_ZIGBEE_ROLE]: { + request: [{name: 'role', type: DataType.UINT8, typed: DeviceType}], + response: [...commonResponse], + }, + // Get Zigbee channels page and mask of the local device + [CommandId.GET_ZIGBEE_CHANNEL_MASK]: { + request: [], + response: [ + ...commonResponse, + {name: 'len', type: DataType.UINT8}, + { + name: 'channels', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'page', type: DataType.UINT8}, + {name: 'mask', type: DataType.UINT32}, + ], + }, + ], + }, + // Set Zigbee channels page and mask + [CommandId.SET_ZIGBEE_CHANNEL_MASK]: { + request: [ + {name: 'page', type: DataType.UINT8}, + {name: 'mask', type: DataType.UINT32}, + ], + response: [...commonResponse], + }, + // Get Zigbee channel + [CommandId.GET_ZIGBEE_CHANNEL]: { + request: [], + response: [...commonResponse, {name: 'page', type: DataType.UINT8}, {name: 'channel', type: DataType.UINT8}], + }, + // Requests current short PAN ID + [CommandId.GET_PAN_ID]: { + request: [], + response: [...commonResponse, {name: 'panID', type: DataType.UINT16}], + }, + // Set short PAN ID + [CommandId.SET_PAN_ID]: { + request: [{name: 'panID', type: DataType.UINT16}], + response: [...commonResponse], + }, + // Requests local IEEE address + [CommandId.GET_LOCAL_IEEE_ADDR]: { + request: [{name: 'mac', type: DataType.UINT8}], + response: [...commonResponse, {name: 'mac', type: DataType.UINT8}, {name: 'ieee', type: DataType.IEEE_ADDR}], + }, + // Set local IEEE address + [CommandId.SET_LOCAL_IEEE_ADDR]: { + request: [ + {name: 'mac', type: DataType.UINT8}, + {name: 'ieee', type: DataType.IEEE_ADDR}, + ], + response: [...commonResponse], + }, + // Get Transmit Power + [CommandId.GET_TX_POWER]: { + request: [], + response: [...commonResponse, {name: 'txPower', type: DataType.UINT8}], + }, + // Set Transmit Power + [CommandId.SET_TX_POWER]: { + request: [{name: 'txPower', type: DataType.UINT8}], + response: [...commonResponse, {name: 'txPower', type: DataType.UINT8}], + }, + // Requests RxOnWhenIdle PIB attribute + [CommandId.GET_RX_ON_WHEN_IDLE]: { + request: [], + response: [...commonResponse, {name: 'rxOn', type: DataType.UINT8}], + }, + // Sets Rx On When Idle PIB attribute + [CommandId.SET_RX_ON_WHEN_IDLE]: { + request: [{name: 'rxOn', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Requests current join status of the device + [CommandId.GET_JOINED]: { + request: [], + response: [...commonResponse, {name: 'joined', type: DataType.UINT8}], + }, + // Requests current authentication status of the device + [CommandId.GET_AUTHENTICATED]: { + request: [], + response: [...commonResponse, {name: 'authenticated', type: DataType.UINT8}], + }, + // Requests current End Device timeout + [CommandId.GET_ED_TIMEOUT]: { + request: [], + response: [...commonResponse, {name: 'timeout', type: DataType.UINT8}], + }, + // Sets End Device timeout + [CommandId.SET_ED_TIMEOUT]: { + request: [{name: 'timeout', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Set NWK Key + [CommandId.SET_NWK_KEY]: { + request: [ + {name: 'nwkKey', type: DataType.SEC_KEY}, + {name: 'index', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Get list of NWK keys + [CommandId.GET_NWK_KEYS]: { + request: [], + response: [ + ...commonResponse, + {name: 'nwkKey1', type: DataType.SEC_KEY}, + {name: 'index1', type: DataType.UINT8}, + {name: 'nwkKey2', type: DataType.SEC_KEY}, + {name: 'index2', type: DataType.UINT8}, + {name: 'nwkKey3', type: DataType.SEC_KEY}, + {name: 'index3', type: DataType.UINT8}, + ], + }, + // Get APS key by IEEE + [CommandId.GET_APS_KEY_BY_IEEE]: { + request: [{name: 'ieee', type: DataType.IEEE_ADDR}], + response: [...commonResponse, {name: 'apsKey', type: DataType.SEC_KEY}], + }, + // Get Parent short address + [CommandId.GET_PARENT_ADDRESS]: { + request: [], + response: [...commonResponse, {name: 'parent', type: DataType.UINT16}], + }, + // Get Extended Pan ID + [CommandId.GET_EXTENDED_PAN_ID]: { + request: [], + response: [...commonResponse, {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}], + }, + // Get Coordinator version + [CommandId.GET_COORDINATOR_VERSION]: { + request: [], + response: [...commonResponse, {name: 'version', type: DataType.UINT8}], + }, + // Get Short Address of the device + [CommandId.GET_SHORT_ADDRESS]: { + request: [], + response: [...commonResponse, {name: 'nwk', type: DataType.UINT16}], + }, + // Get Trust Center IEEE Address + [CommandId.GET_TRUST_CENTER_ADDRESS]: { + request: [], + response: [...commonResponse, {name: 'ieee', type: DataType.IEEE_ADDR}], + }, + // Device Reset Indication with reset source + [CommandId.NCP_RESET_IND]: { + request: [], + response: [...commonResponse], + indication: [{name: 'source', type: DataType.UINT8, typed: ResetSource}], + }, + // Writes NVRAM datasets + [CommandId.NVRAM_WRITE]: { + request: [ + {name: 'len', type: DataType.UINT8}, + {name: 'data', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, + ], + response: [...commonResponse], + }, + // Reads an NVRAM dataset + [CommandId.NVRAM_READ]: { + request: [{name: 'type', type: DataType.UINT8}], + response: [ + ...commonResponse, + {name: 'nvVersion', type: DataType.UINT16}, + {name: 'type', type: DataType.UINT16}, + {name: 'version', type: DataType.UINT16}, + {name: 'len', type: DataType.UINT16}, + {name: 'data', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, + ], + }, + // Erases all datasets in NVRAM + [CommandId.NVRAM_ERASE]: { + request: [], + response: [...commonResponse], + }, + // Erases all datasets in NVRAM except ZB_NVRAM_RESERVED, ZB_IB_COUNTERS and application datasets + [CommandId.NVRAM_CLEAR]: { + request: [], + response: [...commonResponse], + }, + // Sets TC Policy + [CommandId.SET_TC_POLICY]: { + request: [ + {name: 'type', type: DataType.UINT16, typed: PolicyType}, + {name: 'value', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Sets an extended PAN ID + [CommandId.SET_EXTENDED_PAN_ID]: { + request: [{name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}], + response: [...commonResponse], + }, + // Sets the maximum number of children + [CommandId.SET_MAX_CHILDREN]: { + request: [{name: 'children', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Gets the maximum number of children + [CommandId.GET_MAX_CHILDREN]: { + request: [], + response: [...commonResponse, {name: 'children', type: DataType.UINT8}], + }, + + // ------------------------------------------ + // Application Framework + // ------------------------------------------ + + // Add or update Simple descriptor for a specified endpoint + [CommandId.AF_SET_SIMPLE_DESC]: { + request: [ + {name: 'endpoint', type: DataType.UINT8}, + {name: 'profileID', type: DataType.UINT16}, + {name: 'deviceID', type: DataType.UINT16}, + {name: 'version', type: DataType.UINT8}, + {name: 'inputClusterCount', type: DataType.UINT8}, + {name: 'outputClusterCount', type: DataType.UINT8}, + { + name: 'inputClusters', + type: BuffaloZclDataType.LIST_UINT16, + options: (payload, options) => (options.length = payload.inputClusterCount), + }, + { + name: 'outputClusters', + type: BuffaloZclDataType.LIST_UINT16, + options: (payload, options) => (options.length = payload.outputClusterCount), + }, + ], + response: [...commonResponse], + }, + // Delete Simple Descriptor for a specified endpoint + [CommandId.AF_DEL_SIMPLE_DESC]: { + request: [{name: 'endpoint', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Set Node Descriptor + [CommandId.AF_SET_NODE_DESC]: { + request: [ + {name: 'type', type: DataType.UINT8, typed: DeviceType}, + {name: 'macCapabilities', type: DataType.UINT8}, + {name: 'manufacturerCode', type: DataType.UINT16}, + ], + response: [...commonResponse], + }, + // Set power descriptor for the device + [CommandId.AF_SET_POWER_DESC]: { + request: [ + {name: 'powerMode', type: DataType.UINT8}, + {name: 'powerSources', type: DataType.UINT8}, + {name: 'powerSource', type: DataType.UINT8}, + {name: 'powerSourceLevel', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + + // ------------------------------------------ + // Zigbee Device Object + // ------------------------------------------ + + // Request for a remote device NWK address + [CommandId.ZDO_NWK_ADDR_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'type', type: DataType.UINT8}, + {name: 'startIndex', type: DataType.UINT8}, + ], + response: [ + ...commonResponse, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'num', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + {name: 'startIndex', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + {name: 'nwks', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.num)}, + ], + }, + // Request for a remote device IEEE address + [CommandId.ZDO_IEEE_ADDR_REQ]: { + request: [ + {name: 'destNwk', type: DataType.UINT16}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'type', type: DataType.UINT8}, + {name: 'startIndex', type: DataType.UINT8}, + ], + response: [ + ...commonResponse, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'num', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + {name: 'startIndex', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + {name: 'nwks', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.num)}, + ], + }, + // Get the Power Descriptor from a remote device + [CommandId.ZDO_POWER_DESC_REQ]: { + request: [{name: 'nwk', type: DataType.UINT16}], + response: [...commonResponse, {name: 'powerDescriptor', type: DataType.UINT16}, {name: 'nwk', type: DataType.UINT16}], + }, + // Get the Node Descriptor from a remote device + [CommandId.ZDO_NODE_DESC_REQ]: { + request: [{name: 'nwk', type: DataType.UINT16}], + response: [ + ...commonResponse, + {name: 'flags', type: DataType.UINT16}, + {name: 'macCapabilities', type: DataType.UINT8}, + {name: 'manufacturerCode', type: DataType.UINT16}, + {name: 'bufferSize', type: DataType.UINT8}, + {name: 'incomingSize', type: DataType.UINT16}, + {name: 'serverMask', type: DataType.UINT16}, + {name: 'outgoingSize', type: DataType.UINT16}, + {name: 'descriptorCapabilities', type: DataType.UINT8}, + {name: 'nwk', type: DataType.UINT16}, + ], + }, + // Get the Simple Descriptor from a remote device + [CommandId.ZDO_SIMPLE_DESC_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'endpoint', type: DataType.UINT8}, + ], + response: [ + ...commonResponse, + {name: 'endpoint', type: DataType.UINT8}, + {name: 'profileID', type: DataType.UINT16}, + {name: 'deviceID', type: DataType.UINT16}, + {name: 'version', type: DataType.UINT8}, + {name: 'inputClusterCount', type: DataType.UINT8}, + {name: 'outputClusterCount', type: DataType.UINT8}, + { + name: 'inputClusters', + type: BuffaloZclDataType.LIST_UINT16, + options: (payload, options) => (options.length = payload.inputClusterCount), + }, + { + name: 'outputClusters', + type: BuffaloZclDataType.LIST_UINT16, + options: (payload, options) => (options.length = payload.outputClusterCount), + }, + {name: 'nwk', type: DataType.UINT16}, + ], + }, + // Get a list of Active Endpoints from a remote device + [CommandId.ZDO_ACTIVE_EP_REQ]: { + request: [{name: 'nwk', type: DataType.UINT16}], + response: [ + ...commonResponse, + {name: 'len', type: DataType.UINT8}, + {name: 'endpoints', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, + {name: 'nwk', type: DataType.UINT16}, + ], + }, + // Send Match Descriptor request to a remote device + [CommandId.ZDO_MATCH_DESC_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'profileID', type: DataType.UINT16}, + {name: 'inputClusterCount', type: DataType.UINT8}, + {name: 'outputClusterCount', type: DataType.UINT8}, + { + name: 'inputClusters', + type: BuffaloZclDataType.LIST_UINT16, + options: (payload, options) => (options.length = payload.inputClusterCount), + }, + { + name: 'outputClusters', + type: BuffaloZclDataType.LIST_UINT16, + options: (payload, options) => (options.length = payload.outputClusterCount), + }, + ], + response: [ + ...commonResponse, + {name: 'len', type: DataType.UINT8}, + {name: 'endpoints', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, + {name: 'nwk', type: DataType.UINT16}, + ], + }, + // Send Bind request to a remote device + [CommandId.ZDO_BIND_REQ]: { + request: [ + {name: 'target', type: DataType.UINT16}, + {name: 'srcIeee', type: DataType.IEEE_ADDR}, + {name: 'srcEP', type: DataType.UINT8}, + {name: 'clusterID', type: DataType.UINT16}, + {name: 'addrMode', type: DataType.UINT8}, + {name: 'dstIeee', type: DataType.IEEE_ADDR}, + {name: 'dstEP', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Send Unbind request to a remote device + [CommandId.ZDO_UNBIND_REQ]: { + request: [ + {name: 'target', type: DataType.UINT16}, + {name: 'srcIeee', type: DataType.IEEE_ADDR}, + {name: 'srcEP', type: DataType.UINT8}, + {name: 'clusterID', type: DataType.UINT16}, + {name: 'addrMode', type: DataType.UINT8}, + {name: 'dstIeee', type: DataType.IEEE_ADDR}, + {name: 'dstEP', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Request that a Remote Device leave the network + [CommandId.ZDO_MGMT_LEAVE_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'flags', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Request a remote device or devices to allow or disallow association + [CommandId.ZDO_PERMIT_JOINING_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'duration', type: DataType.UINT8}, + {name: 'tcSignificance', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Device announce indication + [CommandId.ZDO_DEV_ANNCE_IND]: { + request: [], + response: [], + indication: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'macCapabilities', type: DataType.UINT8}, + ], + }, + // Rejoin to remote network even if joined already. If joined, clear internal data structures prior to joining. That call is useful for rejoin after parent loss. + [CommandId.ZDO_REJOIN]: { + request: [ + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + {name: 'len', type: DataType.UINT8}, + { + name: 'channels', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'page', type: DataType.UINT8}, + {name: 'mask', type: DataType.UINT32}, + ], + }, + {name: 'secure', type: DataType.UINT8}, + ], + response: [...commonResponse, {name: 'flags', type: DataType.UINT8}], + }, + // Sends a ZDO system server discovery request + [CommandId.ZDO_SYSTEM_SRV_DISCOVERY_REQ]: { + request: [{name: 'serverMask', type: DataType.UINT16}], + response: [...commonResponse], + }, + // Sends a ZDO Mgmt Bind request to a remote device + [CommandId.ZDO_MGMT_BIND_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'startIndex', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Sends a ZDO Mgmt LQI request to a remote device + [CommandId.ZDO_MGMT_LQI_REQ]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'startIndex', type: DataType.UINT8}, + ], + response: [ + ...commonResponse, + {name: 'entries', type: DataType.UINT8}, + {name: 'startIndex', type: DataType.UINT8}, + {name: 'len', type: DataType.UINT8}, + { + name: 'neighbors', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'relationship', type: DataType.UINT8}, + {name: 'joining', type: DataType.UINT8}, + {name: 'depth', type: DataType.UINT8}, + {name: 'lqi', type: DataType.UINT8}, + ], + options: (payload, options) => (options.length = payload.len), + }, + ], + }, + // Sends a ZDO Mgmt NWK Update Request to a remote device + [CommandId.ZDO_MGMT_NWK_UPDATE_REQ]: { + request: [ + {name: 'channelMask', type: DataType.UINT32}, + {name: 'duration', type: DataType.UINT8}, + {name: 'count', type: DataType.UINT8}, + {name: 'managerNwk', type: DataType.UINT16}, + {name: 'nwk', type: DataType.UINT16}, + ], + response: [...commonResponse], + }, + // Require statistics (last message LQI\RSSI, counters, etc.) from the ZDO level + [CommandId.ZDO_GET_STATS]: { + request: [{name: 'cleanup', type: DataType.UINT8}], + response: [ + ...commonResponse, + {name: 'mac_rx_bcast', type: DataType.UINT32}, + {name: 'mac_tx_bcast', type: DataType.UINT32}, + {name: 'mac_rx_ucast', type: DataType.UINT32}, + {name: 'mac_tx_ucast_total_zcl', type: DataType.UINT32}, + {name: 'mac_tx_ucast_failures_zcl', type: DataType.UINT16}, + {name: 'mac_tx_ucast_retries_zcl', type: DataType.UINT16}, + {name: 'mac_tx_ucast_total', type: DataType.UINT16}, + {name: 'mac_tx_ucast_failures', type: DataType.UINT16}, + {name: 'mac_tx_ucast_retries', type: DataType.UINT16}, + {name: 'phy_to_mac_que_lim_reached', type: DataType.UINT16}, + {name: 'mac_validate_drop_cnt', type: DataType.UINT16}, + {name: 'phy_cca_fail_count', type: DataType.UINT16}, + {name: 'period_of_time', type: DataType.UINT8}, + {name: 'last_msg_lqi', type: DataType.UINT8}, + {name: 'last_msg_rssi', type: DataType.UINT8}, + {name: 'number_of_resets', type: DataType.UINT16}, + {name: 'aps_tx_bcast', type: DataType.UINT16}, + {name: 'aps_tx_ucast_success', type: DataType.UINT16}, + {name: 'aps_tx_ucast_retry', type: DataType.UINT16}, + {name: 'aps_tx_ucast_fail', type: DataType.UINT16}, + {name: 'route_disc_initiated', type: DataType.UINT16}, + {name: 'nwk_neighbor_added', type: DataType.UINT16}, + {name: 'nwk_neighbor_removed', type: DataType.UINT16}, + {name: 'nwk_neighbor_stale', type: DataType.UINT16}, + {name: 'join_indication', type: DataType.UINT16}, + {name: 'childs_removed', type: DataType.UINT16}, + {name: 'nwk_fc_failure', type: DataType.UINT16}, + {name: 'aps_fc_failure', type: DataType.UINT16}, + {name: 'aps_unauthorized_key', type: DataType.UINT16}, + {name: 'nwk_decrypt_failure', type: DataType.UINT16}, + {name: 'aps_decrypt_failure', type: DataType.UINT16}, + {name: 'packet_buffer_allocate_failures', type: DataType.UINT16}, + {name: 'average_mac_retry_per_aps_message_sent', type: DataType.UINT16}, + {name: 'nwk_retry_overflow', type: DataType.UINT16}, + {name: 'nwk_bcast_table_full', type: DataType.UINT16}, + {name: 'status', type: DataType.UINT8}, + ], + }, + // Indicates some device in the network was authorized (e.g. received TCLK) + [CommandId.ZDO_DEV_AUTHORIZED_IND]: { + request: [], + response: [], + indication: [ + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'authType', type: DataType.UINT8}, + {name: 'authStatus', type: DataType.UINT8}, + ], + }, + // Indicates some device joined the network + [CommandId.ZDO_DEV_UPDATE_IND]: { + request: [], + response: [], + indication: [ + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'status', type: DataType.UINT8, typed: DeviceUpdateStatus}, + ], + }, + // Sets manufacturer code field in the node descriptor + [CommandId.ZDO_SET_NODE_DESC_MANUF_CODE]: { + request: [{name: 'manufacturerCode', type: DataType.UINT16}], + response: [...commonResponse], + }, + + // ------------------------------------------ + // Application Support Sub-layer + // ------------------------------------------ + + // APSDE-DATA.request + [CommandId.APSDE_DATA_REQ]: { + request: [ + {name: 'paramLength', type: DataType.UINT8}, + {name: 'dataLength', type: DataType.UINT16}, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'profileID', type: DataType.UINT16}, + {name: 'clusterID', type: DataType.UINT16}, + //{name: 'dstEndpoint', type: DataType.UINT8, condition: (payload) => ![2,3].includes(payload.dstAddrMode)}, + {name: 'dstEndpoint', type: DataType.UINT8}, + {name: 'srcEndpoint', type: DataType.UINT8}, + {name: 'radius', type: DataType.UINT8}, + {name: 'dstAddrMode', type: DataType.UINT8}, + {name: 'txOptions', type: DataType.UINT8}, + {name: 'useAlias', type: DataType.UINT8}, + //{name: 'aliasAddr', type: DataType.UINT16, condition: (payload) => payload.useAlias !== 0}, + {name: 'aliasAddr', type: DataType.UINT16}, + {name: 'aliasSequence', type: DataType.UINT8}, + {name: 'data', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.dataLength)}, + ], + response: [ + ...commonResponse, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'dstEndpoint', type: DataType.UINT8, condition: (payload) => ![2, 3].includes(payload.dstAddrMode)}, + {name: 'srcEndpoint', type: DataType.UINT8}, + {name: 'txTime', type: DataType.UINT32}, + {name: 'dstAddrMode', type: DataType.UINT8}, + ], + }, + // APSME-BIND.Request + [CommandId.APSME_BIND]: { + request: [ + {name: 'srcIeee', type: DataType.IEEE_ADDR}, + {name: 'srcEndpoint', type: DataType.UINT8}, + {name: 'clusterID', type: DataType.UINT16}, + {name: 'dstAddrMode', type: DataType.UINT8}, + {name: 'dstIeee', type: DataType.IEEE_ADDR}, + {name: 'dstEndpoint', type: DataType.UINT8}, + ], + response: [...commonResponse, {name: 'index', type: DataType.UINT8}], + }, + // APSME-UNBIND.request + [CommandId.APSME_UNBIND]: { + request: [ + {name: 'srcIeee', type: DataType.IEEE_ADDR}, + {name: 'srcEndpoint', type: DataType.UINT8}, + {name: 'clusterID', type: DataType.UINT16}, + {name: 'dstAddrMode', type: DataType.UINT8}, + {name: 'dstIeee', type: DataType.IEEE_ADDR}, + {name: 'dstEndpoint', type: DataType.UINT8}, + ], + response: [...commonResponse, {name: 'index', type: DataType.UINT8}], + }, + // APSME-ADD-GROUP.request + [CommandId.APSME_ADD_GROUP]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'endpoint', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // APSME-REMOVE-GROUP.request + [CommandId.APSME_RM_GROUP]: { + request: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'endpoint', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // APSDE-DATA.indication + [CommandId.APSDE_DATA_IND]: { + request: [], + response: [], + indication: [ + {name: 'paramLength', type: DataType.UINT8}, + {name: 'dataLength', type: DataType.UINT16}, + {name: 'apsFC', type: DataType.UINT8}, + {name: 'srcNwk', type: DataType.UINT16}, + {name: 'dstNwk', type: DataType.UINT16}, + {name: 'grpNwk', type: DataType.UINT16}, + {name: 'dstEndpoint', type: DataType.UINT8}, + {name: 'srcEndpoint', type: DataType.UINT8}, + {name: 'clusterID', type: DataType.UINT16}, + {name: 'profileID', type: DataType.UINT16}, + {name: 'apsCounter', type: DataType.UINT8}, + {name: 'srcMAC', type: DataType.UINT16}, + {name: 'dstMAC', type: DataType.UINT16}, + {name: 'lqi', type: DataType.UINT8}, + {name: 'rssi', type: DataType.UINT8}, + {name: 'apsKey', type: DataType.UINT8}, + {name: 'data', type: BuffaloZclDataType.BUFFER, options: (payload, options) => (options.length = payload.dataLength)}, + ], + }, + // APSME-REMOVE-ALL-GROUPS.request + [CommandId.APSME_RM_ALL_GROUPS]: { + request: [{name: 'endpoint', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Checks if there are any bindings for specified endpoint and cluster + [CommandId.APS_CHECK_BINDING]: { + request: [ + {name: 'endpoint', type: DataType.UINT8}, + {name: 'clusterID', type: DataType.UINT16}, + ], + response: [...commonResponse, {name: 'exists', type: DataType.UINT8}], + }, + // Gets the APS Group Table + [CommandId.APS_GET_GROUP_TABLE]: { + request: [], + response: [ + ...commonResponse, + {name: 'length', type: DataType.UINT16}, + {name: 'groups', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.length)}, + ], + }, + // Removes all bindings + [CommandId.APSME_UNBIND_ALL]: { + request: [], + response: [...commonResponse], + }, + + // ------------------------------------------ + // NWK Management API + // ------------------------------------------ + + // NLME-NETWORK-FORMATION.request + [CommandId.NWK_FORMATION]: { + request: [ + {name: 'len', type: DataType.UINT8}, + { + name: 'channels', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'page', type: DataType.UINT8}, + {name: 'mask', type: DataType.UINT32}, + ], + }, + {name: 'duration', type: DataType.UINT8}, + {name: 'distribFlag', type: DataType.UINT8}, + {name: 'distribNwk', type: DataType.UINT16}, + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + ], + response: [...commonResponse, {name: 'nwk', type: DataType.UINT16}], + }, + // NLME-NETWORK-DISCOVERY.request + [CommandId.NWK_DISCOVERY]: { + request: [ + {name: 'len', type: DataType.UINT8}, + { + name: 'channels', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'page', type: DataType.UINT8}, + {name: 'mask', type: DataType.UINT32}, + ], + }, + {name: 'duration', type: DataType.UINT8}, + {name: 'macCapabilities', type: DataType.UINT8}, + {name: 'security', type: DataType.UINT8}, + ], + response: [ + ...commonResponse, + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + {name: 'panID', type: DataType.UINT16}, + {name: 'nwkUpdateID', type: DataType.UINT8}, + {name: 'page', type: DataType.UINT8}, + {name: 'channel', type: DataType.UINT8}, + {name: 'flags', type: DataType.UINT8}, + {name: 'lqi', type: DataType.UINT8}, + {name: 'rssi', type: DataType.INT8}, + ], + }, + // Join network, do basic post-join actions + [CommandId.NWK_NLME_JOIN]: { + request: [ + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + {name: 'rejoin', type: DataType.UINT8}, + {name: 'len', type: DataType.UINT8}, + { + name: 'channels', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'page', type: DataType.UINT8}, + {name: 'mask', type: DataType.UINT32}, + ], + }, + ], + response: [ + ...commonResponse, + {name: 'nwk', type: DataType.UINT16}, + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + {name: 'page', type: DataType.UINT8}, + {name: 'channel', type: DataType.UINT8}, + {name: 'beacon', type: DataType.UINT8}, + {name: 'macInterface', type: DataType.UINT8}, + ], + }, + // NLME-PERMIT-JOINING.request + [CommandId.NWK_PERMIT_JOINING]: { + request: [{name: 'duration', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Get IEEE address by short address from the local address translation table + [CommandId.NWK_GET_IEEE_BY_SHORT]: { + request: [{name: 'nwk', type: DataType.UINT16}], + response: [...commonResponse, {name: 'ieee', type: DataType.IEEE_ADDR}], + }, + // Get short address by IEEE address from the local address translation table + [CommandId.NWK_GET_SHORT_BY_IEEE]: { + request: [{name: 'ieee', type: DataType.IEEE_ADDR}], + response: [...commonResponse, {name: 'nwk', type: DataType.UINT16}], + }, + // Get local neighbor table entry by IEEE address + [CommandId.NWK_GET_NEIGHBOR_BY_IEEE]: { + request: [{name: 'ieee', type: DataType.IEEE_ADDR}], + response: [ + ...commonResponse, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'nwk', type: DataType.UINT16}, + {name: 'role', type: DataType.UINT8}, + {name: 'rxOnWhenIdle', type: DataType.UINT8}, + {name: 'edConfig', type: DataType.UINT16}, + {name: 'timeoutCounter', type: DataType.UINT32}, + {name: 'deviceTimeout', type: DataType.UINT32}, + {name: 'relationship', type: DataType.UINT8}, + {name: 'failureCount', type: DataType.UINT8}, + {name: 'lqi', type: DataType.UINT8}, + {name: 'cost', type: DataType.UINT8}, + {name: 'age', type: DataType.UINT8}, + {name: 'keepalive', type: DataType.UINT8}, + {name: 'macInterface', type: DataType.UINT8}, + ], + }, + // Indicates that network rejoining procedure has completed + [CommandId.NWK_REJOINED_IND]: { + request: [], + response: [], + indication: [ + {name: 'nwk', type: DataType.UINT16}, + {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + {name: 'page', type: DataType.UINT8}, + {name: 'channel', type: DataType.UINT8}, + {name: 'beacon', type: DataType.UINT8}, + {name: 'macInterface', type: DataType.UINT8}, + ], + }, + // Indicates that network rejoining procedure has failed + [CommandId.NWK_REJOIN_FAILED_IND]: { + request: [], + response: [], + indication: [...commonResponse], + }, + // Network Leave indication + [CommandId.NWK_LEAVE_IND]: { + request: [], + response: [], + indication: [ + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'rejoin', type: DataType.UINT8}, + ], + }, + // Set Fast Poll Interval PIM attribute + [CommandId.PIM_SET_FAST_POLL_INTERVAL]: { + request: [{name: 'interval', type: DataType.UINT16}], + response: [...commonResponse], + }, + // Set Long Poll Interval PIM attribute + [CommandId.PIM_SET_LONG_POLL_INTERVAL]: { + request: [{name: 'interval', type: DataType.UINT32}], + response: [...commonResponse], + }, + // Start poll with the Fast Poll Interval specified by PIM attribute + [CommandId.PIM_START_FAST_POLL]: { + request: [], + response: [...commonResponse], + }, + // Start Long Poll + [CommandId.PIM_START_LONG_POLL]: { + request: [], + response: [...commonResponse], + }, + // Start poll with the Long Poll Interval specified by PIM attribute + [CommandId.PIM_START_POLL]: { + request: [], + response: [...commonResponse], + }, + // Stop fast poll and restart it with the Long Poll Interval + [CommandId.PIM_STOP_FAST_POLL]: { + request: [], + response: [...commonResponse, {name: 'result', type: DataType.UINT8}], + }, + // Stop automatic ZBOSS poll + [CommandId.PIM_STOP_POLL]: { + request: [], + response: [...commonResponse], + }, + // Enable turbo poll for a given amount of time. + [CommandId.PIM_ENABLE_TURBO_POLL]: { + request: [{name: 'time', type: DataType.UINT32}], + response: [...commonResponse], + }, + // Disable turbo poll for a given amount of time. + [CommandId.PIM_DISABLE_TURBO_POLL]: { + request: [], + response: [...commonResponse], + }, + // Disable turbo poll for a given amount of time. + [CommandId.NWK_ADDRESS_UPDATE_IND]: { + request: [], + response: [], + indication: [{name: 'nwk', type: DataType.UINT16}], + }, + // Start without forming a new network. + [CommandId.NWK_START_WITHOUT_FORMATION]: { + request: [], + response: [...commonResponse], + }, + // NWK NLME start router request + [CommandId.NWK_NLME_ROUTER_START]: { + request: [ + {name: 'beaconOrder', type: DataType.UINT8}, + {name: 'superframeOrder', type: DataType.UINT8}, + {name: 'batteryLife', type: DataType.UINT8}, + ], + response: [...commonResponse], + }, + // Indicates that joined device has no parent + [CommandId.PARENT_LOST_IND]: { + request: [], + response: [], + indication: [], + }, + // PIM_START_TURBO_POLL_PACKETS + // PIM_START_TURBO_POLL_CONTINUOUS + // PIM_TURBO_POLL_CONTINUOUS_LEAVE + // PIM_TURBO_POLL_PACKETS_LEAVE + // PIM_PERMIT_TURBO_POLL + // PIM_SET_FAST_POLL_TIMEOUT + // PIM_GET_LONG_POLL_INTERVAL + // PIM_GET_IN_FAST_POLL_FLAG + // Sets keepalive mode + [CommandId.SET_KEEPALIVE_MOVE]: { + request: [{name: 'mode', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Starts a concentrator mode + [CommandId.START_CONCENTRATOR_MODE]: { + request: [ + {name: 'radius', type: DataType.UINT8}, + {name: 'timeout', type: DataType.UINT32}, + ], + response: [...commonResponse], + }, + // Stops a concentrator mode + [CommandId.STOP_CONCENTRATOR_MODE]: { + request: [], + response: [...commonResponse], + }, + // Enables or disables PAN ID conflict resolution + [CommandId.NWK_ENABLE_PAN_ID_CONFLICT_RESOLUTION]: { + request: [{name: 'enable', type: DataType.UINT8}], + response: [...commonResponse], + }, + // Enables or disables automatic PAN ID conflict resolution + [CommandId.NWK_ENABLE_AUTO_PAN_ID_CONFLICT_RESOLUTION]: { + request: [{name: 'enable', type: DataType.UINT8}], + response: [...commonResponse], + }, + // PIM_TURBO_POLL_CANCEL_PACKET + + // ------------------------------------------ + // Security + // ------------------------------------------ + + // Set local device installcode to ZR/ZED + [CommandId.SECUR_SET_LOCAL_IC]: { + request: [ + {name: 'installCode', type: BuffaloZclDataType.LIST_UINT8}, //8, 10, 14 or 18Installcode, including trailing 2 bytes of CRC + ], + response: [...commonResponse], + }, + // Set remote device installcode to ZC + [CommandId.SECUR_ADD_IC]: { + request: [ + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'installCode', type: BuffaloZclDataType.LIST_UINT8}, //8, 10, 14 or 18Installcode, including trailing 2 bytes of CRC + ], + response: [...commonResponse], + }, + // Delete remote device installcode from ZC + [CommandId.SECUR_DEL_IC]: { + request: [{name: 'ieee', type: DataType.IEEE_ADDR}], + response: [...commonResponse], + }, + // Get local device Installcode + [CommandId.SECUR_GET_LOCAL_IC]: { + request: [], + response: [ + ...commonResponse, + {name: 'installCode', type: BuffaloZclDataType.LIST_UINT8}, //8, 10, 14 or 18Installcode, including trailing 2 bytes of CRC + ], + }, + // TCLK Indication + [CommandId.SECUR_TCLK_IND]: { + request: [], + response: [], + indication: [ + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'keyType', type: DataType.UINT8}, + ], + }, + // TCLK Exchange Indication Failed + [CommandId.SECUR_TCLK_EXCHANGE_FAILED_IND]: { + request: [], + response: [], + indication: [...commonResponse], + }, + // Initiates a key switch procedure + [CommandId.SECUR_NWK_INITIATE_KEY_SWITCH_PROCEDURE]: { + request: [], + response: [...commonResponse], + }, + // Gets the IC list + [CommandId.SECUR_GET_IC_LIST]: { + request: [{name: 'startIndex', type: DataType.UINT8}], + response: [ + ...commonResponse, + {name: 'size', type: DataType.UINT8}, + {name: 'startIndex', type: DataType.UINT8}, + {name: 'count', type: DataType.UINT8}, + { + name: 'table', + type: BuffaloZBOSSDataType.LIST_TYPED, + typed: [ + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'type', type: DataType.UINT8}, + {name: 'installCode', type: BuffaloZclDataType.LIST_UINT8}, //8, 10, 14 or 18Installcode, including trailing 2 bytes of CRC + ], + }, + ], + }, + // Get an IC table entry by index + [CommandId.SECUR_GET_IC_BY_IDX]: { + request: [{name: 'index', type: DataType.UINT8}], + response: [ + ...commonResponse, + {name: 'ieee', type: DataType.IEEE_ADDR}, + {name: 'type', type: DataType.UINT8}, + {name: 'installCode', type: BuffaloZclDataType.LIST_UINT8}, //8, 10, 14 or 18Installcode, including trailing 2 bytes of CRC + ], + }, + // Removes all IC + [CommandId.SECUR_REMOVE_ALL_IC]: { + request: [], + response: [...commonResponse], + }, + + /////////////////// + [CommandId.UNKNOWN_1]: { + request: [], + response: [...commonResponse], + indication: [{name: 'data', type: DataType.UINT8}], + }, +}; diff --git a/src/adapter/zboss/consts.ts b/src/adapter/zboss/consts.ts new file mode 100644 index 0000000000..223e3a1e89 --- /dev/null +++ b/src/adapter/zboss/consts.ts @@ -0,0 +1,9 @@ +export const END = 0xc0; +export const ESCAPE = 0xdb; +export const ESCEND = 0xdc; +export const ESCESC = 0xdd; + +export const SIGNATURE = 0xdead; +export const ZBOSS_NCP_API_HL = 0x06; +export const ZBOSS_FLAG_FIRST_FRAGMENT = 0x40; +export const ZBOSS_FLAG_LAST_FRAGMENT = 0x80; diff --git a/src/adapter/zboss/driver.ts b/src/adapter/zboss/driver.ts new file mode 100644 index 0000000000..5a5aed4ce8 --- /dev/null +++ b/src/adapter/zboss/driver.ts @@ -0,0 +1,436 @@ +/* istanbul ignore file */ + +import EventEmitter from 'events'; + +import equals from 'fast-deep-equal/es6'; + +import {TsType} from '..'; +import {KeyValue} from '../../controller/tstype'; +import {Queue, Waitress} from '../../utils'; +import {logger} from '../../utils/logger'; +import {CommandId, DeviceType, PolicyType, ResetOptions, StatusCodeGeneric} from './enums'; +import {FrameType, makeFrame, ZBOSSFrame} from './frame'; +import {ZBOSSUart} from './uart'; + +const NS = 'zh:zboss:driv'; + +const MAX_INIT_ATTEMPTS = 5; + +type ZBOSSWaitressMatcher = { + tsn: number | null; + commandId: number; +}; + +type ZBOSSNetworkInfo = { + joined: boolean; + nodeType: DeviceType; + ieeeAddr: string; + network: { + panID: number; + extendedPanID: number[]; + channel: number; + }; +}; + +export class ZBOSSDriver extends EventEmitter { + public readonly port: ZBOSSUart; + private waitress: Waitress; + private queue: Queue; + private tsn = 1; // command sequence + private nwkOpt: TsType.NetworkOptions; + public netInfo?: ZBOSSNetworkInfo; + + constructor(options: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions) { + super(); + this.nwkOpt = nwkOpt; + this.queue = new Queue(); + this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); + + this.port = new ZBOSSUart(options); + this.port.on('frame', this.onFrame.bind(this)); + } + + public async connect(): Promise { + logger.info(`Driver connecting`, NS); + + let status: boolean = false; + + for (let i = 0; i < MAX_INIT_ATTEMPTS; i++) { + status = await this.port.resetNcp(); + + // fail early if we couldn't even get the port set up + if (!status) { + return status; + } + + status = await this.port.start(); + + if (status) { + logger.info(`Driver connected`, NS); + return status; + } + } + + return status; + } + + private async reset(options = ResetOptions.NoOptions): Promise { + logger.info(`Driver reset`, NS); + this.port.inReset = true; + await this.execCommand(CommandId.NCP_RESET, {options}, 10000); + } + + public async startup(): Promise { + logger.info(`Driver startup`, NS); + let result: TsType.StartResult = 'resumed'; + + if (await this.needsToBeInitialised(this.nwkOpt)) { + // need to check the backup + // const restore = await this.needsToBeRestore(this.nwkOpt); + const restore = false; + + if (this.netInfo?.joined) { + logger.info(`Leaving current network and forming new network`, NS); + await this.reset(ResetOptions.FactoryReset); + } + + if (restore) { + // // restore + // logger.info('Restore network from backup', NS); + // await this.formNetwork(true); + // result = 'restored'; + } else { + // reset + logger.info('Form network', NS); + await this.formNetwork(); // false + result = 'reset'; + } + } else { + await this.execCommand(CommandId.NWK_START_WITHOUT_FORMATION, {}); + } + await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.LINK_KEY_REQUIRED, value: 0}); + await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.IC_REQUIRED, value: 0}); + await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.TC_REJOIN_ENABLED, value: 1}); + await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.IGNORE_TC_REJOIN, value: 0}); + await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.APS_INSECURE_JOIN, value: 0}); + await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.DISABLE_NWK_MGMT_CHANNEL_UPDATE, value: 0}); + + await this.addEndpoint( + 1, + 260, + 0xbeef, + [0x0000, 0x0003, 0x0006, 0x000a, 0x0019, 0x001a, 0x0300], + [ + 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0020, 0x0300, 0x0400, 0x0402, 0x0405, 0x0406, 0x0500, 0x0b01, 0x0b03, 0x0b04, + 0x0702, 0x1000, 0xfc01, 0xfc02, + ], + ); + await this.addEndpoint(242, 0xa1e0, 0x61, [], [0x0021]); + + await this.execCommand(CommandId.SET_RX_ON_WHEN_IDLE, {rxOn: 1}); + //await this.execCommand(CommandId.SET_ED_TIMEOUT, {timeout: 8}); + //await this.execCommand(CommandId.SET_MAX_CHILDREN, {children: 100}); + + return result; + } + + private async needsToBeInitialised(options: TsType.NetworkOptions): Promise { + let valid = true; + this.netInfo = await this.getNetworkInfo(); + logger.debug(`Current network parameters: ${JSON.stringify(this.netInfo)}`, NS); + if (this.netInfo) { + valid = valid && this.netInfo.nodeType == DeviceType.COORDINATOR; + valid = valid && options.panID == this.netInfo.network.panID; + valid = valid && options.channelList.includes(this.netInfo.network.channel); + valid = valid && equals(Buffer.from(options.extendedPanID || []), Buffer.from(this.netInfo.network.extendedPanID)); + } else { + valid = false; + } + return !valid; + } + + private async getNetworkInfo(): Promise { + let result = await this.execCommand(CommandId.GET_JOINED, {}); + const joined = result.payload.joined == 1; + if (!joined) { + logger.debug('Network not formed', NS); + } + + result = await this.execCommand(CommandId.GET_ZIGBEE_ROLE, {}); + const nodeType = result.payload.role; + + result = await this.execCommand(CommandId.GET_LOCAL_IEEE_ADDR, {mac: 0}); + const ieeeAddr = result.payload.ieee; + + result = await this.execCommand(CommandId.GET_EXTENDED_PAN_ID, {}); + const extendedPanID = result.payload.extendedPanID; + + result = await this.execCommand(CommandId.GET_PAN_ID, {}); + const panID = result.payload.panID; + + result = await this.execCommand(CommandId.GET_ZIGBEE_CHANNEL, {}); + const channel = result.payload.channel; + + return { + joined, + nodeType, + ieeeAddr, + network: { + panID, + extendedPanID, + channel, + }, + }; + } + + private async addEndpoint( + endpoint: number, + profileId: number, + deviceId: number, + inputClusters: number[], + outputClusters: number[], + ): Promise { + const res = await this.execCommand(CommandId.AF_SET_SIMPLE_DESC, { + endpoint: endpoint, + profileID: profileId, + deviceID: deviceId, + version: 0, + inputClusterCount: inputClusters.length, + outputClusterCount: outputClusters.length, + inputClusters: inputClusters, + outputClusters: outputClusters, + }); + + logger.debug(`Adding endpoint: ${JSON.stringify(res)}`, NS); + } + + private getChannelMask(channels: number[]): number { + return channels.reduce((mask, channel) => mask | (1 << channel), 0); + } + + private async formNetwork(): Promise { + const channelMask = this.getChannelMask(this.nwkOpt.channelList); + await this.execCommand(CommandId.SET_ZIGBEE_ROLE, {role: DeviceType.COORDINATOR}); + await this.execCommand(CommandId.SET_ZIGBEE_CHANNEL_MASK, {page: 0, mask: channelMask}); + await this.execCommand(CommandId.SET_PAN_ID, {panID: this.nwkOpt.panID}); + // await this.execCommand(CommandId.SET_EXTENDED_PAN_ID, {extendedPanID: this.nwkOpt.extendedPanID}); + await this.execCommand(CommandId.SET_NWK_KEY, {nwkKey: this.nwkOpt.networkKey, index: 0}); + + const res = await this.execCommand( + CommandId.NWK_FORMATION, + { + len: 1, + channels: [{page: 0, mask: channelMask}], + duration: 0x05, + distribFlag: 0x00, + distribNwk: 0x0000, + extendedPanID: this.nwkOpt.extendedPanID, + }, + 20000, + ); + logger.debug(`Forming network: ${JSON.stringify(res)}`, NS); + } + + public async stop(): Promise { + await this.port.stop(); + + logger.info(`Driver stopped`, NS); + } + + private onFrame(frame: ZBOSSFrame): void { + logger.info(`<== Frame: ${JSON.stringify(frame)}`, NS); + + const handled = this.waitress.resolve(frame); + + if (!handled) { + this.emit('frame', frame); + } + } + + public isInitialized(): boolean | undefined { + return this.port.portOpen && !this.port.inReset; + } + + public async execCommand(commandId: number, params: KeyValue = {}, timeout: number = 10000): Promise { + logger.debug(`==> ${CommandId[commandId]}(${commandId}): ${JSON.stringify(params)}`, NS); + + if (!this.port.portOpen) { + throw new Error('Connection not initialized'); + } + + return this.queue.execute(async (): Promise => { + const frame = makeFrame(FrameType.REQUEST, commandId, params); + frame.tsn = this.tsn; + const waiter = this.waitFor(commandId, commandId == CommandId.NCP_RESET ? null : this.tsn, timeout); + this.tsn = (this.tsn + 1) & 255; + + try { + logger.debug(`==> FRAME: ${JSON.stringify(frame)}`, NS); + await this.port.sendFrame(frame); + + const response = await waiter.start().promise; + if (response?.payload?.status !== StatusCodeGeneric.OK) { + throw new Error(`Error on command ${CommandId[commandId]}(${commandId}): ${JSON.stringify(response)}`); + } + + return response; + } catch (error) { + this.waitress.remove(waiter.ID); + logger.error(`==> Error: ${error}`, NS); + throw new Error(`Failure send ${commandId}:` + JSON.stringify(frame)); + } + }); + } + + public waitFor(commandId: number, tsn: number | null, timeout = 10000): {start: () => {promise: Promise; ID: number}; ID: number} { + return this.waitress.waitFor({commandId, tsn}, timeout); + } + + private waitressTimeoutFormatter(matcher: ZBOSSWaitressMatcher, timeout: number): string { + return `${JSON.stringify(matcher)} after ${timeout}ms`; + } + + private waitressValidator(payload: ZBOSSFrame, matcher: ZBOSSWaitressMatcher): boolean { + return (matcher.tsn == null || payload.tsn === matcher.tsn) && matcher.commandId == payload.commandId; + } + + public async getCoordinator(): Promise { + const message = await this.activeEndpoints(0x0000); + const activeEndpoints = message.payload.endpoints || []; + const ap = []; + for (const ep in activeEndpoints) { + const sd = await this.simpleDescriptor(0x0000, activeEndpoints[ep]); + ap.push({ + ID: sd.payload.endpoint, + profileID: sd.payload.profileID, + deviceID: sd.payload.deviceID, + inputClusters: sd.payload.inputClusters, + outputClusters: sd.payload.outputClusters, + }); + } + return { + ieeeAddr: this.netInfo?.ieeeAddr || '', + networkAddress: 0x0000, + manufacturerID: 0x0000, + endpoints: ap, + }; + } + + public async getCoordinatorVersion(): Promise { + const ver = await this.execCommand(CommandId.GET_MODULE_VERSION, {}); + const cver = await this.execCommand(CommandId.GET_COORDINATOR_VERSION, {}); + const ver2str = (version: number): string => { + const major = (version >> 24) & 0xff; + const minor = (version >> 16) & 0xff; + const revision = (version >> 8) & 0xff; + const commit = version & 0xff; + return `${major}.${minor}.${revision}.${commit}`; + }; + + return { + type: `zboss`, + meta: { + coordinator: cver.payload.version, + stack: ver2str(ver.payload.stackVersion), + protocol: ver2str(ver.payload.protocolVersion), + revision: ver2str(ver.payload.fwVersion), + }, + }; + } + + public async permitJoin(nwk: number, duration: number): Promise { + await this.execCommand(CommandId.ZDO_PERMIT_JOINING_REQ, {nwk: nwk, duration: duration, tcSignificance: 1}); + } + + public async setTXPower(value: number): Promise { + await this.execCommand(CommandId.SET_TX_POWER, {txPower: value}); + } + + public async lqi(nwk: number, index: number): Promise { + return this.execCommand(CommandId.ZDO_MGMT_LQI_REQ, {nwk: nwk, startIndex: index}); + } + + public async neighbors(ieee: string): Promise { + return this.execCommand(CommandId.NWK_GET_NEIGHBOR_BY_IEEE, {ieee: ieee}); + } + + public async nodeDescriptor(nwk: number): Promise { + return this.execCommand(CommandId.ZDO_NODE_DESC_REQ, {nwk: nwk}); + } + + public async activeEndpoints(nwk: number): Promise { + return this.execCommand(CommandId.ZDO_ACTIVE_EP_REQ, {nwk: nwk}); + } + + public async simpleDescriptor(nwk: number, ep: number): Promise { + return this.execCommand(CommandId.ZDO_SIMPLE_DESC_REQ, {nwk: nwk, endpoint: ep}); + } + + public async removeDevice(nwk: number, ieee: string): Promise { + return this.execCommand(CommandId.ZDO_MGMT_LEAVE_REQ, {nwk: nwk, ieee: ieee, flags: 0}); + } + + public async request(ieee: string, profileID: number, clusterID: number, dstEp: number, srcEp: number, data: Buffer): Promise { + const payload = { + paramLength: 21, + dataLength: data.length, + ieee: ieee, + profileID: profileID, + clusterID: clusterID, + dstEndpoint: dstEp, + srcEndpoint: srcEp, + radius: 3, + dstAddrMode: 3, // ADDRESS MODE ieee + txOptions: 2, // ROUTE DISCOVERY + useAlias: 0, + aliasAddr: 0, + aliasSequence: 0, + data: data, + }; + return this.execCommand(CommandId.APSDE_DATA_REQ, payload); + } + + public async bind( + destinationNetworkAddress: number, + sourceIeeeAddress: string, + sourceEndpoint: number, + clusterID: number, + destinationAddressOrGroup: string | number, + type: 'endpoint' | 'group', + destinationEndpoint?: number, + ): Promise { + return this.execCommand(CommandId.ZDO_BIND_REQ, { + target: destinationNetworkAddress, + srcIeee: sourceIeeeAddress, + srcEP: sourceEndpoint, + clusterID: clusterID, + addrMode: type == 'endpoint' ? 3 /* ieee */ : 1 /* group */, + dstIeee: destinationAddressOrGroup, + dstEP: destinationEndpoint || 1, + }); + } + + public async unbind( + destinationNetworkAddress: number, + sourceIeeeAddress: string, + sourceEndpoint: number, + clusterID: number, + destinationAddressOrGroup: string | number, + type: 'endpoint' | 'group', + destinationEndpoint?: number, + ): Promise { + return this.execCommand(CommandId.ZDO_UNBIND_REQ, { + target: destinationNetworkAddress, + srcIeee: sourceIeeeAddress, + srcEP: sourceEndpoint, + clusterID: clusterID, + addrMode: type == 'endpoint' ? 3 /* ieee */ : 1 /* group */, + dstIeee: destinationAddressOrGroup, + dstEP: destinationEndpoint || 1, + }); + } + + public async ieeeByNwk(nwk: number): Promise { + return (await this.execCommand(CommandId.NWK_GET_IEEE_BY_SHORT, {nwk: nwk})).payload.ieee; + } +} diff --git a/src/adapter/zboss/enums.ts b/src/adapter/zboss/enums.ts new file mode 100644 index 0000000000..8df83288f5 --- /dev/null +++ b/src/adapter/zboss/enums.ts @@ -0,0 +1,328 @@ +export enum StatusCategory { + GENERIC = 0, + MAC = 2, + NWK = 3, + APS = 4, + ZDO = 5, + CBKE = 6, +} + +export enum StatusCodeGeneric { + OK = 0, + ERROR = 1, + BLOCKED = 2, + EXIT = 3, + BUSY = 4, + EOF = 5, + OUT_OF_RANGE = 6, + EMPTY = 7, + CANCELLED = 8, + INVALID_PARAMETER_1 = 10, + INVALID_PARAMETER_2 = 11, + INVALID_PARAMETER_3 = 12, + INVALID_PARAMETER_4 = 13, + INVALID_PARAMETER_5 = 14, + INVALID_PARAMETER_6 = 15, + INVALID_PARAMETER_7 = 16, + INVALID_PARAMETER_8 = 17, + INVALID_PARAMETER_9 = 18, + INVALID_PARAMETER_10 = 19, + INVALID_PARAMETER_11_OR_MORE = 20, + PENDING = 21, + NO_MEMORY = 22, + INVALID_PARAMETER = 23, + OPERATION_FAILED = 24, + BUFFER_TOO_SMALL = 25, + END_OF_LIST = 26, + ALREADY_EXISTS = 27, + NOT_FOUND = 28, + OVERFLOW = 29, + TIMEOUT = 30, + NOT_IMPLEMENTED = 31, + NO_RESOURCES = 32, + UNINITIALIZED = 33, + NO_SERVER = 34, + INVALID_STATE = 35, + CONNECTION_FAILED = 37, + CONNECTION_LOST = 38, + UNAUTHORIZED = 40, + CONFLICT = 41, + INVALID_FORMAT = 42, + NO_MATCH = 43, + PROTOCOL_ERROR = 44, + VERSION = 45, + MALFORMED_ADDRESS = 46, + COULD_NOT_READ_FILE = 47, + FILE_NOT_FOUND = 48, + DIRECTORY_NOT_FOUND = 49, + CONVERSION_ERROR = 50, + INCOMPATIBLE_TYPES = 51, + FILE_CORRUPTED = 56, + PAGE_NOT_FOUND = 57, + ILLEGAL_REQUEST = 62, + INVALID_GROUP = 64, + TABLE_FULL = 65, + IGNORE = 69, + AGAIN = 70, + DEVICE_NOT_FOUND = 71, + OBSOLETE = 72, +} + +export enum StatusCodeAPS { + // A request has been executed successfully. + SUCCESS = 0x00, + // A transmit request failed since the ASDU is too large and fragmentation is not supported. + ASDU_TOO_LONG = 0xa0, + // A received fragmented frame could not be defragmented at the current time. + DEFRAG_DEFERRED = 0xa1, + // A received fragmented frame could not be defragmented since the device does not support fragmentation. + DEFRAG_UNSUPPORTED = 0xa2, + // A parameter value was out of range. + ILLEGAL_REQUEST = 0xa3, + // An APSME-UNBIND.request failed due to the requested binding link not existing in the binding table. + INVALID_BINDING = 0xa4, + // An APSME-REMOVE-GROUP.request has been issued with a group identifier that does not appear in the group table. + INVALID_GROUP = 0xa5, + // A parameter value was invalid or out of range. + INVALID_PARAMETER = 0xa6, + // An APSDE-DATA.request requesting acknowledged trans- mission failed due to no acknowledgement being received. + NO_ACK = 0xa7, + // An APSDE-DATA.request with a destination addressing mode set to 0x00 failed due to there being no devices bound to this device. + NO_BOUND_DEVICE = 0xa8, + // An APSDE-DATA.request with a destination addressing mode set to 0x03 failed due to no corresponding short address found in the address map table. + NO_SHORT_ADDRESS = 0xa9, + // An APSDE-DATA.request with a destination addressing mode set to 0x00 failed due to a binding table not being supported on the device. + NOT_SUPPORTED = 0xaa, + // An ASDU was received that was secured using a link key. + SECURED_LINK_KEY = 0xab, + // An ASDU was received that was secured using a network key. + SECURED_NWK_KEY = 0xac, + // An APSDE-DATA.request requesting security has resulted in an error during the corresponding security processing. + SECURITY_FAIL = 0xad, + // An APSME-BIND.request or APSME.ADD-GROUP.request issued when the binding or group tables, respectively, were full. + TABLE_FULL = 0xae, + // An ASDU was received without any security. + UNSECURED = 0xaf, + // An APSME-GET.request or APSME-SET.request has been issued with an unknown attribute identifier. + UNSUPPORTED_ATTRIBUTE = 0xb0, +} + +export enum StatusCodeCBKE { + // The Issuer field within the key establishment partner's certificate is unknown to the sending device + UNKNOWN_ISSUER = 1, + // The device could not confirm that it shares the same key with the corresponding device + BAD_KEY_CONFIRM = 2, + // The device received a bad message from the corresponding device + BAD_MESSAGE = 3, + // The device does not currently have the internal resources necessary to perform key establishment + NO_RESOURCES = 4, + // The device does not support the specified key establishment suite in the partner's Initiate Key Establishment message + UNSUPPORTED_SUITE = 5, + // The received certificate specifies a type, curve, hash, or other parameter that is either unsupported by the device or invalid + INVALID_CERTIFICATE = 6, + // Non-standard ZBOSS extension: SE KE endpoint not found + NO_KE_EP = 7, +} + +/** + * Enum of the network state + */ +export enum NetworkState { + OFFLINE = 0x00 /*!< The network is offline */, + JOINING = 0x01 /*!< Joinging the network */, + CONNECTED = 0x02 /*!< Conneted with the network */, + LEAVING = 0x03 /*!< Leaving the network */, + CONFIRM = 0x04 /*!< Confirm the APS */, + INDICATION = 0x05 /*!< Indication the APS */, +} + +/** + * Enum of the network security mode + */ +export enum EspNCPSecur { + ESP_NCP_NO_SECURITY = 0x00 /*!< The network is no security mode */, + ESP_NCP_PRECONFIGURED_NETWORK_KEY = 0x01 /*!< Pre-configured the network key */, + ESP_NCP_NETWORK_KEY_FROM_TC = 0x02, + ESP_NCP_ONLY_TCLK = 0x03, +} + +export enum DeviceType { + COORDINATOR = 0x00, + ROUTER = 0x01, + ED = 0x02, + NONE = 0x03, +} + +export enum CommandId { + // NCP config + GET_MODULE_VERSION = 0x0001, + NCP_RESET = 0x0002, + GET_ZIGBEE_ROLE = 0x0004, + SET_ZIGBEE_ROLE = 0x0005, + GET_ZIGBEE_CHANNEL_MASK = 0x0006, + SET_ZIGBEE_CHANNEL_MASK = 0x0007, + GET_ZIGBEE_CHANNEL = 0x0008, + GET_PAN_ID = 0x0009, + SET_PAN_ID = 0x000a, + GET_LOCAL_IEEE_ADDR = 0x000b, + SET_LOCAL_IEEE_ADDR = 0x000c, + GET_TX_POWER = 0x0010, + SET_TX_POWER = 0x0011, + GET_RX_ON_WHEN_IDLE = 0x0012, + SET_RX_ON_WHEN_IDLE = 0x0013, + GET_JOINED = 0x0014, + GET_AUTHENTICATED = 0x0015, + GET_ED_TIMEOUT = 0x0016, + SET_ED_TIMEOUT = 0x0017, + SET_NWK_KEY = 0x001b, + GET_NWK_KEYS = 0x001e, + GET_APS_KEY_BY_IEEE = 0x001f, + GET_PARENT_ADDRESS = 0x0022, + GET_EXTENDED_PAN_ID = 0x0023, + GET_COORDINATOR_VERSION = 0x0024, + GET_SHORT_ADDRESS = 0x0025, + GET_TRUST_CENTER_ADDRESS = 0x0026, + NCP_RESET_IND = 0x002b, + NVRAM_WRITE = 0x002e, + NVRAM_READ = 0x002f, + NVRAM_ERASE = 0x0030, + NVRAM_CLEAR = 0x0031, + SET_TC_POLICY = 0x0032, + SET_EXTENDED_PAN_ID = 0x0033, + SET_MAX_CHILDREN = 0x0034, + GET_MAX_CHILDREN = 0x0035, + + // Application Framework + AF_SET_SIMPLE_DESC = 0x0101, + AF_DEL_SIMPLE_DESC = 0x0102, + AF_SET_NODE_DESC = 0x0103, + AF_SET_POWER_DESC = 0x0104, + + // Zigbee Device Object + ZDO_NWK_ADDR_REQ = 0x0201, + ZDO_IEEE_ADDR_REQ = 0x0202, + ZDO_POWER_DESC_REQ = 0x0203, + ZDO_NODE_DESC_REQ = 0x0204, + ZDO_SIMPLE_DESC_REQ = 0x0205, + ZDO_ACTIVE_EP_REQ = 0x0206, + ZDO_MATCH_DESC_REQ = 0x0207, + ZDO_BIND_REQ = 0x0208, + ZDO_UNBIND_REQ = 0x0209, + ZDO_MGMT_LEAVE_REQ = 0x020a, + ZDO_PERMIT_JOINING_REQ = 0x020b, + ZDO_DEV_ANNCE_IND = 0x020c, + ZDO_REJOIN = 0x020d, + ZDO_SYSTEM_SRV_DISCOVERY_REQ = 0x020e, + ZDO_MGMT_BIND_REQ = 0x020f, + ZDO_MGMT_LQI_REQ = 0x0210, + ZDO_MGMT_NWK_UPDATE_REQ = 0x0211, + ZDO_GET_STATS = 0x0213, + ZDO_DEV_AUTHORIZED_IND = 0x0214, + ZDO_DEV_UPDATE_IND = 0x0215, + ZDO_SET_NODE_DESC_MANUF_CODE = 0x0216, + + // Application Support Sub-layer + APSDE_DATA_REQ = 0x0301, + APSME_BIND = 0x0302, + APSME_UNBIND = 0x0303, + APSME_ADD_GROUP = 0x0304, + APSME_RM_GROUP = 0x0305, + APSDE_DATA_IND = 0x0306, + APSME_RM_ALL_GROUPS = 0x0307, + APS_CHECK_BINDING = 0x0308, + APS_GET_GROUP_TABLE = 0x0309, + APSME_UNBIND_ALL = 0x030a, + + // Network Layer + NWK_FORMATION = 0x0401, + NWK_DISCOVERY = 0x0402, + NWK_NLME_JOIN = 0x0403, + NWK_PERMIT_JOINING = 0x0404, + NWK_GET_IEEE_BY_SHORT = 0x0405, + NWK_GET_SHORT_BY_IEEE = 0x0406, + NWK_GET_NEIGHBOR_BY_IEEE = 0x0407, + NWK_REJOINED_IND = 0x0409, + NWK_REJOIN_FAILED_IND = 0x040a, + NWK_LEAVE_IND = 0x040b, + PIM_SET_FAST_POLL_INTERVAL = 0x040e, + PIM_SET_LONG_POLL_INTERVAL = 0x040f, + PIM_START_FAST_POLL = 0x0410, + PIM_START_LONG_POLL = 0x0411, + PIM_START_POLL = 0x0412, + PIM_STOP_FAST_POLL = 0x0414, + PIM_STOP_POLL = 0x0415, + PIM_ENABLE_TURBO_POLL = 0x0416, + PIM_DISABLE_TURBO_POLL = 0x0417, + NWK_PAN_ID_CONFLICT_RESOLVE = 0x041a, + NWK_PAN_ID_CONFLICT_IND = 0x041b, + NWK_ADDRESS_UPDATE_IND = 0x041c, + NWK_START_WITHOUT_FORMATION = 0x041d, + NWK_NLME_ROUTER_START = 0x041e, + PARENT_LOST_IND = 0x0420, + PIM_START_TURBO_POLL_PACKETS = 0x0424, + PIM_START_TURBO_POLL_CONTINUOUS = 0x0425, + PIM_TURBO_POLL_CONTINUOUS_LEAVE = 0x0426, + PIM_TURBO_POLL_PACKETS_LEAVE = 0x0427, + PIM_PERMIT_TURBO_POLL = 0x0428, + PIM_SET_FAST_POLL_TIMEOUT = 0x0429, + PIM_GET_LONG_POLL_INTERVAL = 0x042a, + PIM_GET_IN_FAST_POLL_FLAG = 0x042b, + SET_KEEPALIVE_MOVE = 0x042c, + START_CONCENTRATOR_MODE = 0x042d, + STOP_CONCENTRATOR_MODE = 0x042e, + NWK_ENABLE_PAN_ID_CONFLICT_RESOLUTION = 0x042f, + NWK_ENABLE_AUTO_PAN_ID_CONFLICT_RESOLUTION = 0x0430, + PIM_TURBO_POLL_CANCEL_PACKET = 0x0431, + + // Security + SECUR_SET_LOCAL_IC = 0x0501, + SECUR_ADD_IC = 0x0502, + SECUR_DEL_IC = 0x0503, + SECUR_GET_LOCAL_IC = 0x050d, + SECUR_TCLK_IND = 0x050e, + SECUR_TCLK_EXCHANGE_FAILED_IND = 0x050f, + SECUR_NWK_INITIATE_KEY_SWITCH_PROCEDURE = 0x0517, + SECUR_GET_IC_LIST = 0x0518, + SECUR_GET_IC_BY_IDX = 0x0519, + SECUR_REMOVE_ALL_IC = 0x051a, + + /////////////////// + UNKNOWN_1 = 0x0a02, +} + +export enum ResetOptions { + NoOptions = 0, + EraseNVRAM = 1, + FactoryReset = 2, + LockReadingKeys = 3, +} + +export enum ResetSource { + RESET_SRC_POWER_ON = 0, + RESET_SRC_SW_RESET = 1, + RESET_SRC_RESET_PIN = 2, + RESET_SRC_BROWN_OUT = 3, + RESET_SRC_CLOCK_LOSS = 4, + RESET_SRC_OTHER = 5, +} + +export enum PolicyType { + LINK_KEY_REQUIRED = 0, + IC_REQUIRED = 1, + TC_REJOIN_ENABLED = 2, + IGNORE_TC_REJOIN = 3, + APS_INSECURE_JOIN = 4, + DISABLE_NWK_MGMT_CHANNEL_UPDATE = 5, +} + +export enum BuffaloZBOSSDataType { + LIST_TYPED = 3000, + EXTENDED_PAN_ID = 3001, +} + +export enum DeviceUpdateStatus { + SECURE_REJOIN = 0, + UNSECURE_REJOIN = 1, + LEFT = 2, + TC_REJOIN = 3, +} diff --git a/src/adapter/zboss/frame.ts b/src/adapter/zboss/frame.ts new file mode 100644 index 0000000000..16b9a6d49f --- /dev/null +++ b/src/adapter/zboss/frame.ts @@ -0,0 +1,175 @@ +/* istanbul ignore file */ + +import {KeyValue} from '../../controller/tstype'; +import {DataType} from '../../zspec/zcl'; +import {BuffaloZcl} from '../../zspec/zcl/buffaloZcl'; +import {BuffaloZclDataType} from '../../zspec/zcl/definition/enums'; +import {BuffaloZclOptions} from '../../zspec/zcl/definition/tstype'; +import {FRAMES, ParamsDesc} from './commands'; +import {BuffaloZBOSSDataType, CommandId} from './enums'; + +export class ZBOSSBuffaloZcl extends BuffaloZcl { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public write(type: DataType | BuffaloZclDataType | BuffaloZBOSSDataType, value: any, options: BuffaloZclOptions): void { + switch (type) { + case BuffaloZBOSSDataType.EXTENDED_PAN_ID: { + return this.writeBuffer(value, 8); + } + default: { + return super.write(type as DataType | BuffaloZclDataType, value, options); + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public read(type: DataType | BuffaloZclDataType | BuffaloZBOSSDataType, options: BuffaloZclOptions): any { + switch (type) { + case BuffaloZBOSSDataType.EXTENDED_PAN_ID: { + return this.readBuffer(8); + } + default: { + return super.read(type as DataType | BuffaloZclDataType, options); + } + } + } + + public writeByDesc(payload: KeyValue, params: ParamsDesc[]): number { + const start = this.getPosition(); + for (const parameter of params) { + const options: BuffaloZclOptions = {}; + + if (parameter.condition && !parameter.condition(payload, this)) { + continue; + } + if (parameter.options) parameter.options(payload, options); + + if (parameter.type == BuffaloZBOSSDataType.LIST_TYPED && parameter.typed) { + const internalPaload = payload[parameter.name]; + for (const value of internalPaload) { + this.writeByDesc(value, parameter.typed); + } + } else { + this.write(parameter.type as DataType, payload[parameter.name], options); + } + } + return this.getPosition() - start; + } + + public readByDesc(params: ParamsDesc[]): KeyValue { + const payload: KeyValue = {}; + + for (const parameter of params) { + const options: BuffaloZclOptions = {payload}; + + if (parameter.condition && !parameter.condition(payload, this)) { + continue; + } + if (parameter.options) parameter.options(payload, options); + + if (parameter.type == BuffaloZBOSSDataType.LIST_TYPED && parameter.typed) { + payload[parameter.name] = []; + + if (!this.isMore()) break; + + for (let i = 0; i < (options.length || 0); i++) { + const internalPaload = this.readByDesc(parameter.typed); + payload[parameter.name].push(internalPaload); + } + } else { + if (!this.isMore()) break; + + payload[parameter.name] = this.read(parameter.type as DataType, options); + } + } + + return payload; + } +} + +function getFrameDesc(type: FrameType, key: CommandId): ParamsDesc[] { + const frameDesc = FRAMES[key]; + if (!frameDesc) throw new Error(`Unrecognized frame type from FrameID ${key}`); + switch (type) { + case FrameType.REQUEST: + return frameDesc.request || []; + case FrameType.RESPONSE: + return frameDesc.response || []; + case FrameType.INDICATION: + return frameDesc.indication || []; + } +} + +export function readZBOSSFrame(buffer: Buffer): ZBOSSFrame { + const buf = new ZBOSSBuffaloZcl(buffer); + const version = buf.readUInt8(); + const type = buf.readUInt8(); + const commandId = buf.readUInt16(); + let tsn = 0; + if ([FrameType.REQUEST, FrameType.RESPONSE].includes(type)) { + tsn = buf.readUInt8(); + } + const payload = readPayload(type, commandId, buf); + + return { + version, + type, + commandId, + tsn, + payload, + }; +} + +export function writeZBOSSFrame(frame: ZBOSSFrame): Buffer { + const buf = new ZBOSSBuffaloZcl(Buffer.alloc(247)); + buf.writeInt8(frame.version); + buf.writeInt8(frame.type); + buf.writeUInt16(frame.commandId); + buf.writeUInt8(frame.tsn); + writePayload(frame.type, frame.commandId, frame.payload, buf); + return buf.getWritten(); +} + +export enum FrameType { + REQUEST = 0, + RESPONSE = 1, + INDICATION = 2, +} + +export interface ZBOSSFrame { + version: number; + type: FrameType; + commandId: CommandId; + tsn: number; + payload: KeyValue; +} + +export function makeFrame(type: FrameType, commandId: CommandId, params: KeyValue): ZBOSSFrame { + const frameDesc = getFrameDesc(type, commandId); + const payload: KeyValue = {}; + for (const parameter of frameDesc) { + // const options: BuffaloZclOptions = {payload}; + + if (parameter.condition && !parameter.condition(payload, undefined)) { + continue; + } + + payload[parameter.name] = params[parameter.name]; + } + return { + version: 0, + type: type, + commandId: commandId, + tsn: 0, + payload: payload, + }; +} + +function readPayload(type: FrameType, commandId: CommandId, buffalo: ZBOSSBuffaloZcl): KeyValue { + const frameDesc = getFrameDesc(type, commandId); + return buffalo.readByDesc(frameDesc); +} + +function writePayload(type: FrameType, commandId: CommandId, payload: KeyValue, buffalo: ZBOSSBuffaloZcl): number { + const frameDesc = getFrameDesc(type, commandId); + return buffalo.writeByDesc(payload, frameDesc); +} diff --git a/src/adapter/zboss/reader.ts b/src/adapter/zboss/reader.ts new file mode 100644 index 0000000000..71658e9810 --- /dev/null +++ b/src/adapter/zboss/reader.ts @@ -0,0 +1,64 @@ +/* istanbul ignore file */ + +import {Transform, TransformCallback, TransformOptions} from 'stream'; + +import {logger} from '../../utils/logger'; +import {SIGNATURE} from './consts'; + +const NS = 'zh:zboss:read'; + +export class ZBOSSReader 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; + + logger.debug(`<<< DATA [${chunk.toString('hex')}]`, NS); + // SIGNATURE - start of package + while ((position = data.indexOf(SIGNATURE)) !== -1) { + // need for read length + if (data.length > position + 3) { + const len = data.readUInt16LE(position + 1); + if (data.length >= position - 1 + len) { + const frame = data.subarray(position + 1, position + 1 + len); + logger.debug(`<<< FRAME [${frame.toString('hex')}]`, NS); + // emit the frame via 'data' event + this.push(frame); + + // if position not 1 - try to convert buffer before position to text - chip console output + if (position > 1) { + logger.debug(`<<< CONSOLE:\n\r${data.subarray(0, position - 1).toString()}`, NS); + } + // remove the frame from internal buffer (set below) + data = data.subarray(position + 1 + len); + if (data.length) logger.debug(`<<< TAIL [${data.toString('hex')}]`, NS); + } else { + logger.debug(`<<< Not enough data. Length=${data.length}, frame length=${len}. Waiting`, NS); + break; + } + } else { + logger.debug(`<<< Not enough data. Length=${data.length}. Waiting`, NS); + break; + } + } + + this.buffer = data; + + cb(); + } + + _flush(cb: TransformCallback): void { + this.push(this.buffer); + + this.buffer = Buffer.alloc(0); + + cb(); + } +} diff --git a/src/adapter/zboss/types.ts b/src/adapter/zboss/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/adapter/zboss/uart.ts b/src/adapter/zboss/uart.ts new file mode 100644 index 0000000000..1db8d7ec0a --- /dev/null +++ b/src/adapter/zboss/uart.ts @@ -0,0 +1,417 @@ +/* istanbul ignore file */ + +import {Socket} from 'net'; +import EventEmitter from 'stream'; + +import {Queue, Waitress} from '../../utils'; +import {logger} from '../../utils/logger'; +import wait from '../../utils/wait'; +import {SerialPort} from '../serialPort'; +import SocketPortUtils from '../socketPortUtils'; +import {SerialPortOptions} from '../tstype'; +import {SIGNATURE, ZBOSS_FLAG_FIRST_FRAGMENT, ZBOSS_FLAG_LAST_FRAGMENT, ZBOSS_NCP_API_HL} from './consts'; +import {readZBOSSFrame, writeZBOSSFrame, ZBOSSFrame} from './frame'; +import {ZBOSSReader} from './reader'; +import {crc8, crc16} from './utils'; +import {ZBOSSWriter} from './writer'; + +const NS = 'zh:zboss:uart'; + +export class ZBOSSUart extends EventEmitter { + private readonly portOptions: SerialPortOptions; + private serialPort?: SerialPort; + private socketPort?: Socket; + private writer: ZBOSSWriter; + private reader: ZBOSSReader; + private closing: boolean = false; + private sendSeq = 0; // next frame number to send + private recvSeq = 0; // next frame number to receive + private ackSeq = 0; // next number after the last accepted frame + private waitress: Waitress; + private queue: Queue; + public inReset = false; + + constructor(options: SerialPortOptions) { + super(); + + this.portOptions = options; + this.serialPort = undefined; + this.socketPort = undefined; + this.writer = new ZBOSSWriter(); + this.reader = new ZBOSSReader(); + this.queue = new Queue(1); + this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); + } + + public async resetNcp(): Promise { + if (this.closing) { + return false; + } + + logger.info(`NCP reset`, NS); + + try { + if (!this.portOpen) { + await this.openPort(); + } + + return true; + } catch (err) { + logger.error(`Failed to init port with error ${err}`, NS); + + return false; + } + } + + get portOpen(): boolean | undefined { + if (this.closing) { + return false; + } + if (SocketPortUtils.isTcpPath(this.portOptions.path!)) { + return this.socketPort && !this.socketPort.closed; + } else { + return this.serialPort && this.serialPort.isOpen; + } + } + + public async start(): Promise { + if (!this.portOpen) { + return false; + } + + logger.info(`UART starting`, NS); + + try { + if (this.serialPort != null) { + // clear read/write buffers + await this.serialPort.asyncFlush(); + } + } catch (err) { + logger.error(`Error while flushing before start: ${err}`, NS); + } + + return true; + } + + public async stop(): Promise { + this.closing = true; + this.queue.clear(); + await this.closePort(); + this.closing = false; + logger.info(`UART stopped`, NS); + } + + private async openPort(): Promise { + await this.closePort(); + + if (!SocketPortUtils.isTcpPath(this.portOptions.path!)) { + 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, + }; + + //@ts-expect-error Jest testing + if (this.portOptions.binding != null) { + //@ts-expect-error Jest testing + serialOpts.binding = this.portOptions.binding; + } + + logger.debug(`Opening serial port with ${JSON.stringify(serialOpts)}`, NS); + this.serialPort = new SerialPort(serialOpts); + + this.writer.pipe(this.serialPort); + + this.serialPort.pipe(this.reader); + this.reader.on('data', this.onPackage.bind(this)); + + try { + await this.serialPort.asyncOpen(); + logger.info(`Serial port opened`, NS); + + this.serialPort.once('close', this.onPortClose.bind(this)); + this.serialPort.on('error', this.onPortError.bind(this)); + } catch (error) { + await this.stop(); + + throw error; + } + } else { + const info = SocketPortUtils.parseTcpPath(this.portOptions.path!); + logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS); + + this.socketPort = new Socket(); + this.socketPort.setNoDelay(true); + this.socketPort.setKeepAlive(true, 15000); + + this.writer.pipe(this.socketPort); + + this.socketPort.pipe(this.reader); + this.reader.on('data', this.onPackage.bind(this)); + + return new Promise((resolve, reject): void => { + const openError = async (err: Error): Promise => { + await this.stop(); + + reject(err); + }; + + this.socketPort?.on('connect', () => { + logger.debug(`Socket connected`, NS); + }); + this.socketPort?.on('ready', async (): Promise => { + logger.info(`Socket ready`, NS); + 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); + }); + } + } + + public async closePort(): Promise { + if (this.serialPort?.isOpen) { + try { + await this.serialPort.asyncFlushAndClose(); + } catch (err) { + logger.error(`Failed to close serial port ${err}.`, NS); + } + + this.serialPort.removeAllListeners(); + this.serialPort = undefined; + } else if (this.socketPort != undefined && !this.socketPort.closed) { + this.socketPort.destroy(); + this.socketPort.removeAllListeners(); + this.socketPort = undefined; + } + } + + private async onPortClose(err: boolean | Error): Promise { + logger.info(`Port closed. Error? ${err ?? 'no'}`, NS); + if (this.inReset) { + await wait(3000); + await this.openPort(); + this.inReset = false; + } + } + + private async onPortError(error: Error): Promise { + logger.info(`Port error: ${error}`, NS); + } + + private async onPackage(data: Buffer): Promise { + if (this.inReset) return; + const len = data.readUInt16LE(0); + const pType = data.readUInt8(2); + const pFlags = data.readUInt8(3); + const isACK = (pFlags & 0x1) === 1; + const retransmit = ((pFlags >> 1) & 0x1) === 1; + const sequence = (pFlags >> 2) & 0x3; + const ACKseq = (pFlags >> 4) & 0x3; + const isFirst = ((pFlags >> 6) & 0x1) === 1; + const isLast = ((pFlags >> 7) & 0x1) === 1; + logger.debug( + `<-- package type ${pType}, flags ${pFlags.toString(16)}` + `${JSON.stringify({isACK, retransmit, sequence, ACKseq, isFirst, isLast})}`, + NS, + ); + + if (pType !== ZBOSS_NCP_API_HL) { + logger.error(`<-- Wrong package type: ${pType}`, NS); + return; + } + if (isACK) { + // ACKseq is received + this.handleACK(ACKseq); + return; + } + if (len <= 5) { + logger.debug(`<-- Empty package`, NS); + return; + } + + // header crc + const hCRC = data.readUInt8(4); + const hCRC8 = crc8(data.subarray(0, 4)); + if (hCRC !== hCRC8) { + logger.error(`<-- Wrong package header crc: is ${hCRC}, expected ${hCRC8}`, NS); + return; + } + + // body crc + const bCRC = data.readUInt16LE(5); + const body = data.subarray(7); + const bodyCRC16 = crc16(body); + + if (bCRC !== bodyCRC16) { + logger.error(`<-- Wrong package body crc: is ${bCRC}, expected ${bodyCRC16}`, NS); + return; + } + + this.recvSeq = sequence; + // Send ACK + logger.debug(`--> ACK (${this.recvSeq})`, NS); + await this.sendACK(this.recvSeq); + + try { + logger.debug(`<-- FRAME: ${body.toString('hex')}`, NS); + const frame = readZBOSSFrame(body); + if (frame) { + this.emit('frame', frame); + } + } catch (error) { + logger.debug(`<-- error ${(error as Error).stack}`, NS); + } + } + + public async sendFrame(frame: ZBOSSFrame): Promise { + try { + const buf = writeZBOSSFrame(frame); + logger.debug(`--> FRAME: ${buf.toString('hex')}`, NS); + let flags = (this.sendSeq & 0x03) << 2; // sequence + flags = flags | ZBOSS_FLAG_FIRST_FRAGMENT | ZBOSS_FLAG_LAST_FRAGMENT; + const pack = this.makePack(flags, buf); + const isACK = (flags & 0x1) === 1; + const retransmit = ((flags >> 1) & 0x1) === 1; + const sequence = (flags >> 2) & 0x3; + const ACKseq = (flags >> 4) & 0x3; + const isFirst = ((flags >> 6) & 0x1) === 1; + const isLast = ((flags >> 7) & 0x1) === 1; + logger.debug( + `--> package type ${ZBOSS_NCP_API_HL}, flags ${flags.toString(16)}` + + `${JSON.stringify({isACK, retransmit, sequence, ACKseq, isFirst, isLast})}`, + NS, + ); + logger.debug(`--> PACK: ${pack.toString('hex')}`, NS); + await this.sendDATA(pack); + } catch (error) { + logger.debug(`--> error ${(error as Error).stack}`, NS); + } + } + + private async sendDATA(data: Buffer, isACK: boolean = false): Promise { + const seq = this.sendSeq; + const nextSeq = this.sendSeq; + const ackSeq = this.recvSeq; + + return this.queue.execute(async (): Promise => { + try { + logger.debug(`--> DATA (${seq},${ackSeq},0): ${data.toString('hex')}`, NS); + if (!isACK) { + const waiter = this.waitFor(nextSeq); + this.writeBuffer(data); + logger.debug(`-?- waiting (${nextSeq})`, NS); + await waiter.start().promise; + logger.debug(`-+- waiting (${nextSeq}) success`, NS); + } else { + this.writeBuffer(data); + } + } catch (e1) { + logger.error(`--> Error: ${e1}`, NS); + logger.error(`-!- break waiting (${nextSeq})`, NS); + logger.error(`Can't send DATA frame (${seq},${ackSeq},0): ${data.toString('hex')}`, NS); + throw new Error(`sendDATA error: try 1: ${e1}`); + // try { + // await Wait(500); + // const waiter = this.waitFor(nextSeq); + // logger.debug(`->> DATA (${seq},${ackSeq},1): ${data.toString('hex')}`, NS); + // this.writeBuffer(data); + // logger.debug(`-?- rewaiting (${nextSeq})`, NS); + // await waiter.start().promise; + // logger.debug(`-+- rewaiting (${nextSeq}) success`, NS); + // } catch (e2) { + // logger.error(`--> Error: ${e2}`, NS); + // logger.error(`-!- break rewaiting (${nextSeq})`, NS); + // logger.error(`Can't resend DATA frame (${seq},${ackSeq},1): ${data.toString('hex')}`, NS); + // throw new Error(`sendDATA error: try 1: ${e1}, try 2: ${e2}`); + // } + } + }); + } + + private handleACK(ackSeq: number): boolean { + /* Handle an acknowledgement package */ + // next number after the last accepted package + this.ackSeq = ackSeq & 0x03; + + logger.debug(`<-- ACK (${this.ackSeq})`, NS); + + const handled = this.waitress.resolve(this.ackSeq); + + if (!handled && this.sendSeq !== this.ackSeq) { + // Packet confirmation received for {ackSeq}, but was expected {sendSeq} + // This happens when the chip has not yet received of the packet {sendSeq} from us, + // but has already sent us the next one. + logger.debug(`Unexpected packet sequence ${this.ackSeq} | ${this.sendSeq}`, NS); + } else { + // next + this.sendSeq = {0: 1, 1: 2, 2: 3, 3: 1}[this.sendSeq] || 1; + } + + return handled; + } + + private async sendACK(ackNum: number, retransmit: boolean = false): Promise { + /* Construct a acknowledgement package */ + + let flags = (ackNum & 0x03) << 4; // ACKseq + flags |= 0x01; // isACK + if (retransmit) { + flags |= 0x02; // retransmit + } + const ackPackage = this.makePack(flags, undefined); + const isACK = (flags & 0x1) === 1; + const sequence = (flags >> 2) & 0x3; + const ACKseq = (flags >> 4) & 0x3; + const isFirst = ((flags >> 6) & 0x1) === 1; + const isLast = ((flags >> 7) & 0x1) === 1; + logger.debug( + `--> package type ${ZBOSS_NCP_API_HL}, flags ${flags.toString(16)}` + + `${JSON.stringify({isACK, retransmit, sequence, ACKseq, isFirst, isLast})}`, + NS, + ); + logger.debug(`--> ACK: ${ackPackage.toString('hex')}`, NS); + await this.sendDATA(ackPackage, true); + } + + private writeBuffer(buffer: Buffer): void { + logger.debug(`--> [${buffer.toString('hex')}]`, NS); + this.writer.push(buffer); + } + + private makePack(flags: number, data?: Buffer): Buffer { + /* Construct a package */ + const packLen = 5 + (data ? data.length + 2 : 0); + const header = Buffer.alloc(7); + header.writeUint16BE(SIGNATURE); + header.writeUint16LE(packLen, 2); + header.writeUint8(ZBOSS_NCP_API_HL, 4); + header.writeUint8(flags, 5); + const hCRC8 = crc8(header.subarray(2, 6)); + header.writeUint8(hCRC8, 6); + if (data) { + const pCRC16 = Buffer.alloc(2); + pCRC16.writeUint16LE(crc16(data)); + return Buffer.concat([header, pCRC16, data]); + } else { + return header; + } + } + + private waitFor(sequence: number, timeout = 2000): {start: () => {promise: Promise; ID: number}; ID: number} { + return this.waitress.waitFor(sequence, timeout); + } + + private waitressTimeoutFormatter(matcher: number, timeout: number): string { + return `${matcher} after ${timeout}ms`; + } + + private waitressValidator(sequence: number, matcher: number): boolean { + return sequence === matcher; + } +} diff --git a/src/adapter/zboss/utils.ts b/src/adapter/zboss/utils.ts new file mode 100644 index 0000000000..4d63bf4671 --- /dev/null +++ b/src/adapter/zboss/utils.ts @@ -0,0 +1,58 @@ +/* istanbul ignore file */ + +const crc16Table = [ + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, + 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, + 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, + 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, + 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, + 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, + 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, + 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, + 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, + 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, +]; + +/** + * width=16 poly=0x1021 init=0x0000 refin=true refout=true xorout=0x0000 check=0x2189 residue=0x0000 name="CRC-16/KERMIT" + */ +export function crc16(data: Buffer): number { + let crc = 0x0000; + + for (const byte of data) { + crc = crc16Table[(crc ^ byte) & 0xff] ^ ((crc >> 8) & 0xff); + } + + return crc ^ (0x0 & 0xffff); +} + +const crc8Table = [ + 0xea, 0xd4, 0x96, 0xa8, 0x12, 0x2c, 0x6e, 0x50, 0x7f, 0x41, 0x03, 0x3d, 0x87, 0xb9, 0xfb, 0xc5, 0xa5, 0x9b, 0xd9, 0xe7, 0x5d, 0x63, 0x21, 0x1f, + 0x30, 0x0e, 0x4c, 0x72, 0xc8, 0xf6, 0xb4, 0x8a, 0x74, 0x4a, 0x08, 0x36, 0x8c, 0xb2, 0xf0, 0xce, 0xe1, 0xdf, 0x9d, 0xa3, 0x19, 0x27, 0x65, 0x5b, + 0x3b, 0x05, 0x47, 0x79, 0xc3, 0xfd, 0xbf, 0x81, 0xae, 0x90, 0xd2, 0xec, 0x56, 0x68, 0x2a, 0x14, 0xb3, 0x8d, 0xcf, 0xf1, 0x4b, 0x75, 0x37, 0x09, + 0x26, 0x18, 0x5a, 0x64, 0xde, 0xe0, 0xa2, 0x9c, 0xfc, 0xc2, 0x80, 0xbe, 0x04, 0x3a, 0x78, 0x46, 0x69, 0x57, 0x15, 0x2b, 0x91, 0xaf, 0xed, 0xd3, + 0x2d, 0x13, 0x51, 0x6f, 0xd5, 0xeb, 0xa9, 0x97, 0xb8, 0x86, 0xc4, 0xfa, 0x40, 0x7e, 0x3c, 0x02, 0x62, 0x5c, 0x1e, 0x20, 0x9a, 0xa4, 0xe6, 0xd8, + 0xf7, 0xc9, 0x8b, 0xb5, 0x0f, 0x31, 0x73, 0x4d, 0x58, 0x66, 0x24, 0x1a, 0xa0, 0x9e, 0xdc, 0xe2, 0xcd, 0xf3, 0xb1, 0x8f, 0x35, 0x0b, 0x49, 0x77, + 0x17, 0x29, 0x6b, 0x55, 0xef, 0xd1, 0x93, 0xad, 0x82, 0xbc, 0xfe, 0xc0, 0x7a, 0x44, 0x06, 0x38, 0xc6, 0xf8, 0xba, 0x84, 0x3e, 0x00, 0x42, 0x7c, + 0x53, 0x6d, 0x2f, 0x11, 0xab, 0x95, 0xd7, 0xe9, 0x89, 0xb7, 0xf5, 0xcb, 0x71, 0x4f, 0x0d, 0x33, 0x1c, 0x22, 0x60, 0x5e, 0xe4, 0xda, 0x98, 0xa6, + 0x01, 0x3f, 0x7d, 0x43, 0xf9, 0xc7, 0x85, 0xbb, 0x94, 0xaa, 0xe8, 0xd6, 0x6c, 0x52, 0x10, 0x2e, 0x4e, 0x70, 0x32, 0x0c, 0xb6, 0x88, 0xca, 0xf4, + 0xdb, 0xe5, 0xa7, 0x99, 0x23, 0x1d, 0x5f, 0x61, 0x9f, 0xa1, 0xe3, 0xdd, 0x67, 0x59, 0x1b, 0x25, 0x0a, 0x34, 0x76, 0x48, 0xf2, 0xcc, 0x8e, 0xb0, + 0xd0, 0xee, 0xac, 0x92, 0x28, 0x16, 0x54, 0x6a, 0x45, 0x7b, 0x39, 0x07, 0xbd, 0x83, 0xc1, 0xff, +]; +/** + * width=8 poly=0x4d init=0xff refin=true refout=true xorout=0xff check=0xd8 name="CRC-8/KOOP" + */ +export function crc8(data: Buffer): number { + let crc = 0x00; + + for (const byte of data) { + crc = crc8Table[(crc ^ byte) & 0xff]; + } + + return crc; +} diff --git a/src/adapter/zboss/writer.ts b/src/adapter/zboss/writer.ts new file mode 100644 index 0000000000..c3c20221e4 --- /dev/null +++ b/src/adapter/zboss/writer.ts @@ -0,0 +1,49 @@ +/* istanbul ignore file */ + +import {Readable, ReadableOptions} from 'stream'; + +export class ZBOSSWriter extends Readable { + private bytesToWrite: number[]; + + constructor(opts?: ReadableOptions) { + super(opts); + + this.bytesToWrite = []; + } + + private writeBytes(): void { + const buffer = Buffer.from(this.bytesToWrite); + this.bytesToWrite = []; + + // expensive and very verbose, enable locally only if necessary + // logger.debug(`>>>> [FRAME raw=${buffer.toString('hex')}]`, NS); + + // 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/test/controller.test.ts b/test/controller.test.ts index a4fbf3d72b..6eb861c465 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -7282,7 +7282,7 @@ describe('Controller', () => { try { await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: 'efr'}, null, null); } catch (e) { - expect(e).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate, ezsp, ember`)); + expect(e).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`)); } });