← Talking to a DUPLO train

Talking to a DUPLO train

experiment

How I drive the 2025 LEGO DUPLO train (10427) from a Linux laptop over Bluetooth, and what I learned on the way.

The tool

duplo.py is a single-file command-line tool I wrote to talk to the train over Bluetooth Low Energy (BLE). It speaks LWP3, the LEGO protocol described on the Protocol page, using bleak and no LEGO libraries.

Run it through the ./duplo wrapper. On first run the wrapper creates a Python virtual environment and installs bleak, so there is nothing to set up or activate. It works from any directory.

Commands

./duplo scan                 # find hubs and identify the generation
./duplo scan --all           # every BLE device — is the adapter alive?
./duplo info                 # hub properties + attached ports
./duplo probe                # make the hub describe every port and mode
./duplo watch                # subscribe to everything, print what arrives
./duplo lap --speed 25       # drive at a held speed, naming each tile read
./duplo next                 # drive until ONE new tile is read, then stop
./duplo shell                # interactive
./duplo drive --speed 40 --seconds 5
./duplo tag 9                # write the TAG port (rejected on the 2025 hub)
./duplo sound horn           # 2018 train only
./duplo led green            # 2018 train only

Every command that connects to the train accepts an optional Bluetooth address. Without one, it uses the first DUPLO train it finds. It's best to leave the address out, because the train keeps changing the address it advertises.

Shared options

Option Meaning
--timeout N seconds to wait for the train to appear; 0 waits until Ctrl-C
--forget drop BlueZ's pairing record before connecting (normally automatic)
--pair / --no-pair force bonding on or off; default is to bond for hub type 0x21

BlueZ is Linux's Bluetooth stack. Bonding (saving a pairing) is required by the 2025 train, hub type 0x21.

--timeout 0 is the one to remember: start the command first, then press the train's button.

Per-command options

scan: --seconds N (default: until Ctrl-C), --all

watch: --seconds N, --all-frames (don't dedupe unchanged values), --mode PORT=MODE (repeatable; one mode per port is a protocol limit), --delta N (0 = every sample), --probe / --no-probe, --reconnect / --no-reconnect, --poke-motor (brief motor run on connect, to prove output commands work), --verbose

lap: --speed N (negative = reverse), --seconds N, --period N (how often to re-send the speed, default 0.05 = 20 times a second)

next: --speed N, --seconds N, --period N

drive: --speed N, --seconds N

tag: --sweep LO HI, --gap N, --port N

Interactive shell

./duplo shell keeps a connection open and lets you type commands:

<number>        motor speed, -100..100
s, stop         motor off (also cancels hold)
hold N          keep re-asserting speed N, overriding tile actions
tag N           write index N to the TAG port
sweep LO HI     write every tag index in a range, 2s apart
watch           subscribe to every port
sub P M [D]     subscribe port P to mode M (delta D; 0 = every sample)
unsub P         stop notifications on port P
ports           print the port/mode map
probe           re-interrogate the hub
quiet / loud    stop or resume printing incoming messages
raw 08 00 81..  send arbitrary LWP3 bytes
off             shut the hub down
q, quit         disconnect

raw is the escape hatch: it lets you send any protocol bytes by hand.

What it handles for you

  • Pairing: it registers a BlueZ pairing agent and bonds with hubs that need it.
  • Stale bonds: if connecting times out while the train is advertising, it clears the out-of-date bond and tries again.
  • Old scan results: it ignores the cached devices BlueZ replays when a scan starts, so "found" means the train is really there.
  • Both train generations: it finds ports by device type, not port number, so it works with the 2018 and 2025 hubs.
  • Port cache: it saves the probed port map to .duplo-cache.json, per hub type, so later runs start faster.
  • Reconnecting: it reconnects when the train sleeps or drops the connection.
  • Lost connections: writing to a dead link prints a warning instead of crashing.
  • Ctrl-C: it stops the scanner cleanly when you press Ctrl-C.