Status: Draft (2026-10-18), Measured. Measurements were taken from 2026-10-17 23:55 to 2026-10-18 00:45 (KST) in a temporary pod on a home lab Kubernetes cluster. The node is a single home lab GPU node (AMD Ryzen 7 series, 16 threads), but no GPU was used. The flight pod was launched with a simulator container (capped at 3.5 CPU cores and 3 GiB of memory) and a sidecar container running MAVSDK
python:3.12-slim(capped at 0.5 CPU cores and 1 GiB), while the build pod was launched with a capped CPU of 4 cores and 4 GiB of memory. The software versions used are the pre-built image PX4 v1.18.0-beta1-660 (61907962c9, 2026-09-11), source-built PX4-Autopilot main 93579610c6 (2026-10-16), Gazebo Sim 8.15.0 (Harmonic), and MAVSDK-Python 3.17.4. Since each flight was conducted only once, the values in the table are single-measurement results. Raw data (two ULogs, summary JSON, console logs) are located in the working directorymeasure/sim-px4/, and all pods were deleted after the measurements.
Table of Contents
- 1. Intuition: Putting Real Flight Software into a Fake Body
- 2. Choosing an Image: Fly Immediately or Build and Modify?
- 3. Square Mission Flight
- 4. Source Build Version: Flying with Shell Commands Only
- 5. Six Issues Encountered
- 6. MAVLink Does Not Listen on Port 14550
- 7. Limitations
- 8. Next Up
- References
1. Intuition: Putting Real Flight Software into a Fake Body
SITL (Software In The Loop) runs the actual flight software destined for the drone as-is on a computer, while a simulator emulates only the sensors and motors. To PX4, it is indistinguishable from a real aircraft, allowing mission planning, emergency behaviors, and ground control station connections to be tested without physical hardware. While the wheeled robot in Part 1 moved on ROS 2, this time the flight controller itself is the test subject.
2. Choosing an Image: Fly Immediately or Build and Modify?
| Image | Compressed Size | Download Time | Contents | Usage |
|---|---|---|---|---|
px4io/px4-sitl-gazebo:latest |
656 MiB | 32.5 s | Built PX4 1.18 + Gazebo Harmonic, startup script | Quick test flights, mission and ground station testing |
px4io/px4-dev:v1.18.0-rc1 |
1.77 GiB | 2 m 3.9 s | Ubuntu 24.04 build tools (excluding simulation tools) | Modifying and building PX4 source code |
When launching the pre-built image headlessly with HEADLESS=1 , it printed "Startup script returned successfully" within 25 s. Because the development image is nearly three times larger and requires manually installing Gazebo and simulation dependencies, the pre-built image is preferable unless you need to modify the code.
3. Square Mission Flight
Since the simulator container sends MAVLink to localhost:14540 in the same pod, MAVSDK in the sidecar container receives it at udpin://0.0.0.0:14540 . The mission consists of a 10 m takeoff, 4 square waypoints with 40 m sides pointing North and East (speed 5 m/s, fly-through), followed by Return-to-Launch (RTL) and landing.
corners_ne = [(SIDE, 0), (SIDE, SIDE), (0, SIDE), (0, 0)]
wps = [offset(lat0, lon0, n, e) for n, e in corners_ne]
await drone.mission.set_return_to_launch_after_mission(True)
await drone.mission.upload_mission(MissionPlan([item(a, b) for a, b in wps]))
await drone.action.set_takeoff_altitude(ALT)
await drone.action.arm()
await drone.action.takeoff()
(Excerpt from TT Lab experiment code fly_square.py . item() is a small function that generates the 15 arguments for MAVSDK 3.x MissionItem .)
| Metric | Value |
|---|---|
| Takeoff command → Reached 9.5 m | 12.9 s |
| Mission (AUTO_MISSION) | 41.0 s |
| Return and Land (AUTO_RTL) | 20.6 s |
| From connection to disarm | 79 s |
| Maximum altitude | 10.03 m |
| Altitude error during mission (Average / Maximum) | 0.048 / 0.189 m |
| Square cross-track error (Average / RMS / p95 / Maximum) | 0.256 / 0.328 / 0.657 / 0.722 m |
| Closest distance to waypoints (WP1–4) | 0.80, 0.70, 0.62, 0.00 m |
| Landing position (relative to home) | 0.055 m, landing judgment true |
| Maximum Roll / Pitch | 19.1° / 18.4° |
| Simulator Container Resources | CPU 62.8 s / Elapsed 215 s (Average 0.29 cores), memory.peak 205 MiB |
Cross-track error measures how far the EKF-estimated vehicle_local_position deviated from the line segments connecting the home point and the four waypoints. The waypoints being passed 0.6–0.8 m on the inside is presumed (estimated) to be due to the "fly-through" setting and the acceptance radius (NAV_ACC_RAD 10 m) cutting the corners slightly. The figure was rendered by directly reading the ULog (28 MB) using a standard library, and the landing position of 0.055 m and phase-by-phase timestamps matched the analysis summary.
The simulator's battery is configured to drop to 50% over 60 s, decreasing from 100% to 50% and 16.2 V to 15.3 V. This does not reflect actual battery consumption.
4. Source Build Version: Flying with Shell Commands Only
We cloned PX4-Autopilot from the development image (--depth 1, including submodules, 117–121 s, 1.9 GiB), built it, and this time flew it using only PX4 shell commands without MAVSDK. Shell inputs were passed via a FIFO.
param set NAV_DLL_ACT 0 # 지상국이 없어도 이륙을 거부하지 않게
param set MIS_TAKEOFF_ALT 10
commander takeoff
listener vehicle_local_position -n 1
commander land
| Metric | Value |
|---|---|
| Dependency installation (Gazebo Harmonic + simulation dependencies) | 157 s |
HEADLESS=1 make px4_sitl gz_x500 PX4_MAKE_ARGS=-j2 Startup after build |
182 s |
| Hold arm time | 37.4 s |
| Maximum altitude | 10.04 m |
| Maximum horizontal jitter during hover | 0.092 m |
| Runtime memory | anon 156 MiB, gz sim RSS 209 MiB, px4 RSS 21 MiB |
commander takeoff After 20 s, an altitude of 10.015 m was read, and it ended with "Disarmed by landing" after commander land . Testing can be done using only the flight controller, without a ground station or Python.
5. Six Issues Encountered
- MAVSDK 4 API Changes. The latest mavsdk 4.0.6 switched to native bindings, preventing us from calling
mavsdk.mission. We pinned the version tomavsdk<4(3.17.4). The successor package for the old approach is named mavsdk-grpc. - Gazebo Dependencies Must Precede the Build. A build directory configured once without Gazebo leaves "Gazebo simulation dependencies not found" in the CMake cache, causing
gz_x500to fail even if Gazebo is installed later. Install dependencies first, then build. - OpenCV.
gz-harmonicInstalling onlyubuntu.shresulted in the optical_flow plugin failing to find OpenCV. We had to install PX4's minimal simulation dependency list as-is (211 packages, 171 MB added, 39 s). - Parallel Build OOM and
PX4_MAKE_ARGS. . Without specifying the parallel job count, ninja used the node's core count (16) + 2, causing an OOMKilled error under the 4 GiB memory limit. However, since the PX4 Makefile reads-jfrom the terminal (ps T),nohupmay not be propagated inmake -j2executions lacking a terminal. We resolved this by passingPX4_MAKE_ARGS=-j2as a make variable. - ULog Path. Logs from the source build were located in
build/px4_sitl_default/rootfs/log/rather thanbuild/px4_sitl_default/rootfs/fs/log/<날짜>/*.ulg. - Daemon Mode Output.
px4 -dWhen launched withpx4-<모듈>(daemon), the output of client commands (e.g.,mavlink status) goes to the server log rather than the command's terminal. The results were read from the px4 logs.
As a side note, when reading ULogs with pyulog, subtracting timestamps (uint64) directly causes an underflow on negative values. You must cast them to floating-point numbers before subtraction.
6. MAVLink Does Not Listen on Port 14550
Ground control stations (QGroundControl) typically listen on UDP 14550. This makes it easy to assume that "port 14550 on the SITL container must be opened," but actual measurements showed the opposite.
- Checking
mavlink statusin a standard Kubernetes pod reveals that the first instance isUDP (18570, remote port: 14550), broadcasting is disabled, and the log prints "MAVLink only on localhost." - The UDP socket table inside the container (
/proc/net/udp) contained only ports like 18570, 14580, 14280, and 13030, with no socket bound to 14550. PX4 is not the party listening on 14550, but rather the one sending to 14550. - The image's startup script appends
host.docker.internalto everymavlink startline only when-t <그 주소>resolves to an IPv4 address (for Docker Desktop). In a standard pod, the name does not resolve, so nothing was changed. - By configuring the pod's hostAliases to resolve that name to 127.0.0.2, all five lines changed to
-t 127.0.0.2, and a receiver opening 0.0.0.0:14550 in the same pod received 10,004 packets from source port 18570 over 30 s. For port 14540, it received 11,492 packets from source port 14580. - The
EXPOSE 14540/udp 14550/udp 8888/udpin the image configuration is merely metadata and does not open ports.
In summary, the ground control station must hold port 14550, and there is no reason for the SITL side to bind or publish 14550. There are reports that publishing container ports using -p 14550:14550/udp on Docker Desktop can cause the container side to occupy the host's 14550 and prevent the ground control station from binding, but we could not reproduce or verify this in our environment (containerd) (unmeasured).
7. Limitations
- Each flight was conducted only once. Conditions such as wind, sensor noise, and GPS dropouts were not introduced.
- Cross-track error was measured using the EKF's estimated position. Based on the lessons from Part 1, it should be evaluated alongside the simulator's true position (
vehicle_local_position_groundtruth, recorded together in the ULog), which has not yet been compared in this post. - Ground control station connectivity (QGroundControl) and Docker Desktop port conflicts remain unmeasured.
8. Next Up
The next post, Part 5, returns to the ping-pong robot and covers the return success rate of the classical control baseline run on the ball model from Part 2.
TT's Take: The flight controller is real, but the body is fake. That means you can fly square patterns over and over without worrying about crashing the aircraft. And don't forget: the ground control station port should be held by the ground station, not the drone!
References
All links were verified to be active when accessed on 2026-10-18.
- PX4 Documentation (Gazebo Simulation): https://docs.px4.io/main/en/sim_gazebo_gz/
- PX4-Autopilot Repository: https://github.com/PX4/PX4-Autopilot, Related Issue: https://github.com/PX4/PX4-Autopilot/issues/29292
- PX4 Container Image: https://hub.docker.com/r/px4io/px4-sitl-gazebo, https://hub.docker.com/r/px4io/px4-dev
- MAVSDK-Python: https://github.com/mavlink/MAVSDK-Python
- ULog Format: https://docs.px4.io/main/en/dev_log/ulog_file_format.html
- Cited code and commands are from TT Lab experimental code.