From 91ddfa2da9dd51376b453378bbe787cb8c29ffae Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 24 Oct 2024 23:17:48 +0200 Subject: [PATCH 1/3] test: support simulating the bootloader in mocks and tests --- packages/testing/src/MockController.ts | 18 +++++++++++++++++- .../src/lib/test/integrationTestSuite.ts | 6 ++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/MockController.ts b/packages/testing/src/MockController.ts index 2b26e3d4193b..b2fc979d71d5 100644 --- a/packages/testing/src/MockController.ts +++ b/packages/testing/src/MockController.ts @@ -40,7 +40,14 @@ export class MockController { this.serial = options.serial; // Pipe the serial data through a parser, so we get complete message buffers or headers out the other end this.serialParser = new SerialAPIParser(); - this.serial.on("write", (data) => { + this.serial.on("write", async (data) => { + // Execute hooks for inspecting the raw data first + for (const behavior of this.behaviors) { + if (await behavior.onHostData?.(this.host, this, data)) { + return; + } + } + // Then parse the data normally this.serialParser.write(data); }); this.serialParser.on("data", (data) => this.serialOnData(data)); @@ -486,6 +493,15 @@ export class MockController { } export interface MockControllerBehavior { + /** + * Can be used to inspect raw data received from the host before it is processed by the serial parser and the mock controller. + * Return `true` to indicate that the data has been handled and should not be processed further. + */ + onHostData?: ( + host: ZWaveHost, + controller: MockController, + data: Buffer, + ) => Promise | boolean | undefined; /** Gets called when a message from the host is received. Return `true` to indicate that the message has been handled. */ onHostMessage?: ( host: ZWaveHost, diff --git a/packages/zwave-js/src/lib/test/integrationTestSuite.ts b/packages/zwave-js/src/lib/test/integrationTestSuite.ts index 95a8f6afc715..9f738899a40b 100644 --- a/packages/zwave-js/src/lib/test/integrationTestSuite.ts +++ b/packages/zwave-js/src/lib/test/integrationTestSuite.ts @@ -144,6 +144,12 @@ function suite( } }); + if (options.additionalDriverOptions?.allowBootloaderOnly) { + driver.once("bootloader ready", () => { + process.nextTick(resolve); + }); + } + continueStartup(); }); } From bdbe73a6424c249712806cdd7cf532eb14424420 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 24 Oct 2024 23:18:38 +0200 Subject: [PATCH 2/3] test: add reproduction for issue --- .../test/driver/bootloaderDetection.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/zwave-js/src/lib/test/driver/bootloaderDetection.test.ts diff --git a/packages/zwave-js/src/lib/test/driver/bootloaderDetection.test.ts b/packages/zwave-js/src/lib/test/driver/bootloaderDetection.test.ts new file mode 100644 index 000000000000..0f0e02934987 --- /dev/null +++ b/packages/zwave-js/src/lib/test/driver/bootloaderDetection.test.ts @@ -0,0 +1,45 @@ +import { type MockControllerBehavior } from "@zwave-js/testing"; +import { wait } from "alcalzone-shared/async"; +import { integrationTest } from "../integrationTestSuite"; + +integrationTest( + "The bootloader is detected when received in smaller chunks", + { + // Reproduction for issue #7316 + // debug: true, + + additionalDriverOptions: { + allowBootloaderOnly: true, + }, + + async customSetup(driver, mockController, mockNode) { + const sendBootloaderMessageInChunks: MockControllerBehavior = { + async onHostData(host, self, ctrl) { + // if ( + // ctrl.length === 1 + // && (ctrl[0] === MessageHeaders.NAK || ctrl[0] === 0x32) + // ) { + self.serial.emitData( + Buffer.from("\0\r\nGecko Bootloa", "ascii"), + ); + await wait(20); + self.serial.emitData(Buffer.from( + `der v2.05.01 +1. upload gbl +2. run +3. ebl info +BL >\0`, + "ascii", + )); + return true; + // } + }, + }; + mockController.defineBehavior(sendBootloaderMessageInChunks); + }, + + testBody: async (t, driver, node, mockController, mockNode) => { + t.true(driver.isInBootloader()); + }, + }, +); From 68aedf6969856f62891fcd3e33f3879cbac77bbf Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 24 Oct 2024 23:19:04 +0200 Subject: [PATCH 3/3] fix: shorten preamble, skip empty screens --- packages/serial/src/ZWaveSerialPortBase.ts | 4 +++- packages/serial/src/parsers/BootloaderParsers.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/serial/src/ZWaveSerialPortBase.ts b/packages/serial/src/ZWaveSerialPortBase.ts index 803b6e4465e9..0f445df97f77 100644 --- a/packages/serial/src/ZWaveSerialPortBase.ts +++ b/packages/serial/src/ZWaveSerialPortBase.ts @@ -158,7 +158,9 @@ export class ZWaveSerialPortBase extends PassThrough { if (this.mode == undefined) { // If we haven't figured out the startup mode yet, // inspect the chunk to see if it contains the bootloader preamble - const str = (data as Buffer).toString("ascii").trim(); + const str = (data as Buffer).toString("ascii") + // like .trim(), but including null bytes + .replaceAll(/^[\s\0]+|[\s\0]+$/g, ""); this.mode = str.startsWith(bootloaderMenuPreamble) ? ZWaveSerialMode.Bootloader : ZWaveSerialMode.SerialAPI; diff --git a/packages/serial/src/parsers/BootloaderParsers.ts b/packages/serial/src/parsers/BootloaderParsers.ts index 18836aa1f0a5..c97d2be2efe2 100644 --- a/packages/serial/src/parsers/BootloaderParsers.ts +++ b/packages/serial/src/parsers/BootloaderParsers.ts @@ -79,8 +79,9 @@ export class BootloaderScreenParser extends Transform { const screen = this.receiveBuffer.slice(0, nulCharIndex).trim(); this.receiveBuffer = this.receiveBuffer.slice(nulCharIndex + 1); - this.logger?.bootloaderScreen(screen); + if (screen === "") continue; + this.logger?.bootloaderScreen(screen); this.push(screen); } @@ -107,7 +108,9 @@ export class BootloaderScreenParser extends Transform { } } -export const bootloaderMenuPreamble = "Gecko Bootloader"; +// Sometimes the first chunk of the bootloader screen is relatively short, +// so we consider the following enough to detect the bootloader menu: +export const bootloaderMenuPreamble = "Gecko Boo"; const preambleRegex = /^Gecko Bootloader v(?\d+\.\d+\.\d+)/; const menuSuffix = "BL >"; const optionsRegex = /^(?\d+)\. (?