One container stack for three machines. It flies the Chimera flight code against PX4 SITL and Gazebo Harmonic, it runs the real vehicle's companion computer on the aircraft, and it runs the fielded ground station on the base station laptop. The flight code from 5g_drone and MAVInsight is the same code in all three, on the same ROS domains, ports and frame names.
.env says which of the three worlds this machine is. COMPOSE_PROFILES says
what runs and UAS_BASE says which world the numbers belong to.
docs/front-doors.md works one example for each world.
docs/uas-contract.md states what a simulated vehicle must present. The
simulator satisfies that contract. The flight code does not accommodate the
simulator. The diagram below is the simulator, which is the world with the
most parts.
┌──────────────────┐
│ sim │ Gazebo Harmonic, one world
│ PX4 SITL x N │ cameras, gimbal, rangefinders
└──┬────────────┬──┘
MAVLink │ │ H.265
▼ ▼
┌──────────────┐ ┌──────────────┐
│ uas11..uas19 │ │ video-router │ RTSP, one path per stream
│ mavlink- │ └───┬──────┬───┘
│ router │ │ │
└──┬────────┬──┘ │ │
│ │ │ │
▼ └─────────┼──┐ │
┌────────────┐ │ │ │
│ onboard11 │◄───────────┘ │ │
│ ..onboard14│ ds_node, MAVROS, MAVInsight
└─────┬──────┘ │ │
domain │ 99 │ │
▼ ▼ ▼
┌───────────────┐ ┌───────┐
│ offboard │ │ qgc │
│ ground-router │ └───────┘
└───────────────┘
the ground station, every vehicle at once
| Service | Contents | Profile |
|---|---|---|
sim |
Gazebo Harmonic, one PX4 SITL instance for each vehicle, the camera encoders | sim |
uas11 to uas19 |
mavlink-router, one container for each vehicle | from UAS_FLEET |
onboard11 to onboard14 |
The companion computer: MAVROS, ds_node, MAVInsight |
from UAS_FLEET |
offboard |
The simulated ground station for the whole fleet, plus its ground-router |
offboard |
video-router |
MediaMTX. Every camera enters here and leaves as RTSP | sim, offboard |
qgc |
QGroundControl v5.0.8, released build | sim |
ground |
The real ground station on t500: the offboard image on the host network, beside the native mavlink-router and lcam | ground |
onboard |
The aircraft: the companion image on the host network, beside the native mavlink-router and rcam | aircraft |
scenegen |
Builds a scene from map data. Runs on demand and exits | scenegen |
xrce-agent |
The uXRCE-DDS bridge, for a stack that needs px4_msgs |
xrce |
A vehicle is one machine: the companion container shares its router's network
namespace, so the router reaches MAVROS on loopback, as it does on the Orin.
The ground station is one machine too, so offboard holds one MAVROS for each
vehicle and ground-router shares its namespace. On the real machines the
container shares the host's namespace instead. The aircraft's native router
pushes to 127.0.0.1:14402. The ground station's router pushes each vehicle to
its own 127.0.0.<N>:14402.
UAS_BASE in .env says which world the numbers belong to: 10 numbers a
simulated fleet from uas11, 0 the real one from uas1. COMPOSE_PROFILES says
what runs. ./px4sim refuses a .env where the two disagree.
.env says which machine this is. .env.example holds the three pairs of
COMPOSE_PROFILES and UAS_BASE, one for each machine.
docs/front-doors.md gives the whole command line for
each world, and for every other door into this system.
The simulator, on one laptop, with COMPOSE_PROFILES=sim,offboard and
UAS_BASE=10:
./px4sim doctor # check the driver, docker, GPU runtime and X11
./px4sim prepare # download dependencies and build images once online
./px4sim setup # clone PX4 into ./src and build the scenes
./px4sim start # start local images without building or pullingThe real ground station on t500, with COMPOSE_PROFILES=ground, UAS_BASE=0
and GROUND_DOMAIN=60:
./px4sim doctor # also lcam, mavlink-router, and the host ports 14402 and 8765
./px4sim prepare # prepare dependencies and ground images once online
./px4sim start # launch the prepared images, including offlineThe aircraft, with COMPOSE_PROFILES=aircraft and UAS_BASE=0, after
chimera-deploy/remote/deploy_onboard.sh wrote its .env:
./px4sim doctor # also rcam, its sockets, the clock, the power mode and the lens
./px4sim prepare # prepare dependencies and arm64 images once online
./px4sim start # launch onboard from local imagesThe first build on the Orin compiles the whole ROS workspace at 15 W and takes
tens of minutes. Cached builds after a flight-code change are short enough to
run on every start. Until user is in group docker there, every call needs sudo:
sudo -E env HOME=/home/user UAS_NUM=1 ./px4sim restart. The boot unit does not
need that group.
Each machine builds its own architecture from its own checkout. No machine copies an image from another.
./px4sim ui can show one-second per-vehicle bandwidth for all traffic on the
vehicle IPs, including MAVLink, ROS/DDS, video, and Foxglove. This is separate
from the normal stack setup and is not required to run PX4Sim.
Install the host packet-capture tool:
sudo apt install tcpdumpPX4Sim starts a filtered, read-only capture with sudo -n, so configure a
narrow sudo rule for the operator running PX4Sim. Replace user with the
actual login name:
sudo visudo -f /etc/sudoers.d/px4sim-tcpdumpAdd:
user ALL=(root) NOPASSWD: /usr/bin/tcpdump
Validate it before opening the UI:
sudo -n tcpdump -i any -n -q -c 1
./px4sim uiThe capture is filtered to the configured vehicle addresses, uses one parser
thread, and does not transmit packets. If permission is not configured, the
UI still runs but bandwidth remains unavailable (↓- / ↑-).
To stop it:
./px4sim stopThe first ./px4sim restart builds PX4 inside the sim container. That takes 10 to
20 minutes and happens once, because the build output lands in
./src/PX4-Autopilot on the host. Watch it with ./px4sim logs sim.
The onboard and offboard images build 5g_drone, MAVInsight and cdcl_umd_msgs
from local checkouts under ROS2_WS_DIR/src. Each package builds in its own
stage, so a Python edit does not rebuild messages or the other Python package.
./px4sim restart builds source changes with networking disabled
before stopping the existing containers. ./px4sim build builds without
restarting; ./px4sim restart --no-build restarts existing images only.
Run ./px4sim prepare online when first setting up a machine or deliberately
changing dependencies. It creates local ROS and CUDA dependency images and
prepares the selected services. Keep those images and the Docker build cache.
Models remain on their mounted volume and need no image rebuild. See
the build and offline workflow for details.
./px4sim with no arguments prints every command, and names which of the three
worlds this machine is. It is the front door: it reads .env, resolves the
DeepStream release for this GPU, and resolves the world origin from the scene
and the scenario. The Makefile keeps the same target names and forwards each
one to it.
A joystick plugged into the host works in QGroundControl. The container mounts
/dev/input and joins the host input group, whose id ./px4sim doctor writes
into .env as INPUT_GID. Calibrate it once under Vehicle Setup, Joystick. If
the Joystick page does not appear, restart the qgc service with the controller
already plugged in.
N is the UAS number, 11 to 19 for a simulated vehicle.
| What | Address |
|---|---|
| MAVLink, to the ground station | udp://<gcs>:14550 + N, so 14561 for uas11 |
| MAVLink, to the companion | udp://127.0.0.1:14402, inside the vehicle |
| MAVLink, for scripts | tcp://localhost:5761 upwards |
| Video | rtsp://localhost:8554/rgb11, /pilot11, /thermal11. RTSP_HOST_PORT moves the host side |
| Low-rate video | the same names with l appended: rgbl11 |
| Foxglove, ground station | ws://localhost:8765 |
| Foxglove, one vehicle | ws://localhost:8771 for uas11 |
The real fleet, N 1 to 9, with UAS_BASE=0:
| What | Address |
|---|---|
| MAVLink, to the ground station | udp://10.200.142.60:14550 + N, so 14551 for uas1, into the native router |
| MAVLink, to MAVROS | udp://127.0.0.1:14402 on the aircraft. On the ground the native router gives each vehicle udp://127.0.0.<N>:14402 |
| MAVLink, for scripts | tcp://127.0.0.1:5760, this machine's native router |
| Low-rate video, on the ground | rtsp://127.0.0.1:8554/rgbl1 from lcam, pilotl1, thermall1 |
| Video, on the aircraft | rtsp://127.0.0.1:8554/rgb from rcam, and rgbl, pilot, pilotl, thermal, thermall |
| Foxglove | ws://localhost:8765 on each machine. From the ground, ws://10.200.142.61:8765 is uas1's |
./px4sim status prints this list for the fleet that is running.
In ./px4sim ui, select the UGV casualty image VLM filter entry in the Stack
menu to choose off, shadow or on. The choice is saved
in .env and restarts the local stack; the header shows the selected setting.
The same action is available as ./px4sim vlm-filter off|shadow|on. Shadow mode
records the filter decisions while forwarding every image and clip. With filtering
on or in shadow mode, start and restart launch the installed host scorer
in tmux session vlm_gate and wait up to 60 seconds for readiness. Install its
venv and weights first, following 5g_drone/scripts/vlm_gate/README.md.
The scorer stays running when filtering is off or the stack stops; it can be
reused on the next start. Scorer failures during a run pass images through. In the
Vehicle and Stream menus, v continues to play the selected camera.
./px4sim help is the one truthful list, and it names the world this machine
is. These work in all three worlds:
./px4sim ui # the console: what is running, and a key for every command
./px4sim restart # stop, build, and start what .env selects
./px4sim stop # stop everything
./px4sim status # what is running, and the addresses
./px4sim logs onboard11 # follow one service. `logs --since 5m ground` reads back
./px4sim streams # which video streams are live
./px4sim view 1 # play a vehicle's gimbal camera
./px4sim topics 1 # a vehicle's ROS topics
./px4sim layout # render the Foxglove layout for this fleet
./px4sim shell ground # a shell in any container
./px4sim check # validate the compose file and lint the docs
./px4sim clean # remove containers, networks and volumesAsk one vehicle what its nodes make of the world. On a real fleet the ground station answers for the vehicle, and each command reaches the machine that holds the answer:
./px4sim uas 1 status # mode, arming, position, battery, GPS, gimbal
./px4sim uas 1 gimbal -30 # degrees below the horizon
./px4sim uas 1 detect on # continuous detection, off until asked
./px4sim uas 1 detections # what it found, and where that landed
./px4sim uas 1 heading # which way the vehicle, its camera and its footprint point
./px4sim capture 1 mosaic # mosaic, vlm or fiducial
./px4sim zoom 1 wide # a v3 lens: narrow, mid or wide
./px4sim probe 1 # what its ROS graph carries right now
./px4sim probe ground # the same, on the ground station
./px4sim foxglove 1 # what a panel is offered, over Foxglove's own protocolThe simulator adds the commands that fly it and maintain it:
./px4sim uas 11 takeoff 40 # arm, takeoff, goto, land
./px4sim fly 11 20 # respawn, then climb
./px4sim console # the pxh> prompt. Detach with Ctrl-P Ctrl-Q
./px4sim scene uroc # change the world and restart the simulator
./px4sim scenario <name> # change the targets and reload what scores them
./px4sim fleet add # fly one more vehicle. `fleet remove 13` retires one
./px4sim snap rgb11 # one frame of a stream, to look atWith UAS_BASE=0 a vehicle number is a real aircraft, so ./px4sim refuses
every command that flies the simulator: core, fly, place, fiducial,
reset, px4, console, snap, and uas N arm, takeoff, land and
goto. Nothing is sent. scene, scenario and genscene stay: a scene is
map data, and the ground station builds and selects one the way the simulator
does. An aircraft refuses those three and reads the scene it is given. It also
refuses
the commands that maintain the simulator, and each of those says what it did
not change: setup, clean-src, nuke, fleet add and fleet remove.
./px4sim help holds that list under "The simulator alone", and it names the
world this machine is. verify is not refused: it says for itself which of
its stages a real machine can answer. router is refused too, because the real
fleet's router is native: systemctl status mavlink-router.
./px4sim ui draws the stack in the terminal and runs these commands for you.
It shows the containers, every vehicle, the video paths and the Foxglove ports,
and it reads them again about every two seconds.
Move between the panes with tab, and pick a row with the arrow keys. Press
enter for what can be done to that row, or press the key beside an action.
Press : to type any px4sim command, and ? to see every action with the
command it runs. Press esc to stop a command, and q to leave.
In the simulator, press = to fly one more vehicle and - to retire the
selected one. Both write UAS_FLEET in .env and bring the stack to the new
fleet.
The console offers the keys this world accepts, and no others. On a ground
station and on an aircraft the eighteen actions that fly or maintain the
simulator are not in the menus, not on a key and not in ?. A ground station
and an aircraft also get a NATIVE row, which is systemctl is-active for the
services this machine boots beside the containers: lcam and
mavlink-router on the ground, rcam, mavlink-router and onboard on the
aircraft. Every bridge of the real fleet holds port 8765, so the FOXGLOVE row
names each one by its machine.
The console starts nothing of its own. Every action runs ./px4sim ..., so
what it does is what the prompt does. It is a way to reach this script, not a
second one.
Each reading comes from the thing itself, never from a log message:
| Reading | Where it comes from |
|---|---|
| A container | The container engine's own state |
| A video path | The video router API in the simulator, with the bytes that path carried since the report before. One probe by name on a real machine, where lcam and rcam serve no API |
| A vehicle | MAVLink on tcp://localhost:5761 upwards, or on the native router's 5760 for the real fleet: mode, arming, battery, GPS, height and gimbal |
| A Foxglove bridge | A connection to the port, opened and closed |
| A native service | systemctl is-active, on a ground station and on an aircraft |
| The GPU | nvidia-smi: what the cameras, the encoders and the detector are using. A Jetson has no nvidia-smi, and the field is empty |
./px4sim state prints the same picture as JSON, and ./px4sim state --watch
keeps it coming, one object for each line. The console reads that stream, and
so can a script.
UAS_FLEET is a list, and the place in the list is the vehicle number. The
first entry is uas11, the second uas12, and so on.
./px4sim fleet # what flies now
./px4sim fleet add # one more of the last airframe
./px4sim fleet add chimera_v2 # one more, of this airframe
./px4sim fleet remove # the last vehicle
./px4sim fleet remove 12 # this vehicleEach of these writes .env, removes the containers the fleet no longer holds,
and reloads the world, so every vehicle respawns. With nothing running it
writes the file and says so. fleet on its own reads the fleet in every world.
fleet add and fleet remove belong to the simulator, and a real machine
refuses both and says to edit UAS_FLEET by hand.
A vehicle taken from anywhere but the end moves every vehicle after it down
one, which gives each of them other frames, ports, ROS domains and stream
names. The command says so and stops. Add --renumber to say that this is
what you want.
The simulator flies nine vehicles at the most, uas11 to uas19. compose.yaml
holds a companion for uas11 to uas14 only, so a fifth vehicle flies with a
router and no ROS stack until you add an onboard15 service.
./px4sim verify # every stage
./px4sim verify help # what the stages are
./px4sim verify airframes # one of themairframes, contract, units and localize need nothing running.
vehicle, flight, ground, fleet, captures and foxglove read the
running stack and say so if it is not up. fleet and captures are skipped
without VERIFY_FULL=1. What each one measured last is in
docs/development.md.
A real machine runs the stages its own world can answer, and says which those
are. A ground station runs ground and foxglove, because it holds no vehicle
of its own and reads the picture it rebuilt from the radio. An aircraft runs
units, vehicle and foxglove, because it is the vehicle: it runs the
companion and serves its own bridge. The other stages expand the simulator's
airframes or fly a vehicle, so verify refuses them by name. The last
simulator run passed 71 checks, the last ground run 9, and the last aircraft
run 12 of 13.
Start a subset by naming the profiles:
./px4sim restart "" # the vehicles and QGC, with no ground station
./px4sim restart offboard # the default. ./px4sim adds sim itself in the simulatorThe routers and the companions come from UAS_FLEET, so they need no profile
name here.
The aircraft world and the ground world ran on the real hardware for the first time on 2026-09-03, and uas1 started the stack by itself from a cold boot on 2026-09-04. Nothing armed and nothing flew. The evidence is component tests on uas1 and on t500.
Verified on the aircraft, uas1. onboard.service starts the container
after docker, rcam, the native router and a clock step, and it reaches
active (exited) in 39 seconds. MAVROS connects to PX4 v1.18 and reads
capabilities=321791. The preview publishes at 27.4 Hz from the rcam NVMM
socket, and ds_node reports pipelines PLAYING 57 seconds after the
container starts. The detector runs on the GPU and publishes at 1.03 to 1.06
Hz, at 99% GR3D and 15.9 W. The SCF4 lens reaches a framing in about one
second. The gimbal publishes its state and its attitude at 4 Hz. The engines
deserialize from perception_models/orin, with no rebuild. 46 ROS nodes come
up and none restarts. The front door refuses all seven flight commands.
./px4sim verify runs the aircraft's own stages and passes 12 of 13: the one
failure is the gimbal rangefinder, which is not fitted yet.
The cold boot, uas1, 2026-09-04. onboard.service was enabled but had
never run at a boot: an ordering cycle through the aircraft's own
mavlink-router.service made systemd delete its start job every time, and the
unit was only ever proved with systemctl start. With the cycle broken, a
power cycle brings the stack up unattended. systemd reports no ordering cycle,
the clock steps before the container, and the unit is active 38 seconds after
power on. user is now in group docker, so ./px4sim also runs from a
prompt without sudo.
Verified on the ground station, t500. The ground profile runs one
container on the host network, beside the native lcam and mavlink-router. MAVROS
on domain 60 reads uas1 as connected. The air bridges carry 16 topics on domain
99 and 2 on domain 69. ./px4sim streams lists all three lcam mounts.
./px4sim layout selects the live chimera_real.json front door and renders
it for uas1. The simulation world selects chimera_sim.json instead.
./px4sim verify checks every topic and service the selected layout uses
against the live bridge.
What a bench cannot prove. A forward camera means no ray meets the ground.
The live VLM capture did return a rangefinder localization, but terrain-based
target locations, the mosaic map, and a fiducial survey still need a downward
view or flight. The dedicated PX4 range sensors are silent on this vehicle:
drone_lidar_200m, drone_lidar_6m and gimbal_lidar_50m are advertised but
PX4 sends no DISTANCE_SENSOR on any id. They are a vehicle fit, not a stack
fault.
Open items.
| Item | What it needs |
|---|---|
| The drone password | Rotate it. It is out of the chimera-deploy tree and still in that repository's history on GitHub |
| The branches | Current real-drone changes use feature/real-fixes in px4-sim-stack, 5g_drone, MAVInsight and chimera-deploy. The drone reads them from the mirrors in /srv/git before they reach GitHub |
| The thermal mount | The Boson disconnected from USB after repeated protocol errors while rcam stayed active. Inspect hub power, cabling and bandwidth, then re-enumerate the camera; restarting rcam alone cannot recover an absent USB device |
ROS_DOMAIN_ID in a shell |
The entry points export it into PID 1 alone, so a hand docker exec shell joins domain 0. Every front-door command passes the domain itself |
| Dead bridge files | 5g_drone still carries eight per-vehicle domain bridge files that no launch file reads. Delete them or keep them |
| To change | Do this | Read |
|---|---|---|
| The world | SCENE= in .env |
docs/development.md |
| The targets | Edit a file in modules/sim/scenes/scenarios/ |
docs/development.md |
| The airframe or its sensors | Edit modules/sim/scenes/models/chimera_v2 or chimera_v3 |
docs/development.md |
| The fleet | ./px4sim fleet add [model] and ./px4sim fleet remove [N], or UAS_FLEET= in .env |
docs/uas-contract.md |
| Which cameras each vehicle serves | UAS_STREAMS= in .env |
docs/troubleshooting.md |
| The zoom preset a v3 lens flies at | UAS_ZOOM= in .env |
docs/development.md |
| PX4 itself | Edit src/PX4-Autopilot, then restart sim |
docs/development.md |
| The flight code | Edit the tree at ROS2_WS_DIR, then rebuild |
docs/development.md |
| QGroundControl itself | ./scripts/bootstrap.sh qgc, then the qgc-dev profile |
docs/development.md |
| A scene from map data | ./px4sim genscene --help |
modules/scenegen/README.md |
| Component | Version | Reason |
|---|---|---|
| PX4 | v1.17.0 | Current stable, May 2026. |
| ROS 2 | Humble | What the aircraft runs. cdcl_umd_msgs does not decode across distributions, so the whole fleet is Humble and nothing here may choose otherwise. |
| Gazebo | Harmonic | The Gazebo release that PX4 v1.17 installs. |
| QGroundControl | v5.0.8 | The mature v5.0 line. Set QGC_REF in .env to move. |
| DeepStream | 7.1 | The last release on Ubuntu 22.04, which is what Humble needs, and what the Orin runs. On a GPU newer than its TensorRT, scripts/ds-select.sh installs a TensorRT that fits and changes nothing else. On a Jetson it installs the host's own TensorRT build. ./px4sim doctor explains. |
- Linux with an X11 session, where a window is wanted. Wayland works through XWayland, and is less tested. The aircraft runs with no display.
- An NVIDIA GPU, driver 535.183 or later, and
nvidia-container-toolkit. A Blackwell card needs 570.133 or later, for the TensorRT that can build engines for it. - Docker 24 or later with Compose v2.
- About 80 GB of free disk for a full build, and 16 GB of RAM.
./px4sim doctorwarns below either. Every vehicle costs GPU: a fleet of four renders ten cameras, encodes twenty streams and runs four detectors. chimera-deploybeside this checkout, with itsmavrosandanglessubmodules checked out.ros-basebuilds the PX4 v1.18 MAVROS patch from them.MAVROS_PATCH=0in.envremoves that need.- On the aircraft,
UAS_NUMin/etc/environment, whichchimera-deploy/deploy.shwrites.userin groupdockerfor interactive use, whichchimera-deploy/remote/deploy_onboard.shsets up. The boot unit carriesSupplementaryGroups=dockerand works without it. - On a real machine,
gst-discoverer-1.0fromgstreamer1.0-plugins-base-apps, which is how./px4sim streamsprobes a mount that serves no API.
make preflight checks all of it and says what is missing.
| File | Contents |
|---|---|
| docs/front-doors.md | Every door into this system, with the exact commands and one worked example for each world |
| docs/uas-contract.md | What a simulated vehicle must present. The specification |
| docs/architecture.md | Why the containers split where they do |
| docs/interfaces.md | What the simulator produces, and the frame conventions |
| docs/development.md | How to change PX4, the airframe, the scenes and the targets |
| docs/px4-simulated-gimbal.md | How the PX4 simulated gimbal behaves, and how to command it |
| docs/localization-error.md | Where a target position's error comes from, and which parts are floors |
| docs/localization-report.md | What the 2026-08-21 accuracy pass changed, and what it measured |
| docs/altitude-datums.md | Which altitude the ground is measured from, and the bias that came of it |
| docs/troubleshooting.md | What breaks, and what to do |