Files
code-test-drone/docs/树莓派4B部署注意事项.md
keita df91b1110e 精简仓库结构并增强任务链路:归档双目可选后端、新增LOS判据与自定义返航
归档 legacy/:stereo_yolo/stereo_ranger/target_tracker 三包 + 175 条测试 + demo/配置/权重/三份规格;运行链路不再依赖,默认 pytest 446 条,归档测试按需运行。

目标有效性第二判据改为 LOS 运动学一致性(pod_bridge/los_consistency.py),删除 require_vision;pod_locked() 叠加该判据,状态机随之后退重搜。

自定义返航(task_runner/return_home.py):保持转场高度回起飞点 → 恢复初始航向 → 垂直降落,不再爬 RTL_RETURN_ALT;未起飞/无起飞点/超时/异常仍退回 PX4 原生 RTL。

任务链安全兜底:主循环异常→紧急返航、stop 幂等、landing_stop 保留遥测、in_air 流复用+超时、解锁/起飞与吊舱识别仅执行一次、rtl_timeout 上限。

入口共享件拆到 utils/sitl_support.py;pod_sitl_test.py→l1_sitl_test.py;删除旧架构 test.py;相机脚本移入 tools/。

新增 .gitignore 并剔除历史误跟踪的 59 个 .pyc;requirements 按实际依赖重写;文档全面对齐,新增 legacy/README.md 与 tools/README.md。
2026-10-21 14:48:41 +08:00

18 KiB

pod_bridge 移交树莓派 4B 部署注意事项

目标平台:树莓派 4B + Raspberry Pi OS 官方镜像(64 位,Bookworm / Trixie)。 (2026-09-23 由 Ubuntu 24.04 换回官方镜像:Ubuntu 镜像上串口配置与 libstdc++ 符号问题频出,官方镜像对 Pi 串口/固件支持最完整。) 范围:utils/pod_bridge(及其依赖的 visual_track_control 控制原语库)。 核对日期:2026-09-23,对照规格《光电吊舱控制协议G版-PB.docx》; 依赖轮子已按 PyPI 2026-09-23 上游信息复核。


1. 系统与 Python 版本(硬性)

  • 必须烧录 64 位镜像(Raspberry Pi Imager 里选 "Raspberry Pi OS (64-bit)"), 理由见 §3 MAVSDK 对 aarch64 的要求;32 位镜像不要碰。

  • 烧完先确认:cat /etc/os-release。Bookworm 自带 Python 3.11、 Trixie 自带 Python 3.13,均满足 PEP 604 标注(float | None / FrameParser | None)要求的 ≥3.10,直接用。

  • Bookworm 起系统 Python 受 PEP 668 保护,禁止 pip install 进系统环境 (报 externally-managed-environment),一律建 venv;venv 支持若缺失先 apt 补:

    sudo apt update && sudo apt install -y python3-venv python3-pip
    python3 -m venv ~/venv-pod
    ~/venv-pod/bin/pip install -r requirements-pod.txt
    

2. 依赖裁剪(树莓派上别全装)

requirements.txt 是全仓库清单,吊舱桥接只需要其中两个,务必裁剪, 否则 torch/cv2 在 Pi 4B 上是几百 MB 且毫无意义:

# requirements-pod.txt(桥接最小集)
mavsdk<4    # ⚠ 锁版本,原因见 §3
pyserial
pytest      # 仅上机自测需要,部署运行可省
  • pod_bridge 本身纯 Python + 惰性导入:pyserial 只在 PodSerial 实例化时加载, mavsdk 只在 Px4Gateway.connect / 指令发送时加载——单测全绿不需要任何硬件库。
  • 不要在 Pi 上装 ultralytics/torch/opencv:那是 stereo_yolo 检测链的依赖, Pi 4B 跑不动实时 YOLO,检测应留在机载算力更强的平台或吊舱内部。
  • 若确实要装 opencv,用 opencv-python-headless(无 GUI 依赖), 并 sudo apt install libopenblas-dev libatlas-base-dev。

3. MAVSDK 在 ARM 上的坑

  • mavsdk 4.x 是全新原生 API 重写,与经典 gRPC API 完全不兼容(2026-09-24 树莓派实测):没有 mavsdk.offboard / System / telemetry,本仓库全部 飞控代码(Px4Gateway / MavsdkCommandGateway / 各入口脚本)都是经典 API, 装 4.x 直接 LegacyModuleError。必须锁 mavsdk<4(最新 3.17.4, aarch64 官方 wheel manylinux2014_aarch64,PyPI 已核对); 32 位 armv7l 支持不稳定——这是必须 64 位镜像的主因。

  • 3.x 为 gRPC 架构,pip 包内嵌 mavsdk_server 可执行文件,不加载 libcmavsdk.so,没有 libstdc++ 符号问题——此前两系统上轮番出现的 undefined symbol: ..._M_futex_wait_until... 是 4.x 原生库专属坑, 换回 <4 后自然消失。

  • 安装后自检三步(缺一不可;单测全绿不证明 mavsdk 已装,测试已打桩脱钩):

    pip list | grep -i mavsdk           # 必须显示 3.x(如 3.17.4)
    python -c "from mavsdk import System; print('api OK')"
    find ~/venv-pod -name mavsdk_server # 应有输出;直接执行能起服务更佳
    
  • 连接串写法(pod_bridge.json 的 px4.connection_string):

    • Pi 串口直连飞控 TELEM:serial:///dev/ttyUSB0:57600(波特率按飞控侧设置);
    • 经数传/仿真:udpin://0.0.0.0:14540(本机 SITL 实测可用;udp://:14540 亦可,语义为监听该端口)。
  • PX4 侧需确认 TELEM 口已配 MAVLink 且 offboard 心跳未被占用。

4. 串口硬件(吊舱侧,最容易翻车)

  • 协议为 115200 8N1 无校验(规格 §2.1)。先确认吊舱串口电平:
    • RS232 → 必须加电平转换或直接用 USB 转串口模块(FTDI/CH340,推荐, 免折腾 GPIO UART,设备号 /dev/ttyUSB0);
    • TTL 3.3V → 可用 Pi GPIO14/15(/dev/serial0)。
  • 用 GPIO UART 时(官方镜像自带 raspi-config):
    • sudo raspi-config → Interface Options → Serial Port:控制台选 No、 串口硬件选 Yes(等价于 enable_uart=1 + 关闭 serial-getty 抢占);
    • Pi 4B 上 /dev/serial0 默认指向 mini-UART(ttyS0,波特率随主频漂移), 115200 下不稳——在 /boot/firmware/config.txt 加 dtoverlay=disable-bt 把 PL011(ttyAMA0)换回来,/dev/serial0 随之改指 ttyAMA0 (想保留蓝牙改用 dtoverlay=miniuart-bt);
    • 必须同时清掉内核控制台(2026-09-28 实测踩坑):dmesg 出现 printk: legacy console [ttyS0] enabled 说明内核 printk 也在往同一个口 吐日志,与吊舱协议流混在一起(表现为能收几帧后 OSError errno 5 或 持续坏帧)。编辑 /boot/firmware/cmdline.txt(单行文件,务必先备份) 删掉 console=serial0,115200 / console=ttyS0,115200 后重启;
    • 串口静默后三连验证(缺一不可):ls -l /dev/serial0→ttyAMA0、 ps aux | grep "[a]getty" 无串口 getty、dmesg | grep -i "console \[" 不再出现 ttyS0;
    • Pi 是 3.3V 电平,绝不直连 5V/RS232 电平,会烧 GPIO。
  • 接线三线:TX/RX 交叉、必须共地(GND),不共地是通讯乱码的第一大原因。
  • 用户权限:sudo usermod -aG dialout $USER,重登录生效,否则 open 串口 Permission denied。

5. 时钟同步(规格表3 依赖)

  • bridge._build_attitude 用宿主机 UTC 墙钟填表3 的年月日时分秒; Pi 4B 无 RTC,断电后时间是错的,吊舱会拿到错误时间戳。
  • 上机后确认 timedatectl 显示 System clock synchronized: yes; 无网环境考虑给 Pi 加 RTC 模块,或由飞控 GPS 授时(需改代码,见 §8 已知问题)。

6. 配置文件(utils/config/pod_bridge.json)上机必改两处

字段 改成
pod_serial.port 实际设备号,如 /dev/ttyUSB0(留空会 fail-fast 报错,这是刻意的)
px4.connection_string 实际飞控连接,如 serial:///dev/ttyUSB1:57600
  • 引导参数(guidance.*)全部是占位值,未经标定不得用于飞行决策; 首次实机只验证通讯收发,速度上限先调小。
  • 配置加载拒绝未知键——改 JSON 时注意拼写,多写的键会直接报错(防呆设计)。

7. 上机验证顺序(每步过了再下一步)

  1. 纯软件自测(无需任何接线): cd utils && ~/venv-pod/bin/python -m pytest tests/pod_bridge -q → 应 116 全绿 (全量 372 条;pod_bridge 含 sim_fc 的 21 条。单测已打桩 mavsdk 设定点类型, 与 libcmavsdk 原生库健康状态无关,也无需真实串口)。
  2. 串口回环:USB 转串口模块 TX/RX 短接,跑一个 feed/echo 小脚本验证字节收发。
  3. 只接吊舱:用仓库自带诊断脚本 python utils/pod_bridge/listen_pod.py /dev/ttyUSB0 --seconds 5 (加 --center 可同时下发归中 0x2B 验证下行);脚本自动统计帧数并判读, 遥测帧率 ≥48Hz(标称 60Hz 的 80%)退出码为 0 即链路正常。
  4. 验证吊舱能否解算目标坐标(不接飞控,避免绕过这一步): python -m pod_bridge.sim_fc /dev/ttyAMA0 --seconds 60 --json ~/pod_logs/phase1.json 由树莓派伪造 表3 喂给吊舱,看是否回 表4。这一步的结论直接决定后续架构取舍, 详见 docs/吊舱解算验证方案-模拟飞控喂表3.md。 ⚠ 跑它的时候关掉 listen_pod / pod_console / bridge —— 同一个串口不能两人占。
  5. 只接飞控:Px4Gateway.connect 后确认 own_state().position_valid 为 True。
  6. 全链路地面联试:电机不装桨,观察 offboard 心跳与引导输出。
  7. 最后才考虑带动力测试。

8. 已知问题处置状态(2026-09-23 更新)

代码审查(2026-09-22)发现 4 项,其中 3 项安全相关缺陷已于 2026-09-23 修复, 新增回归测试 8 条(pod_bridge 测试 84 → 92,全量 348 条全绿):

  1. Px4Gateway 四路遥测共用 OwnState.time_ms → 已修:OwnState 拆出 position_time_ms(仅 position 流刷新),引导的位置超龄判定改用它;
  2. 表2-2 锁定状态(bit2)未参与引导门控 → 已修:PodBridge._pod_locked 要求遥测在 link.telemetry_timeout_ms 内且 bit2=锁定,表4 坐标才视为有效 (该配置项由此从死配置变为生效配置);
  3. PodBridge.run 主循环无异常护栏 → 已修:单周期异常按「第 1 次 + 每满 100 次」告警并补发零速刹车保 offboard 心跳,任务不终止;
  4. 表3 时间戳用宿主机墙钟而非 GPS 时间(见 §5)——仍未修,属可接受风险, 如需精确授时按 §5 对策处理。

9. 常驻运行(systemd 示例)

# /etc/systemd/system/pod-bridge.service
[Unit]
Description=Pod-PX4 Bridge
After=network.target

[Service]
ExecStart=/home/<user>/venv-pod/bin/python -m pod_bridge_app
WorkingDirectory=/home/<user>/code_drone-test/utils
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
  • 热路径不打日志是设计使然(告警走 WarnThrottle),现场排查用 journalctl -u pod-bridge -f。
  • 代码热路径按 10Hz 引导 + 20Hz 姿态下发,Pi 4B 单核占用可忽略,无需实时内核。

10. 本设备实测特性(2026-09-28 厂商确认 + 2026-10-08 实测)

  • 本吊舱不含激光测距模块。后果:

    • 表2-2 激光测距字段恒为 0(无效),不可作为判据;
    • pod_bridge.json 的 link.require_laser_valid 必须为 false,否则引导 永久刹车(该闸门是为"有激光且锁定位置位粘滞"的机型准备的);
    • 🔴 无激光时吊舱不输出 表4(目标经纬度) —— 实测确认,2026-10-08。 详见 §10.0,这是本项目迄今最重要的架构性结论。
  • 🔒 厂商确认:该设备不允许二次开发(2026-10-09)。含义与影响:

    • 不能改吊舱固件/内部参数让它输出 表4,也不能让它接受外部距离输入;
    • 但 表2-1 控制指令仍然开放(指向、变焦、识别、跟踪、归中等我方本来就在发), 所以吊舱仍可作为传感器 + 执行末端正常使用;
    • → 实际损失只有"吊舱侧白送的目标坐标",而这部分本就该由我方自算 (见 §10.0 架构走向)。对本项目的整体影响小于预期。
    • 选型结论:不必为"能二次开发"换型号;若将来要吊舱侧定位,才需要换 带激光测距的型号(且要确认激光能接入其解算链路)。
  • 锁定位置位(表2-2 状态字节1 bit2)在目标丢失后不复位——实测观察; 表2-2 脱靶量是定长字段且协议没有无效标志位,丢失时表现为 0, 无法区分"不输出"与"输出 0"。 → 单靠 bit2 会在目标丢失后继续朝过期坐标飞,必须有第二判据(见 §8/§11)。

  • 主动解除锁定有效:发 0x0E 停止跟踪 后 bit2 复位为未锁定,厂商测试软件的 锁定框同步消失(2026-09-28 实测)。

  • ⚠ 0x0F「发现目标自动锁定」是常驻模式,不是一次性动作:0x0E 只停止这一次 跟踪,并不退出自动锁定——目标重新进入画面时吊舱会自动重新锁定,无需重发 0x07 / 0x0F / 0x10(实测结论,2026-09-28 纠正)。只有发了 0x08 目标识别关 才真正退出识别,此时才需要按 d / a 重新开启。

10.0 实测结论:无激光 → 表4 不输出(✅ 已钉死,2026-10-09)

决定性复测(2026-10-09,结论已坐实)

python -m pod_bridge.sim_fc /dev/ttyAMA0 --detect person --auto-lock \
    --seconds 60 --json ~/pod_logs/phase1.json
观测项 实测值 说明
表3 发送 1200 帧 / 20.0Hz 我方喂入正常
表2-2 遥测 3599 帧 / 59.98Hz 链路正常、无丢帧
锁定 锁定次数 1,首次 0.13s ✅ 确实锁定了(bit2 已置位),排除了"没锁定所以不解算"的猜测
表4 目标 0 帧 / 0.00Hz ❌ 持续锁定 60s,一帧都没发
回环/其他帧 0 / 0 接线正常,非自发自收

结论:吊舱在已锁定 + 表3 持续合法喂入的条件下,依然完全不输出 表4。

判读

吊舱的目标地理解算依赖激光测距——没有斜距就无法把视场角换算成地面坐标, 于是根本不发 表4(不是发 0、不是吐旧值)。此前"无激光时如何解算高度/是否 假设目标贴地"的疑问随之作废:它压根没解算。

🔴 硬证据:协议上根本没有"把距离喂给吊舱"的通道

这一条比实测更致命,它决定了连补救都没门:

  • 激光测距字段只存在于 表2-2(吊舱 → 控制器),是吊舱上报给我们看的;
  • 控制器 → 吊舱方向的两个帧——表2-1 控制帧、表3 载机姿态帧—— 都没有任何距离/测距输入字段(protocol.py:111 ControlCommand 与 protocol.py:248 CarrierAttitude 可逐字段核对)。

→ 即便我方 stereo_ranger 算出了精确距离,也无法通过本协议回灌给吊舱让它 去解算表4。外挂激光模块也帮不上(除非硬件直连吊舱的测距接口或换固件)。

两种"翻盘可能"的处置

此前保留的可能 现状
① 本次没真锁定,所以不解算 ❌ 已排除:实测首次锁定 0.13s,bit2 已置位
② 吊舱校验 表3 时戳/位置有效性 ❌ 无必要再测:本次喂的是真实 UTC 递增 + 合法经纬度,已经是它能拿到的最标准输入;--freeze-time 只会让输入更不合法,测了也证明不了什么

唯一还能翻盘的途径是问厂商一句话:该型号在无激光配置下,表4 是否设计上就 不输出?是否支持外挂测距接入?——在拿到答复前,按"表4 不存在"设计架构。

直接后果(务必读完再决定下一步):

受影响项 后果
guidance.py 地理位置引导律 表4 路径在本设备上恒不可用(target_valid 恒假)。现行做法:坐标走地面站 UDP 注入(ground_link → ground_target_fn),guidance 吃注入坐标;表4 仅作回退
目标距离/坐标来源 坐标 = 地面站注入(已落地)。距离尚未接入:规划由激光雷达提供(低算力);stereo_yolo 检测 → stereo_ranger 双目视距 因 Pi 4 跑不动实时 YOLO,暂不接入
--auto-unlock(§10.1) 必然误触发,已实测禁用:last_tgt_s 恒 None 被当成超时。见 §10.1
后续选型 若要保留吊舱侧目标定位,只能换带激光测距的吊舱型号(且需确认激光能接入吊舱解算链路)

架构该怎么办(2026-10-19 已定稿)

既然距离无法回灌、表4 不会有,目标地理坐标就必须来自吊舱之外。当时的设想是 "双目自算坐标";实际实现改成了更简单的分工:

目标**坐标**  ← 地面站 UDP 注入(ground_link)     ← 已落地
目标**方位**  ← 吊舱表2-2 脱靶量(角度直入 L1)     ← 已落地
目标**距离**  ← 待接激光雷达(串口,低算力)        ← 未接

吊舱在本设备上退化为:伺服稳定指向 + 表2-2 脱靶量/框架角传感器。 它仍然很有用(稳像、跟得住、给得出视场角),只是不再提供坐标。

下文"stereo_yolo → stereo_ranger 自算经纬度再喂 guidance"的方案 已被上述 分工取代:guidance.py 的输入改为注入坐标(已实现),不需要我方自算经纬度。 距离接入后,只影响控制律的 distance_valid 分支,不影响坐标来源。

复测命令(--json 后是输出产物路径,非预置输入文件,建议给绝对路径):

cd utils && mkdir -p ~/pod_logs
python -m pod_bridge.sim_fc /dev/ttyAMA0 --detect person --auto-lock \
    --seconds 60 --json ~/pod_logs/phase1.json

退出码 1 = 仍无表4。落盘内容结构见方案文档 §8.1.1。

10.1 自动解锁机制(🔴 本设备实测禁用,2026-10-08)

pod_console.py --auto-unlock SEC 现按「锁定中 + 表4 超过 SEC 秒未更新」触发 0x0E。 该判据在本设备上 100% 误触发,不要用:因为 §10.0 实测确认表4 根本不发, last_tgt_s 恒为 None,而 pod_console.py:232 的条件把 last_tgt_s is None 直接当成超时 → 一旦锁定满 SEC 秒就发 0x0E,并且每 SEC 秒重复一次(与"假锁定"形成持续震荡:解锁 → 自动锁定 → 解锁 → …)。

待确认项 现状(2026-10-08 更新)
本设备无激光,锁定后到底发不发 表4 ✅ 已实测:不发。判据前提不成立
表4 的实际发送频率 ❌ 无从谈起(无输出)
解锁后是否自动重发 0x0F/0x10 重新捕获 不需要(0x0F 常驻),但这反而让误解锁的震荡可以一直循环,所以更不能用

候选替代判据(按可靠性排序):

  1. 操作员按键 u(台架阶段推荐,零误杀);
  2. 表4 超时(需先确认表4 存在);
  3. 锁定持续超过 T 秒且从未收到表4 → 判为假锁定,解锁一次并告警(不循环);
  4. 脱靶量恒为 0 持续 T 秒 —— 居中跟踪时也会为 0,误杀风险高,不推荐;
  5. 由我方视觉链路(stereo_yolo / target_tracker)交叉验证 → 已实行为 LOS 运动学一致性 (pod_bridge/los_consistency.py,2026-10-19):无电机、刚性固连 → 机头转 Δψ 时脱靶量 必变 ≈−Δψ,不动即判丢。无需额外传感器;局限是只证"角度是活的",不证"是那个目标"。

飞行期的正确行为是刹车保护(现有 target_stale_ms 已覆盖),与"解锁以便重新 搜索"是两件事,不要混为一谈。

对自动解锁风险评估的修正:既然 0x0E 之后自动锁定仍生效、目标重现会自动锁定, 误触发一次 0x0E 的代价只是"本次跟踪中断、(我方)刹车一次",吊舱会自行恢复捕获 ——所以自动解锁的副作用可控,启用与否主要取决于触发判据准不准(表4 是否存在), 而不是担心解锁后锁不回来。