Docs menu

Git, submodules and car profiles

Which packages are submodules and which are vendored, how to update each safely, and where to change a value for one car without touching the other.

Both cars run one workspace repository, Racerbot-Car-Workspace. Some of its packages are Git submodules that point at a commit in another repository, one is a vendored copy with local fixes, and the rest are the workspace’s own code. The full reference is docs/git-setup.mdGitHub.

Submodules and vendored code

Package Kind Source
particle_filter, range_libc Submodule f1tenth, branch humble-devel
transport_drivers Submodule ros-drivers, branch humble
racerbot_a, racerbot_b Submodule The two team repositories, main
web_dashboards Submodule sfu-racerbot/web-dashboards, main
realsense-ros Submodule, initialized on car 2 only Intel’s RealSense driver, car 2’s camera. It needs librealsense to build, so car 1 leaves it empty.
f1tenth_system Vendored: plain tracked files Was f1tenth/f1tenth_system

A clone without --recurse-submodules leaves the submodule folders empty. git submodule update --init --recursive fills them all; on car 1, name the submodules instead and leave out src/realsense-ros, as Set up the workspace shows.

To take a newer commit of a submodule, update it, build, test, then commit the new pointer in the workspace. Push the submodule’s commit before the workspace’s pointer, or everyone else’s clone points at a commit they cannot fetch.

git -C src/web_dashboards fetch origin && git -C src/web_dashboards checkout origin/main
colcon build --symlink-install --packages-select web_dashboard usb_cam_stream
git add src/web_dashboards

f1tenth_system

f1tenth_system was a submodule and was converted to plain files so that local fixes could be committed. A naive copy of upstream over it removes every one of them, and two of the losses fail silently.

Fix File Without it
Steering on the F710’s right stick, axis: 3 joy_teleop.yaml Steering follows the left trigger
joy_teleop in its own teleop_launch.py bringup_launch.py, teleop_launch.py Bringup starts manual control, and /teleop masks every autonomy run
The measured geometry: wheelbase 0.36, LiDAR at 0.26 vesc.yaml, bringup_launch.py Every pose and collision check models the wrong car
No per-car values in the shared vesc.yaml, sensors.yaml and joy_teleop.yaml All three A per-car value left in a shared file applies to both cars until a car’s overlay replaces it

The negative odometry gain, the servo map, the LiDAR connection and the full-stick steering angle are per-car values, so they live in each car’s profile under src/racerbot_car/cars/<id>/calibration/, not in f1tenth_system. To take an upstream change, compare the vendored copy against upstream and merge only the change you need by hand, then check every fix.

One more difference from upstream is deliberate but not a fix. Upstream’s bringups remap the mux’s ackermann_cmd_out to ackermann_drive, a name the mux never uses, so the remap did nothing; it was deleted in October 2026. If a sync brings it back, test_drive_path_wiring.py fails, and the line should be deleted again.

Change a value for one car

Both cars build from the same commit, so a change to shared code or shared configuration reaches both cars. A value that belongs to one car goes in that car’s profile, src/racerbot_car/cars/rb1/ or src/racerbot_car/cars/rb2/, which every launch loads on top of the shared configuration. The cars lists what each profile file holds.

The change Where it goes
A measured hardware value: a servo map, a speed gain, a steering limit, the LiDAR connection The car’s calibration/ file, with the same node block as the shared file
A device: a new LiDAR or camera The car’s car.yaml, and its calibration/sensors.yaml for a LiDAR
Algorithm tuning both cars should run The shared config file, such as src/gap_follow/config/gap_follow.yaml
Algorithm tuning one car needs The car’s tuning/ file, with a # why: comment above every value

A calibration file may only hold the keys listed in CALIBRATION_KEYS in guards.pyGitHub, and a launch stops with MissingCalibrationError if a required key is missing. A tuning file can never relax a safety setting, so a launch stops with TuningOverlayError instead of running with one that turns off the deadman, the TTC brake or the swept-clearance check, changes the deadman button, raises a speed cap or a timeout, or lowers a clearance. The rules are in docs/car-profiles.mdGitHub.

After any change to a profile, a shared configuration file or a launch file, run the profile and launch tests:

source /opt/ros/jazzy/setup.bash
source ~/racerbot-ws/install/setup.bash
export ROS_DOMAIN_ID=79 ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST
cd ~/racerbot-ws
python3 -m pytest src/racerbot_launch/test src/racerbot_car/test -q

test_car_profiles.pyGitHub checks both cars at once, whichever computer runs it. It fails if a car’s LiDAR, camera, dashboard settings, servo map or steering limits, or car 2’s odometry gain, no longer match its profile and the hardware reference, or if anything car 1 starts at boot would launch a RealSense driver. A failure means the change overwrote a car’s hardware setting: fix the change, not the test.

Then, on the car, with bringup running, run ros2 run racerbot_doctor doctor. Its first line names the car and lists any tuning overrides, and on car 1 the LiDAR section shows /dev/sensors/hokuyo. A new or changed calibration still drives in the usual order: static checks, then wheels off the ground with LB held, then the floor at low speed.

When you find a new difference between the cars, add it to the car’s profile, add a row to docs/car1-vs-car2.mdGitHub, and add a check to test_car_profiles.py.

Checked against workspace commit 556284c.