Overview / Software
The software

One brain, one wire.

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.

ROS 2 · one graph500 Hz twin35 environmentsMIT license
The stack

Commands in, state out.

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_WS · LIVE GRAPH▶ P1: TELEOP   P2: AUTONOMY
teleopVision Pro · iPhone · web
autonomyNav2 · policies · WBC
COMMAND TOPICS /mabel_cmd /arms /hands /body
swerve_nodebase + lift
body_nodetorso · neck
arms_node14-DOF arms
orca_hands2× 17-DOF
head_stereoZED eyes
wrist_camsL + R
rplidar2-D LiDAR
mujoco_simuse_sim:=true
STATE TOPICS /joint_states /odom /images /scan
whole_body_controlIK · compliance
nav2 · slammap · localize
learninglog · train
The controller

Everything passes one gate.

A 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.

Push me — I'm compliant!
Loading MABEL · pick a mode
Toggle mode · drive the command
Control mode
Forward vx0.00 m/s
Strafe vy0.00 m/s
Yaw rate ω0.0 rad/s
Lift height0 cm

Swerve base, holonomic. Caps: 0.8 m/s linear, 2.0 rad/s yaw — the tip-over model trims them as the lift rises.

Commanding hand
+0.00 m+0.00 m
Palm height Z+0 cm

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.

Whole-body QP · 200 Hz q̇* = argmin ‖r(q̇)‖²_W + w_r‖q̇ − K_p(q_rest − q)‖² + w_s‖q̇ − q̇⁻‖² + λ²‖q̇‖²_W s.t. q_lb + ε ≤ q + q̇·Δt ≤ q_ub − ε // one-step joint box |q̇| ≤ q̇_max , |q̇ − q̇⁻| ≤ q̈_max·Δt // rate + accel J_ab q̇ ≥ −κ (d_ab − d_s) ∀(a,b) ∈ C // discrete CBF r(q̇) stacks the task residuals; the last row is a velocity-damper control-barrier condition — it admits any velocity that does not close the pair distance d_ab faster than κ permits, so self-contact is excluded by construction, not by detection. All three families act on the same q̇ the tasks compete over, so an infeasible command concedes task error instead of violating a bound. The reference is rendered at 500 Hz by gravity-compensated impedance control.
PropertyResult
Gravity-comp hold (3 s)< 0.1 rad · sags > 1 rad w/o
Felt residual at ±40% mass error< 0.06 N·m
Contact localization0.5 N · 1.7 cm, correct link
Self-collision, hand→chest−3.5 cm → +4.1 cm standoff
The motion model

It knows when it would tip.

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.

STILL CARRYING18 kglift 0 m16 kglift 0.30 m14 kglift 0.64 mstatic tip payload at the hand, per lift heightSAME SPEED PULSE13.1°envelope OFF0.1°envelope ONpeak body tilt — the envelope trims first
PLANCoMwheelvectors vᵢsupport polygon · 3 swerve modulesELEVATIONCoM rises0.37–0.66 mtip lever grows with the lift — so the trims tighten
Tip-over envelope · ZMP η_k(q, a) = SF_k · h_c(q) · [−n_kᵀ a]₊ / ( g · ℓ_k(q) ) u_s(q, ν_b) = max_k η_k( q, a_ω(ν_b) ) // admissible ⟺ u_s ≤ 1 u_s(q, α ν_b) = α² u_s(q, ν_b) ⟹ ν_b^safe = s · ν_b , s = min(1, u_s^(−1/2)) Per support edge k: h_c is CoM height, ℓ_k the CoM lever arm to that edge, n_k its outward normal, SF_k the safety factor. Because steady acceleration is quadratic in ν_b at fixed configuration, an inadmissible twist is projected straight onto the stability boundary in closed form — no search. Acceleration and steering-rate limits are then enforced on the projected twist, and the same envelope serves teleop, navigation, and policy execution.
Retargeting

Looking around isn't driving.

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.

HEAD poseL WRIST poseR WRIST poseANALYTICno trackers · no MLTORSO framegaze → just looksintent → drives
Body-frame retargeting e_i(T) = ‖ p_d,i − T · p_h,i ‖ // per-anchor residual T̂ = argmin_{T ∈ SE(3)} Σ_i w_i · e_i(T)² // head + both wrists Δψ_b = (1 − χ) Δψ_rig + χ · γ(Δψ_h, ψ_β) · ψ_β χ ∈ [0,1] is the ambiguity between arm sweep and body rotation: isolated head motion drives the neck; coordinated head–wrist rotation drives the base. The torso frame is fit in closed form from the three observed poses — no waist tracker, no mode switch, no learned prior. Measured: 2.3° RMS on commanded torso rotation with 0.0% false base motion over 60 trials.
The idea

Off the vendor tools.

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.

01
Modular — one target per limb

One folder per subsystem (swerve_drive, openarm, orca_hand, head, lift); flash or swap a board without touching the rest.

02
Own the protocol

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).

03
Cross-platform on purpose

GUI and host libraries run on macOS and Ubuntu. Windows isn't tested yet. No vendor binary, no OS lock-in.

04
Always-usable

Bus down or driver unbuilt? The GUI opens in SIM mode with simulated telemetry — design and test with zero hardware.

Swerve drive · the deep cut

Reverse-engineering REV SPARK.

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.

29-bit extended arbitration ID
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
01
Control + register frames

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.

02
Telemetry frames

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.

03
One socket, cached

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.

The fleet

Every actuator, owned.

01

Swerve base · REV SPARK

NEO / NEO550 on 3 delta modules. Reverse-engineered SPARK CAN in Python + Teensy C++.

02

Arms · Damiao DM

OpenArm-derived 7-DOF arms, quasi-direct-drive over CAN (Teensy + FlexCAN_T4), MIT-mode.

03

Body & torso · Damiao

Torso pitch shares the arms' Damiao/CAN driver, status, and quick-control.

04

Hands · Feetech

16× HLS3915 fingers + 1× HLS3930 wrist on a 1 MHz TTL chain; USB adapter auto-detected.

05

Neck · Dynamixel

3-DOF neck (yaw / pitch / roll) via our dynamixel_neck library.

06

Lift · RP2040 Pico

MicroPython, BTS7960 + quad encoder, cascaded position + velocity, host lib + iOS GUI.

Setup Studio

Make the numbers true.

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.

SETUP STUDIO · :8082▶ module by module, in order
ARMbus + zero + limits
BASEsteer offsets
NECKserial, direct bus
LIFTstall-seek travel
HAND · L17 DOF end-stops
HAND · R17 DOF end-stops
WRITES hardware_bridge/calibration_data/mabel.json
calib serverZenoh queryable
mabel/calib/<path>any node, at startup

Every joint gets its own zero and its own stops

Each 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.

Files as truth, served over the wire

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.

Simulation

The twin is the same robot.

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.

Holonomic swerve — translating and spinning at once (studio nav clips)
ORCA hands — 17 tendon DOF each, per-finger retargeting
Lift — 0.635 m of travel, tip-over envelope tightening as it rises
Wave Hello — a Control Studio animation clip played on the twin
Twelve runs, one controller

Every clip below is the production stack driving MuJoCo headless — the same safe_base_step gate, the same whole-body IK. Nothing is keyframed.

Long-horizon navHomeA* through the apartment, body facing forward the whole way
Long-horizon navOfficeWeaving desks — the base translates, the head never turns away
Long-horizon navSupermarketAisle run across 4 m of floor, obstacles either side
Pick and placeWork tableApproach, close, lift, carry — 8.6 cm off the table
Pick and placeKitchenSame controller, different clutter — 10.9 cm lift
Pick and placeLiving roomMug off the coffee table — 19.2 cm lift
Reach up highSupermarketLift column to the top shelf, envelope tightening as it rises
Reach up highWarehouse0.635 m of travel puts the hands above the rack
Reach up highHomeTop cupboard — torso and lift solved together
Reach the floorLiving roomColumn down, torso forward, hands to the rug
Reach the floorBedroomSame descent beside the bed
Reach the floorGymDown to the mat without the base ever tipping
1 · THE CANONICAL MODEL mabel_full.xml MuJoCo MJCF — one source of truth URDF + TF meshes + GLB balance params 67 q · 59 actuators 9 cameras · 370 sensors 2 · ONE COMMAND CONTRACT — EITHER PLANT VISION PRO IPHONE WEB POLICY NAV2 RobotCommand base · body · hands controller retarget · WBC · impedance · safe_base_step MuJoCo plant the twin — 500 Hz OR hardware bridge the real robot ONE LAUNCH FLAG joint state · TF · RGB-D · LiDAR · diagnostics — back to every client 35 scenes · 19 static · 16 with moving actors