Skip to content

Commit

Permalink
fix: detect bootloader when short chunks are received (#7318)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Oct 24, 2024
1 parent 5dd9e9d commit 00c6360
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 4 deletions.
4 changes: 3 additions & 1 deletion packages/serial/src/ZWaveSerialPortBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions packages/serial/src/parsers/BootloaderParsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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(?<version>\d+\.\d+\.\d+)/;
const menuSuffix = "BL >";
const optionsRegex = /^(?<num>\d+)\. (?<option>.+)/gm;
Expand Down
18 changes: 17 additions & 1 deletion packages/testing/src/MockController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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> | 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,
Expand Down
45 changes: 45 additions & 0 deletions packages/zwave-js/src/lib/test/driver/bootloaderDetection.test.ts
Original file line number Diff line number Diff line change
@@ -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());
},
},
);
6 changes: 6 additions & 0 deletions packages/zwave-js/src/lib/test/integrationTestSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ function suite(
}
});

if (options.additionalDriverOptions?.allowBootloaderOnly) {
driver.once("bootloader ready", () => {
process.nextTick(resolve);
});
}

continueStartup();
});
}
Expand Down

0 comments on commit 00c6360

Please sign in to comment.