我做了一个 DJI 第一代 4G 模块模式切换工具:DJIModeSwitcher
我做了一个 DJI 第一代 4G 模块模式切换工具
最近,我为 DJI 第一代 4G 蜂窝模块开发了一个 macOS 桌面工具:DJIModeSwitcher。
它可以识别连接到 Mac 的目标模块,并在“原模式”和“USB 上网模式”之间切换。整个过程包括设备检测、AT 接口探测、参数写入、模块重启、USB 重新枚举和最终结果验证。
项目已经开源:
https://github.com/hiwangchuan/DJIModeSwitcher
当前版本为 1.1。
这是一个由社区独立开发的第三方项目,与 DJI、大疆创新、Quectel、运营商或相关硬件厂商不存在授权、隶属或合作关系。
为什么要做这个工具
第一代 DJI 4G 模块本质上包含蜂窝通信和 USB 接口能力,但不同工作模式下,它向主机暴露的 USB 接口组合并不相同。
在合适的 AT 接口上,可以通过以下参数读取当前 USB 网络模式:
AT+QCFG="usbnet"?
切换到 USB 上网模式:
AT+QCFG="usbnet",1
恢复原模式:
AT+QCFG="usbnet",0
写入参数之后,还需要重启模块:
AT+CFUN=1,1
看起来只是几条命令,但真正做成一个普通用户可以操作的桌面应用,问题会复杂很多:
- 如何准确识别目标 USB 设备?
- 如何从多个接口中找到真正的 AT 接口?
- 如何避免把命令发到错误的 Bulk 端点?
- 模块重启并从 USB 总线上消失时,如何判断这是正常现象还是操作失败?
- 设备重新出现后,如何确认配置确实生效?
- 如何保证最终用户不用安装 Homebrew 和 libusb?
- 如何让应用在 macOS 13 及更高版本上运行?
DJIModeSwitcher 就是围绕这些问题开发的。
当前支持范围
当前版本支持:
- Apple Silicon Mac
- macOS 13.0 或更高版本
- 目标 USB 标识:
2CA3:4006 - DJI 第一代 4G 模块
- 原模式:
usbnet=0 - USB 上网模式:
usbnet=1
暂时不支持 Intel Mac。
这个项目也不会扩展成通用 AT 终端。目前明确不包含:
- 短信读取
- SIM 或 eSIM 信息读取
- APN 管理
- 流量统计
- 固件刷写
- 账号登录
- 云服务访问
- 任意 AT 命令执行
限制功能范围是有意为之。对于会直接操作硬件的工具,我更倾向于让它只做少量、明确、可以验证的事情。
它是怎么工作的
一次完整的模式切换大致经过以下步骤:
识别 USB 设备
↓
枚举配置、接口和端点
↓
寻找成对的 Bulk IN/OUT 端点
↓
发送 AT 探测命令
↓
确认返回独立的 OK 行
↓
读取当前 usbnet 配置
↓
写入目标模式
↓
请求模块重启
↓
关闭旧 USB 句柄
↓
等待设备断开并重新出现
↓
重新连接设备
↓
再次读取配置并验证结果
其中比较关键的一点,是不能简单地认为“第一个 Bulk 接口就是 AT 接口”。
应用会枚举所有可能的接口和 alternate setting,然后向候选端点发送最基本的 AT 命令。只有收到格式正确、独立成行的 OK 响应后,才会接受这个接口。
这样可以尽量避免把后续命令发送到错误的 USB 接口。
为什么不能在重启时立即判定失败
模块收到 AT+CFUN=1,1 后会主动重启。
从主机的角度看,USB 设备可能会突然消失,正在进行的传输也可能返回超时、I/O 错误或者设备断开。如果把这些结果直接当作失败,应用就会在正常重启过程中错误地报告操作失败。
DJIModeSwitcher 会将重启阶段的断开视为预期事件:
- 关闭旧设备句柄;
- 等待设备从 USB 总线消失;
- 等待设备重新枚举;
- 重新寻找 AT 接口;
- 再次读取
usbnet; - 只有读取结果与目标模式一致时才报告成功。
换句话说,应用不会因为“命令已经发出去”就宣告完成,而是等待模块回来并验证最终状态。
项目架构
项目采用 SwiftPM 管理,主要分为四个部分。
DJIModeCore
使用 C 编写的底层核心,负责:
- 动态加载 libusb
- USB 设备枚举
- 接口和端点探测
- Bulk 数据传输
- AT 响应解析
- 固定命令白名单
- libusb 错误映射
C 层不会接受任意 AT 命令,只允许项目预先定义的有限命令。
DJIModeKit
Swift 服务层,负责:
- 使用 actor 串行化设备访问
- 设备出现和消失监控
- 模式切换状态机
- 重启与重新枚举协调
- 错误转换
- 诊断日志
- Mock 设备和演示流程
DJIModeSwitcher
基于 SwiftUI 的 macOS 图形界面,负责:
- 显示设备连接状态
- 显示当前模式
- 发起切换或恢复操作
- 操作确认
- 阶段进度显示
- 错误提示
- 诊断信息复制
- 操作期间的退出保护
mode-switcher
用于开发和协议验证的命令行工具,提供:
swift run mode-switcher detect
swift run mode-switcher status
swift run mode-switcher network
swift run mode-switcher restore
如何做到不依赖 Homebrew
这个项目使用 libusb 与模块通信。
开发阶段最简单的方式可能是要求用户先运行:
brew install libusb
但这对最终用户并不友好。用户拿到一个 macOS 应用,不应该还要理解 Homebrew、动态库路径或者命令行环境。
因此,发行版本会将 libusb 直接放入应用包:
DJIModeSwitcher.app/
└── Contents/
├── MacOS/
│ └── DJIModeSwitcher
└── Frameworks/
└── libusb-1.0.0.dylib
运行时优先通过相对路径加载:
@executable_path/../Frameworks/libusb-1.0.0.dylib
最终用户不需要安装:
- Homebrew
- Xcode
- Swift
- Python
- 单独的 libusb
除了应用内置的 libusb,程序只链接 macOS 自带的系统框架。
固定并验证第三方源码
首次从源码构建时,脚本会下载官方 libusb 1.0.30 源码包。
下载完成后会验证固定的 SHA-256:
fea36f34f9156400209595e300840767ab1a385ede1dc7ee893015aea9c6dbaf
只有校验通过才会继续编译。
这样做不能解决所有供应链安全问题,但至少可以防止下载内容在不知情的情况下发生变化。
libusb 保持其 LGPL-2.1-or-later 许可证。应用包中也会同时包含对应许可证和第三方组件声明。
macOS 13 兼容问题
在打包过程中,我还遇到了一个比较隐蔽的问题。
使用较新的 macOS SDK 编译 libusb 时,配置脚本会检测到 pipe2()。但是这个符号只存在于更新的 macOS 版本中。如果直接使用检测结果,即使动态库声明的部署目标是 macOS 13,运行时仍可能在旧系统上出现符号缺失。
最终的处理方式是:
- 明确设置 libusb 的最低部署目标为 macOS 13;
- 禁用配置阶段对
pipe2()的使用; - 强制使用兼容性更好的
pipe()实现; - 检查最终 Mach-O 的最低系统版本;
- 使用
nm确认动态库不再引用_pipe2。
这也是为什么“编译成功”并不等于“真的兼容目标系统”。
ZIP 和 PKG 两种交付方式
项目目前支持生成两种发行格式。
ZIP 便携包:
./script/archive.sh
输出:
dist/DJIModeSwitcher-1.1-arm64.zip
PKG 安装包:
./script/package.sh
输出:
dist/DJIModeSwitcher-1.1-arm64.pkg
PKG 会将完整应用安装到:
/Applications/DJIModeSwitcher.app
PKG 中没有用于联网下载依赖的安装脚本,libusb 已经包含在应用本身。
签名、公证和 Gatekeeper
“依赖已经全部打包”与“其他 Mac 可以无警告安装”是两个不同的问题。
前者解决的是运行环境:
- 不需要 Homebrew;
- 不需要外部 libusb;
- 不需要开发工具;
- 不需要首次启动下载组件。
后者属于 Apple 的分发信任体系,需要:
Developer ID Application证书;Developer ID Installer证书;- Hardened Runtime;
- Apple Notary Service 公证;
- 将公证票据 staple 到应用或 PKG。
如果没有 Developer ID 和公证,应用即使技术上完全可运行,仍可能被 Gatekeeper 拦截。
项目已经准备了签名和公证脚本入口,但正式公开发布仍需要发布者自己的 Apple Developer 证书和账号。
日志与隐私
应用不包含:
- 用户账号
- 云端 API
- 自动更新服务
- 运行时遥测
- 远程日志上传
诊断日志只写入本机:
~/Library/Logs/DJIModeSwitcher/
日志采用 2 MB × 5 文件轮转策略。
日志可能包含 USB 标识、接口号、操作阶段和 AT 响应。因此,在公开分享诊断信息之前,仍建议用户先检查内容。
无硬件演示模式
为了让开发和界面测试不完全依赖真实模块,项目提供了 Mock 传输层和演示模式:
DJI_MODE_SWITCHER_DEMO=1 ./script/build_and_run.sh
演示模式可以完整体验:
- 设备识别
- 模式读取
- 操作确认
- 参数写入
- 模拟重启
- USB 重新连接
- 最终验证
不过,Mock 测试不能代替真实硬件测试。
当前仍需完成的工作
目前项目的主要代码、独立打包和文档已经完成,但正式发布仍有一些外部门槛:
- 使用更多第一代 DJI 4G 模块和固件版本测试;
- 在多台 Apple Silicon Mac 上验证;
- 在 macOS 13 实机上进行最低版本测试;
- 完成至少 50 次“原模式 → 上网模式 → 原模式”耐久循环;
- 使用 Developer ID 完成正式签名;
- 完成 Apple 公证;
- 在没有 Homebrew 的干净 Mac 上验证首次安装。
对于硬件工具来说,我认为明确写出“还没有验证什么”,和说明“已经实现什么”同样重要。
开源地址
项目源码、构建脚本、架构说明、测试指南和分发文档均已发布到 GitHub:
https://github.com/hiwangchuan/DJIModeSwitcher
项目原创代码和文档采用 Apache License 2.0。
libusb 保持其原有 LGPL-2.1-or-later 许可证。
写在最后
这个项目表面上只是切换一个 usbnet 参数,但真正实现时,涉及 USB 枚举、接口探测、AT 通信、设备重启、状态机、动态库封装、macOS 版本兼容、代码签名和开源许可证等多个问题。
对我来说,最有价值的部分并不是“成功发送了几条 AT 命令”,而是把一次容易出错的硬件操作,整理成一个边界明确、过程可见、结果可验证的桌面工具。
如果你手上有第一代 DJI 4G 模块,并且愿意参与不同固件、Mac 型号或连接方式的测试,欢迎在 GitHub 提交 Issue 或测试结果。