diff --git a/frontend/src/simulation/Esp32Bridge.ts b/frontend/src/simulation/Esp32Bridge.ts index 8f35ba72..c381b860 100644 --- a/frontend/src/simulation/Esp32Bridge.ts +++ b/frontend/src/simulation/Esp32Bridge.ts @@ -110,6 +110,12 @@ export class Esp32Bridge { onI2cEvent: ((addr: number, data: number) => void) | null = null; onI2cTransaction: ((addr: number, data: number[]) => void) | null = null; onSpiEvent: ((data: number) => void) | null = null; + /** Same as onSpiEvent but more explicit (a single MOSI byte). */ + onSpiByte: ((mosi: number) => void) | null = null; + /** Fires on every CS line change emitted by the SoC's SPI peripheral. + * `csIdx` is the index of the CS pin within the SPI bus (0-3 typical), + * `low` is true when CS goes LOW (slave selected), false when HIGH. */ + onSpiCsChange: ((csIdx: number, low: boolean) => void) | null = null; onConnected: (() => void) | null = null; onDisconnected: (() => void) | null = null; onError: ((msg: string) => void) | null = null; @@ -287,8 +293,25 @@ export class Esp32Bridge { break; } case 'spi_event': { - const data = msg.data.data as number; - this.onSpiEvent?.(data); + // Worker emits {bus, event, response}. The 'event' field encodes: + // event = mosi << 8 (op = event & 0xFF == 0x00) → byte transfer + // event = ((cs<<1)|level) << 8 | 0x01 (op == 0x01) → CS line change + // See backend/app/services/esp32_worker.py::_on_spi_event. + const event = msg.data.event as number; + const op = (event ?? 0) & 0xFF; + if (op === 0x00) { + const mosi = (event >> 8) & 0xFF; + this.onSpiEvent?.(mosi); + this.onSpiByte?.(mosi); + } else if (op === 0x01) { + const csIdx = (event >> 9) & 0x3; + const level = (event >> 8) & 0x1; + this.onSpiCsChange?.(csIdx, level === 1); + } + // Backwards-compat path for callers reading the old `data` field. + if (msg.data.data !== undefined) { + this.onSpiEvent?.(msg.data.data as number); + } break; } case 'system': { diff --git a/frontend/src/simulation/parts/ComplexParts.ts b/frontend/src/simulation/parts/ComplexParts.ts index f277aa2f..12eb7022 100644 --- a/frontend/src/simulation/parts/ComplexParts.ts +++ b/frontend/src/simulation/parts/ComplexParts.ts @@ -797,8 +797,13 @@ const ili9341Simulation = { const el = element as any; const pinManager = (avrSimulator as any).pinManager; const spi = (avrSimulator as any).spi; + // ESP32 path: simulator is Esp32BridgeShim — no .spi member, but it + // exposes getBridge() to subscribe to the worker's spi_event stream. + const getBridge = (avrSimulator as any).getBridge; + const esp32Bridge = typeof getBridge === 'function' ? getBridge.call(avrSimulator) : null; - if (!pinManager || !spi) return () => {}; + if (!pinManager) return () => {}; + if (!spi && !esp32Bridge) return () => {}; // ── Canvas setup ────────────────────────────────────────────────── const SCREEN_W = 240; @@ -951,20 +956,38 @@ const ili9341Simulation = { }; // ── Intercept SPI ───────────────────────────────────────────────── - const prevOnByte = spi.onByte.bind(spi); + let prevOnByte: ((value: number) => void) | null = null; + let prevSpiByte: ((mosi: number) => void) | null = null; - spi.onByte = (value: number) => { - if (!dcState) { - processCommand(value); - } else { - processData(value); - } - spi.completeTransfer(0xff); // Unblock CPU immediately - }; + if (spi) { + // AVR (Arduino) path — hook the simulator's SPI peripheral + prevOnByte = spi.onByte.bind(spi); + spi.onByte = (value: number) => { + if (!dcState) processCommand(value); + else processData(value); + spi.completeTransfer(0xff); + }; + } else if (esp32Bridge) { + // ESP32 path — subscribe to the QEMU worker's SPI byte stream + // routed through the Esp32Bridge. Each byte arrives via onSpiByte + // (CS gating is left to the user's wiring; with one ILI9341 on the + // bus this works without explicit CS tracking). DC tracking still + // happens via pinManager.onPinChange above — that path is shared + // because the Esp32BridgeShim's pinManager fires on every gpio + // change emitted by the worker. + prevSpiByte = esp32Bridge.onSpiByte; + esp32Bridge.onSpiByte = (mosi: number) => { + if (!dcState) processCommand(mosi); + else processData(mosi); + // Chain to any prior subscriber (defensive — there shouldn't be one) + if (prevSpiByte) prevSpiByte(mosi); + }; + } // ── Cleanup ─────────────────────────────────────────────────────── return () => { - spi.onByte = prevOnByte; + if (spi && prevOnByte) spi.onByte = prevOnByte; + if (esp32Bridge) esp32Bridge.onSpiByte = prevSpiByte; if (rafId !== null) cancelAnimationFrame(rafId); el.removeEventListener('canvas-ready', onCanvasReady); unsubscribers.forEach((u) => u());