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
/takeoff and /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.

setpointPX4 estimateGazebo truth Bar: the action's phase.
0.0 s
phase
waiting
PX4 mode
altitude
0.00 m
setpoint
climb rate
0.0 m/s
armed
no
    Every flight, measured from its recording. Rise time is 10 to 90 percent of the goal; settled means inside 0.25 m from then on.
    FlightResultPeakRiseMax climbOvershootSettledHover RMSDriftLanded
    Offboard, 5 msucceeded5.06 m3.98 s1.21 m/s0.06 m5.8 s0.046 m0.07 myes
    Offboard, 5 m, reference on a fixed clocksucceeded4.99 m2.40 s2.57 m/s0.00 m4.5 s0.047 m0.06 myes
    Takeoff mode, 5 msucceeded5.00 m4.40 s1.27 m/s0.00 m6.3 s0.045 m0.05 myes
    Offboard, 10 m at 2 m/ssucceeded9.98 m4.17 s2.12 m/s0.00 m6.1 s0.058 m0.11 myes
    Canceled 4 s after lift offcanceled4.53 mn/a s1.06 m/s0.00 mn/a sn/a m0.04 myes
    Pure Python over XRCE, 5 msucceeded5.01 m4.03 s1.22 m/s0.01 m6.7 s0.038 m0.05 myes

    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.

    Method
    What goes wrong

      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).

          TopicTypePX4QoSSamplesRateBytes
          /fmu/out/sensor_combinedSensorCombinedpublishesbest effort, transient_local2,79775.7 Hz48
          /fmu/out/vehicle_attitudeVehicleAttitudepublishesbest effort, transient_local2,79575.7 Hz49
          /fmu/out/vehicle_odometryVehicleOdometrypublishesbest effort, transient_local2,79275.6 Hz114
          /fmu/out/vehicle_global_positionVehicleGlobalPositionpublishesbest effort, transient_local1,40037.9 Hz70
          /fmu/out/vehicle_local_position_v1VehicleLocalPositionpublishesbest effort, transient_local1,40037.9 Hz216
          /fmu/out/vehicle_gps_positionSensorGpspublishesbest effort, transient_local84923.0 Hz163
          /fmu/out/timesync_statusTimesyncStatuspublishesbest effort, transient_local631.7 Hz44
          /fmu/out/vehicle_control_modeVehicleControlModepublishesbest effort, transient_local611.7 Hz23
          /fmu/out/vehicle_status_v1VehicleStatuspublishesbest effort, transient_local611.7 Hz80
          /fmu/out/failsafe_flagsFailsafeFlagspublishesbest effort, transient_local551.5 Hz91
          /fmu/out/mode_completedModeCompletedpublishesbest effort, transient_local4220.8 Hz10
          /fmu/out/vehicle_land_detectedVehicleLandDetectedpublishesbest effort, transient_local381.0 Hz20
          /fmu/out/estimator_status_flagsEstimatorStatusFlagspublishesbest effort, transient_local340.9 Hz98
          /fmu/out/battery_status_v1BatteryStatuspublishesbest effort, transient_local290.8 Hz176
          /fmu/out/position_setpoint_tripletPositionSetpointTripletpublishesbest effort, transient_local190.5 Hz272
          /fmu/out/vehicle_command_ackVehicleCommandAckpublishesbest effort, transient_local40.1 Hz25
          /fmu/out/home_position_v1HomePositionpublishesbest effort, transient_local40.2 Hz60
          /fmu/out/manual_control_setpointManualControlSetpointpublishesbest effort, transient_local1n/a68
          /fmu/out/transponder_reportTransponderReportpublishesbest effort, transient_local1n/a86
          /fmu/in/trajectory_setpointTrajectorySetpointsubscribesbest effort, volatile47120.0 Hz64
          /fmu/in/offboard_control_modeOffboardControlModesubscribesbest effort, volatile46619.7 Hz16
          /fmu/in/vehicle_commandVehicleCommandsubscribesbest effort, volatile30.1 Hz56
          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.

          1. takeoff_clientsends a ugs_interfaces/action/Takeoff goal: altitude, climb rate, method. It gets feedback at 10 Hz and the result, and can cancel.
          2. 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.
          3. ROS 2 topics/fmu/in/offboard_control_mode, /fmu/in/trajectory_setpoint and /fmu/in/vehicle_command out; /fmu/out/vehicle_status_v1, vehicle_local_position_v1, vehicle_land_detected and vehicle_command_ack in, subscribed best effort or they never match.
          4. 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.
          5. uxrce_dds_clientPX4's module: turns XRCE samples into uORB messages and back. PX4 1.16 and later suffix versioned messages, hence _v1.
          6. 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.
          7. 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.

          1. The disk fills upno space left on device from 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.
          2. PX4's setup script cannot upgrade numpy inside the ROS imageERROR: Cannot uninstall numpy 1.26.4, RECORD file not found. Hint: The package was installed by debian.Run Tools/setup/ubuntu.sh with PIP_IGNORE_INSTALLED=1, so its Python packages install into /usr/local beside Debian's instead of trying to replace them.
          3. PX4 will not arm without a ground stationPreflight Fail: No connection to the GCS in the PX4 log, and VehicleStatus.pre_flight_checks_pass stays false forever.The x500 airframe sets NAV_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, set NAV_DLL_ACT 0. sitl.launch.py does it through the running PX4 (px4-param set NAV_DLL_ACT 0); pass gcs:=true to leave it alone when QGroundControl is connected.
          4. PX4_PARAM_NAV_DLL_ACT=0 does nothingPX4's startup script accepts PX4_PARAM_<name> overrides, but with PX4_PARAM_NAV_DLL_ACT=0 the 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 later param set-default NAV_DLL_ACT 2 wins. Set it after startup instead: the launch waits until px4-param compare NAV_DLL_ACT 2 succeeds, then sets 0.
          5. The topic you subscribed to is silentros2 topic echo /fmu/out/vehicle_status prints nothing, although ros2 topic list shows plenty of /fmu/out topics.PX4 1.16 and later suffix versioned messages with their version: it is /fmu/out/vehicle_status_v1 and vehicle_local_position_v1 (also battery_status_v1, home_position_v1 and others; ros2 topic list tells you). The server subscribes to both names, so it works with older firmware too.
          6. Subscriptions match nothing at allThe suffixed topic exists, ros2 topic info shows a publisher, and the callback still never runs.PX4's writers are best effort (the capture in RESULTS.md shows every /fmu/out writer's QoS); a default, reliable ROS 2 subscription is incompatible and silently never matches. Subscribe with qos_profile_sensor_data, as the PX4 guide says, or ros2 topic echo --qos-reliability best_effort. Week 5 measured exactly this: 0 of 20 messages.
          7. 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 over MPC_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 0 reproduces it), 1.21 m/s and 0.30 m RMS with the governor. Both are in RESULTS.md.
          8. Gazebo's topics are invisible from a second processgz topic -l lists nothing, and the ground truth recorder receives no poses, although Gazebo is running.PX4 starts Gazebo with GZ_IP=127.0.0.1, and gz-transport only discovers peers on the same interface. Export GZ_IP=127.0.0.1 in every process that talks to Gazebo (week06/truth.py sets it itself).
          9. 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.
          10. PX4 refuses offboard modeVehicleCommandAck for DO_SET_MODE comes back TEMPORARILY_REJECTED, and the vehicle stays in hold.PX4 only accepts offboard while OffboardControlMode is already streaming (at least 2 Hz). The server streams a hold setpoint from the moment PX4 answers, and asks for offboard a second later.
          11. The drone drops back to hold after the goal succeedsThe action reports success at 5 m, then PX4 switches to AUTO_LOITER a second later.Losing the offboard stream for COM_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.
          12. Setpoints in the wrong frameA setpoint of z = 5 drives the vehicle into the ground.PX4 is NED: z points down, so 5 m up is z = 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.
          13. Takeoff mode wants an AMSL altitudeNAV_TAKEOFF with param7 = 5 climbs to the wrong height or not at all.param7 is metres above mean sea level. The server sends ref_alt (the local origin's AMSL altitude from VehicleLocalPosition; 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.

          Dockerpython -m week06
          $ 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
          ROS 2 Jazzyros2_ws
          $ 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