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 (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); 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. 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, 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, vendor or placeholder). 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 ChassisSpec fields (what the simulator receives) with the axle midpoint as the origin, so a build can be run today with openbricks 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_length size the chassis at load time. The DriveBase(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 a preview shows the same robot.

  • line_sensor_x places the first reflectance-array site (chassis_line) ahead of the axle; line_sensor_2_x / _y place 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 first QTRArray / QTRLineSensor / QTRChannel the script constructs reads chassis_line, the second reads chassis_line2, and a third raises RuntimeError (two sites is the chassis’s limit). The counter resets when the shim is installed for a run, so every sim run starts with both sites free. color_sensor_x / _y / _z place the centre colour camera (chassis_cam_down, the no-mux TCS34725) in the chassis frame (the floor is at -(wheel_radius + 0.005)); color_sensor_yaw / _pitch aim it (default straight down; a sensor on the robot’s left flank reading bricks beside the line is yaw 90, pitch 0 at brick height); color_sensor_fov is the cone it integrates (degrees, 0 = one ray) and color_sensor_range how 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_deg are the spawn pose; --x / --y / --yaw on the command line override them one at a time. yaw_deg is counter-clockwise from +X seen from above (0 = facing +X).

What the shim simulates

Firmware class

Sim binding

ST3032Motor / ST3215Motor

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 DriveBase always gets the physical wheels for the pair it adopts, whatever order the script constructed its motors in, and re-constructing a motor for a servo id yields the same motor — both firmware rules.

DriveBase

The firmware engine over an emulated st_bus; use_gyro(True) reads the chassis’s true yaw.

ICM45686 / BNO055

Ground-truth chassis heading; the ICM’s bias estimator reports calibrated at once.

TCS34725

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; rgb() / ambient() are the driver’s channel-over-clear arithmetic, so white reads about (85, 85, 85) and a blue brick has the largest b, as on the robot.

QTRLineSensor / QTRArray / QTRChannel

The firmware driver over a reflectance model: one element per array position (QTRLineSensor(channels=8) gives the eight-channel front layout, exactly as on the hub), spread left-to-right from the site the array bound at construction — the first array a run constructs reads chassis_line, the second chassis_line2, a third raises RuntimeError — each element averaging the floor over a 3 mm spot so an edge reads as a gradient, which is what makes 50 - reading[i].ambient() proportional. load_calibration("/qtr_front.cal") and calibrate() need no file — the sim’s reflectance is born normalised.

Distance sensors

A forward ray from the chassis_dist site.

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 plain pipx 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.