Skip to content

HowdyMoto/circuitpython-simhub-dash

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimHub CircuitPython Dashboard

A CircuitPython sim-racing telemetry dashboard driven by SimHub over USB. Plug a CircuitPython display board into your PC, point SimHub's Custom Serial Device plugin at it, and get a live dash: speed, gear, RPM shift-bar, lap times, delta, fuel, position, flags and tyre temps/pressures.

As far as I can tell, this is the only CircuitPython SimHub dashboard — the SimHub microcontroller ecosystem is otherwise entirely Arduino/C++.

Two reference devices included and tested:

Board Chip Screen Status
Adafruit PyPortal Titano SAMD51 + ESP32 3.5″ 480×320 (SPI)
LilyGo T-Display S3 ESP32-S3 1.9″ 320×170 (parallel)

Architecture

The project is split into a communication layer (shared, board-agnostic) and a presentation layer (one per device, because screens and chips differ wildly). They meet at a single data object, Telemetry.

        USB Custom Serial (one ';'-delimited line per frame)
                              │
        ┌─────────────────────▼──────────────────────┐
        │ COMMUNICATION LAYER  (shared, identical)     │
        │   shared/simhub_telemetry.py                 │
        │     SimHubReader.poll() ──► Telemetry        │
        └─────────────────────┬──────────────────────┘
                              │  Telemetry (the contract)
        ┌─────────────────────▼──────────────────────┐
        │ PRESENTATION LAYER  (per device)             │
        │   shared/dashboard_base.py  (reusable tools) │
        │   devices/<board>/dashboard.py (the layout)  │
        └─────────────────────────────────────────────┘
  • shared/simhub_telemetry.py — owns the USB serial channel, buffers and decodes the freshest line into a reused Telemetry object. No display code. Copy it to every board unchanged.
  • shared/dashboard_base.pyBaseDashboard: the reusable presentation plumbing (value-level change detection, batched single-refresh-per-frame, status overlay, widget helpers). Board-agnostic.
  • devices/<board>/dashboard.py — subclass of BaseDashboard that defines only the screen layout (_build) and the Telemetry→widget mapping (update).

Repository layout

shared/
  simhub_telemetry.py        # communication layer  (copy to every device)
  dashboard_base.py          # presentation toolkit (copy to every device)
devices/
  pyportal_titano/           # copy THIS folder (incl. fonts/) + shared/ to CIRCUITPY
    boot.py  code.py  dashboard.py  theme.py  fonts/
  lilygo_tdisplay_s3/
    boot.py  code.py  display.py  dashboard.py  theme.py  fonts/
simhub/custom_serial_formula.js   # the SimHub formula (same for all devices)
tools/
  fake_sender.py             # test the screen with no sim running
  ttf_to_bdf.py              # regenerate the bitmap fonts from any .ttf

Fonts are per-device (devices/<board>/fonts/) because the two screens want different sizes; both sets are Geist Mono, named font-big/med/sml.bdf.

Performance design (why it stays smooth in CircuitPython)

  • Display auto_refresh is off; the screen is pushed once per frame and only when something changed (a dirty flag).
  • Change detection compares raw values, so an unchanged field never even builds a formatted string — this kills per-frame heap churn (the main cause of GC stutter).
  • The Telemetry object is reused, not reallocated each frame.

Why USB (not WiFi)?

SimHub has no built-in way to broadcast chosen telemetry as UDP/JSON to an arbitrary device — its UDP forwarding only relays a game's native binary packets. The robust, documented path to a microcontroller is the Custom Serial Device plugin over USB, which is what this uses. (WiFi is possible but needs a PC-side bridge — see Going wireless below.)


Setup

1. Flash CircuitPython

Install CircuitPython (9.x or 10.x) for your board from circuitpython.org. The board mounts as a USB drive named CIRCUITPY.

2. Add the required libraries to CIRCUITPY/lib/

From the matching CircuitPython Library Bundle:

Board Libraries needed in lib/
PyPortal Titano adafruit_display_text, adafruit_bitmap_font
LilyGo T-Display S3 adafruit_display_text, adafruit_bitmap_font, adafruit_st7789

Easiest is circup: circup install adafruit_display_text adafruit_st7789 (it pulls adafruit_bitmap_font automatically). board, displayio, terminalio, vectorio, usb_cdc, digitalio and paralleldisplaybus are built into CircuitPython.

Fonts: copy your board's devices/<board>/fonts/ folder to CIRCUITPY/fonts/. These are Geist Mono bitmap fonts (font-big/med/sml.bdf), subset to ASCII. If a font file is missing the dashboard still runs, just falling back to the built-in font. To use a different typeface or size, regenerate them with python tools/ttf_to_bdf.py YOUR.ttf --sizes 18 32 64 --out devices/<board>/fonts (then rename to the font-* slots).

3. Copy the code to CIRCUITPY/

Copy both the shared files and your board's device files to the root of CIRCUITPY (CircuitPython runs everything from the root):

CIRCUITPY/
├── boot.py              (from devices/<board>/)
├── code.py              (from devices/<board>/)
├── dashboard.py         (from devices/<board>/)
├── theme.py             (from devices/<board>/)
├── display.py           (T-Display S3 only)
├── simhub_telemetry.py  (from shared/)
├── dashboard_base.py    (from shared/)
├── fonts/               (from devices/<board>/fonts/)
└── lib/ ...

4. Power-cycle the board

boot.py only runs at hard reset. Fully unplug and replug (a soft reload is not enough). The board now presents two COM ports: the REPL/console port and a new data port. SimHub talks to the data port.

5. Find the data COM port (Windows)

  • Device Manager → Ports (COM & LPT) shows two entries for the board. The CircuitPython data port is labelled like CircuitPython CDC2 (the higher COM number). Unplug/replug to see which two appear/disappear.

6. Configure SimHub

  1. Settings → Plugins → Custom Serial devices → enable (restart SimHub).
  2. Custom serial devices → Add new serial device.
  3. Serial settings: Serial port = your data port; Baudrate = 115200 (ignored on native USB, set it anyway); enable Automatic reconnect.
  4. Protocol definition → Update messages → EDIT: turn ON Use javascript, paste the entire contents of simhub/custom_serial_formula.js, Ok. Set that row's rate to 30 Hz.
    • Leave Message after device connect and before disconnect empty (putting the formula there sends it once → "SIGNAL LOST").
  5. Load a session (or hit Replay). The dash goes live within ~1 second.

Test without a sim

SimHub holds the COM port, so toggle the device off first, then:

pip install pyserial
python tools/fake_sender.py COM5      # your data port

Animated dash = the board side is perfect; any remaining issue is SimHub config.


Adding a new device

  1. Create devices/<your_board>/ with boot.py (copy any), theme.py, dashboard.py, code.py, and a display.py if the board doesn't auto-create board.DISPLAY.
  2. In dashboard.py, subclass BaseDashboard and implement just two methods:
    from dashboard_base import BaseDashboard, fmt_laptime
    import theme
    
    class Dashboard(BaseDashboard):
        def _build(self):
            # create widgets with self._text(...) / self._bg(...)
            # MUST assign self.status = self._text(...)  (the overlay)
            ...
        def update(self, t):
            c = self._changed
            if c("spd", t.speed):
                self.spd.text = str(t.speed)
            ...
  3. code.py just builds the display, then runs the standard loop (copy one of the examples). The communication layer and loop are identical everywhere.

The full Telemetry field list is documented at the top of shared/simhub_telemetry.py.


Customizing

  • Colours, flag labels, shift-light & tyre bands: devices/<board>/theme.py.
  • Units (mph / °F / psi): change the value in the SimHub formula and the matching label (SPEED_UNIT etc.) in theme.py.
  • Layout: Dashboard._build() in your board's dashboard.py — plain x/y.
  • Add/remove fields: the field order is the contract between custom_serial_formula.js and simhub_telemetry.py; change both together.

Wire protocol (quick reference)

One ;-delimited ASCII line per update, $-prefixed, \n-terminated:

$speed;gear;rpm;maxrpm;fuel;curlap_ms;lastlap_ms;bestlap_ms;delta_s;pos;lap;totallaps;flag;tFL;tFR;tRL;tRR;pFL;pFR;pRL;pRR\n

Troubleshooting

Symptom Fix
"ENABLE usb_cdc.data" / "NEED usb_cdc.data" boot.py didn't run — copy it to root and power-cycle (full unplug).
Stuck on "WAITING FOR SIMHUB" SimHub on the console port, not data; or formula in the wrong box. Try tools/fake_sender.py.
"SIGNAL LOST" right after connecting Formula is in Message after device connect — move it to Update messages.
Lap times stay 0 Game returns lap time as a number, not a TimeSpan — edit the ms() helper in the JS formula.
A field shows 0 Property name differs for your game — use SimHub's "…" picker for the right $prop('…').
ESP32-S3: safe mode, "USB devices need more endpoints than are available" Native USB has too few endpoints for console-CDC + data-CDC + the drive. boot.py handles this: it disables HID/MIDI and, on ESP32 chips, drops the console channel (so an S3 shows a single COM port = the data port, and has no USB REPL). Hard-reset to clear safe mode after editing boot.py.
T-Display S3: dark screen adafruit_st7789 missing from lib/, or power pin — display.py drives LCD_POWER_ON; if your panel offset looks wrong, adjust colstart/rotation. Lower frequency to 15_000_000 if glitchy.

Going wireless

The presentation layer is fully decoupled from the transport. To go untethered, keep your Dashboard and swap SimHubReader for one that HTTP-polls JSON, plus a small PC bridge that reads the same Custom Serial line and serves it. Open an issue if you want this added as a second transport.


Credits & sources

License

MIT.

About

CircuitPython sim-racing telemetry dashboard driven by SimHub over USB. Reference builds for Adafruit PyPortal Titano and LilyGo T-Display S3.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors