Simulation bus
The simulation bus is the observation-and-injection channel that connects firmware running on the host to simulated components, test tools, and the dashboard. It is part of the POSIX port, not the HAL — it does not exist on a real MCU.
Architecture
Section titled “Architecture”The firmware process acts as a server on a Unix domain socket. Each client (simulated component, visualizer, test script) connects and can receive events or send injections. Multiple clients can connect at once.
┌─────────────────────────────────────────┐│ firmware process ││ ┌──────────────────────────────────┐ ││ │ hal/posix │ ││ │ gpio_state[] uart pipes timer │ ││ │ sim bus server │ ││ └──────────┬───────────────────────┘ │└─────────────┼────────────────────────────┘ │ Unix domain socket ┌─────────┼──────────────┐┌───▼───┐ ┌───▼─────┐ ┌──────▼─────┐│ LED │ │ button │ │ visualizer ││ (py) │ │ (py) │ │ (browser) │└───────┘ └─────────┘ └────────────┘The socket path is configurable via the LPL_SIM_SOCKET environment variable
(default /tmp/lpl_sim_<pid>.sock). If the socket cannot be created, the
firmware runs normally without the bus — the bus is always optional.
Protocol
Section titled “Protocol”The protocol is text, line-based: every message is a single ASCII line
terminated by \n. There is no JSON parser in the C code — just
write(fd, line, len). Any language can implement a client with readline().
DIRECTION TYPE ARGS...\nDIRECTION—>firmware → client (event),<client → firmware (injection)TYPE— the event type identifierARGS— space-separated parameters
Events emitted by the firmware (>)
Section titled “Events emitted by the firmware (>)”> GPIO_WRITE <pin> <0|1>> GPIO_DIR <pin> <IN|OUT>> GPIO_TOGGLE <pin>> UART_TX <port> <hex_byte>> TIMER_TICK <ms>> ADC_READ <channel> <value>> SPI_CS <port> <cs_pin> <ASSERT|DEASSERT>> SPI_TX <port> <b0> <b1> … # requires < SPI_RX> I2C_WRITE <port> <addr_hex> <b0> … # requires < I2C_WRITE_ACK> I2C_READ <port> <addr_hex> <len> # requires < I2C_READInjections accepted by the firmware (<)
Section titled “Injections accepted by the firmware (<)”< GPIO_SET <pin> <0|1> # forces the level read by gpio_read< UART_RX <port> <hex_byte> # injects a byte into the UART buffer< ADC_SET <channel> <value> # sets the ADC value (0–4095, 12-bit)< SPI_RX <port> <b0> <b1> … # MISO data for the pending transfer< I2C_WRITE_ACK <port> <addr_hex> <0|1># 1 = ACK, 0 = NACK< I2C_READ <port> <addr_hex> <b0>…# data read from the deviceRequest/response for SPI and I2C
Section titled “Request/response for SPI and I2C”When the firmware emits SPI_TX, I2C_WRITE, or I2C_READ, the HAL thread
blocks on a pthread_cond_timedwait for up to 50 ms waiting for the reply. On
timeout, SPI fills rx with 0xFF (MISO idle) and I2C returns
LPL_ERR_TIMEOUT. The bridge replies within ≤ 40 ms — directly (a virtual
device) or through a scheduled auto-reply — so the firmware timeout never fires
under normal conditions.
An SPI session reading a MAX31855 thermocouple:
> SPI_CS 0 12 ASSERT> SPI_TX 0 00 00 00 00< SPI_RX 0 19 10 00 00> SPI_CS 0 12 DEASSERTAn I2C session reading an SHT31:
> I2C_WRITE 0 44 2C 06< I2C_WRITE_ACK 0 44 1> I2C_READ 0 44 06< I2C_READ 0 44 60 EB 1E 64 3A 99You can inspect the protocol live with nc -U /tmp/lpl_sim_<pid>.sock or
socat.
Simulated components and virtual devices
Section titled “Simulated components and virtual devices”A simulated component is a separate process (typically Python, stdlib only) that connects to the socket, filters the events it cares about, keeps its own state, and can send injections in response. The core knows nothing about them — it only observes them through the protocol. Each component is an independent lapillo.
A virtual device is a Python module the bridge loads to answer SPI/I2C transactions automatically. See Virtual devices for how to write one.
The bridge and the WebSocket layer
Section titled “The bridge and the WebSocket layer”The dashboard bridge (bridge.py, asyncio + the websockets library) is the
firmware process manager: it spawns N firmware processes, discovers their
sockets, reads every sim bus in parallel, and multiplexes all events onto a
single WebSocket toward the browser. Each WS message includes a nodeId (source
node) and a bridge-authoritative ts (milliseconds since the bridge started).
Browser → bridge messages carry injections and lifecycle control
(GPIO_SET, UART_RX, ADC_SET, SPI_RX, I2C_WRITE_ACK, I2C_READ) plus
CTRL_RESTART, CTRL_STOP, CTRL_PAUSE, and CTRL_RESUME. Pause/resume is
implemented with SIGSTOP/SIGCONT on the firmware process.
This decoupling means the C project has no native graphics dependency, the visualizer can be replaced without recompiling firmware, and a single bridge drives N firmware processes in parallel on a headless server.