diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index f7f71607a7..df28b10d44 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -36,7 +36,6 @@ interface WaitressMatcher { class EZSPAdapter extends Adapter { private driver: Driver; - private port: SerialPortOptions; private waitress: Waitress; private interpanLock: boolean; private backupMan: EZSPAdapterBackup; @@ -46,24 +45,17 @@ class EZSPAdapter extends Adapter { public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); - this.port = serialPortOptions; + this.waitress = new Waitress( this.waitressValidator, this.waitressTimeoutFormatter ); this.interpanLock = false; - + const concurrent = adapterOptions && adapterOptions.concurrent ? adapterOptions.concurrent : 8; debug(`Adapter concurrent: ${concurrent}`); this.queue = new Queue(concurrent); - - this.driver = new Driver(this.port.path, { - baudRate: this.port.baudRate || 115200, - rtscts: this.port.rtscts, - parity: 'none', - stopBits: 1, - xon: true, - xoff: true - }, this.networkOptions, this.greenPowerGroup); + + this.driver = new Driver(this.serialPortOptions, this.networkOptions, this.greenPowerGroup); this.driver.on('deviceJoined', this.handleDeviceJoin.bind(this)); this.driver.on('deviceLeft', this.handleDeviceLeft.bind(this)); this.driver.on('incomingMessage', this.processMessage.bind(this)); diff --git a/src/adapter/ezsp/driver/driver.ts b/src/adapter/ezsp/driver/driver.ts index 8f3ebd8577..5dbffb0b5e 100644 --- a/src/adapter/ezsp/driver/driver.ts +++ b/src/adapter/ezsp/driver/driver.ts @@ -73,6 +73,8 @@ const IEEE_PREFIX_MFG_ID: IeeeMfg[] = [ {mfgId: 0x115F, prefix: [0x54,0xef,0x44]}, ]; const DEFAULT_MFG_ID = 0x1049; +// we make three attempts to send the request +const REQUEST_ATTEMPT_DELAYS = [500, 1000, 1500]; export class Driver extends EventEmitter { public ezsp: Ezsp; @@ -88,16 +90,12 @@ export class Driver extends EventEmitter { private multicast: Multicast; private waitress: Waitress; private transactionID = 1; - private port: string; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - private serialOpt: Record; + private serialOpt: TsType.SerialPortOptions; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - constructor(port: string, serialOpt: Record, nwkOpt: TsType.NetworkOptions, greenPowerGroup: number) { + constructor(serialOpt: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions, greenPowerGroup: number) { super(); this.nwkOpt = nwkOpt; - this.port = port; this.serialOpt = serialOpt; this.greenPowerGroup = greenPowerGroup; this.waitress = new Waitress( @@ -109,7 +107,7 @@ export class Driver extends EventEmitter { const pauses = [10, 30, 60]; let pause = 0; - // infinite retries + // infinite retries XXX: might want to hard fail after a while..? while (true) { debug.log(`Reset connection. Try ${attempts}`); try { @@ -119,9 +117,11 @@ export class Driver extends EventEmitter { } catch (e) { debug.error(`Reset error ${e.stack}`); attempts += 1; + if (pauses.length) { pause = pauses.shift(); } + debug.log(`Pause ${pause}sec before try ${attempts}`); await Wait(pause*1000); } @@ -140,7 +140,7 @@ export class Driver extends EventEmitter { this.ezsp.on('close', this.onClose.bind(this)); try { - await this.ezsp.connect(this.port, this.serialOpt); + await this.ezsp.connect(this.serialOpt); } catch (error) { debug.error(`EZSP could not connect: ${error.cause ?? error}`); @@ -195,13 +195,19 @@ export class Driver extends EventEmitter { if (await this.needsToBeInitialised(this.nwkOpt)) { const res = await this.ezsp.execCommand('networkState'); + debug.log(`Network state ${res.status}`); + if (res.status == EmberNetworkStatus.JOINED_NETWORK) { debug.log(`Leaving current network and forming new network`); + const st = await this.ezsp.leaveNetwork(); + console.assert(st == EmberStatus.NETWORK_DOWN, `leaveNetwork returned unexpected status: ${st}`); } - await this.form_network(); + + await this.formNetwork(); + result = 'reset'; } const state = (await this.ezsp.execCommand('networkState')).status; @@ -224,6 +230,7 @@ export class Driver extends EventEmitter { debug.log(`TRUST_CENTER_LINK_KEY: ${JSON.stringify(linkResult)}`); const netResult = await this.getKey(EmberKeyType.CURRENT_NETWORK_KEY); debug.log(`CURRENT_NETWORK_KEY: ${JSON.stringify(netResult)}`); + await Wait(1000); await this.ezsp.execCommand('setManufacturerCode', {code: DEFAULT_MFG_ID}); @@ -231,6 +238,7 @@ export class Driver extends EventEmitter { await this.multicast.startup([]); await this.multicast.subscribe(this.greenPowerGroup, 242); // await this.multicast.subscribe(1, 901); + return result; } @@ -248,7 +256,7 @@ export class Driver extends EventEmitter { return !valid; } - private async form_network(): Promise { + private async formNetwork(): Promise { let status; status = (await this.ezsp.execCommand('clearKeyTable')).status; console.assert(status == EmberStatus.SUCCESS, @@ -277,8 +285,12 @@ export class Driver extends EventEmitter { switch (true) { case (frameName === 'incomingMessageHandler'): { const eui64 = this.eui64ToNodeId.get(frame.sender); - const handled = this.waitress.resolve({address: frame.sender, payload: frame.message, - frame: frame.apsFrame}); + const handled = this.waitress.resolve({ + address: frame.sender, + payload: frame.message, + frame: frame.apsFrame + }); + if (!handled) { this.emit('incomingMessage', { messageType: frame.type, @@ -384,6 +396,7 @@ export class Driver extends EventEmitter { private async cleanupTClinkKey(ieee: EmberEUI64): Promise { // Remove tc link_key for the given device. const index = (await this.ezsp.execCommand('findKeyTableEntry', {address: ieee, linkKey: true})).index; + if (index != 0xFF) { await this.ezsp.execCommand('eraseKeyTableEntry', {index: index}); } @@ -393,6 +406,7 @@ export class Driver extends EventEmitter { relays: number): void { // todo debug.log(`handleRouteRecord: nwk=${nwk}, ieee=${ieee}, lqi=${lqi}, rssi=${rssi}, relays=${relays}`); + this.setNode(nwk, ieee); // if (ieee && !(ieee instanceof EmberEUI64)) { // ieee = new EmberEUI64(ieee); @@ -412,6 +426,7 @@ export class Driver extends EventEmitter { if (ieee && !(ieee instanceof EmberEUI64)) { ieee = new EmberEUI64(ieee); } + this.eui64ToNodeId.delete(ieee.toString()); this.emit('deviceLeft', [nwk, ieee]); } @@ -427,6 +442,7 @@ export class Driver extends EventEmitter { if (ieee && !(ieee instanceof EmberEUI64)) { ieee = new EmberEUI64(ieee); } + for(const rec of IEEE_PREFIX_MFG_ID) { if ((Buffer.from((ieee as EmberEUI64).value)).indexOf(Buffer.from(rec.prefix)) == 0) { // set ManufacturerCode @@ -444,24 +460,27 @@ export class Driver extends EventEmitter { if (ieee && !(ieee instanceof EmberEUI64)) { ieee = new EmberEUI64(ieee); } + this.eui64ToNodeId.set(ieee.toString(), nwk); } public async request(nwk: number | EmberEUI64, apsFrame: EmberApsFrame, - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ data: Buffer, extendedTimeout = false): Promise { let result = false; - // we make three attempts to send the request - for (const delay of [500, 1000, 1500]) { + + for (const delay of REQUEST_ATTEMPT_DELAYS) { try { const seq = (apsFrame.sequence + 1) & 0xFF; let eui64: EmberEUI64; + if (typeof nwk !== 'number') { eui64 = nwk as EmberEUI64; const strEui64 = eui64.toString(); let nodeId = this.eui64ToNodeId.get(strEui64); + if (nodeId === undefined) { nodeId = (await this.ezsp.execCommand('lookupNodeIdByEui64', {eui64: eui64})).nodeId; + if (nodeId && nodeId !== 0xFFFF) { this.eui64ToNodeId.set(strEui64, nodeId); } else { @@ -472,23 +491,28 @@ export class Driver extends EventEmitter { } else { eui64 = await this.networkIdToEUI64(nwk); } + if (this.ezsp.ezspV < 8) { // const route = this.eui64ToRelays.get(eui64.toString()); // if (route) { // const = await this.ezsp.execCommand('setSourceRoute', {eui64}); // // } } + if (extendedTimeout) { await this.ezsp.execCommand('setExtendedTimeout', {remoteEui64: eui64, extendedTimeout: true}); } + const sendResult = await this.ezsp.sendUnicast( EmberOutgoingMessageType.OUTGOING_DIRECT, nwk, apsFrame, seq, data ); + // repeat only for these statuses if ([EmberStatus.MAX_MESSAGE_LIMIT_REACHED, EmberStatus.NO_BUFFERS, EmberStatus.NETWORK_BUSY] .includes(sendResult.status)) { // need to repeat after pause debug.log(`Request send status ${sendResult.status}. Attempt to repeat the request`); + await Wait(delay); } else { result = (sendResult.status == EmberStatus.SUCCESS); @@ -499,6 +523,7 @@ export class Driver extends EventEmitter { break; } } + return result; } @@ -563,9 +588,11 @@ export class Driver extends EventEmitter { frame.groupId = 0; frame.options = (EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY || EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY); + if (!disableResponse) { frame.options ||= EmberApsOption.APS_OPTION_RETRY; } + return frame; } @@ -585,23 +612,35 @@ export class Driver extends EventEmitter { responseCmd: EmberZDOCmd, params: ParamsDesc): Promise { const requestName = EmberZDOCmd.valueName(EmberZDOCmd, requestCmd); const responseName = EmberZDOCmd.valueName(EmberZDOCmd, responseCmd); + debug.log(`ZDO ${requestName} params: ${JSON.stringify(params)}`); + const frame = this.makeApsFrame(requestCmd as number, false); const payload = this.makeZDOframe(requestCmd as number, {transId: frame.sequence, ...params}); - const waiter = this.waitFor(networkAddress, responseCmd as number, frame.sequence).start(); - // if the request takes longer than the timeout, avoid an unhandled promise rejection. - waiter.promise.catch(() => {}); - const res = await this.request(networkAddress, frame, payload); - if (!res) { - debug.error(`zdoRequest error`); + const waiter = this.waitFor(networkAddress, responseCmd as number, frame.sequence); + + try { + const res = await this.request(networkAddress, frame, payload); + + if (!res) { + throw Error('zdoRequest>request error'); + } + + const response = await waiter.start().promise; + + debug.log(`${responseName} frame: ${JSON.stringify(response.payload)}`); + + const result = new EZSPZDOResponseFrameData(responseCmd as number, response.payload); + + debug.log(`${responseName} parsed: ${JSON.stringify(result)}`); + + return result; + } catch (e) { this.waitress.remove(waiter.ID); - throw Error('ZdoRequest error'); + debug.error(`zdoRequest error: ${e} ${e.stack}`); + + throw e; } - const message = await waiter.promise; - debug.log(`${responseName} frame: ${JSON.stringify(message.payload)}`); - const result = this.parse_frame_payload(responseCmd as number, message.payload); - debug.log(`${responseName} parsed: ${JSON.stringify(result)}`); - return result; } private onClose(): void { @@ -611,7 +650,7 @@ export class Driver extends EventEmitter { public async stop(): Promise { if (this.ezsp) { debug.log('Stop driver'); - return this.ezsp.close(true); + return this.ezsp.close(); } } @@ -619,10 +658,13 @@ export class Driver extends EventEmitter { for (const [eUI64, value] of this.eui64ToNodeId) { if (value === nwk) return new EmberEUI64(eUI64); } + const value = await this.ezsp.execCommand('lookupEui64ByNodeId', {nodeId: nwk}); + if (value.status === EmberStatus.SUCCESS) { const eUI64 = new EmberEUI64(value.eui64); this.eui64ToNodeId.set(eUI64.toString(), nwk); + return eUI64; } else { throw new Error('Unrecognized nodeId:' + nwk); @@ -634,9 +676,11 @@ export class Driver extends EventEmitter { const linkKey = new EmberKeyData(); linkKey.contents = Buffer.from("ZigBeeAlliance09"); const result = await this.addTransientLinkKey(ieee, linkKey); + if (result.status !== EmberStatus.SUCCESS) { throw new Error(`Add Transient Link Key for '${ieee}' failed`); } + if (this.ezsp.ezspV >= 8) { await this.ezsp.setPolicy(EzspPolicyId.TRUST_CENTER_POLICY, EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS | EzspDecisionBitmask.ALLOW_JOINS); @@ -648,14 +692,10 @@ export class Driver extends EventEmitter { return this.ezsp.execCommand('permitJoining', {duration: seconds}); } - public makeZDOframe(name: string|number, params: ParamsDesc): Buffer { + public makeZDOframe(name: string | number, params: ParamsDesc): Buffer { return this.ezsp.makeZDOframe(name, params); } - public parse_frame_payload(name: string|number, obj: Buffer): EZSPZDOResponseFrameData { - return this.ezsp.parse_frame_payload(name, obj); - } - public async addEndpoint({ endpoint = 1, profileId = 260, diff --git a/src/adapter/ezsp/driver/ezsp.ts b/src/adapter/ezsp/driver/ezsp.ts index 58c4f8dffd..dd82d0dd7a 100644 --- a/src/adapter/ezsp/driver/ezsp.ts +++ b/src/adapter/ezsp/driver/ezsp.ts @@ -2,9 +2,16 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import * as t from './types'; import {SerialDriver} from './uart'; -import {FRAMES, FRAME_NAMES_BY_ID, EZSPFrameDesc, ParamsDesc, ZDOREQUESTS, ZDOREQUEST_NAME_BY_ID, - ZDORESPONSES, ZDORESPONSE_NAME_BY_ID} from './commands'; - +import { + FRAMES, + FRAME_NAMES_BY_ID, + EZSPFrameDesc, + ParamsDesc, + ZDOREQUESTS, + ZDOREQUEST_NAME_BY_ID, + ZDORESPONSES, + ZDORESPONSE_NAME_BY_ID +} from './commands'; import { EmberStatus, EmberOutgoingMessageType, @@ -19,6 +26,7 @@ import {EventEmitter} from 'events'; import {EmberApsFrame, EmberNetworkParameters} from './types/struct'; import {Queue, Waitress, Wait} from '../../../utils'; import Debug from "debug"; +import {SerialPortOptions} from '../../tstype'; const debug = { @@ -262,12 +270,12 @@ export class Ezsp extends EventEmitter { this.serialDriver.on('close', this.onClose.bind(this)); } - public async connect(path: string, options: Record): Promise { + public async connect(options: SerialPortOptions): Promise { let lastError = null; for (let i = 1; i <= MAX_SERIAL_CONNECT_ATTEMPTS; i++) { try { - await this.serialDriver.connect(path, options); + await this.serialDriver.connect(options); break; } catch (error) { debug.error(`Connection attempt ${i} error: ${error.stack}`); @@ -297,60 +305,68 @@ export class Ezsp extends EventEmitter { private onClose(): void { debug.log('Close ezsp'); + this.emit('close'); } - public async close(force: boolean): Promise { + public async close(): Promise { debug.log('Stop ezsp'); - if (force) { - clearTimeout(this.watchdogTimer); - } + + clearTimeout(this.watchdogTimer); this.queue.clear(); await this.serialDriver.close(); } - private getFrameDesc(name: string): EZSPFrameDesc { - return (name in FRAMES) ? FRAMES[name] : null; - } - + /** + * Handle a received EZSP frame + * + * The protocol has taken care of UART specific framing etc, so we should + * just have EZSP application stuff here, with all escaping/stuffing and + * data randomization removed. + * @param data + */ private onFrameReceived(data: Buffer): void { - /*Handle a received EZSP frame - - The protocol has taken care of UART specific framing etc, so we should - just have EZSP application stuff here, with all escaping/stuffing and - data randomization removed. - */ debug.log(`<== Frame: ${data.toString('hex')}`); - let frame_id: number, sequence; + + let frameId: number; + const sequence = data[0]; + if ((this.ezspV < 8)) { - [sequence, frame_id, data] = [data[0], data[2], data.subarray(3)]; + [frameId, data] = [data[2], data.subarray(3)]; } else { - sequence = data[0]; - [[frame_id], data] = t.deserialize(data.subarray(3), [t.uint16_t]); + [[frameId], data] = t.deserialize(data.subarray(3), [t.uint16_t]); } - if ((frame_id === 255)) { - frame_id = 0; + + if ((frameId === 255)) { + frameId = 0; + if ((data.length > 1)) { - frame_id = data[1]; + frameId = data[1]; data = data.subarray(2); } } - const frm = EZSPFrameData.createFrame(this.ezspV, frame_id, false, data); + + const frm = EZSPFrameData.createFrame(this.ezspV, frameId, false, data); + if (!frm) { - debug.error(`Unparsed frame 0x${frame_id.toString(16)}. Skipped`); + debug.error(`Unparsed frame 0x${frameId.toString(16)}. Skipped`); return; } - debug.log(`<== 0x${frame_id.toString(16)}: ${JSON.stringify(frm)}`); + + debug.log(`<== 0x${frameId.toString(16)}: ${JSON.stringify(frm)}`); + const handled = this.waitress.resolve({ - frameId: frame_id, + frameId, frameName: frm.name, - sequence: sequence, + sequence, payload: frm }); - if (!handled) this.emit('frame', frm.name, frm); + if (!handled) { + this.emit('frame', frm.name, frm); + } - if ((frame_id === 0)) { + if ((frameId === 0)) { this.ezspV = frm.protocolVersion; } } @@ -358,44 +374,52 @@ export class Ezsp extends EventEmitter { async version(): Promise { const version = this.ezspV; const result = await this.execCommand("version", {desiredProtocolVersion: version}); + if ((result.protocolVersion !== version)) { debug.log("Switching to eszp version %d", result.protocolVersion); + await this.execCommand("version", {desiredProtocolVersion: result.protocolVersion}); } + return result.protocolVersion; } async networkInit(): Promise { - const waiter = this.waitFor("stackStatusHandler", null).start(); - + const waiter = this.waitFor("stackStatusHandler", null); const result = await this.execCommand("networkInit"); + debug.log('network init result: ', JSON.stringify(result)); + if ((result.status !== EmberStatus.SUCCESS)) { this.waitress.remove(waiter.ID); debug.log("Failure to init network"); return false; } - const response = await waiter.promise; + const response = await waiter.start().promise; + return response.payload.status == EmberStatus.NETWORK_UP; } async leaveNetwork(): Promise { - const waiter = this.waitFor("stackStatusHandler", null).start(); - + const waiter = this.waitFor("stackStatusHandler", null); const result = await this.execCommand("leaveNetwork"); + debug.log('network init result', JSON.stringify(result)); + if ((result.status !== EmberStatus.SUCCESS)) { this.waitress.remove(waiter.ID); debug.log("Failure to leave network"); throw new Error(("Failure to leave network: " + JSON.stringify(result))); } - const response = await waiter.promise; + const response = await waiter.start().promise; + if ((response.payload.status !== EmberStatus.NETWORK_DOWN)) { debug.log("Wrong network status: " + JSON.stringify(response.payload)); throw new Error(("Wrong network status: " + JSON.stringify(response.payload))); } + return response.payload.status; } @@ -541,8 +565,11 @@ export class Ezsp extends EventEmitter { private makeFrame(name: string, params: ParamsDesc, seq: number): Buffer { const frmData = new EZSPFrameData(name, true, params); + debug.log(`==> ${JSON.stringify(frmData)}`); + const frame = [(seq & 255)]; + if ((this.ezspV < 8)) { if ((this.ezspV >= 5)) { frame.push(0x00, 0xFF, 0x00, frmData.id); @@ -551,49 +578,59 @@ export class Ezsp extends EventEmitter { } } else { const cmd_id = t.serialize([frmData.id], [t.uint16_t]); + frame.push(0x00, 0x01, ...cmd_id); } + return Buffer.concat([Buffer.from(frame), frmData.serialize()]); } public async execCommand(name: string, params: ParamsDesc = null): Promise { debug.log(`==> ${name}: ${JSON.stringify(params)}`); + if (!this.serialDriver.isInitialized()) { throw new Error('Connection not initialized'); } + return this.queue.execute(async (): Promise => { const data = this.makeFrame(name, params, this.cmdSeq); const waiter = this.waitFor(name, this.cmdSeq); this.cmdSeq = (this.cmdSeq + 1) & 255; - return this.serialDriver.sendDATA(data).then(async ()=>{ + + try { + await this.serialDriver.sendDATA(data); + const response = await waiter.start().promise; + return response.payload; - }).catch(() => { + } catch (error) { this.waitress.remove(waiter.ID); throw new Error(`Failure send ${name}:` + JSON.stringify(data)); - }); + } }); } async formNetwork(params: EmberNetworkParameters): Promise { - const waiter = this.waitFor("stackStatusHandler", null).start(); + const waiter = this.waitFor("stackStatusHandler", null); const v = await this.execCommand("formNetwork", {parameters: params}); + if ((v.status !== EmberStatus.SUCCESS)) { this.waitress.remove(waiter.ID); + debug.error("Failure forming network: " + JSON.stringify(v)); + throw new Error(("Failure forming network: " + JSON.stringify(v))); } - const response = await waiter.promise; + + const response = await waiter.start().promise; + if ((response.payload.status !== EmberStatus.NETWORK_UP)) { debug.error("Wrong network status: " + JSON.stringify(response.payload)); + throw new Error(("Wrong network status: " + JSON.stringify(response.payload))); } - return response.payload.status; - } - public parse_frame_payload(name: string|number, data: Buffer): EZSPZDOResponseFrameData { - const frame = new EZSPZDOResponseFrameData(name, data); - return frame; + return response.payload.status; } public sendUnicast(direct: EmberOutgoingMessageType, nwk: number, apsFrame: @@ -627,10 +664,13 @@ export class Ezsp extends EventEmitter { deliveryFailureThreshold: MTOR_DELIVERY_FAIL_THRESHOLD, maxHops: 0, }); + debug.log("Set concentrator type: %s", JSON.stringify(res)); + if (res.status != EmberStatus.SUCCESS) { debug.log("Couldn't set concentrator type %s: %s", true, JSON.stringify(res)); } + if (this.ezspV >= 8) { await this.execCommand('setSourceRouteDiscoveryMode', {mode: 1}); } @@ -667,11 +707,14 @@ export class Ezsp extends EventEmitter { private async watchdogHandler(): Promise { debug.log(`Time to watchdog ... ${this.failures}`); + try { await this.execCommand('nop'); } catch (error) { debug.error(`Watchdog heartbeat timeout ${error.stack}`); + this.failures += 1; + if (this.failures > MAX_WATCHDOG_FAILURES) { this.failures = 0; this.reset(); diff --git a/src/adapter/ezsp/driver/frame.ts b/src/adapter/ezsp/driver/frame.ts new file mode 100644 index 0000000000..3d2b874d3c --- /dev/null +++ b/src/adapter/ezsp/driver/frame.ts @@ -0,0 +1,100 @@ +/* istanbul ignore file */ +import {RANDOMIZE_SEQ, RANDOMIZE_START} from "./consts"; +import crc16ccitt from "./utils/crc16ccitt"; + +export enum FrameType { + UNKNOWN = 0, + ERROR = 1, + DATA = 2, + ACK = 3, + NAK = 4, + RST = 5, + RSTACK = 6, +} + +/** + * Basic class to handle uart-level frames + * https://www.silabs.com/documents/public/user-guides/ug101-uart-gateway-protocol-reference.pdf + */ +export class Frame { + /** + * Type of the Frame as determined by its control byte. + */ + public readonly type: FrameType; + public readonly buffer: Buffer; + + public constructor(buffer: Buffer) { + this.buffer = buffer; + + const ctrlByte = this.buffer[0]; + + if ((ctrlByte & 0x80) === 0) { + this.type = FrameType.DATA; + } else if ((ctrlByte & 0xE0) === 0x80) { + this.type = FrameType.ACK; + } else if ((ctrlByte & 0xE0) === 0xA0) { + this.type = FrameType.NAK; + } else if (ctrlByte === 0xC0) { + this.type = FrameType.RST; + } else if (ctrlByte === 0xC1) { + this.type = FrameType.RSTACK; + } else if (ctrlByte === 0xC2) { + this.type = FrameType.ERROR; + } else { + this.type = FrameType.UNKNOWN; + } + } + + get control(): number { + return this.buffer[0]; + } + + public static fromBuffer(buffer: Buffer): Frame { + return new Frame(buffer); + } + + /** + * XOR s with a pseudo-random sequence for transmission. + * Used only in data frames. + */ + public static makeRandomizedBuffer(buffer: Buffer): Buffer { + let rand = RANDOMIZE_START; + const out = Buffer.alloc(buffer.length); + let outIdx = 0; + + for (const c of buffer) { + out.writeUInt8(c ^ rand, outIdx++); + + if ((rand % 2)) { + rand = ((rand >> 1) ^ RANDOMIZE_SEQ); + } else { + rand = (rand >> 1); + } + } + + return out; + } + + /** + * Throws on CRC error. + */ + public checkCRC(): void { + const crc = crc16ccitt(this.buffer.subarray(0, -3), 65535); + const crcArr = Buffer.from([(crc >> 8), (crc % 256)]); + const subArr = this.buffer.subarray(-3, -1); + + if (!subArr.equals(crcArr)) { + throw new Error(`<-- CRC error: ${this.toString()}|${subArr.toString('hex')}|${crcArr.toString('hex')}`); + } + } + + /** + * + * @returns Buffer to hex string + */ + public toString(): string { + return this.buffer.toString('hex'); + } +} + +export default Frame; diff --git a/src/adapter/ezsp/driver/parser.ts b/src/adapter/ezsp/driver/parser.ts index 1974fccd18..69b7b92abb 100644 --- a/src/adapter/ezsp/driver/parser.ts +++ b/src/adapter/ezsp/driver/parser.ts @@ -2,6 +2,7 @@ import * as stream from 'stream'; import * as consts from './consts'; import Debug from "debug"; +import Frame from './frame'; const debug = Debug('zigbee-herdsman:adapter:ezsp:uart'); @@ -10,6 +11,7 @@ export class Parser extends stream.Transform { public constructor() { super(); + this.buffer = Buffer.from([]); } @@ -18,40 +20,45 @@ export class Parser extends stream.Transform { this.buffer = Buffer.from([]); chunk = chunk.subarray(chunk.lastIndexOf(consts.CANCEL) + 1); } + if (chunk.indexOf(consts.SUBSTITUTE) >= 0) { this.buffer = Buffer.from([]); chunk = chunk.subarray(chunk.indexOf(consts.FLAG) + 1); } + debug(`<-- [${chunk.toString('hex')}]`); + this.buffer = Buffer.concat([this.buffer, chunk]); + this.parseNext(); cb(); } private parseNext(): void { - if (this.buffer.length && this.buffer.indexOf(consts.FLAG) >= 0) { - //debug(`<-- [${this.buffer.toString('hex')}]`); - try { - const frame = this.extractFrame(); - if (frame) { - this.emit('parsed', frame); + if (this.buffer.length) { + const place = this.buffer.indexOf(consts.FLAG); + + if (place >= 0) { + const frameLength = place + 1; + + if (this.buffer.length >= frameLength) { + const frameBuffer = this.unstuff(this.buffer.subarray(0, frameLength)); + + try { + const frame = Frame.fromBuffer(frameBuffer); + + if (frame) { + debug(`--> parsed ${frame}`); + this.emit('parsed', frame); + } + } catch (error) { + debug(`--> error ${error.stack}`); + } + + this.buffer = this.buffer.subarray(frameLength); + this.parseNext(); } - } catch (error) { - debug(`<-- error ${error.stack}`); } - this.parseNext(); - } - } - - private extractFrame(): Buffer { - /* Extract a frame from the data buffer */ - const place = this.buffer.indexOf(consts.FLAG); - if (place >= 0) { - const result = this.unstuff(this.buffer.subarray(0, place + 1)); - this.buffer = this.buffer.subarray(place + 1); - return result; - } else { - return null; } } @@ -60,10 +67,13 @@ export class Parser extends stream.Transform { let escaped = false; const out = Buffer.alloc(s.length); let outIdx = 0; + for (let idx = 0; idx < s.length; idx += 1) { const c = s[idx]; + if (escaped) { out.writeUInt8(c ^ consts.STUFF, outIdx++); + escaped = false; } else { if (c === consts.ESCAPE) { @@ -75,6 +85,7 @@ export class Parser extends stream.Transform { } } } + return out.subarray(0, outIdx); } diff --git a/src/adapter/ezsp/driver/uart.ts b/src/adapter/ezsp/driver/uart.ts index fd8fcbf821..5bf6225ae0 100644 --- a/src/adapter/ezsp/driver/uart.ts +++ b/src/adapter/ezsp/driver/uart.ts @@ -3,12 +3,12 @@ import {EventEmitter} from 'events'; import net from 'net'; import {SerialPort} from '../../serialPort'; import SocketPortUtils from '../../socketPortUtils'; -import {crc16ccitt} from './utils'; import {Queue, Waitress, Wait} from '../../../utils'; -import * as consts from './consts'; import {Writer} from './writer'; import {Parser} from './parser'; +import {Frame as NpiFrame, FrameType} from './frame'; import Debug from "debug"; +import {SerialPortOptions} from '../../tstype'; const debug = Debug('zigbee-herdsman:adapter:ezsp:uart'); @@ -55,29 +55,33 @@ export class SerialDriver extends EventEmitter { this.waitressValidator, this.waitressTimeoutFormatter); } - async connect(path: string, options: Record): Promise { - this.portType = SocketPortUtils.isTcpPath(path) ? 'socket' : 'serial'; + async connect(options: SerialPortOptions): Promise { + this.portType = SocketPortUtils.isTcpPath(options.path) ? 'socket' : 'serial'; if (this.portType === 'serial') { - await this.openSerialPort(path, options); + await this.openSerialPort(options.path, options.baudRate, options.rtscts); } else { - await this.openSocketPort(path); + await this.openSocketPort(options.path); } } - private async openSerialPort(path: string, opt: Record): Promise { + private async openSerialPort(path: string, baudRate: number, rtscts: boolean): Promise { const options = { path, - baudRate: typeof opt.baudRate === 'number' ? opt.baudRate : 115200, - rtscts: typeof opt.rtscts === 'boolean' ? opt.rtscts : false, - autoOpen: false + baudRate: typeof baudRate === 'number' ? baudRate : 115200, + rtscts: typeof rtscts === 'boolean' ? rtscts : false, + autoOpen: false, + parity: 'none', + stopBits: 1, + xon: true, + xoff: true, }; debug(`Opening SerialPort with ${JSON.stringify(options)}`); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this.serialPort = new SerialPort(options); this.writer = new Writer(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore this.writer.pipe(this.serialPort); this.parser = new Parser(); @@ -122,25 +126,27 @@ export class SerialDriver extends EventEmitter { this.parser.on('parsed', this.onParsed.bind(this)); return new Promise((resolve, reject): void => { - this.socketPort.on('connect', function () { + this.socketPort.on('connect', () => { debug('Socket connected'); }); - // eslint-disable-next-line - const self = this; this.socketPort.on('ready', async (): Promise => { debug('Socket ready'); // reset await this.reset(); - self.initialized = true; + + this.initialized = true; + resolve(); }); this.socketPort.once('close', this.onPortClose.bind(this)); - this.socketPort.on('error', function () { + this.socketPort.on('error', () => { debug('Socket error'); - self.initialized = false; + + this.initialized = false; + reject(new Error(`Error while opening socket`)); }); @@ -148,55 +154,41 @@ export class SerialDriver extends EventEmitter { }); } - private onParsed(data: Buffer): void { - // check CRC - const crc = crc16ccitt(data.subarray(0, -3), 65535); - const crcArr = Buffer.from([(crc >> 8), (crc % 256)]); - if (!data.subarray(-3, -1).equals(crcArr)) { - // CRC error - debug(`<-- CRC error: ${data.toString('hex')}|` + - `${data.subarray(-3, -1).toString('hex')}|` + - `${crcArr.toString('hex')}`); + private onParsed(frame: NpiFrame): void { + try { + frame.checkCRC(); + } catch (error) { + debug(error); + // send NAK this.writer.sendNAK(this.recvSeq); // skip handler return; } + try { /* Frame receive handler */ - switch (true) { - case ((data[0] & 0x80) === 0): - debug(`<-- DATA (${(data[0] & 0x70) >> 4},`+ - `${data[0] & 0x07},${(data[0] & 0x08) >> 3}): ${data.toString('hex')}`); - this.handleDATA(data); + switch (frame.type) { + case FrameType.DATA: + this.handleDATA(frame); break; - - case ((data[0] & 0xE0) === 0x80): - debug(`<-- ACK (${data[0] & 0x07}): ${data.toString('hex')}`); - this.handleACK(data[0]); + case FrameType.ACK: + this.handleACK(frame); break; - - case ((data[0] & 0xE0) === 0xA0): - debug(`<-- NAK (${data[0] & 0x07}): ${data.toString('hex')}`); - this.handleNAK(data[0]); + case FrameType.NAK: + this.handleNAK(frame); break; - - case (data[0] === 0xC0): - debug(`<-- RST: ${data.toString('hex')}`); + case FrameType.RST: + this.handleRST(frame); break; - - case (data[0] === 0xC1): - debug(`<-- RSTACK: ${data.toString('hex')}`); - this.rstack_frame_received(data); + case FrameType.RSTACK: + this.handleRSTACK(frame); break; - - case (data[0] === 0xC2): - debug(`<-- Error: ${data.toString('hex')}`); - // send reset - this.reset().catch((e) => debug(`Failed to reset: ${e}`)); + case FrameType.ERROR: + this.handleError(frame); break; default: - debug("UNKNOWN FRAME RECEIVED: %r", data); + debug(`UNKNOWN FRAME RECEIVED: ${frame}`); } } catch (error) { @@ -204,40 +196,58 @@ export class SerialDriver extends EventEmitter { } } - private handleDATA(data: Buffer): void { + private handleDATA(frame: NpiFrame): void { /* Data frame receive handler */ - const frmNum = (data[0] & 0x70) >> 4; - const reTx = (data[0] & 0x08) >> 3; + const frmNum = (frame.control & 0x70) >> 4; + const reTx = (frame.control & 0x08) >> 3; + + debug(`<-- DATA (${frmNum},${frame.control & 0x07},${reTx}): ${frame}`); + this.recvSeq = (frmNum + 1) & 7; // next + debug(`--> ACK (${this.recvSeq})`); + this.writer.sendACK(this.recvSeq); - const handled = this.handleACK(data[0]); + + const handled = this.handleACK(frame); + if (reTx && !handled) { // if the package is resent and did not expect it, // then will skip it - already processed it earlier debug(`Skipping the packet as repeated (${this.recvSeq})`); + return; - } - data = data.subarray(1, -3); - const frame = this.randomize(data); - this.emit('received', frame); + } + + const data = frame.buffer.subarray(1, -3); + + this.emit('received', NpiFrame.makeRandomizedBuffer(data)); } - private handleACK(control: number): boolean { + private handleACK(frame: NpiFrame): boolean { /* Handle an acknowledgement frame */ // next number after the last accepted frame - this.ackSeq = control & 0x07; + this.ackSeq = frame.control & 0x07; + + debug.log(`<-- ACK (${this.ackSeq}): ${frame}`); + const handled = this.waitress.resolve({sequence: this.ackSeq}); + if (!handled && this.sendSeq !== this.ackSeq) { debug(`Unexpected packet sequence ${this.ackSeq} | ${this.sendSeq}`); } + return handled; } - private handleNAK(control: number): void { + private handleNAK(frame: NpiFrame): void { /* Handle negative acknowledgment frame */ - const nakNum = control & 0x07; + const nakNum = frame.control & 0x07; + + debug.log(`<-- NAK (${nakNum}): ${frame}`); + const handled = this.waitress.reject({sequence: nakNum}, 'Recv NAK frame'); + if (!handled) { // send NAK debug(`NAK Unexpected packet sequence ${nakNum}`); @@ -246,56 +256,63 @@ export class SerialDriver extends EventEmitter { } } - private rstack_frame_received(data: Buffer): void { + private handleRST(frame: NpiFrame): void { + debug(`<-- RST: ${frame}`); + } + + private handleRSTACK(frame: NpiFrame): void { /* Reset acknowledgement frame receive handler */ let code; this.sendSeq = 0; this.recvSeq = 0; + + debug(`<-- RSTACK ${frame}`); + try { - code = NcpResetCode[data[2]]; + code = NcpResetCode[frame.buffer[2]]; } catch (e) { code = NcpResetCode.ERROR_UNKNOWN_EM3XX_ERROR; } - debug("RSTACK Version: %d Reason: %s frame: %s", data[1], code.toString(), data.toString('hex')); + + debug(`RSTACK Version: ${frame.buffer[1]} Reason: ${code.toString()} frame: ${frame}`); + if (NcpResetCode[code].toString() !== NcpResetCode.RESET_SOFTWARE.toString()) { return; } + this.waitress.resolve({sequence: -1}); } - private randomize(s: Buffer): Buffer { - /*XOR s with a pseudo-random sequence for transmission - Used only in data frames - */ - let rand = consts.RANDOMIZE_START; - const out = Buffer.alloc(s.length); - let outIdx = 0; - for (const c of s) { - out.writeUInt8(c ^ rand, outIdx++); - if ((rand % 2)) { - rand = ((rand >> 1) ^ consts.RANDOMIZE_SEQ); - } else { - rand = (rand >> 1); - } + private async handleError(frame: NpiFrame): Promise { + debug(`<-- Error ${frame}`); + + try { + // send reset + await this.reset(); + } catch (error) { + debug(`Failed to reset on Error Frame: ${error}`); } - return out; } async reset(): Promise { debug('Uart reseting'); this.parser.reset(); this.queue.clear(); + return this.queue.execute(async (): Promise => { try { debug(`--> Write reset`); const waiter = this.waitFor(-1, 10000); + this.writer.sendReset(); debug(`-?- waiting reset`); await waiter.start().promise; debug(`-+- waiting reset success`); } catch (e) { debug(`--> Error: ${e}`); + this.emit('reset'); + throw new Error(`Reset error: ${e}`); } }); @@ -330,7 +347,9 @@ export class SerialDriver extends EventEmitter { private onPortClose(): void { debug('Port closed'); + this.initialized = false; + this.emit('close'); } @@ -345,7 +364,7 @@ export class SerialDriver extends EventEmitter { const ackSeq = this.recvSeq; return this.queue.execute(async (): Promise => { - const randData = this.randomize(data); + const randData = NpiFrame.makeRandomizedBuffer(data); try { const waiter = this.waitFor(nextSeq); @@ -371,7 +390,9 @@ export class SerialDriver extends EventEmitter { debug(`--> Error: ${e2}`); debug(`-!- break rewaiting (${nextSeq})`); debug(`Can't resend DATA frame (${seq},${ackSeq},1): ${data.toString('hex')}`); + this.emit('reset'); + throw new Error(`sendDATA error: try 1: ${e1}, try 2: ${e2}`); } } diff --git a/test/adapter/ezsp/uart.test.ts b/test/adapter/ezsp/uart.test.ts index e1cf2741c6..44774cf3cd 100644 --- a/test/adapter/ezsp/uart.test.ts +++ b/test/adapter/ezsp/uart.test.ts @@ -81,12 +81,19 @@ describe('UART', () => { }); it('Connect', async () => { - await serialDriver.connect("/dev/ttyACM0", {}); + await serialDriver.connect({path: "/dev/ttyACM0"}); expect(SerialPort).toHaveBeenCalledTimes(1); - expect(SerialPort).toHaveBeenCalledWith( - {"path": "/dev/ttyACM0", "autoOpen": false, "baudRate": 115200, "rtscts": false}, - ); + expect(SerialPort).toHaveBeenCalledWith( { + path: "/dev/ttyACM0", + baudRate: 115200, + rtscts: false, + autoOpen: false, + parity: 'none', + stopBits: 1, + xon: true, + xoff: true, + }); expect(mockSerialPortPipe).toHaveBeenCalledTimes(1); expect(mockSerialPortAsyncOpen).toHaveBeenCalledTimes(1);