Simulator
The [sim] extra ships a MuJoCo-backed physics simulator, so you can
develop robot programs without a hub on the desk:
$ pipx install 'openbricks[sim]'
$ openbricks sim run examples/full_robot.py --viewer
The sim runs the same script you’d push to the hub — a driver shim
maps the openbricks API onto simulated motors and sensors, so
ST3032Motor, DriveBase, color sensors, and distance sensors behave
like their hardware counterparts.
Commands
$ openbricks sim [robot.assembly.json] [--bricks more.json] [--bin PATH] [--no-download]
Launches the sim, the native desktop application: the
Assembly Workbench with LEGO Technic bricks in exact geometry, your own
STL parts, and the robot as the top component. The first run downloads
the signed build for your platform (about 15 MB) from the release that
matches the installed version into ~/.cache/openbricks/sim; it is
checked against the same project key that signs firmware images before
it runs. OPENBRICKS_SIM_BIN points at a build of your own
(cargo build --release in tools/sim). openbricks sim app is the
explicit form.
$ openbricks sim workbench [robot.assembly.json] [--bricks more.json] [--port N] [--no-browser]
The same workbench as a page in your browser, for a machine without the native build.
$ openbricks sim preview [--world WORLD] [--x X] [--y Y] [--headless] [--duration S] [--seed N]
Loads the named world (an alias or a path to an MJCF file), splices in
the default chassis, and opens the MuJoCo viewer so you can inspect the
scene. --headless steps the physics for --duration seconds without
opening a window — useful as a smoke test.
$ openbricks sim run SCRIPT [--world WORLD] [--chassis FILE] [--x X] [--y Y] [--yaw DEG] [--viewer] [--no-shim] [--seed N]
Loads the world plus the chassis and executes SCRIPT against the
simulated robot. --viewer opens the interactive MuJoCo window;
without it the sim runs headless (CI-friendly). --seed makes
randomized worlds reproducible.
Run openbricks sim --help for the full, always-current option list.
The sim
openbricks sim opens a window with two tabs. Workbench is the
editor described in the next section: the library on the left (your
components, the LEGO Technic set, other bricks, each with a rendered
thumbnail), the 3D view in the
middle, the contents of the component you are editing and the
inspector on the right. Drag in the view to orbit, right-drag to pan,
scroll to zoom, F to fit; drag a brick to move it on the ground
plane in 8 mm steps (shift lifts it). The selection carries handles:
three arrows (Move, W) to slide it along one world axis in grid
steps, or three rings (Rotate, E) to turn it about one axis in
15° steps — hold shift for free movement or rotation; several
selected items move and turn together about the first one’s origin.
R turns the selection 90°, S snaps it into the nearest hole,
arrows nudge, Delete removes, ⌘Z undoes. ⌘C copies the selection
and ⌘V pastes it: back into the same component two modules over,
into another component as it was, or into another window (the
clipboard carries every part and component definition it needs).
⌘L locks the selection so nothing moves, turns or removes it until
⌘⇧L unlocks it; locked bricks draw faded and show 🔒 in the
contents list. Import STL… in the library brings in a part from a
mesh file: choose the file’s units, where its origin goes (as in the
file, the bounding-box centre or the bottom centre), and a weighed
mass or a density; the part gets its exact volume, centre of mass and
inertia from the closed mesh (an open mesh gets a box’s inertia and
needs a mass), plus any 4.8 mm pin holes found on it, and lands in
the library and the view. A component can start from nothing: type
a name beside the library’s Components and press New (or
Enter), and an empty component of that name joins the library and
opens for building; add bricks to it, then + add it to the robot
or any other component.
Double-click a component to edit its definition in place; every use
follows. Open and save robot.assembly.json from the toolbar.
Import… brings a saved build’s components and bricks into this
library, to add from: what is here already, the same, is left, and a
clash comes in under a new id. A component’s save in the library
(or Save as build… on its page) writes it as a build of its own —
a file whose robot is that component, with the bricks and components
it needs — to open, import or put on a map elsewhere.
Unsaved work is kept: a couple of seconds after a change settles, the
build and the route are written as drafts under the data directory
(~/.local/share/openbricks/drafts, or $OPENBRICKS_DATA_DIR), and
the next openbricks sim started without a file brings them back —
the status line says so, and Save writes the build to the file it came
from. Saving drops the draft. (The map editor’s props are saved by
Save map and are not drafted.)
Simulate runs your program on a map with the chassis you
assembled. Pick the map (a shipped world, or one you saved from the
Map tab) and it appears in the view at once, as does the chassis (a
robot.assembly.json, or the build open in the Workbench tab once it
is saved) when you choose it; pick the program (main.py), then Run.
The view is a plan: the whole map seen straight from above, north up,
with no perspective, fitted edge to edge and never panned or zoomed
(the Workbench keeps its own 3D camera). Pause, Resume and Stop do
what they say; the speed slider runs the physics slower or faster
than wall time;
the program’s prints and errors appear in the log panel below the
view, and while the map is not there yet the view says what the run
server is doing. The loaded chassis stands on the map at true scale:
drag it to put it where a run should start — a 70 % transparent copy
follows the pointer with its axle centre under it, and letting go
places the robot there (shift turns it) — or type the pose in the
Route panel. That panel plans a route as actions placed on the
map. Click a tool — → Straight, ⌒ Curve, ↻ Turn,
■ Stop or ƒ Custom — then click the map: a straight takes
its start and its end (the start snaps to where the previous action
ends, or to the chassis, so paths chain; an end clicked within 7.5°
of the way the robot arrives lands exactly on that heading, so
chained straights run on with no turn between them — shift keeps any
angle); a curve takes its start,
its end, and a point to face at the end — it enters the way the robot
arrives at its start and is one arc when the end pose allows it, else
two arcs meeting smoothly, which the program drives as one continuous
move; a turn takes where it turns and a point to face; a stop or a
custom call takes one point. Every click leaves a marker; after the first click the line (or
the arc, or the turn’s arrow) follows the pointer — after a curve’s
second click, its end swings to face the pointer — with its length,
radius and angle, or heading written beside it, and once placed each
action keeps that label next to its path, and an arrowhead at the end
of every path shows the way the robot faces there (a turn’s arrow
shows its own). Paths are drawn three pixels wide at any zoom, over
everything on the map (markers sit between the map and the paths), in
the kind’s colour — blue straights, green curves, orange turns, stops
and calls — or a colour of your own, picked in the popup or the panel
(“default” goes back to the kind’s). A popup then asks for the parameters: the speed (the drive
base’s default, shown in mm/s for your wheels), continuous for a
move that flows into the next one without slowing (then=Stop.NONE)
or otherwise the end state (coast, brake, hold) — a curve asks for
nothing more: the popup shows the headings it enters and ends with
and the radius of each arc, all set by the clicks — a
turn’s heading and rate, a
stop’s wait, a custom action’s call (the picker lists what the
Definitions box defines, such as def line_follow(): …) and
whether it moves the robot, in which case one more click says where it
ends. Where an action does not start where the previous one ends, a
dashed line shows the drive the program inserts to get there; the
first action placed on an empty route starts it, so the chassis goes
to where that action begins, facing its way (drag the chassis
afterwards for another start). Every
action is an object on the map: click its path to select it and edit
its parameters in the panel, drag its handles (a straight’s ends; a
curve’s start, which carries its end along, its end, and the arrow at
its end for the heading it ends facing; a turn’s heading arrow) or
the path itself to move it, ⌘C / ⌘V to copy and paste it (the copy
lands a little to the side), ⌘L / ⌘⇧L to lock and unlock it (a locked
action shows 🔒 and cannot be moved, edited or deleted), Delete to
remove it, ⌘Z to undo, Esc to cancel a placement. The numbers on the
map are the order the program runs them in; rows drag by their ≡
grip (or ↑ ↓) to reorder.
Markers are named points you add to a map — a corner of the mat, a
mission object, a line junction: click ◉ Marker, click the map,
name it in the popup. They show as flags with their names, drag to
move, rename or remove in the panel, and route clicks snap to them,
so a path can start or end exactly on a marker. They are kept with
the map on your machine (under ~/.local/share/openbricks/markers/,
or $OPENBRICKS_DATA_DIR), not in the route file, and come back
whenever that map loads.
Routes save and load as *.route.json (the map, the start pose, the
actions and the definitions); ▶ Run route writes the route as a
hub-style program (ST3032Motor wheels and a DriveBase sized from
the chassis; edit the motor lines for other wiring) and runs it from
the route’s start — the chassis is put there first, so a second run
repeats the route — and
“show the program” prints it. Under the hood the sim starts the
MuJoCo runtime as a child process — the same runtime, driver shim and
C cores
openbricks sim run uses — and draws the run from the poses it
streams, with every brick of the chassis in its exact geometry and
the map’s mesh props (the WRO senior mosaic frame) as MuJoCo has
them. The
chassis is built from the assembly: the wheel, caster and sensor
roles place the physics skeleton, the brick-by-brick mass properties
become the body’s inertia, and each brick rides along as a visual
geom.
The map editor
Map edits the map itself in a 3D view of its own — drag to orbit,
shift-drag (or right-drag) to pan, scroll or pinch to zoom, Fit or
F to frame the map, Iso / Top / Side / Front as on the Workbench.
Once a map is framed the view is yours: moving, adding, removing or
sticking a prop rebuilds the map on the run server, but the camera
stays where you put it, and so it does when you save the map under a
name of your own; only loading another map, or Fit, frames it
again. The toolbar on this tab names the map shown — the assembly’s
Open / Save / Save as… buttons and its component path belong to the
Workbench tab. The props on the map are the LEGO-built objects a
mission puts on the mat (each a <lego_prop> in the world’s MJCF) and
whatever you add: drag a prop to move it (its outline lights under the
pointer, the selected one carries its name), shift-drag to turn it by
hand, R or Turn 90° for a quarter turn, or set the heading
field outright (degrees counter-clockwise from the map’s x axis);
click one, or its row in the panel, to select it; ⌘D or
Duplicate puts another like it a little to the side, Del or
Remove takes it away, Add… lists the kinds of prop the map has
and puts one of that kind at the map’s origin.
Adding what you built. The panel lists the components of a build
from the Workbench — the build open there, or any saved build:
Open a build… takes an .assembly.json, and the builds opened
are listed under the Workbench’s; click one to list its components.
The whole build comes first, then each component that holds bricks,
each with a + button that puts it on the map (the whole build is
named after its file). Or add one brick
from the library (search it by number or name, then + to map).
Either lands at the origin as a prop of its own — an <assembly_prop> whose model
is an openbricks-assembly/1 document, kept under the data
directory until the map is saved — drawn with the exact bricks and
colliding as their boxes with their catalogue masses.
Free or stuck. A prop is free by default: it has a free joint, so the physics settles it and the robot can push it. Tick stuck to the map on the selected prop to weld it there — no joint, nothing but the editor moves it; a stuck prop wears a pin through its centre, and its row says so. Every move, add, remove, stick and unstick is sent to the run server, which moves the live body at once (a chassis place, which resets the physics, keeps the prop where it was put) and rewrites the prop’s placeholder in the world text it holds, so the physics, the picture and the text agree; adding, removing, sticking and unsticking rebuild the world with the chassis where it stands. Nothing moves while a program runs.
Save as a new map writes the world text as it stands — every prop
where it is, the ones added included, with the map’s artwork and the
props’ models (documents added since the load copied into the map’s
props/) — to worlds/<name>/ under the data directory
($OPENBRICKS_DATA_DIR, else $XDG_DATA_HOME/openbricks, else
~/.local/share/openbricks, the same place the markers live). The
run server lists your maps beside the shipped ones (marked “yours”),
the tab switches to the new map, and its markers come along; saving
again under the same name replaces it, and a shipped map’s name is
refused so it is never shadowed. Routes remember the map they were
planned on by that name.
The Assembly Workbench
The sim and openbricks sim workbench (the same editor as a page in
the browser) read and write one file. The editor is a 3D view of the
robot as a tree of components:
Bricks are recorded once, with their geometry, mass and provenance (
measured,datasheet,vendororplaceholder). The library that ships in the wheel holds a curated set of popular LEGO Technic parts converted from the LDraw parts library (CC BY 2.0 / 4.0): beams in every common length, bent and L beams, frames, Technic bricks and plates, pins, axles, bushes, connectors, gears, a few rims and tyres, and fairing panels, with BrickLink catalogue weights where known — and every part of the two WRO sets, 45811 (the Brick Set: the mission bricks, tiles, hoses and balls) and 45819 (the Expansion Set). Type a set number, or “WRO”, into the library’s search to see a set’s bricks, each with how many the set holds. Type a part number the library lacks — the LEGO design id printed on the part,2458— and the library says so and offers Fetch 2458 from LDraw: the part’s own file and the few subparts and primitives it references come from ldraw.org (a few dozen kilobytes, not the whole 145 MB library; the first fetch also brings Rebrickable’s colour tables, 1.1 MB, once), are converted like the shipped parts, get the colours Rebrickable lists for the part (or a note that it lists none), and the part joins the library at once and for every later launch, kept under~/.local/share/openbricks/bricks(or$OPENBRICKS_DATA_DIR; on Windows~is%USERPROFILE%). One fetch runs at a time, and Cancel stops it. A build that uses a fetched part carries its record along — mesh, mass properties and connectors, the stud sockets derived on arrival — so it opens, and stacks, on a machine whose library never fetched it. Fetching needs the sim to have been started byopenbricks sim(it runs the package’s Python);openbricks bricks fetch 2458 3005does the same from a terminal, and a fetched part is in the MuJoCo runtime’s library too, so a fetched wheel drives. Servos, boards and wheels are recorded as boxes, cylinders and spheres, and any part you have as a mesh comes in through Import a part from an STL file (binary or ASCII; mm, cm, inch or m; a weighed mass or a density such as PLA 1.24 g/cm³).Components are lists of bricks and other components, each placed by a position and a roll / pitch / yaw. Drag bricks from the library into the view, move and rotate them with the gizmos, select what you built and Group it — or, in the sim’s library, name a component and press New to start it empty and build it from bricks. Either way the new component joins the library and can be dropped anywhere, as many times as you like. Double-click an instance to edit its definition in place; every use follows. Every brick is placed in a LEGO colour: the library lists, for each part, the colours it really comes in (from Rebrickable’s tables), a row’s colour combo picks the one the next + places it in, and a placed brick’s page (or a selection’s, for the colours they all share) changes it. A part in a given colour is its own LEGO element with the number set inventories print, and the page names it; typing an element number into the library’s search finds the part and picks that colour. “—” leaves a brick in its category’s colour. The file records the colour as its LDraw id (
coloron the instance), and the Map and Simulate tabs draw builds in their colours too.Connections. Pins, axles and studs are real features of the LDraw parts, and 4.8 mm bores are recognised as pin holes on every mesh, imported STL files included. Let go of a part near a hole and it snaps: the pin axis aligns to the hole, a pin half centres in its module, an axle keeps its position along the hole. Bricks and plates stack: under every stud a part has a socket, so a plate let go over a brick lands on its stud grid, squared up, with every stud it covers mated — the turn and shift that mate the most features win, which also seats a two-pin connector in both holes. Dragging a part near a hole or a grid pulls it on (it lets go again as the pointer moves away); a stud also seats in a Technic hole or, for the half-stud offset, in a tube. The inspector lists what each part is mated to. So that a stack stays legible, the sim draws every brick with its edges — the seams between stacked bricks, the rims of studs and holes — in a darker shade of its own colour (lighter on a black brick); edges in the toolbar turns them off.
No two bricks overlap. A brick let go, nudged, turned or given a pose where its material would pass through a neighbour’s is put back where it was, and the status line names both — while it is dragged it flushes red wherever it would overlap, and Escape abandons the drag. Touching is not overlapping, and two parts with a feature seated in the other’s (a pin all the way in its hole, a plate down on its studs, an axle anywhere along a hole it runs through) are a joint whatever LDraw’s geometry does at the join. A new brick lands beside what is already at the origin; a duplicate or a paste goes as far along −y as it needs to be clear. A file that already holds overlaps opens and says how many, and stays editable: an old overlap may be moved out of, not further in.
Mass properties are never typed in above the brick level. Volume, centre of mass and the inertia tensor of every LDraw and STL part come from its closed mesh, so a recorded weight becomes a full inertia tensor; components and the robot roll their children up with the parallel-axis theorem. Weight divided by exact volume is shown as a density on every part, which catches a wrong weight or a wrong part at a glance (ABS is about 1.05 g/cm³).
Roles name the parts the simulator binds: the two drive wheels, the caster, the reflectance arrays, the colour sensor, the range sensor and the IMU. From them the page derives the flat
ChassisSpecfields (what the simulator receives) with the axle midpoint as the origin, so a build can be run today withopenbricks sim run --chassis.
The file the editor reads and writes, robot.assembly.json, stores
recorded facts only: bricks, poses, roles, spawn pose. Everything
computed is recomputed on load. Open one with openbricks sim robot.assembly.json (or openbricks sim workbench robot.assembly.json in the browser, which also keeps your last draft
between visits).
The brick library
$ openbricks bricks fetch NUMBER [NUMBER ...] [--dest DIR] [--force] [--no-colors]
$ openbricks bricks fetch [--dest DIR] [--force]
$ openbricks bricks convert NUMBER [NUMBER ...] [--out FILE] [--weights FILE] [--ldraw DIR]
$ openbricks sim workbench --bricks FILE
The wheel ships the curated Technic set and every part of the WRO
sets 45811 and 45819 (the library says which sets hold a brick, and
how many). Any other part is a number away: bricks fetch 2458
gets the part’s files from ldraw.org one by one (the same
~/.cache/openbricks/ldraw layout the whole library unpacks into, so
a full library is used as it is and a sparse one grows), converts it,
adds the colours Rebrickable lists for it, and keeps it under the
sim’s data directory, where openbricks sim finds it on every launch
— what the sim’s own library does from its search box. A number
ldraw.org has not, official or on the tracker, is said so; a file the
part needs that ldraw.org has not is an error naming the part and the
file, never a part with a hole in it; every pause ldraw.org asks for
(sixty requests a minute; a part of two hundred files takes a few) is
waited out, a connection that stalls for thirty seconds is an error,
and a page served in place of a file is refused. A number the library
ships is refused — the shipped record, weighed and in its sets, wins
over a fetched copy, and one left under the data directory by an
earlier fetch is ignored and noted at launch. --force fetches a
part’s files (its own and its subparts, not the primitives) and
Rebrickable’s tables again; --dest is the LDraw cache for both
forms. The whole
LDraw library (every LEGO part ever catalogued, 145 MB to download,
about 600 MB unpacked) is bricks fetch alone, and bricks convert
turns any part numbers — the LEGO design ids printed on the parts,
3648 for the 24-tooth gear — from it into a bundle file that
openbricks sim --bricks (and openbricks sim workbench --bricks)
adds to the library. Converted parts without a weight carry a volume
estimate at 1.05 g/cm³ and are flagged until you weigh them; pass
--weights with a JSON of {"3648": {"g": 1.62}} to record real
ones.
LEGO® and Technic are trademarks of the LEGO Group, which does not sponsor or endorse openbricks. The geometry is the LDraw community’s work; the bundle carries its attribution.
Describing your robot
The default chassis is a 60 mm-wheel, 150 mm-axle box with every
down-facing sensor 60 mm ahead of the axle. A real robot differs, and
those differences decide whether a mission script’s numbers work:
--chassis FILE loads a JSON object of ChassisSpec fields (metres,
kilograms, degrees) that describe the robot the script was written
for. Fields not given keep the defaults.
{
"wheel_radius": 0.0432, "axle_length": 0.135,
"body_length": 0.16, "body_width": 0.12,
"line_sensor_x": 0.06,
"line_sensor_2_x": -0.03, "line_sensor_2_y": 0.0,
"color_sensor_x": 0.06, "color_sensor_y": 0.184,
"pos_x": -0.547, "pos_y": -0.15, "yaw_deg": 90
}
wheel_radius/axle_lengthsize the chassis at load time. TheDriveBase(wheel_diameter_mm=…, axle_track_mm=…)in the script resizes it again at adoption, so the script’s geometry always wins — set them here so apreviewshows the same robot.line_sensor_xplaces the first reflectance-array site (chassis_line) ahead of the axle;line_sensor_2_x/_yplace the second (chassis_line2, default 30 mm behind the axle on the centre line, the same height). Reflectance arrays bind these sites in construction order within one run: the firstQTRArray/QTRLineSensor/QTRChannelthe script constructs readschassis_line, the second readschassis_line2, and a third raisesRuntimeError(two sites is the chassis’s limit). The counter resets when the shim is installed for a run, so everysim runstarts with both sites free.color_sensor_x/_y/_zplace the centre colour camera (chassis_cam_down, the no-muxTCS34725) in the chassis frame (the floor is at-(wheel_radius + 0.005));color_sensor_yaw/_pitchaim it (default straight down; a sensor on the robot’s left flank reading bricks beside the line isyaw 90, pitch 0at brick height);color_sensor_fovis the cone it integrates (degrees, 0 = one ray) andcolor_sensor_rangehow far it sees. The left/right down pair rides 18 mm either side of (color_sensor_x,color_sensor_y).pos_x/pos_y/yaw_degare the spawn pose;--x/--y/--yawon the command line override them one at a time.yaw_degis counter-clockwise from +X seen from above (0 = facing +X).
What the shim simulates
Firmware class |
Sim binding |
|---|---|
|
The first two servo ids become the chassis wheels, the third and fourth kinematic task shafts (a gripper motor that turns but pushes nothing). A |
|
The firmware engine over an emulated |
|
Ground-truth chassis heading; the ICM’s bias estimator reports calibrated at once. |
|
The firmware driver class over a synthesised raw read: the centre camera (no mux) or the left/right pair (mux channels 1 / 0) casts along its own axis — optionally a cone, with a range — and the first geom hit (a mat texel, a LEGO brick’s material) gives the reflectance; |
|
The firmware driver over a reflectance model: one element per array position ( |
Distance sensors |
A forward ray from the |
Nothing above has a load: task motors don’t grip, and a prop is only pushed when the chassis body drives into it.
Notes
The sim needs the
[sim]extra (mujoco,numpy). Without it,openbricks sim …prints an install hint instead of crashing.The wheel carries the firmware package (
openbricks.drivers.*,openbricks.parameters, …) since 3.6.0, so a plainpipx install 'openbricks[sim]'runs hub-style scripts; earlier releases needed a repo checkout for that.Firmware-only users never need the simulator — it’s strictly host-side tooling.