feat(serial): hardware-serial seams for the pro Web Serial monitor
appendHardwareSerial pushes real-UART bytes through the existing per-frame serial batcher (no SerialMonitor changes), and lib/proHardwareSerial lets the overlay intercept a board's serial input while a physical monitor is attached — TX and RX switch to the hardware together, detaching hands the tab back to the simulator. Also adds @types/w3c-web-serial (esptool-js references it but only dev-depends on it).
This commit is contained in:
parent
def450f4f1
commit
1980d70942
|
|
@ -33,7 +33,6 @@
|
|||
"@google/genai": "^0.6.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"monaco-editor": "0.55.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@wokwi/elements": "1.9.2",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
|
|
@ -47,6 +46,7 @@
|
|||
"jszip": "^3.10.1",
|
||||
"littlefs": "^0.1.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^15.1.0",
|
||||
|
|
@ -65,6 +65,7 @@
|
|||
"@types/node": "^24.11.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/w3c-web-serial": "^1.0.8",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* The hardware-serial TX seam (lib/proHardwareSerial.ts): while a real
|
||||
* board's monitor is attached, serial input routes to the interceptor;
|
||||
* detaching restores simulator routing. Per-board, inert by default.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
getSerialTxInterceptor,
|
||||
installSerialTxInterceptor,
|
||||
} from '../lib/proHardwareSerial';
|
||||
|
||||
describe('proHardwareSerial seam', () => {
|
||||
it('has no interceptor by default (pure OSS build)', () => {
|
||||
expect(getSerialTxInterceptor('board-1')).toBeNull();
|
||||
});
|
||||
|
||||
it('routes per board and uninstalls with null', () => {
|
||||
const toHardware = vi.fn();
|
||||
installSerialTxInterceptor('board-1', toHardware);
|
||||
|
||||
expect(getSerialTxInterceptor('board-2')).toBeNull();
|
||||
getSerialTxInterceptor('board-1')?.('hello\n');
|
||||
expect(toHardware).toHaveBeenCalledWith('hello\n');
|
||||
|
||||
installSerialTxInterceptor('board-1', null);
|
||||
expect(getSerialTxInterceptor('board-1')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Pro hardware-serial TX registry.
|
||||
*
|
||||
* The hardware serial monitor (pro overlay) mirrors a REAL board's UART
|
||||
* into the existing SerialMonitor. Incoming bytes need no seam — the
|
||||
* overlay appends them via `appendHardwareSerial` (useSimulatorStore).
|
||||
* Outgoing text does: `serialWriteToBoard` normally targets the board's
|
||||
* simulator, so while a hardware monitor is attached to a board, the
|
||||
* overlay installs an interceptor here and the user's input goes to the
|
||||
* physical port instead.
|
||||
*
|
||||
* Mirrors the other OSS->Pro seams (`proWebFlash.ts`, `proBoardGate.ts`).
|
||||
* No imports on purpose: the store consults this module, never the other
|
||||
* way around, so there is no cycle.
|
||||
*/
|
||||
|
||||
type SerialTxInterceptor = (text: string) => void;
|
||||
|
||||
const _interceptors = new Map<string, SerialTxInterceptor>();
|
||||
|
||||
/**
|
||||
* Route a board's serial input to the given sink instead of its simulator.
|
||||
* Pass null to restore the default (simulator) routing. Installed by the
|
||||
* pro overlay while its hardware serial monitor is connected.
|
||||
*/
|
||||
export function installSerialTxInterceptor(
|
||||
boardId: string,
|
||||
fn: SerialTxInterceptor | null,
|
||||
): void {
|
||||
if (fn) _interceptors.set(boardId, fn);
|
||||
else _interceptors.delete(boardId);
|
||||
}
|
||||
|
||||
/** The interceptor for a board, or null when input should go to the sim. */
|
||||
export function getSerialTxInterceptor(boardId: string): SerialTxInterceptor | null {
|
||||
return _interceptors.get(boardId) ?? null;
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
|
|||
import type { BoardKind, BoardInstance, LanguageMode, WifiStatus } from '../types/board';
|
||||
import { BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, isStm32BoardKind } from '../types/board';
|
||||
import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../lib/proBoardGate';
|
||||
import { getSerialTxInterceptor } from '../lib/proHardwareSerial';
|
||||
import { calculatePinPosition } from '../utils/pinPositionCalculator';
|
||||
import { useOscilloscopeStore } from './useOscilloscopeStore';
|
||||
import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
|
||||
|
|
@ -1291,6 +1292,16 @@ const { append: appendSerial } = createSerialBatcher((perBoard) => {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Pro overlay seam: append bytes coming from a REAL board's UART (Web
|
||||
* Serial hardware monitor) into a board's serial output. Goes through the
|
||||
* same per-frame batcher as simulator bytes, so the SerialMonitor needs no
|
||||
* changes and hardware chatter cannot trigger the per-byte set() storm.
|
||||
*/
|
||||
export function appendHardwareSerial(boardId: string, chunk: string): void {
|
||||
appendSerial(boardId, chunk);
|
||||
}
|
||||
|
||||
// ── Store ─────────────────────────────────────────────────────────────────
|
||||
export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||
// Initialise runtime objects for the default board
|
||||
|
|
@ -3756,6 +3767,13 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
serialWriteToBoard: (boardId: string, text: string) => {
|
||||
const board = get().boards.find((b) => b.id === boardId);
|
||||
if (!board) return;
|
||||
// A connected hardware serial monitor (pro overlay) takes the input
|
||||
// instead of the board's simulator.
|
||||
const hardwareTx = getSerialTxInterceptor(boardId);
|
||||
if (hardwareTx) {
|
||||
hardwareTx(text);
|
||||
return;
|
||||
}
|
||||
if (isPiBoardKind(board.boardKind)) {
|
||||
const bridge = getBoardBridge(boardId);
|
||||
if (bridge) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue