Differential Drive (Python)
Converts CmdVel velocity commands into left/right wheel speeds with RPM clamping, dead-reckoning odometry, and safe shutdown.
Problem
You need to drive a 2-wheel robot from CmdVel commands, with safety limits and position tracking.
When To Use
- Any 2-wheel differential drive robot (TurtleBot, AGV, hobby bots)
- Converting
CmdVelfrom a planner or teleoperation to motor outputs - When you need velocity clamping and safe shutdown
Prerequisites
- HORUS installed (Installation Guide)
- Familiarity with CmdVel
Kinematics
v_left = v - ω * L / 2
v_right = v + ω * L / 2
Where v is linear velocity (m/s), ω is angular velocity (rad/s), and L is the wheel base (m).
horus.toml
[package]
name = "diff-drive-py"
version = "0.1.0"
language = "python"
Complete Code
import horus
from horus import Node, CmdVel, run, us
import math
# ── Robot parameters ──────────────────────────────────
WHEEL_BASE = 0.3 # meters between wheels
WHEEL_RADIUS = 0.05 # meters
MAX_RPM = 200.0 # motor safety limit
# ── State ─────────────────────────────────────────────
x, y, theta = [0.0], [0.0], [0.0]
# ── Hardware stub ─────────────────────────────────────
def write_motors(left_rpm, right_rpm):
"""Send to motor controller — replace with serial/CAN driver."""
pass
# ── Node callbacks ────────────────────────────────────
def drive_tick(node):
cmd = node.recv("cmd_vel")
if cmd is None:
return
v = cmd.linear # m/s forward
w = cmd.angular # rad/s counter-clockwise
# IMPORTANT: differential drive kinematics
v_left = v - w * WHEEL_BASE / 2.0
v_right = v + w * WHEEL_BASE / 2.0
# Convert m/s to RPM
left_rpm = v_left / WHEEL_RADIUS * 60.0 / (2.0 * math.pi)
right_rpm = v_right / WHEEL_RADIUS * 60.0 / (2.0 * math.pi)
# SAFETY: clamp to motor limits
left_rpm = max(-MAX_RPM, min(MAX_RPM, left_rpm))
right_rpm = max(-MAX_RPM, min(MAX_RPM, right_rpm))
write_motors(left_rpm, right_rpm)
# Dead-reckoning odometry
dt = 1.0 / 50.0 # 50 Hz
theta[0] += w * dt
x[0] += v * dt * math.cos(theta[0])
y[0] += v * dt * math.sin(theta[0])
node.send("odom", {
"x": x[0], "y": y[0], "theta": theta[0],
"v_linear": v, "v_angular": w,
"left_rpm": left_rpm, "right_rpm": right_rpm,
})
def drive_shutdown(node):
# SAFETY: stop both motors before exit
write_motors(0.0, 0.0)
node.log_info(f"Shutdown at ({x[0]:.2f}, {y[0]:.2f})")
# ── Main ──────────────────────────────────────────────
drive = Node(
name="DiffDrive",
tick=drive_tick,
shutdown=drive_shutdown,
rate=50,
order=10,
budget=500 * us,
on_miss="safe_mode",
subs=[CmdVel],
pubs=["odom"],
)
run(drive, tick_rate=100, rt=True)
Expected Output
[HORUS] Scheduler running — tick_rate: 100 Hz
[HORUS] Node "DiffDrive" started (Rt, 50 Hz, budget: 500μs)
^C
[HORUS] Shutting down...
[HORUS] DiffDrive: Shutdown at (1.23, 0.45)
[HORUS] Node "DiffDrive" shutdown complete
Key Points
- RPM clamping prevents motor damage from bad upstream commands
shutdown()zeros both motors — critical for robots under gravity or in motionon_miss="safe_mode"stops motors if the tick budget is exceeded- Dead-reckoning drifts — in production, fuse with IMU or wheel encoders
write_motors()is a stub — replace withpyserial,python-can, or GPIO. See Real Hardware for complete examples.
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
| Robot spins in circles | WHEEL_BASE wrong (too small) | Measure wheel center-to-center distance |
| Motors saturate at low speed | WHEEL_RADIUS too small → RPM too high | Measure actual wheel radius |
| Odometry drifts badly | Pure dead-reckoning, no correction | Fuse with IMU (Multi-Sensor Fusion) |
| Robot does not stop on Ctrl+C | Missing shutdown callback | Always implement shutdown for actuator nodes |
See Also
- Differential Drive (Rust) — Rust version with full kinematics diagram
- CmdVel — Velocity command reference
- Real Hardware — Complete serial motor examples
- PID Controller (Python) — Closed-loop speed control