Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: work around corrupted high nibble in ACK after soft-reset on 7.19.x #6409

Merged
merged 4 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
22 changes: 22 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 Down Expand Up @@ -58,6 +62,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 @@ -2519,6 +2519,15 @@ export class Driver extends TypedEventEmitter<DriverEventCallbacks>
await this.sendMessage(new SoftResetRequest(this), {
supportCheck: false,
pauseSendThread: true,
// Work around an issue in the 700 series firmware where the ACK after a soft-reset has a random high nibble
onProgress: (progress) => {
// This was broken in 7.19, not fixed so far
if (this.controller.sdkVersionGte("7.19.0")) {
if (progress.state === TransactionState.Active) {
this.serial?.ignoreAckHighNibbleOnce();
}
}
},
});
} catch (e) {
this.controllerLog.print(
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;
},
},
);