Junhyeok Lee

Applies to v0.1.0.

Vehicle control · ROS 2

/cmd_vel carries a geometry_msgs/Twist in physical units: linear.x in m/s and angular.z as a yaw rate in rad/s. The yaw rate is the part that catches people.

Speed is metres per second

The bridge divides by moveSpeed and the vehicle multiplies it straight back, so the two cancel: linear.x arrives as m/s, up to the ceiling. Past that the clamp takes over silently; see the speed ceiling.

Steering is a rate, not an angle

The simulator recovers the steering angle with the bicycle model:

delta = atan(-omega * L / v)          L = wheelbase, v = linear.x

v is in the denominator, so a fixed angular.zgives an angle that shrinks as speed rises. The same 2.0 rad/s is 33.5° at 0.5 m/s but only 2.7° at 7 m/s. A client that hard-codes a yaw rate loses almost all its steering authority the moment it speeds up, and the symptom looks like “steering barely works”.

Command an angle and convert with the current speed, and ±1.0 is full lock at every speed:

omega = speed * math.tan(math.radians(steer * max_steer_deg)) / wheelbase

At v = 0 the result is 0. An Ackermann chassis cannot rotate in place, so steering while stopped does nothing. Positive angular.z is left, the opposite of the Python API.

Worked example

limits = read_vehicle_info(node)           # /ego/vehicle_info, JSON in a String
vmax   = limits["max_speed_ms"]

cmd = Twist()
cmd.linear.x  = max(-vmax, min(vmax, 0.4))
cmd.angular.z = cmd.linear.x * math.tan(math.radians(
    -1.0 * limits["max_steer_deg"])) / limits["wheelbase_m"]  # -1.0 here = full right (ROS: positive angular.z is LEFT)
pub.publish(cmd)

Clamp the speed first, then convert the steering with that same speed. The teleop example does exactly this.

What the car actually did

/ego/cmd_applied (geometry_msgs/Twist) reports what the vehicle is obeying, after the actuator latency queue:

  • linear.x — applied throttle, −1..1. Not a speed: 1.0 means full throttle.
  • linear.z — steering angle actually reached, in degrees.
  • angular.z — applied steer, −1..1.

If linear.x sits at 1.0 while you keep raising the command, you are past the ceiling. And linear.z is not redundant: maxSteerRate caps how fast the servo follows, so on a quick input the wheels are not yet where you asked.