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: detect bootloader when short chunks are received #7318

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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
Loading