Control Messages
Control messages are how Python users make robots move — commanding motors, servos, wheels, and arm joints. The factory methods and presets handle the domain-specific math (kinematics, PID tuning) so you don't have to.
When you need these: If your robot has actuators — motors, servos, wheels, arm joints — you need control messages. CmdVel for simple differential drive, MotorCommand for direct motor control, JointCommand for multi-DOF arms.
from horus import (
CmdVel, MotorCommand, ServoCommand,
DifferentialDriveCommand, PidConfig,
TrajectoryPoint, JointCommand,
)
Note:
CmdVelis documented in Geometry Messages since it's a velocity type, but it's listed here too because it's the most common control message.
MotorCommand
Individual motor control with mode-specific factory methods. The factories set the right mode, enable flag, and limits — use them instead of setting raw integers.
Constructor
cmd = MotorCommand(motor_id=0, mode=0, target=1.5,
max_velocity=10.0, max_acceleration=5.0, enable=True)
.velocity(motor_id, velocity) — Speed Control
cmd = MotorCommand.velocity(motor_id=0, velocity=1.5) # 1.5 rad/s
Creates a velocity-mode command. The motor controller maintains the target speed using its internal PID loop. Use this for wheels, conveyors, or any continuous rotation.
The velocity parameter is in rad/s for rotary motors or m/s for linear actuators — check your motor controller's documentation.
.position(motor_id, position, max_velocity) — Position Control
cmd = MotorCommand.position(motor_id=0, position=3.14, max_velocity=2.0)
Creates a position-mode command. The motor moves to the target position at up to max_velocity. Use this for precise positioning — homing, tool changes, pan/tilt control.
The motor controller handles the trajectory — it accelerates, cruises, and decelerates to reach the target smoothly.
.stop(motor_id) — Emergency Stop
cmd = MotorCommand.stop(motor_id=0)
Immediately stops the motor (zero target, disable). Use this for safety — collision detected, e-stop pressed, fault condition.
Common mistake: Sending
velocity(id, 0.0)instead ofstop(). A zero velocity command keeps the motor enabled and holding position.stop()actually disables the motor, allowing it to coast to a stop and drawing less power.
.is_valid() — Validation
if not cmd.is_valid():
print("Invalid motor command — check parameters")
ServoCommand
Angle-based servo control with speed limiting and degree-to-radian conversion.
Constructor
cmd = ServoCommand(servo_id=0, position=1.57) # Radians
.from_degrees(servo_id, degrees) — From Angle in Degrees
cmd = ServoCommand.from_degrees(servo_id=0, degrees=90.0)
# Internally converts to 1.5708 radians
Most servo specifications list angle ranges in degrees (0-180°, 0-270°, etc.), but horus uses radians internally. This factory handles the conversion so you can think in degrees.
Common mistake: Using
ServoCommand(servo_id=0, position=90.0)— that's 90 radians (≈ 14 full rotations), which will slam the servo to its limit. Usefrom_degrees()for degree values.
.with_speed(servo_id, position, speed) — Speed-Limited Movement
cmd = ServoCommand.with_speed(servo_id=0, position=1.57, speed=0.5)
Creates a command with a speed limit in rad/s. Without a speed limit, the servo moves as fast as its hardware allows — which can be dangerous for large servos or when carrying a load.
.disable(servo_id) — Power Off
cmd = ServoCommand.disable(servo_id=0)
Powers off the servo motor. The servo becomes free-moving (no holding torque). Important for:
- Reducing power consumption when the servo doesn't need to hold position
- Allowing manual positioning of robot arms
- Safety — a disabled servo can't exert force
.is_valid() — Validation
print(cmd.is_valid())
DifferentialDriveCommand
Wheel-level control for differential drive robots (two-wheeled robots like TurtleBot, iRobot Create, most ground robots). The from_twist() factory is the key method — it converts a velocity command to left/right wheel speeds using inverse kinematics.
Constructor
cmd = DifferentialDriveCommand(left_velocity=1.0, right_velocity=-1.0)
# Equal and opposite = spin in place
.from_twist(linear, angular, wheel_base, wheel_radius) — Inverse Kinematics
cmd = DifferentialDriveCommand.from_twist(
linear=0.5, # m/s forward
angular=0.3, # rad/s turning (positive = left)
wheel_base=0.3, # meters between wheel centers
wheel_radius=0.05, # wheel radius in meters
)
Converts a desired robot velocity (linear + angular) to individual wheel speeds. The math:
v_left = (linear - angular × wheel_base / 2) / wheel_radius
v_right = (linear + angular × wheel_base / 2) / wheel_radius
Parameters you need to measure on your robot:
wheel_base: distance between the centers of the two drive wheels (measure with a ruler)wheel_radius: radius of each drive wheel (measure the diameter and divide by 2)
When angular > 0, the robot turns left (counterclockwise from above). The left wheel slows down and the right wheel speeds up.
Common mistake: Swapping
wheel_baseandwheel_radius. If your robot drives 10x too fast or too slow, you probably swapped them.wheel_baseis always larger (typically 0.1-0.5m) andwheel_radiusis smaller (typically 0.02-0.1m).
.stop() — Stop Both Wheels
cmd = DifferentialDriveCommand.stop()
Zero velocity on both wheels with enable=true. The motors actively hold position (zero speed).
.is_valid() — Validation
print(cmd.is_valid())
Example — Teleop with Kinematics:
from horus import Node, run, CmdVel, DifferentialDriveCommand, Topic
cmd_vel_topic = Topic(CmdVel)
wheel_topic = Topic(DifferentialDriveCommand)
WHEEL_BASE = 0.287 # TurtleBot3 Burger
WHEEL_RADIUS = 0.033 # TurtleBot3 Burger
def convert_to_wheels(node):
cmd = cmd_vel_topic.recv(node)
if cmd is None:
return
wheel_cmd = DifferentialDriveCommand.from_twist(
linear=cmd.linear,
angular=cmd.angular,
wheel_base=WHEEL_BASE,
wheel_radius=WHEEL_RADIUS,
)
wheel_topic.send(wheel_cmd, node)
run(Node(tick=convert_to_wheels, rate=50,
pubs=["wheel_cmd"], subs=["cmd_vel"]))
PidConfig
PID controller gains with preset configurations. The presets are the easiest way to get started — pick the simplest controller that works, then add complexity only if needed.
Constructor
pid = PidConfig(kp=2.0, ki=0.1, kd=0.05)
.proportional(kp) — P Controller
pid = PidConfig.proportional(kp=1.0)
Proportional-only. The simplest controller — output is proportional to error. Fast response, but always has steady-state error (the system never quite reaches the target).
Use when: Speed matters more than precision. Coarse positioning, rough speed control.
.pi(kp, ki) — PI Controller
pid = PidConfig.pi(kp=1.0, ki=0.1)
Proportional + Integral. The integral term eliminates steady-state error — given enough time, the system reaches the exact target. But high ki causes overshoot and oscillation.
Use when: You need zero steady-state error. Temperature control, steady-state speed tracking.
.pd(kp, kd) — PD Controller
pid = PidConfig.pd(kp=1.0, kd=0.05)
Proportional + Derivative. The derivative term damps oscillations and improves transient response. But there's no integral term, so steady-state error persists.
Use when: You need fast, smooth response without overshoot. Joint angle control, quick positioning.
.with_limits(integral_limit, output_limit) — Anti-Windup
pid = PidConfig(kp=2.0, ki=0.5, kd=0.1).with_limits(
integral_limit=10.0, # Clamp integral accumulator
output_limit=100.0, # Clamp output value
)
Sets bounds on the integral accumulator and the total output. Without limits, the integral term can "wind up" during sustained error (e.g., when the motor is stalled) and cause massive overshoot when the error finally clears.
Returns a new PidConfig with the limits applied.
Common mistake: Setting
kiwithoutwith_limits(). The integral will accumulate unboundedly, eventually producing huge output spikes. Always set limits when using integral control.
.is_valid() — Validation
if not pid.is_valid():
print("Invalid PID gains — kp must be >= 0, all values finite")
TrajectoryPoint
A point along a trajectory with position, velocity, and time. Used for smooth motion planning where you specify the desired state at each time step.
.new_2d(x, y, vx, vy, time) — 2D Trajectory Point
tp = TrajectoryPoint.new_2d(x=1.0, y=2.0, vx=0.5, vy=0.0, time=1.0)
Creates a 2D trajectory point with position (x, y), velocity (vx, vy), and time-from-start in seconds. Used for 2D path planning where you want the robot at specific positions at specific times.
.stationary(x, y, z) — Hold Position
hold = TrajectoryPoint.stationary(x=5.0, y=3.0, z=0.0)
Creates a point with zero velocity — the robot should be stationary at this position. Use this for the final point in a trajectory (the "stop here" point).
JointCommand
Multi-joint command for robot arms and legged robots. Add position or velocity commands for individual joints by name.
Constructor
cmd = JointCommand()
.add_position(name, position) — Position a Joint
cmd = JointCommand()
cmd.add_position("shoulder", 1.57) # Move shoulder to 90°
cmd.add_position("elbow", 0.785) # Move elbow to 45°
cmd.add_position("wrist_rotate", 0.0) # Center wrist
Adds a position command for the named joint. Position is in radians for revolute joints, meters for prismatic joints. Raises ValueError if the joint limit (16 joints) is exceeded.
Common mistake: Joint names must match exactly what the arm driver publishes in
JointState. If the driver uses "joint_1" but you send "shoulder", the command is ignored. CheckJointState.namesto see what names your hardware uses.
.add_velocity(name, velocity) — Velocity-Control a Joint
cmd = JointCommand()
cmd.add_velocity("wheel_left", 1.0) # 1 rad/s
cmd.add_velocity("wheel_right", 1.0) # 1 rad/s
Same pattern but for velocity control. Typically used for wheels or continuous-rotation joints.
.is_valid() — Validation
if not cmd.is_valid():
print("Invalid joint command")
Example — Robot Arm Homing:
from horus import JointCommand, Topic
arm_cmd = Topic(JointCommand)
def home_arm(node):
cmd = JointCommand()
cmd.add_position("shoulder", 0.0)
cmd.add_position("elbow", 0.0)
cmd.add_position("wrist_pitch", 0.0)
cmd.add_position("wrist_rotate", 0.0)
cmd.add_position("gripper", 0.0)
arm_cmd.send(cmd, node)
See Also
- CmdVel — 2D velocity command (in Geometry page)
- Geometry Messages — Twist, Pose2D for navigation
- Navigation Messages — NavGoal, path following
- Rust Control Messages — Rust API reference