Gazebo Classic HITL 接入

本文记录 PX4 HITL runtime 接入步骤:真实飞控通过 USB 接入宿主机,Ubuntu VM 运行 Gazebo Classic,QGroundControl 通过 UDP 接收 telemetry 并下发 arm、mode、mission 命令。
该流程会让真实飞控进入可解锁状态。台架测试前必须移除桨叶,断开电机动力电源,并确认 QGroundControl 没有同时直连飞控 USB 和 HITL UDP 链路。

目标状态

项目目标
飞控PX4 FMU v6C 已刷入 HITL 固件,SYS_AUTOSTART=1001
USB宿主机独占飞控串口,并通过 TCP 转发给 Ubuntu VM
Gazebo Classic运行 hitl_iris.world,通过 /tmp/ttyPX4 与飞控闭环
QGroundControl通过 UDP 14550 连接 HITL vehicle
Health checkEKF 有姿态、位置和航向;普通 arm 命令返回 accepted

Runtime 拓扑

如果飞控 USB 可以直接出现在 Ubuntu VM 内,可以省略宿主机 serial bridge 和 socat,直接把 iris_hitl.sdfserialDevice 指向实际串口。Orb/VM 环境通常看不到真实 USB 串口,因此推荐使用上图的 TCP/PTY 桥接方式。

1. QGroundControl 设置

HITL runtime 中,QGroundControl 不应抢占飞控 USB。进入 Application Settings -> Comm Links
AutoConnect 项建议
Pixhawk关闭
SiK Radio关闭
LibrePilot关闭
UDP开启
Zero-Conf可开启
RTK关闭
NMEA GPSDisabled
确认 QGC 没有占用 USB:
lsof /dev/ttyACM0
如果宿主机串口名不是 /dev/ttyACM0,使用实际设备名。启动 HITL 后,QGC 应只通过 UDP 14550 连接 vehicle。

2. 准备 Gazebo Classic

Gazebo Classic HITL 使用 PX4 的 hitl_iris.worldiris_hitl model。需要先在 Ubuntu VM 中完成 Classic plugin 构建:
export PX4_DIR="$HOME/src/PX4-Autopilot"
cd "$PX4_DIR"
export PATH="$HOME/.local/bin:$PATH"
DONT_RUN=1 make px4_sitl_default gazebo-classic -j2
确认插件存在:
test -f "$PX4_DIR/build/px4_sitl_default/build_gazebo-classic/libgazebo_mavlink_interface.so"
test -f "$PX4_DIR/build/px4_sitl_default/build_gazebo-classic/libgazebo_motor_model.so"

3. 启动 HITL runtime

推荐使用文档仓库里的脚本启动桥接和 Gazebo:
export PX4_DIR="$HOME/src/PX4-Autopilot"
export ORB_VM="px4-hitl-classic-amd64"
export PX4_USB="/dev/ttyACM0"

./scripts/px4/start-gazebo-classic-hitl.sh
脚本默认启动:
进程作用
host serial bridge打开 $PX4_USB,监听 TCP 5760
VM socat连接宿主机 5760,创建 /tmp/ttyPX4
VM QGC relay监听 VM 14551,转发到宿主机 14551
host QGC relay监听宿主机 14551,转发到 QGC 14550
gzserver运行 hitl_iris.world
脚本不会修改 PX4 源码里的 model 文件,而是复制一份临时 model 到 /tmp/px4-hitl-models,并改两项:
<serialDevice>/tmp/ttyPX4</serialDevice>
<qgc_udp_port>14551</qgc_udp_port>

4. PX4 参数与模拟磁罗盘

进入 PX4 shell:
MAVLINK20=1 python3 "$PX4_DIR/Tools/mavlink_shell.py" udp:127.0.0.1:14550
确认并保存关键参数:
param set SYS_AUTOSTART 1001
param set SYS_HAS_MAG 1
param set COM_RC_IN_MODE 1
param save
reboot
不要用自写 MAVLink PARAM_SET 脚本写 int 参数。SYS_AUTOSTARTSYS_HAS_MAGCOM_RC_IN_MODE 都是 int 参数,错误的 float/bit 写法可能导致类似 1082130432 的异常值。推荐使用 PX4 shell,并且执行 param save
重启后,重新接 runtime,并手动启动模拟磁罗盘:
sensor_mag_sim start
sensor_mag_sim status
listener sensor_mag 1
期望看到:
sensor_mag_sim status
running

TOPIC: sensor_mag
device_id: ... SIMULATION:1
为什么需要这一步:
现象原因处理
Found 0 compassSYS_HAS_MAG=1 但没有健康 sensor_mag启动 sensor_mag_sim
No heading referenceEKF 缺航向参考保持 SYS_HAS_MAG=1 并启动模拟磁罗盘
XY_POSITION_CONTROL health=falseEKF 水平位置/速度融合未稳定等待 HITL 数据稳定,检查模拟磁罗盘和 GPS
No manual control input没有 RC/JoystickCOM_RC_IN_MODE=1 使用 MAVLink/Joystick;自动任务不依赖 RC

5. 验证可飞状态

用 MAVLink 直接检查 health 和 arm:
/usr/bin/python3 - <<'PY'
from pymavlink import mavutil
import time

m = mavutil.mavlink_connection("udp:127.0.0.1:14550", source_system=243, source_component=201)
m.wait_heartbeat(timeout=8)

end = time.time() + 8
last_sys = None
last_est = None
while time.time() < end:
    msg = m.recv_match(type=["SYS_STATUS", "ESTIMATOR_STATUS", "STATUSTEXT"], blocking=True, timeout=1)
    if not msg:
        continue
    if msg.get_type() == "SYS_STATUS":
        last_sys = msg
    elif msg.get_type() == "ESTIMATOR_STATUS":
        last_est = msg
    elif msg.get_type() == "STATUSTEXT":
        print("STATUSTEXT", msg.severity, repr(msg.text))

print("SYS_STATUS", last_sys)
print("ESTIMATOR_STATUS", last_est)

m.mav.command_long_send(
    m.target_system,
    m.target_component,
    mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
    0,
    1,
    0,
    0,
    0,
    0,
    0,
    0,
)

end = time.time() + 7
while time.time() < end:
    msg = m.recv_match(type=["COMMAND_ACK", "STATUSTEXT", "HEARTBEAT"], blocking=True, timeout=1)
    if not msg:
        continue
    print(msg)
PY
成功时应看到:
COMMAND_ACK {command : 400, result : 0, ...}
HEARTBEAT ... base_mode : 189 ...
result : 0 表示普通 arm 成功。base_mode 包含 armed 位后,QGroundControl 顶部应从 Not Ready 转为可执行状态。

6. 常见问题

QGC 已连接但仍显示 Not Ready

先不要只看历史消息列表。使用 arm 命令抓最新拒绝原因:
/usr/bin/python3 - <<'PY'
from pymavlink import mavutil
import time

m = mavutil.mavlink_connection("udp:127.0.0.1:14550")
m.wait_heartbeat(timeout=8)
m.mav.command_long_send(m.target_system, m.target_component, mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, 0, 1, 0, 0, 0, 0, 0, 0)

end = time.time() + 8
while time.time() < end:
    msg = m.recv_match(type=["COMMAND_ACK", "STATUSTEXT"], blocking=True, timeout=1)
    if msg:
        print(msg)
PY
如果只看到 Resolve system health failures first,继续解 SYS_STATUS 位图,重点看 3D_MAGRC_RECEIVERXY_POSITION_CONTROL

QGC 不显示 vehicle

检查链路计数是否增长:
lsof -nP -iUDP:14550 -iUDP:14551
应看到:
QGroundControl ... UDP *:14550
Python         ... UDP 127.0.0.1:14551
VM 内检查:
ss -lunp | grep 14551
ps -eo pid,comm,args | grep -E 'gzserver|socat|python3' | grep -v grep

飞控重启后链路断开

飞控重启会让 USB CDC 重新枚举,host serial bridge、VM socat 和 Gazebo 都可能握着旧句柄。按顺序重启:
pkill -f px4-hitl-bridge || true
orb -m "$ORB_VM" bash -lc "pkill -f 'PTY,link=/tmp/ttyPX4' || true; pkill -x gzserver || true; rm -f /tmp/ttyPX4"
./scripts/px4/start-gazebo-classic-hitl.sh
然后重新执行:
sensor_mag_sim start
sensor_mag_sim status

EKF2_MAG_TYPE invalid, resetting to default

EKF2_MAG_TYPE=0 是默认 Automatic,可用于 HITL。看到该消息时重点检查是不是之前用错误方式写过 int 参数。如果 param show COM_RC_IN_MODE 出现巨大整数,应通过 PX4 shell 修复:
param set COM_RC_IN_MODE 1
param save
reboot

7. 收尾

停止 runtime:
pkill -f px4-hitl-bridge || true
pkill -f px4-qgc-host-relay || true
orb -m "$ORB_VM" bash -lc "pkill -f qgc-relay-vm || true; pkill -f 'PTY,link=/tmp/ttyPX4' || true; pkill -x gzserver || true"
HITL arm 成功只代表台架链路、参数和模拟闭环可工作。实机飞行前仍需完整传感器校准、方向检查、安全开关、failsafe、任务边界和场地检查。