跳到正文
title:
"大疆 4G 模块和 VoHive 的完整部署"
published:
lang:
zh-CN
reading:
4 min
tags:
随笔

大疆 4G 模块和 VoHive 的完整部署

家里有一台长期运行的 PVE 主机,手上刚好有一个大疆 4G 模块,我想把它接入 VoHive,用来管理短信、整合下手上的esim,自动保活。 下面记录的是我实际走通的记录,环境和路径也都来自这次部署。

过程中我参考了 wlzh/dji-4g-vohive-mac、当前使用镜像的构建仓库 Orson-Yan/Vohive-155

环境和部署结构

flowchart LR
    DJI["大疆 4G 模块<br/>初始 ID 2CA3:4006<br/>目标 ID 2C7C:0125"]
    PVE["PVE 物理主机<br/>option / qmi_wwan"]
    CT["LXC 110<br/>docker-portainter<br/>192.168.31.75"]
    VH["VoHive Docker<br/>network_mode: host<br/>privileged + /dev:/dev"]
    QMI["/dev/cdc-wdm0<br/>/dev/ttyUSB*<br/>wwan0"]
    LAN["CT eth0<br/>默认网关 192.168.31.250"]
    EPDG["互联网 / ePDG / IMS"]

    DJI --> PVE
    PVE --> QMI
    QMI --> CT
    CT --> VH
    CT --> LAN --> EPDG

最终跑通后的结构如上。 具体环境配置是:

  • PVE 8.x。
  • Docker 环境在LXC中,部署了Portainer,用于docker服务管理
  • VoHive 使用 orsoman/vohive 镜像。
  • 大疆模块固定插在 PVE 主机的同一个 USB 口

VoHive 部署

目录准备:

mkdir -p /root/config/vohive/{config,data,logs}

由于一些原因,原作者已经闭源了,下面是另一个可用的镜像,然后在 Portainer 里创建 Stack:

services:
  vohive:
    # 我固定了这次实际验证过的 tag,避免 latest 更新后行为变化
    image: orsoman/vohive:sha-2cc8376
    container_name: vohive
    restart: unless-stopped
    network_mode: host
    privileged: true
    volumes:
      - /dev:/dev
      - /root/config/vohive/config:/app/config
      - /root/config/vohive/data:/app/data
      - /root/config/vohive/logs:/app/logs
    environment:
      TZ: Asia/Shanghai

初始化模块

接下来的硬件操作都在 PVE 主机完成,而不是在 Docker 容器里完成。

lsusb
lsusb -d 2ca3:4006
lsusb -t

刚插入时,可以看到:

2ca3:4006 DJI Technology Co., Ltd. Baiwang

生成 AT 串口

# 加载 option 驱动、注册大疆当前 USB ID,并等待 ttyUSB 设备生成
modprobe option && echo 2ca3 4006 | tee /sys/bus/usb-serial/drivers/option1/new_id && udevadm settle && sleep 2 && lsusb -t && ls -l /dev/ttyUSB*

执行后,系统里出现了 5 个大疆串口,加上原有的 CH340,一共能看到 /dev/ttyUSB0/dev/ttyUSB5

确认实际的 AT 端口

串口编号是全机共享的。我这台机器上的旧 CH340 已经占用了 /dev/ttyUSB0,所以大疆模块从 /dev/ttyUSB1 开始编号。我把每个串口映射回 USB 物理接口:

udevadm info --query=path --name=/dev/ttyUSB1
udevadm info --query=path --name=/dev/ttyUSB2
udevadm info --query=path --name=/dev/ttyUSB3
udevadm info --query=path --name=/dev/ttyUSB4

参考项目使用 USB 接口 1.2 作为 AT 口,我这里的实际映射是:

USB 1-1.1:1.2 -> /dev/ttyUSB3

换一台机器后编号可能不同,所以真正有用的是 1.2 这个接口关系,而不是固定记住某个 ttyUSB 数字。

把模块切换成标准 QMI 模式

查询结果确认模块型号为 QDC507,当前 USB 配置为:

+QCFG: "usbcfg",0x2CA3,0x4006,1,1,1,1,1,0,0
OK

确认结果无误后,把 USB ID 改成 2C7C:0125,然后重启模块:

# PVE 没有 socat 时先安装,已经安装可以跳过
apt-get update && apt-get install -y socat

# 通过实际确认的 AT 口 /dev/ttyUSB3,永久把 USB ID 改为移远 2C7C:0125
echo 'AT+QCFG="usbcfg",0x2C7C,0x0125,1,1,1,1,1,0,0' | timeout 5 socat -T 2 - /dev/ttyUSB3,raw,echo=0,crnl

# 软重启模块,让新的 USB ID 生效
echo 'AT+CFUN=1,1' | timeout 5 socat -T 2 - /dev/ttyUSB3,raw,echo=0,crnl

# 等待模块重新枚举,并确认已经变成 2c7c:0125
sleep 20 && lsusb -d 2c7c:0125

模块一会后重新出现,结果是:

  • USB ID 变成 2c7c:0125 Quectel EC25 LTE modem

把设备交给 LXC,并自动恢复

PVE 已经能看到 cdc-wdm0、4 个大疆串口和 wwan0,接下来直接配置自动挂载。CT 启动后执行挂载脚本,模块热插入时由 udev 再次触发;设备不存在时脚本直接跳过,不影响这台 Docker CT 启动。

脚本会处理 /dev/ttyUSB0/dev/ttyUSB4。这样即使 PVE 重启后 CH340 和大疆模块的枚举顺序发生变化,VoHive 仍能看到完整的串口范围。

在 PVE 上创建了下面的脚本:

install -Dm0755 /dev/stdin /usr/local/sbin/dji4g-ct110-attach <<'EOF'
#!/bin/bash
set -u

CT=110

for _ in $(seq 1 30); do
  [ "$(lxc-info -n "$CT" -sH 2>/dev/null)" = "RUNNING" ] && break
  sleep 1
done
[ "$(lxc-info -n "$CT" -sH 2>/dev/null)" = "RUNNING" ] || exit 0

for dev in /dev/ttyUSB0 /dev/ttyUSB1 /dev/ttyUSB2 \
           /dev/ttyUSB3 /dev/ttyUSB4 /dev/cdc-wdm0; do
  [ -c "$dev" ] || continue
  if ! lxc-attach -n "$CT" -- test -c "$dev" 2>/dev/null; then
    lxc-device -n "$CT" add "$dev" || logger -t dji4g-ct110 "Unable to attach $dev"
  fi
done

if ip link show wwan0 >/dev/null 2>&1; then
  lxc-device -n "$CT" add wwan0 || logger -t dji4g-ct110 "Unable to attach wwan0"
fi
EOF

然后给部署docker的LXC的 systemd 服务加了一个启动后动作:

install -Dm0644 /dev/stdin /etc/systemd/system/pve-container@110.service.d/dji4g-passthrough.conf <<'EOF'
[Service]
ExecStartPost=/usr/local/sbin/dji4g-ct110-attach
EOF

热插拔则交给一个单独的 oneshot 服务:

install -Dm0644 /dev/stdin /etc/systemd/system/dji4g-ct110-attach.service <<'EOF'
[Unit]
Description=Attach DJI 4G modem to LXC 110
After=pve-container@110.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/dji4g-ct110-attach
EOF

最后加一条 udev 规则,只匹配这个 Quectel USB ID,不处理其他 USB 设备:

install -Dm0644 /dev/stdin /etc/udev/rules.d/99-dji4g-ct110.rules <<'EOF'
ACTION=="add", SUBSYSTEM=="net", KERNEL=="wwan*", ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0125", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dji4g-ct110-attach.service"
EOF

systemctl daemon-reload
udevadm control --reload-rules
systemctl start dji4g-ct110-attach.service

配置加载后,前面的 systemctl start 会立即把当前设备交给 CT。最后确认 systemd 启动动作和 CT 内的 wwan0

systemctl show dji4g-ct110-attach.service -p Result -p ExecMainStatus
systemctl show pve-container@110.service -p DropInPaths
pct exec 110 -- ip link show wwan0

整个过程不需要重启 CT,也不会重建 Docker 容器。服务返回 success 后,CT 里可以看到 cdc-wdm0、全部串口和 wwan0

在 VoHive 里添加这个模块

直通完成后,我回到 VoHive 的设备页面执行重新扫描。页面会发现了这个 QMI 设备:

wwan0 · qmi_wwan · QMI
/dev/cdc-wdm0 · AT: /dev/ttyUSB3

只需要填写名称。IMEI、USB 路径、wwan0、AT 端口和 /dev/cdc-wdm0 都使用扫描结果,不用改动。后端模式选择QMI

热拔插

持久化完成后,按下面的顺序测试热拔插。

  1. 拔出大疆模块。
  2. 等待约 5 秒。
  3. 插回原来的 USB 口。
  4. 等待 10 到 30 秒。
  5. 在 VoHive 页面执行一次“重新扫描”。

插回后,先看 PVE:

lsusb -d 2c7c:0125
ls -l /dev/cdc-wdm* /dev/ttyUSB*
ip link show wwan0

接着看 CT:

pct exec 110 -- ls -l /dev/cdc-wdm0 /dev/ttyUSB3
pct exec 110 -- ip link show wwan0

回退方法

把模块切回原来的 USB ID

如果后面需要恢复,先重新确认当前 AT 口,再写回最初保存的配置:

printf 'AT+QCFG="usbcfg",0x2CA3,0x4006,1,1,1,1,1,0,0\r' >&3
timeout 3 cat <&3
printf 'AT+CFUN=1,1\r' >&3
timeout 3 cat <&3 || true
exec 3>&-

回滚后 USB 会重新枚举,AT 设备编号也可能变化,可以用 udevadm 确认端口。

删除 PVE 上的自动挂载

自动挂载只新增了 4 个文件,需要移除时可以这样处理:

rm -f /usr/local/sbin/dji4g-ct110-attach
rm -f /etc/systemd/system/pve-container@110.service.d/dji4g-passthrough.conf
rm -f /etc/systemd/system/dji4g-ct110-attach.service
rm -f /etc/udev/rules.d/99-dji4g-ct110.rules
rmdir /etc/systemd/system/pve-container@110.service.d 2>/dev/null || true
systemctl daemon-reload
udevadm control --reload-rules

如果还要立即移除当前已经挂载的设备:

lxc-device -n 110 del wwan0
lxc-device -n 110 del /dev/cdc-wdm0
lxc-device -n 110 del /dev/ttyUSB4
lxc-device -n 110 del /dev/ttyUSB3
lxc-device -n 110 del /dev/ttyUSB2
lxc-device -n 110 del /dev/ttyUSB1

参考资料

这篇记录里的 USB 编号、CT 编号和路径都来自我的实际环境。换一台机器后,USB 物理路径、串口编号、网关或 CT ID 可能不同,实际操作时需要以当前机器的 udevadmlsusb -t 输出为准。