velxio/frontend/src/simulation/SpiBus.ts

35 lines
1.6 KiB
TypeScript
Raw Normal View History

refactor(spi): unify SPI bus interface across all simulators Previous fix added an ESP32-specific code path inside ili9341Simulation to subscribe to the QEMU worker's spi_event stream. That made the LCD work on ESP32-CAM but left the underlying issue unsolved: every other SPI part (custom chips, future SD-card emulators, the SSD168x ePaper already in the codebase) would also need its own per-board branching. The right shape: every simulator exposes a `.spi` member matching the SAME SpiBusLike interface, and SPI parts hook .spi.onByte without caring which board they're attached to. AVRSimulator already had this — now everything else does too. frontend/src/simulation/SpiBus.ts (new) Defines the contract — `onByte: (mosi) => void | null` plus optional `completeTransfer(miso)`. Documents the single-listener semantics that AVR has had since day one. frontend/src/store/useSimulatorStore.ts Esp32BridgeShim gets a lazy `.spi` getter that wraps bridge.onSpiByte (the per-byte WS event from the QEMU worker). completeTransfer is a no-op because the worker drives MISO via its own _spi_response global. Covers ESP32 (Xtensa), ESP32-S3, ESP32-CAM, ESP32-C3 — every kind that routes through Esp32Bridge. frontend/src/simulation/RP2040Simulator.ts Adds a lazy `.spi` getter that re-routes rp2040.spi[0].onTransmit through the adapter. Default loopback (the prior behaviour) is preserved when no part has accessed `.spi` yet — only consumers that opt in see their handler invoked. Covers Pico and Pico W. frontend/src/simulation/parts/ComplexParts.ts ili9341Simulation no longer has an ESP32 special case. Single code path: `simulator.spi.onByte = handler`. Works on AVR, RP2040, all ESP32 variants. Same pattern is now available to every future SPI part — ssd1306, sd-card, oled, etc. The Esp32Bridge.ts spi_event field-name fix from 6afa62e (msg.data.event instead of the non-existent msg.data.data) stays in place — that's what makes the per-byte stream actually arrive in the bridge. Verified: ILI9341 + ESP32-CAM gallery example renders the live webcam preview after a hard refresh. The same simulation code works on Arduino Uno + ILI9341 (the existing ili9341-test-sketch in example_zip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:36:29 +07:00
/**
* Generic SPI bus interface, board-agnostic.
*
* Every simulator (AVR, RP2040, ESP32 Xtensa/RISC-V, RiscV bare-metal,
* Pi-Pico-W, …) exposes a `.spi` member matching this shape so SPI-driven
* components — ILI9341 displays, custom chips, SD cards, etc. — can hook
* the bus without knowing which board they're attached to.
*
* The contract follows the AVRSimulator's existing shape (the original
* implementation): the consumer assigns its handler to `onByte`, which
* receives one MOSI byte per SPI clock cycle. If the consumer is a slave
* that wants to drive the master's MISO line for that cycle, it calls
* `completeTransfer(miso)`. For boards where MISO is driven externally
* (ESP32 — the QEMU worker handles the response via _spi_response),
* `completeTransfer` is a no-op.
*
* Each simulator's `.spi` is a SINGLE-LISTENER channel: assigning to
* `onByte` overwrites any previous handler. Components that wrap an
* existing handler must save the old one and chain it in their own
* implementation (the standard pattern; see ili9341Simulation in
* ComplexParts.ts).
*/
export interface SpiBusLike {
/** Settable per-cycle byte handler. Null when no consumer is attached. */
onByte: ((mosi: number) => void) | null;
/**
* Tell the master what to place on MISO for the current cycle.
* Optional — boards where MISO is owned by the emulator (ESP32) treat
* this as a no-op. Hardware-faithful boards (AVR, RP2040) must call
* this before the next byte arrives or the master will read 0.
*/
completeTransfer?(miso: number): void;
}