diff --git a/frontend/src/lib/proBoardRegistry.ts b/frontend/src/lib/proBoardRegistry.ts index 0886efd9..7685577b 100644 --- a/frontend/src/lib/proBoardRegistry.ts +++ b/frontend/src/lib/proBoardRegistry.ts @@ -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 ``. */ render?: (props: { id: string; x: number; y: number; running: boolean }) => React.ReactNode; diff --git a/frontend/src/simulation/RaspberryPi3Bridge.ts b/frontend/src/simulation/RaspberryPi3Bridge.ts index 99429144..d04f03fc 100644 --- a/frontend/src/simulation/RaspberryPi3Bridge.ts +++ b/frontend/src/simulation/RaspberryPi3Bridge.ts @@ -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 | 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] } }); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index c0b908de..da5f4b32 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -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((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).