BUILD_AND_DEPLOY.md 13 KB

Build And Deploy

These instructions cover firmware builds for macOS, Linux, and Windows, plus static hosting of the WebUSB app through GitHub Pages.

1. Prerequisites

Hardware:

  • nRF52840 board supported by the Adafruit nRF52 Arduino core
  • USB cable
  • Steam Controller 2 controller

Software:

  • arduino-cli
  • Adafruit nRF52 Arduino core
  • adafruit-nrfutil (Python package, for DFU packaging - see "Build the firmware")
  • Chrome or Edge for the WebUSB app

2. Install Arduino CLI

macOS

brew install arduino-cli

Linux

Use your package manager if it ships a recent arduino-cli, or download the release archive from Arduino and place arduino-cli on PATH.

Windows

Use choco and do a choco install arduino-cli. If you don't have that then install the official Arduino CLI zip or MSI, then ensure arduino-cli.exe is on PATH.

3. Install the board core

Run once on any platform:

pip install adafruit-nrfutil         # DFU packaging helper (required by the Adafruit nRF52 build recipe)
arduino-cli config init
arduino-cli core update-index
arduino-cli core install adafruit:nrf52 --additional-urls https://adafruit.github.io/arduino-board-index/package_adafruit_index.json

4. Build the firmware

From the repository root:

make build

That's the whole command — the USB flags the firmware needs are baked in, so you don't pass them yourself:

  • CFG_TUD_HID=6 — Steam mode exposes four HID interfaces (the Adafruit nRF core defaults to 2); one extra for mouse and one for WebUSB brings the total to 6.
  • CFG_TUD_TASK_QUEUE_SZ=512 — a deeper TinyUSB device event queue; the default of 16 can deadlock the firmware's loop under heavy USB traffic and trip the watchdog.
  • CFG_TUD_VENDOR_TX_BUFSIZE=256 — the WebUSB status blob (~118 B) must fit the vendor TX FIFO in one write; the default 64 is too small and the panel (which drops frames rather than block the loop) would send nothing — a blank dashboard.

Overriding the defaults (only if you need to) — pass them as make variables:

make build CFG_TUD_HID=6 CFG_TUD_TASK_QUEUE_SZ=128   # different interface count / queue depth
make build EXTRA_FLAGS="-DOPK_LOG=1"                  # add your own defines
make build FQBN=adafruit:nrf52:somethingelse          # a different nRF52840 board

Calling arduino-cli directly instead of make? Then you must supply the flags yourself — the build #errors without them (so a forgotten flag fails loudly instead of shipping a broken/deadlock-prone image):

arduino-cli compile -b adafruit:nrf52:feather52840 --build-property "build.extra_flags=-DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_HID=6 -DCFG_TUD_TASK_QUEUE_SZ=512 -DCFG_TUD_VENDOR_TX_BUFSIZE=256" --build-property "compiler.c.elf.extra_flags=-Wl,--wrap=tud_vendor_control_xfer_cb" OpenPuck

4a. Raytac MDBT50Q-CX-40

Retail listings may shorten MDBT50Q-CX-40 to MDBT50Q-CX. This target is only for the nRF52840 model with 1 MB flash and 256 KB RAM, not the CX-33.

The build borrows Adafruit's adafruit:nrf52:mdbt50qrx profile for its nRF52840, S140, and linker settings; it does not modify that installed profile. CX-40 GPIO differences remain in OpenPuck.

Prerequisites

Download nRF Util and put it in your path.

mkdir -p ~/.local/bin
install -m 755 ~/Downloads/nrfutil ~/.local/bin/nrfutil
export PATH="$HOME/.local/bin:$PATH"

# Install the DFU packaging and flashing commands.
nrfutil --version
nrfutil install nrf5sdk-tools
nrfutil install device

First-time S140 provisioning

OpenPuck requires S140 6.1.1 (0xB6). Download and extract Nordic's nRF5 SDK, then provision its SoftDevice before installing OpenPuck for the first time:

# Hold the dongle button while inserting it; release when the LED fades.
SDK=/path/to/nRF5_SDK
make provision-raytac-softdevice \
  SOFTDEVICE_HEX="$SDK/components/softdevice/s140/hex/\
s140_nrf52_6.1.1_softdevice.hex"

# Unplug the dongle and re-enter Open DFU before installing the application.

Provisioning is separate from routine builds and updates.

Build and flash

# Build and create a Nordic Secure DFU application package.
./gen_version.sh   # recommended: embeds version (when tagged) + git hash provenance
make build-raytac
make package-raytac

# Enter physical Open DFU mode, then flash the package.
make flash-raytac

# For later updates, this runs all three commands above.
make deploy-raytac

The Adafruit compile recipe also emits build/raytac/OpenPuck.ino.zip in its older DFU v0.5 format; ignore that automatic output. make package-raytac creates OpenPuck-mdbt50q-cx-40.zip, the Secure DFU package used by the factory Open DFU bootloader. These targets do not replace that bootloader or use arduino-cli upload.

The relevant flash layout is:

  • S140 and the MBR occupy the region below the application.
  • OpenPuck starts at 0x26000 and must end before 0xED000.
  • InternalFS occupies 0xED000 through 0xF3FFF.
  • The factory bootloader occupies 0xF4000 through 0xFFFFF.

The populated active-low blue LED is P0.08. OpenPuck reserves it for the existing remote-wake diagnostic rather than connection or pairing state. The WebUSB firmware updater (§5b), its serial/UF2 DFU buttons, and the debug full-board wipe all target the Adafruit bootloader and are rejected by this build; use the physical Open DFU workflow above instead. The panel's factory erase (§6, filesystem-only) and the serial ERASE-ALL console command work normally.

5. Upload the firmware

The quickest path is make. The serial port is a required argument (find it with arduino-cli board list):

make flash /dev/cu.usbmodem1101    # upload the most recent build to that port
make deploy /dev/cu.usbmodem1101   # build + flash in one step (same build overrides as `make build`)

Use the port for your OS: macOS /dev/cu.usbmodem*, Linux /dev/ttyACM0, Windows COM5.

DFU note: in puck (Steam/Lizard) mode the firmware drops the CDC serial port to free a USB endpoint, so arduino-cli can't auto-reset the board into its bootloader. If the upload can't connect, put the board in DFU mode first by double-tapping RST, then make flash <bootloader-port> (re-check arduino-cli board list — the port can change in DFU mode). The drag-and-drop UF2 path in §5b also works.

Manual upload (without make)

Find the board port with arduino-cli board list, then:

macOS / Linux

arduino-cli upload \
  -b adafruit:nrf52:feather52840 \
  -p /dev/ttyACM0 \
  OpenPuck

Replace /dev/ttyACM0 with the actual port. On macOS it is usually /dev/cu.usbmodem*.

Windows

Find the COM port:

arduino-cli board list

Upload:

arduino-cli upload `
  -b adafruit:nrf52:feather52840 `
  -p COM5 `
  OpenPuck

Replace COM5 with the actual board port.

5b. Upload via DFU (nRF52840 UF2 bootloader)

If the board has the Adafruit nRF52 UF2 bootloader (common on Pro Micro nRF52840 boards), you can upload by dragging the compiled .uf2 file onto the board's mass-storage volume:

  1. Double-tap the RST button. The board mounts as a UF2BOOT / NRF52BOOT drive.
  2. Locate the compiled .uf2 file:

    # After a successful `arduino-cli compile`, find the .uf2 in the build directory:
    ls /tmp/arduino/cores/adafruit_nrf52_adafruit52840/*.uf2
    # or, on Windows, look in %TEMP%\arduino\...
    
  3. Copy the .uf2 file onto the UF2BOOT drive. The board auto-ejects and reboots with the new firmware.

Alternatively, use the adafruit-nrfutil DFU Python tool with the board in DFU mode (bootloader LED pulsing):

# Enter DFU mode (double-tap RST). On Linux/macOS:
adafruit-nrfutil --verbose dfu serial --package OpenPuck/OpenPuck.ino.adafruit_nrf52_feather52840.zip -p /dev/ttyACM0 -b 115200

# On Windows (PowerShell):
adafruit-nrfutil --verbose dfu serial --package OpenPuck/OpenPuck.ino.adafruit_nrf52_feather52840.zip -p COM5 -b 115200

Replace the port (/dev/ttyACM0 / COM5) with the actual board port.

Note: The .zip package is generated automatically by arduino-cli compile when the Adafruit nRF52 core is used. If it is missing, ensure adafruit-nrfutil is installed and recompile.

Flashing from the WebUSB panel (no tools, no drag-and-drop)

A board already running OpenPuck (status protocol v15+) can be updated entirely from the WebUSB configurator's Firmware update tab: drag-and-drop a .uf2 (or click to browse), or pick a version from the built-in releases list — each release offers the standard build or, via its checkbox, the -factory-reset build (wipes settings + pairing once on first boot). A blocking modal shows download/transfer/verify/apply progress and reports the old → new build once the puck reconnects. Under the hood the panel extracts the app image from the .uf2 and streams it over the normal WebUSB connection into spare flash high in the app region (~15 s; the running firmware keeps working); the firmware CRC32-verifies what landed and commits a one-page "apply on reboot" record; a final automatic reboot copies staged→app from RAM (~5 s dark) and comes back up on the new firmware. See OpenPuck/fw_update.h for the design.

Release downloads / the firmware branch: GitHub's release-asset CDN sends no CORS headers, so the browser cannot fetch release assets directly. The release workflow therefore mirrors every OpenPuck .uf2 onto the orphan firmware branch, and the panel downloads from raw.githubusercontent.com/safijari/openpuck/firmware/<asset> (which is CORS-clean). If a release is missing from the mirror, the panel falls back to opening the asset in a new tab for manual drag-and-drop.

Failure safety: nothing is armed until the staged image verifies in flash, so a disconnect, error, or power cut during the transfer leaves the current firmware untouched. The apply step erases the app's vector page first and rewrites it last (first word dead-last), so even a power cut mid-apply leaves the board "app-less" — the resident UF2 bootloader then keeps it as the UF2BOOT drive for drag-and-drop recovery. A half-flashed, crash-looping state is not reachable.

6. Factory reset (erase persistent storage)

Re-flashing firmware does not erase the board's internal LittleFS. The paired-controller bond (bonds.bin) and every saved setting (cfg.bin: USB mode, chord assignments, back-paddle map, mouse sensitivity) survive a fresh build and upload. To bring a board up in a truly clean state — a new unit, a hand-me-down with a stale bond, or a corrupted config — wipe the filesystem with one of:

  • Recovery build (-DOPK_FACTORY_RESET=1): a firmware that wipes all persistent storage once, on the first boot after flashing, then behaves like a normal build that persists settings. Use it to recover a board from a bad config/bond without a console or panel:

    ./gen_version.sh   # recommended: embeds version (when tagged) + git hash provenance
    make build-recovery
    

(make build-recovery is just make build plus -DOPK_FACTORY_RESET=1; the usual USB flags are still baked in.)

It is not a wipe-every-boot image: after the one-time reset it stamps a tag file with the build's git hash, so subsequent boots skip the wipe and persist normally. Flashing this same image again won't re-wipe (the tag matches); flashing a different build (different git hash) re-triggers the one-time reset. For an on-demand wipe at any time, use the WebUSB button or serial ERASE-ALL below. Re-pair the controller after a reset.

  • WebUSB panel (any mode): open the panel (§8), and in the maintenance card click ⚠ Factory erase. Confirm the two warning dialogs and type ERASE when prompted. The board reformats its filesystem and reboots to factory defaults. This works in every USB mode.
  • Serial console (CDC): connect to the board's serial port at 115200 baud and send the line ERASE-ALL (exact, all caps). Same effect: reformat + reboot.

Both reformat the entire internal filesystem (cfg.bin + bonds.bin and anything else), so the action is irreversible and the controller must be re-paired afterwards (see §7).

Note on the serial method: puck (Steam/Lizard) mode drops the CDC console by default to free a USB endpoint for the wake-mouse interface, so the serial port may not be present in that mode. Either arm the one-shot debug CDC first (panel debug-CDC toggle / D console command, which keeps the console for the next boot), or just use the WebUSB Factory erase button, which is available in all modes.

7. Pair and verify

  1. Flash the board.
  2. Plug it into the host.
  3. In Steam mode it enumerates as a puck-compatible device.
  4. Pair the controller to one of the bond slots.
  5. Verify that the slot returns 0xB4 = 0x02 when connected.

8. Run the WebUSB app locally

WebUSB requires a secure context. http://localhost qualifies.

macOS / Linux

cd docs
python3 -m http.server 8008

Open:

http://localhost:8008

Windows

cd docs
py -m http.server 8008

Open:

http://localhost:8008

9. Known operational details

  • Chrome or Edge is required for WebUSB.
  • The board re-enumerates on USB mode switches.
  • Poll interval tuning in the WebUSB app is session-only by design.
  • Bonds persist in the board's internal filesystem.