feat(pi-family): quietBoot — hide the shared rootfs' branded boot chatter

The generic arm64 image prints another product's banner/motd/login line
during boot, before guestSetup can re-brand the guest. Boards with
quietBoot show a neutral '[Velxio] Booting <label> (Linux guest)...'
progress line (dots every 4 s) while boot detection and the prompt-gated
upload still run underneath; the shell is revealed (already re-branded)
right before the auto-run command, so the user's first visible output is
their own program.
This commit is contained in:
David Montero Crespo 2026-07-28 21:07:21 +02:00
parent a766d0cde3
commit 330b9ed1b2
3 changed files with 63 additions and 3 deletions

View File

@ -71,6 +71,12 @@ export interface ProBoardDef {
* this line executed, so a single click on Run boots, uploads and
* starts the user's script (piFamily boards only). */
autoRun?: string;
/** Suppress the guest's boot chatter in the terminal (piFamily boards
* only): the shared rootfs prints another product's banner/motd during
* boot, before guestSetup can re-brand it. With quietBoot the terminal
* shows a neutral Velxio boot-progress line instead and reveals the
* shell right before the auto-run command executes. */
quietBoot?: boolean;
/** Canvas renderer. Receives the placed board's props; return a React node.
* When omitted, the canvas renders `<tag id=... style=absolute@x,y>`. */
render?: (props: { id: string; x: number; y: number; running: boolean }) => React.ReactNode;

View File

@ -75,6 +75,15 @@ export class RaspberryPi3Bridge {
private socket: WebSocket | null = null;
private _connected = false;
private _booted = false;
/** quietBoot (ProBoardDef): while true, guest serial output is withheld
* from onSerialData (boot detection still runs) and a neutral Velxio
* progress line + dots show instead. The run path calls setQuiet(false)
* to reveal the shell right before the script starts. */
quietBootDefault = false;
/** Board label for the quiet-boot progress line. */
quietBootLabel = '';
private _quiet = false;
private _quietTimer: ReturnType<typeof setInterval> | null = null;
/** Rolling, escape-stripped tail of recent guest output, used to detect the
* boot-complete marker and shell prompts for flow-controlled sends. */
private _serialTail = '';
@ -110,6 +119,12 @@ export class RaspberryPi3Bridge {
this.onConnected?.();
// Tell the backend which Pi family member to boot.
this._send({ type: 'start_pi', data: { board: this.boardKind } });
if (this.quietBootDefault) {
this._quiet = true;
const label = this.quietBootLabel || this.boardKind;
this._emitLocal(`[Velxio] Booting ${label} (Linux guest)`);
this._quietTimer = setInterval(() => this._emitLocal('.', false), 4000);
}
};
socket.onmessage = (event: MessageEvent) => {
@ -123,7 +138,9 @@ export class RaspberryPi3Bridge {
switch (msg.type) {
case 'serial_output': {
const text = (msg.data.data as string) ?? '';
if (this.onSerialData) {
// quietBoot: keep observing (boot marker + prompt waiters drive
// guestSetup and the upload) but withhold the branded chatter.
if (!this._quiet && this.onSerialData) {
for (const ch of text) this.onSerialData(ch);
}
this._observeSerial(text);
@ -183,11 +200,37 @@ export class RaspberryPi3Bridge {
private _resetBootState(): void {
this._booted = false;
this._serialTail = '';
this._quiet = false;
if (this._quietTimer) {
clearInterval(this._quietTimer);
this._quietTimer = null;
}
const waiters = this._promptWaiters;
this._promptWaiters = [];
for (const w of waiters) w();
}
/** Feed locally-generated status text to the serial consumers. */
private _emitLocal(text: string, newline = true): void {
if (!this.onSerialData) return;
const chunk = newline ? `\r\n${text}` : text;
for (const ch of chunk) this.onSerialData(ch);
}
/** Reveal (or re-hide) the guest's serial stream. Turning quiet off ends
* the progress dots and prints a ready line. */
setQuiet(on: boolean): void {
if (this._quiet === on) return;
this._quiet = on;
if (!on) {
if (this._quietTimer) {
clearInterval(this._quietTimer);
this._quietTimer = null;
}
this._emitLocal(' ready.\r\n');
}
}
/** Send a byte to the Pi's ttyAMA0 (user serial) */
sendSerialByte(byte: number): void {
this._send({ type: 'serial_input', data: { bytes: [byte] } });

View File

@ -777,8 +777,14 @@ export async function piSyncAndRunScript(boardId: string, boardKind: string): Pr
.getState()
.getGroupFiles(groupId)
.map((f) => ({ path: `${home}/${f.name}`, content: f.content }));
const { uploadFilesToPi } = await import('../utils/piUpload');
await uploadFilesToPi(bridge, files);
try {
const { uploadFilesToPi } = await import('../utils/piUpload');
await uploadFilesToPi(bridge, files);
} finally {
// Reveal the shell (ends quietBoot) right before the script starts, so
// the user's first visible output is their own program.
bridge.setQuiet(false);
}
const cmd = proDef?.autoRun ?? `python3 ${home}/script.py`;
bridge.sendSerialText(cmd.endsWith('\n') ? cmd : cmd + '\n');
}
@ -1259,6 +1265,11 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
if (isPiBoardKind(boardKind)) {
const bridge = new RaspberryPi3Bridge(id, boardKind);
const piDef = getProBoard(boardKind);
if (piDef?.quietBoot) {
bridge.quietBootDefault = true;
bridge.quietBootLabel = piDef.label;
}
bridge.onSerialData = (ch: string) => {
serialCallback(ch);
// Cross-board routing now handled by Interconnect (see bind below).