Setting up a dev board
Last updated
A dev board trusts the team dev key, so it will install anything anyone on the team builds. A customer robot is set up differently and deliberately refuses those builds.
Everything here assumes a dev board, never a customer robot.
Nothing is relaxed
This is worth being clear about, because it is easy to assume otherwise. A dev build gets the same signature and hash verification, the same health gate, the same auto-rollback. The only difference is which key signed it.
That is also what keeps these builds off customer robots, which refuse a dev key twice over: allow_dev_keys = false, and a trusted key only counts as a dev key if its filename ends .dev.pub. Both halves of the setup below exist to flip exactly that.
Flash the board
Use the Armbian imager: pick Radxa Zero 3, then Armbian 26.2.1 Minimal.
Fill in the imager’s profile before writing — wifi network and password, and the username and password you want. Doing it there saves a serial console later: the board joins your network on first boot and is reachable over ssh straight away.
Then send your ssh key, so provisioning can reconnect after it reboots the board:
ssh-copy-id radxa@192.168.1.42
What you need
- The board’s IP address. mDNS on this image is unreliable.
duckctl ipasks the robot over Bluetooth, which needs no network of your own; your router’s lease table is the fallback. - ssh key access, from the step above. Provisioning reboots the board and reconnects by itself, and a password prompt cannot survive that.
- A GitHub token while the repository is private — its release assets are unreachable without one.
- A clone of the repo. The dev key it needs is committed at
deploy/dev-key/team.dev.pub, so there is nothing to ask anyone for.
Install
export DUCK_TOKEN=github_pat_replace_with_your_token
./scripts/provision-board.sh --pause-btd-on-pair --name <MY_ROBOT> radxa@192.168.1.42
That sends your dev key, starts provisioning, waits out the reboot, streams the log, and ends on robotctl health.
The log output is a viewer, not the thing doing the work. Provisioning installs a systemd unit that resumes at boot, so the board finishes whether or not you are still watching. Ctrl-C costs you nothing:
ssh -t radxa@192.168.1.42 'sudo tail -f /var/lib/robot/provision.log'
Why --pause-btd-on-pair is in that command
On the aic8800 radio a pad cannot form a new bond while btd is advertising. The flag leaves a marker so robotctl pad pair stops btd and power-cycles the adapter for the pairing window, then starts it again. An existing bond is unaffected.
It is the default here for a practical reason: a board that needed it and was provisioned without it presents as a gamepad that will not pair, and every plausible cause you chase first is somewhere else.
The three configurations, and how to tell which you have
There are two independent faults, so there are two flags. Pair a pad and read the failure, then pick:
| What you see | What the board wants |
|---|---|
| The pad bonds and drives | Nothing — provision with no flag |
| The pad will not bond; the last SMP step never completes | --pause-btd-on-pair |
The pad will not bond even with btd paused |
--weird-ble (implies the pause, adds Privacy = device) |
The pad bonds, then flaps — PIN or Key Missing (0x06), no input device |
--weird-ble is wrong here: drop it, keep the pause |
That last row is the one to watch for. Privacy = device on a board that only needed the pause produces a bond that immediately stops working, which is harder to diagnose than a pad that plainly will not pair — measured on one board as 46 flaps in 45 seconds where off plus the pause held. --weird-ble is therefore not the default.
Moving a board between the two
# from --weird-ble back to the pause alone
sudo sed -i 's/^Privacy = device/Privacy = off/' /etc/bluetooth/main.conf && sudo reboot
# the other way, with the copy provisioning leaves on the board
sudo DUCK_WEIRD_BLE=1 /usr/local/sbin/robot-setup-board && sudo reboot
# check the board needs neither
sudo rm /var/lib/robot/weird-ble
sudo sed -i '/^Privacy = /d' /etc/bluetooth/main.conf && sudo reboot
Re-pair after any change to
Privacy. It changes the address the stored keys were derived against, so existing bonds stop matching and flap withPIN or Key Missinguntil they are re-made.
Provisioning from a branch
./scripts/provision-board.sh --ref BRANCH radxa@192.168.1.42
One design decision is worth understanding: golden stays the stable release — it is the boot recovery net’s fallback, and a branch build as golden would give a broken branch a broken fallback — while current is the branch.
Provisioning fails if that build cannot be installed, or if it is installed and then rolled back by the health gate. This is deliberate:
A dev board quietly running the stable release when a branch was asked for is the worst failure to debug: everything looks installed and the code under test is not there.
Give CI its minute or two, and check with gh run list --branch BRANCH if it stalls.
Other useful flags
--name Duckynames the robot instead of leaving it theduck-7f3ait derives from its serial--localsends this clone’sprovision.shinstead of fetching it — how to test a change to the provisioning scripts without merging first--no-dev-keymakes a board that only takes releases
Check it worked
robotctl health
grep -c 'DEV BOARD' /var/lib/robot/provision.log
The board only counts as a dev board if the key really installed, and that is checkable rather than something to remember. 1 means yes. 0 means the key did not land, and --ref will be refused later with an error that reads like a corrupt release. That is the failure this check exists to catch early.