Docs Run the car Workspace repo
Docs menu

Map a course

Drive laps of a new course with gap_follow while SLAM builds the map, then save the map, a racing line and the start pose, automatically with map_course or by hand.

Mapping is the first half of racing a course. The car drives laps on its own with gap_follow while slam_toolbox builds an occupancy grid. When it has driven enough clean laps, it saves the map, the last lap as a racing line, and the spot where it stopped, then shuts itself down and prints the commands to race the saved map.

Map automatically

map_course needs bringup running and never starts it.

Terminal 1, left running for mapping and racing:

ros2 launch f1tenth_stack bringup_launch.py

Put the car on the course, pointing in the direction it will lap. Then, in terminal 2:

ros2 run racerbot_launch map_course

map_course maps on the gyro by default on both cars. It starts the BNO085 and the IMU side, and SLAM takes its heading from the BNO085, or from the VESC’s gyro if the BNO085 stops. Keep the car still for the first 3 seconds while the gyro bias is measured. Then hold LB for the whole run. The car stays still until SLAM has recorded the start pose and printed Start pose recorded: .... Releasing LB pauses mapping, and holding it again continues.

It is working when the terminal shows Closed mapping lap 1/3, 2/3 and 3/3, then Saved occupancy map successfully., and ends with a block starting MAPPING FINISHED. that contains the race commands for this run.

Leave the car where it stopped. That spot is the start pose the race uses. To race on another day, mark the spot and the car’s direction on the floor first.

After the run

Once the launch has stopped, whether the laps finished or Ctrl+C stopped it, map_course checks the map against the run’s own recorded scans. It writes a folder named <run>-rebuilt next to the map’s folder in ~/racerbot-ws/data/<id>/racerbot_auto/ (<id> is rb1 or rb2) and prints GO: or NO-GO: with the reason.

  • GO: the map is usable, and the race commands printed at the end point at that folder. Its racing line comes from where the car was on the finished map, capped at 1.5 m/s for the first drive.
  • NO-GO: do not race on this map. The most common reason is walls drawn twice. Map the course again.

After Ctrl+C, the run stops the car, allows up to 15 s for the nodes to shut down, then runs the check and prints a RUN STOPPED summary with the laps closed, the verdict and where the recording is. Wait for the prompt to return. A second Ctrl+C skips the check and prints the command to run it later. rebuild:=false skips the check, and with recording off (diagnostics:=false) there is nothing to check.

The longer reports, summarize_run, the IMU report and the SLAM report, take minutes, so they run only with analysis:=true. Each saves its full report in the recorded run’s analysis/map_course/ folder. The Recording run to: line near the start of the output gives that run’s path, which is a different folder from the map’s. To run them later instead:

cd ~/racerbot-ws
python3 tools/run_analysis/slam_report.py ~/racerbot-ws/data/<id>/racerbot_runs/<ts>
python3 tools/run_analysis/imu_report.py ~/racerbot-ws/data/<id>/racerbot_runs/<ts>
ros2 run race_diagnostics summarize_run ~/racerbot-ws/data/<id>/racerbot_runs/<ts>

Options

Option Effect
mapping_max_speed:=1.5 Caps the mapping speed. Use 1.5 on a course nobody has driven. Default 3.5 m/s.
laps:=4 A different number of laps, at least 2
record_bag:=false Skips the rosbag of the run. Default true.
imu:=false Runs with no IMU at all: no BNO085, no vehicle_state_node, and SLAM on the steering model. The map is worse; use it only when the IMU side itself is broken.
gyro_odom:=false SLAM maps on the steering model instead of gyro odometry. Default true whenever imu is on.
imu_lap_turns:=false Counts lap turning in the steering model’s odom frame. Default follows gyro_odom. Do not combine it with gyro odometry: a hard turn then reads as a SLAM correction, and laps stop closing.
fused_speed:=true gap_follow uses the IMU-fused speed. Needs imu:=true. Not yet driven on. Keep it off on car 2 until its BNO085 mount yaw is measured.
imu_all:=true Turns on fused_speed as well. An explicit false wins.
analysis:=true Runs the longer reports after the run
rebuild:=false Skips the map check after the run. A run stopped early then keeps nothing.
opponent_awareness:=true Turns on gap_follow’s tracking of other cars, which mapping turns off because mapping is one car on the course
imu_hardware:=false Uses the simulator’s IMU instead of starting the real sensor readers. map_course adds it when the simulator is running.

The 3.5 m/s default exists because the gyro SLAM profile has been validated by replay only up to about 3.3 m/s. It is raised only after a recorded gyro run at a higher speed produces a clean SLAM report. Car 1 had not mapped on the floor with the gyro SLAM settings when the two workspaces merged on November 1, 2026, so map on car 1 at mapping_max_speed:=1.5 until it has.

imu_all here is map_course’s own flag. gap_follow’s flag of the same name turns on its driving features instead (see IMU features). With gyro_odom:=false, also pass imu_lap_turns:=false, because lap turns from the gyro need gyro odometry and the launch refuses to start otherwise.

What happens during a run

Counting laps

A lap has to be detected from the car’s own pose, without knowing how long the course is. auto_map_race_node records the car’s position in the SLAM map every 0.15 m, and uses two tests together.

It has turned once round. Driving once round any closed circuit turns the car through a total of 360°, whatever its size or shape, as long as the course does not cross itself: the left turns minus the right turns add up to exactly one revolution. This is the turning-tangent theorem. The node adds up the car’s turning from odometry, not from the SLAM map, because a SLAM correction rotates the map without the car turning.

It is back at the start, facing the same way. After first moving at least 2.0 m away (departure_distance), the car must come back within 1.5 m of where the lap started (closure_distance), heading within 30° of its starting heading (closure_heading_deg).

If a lap has turned more than 1.25 revolutions without closing, the distance gate widens, so a car that is slightly off its start line on every lap still closes eventually.

It took long enough. A lap must also last a minimum time, so a car that only wobbles near the start cannot close one. The first lap needs 4 s (minimum_lap_duration_sec). From the second lap on, a lap also needs half the previous lap’s time (minimum_lap_duration_fraction, 0.5). The previous lap’s time is the course length divided by the speed driven, so the gate follows the course: on car 1’s 28.7 m course, driven in 16 to 17 s, it is about 8 s. The fixed 15 s it replaced was longer than one fast lap of a short course: on the simulator’s 29 m indoor_oval it blocked the first return to the start, so each recorded lap held two circuits.

Turning from the gyro. With gyro odometry, the default, the turning is read from odom_imu, the gyro heading, while the recorded path still comes from the map. With imu_lap_turns:=false it is read from the odom frame, whose heading comes from the steering command. If odometry pauses for at most 0.25 s, the counter adds the map’s heading change for the gap provisionally and replaces it with the gyro’s when odometry returns, so the gap is not counted twice. A longer gap, or one with more than 90° of provisional turning, keeps the map’s value, because a wrapped heading difference could hide a full turn.

SLAM corrections are not motion. slam_toolbox keeps re-optimizing its map, and each correction moves the car’s pose in the map without the car moving. Recorded as-is, those jumps would become kinks in the racing line. Instead, each correction is applied to the points already recorded, so the recorded shape moves with the map.

Checking each lap

The first lap discovers the course. Each lap after it is checked before it is accepted as the racing line:

Check Limit What a failure suggests
SLAM corrections during the lap At most 20 Localization was unsettled for much of the lap
Closed on the original 1.5 m gate Required The car only found its way back after the gate widened
Heading error at closure At most half the 30° gate, 15° The lap ended at an angle to where it started
Revolutions At most 1.5 The lap ran over about two overlapping circuits

If the last lap fails, the run drives up to two more, and the log names the check that failed. The checks have not yet been tuned against real good and bad laps.

Saving

When the laps are done, the run stops the car and writes everything to one folder, ~/racerbot-ws/data/<id>/racerbot_auto/<YYYYMMDD-HHMMSS>/: the map, SLAM’s pose graph, the racing line as recorded, profiled and optimized, the start pose, and the race commands. See The run folder. The racing line goes through the same profile and optimizer steps described on Racing line and speed profile, and the run refuses a line the car could not steer.

Map by hand

To map with manual driving, run SLAM beside teleop and save the map yourself.

Terminal Command
1 ros2 launch f1tenth_stack bringup_launch.py
2 ros2 launch f1tenth_stack teleop_launch.py
3 ros2 launch racerbot_launch slam_launch.py

Drive the whole course slowly and finish where you started, because closing the loop is what lets SLAM correct its drift. Then save the map:

ros2 run nav2_map_server map_saver_cli -f <map_name>

This writes <map_name>.yaml and <map_name>.pgm into the folder the command ran from. A hand-saved map has no racing line or start pose, so racing it means recording a line with the waypoint recorder and seeding the particle filter by hand.

Troubleshooting

Symptom Cause Fix
the hardware is not running map_course needs bringup and never starts it. Start bringup in terminal 1.
The car never moves teleop_launch.py is running, or LB is not held. Stop teleop. Hold LB.
The car waits at the start SLAM has not recorded the start pose yet. Keep holding LB; it starts once Start pose recorded appears.
A lap is driven again after the last one The lap failed a quality check. The log names it. Let it finish. It stops after two extra laps. Read the lap verdicts in the SLAM report afterwards.
imu_lap_turns:=true needs gyro_odom:=true gyro_odom:=false was passed while lap turns still follow the gyro. Add imu_lap_turns:=false as well.
... needs imu:=true An IMU feature was asked for with the IMU side off. Pass imu:=true, or turn the named feature off.
NO-GO: after the run The map check found the map inconsistent with the run’s own scans, most often walls drawn twice. Do not race it. Map again with the IMU side on.
No lap ever closes The car does not return near its start, or SLAM is drifting badly. Start from a spot the car passes on every lap, and drive slower with mapping_max_speed:=.
The map folds onto itself A false loop closure in similar corridors See SLAM and localization.

Checked against workspace commit 556284c.