Airfield
Airfield's own documentation
This page covers how the car's workspace uses airfield. For airfield itself, see its documentation website, airfield.io. Good places to start there are the Overview, the Quick Start and the CLI Reference.
The car's software, the roboracer_ws workspace, is built and launched with airfield. Airfield builds one container image per ROS 2 package, and launches a plan (a named set of packages) as a tmux session with one pane per program.
On an F1TENTH car, the setup script installs airfield and builds everything. This page explains how the workspace works once it's there.
How the workspace is laid out
~/roboracer_ws/ the project (airfield.yaml at the top)
├── airfield.yaml project settings: the base image, the list of package repos
├── packages/<name>/ one ROS 2 package, its own git repo -> one container image
│ └── airfield.yaml its dependencies, devices, groups, build flags
├── dependencies/ how to install each dependency into an image
│ ├── xplatform/ recipes for every platform
│ └── arm64/ | x86_64/ platform-specific recipes, and the Jetson base image
├── plans/<name>.yaml what to launch: navstack, teleop, car_calibration, ...
├── scripts/ build / up / down helpers
├── .air this machine's settings (not in git)
└── .airfield/workspace/ the compiled code (not in git)
└── build/ install/ log/
Two unrelated things are called "packages": ~/roboracer_ws/packages/ is the car's ROS code, and ~/.cache/airfield/packages/ is airfield's shared recipe book for installing system software (ROS libraries, OpenCV, ...) into images.
The workspace has to be at ~/roboracer_ws, directly in your home folder, because some of the code refers to that path.
Build once, launch many
Every package's container mounts the same compiled workspace, so the ROS 2 code is compiled once, into install/. Each pane then only sources that and runs its program. Compiling in every pane at once needs far more memory than the Orin's 8 GB, and runs it out of memory.
The compiled workspace has two paths:
- On the car, it's
~/roboracer_ws/.airfield/workspace/, inside the project. Each project gets its own, so two projects that both have a package with the same name can't pick up each other's binaries. - Inside a container, it's always
~/workspace.
AIRFIELD_WORKSPACE moves the car-side folder, if you ever want several projects to share one. See What gets mounted on airfield.io for everything a container receives.
Building
cd ~/roboracer_ws
scripts/build # everything the navstack plan uses
scripts/build ut_automata # just this package and what it needs
scripts/build builds each package's image, then compiles the code one package at a time with limited parallelism, so the Orin doesn't run out of memory. The first build is slow, mostly downloading and installing software into the images. Later builds reuse that.
After you edit a package, build it again with scripts/build <package>. Panes only compile packages that are missing from install/, so without a rebuild the change never reaches the car. The usual symptom is a "file not found in the share directory of package" error.
When a pane starts, it checks whether its package is already compiled. If not, it compiles it first, one pane at a time, so launching a plan on a fresh workspace also works, just more slowly. Build flags for a package come from colcon_args: in its airfield.yaml (for example ut_automata's -DCMAKE_BUILD_MODE=Hardware).
To force a clean compile:
cd ~/roboracer_ws
rm -rf .airfield/workspace/build .airfield/workspace/install
scripts/build
Launching and stopping
cd ~/roboracer_ws
scripts/up # the navstack plan
scripts/up teleop # any plan in plans/
scripts/down # stop everything
scripts/up clears leftovers from earlier runs, builds anything missing, then starts the plan. airfield project up <plan> and airfield project down do the same day to day. scripts/up and scripts/down also clean up after a crash: a stale VNC display lock, and a stuck camera service.
The navigation stack starts on the gdc_3n map. To use another one: MAP=<name> scripts/up. The maps are in packages/av_navigation.
In the tmux session:
- Move between panes:
Ctrl-b, then an arrow key. - Leave everything running and get your terminal back:
Ctrl-b, thend. Return withtmux attach -t navstack(the plan's name). - A pane whose hardware isn't plugged in shows errors. The other panes keep working.
Always stop with scripts/down or airfield project down, not by closing tmux. Each pane stops its container when its tmux session ends normally, but killing the tmux server leaves containers running in the background. After a power loss or a hard crash, airfield project down --prune removes any leftover containers.
Plans live in plans/. Create a Plan on airfield.io explains how to write one.
To watch from your laptop, point Foxglove Studio at ws://<car>:8765, or a VNC viewer at <car>:5909 for rviz and the GUI.
Running a command in a package's container
cd ~/roboracer_ws
airfield package shell ut_automata # an interactive shell
airfield package cmd <package> -- <command> # one command
airfield package run orin_rp2_csi mono_processor # a named command from the package's airfield.yaml
Every command and option is in airfield's CLI Reference.
Per-machine settings
The .air file
.air, at the top of the project, lists folders on this machine that get shared into every container. It depends on the machine, so it isn't in git. The setup script writes it on a car. By hand:
cd ~/roboracer_ws
cat > .air <<'EOF'
mounts:
- ~/.bash_history
- ~/.ssh/authorized_keys
# let Qt windows (ut_automata gui, rviz2) reach the touchscreen (:0) or the VNC display (:9)
- /tmp/.X11-unix
- /run/user/$UID/gdm
EOF
Keep $UID exactly as written: airfield fills in your user ID. A hardcoded number resolves to nothing on a machine whose user has a different ID, and the touchscreen GUI then can't draw. Folders that don't exist are skipped with a warning.
.air belongs in the project, not in packages/ut_automata, because ut_automata is shared course code that other robots use too.
Device groups
The containers reach the motor controller, the IMU and the controller through group numbers in packages/ut_automata/airfield.yaml:
devices: [/dev/ttyACM0, /dev/i2c-7, /dev/input, /dev/ttyUSB0]
group_add: ["20", "108", "996"] # dialout (VESC, RPLIDAR), i2c (IMU), input (controller)
Check a machine's numbers with getent group dialout i2c input. If they differ, change group_add: to match and don't commit that change. A device that isn't present is skipped with a warning, and the container still starts. If the motor controller or IMU is on a different device, change it both here and in vesc.lua.
Dependencies
A package's airfield.yaml lists its dependencies by name, and each name needs a recipe (a dependency manifest) that says how to install it. Airfield looks in the project's dependencies/ folder, then in its shared recipe book, a copy of airfield/packages in ~/.cache/airfield/packages.
airfield package dependencies check # compare the project's recipes with the shared ones
airfield package dependencies pull # get the newest shared recipes
A dependency with no recipe fails the build with an unresolved-dependency error. Add the recipe to the project's dependencies/xplatform/, or to the shared recipe book. Managing Dependencies on airfield.io explains how to write one.
The base image and JetPack
On the Orin, every package image starts from roboracer/l4t-jazzy:r39.2.1: ROS 2 Jazzy plus NVIDIA's camera and GPU libraries at exactly the car's JetPack version (L4T 39.2.1 is JetPack 7.2.1). The car's camera plugins are shared into the containers when they run, so the libraries inside have to match the car exactly, or the camera breaks in ways that are hard to trace.
The image isn't in any registry, so it's built on each car:
cd ~/roboracer_ws
dependencies/arm64/l4t-jazzy/build.sh
The script reads the car's L4T version, pins every NVIDIA package in the image to it, and tags the image with it. Nothing in the script is tied to one JetPack release. The project's airfield.yaml names the image every package uses (base_image:) and says not to download it (pull_base_image: false).
- A car on the fleet's JetPack: run
build.sh. The setup script does. - A car on another JetPack:
build.shstill works, tags the image for that version, and warns that it isn't the oneairfield.yamlnames. Reflash the car to the fleet's version, or pointbase_image:at the tag it built, locally and uncommitted. - The whole fleet moving to a new JetPack: change
base_image:once, then rebuild on every car. build.shfails its first check: NVIDIA has removed that L4T version from its package server. The error lists what's still available.L4T_VERSION=<version> dependencies/arm64/l4t-jazzy/build.shbuilds with one of those, but the image then no longer matches the car exactly.
Updating
On an F1TENTH car, the setup script updates airfield, its recipes, and the code: see Keeping a car up to date.
On another machine:
pipx reinstall airfield # the newest commit of the branch it was installed from
airfield package dependencies pull # the newest recipes, after updating airfield
Update airfield before its recipes: newer recipes can use a format an older airfield reads without complaint but installs nothing from. airfield system update compares version numbers and may say it's up to date when it isn't; airfield system update --force always reinstalls.
The first build after an airfield update rebuilds every image, because airfield copies itself into each one. To do that before you launch, run airfield package cmd <package> -- true for each package.
Setting up another machine
The F1TENTH cars use the setup script. On any other machine:
- Install Docker (on a Jetson, also NVIDIA's container runtime:
sudo nvidia-ctk runtime configure --runtime=docker), and add yourself to thedockergroup. - Install airfield:
pipx install git+https://github.com/airfield/airfield.git, then runairfield doctor. - Get the code:
cd ~ git clone https://github.com/ut-av/roboracer_ws.git cd roboracer_ws airfield subpackages checkout git -C packages/ut_automata submodule update --init --recursive - On a Jetson, build the base image.
- Create the
.airfile, then runscripts/build.
Troubleshooting
- Foxglove bridge logs "Failed to load schemaDefinition ... not found". Harmless: those topics just can't be shown in Foxglove, because the bridge's image doesn't have their message packages. Add them to
dependencies:inpackages/foxglove_bridge/airfield.yamlto see them. - The GUI or rviz panes crash at startup. They draw on the VNC pane's display
:9, and a crash can leave a stale lock for it.scripts/downthenscripts/upclears it. - The camera pane says "Captured image is invalid or empty". See Raspberry Pi Camera troubleshooting.
nvidia-smisays "No devices" and the camera can't start. The GPU occasionally fails to start at boot. Reboot the car.- A change to a package doesn't show up. Run
scripts/build <package>; see Building.