Axol opens one SocketCAN bus per arm on entry, enables all 16 motors, and calibrates the gripper open-stop. Sim is a drop-in replacement that renders the robot in a browser using viser (requires the sim extra).
Axol
left_channel=None or right_channel=None to operate a single arm. Both arms are brought up concurrently on __aenter__.
Lifecycle methods
State reads
Each returns(left_array, right_array) where the absent arm is None.
State writes
Individual arms are accessible via
axol.left and axol.right (AxolArm), which expose the same methods operating on a single arm.
Sim
Sim implements the same interface as Axol. Use it to visualise motion without hardware. Requires the sim extra.
http://localhost:8002 in a browser to view the robot.
Sim renders whatever you command with motion_control. Running axol teleop --sim instead wraps a Sim in the full teleop stack — with no headset connected the arms just hold the rest pose. To drive them without a headset (e.g. in a test), stream VRFrame messages to the teleop VR WebSocket at wss://localhost:8000/ws (self-signed cert — disable TLS verification) with both l_lock and r_lock set true to engage tracking.Configuration — AxolConfig, ArmConfig, JointConfig
Each arm joint is configured with a single JointConfig carrying its impedance gains, friction-comp model, and the inertial of the body it drives:
dataclasses.replace (start from the AxolConfig defaults so you keep the per-side friction values that get injected at construction):
JointConfig fields
Gravity feedforward is computed centrally from the URDF — see Gravity compensation — and uses the per-joint
mass and com directly.
ArmConfig.gripper is a PositionForceConfig with torque_limit (Nm) and max_speed (rad/s); the gripper’s mass is already lumped into wrist_3.mass (the gripper joint is fixed).
AxolConfig also exposes top-level parameters:
ArmConfig defaults for gains and masses; the right arm gets CoMs mirrored across X via ArmConfig.mirror_to_right(). Per-motor friction values are identified separately for each arm (left/right motors measurably differ) — see _LEFT_FRICTION / _RIGHT_FRICTION in almond_axol/robot/config.py. Pass an explicit left= / right= to override either side.
Gravity compensation
almond_axol.robot.gravity.GravityCompensator builds a MuJoCo model from the bundled URDF and computes per-joint gravity torques as qfrc_bias with qvel=0 (Coriolis terms vanish). Because the URDF is the full kinematic chain, each parent joint’s gravity load includes the contribution of every child link — this is the main improvement over the previous per-joint ga·cos(q) + gb·sin(q) model, which silently ignored child-link mass.
Per-link masses are not taken from the bundled URDF — the Onshape exporter leaves placeholder sub-gram values that produce essentially zero gravity. Real per-link mass and CoM live on each JointConfig.mass / JointConfig.com in almond_axol/robot/config.py (CoMs come from the CAD inertial origins; masses are tuned in place against measured joint torques and are typically lower than the CAD values, since Onshape often over-assigns aluminum-class densities to parts that are hollow / 3D-printed).
If the arms sag or push back in gravity-comp mode, tune the relevant joint’s mass and com on AxolConfig and pass it to Axol:
gravity-comp CLI command to hold the arms in gravity-compensation mode interactively.