Skip to content

Commit

Permalink
fix: Fixes & general cleanup in EZSP codebase (#874)
Browse files Browse the repository at this point in the history
* Fixes & general cleanup in EZSP codebase

* Jest coverage fix.

* Improved naming & ref PDF link added.
  • Loading branch information
Nerivec authored Jan 15, 2024
1 parent 1952f02 commit 39bc90a
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 203 deletions.
16 changes: 4 additions & 12 deletions src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ interface WaitressMatcher {

class EZSPAdapter extends Adapter {
private driver: Driver;
private port: SerialPortOptions;
private waitress: Waitress<Events.ZclDataPayload, WaitressMatcher>;
private interpanLock: boolean;
private backupMan: EZSPAdapterBackup;
Expand All @@ -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<Events.ZclDataPayload, WaitressMatcher>(
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));
Expand Down
106 changes: 73 additions & 33 deletions src/adapter/ezsp/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -88,16 +90,12 @@ export class Driver extends EventEmitter {
private multicast: Multicast;
private waitress: Waitress<EmberFrame, EmberWaitressMatcher>;
private transactionID = 1;
private port: string;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any*/
private serialOpt: Record<string, any>;
private serialOpt: TsType.SerialPortOptions;

/* eslint-disable-next-line @typescript-eslint/no-explicit-any*/
constructor(port: string, serialOpt: Record<string, any>, 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<EmberFrame, EmberWaitressMatcher>(
Expand All @@ -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 {
Expand All @@ -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);
}
Expand All @@ -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}`);

Expand Down Expand Up @@ -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;
Expand All @@ -224,13 +230,15 @@ 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});

this.multicast = new Multicast(this);
await this.multicast.startup([]);
await this.multicast.subscribe(this.greenPowerGroup, 242);
// await this.multicast.subscribe(1, 901);

return result;
}

Expand All @@ -248,7 +256,7 @@ export class Driver extends EventEmitter {
return !valid;
}

private async form_network(): Promise<void> {
private async formNetwork(): Promise<void> {
let status;
status = (await this.ezsp.execCommand('clearKeyTable')).status;
console.assert(status == EmberStatus.SUCCESS,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -384,6 +396,7 @@ export class Driver extends EventEmitter {
private async cleanupTClinkKey(ieee: EmberEUI64): Promise<void> {
// 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});
}
Expand All @@ -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);
Expand All @@ -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]);
}
Expand All @@ -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
Expand All @@ -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<boolean> {
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 {
Expand All @@ -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);
Expand All @@ -499,6 +523,7 @@ export class Driver extends EventEmitter {
break;
}
}

return result;
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -585,23 +612,35 @@ export class Driver extends EventEmitter {
responseCmd: EmberZDOCmd, params: ParamsDesc): Promise<EZSPZDOResponseFrameData> {
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 {
Expand All @@ -611,18 +650,21 @@ export class Driver extends EventEmitter {
public async stop(): Promise<void> {
if (this.ezsp) {
debug.log('Stop driver');
return this.ezsp.close(true);
return this.ezsp.close();
}
}

public async networkIdToEUI64(nwk: number): Promise<EmberEUI64> {
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);
Expand All @@ -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);
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 39bc90a

Please sign in to comment.