Repository navigation
overview
What mbdeploy solves, what it depends on, and how to install it on macOS and Linux.
mbdeploy is a standalone command-line tool that builds micro:bit firmware and
flashes it over USB via pyOCD, to one board out of a
classroom fleet. It keeps a persistent JSON registry of every board it has seen,
so boards can be addressed by a short stable name rather than by a 48-character
hardware UID. Since the fleet daemon landed it also serves boards over the
network: mbdeploy serve on a host with micro:bits attached makes each of them
reachable from any machine on the same LAN.
- Classroom operators. Plug in a board, find out which board it is, build the firmware, flash it, then talk to it over serial to check that it came up.
-
AI coding agents. The reason
mbdeploy --agentexists. An agent drives the tool non-interactively, cannot see the desk, and must be able to trust the exit code rather than scraping human-readable text. The whole command surface is shaped around that: a strict0/ non-zero contract, the board's serial reply alone on stdout, every status line on stderr.
Nearly every design decision in mbdeploy traces back to one of these.
Boards have no usable address. pyOCD identifies a board by the USB serial
number of its on-board DAPLink interface chip — 40 to 52 hex characters. Nobody
can type that, and nothing about it says which board on the bench it names.
Meanwhile the board does have a human-scale identity: the five-letter word
(tovez, gopiv) its runtime derives and shows when pairing. But that word
belongs to a different chip, so it cannot be computed from the UID.
mbdeploy closes the gap by attaching through the debug probe the UID names and
reading the target's device id over SWD. See
The device model.
Ports move. The OS re-issues serial port names on every reconnect, so two
boards routinely trade paths between sessions. A remembered port is a trap:
acting on a stale one means acting on a different board than the path names.
That has already produced a real wrong-board flash, and the fix is now an
explicit invariant in the code — deploy resolves a /dev/… path against the
live port map and refuses rather than guessing.
Relays must not be reflashed by accident. A fleet has robots and it has one
or two radio bridges. Flashing robot firmware onto the bridge takes the whole
classroom off the air. mbdeploy reads a board's role out of its announcement,
treats anything containing RELAY or BRIDGE as a gateway, and refuses to flash
it unless --force-relay says the operator meant it. (This guard depends on the
board having announced a role at all — see the caveat in
Open tasks.)
| Subcommand | Role in the workflow |
|---|---|
probe |
Walks every connected probe, refreshes ports, captures announcements, reads missing board names over SWD, saves the registry. |
list |
Prints every board the registry has ever seen plus anything newly attached, with a CONN column. Cheap; --fast makes it cheaper. |
build |
Shells out to the project's build.py. |
deploy |
Resolves a target, checks the relay guard, confirms the board is live, optionally builds, then flashes and resets — recovering a locked part with a mass erase if the first flash fails. |
connect |
Opens the board's serial port with DTR held low, and either runs one request/response exchange or hands the port to the user. |
serve |
Runs the fleet daemon: watches USB and advertises each board's serial and flash services over mDNS. |
Full detail in the command reference.
| Python | ≥ 3.10 |
| Dependencies |
pyocd>=0.44.1, pyserial>=3.5, zeroconf>=0.150.0 (plus their transitive requirements — 23 packages in total) |
| Platforms | macOS and Linux, including Raspberry Pi OS on aarch64 |
| Hardware | A micro:bit whose on-board DAPLink interface enumerates as USB 0d28:0204
|
mbdeploy never needs a compiler at install time on either platform: every
dependency ships as a prebuilt wheel, aarch64 included.
The tool is normally installed with pipx, into its own isolated environment:
pipx install git+https://github.com/Busboombot/mbdeploy.gitFor local development, clone and install editable:
git clone https://github.com/Busboombot/mbdeploy.git
pipx install --editable ./mbdeployRe-install after editing the source:
pipx install --editable --force ./mbdeployConfirm what you actually got:
mbdeploy --version # prints "mbdeploy <version>"
mbdeploy --agent # prints the bundled agent manual to stdoutpyocd is a declared dependency, so it is importable inside the same
environment even when its console script is not on PATH. mbdeploy therefore
always invokes it as <python> -m pyocd, never as a bare pyocd — a pipx
install works with no extra setup.
Run mbdeploy from the root of the project whose firmware you are deploying.
All project paths are relative to the current working directory:
| Path | Used by | Override |
|---|---|---|
./build.py |
build, deploy --build
|
--build-cmd (on build) |
./MICROBIT.hex |
deploy |
--hex PATH |
./config/devices.json |
deploy, list, probe, connect, serve
|
--config PATH |
This matters for serve under a service manager, which gives the process no
useful working directory of its own — see
Deploying the daemon.
mbdeploy behaves identically on Linux. The live port map is a pyserial
VID:PID scan rather than any OS-specific tool, so nothing about target
resolution or the registry is macOS-only. Four Linux-specific facts are worth
knowing, all verified on Raspberry Pi OS (Debian Bookworm, aarch64, Python
3.13.5) against real micro:bit V2 hardware:
-
Port naming. A micro:bit enumerates as
/dev/ttyACM0, not the macOS/dev/cu.usbmodem*spelling. Use that form when targeting by path. -
Group membership is the whole of the access story. The user running
mbdeploymust be inplugdev(raw USB, needed by pyOCD) anddialout(the serial port). Without both, the live port map comes back empty even though a probe is connected,deployrefuses a/dev/…target, and no announcement can be read:sudo usermod -aG plugdev,dialout <user>
Re-login (or reboot) before testing — group membership is only picked up by a new session.
-
No new udev rule is needed on Raspberry Pi OS. It already ships
/lib/udev/rules.d/70-microbit.rules, which matchesSUBSYSTEM=="usb", ATTR{idVendor}=="0d28"and appliesTAG+="uaccess". Both/dev/ttyACM0and the underlying USB device node come uproot:plugdev 0660, soplugdevmembership alone is what grants headless access. Do not add aMODE="0666"rule for this vendor id — an early draft of this document proposed one and it is unnecessary. -
pyOCD runs headless and unprivileged. With the two group memberships in place,
python -m pyocd listenumerates the probe and identifies the target as nRF52833 / micro:bit V2 with no root and no additional pyOCD configuration.
Install footprint on aarch64: all 23 dependencies resolve to prebuilt
aarch64 wheels on Raspberry Pi OS — there is no compilation step at any point —
and a fresh virtualenv with mbdeploy installed measures about 83 MB. That
is small enough to sit on a Pi alongside whatever else the node runs.