Docs menu
Odometry calibration
Calibrate the speed gain, the servo centre, the steering gain and the steering limits with a tape measure and a browser wizard.
The conversions in each car’s calibration/vesc.yaml decide how far the car believes it went and how sharply it believes it turned. They drift from the real car as tires wear and the linkage is adjusted. odom_calibration is a browser wizard that compares a tape measure on the floor with what the car counted, and suggests corrected values. It publishes nothing and cannot move the car: a person drives with the F710 and LB while the wizard records.
The package is src/odom_calibration/GitHub. Car-specific values (the model name, dimensions, rear track and planner limit) are in its config/odom_calibration.yaml.
Topics
The node only listens. It writes to the YAML files on disk when a Write button is pressed, and never publishes.
| Topic and type | Used for |
|---|---|
/odom, nav_msgs/Odometry |
What the odometry believed during each run |
/sensors/core, vesc_msgs/VescStateStamped |
The motor’s ERPM, to integrate into distance |
/sensors/servo_position_command, std_msgs/Float64 |
The servo value after the VESC driver’s clamp |
/commands/servo/position, std_msgs/Float64 |
The servo value before the clamp, compared with the one above to spot clipping |
/ackermann_cmd, ackermann_msgs/AckermannDriveStamped |
The commanded steering and speed |
/joy, sensor_msgs/Joy |
LB: only samples taken while LB is held count |
The calculations are in calibration_math.pyGitHub and its neighbours, which import no ROS. The excerpts below are from it, with docstrings and input checks removed.
Start it
Terminal 1: ros2 launch f1tenth_stack bringup_launch.py
Terminal 2: ros2 launch f1tenth_stack teleop_launch.py
Terminal 3:
ros2 launch odom_calibration odom_calibration_launch.pyTerminal 3 prints the wizard’s address, http://<car-ip>:8090/, and confirms the node is read-only. Open it in a browser on a computer that can reach the car. Each test is a numbered step in the page’s sidebar, with a diagram of the setup.
Wheel calibration
Use it when distances read wrong, for example when the car counts 4 m over a 5 m lane. The wizard records a stationary baseline, then three 5 to 10 m runs along a tape, two forward and one in reverse. The baseline gives the ERPM offset , the reading while the car is still. For each run, the wizard integrates the motor’s ERPM over the run’s duration , removes the offset, and divides by the measured distance:
That is the gain that would have made the odometry read exactly the taped distance. The suggestion is the median over the runs, because one bad run, such as a wheelspin at the start, moves a median far less than a mean.
The integral is computed with the trapezoid rule: between two samples, the area under the ERPM curve is approximated by a trapezoid, the average of the two readings times the time between them. A gap longer than 0.5 s is skipped rather than bridged, because assuming the motor held a steady speed through missing data would invent distance.
for (time_a, value_a), (time_b, value_b) in zip(valid, valid[1:]):
dt = time_b - time_a
if dt <= 0.0:
non_increasing += 1
continue
if dt > max_gap_sec:
skipped_duration += dt
continue
integral += 0.5 * (value_a + value_b) * dt
integrated_duration += dt
``` Car 1's current gain came from five such runs on October 21, 2026, whose implied gains were 3959, 3936, 4044, 4088 and 4179.
## Steering calibration
Use it when the car pulls to one side with the stick centred, or when its turns do not match what was commanded. It needs three runs before the report unlocks.
1. **Drive straight** with the stick centred, starting with the rear axle on a taped line. Measure how far along the line the rear axle ended up, $s$, and how far it drifted off it, $d$. The car drove an arc, tangent to the line at the start, through the end point. That is the same geometry as [pure pursuit's arc](../../autonomy/pure-pursuit/#curvature-of-the-arc), with the end point as the target:
$$
\kappa = \frac{2d}{s^2 + d^2},
\qquad
\delta_{\text{drift}} = \arctan(L\kappa)
$$
For example, 0.10 m of drift over 5.0 m gives $\kappa = 0.008\ \text{m}^{-1}$ and $\delta = 0.0029$ rad, 0.17°. The wheels were pointing that far off straight while the servo held its median value $u$ during the run. On the line $u = k_\delta\,\delta + u_0$, the servo value for straight ahead is therefore $u - k_\delta\,\delta_{\text{drift}}$, and that becomes the suggested `steering_angle_to_servo_offset`. Write it, restart bringup, and repeat until the wizard reports the car drives straight, within 0.14°.
```python
def drift_steering_angle(wheelbase, forward_m, lateral_m):
curvature = 2.0 * lateral_m / (forward_m ** 2 + lateral_m ** 2)
return math.atan(wheelbase * curvature)- Full-lock circles, one left and one right. Mark the inner rear tire’s track and measure the circle’s diameter across it. The wizard adds the rear track and finds the rear axle’s radius and the wheel angle the car reached:
The rear axle’s centre is midway between the rear tyres, half a track from each, which is where the comes from. The wizard also accepts the diameter of the axle centre’s own circle, or of both rear tyres’ circles. With both tyres, the axle centre’s radius is the mean of the two tyre radii, and their difference is the track, which then serves as a check on the measurement.
if method == 'inner_tire':
return (float(inner) + float(rear_track_m)) / 2.0, None
if method == 'rear_tires':
return (float(inner) + float(outer)) / 4.0, \
(float(outer) - float(inner)) / 2.0The steering gain is the line through the straight-ahead point that best fits the circles in . Only samples taken while LB is held and the car is moving count toward a circle.
Find how far the steering goes
Near full lock each servo step turns the wheels less, until a step adds nothing. The wizard can raise servo_max, or lower servo_min, by 0.05 at a time and ask for another circle after each step. The side has reached its stop when a step adds less than 1°, about one tape measurement’s error. The report then suggests a servo limit at the last circle before the stop, and a full-stick angle that both sides reach, less 0.5°.
Check every new servo limit with the wheels off the ground first: hold LB and full lock for 3 s, and undo the change at any buzzing, heat or contact. A servo held against a stop overheats.
The wizard also works out, from the settings alone, whether servo_min or servo_max clips full stick or max_steering_angle on either side. On car 2 on October 10, 2026, a clamp at servo_max: 0.85 left only 12.3° of right steering against 19.5° on the left, with nothing in the logs.
Write the values
Write buttons put an accepted value into the car’s own calibration/vesc.yaml (gains, centre, servo limits) or calibration/joy_teleop.yaml (full-stick angle), after a confirmation. The wizard finds the car the same way a launch does, and never writes the shared files unless vesc_yaml_path or joy_teleop_yaml_path names one; check both parameters are empty before writing. A timestamped backup of each file is saved first, in ~/.ros/odom_calibration/backups/. Undo the last change restores every file one write touched, and Undo everything from this session restores them all.
A value that cannot be right, such as a zero or wrong-signed gain or a centre outside the servo range, is blocked with its reason. Any suggestion can be replaced with a typed value, which goes through the same checks.
Bringup reads the calibration only when it starts, so restart bringup after every write, and restart teleop after a full-stick change. The page shows a banner until you do.
Limitations
- A tape measure cannot see tire slip, surface changes, tire wear, battery state or backlash in the drivetrain.
- The autonomy limit
max_steering_angleis never written by the wizard. Change it by hand after a calibration, keeping it below full stick. - The full procedure, including how the wizard was tested against the real conversion nodes with simulated hardware, is in
docs/odom-calibration.mdGitHub.
Checked against
car 1 commit 0265263
and
car 2 commit b81b291, before the two workspaces merged.