Command-line tool
pipx install openbricks installs one console script, openbricks,
which mirrors the pybricksdev workflow: flash firmware over USB, then
run / upload / stop programs and pull logs over BLE. With the [sim]
extra installed, openbricks sim … forwards to the
MuJoCo-backed simulator.
A typical session:
$ openbricks flash --name RobotA # port, chip and newest firmware auto-detected
$ openbricks list # hubs in BLE range
$ openbricks run -n RobotA main.py # push + stream output
$ openbricks upload -n RobotA main.py # stage; start it with the hub button
$ openbricks stop -n RobotA # Ctrl-C a running program
$ openbricks log -n RobotA # dump the most recent run log
$ openbricks docs hardware # open this manual offline in your browser
Firmware versions and provenance
openbricks flash first reports the firmware already on the chip —
version plus an (official) / (customized) suffix — before it
looks up the newest release. Flashing the same version again, or
an older one, asks for confirmation first; pass --yes to skip
the prompt in scripts.
The default output is step-level (probe, download, erase, write,
hub name, marker, reboot); pass --verbose / -v to also echo
every underlying mpremote / esptool command line and the
firmware cache paths — useful when reporting a flash problem.
Every firmware image published by CI is signed (Ed25519), and the
CLI ships the matching public key. An image whose .bin.sig
verifies is labeled (official); anything else — a self-built
image, a missing or wrong signature — is (customized). Customized
firmware flashes normally: the suffix is provenance, not a gate.
After each flash the verdict is stored on the hub, which is how the
next openbricks flash labels the current firmware.
The suffix follows the version everywhere it reaches you: the
firmware 1.79.0 (official) banner at the top of every
openbricks run, the started: header line in every run log
(openbricks log), and the flash preflight above. On the hub,
openbricks.firmware_label() returns the same string.
Programs are compiled on the host
Since 1.92.0, openbricks run and openbricks upload cross-compile
your script with mpy-cross before connecting, and stage
compiled bytecode instead of source (like Pybricks). Three things
get better:
syntax errors surface in milliseconds, on your terminal, naming your file and line and quoting the offending source line — no BLE scan, no connect, no upload round-trip;
programs start faster: the hub loads bytecode directly and skips its on-device parse/compile step;
tracebacks name your real file and line (
File "square.py", line 12) instead ofFile "<string>".
No flags, nothing to configure. Firmware older than 1.92.0 can’t run
compiled programs, so the CLI probes the hub’s version in-session and
sends plain source instead — announced on stderr, never silently.
upload --path (custom boot flows) always stages your file verbatim,
uncompiled, at the path you give.
run is an upload-then-run (since 2.7.0, deliberately
different from Pybricks): it stages your script at the button’s
/program.mpy before executing it, so even a run that fails midway
leaves the program on the hub — press the start button to rerun it,
no BLE round trip needed. The flip side: running a calibration or a
one-shot diagnostic replaces the button’s program too, so re-upload
your mission after such tools (upload alone stages without
running).
flash --with-qtr-init additionally stores a starter QTR
line-sensor calibration at /qtr.cal (recorded on the reference
bench, default pins 1-10), so the line-follow examples work on a
fresh hub out of the box. Heights, mats and lighting differ — run
examples/qtr_calibrate.py once for a calibration measured on your
own rig.
Fast uploads
An upload is one BLE session and, since 4.10.0, one round trip of work on the hub. The CLI connects (discovering only the hub’s UART service), interrupts whatever the hub runs and enters the raw REPL in a single write, and pastes one program that prints the firmware version, writes the file, syncs the clock, prints the size confirmation and drops straight back into the button’s idle loop; the host reads the confirmation and the idle banner off the stream and hangs up. Earlier versions spent four such execs — a version probe, the file, a confirmation, the idle-loop restart — each with its own handshake, plus a fixed settle wait, on every upload.
The CLI remembers each hub’s firmware version (in
~/.cache/openbricks/hubs.json, or $OPENBRICKS_CACHE_DIR), so a hub
it has met before is not probed again. The staged program still
prints its version first and refuses to write compiled code on
firmware that cannot run it, so a hub re-flashed to something older
is caught in-session: the CLI says so and stages source instead.
On the hub side, firmware 4.10.0 asks for a 512-byte BLE packet size (the stack’s default of 256 capped every write at 253 bytes) and advertises every 40 ms instead of 100 ms, so discovery and the connection start sooner.
Every upload ends with a line saying where its time went:
staged in 1.42 s (scan 0.31, connect 0.58, subscribe 0.05, raw repl 0.09, paste 0.21, confirm 0.12, close 0.06)
scan and connect are the operating system’s Bluetooth stack
(scanning for the advertisement, then connecting and discovering the
service); the rest is the hub. --debug adds the packet-level trace.
One upload at a time
openbricks run and openbricks upload push a program through the
same raw-paste channel, and the operating system shares a single BLE
link between every process that connects to the same hub (macOS,
Linux and Windows all multiplex). Two transfers started from two
terminals used to interleave their bytes on the hub’s REPL — a
corrupted program, or both terminals reading each other’s output —
and the hub, which sees one connection, could not tell them apart.
Since 3.10.0 the host refuses at once, before any scan:
$ openbricks run -n RobotA main.py
error: an upload is ongoing (another openbricks run/upload is transferring to 'RobotA'; wait for it to finish)
The guard is a per-hub OS file lock held for the transfer: upload
holds it until its confirmation returns, run releases it the moment
the program is staged and started, so a second run while the first
is only streaming output supersedes it the way a button press would.
A CLI that crashes or is killed mid-transfer leaves nothing behind —
the kernel releases the lock with the process. Other hubs are
unaffected, and so is openbricks stop.
Reference
The reference below is generated from the CLI’s own argument parser, so it always matches the installed version.
Host-side CLI for flashing and running code on openbricks hubs, plus a MuJoCo-backed simulator (openbricks sim …).
usage: openbricks [-h] [--version] COMMAND ...
Positional Arguments
- COMMAND
Possible choices: flash, run, upload, stop, list, log, servo-id, paste-probe, docs, doc, bricks, sim
Named Arguments
- --version
Print the openbricks package version and exit.
Sub-commands
flash
Flash a firmware image onto a hub (via esptool) and write the hub’s BLE advertising name into NVS (via mpremote). –name is mandatory so every hub gets a unique identifier — two hubs with the same name can’t be individually addressed over BLE.
openbricks flash [-h] --name NAME [--port PORT] [--firmware FIRMWARE]
[--chip CHIP] [--baud BAUD] [--with-qtr-init] [--skip-erase]
[--yes] [--verbose]
Named Arguments
- --name
Hub identifier for BLE (required, <=20 chars recommended).
- --port
Serial port (/dev/ttyUSB0, /dev/cu.usbserial-XXXX, COM5 …). Omit to auto-detect — works when exactly ONE ESP device is connected (Espressif native USB or a CP210x/CH340/FTDI bridge).
- --firmware
Path to firmware.bin produced by scripts/build_firmware.sh or downloaded from the Releases page. Omit to download the newest release automatically for the detected chip (cached under ~/.cache/openbricks/firmware).
- --chip
esptool –chip value (esp32, esp32s3, auto). Default: auto.
Default:
'auto'- --baud
esptool flash baud rate. Default: 460800.
Default:
'460800'- --with-qtr-init
After flashing, store a starter QTR line-sensor calibration at /qtr.cal (recorded on the reference bench, default pins 1-10) so line-follow examples work out of the box. Re-run examples/qtr_calibrate.py for a calibration measured on your own mat and lighting.
Default:
False- --skip-erase
Skip erase_flash (faster dev loop; leaves stale NVS keys).
Default:
False- --yes
Skip the confirmation prompt when the target firmware is the same version as (or older than) the current one.
Default:
False- --verbose, -v
Echo every subprocess command line (mpremote/esptool) and cache paths. Default output is step-level only.
Default:
False
run
Connect to the named hub over BLE, push SCRIPT to its REPL (via paste mode), and stream stdout/stderr back to this terminal until the script finishes. Ctrl-C interrupts the remote program. While another openbricks run / upload from this machine is still transferring to the same hub, this one refuses at once with error: an upload is ongoing.
openbricks run [-h] -n NAME [-c CODE] [--scan-timeout SCAN_TIMEOUT] [--debug]
[SCRIPT]
Positional Arguments
- SCRIPT
Path to the local Python script to run on the hub. Mutually exclusive with -c.
Named Arguments
- -n, --name
Hub name baked in at flash time (
openbricks flash --name).- -c, --code
Inline Python code to run on the hub (analogous to
python -c CODE). Useful for quick diagnostics — e.g.openbricks run -n ls -c 'import openbricks; print(openbricks.__version__)'. Mutually exclusive with the SCRIPT positional.- --scan-timeout
How long to scan for the named hub before giving up. Default: 5.0 s.
Default:
5.0- --debug
Print every BLE notify packet (timestamp + hex + ascii) to stderr as it arrives. Use to diagnose ‘timed out reading from hub’ errors — tells you whether the hub is sending anything at all.
Default:
False
upload
Upload SCRIPT to the hub’s filesystem (default path /program.py). The uploaded code does NOT run automatically — the hub’s frozen main.py watches the hub button and exec’s the staged script on each short press. Second short-press stops a running program. (Pybricks calls this same operation download from the hub’s perspective; we name by direction-of-data-travel — bytes flow up to the hub.) While another openbricks run / upload from this machine is still transferring to the same hub, this one refuses at once with error: an upload is ongoing.
openbricks upload [-h] -n NAME [--path PATH] [--scan-timeout SCAN_TIMEOUT]
[--debug]
SCRIPT
Positional Arguments
- SCRIPT
Path to the local Python script to stage.
Named Arguments
- -n, --name
Hub name baked in at flash time.
- --path
Destination path on the hub’s filesystem; the file is staged VERBATIM there (no compilation) for custom boot flows. Default: compile with mpy-cross and stage /program.mpy (which the frozen launcher runs; older firmware gets the source at /program.py).
- --scan-timeout
BLE scan timeout. Default: 5.0 s.
Default:
5.0- --debug
Print every BLE notify packet and the connect timing breakdown to stderr (the per-phase line is printed regardless).
Default:
False
stop
Connect to the named hub over BLE and send a single Ctrl-C, which MicroPython surfaces as KeyboardInterrupt. Use when a long-running openbricks run has already ended and you just want the hub to idle again.
openbricks stop [-h] -n NAME [--scan-timeout SCAN_TIMEOUT]
Named Arguments
- -n, --name
Hub name.
- --scan-timeout
BLE scan timeout. Default: 5.0 s.
Default:
5.0
list
Run a BLE scan and print every device found, sorted by RSSI (strongest first). Unnamed devices are shown with a placeholder so you can still spot a hub whose name wasn’t flashed.
openbricks list [-h] [--timeout TIMEOUT] [--all]
Named Arguments
- --timeout
Scan duration in seconds. Default: 5.0.
Default:
5.0- --all
Show every BLE device, not just those with names. Useful when debugging a hub that came up without a flashed name.
Default:
False
log
Every program executed via the launcher (button press OR openbricks run) gets its stdout / stderr tee’d to a flash file under /openbricks_logs/. Ten rotating slots are kept. With no flags this prints the most recent run; --list shows the index; --run N selects a specific slot. Useful for post-mortem on an untethered run where no live console was attached.
openbricks log [-h] -n NAME [--list] [--run RUN] [--scan-timeout SCAN_TIMEOUT]
Named Arguments
- -n, --name
Hub name baked in at flash time.
- --list
List the available run indices + their on-flash size, instead of dumping a run’s contents.
Default:
False- --run
Specific run index to dump. Defaults to the most recent.
- --scan-timeout
BLE scan timeout. Default: 5.0 s.
Default:
5.0
servo-id
Scans the bus (IDs 0..253), rewrites the servo’s EEPROM ID register, and verifies the result. With several servos attached, –old-id is required so the tool never guesses which one to re-ID. Wire the servo to a USB half-duplex adapter (e.g. the URT-2 board) — this talks directly to the adapter’s serial port, no hub involved.
openbricks servo-id [-h] [-p PORT] [-n NAME] [--tx TX] [--rx RX]
[--scan-timeout SCAN_TIMEOUT] [--scan] [--old-id OLD_ID]
[--baudrate BAUDRATE] [--timeout TIMEOUT]
[new_id]
Positional Arguments
- new_id
Bus ID to assign (0..253). Omit with –scan.
Named Arguments
- -p, --port
Serial port of the USB adapter, e.g. /dev/cu.usbmodem123. Omitted (and no -n): auto-detected when exactly one USB serial device is connected.
- -n, --name
Hub name: run the scan/re-ID THROUGH THE HUB over BLE instead of a USB adapter — the servo stays wired to the robot. Mutually exclusive with -p.
- --tx
Hub path only: servo-bus TX pin (default 14).
Default:
14- --rx
Hub path only: servo-bus RX pin (default 41).
Default:
41- --scan-timeout
Hub path only: BLE scan timeout. Default: 5.0 s.
Default:
5.0- --scan
Just list the IDs that answer on the bus; change nothing.
Default:
False- --old-id
Current ID of the servo to re-ID. Required when more than one servo is attached; otherwise auto-detected.
- --baudrate
Bus baudrate. Default: 1000000 (Feetech factory).
Default:
1000000- --timeout
Per-ping serial read timeout in seconds. Default: 0.02 (a full 254-ID scan takes ~5 s).
Default:
0.02
paste-probe
Pastes padded no-op programs of increasing size through the real raw-paste path and reports the largest that completes, plus how each failure presents (truncated vs hung). Use before changing the firmware’s MICROPY_REPL_STDIN_BUFFER_MAX: two windows may be in flight at once, so that setting is only safe at or below the measured limit.
openbricks paste-probe [-h] -n NAME [--scan-timeout SCAN_TIMEOUT] [--max MAX]
[--timeout TIMEOUT]
Named Arguments
- -n, --name
Hub BLE name.
- --scan-timeout
BLE scan timeout in seconds. Default: 5.
Default:
5.0- --max
Largest size to try, bytes. Default: 8192.
Default:
8192- --timeout
Per-size timeout in seconds. Default: 15.
Default:
15.0
docs (doc)
Opens the full manual in your browser — the same Sphinx build as docs.openbricks.dev, API reference included, bundled and served from disk so no internet is needed. Pass a topic to jump straight to that page.
openbricks docs [-h] [topic]
Positional Arguments
- topic
Page to open (e.g. install, hardware, robotics). Guides and API pages both work. Omit for the index.
bricks
The Assembly Workbench (openbricks sim) ships a curated set of LEGO Technic parts with exact LDraw geometry. bricks fetch downloads the whole LDraw library into a cache; bricks convert turns any part numbers into a bundle file for openbricks sim workbench --bricks.
openbricks bricks [-h] ACTION ...
Positional Arguments
- ACTION
Possible choices: fetch, convert
Sub-commands
fetch
Download and unpack the LDraw parts library (145 MB).
openbricks bricks fetch [-h] [--dest DIR] [--force]
Named Arguments
- --dest
Where to unpack it. Default: $OPENBRICKS_LDRAW_DIR, else ~/.cache/openbricks/ldraw.
- --force
Download again even if the library is already there.
Default:
False
convert
Convert LDraw part numbers into a brick bundle.
openbricks bricks convert [-h] [--out FILE] [--weights FILE] [--ldraw DIR]
NUMBER [NUMBER ...]
Positional Arguments
- NUMBER
LDraw part numbers (the LEGO design id, e.g. 32270).
Named Arguments
- --out
Write the bundle JSON here (default: stdout).
- --weights
JSON of {number: {“g”: grams}} to record as the parts’ masses; unknown parts get a volume estimate.
- --ldraw
The unpacked LDraw library (default: the cache
bricks fetchfills).
sim
Forwards all remaining arguments to the MuJoCo-backed simulator’s CLI. Use openbricks sim --help to see the sim’s own subcommand list.
openbricks sim
openbricks sim … forwards to the simulator’s own parser; bare
openbricks sim launches the sim, the native desktop application:
MuJoCo-backed simulator for openbricks firmware.
usage: openbricks sim [-h] [--version] COMMAND ...
Positional Arguments
- COMMAND
Possible choices: app, workbench, preview, run
Named Arguments
- --version
Print the openbricks package version and exit.
Sub-commands
app
Launches the sim: the Assembly Workbench as a native program with LEGO Technic bricks in exact LDraw geometry, your own STL parts, components, and the robot as the top component. The first run downloads the signed build for this platform into the cache (~/.cache/openbricks/sim); OPENBRICKS_SIM_BIN points at a build of your own.
openbricks sim app [-h] [--bricks FILE] [--bin PATH] [--no-download] [file]
Positional Arguments
- file
A robot.assembly.json to open.
Named Arguments
- --bricks
An extra brick bundle from
openbricks bricks convertto add to the library (repeatable).Default:
[]- --bin
Run this sim binary instead of the cached release build.
- --no-download
Never download: fail if the build is not cached.
Default:
False
workbench
Serves the Assembly Workbench on localhost and opens it: LEGO Technic bricks with exact LDraw geometry, STL imports, components, and the robot as the top component with mass, centre of mass and inertia computed at every level. Pass a robot.assembly.json to open it.
openbricks sim workbench [-h] [--bricks FILE] [--port PORT] [--no-browser]
[file]
Positional Arguments
- file
A robot.assembly.json to open (otherwise the browser’s last draft, else the example).
Named Arguments
- --bricks
An extra brick bundle from
openbricks bricks convertto add to the library (repeatable).Default:
[]- --port
Port to serve on. Default: a free one.
Default:
0- --no-browser
Print the URL instead of opening a browser.
Default:
False
preview
Loads the named world (alias or path), splices the default openbricks-sim chassis in, and hands off to mujoco.viewer for interactive inspection. Pass --headless to step N seconds of physics without opening a window (useful for CI smoke tests).
openbricks sim preview [-h] [--world WORLD] [--chassis FILE] [--x X] [--y Y]
[--yaw YAW] [--headless] [--duration DURATION]
[--seed SEED]
Named Arguments
- --world
World alias or path. Aliases: empty, wro-2026-elementary, wro-2026-junior, wro-2026-senior. Default: empty.
Default:
'empty'- --chassis
JSON file of ChassisSpec fields describing YOUR robot: wheel_radius / axle_length (m), body size, line_sensor_x, color_sensor_x/y (sensor placement, m), pos_x / pos_y / yaw_deg (spawn pose). Fields not given keep the default chassis values.
- --x
Chassis spawn x (m). Default 0, or the chassis file’s pos_x.
- --y
Chassis spawn y (m). Default 0, or the chassis file’s pos_y.
- --yaw
Chassis spawn heading (deg, counter-clockwise from +X). Default 0, or the chassis file’s yaw_deg.
- --headless
Skip the viewer; step
--durationseconds and exit.Default:
False- --duration
Headless step duration in seconds. Default: 2.0.
Default:
2.0- --seed
Randomization seed for per-round prop permutation. Set to a different integer each round to mimic the WRO judge’s pre-round randomization; same seed produces same layout. Has no effect on worlds without a randomization spec.
run
Loads the named world + the default chassis, constructs a SimRobot over them, and execs the script with robot (plus drivebase, left, right aliases) in its globals. The script drives the sim by calling robot.run_for / run_until between actions.
openbricks sim run [-h] [--world WORLD] [--chassis FILE] [--x X] [--y Y]
[--yaw YAW] [--viewer] [--no-shim] [--seed SEED]
[--trace FILE] [--max-sim-s MAX_SIM_S]
script
Positional Arguments
- script
Path to the Python script to execute.
Named Arguments
- --world
World alias or path (same set as preview).
Default:
'empty'- --chassis
JSON file of ChassisSpec fields describing YOUR robot: wheel_radius / axle_length (m), body size, line_sensor_x, color_sensor_x/y (sensor placement, m), pos_x / pos_y / yaw_deg (spawn pose). Fields not given keep the default chassis values.
- --x
Chassis spawn x (m). Default 0, or the chassis file’s pos_x.
- --y
Chassis spawn y (m). Default 0, or the chassis file’s pos_y.
- --yaw
Chassis spawn heading (deg, counter-clockwise from +X). Default 0, or the chassis file’s yaw_deg.
- --viewer
Drop into the MuJoCo viewer after the script returns so you can orbit the final scene.
Default:
False- --no-shim
Skip installing the driver shim. The default behaviour
install``s shims for ``machine+openbricks._nativeand patchestime.sleep_msto advance the sim — disable when your script uses the openbricks_sim API directly.Default:
False- --seed
Randomization seed (same semantics as
preview --seed).- --trace
Write the chassis pose (t_ms, x_mm, y_mm, yaw_deg) every 50 ms of sim time to this CSV — the run’s path, for plotting or asserting on afterwards.
- --max-sim-s
Stop the script once this much SIM time has elapsed (exit status 3). A mission loop that never sees its stop condition otherwise runs forever.