Skip to content

Commit

Permalink
fix: work around corrupted high nibble in ACK after soft-reset on 7.1…
Browse files Browse the repository at this point in the history
…9.x (#6409)
  • Loading branch information
AlCalzone authored Oct 17, 2023
1 parent 61b3fc9 commit 7f8f5b1
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/serial/src/ZWaveSerialPortBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export class ZWaveSerialPortBase extends PassThrough {
// Allow switching between modes
public mode: ZWaveSerialMode | undefined;

// Allow ignoring the high nibble of an ACK once to work around an issue in the 700 series firmware
public ignoreAckHighNibbleOnce(): void {
this.parser.ignoreAckHighNibble = true;
}

// Allow strongly-typed async iteration
declare public [Symbol.asyncIterator]: () => AsyncIterableIterator<
ZWaveSerialChunk
Expand Down
23 changes: 23 additions & 0 deletions packages/serial/src/parsers/SerialAPIParser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { num2hex } from "@zwave-js/shared";
import { Transform, type TransformCallback } from "node:stream";
import type { SerialLogger } from "../Logger";
import { MessageHeaders } from "../MessageHeaders";
Expand Down Expand Up @@ -26,6 +27,9 @@ export class SerialAPIParser extends Transform {

private receiveBuffer = Buffer.allocUnsafe(0);

// Allow ignoring the high nibble of an ACK once to work around an issue in the 700 series firmware
public ignoreAckHighNibble: boolean = false;

_transform(
chunk: any,
encoding: string,
Expand All @@ -42,6 +46,7 @@ export class SerialAPIParser extends Transform {
case MessageHeaders.ACK: {
this.logger?.ACK("inbound");
this.push(MessageHeaders.ACK);
this.ignoreAckHighNibble = false;
break;
}
case MessageHeaders.NAK: {
Expand All @@ -58,6 +63,24 @@ export class SerialAPIParser extends Transform {
// INS12350: A host or a Z-Wave chip waiting for new traffic MUST ignore all other
// byte values than 0x06 (ACK), 0x15 (NAK), 0x18 (CAN) or 0x01 (Data frame).

// Work around a bug in the 700 series firmware that causes the high nibble of an ACK
// to be corrupted after a soft reset
if (
this.ignoreAckHighNibble
&& (this.receiveBuffer[0] & 0x0f)
=== MessageHeaders.ACK
) {
this.logger?.message(
`received corrupted ACK: ${
num2hex(this.receiveBuffer[0])
}`,
);
this.logger?.ACK("inbound");
this.push(MessageHeaders.ACK);
this.ignoreAckHighNibble = false;
break;
}

// Scan ahead until the next valid byte and log the invalid bytes
while (skip < this.receiveBuffer.length) {
const byte = this.receiveBuffer[skip];
Expand Down
12 changes: 11 additions & 1 deletion packages/testing/src/MockController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { MockPortBinding } from "@zwave-js/serial/mock";
import { AsyncQueue } from "@zwave-js/shared";
import { TimedExpectation, createWrappingCounter } from "@zwave-js/shared/safe";
import { wait } from "alcalzone-shared/async";
import { randomInt } from "node:crypto";
import {
type MockControllerCapabilities,
getDefaultMockControllerCapabilities,
Expand Down Expand Up @@ -151,6 +152,8 @@ export class MockController {
public autoAckHostMessages: boolean = true;
/** Controls whether the controller automatically ACKs node frames before handling them */
public autoAckNodeFrames: boolean = true;
/** Allows reproducing issues with the 7.19.x firmware where the high nibble of the ACK after soft-reset is corrupted */
public corruptACK: boolean = false;

/** Gets called when parsed/chunked data is received from the serial port */
private async serialOnData(
Expand Down Expand Up @@ -342,7 +345,14 @@ export class MockController {
* Sends an ACK frame to the host
*/
public ackHostMessage(): void {
this.sendHeaderToHost(MessageHeaders.ACK);
if (this.corruptACK) {
const highNibble = randomInt(1, 0xf) << 4;
this.serial.emitData(
Buffer.from([highNibble | MessageHeaders.ACK]),
);
} else {
this.sendHeaderToHost(MessageHeaders.ACK);
}
}

/** Gets called when a {@link MockZWaveFrame} is received from a {@link MockNode} */
Expand Down
9 changes: 9 additions & 0 deletions packages/zwave-js/src/lib/driver/Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5013,6 +5013,15 @@ ${handlers.length} left`,

const result = createDeferredPromise<Message | undefined>();

// Work around an issue in the 700 series firmware where the ACK after a soft-reset has a random high nibble.
// This was broken in 7.19, not fixed so far
if (
msg.functionType === FunctionType.SoftReset
&& this.controller.sdkVersionGte("7.19.0")
) {
this.serial?.ignoreAckHighNibbleOnce();
}

this.serialAPIInterpreter = interpret(machine).onDone((evt) => {
this.serialAPIInterpreter?.stop();
this.serialAPIInterpreter = undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { integrationTest } from "../integrationTestSuite";

// Repro for https://github.com/zwave-js/node-zwave-js/issues/6399

integrationTest(
"Accept corrupted ACKs with a random high nibble after Soft Reset",
{
// debug: true,

controllerCapabilities: {
libraryVersion: "Z-Wave 7.19.1",
},

testBody: async (t, driver, node, mockController, mockNode) => {
mockController.clearReceivedHostMessages();

mockController.corruptACK = true;
await t.notThrowsAsync(driver.softReset());
mockController.corruptACK = false;
},
},
);

0 comments on commit 7f8f5b1

Please sign in to comment.