Virtual devices
A virtual device is a Python module that simulates a real hardware component (a sensor, an actuator, a memory) connected to the firmware over SPI or I2C. The bridge loads devices at boot and uses them to answer the firmware’s transactions automatically — no physical hardware required.
Minimal structure
Section titled “Minimal structure”Create a file under dashboard/devices/, named after the device (lowercase,
underscores):
from ._base import Device
class MySensor(Device): bus = "spi" # "spi" | "i2c" port = 0 # bus port (uint) cs = 12 # SPI: CS pin (int) — required for SPI # addr = 0x44 # I2C: 7-bit address (int) — required for I2C
def on_transfer(self, tx: bytes) -> bytes: """Called for every SPI_TX: tx = bytes received from the firmware.""" return bytes(len(tx)) # reply: len(tx) bytesThe bridge picks the first Device subclass in the module. The class name does
not matter — only that it inherits from Device.
Available hooks
Section titled “Available hooks”| Hook | Called when | Return value |
|---|---|---|
| on_transfer(tx) | an SPI_TX arrives | reply bytes (length = len(tx)) |
| on_write(data) | an I2C_WRITE arrives | None (always ACKs) |
| on_read(length) | an I2C_READ arrives | reply bytes (length = length) |
The base class default returns bytes(len(tx)) / bytes(length) — all zeros.
Override only the hooks you need.
Binding metadata
Section titled “Binding metadata”A device binds to a specific hardware instance through class attributes or CLI arguments. The values in code are defaults; the operator can override them:
python3 dashboard/run.py \ --firmware build/host/examples/spi_thermo/spi_thermo \ --device my_sensor bus=spi port=0 cs=5--device NAME [key=value …] looks up dashboard/devices/NAME.py, instantiates
the device, and applies each key=value as a setattr. Numeric values (0,
12, 0x44) are converted automatically.
Full SPI example: MAX31855
Section titled “Full SPI example: MAX31855”from ._base import Device
class Max31855(Device): bus = "spi" port = 0 cs = 12
_temperature: float = 25.0 _fault: bool = False
def set_temperature(self, celsius: float) -> None: self._temperature = celsius
def on_transfer(self, tx: bytes) -> bytes: raw_tc = int(self._temperature / 0.25) & 0x3FFF word = (raw_tc << 18) if self._fault: word |= (1 << 16) return word.to_bytes(4, "big")Full I2C example: SHT31
Section titled “Full I2C example: SHT31”from ._base import Device
def _crc8(data: bytes) -> int: crc = 0xFF for byte in data: crc ^= byte for _ in range(8): crc = (crc << 1) ^ 0x31 if crc & 0x80 else crc << 1 crc &= 0xFF return crc
class Sht31(Device): bus = "i2c" addr = 0x44
_temperature: float = 25.0 _humidity: float = 50.0
def on_write(self, data: bytes) -> None: if data == bytes([0x2C, 0x06]): # single-shot high-repeatability self._pending_read = True
def on_read(self, length: int) -> bytes: if length != 6: return bytes(length) t_raw = int((self._temperature + 45.0) / 175.0 * 65535.0) rh_raw = int(self._humidity / 100.0 * 65535.0) t_msb, t_lsb = (t_raw >> 8) & 0xFF, t_raw & 0xFF rh_msb, rh_lsb = (rh_raw >> 8) & 0xFF, rh_raw & 0xFF return bytes([t_msb, t_lsb, _crc8([t_msb, t_lsb]), rh_msb, rh_lsb, _crc8([rh_msb, rh_lsb])])Devices can expose set_* methods so test scripts can vary the simulated values
— this is the pattern used by the unit tests in dashboard/tests/test_bridge.py.
Passthrough (no device configured)
Section titled “Passthrough (no device configured)”If no device is registered for an SPI or I2C transaction, the bridge schedules an auto-reply after 40 ms:
- SPI:
SPI_RX 0xFF …(MISO idle) - I2C write:
I2C_WRITE_ACK 1(ACK) - I2C read:
I2C_READ 00 …(all zeros)
The firmware has a 50 ms timeout, so the 40 ms auto-reply never trips it under normal conditions. A WebSocket client that injects a reply manually cancels the pending auto-reply.
Checklist for a new device
Section titled “Checklist for a new device”- Create
dashboard/devices/<name>.pywith a class that inherits fromDevice. - Set
bus,port, andcs(SPI) oraddr(I2C) as class attributes. - Implement
on_transfer(SPI) oron_write+on_read(I2C). - Add a
TestMyDeviceclass indashboard/tests/test_bridge.py. - Verify with
python3 -m pytestfromdashboard/. - Document the
run.py --devicelaunch command.