状态:草稿(2026-10-18),实测。 测量时间为 2026-10-17 23:55 至 2026-10-18 00:45 (KST),在私有实验室 Kubernetes 的临时 Pod 中进行。 节点为私有实验室的一台 GPU 节点(AMD Ryzen 7 系列,16 线程),但未显式使用 GPU。飞行 Pod 由模拟器容器(CPU 上限 3.5 核,内存 3 GiB)和运行 MAVSDK 的
python:3.12-slim边车容器(上限 0.5 核、1 GiB)组成,编译 Pod 的 CPU 上限设为 4 核、内存 4 GiB。 所用版本为预制镜像 PX4 v1.18.0-beta1-660(61907962c9,2026-09-11)、源码编译版 PX4-Autopilot main 93579610c6(2026-10-16)、Gazebo Sim 8.15.0(Harmonic)以及 MAVSDK-Python 3.17.4。 飞行各进行了一次,因此表中的数值均为单次测量结果。原始数据(两个 ULog 文件、摘要 JSON、控制台日志)位于工作目录measure/sim-px4/中,Pod 在测量结束后均已被删除。
目录
- 1. 直觉:将真正的飞行软件装进虚拟的躯壳
- 2. 选择镜像:直接飞行还是修改后编译
- 3. 矩形任务飞行
- 4. 源码编译版:仅通过 shell 命令飞行
- 5. 遇到的六个问题
- 6. MAVLink 不监听 14550 端口
- 7. 局限性
- 8. 下篇预告
- 参考
1. 直觉:将真正的飞行软件装进虚拟的躯壳
SITL(Software In The Loop,软件在环)是一种将无人机飞行软件 原封不动 地在计算机上运行、仅由模拟器替代传感器和电机的方案。 在 PX4 看来,这与真实飞行器无法区分,因此无需实体机即可测试任务规划、应急动作和地面站连接。 如果说第 1 部分的轮式机器人在 ROS 2 上运行,那么这一次,飞行控制器本身成为了测试对象。
2. 选择镜像:直接飞行还是修改后编译
| 镜像 | 压缩后大小 | 下载时间 | 包含内容 | 用途 |
|---|---|---|---|---|
px4io/px4-sitl-gazebo:latest |
656 MiB | 32.5 s | 已编译的 PX4 1.18 + Gazebo Harmonic、启动脚本 | 快速试飞、测试任务与地面站 |
px4io/px4-dev:v1.18.0-rc1 |
1.77 GiB | 2 分 3.9 秒 | Ubuntu 24.04 编译工具链(不含仿真工具) | 修改并编译 PX4 代码 |
如果使用预制镜像通过 HEADLESS=1 在无界面环境下启动,不到 25 秒即可打印出“Startup script returned successfully”。
开发镜像的体积接近三倍,且需要手动安装 Gazebo 及仿真依赖,因此如果没有修改代码的需求,使用预制镜像更为合适。
3. 矩形任务飞行
由于模拟器容器将 MAVLink 发送到同一 Pod 的 localhost:14540 ,边车容器中的 MAVSDK 会从 udpin://0.0.0.0:14540 接收数据。
任务设定为:起飞至 10 米,按顺时针方向飞往北侧和东侧边长 40 米的 4 个矩形航点(速度 5 m/s,采用飞越模式),完成后返航(RTL)并降落。
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()
(摘自 TT Lab 实验代码 fly_square.py 。 item() 是用于生成 MAVSDK 3.x 中 15 个参数的 MissionItem 的小函数。)
| 项目 | 数值 |
|---|---|
| 起飞指令 → 达到 9.5 m | 12.9 s |
| 任务(AUTO_MISSION) | 41.0 s |
| 返航·降落(AUTO_RTL) | 20.6 s |
| 从连接到解锁解除 | 79 s |
| 最大高度 | 10.03 m |
| 任务期间高度误差 平均值 / 最大值 | 0.048 / 0.189 m |
| 矩形轨迹偏差(cross-track) 平均值 / RMS / p95 / 最大值 | 0.256 / 0.328 / 0.657 / 0.722 m |
| 距航点最近距离(WP1–4) | 0.80, 0.70, 0.62, 0.00 m |
| 降落位置(相对于起点) | 0.055 m,降落判定:真 |
| 最大横滚 / 俯仰 | 19.1° / 18.4° |
| 模拟器容器资源 | CPU 62.8 s / 经过时间 215 s(平均 0.29 核),memory.peak 205 MiB |
轨迹偏差是指 EKF 估计的 vehicle_local_position 偏离起点与四个航点连线段的距离。
航点内切 0.6–0.8 米被认为是由于“飞越”设置和到达判定半径(NAV_ACC_RAD 10 m)导致角被稍微切小了(推测)。
该图通过标准库直接读取 ULog(28 MB)绘制,降落位置 0.055 m 以及各阶段耗时与分析摘要保持一致。
模拟器的电池被设置为在 60 秒内降至 50%,因此从 100% 降至 50%、电压从 16.2 V 降至 15.3 V。这并不代表真实的电池消耗。
4. 源码编译版:仅通过 shell 命令飞行
从开发镜像中获取 PX4-Autopilot(--depth 1,含子模块,耗时 117–121 秒,大小 1.9 GiB)并进行编译。这一次没有使用 MAVSDK,仅通过 PX4 shell 命令完成飞行。
shell 输入通过 FIFO 传入。
param set NAV_DLL_ACT 0 # 지상국이 없어도 이륙을 거부하지 않게
param set MIS_TAKEOFF_ALT 10
commander takeoff
listener vehicle_local_position -n 1
commander land
| 项目 | 数值 |
|---|---|
| 安装依赖(Gazebo Harmonic + 仿真依赖) | 157 s |
HEADLESS=1 make px4_sitl gz_x500 PX4_MAKE_ARGS=-j2 编译后启动 |
182 s |
| 维持解锁状态 | 37.4 s |
| 最大高度 | 10.04 m |
| 悬停期间最大水平晃动 | 0.092 m |
| 运行中内存 | anon 156 MiB, gz sim RSS 209 MiB, px4 RSS 21 MiB |
commander takeoff 后 20 秒读取到高度 10.015 米,并在 commander land 后以“Disarmed by landing”结束。无需地面站或 Python,仅凭飞行控制器即可完成测试。
5. 遇到的六个问题
- MAVSDK 4 的 API 更改。 最新的 mavsdk 4.0.6 改为使用原生绑定,无法调用
mavsdk.mission。我们将其固定在了mavsdk<4(3.17.4)。旧方式的后续包名为 mavsdk-grpc。 - Gazebo 依赖必须先于编译安装。 如果在没有 Gazebo 的情况下配置过一次构建目录,CMake 缓存中会残留“Gazebo simulation dependencies not found”,导致后续即使安装了 Gazebo,
gz_x500也会失败。应先安装依赖再进行编译。 - OpenCV。
gz-harmonic如果仅安装,optical_flow 插件将找不到 OpenCV。必须完整安装 PX4 的ubuntu.sh所列出的精简仿真依赖列表(新增 211 个包,171 MB,39 秒)。 - 并行编译 OOM 与
PX4_MAKE_ARGS. 如果不指定并行数,ninja 会使用节点核心数 (16) + 2 个任务进行编译,导致在 4 GiB 内存上限下触发 OOMKilled。 然而,PX4 Makefile 通过终端(-j)读取ps T,因此在没有终端的nohup执行中,make -j2可能无法传递。我们通过将PX4_MAKE_ARGS=-j2作为 make 变量传递解决了这个问题。 - ULog 路径。 源码编译的日志不在
build/px4_sitl_default/rootfs/log/,而是在build/px4_sitl_default/rootfs/fs/log/<날짜>/*.ulg。 - 守护进程模式的输出。
px4 -d如果以(守护进程)启动,px4-<모듈>客户端命令(例如mavlink status)的输出不会流向该命令的终端,而是进入服务器日志。结果是从 px4 日志中读取的。
顺便一提,用 pyulog 读取 ULog 时,如果对时间戳(uint64)直接做减法会导致负数溢出。必须先转换为浮点数再做减法。
6. MAVLink 不监听 14550 端口
地面站(QGroundControl)通常在 UDP 14550 端口等待连接。因此很容易让人产生“必须打开 SITL 容器的 14550 端口”的想法,但实测结果恰恰相反。
- 在常规 Kubernetes Pod 中查看
mavlink status时,会发现第一个实例为UDP (18570, remote port: 14550),广播已关闭,且日志中打印了“MAVLink only on localhost”。 - 容器内的 UDP 套接字表(
/proc/net/udp)中只有 18570、14580、14280、13030 等端口, 并没有绑定 14550 端口的套接字。 PX4 并不是监听 14550 端口的一方,而是向 14550 发送 数据的一方。 - 镜像的启动脚本仅在
host.docker.internal能够解析为 IPv4 时,才会在所有mavlink start行中附加-t <그 주소>(针对 Docker Desktop)。在普通 Pod 中由于无法解析该名称,因此未做任何更改。 - 通过 Pod 的 hostAliases 将该名称解析为 127.0.0.2 后,所有五行都变成了
-t 127.0.0.2,在同一 Pod 中打开 0.0.0.0:14550 的接收器在 30 秒内收到了来自源端口 18570 的 10,004 个数据包。14540 端口则从源端口 14580 收到了 11,492 个数据包。 - 镜像配置中的
EXPOSE 14540/udp 14550/udp 8888/udp仅为元数据,并不代表会打开该端口。
综上所述,地面站应当持有 14550 端口,SITL 端没有理由绑定或发布 14550 端口。有报告称在 Docker Desktop 中使用 -p 14550:14550/udp 发布容器端口时,主机的 14550 可能会被容器端占用,导致地面站绑定失败,但在当前环境(containerd)中无法复现,故未作验证(未实测)。
7. 局限性
- 飞行各进行了一次。未引入风阻、传感器噪声、GPS 丢失等条件。
- 轨迹偏差是通过 EKF 估计位置计算的。按照第 1 部分的经验,应当与模拟器的真实位置(
vehicle_local_position_groundtruth,已同步记录在 ULog 中)进行对照,但在本文中尚未进行比较。 - 地面站连接(QGroundControl)与 Docker Desktop 端口冲突的情况未进行实测。
8. 下篇预告
接下来的 第 5 篇将重新回到乒乓机器人,讨论在第 2 部分小球模型上运行的经典控制基准算法的击球成功率。
TT 寄语。 飞行控制器是真实的,而躯壳是虚拟的。因此,你可以不用担心摔坏飞机,放心地无数次测试矩形飞行。另外别忘了,地面站端口应该由地面站掌握,而不是无人机!
参考
所有链接均已于 2026-10-18 验证可正常访问。
- PX4 文档(Gazebo 仿真): https://docs.px4.io/main/en/sim_gazebo_gz/
- PX4-Autopilot 仓库: https://github.com/PX4/PX4-Autopilot,相关 Issue: https://github.com/PX4/PX4-Autopilot/issues/29292
- PX4 容器镜像: 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 格式: https://docs.px4.io/main/en/dev_log/ulog_file_format.html
- 引用的代码与命令均为 TT Lab 实验代码。