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.
Double-click a component to edit its definition in place; every use
follows. Open and save robot.assembly.json from the toolbar.
Simulate runs your program on a map with the chassis you
assembled. Pick the map (any built-in world) 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. Pause, Resume and
Stop do what they say; the speed slider runs the physics slower or
faster than wall time; “follow the robot” keeps the camera on the
chassis; the program’s prints and errors appear in the log panel
below the view. 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 a list of actions:
straight (mm), turn (degrees, positive right), curve
(radius and degrees), stop (coast, brake or hold, with an
optional wait) and custom (a line of code, such as
line_follow()). Add one with the + buttons and type its
parameters, or arm → point / ⌒ point and click the map: the
sim works out what reaches the point — the turn to face it and the
distance for a straight line, the arc tangent to the current heading
through it for a curve. Every move has a handle on the map: drag it
to set the distance, the heading or the arc, with the transparent
chassis showing where the robot ends up. Straights and curves have an
end state — coast, brake, hold, or continue for a run that flows
into the next action without slowing (then=Stop.NONE). Rows drag
by their ≡ grip to reorder (or ↑ ↓), × removes one. Custom
actions call whatever the Definitions box defines: type
def line_follow(): … there and the custom row’s picker lists it.
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, 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 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. 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: 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.
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. The inspector lists what each part is mated to.
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 [--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; the whole LDraw library
(every LEGO part ever catalogued, 145 MB to download, about 600 MB
unpacked) is one command away. bricks fetch unpacks it into
~/.cache/openbricks/ldraw (or $OPENBRICKS_LDRAW_DIR), and bricks convert turns any part numbers — the LEGO design ids printed on the
parts, 3648 for the 24-tooth gear — 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.