RL Deployment FAQ
Tip
If the robot itself misbehaves (motors in error, controllers do not start, change_controllers times out, e-stop), open the Kangaroo Doctor at http://kangaroo-5c:8001 first. This FAQ covers policy and container problems.
Docker → Real Robot: Setting ROS_IP
If ROS commands from the Docker container hang (e.g. Waiting for Action Server ...),
ROS_IP is missing. Without it the robot does not know how to communicate back to the container.
export ROS_IP=$(hostname -I | awk '{print $1}')
echo $ROS_IP # should be 10.68.0.209
Afterwards all ROS commands from the container work normally.
Complete setup for Docker → robot:
export ROS_MASTER_URI=http://kangaroo-5c:11311
export ROS_IP=$(hostname -I | awk '{print $1}')
source /catkin_ws/devel/setup.bash
rosrun pal_policy_deployer debug_obs.py _model:=v56 _task=loco
/catkin_ws/src/kangaroo_robot/kangaroo_controller_configuration/config/local_joint_control/impedance_control/
On the real robot you need to overwrite the 10 files (5 joints × left+right) in /opt/pal/ferrum/share/...
Changes for 7DOF + Gripper Simulation
Problem
The subscriber_controller failed with:
gains_param.size() = 26 != actual_js_state_.name.size() = 34
file does not exist [.../impedance_control/arm_left_5_joint.yaml]
The configs were designed for 4-DOF arms only; the real robot has 7-DOF + gripper (34 joints).
Fix 1: gains YAML extended
File: /catkin_ws/src/subscriber_controller/config/kangaroo_subscriber_controller.yaml
8 missing joints added:
- arm_left_5_joint, arm_left_6_joint, arm_left_7_joint
- arm_right_5_joint, arm_right_6_joint, arm_right_7_joint
- gripper_left_finger_joint, gripper_right_finger_joint
All with p: 0.0, i: 0.0, d: 0.0, i_clamp: 0.0
Fix 2: Impedance Control YAMLs created
Directory: /catkin_ws/src/kangaroo_robot/kangaroo_controller_configuration/config/local_joint_control/impedance_control/
6 new files created (same values as arm_*_4_joint.yaml):
- arm_left_5_joint.yaml
- arm_left_6_joint.yaml
- arm_left_7_joint.yaml
- arm_right_5_joint.yaml
- arm_right_6_joint.yaml
- arm_right_7_joint.yaml
Content of each file:
stiffness_factor: 26.177
damping_factor: 1.666
feedforward_prev_cmd: true
integral_factor: 0.0
Gripper joints do not need an impedance YAML — they use no_control.launch.
Starting simulation (7DOF + Gripper)
roslaunch kangaroo_pal_physics_simulator kangaroo_pal_physics_simulator.launch \
with_arms:=true arm_type:=7dof with_pelvis:=true end_effector_type:=gripper
Starting subscriber controller
source /catkin_ws/devel/setup.bash
roslaunch subscriber_controller kangaroo_subscriber_controller.launch simulation:=true
Important: the workspace must be sourced so that
rospack find subscriber_controllerfinds the catkin_ws version (not/opt/pal/ferrum).
On the real robot
The same 6 impedance YAML files must also be copied to /opt/pal/ferrum/share/kangaroo_controller_configuration/config/local_joint_control/impedance_control/.
roslaunch pal_policy_deployer kang_rl_deployer.launch model:=v56 task:=loco lab:=mjlab
Why the Policy Works for Both 4-DOF and 7-DOF Arms
Principle: name-based matching
The policy communicates exclusively via joint names, not via indices or the number of joints.
The node reads joint_names from the ONNX metadata at startup:
config.joint_names = splitString(metadata_map["joint_names"], ',');
In the JointState callback only these names are looked up:
if (msg->name[i] == config.joint_names[k]) {
term->current[k] = msg->position[i] - config.default_joint_pos[k];
}
The action command also contains only the policy joints:
cmd_msg.name = config.action_joint_names; // only what the policy knows
What happens to the "unknown" joints
| Joints | Trained with 4-DOF policy | Trained with 7-DOF policy |
|---|---|---|
| arm_*_1–4 | Policy controls them | Policy controls them |
| arm_*_5–7 | Policy ignores them → fallback | Policy controls them |
| gripper_* | Policy ignores them → fallback | Policy ignores them → fallback |
Fallback = subscriber_controller holds the joints at their init position:
τ = K*(q_init - q) - D*q̇
This means a policy trained only with 4-DOF arms simply "leaves" joints 5/6/7 hanging — they are held at initial position by the PD controller.
Preparing the Real Robot (7DOF + Gripper)
The real robot loads configs from /opt/pal/ferrum/share/ — not from catkin_ws.
The following files must be copied manually:
1. Impedance Control YAMLs (6 new files)
DEST=/opt/pal/ferrum/share/kangaroo_controller_configuration/config/local_joint_control/impedance_control
SRC=/catkin_ws/src/kangaroo_robot/kangaroo_controller_configuration/config/local_joint_control/impedance_control
cp $SRC/arm_left_5_joint.yaml $DEST/
cp $SRC/arm_left_6_joint.yaml $DEST/
cp $SRC/arm_left_7_joint.yaml $DEST/
cp $SRC/arm_right_5_joint.yaml $DEST/
cp $SRC/arm_right_6_joint.yaml $DEST/
cp $SRC/arm_right_7_joint.yaml $DEST/
Content of each file (same as arm_*_4_joint):
stiffness_factor: 26.177
damping_factor: 1.666
feedforward_prev_cmd: true
integral_factor: 0.0
2. Subscriber Controller gains YAML (34 joints)
cp /catkin_ws/src/subscriber_controller/config/kangaroo_subscriber_controller.yaml \
/opt/pal/ferrum/share/subscriber_controller/config/kangaroo_subscriber_controller.yaml
This file was extended from 26 to 34 joints (arm_5/6/7 + gripper added).
Why these two files?
| File | Problem without fix |
|---|---|
impedance_control/arm_*_5/6/7_joint.yaml |
Launch aborts: file does not exist |
kangaroo_subscriber_controller.yaml |
Controller fails to load: gains_param.size() = 26 != 34 |
Deploy on real robot
# Source workspace so catkin_ws packages take priority
source /catkin_ws/devel/setup.bash
# Start policy (starts subscriber_controller automatically)
roslaunch pal_policy_deployer kang_rl_deployer.launch model:=v56 task:=loco lab:=mjlab
Note: When roslaunch is started from inside the Docker container (with
ROS_MASTER_URI=http://kangaroo-5c:11311) and catkin_ws is sourced, YAMLs are automatically loaded from catkin_ws — no manual copying to the robot needed.
Policy Model: What Must Be in the Folder?
Minimum content
models/loco/loco_v60_mjlab/
└── 2026-04-01_07-08-05.onnx ← only required file
The node automatically searches for the first .onnx file in the directory:
pkg_root/models/<task>/<task>_<version>_mjlab/
What is inside the ONNX model?
All relevant parameters are embedded as metadata in the model:
| Metadata key | Content |
|---|---|
joint_names |
List of all joints the policy knows |
default_joint_pos |
Zero position per joint (= action_offset) |
action_scale |
Scaling factor per action dimension |
observation_names |
Order of observation terms |
joint_stiffness |
K values (optional, informational) |
joint_damping |
D values (optional, informational) |
Observation normalization and action scaling
Raw sensor → ONNX model → Raw output → action_scale * output + default_joint_pos → q_des
↑
Normalization internal (mean/std embedded at training export)
Observations (node sends raw, no preprocessing):
| Term | Content | Dim |
|---|---|---|
joint_pos |
q - default_joint_pos |
n_joints |
joint_vel |
q̇ measured |
n_joints |
base_ang_vel |
IMU angular velocity | 3 |
projected_gravity |
gravity vector in body frame (from IMU) | 3 |
command |
vx, vy, wz from /cmd_vel |
3 |
actions |
last policy output (raw, without scaling) | n_actions |
Normalization: obs_normalization: true in training → mean/std embedded in ONNX, the node does nothing.
Action scaling in node:
action[i] = config.action_scale[i] * net_output[i] + config.action_offset[i];
// └── default_joint_pos
Clipping: clip_actions: null → no clipping.
Safety: subscriber_controller Without Active Policy
What happens at startup without a policy?
- subscriber_controller stores
q_init(current position at startup) - Sends
q_des = q_initto the motor driver - Motor driver holds position via DirectImpedanceControl:
τ = stiffness_factor * (q_init - q) - damping_factor * q̇ - The gains in
kangaroo_subscriber_controller.yamlare all 0 → no extra PD
feedforward_prev_cmd — motor driver parameter (unverified)
On the real robot all joints are configured with feedforward_prev_cmd: true:
{damping_factor: 6.366, feedforward_prev_cmd: true, integral_factor: 0.0, stiffness_factor: 100.0}
Note:
feedforward_prev_cmdis not present in the ROS-sideImpedanceControlParamsstruct (verified indirect_impedance_control.h). The parameter is passed to the motor driver firmware — what it does with it cannot be verified from source code (closed-source firmware).
The name suggests that the motor driver adds the previous torque command as feedforward (τ_t = K*(q_des-q) - D*q̇ + τ_(t-1)), but this is an assumption based on the parameter name.