feat(seams): instant-engine registry for QEMU-Linux boards
Generic seam only — no board, OS or runtime specifics in the OSS tree: an overlay may register an engine that decides, per board, whether a run needs the Linux guest or can happen in the browser. The decision is data (engine + reason + where) so the UI can explain a 90 s boot instead of just taking it, and BoardInstance carries engineMode / enginePinned so the user's choice is predictable and the terminal panel knows whether an interactive shell exists. Nothing registers in OSS: the QEMU path is untouched.
This commit is contained in:
parent
8163869d31
commit
43ef6c9f1d
|
|
@ -256,7 +256,9 @@ export const SerialMonitor: React.FC = () => {
|
|||
{/* Output area. QEMU-Linux boards get the interactive xterm (shell
|
||||
input, line editing, ANSI) instead of the read-only mirror — this
|
||||
replaced the separate RaspberryPiWorkspace as the one terminal. */}
|
||||
{isPiBoardKind(activeBoard?.boardKind ?? '') && activeBoard?.running ? (
|
||||
{isPiBoardKind(activeBoard?.boardKind ?? '') &&
|
||||
activeBoard?.running &&
|
||||
activeBoard?.engineMode !== 'instant' ? (
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||
<PiTerminal key={activeBoard.id} boardId={activeBoard.id} />
|
||||
</div>
|
||||
|
|
@ -343,7 +345,11 @@ export const SerialMonitor: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* Input row — the xterm handles Pi input itself */}
|
||||
{!(isPiBoardKind(activeBoard?.boardKind ?? '') && activeBoard?.running) && (
|
||||
{!(
|
||||
isPiBoardKind(activeBoard?.boardKind ?? '') &&
|
||||
activeBoard?.running &&
|
||||
activeBoard?.engineMode !== 'instant'
|
||||
) && (
|
||||
<div style={styles.inputRow}>
|
||||
<input
|
||||
type="text"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* instantEngine — runtime seam for an in-browser engine that can replace a
|
||||
* QEMU-Linux boot for the boards that don't need a real OS.
|
||||
*
|
||||
* QEMU-Linux boards (Raspberry Pi family + overlay piFamily kinds) cost a
|
||||
* backend process and ~90 s of boot per run. Most projects are just a
|
||||
* Python script driving GPIO and a screen — those can run in the browser
|
||||
* in a couple of seconds. An overlay registers an engine here; the store
|
||||
* asks it, per board, whether it can take this run:
|
||||
*
|
||||
* Run pressed
|
||||
* -> engine.decide(boardId) -> 'instant' : engine.run(boardId)
|
||||
* 'linux' : boot the QEMU guest
|
||||
*
|
||||
* The decision is data (`EngineDecision`), not a boolean, so the UI can
|
||||
* explain WHY a board needs Linux ("script.py:12 uses subprocess") instead
|
||||
* of silently taking 90 s. Nothing is registered in the OSS build, so the
|
||||
* QEMU path stays exactly as it was.
|
||||
*/
|
||||
|
||||
export interface EngineDecision {
|
||||
/** Which engine should take this run. */
|
||||
engine: 'instant' | 'linux';
|
||||
/** Short, user-facing reason. Empty when 'instant' with nothing to say. */
|
||||
reason: string;
|
||||
/** Optional `file:line` the reason refers to, for the tooltip. */
|
||||
where?: string;
|
||||
}
|
||||
|
||||
export interface InstantEngine {
|
||||
/** Decide which engine runs this board's current files. */
|
||||
decide(boardId: string): EngineDecision;
|
||||
/** Run the board's files in the browser. Resolves when the script ends. */
|
||||
run(boardId: string): Promise<void>;
|
||||
/** Cancel a running script (the runtime may stay warm). */
|
||||
stop(boardId: string): void;
|
||||
}
|
||||
|
||||
let engine: InstantEngine | null = null;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
export function registerInstantEngine(e: InstantEngine | null): void {
|
||||
engine = e;
|
||||
for (const l of listeners) l();
|
||||
}
|
||||
|
||||
export function getInstantEngine(): InstantEngine | null {
|
||||
return engine;
|
||||
}
|
||||
|
||||
/** Subscribe to registration (the overlay loads asynchronously). */
|
||||
export function subscribeInstantEngine(cb: () => void): () => void {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which engine should run this board, honouring a user override.
|
||||
*
|
||||
* `pinned` comes from the board instance (the user clicked the mode chip
|
||||
* or turned on the Linux terminal); when set it wins over the detector,
|
||||
* because predictability beats cleverness for a project you already know.
|
||||
*/
|
||||
export function decideEngine(
|
||||
boardId: string,
|
||||
pinned?: 'instant' | 'linux',
|
||||
): EngineDecision {
|
||||
if (pinned === 'linux') {
|
||||
return { engine: 'linux', reason: 'Linux mode is on for this project' };
|
||||
}
|
||||
if (!engine) {
|
||||
return { engine: 'linux', reason: 'the instant engine is not available' };
|
||||
}
|
||||
if (pinned === 'instant') {
|
||||
return { engine: 'instant', reason: 'instant mode is pinned for this project' };
|
||||
}
|
||||
try {
|
||||
return engine.decide(boardId);
|
||||
} catch {
|
||||
// A detector bug must never block a run: fall back to the real machine.
|
||||
return { engine: 'linux', reason: 'could not analyse the project' };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
import { decideEngine, getInstantEngine } from '../lib/instantEngine';
|
||||
import { getProBoard, isProBoardSimulator, type ProBoardSimulator } from '../lib/proBoardRegistry';
|
||||
import { AVRSimulator } from '../simulation/AVRSimulator';
|
||||
import { RP2040Simulator } from '../simulation/RP2040Simulator';
|
||||
|
|
@ -1898,11 +1899,31 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
}
|
||||
|
||||
if (isPiBoardKind(board.boardKind)) {
|
||||
// Engine routing: most projects are a Python script driving GPIO and
|
||||
// a screen, and those run in the browser in seconds instead of
|
||||
// booting a Linux guest (a backend process + ~90 s). The detector
|
||||
// lives in the overlay; `enginePinned` lets the user override it.
|
||||
const decision = decideEngine(boardId, board.enginePinned);
|
||||
set((s) => ({
|
||||
boards: s.boards.map((b) =>
|
||||
b.id === boardId ? { ...b, engineMode: decision.engine } : b,
|
||||
),
|
||||
serialMonitorOpen: true,
|
||||
}));
|
||||
if (decision.engine === 'instant') {
|
||||
const instant = getInstantEngine();
|
||||
void instant?.run(boardId).finally(() => {
|
||||
set((s) => {
|
||||
const boards = s.boards.map((b) =>
|
||||
b.id === boardId ? { ...b, running: false } : b,
|
||||
);
|
||||
const isActive = s.activeBoardId === boardId;
|
||||
return { boards, ...(isActive ? { running: false } : {}) };
|
||||
});
|
||||
});
|
||||
} else {
|
||||
getBoardBridge(boardId)?.connect();
|
||||
// Pop the terminal open so the user watches the guest boot and gets
|
||||
// the interactive shell — without it a Linux board looks frozen for
|
||||
// the ~45 s boot.
|
||||
set({ serialMonitorOpen: true });
|
||||
}
|
||||
} else if (isEsp32Kind(board.boardKind)) {
|
||||
// Pre-register sensors connected to this board so the QEMU worker
|
||||
// has them ready before the firmware starts executing.
|
||||
|
|
@ -2142,7 +2163,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
if (!board) return;
|
||||
|
||||
if (isPiBoardKind(board.boardKind)) {
|
||||
getBoardBridge(boardId)?.disconnect();
|
||||
if (board.engineMode === 'instant') getInstantEngine()?.stop(boardId);
|
||||
else getBoardBridge(boardId)?.disconnect();
|
||||
} else if (isEsp32Kind(board.boardKind)) {
|
||||
getEsp32Bridge(boardId)?.disconnect();
|
||||
} else if (isStm32BoardKind(board.boardKind)) {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,15 @@ export interface BoardInstance {
|
|||
// the bridge sees the boot-complete marker, and drives the "Booting…" overlay
|
||||
// and gates file uploads. Undefined/false for non-Pi boards and pre-boot.
|
||||
piBooted?: boolean;
|
||||
/** QEMU-Linux boards only. Which engine this board is running on (or ran
|
||||
* last): 'instant' = in-browser Python, 'linux' = the QEMU guest. Drives
|
||||
* the mode chip and the terminal panel (interactive shell only exists in
|
||||
* Linux mode). Undefined until the first run. */
|
||||
engineMode?: 'instant' | 'linux';
|
||||
/** User override for the engine, persisted with the project. Set by the
|
||||
* mode chip or by turning on the Linux terminal; wins over the detector
|
||||
* so a project doesn't silently change behaviour between runs. */
|
||||
enginePinned?: 'instant' | 'linux';
|
||||
compiledProgram: string | null; // hex for AVR/RP2040, null for Pi (runs Python)
|
||||
serialOutput: string;
|
||||
serialBaudRate: number;
|
||||
|
|
|
|||
Loading…
Reference in New Issue