Firmware

The gadget runs MicroPython on the RP2350. Source lives at firmware/ in the public repo. You flash it over USB-C using a helper script.

Flashing

Two flashes happen the first time you build a board:

  1. MicroPython runtime — a one-time drag-and-drop in BOOTSEL mode. Done once per board.
  2. The OTP firmware itselfscripts/flash.sh over USB-C. Done every time the source changes.

scripts/flash.sh only works after MicroPython is on the board, so step 1 has to happen first. Do both before you assemble the case — the BOOTSEL button is on the bare Waveshare board and is not exposed through the case. See Assembly → Step 0 for the full walkthrough with photos. If you bought a ready-built device, you don't need to do any of this — it's already flashed.

First-time — install MicroPython

A fresh Waveshare RP2350-Touch-LCD-3.5 ships with a Waveshare C demo, not MicroPython. To put MicroPython on it:

  1. Download the Waveshare MicroPython build for this exact board: WAVESHARE_RP2350_Touch_LCD_3.5.uf2
  2. Hold BOOTSEL on the Waveshare board, plug it in over a USB-C data cable (charge-only cables won't work), then release.
  3. The board mounts as a USB drive named RPI-RP2. Drag the .uf2 onto it. The drive disappears — MicroPython is installed.

You only do this once per board.

Deploying the OTP firmware

The repo ships a flash.sh that wraps mpremote and handles all the fiddly bits.

# Clone the public repo
git clone https://github.com/matrix-mole/otp-gadget.git
cd otp-gadget

# Install mpremote (one-time per machine)
pip install mpremote

# Plug the gadget in over USB-C, then:
./scripts/flash.sh

The interactive run lists every connected board, asks you to pick one, prompts for a label the first time, and uploads only the files that changed since the last flash.

Useful flags

./scripts/flash.sh --label alice         # non-interactive: flash a specific board
./scripts/flash.sh --label alice --clean # wipe firmware first (use when something's wrong)
./scripts/flash.sh --all                 # flash every connected board in sequence
./scripts/flash.sh --refresh-list        # re-probe boards (if the cache is stale)

Each board you flash gets a label written to /flash/device_label.txt on it. The script caches board info between runs so it doesn't have to interrupt idle boards just to list them.

If a flash transiently fails with a USB error ("Device not configured" on macOS, that sort of thing), the script retries up to three times with backoff.

SD card setup

The gadget needs two MicroSD cards to actually do anything useful:

  • Own card — sits in the gadget's onboard slot. Holds your encrypted secrets (master key, contacts, send and receive pads). Encrypted at rest with AES-256-CTR under a PIN-derived key.
  • Guest card — used briefly during key exchange with another gadget. After the exchange completes, the guest card is wiped and you get it back.

Both cards must be FAT32. See Bill of Materials for the recommended card.

On first power-on with a blank own card, the gadget walks you through:

  1. PIN entry (4–6 digits).
  2. Generation of the on-card key material — master key, recovery token, salt, verify blob.
  3. A pointer to Contacts → + Add contact so you can run the key exchange with another gadget.

You don't pre-format the card with any special structure. The firmware creates everything it needs on the fly.

Updating

To pick up a newer firmware version:

  1. git pull in your cloned repo.
  2. Plug the gadget in.
  3. ./scripts/flash.sh --label <name>.

The script computes SHA-256 of every source file and compares against the manifest on the board. Only changed files are uploaded; if nothing changed, the upload is skipped entirely. Your /flash/ files (the device label, the device secret, the firmware manifest) are preserved across normal flashes.

If you want a guaranteed clean slate (e.g. the board is in a weird state), add --clean. That wipes /firmware/ before uploading. It does not wipe /flash/ — your device_secret is safe.

What does not happen during a flash

  • Your SD cards are untouched. Encrypted pads, contacts, settings — all on the card, all preserved.
  • Your device_secret is untouched. It lives at /flash/device_secret.bin; flash.sh only replaces /firmware/ and /main.py.
  • Your PIN attempt counter is untouched.

Flashing only replaces application code. To wipe state, you do it explicitly from inside the firmware itself — there's a "Wipe card & start fresh" flow under PIN screen → ? button, and a "Full factory reset" flow under that, which clears the device_secret too.

Where the code lives

Source-of-truth structure inside firmware/:

FolderWhat's inside
core/Pure Python business logic — OTP encoding, SD data layout, message flows. No hardware calls. Runs anywhere.
hal/Hardware abstraction layer. real.py is the on-board implementation; sim.py runs on a laptop with a browser-based simulator.
hal/drivers/Vendored MicroPython drivers (ST7789, FT6336U, sdcard, AXP2101, PCF85063).
sim/The localhost simulator UI (Flask). Lets you exercise the firmware without hardware.
tests/Unit tests for core/.
setup/One-time board setup scripts.
main.pyBoard entry point. Constructs RealHAL and calls main_loop.

The simulator is worth trying before you build a physical device — it's the same core/ running against sim.py instead of real.py, so flows behave identically. Run it with ./scripts/run.sh.


Next: Using the gadget — the actual day-to-day flows: key exchange, sending, receiving, history.

Ask a question

Missing something?

Send a question about this page. Useful questions may become FAQ or troubleshooting entries later.


Pinned to 0e7c8e4 · 2026-06-02