Adding New Hardware¶
ScrollKit runs the same app on more than one HUB75 driver board. Two boards are supported today:
| Board | Chip | Notes |
|---|---|---|
| Adafruit MatrixPortal S3 | ESP32-S3 | The default. Calibrated from a real device. |
| Pimoroni Interstate 75 W | RP2350 | Wired in code; ships with an uncalibrated estimate profile until a baseline is captured on the board. |
A board differs from the others in only three places: how the RGB matrix is constructed on CircuitPython, the default panel geometry, and the calibrated performance profile the feasibility model uses. Everything else (the content types, effects, transitions, the web/OTA stack) is board-agnostic. This page covers how the abstraction works and how to add a board.
How board selection works¶
UnifiedDisplay resolves which board it's on at construction time, in this order:
- An explicit
board=argument. - The
SCROLLKIT_HW_BOARDenvironment variable. - Auto-detection on CircuitPython, reading
board.board_id. - Falling back to the MatrixPortal S3.
from scrollkit.display.unified import UnifiedDisplay
UnifiedDisplay() # auto-detect; S3 on the desktop
UnifiedDisplay(board="pimoroni_interstate75_w") # force a board
On the real device step 3 means a flashed board "just works" with no code change.
On the desktop there is no board, so auto-detect returns the S3 and you select a
different board explicitly (or via SCROLLKIT_HW_BOARD) when you want to model its
performance in the simulator. The registry and resolver live in
src/scrollkit/display/boards.py; that module is import-safe on the device (all
hardware imports are function-local), so it never drags rgbmatrix onto a desktop
or test import.
Estimate vs. calibrated
A board's performance profile is calibrated once real timing is captured
from the device (a *_baseline.json ships in the package); until then it uses
a clearly-labeled ROUGH_ESTIMATE_UNCALIBRATED profile. Feasibility reports
built from an estimate say so and round to one significant figure. The
Interstate 75 W is in the estimate state today. See the
Performance guide for the cost model behind these profiles.
Adding a board, step by step¶
-
Register the board in
BOARDSinsrc/scrollkit/display/boards.py. Add aBoardSpecwith its geometry, LED pitch, HUB75 address-pin count (4 for a 64-row panel, 5 for 64-tall), and amake_matrixbuilder that constructs the panel on-device. Prefer the board's own RGBMatrix aliases so no GPIO numbers are hard-coded; fall back to an explicit pin list:def _make_matrix_myboard(spec, width, height, bit_depth): import board, rgbmatrix, framebufferio matrix = rgbmatrix.RGBMatrix( width=width, height=height, bit_depth=bit_depth, addr_pins=board.MTX_ADDRESS[:spec.addr_pin_count], **board.MTX_COMMON) # rgb_pins, clock/latch/oe display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False) return matrix, display, matrix # (hardware, display, matrix) -
Map its on-device id by adding the board's
board.board_idstring(s) to_ONDEVICE_ID_MAPso auto-detection resolves it to your canonical id. -
Add an estimate profile in
src/scrollkit/simulator/core/hardware_profile.py(an*_estimate()function returning aHardwareProfilewithconfidence=CONFIDENCE_ESTIMATE). Set the RAM and timing fields to the chip's honest ballpark; this is what the feasibility gate uses until you calibrate. Register it in_estimate_for()and give the board a baseline filename in_BASELINE_FILENAMES. -
Calibrate once you have the board. Flash CircuitPython, wire the panel, and capture real numbers over USB:
PYTHONSAFEPATH=1 python test/claude/calibrate_device.py --board <id> --cp 10.2.1 PYTHONSAFEPATH=1 python test/claude/device_benchmarks.py --board <id> --cp 10.2.1These write
<id>_baseline.jsonand<id>_benchmarks.jsonintosrc/scrollkit/simulator/core/. The profile then auto-upgrades toCALIBRATED_FROM_DEVICEwith no further code change. -
Verify. On the desktop, model the board and prove your app stays in budget:
from scrollkit.dev import run_headless result = run_headless(my_app, frames=120, strict=True) # board via SCROLLKIT_HW_BOARD assert result.okThen flash the real board and confirm the panel drives correctly.
Confirm the board id and pin aliases on real hardware
The exact board.board_id string and whether a given CircuitPython build
exposes board.MTX_COMMON / board.MTX_ADDRESS (vs. individual pin names)
should be checked on the device. The make_matrix builder falls back to an
explicit pin list when those aliases are absent.
See also the Performance guide for what the feasibility budget
means, and the Display guide for the UnifiedDisplay API your app
talks to.