Every command source — a headset, a phone, a browser, a learned policy — speaks one wire protocol and passes one whole-body controller. The MuJoCo twin runs the identical stack at 500 Hz. Nothing here is a demo copy.
Teleop and autonomy publish commands; driver nodes turn them into torque and
publish state back. Boot the twin and one mujoco_sim node stands in for every
driver — consumers never notice.
/mabel_cmd /arms /hands /body/joint_states /odom /images /scanA compliant whole-body controller renders every reference — a teleoperator, a collaborator's push, or a learned policy — through the same quadratic program, with self-collision avoidance built in. Try it: drive the modes below.
Swerve base, holonomic. Caps: 0.8 m/s linear, 2.0 rad/s yaw — the tip-over model trims them as the lift rises.
Pick a hand, then move the joystick — its green palm tracking point follows and that arm tracks it by IK. Both palms are tracked bimanually; base and lift are held.
Same green palm targets, but now the swerve base follows the hand centroid through the comfort box, and the lift & torso are jointly engaged — lazier (50× / 2× weighted) — to keep both hands in reach.
| Property | Result |
|---|---|
| Gravity-comp hold (3 s) | < 0.1 rad · sags > 1 rad w/o |
| Felt residual at ±40% mass error | < 0.06 N·m |
| Contact localization | 0.5 N · 1.7 cm, correct link |
| Self-collision, hand→chest | −3.5 cm → +4.1 cm standoff |
A lift-aware tip-over envelope trims velocity before the physics does: a speed pulse that tilts the bare platform 13.1° passes at 0.1° with the envelope on — and the robot still carries 18 / 16 / 14 kg at lift 0 / 0.30 / 0.64 m.
The operator's unobserved torso frame is recovered analytically from head and wrist poses alone — so glancing sideways never steers the base. No waist trackers, no mode switches, no learned priors.
Every actuator company hands you a proprietary GUI — REV's Hardware Client, the Feetech debugger, Damiao's assistant, Dynamixel Wizard — usually Windows-only, each its own silo. We pulled the parts we need into our own code so the whole robot speaks one language.
One folder per subsystem (swerve_drive, openarm, orca_hand,
head, lift); flash or swap a board without touching the rest.
Where a vendor hides setup in a closed app, we re-implement it natively — most of all the REV SPARK CAN stack, reverse-engineered end-to-end (below).
GUI and host libraries run on macOS and Ubuntu. Windows isn't tested yet. No vendor binary, no OS lock-in.
Bus down or driver unbuilt? The GUI opens in SIM mode with simulated telemetry — design and test with zero hardware.
The base runs REV SPARK controllers on a 3-module delta swerve. REV only exposes them
through their Hardware Client and vendor libraries — so we read the sparkcan source,
documented the wire protocol, and re-implemented it raw in Python (SocketCAN) and C++ (Teensy
FlexCAN_T4). Zero dependency on REV's stack.
arbId = (0x02 << 24) // DEVICE_TYPE = motor controller | (0x05 << 16) // MANUFACTURER = REV Robotics | (apiClass << 10) | (apiIndex << 6) | deviceId // 1,3,5 = drive · 2,4,6 = steer
Velocity / Position / DutyCycle share one payload — an IEEE-754 LE float in bytes 0–3. Registers
live under API class 48 with a type tag, so we set kCtrlType, kIdleMode,
PID kP_0, and the steering zero kDutyCycleZeroOffset directly.
Periodic status: Period 0 (duty / volts / current / temp), Period 2 (encoder vel +
pos), Period 5 (absolute steer encoder) — the last only streams once you flip
kForceEnableStatus5.
A single RX thread reads the bus non-blocking, masks the device ID (can_id & 0x3F),
and caches each motor's latest value behind a mutex — GetVelocity() is a free read.
Full mapping in rev_can_protocol.md.
NEO / NEO550 on 3 delta modules. Reverse-engineered SPARK CAN in Python + Teensy C++.
OpenArm-derived 7-DOF arms, quasi-direct-drive over CAN (Teensy + FlexCAN_T4), MIT-mode.
Torso pitch shares the arms' Damiao/CAN driver, status, and quick-control.
16× HLS3915 fingers + 1× HLS3930 wrist on a 1 MHz TTL chain; USB adapter auto-detected.
3-DOF neck (yaw / pitch / roll) via our dynamixel_neck library.
MicroPython, BTS7960 + quad encoder, cascaded position + velocity, host lib + iOS GUI.
Bring-up runs in a browser: a per-module status · control · calibration studio that walks the robot module by module — Arm, Base, Neck, Lift, Hand L, Hand R — and writes what it measures into one file the whole robot reads.
hardware_bridge/calibration_data/mabel.jsonEach module opens on a Control tab (enable torque, jog the joint)
and a Calibration tab. The lift is the clearest example: ⟳ Auto-calibrate
— an armed two-click confirm, because the column moves — fires the Pico's own
floor→ceiling stall-seek, and the studio records the measured travel window
(min_pos / max_pos / span) under the
lift/pico slice. The height slider only appears once that window exists, so you
can't drive a column whose limits nobody has measured.
Nothing is hard-coded in a node. Calibration lands in a
human-readable JSON file — diffable, reviewable, shareable — and a thin
service answers Zenoh queries against it, so a node asks
mabel/calib/arm/openarm-dual at startup and gets either one value or the whole
slice. Three families share the tree: calibration (written here),
network identity (human-set), and nominal physical (seeded
from the MuJoCo model) — the as-deployed counterpart to the as-designed model.
Don't skip the CoM check after any mass change:
wbc_stability.py --only 4. If E4 shows >1 cm bias, update the tip-over
constants — that measurement is what keeps MABEL from tipping.
One canonical model generates the URDF, the MJCF, and every derived artifact.
The production controller drives MuJoCo unmodified at 500 Hz across 35 released environments —
use_sim:=true is the only difference.
Every clip below is the production stack driving MuJoCo headless — the same
safe_base_step gate, the same whole-body IK. Nothing is keyframed.