eduroam first
After a cold-boot, the car might fail to connect to eduroam, possibly due to a time-sync issue. Either connect it to a phone hotspot and allow the time to synchronize, or connect a keyboard & monitor, and fix the time manually. Once that’s handled, use
nmtuito re-connect it toeduroam
ssh rtis-jetson@rtis-jetsonrtis2244
ssh-copy-id rtis-jetson@rtis-jetson from your pc or laptop - then it’ll connect without a passwordcd f1tenth_car_dockerOnly do this is the controller absolutely refuses to connect.
sudo bluetoothctl
sudo systemctl restart bluetooth.serviceremove <address> - find the address by running paired-devices firstscan onController line should appear in the scan results. Note the address (in the form of 00:00:00:00 etc) in that linepair 00:00:00..., using the address found in the previous step. Tab completion works here - type the first few digits and press tabtrust 00:00:00... and connect 00:00:00...bluetoothctl console with exit/dev/input using ls /dev/input/ - there should be a js0 in the list.The docker containers will only recognize the controller if the controller is connected before starting the containers. If you’ve already started them, you may need to restart them after connecting the controller.
https://github.com/RTIS-Lab/f1tenth_car_docker . On the car, it’s in /home/rtis-jetson/f1tenth_car_docker
Everything runs in docker containers. All containers are derived from a base.Dockerfile file. The base container installs ROS2 Humble and uv, and installs other common dependencies.
There are three major containers:
docker compose exec system bash to get a shell into the system container. Replace system with drive to get a shell into the drive container.
You can also SSH directly into drive or system. drive uses port 2222, and system uses port 2223. The username is root, and the password is password. This is useful for Zed or VSCode’s SSH remote option.
From the car, do ssh root@localhost -p2222 (or 2223), or from a laptop ssh root@rtis-jetson -p2222.
run ros_interactive so commands like ros2 topic list work. You only need to do this once per shell session.
We’re using the FastDDS Discovery Server to handle connections through Eduroam and Tailscale (multicast doesn’t work on either of these!). For efficiency, the discovery server only informs nodes of topics when they explicitly request them. Commands like ros2 topic list don’t get a full list by default. The ros_interactive script changes the current shell to a SUPER_CLIENT config, which causes information about all topics to be requested from the discovery server. Careful though - when you start any nodes from this configuration, they’ll inherit the SUPER_CLIENT config, which may be inefficient for normal operations.
The system container, defined in f1tenth_system, runs the sensor, joystick, and motor drivers, and some utility publishers. It also contains some launch files for sensor fusion, localization, and mapping.
The drive container, defined in f1tenth_drive, handles all the driving algorithms. It also runs the joy->drive bridge.
The discovery container runs a ROS2 discovery server. This must be running for the other containers to discover each other.
A separate container for the rl task. This container does not extend the base container - instead it uses dusty’s container to expose CUDA to the container.
As of right now (July 23, 2026), the rl program will start automatically with the container, and uses config/vanilla_driver.yaml. If this container is running and you don’t need it, stop it with docker compose down rl_inference to save resources.
From the f1tenth_car_docker folder, run docker compose up -d to start all containers in the background. To see the log output from them, run docker compose logs -f.
you can also run
docker compose up discovery system driveto run everything without the rl_inference container.
It takes 20 to 30 seconds for the containers to fully initialize. The containers run a colcon build on startup. If you haven’t changed any source code, the build will be cached and complete quickly.
Each container mounts it’s source and build folders. They all use host mode networking and shared memory. The containers have an entrypoint.sh folder that starts a default launch file, and an ssh server.
/teleop/drive: an AckermannDriveStamped message, that contains a target speed value and steering angle. Publish to this to control the vehicle.
/scan: a LaserScan message, that contains the lidar scan data. Subscribe to this to get the lidar data. This publishes 1080 points, with a 270 degree field of view at 40Hz.
/odom: a Odometry message, that contains the vehicle’s position and velocity. Subscribe to this to get the vehicle’s position.
/run_auto: A Bool message, that contains a boolean value. Publish a true value to this topic to start the vehicle’s automatic control package. Your driving code should listen to this topic, and only publish to /drive when /run_auto is true. This topic is published by the teleop node whenever the correct button (X on our PS5 controllers) is pressed on a gamepad.
sudo systemctl stop gdmsudo systemctl disable gdmsudo systemctl start gdm
sudo jetson_clocksThere’s some AI generated notes and reference commands in the car’s workspace in notes.txt
ros2 launch f1tenth_stack mapping.launch.py
To save a map: ros2 service call /slam_toolbox/save_map \ slam_toolbox/srv/SaveMap "{name: {data: track_name}}" in another terminal
you must save the map before stopping the mapping process!
ros2 launch f1tenth_stack fusion.launch.py \ localization_mode:=amcl map:=track_name.yaml