Stopping btd is half the workaround. Its advertisement and the IO capability its default pairing agent gave the controller outlive the process, and a pad still refuses to bond — every manual pairing that worked had a reboot after the stop. Measured 2026-08-19: bluetoothctl power off/on substitutes for that reboot, and `sudo robotctl pad pair` then succeeds first try on a fully installed board. That is what keeps this one command instead of two with a reboot between. The cycle runs on any marker board, even when btd was already stopped: it may have run earlier this boot and left the same residue, and a board someone stopped btd on by hand should not pair differently from one where the tool did it. bluetoothctl power off/on and never `systemctl restart bluetooth`, which leaves this board with no adapter until a reboot. Assisted-by: Claude:claude-opus-5
9.7 KiB
The minimal setup a gamepad bonds under
Recorded 2026-08-18, on Radxa Zero 3W 50:37:CD:16:2A:39, with an Xbox Wireless Controller
78:86:2E:92:47:67. Confirmed on a second Zero 3W with the same card.
The sequence that works
Flash Armbian for Radxa Zero 3, Minimal. Fill in wifi and the username in the imager. Install nothing else — no daemons, no provisioning.
sudo sed -i -E 's|^[[:space:]]*#?[[:space:]]*Privacy[[:space:]]*=.*|Privacy = device|' /etc/bluetooth/main.conf
grep -n "^Privacy" /etc/bluetooth/main.conf
sudo reboot
A reboot rather than systemctl restart bluetooth: the restart sometimes leaves the kernel
holding hci0 with No default controller available, and only a reboot clears that.
Hold the pad's pair button until it blinks fast, then:
bluetoothctl
scan on
Wait for [NEW] Device 78:86:2E:92:47:67 Xbox Wireless Controller, then:
scan off
connect 78:86:2E:92:47:67
Answer yes to Request authorization, then:
trust 78:86:2E:92:47:67
pair is never typed.
What it looks like when it worked
ls /dev/input/js*
dmesg | tail -3
input: Xbox Wireless Controller as /devices/virtual/misc/uhid/0005:045E:0B13.0001/input/input5
microsoft 0005:045E:0B13.0001: input,hidraw0: BLUETOOTH HID v5.09 Gamepad [Xbox Wireless Controller]
Input actually flowing — move the left stick throughout, and look past the first 184 bytes for
events with type 0x02 and advancing timestamps:
sudo timeout 5 cat /dev/input/js0 | od -Ad -tx1 | head -20
The bond survives a pad power cycle (hold the Xbox button ~6 s, then switch back on):
ls /dev/input/js*; bluetoothctl info 78:86:2E:92:47:67 | grep -E "Connected|Bonded|Trusted"
The board it worked on
Not the configuration anyone expected, which is why each is written down:
cat /sys/module/aic8800_bsp/srcversion |
738316A2E9D9825966BDB6B (86016) |
conn_min_interval / conn_max_interval |
24 / 40 — kernel defaults, 30–50 ms |
/etc/bluetooth/main.conf |
Privacy = device |
| daemons running | none |
The driver is the build design/pad-bond-failure.md calls broken. The connection interval is
untouched. Neither is what decides whether a pad bonds.
What has failed
| setup | result |
|---|---|
Bare Armbian, Privacy = off (or unset — BlueZ defaults to off) |
connect returns le-connection-abort-by-local; Paired: no; no SMP exchange at all |
Bare Armbian, no Privacy line, leading with pair instead of connect |
bonds, then every reconnect fails Encryption Change: PIN or Key Missing (0x06), flapping ~1/s |
Provisioned board (Privacy = device confirmed at line 99), padd and btd stopped, manual connect |
Request authorization accepted, then ServicesResolved: no — the pad drops immediately, no js0 |
The third row is the open one: the same card image and the same Privacy value fail once the
board has been provisioned. So it is something provisioning changes rather than a process that
happens to be running — stopping padd and btd did not bring pairing back.
Making a bond and keeping one are different
On a fully provisioned board with everything running, a pad that is already bonded connects
and drives. A fresh robotctl pad pair, after pad forget and with the pad held in pairing
mode, fails.
So nothing here breaks an existing bond. Something in the installed system stops a new one being
made. configd and btd logs say nothing useful about it.
Tomorrow: add one thing at a time
From a board reflashed and confirmed working by the sequence at the top of this page, add one layer, reboot, and try a fresh pairing before adding the next.
Copy the scripts to ~, not /tmp — every step here reboots, and /tmp does not survive
one. Same for any btmon capture worth keeping.
scp scripts/setup-board.sh scripts/migrate-network.sh pierre@BOARD:~/
| step | what it adds | pad pairs? | notes |
|---|---|---|---|
| 0 | nothing — the minimal sequence above | yes | the control |
| 1 | sudo sh ~/setup-board.sh — overlays, console=display, getty mask, onnxruntime |
yes | |
| 2 | sudo sh ~/migrate-network.sh — netplan → NetworkManager |
yes | |
| 3 | sudo -E sh ~/install.sh |
no | first attempts; the daemons were running |
| 3b | DUCK_NO_START=1, then reboot |
yes | so nothing install.sh writes is at fault |
| 4 | systemctl enable --now updaterd |
||
| 5 | ... robotd |
||
| 6 | ... configd |
||
| 7 | ... btd |
||
| 8 | ... padd |
Ran 2026-08-19, pairing manually with bluetoothctl at each step and removing the pad again
afterwards. Steps 1 and 2 bond fine; install.sh is where it stops. So the board bring-up, the
device-tree overlays, the console move and the NetworkManager cutover are all exonerated.
Split inside install.sh on 2026-08-19: with DUCK_NO_START=1 and a reboot, a pad bonds. So
nothing install.sh writes to disk is at fault — not the units, the users, the groups, the release
tree or the token drop-in. It is one of the five daemons, running.
Three attempts at that measurement were wasted before it worked, all for the same reason:
hooks/postinstall inside the release does systemctl enable --now on every unit it ships, from
inside updaterd install, which happens before install_units — so the daemons were up during
every test, and systemctl disable --now afterwards does not undo what they pushed to the adapter.
btd leaves Pairable set, an advertising instance, and the IO capability its default pairing
agent gave the adapter. Reboot before measuring.
It is btd
2026-08-19, one variable, reproducible both ways on 2A:39:
btd enabled, reboot, pad reset, fresh pair |
fails |
btd disabled, reboot, pad reset, fresh pair |
works |
/var/lib/bluetooth was not wiped for the working run, so the two files btd causes BlueZ to
write — attributes, the persisted local GATT database, and identity, the adapter's local IRK —
are both exonerated. So is anything else persisted.
An earlier round where disabling btd did not restore pairing was the pad's own bond slot: an
Xbox pad holds one host bond, and a half-completed attempt leaves it holding a key the board no
longer has. Reset the pad on a laptop between attempts, or the fault is indistinguishable from
a poisoned pad — this cost most of two days.
Nothing running keeps an existing bond from working: a bonded pad connects and drives with the whole stack up. Only making a bond fails.
The mechanism, not yet measured
btd advertises continuously as a peripheral. With Privacy = device that advertising uses a
resolvable private address, while the same adapter acts as central to bond the pad. The SMP DHKey
check is computed over both devices' addresses, so this is the shape that produces
DHKey check failed (0x0b) — the failure that made this tree abandon Privacy = device in the
first place, at a time when btd was running. A btmon capture of a failing pair, read for own
address type and the SMP failure reason, settles it.
Two faults, and they were being read as one
Re-tested with a reset pad, so neither result rests on a poisoned bond slot:
- The board split. On a fresh Armbian with nothing installed, some Zero 3W units bond a pad
under BlueZ's default
Privacy = off. Others do not bond at all underoff, and onlydeviceworks. Roughly half of ten in each group, with nothing measurable separating them. deviceversusbtd. Underdevice, a pad cannot form a new bond whilebtdadvertises.
Together they explain the DHKey check failed (0x0b) that made this tree set Privacy = off in the
first place: that was fault 2, measured on a board that needed device for fault 1, and it was read
as proof that device broke pairing. The result was a setting that could not bond a pad on half the
boards, for a fortnight.
Stopping btd is not sufficient on its own: what it pushed to the controller outlives the process,
so every manual pairing that worked had a reboot after the stop. An adapter power cycle
(bluetoothctl power off && power on) substitutes for that reboot — measured 2026-08-19, pad pair
succeeding first try on a fully installed board — which is what keeps pairing one command.
Both workarounds are behind provision-board.sh --weird-ble, so a board that does not need them
does not carry them: the flag sets Privacy = device and leaves /var/lib/robot/weird-ble, and
robotctl pad pair pauses btd only on a board with that marker. Try a board without the flag
first. Both go when the aic8800 does.
Reboot after each step, and clear both halves of the bond before each attempt: pad forget
or bluetoothctl remove on the board, and the pad held in pairing mode. An Xbox pad keeps one
host bond, and a half-completed attempt leaves it holding a key the board no longer has — which
looks exactly like the fault.
Step 3 needs the environment install.sh reads:
export DUCK_TOKEN=github_pat_replace_with_your_token
export DUCK_REF=pad-privacy-device-not-off
export DUCK_DEV_KEY=$HOME/team.dev.pub
Steps 1 and 2 need neither a token nor the network.
Untested differences against microduck_runtime
microduck_runtime's installer disables wifi powersave on the active NetworkManager connection
(install.sh:244, :383):
sudo nmcli con mod "$WIFI_CON" wifi.powersave 2
scripts/ has no equivalent. The aic8800 is a combined wifi and Bluetooth part sharing one radio
over SDIO, so this is a candidate for the third row above — but the value on the bare board that
worked was never read, so it is a candidate and not a finding.
iw dev wlan0 get power_save