基于 C++17 的 OTA(Over-the-Air,空中升级)服务端项目。项目以 sherry 网络库为基础,通过 HTTP 提交升级通知、查询设备状态和下载文件,通过 MQTT 与设备交换消息,并使用 Redis 保存状态和传递异步命令。
当前 OTA 服务入口是 tests/test_ota_server.cc,构建产物为 bin/test_ota_server。仓库保留了演示固件数据、旧测试代码和待完善的部署配置,接入设备前请先阅读本文的「当前限制」。
- HTTP 管理接口:升级通知提交、设备版本缓存查询、下载状态缓存查询、文件下载,以及简易管理页面。
- MQTT 消息通信:按设备类型建立客户端,订阅设备上报,并定时发送升级通知。
- Redis 状态与队列:设备消息按配置写入缓存;HTTP 提交的通知进入 Redis List,由后台线程消费。
- 底层网络组件:协程、线程调度、基于 epoll 的 IOManager、定时器、Socket、HTTP 解析与连接、日志、YAML 配置、字节数组和压缩流。
flowchart LR
User[管理页面 / HTTP 客户端] --> HTTP[OTAHttpManager]
HTTP -->|LPUSH 通知命令| Queue[(Redis 命令队列)]
Queue -->|BRPOP| Dispatcher[OTACommandDispatcher]
Dispatcher --> Manager[OTAMqttManager / OTADevice]
Manager -->|定时发布升级通知| Broker[MQTT Broker]
Broker --> Device[设备]
Device -->|版本 / 下载状态上报| Broker
Broker --> Callback[OTAClientCallbackManager]
Callback -->|保存消息| Cache[(Redis 状态缓存)]
HTTP -->|查询缓存| Cache
Device -->|HTTP 下载| HTTP
HTTP --> Files[本地 file 目录]
HTTP 查询接口直接读取缓存。通知提交成功表示命令已入队,设备是否收到通知、下载或完成升级,需要结合 MQTT 消息和服务日志确认。
.
├── CMakeLists.txt # sherry 共享库、测试与示例目标
├── cmake/ # Ragel 生成规则及构建辅助函数
├── config/ota_system.yaml # Redis 连接池与 MQTT 设备配置
├── file/ # 管理页面和下载示例文件
├── include/json/ # 内置 nlohmann/json 头文件
├── sherry/
│ ├── http/ # HTTP 协议、解析器、服务端与客户端
│ ├── db/ # Redis、连接池与异步线程封装
│ ├── ota/ # OTADevice 等设备相关实现
│ ├── ota_* # HTTP/MQTT 管理、命令分发与通知
│ └── streams/ # zlib 压缩流
├── tests/ # 组件测试、联调程序与 OTA 服务入口
├── examples/echo_server.cc # TCP Echo 示例
├── scripts/ # HTTP 请求及 wrk Lua 脚本
├── recording/ # 开发笔记,部分内容与当前实现有差异
└── share/doc/Eclipse Paho C/ # Paho C 文档、样例及许可文件
使用 Linux 开发环境。底层代码依赖 epoll、ucontext 和 Linux sendfile,不能直接按原样在 macOS 或 Windows 原生环境编译。
| 依赖 | 用途 |
|---|---|
| 支持 C++17 的编译器、GNU Make、CMake 3.x | 编译与构建 |
| Ragel | 从 .rl 文件生成 HTTP/URI 解析器 |
| yaml-cpp、Boost 头文件 | 配置解析与类型转换 |
| OpenSSL、zlib | SSL、哈希与压缩 |
| Eclipse Paho MQTT C++ 与 C | paho-mqttpp3、paho-mqtt3as |
| hiredis、hiredis-vip、libevent | Redis 普通/集群连接与异步 IO |
| Redis 服务、MQTT Broker | OTA 服务运行时依赖 |
以 Ubuntu 22.04 的包名为例,安装基础依赖及本地联调工具:
sudo apt-get update
sudo apt-get install -y \
build-essential cmake git pkg-config ragel \
libyaml-cpp-dev libboost-all-dev libssl-dev zlib1g-dev \
libevent-dev libhiredis-dev libpaho-mqtt-dev libpaho-mqttpp-dev \
redis-server mosquitto mosquitto-clients curl还需单独安装 hiredis-vip,例如在项目目录之外执行:
git clone https://github.com/vipshop/hiredis-vip.git
cd hiredis-vip
make -j2
sudo make install
sudo ldconfig安装后应能找到 hiredis-vip/hircluster.h 和 libhiredis_vip.so。虽然 CMake 对缺失 hiredis-vip 只打印警告,但源码无条件包含其头文件并链接其库,因此当前构建需要它。该上游仓库已归档,本项目也未锁定第三方依赖版本,升级依赖后需要重新验证兼容性。
git clone https://github.com/BabyDrangoner/ota-new.git
cd ota-new
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target test_ota_server -j2产物输出在项目根目录下:
lib/libsherry.so:共享库。bin/test_ota_server:OTA 服务入口。- 其他测试与示例目标也输出到
bin/。
构建前请核对 CMakeLists.txt 中的环境假设:
- 保留了
/build/yaml-cpp-0.8.0/include、/apps/sherry/lib以及 x86_64 Redis 库搜索路径;使用自定义安装目录时需按实际环境调整,避免混用不同版本的头文件和库。 - 项目声明的 CMake 最低版本仍为
2.8,使用 CMake 4.x 时需要先更新兼容性声明。 - 编译参数包含
-O0 -ggdb -Werror;编译器或依赖版本变化可能把警告变成构建失败。 - CMake 显式列举源码与可执行目标;新增文件后需要同步更新构建列表。
编辑 config/ota_system.yaml,并启动对应的 Redis 服务与 MQTT Broker。
| 配置项 | 当前用途 / 默认值 |
|---|---|
redis.config.http_pool |
HTTP 请求访问 Redis 的连接池,默认 10 个连接 |
redis.config.ota_pool |
OTA 状态与 MQTT 回调使用的连接池,默认 10 个连接 |
redis.config.ota_mq_pool |
阻塞消费队列的独立连接池,默认 1 个连接、timeout: 0 |
Redis host |
默认 127.0.0.1:6379 |
Redis passwd |
三个连接池都需与实际 Redis 密码一致;本地无密码服务设为 "" |
mqtt.devices |
默认配置设备类型 1、2,Broker 为 tcp://127.0.0.1:1883 |
每个 MQTT 设备配置中的 sub_topics、sub_qos、redis_keys 和 expire_seconds 按索引一一对应,必须保持相同长度和顺序。expire_seconds 为 0 时不过期,大于 0 时按秒设置缓存有效期。
新增设备类型时,在 mqtt.devices 中新增条目;新增模块时,为其配置上报 topic 和对应的 Redis key。HTTP 查询按 device_type 与 name 拼接 key,必须与配置保持一致。当前 MQTT 客户端 ID 使用设备类型的字符串,在同一个 Broker 上启动多个服务实例时需避免 ID 冲突。
file/ota.html 中的 const api 当前写死了远程地址。通过本服务的 /ota/ 页面访问时,可将其改为:
const api = window.location.origin;LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ./bin/test_ota_server服务默认监听 0.0.0.0:8020,管理页面地址为 http://127.0.0.1:8020/ota/。监听地址写在 tests/test_ota_server.cc 中,尚未接入 YAML 或命令行参数。
程序从当前工作目录加载 ./config/ota_system.yaml、./file/ota.html 和 ./file/ 下的下载文件,因此需要在项目根目录启动。检查日志中的配置加载、Redis 连接、MQTT 连接和 topic 订阅结果;配置加载失败后程序仍会继续执行,不能仅凭进程存在判断启动成功。
以下方法是客户端调用约定;当前路由处理器没有完整限制 HTTP 方法。接口实现见 sherry/ota_http_manager.cc。
| 方法 | 路径 | 请求 / 行为 |
|---|---|---|
GET |
/ota/ |
返回管理页面 |
POST |
/ota/notify |
JSON:command、device_type、name、version;提交通知命令 |
POST |
/ota/query |
JSON:device_type、name;读取版本上报缓存 |
POST |
/ota/query_download |
JSON:device_type、name;读取下载状态缓存 |
GET |
/ota/file_download/{device_type}/{name}/{version} |
下载本地文件 |
服务完成 MQTT 订阅后,可以使用默认设备类型 1、模块 gps 做本地联调。以下 JSON 是演示 payload;服务当前按原文缓存上报内容,并未强制规定其字段结构。
mosquitto_pub -h 127.0.0.1 -p 1883 -q 1 \
-t '/ota/device/query/1/gps' \
-m '{"device_type":1,"name":"gps","version":"1.0.01"}'
curl -i -X POST http://127.0.0.1:8020/ota/query \
-H 'Content-Type: application/json' \
-d '{"device_type":1,"name":"gps"}'等待 MQTT 回调写入 Redis 后,查询应返回上报的原始内容。下载状态可类似验证:
mosquitto_pub -h 127.0.0.1 -p 1883 -q 1 \
-t '/ota/device/query_download/1/gps' \
-m '{"device_type":1,"name":"gps","progress":50}'
curl -i -X POST http://127.0.0.1:8020/ota/query_download \
-H 'Content-Type: application/json' \
-d '{"device_type":1,"name":"gps"}'默认下载状态缓存 20 秒后过期,需要设备持续上报。缓存不存在时,接口仍返回 HTTP 200,正文为 device not exists. 或 device is not downloading.。
在一个终端订阅通知 topic:
mosquitto_sub -h 127.0.0.1 -p 1883 -q 1 -v -t '/ota/1/gps/notify'在另一个终端提交:
curl -i -X POST http://127.0.0.1:8020/ota/notify \
-H 'Content-Type: application/json' \
-d '{"command":"notify","device_type":1,"name":"gps","version":"1.0.01"}'command: "notify" 必须随请求发送:HTTP 层将原始请求正文入队,后台分发器依赖该字段。入队成功时正文为 task submits successfully.;同一设备类型、模块已有相同版本记录时会返回已发布提示。
在消费者正常运行且 MQTT 已连接的情况下,通知器每 10 秒向 /ota/{device_type}/{name}/notify 发布一次消息,使用 QoS 1、retain=true。消息字段包括 name、version、file_name、file_size、url_path、md5_value、launch_mode、upgrade_mode、time。其中固件信息仍含硬编码示例值,尚未自动关联本地下载文件。
HTTP 响应目前混用 JSON 和纯文本,客户端应先读取响应正文,不能假设所有响应都能用 res.json() 解析。
文件命名规则为 file/ota_{device_type}_{version}_{name}.jpg。仓库内置 file/ota_1_1.0.01_gps.jpg,对应:
curl --fail http://127.0.0.1:8020/ota/file_download/1/gps/1.0.01 \
--output downloaded-gps.jpg当前下载响应固定使用 image/jpeg,X-Content-MD5 也是占位值。新增真实固件格式时,需要同步调整文件命名、内容类型、校验值和通知中的下载 URL;仓库尚无固件上传接口。
下表中的 T 表示设备类型,N 表示模块名。上报 topic 和缓存 key 由 YAML 决定,表内为现有配置遵循的格式。
| 用途 | MQTT topic / Redis key | 说明 |
|---|---|---|
| 服务发送通知 | /ota/T/N/notify |
周期发布,保留消息 |
| 设备上报版本 | /ota/device/query/T/N → ota:device:query:T:N |
默认不过期 |
| 设备上报下载状态 | /ota/device/query_download/T/N → ota:device:query_download:T:N |
默认有效期 20 秒 |
| 已通知版本 | ota:device:notify:T:N |
记录版本字符串,用于重复提交判断 |
| 后台命令队列 | ota:device:message |
HTTP LPUSH,消费者 BRPOP |
队列分发器目前仅注册 notify 和 stop_notify。停止通知命令的 JSON 格式为:
{"command":"stop_notify","device_type":1,"name":"gps","version":"1.0.01"}该命令需由 Redis 客户端写入 ota:device:message 队列;当前没有注册 /ota/stop_notify HTTP 路由。停止逻辑取消定时器并删除版本记录,尚未清除 Broker 中已保留的通知消息。
CMakeLists.txt 已注册多个独立可执行目标,未接入 CTest。可先构建并运行不依赖外部网络服务的组件检查:
cmake --build build --target test_bytearray test_http test_http_parser -j2
export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
./bin/test_bytearray
./bin/test_http
./bin/test_http_parsertest_bytearray 会在 /tmp/ 写入测试文件;HTTP 两个示例主要输出序列化和解析结果。test_redis、test_mqtt_client 以及各种服务端示例需要额外的服务、地址或配置。部分历史测试含本机绝对路径,运行前请检查源码。tests/ 中并非每个 .cc 文件都已注册到 CMake。
| 范围 | 当前实现与使用影响 |
|---|---|
| 通知队列启动 | OTAMqttManager::start() 创建消费线程后才设置 m_running=true,消费者可能提前退出。若出现命令入队但没有通知,检查 redis message queue thread exit 日志;需要完善启动同步。 |
| 固件元数据 | OTANotifier::get_notify_message() 中的文件名、大小、URL 和 MD5 是示例值,URL 与当前 HTTP 下载路由不一致。 |
| 通知管理 | 每个 OTADevice 只保存一个通知器;同一设备类型下多个模块的并行通知、进程重启后的通知恢复仍需完善。 |
| 管理页面 | API 地址写死,且页面按 JSON 解析部分纯文本响应,需要按实际部署和接口响应调整。 |
| Docker 部署 | 当前 Dockerfile 依赖仓库中不存在的 lib64/,缺少部分构建/运行依赖,并从 /tmp/build/bin/test_server 复制程序。实际 OTA 产物是 /app/bin/test_ota_server,运行镜像还需包含配置、页面、文件目录和所需动态库,并设置工作目录。当前不能直接作为可用部署方案。 |
以上说明以当前构建目标和服务入口为准。扩展功能时,优先沿 test_ota_server → OTAHttpManager / OTAMqttManager → OTADevice 的调用链阅读;recording/ota_command_dispatcher.md 中的历史命令列表需对照现有分发器确认。