Week 6 (optional): PX4 SITL, Gazebo and uXRCE-DDS
Take off, for real.
A ROS 2 action server that takes a PX4 quadcopter off to 5 m, and an action client that asks it to. The drone is PX4 v1.17.0 itself, flying the x500 in Gazebo Harmonic, reached through the Micro XRCE-DDS Agent exactly as the PX4 guides set it up, on an 8 GB M1 in Docker. Then the link between them, DDS-XRCE, rebuilt in pure Python, flies the same takeoff with no ROS 2 at all.
- simulator
- PX4 v1.17.0 SITL, gz_x500 in Gazebo Harmonic, headless
- bridge
- Micro XRCE-DDS Agent v2.4.3 on UDP 8888, px4_msgs v1.17.0
- action
/takeoffand/land: goal, 10 Hz feedback, result, cancel, in offboard or takeoff mode- from scratch
- DDS-XRCE: a dissector and a client in pure Python
- 5.03 mheld after the 5 m goal by PX4's estimate; Gazebo's true pose says 5.06 m
- 3.98 srise time, 10 to 90 percent, on a 1 m/s profile
- 0.06 movershoot; settled within 0.25 m 5.8 s after lift off
- 0.09 mRMS between PX4's altitude estimate and Gazebo's true pose, over the whole flight
- 65 topicsPX4 created on the agent, dissected byte by byte
The takeoff, replayed.
Recorded off ROS 2 by flight_recorder while ros2 launch ugs_px4 sitl.launch.py flew PX4 in Gazebo: PX4's estimate, the server's setpoints, the action's feedback and every log line. Pick a flight. The orange line is where the server asked PX4 to be, the white one is where PX4 says it was, and the blue one, when Gazebo's pose was recorded, is where it really was.
- phase
- waiting
- PX4 mode
- altitude
- 0.00 m
- setpoint
- climb rate
- 0.0 m/s
- armed
- no
| Flight | Result | Peak | Rise | Max climb | Overshoot | Settled | Hover RMS | Drift | Landed |
|---|---|---|---|---|---|---|---|---|---|
| Offboard, 5 m | succeeded | 5.06 m | 3.98 s | 1.21 m/s | 0.06 m | 5.8 s | 0.046 m | 0.07 m | yes |
| Offboard, 5 m, reference on a fixed clock | succeeded | 4.99 m | 2.40 s | 2.57 m/s | 0.00 m | 4.5 s | 0.047 m | 0.06 m | yes |
| Takeoff mode, 5 m | succeeded | 5.00 m | 4.40 s | 1.27 m/s | 0.00 m | 6.3 s | 0.045 m | 0.05 m | yes |
| Offboard, 10 m at 2 m/s | succeeded | 9.98 m | 4.17 s | 2.12 m/s | 0.00 m | 6.1 s | 0.058 m | 0.11 m | yes |
| Canceled 4 s after lift off | canceled | 4.53 m | n/a s | 1.06 m/s | 0.00 m | n/a s | n/a m | 0.04 m | yes |
| Pure Python over XRCE, 5 m | succeeded | 5.01 m | 4.03 s | 1.22 m/s | 0.01 m | 6.7 s | 0.038 m | 0.05 m | yes |
Takeoff lab.
The action server's logic and the fake PX4 its launch tests fly against, running in your browser: a JavaScript port of takeoff.py and model.py that produces the same trace as the Python, bit for bit, on all 9 scenarios the tests check. Break PX4 and watch what the server does.
DDS-XRCE, by hand.
PX4 does not speak DDS. It asks the agent, in DDS-XRCE, to create a participant, then 65 topics with their writers and readers, in eProsima's binary representations. week06/xrce.py decodes and encodes all of it from the specification; week06/dissect.py rebuilt this session from a capture of the 5 m flight, and week06/xrce_client.py uses the same code to log in to the agent itself.
What PX4 created, and what carried data.
From the whole flight's capture: 324 kbit/s from PX4 and 33 kbit/s to it, over 197 objects. Rates are per second of wall clock; the M1 ran Gazebo below real time for part of this flight, so in PX4's own time they are higher (sensor_combined runs at 100 Hz).
| Topic | Type | PX4 | QoS | Samples | Rate | Bytes |
|---|---|---|---|---|---|---|
/fmu/out/sensor_combined | SensorCombined | publishes | best effort, transient_local | 2,797 | 75.7 Hz | 48 |
/fmu/out/vehicle_attitude | VehicleAttitude | publishes | best effort, transient_local | 2,795 | 75.7 Hz | 49 |
/fmu/out/vehicle_odometry | VehicleOdometry | publishes | best effort, transient_local | 2,792 | 75.6 Hz | 114 |
/fmu/out/vehicle_global_position | VehicleGlobalPosition | publishes | best effort, transient_local | 1,400 | 37.9 Hz | 70 |
/fmu/out/vehicle_local_position_v1 | VehicleLocalPosition | publishes | best effort, transient_local | 1,400 | 37.9 Hz | 216 |
/fmu/out/vehicle_gps_position | SensorGps | publishes | best effort, transient_local | 849 | 23.0 Hz | 163 |
/fmu/out/timesync_status | TimesyncStatus | publishes | best effort, transient_local | 63 | 1.7 Hz | 44 |
/fmu/out/vehicle_control_mode | VehicleControlMode | publishes | best effort, transient_local | 61 | 1.7 Hz | 23 |
/fmu/out/vehicle_status_v1 | VehicleStatus | publishes | best effort, transient_local | 61 | 1.7 Hz | 80 |
/fmu/out/failsafe_flags | FailsafeFlags | publishes | best effort, transient_local | 55 | 1.5 Hz | 91 |
/fmu/out/mode_completed | ModeCompleted | publishes | best effort, transient_local | 42 | 20.8 Hz | 10 |
/fmu/out/vehicle_land_detected | VehicleLandDetected | publishes | best effort, transient_local | 38 | 1.0 Hz | 20 |
/fmu/out/estimator_status_flags | EstimatorStatusFlags | publishes | best effort, transient_local | 34 | 0.9 Hz | 98 |
/fmu/out/battery_status_v1 | BatteryStatus | publishes | best effort, transient_local | 29 | 0.8 Hz | 176 |
/fmu/out/position_setpoint_triplet | PositionSetpointTriplet | publishes | best effort, transient_local | 19 | 0.5 Hz | 272 |
/fmu/out/vehicle_command_ack | VehicleCommandAck | publishes | best effort, transient_local | 4 | 0.1 Hz | 25 |
/fmu/out/home_position_v1 | HomePosition | publishes | best effort, transient_local | 4 | 0.2 Hz | 60 |
/fmu/out/manual_control_setpoint | ManualControlSetpoint | publishes | best effort, transient_local | 1 | n/a | 68 |
/fmu/out/transponder_report | TransponderReport | publishes | best effort, transient_local | 1 | n/a | 86 |
/fmu/in/trajectory_setpoint | TrajectorySetpoint | subscribes | best effort, volatile | 471 | 20.0 Hz | 64 |
/fmu/in/offboard_control_mode | OffboardControlMode | subscribes | best effort, volatile | 466 | 19.7 Hz | 16 |
/fmu/in/vehicle_command | VehicleCommand | subscribes | best effort, volatile | 3 | 0.1 Hz | 56 |
| and 43 more topics PX4 created that carried nothing in this flight | ||||||
- 4.97 m
python -m week06 xrce fly: the takeoff server's state machine, fed by this repo's XRCE client instead of rclpy. PX4 held it after the 5 m goal, then landed, with no ROS 2 installed in the loop.- 2,368 messages
- The pure Python client's session for that flight: log in, 7 topics with their readers and writers, then setpoints at 20 Hz and telemetry back.
- 61 types
- px4_msgs definitions PX4 bridges by default, parsed by week 5's IDL code and encoded with its CDR, so samples decode to PX4's own field names.
Seven hops from a goal to a motor.
What happens between takeoff_client asking for 5 m and the x500's rotors spinning up. Every hop is something that has to be right before the drone leaves the ground.
- takeoff_clientsends a
ugs_interfaces/action/Takeoffgoal: altitude, climb rate, method. It gets feedback at 10 Hz and the result, and can cancel. - takeoff_serveraccepts or rejects the goal (altitude 1 to 120 m, one goal at a time), then runs the state machine at 20 Hz: wait for PX4, preflight, stream setpoints, switch to offboard, arm, climb a trapezoidal profile, settle.
- ROS 2 topics
/fmu/in/offboard_control_mode,/fmu/in/trajectory_setpointand/fmu/in/vehicle_commandout;/fmu/out/vehicle_status_v1,vehicle_local_position_v1,vehicle_land_detectedandvehicle_command_ackin, subscribed best effort or they never match. - Micro XRCE-DDS Agentowns the DDS participant, readers and writers PX4 asked for. To ROS 2 they look like any other node's; to PX4 they are objects in an XRCE session on UDP 8888.
- uxrce_dds_clientPX4's module: turns XRCE samples into uORB messages and back. PX4 1.16 and later suffix versioned messages, hence
_v1. - commander and the controllersrefuse offboard until setpoints stream, refuse to arm until the preflight checks pass, fall back to hold one second after the setpoints stop, and fly the position setpoints through the multicopter position and attitude controllers.
- gz_bridge and Gazebosend motor commands to the simulated x500 and sensor data (IMU, barometer, magnetometer, GPS) back to PX4's estimator, in lockstep.
The problems you will hit.
The brief warns that this is not straightforward. The first nine are the ones this repo hit, in the order it hit them; the last four are PX4 rules it was built around, each broken on purpose by the tests. Every fix is in the code, and in docs/week06/NOTES.md.
- The disk fills up
no space left on devicefrom Docker and the shell, partway through the image build.Budget about 10 GB: the finished image is 8.2 GB, most of it Gazebo Harmonic's packages and PX4's build. The image deletes PX4's object files after linking, and the PX4 clone is shallow with shallow submodules. - PX4's setup script cannot upgrade numpy inside the ROS image
ERROR: Cannot uninstall numpy 1.26.4, RECORD file not found. Hint: The package was installed by debian.RunTools/setup/ubuntu.shwithPIP_IGNORE_INSTALLED=1, so its Python packages install into /usr/local beside Debian's instead of trying to replace them. - PX4 will not arm without a ground station
Preflight Fail: No connection to the GCSin the PX4 log, andVehicleStatus.pre_flight_checks_passstays false forever.The x500 airframe setsNAV_DLL_ACT 2(return home if the ground station link drops), which also makes a ground station link an arming requirement. With no QGroundControl running, setNAV_DLL_ACT 0.sitl.launch.pydoes it through the running PX4 (px4-param set NAV_DLL_ACT 0); passgcs:=trueto leave it alone when QGroundControl is connected. - PX4_PARAM_NAV_DLL_ACT=0 does nothingPX4's startup script accepts
PX4_PARAM_<name>overrides, but withPX4_PARAM_NAV_DLL_ACT=0the parameter still reads 2.The override runs before the airframe file. Setting a parameter to its firmware default (0) leaves it marked as default, so the airframe's laterparam set-default NAV_DLL_ACT 2wins. Set it after startup instead: the launch waits untilpx4-param compare NAV_DLL_ACT 2succeeds, then sets 0. - The topic you subscribed to is silent
ros2 topic echo /fmu/out/vehicle_statusprints nothing, althoughros2 topic listshows plenty of/fmu/outtopics.PX4 1.16 and later suffix versioned messages with their version: it is/fmu/out/vehicle_status_v1andvehicle_local_position_v1(alsobattery_status_v1,home_position_v1and others;ros2 topic listtells you). The server subscribes to both names, so it works with older firmware too. - Subscriptions match nothing at allThe suffixed topic exists,
ros2 topic infoshows a publisher, and the callback still never runs.PX4's writers are best effort (the capture in RESULTS.md shows every/fmu/outwriter's QoS); a default, reliable ROS 2 subscription is incompatible and silently never matches. Subscribe withqos_profile_sensor_data, as the PX4 guide says, orros2 topic echo --qos-reliability best_effort. Week 5 measured exactly this: 0 of 20 messages. - The climb sits still, then racesThe first SITL flight armed, then sat on the ground for 3.2 s while the server's setpoint climbed away to 2.5 m; PX4 then chased it at 2.6 m/s, more than twice the 1 m/s the goal asked for, with 1.2 m RMS tracking error.After arming PX4 spools the motors up for
COM_SPOOLUP_TIME(1 s) and ramps its takeoff overMPC_TKO_RAMP_T(3 s). A reference on a fixed clock runs ahead of all that. The server's reference now stops whenever it is 0.4 m ahead of the vehicle (max_lead). In the final flights: 2.57 m/s and 1.22 m RMS with the reference on a fixed clock (--max-lead 0reproduces it), 1.21 m/s and 0.30 m RMS with the governor. Both are in RESULTS.md. - Gazebo's topics are invisible from a second process
gz topic -llists nothing, and the ground truth recorder receives no poses, although Gazebo is running.PX4 starts Gazebo withGZ_IP=127.0.0.1, and gz-transport only discovers peers on the same interface. ExportGZ_IP=127.0.0.1in every process that talks to Gazebo (week06/truth.pysets it itself). - The setpoint stream stalls on a busy executorIn the launch tests the fake PX4 dropped out of offboard into hold one second after accepting it, with nothing wrong in the logs; the server's arming retries came 1.8 s apart instead of 1 s.The 20 Hz loop was an rclpy timer on the MultiThreadedExecutor, beside 50 Hz telemetry and two action servers, and went 0.4 to 1.2 s between calls: longer than PX4's 1 s offboard loss failsafe. Giving the timer its own callback group did not help. Three changes, each found by a failure: the loop runs in a thread of its own (rclpy publishers are thread safe) and is never more than 0.3 s late; PX4's telemetry gets its own node on a SingleThreadedExecutor in another thread, after GitHub's runner let it fall 2 s behind; and liveness is judged by the 50 Hz local position (2 s), not
vehicle_status_v1, which SITL left silent for 2.5 s while Gazebo ran at half speed. All six launch tests now pass with four CPU-burning processes competing on four cores. - PX4 refuses offboard mode
VehicleCommandAckforDO_SET_MODEcomes backTEMPORARILY_REJECTED, and the vehicle stays in hold.PX4 only accepts offboard whileOffboardControlModeis already streaming (at least 2 Hz). The server streams a hold setpoint from the moment PX4 answers, and asks for offboard a second later. - The drone drops back to hold after the goal succeedsThe action reports success at 5 m, then PX4 switches to
AUTO_LOITERa second later.Losing the offboard stream forCOM_OF_LOSS_T(1 s) is a failsafe. The server keeps streaming the last setpoint between goals, from its 20 Hz timer, not from the goal's thread. The launch tests and the takeoff lab drop the stream on purpose to check the server reports it. - Setpoints in the wrong frameA setpoint of
z = 5drives the vehicle into the ground.PX4 is NED: z points down, so 5 m up isz = ground_z - 5, where the ground is wherever z was while landed (0.09 m in SITL, not 0). ROS is ENU; the server converts at the boundary and the action speaks metres up. - Takeoff mode wants an AMSL altitude
NAV_TAKEOFFwithparam7 = 5climbs to the wrong height or not at all.param7 is metres above mean sea level. The server sendsref_alt(the local origin's AMSL altitude fromVehicleLocalPosition; 0.24 m in PX4's default Gazebo world) minus the ground's z plus the goal, with NaN latitude, longitude and yaw to take off in place.
Read it, run it.
Everything runs in one image, ugs-px4:jazzy, built from ros2_ws/px4/Dockerfile on top of week 5's: PX4 v1.17.0 built for SITL, Gazebo Harmonic, px4_msgs and the agent. On Ubuntu 24.04 with ROS 2 Jazzy, the PX4 guides' own steps work just as well; sitl.launch.py only needs PX4_DIR.
- ros2_ws/src/ugs_px4: the action server, client, fake PX4, recorder and launch files.
- week06/README.md: the write up.
- docs/week06/RESULTS.md: every number on this page.
$ python -m week06 docker build
$ python -m week06 fly # 5 m, hover, land
$ python -m week06 fly --method takeoff_mode
$ python -m week06 fly --xrce # no ROS 2
$ ros2 launch ugs_px4 sitl.launch.py altitude:=5
$ ros2 run ugs_px4 takeoff_client 5 --land
$ ros2 launch ugs_px4 takeoff.launch.py fake:=true