Locomotion Contract
12-joint quadruped robot running on the Limit platform (Jetson Thor, Ubuntu 24.04, ROS 2 Jazzy). The robot carries its own locomotion controller, CHAMP, which plans the gait and calculates the 12 joint positions 200 times per second (when the locomotion stack is running). It picks up the velocity command over DDS (/cmd_vel, geometry_msgs/Twist), sends the motor commands out on the CAN buses as GIM6010-8 motor frames, and writes the live joint angles back as /joint_states and /foot_contacts. The server's sim stack runs its own CHAMP (Gazebo) under the sim_* topic convention, so live and sim topics never collide. Operators drive the robot with W/S + Q/E + A/D keyboard controls in Limit Dash. Emergency stop lives on the robot itself.
Live vs Sim
There are two CHAMP instances, and the topic convention is what keeps them apart on a shared ROS domain: one on the robot (live), one on the server (sim). Teleop only ever drives the live one; the server's Nav2 only drives the sim one, via the sim_* topics.
| Aspect | Live | Sim |
|---|---|---|
| Runs on | robot, the live driving data (Jetson Thor, ROS 2 Jazzy) | server, the sim (Gazebo) |
| CHAMP | the live instance | the sim instance |
| Driven by | Limit Dash teleop only (live mode, /cmd_vel) | the server's Nav2, via the sim_* topics (sim mode in Limit Dash) |
| Motors | the real 12× GIM6010-8 on CAN0/CAN1 | the Gazebo leg joints |
| Topics | bare names: /joint_states, /foot_contacts, /locomotion/* | sim_* convention: /sim_joint_states, /sim_foot_contacts, … |
Teleop only ever drives the live CHAMP on the robot, which drives the real motors and owns the bare topic names. The sim CHAMP on the server is driven by the server's Nav2 (sim mode in Limit Dash, with Gazebo running) and stays on the sim_* topics, so the two can run at the same time without talking past each other.
ROS Topic Contract
CHAMP input (subscribes)
| Topic | Type | QoS | Semantics |
|---|---|---|---|
| /cmd_vel | geometry_msgs/Twist | RMW default (reliable, volatile, depth 10) | Velocity command, Limit Dash teleop (W/S/Q/E/A/D) or the Nav2 stack (Twist, stamp stripped by the adapter). A silent command is a stop signal (watchdog, see the Safety Contract). |
| /body_pose | geometry_msgs/msg/Pose | RMW default | Estimated body/base pose (from state_estimation_node / EKF chain). Optional to consume; never required for safety. |
| /safety/e_stop | std_srvs/srv/Trigger | n/a | Mandatory. Request → immediate all-axis stop. |
| /safety/reset | std_srvs/srv/Trigger | n/a | Mandatory. Clear stop/fault state, re-enter closed loop. |
CHAMP output (publishes)
| Topic | Type | Rate | Semantics |
|---|---|---|---|
| /joint_states | sensor_msgs/msg/JointState | ≥ 50 Hz | 12 joint names in Node-ID Table order. Position in radians, velocity in rad/s, current in amps (Iq). Used by robot_state_publisher and EKF. |
| /foot_contacts | champ_msgs/msg/ContactsStamped | ≥ 50 Hz | Four booleans in order: lf (left front), rf (right front), lr (left rear), rr (right rear). The timestamp must always advance, even when all feet are off the ground. |
| /locomotion/status | std_msgs/msg/String (JSON) | 1 Hz + on-change | Health summary: per-motor {node_id, ok, last_heartbeat_age_ms, iq_a, fault_code}, watchdog state, e-stop state. |
| /locomotion/can | std_msgs/msg/String (hex) | on traffic | Verbose CAN trace for bring-up; default OFF. |
Inbound stack behavior
- Nav2 is configured so that every motion command goes through
/cmd_vel(controller →/cmd_vel_raw→ smoother →/cmd_vel; Spin/BackUp publish directly to/cmd_vel). velocity_smootherenforcesFollowPath.max_linear_vel=1.0 m/s, angular accel 3.0 rad/s²; Spin capped at 1.5 rad/s.- When Nav2 is idle,
/cmd_velstops. CHAMP must treat a silent velocity command as a stop signal (watchdog), not a hold-in-place signal. - Odometry (
/odom) and the odom→base_link transform come from the EKF on the robot (live) or state_estimation + EKF on the server (sim). CHAMP must not publish/odomor the odom→base transform.
GIM6010-8 CAN bus
The 12 motors are driven over two CAN buses, can0 (front legs) and can1 (rear legs). The low-level protocol (GIM6010-8 frame and command table), the 12-motor node-ID mapping, the motor datasheet, and hardware bring-up are on the Canbus page.
Safety Contract
Authority & limits
| Param | Default | Notes |
|---|---|---|
| Max body speed | 0.5 m/s (Nav2 already caps at 1.0) | CHAMP hard-caps; ignore cmd above cap with warning |
| Max yaw rate | 1.5 rad/s | matches Spin cap |
| Per-joint velocity limit | 10 rad/s via Set_Limits (0x00F) | set at startup |
| Per-joint current limit | 15 A via Set_Limits + monitor Get_Iq | trip: |Iq| > 15 A for > 200 ms → stop that axis |
| Bus voltage floor | warn < 24 V, stop < 20 V | via Get_Bus_Voltage_Current |
Velocity-command watchdog
- Timeout: 500 ms without a fresh velocity command on
/cmd_vel→ all 12 axes:Stop(0x002) + clear inputs, publish zero-velocity/joint_states, set statusWATCHDOG_STOP. - Recovery: after a watchdog stop, the first new velocity command requires a fresh closed-loop re-init (set mode + axis_state=8), or an explicit
/safety/reset. CHAMP must not slowly resume from a watchdog stop. - Validity: a velocity command with non-finite values counts as timeout and logs.
E-stop
/safety/e_stop(Trigger) → immediately (same tick):Stop(0x002) to all axes, disable closed loop, publish zero/joint_states+ statusE_STOP.- Physical e-stop (wired): the e-stop button cuts CAN power or sets a GPIO that CHAMP monitors (via a CANopen watchdog or MCU relay).
/safety/resetclears E_STOP/WATCHDOG/fault states and re-runs startup sequence.
Motor-level fault handling
| Condition | Detection | Action |
|---|---|---|
| Heartbeat lost | no 0x001 for > 3 × 100 ms | stop that axis, flag HEARTBEAT_LOST, count fault |
| Motor error flag | 0x001 Axis_Error bit or 0x003 | Clear_Errors (0x018) once, then stop axis if persists |
| Overcurrent | Get_Iq | see Authority & limits |
| Overheat | (no temp channel on GIM6010-8) | monitor electrical power via 0x01D; trip if > 100 W for > 5 s |
| Bus voltage | 0x017 | see Authority & limits |
| 3+ axes in fault | any | global e-stop state FAULT_LOCKED; only /safety/reset releases |
Startup / shutdown sequence
STARTUP: [per motor] Set_Limits(0x00F) → Set_Controller_Mode(0x00B: 2/2) → Set_Axis_State(0x007: 8)
→ verify via Heartbeat (axis_state bit) → mark READY
SHUTDOWN: [per motor] Set_Input_* (0.0) → Stop (0x002)
E-STOP: [per motor] Stop (0x002)
CAN capture
All frames (TX+RX) appended to <log_dir>/can_capture_<UTC>.log, one line per frame: <monotonic_ns> <TX|RX> <can_id_hex:04X> <d0> ... <d7>. Default ON; parameter can_log=false disables.
Foot-Contact Inference
Foot contact state is synthesized from the gait model: stance phase = true, swing phase = false. The CHAMP controller can also publish /foot_contacts directly.
CHAMP Architecture
CHAMP is the locomotion controller. The live instance runs on the robot (Jetson Thor, ROS 2 Jazzy): it subscribes to the velocity command over DDS, runs the 200 Hz gait + IK loop, and drives the 12 GIM6010-8 motors directly on the CAN buses. The sim instance runs in Gazebo on the server, under the sim_* topic convention. Do not re-implement gait planning or inverse kinematics: CHAMP owns both; the CAN layer only turns its 12-joint position targets into GIM6010-8 frames.
Layer breakdown
| Layer | File | Role |
|---|---|---|
| Math core (IK) | champ/include/champ/kinematics/kinematics.h | inverse(12 joints, 4 foot_positions): 2-link IK per leg |
| Body pose → feet | champ/include/champ/body_controller/body_controller.h | poseCommand(): translates/rotates zero_stance by body pose |
| Gait / planning | champ/include/champ/leg_controller/leg_controller.h | velocityCommand(): Raibert heuristic: step = (stance_duration/2) × velocity |
| Gait timing | champ/include/champ/leg_controller/phase_generator.h | Trot: Left Front/Right Rear offset 0.0; Right Front/Left Rear offset 0.5; stride 0.5 s |
| Trajectory planner | champ/include/champ/leg_controller/trajectory_planner.h | Swing = cosine arc; Stance = foot held |
| ROS 2 wrapper | champ_base/src/quadruped_controller.cpp | 200 Hz loop → drive the 12 joint position targets out on CAN |
| Gait config | limitx_limit_sim/config/gait/gait.yaml | See the Gait params section below |
Signal flow (verified from quadruped_controller.cpp)
/cmd_vel (Twist: Limit Dash teleop, or Nav2 path via the adapter)
→ CHAMP controlLoop_() @ 200 Hz (on the robot):
body_controller_.poseCommand() # body pose → 4 foot target positions
leg_controller_.velocityCommand() # Raibert: foot step = (stance/2)*vel
kinematics_.inverse() # 2-link IK → 12 joint position targets
→ CAN driver: Set_Input_Pos frames → 12× GIM6010-8 (1 Mbps)
→ publish /joint_states (encoder feedback), /foot_contacts, /locomotion/status
Source-verified details:
quadruped_controller.cpp:49→double loop_rate = 200.0;(200 Hz control loop)- Subscribes:
cmd_vel/smooth(Twist: an adapter republishes/cmd_vel→/cmd_vel/smooth),body_pose(Pose) - Publishes (enabled by params):
joint_group_position_controller/command(JointTrajectory: the 12 joint position targets the CAN layer drives),foot_contacts, andjoint_statesin sim; the CAN frames themselves go on the wire, not DDS controlLoop_():poseCommand()→velocityCommand()→inverse()→ publish foot contacts + joints
12 joint names (verified from joints.yaml)
lf_hip_joint lf_upper_leg_joint lf_lower_leg_joint
rf_hip_joint rf_upper_leg_joint rf_lower_leg_joint
lh_hip_joint lh_upper_leg_joint lh_lower_leg_joint
rh_hip_joint rh_upper_leg_joint rh_lower_leg_joint
The CAN layer (bridge node) MUST map these names → node IDs (see the 12-Motor Node-ID Table). CHAMP publishes them in this order on joint_group_position_controller/command.
Gait params (gait.yaml)
max_x = 0.3m/s,max_y = 0.25m/s,max_wz = 0.5rad/s (caps)- Swing height 0.04 m; stance duration 0.25 s; stride 0.5 s (trot)
- Body height 0.225 m
- Gait: trot (Left Front/Right Rear offset 0; Right Front/Left Rear offset 0.5)
- NOTE: caps are 0.3 m/s max, see the Speed Cap section.
Launch wiring
- Sim (
limit_limit_launch.py):publish_joint_control: Truewithjoint_controller_topic: "sim_joint_group_effort_controller/joint_trajectory"(Gazebo ros2_control); acmd_veladapter republishes/cmd_vel→/cmd_vel/smoothfor CHAMP. - Production (robot): CHAMP uses its default
joint_group_position_controller/command(JointTrajectory); thelocomotion_harnessbridge node (backend=real, bridge=true) consumes it and drives the 12 GIM6010-8 over SocketCAN. In production the bridge is the sole/joint_statespublisher (encoder feedback), so CHAMP runs withpublish_joint_states: false, never two publishers on the topic.
Operator Teleop (Limit Dash)
The only change to Limit Dash's keyboard handler: it also publishes a geometry_msgs/Twist message to /cmd_vel, W/S for forward/backward, Q/E for left/right, A/D for turn. This is what sends the command to the robot.
End-to-end flow
Key map
| Key | Twist field | While held (≈20 Hz) | On release |
|---|---|---|---|
| W | linear.x + | +0.2 m/s | → 0 |
| S | linear.x − | −0.2 m/s | → 0 |
| Q | linear.y + | +0.25 m/s | → 0 |
| E | linear.y − | −0.25 m/s | → 0 |
| A | angular.z + | +0.4 rad/s | → 0 |
| D | angular.z − | −0.4 rad/s | → 0 |
- While any drive key is held, publish the active velocity at ~20 Hz. When all keys are released, publish one zero-velocity message (so the robot doesn't coast).
- Speed caps enforced in
ros_bridge.py: |vx| ≤ 0.3 m/s, |vy| ≤ 0.25 m/s, |ωz| ≤ 0.5 rad/s (matchesgait.yaml). - Publisher QoS (quality of service): reliable delivery, not persisted, keep last 10 messages.
- The 3D preview animation stays as-is (pressing W still animates the dog in the viewer); publishing is additive.
- Safe default: 0 m/s when no key is held. Also publish a zero-velocity message when the browser window loses focus or the tab is switched away.
Speed Cap
- CHAMP gait caps (
gait.yaml): max forward speed 0.3 m/s, max lateral speed 0.25 m/s, max turn rate 0.5 rad/s. - The velocity smoother allows up to 1.0 m/s / 1.5 rad/s, but the gait caps are the binding limit for CHAMP-driven motion.
- Mechanical limit (see the Datasheet): GIM6010-8 max 120 rpm; sustained body speeds near 1.0 m/s may exceed comfortable lower-leg joint speeds and torque margin.
- The effective speed cap is the lower of the gait caps and the smoother limits. The teleop key-map caps match
gait.yaml.