Connecting
This is where almost all my time went. The protocol was the easy part.
My setup: Arch Linux, BlueZ 5.87 (the Linux Bluetooth stack), bleak 3.0.2 (a Python Bluetooth library), Python 3.14 and an Intel AX211 adapter. Most of this page is specific to BlueZ.
No bond without an agent
A bond is the saved pairing between two Bluetooth devices. The 10427 ignores every command until the link is bonded. The ble2mqtt project already documents that.
What isn't documented, and what actually blocked me: BlueZ won't finish a bond unless some
program has registered an org.bluez.Agent1, a pairing agent that answers BlueZ's pairing
requests. bleak doesn't provide one. Without an agent you get a connection that looks
perfectly healthy and a train that does nothing. That's the exact symptom people report and
then blame on the protocol.
The train has no keypad and no screen, so it can only use "Just Works" pairing (no PIN to
type or confirm). The fix is to register a NoInputNoOutput agent that accepts every
request. duplo.py does this over D-Bus (Linux's system message bus) using dbus-fast,
which bleak already depends on under Linux. It registers the agent as the default, pairs,
and removes the agent on exit.
If you can't register an agent yourself, bluetoothctl pair <address> works as a manual
fallback, because bluetoothctl registers its own agent while it runs.
Stale bonds fail silently
This is the most important thing on this page.
The train forgets its side of the bond whenever it sleeps or is reset, but BlueZ keeps its
side. Encryption then fails, and the connection attempt just hangs: no error, no
rejection, only a bare TimeoutError once the timeout runs out.
The sign to look for:
The connection times out while the train is advertising normally.
Advertising means the train is broadcasting that it's available. If you can see fresh advertisements and the connection still times out, suspect a stale bond first. I lost a lot of time here, because "it's advertising, so it must be reachable" sounds so reasonable.
The fix is to delete BlueZ's pairing record and bond again. duplo.py spots this case (a
timeout, while the train is advertising, with a bond on record), clears the bond and pairs
again automatically. --forget clears it up front.
Forget the bond before scanning
Removing a device deletes everything BlueZ knows about it. If you do it after a scan, two things break:
- You delete the very device you were about to connect to, and bleak then fails with
device 'dev_XX_XX_...' not found. - Without the bond, BlueZ can no longer link the train's changing address back to the train (see below), so your next scan sees a stranger.
I ran into both. duplo.py now removes old bonds before it starts scanning.
Scans start with cached devices
When a scan starts, BlueZ immediately reports the devices it already has in its cache (as
InterfacesAdded / PropertiesChanged events). bleak passes these on as if they were just
found, so a train that's switched off still shows up as "found". The connection then
times out, because there's nothing there.
That's confusing, because the log says found <address> followed by a timeout, which looks
exactly like the stale bond above.
The fix is to ignore anything seen in the first second or so of a scan. Only a sighting
after that proves the train is really advertising. duplo.py waits 1.2 s, and when it skips
a cached entry it prints seen ... in BlueZ's cache; waiting for a live advertisement, so
you can tell the two problems apart in the log.
The address keeps changing
For privacy, the train advertises a random private address that changes every minute or two. Three different addresses I saw within a few minutes all belonged to the same train, and none was its fixed address (its identity address).
What this means for you:
- Never hardcode an address from a scan. It will be out of date within minutes.
- Once bonded, BlueZ uses a key it got during pairing (the IRK) to translate the changing address into the fixed one, so scans become stable. If you delete the bond, you're back to changing addresses.
- Pass bleak the
BLEDeviceobject you just found, rather than a plain address string. - When retrying, search for the train again instead of reusing the address that failed, unless the user gave one explicitly.
bleak and BlueZ gotchas
Don't pass services=[...] to BleakClient. If BlueZ hasn't cached the train's list of
services yet, the connection fails with an empty error message. That empty error cost me a
lot of debugging, and removing the filter fixed it. Always print type(exc).__name__ next to
str(exc), because several of these errors print as nothing at all.
org.bluez.Error.InProgress on connect is temporary. BlueZ is still finishing the scan.
Wait about 1.5 s and retry the same device; don't scan again.
org.bluez.Error.Failed: No discovery started on scanner.stop(). This happens when
something else has already ended the scan. The results you collected are still good, so catch
the error and carry on. Otherwise it breaks a scan that actually worked.
br-connection-canceled appeared once. It looks like an error from classic Bluetooth,
but the train only supports Bluetooth Low Energy (it advertises BR/EDR not supported). So
it's BlueZ getting confused, not a real problem. It didn't happen again.
Writes fail after a disconnect. Writing to a dead connection raises
BleakError: Service Discovery has not been performed yet, not an obvious "disconnected"
error. Catch it, treat it as a disconnect, and reconnect.
Handle Ctrl-C as a signal, not as KeyboardInterrupt. A KeyboardInterrupt cuts through
asyncio.run before scanner.stop() can finish, which leaves BlueZ scanning in the
background. Instead, install a SIGINT handler that sets an asyncio.Event, and let the
normal shutdown code run.
Useful checks
bluetoothctl info <address> # Paired / Bonded / Connected / AddressType / UUIDs
bluetoothctl devices Paired # what bonds exist
bluetoothctl devices Connected # is something holding the train?
./duplo scan --all # is the adapter working at all?
./duplo scan --all tells "the train is asleep" apart from "the adapter is broken". If it
lists other Bluetooth devices, your setup is fine.