TrickFire Robotics

Dev Notes

Internal notes, tips, and architectural decisions for contributors.

Architecture decisions

Docker vs. pixi

Both workflows produce the same ROS 2 Jazzy + Gazebo Harmonic environment. Docker is the primary, recommended workflow; pixi is a supporting option for a faster Gazebo-only loop.

ROS 2 + Gazebo + Chrono are notoriously painful to install and keep in sync natively, especially across different OS versions. The Dev Container pins every dependency (ROS 2 Jazzy, Gazebo Harmonic, VSG, Chrono, VirtualGL) inside a pre-built image, so it behaves the same on Linux, macOS, and Windows, with or without a GPU. The display/GPU quirks that used to make Docker painful - X11 forwarding, VirtualGL, NVIDIA runtime detection - are now handled automatically by .devcontainer/host-env.sh and .devcontainer/x_server.sh (see below), so the container works out of the box. It's also the only workflow that supports Chrono.

Docker environment

The Dev Container is built from two Dockerfiles that work together, plus a pair of host-side scripts that configure display and GPU access before the container starts.

Image build

docker/vsg-chrono.Dockerfile builds VSG and Chrono from source - a slow, rarely-changing C++ build - and publishes it as ghcr.io/trickfirerobotics/simulations-vsg-chrono via CI on every push to main that touches it (or on manual dispatch), for both amd64 and arm64. docker/Dockerfile's sim stage then copies /opt/vsg and /home/trickfire/chrono out of that image and builds everything else on top as simulations:latest - changing the app Dockerfile never triggers a Chrono rebuild.

SectionContents
BASELocale, trickfire user, a libdrm stub needed for Jetson Mesa rendering
VSG + CHRONOCopied in from the vsg-chrono base image
GAZEBOROS 2 Jazzy + Gazebo Harmonic, ros-gz, controllers, RViz, xacro
VNC / DISPLAYHeadless X stack: Xorg/Xvfb, Openbox, x11vnc, noVNC
VirtualGLGL proxying for remote (XQuartz/VcXsrv) displays
DEV TOOLINGSSH server, ruff, pre-commit, shfmt, clangd

Build parallelism

A BUILD_JOBS build arg controls C++ build parallelism (defaults to nproc). Override it in docker/.env.local if a full-parallel build overwhelms your machine. (my Macbook Air reaches 50°C normally doing a all 8-core build 😭)

Host detection

.devcontainer/host-env.sh runs as the devcontainer's initializeCommand, before the container starts. It writes docker/.env (loaded by compose) from three sources, in order:

docker/.env.defaults - committed defaults (ports, etc.)
docker/.env.local - your machine-only overrides, gitignored

Host/GPU detection - DISPLAY, VGL_DISPLAY, and SIM_GPU_RUNTIME based on uname and whether nvidia-smi + the nvidia Docker runtime are both present

VariableSet byPurpose
DISPLAYhost-env.shHost display to forward (local or TCP)
VGL_DISPLAYhost-env.sh (macOS/Windows)VirtualGL's in-container 3D X server
SIM_GPU_RUNTIMEhost-env.shnvidia or empty - feeds runtime: in compose
WAYLAND_DISPLAY, XDG_RUNTIME_DIRdocker-compose.ymlWayland socket passthrough for Vulkan (Chrono)

Display architecture

.devcontainer/x_server.sh runs as postStartCommand, after the container is up, and picks the cheapest option that works:

SituationWhat happens
Host Wayland socket (/run/host-runtime) reachableUsed directly - no extra services started
Host X11 socket (/tmp/.X11-unix) reachableUsed directly - no extra services started
Neither reachableFalls back to a self-contained stack: Xorg/Xvfb + Openbox + x11vnc + noVNC

Remote displays (XQuartz/VcXsrv)

Over TCP it additionally starts a headless VirtualGL 3D X server on :88 - that's what vglrun (used automatically by sim gazebo) renders against, so Gazebo/RViz get a real GL context even though the host X server can't provide one. See Docker setup for the per-OS walkthrough.

When falling back to VNC, the backend depends on available hardware:

  • Xorg with the NVIDIA driver on desktop GPUs
  • vkms for a virtual DRI3-capable display when no GPU is present
  • Xvfb + EGL on Jetson/Tegra (no /dev/dri)

Chrono's Vulkan rendering uses the Wayland socket when present, independent of this X11 path.

Container user

Runs as non-root trickfire (uid 1000) with passwordless sudo, so files created in the bind-mounted repo stay owned by your host user.

Extending the container

Edit docker/Dockerfile (or docker/vsg-chrono.Dockerfile for the VSG/Chrono base) to add system packages.

Rebuild with Dev Containers: Rebuild Container.

Python packages for simulation-time use go in the pip3 install line in the GAZEBO section; dev-only tools go in DEV TOOLING.

Useful CLI commands

Gazebo camera position

To set the camera position in the Gazebo viewer:

Terminal
$ gz service -s /gui/move_to/pose \
    --reqtype gz.msgs.GUICamera \
    --reptype gz.msgs.Boolean \
    --timeout 2000 \
    --req "pose: {position: {x: 0.0, y: -2.0, z: 2.0} orientation: {x: -0.2706, y: 0.2706, z: 0.6533, w: 0.6533}}"

To read the current camera position:

Terminal
$ gz topic -e -t /gui/camera/pose

ROS 2 inspection

Terminal
# List all topics
$ ros2 topic list

# See joint states in real time
$ ros2 topic echo /joint_states

# List active controllers
$ ros2 control list_controllers

# Check controller manager status
$ ros2 control list_hardware_interfaces

Building a single package

Terminal
$ cd gazebo
$ colcon build --packages-select arm_description
$ source install/setup.bash

Community apt repository (Dev Container)

Some ROS packages live in the universe repository rather than main. If apt can't find a package inside the container:

Inside devcontainer
$ apt-get update
$ apt-get install -y software-properties-common
$ add-apt-repository -y universe
$ apt-get update

Note

The Dockerfile already enables universe for the packages that need it.

Common pitfalls

On this page