PAL Policy Deployer — Documentation
Repository: pal_policy_deployer
1. Start
Before you start a policy, check that the Kangaroo Doctor at http://kangaroo-5c:8001 is green.
roslaunch pal_policy_deployer kang_rl_deployer.launch model:=v60 task:=loco lab:=mjlab
If the subscriber_controller does not start, change_controllers times out or motors do not respond, open the Kangaroo Doctor and follow its suggested step before you debug the policy.
This launches two components at the same time:
- Policy node (
pal_policy_inferer) — reads sensors, runs ONNX inference, and outputs joint positions - Subscriber controller — receives target positions and forwards them to the impedance controller
Launch chain
kang_rl_deployer.launch
└─► kang_mjlab_deployer.launch
├─► pal_policy_inferer_mjlab_node (model=v60, task=loco)
└─► kangaroo_subscriber_controller.launch
├─► Loads YAML parameters (gains, estimator, IMU)
└─► Spawns: subscriber_controller
`pal_policy_deployer/launch/kang_rl_deployer.launch`
`pal_policy_deployer/launch/kang_mjlab_deployer.launch`
`subscriber_controller/launch/kangaroo_subscriber_controller.launch`
2. Package Structure
| Package | Role |
|---|---|
pal_policy_deployer |
Launch files, ONNX models (v21–v60), debug script |
pal_policy_inferer |
C++ node — this is where ONNX inference runs |
pal_policy_preprocessor |
Joystick / RViz → /cmd_vel (currently commented out) |
pal_policy_preprocessor ──► pal_policy_deployer
(Joystick / RViz) (Models + launch)
│
▼
pal_policy_inferer
(ONNX inference, 50 Hz)
│
▼
/subscriber_controller/desired_state
3. Data Flow — Overview
Observations Topics
───────────────────── ─────────────
base_ang_vel ┐
base_lin_acc ├── /base_imu
projected_gravity ┘
joint_pos ┐
joint_vel ├── /joint_states
┘
command (vx,vy,ωz) ─── /cmd_vel
actions ─── internal (previous policy output)
──────────────────────────────────────────────────────────
│
▼
kang_mjlab_node (50 Hz, ONNX)
│
│ 415 floats → 16 floats (raw [-1..1])
│ action[i] = scale[i] * output[i] + default_pos[i]
│ → target joint position [rad] per joint (NOT torques!)
▼
/subscriber_controller/desired_state (JointState)
│
▼
subscriber_controller (pass-through, p=i=d=0)
│
▼
pal_local_joint_control (impedance control)
│
│ τ = K×(q_des−q_act) + D×(dq_des−dq_act)
│ joint_handle_.setCommand(τ) ← no topic, shared memory
▼
EtherCAT → motor controller → motor
4. Policy Node — Execution Flow
pal_policy_inferer/src/kang_mjlab_node.cpp
4.1 Load ONNX model (loadPolicyConfig())
At startup, metadata is read directly from the ONNX model:
| Metadata key | Content |
|---|---|
joint_names |
which joints are observed |
default_joint_pos |
zero position (default pose) |
action_scale |
scaling factors for the output |
observation_names |
e.g. base_ang_vel, projected_gravity, joint_pos, joint_vel, actions |
pal_policy_deployer/models/loco/loco_v60_mjlab/*.onnx
4.2 Read sensor data (callbacks)
| Topic | Callback | Observation |
|---|---|---|
/base_imu |
imuCB() |
base_ang_vel, base_lin_acc, projected_gravity |
/joint_states |
jsCB() |
joint_pos (−default), joint_vel |
/cmd_vel |
velCmdCB() |
command (vx, vy, ωz) |
4.3 50 Hz timer — inference (timerCallback())
1. Update history buffer (ring buffer, 5 time steps)
2. Build observation vector: 5 × 83 = 415 floats (term-major)
3. Run ONNX inference: 415 → 16 outputs
4. Scale output: action[i] = scale[i] * output[i] + default_pos[i]
5. Publish to /subscriber_controller/desired_state
5. Impedance Control
5.1 Basic principle
This is not a pure position controller — it simulates a spring + damper for each joint:
ToDo: We need to figure out, if it actually performs impedance control. PAL mentioned that it doesn't use a model...
| Symbol | Name | Meaning |
|---|---|---|
| K | stiffness | spring stiffness — how strongly the joint is pulled toward the target |
| D | damping | damping — suppresses velocity deviations |
| q_desired | target position | provided by the policy (50 Hz) |
| q_actual | actual position | measured encoder value |
Advantage: the joint can yield under external forces such as ground contact, which is essential for walking.
5.2 Two controller types
| Type | Motors | Joints | Transmission |
|---|---|---|---|
DirectImpedanceControl |
1 motor → 1 joint | leg_*_1, leg_*_length |
gearbox + ball screw |
DifferentialImpedanceControl |
2 motors → 2 joints | leg_*_2+3 (hip), leg_*_4+5 (ankle) |
differential gearbox |
For the differential setup: both motors in the same direction → roll, opposite directions → pitch:
Motor A ──┐
├─► Differential ─► Joint Roll
Motor B ──┘ └─► Joint Pitch
kangaroo_controller_configuration/launch/local_joint_control/legs_impedance_control.launch
kangaroo_controller_configuration/launch/local_joint_control/leg_left.launch
pal_local_joint_control/launch/direct_impedance_control.launch
pal_local_joint_control/include/pal_local_joint_control/impedance_base_effort_controller.h
5.3 From torque to motor current
Rotational joint: I = τ / (Kt × i)
Linear joint: I = τ × p / (Kt × i × η × 2π)
| Symbol | YAML parameter | Unit | Meaning |
|---|---|---|---|
| Kt | motor_torque_constant |
Nm/A | torque per ampere |
| i | reduction_ratio |
— | gearbox reduction ratio (5) |
| η | ball_screw_efficiency |
— | ball screw efficiency (0.9) |
| p | ball_screw_lead |
m/rev | ball screw lead (0.01 / 0.005) |
`kangaroo_controller_configuration/config/local_joint_control/actuator_parameters/leg_left_length_joint.yaml`
`kangaroo_controller_configuration/config/local_joint_control/actuator_parameters/leg_left_1_joint.yaml`
5.4 Impedance parameters per joint
stiffness_factor: 1600.0 # K [Nm/rad]
damping_factor: 101.859 # D [Nm·s/rad]
feedforward_prev_cmd: true # use previous command as feedforward → reduces phase lag
integral_factor: 0.0 # not used
kangaroo_controller_configuration/config/local_joint_control/impedance_control/*.yaml
5.5 RL gains vs. default gains
| Joint | Role | Default K | RL K | Default D | RL D |
|---|---|---|---|---|---|
leg_*_length_joint |
leg length (linear actuator) | 11000 | 1600 | 650.0 | 101.859 |
leg_*_1_joint |
hip Z (rotation) | 40 | 100 | 1.0 | 6.366 |
leg_*_2_joint |
hip pitch (differential) | 100 | 100 | 4.0 | 6.366 |
leg_*_3_joint |
hip roll (differential) | 100 | 100 | 4.0 | 6.366 |
leg_*_4_joint |
ankle roll (differential) | 300 | 30 | 5.0 | 1.91 |
leg_*_5_joint |
ankle pitch (differential) | 300 | 30 | 5.0 | 1.91 |
The gains used for RL differ from the default gains
leg_*_length_jointwith K=11000: a 0.1 rad error would result in 1100 Nmleg_*_4/5_jointwith K=300: ankle too stiff → no compliance during ground contact- The RL gains match the virtual spring-damper setup used during MuJoCo training
Workspace: `kangaroo_controller_configuration/config/local_joint_control/impedance_control/*.yaml`
Robot: `/opt/pal/ferrum/share/kangaroo_controller_configuration/config/local_joint_control/impedance_control/*.yaml`
6. Known Sim-to-Real Issues
6.1 IMU frame mismatch
- In MuJoCo training, the IMU is mounted with
quat="0 -0.707107 0.707107 0"(180° rotation) - The real IMU driver publishes raw sensor data without compensating for this mounting rotation
- Result:
projected_gravity z = +1.0instead of-1.0→ the policy believes the robot is upside down
`Stand_alone/robots/mjLab/pal_kangaroo/xmls/kangaroo_accurate.xml` — IMU site (line 67)
`pal_policy_inferer/src/kang_mjlab_node.cpp` — `imuCB()` where the correction is missing
6.2 Incorrect default impedance gains
- The default installation uses gains that are about 7–10× too stiff (see table above)
- Fix: clone
kangaroo_roboton branchgains/rlinto/catkin_ws/src/kangaroo_robot - Simulation: the workspace override is applied automatically after
catkin_make - Real robot: copy the YAML files manually to
/opt/pal/ferrum/share/...