Skip to content

Commit

Permalink
fix: EZSP: Some refactoring of UART level and packet processing. (#916)
Browse files Browse the repository at this point in the history
* simple parser tests

* Minor refactoring of UART level and packet processing

* lint

* slightly increased the packet sending timeout for slow connections (for example, when using the Multiprotocol addon on rpi3).

* Pause during stack initialization (for slow cases Multiprotocol on rpi3)

* fix test

* reduced the pause

* Allow error on stop connection
  • Loading branch information
kirovilya committed Feb 18, 2024
1 parent 1c68ce3 commit e397e38
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 100 deletions.
40 changes: 22 additions & 18 deletions src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,24 +249,26 @@ class EZSPAdapter extends Adapter {
}

public async permitJoin(seconds: number, networkAddress: number): Promise<void> {
return this.queue.execute<void>(async () => {
this.checkInterpanLock();
if (seconds) {
this.driver.preJoining();
}
if (networkAddress) {
const result = await this.driver.zdoRequest(
networkAddress, EmberZDOCmd.Mgmt_Permit_Joining_req,
EmberZDOCmd.Mgmt_Permit_Joining_rsp,
{duration: seconds, tcSignificant: false}
);
if (result.status !== EmberStatus.SUCCESS) {
throw new Error(`permitJoin for '${networkAddress}' failed`);
if (this.driver.ezsp.isInitialized()) {
return this.queue.execute<void>(async () => {
this.checkInterpanLock();
if (seconds) {
this.driver.preJoining();
}
} else {
await this.driver.permitJoining(seconds);
}
});
if (networkAddress) {
const result = await this.driver.zdoRequest(
networkAddress, EmberZDOCmd.Mgmt_Permit_Joining_req,
EmberZDOCmd.Mgmt_Permit_Joining_rsp,
{duration: seconds, tcSignificant: false}
);
if (result.status !== EmberStatus.SUCCESS) {
throw new Error(`permitJoin for '${networkAddress}' failed`);
}
} else {
await this.driver.permitJoining(seconds);
}
});
}
}

public async getCoordinatorVersion(): Promise<CoordinatorVersion> {
Expand Down Expand Up @@ -626,7 +628,9 @@ class EZSPAdapter extends Adapter {
}

public async backup(): Promise<Models.Backup> {
return this.backupMan.createBackup();
if (this.driver.ezsp.isInitialized()) {
return this.backupMan.createBackup();
}
}

public async restoreChannelInterPAN(): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions src/adapter/ezsp/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ export class Driver extends EventEmitter {
try {
// don't emit 'close' on stop since we don't want this to bubble back up as 'disconnected' to the controller.
await this.stop(false);
} catch (err) {
debug.error(`Stop error ${err.stack}`);
}
try {
await Wait(1000);
debug.log(`Startup again.`);
await this.startup();
} catch (err) {
debug.error(`Reset error ${err.stack}`);
Expand Down
34 changes: 29 additions & 5 deletions src/adapter/ezsp/driver/ezsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ export class Ezsp extends EventEmitter {
private queue: Queue;
private watchdogTimer: NodeJS.Timeout;
private failures = 0;
private inResetingProcess = false;

constructor() {
super();
Expand All @@ -344,6 +345,11 @@ export class Ezsp extends EventEmitter {
public async connect(options: SerialPortOptions): Promise<void> {
let lastError = null;

const resetForReconnect = (): void => {
throw new Error("Failure to connect");
};
this.serialDriver.on('reset', resetForReconnect);

for (let i = 1; i <= MAX_SERIAL_CONNECT_ATTEMPTS; i++) {
try {
await this.serialDriver.connect(options);
Expand All @@ -360,10 +366,14 @@ export class Ezsp extends EventEmitter {
}
}

this.serialDriver.off('reset', resetForReconnect);

if (!this.serialDriver.isInitialized()) {
throw new Error("Failure to connect", {cause: lastError});
}

this.inResetingProcess = false;

this.serialDriver.on('reset', this.onSerialReset.bind(this));

if (WATCHDOG_WAKE_PERIOD) {
Expand All @@ -374,14 +384,21 @@ export class Ezsp extends EventEmitter {
}
}

public isInitialized(): boolean {
return this.serialDriver?.isInitialized();
}

private onSerialReset(): void {
debug.log('onSerialReset()');
this.inResetingProcess = true;
this.emit('reset');
}

private onSerialClose(): void {
debug.log('onSerialClose()');
this.emit('close');
if (!this.inResetingProcess) {
this.emit('close');
}
}

public async close(emitClose: boolean): Promise<void> {
Expand Down Expand Up @@ -746,17 +763,24 @@ export class Ezsp extends EventEmitter {
private async watchdogHandler(): Promise<void> {
debug.log(`Time to watchdog ... ${this.failures}`);

if (this.inResetingProcess) {
debug.log('The reset process is in progress...');
return;
}

try {
await this.execCommand('nop');
} catch (error) {
debug.error(`Watchdog heartbeat timeout ${error.stack}`);

this.failures += 1;
if (!this.inResetingProcess) {
this.failures += 1;

if (this.failures > MAX_WATCHDOG_FAILURES) {
this.failures = 0;
if (this.failures > MAX_WATCHDOG_FAILURES) {
this.failures = 0;

this.emit('reset');
this.emit('reset');
}
}
}
}
Expand Down
80 changes: 32 additions & 48 deletions src/adapter/ezsp/driver/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import Frame from './frame';
const debug = Debug('zigbee-herdsman:adapter:ezsp:uart');

export class Parser extends stream.Transform {
private buffer: Buffer;
private tail: Buffer[];
private flagXONXOFF: boolean;

public constructor(flagXONXOFF: boolean = false) {
super();

this.flagXONXOFF = flagXONXOFF;
this.buffer = Buffer.from([]);
this.tail = [];
}

public _transform(chunk: Buffer, _: string, cb: () => void): void {
Expand All @@ -24,80 +24,64 @@ export class Parser extends stream.Transform {
}

if (chunk.indexOf(consts.CANCEL) >= 0) {
this.buffer = Buffer.from([]);
this.reset();
chunk = chunk.subarray(chunk.lastIndexOf(consts.CANCEL) + 1);
}

if (chunk.indexOf(consts.SUBSTITUTE) >= 0) {
this.buffer = Buffer.from([]);
this.reset();
chunk = chunk.subarray(chunk.indexOf(consts.FLAG) + 1);
}

debug(`<-- [${chunk.toString('hex')}]`);

this.buffer = Buffer.concat([this.buffer, chunk]);
let delimiterPlace = chunk.indexOf(consts.FLAG);

this.parseNext();
cb();
}

private parseNext(): void {
if (this.buffer.length) {
const place = this.buffer.indexOf(consts.FLAG);

if (place >= 0) {
const frameLength = place + 1;
while (delimiterPlace >= 0) {
const buffer = chunk.subarray(0, delimiterPlace + 1);
const frameBuffer = Buffer.from([...this.unstuff(Buffer.concat([...this.tail, buffer]))]);
this.reset();

if (this.buffer.length >= frameLength) {
const frameBuffer = this.unstuff(this.buffer.subarray(0, frameLength));
try {
const frame = Frame.fromBuffer(frameBuffer);

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();
if (frame) {
this.emit('parsed', frame);
}

} catch (error) {
debug(`<-- error ${error.stack}`);
}

chunk = chunk.subarray(delimiterPlace + 1);
delimiterPlace = chunk.indexOf(consts.FLAG);
}
}

private unstuff(s: Buffer): Buffer {
/* Unstuff (unescape) a string after receipt */
this.tail.push(chunk);
cb();
}

private* unstuff(buffer: Buffer): Generator<number> {
/* Unstuff (unescape) a buffer after receipt */
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];

for (const byte of buffer) {
if (escaped) {
out.writeUInt8(c ^ consts.STUFF, outIdx++);

yield byte ^ consts.STUFF;
escaped = false;
} else {
if (c === consts.ESCAPE) {
if (byte === consts.ESCAPE) {
escaped = true;
} else if (c === consts.XOFF || c === consts.XON) {
} else if (byte === consts.XOFF || byte === consts.XON) {
// skip
} else {
out.writeUInt8(c, outIdx++);
yield byte;
}
}
}

return out.subarray(0, outIdx);
}

public reset(): void {
// clear buffer
this.buffer = Buffer.from([]);
// clear tail
this.tail.length = 0;
}
}
Loading

0 comments on commit e397e38

Please sign in to comment.