Writing Your Own Controller¶
The robot_driver runtime accepts any class that implements the LegController abstract interface. Drop your controller in, register it, and select it via launch argument.
Anatomy of a controller¶
// robot_driver/include/robot_driver/leg_controller.h
class LegController {
public:
virtual ~LegController() = default;
// Populate `cmd` with per-leg joint torques / PD setpoints for the next tick.
virtual void computeLegCommandArray(
const quad_msgs::msg::RobotState& state,
const quad_msgs::msg::LocalPlan& plan,
quad_msgs::msg::LegCommandArray& cmd) = 0;
};
Existing reference implementations live in robot_driver/src/controllers/ — the simplest is joint_controller.cpp.
Step 1 — New source files¶
Place:
robot_driver/include/robot_driver/controllers/<your>_controller.hrobot_driver/src/controllers/<your>_controller.cpp
Skeleton:
// <your>_controller.h
#pragma once
#include "robot_driver/leg_controller.h"
class YourController : public LegController {
public:
void computeLegCommandArray(
const quad_msgs::msg::RobotState& state,
const quad_msgs::msg::LocalPlan& plan,
quad_msgs::msg::LegCommandArray& cmd) override;
};
// <your>_controller.cpp
#include "robot_driver/controllers/<your>_controller.h"
void YourController::computeLegCommandArray(
const quad_msgs::msg::RobotState& state,
const quad_msgs::msg::LocalPlan& plan,
quad_msgs::msg::LegCommandArray& cmd) {
// ... fill cmd.leg_commands[0..3] with your control law
}
Step 2 — Build wiring¶
Add the source to robot_driver/CMakeLists.txt:
Build:
Step 3 — Tests¶
Create robot_driver/test/test_<your>_controller.cpp. Register in CMakeLists.txt:
ament_add_gtest(test_<your>_controller
test/test_<your>_controller.cpp)
target_link_libraries(test_<your>_controller robot_driver)
Run:
Step 4 — Register in the runtime¶
robot_driver/include/robot_driver/robot_driver.h:
robot_driver/src/robot_driver.cpp (constructor):
} else if (controller_id_ == "<your_controller>") {
leg_controller_ = std::make_shared<YourController>();
}
Step 5 — Run it¶
Pick your controller via launch arg:
ros2 launch quad_utils quad_gazebo.py \
robot_configs:='[{"name":"robot_1","type":"go2","controller":"<your_controller>","init_pose":"-x 0 -y 0 -z 0.5"}]'
Then drive the mode topic as usual:
ros2 topic pub /robot_1/control/mode std_msgs/UInt8 "data: 1" --once # stand
ros2 topic pub /robot_1/control/mode std_msgs/UInt8 "data: 2" --once # walk
Patterns to follow¶
- Don't allocate in the control loop.
computeLegCommandArrayis called at 1 kHz on a real-time-ish thread. Allocate buffers in the constructor. - Trust
state.foot_in_contact— the contact estimator publishes per-leg booleans. Branch on these rather than reinventing contact detection. - Use
QuadKD2for FK/IK. Don't roll your own — joint sign/offset is per-robot config. - Log via
RCLCPP_DEBUG_THROTTLEwhen tuning. PlainRCLCPP_INFOat 1 kHz floods the console.
What about learned policies?¶
For ONNX-based learned controllers, inherit from LearnedController (which itself derives from LegController). See Specialized Controllers.