# 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 补: ```bash 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 且毫无意义: ```text # 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 / test.py)都是经典 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 已装,测试已打桩脱钩): ```bash 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`(波特率按飞控侧设置); - 经数传/仿真:`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 示例) ```ini # /etc/systemd/system/pod-bridge.service [Unit] Description=Pod-PX4 Bridge After=network.target [Service] ExecStart=/home//venv-pod/bin/python -m pod_bridge_app WorkingDirectory=/home//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,这是本项目迄今最重要的架构性结论。 - **锁定位置位(表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` 地理位置引导律 | **永久刹车**。桥接层 `bridge.py:169` 的 `target_valid` 依赖表4;表4 恒缺 → `_latest_target` 恒 None → `GuidanceLaw.update()` 每周期走 `_brake("载机位置或目标不可用/超龄")`。安全方向正确(降级刹车、不会乱飞),但**吊舱表4 这条引导路径在本设备上等于不存在** | | 目标距离/坐标来源 | 必须改由**我方自有链路**提供:`stereo_yolo` 检测 → `stereo_ranger` 双目视距。吊舱只当作"稳定指向 + 表2-2 脱靶量"的伺服/传感器用 | | `--auto-unlock`(§10.1) | **必然误触发,已实测禁用**:`last_tgt_s` 恒 None 被当成超时。见 §10.1 | | 后续选型 | 若要保留吊舱侧目标定位,只能换**带激光测距的吊舱型号**(且需确认激光能接入吊舱解算链路) | #### 架构该怎么办(分叉已选边) 既然距离无法回灌、表4 不会有,目标地理坐标就**必须由我方自己解算**: ``` stereo_yolo 检测 → stereo_ranger 双目视距(斜距/前向距离) → 载机 GPS + 姿态(表3 我方已有) + 吊舱框架角(表2-2) → **我方自己算出目标经纬度**,再喂给 guidance.py ``` 吊舱在本设备上退化为:**伺服稳定指向 + 表2-2 脱靶量/框架角传感器**。 它仍然很有用(稳像、跟得住、给得出视场角),只是不再提供坐标。 > 这意味着 `guidance.py` 的输入要从"吊舱表4"换成"我方自算的目标经纬度", > `bridge.py:169` 的 `target_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)交叉验证 —— 最可靠,待集成。 飞行期的正确行为是**刹车保护**(现有 `target_stale_ms` 已覆盖),与"解锁以便重新 搜索"是两件事,不要混为一谈。 **对自动解锁风险评估的修正**:既然 0x0E 之后自动锁定仍生效、目标重现会自动锁定, 误触发一次 0x0E 的代价只是"本次跟踪中断、(我方)刹车一次",吊舱会自行恢复捕获 ——所以自动解锁的**副作用可控**,启用与否主要取决于触发判据准不准(表4 是否存在), 而不是担心解锁后锁不回来。