Commit 8f3a151f authored by 957dd's avatar 957dd

Initial commit

parents
CompileFlags:
Remove: [-f*, -m*]
# =============================================================================
# ESP32-S3 + ESP-IDF 固件工程(ESPRCCar)
# =============================================================================
# 【建议提交】
# main/ docs/ www/ partitions.csv CMakeLists.txt
# sdkconfig.defaults sdkconfig.defaults.release main/idf_component.yml
# firmware/release/ ← 量产烧录 + OTA 用 .bin(见 firmware/README.md)
# 【不要提交】
# build/ managed_components/ dependencies.lock sdkconfig .devcontainer/
# =============================================================================
# --- ESP-IDF 构建输出(本地 idf.py build,勿入库)---
build/
**/build/
sdkconfig.old
*.map
*.elf
flasher_args.json
flash_project_args
flash_app_args
project_description.json
config.env
.gdbinit/
*.o
*.a
*.out
# 固件 .bin:默认忽略;仅 firmware/ 下发布包可提交(供 OTA / 产线下载)
*.bin
!firmware/
!firmware/**
# --- Component Manager(克隆后 idf.py build 会自动拉取)---
managed_components/
dependencies.lock
# --- Dev Container(仅 VS Code/Cursor 用 Docker 编译时可选,非必须)---
.devcontainer/
# --- 本地 ESP-IDF 工具/环境 ---
.espressif/
.idf/
# --- menuconfig 本地生成 ---
sdkconfig
# --- CMake / Ninja ---
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
install_manifest.txt
CTestTestfile.cmake
CMakeUserPresets.json
.ninja_deps
.ninja_log
# --- Python ---
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
.eggs/
venv/
.venv/
env/
.pytest_cache/
.coverage
htmlcov/
# --- IDE / 语言服务 ---
.vscode/
.idea/
*.iml
.clangd/
.ccls-cache/
compile_commands.json
.cursor/
# --- 日志与临时文件 ---
*.log
*~
*.swp
*.swo
*.bak
*.tmp
# --- 操作系统 ---
.DS_Store
.AppleDouble
.LSOverride
.directory
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.user
*.workspace
*.suo
*.sln.docstates
*.exe
.cache/
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESPRCCar)
# ESPRCCar - ESP32-S3 遥控设备固件
## 1. 项目用途
本项目是基于 ESP32-S3 的设备控制固件,**编译期三选一**链路模式(未选中的链路代码不会编入固件):
| 模式 | menuconfig | 说明 |
|------|------------|------|
| **WiFi + MQTT** | `APP_LINK_WIFI` | STA + MQTT JSON;OTA 走 HTTPS |
| **BLE** | `APP_LINK_BLE` | NimBLE GATT JSON;OTA 走 0xFFE2 二进制流 |
| **UART** | `APP_LINK_UART` | UART1(GPIO17/18)JSON + `\n`;OTA 与 BLE 相同协议 |
设备控制指令统一由 `remote_control` 模块处理。Android 对接说明见 **`docs/Android端设备对接文档_v1.0.1.md`**(含 WiFi / BLE / UART 三章)。
## 2. 目录结构(核心)
- `main/main.c`:最小入口,仅调用 `app_run()`
- `main/app`:应用流程与模式切换入口(`app_run()` 位于 `main/app/app_run.c`)。
- `main/core`:系统级初始化(NVS/SPIFFS/事件循环)与 FreeRTOS 任务统一管理(`task_manager.c`)。
- `main/provision`:配网/配参门户(SoftAP + HTTP + DNS 劫持)。
- `main/link_wifi`:MQTT 连接、心跳、消息处理(仅 `APP_LINK_WIFI` 编译)。
- `main/link_ble`:NimBLE 广播、连接、GATT 写入处理(仅 `APP_LINK_BLE` 编译)。
- `main/link_uart`:UART1 串口 JSON/OTA(仅 `APP_LINK_UART` 编译)。
- `main/protocol`:跨链路共用 JSON 协议解析与执行。
- `main/drivers`:驱动管理层与通用驱动能力。
- `main/drivers/gpiotrol/devices`:具体设备策略(按型号拆分)。
- `www`:配网页静态资源(SPIFFS 镜像来源):`index.html`(WiFi)、`index_ble.html`(BLE)、`index_uart.html`(UART,仅设备号)。
### 2.1 FreeRTOS 任务统一管理
自建 FreeRTOS 任务统一在 `main/core/task_manager.c` 的任务表中维护:任务名、栈大小、优先级、核心绑定都集中在这里。业务模块只调用 `app_task_start(APP_TASK_xxx, task_entry, arg, handle)`,不要直接调用 `xTaskCreate()`
当前已纳管任务包括:DNS、按键监控、BLE 心跳、MQTT 心跳、MQTT 初始化、MQTT 异常监控、OTA 信息延迟上报、UART 通信示例。NimBLE Host 任务通过 `app_task_start_nimble_host()` 统一从任务管理器启动。
## 3. 多设备扩展设计
为支持后续新增设备,项目按“内核分层”思路拆分为:
- `core`:系统初始化(类似内核 init)。
- `drivers`:驱动管理与硬件抽象(类似子系统层)。
- `drivers/.../devices`:具体设备实现(类似具体驱动)。
- `protocol/link/app`:协议、链路、业务层。
其中 `gpiotrol` 已拆分为“通用 PWM 底座 + 设备策略”:
- `main/drivers/gpiotrol/rc_pwm_control.c`:统一 RC 车 PWM 控制(含 6 路 50Hz 初始化、AUX 角色选择、PID 接口、策略分发)。
- `main/drivers/driver_manager.c`:统一初始化并绑定设备策略(业务层不直接碰具体驱动细节)。
- `main/drivers/gpiotrol/device_drive.h`:设备策略接口定义(`stop/control/shot`)。
- `main/drivers/gpiotrol/devices/device_1201.c`:1201 设备控制映射。
- `main/drivers/gpiotrol/devices/device_1101.c`:1101 设备控制映射。
当前统一引脚约定(均为 50Hz):
- 驱动芯片1:`IO10``IO21`
- 驱动芯片2:`IO11``IO12`
- AUX:`IO15``IO16`(在 `menuconfig` 中分别选择舵机/电调)
`IO15/IO16` 初始化规则:
- 设为舵机:初始化到 90 度(1500us)
- 设为电调:初始化到 1500us 中位
新增设备建议流程:
1. 新建 `device_xxxx.c/.h`,实现 `device_xxxx_get_ops()`
2.`rc_pwm_control.c` 的型号选择逻辑里增加映射。
3.`main/CMakeLists.txt` 加入对应源码文件。
4. 保持 `remote_control` 协议层不变,避免影响 App/BLE/WiFi 上层。
## 4. 构建类型与优化选项(Debug / Release)
本项目支持两种构建类型,通过 `menuconfig``robot-esp32s3`**编译优化级别** 选择:
| 构建类型 | 优化级别 | 日志输出 | 适用场景 |
|----------|----------|----------|----------|
| **Debug** | -Og | 完整 ESP_LOG 输出 | 开发调试 |
| **Release** | -Os | **默认关闭 UART 控制台**;W/E 经 BLE **0xFFE3** Notify | 量产发布 |
### 4.1 配置说明
```bash
idf.py menuconfig
# → robot-esp32s3 → 编译优化级别
# - Debug(Og 优化,调试)
# - Release(Os 优化,最小体积)
```
### 4.2 Release 模式下串口使用选项
Release 默认(`sdkconfig.defaults.release`):**`CONFIG_ESP_CONSOLE_NONE`**,不向 UART 打印;告警/错误通过 **0xFFE3** 即时 Notify(见 BLE 文档 §3.5)。
仅在 **Release** 构建时可选手动启用:
> **调试打印串口作为普通通信串口使用(仅 Release 有效)**
> - 默认:未选中 → 无 UART 日志(推荐量产)
> - 选中 → **UART0** 由 `uart_comm` 接管做普通收发
本项目使用 **UART0**(默认下载/调试串口):
- TX = GPIO43,RX = GPIO44(ESP32-S3 固定)
- Debug 模式:正常打印日志
- Release + 通信模式:UART0 转为普通通信口,可用 `uart_comm.h` 收发数据
### 4.3 构建命令示例
**Debug 版本(默认):**
```bat
idf.py build
```
**Release 版本(Os 优化):**
```bat
# 方式1:在 menuconfig 中选择 Release,然后构建
idf.py build
# 方式2:使用 Release 配置覆盖文件(CI/CD 推荐)
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
## 5. 文档索引(docs/)
项目文档统一放在 `docs/` 目录,按需查阅:
| 文档 | 内容 |
|------|------|
| [`安装与配网指南.md`](docs/安装与配网指南.md) | **安装必读**:环境、编译烧录、三种模式配网页与按键配网、量产包 |
| [`Android端设备对接文档_v1.0.1.md`](docs/Android端设备对接文档_v1.0.1.md) | **Android 必读**(固件 **1.0.1**):**WiFi/MQTT** §8、**BLE** §2~§6、**UART** §7 |
| `Android端蓝牙对接文档.md` | 旧名索引,请改用 `Android端设备对接文档_v1.0.1.md` |
| `firmware/README.md` | **Git 发布固件包**:OTA 用 `ESPRCCar.bin`、量产烧录用 `factory/` 全套 bin(无需克隆者本地编译) |
| `编译类型与UART模式配置指南.md` | Debug/Release 构建、UART0 调试口复用、日志配置 |
### 5.1 BLE 功能速查(基于 NimBLE)
- **0xFFE1**:UTF-8 JSON 控制通道(与 MQTT 共用 `remote_control` 协议)
- **0xFFE2**:OTA 固件二进制流(opcode: 0x01 开始、0x02 数据、0x03 结束),受 `APP_BLE_OTA` 控制
- **0xFFE3**:心跳(`message_type=1`,约 3s)+ 告警/错误(`4`/`5`,出现时即时 Notify)
- **0xFFE4**:OTA 状态 JSON(Read + Notify,每包应答进度)
> 详见 `docs/Android端设备对接文档_v1.0.1.md` §3.2 ~ §3.6
## 6. 构建与烧录(Windows)
完整步骤(环境、链路模式、配网、量产)见 **[`docs/安装与配网指南.md`](docs/安装与配网指南.md)**
```bat
cd /d C:\Users\17122\esp\v5.5.1\esp-idf
call export.bat
cd /d d:\myproject\esp32project\Android_control_driver
idf.py build
idf.py -p COM16 flash
```
默认链路为 **UART**`sdkconfig.defaults``CONFIG_APP_LINK_UART=y`)。切换模式后建议 `idf.py fullclean` 再编译。
如串口连接失败,请优先检查:数据线、端口号、下载模式(BOOT/RESET)。
## 7. 配网速查
| 模式 | 热点页 | 必填 |
|------|--------|------|
| WiFi | `index.html` | WiFi SSID/密码 + 设备 ID |
| BLE | `index_ble.html` | 设备 ID + 蓝牙广播名 |
| UART | `index_uart.html` | **仅设备 ID** |
手机连热点 `esp32-apconfig` → 打开 **http://192.168.4.1**。UART 模式长按 **GPIO4** 约 2 秒可重新配网。
## 8. 固件文件与烧录方式
### 8.1 编译生成的文件
执行 `idf.py build` 后,`build/` 目录生成关键文件:
| 文件 | 说明 | 用途 |
|------|------|------|
| `ESPRCCar.bin` | **应用固件**(6MiB 以内) | **BLE OTA 使用此文件**;串口烧录也用它 |
| `ESPRCCar.elf` | ELF 调试文件 | GDB 调试、崩溃分析 |
| `bootloader/bootloader.bin` | 启动引导程序 | 串口烧录时需要 |
| `partition_table/partition-table.bin` | 分区表 | 串口烧录时需要 |
| `flash_args` / `flasher_args.json` | 烧录参数 | 内含各文件地址 |
> 项目名 `ESPRCCar` 来自 `CMakeLists.txt` 中的 `project(ESPRCCar)`
### 8.2 串口烧录(USB 线刷)
**方式1:idf.py(推荐开发阶段)**
```bat
idf.py -p COM16 flash :: 烧录应用 + 分区表 + bootloader
idf.py -p COM16 flash_app :: 只烧应用(保留原分区表)
idf.py -p COM16 flash_monitor :: 烧录并打开串口监控
```
**方式2:esptool.py(生产/离线烧录)**
```bat
:: 整片烧录(含 bootloader、分区表、应用)
esptool.py --chip esp32s3 --port COM16 write_flash 0x0 build/ESPRCCar.bin
:: 或按分区烧录(参考 build/flash_args 中的地址)
esptool.py --chip esp32s3 --port COM16 write_flash \
0x1000 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0x20000 build/ESPRCCar.bin
```
**进入下载模式**
- 按住 **BOOT** 键 → 按 **RESET** 键 → 松开 **BOOT**
- 或 ESP32-S3 某些开发板自动进下载模式
### 8.3 OTA 升级
本项目支持 **BLE GATT OTA**(通过手机 App):
| 方式 | 通道 | 文件 | 说明 |
|------|------|------|------|
| BLE OTA | 0xFFE2 | `firmware/release/ota/ESPRCCar.bin`(或本地 `build/ESPRCCar.bin`) | 手机分包推送,设备写入 OTA 分区后重启 |
**BLE OTA 流程**(手机端):
1. 连接设备 → 使能 0xFFE4 Notify(进度/状态)
2. 发送 `0x01` + 4字节固件长度(小端)
3. 循环发送 `0x02` + 数据包(每包 ≤511 字节)
4. 发送 `0x03` 结束,设备校验后自动重启
> 详细协议见 `docs/Android端设备对接文档_v1.0.1.md` **§3.4~3.4.11**(含 MTU、ATT 错误、排错)及 **§5.7**(Android 测试步骤);UART 见 **§7**;WiFi/MQTT 见 **§8**
**注意**
- OTA 文件必须是 **应用镜像**`.bin`),不是合并后的整片 flash 镜像
- 文件大小必须 ≤ 分区表中的 OTA 分区大小(当前 6MiB)
- 当前 BLE OTA 开关由 `APP_BLE_OTA` 控制(默认启用)
### 8.4 生产烧录建议
**批量烧录方案**(推荐直接使用仓库内已编译包,见 `firmware/README.md`):
1. **从 Git 下载** `firmware/release/factory/` 下全部 `.bin`,或克隆仓库后进入该目录。
2. **Flash Download Tools**(乐鑫官方 GUI 工具)
- 加载 `bootloader.bin``partition-table.bin``ESPRCCar.bin``ota_data_initial.bin``storage.bin`
- 地址见 `firmware/release/factory/flash_args.txt`(与 `build/flash_args` 一致,Bootloader 在 `0x0`
3. **乐鑫 Flash Download Tools**:加载 `firmware/release/factory/` 下各 bin,地址见 `flash_args.txt``firmware/README.md`
4. **更新 Git 上的 bin**`idf.py build` 后双击或运行 `scripts\copy_firmware_release.bat`**不会**在每次编译时自动执行),核对 `firmware/release/VERSION.txt``manifest.json` 再提交
3. **JTAG 烧录**(Debug 调试时)
- 使用 ESP-Prog 或内置 JTAG
- `idf.py gdb` / `idf.py openocd`
# 文档已更名
# 文档已更名
Android 对接说明已合并为 **三种链路模式**(WiFi + MQTT / BLE / UART),请使用:
**[Android端设备对接文档_v1.0.1.md](./Android端设备对接文档_v1.0.1.md)**
旧文件名 `Android端蓝牙对接文档_v1.0.1.md` 仅保留 BLE 章节含义,已不再单独维护。
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
| 项 | 值 |
|----|-----|
| **固件版本** | `1.0.1``CONFIG_MY_APP_VERSION`,心跳 `body.version` 同值) |
| **文档版本** | `1.0.1`(与固件对齐;文件名 `Android端设备对接文档_v1.0.1.md`) |
| **协议/GATT 基线** | BLE: 服务 `0xFFE0`;特征 **FFE1~FFE4****已移除 FFE5**<br>UART: 串口 JSON + `\n`<br>WiFi: MQTT JSON |
| **芯片 / 分区** | ESP32-S3,16MB Flash;OTA 镜像 `ESPRCCar.bin` ≤ 6MiB |
| **推荐构建** | Release(`sdkconfig.defaults.release`):无 UART 日志,W/E 走 **0xFFE3** Notify |
本文档面向 Android 应用开发者,说明如何与本仓库 **ESP32-S3 固件**进行连接与数据交互。
固件支持**三种链路模式**(编译期三选一):
| 模式 | 宏定义 | 通信方式 | 本文档章节 |
|------|--------|----------|-----------|
| **WiFi + MQTT** | `CONFIG_APP_LINK_WIFI` | 数据流量/热点配网,MQTT 通信 | §8 |
| **BLE(蓝牙)** | `CONFIG_APP_LINK_BLE` | 低功耗蓝牙 GATT 通信 | §2~§6 BLE章节 |
| **UART(串口)** | `CONFIG_APP_LINK_UART` | FT232/Type-C 转串口,有线通信 | §7 UART章节 |
三种模式的**控制/心跳 JSON 协议完全相同**,仅物理层不同。OTA 机制:WiFi 模式走 HTTP,BLE/UART 模式走二进制流(opcode 0x01/0x02/0x03)。
协议以固件源码为准:`main/link_ble/link_ble.c``main/link_uart/link_uart.c``main/link_wifi/mqttconf_commun.c``main/protocol/remote_control.c``main/protocol/heart_payload.c`**固件的 `idf.py` 编译与烧录由设备/产线侧自行完成,本文不展开构建步骤。**
**快速导航**
| 链路 | 入口章节 |
|------|----------|
| **BLE** | §2 广播连接 → §3 GATT → §3.4 OTA → §5 Android 示例 → §6 检查清单 |
| **UART** | §7 硬件/串口 → §7.5 OTA → §7.8 检查清单 |
| **WiFi + MQTT** | §8 热点配网 → §8.2 MQTT 主题与 JSON → §8.3 HTTP OTA |
### 随包交付物(给 Android 联调/升级)
与本文 **v1.0.1** 一并提供的二进制建议来自 `firmware/release/`(运行 `scripts\copy_firmware_release.bat` 后生成):
| 文件 | 用途 |
|------|------|
| **`release/ota/ESPRCCar.bin`** | **BLE / UART** 二进制 OTA 包;**WiFi** 模式由云端 HTTPS 下发 URL 升级 |
| **`release/VERSION.txt`** | 版本号、编译时间、OTA 文件 SHA256 |
| **`release/manifest.json`** | 同上 + factory 各 bin 校验 |
| **本文档** | `Android端设备对接文档_v1.0.1.md`(含 WiFi / BLE / UART 三种链路) |
联调前请确认:固件 `menuconfig` 链路模式与 App 一致;心跳 `body.version`**`1.0.1`**。BLE 模式另需按 §3.5 解析 **0xFFE3**`message_type` **1 / 4 / 5**(勿再订阅已删除的 **0xFFE5**)。
### 相对旧版 App 的协议变更摘要(v1.0.1)
- **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。
- **心跳仍在 `0xFFE3`**`message_type` **1**,周期默认 **3s**,字段见 §3.5-A。
- **Release 固件默认无串口日志**;诊断依赖 BLE Notify,勿依赖 USB 监视器。
- **设备无 WS2812 状态灯**,日志与连接状态请仅依据 JSON / App UI。
- **手机 → 设备** 写入 **0xFFE1**`message_type` **4** 仍为 **GPIO `pin_setctrl`**(与 **0xFFE3** 上设备→手机的 **4/5** 方向不同,勿混淆)。
---
## 1. 固件侧能力与前提
### 1.1 链路模式(编译期三选一)
固件通过 `menuconfig``esp32s3_MYSDK`**链路模式** 选择。**同一时间仅有一种模式被编译进固件**(未选中的 `link_wifi` / `link_ble` / `link_uart` 源码不会链接),可节省 Flash。
| 模式 | 通信方式 | 配网方式 | OTA 方式 | 本文档章节 |
|------|----------|----------|----------|-----------|
| **WiFi + MQTT** | STA 连路由器,MQTT 收发 JSON | SoftAP 热点写入 WiFi + `device_id` | 云端 JSON 触发 **HTTPS** 下载 | §8 |
| **BLE(NimBLE)** | GATT 特征值收发 JSON | SoftAP 写入 `device_id` + `ble_adv_name` | **0xFFE2** 二进制流 | §2~§6 |
| **UART(串口)** | UART1(GPIO17 TX / GPIO18 RX)JSON + `\n` | SoftAP 仅写入 `device_id`;长按 GPIO4 重置 | 与 BLE 相同 opcode **0x01/0x02/0x03** | §7 |
默认示例配置见 `sdkconfig.defaults`(当前为 **`CONFIG_APP_LINK_UART=y`**)。
**BLE** 链路下还可通过:
- **`APP_BLE_OTA`**:启用/关闭 **0xFFE2** 固件推送(默认 `CONFIG_APP_BLE_OTA=y`
关闭 `APP_BLE_OTA` 后向 **0xFFE2** 写入将返回 **Write Not Permitted**
**Release 构建**:默认关闭 UART 控制台(`CONFIG_ESP_CONSOLE_NONE`);固件 `W`/`E` 日志在 BLE 已连接时经 **0xFFE3** 即时 Notify 推送(与周期心跳共用特征值,见 §3.5),或经 UART 发送(见 §7.5)。
### 1.2 配网(SoftAP 热点)与各模式必填项
三种模式在未完成必填 NVS 时,均会进入 **SoftAP 配网页**(默认热点名见 `CONFIG_ROBIOT_WIFI_SSID`,一般为 `esp32-apconfig`),手机连接后访问 **`192.168.4.1`** 提交表单。
| 链路 | 配网 HTML(SPIFFS) | 页面上显示的字段 |
|------|---------------------|------------------|
| WiFi | `www/index.html` | WiFi 名称/密码 + 设备 ID |
| BLE | `www/index_ble.html` | 设备 ID + 蓝牙广播名(**无** WiFi 项) |
| UART | `www/index_uart.html` | **仅设备 ID****无** WiFi、无蓝牙名) |
| NVS 键 | WiFi 模式 | BLE 模式 | UART 模式 |
|--------|-----------|----------|-----------|
| `device_id` | 必填 | 必填 | 必填 |
| `wifi_ssid` / `wifi_pass` | 必填 | — | — |
| `ble_adv_name` | — | 必填 | — |
> 安装、烧录与改配网页后如何刷新 SPIFFS,见 [`安装与配网指南.md`](安装与配网指南.md)。
**各模式启动条件**`main/app/app_run.c`):
- **WiFi**:有 `wifi_ssid` 则 STA 联网并拉取 MQTT 配置;无则进配网。
- **BLE**`device_id``ble_adv_name` 均非空才启动广播;否则进配网。
- **UART**`device_id` 非空则启动 UART1;否则进配网。正常运行时**不连 WiFi、不启 BLE**
**按键**:UART 模式仅 **GPIO4** 长按约 2 秒可再次进入配网;WiFi/BLE 另有 GPIO0 等逻辑见 `wifidevnum_config.c`
### 1.3 设备号(`device_id`)与蓝牙广播名(`ble_adv_name`)的对应关系
固件**不会**在运行时根据 `device_id` 自动计算 `ble_adv_name`;两项均在配网页分别写入 NVS。为便于用户从机身标贴上的**标准设备号**联想到如何扫描蓝牙,建议产线/Android 端采用**统一命名约定**(以下仅为**推荐规则**,截取细节以你们出厂脚本为准)。
| 概念 | 示例 | 说明 |
|------|------|------|
| 标准 **`device_id`** | `CN110200000001` | 前缀(如 `CN`)+ **型号四位** `1102` + **序列**(如 `000001`)等;完整串用于 MQTT 主题、心跳 `device_ID`、以及固件按第 3~6 字符选择驱动策略(见 `device_model.h`)。 |
| NVS **`ble_adv_name`(基础名)** | `110201` | 建议由 **型号 `1102`** + **序列简写**(如将 `000001` 规范为两位 `01`)拼接;须为 **ASCII**,且预留固件追加 **2 字节后缀 `MP`** 后仍不超过广播/缓冲区限制(见 §2.2)。 |
| 手机扫描到的 **完整本地名** | `110201MP` | **仅**在基础名后追加 **`MP`**(字母 **M****P**),由 `link_ble.c` 实现;若基础名已以 `MP` 结尾则不再重复追加。 |
**后缀拼写**:对外广播与 App 过滤一律以 **`MP`** 为准,**无 `PM` 等其它后缀**
### 1.4 车型 1102:舵机不动时先查 GPIO16 编译角色
**1102** 的左转/右转在固件里**固定使用 `GPIO16` 的舵机 PWM**`device_1102.c``rc_pwm_set_aux_servo_angle_deg(16, …)`)。但 `rc_pwm_control.c` 规定:**仅当该引脚在 menuconfig 中被配置为「舵机 / SERVO」时才会更新占空比**;若配置为 **电调 / ESC**,则函数直接返回,**LEDC 脉宽不会随 JSON 变化**,现象就是 **BLE 已收到 `pwm_ctrl`、日志正常,舵机仍不转**
- 修改路径:`idf.py menuconfig`**`esp32s3_MYSDK`****`GPIO16 PWM 角色`** → 选 **「舵机(初始化 90 度 / 1500us)」**(对应 `CONFIG_APP_PWM_IO16_SERVO=y`)。
- 仓库默认:`sdkconfig.defaults` 已按 **1102 接转向舵机** 将 GPIO16 设为 **SERVO**;若你们产线该脚接电调,请改回 **ESC** 并勿用 1102 的 IO16 转向映射。
---
## 2. BLE 广播与连接参数
### 2.1 广播类型
- **可连接、非定向**`BLE_GAP_CONN_MODE_UND`
- **通用可发现**`BLE_GAP_DISC_MODE_GEN`
- 广播字段含:**通用发现标志****BR/EDR 不支持****完整本地名称**(与下方名称一致)
### 2.2 设备名(扫描过滤依据)
- GAP 设备名与广播中的 **Complete Local Name** 均来自 NVS 键 **`ble_adv_name`**,再经固件追加 **`MP`**(见 §1.3、§2.4)。
- 若未配置或读失败,固件默认基础名为 **`ESP32-BLE`**,完整扫描名一般为 **`ESP32-BLEMP`**
- 名称缓冲区长度为 **32 字节**(含结尾 `'\0'` 的 C 字符串语义),应用侧扫描过滤时请使用 UTF-8 字节长度,并注意经典广播包对名称长度的限制(通常完整名称不宜过长,以免无法放入 31 字节 AD 结构)。
### 2.3 地址
- 使用 **Public 地址** 发起广播与连接(`BLE_OWN_ADDR_PUBLIC`)。
### 2.4 广播名后缀 `MP`(便于 App 过滤)
固件在设置 GAP / 广播 **Complete Local Name** 时,会在 NVS 中的 `ble_adv_name`**自动追加后缀 `MP`**(若已以 `MP` 结尾则不再追加)。若名称过长会截断后再追加,以保证仍落在缓冲区与广播包长度限制内。
**Android 扫描过滤建议**:使用 `ScanRecord.getDeviceName()` 或过滤条件 **`endsWith("MP")` / `contains("MP")`**,以区分本协议设备与其它 BLE 广播(手机端无需在系统蓝牙设置里填写名称,按广播名过滤即可)。
---
## 3. GATT 服务与特征
所有 UUID 均为 **Bluetooth SIG 16-bit UUID**(客户端构造为 `0000ffe0-0000-1000-8000-00805f9b34fb` 形式)。
### 3.1 服务
| 名称 | UUID(16-bit) |
|------|----------------|
| 主服务 | `0xFFE0` |
### 3.2 特征
| 名称 | UUID | 属性 | 说明 |
|------|------|------|------|
| 控制通道 | `0xFFE1` | Write、Write Without Response | 写入 **UTF-8 JSON 文本**,单次最大 **2048 字节**(固件侧缓冲区限制) |
| OTA 通道 | `0xFFE2` | Write、Write Without Response | **二进制分包 OTA**(见 **§3.4、§3.4.5~3.4.11**);若 `CONFIG_APP_BLE_OTA` 未启用则写入返回 **Write Not Permitted**(ATT `0x03`) |
| 心跳/告警通道 | `0xFFE3` | Read、Notify | **周期心跳**`message_type=1`,默认 3s)+ **即时告警/错误**`4`/`5`,见 §3.5);Read 为最近一次 Notify 的 UTF-8 JSON |
| OTA 状态 | `0xFFE4` | Read、Notify | **OTA 过程 JSON 状态**:Read 为最近一次状态串;成功处理 **0x01 / 每包 0x02 / 0x03 收尾** 后会 **Notify**(需订阅 CCCD) |
### 3.3 控制通道(0xFFE1)写入语义
- 仅处理 **Write / Write Without Response**;不支持 Read、Notify、Indicate。
- 载荷为 **一段完整 JSON 字符串**(固件用 `cJSON_Parse` 解析)。**不支持跨多包 ATT 写自动拼帧**——若未在更高层自行拼包,请保证 **单次写入即完整 JSON**
- 固件使用 **2049 字节** 缓冲(含预留 `'\0'`),有效 JSON 长度受 **2048** 限制。
### 3.4 OTA 通道(0xFFE2)与手机侧文件
#### 3.4.1 应使用哪个文件
- 使用与本工程 **相同分区表****同一芯片与 Flash 配置**`idf.py build` 生成的 **应用镜像**即可。
- 本仓库 CMake `project(ESPRCCar)`
- **从 Git 下载(推荐联调/量产)****`firmware/release/ota/ESPRCCar.bin`**(见 `firmware/README.md`
- **本地刚编译****`build/ESPRCCar.bin`**
**待写入 OTA 分区的纯固件镜像**(不是整片 `merged.bin` / 不是带 Bootloader 的合并包)。
- 镜像 **字节长度**必须 **不大于** OTA 应用分区大小。当前 `partitions.csv``ota_0` / `ota_1` 各为 **0x600000(6 MiB)**,且需 **16 MB Flash**(见 `sdkconfig.defaults`)。
#### 3.4.2 二进制协议(0xFFE2)
首字节为操作码(固件总长度使用 **小端 uint32**):
| Opcode | 载荷 | 含义 |
|--------|------|------|
| `0x01` | 后跟 **4 字节** `uint32_t` 固件总长度(**小端**) | 开始 OTA 会话,长度须与随后所有 `0x02` 数据之和一致 |
| `0x02` | 后跟 **1~511 字节**原始固件数据 | 顺序写入一块镜像数据(整包 ATT 写长度 ≤512,故数据最多 511 字节) |
| `0x03` | 无额外数据 | 结束会话:校验已写字节等于 `0x01` 声明长度 → `esp_ota_end` → 切换启动分区 → **延迟后重启** |
单包拷贝上限 **512 字节**(含 opcode)。建议在手机端 **协商较大 MTU** 后再传大包,但仍须满足上述单包上限。
#### 3.4.3 「传一包、应一包」
1. **ATT 写应答**:对 **0xFFE2** 使用 **带响应的 Write**`WRITE_TYPE_DEFAULT`)时,从机返回 **Write Response** 即表示本包已被固件处理完毕(opcode 合法且 `0x02` 已写入 Flash 缓冲);若返回错误 ATT 码则表示本包失败,应重试或中止会话。
2. **JSON 状态 Notify(推荐与日志联调)**:订阅 **`0xFFE4`** 的 Notify 后,固件在 **`0x01` 成功、`0x02` 每成功一包、`0x03` 成功或失败路径** 会推送短 **UTF-8 JSON**(见 §3.6)。App 可在 `onCharacteristicChanged` 中解析 `written` 等字段做进度条。
若使用 **Write Without Response**,则无 ATT 层「单包确认」,请依赖 **0xFFE4** 或应用层超时重传策略。
#### 3.4.4 与 menuconfig 的关系
- **`CONFIG_APP_BLE_OTA=y`**(默认):允许 **0xFFE2** OTA,并推送 **0xFFE4** 状态。
- 关闭 **`APP_BLE_OTA`****0xFFE2** 一律 **Write Not Permitted****0xFFE4** 仍可发现,但 OTA 流程不会产生状态 Notify。
#### 3.4.5 会话状态机(必读)
固件在 `link_ble.c` 中维护 **单次 OTA 会话**,规则如下:
| 规则 | 说明 |
|------|------|
| 顺序 | 必须先 **`0x01`** 成功,再发若干 **`0x02`**,最后 **`0x03`** |
| 新会话 | 再次写入 **`0x01`****中止上一会话**`esp_ota_abort`)并重新开始 |
| `0x02` 前置 | 未 `begin` 就发 `0x02` / `0x03` → ATT **`0x0E`(UNLIKELY)** |
| 长度一致 | 所有 `0x02` 载荷之和 **必须等于** `0x01` 声明的 `image_size`,否则 `0x03` 返回 **`incomplete`** |
| 溢出 | 累计写入超过声明长度 → **`overflow`**,会话被中止,需重新 `0x01` |
| `0x02` 空数据 | 仅 opcode、无数据字节 → **`0x0E`(UNLIKELY)** |
| 结束重启 | `0x03` 成功:`esp_ota_end``esp_ota_set_boot_partition` → 约 **2 秒** 后自动重启 |
| 断开连接 | BLE 断开 **不会** 自动 `abort` 未完成的 OTA 句柄;建议 App 断连前发 `0x03` 失败则重新 `0x01` 全量重传 |
目标分区:`esp_ota_get_next_update_partition(NULL)``ota_0` / `ota_1` 双分区交替写入,与 USB 烧录分区表一致)。
**OTA 支持模式**:BLE 和 UART 模式使用**完全相同的 OTA 二进制协议**(opcode 0x01/0x02/0x03)。
| 传输模式 | 发送通道 | 状态回复通道 | 数据格式 |
|----------|----------|--------------|----------|
| BLE | Write 0xFFE2 | Notify 0xFFE4 | 原始二进制 |
| UART | 串口发送 | 串口接收 JSON | 原始二进制 |
**OTA 失败回退机制**
- 固件内置 OTA 状态管理器,启动时自动检测上次 OTA 是否成功
- 如果新固件连续启动失败(最多3次),自动回退到上一版本
- 心跳 JSON 中 `body.version` 字段可用于确认当前运行版本
#### 3.4.6 单包字节布局(示例)
设固件文件 `ESPRCCar.bin` 大小为 **1,048,576**(0x00100000)字节:
**① 开始(5 字节)**
| 偏移 | 值 | 含义 |
|------|-----|------|
| 0 | `0x01` | begin |
| 1~4 | `00 00 10 00` | `image_size` 小端 = 1048576 |
**② 数据(1 + N 字节,N ≤ 511)**
| 偏移 | 值 | 含义 |
|------|-----|------|
| 0 | `0x02` | chunk |
| 1~N | 固件原始字节 | 按文件顺序,**不可跳变、不可重复计数** |
**③ 结束(1 字节)**
| 偏移 | 值 | 含义 |
|------|-----|------|
| 0 | `0x03` | end(无后续字节;多带字节会被忽略) |
#### 3.4.7 MTU 与每包数据长度
固件单包处理缓冲 **512 字节**(含 opcode),因此:
- **`0x02` 最大数据长度** = `min(511, ATT有效载荷 - 1)`
- **ATT 有效载荷**`negotiatedMtu - 3`(ATT 头 3 字节)
| 协商 MTU | 约等于每包固件数据 | 约需 `0x02` 包数(1MiB 镜像) |
|----------|-------------------|------------------------------|
| 23(默认) | 19 字节 | ~55000(极慢,务必协商 MTU) |
| 247 | 243 字节 | ~4300 |
| 517 | 511 字节 | ~2050 |
**Android 建议**
1. `connectGatt``requestMtu(517)`,在 `onMtuChanged` 里用 **实际 MTU** 计算 `chunkSize = min(511, mtu - 4)`
2. OTA 阶段使用 **`WRITE_TYPE_DEFAULT`(带响应)**,在 `onCharacteristicWrite` 收到 **GATT_SUCCESS** 后再发下一包 `0x02`
3. 进度条用 **0xFFE4**`written` / `expect`,不要假设每包 Notify 都到达(可丢包,以 Write 成功为准)。
#### 3.4.8 ATT 错误码(写 0xFFE2 失败时)
| ATT 码 | 宏名(常见) | 典型原因 |
|--------|--------------|----------|
| `0x03` | Write Not Permitted | `CONFIG_APP_BLE_OTA=n` |
| `0x0D` | Invalid Attribute Value Length | `0x01` 长度字段不全;`0x02` 导致累计超长 |
| `0x0E` | Unlikely | 未 begin 就 chunk/end;`0x02` 无数据;Flash 写入失败;`esp_ota_begin/end` 失败等 |
App 在 `onCharacteristicWrite(status != GATT_SUCCESS)` 时应 **停止发送** 并提示用户;可 Read **0xFFE4** 最近一次 JSON 查 `err` 字段。
#### 3.4.9 0xFFE4 状态 JSON 完整字段
| `ota` 字段 | `ok` | 其它字段 | 含义 |
|------------|------|----------|------|
| `begin` | `1` | `expect` | 会话已开始,可发 `0x02` |
| `begin` | `0` | `err` | `size` / `no_partition` / `esp_ota_begin` |
| `chunk` | `1` | `written` | 已累计写入字节(从 0 递增) |
| `chunk` | `0` | `err` | `overflow` / `write` |
| `end` | `1` | — | 即将重启(设备约 300 ms 内断开) |
| `end` | `0` | `err` | `incomplete` / `esp_ota_end` / `set_boot` |
**注意**:每个成功的 `0x02` 都会 Notify 一次 `chunk`(大包时 Notify 较密),UI 可做节流,仅每 N 包刷新一次。
#### 3.4.10 OTA 测试前置(固件侧)
1. `menuconfig`**链路模式 = BLE****启用 BLE GATT OTA**`APP_BLE_OTA=y`)。
2. `idf.py build` 生成待测镜像:**`build/ESPRCCar.bin`**(非 `flash_project_args` 整片包)。
3. 确认 `.bin` 大小 **≤ 6 MiB**`partitions.csv``ota_0`/`ota_1``0x600000`)。
4. 设备已配网:`device_id``ble_adv_name` 非空,能扫描到 `*MP` 广播名。
5. **首次 USB 烧录****OTA 包** 须为 **同一分区表、同一芯片目标(esp32s3)**;否则可能 `esp_ota_end` 失败或启动异常。
#### 3.4.11 常见失败与排查
| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 写 0xFFE2 立即 `0x03` | 未开 `APP_BLE_OTA` | 重编固件并确认 `sdkconfig` |
| `begin` / `esp_ota_begin` | 镜像大于 OTA 分区、分区表不匹配 | 换小镜像或改 `partitions.csv` 后全量重烧 |
| `chunk` / `overflow` | 多发数据或 `image_size` 填错 | 重新 `0x01`,严格按文件大小 |
| `end` / `incomplete` | 少发字节就 `0x03` | 核对 `written` 与文件 MD5/长度 |
| `end` / `esp_ota_end` | 镜像损坏、非合法 app bin | 确认是 `ESPRCCar.bin` 应用镜像 |
| OTA 后版本未变 | OTA 失败、回退或设备重启未连上同一设备 | 读 **0xFFE3** 心跳 `body.version` 确认;检查OTA失败回退日志 |
| 传输极慢 | MTU 仍为 23 | `requestMtu` 并等 `onMtuChanged` 后再传 |
| 中途断连 | 距离/干扰 | 靠近设备;实现断点续传需 App 侧重新 `0x01` 全量 |
### 3.5 心跳 / 告警通道(0xFFE3)Notify
- **属性**:Read + Notify(手机须对 **0xFFE3** 写 CCCD 开启 Notify)。
- **两类载荷共用本特征值**(通过 `head.message_type` 区分;Android 在 `onCharacteristicChanged` 中分支解析):
#### A. 周期心跳(设备 → 手机,约 3s 一条)
- 生成逻辑:`heart_payload_json_malloc()``main/protocol/heart_payload.c`)。
- `head.message_type` = **1**
- `body`**BLE 不含 `ip`**;MQTT 仍带 STA `ip`):
- **`voltage`**:两位小数字符串,电压值(见下方电压计算说明)
- **`device_ID`**`app2dev/<device_id>`
- **`version`**`CONFIG_MY_APP_VERSION`(当前运行固件版本,用于判断OTA是否成功)
- **周期**:默认 **3000 ms**`BLE_HEART_PERIOD_MS`,编译期可改)。
**示例:**
```json
{
"head": { "message_type": 1 },
"body": {
"voltage": "12.34",
"device_ID": "app2dev/CN110200000001",
"version": "1.0.0"
}
}
```
**电压计算说明**
固件通过 **GPIO5** 的 ADC 采样,使用分压电阻(上拉100kΩ + 下拉10kΩ)测量电池电压。
计算公式:
```
V电池 = VADC × 分压系数
分压系数 = (R上拉 + R下拉) / R下拉 = (100k + 10k) / 10k = 11
因此:V电池 = ADC读数 × 11
```
- 电池电压范围:支持约 0~36V(受限于ESP32-S3 ADC输入范围和分压比)
- 测量精度:约 ±0.1V(受ADC精度和分压电阻精度影响)
**OTA 版本确认**
- 心跳中的 `body.version` 字段表示当前运行的固件版本
- OTA 成功后设备重启,重新连接后读取心跳中的 `version`,应与目标版本一致
- 如果版本未变化,可能是 OTA 失败或自动回退到上一版本
#### B. 告警 / 错误(设备 → 手机,**出现时立即另发一条**,与心跳无关)
- **仅 Release 构建**且 BLE 已连接时,固件对 `ESP_LOGW` / `ESP_LOGE` 触发即时 Notify(不向 UART 打印)。
- `head.message_type` = **4**(Warning)或 **5**(Error)
- `body`**仅**字段 **`msg`**(字符串,为日志正文,通常为 `TAG: 说明`
**告警示例(`message_type = 4`):**
```json
{
"head": { "message_type": 4 },
"body": { "msg": "LINK_BLE: heartbeat notify rc=8" }
}
```
**错误示例(`message_type = 5`):**
```json
{
"head": { "message_type": 5 },
"body": { "msg": "DRV_MGR: init failed" }
}
```
> **与 §4.4 区分**:**手机 → 设备** 写入 **0xFFE1** 时 `message_type = 4` 表示 **GPIO `pin_setctrl`**;**设备 → 手机** 经 **0xFFE3 Notify** 的 `message_type = 4/5` 表示 **告警/错误日志**,方向与特征值均不同。
- **Read**:返回**最近一次**经 0xFFE3 Notify 的 JSON(可能是心跳,也可能是告警/错误)。
- **离线**:未连接期间不缓存日志。
### 3.6 OTA 状态通道(0xFFE4)
- **属性**:Read + Notify。Read 返回 **最近一次** OTA 状态 JSON(UTF-8);Notify 在 OTA 各步骤更新(需对 **0xFFE4** 写 CCCD 开启 Notify)。
- **与 0xFFE3 区分**`0xFFE3` 承载心跳与告警 JSON;`0xFFE4` 仅在 **OTA 会话** 内随 **0xFFE2** 变化。`onCharacteristicChanged` 中按 UUID 与 `message_type` 分支。
- **Notify 载荷示例**(字段以固件为准,以下为典型形状):
- 开始成功:`{"ota":"begin","ok":1,"expect":<uint>}`
- 开始失败:`{"ota":"begin","ok":0,"err":"no_partition"}``"esp_ota_begin"``"size"`
- 数据包成功:`{"ota":"chunk","ok":1,"written":<uint>}``written` 为已累计写入总字节)
- 数据包失败:`{"ota":"chunk","ok":0,"err":"overflow"}``"write"`
- 结束成功(重启前):`{"ota":"end","ok":1}`
- 结束失败:`{"ota":"end","ok":0,"err":"incomplete"}``"esp_ota_end"``"set_boot"`
---
## 4. 应用层 JSON 协议(写入 0xFFE1)
解析入口:`remote_control_apply_json()``main/protocol/remote_control.c`)。
### 4.1 基本结构
必须包含顶层对象 **`head`**,且为 JSON **对象**;否则:
- **仅 WiFi 构建**`CONFIG_APP_LINK_WIFI`):会走 MQTT/OTA 相关分支。
- **仅 BLE 构建****静默丢弃**(不执行控制)。
有效消息需满足:
- `head.message_type`**数字**
- **`message_type == 1`(远程重启)**:仅需合法 **`head`****不要求**顶层 **`body`**;解析成功后固件立即 `esp_restart()`(与设备型号无关;**MQTT 与 BLE 写入同一套 JSON 时同样生效**)。
- **`message_type == 3` / `4`**:须存在顶层 **`body`**(类型未强制校验为 object,但下文字段均按 object 使用)。
> **与 §3.5 心跳区分**:设备经 **0xFFE3 Notify** 下发的业务心跳里 `message_type` 也为 **1**,那是**设备→手机**方向;此处 **1** 指**手机/云端→设备**写入 **0xFFE1**(或 MQTT 载荷)时的**控制语义**:收到即重启。
### 4.2 `message_type == 1`:远程重启
- **行为**:校验 JSON 含对象 **`head`**`head.message_type` 为数字 **1** 后,**立即重启**(不解析 `body`)。
- **用途**:全型号统一远程复位;请谨慎使用(无额外鉴权时,能发 JSON 的客户端均可触发)。
**示例:**
```json
{
"head": {
"message_type": 1
}
}
```
### 4.3 `message_type == 3`:PWM 控制
`body` 内对象键:**`pwm_ctrl`**(对象)。
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `mode` | number | 是 | 控制模式,见 4.5 节设备映射 |
| `val` | number | 是 | 控制量,与 `mode` 联合解释 |
| `type` | number | 否 | 固件读取;若缺省或非数字,按 **0** 处理(当前映射逻辑未使用 `type` 分支) |
### 4.4 `message_type == 4`:GPIO 脉冲/shot
`body` 内对象键:**`pin_setctrl`**(对象)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `pin` | number | 引脚号 |
| `val` | number | 电平/值 |
**注意**:当前默认设备策略 **1101 / 1201**`shot` 实现为 **空操作**`device_1101.c` / `device_1201.c``(void)pin; (void)val;`)。若未来某型号实现 `shot`,协议形状已预留。
### 4.5 `pwm_ctrl.mode` 与 `val`(1101/1201 当前映射)
两型号在固件中 **control 映射一致**(见 `device_1101.c` 注释:复用 1201 映射)。
| `mode` | 含义(概括) | `val` 行为摘要 |
|--------|----------------|----------------|
| `1` | 电机 A 侧(与 B 互斥) | `val < 50`:双电机 0%;否则电机 A 占空按 `val/2 - 10`,B 为 0 |
| `2` | 电机 B 侧(与 A 互斥) | `val < 50`:双电机 0%;否则电机 B 占空按 `val/2 - 10`,A 为 0 |
| `3` | 舵机角度(一个方向) | `val < 45`:90°;`val < 70``50 + val + 7` 度;否则 135° |
| `4` | 舵机角度(另一方向) | `val < 45`:90°;`val < 70``130 - val - 7` 度;否则 45° |
PWM 控制底座为 `rc_pwm_control`(统一 50Hz),引脚定义:
- 驱动芯片1:`IO10``IO21`
- 驱动芯片2:`IO11``IO12`
- AUX:`IO15``IO16`(通过 `menuconfig` 选择舵机/电调)
`IO15/IO16` 初始化:
- 若设为舵机:90 度(1500us)
- 若设为电调:1500us 中位
> 注:当前 `device_1101` / `device_1201` 的 `mode=1/2` 主要使用 `IO10/IO11` 输出,`IO21/IO12` 在底座层已初始化并预留给后续车型策略。
### 4.6 JSON 示例
**PWM(例如左电机通道,mode=1):**
```json
{
"head": {
"message_type": 3
},
"body": {
"pwm_ctrl": {
"mode": 1,
"type": 0,
"val": 80
}
}
}
```
**GPIO 协议形状(当前 1101/1201 无实际 shot 效果):**
```json
{
"head": {
"message_type": 4
},
"body": {
"pin_setctrl": {
"pin": 10,
"val": 1
}
}
}
```
### 4.7 错误与静默失败
- JSON 无法解析(`cJSON_Parse` 失败):**直接返回**,无错误码回传(BLE GATT Write Success 仍可能返回,属应用层协议问题)。
- 缺少 `head` / `message_type` 非数字 / 缺 `body`**不执行控制**(BLE 构建下无其它副作用)。
---
## 5. Android 实现要点
### 5.1 权限与系统能力
- Android 12(API 31)及以上:需 **`BLUETOOTH_SCAN`****`BLUETOOTH_CONNECT`** 等运行时权限;扫描时若需定位行为,仍须遵守系统对 **位置权限** 的要求(依目标 SDK 与厂商策略而定)。
- 在 Manifest 中声明硬件:`android.hardware.bluetooth_le`
- 使用手机 **BLE Central** 角色连接外围设备。
### 5.2 推荐连接流程
1. 确认已授予蓝牙与(若需要)位置权限。
2. **扫描**:设备广播完整名为 NVS 中 **`ble_adv_name` + 固件自动追加的 `MP`**(默认 `ESP32-BLEMP`)。`ScanRecord.getDeviceName()` 建议过滤 **`endsWith("MP")`****`contains("MP")`**(注意 UTF-8 与截断)。
3. **连接**`device.connectGatt(context, false, callback)`(是否自动连接按产品需求)。
4. **发现服务**`BluetoothGatt.discoverServices()`
5.`onServicesDiscovered` 中取得 **Service `0000ffe0-...`**,再取得 **Characteristic `0000ffe1-...`**
6. **协商 MTU**(建议):`requestMtu(517)`(实际以 `onMtuChanged` 回调为准)。单包 ATT 最大载荷与 MTU 相关;在 MTU 较小时,**完整 JSON 必须仍能通过一次 Write 送达**,否则需固件侧支持拼包(当前不支持)。
7. 将 JSON 转为 **UTF-8 字节数组**,长度 ≤ **2048**,调用 `writeCharacteristic`
- 特征支持 **Write Without Response**,可使用 `WRITE_TYPE_NO_RESPONSE` 以降低延迟(需确认 `PROPERTY_WRITE_NO_RESPONSE`)。
8. **心跳 / 告警**:对 **`0xFFE3`** 开启 **Notify**(CCCD `0x0001`)。在 `onCharacteristicChanged` 中按 `head.message_type` 解析:**1**=周期心跳(§3.5-A),**4**=告警、**5**=错误(§3.5-B,Release 固件即时推送)。
9. **OTA(可选)**:对 **`0xFFE4`** 开启 **Notify**,再按 **§3.4****`0xFFE2`** 顺序写入 `0x01` → 多包 `0x02``0x03`;建议带响应 Write 并解析 **§3.6** JSON。
### 5.3 UUID 常量参考(Kotlin)
```kotlin
val SVC_ROBOT = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")
val CHR_CONTROL = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
val CHR_OTA = UUID.fromString("0000ffe2-0000-1000-8000-00805f9b34fb")
val CHR_HEARTBEAT = UUID.fromString("0000ffe3-0000-1000-8000-00805f9b34fb")
val CHR_OTA_STATUS = UUID.fromString("0000ffe4-0000-1000-8000-00805f9b34fb")
```
### 5.4 断开与重连
固件在断开后会 **自动重新启动广播**`BLE_GAP_EVENT_DISCONNECT``ble_gap_adv_start`)。应用断开后可再次扫描连接。
### 5.5 与 WiFi 版协议的关系
同一套 **`remote_control_apply_json`** 协议也用于 MQTT 链路;BLE 模式编译下无 MQTT。设计 App 时可将「JSON 命令构造层」复用,仅将发送路径替换为 **GATT Write**
### 5.6 BLE OTA 参考实现(Kotlin 骨架)
下列逻辑与固件 `gatt_svr_chr_ota_access` / `ble_ota_*` 一致,便于直接联调;需自行接入 `BluetoothGatt`、权限与线程。
```kotlin
// CCCD:对 0xFFE4 开启 Notify
val CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
fun enableOtaStatusNotify(gatt: BluetoothGatt, otaStat: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(otaStat, true)
val cccd = otaStat.getDescriptor(CCCD) ?: return
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)
}
/** 根据协商 MTU 计算每包固件数据上限(不含 opcode 0x02) */
fun otaChunkDataSize(mtu: Int): Int = minOf(511, (mtu - 3) - 1)
/** 组包:0x01 + imageSize LE */
fun buildBeginPacket(imageSize: Int): ByteArray {
require(imageSize > 0)
return byteArrayOf(
0x01,
(imageSize and 0xFF).toByte(),
((imageSize shr 8) and 0xFF).toByte(),
((imageSize shr 16) and 0xFF).toByte(),
((imageSize shr 24) and 0xFF).toByte()
)
}
/** 组包:0x02 + slice */
fun buildChunkPacket(slice: ByteArray, offset: Int, len: Int): ByteArray {
val out = ByteArray(1 + len)
out[0] = 0x02
System.arraycopy(slice, offset, out, 1, len)
return out
}
fun buildEndPacket(): ByteArray = byteArrayOf(0x03)
/** 解析 0xFFE4 Notify(可用 org.json 或 Gson) */
fun parseOtaStatusJson(json: String): OtaStatus? {
// 期望字段:ota, ok, expect?, written?, err?
// 示例:{"ota":"chunk","ok":1,"written":65536}
...
}
/**
* 发送顺序(须在主线程/GATT 队列中串行):
* 1. writeCharacteristic(CHR_OTA, begin, WRITE_TYPE_DEFAULT)
* 2. onCharacteristicWrite 成功 → 循环 write chunk,直到 fileOffset == imageSize
* 3. write end
* 4. 收到 0xFFE4 {"ota":"end","ok":1} 或断连后重新扫描,读 0xFFE3 version 校验
*/
```
**`onCharacteristicChanged` 分支示例**
```kotlin
when (characteristic.uuid) {
CHR_HEARTBEAT -> {
val json = JSONObject(String(value, Charsets.UTF_8))
when (json.getJSONObject("head").getInt("message_type")) {
1 -> { /* §3.5-A 心跳 */ }
4, 5 -> { /* §3.5-B 告警/错误 */ val msg = json.getJSONObject("body").getString("msg") }
}
}
CHR_OTA_STATUS -> { parseOtaStatusJson(String(value, Charsets.UTF_8)) }
}
```
### 5.7 OTA 联调推荐顺序(Android 测试)
1. 扫描连接 → `discoverServices``requestMtu(517)`
2.**0xFFE4** 写 CCCD 开 Notify;可选开 **0xFFE3** 便于升级后看 `version`
3. 从 assets 或下载目录读取 **`ESPRCCar.bin`**,记录 `fileSize`
4.**0xFFE2** `buildBeginPacket(fileSize)`,等 **Write Success**,并确认 Notify:`{"ota":"begin","ok":1,"expect":...}`
5.`otaChunkDataSize(mtu)` 循环 **0x02**,每包等 Write Success(或观察 `written` 递增)。
6.**0x03**,等 Notify `{"ota":"end","ok":1}`,设备重启。
7. 重连后读心跳 **`body.version`**,应与新固件 `CONFIG_MY_APP_VERSION` 一致。
---
## 6. 联调检查清单
- [ ] 固件已烧录 **BLE 链路** 构建,且 NVS 已写入非空 **`device_id`****`ble_adv_name`**
- [ ] 手机蓝牙已打开,应用已获 **扫描/连接** 权限。
- [ ] 扫描到的设备名以 **`MP` 结尾**(或包含 `MP`),与固件广播规则一致。
- [ ] 已发现 **0xFFE0** 服务及 **0xFFE1****0xFFE3** 特征;已对 **0xFFE3** 开启 **Notify** 并能收到心跳 JSON。
- [ ] (OTA)固件 **`CONFIG_APP_BLE_OTA=y`**,镜像为 **`build/ESPRCCar.bin`**,大小 **≤ 6 MiB**,与设备 **分区表/芯片** 一致。
- [ ] (OTA)已对 **0xFFE4** 写 CCCD 开启 **Notify****0xFFE2** 使用 **带响应 Write**,且 **等上一包 Success 再发下一包**
- [ ] (OTA)已 `requestMtu(517)``chunk` 长度 ≤ `min(511, mtu-4)`
- [ ] (OTA)流程:**0x01(5B)→ 多包 0x02 → 0x03(1B)**`0x01` 中长度与文件字节数一致。
- [ ] (OTA)收到 `{"ota":"end","ok":1}` 后设备重启;重连后 **0xFFE3**`version` 已更新。
- [ ] (OTA)失败时能读到 **0xFFE4**`err``overflow` / `incomplete` / `esp_ota_end` 等)或 ATT **0x0D / 0x0E**
- [ ] (OTA)固件支持失败自动回退:如果新固件启动失败(最多3次),自动回退到上一版本。
- [ ] 心跳 `body.voltage` 显示正确的电池电压值(分压系数=11,GPIO5 ADC采样)。
- [ ] (Release)`onCharacteristicChanged` 能区分 **0xFFE3**`message_type` **1 / 4 / 5**(告警与心跳同特征值)。
- [ ] 已协商足够 **MTU**(控制 JSON ≤2048 字节时亦建议协商,避免未来扩展踩坑)。
- [ ] 单次写入的 UTF-8 长度 **≤ 2048**,且为 **合法完整 JSON**
- [ ] `head.message_type``body` 字段与本文 **第 4 节** 一致。
- [ ] 若固件关闭了 **`APP_BLE_OTA`**,勿向 **0xFFE2** 发送 OTA 数据(将返回 Write Not Permitted)。
---
## 7. UART 串口模式(FT232/Type-C 转串口)
固件支持通过 **UART 串口** 与 Android 设备通信,使用 **FT232****Type-C 转串口** 模块连接。此模式下协议与 BLE 模式**完全一致**(相同的 JSON 格式),仅物理层从 BLE GATT 改为 UART 串口。
### 7.1 硬件连接(FT232/Type-C 转串口)
使用 **FT232RL****CH340** 等 USB 转 TTL 串口模块,通过 Type-C OTG 转接线连接 Android 设备:
| ESP32-S3 引脚 | 功能 | 连接至 FT232 模块 | 说明 |
|--------------|------|------------------|------|
| **GPIO17** | U1TXD | RXD (接收) | ESP32 发送 → FT232 接收 |
| **GPIO18** | U1RXD | TXD (发送) | ESP32 接收 ← FT232 发送 |
| **GND** | 地线 | GND | **必须共地** |
**连接示意图:**
```
Android 手机 (Type-C OTG)
│ Type-C OTG 线
┌─────────────┐
│ FT232 模块 │
│ (USB-TTL) │
└─────────────┘
│ RXD ────────┐
│ TXD ────┐ │
│ GND ──┐ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────┐
│ ESP32-S3 │
│ GPIO17(TX) ─────┐ │
│ GPIO18(RX) ─┐ │ │
│ GND ────────┼───┼───┘
└──────────────┘ │
车辆控制板
```
### 7.2 UART 通信参数
| 参数 | 值 | 说明 |
|------|-----|------|
| **波特率** | 115200 | 可在 menuconfig 中配置 `CONFIG_APP_UART_LINK_BAUDRATE` |
| **数据位** | 8 bits | - |
| **校验位** | None | - |
| **停止位** | 1 bit | - |
| **流控** | None | 无硬件流控 |
| **帧格式** | JSON + `\n` | 每条 JSON 以换行符 `\n` 结尾 |
### 7.3 Android 串口开发指南
Android 端可使用以下库进行 USB 串口通信:
**推荐库:**
- [usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android)(开源,支持 FT232/CH340/CP2102 等)
**Gradle 依赖:**
```gradle
implementation 'com.github.mik3y:usb-serial-for-android:3.4.0'
```
**基本使用示例:**
```kotlin
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
class UartConnectionManager(private val context: Context) {
private var port: UsbSerialPort? = null
private var ioManager: SerialInputOutputManager? = null
// 查找并连接 FT232 设备
fun connectToDevice(): Boolean {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
val driver = availableDrivers.firstOrNull {
it.device.productName?.contains("FT232", ignoreCase = true) == true ||
it.device.productName?.contains("USB Serial", ignoreCase = true) == true
} ?: return false
val connection = manager.openDevice(driver.device) ?: return false
port = driver.ports[0]
port?.open(connection)
// 配置串口参数(必须匹配固件配置)
port?.setParameters(115200, UsbSerialPort.DATABITS_8,
UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
// 启动 I/O 管理器监听接收数据
ioManager = SerialInputOutputManager(port, object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray?) {
data?.let { processReceivedData(it) }
}
override fun onRunError(e: Exception?) {
Log.e("UART", "Serial error", e)
}
})
ioManager?.start()
return true
}
// 发送 JSON 命令(自动添加换行符)
fun sendJsonCommand(json: String) {
val data = (json + "\n").toByteArray(Charsets.UTF_8)
port?.write(data, 1000)
}
// 处理接收数据(按换行符分包)
private val rxBuffer = StringBuilder()
private fun processReceivedData(data: ByteArray) {
val text = String(data, Charsets.UTF_8)
rxBuffer.append(text)
// 按换行符分割 JSON
var newlineIndex: Int
while (rxBuffer.indexOf("\n").also { newlineIndex = it } >= 0) {
val json = rxBuffer.substring(0, newlineIndex)
rxBuffer.delete(0, newlineIndex + 1)
if (json.isNotBlank()) {
parseJsonFromDevice(json)
}
}
}
// 解析设备发来的 JSON(与 BLE 0xFFE3 相同格式)
private fun parseJsonFromDevice(json: String) {
try {
val obj = JSONObject(json)
val head = obj.getJSONObject("head")
val messageType = head.getInt("message_type")
when (messageType) {
1 -> { /* 心跳消息 - 同 §3.5-A */ }
4 -> { /* 告警消息 */ }
5 -> { /* 错误消息 */ }
}
} catch (e: Exception) {
Log.e("UART", "JSON parse error", e)
}
}
fun disconnect() {
ioManager?.stop()
port?.close()
}
}
```
### 7.4 UART 模式与 BLE 模式协议对比
| 功能 | BLE 模式 (GATT) | UART 模式 (串口) |
|------|----------------|-----------------|
| **控制指令下发** | Write 0xFFE1 | 发送 JSON + `\n` |
| **心跳接收** | Notify 0xFFE3 | 接收 JSON + `\n` |
| **告警/错误** | Notify 0xFFE3 (message_type 4/5) | 接收 JSON + `\n` (message_type 4/5) |
| **JSON 格式** | 完全相同 | 完全相同 |
| **连接检测** | `onConnectionStateChange` | 5秒内未收到数据认为离线 |
**示例 - 发送控制指令(UART):**
```kotlin
// 与 BLE 0xFFE1 完全相同的 JSON
val controlJson = """
{
"head": {"message_type": 1},
"body": {
"pwm_ctrl": {"throttle": 50, "steer": 30}
}
}
""".trimIndent()
uartManager.sendJsonCommand(controlJson)
```
**示例 - 接收心跳(UART):**
```kotlin
// 固件每3秒自动发送,与 BLE 0xFFE3 相同格式
{"head":{"message_type":1},"body":{"device_ID":"110201","voltage":12.5,"percent":80}}
```
### 7.5 UART OTA(固件升级)
UART 模式支持与 BLE 模式**完全相同的 OTA 二进制协议**(opcode 0x01/0x02/0x03)。
**OTA 状态回复**(通过串口返回 JSON + `\n`):
```json
{"ota":"begin","ok":1,"expect":1048576}
{"ota":"chunk","ok":1,"written":8192}
{"ota":"end","ok":1} // 完成后自动重启
```
### 7.6 设备重置(配网模式)
UART 模式下,设备通过 **长按重置按键**(GPIO4,硬件上对应某个物理按键)进入配网模式:
- **长按 2 秒**(GPIO4 接地持续 2 秒):进入 SoftAP 配网模式
- 此时设备开启 WiFi 热点(SSID: `esp32-apconfig`
- Android 连接热点后访问 `192.168.4.1` 配置新的 `device_id`
- 配置完成后设备重启并恢复 UART 通信
**注意:** UART 模式下无需配置 WiFi 密码或蓝牙广播名,仅需配置 `device_id`
### 7.7 调试建议
1. **FT232 模块选择**:推荐使用带 **TX/RX LED 指示灯** 的模块,便于观察数据收发
2. **波特率匹配**:确保 Android 端设置的波特率与固件 `CONFIG_APP_UART_LINK_BAUDRATE` 一致(默认 115200)
3. **数据格式**:每条 JSON 必须以换行符 `\n` 结尾,否则固件无法正确解析
4. **OTG 权限**:Android 需要申请 USB 设备权限才能访问 FT232 模块
```xml
<!-- AndroidManifest.xml -->
<uses-feature android:name="android.hardware.usb.host" />
<uses-permission android:name="android.hardware.usb.usb_accessory" />
```
### 7.8 检查清单(UART 模式)
- [ ] 固件已烧录 **UART 链路** 构建(`CONFIG_APP_LINK_UART=y`),且 NVS 已写入非空 **`device_id`**
- [ ] FT232/CH340 模块与 ESP32-S3 正确连接:GPIO17→RXD, GPIO18→TXD, GND→GND。
- [ ] Android 手机支持 OTG 功能,已开启 OTG 开关。
- [ ] Android 应用已获 **USB 设备访问权限**
- [ ] 串口参数匹配:波特率 115200/8/N/1(与固件配置一致)。
- [ ] JSON 以换行符 `\n` 结尾,UTF-8 编码。
- [ ] 能收到周期心跳(默认 3 秒一次),`message_type=1`
- [ ] 发送控制 JSON 后车辆有响应(马达/舵机动作)。
- [ ] 心跳 `body.voltage` 显示正确电池电压值。
- [ ] 心跳 `body.version` 显示正确固件版本号。
- [ ] 5 秒内无数据则认为设备离线。
- [ ] 长按重置键 2 秒可进入配网模式修改 `device_id`
- [ ] (OTA)支持通过串口发送二进制数据升级固件(opcode 0x01/0x02/0x03)。
---
## 8. WiFi + MQTT 模式(热点配网 + 数据流量)
本章节适用于 `CONFIG_APP_LINK_WIFI=y` 的固件。设备作为 **MQTT 客户端** 连接 Broker,控制与心跳均为 **UTF-8 JSON**(与 BLE/UART 的 `remote_control` / `heart_payload` 结构一致)。
### 8.1 配网流程
1. 设备上电后若无 `wifi_ssid`,开启 SoftAP(见 §1.2)。
2. 手机连接设备热点,浏览器打开 `http://192.168.4.1`,填写 **WiFi SSID/密码****`device_id`**
3. 提交后设备重启,STA 连接路由器,从云端拉取 MQTT Broker 列表(`CONFIG_ROBOIOT_MQTT_URL` + `device_id`)。
4. 拉取失败时使用默认 Broker(固件内 `DEFAULT_MQTT_HOST`,见 `mqttconf_commun.c`)。
### 8.2 MQTT 主题与 JSON
连接成功后设备订阅(`device_id` 为 NVS 中的完整设备号):
| 方向 | 主题示例 | 说明 |
|------|----------|------|
| 云端/App → 设备 | `app2dev/<device_id>` | 遥控 JSON,走 `remote_control_apply_json` |
| 云端/App → 设备 | `ser2dev/<device_id>` | 同上(服务侧下发) |
| 设备 → 云端/App | `<device_id>` | 周期心跳(约 **3s**) |
| 设备 → 云端/App | `dev2app/<device_id>` | 同上副本 |
**心跳 JSON**`heart_payload_json_malloc`,含 STA `ip`)示例:
```json
{
"head": { "message_type": 1 },
"body": {
"ip": "192.168.1.100",
"voltage": "12.34",
"device_ID": "app2dev/CN110200000001",
"version": "1.0.1"
}
}
```
**控制 JSON**:与 §4 节 BLE **0xFFE1** 写入格式相同(`head` + `body`,含 `pwm_ctrl` 等)。
### 8.3 OTA(HTTPS,非 GATT/串口二进制)
WiFi 模式 OTA **不走** §3.4 的 `0x01/0x02/0x03`。云端通过 MQTT 下发 JSON,固件 `ota_update_recmqtt()` 解析后 **HTTPS 下载** 固件并写入 OTA 分区。
典型字段(见 `main/ota.c`):
| 字段 | 说明 |
|------|------|
| `new_version` | 目标版本;与当前 `CONFIG_MY_APP_VERSION` 相同则跳过 |
| `esp32_url` | 固件 `.bin` 的 HTTPS 下载地址 |
升级成功后设备重启;可通过心跳 **`body.version`** 或 MQTT 上报主题 **`esp32updata`** 核对版本。
### 8.4 检查清单(WiFi 模式)
- [ ] 固件为 **WiFi 链路** 构建(`CONFIG_APP_LINK_WIFI=y`)。
- [ ] NVS 已写入 **`wifi_ssid` / `wifi_pass` / `device_id`**
- [ ] 设备 STA 已获取 IP,MQTT 已连接。
- [ ] App/云端向 `app2dev/<device_id>` 下发控制 JSON 有响应。
- [ ] 约 3s 周期收到心跳,含 **`version`****`voltage`**
- [ ] OTA 使用 HTTPS URL,勿向 BLE 特征或串口发二进制 OTA 包。
---
## 9. 版本与维护
| 字段 | 说明 |
|------|------|
| **固件版本号** | menuconfig → `MY_APP_VERSION` → 编译宏 `CONFIG_MY_APP_VERSION`(当前 **`1.0.1`**) |
| **运行时可见** | 心跳 JSON → `body.version`(BLE **0xFFE3** / UART 串口 / WiFi MQTT 均同结构) |
| **OTA 校验** | WiFi:HTTPS 升级后读 `body.version`;BLE/UART:二进制 OTA 后读 `body.version`,失败可自动回退(见 §3.4) |
| **文档版本** | 与固件主版本一致;发版时复制/重命名为 `Android端设备对接文档_v<版本>.md` |
**发版检查(维护者)**
1. 修改 `MY_APP_VERSION``idf.py build``scripts\copy_firmware_release.bat`
2. 核对 `firmware/release/VERSION.txt``version=1.0.1`
3.`docs/Android端设备对接文档_v1.0.1.md``release/ota/ESPRCCar.bin`(及可选 `manifest.json`)一并交给 Android
- 文档生成依据:仓库内 `link_ble.c``link_uart.c``mqttconf_commun.c``ota.c``heart_payload.c``remote_control.c``app_run.c``Kconfig.projbuild``partitions.csv``README.md`
- 若固件修改 UUID、最大长度、JSON 字段或 OTA 行为,请 **递增版本号** 并同步更新本文档文件名与 §0 交付物表。
---
**文档路径**`docs/Android端设备对接文档_v1.0.1.md`
**配套固件**`firmware/release/ota/ESPRCCar.bin``CONFIG_MY_APP_VERSION="1.0.1"`**仅 BLE/UART 二进制 OTA 使用**
# 安装与配网指南
# 安装与配网指南
本文说明如何在本机编译、烧录 ESP32-S3 固件,以及三种链路模式下的**首次配网****重新配网**。Android 协议细节见 [`Android端设备对接文档_v1.0.1.md`](Android端设备对接文档_v1.0.1.md)
---
## 1. 环境准备
| 项 | 要求 |
|----|------|
| 芯片 | ESP32-S3 |
| ESP-IDF | **v5.5.x**(与团队 `.vscode/settings.json``idf.espIdfPathWin` 一致) |
| Flash | **16MB**(见 `partitions.csv`) |
| 系统 | Windows 10/11(下文以 PowerShell/CMD 为例) |
### 1.1 配置 ESP-IDF 环境
1. 安装 [ESP-IDF Windows 工具安装器](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/get-started/windows-setup.html),或确认本机已有 `esp\v5.5.x\esp-idf`
2. 将用户环境变量 **`IDF_PATH`** 指向真实的 `esp-idf` 根目录(勿使用已删除的旧路径)。
3. 每次编译前激活环境(二选一):
- 开始菜单 → **「ESP-IDF 5.5 PowerShell」** / **CMD**
- 项目目录执行:`call <IDF_PATH>\export.bat`(路径按本机修改)
若提示找不到 `idf.py`,参见 [`编译类型与UART模式配置指南.md`](编译类型与UART模式配置指南.md) §2.0。
### 1.2 克隆与进入工程
```bat
cd /d d:\myproject\esp32project\Android_control_driver
```
首次构建会由 `sdkconfig.defaults` 生成 `sdkconfig`;已存在 `sdkconfig` 时以本地保存的配置为准。
---
## 2. 选择链路模式(编译期三选一)
**`idf.py menuconfig`****`esp32s3_MYSDK`****链路模式** 中三选一。**同一时间仅一种模式编入固件**
| 模式 | menuconfig | 与 Android 通信 | 仓库默认(`sdkconfig.defaults`) |
|------|------------|-----------------|----------------------------------|
| WiFi + MQTT | `APP_LINK_WIFI` | 路由器 + MQTT | 否 |
| BLE | `APP_LINK_BLE` | NimBLE GATT | 否 |
| **UART 串口** | `APP_LINK_UART` | UART1:GPIO17=TX、GPIO18=RX | **是** |
切换模式后建议:
```bat
idf.py fullclean
idf.py build
```
> **注意**:`APP_LINK_UART`(业务串口 GPIO17/18)与 **UART0 调试口**(GPIO43/44,下载/日志)不是同一路。调试口说明见 [`编译类型与UART模式配置指南.md`](编译类型与UART模式配置指南.md)。
---
## 3. 编译与开发烧录
### 3.1 编译
```bat
cd /d d:\myproject\esp32project\Android_control_driver
idf.py build
```
**Release 量产构建**(可选):
```bat
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
### 3.2 USB 串口烧录(开发推荐)
```bat
idf.py -p COM16 flash monitor
```
| 命令 | 说明 |
|------|------|
| `idf.py -p COM16 flash` | 烧录 bootloader + 分区表 + 应用 + **SPIFFS(配网页)** |
| `idf.py -p COM16 flash_app` | 仅烧应用(**不更新** `www` 配网页,改 HTML 后勿单独用这个) |
| `idf.py -p COM16 erase_flash` | 擦除整片(清空 NVS,会重新进配网) |
**进下载模式**:按住 **BOOT** → 按 **RESET** → 松开 **BOOT**;部分开发板可自动进入。
烧录失败时请检查:数据线、COM 口、驱动、是否被占用。
### 3.3 修改配网页后
配网 UI 来自 `www/`,经 CMake 打入 SPIFFS 分区 `storage``main/CMakeLists.txt``spiffs_create_partition_image`)。
修改 `www/index*.html` 后须 **完整 `idf.py build` + `flash`**(或至少包含 `storage.bin` 的烧录),仅 `flash_app` **不会**更新配网页。
---
## 4. 配网(SoftAP + 网页)
三种模式在缺少必填 NVS 时,都会开启 **SoftAP 热点**(SSID 默认 `esp32-apconfig`,可在 menuconfig **`ROBITO WIFI SSID`** 修改)。手机连接热点后浏览器访问:
**http://192.168.4.1**
(设备侧 DNS 会劫持域名,直接输 IP 即可。)
### 4.1 各模式配网页与必填项
| 链路模式 | SPIFFS 页面 | 页面上需填写 | 写入 NVS |
|----------|-------------|--------------|----------|
| **WiFi** | `www/index.html` | WiFi 名称、WiFi 密码、设备 ID | `wifi_ssid``wifi_pass``device_id` |
| **BLE** | `www/index_ble.html` | 设备 ID、蓝牙广播名 | `device_id``ble_adv_name` |
| **UART** | `www/index_uart.html` | **仅设备 ID** | `device_id` |
**UART / BLE 模式说明**
- 热点仅用于**写入 NVS**,不是让设备长期连路由器。
- **UART 模式**配网页**不出现** WiFi 名称/密码;正常运行时不启 BLE、不连 STA。
- 提交表单后设备约 1.5s 后自动重启。
### 4.2 何时自动进入配网
| 模式 | 条件 |
|------|------|
| WiFi | NVS 无 `wifi_ssid` |
| BLE | `device_id``ble_adv_name` 为空 |
| UART | `device_id` 为空 |
### 4.3 运行时再次进入配网(按键)
| 模式 | 操作 |
|------|------|
| **UART** | **GPIO4** 长按约 **2 秒** → 开热点,仅可改 **设备 ID** |
| **WiFi** | **GPIO0** 长按约 2 秒 → 可改 WiFi + 设备 ID;**GPIO4** → 仅设备 ID |
| **BLE** | **GPIO0****GPIO4** 长按约 2 秒 → 可改设备 ID / 蓝牙名(逻辑见 `wifidevnum_config.c`) |
UART 模式下进入配网时会暂停 UART1 业务通信,配网完成后重启恢复。
---
## 5. 量产烧录(无需本地编译)
产线或测试人员可直接使用仓库内已编译包,无需安装 ESP-IDF:
1. 打开 [`firmware/README.md`](../firmware/README.md)
2. 下载 `firmware/release/factory/` 下全部 `.bin`
3. 使用 **乐鑫 Flash Download Tools**,按 `flash_args.txt` 地址烧录(含 `storage.bin` 配网页)。
维护者更新发布包:
```bat
idf.py build
scripts\copy_firmware_release.bat
```
核对 `firmware/release/VERSION.txt` 后再提交 Git。
---
## 6. 烧录后自检
| 模式 | 建议检查 |
|------|----------|
| **UART** | 串口 115200/8/N/1;GPIO17→模块 RX、GPIO18→模块 TX;能收到心跳 JSON;`device_id` 与 App 一致 |
| **BLE** | 能扫描到 `ble_adv_name` + 后缀 `MP`;GATT 0xFFE1 可写 JSON |
| **WiFi** | 串口日志见 STA 获 IP;MQTT 订阅成功 |
协议与引脚详见 [`Android端设备对接文档_v1.0.1.md`](Android端设备对接文档_v1.0.1.md)
---
## 7. 常见问题
| 现象 | 处理 |
|------|------|
| UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` |
| 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 |
| `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` |
| 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` |
---
**文档版本**:1.1
**最后更新**:2026年5月
# 编译类型与 UART 模式配置指南
# 编译类型与 UART 模式配置指南
本文档说明如何使用项目的 **Release/Debug 构建类型****调试串口复用** 功能。
> **安装、烧录、三种链路模式选择与 SoftAP 配网**(含 UART 仅填设备号)见 **[`安装与配网指南.md`](安装与配网指南.md)**。
> 本文中的「UART」若未特别说明,指 **UART0 调试口(GPIO43/44)**;与业务链路 **`APP_LINK_UART`(UART1,GPIO17/18)** 不是同一路。
---
## 0. 串口是什么?和板子上的 RX1、TX1 是什么关系?
日常说的 **「串口」** 一般指 **UART**(异步串行口):用一根 **TX(发)** 和一根 **RX(收)** 加上地线,按约定波特率(如 115200)收发字节流。
### 本项目使用 UART0(默认调试串口)
| 项目 | 值 | 说明 |
|------|-----|------|
| **UART** | UART0 | ESP32-S3 默认下载/调试串口 |
| **TX** | GPIO43 | 固定引脚,不可更改 |
| **RX** | GPIO44 | 固定引脚,不可更改 |
| **用途** | 调试打印 / 普通通信 | Release 模式下可切换 |
**UART0 是 ESP32-S3 的默认串口**,用于:
- 固件下载(boot 模式)
- `ESP_LOG` / `printf` 调试输出
- 本项目 Release 模式下可选为普通通信串口
---
## 1. 功能概述
通过 `menuconfig` 可以选择:
1. **Debug 构建**(默认):-Og 优化,完整调试日志输出
2. **Release 构建**:-Os 优化,最小代码体积,可选将 **指定 UART** 用作通信串口
---
## 2. 配置方法
### 2.0 PowerShell 提示找不到 `...\esp\esp-idf\tools\idf.py`
说明当前终端里 **`IDF_PATH` 或 `idf.py` 启动方式仍指向不存在的目录**(常见为旧路径 `C:\Users\<你>\esp\esp-idf`),而本机 ESP-IDF 实际在 **`...\esp\v5.5.1\esp-idf`**(与 `.vscode/settings.json`**`idf.espIdfPathWin`** 一致)。
**处理**:在 Windows **用户环境变量**中将 **`IDF_PATH`** 改为上述真实 **`esp-idf` 根目录**;打开「开始菜单」里的 **「ESP-IDF PowerShell / CMD」**(随 ESP-IDF 安装生成)再执行 `idf.py`,或删除 PATH 里指向旧 `esp-idf` 的重复项后重开终端。Cursor/VS Code 也可用扩展命令 **「ESP-IDF: Open ESP-IDF Terminal」**
### 2.1 使用 Menuconfig(推荐开发时使用)
```bash
idf.py menuconfig
# → robot-esp32s3
# → 编译优化级别
# - Debug(Og 优化,调试)
# - Release(Os 优化,最小体积)
#
# 选择 Release 后会出现额外选项:
# → 调试打印串口作为普通通信串口使用(仅 Release 有效)
```
### 2.2 使用 SDKCONFIG 覆盖文件(推荐 CI/CD 使用)
**Debug 版本:**
```bash
# 使用默认 sdkconfig.defaults
idf.py build
```
**Release 版本:**
```bash
# 方式1:在 menuconfig 中选择 Release 后保存
idf.py build
# 方式2:使用覆盖文件(自动化构建)
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
---
## 3. Release 模式下 UART 串口两种工作模式
### 3.1 模式 A:Release 默认(无 UART 日志,BLE 转发 W/E)
**配置:**
- `ROBO_APP_FW_RELEASE=y`
- 使用 `sdkconfig.defaults.release`(含 **`CONFIG_ESP_CONSOLE_NONE=y`**
- `APP_UART_MODE_DEBUG=n`(默认)
**行为:**
- **不向串口输出** `ESP_LOG`(避免无人读取时阻塞 BLE)
- `ESP_LOGW` / `ESP_LOGE` 在 BLE 已连接时经 **0xFFE3 Notify** 推送(`message_type` 4/5,见 `docs/Android端设备对接文档_v1.0.1.md` §3.5-B)
- 周期心跳仍为 **0xFFE3**,约 3s,`message_type` 1
### 3.2 模式 B:UART0 转为普通通信串口
**配置:**
- `ROBO_APP_FW_RELEASE=y`(menuconfig 中「编译优化级别」选 Release)
- `APP_UART_MODE_DEBUG=y`(启用此选项)
**行为:**
- **UART0**`uart_comm` 接管做普通收发(GPIO43/44)
- `ESP_LOG` / `printf` 等日志输出不可用(没有日志输出)
- 代码中使用 `uart_comm.h` 接口进行数据传输
- 固件下载仍使用 UART0(bootrom 固定)
**完整配置步骤:**
```bash
idf.py menuconfig
# 1. robot-esp32s3 → 编译优化级别 → Release
# 2. robot-esp32s3 → 调试打印串口作为普通通信串口使用 → 启用
# 3. Component config → ESP System Settings → Channel for console output → None
# 保存并退出
```
---
## 4. 代码中的条件编译
项目提供了 `build_config.h` 头文件,包含预定义的宏:
### 4.1 头文件位置
```c
#include "core/build_config.h"
```
### 4.2 可用宏定义
| 宏 | 含义 |
|----|------|
| `BUILD_IS_RELEASE` | 1=Release 构建,0=Debug 构建 |
| `BUILD_IS_DEBUG` | 1=Debug 构建,0=Release 构建 |
| `UART_MODE_COMMUNICATION` | 1=UART 作为通信串口,0=默认 |
| `UART_MODE_DEBUG` | 1=调试串口模式,0=通信模式 |
| `BUILD_SERIAL_LOG_ENABLED` | 1=可向 UART 打日志,0=Release 默认关闭 |
### 4.3 条件日志宏
无论何种模式,都可以使用统一的日志宏:
```c
LOG_I(TAG, "信息日志"); // Info 级别
LOG_W(TAG, "警告日志"); // Warning 级别
LOG_E(TAG, "错误日志"); // Error 级别
LOG_D(TAG, "调试日志"); // Debug 级别
```
**Release + UART 通信模式** 下,这些宏自动展开为空操作(不输出任何内容)。
在其他模式下,这些宏等价于 `ESP_LOGI` / `ESP_LOGW` / `ESP_LOGE` / `ESP_LOGD`
### 4.4 UART 通信 API(仅在 Release+通信模式下有效)
```c
#include "drivers/uart_comm/uart_comm.h"
/* 初始化(波特率,0 表示使用默认值 115200) */
esp_err_t err = uart_comm_init(115200);
/* 发送数据 */
uart_comm_send((uint8_t*)data, len);
/* 发送字符串 */
uart_comm_send_string("Hello\r\n");
/* 接收数据(非阻塞,timeout_ms 超时) */
uint8_t buf[256];
int len = uart_comm_receive(buf, sizeof(buf), 100); // 100ms 超时
/* 反初始化 */
uart_comm_deinit();
```
在非 Release+通信模式下,这些函数返回 `ESP_ERR_NOT_SUPPORTED` 或 -1。
---
## 5. 完整示例代码
### 5.1 条件编译示例
```c
#include "core/build_config.h"
#include "drivers/uart_comm/uart_comm.h"
void my_init(void)
{
#if BUILD_IS_RELEASE
#if UART_MODE_COMMUNICATION
/* Release + 通信模式:初始化 UART 通信 */
uart_comm_init(115200);
uart_comm_send_string("System started\r\n");
#else
/* Release + 调试模式:减少日志输出 */
ESP_LOGI(TAG, "Release build started");
#endif
#else
/* Debug 模式:完整日志 */
ESP_LOGI(TAG, "Debug build started");
#endif
}
```
### 5.2 使用统一日志宏(推荐)
```c
#include "core/build_config.h"
void my_function(void)
{
/* 在任何构建类型下都可以使用,自动适配 */
LOG_I(TAG, "This is info message");
LOG_W(TAG, "This is warning message");
LOG_E(TAG, "This is error message");
}
```
---
## 6. 构建输出对比
### 6.1 Debug 构建
```
[1/1] Linking project.elf
构建完成,固件大小约:
- .bin 文件:~2.5MB
- 内存占用:~180KB
```
### 6.2 Release 构建
```
[1/1] Linking project.elf
构建完成,固件大小约:
- .bin 文件:~2.1MB(减少约 15-20%)
- 内存占用:~150KB
```
---
## 7. 注意事项
### 7.1 从 Debug 切换到 Release 时
1. 执行完整清理:
```bash
idf.py fullclean
```
2. 如果使用 `sdkconfig.defaults.release`,确保重新生成 `sdkconfig`
```bash
rm sdkconfig
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
### 7.2 UART 通信模式下的调试
启用 `APP_UART_MODE_DEBUG` 后:
- **无法使用 `idf.py monitor` 查看日志**(因为没有日志输出)
- **烧录时仍然使用 UART0**,请确保外部设备在烧录时不会干扰
- 建议在开发阶段先用 **模式 A(调试打印)** 验证功能,再切换到 **模式 B(通信)**
### 7.3 推荐的开发流程
```
阶段1:功能开发
→ 使用 Debug 构建,完整日志输出
阶段2:性能优化
→ 使用 Release + 调试打印模式
→ 验证功能正确性,检查是否有 Release 模式特有 bug
阶段3:量产准备
→ 使用 Release + UART 通信模式(如需要)
→ 最终验证,关闭所有调试功能
```
---
## 8. 故障排除
### Q: 切换到 Release 后代码运行异常?
A: 检查是否有以下问题:
- 断言被禁用导致错误未捕获(`CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y`
- 代码依赖 Debug 模式下的未初始化变量清零(Debug 模式通常内存初始化为 0)
- 优化导致的时序问题(如未加 `volatile` 的硬件寄存器访问)
### Q: 启用 UART 通信模式后无法烧录?
A: 正常现象。启用 `CONFIG_ESP_CONSOLE_UART_NONE` 后固件启动时不会占用 UART0,但 **esptool 烧录过程不受影响**。烧录时确保:
- 外部设备未连接 UART0,或已断开
- 正确进入下载模式(BOOT 按钮)
### Q: 如何临时恢复日志输出进行调试?
A: 在 `menuconfig` 中:
1. 关闭 **调试打印串口作为普通通信串口使用**
2. 或重新选择 **Debug** 构建类型
---
**文档版本**:1.1
**最后更新**:2026年5月
# 固件发布包(Git 可下载)
本目录存放 **准备提交到 Git 的已编译二进制**,与本地 `build/` 分离:
| 目录 | 进 Git? | 何时更新 |
|------|----------|----------|
| `build/` | 否 | 每次 `idf.py build` 自动覆盖 |
| `firmware/release/` | **是** | **仅**在你手动运行复制脚本时更新(见下) |
> **`idf.py build` 不会自动往 `firmware/` 里拷文件。**
> 日常编译只用 `build/`;要发版、给产线/安卓下载时,编译成功后执行一次脚本即可。
## 版本信息(每次运行脚本会刷新)
| 文件 | 说明 |
|------|------|
| **`release/VERSION.txt`** | 人类可读:版本号、编译时间、OTA 文件大小与 SHA256 |
| **`release/manifest.json`** | 机器可读:同上 + 全部 factory 文件校验 |
| **`release/Android端设备对接文档_v<版本>.md`** | 与固件同版本的 Android 对接说明(WiFi/BLE/UART,脚本从 `docs/` 复制) |
版本号来自 menuconfig 项 **`CONFIG_MY_APP_VERSION`**`main/Kconfig.projbuild`,默认在 `sdkconfig.defaults` 里为 `1.0.1`)。发版前请先改版本再 `idf.py build`
## 目录说明
| 路径 | 内容 |
|------|------|
| `release/ota/ESPRCCar.bin` | **BLE OTA / Android 升级**(仅应用镜像) |
| `release/factory/*.bin` | **量产烧录**(乐鑫 Flash Download Tools 加载) |
| `release/factory/flash_args.txt` | 烧录地址摘要(与 `build/flash_args` 一致) |
## 烧录地址(ESP32-S3,16MB Flash)
| 文件 | 偏移 |
|------|------|
| `bootloader.bin` | `0x0` |
| `partition-table.bin` | `0x8000` |
| `ota_data_initial.bin` | `0xf000` |
| `ESPRCCar.bin` | `0x20000` |
| `storage.bin` | `0xc20000` |
Flash:`dio``80m``16MB`。产线请使用 **乐鑫 Flash Download Tools**,按上表地址加载 `factory/` 内各文件。
## 更新发布包(维护者)
```bat
idf.py build
scripts\copy_firmware_release.bat
```
脚本会刷新 **`VERSION.txt`****`manifest.json`**。打开 `VERSION.txt` 核对版本与时间;**Git 提交由你自行操作**,脚本不会执行任何 git 命令。
## 他人使用
- **OTA**:下载 `release/ota/ESPRCCar.bin`(版本看 `VERSION.txt`)。
- **Android 联调**:同目录下的 **`Android端设备对接文档_v<版本>.md`** + `VERSION.txt` / `manifest.json`
- **量产**:下载 `release/factory/` 全部 bin,用 Flash Download Tools 烧录。
- **安装与配网**(环境、三种链路、热点填什么):见仓库 [`docs/安装与配网指南.md`](../docs/安装与配网指南.md)
ESPRCCar firmware release
=========================
version: 1.0.1
project: ESPRCCar
chip: esp32s3
built_utc: 2026-05-19T07:53:23Z
built_local: 2026/05/19 ܶ 15:53:23.22
git_revision: (no git)
OTA:
path: firmware/release/ota/ESPRCCar.bin
bytes: 1115760
sha256: 724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6
Factory: firmware/release/factory/
tool: Espressif Flash Download Tools
addrs: see factory/flash_args.txt
machine_readable: manifest.json
--flash_mode dio --flash_freq 80m --flash_size 16MB
0x0 bootloader/bootloader.bin
0x20000 ESPRCCar.bin
0x8000 partition_table/partition-table.bin
0xf000 ota_data_initial.bin
0xc20000 storage.bin
{
"project": "ESPRCCar",
"chip": "esp32s3",
"flash_mode": "dio",
"flash_freq": "80m",
"flash_size": "16MB",
"version": "1.0.1",
"git_revision": "",
"generated_utc": "2026-05-19T07:53:23Z",
"ota_image": "release/ota/ESPRCCar.bin",
"factory_dir": "release/factory",
"files": [
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"},
{"name":"bootloader.bin","bytes":21056,"sha256":"740f1b3c78ca63acc0a5e3c788bf18f77585a28504e5d8722ac253991c6f1f7a"},
{"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"},
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"},
{"name":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"},
{"name":"storage.bin","bytes":327680,"sha256":"05be75d1159d4b3043e4b3847f7d8307b4a4955ae30c1baf93ce42eafb1308cf"}
]
}
set(SOURCES
"core/system_init.c"
"core/task_manager.c"
"provision/wifidevnum_config.c"
"device/device_model.c"
"protocol/remote_control.c"
"protocol/heart_payload.c"
"drivers/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/devices/device_1201.c"
"drivers/gpiotrol/devices/device_1101.c"
"drivers/gpiotrol/devices/device_1102.c"
"drivers/gpiotrol/betteryread.c"
"drivers/uart_comm/uart_comm.c"
"app/app_run.c"
"app/example_uart_comm_usage.c"
"main.c"
"ota_manager.c"
"ota_binary_stream.c")
if(CONFIG_APP_LINK_BLE)
list(APPEND SOURCES "link_ble/link_ble.c")
elseif(CONFIG_APP_LINK_UART)
list(APPEND SOURCES "link_uart/link_uart.c")
else()
list(APPEND SOURCES "link_wifi/mqttconf_commun.c" "ota.c")
endif()
set(INCLUDE_DIRS
"."
"core"
"device"
"provision"
"protocol"
"app"
"drivers"
"drivers/gpiotrol"
"drivers/gpiotrol/devices"
"drivers/uart_comm")
if(CONFIG_APP_LINK_BLE)
list(APPEND INCLUDE_DIRS "link_ble")
elseif(CONFIG_APP_LINK_UART)
list(APPEND INCLUDE_DIRS "link_uart")
else()
list(APPEND INCLUDE_DIRS "link_wifi")
endif()
set(MAIN_REQUIRES
driver
app_update
nvs_flash
esp_http_server
spiffs
esp-tls
esp_netif
esp_event
esp_wifi
esp_adc
json)
set(MAIN_PRIV_REQUIRES)
# 避免配置切换/缓存导致 main 编译 link_ble.c 时丢失 bt 头文件路径,
# 这里始终将 bt 放入 main 的私有依赖。
list(APPEND MAIN_PRIV_REQUIRES bt)
if(CONFIG_APP_LINK_WIFI)
list(APPEND MAIN_REQUIRES mqtt esp_https_ota esp_http_client)
endif()
idf_component_register(SRCS ${SOURCES}
INCLUDE_DIRS ${INCLUDE_DIRS}
REQUIRES ${MAIN_REQUIRES}
PRIV_REQUIRES ${MAIN_PRIV_REQUIRES})
spiffs_create_partition_image(storage ../www FLASH_IN_PROJECT)
menu "esp32s3_MYSDK"
config ROBIOT_WIFI_SSID
string "ROBITO WIFI SSID"
default "esp32-apconfig"
help
Hotspot Name Settings.
config ROBOIOT_MQTT_URL
string "MQTT URL"
default "https://fcrs-api.yd-ss.com/device/getConfig?deviceNo="
help
MQTT Config URL
config MY_APP_VERSION
string "Project Version"
default "1.0.1"
help
The version number of this firmware.
config APP_DEBUG_UART_NUM
int "调试与通信 UART 外设编号"
default 0
range 0 2
help
ESP32-S3 片上有多路 UART:UART0、UART1、UART2。
UART0 默认引脚 GPIO43(TX)/GPIO44(RX),用于下载和调试打印。
UART1 默认引脚 GPIO17(TX)/GPIO18(RX),在 UART 链路模式下用于与安卓通信。
本项目使用 UART0 作为调试/通信串口,Release 下可切换为普通通信模式。
ESP_LOG / printf 走哪一路 UART,由 menuconfig「Component config → ESP System Settings」
里控制台 UART 编号决定;请与本项保持一致。
config APP_DEBUG_UART_TX_GPIO
int "调试串口 TX GPIO 编号(UART0 无需设置,保持 -1)"
default -1
range -1 48
help
UART0 固定使用 GPIO43(TX)/GPIO44(RX),无需设置。
UART1 模式下使用 GPIO17(TX)。
此项保留用于兼容性,请保持为 -1。
config APP_DEBUG_UART_RX_GPIO
int "调试串口 RX GPIO 编号(UART0 无需设置,保持 -1)"
default -1
range -1 48
help
UART0 固定使用 GPIO43(TX)/GPIO44(RX),无需设置。
UART1 模式下使用 GPIO18(RX)。
此项保留用于兼容性,请保持为 -1。
config APP_UART_LINK_BAUDRATE
int "UART 链路模式波特率"
default 115200
range 9600 921600
help
UART 链路模式下的通信波特率。默认 115200。
适用于 APP_LINK_UART 模式下的 UART1 通信。
config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号"
default 17
range -1 48
help
UART 链路模式下 TX 引脚。UART1 默认使用 GPIO17。
ESP32-S3 UART1 默认 TX 为 GPIO17。
config APP_UART_LINK_RX_GPIO
int "UART 链路 RX GPIO 编号"
default 18
range -1 48
help
UART 链路模式下 RX 引脚。UART1 默认使用 GPIO18。
ESP32-S3 UART1 默认 RX 为 GPIO18。
choice APP_LINK_MODE
prompt "链路模式(编译期三选一)"
default APP_LINK_WIFI
help
WiFi:STA + MQTT。BLE:仅 NimBLE 与安卓 JSON,不走 STA/MQTT;配网页仅设备号与蓝牙广播名。
UART:使用串口1 (GPIO17 TX / GPIO18 RX) 与安卓通信,无需蓝牙或WiFi。
config APP_LINK_WIFI
bool "WiFi + MQTT"
help
保存 WiFi 后联网,MQTT 控制。
config APP_LINK_BLE
bool "BLE(NimBLE)"
help
热点网页仅写入 device_id 与 ble_adv_name;正常运行时不连 WiFi、不启 MQTT。
config APP_LINK_UART
bool "UART 串口(GPIO17 TX / GPIO18 RX)"
help
使用 UART1 (GPIO17=TX, GPIO18=RX) 与安卓进行串口通信。
无需蓝牙或WiFi,适合有线连接场景。
长按设备重置按键可进入配网模式重置设备号。
endchoice
config APP_BLE_OTA
bool "启用 BLE GATT OTA(0xFFE2)与状态 Notify(0xFFE4)"
depends on APP_LINK_BLE
default y
help
允许手机通过 0xFFE2 写入固件分包升级;每步成功后可通过 0xFFE4 Notify 收到 JSON 应答摘要。
关闭后向 0xFFE2 写入将返回 Write Not Permitted;0xFFE4 仍可发现,但无 OTA 过程推送。
choice APP_PWM_IO15_ROLE
prompt "GPIO15 PWM 角色"
default APP_PWM_IO15_SERVO
help
选择 GPIO15 在 50Hz PWM 下的用途。
config APP_PWM_IO15_SERVO
bool "舵机(初始化 90 度 / 1500us)"
config APP_PWM_IO15_ESC
bool "电调 ESC(初始化 1500us)"
endchoice
choice APP_PWM_IO16_ROLE
prompt "GPIO16 PWM 角色"
default APP_PWM_IO16_ESC
help
选择 GPIO16 在 50Hz PWM 下的用途。
config APP_PWM_IO16_SERVO
bool "舵机(初始化 90 度 / 1500us)"
config APP_PWM_IO16_ESC
bool "电调 ESC(初始化 1500us)"
endchoice
choice ROBO_APP_FW_BUILD
prompt "编译优化级别(固件 Debug/Release)"
default ROBO_APP_FW_DEBUG
help
注意:勿使用名称 APP_BUILD_TYPE,与 ESP-IDF 全局 Kconfig 冲突。
Debug:-Og;Release:-Os(另见 sdkconfig.defaults.release 或 menuconfig 编译器选项)。
config ROBO_APP_FW_DEBUG
bool "Debug(Og 优化,调试)"
help
优化级别 -Og,保留调试符号,适合开发和调试。
config ROBO_APP_FW_RELEASE
bool "Release(Os 优化,最小体积)"
help
优化级别 -Os,最小代码体积,适合量产发布。
默认关闭 UART 控制台(见 sdkconfig.defaults.release),W/E 日志经 BLE 0xFFE3 Notify 推送。
endchoice
config APP_UART_MODE_DEBUG
bool "调试打印串口作为普通通信串口使用(仅 Release 有效)"
depends on ROBO_APP_FW_RELEASE
default n
help
仅在 Release 版本下可用。启用后,由「调试与通信 UART 外设编号」指定的那一路 UART
不再作为日志输出,可由 uart_comm 做普通收发。禁用则保持调试打印。
注意:启用后 ESP_LOG / printf 等输出将不可用(建议同时关闭控制台串口)。
endmenu
#include "app_run.h"
#include "sdkconfig.h"
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "core/system_init.h"
#include "core/build_config.h"
#include "wifidevnum_config.h"
#include "device_nvs.h"
#include "driver_manager.h"
#include "ota_manager.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define OTA_CONFIRM_DELAY_MS 30000
#if CONFIG_APP_LINK_WIFI
#include "mqttconf_commun.h"
#endif
#if CONFIG_APP_LINK_BLE
#include "link_ble.h"
#endif
#if CONFIG_APP_LINK_UART
#include "link_uart.h"
#endif
extern void example_uart_comm_start(void);
static const char *tag_s = "APP";
#if CONFIG_APP_LINK_WIFI
static bool init_task_triggered_s = false;
static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)data;
ESP_LOGI(tag_s, "获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
char devid_check[32] = {0};
if (read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid_check, sizeof(devid_check)) == ESP_OK &&
strlen(devid_check) > 0) {
if (!init_task_triggered_s) {
ESP_LOGI(tag_s, "启动 MQTT");
mqtt_manager_init_sequence();
init_task_triggered_s = true;
}
} else {
ESP_LOGE(tag_s, "device_id 为空,无法启动 MQTT");
}
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGW(tag_s, "WiFi 断开,重连...");
esp_wifi_connect();
}
}
#endif
#if CONFIG_APP_LINK_WIFI
static void run_wifi_mode(void)
{
char ssid[32] = {0}, pass[64] = {0};
esp_err_t wifi_ret = read_from_nvs(DEVICE_CFG_KEY_WIFI_SSID, ssid, sizeof(ssid));
if (wifi_ret == ESP_OK) {
read_from_nvs(DEVICE_CFG_KEY_WIFI_PASS, pass, sizeof(pass));
esp_netif_create_default_wifi_sta();
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL);
esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &wifi_event_handler, NULL,
NULL);
button_monitor_task_init();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
wifi_config_t sta_cfg = {
.sta =
{
.scan_method = WIFI_ALL_CHANNEL_SCAN,
.sort_method = WIFI_CONNECT_AP_BY_SIGNAL,
},
};
strncpy((char *)sta_cfg.sta.ssid, ssid, sizeof(sta_cfg.sta.ssid));
strncpy((char *)sta_cfg.sta.password, pass, sizeof(sta_cfg.sta.password));
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &sta_cfg);
esp_wifi_start();
esp_wifi_connect();
} else {
ESP_LOGI(tag_s, "无 WiFi 配置,启动配网页");
nowifidata_start_config_web();
}
}
#endif
#if CONFIG_APP_LINK_BLE
static void run_ble_mode(void)
{
char dev[32] = {0};
char ble[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
read_from_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, ble, sizeof(ble));
if (strlen(dev) == 0 || strlen(ble) == 0) {
ESP_LOGI(tag_s, "需配置 device_id 与蓝牙名,启动热点配网");
nowifidata_start_config_web();
return;
}
button_monitor_task_init();
if (link_ble_start() != ESP_OK) {
ESP_LOGE(tag_s, "BLE 启动失败");
}
}
#endif
#if CONFIG_APP_LINK_UART
static void run_uart_mode(void)
{
char dev[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
if (strlen(dev) == 0) {
ESP_LOGI(tag_s, "UART 模式: 需配置 device_id,启动配网热点");
nowifidata_start_config_web();
return;
}
ESP_LOGI(tag_s, "UART 模式: device_id=%s", dev);
/* 启动按键监控任务(长按重置设备号) */
button_monitor_task_init();
/* 启动 UART 链路通信 */
if (link_uart_start() != ESP_OK) {
ESP_LOGE(tag_s, "UART 链路启动失败");
}
}
#endif
static void ota_confirm_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(OTA_CONFIRM_DELAY_MS));
if (ota_manager_get_state() == OTA_STATE_SUCCESS) {
ESP_LOGI(tag_s, "OTA 稳定运行,确认新固件");
ota_manager_mark_success();
}
vTaskDelete(NULL);
}
static void schedule_ota_confirm_if_needed(void)
{
if (ota_manager_get_state() == OTA_STATE_SUCCESS) {
xTaskCreate(ota_confirm_task, "ota_confirm", 3072, NULL, 1, NULL);
}
}
void app_run(void)
{
ESP_ERROR_CHECK(system_core_init());
ESP_ERROR_CHECK(driver_manager_init_from_nvs());
#if BUILD_SERIAL_LOG_ENABLED
#if BUILD_IS_RELEASE
ESP_LOGI(tag_s, "构建类型: Release (Os优化)");
#if UART_MODE_COMMUNICATION
ESP_LOGI(tag_s, "UART模式: 普通通信串口");
#else
ESP_LOGI(tag_s, "UART模式: 调试打印");
#endif
#else
ESP_LOGI(tag_s, "构建类型: Debug (Og优化)");
ESP_LOGI(tag_s, "UART模式: 调试打印");
#endif
#endif
#if CONFIG_APP_LINK_WIFI
run_wifi_mode();
#elif CONFIG_APP_LINK_BLE
run_ble_mode();
#elif CONFIG_APP_LINK_UART
run_uart_mode();
#else
#error "请在 menuconfig 中选择 APP_LINK_WIFI / APP_LINK_BLE / APP_LINK_UART"
#endif
#if BUILD_IS_RELEASE && UART_MODE_COMMUNICATION
example_uart_comm_start();
#endif
schedule_ota_confirm_if_needed();
}
#ifndef APP_RUN_H
#define APP_RUN_H
void app_run(void);
#endif
/*
* 示例:Release 模式下将「APP_DEBUG_UART_NUM」这一路 UART 作为普通通信串口
*
* 编译条件:
* - CONFIG_ROBO_APP_FW_RELEASE=y
* - CONFIG_APP_UART_MODE_DEBUG=y
*
* 使用场景:
* - 通过板载 RX1/TX1(UART1)等与外部 MCU 透明传输
* - 量产固件不需要日志输出
*/
#include "core/build_config.h"
#include "task_manager.h"
#include "drivers/uart_comm/uart_comm.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/* 仅在 Release + UART 通信模式下编译此示例 */
#if BUILD_IS_RELEASE && UART_MODE_COMMUNICATION
static const char *TEST_MSG = "Hello from ESP32 UART communication mode!\r\n";
void example_uart_comm_task(void *pvParameters)
{
(void)pvParameters;
/* 初始化指定 UART 作为通信串口(115200 波特率,引脚已在 menuconfig 配置) */
esp_err_t err = uart_comm_init(115200);
if (err != ESP_OK) {
/* 初始化失败(无日志输出时无法打印错误) */
vTaskDelete(NULL);
return;
}
/* 发送测试消息 */
uart_comm_send_string(TEST_MSG);
uint8_t rx_buf[256];
while (1) {
/* 非阻塞接收(100ms 超时) */
int len = uart_comm_receive(rx_buf, sizeof(rx_buf), 100);
if (len > 0) {
/* 回传接收到的数据(Echo) */
uart_comm_send(rx_buf, len);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
/* 启动示例任务的入口函数 */
void example_uart_comm_start(void)
{
(void)app_task_start(APP_TASK_UART_COMM_EXAMPLE, example_uart_comm_task, NULL, NULL);
}
#else /* Debug 模式或 UART 作为调试模式 */
/* 空实现,避免编译错误 */
void example_uart_comm_start(void)
{
/* 在非 Release+通信模式下,此功能不可用 */
}
#endif
/*
* UART 链路模式示例使用文件
* 演示如何使用 link_uart 模块与安卓进行串口通信
*
* 硬件连接:
* - ESP32-S3 GPIO17 (U1TXD) -> 安卓设备 RX
* - ESP32-S3 GPIO18 (U1RXD) -> 安卓设备 TX
* - GND 共地
*
* 通信协议:
* - 波特率:115200 (可在 menuconfig 中配置)
* - 数据格式:8N1
* - 帧格式:JSON + 换行符 (\\n)
*
* 协议示例:
* 安卓发送:{"head":{"message_type":1},"body":{"throttle":50,"steer":30}}
* 设备回复:{"head":{"message_type":1},"body":{"device_ID":"110201","voltage":12.5,"percent":80}}
*/
#include "link_uart.h"
#include "sdkconfig.h"
#if CONFIG_APP_LINK_UART
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *tag_s = "UART_LINK_EX";
/**
* @brief 示例:直接发送原始数据
* 可用于发送自定义协议数据
*/
void example_uart_link_send_raw(void)
{
const char *test_data = "{\"test\":\"hello from esp32\"}\n";
int ret = link_uart_send_raw((const uint8_t *)test_data, strlen(test_data));
if (ret > 0) {
ESP_LOGI(tag_s, "原始数据发送成功: %d bytes", ret);
} else {
ESP_LOGE(tag_s, "原始数据发送失败");
}
}
/**
* @brief 示例:发送心跳
* 实际使用中由 link_uart.c 中的心跳任务自动发送
*/
void example_uart_link_send_heartbeat(void)
{
const char *heartbeat = "{\"head\":{\"message_type\":1},\"body\":{\"device_ID\":\"110201\"}}";
int ret = link_uart_send_heartbeat(heartbeat);
if (ret > 0) {
ESP_LOGI(tag_s, "心跳发送成功");
} else {
ESP_LOGE(tag_s, "心跳发送失败");
}
}
/**
* @brief 示例:发送日志/告警
*/
void example_uart_link_send_log(void)
{
const char *log_json = "{\"head\":{\"message_type\":4},\"body\":{\"msg\":\"系统运行正常\"}}";
int ret = link_uart_send_log(log_json);
if (ret > 0) {
ESP_LOGI(tag_s, "日志发送成功");
} else {
ESP_LOGE(tag_s, "日志发送失败");
}
}
/**
* @brief 示例:检查连接状态
*/
void example_uart_link_check_connection(void)
{
if (link_uart_is_connected()) {
ESP_LOGI(tag_s, "UART 链路已连接");
} else {
ESP_LOGW(tag_s, "UART 链路未连接");
}
}
/**
* @brief 启动 UART 链路模式示例任务
* 演示在应用层如何使用 link_uart 接口
*/
void example_uart_link_start(void)
{
ESP_LOGI(tag_s, "启动 UART 链路示例任务");
ESP_LOGI(tag_s, "UART 配置: GPIO%d TX / GPIO%d RX, 波特率 %d",
CONFIG_APP_UART_LINK_TX_GPIO,
CONFIG_APP_UART_LINK_RX_GPIO,
CONFIG_APP_UART_LINK_BAUDRATE);
/* 注意:link_uart_start() 已在 app_run.c 中调用
* 这里仅演示如何使用 link_uart 提供的接口 */
/* 示例:发送一条测试数据 */
example_uart_link_send_raw();
}
#endif /* CONFIG_APP_LINK_UART */
/*
* 构建配置检测头文件
* 用于在代码中区分 Debug/Release 构建和串口使用模式
*/
#ifndef BUILD_CONFIG_H
#define BUILD_CONFIG_H
#include "sdkconfig.h"
/* ========================
* 编译优化级别检测
* ======================== */
#if defined(CONFIG_ROBO_APP_FW_RELEASE)
#define BUILD_IS_RELEASE 1
#define BUILD_IS_DEBUG 0
#define BUILD_OPTIMIZATION "Os"
#else
#define BUILD_IS_RELEASE 0
#define BUILD_IS_DEBUG 1
#define BUILD_OPTIMIZATION "Og"
#endif
/* ========================
* 串口 / 日志输出
* ======================== */
/*
* Release:关闭串口调试打印(sdkconfig 建议 CONFIG_ESP_CONSOLE_NONE),
* W/E 经 BLE 0xFFE3 Notify 推送(见 link_ble.c)。
*
* Release + APP_UART_MODE_DEBUG:指定 UART 给 uart_comm,同样无 ESP_LOG。
*/
#if defined(CONFIG_APP_UART_MODE_DEBUG) && defined(CONFIG_ROBO_APP_FW_RELEASE)
#define UART_MODE_COMMUNICATION 1
#define UART_MODE_DEBUG 0
#define BUILD_SERIAL_LOG_ENABLED 0
#else
#define UART_MODE_COMMUNICATION 0
#define UART_MODE_DEBUG 1
#if BUILD_IS_RELEASE
#define BUILD_SERIAL_LOG_ENABLED 0
#else
#define BUILD_SERIAL_LOG_ENABLED 1
#endif
#endif
/* ========================
* 条件日志宏(Release 或无串口时禁用 LOG_* 宏)
* ======================== */
#if !BUILD_SERIAL_LOG_ENABLED
#define BUILD_LOG_LEVEL_NONE 1
#define LOG_I(tag, fmt, ...) ((void)0)
#define LOG_W(tag, fmt, ...) ((void)0)
#define LOG_E(tag, fmt, ...) ((void)0)
#define LOG_D(tag, fmt, ...) ((void)0)
#else
#include "esp_log.h"
#define BUILD_LOG_LEVEL_NONE 0
#define LOG_I(tag, fmt, ...) ESP_LOGI(tag, fmt, ##__VA_ARGS__)
#define LOG_W(tag, fmt, ...) ESP_LOGW(tag, fmt, ##__VA_ARGS__)
#define LOG_E(tag, fmt, ...) ESP_LOGE(tag, fmt, ##__VA_ARGS__)
#define LOG_D(tag, fmt, ...) ESP_LOGD(tag, fmt, ##__VA_ARGS__)
#endif
#ifdef __cplusplus
extern "C" {
#endif
static inline const char* build_get_type(void) {
return BUILD_IS_RELEASE ? "Release" : "Debug";
}
static inline const char* build_get_uart_mode(void) {
return UART_MODE_COMMUNICATION ? "Communication" : "Debug";
}
static inline int build_can_log(void) {
return BUILD_SERIAL_LOG_ENABLED;
}
#ifdef __cplusplus
}
#endif
#endif /* BUILD_CONFIG_H */
#include "system_init.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "wifidevnum_config.h"
#include "ota_manager.h"
static const char *tag_s = "SYS_INIT";
esp_err_t system_core_init(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* 初始化 OTA 管理器(检查上次OTA状态,如有必要则回退) */
ret = ota_manager_init();
if (ret != ESP_OK) {
ESP_LOGW(tag_s, "OTA manager triggered rollback, restarting...");
/* ota_manager_init 内部会调用 esp_restart() */
/* 如果能走到这里,说明没有触发回退,继续启动 */
}
ESP_ERROR_CHECK(init_spiffs());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
return ESP_OK;
}
#ifndef SYSTEM_INIT_H
#define SYSTEM_INIT_H
#include "esp_err.h"
esp_err_t system_core_init(void);
#endif
#include "task_manager.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include <stddef.h>
#if CONFIG_BT_NIMBLE_ENABLED
#include "nimble/nimble_port_freertos.h"
#endif
static const char *tag_s = "TASK_MGR";
typedef struct {
const char *name;
uint32_t stack_depth;
UBaseType_t priority;
int core_id;
} app_task_config_t;
static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = {
[APP_TASK_DNS_SERVER] = {
.name = "dns",
.stack_depth = 3072,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_BUTTON_MONITOR] = {
.name = "btn_task",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_BLE_HEARTBEAT] = {
.name = "ble_hb",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_HEARTBEAT] = {
.name = "mqtt_hb",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_OTA_REPORT] = {
.name = "ota_msg_task",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_EXIT] = {
.name = "mqtt_exit",
.stack_depth = 512,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_INIT] = {
.name = "mqtt_init",
.stack_depth = 10240,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_UART_COMM_EXAMPLE] = {
.name = "uart_comm",
.stack_depth = 2048,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
};
static esp_err_t app_task_create(const char *name,
TaskFunction_t entry,
uint32_t stack_depth,
void *arg,
UBaseType_t priority,
int core_id,
TaskHandle_t *out_handle)
{
BaseType_t rc = pdFAIL;
if (core_id == APP_TASK_CORE_NO_AFFINITY) {
rc = xTaskCreate(entry, name, stack_depth, arg, priority, out_handle);
} else {
rc = xTaskCreatePinnedToCore(entry, name, stack_depth, arg, priority, out_handle, core_id);
}
if (rc != pdPASS) {
ESP_LOGE(tag_s, "create task fail: %s core=%d", name ? name : "unknown", core_id);
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t app_task_start(app_task_id_t id,
TaskFunction_t entry,
void *arg,
TaskHandle_t *out_handle)
{
if (id < 0 || id >= APP_TASK_COUNT || entry == NULL) {
ESP_LOGE(tag_s, "invalid task id=%d", (int)id);
return ESP_ERR_INVALID_ARG;
}
const app_task_config_t *cfg = &task_cfg_s[id];
return app_task_create(cfg->name, entry, cfg->stack_depth, arg, cfg->priority, cfg->core_id, out_handle);
}
void app_task_stop(TaskHandle_t *handle)
{
if (handle == NULL || *handle == NULL) {
return;
}
vTaskDelete(*handle);
*handle = NULL;
}
esp_err_t app_task_start_nimble_host(TaskFunction_t entry)
{
if (entry == NULL) {
return ESP_ERR_INVALID_ARG;
}
#if CONFIG_BT_NIMBLE_ENABLED
nimble_port_freertos_init(entry);
return ESP_OK;
#else
ESP_LOGE(tag_s, "NimBLE is disabled");
return ESP_ERR_NOT_SUPPORTED;
#endif
}
#ifndef TASK_MANAGER_H
#define TASK_MANAGER_H
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define APP_TASK_CORE_NO_AFFINITY (-1)
typedef enum {
APP_TASK_DNS_SERVER = 0,
APP_TASK_BUTTON_MONITOR,
APP_TASK_BLE_HEARTBEAT,
APP_TASK_MQTT_HEARTBEAT,
APP_TASK_MQTT_OTA_REPORT,
APP_TASK_MQTT_EXIT,
APP_TASK_MQTT_INIT,
APP_TASK_UART_COMM_EXAMPLE,
APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */
APP_TASK_COUNT,
} app_task_id_t;
esp_err_t app_task_start(app_task_id_t id,
TaskFunction_t entry,
void *arg,
TaskHandle_t *out_handle);
void app_task_stop(TaskHandle_t *handle);
esp_err_t app_task_start_nimble_host(TaskFunction_t entry);
#endif
#include "device_model.h"
#include <string.h>
#include "esp_log.h"
static const char *tag_s = "DEVICE_MODEL";
device_model_t device_model_from_full_id(const char *device_id)
{
if (!device_id || strlen(device_id) < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN)) {
ESP_LOGW(tag_s, "device_id 为空或长度不足,按 1101");
return DEVICE_MODEL_1101;
}
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START;
if (strncmp(slice, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1201 → 型号 1201");
return DEVICE_MODEL_1201;
}
if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101");
return DEVICE_MODEL_1101;
}
if (strncmp(slice, "1102", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1102 → 型号 1102");
return DEVICE_MODEL_1102;
}
ESP_LOGW(tag_s, "片段 [3..6]=\"%.4s\" 未识别,默认 1101", slice);
return DEVICE_MODEL_1101;
}
#ifndef DEVICE_MODEL_H
#define DEVICE_MODEL_H
#include <stdbool.h>
/**
* 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。
* 规则:取 device_id 第 3~第 6 个字符(从第 1 个字符起算,共 4 字符),
* 即 C 字符串下标 [2..5],与 "1201" / "1101" 比较。
*/
typedef enum {
DEVICE_MODEL_1201 = 0,
DEVICE_MODEL_1101,
DEVICE_MODEL_1102,
} device_model_t;
#define DEVICE_MODEL_SLICE_0BASE_START 2
#define DEVICE_MODEL_SLICE_CHAR_LEN 4
device_model_t device_model_from_full_id(const char *device_id);
static inline bool device_model_is_1101(device_model_t m)
{
return m == DEVICE_MODEL_1101;
}
static inline bool device_model_is_1102(device_model_t m)
{
return m == DEVICE_MODEL_1102;
}
#endif
#ifndef DEVICE_NVS_H
#define DEVICE_NVS_H
/** NVS 命名空间(与网页配网写入一致) */
#define DEVICE_CFG_NVS_NAMESPACE "storage"
#define DEVICE_CFG_KEY_WIFI_SSID "wifi_ssid"
#define DEVICE_CFG_KEY_WIFI_PASS "wifi_pass"
#define DEVICE_CFG_KEY_DEVICE_ID "device_id"
#define DEVICE_CFG_KEY_BLE_ADV_NAME "ble_adv_name"
#endif
#include "driver_manager.h"
#include <string.h>
#include "betteryread.h"
#include "device_nvs.h"
#include "esp_log.h"
#include "rc_pwm_control.h"
#include "wifidevnum_config.h"
static const char *tag_s = "DRV_MGR";
esp_err_t driver_manager_init_from_nvs(void)
{
char devid[32] = {0};
if (read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid)) == ESP_OK && strlen(devid) > 0) {
rc_pwm_control_set_drive_from_device_id(devid);
} else {
rc_pwm_control_set_drive_from_device_id(NULL);
}
ESP_ERROR_CHECK(board_adc_init());
ESP_ERROR_CHECK(rc_pwm_control_init());
ESP_LOGI(tag_s, "drivers init done");
return ESP_OK;
}
#ifndef DRIVER_MANAGER_H
#define DRIVER_MANAGER_H
#include "esp_err.h"
esp_err_t driver_manager_init_from_nvs(void);
#endif
#include "betteryread.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <stdbool.h>
static const char *tag_s = "BATT_ADC";
static adc_oneshot_unit_handle_t adc_handle_s;
static adc_cali_handle_t adc_cali_s;
static adc_channel_t adc_channel_s;
static adc_unit_t adc_unit_s;
static bool adc_ready_s;
static bool adc_cali_ready_s;
static float voltage_cache_v_s;
static int64_t voltage_cache_ts_us_s;
static bool voltage_cache_valid_s;
#define BATT_READ_MIN_INTERVAL_US (2500LL * 1000LL)
#define BATT_FILTER_ALPHA (0.35f)
esp_err_t board_adc_init(void)
{
esp_err_t err = adc_oneshot_io_to_channel(ADC_GET_VOLTAGE_GPIO, &adc_unit_s, &adc_channel_s);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "gpio%d no ADC channel: %s", ADC_GET_VOLTAGE_GPIO, esp_err_to_name(err));
return err;
}
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = adc_unit_s,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
err = adc_oneshot_new_unit(&unit_cfg, &adc_handle_s);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "adc new unit fail: %s", esp_err_to_name(err));
return err;
}
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
err = adc_oneshot_config_channel(adc_handle_s, adc_channel_s, &chan_cfg);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "adc cfg channel fail: %s", esp_err_to_name(err));
return err;
}
adc_cali_curve_fitting_config_t cali_cfg = {
.unit_id = adc_unit_s,
.atten = chan_cfg.atten,
.bitwidth = chan_cfg.bitwidth,
};
err = adc_cali_create_scheme_curve_fitting(&cali_cfg, &adc_cali_s);
if (err == ESP_OK) {
adc_cali_ready_s = true;
} else {
adc_cali_ready_s = false;
ESP_LOGW(tag_s, "adc calibration unavailable, fallback raw");
}
adc_ready_s = true;
ESP_LOGI(tag_s, "ADC ready gpio=%d unit=%d ch=%d", ADC_GET_VOLTAGE_GPIO, adc_unit_s, adc_channel_s);
return ESP_OK;
}
float board_get_voltage_v(void)
{
if (!adc_ready_s) {
return 0.0f;
}
int64_t now_us = esp_timer_get_time();
if (voltage_cache_valid_s && (now_us - voltage_cache_ts_us_s) < BATT_READ_MIN_INTERVAL_US) {
return voltage_cache_v_s;
}
const int sample_count = 16;
int raw_sum = 0;
int valid_count = 0;
for (int i = 0; i < sample_count; ++i) {
int raw = 0;
if (adc_oneshot_read(adc_handle_s, adc_channel_s, &raw) != ESP_OK) {
continue;
}
raw_sum += raw;
++valid_count;
}
if (valid_count == 0) {
return 0.0f;
}
int raw_avg = raw_sum / valid_count;
int adc_mv = 0;
if (adc_cali_ready_s) {
if (adc_cali_raw_to_voltage(adc_cali_s, raw_avg, &adc_mv) != ESP_OK) {
adc_mv = (raw_avg * 3300) / 4095;
}
} else {
adc_mv = (raw_avg * 3300) / 4095;
}
const float v_adc = (float)adc_mv / 1000.0f;
/* 分压系数 = (R上拉 + R下拉) / R下拉 = (100k + 10k) / 10k = 11 */
float measured_v = v_adc * BOARD_VOLTAGE_DIVIDER_GAIN;
if (!voltage_cache_valid_s) {
voltage_cache_v_s = measured_v;
voltage_cache_valid_s = true;
} else {
voltage_cache_v_s = (BATT_FILTER_ALPHA * measured_v) +
((1.0f - BATT_FILTER_ALPHA) * voltage_cache_v_s);
}
voltage_cache_ts_us_s = now_us;
return voltage_cache_v_s;
}
\ No newline at end of file
#ifndef BETTERYREAD_H
#define BETTERYREAD_H
#include "esp_err.h"
#define ADC_GET_VOLTAGE_GPIO 5
/* 分压:上拉 100k 到电池正极、下拉 10k 到 GND,采样点接 GPIO5
* 分压系数 = (R上拉 + R下拉) / R下拉 = (100 + 10) / 10 = 11
* 所以电压 = ADC电压 × 11
*/
#define BOARD_VOLTAGE_DIVIDER_R_TOP_KOHM 100.0f
#define BOARD_VOLTAGE_DIVIDER_R_BOTTOM_KOHM 10.0f
#define BOARD_VOLTAGE_DIVIDER_GAIN 11.0f
float board_get_voltage_v(void);
/**
* @brief 电压采样初始化(GPIO5 ADC)
*/
esp_err_t board_adc_init(void);
#define get_voltage_v() board_get_voltage_v()
#define get_temperature() (0.0f)
#endif
\ No newline at end of file
#ifndef DEVICE_DRIVE_H
#define DEVICE_DRIVE_H
typedef struct device_drive_ops_s {
const char *name;
void (*stop)(void);
void (*control)(int mode, int val);
void (*shot)(int pin, int val);
} device_drive_ops_t;
#endif
#include "device_1101.h"
#include "rc_pwm_control.h"
static void device_1101_stop(void)
{
rc_pwm_stop_all_drive_outputs();
}
/*
* 当前 1101 先复用 1201 的控制映射,后续若协议或硬件差异扩大,
* 只需在本文件内调整映射,不影响上层协议处理。
*/
static void device_1101_control(int mode, int val)
{
if (mode == 1) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1101_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1101",
.stop = device_1101_stop,
.control = device_1101_control,
.shot = device_1101_shot,
};
const device_drive_ops_t *device_1101_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1101_H
#define DEVICE_1101_H
#include "device_drive.h"
const device_drive_ops_t *device_1101_get_ops(void);
#endif
#include "device_1102.h"
#include "rc_pwm_control.h"
#define DEV1102_VAL_MAX 200
#define DEV1102_DRIVE_START_TH 50
/* 转向:App 侧 val 在 [47,62] 线性对应角度;中位 90°,左右各 ±30° */
#define DEV1102_STEER_VAL_LO 47
#define DEV1102_STEER_VAL_HI 62
#define DEV1102_STEER_CENTER_ANG 90
#define DEV1102_STEER_DELTA_ANG 30
#define DEV1102_STEER_LEFT_MAX (DEV1102_STEER_CENTER_ANG + DEV1102_STEER_DELTA_ANG)
#define DEV1102_STEER_RIGHT_MAX (DEV1102_STEER_CENTER_ANG - DEV1102_STEER_DELTA_ANG)
#define DEV1102_STEER_VAL_SPAN (DEV1102_STEER_VAL_HI - DEV1102_STEER_VAL_LO)
static int clamp_int(int v, int lo, int hi)
{
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static uint32_t drive_percent_from_val_200(int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_DRIVE_START_TH) {
return 0;
}
/* 50~200 线性映射到 0~100% */
return (uint32_t)(((v - DEV1102_DRIVE_START_TH) * 100) /
(DEV1102_VAL_MAX - DEV1102_DRIVE_START_TH));
}
static uint32_t steer_left_angle_from_val(int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_STEER_VAL_LO) {
return DEV1102_STEER_CENTER_ANG;
}
if (v >= DEV1102_STEER_VAL_HI) {
return DEV1102_STEER_LEFT_MAX;
}
return (uint32_t)(DEV1102_STEER_CENTER_ANG +
(uint32_t)(v - DEV1102_STEER_VAL_LO) * DEV1102_STEER_DELTA_ANG /
(uint32_t)DEV1102_STEER_VAL_SPAN);
}
static uint32_t steer_right_angle_from_val(int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_STEER_VAL_LO) {
return DEV1102_STEER_CENTER_ANG;
}
if (v >= DEV1102_STEER_VAL_HI) {
return DEV1102_STEER_RIGHT_MAX;
}
return (uint32_t)(DEV1102_STEER_CENTER_ANG -
(uint32_t)(v - DEV1102_STEER_VAL_LO) * DEV1102_STEER_DELTA_ANG /
(uint32_t)DEV1102_STEER_VAL_SPAN);
}
static void device_1102_stop(void)
{
/* 1102: 10 前进,21 后退;全部拉低并将 IO16 回中 */
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
}
static void device_1102_control(int mode, int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
switch (mode) {
case 1: { /* 前进:IO10 */
uint32_t pct = drive_percent_from_val_200(v);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
break;
}
case 2: { /* 后退:IO21 */
uint32_t pct = drive_percent_from_val_200(v);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct);
break;
}
case 3: { /* 左转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(v));
break;
}
case 4: { /* 右转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(v));
break;
}
default:
break;
}
}
static void device_1102_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1102",
.stop = device_1102_stop,
.control = device_1102_control,
.shot = device_1102_shot,
};
const device_drive_ops_t *device_1102_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1102_H
#define DEVICE_1102_H
#include "device_drive.h"
const device_drive_ops_t *device_1102_get_ops(void);
#endif
#include "device_1201.h"
#include "rc_pwm_control.h"
static void device_1201_stop(void)
{
rc_pwm_stop_all_drive_outputs();
}
static void device_1201_control(int mode, int val)
{
if (mode == 1) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1201_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1201",
.stop = device_1201_stop,
.control = device_1201_control,
.shot = device_1201_shot,
};
const device_drive_ops_t *device_1201_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1201_H
#define DEVICE_1201_H
#include "device_drive.h"
const device_drive_ops_t *device_1201_get_ops(void);
#endif
#include "rc_pwm_control.h"
#include "device_drive.h"
#include "device_model.h"
#include "devices/device_1101.h"
#include "devices/device_1102.h"
#include "devices/device_1201.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include <stdbool.h>
static const char *tag_s = "RC_PWM_CTRL";
static const device_drive_ops_t *drive_ops_s = NULL;
typedef struct {
float kp;
float ki;
float kd;
float out_min;
float out_max;
float i_min;
float i_max;
float integral;
float prev_error;
bool enabled;
rc_pid_actuator_t actuator;
bool configured;
} rc_pid_ctx_t;
static rc_pid_ctx_t pid_ctx_s[RC_PID_CH_MAX];
static float clampf(float v, float lo, float hi)
{
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static ledc_channel_t rc_pwm_channel_from_gpio(int gpio)
{
switch (gpio) {
case RC_PWM_PIN_DRV1_A:
return LEDC_CHANNEL_0;
case RC_PWM_PIN_DRV2_A:
return LEDC_CHANNEL_1;
case RC_PWM_PIN_DRV2_B:
return LEDC_CHANNEL_2;
case RC_PWM_PIN_DRV1_B:
return LEDC_CHANNEL_3;
case RC_PWM_PIN_AUX_15:
return LEDC_CHANNEL_4;
case RC_PWM_PIN_AUX_16:
return LEDC_CHANNEL_5;
default:
return LEDC_CHANNEL_MAX;
}
}
static rc_aux_role_t rc_pwm_aux_role_of_pin(int gpio)
{
if (gpio == RC_PWM_PIN_AUX_15) {
#if CONFIG_APP_PWM_IO15_ESC
return RC_AUX_ROLE_ESC;
#else
return RC_AUX_ROLE_SERVO;
#endif
}
if (gpio == RC_PWM_PIN_AUX_16) {
#if CONFIG_APP_PWM_IO16_ESC
return RC_AUX_ROLE_ESC;
#else
return RC_AUX_ROLE_SERVO;
#endif
}
return RC_AUX_ROLE_ESC;
}
static void rc_pwm_set_duty_by_gpio(int gpio, uint32_t duty)
{
ledc_channel_t ch = rc_pwm_channel_from_gpio(gpio);
if (ch == LEDC_CHANNEL_MAX) {
return;
}
ledc_set_duty(RC_PWM_LEDC_MODE, ch, duty);
ledc_update_duty(RC_PWM_LEDC_MODE, ch);
}
static uint32_t rc_pwm_angle_to_duty(uint32_t angle)
{
if (angle > 180U) {
angle = 180U;
}
return RC_PWM_DUTY_500US + (angle * (RC_PWM_DUTY_2500US - RC_PWM_DUTY_500US) / 180U);
}
static uint32_t rc_pwm_esc_percent_to_duty(uint32_t percent)
{
if (percent > 100U) {
percent = 100U;
}
return RC_ESC_DUTY_MIN + (RC_ESC_DUTY_MAX - RC_ESC_DUTY_MIN) * percent / 100U;
}
static void rc_pid_apply_output(rc_pid_actuator_t actuator, float output)
{
switch (actuator) {
case RC_PID_ACT_ESC_AUX_BOTH:
rc_pwm_set_dual_esc_percent((uint32_t)clampf(output, 0.0f, 100.0f));
break;
case RC_PID_ACT_DRV1_A:
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, (uint32_t)clampf(output, 0.0f, 100.0f));
break;
case RC_PID_ACT_DRV1_B:
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, (uint32_t)clampf(output, 0.0f, 100.0f));
break;
case RC_PID_ACT_DRV2_A:
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, (uint32_t)clampf(output, 0.0f, 100.0f));
break;
case RC_PID_ACT_DRV2_B:
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_B, (uint32_t)clampf(output, 0.0f, 100.0f));
break;
case RC_PID_ACT_STEERING_SERVO:
rc_pwm_set_steering_angle_deg((uint32_t)clampf(output, 0.0f, 180.0f));
break;
default:
break;
}
}
esp_err_t rc_pwm_control_init(void)
{
const ledc_timer_config_t ledc_timer = {
.speed_mode = RC_PWM_LEDC_MODE,
.timer_num = RC_PWM_LEDC_TIMER,
.duty_resolution = RC_PWM_DUTY_RES,
.freq_hz = RC_PWM_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
const uint32_t init_15 = (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO) ? RC_PWM_DUTY_1500US : RC_ESC_DUTY_MID;
const uint32_t init_16 = (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_16) == RC_AUX_ROLE_SERVO) ? RC_PWM_DUTY_1500US : RC_ESC_DUTY_MID;
const ledc_channel_config_t channels[] = {
{.gpio_num = RC_PWM_PIN_DRV1_A, .speed_mode = RC_PWM_LEDC_MODE, .channel = LEDC_CHANNEL_0, .intr_type = LEDC_INTR_DISABLE, .timer_sel = RC_PWM_LEDC_TIMER, .duty = 0, .hpoint = 0},
{.gpio_num = RC_PWM_PIN_DRV2_A, .speed_mode = RC_PWM_LEDC_MODE, .channel = LEDC_CHANNEL_1, .intr_type = LEDC_INTR_DISABLE, .timer_sel = RC_PWM_LEDC_TIMER, .duty = 0, .hpoint = 0},
{.gpio_num = RC_PWM_PIN_DRV2_B, .speed_mode = RC_PWM_LEDC_MODE, .channel = LEDC_CHANNEL_2, .intr_type = LEDC_INTR_DISABLE, .timer_sel = RC_PWM_LEDC_TIMER, .duty = 0, .hpoint = 0},
{.gpio_num = RC_PWM_PIN_DRV1_B, .speed_mode = RC_PWM_LEDC_MODE, .channel = LEDC_CHANNEL_3, .intr_type = LEDC_INTR_DISABLE, .timer_sel = RC_PWM_LEDC_TIMER, .duty = 0, .hpoint = 0},
{.gpio_num = RC_PWM_PIN_AUX_15, .speed_mode = RC_PWM_LEDC_MODE, .channel = LEDC_CHANNEL_4, .intr_type = LEDC_INTR_DISABLE, .timer_sel = RC_PWM_LEDC_TIMER, .duty = init_15, .hpoint = 0},
{.gpio_num = RC_PWM_PIN_AUX_16, .speed_mode = RC_PWM_LEDC_MODE, .channel = LEDC_CHANNEL_5, .intr_type = LEDC_INTR_DISABLE, .timer_sel = RC_PWM_LEDC_TIMER, .duty = init_16, .hpoint = 0},
};
for (int i = 0; i < (int)(sizeof(channels) / sizeof(channels[0])); ++i) {
ESP_ERROR_CHECK(ledc_channel_config(&channels[i]));
}
if (!drive_ops_s) {
drive_ops_s = device_1201_get_ops();
}
ESP_LOGI(tag_s, "PWM init done: D1(10/21) D2(11/12) AUX(15:%s 16:%s) 50Hz profile=%s",
rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO ? "servo" : "esc",
rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_16) == RC_AUX_ROLE_SERVO ? "servo" : "esc",
drive_ops_s->name);
return ESP_OK;
}
void rc_pwm_control_set_drive_from_device_id(const char *device_id)
{
const device_model_t m = device_model_from_full_id(device_id);
if (device_model_is_1101(m)) {
drive_ops_s = device_1101_get_ops();
} else if (device_model_is_1102(m)) {
drive_ops_s = device_1102_get_ops();
#if CONFIG_APP_PWM_IO16_ESC
ESP_LOGW(tag_s, "1102 需要 IO16 作为舵机,请在 menuconfig 将 GPIO16 角色改为 SERVO");
#endif
} else {
drive_ops_s = device_1201_get_ops();
}
ESP_LOGI(tag_s, "当前设备策略: %s", drive_ops_s->name);
}
void rc_vehicle_stop(void)
{
if (drive_ops_s && drive_ops_s->stop) {
drive_ops_s->stop();
}
}
void rc_vehicle_shot(int pin, int val)
{
if (drive_ops_s && drive_ops_s->shot) {
drive_ops_s->shot(pin, val);
}
}
void rc_vehicle_control(int mode, int val)
{
if (drive_ops_s && drive_ops_s->control) {
drive_ops_s->control(mode, val);
}
}
void rc_pwm_set_drive_percent(int gpio, uint32_t duty_percent)
{
if (duty_percent > 100U) {
duty_percent = 100U;
}
if (gpio != RC_PWM_PIN_DRV1_A && gpio != RC_PWM_PIN_DRV1_B &&
gpio != RC_PWM_PIN_DRV2_A && gpio != RC_PWM_PIN_DRV2_B) {
return;
}
const uint32_t duty = (duty_percent * 8191U) / 100U;
rc_pwm_set_duty_by_gpio(gpio, duty);
}
void rc_pwm_stop_all_drive_outputs(void)
{
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_B, 0);
}
void rc_pwm_set_aux_servo_angle_deg(int gpio, uint32_t angle)
{
if (gpio != RC_PWM_PIN_AUX_15 && gpio != RC_PWM_PIN_AUX_16) {
return;
}
if (rc_pwm_aux_role_of_pin(gpio) != RC_AUX_ROLE_SERVO) {
return;
}
rc_pwm_set_duty_by_gpio(gpio, rc_pwm_angle_to_duty(angle));
}
void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent)
{
if (gpio != RC_PWM_PIN_AUX_15 && gpio != RC_PWM_PIN_AUX_16) {
return;
}
if (rc_pwm_aux_role_of_pin(gpio) != RC_AUX_ROLE_ESC) {
return;
}
rc_pwm_set_duty_by_gpio(gpio, rc_pwm_esc_percent_to_duty(percent));
}
void rc_pwm_set_dual_esc_percent(uint32_t percent)
{
if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_ESC) {
rc_pwm_set_aux_esc_percent(RC_PWM_PIN_AUX_15, percent);
}
if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_16) == RC_AUX_ROLE_ESC) {
rc_pwm_set_aux_esc_percent(RC_PWM_PIN_AUX_16, percent);
}
}
void rc_pwm_set_steering_angle_deg(uint32_t angle)
{
if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO) {
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_15, angle);
return;
}
if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_16) == RC_AUX_ROLE_SERVO) {
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, angle);
}
}
esp_err_t rc_pid_config(rc_pid_channel_t ch, const rc_pid_config_t *cfg)
{
if (cfg == NULL || ch < RC_PID_CH_0 || ch >= RC_PID_CH_MAX) {
return ESP_ERR_INVALID_ARG;
}
if (cfg->out_min >= cfg->out_max || cfg->i_min >= cfg->i_max) {
return ESP_ERR_INVALID_ARG;
}
rc_pid_ctx_t *ctx = &pid_ctx_s[ch];
ctx->kp = cfg->kp;
ctx->ki = cfg->ki;
ctx->kd = cfg->kd;
ctx->out_min = cfg->out_min;
ctx->out_max = cfg->out_max;
ctx->i_min = cfg->i_min;
ctx->i_max = cfg->i_max;
ctx->enabled = cfg->enabled;
ctx->actuator = cfg->actuator;
ctx->integral = 0.0f;
ctx->prev_error = 0.0f;
ctx->configured = true;
return ESP_OK;
}
void rc_pid_reset(rc_pid_channel_t ch)
{
if (ch < RC_PID_CH_0 || ch >= RC_PID_CH_MAX) {
return;
}
pid_ctx_s[ch].integral = 0.0f;
pid_ctx_s[ch].prev_error = 0.0f;
}
void rc_pid_enable(rc_pid_channel_t ch, bool enabled)
{
if (ch < RC_PID_CH_0 || ch >= RC_PID_CH_MAX) {
return;
}
pid_ctx_s[ch].enabled = enabled;
}
float rc_pid_step(rc_pid_channel_t ch,
float target,
float feedback,
float dt_s,
bool apply_output)
{
if (ch < RC_PID_CH_0 || ch >= RC_PID_CH_MAX || dt_s <= 0.0f) {
return 0.0f;
}
rc_pid_ctx_t *ctx = &pid_ctx_s[ch];
if (!ctx->configured || !ctx->enabled) {
return 0.0f;
}
const float error = target - feedback;
ctx->integral += error * dt_s;
ctx->integral = clampf(ctx->integral, ctx->i_min, ctx->i_max);
const float derivative = (error - ctx->prev_error) / dt_s;
ctx->prev_error = error;
float output = ctx->kp * error + ctx->ki * ctx->integral + ctx->kd * derivative;
output = clampf(output, ctx->out_min, ctx->out_max);
if (apply_output) {
rc_pid_apply_output(ctx->actuator, output);
}
return output;
}
#ifndef RC_PWM_CONTROL_H
#define RC_PWM_CONTROL_H
#include "driver/ledc.h"
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/* RC 车统一 PWM 引脚定义(6 路均为 50Hz) */
#define RC_PWM_PIN_DRV1_A 10 /* 驱动芯片1 */
#define RC_PWM_PIN_DRV1_B 21 /* 驱动芯片1 */
#define RC_PWM_PIN_DRV2_A 11 /* 驱动芯片2 */
#define RC_PWM_PIN_DRV2_B 12 /* 驱动芯片2 */
#define RC_PWM_PIN_AUX_15 15 /* 舵机/电调(Kconfig 选择) */
#define RC_PWM_PIN_AUX_16 16 /* 舵机/电调(Kconfig 选择) */
#define RC_PWM_LEDC_TIMER LEDC_TIMER_0
#define RC_PWM_LEDC_MODE LEDC_LOW_SPEED_MODE
#define RC_PWM_DUTY_RES LEDC_TIMER_13_BIT
#define RC_PWM_FREQ_HZ 50
/* 50Hz + 13bit 下的典型占空比(20ms 周期) */
#define RC_PWM_DUTY_500US 205U /* 舵机 0 度 */
#define RC_PWM_DUTY_1500US 614U /* 舵机 90 度 / ESC 中位 */
#define RC_PWM_DUTY_2500US 1024U /* 舵机 180 度 */
#define RC_ESC_DUTY_MIN 410U /* 1000us */
#define RC_ESC_DUTY_MID RC_PWM_DUTY_1500US
#define RC_ESC_DUTY_MAX 819U /* 2000us */
typedef enum {
RC_AUX_ROLE_SERVO = 0,
RC_AUX_ROLE_ESC = 1,
} rc_aux_role_t;
/**
* @brief 初始化全部 6 路 PWM 输出
*
* IO15/16 初始值根据 Kconfig 选择:
* - 舵机:90 度(1500us)
* - 电调:1500us 中位
*/
esp_err_t rc_pwm_control_init(void);
/**
* @brief 根据 device_id 选择设备控制策略
*/
void rc_pwm_control_set_drive_from_device_id(const char *device_id);
void rc_vehicle_stop(void);
void rc_vehicle_shot(int pin, int val);
void rc_vehicle_control(int mode, int val);
/**
* @brief 设置驱动芯片控制引脚百分比(支持 10/21/11/12)
*/
void rc_pwm_set_drive_percent(int gpio, uint32_t duty_percent);
/**
* @brief 停止驱动芯片相关输出(10/21/11/12)
*/
void rc_pwm_stop_all_drive_outputs(void);
/**
* @brief 将指定 AUX 引脚按舵机角度输出(仅当该引脚被配置为 SERVO)
*/
void rc_pwm_set_aux_servo_angle_deg(int gpio, uint32_t angle);
/**
* @brief 将指定 AUX 引脚按电调油门百分比输出(仅当该引脚被配置为 ESC)
*/
void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent);
/**
* @brief 双电调同步输出(会对配置为 ESC 的 AUX 引脚生效)
*/
void rc_pwm_set_dual_esc_percent(uint32_t percent);
/**
* @brief 给“默认转向舵机”输出角度(优先 IO15,其次 IO16)
*/
void rc_pwm_set_steering_angle_deg(uint32_t angle);
/* ======================== PID 接口 ======================== */
typedef enum {
RC_PID_ACT_ESC_AUX_BOTH = 0, /* 对 IO15/16 中被配置为 ESC 的引脚生效 */
RC_PID_ACT_DRV1_A, /* IO10 */
RC_PID_ACT_DRV1_B, /* IO21 */
RC_PID_ACT_DRV2_A, /* IO11 */
RC_PID_ACT_DRV2_B, /* IO12 */
RC_PID_ACT_STEERING_SERVO, /* 默认舵机(IO15 优先) */
} rc_pid_actuator_t;
typedef enum {
RC_PID_CH_0 = 0,
RC_PID_CH_1,
RC_PID_CH_2,
RC_PID_CH_3,
RC_PID_CH_MAX
} rc_pid_channel_t;
typedef struct {
float kp;
float ki;
float kd;
float out_min;
float out_max;
float i_min;
float i_max;
rc_pid_actuator_t actuator;
bool enabled;
} rc_pid_config_t;
esp_err_t rc_pid_config(rc_pid_channel_t ch, const rc_pid_config_t *cfg);
void rc_pid_reset(rc_pid_channel_t ch);
void rc_pid_enable(rc_pid_channel_t ch, bool enabled);
float rc_pid_step(rc_pid_channel_t ch,
float target,
float feedback,
float dt_s,
bool apply_output);
#endif
/*
* UART 通信驱动实现
* 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口
*/
#include "uart_comm.h"
#include "core/build_config.h"
#if defined(CONFIG_APP_UART_MODE_DEBUG) && defined(CONFIG_ROBO_APP_FW_RELEASE)
#include "driver/uart.h"
#include <string.h>
#define UART_COMM_PORT ((uart_port_t)CONFIG_APP_DEBUG_UART_NUM)
static int initialized_s = 0;
esp_err_t uart_comm_init(int baudrate)
{
if (initialized_s) {
return ESP_OK;
}
if (baudrate <= 0) {
baudrate = UART_COMM_DEFAULT_BAUDRATE;
}
uart_config_t uart_cfg = {
.baud_rate = baudrate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_param_config(UART_COMM_PORT, &uart_cfg);
if (err != ESP_OK) {
return err;
}
/* 设置 TX/RX 引脚(UART1/UART2 必须显式设置,UART0 有默认 GPIO43/44) */
int tx_gpio = CONFIG_APP_DEBUG_UART_TX_GPIO;
int rx_gpio = CONFIG_APP_DEBUG_UART_RX_GPIO;
if (tx_gpio >= 0 && rx_gpio >= 0) {
err = uart_set_pin(UART_COMM_PORT, tx_gpio, rx_gpio,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
return err;
}
}
err = uart_driver_install(UART_COMM_PORT, UART_COMM_BUFFER_SIZE,
UART_COMM_BUFFER_SIZE, UART_COMM_QUEUE_SIZE,
NULL, 0);
if (err != ESP_OK) {
return err;
}
initialized_s = 1;
return ESP_OK;
}
void uart_comm_deinit(void)
{
if (!initialized_s) {
return;
}
uart_driver_delete(UART_COMM_PORT);
initialized_s = 0;
}
int uart_comm_send(const uint8_t *data, size_t len)
{
if (!initialized_s || !data || len == 0) {
return -1;
}
int written = uart_write_bytes(UART_COMM_PORT, data, len);
return written;
}
int uart_comm_send_string(const char *str)
{
if (!str) {
return -1;
}
return uart_comm_send((const uint8_t *)str, strlen(str));
}
int uart_comm_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms)
{
if (!initialized_s || !buf || buf_size == 0) {
return -1;
}
int len = uart_read_bytes(UART_COMM_PORT, buf, buf_size - 1,
pdMS_TO_TICKS(timeout_ms));
if (len > 0) {
buf[len] = '\0';
}
return len;
}
int uart_comm_is_initialized(void)
{
return initialized_s;
}
void uart_comm_flush(void)
{
if (!initialized_s) {
return;
}
uart_flush(UART_COMM_PORT);
}
#else /* !(CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE) */
/* 当功能未启用时,提供空实现(避免编译错误) */
esp_err_t uart_comm_init(int baudrate)
{
(void)baudrate;
return ESP_ERR_NOT_SUPPORTED;
}
void uart_comm_deinit(void) {}
int uart_comm_send(const uint8_t *data, size_t len)
{
(void)data;
(void)len;
return -1;
}
int uart_comm_send_string(const char *str)
{
(void)str;
return -1;
}
int uart_comm_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms)
{
(void)buf;
(void)buf_size;
(void)timeout_ms;
return -1;
}
int uart_comm_is_initialized(void)
{
return 0;
}
void uart_comm_flush(void) {}
#endif /* CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE */
/*
* UART 通信驱动
* 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口
* 注意:启用此功能后,ESP_LOG/printf 将不可用
*/
#ifndef UART_COMM_H
#define UART_COMM_H
#include "esp_err.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* UART 配置参数 */
#define UART_COMM_DEFAULT_BAUDRATE 115200
#define UART_COMM_BUFFER_SIZE 1024
#define UART_COMM_QUEUE_SIZE 8
/* 初始化 UART 作为普通通信串口
* 仅在 APP_UART_MODE_DEBUG=y 时有效
* 返回 ESP_OK 成功,其他错误码失败
*/
esp_err_t uart_comm_init(int baudrate);
/* 反初始化,恢复默认状态 */
void uart_comm_deinit(void);
/* 发送数据 */
int uart_comm_send(const uint8_t *data, size_t len);
int uart_comm_send_string(const char *str);
/* 接收数据(非阻塞) */
int uart_comm_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms);
/* 检查是否已初始化 */
int uart_comm_is_initialized(void);
/* 清空接收缓冲区 */
void uart_comm_flush(void);
#ifdef __cplusplus
}
#endif
#endif /* UART_COMM_H */
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/cJSON: '*'
\ No newline at end of file
#include "link_ble.h"
#include "remote_control.h"
#include "wifidevnum_config.h"
#include "device_nvs.h"
#include "rc_pwm_control.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/portmacro.h"
#include <stdlib.h>
#include <string.h>
#include "nimble/nimble_port.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "host/ble_uuid.h"
/* ESP-IDF v5.5.x NimBLE:GATT Server API(ble_gatts_* 等)由 ble_hs.h 引入,勿再包含已移除的 host/ble_gatts.h */
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "os/os_mbuf.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "sdkconfig.h"
/* 引入构建配置头文件,支持 Release 模式下的条件日志 */
#include "core/build_config.h"
#include "task_manager.h"
#include "heart_payload.h"
/* OTA 二进制流模块(BLE和UART共用) */
#include "ota_binary_stream.h"
static const char *tag_s = "LINK_BLE";
/** 0xFFE1 JSON 接收缓冲(避免 GATT 回调栈上分配 2KB) */
static char s_ffe1_json_buf_s[REMOTE_CTRL_JSON_MAX_BYTES + 1];
#if !BUILD_IS_RELEASE
static void log_ble_rx_payload(const char *channel, const uint8_t *data, uint16_t len)
{
if (data == NULL || len == 0) {
ESP_LOGI(tag_s, "%s RX empty", channel);
return;
}
ESP_LOGI(tag_s, "%s RX len=%u", channel, (unsigned)len);
uint16_t text_len = len < 64 ? len : 64;
char text[65];
memcpy(text, data, text_len);
text[text_len] = '\0';
ESP_LOGI(tag_s, "%s RX text: %s%s", channel, text, (len > text_len) ? " ..." : "");
}
#endif
static const struct ble_gap_adv_params adv_params_s = {
.conn_mode = BLE_GAP_CONN_MODE_UND,
.disc_mode = BLE_GAP_DISC_MODE_GEN,
};
static int gap_event(struct ble_gap_event *event, void *arg);
static int gatt_svr_chr_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg);
static int gatt_svr_chr_ota_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg);
static int gatt_svr_chr_hb_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg);
static int gatt_svr_chr_ota_stat_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg);
static void link_ble_gatt_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
static int ble_hb_notify_json(const char *json);
static void ble_heartbeat_task(void *param);
/** GATT 0xFFE3:与 MQTT 同结构心跳 JSON,Notify 周期推送;可读为最近一次内容 */
static uint16_t hb_chr_val_handle_s;
static volatile uint16_t ble_conn_handle_s;
static volatile bool ble_link_up_s;
static uint8_t last_hb_buf_s[512];
static uint16_t last_hb_len_s;
/** GATT 0xFFE4:OTA 过程 JSON 状态,Read 为最近一次;Notify 在 begin/chunk/end 各步推送 */
static uint16_t ota_stat_chr_val_handle_s;
static char last_ota_stat_json_s[192];
#if BUILD_IS_RELEASE
static bool log_hook_installed_s;
static volatile bool log_forward_busy_s;
#endif
#ifndef BLE_HEART_PERIOD_MS
#define BLE_HEART_PERIOD_MS 3000
#endif
static void ble_adv_name_apply_mp_suffix(char *name, size_t cap)
{
const char suf[] = "MP";
const size_t slen = sizeof(suf) - 1U;
size_t n = strnlen(name, cap);
if (n >= slen && memcmp(name + n - slen, suf, slen) == 0) {
return;
}
if (cap < slen + 2U) {
return;
}
if (n + slen + 1U > cap) {
size_t cut = cap - slen - 1U;
name[cut] = '\0';
n = cut;
}
memcpy(name + n, suf, slen);
name[n + slen] = '\0';
}
/** 0xFFE3:心跳 / 告警 / 错误共用 Notify;Read 为最近一次下发内容 */
static int ble_hb_notify_json(const char *json)
{
if (json == NULL || !ble_link_up_s || hb_chr_val_handle_s == 0U) {
return -1;
}
size_t jlen = strlen(json);
if (jlen == 0U || jlen >= sizeof(last_hb_buf_s)) {
return -1;
}
memcpy(last_hb_buf_s, json, jlen);
last_hb_buf_s[jlen] = '\0';
last_hb_len_s = (uint16_t)jlen;
struct os_mbuf *om = ble_hs_mbuf_from_flat(last_hb_buf_s, last_hb_len_s);
if (om == NULL) {
return -1;
}
int nrc = ble_gatts_notify_custom(ble_conn_handle_s, hb_chr_val_handle_s, om);
if (nrc != 0) {
os_mbuf_free_chain(om);
}
return nrc;
}
#if BUILD_IS_RELEASE
static void ble_log_line_to_msg(const char *line, char *msg, size_t msg_cap)
{
const char *colon = strchr(line, ':');
if (colon != NULL && colon[1] == ' ') {
strncpy(msg, colon + 2, msg_cap - 1U);
} else {
strncpy(msg, line, msg_cap - 1U);
}
msg[msg_cap - 1U] = '\0';
size_t ml = strlen(msg);
while (ml > 0U && (msg[ml - 1U] == '\n' || msg[ml - 1U] == '\r')) {
msg[--ml] = '\0';
}
}
static int ble_log_vprintf_hook(const char *fmt, va_list ap)
{
(void)fmt;
if (xPortInIsrContext() || log_forward_busy_s) {
return 0;
}
char line[192];
va_list ap_line;
va_copy(ap_line, ap);
int ln = vsnprintf(line, sizeof(line), fmt, ap_line);
va_end(ap_line);
if (ln <= 0) {
return 0;
}
int msg_type = 0;
if (line[0] == 'W') {
msg_type = HEART_MSG_TYPE_LOG_WARN;
} else if (line[0] == 'E') {
msg_type = HEART_MSG_TYPE_LOG_ERROR;
} else {
return 0;
}
char msg[160];
ble_log_line_to_msg(line, msg, sizeof(msg));
char *json = heart_payload_alert_json_malloc(msg_type, msg);
if (json == NULL) {
return 0;
}
log_forward_busy_s = true;
(void)ble_hb_notify_json(json);
log_forward_busy_s = false;
free(json);
return 0;
}
static void ble_log_hook_install_if_needed(void)
{
if (log_hook_installed_s) {
return;
}
(void)esp_log_set_vprintf(ble_log_vprintf_hook);
log_hook_installed_s = true;
}
#endif
static void link_ble_gatt_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
{
(void)arg;
if (ctxt->op != BLE_GATT_REGISTER_OP_CHR) {
return;
}
uint16_t u16 = ble_uuid_u16(ctxt->chr.chr_def->uuid);
if (u16 == 0xFFE3) {
hb_chr_val_handle_s = ctxt->chr.val_handle;
ESP_LOGI(tag_s, "GATT: FFE3 val_handle=%u", (unsigned)hb_chr_val_handle_s);
} else if (u16 == 0xFFE4) {
ota_stat_chr_val_handle_s = ctxt->chr.val_handle;
ESP_LOGI(tag_s, "GATT: FFE4 val_handle=%u", (unsigned)ota_stat_chr_val_handle_s);
}
}
static int gatt_svr_chr_hb_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
(void)conn_handle;
(void)attr_handle;
(void)arg;
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
if (last_hb_len_s > 0) {
int rc = os_mbuf_append(ctxt->om, last_hb_buf_s, last_hb_len_s);
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
return 0;
}
return BLE_ATT_ERR_UNLIKELY;
}
static int gatt_svr_chr_ota_stat_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
(void)conn_handle;
(void)attr_handle;
(void)arg;
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
if (last_ota_stat_json_s[0] != '\0') {
uint16_t slen = (uint16_t)strnlen(last_ota_stat_json_s, sizeof(last_ota_stat_json_s));
int rc = os_mbuf_append(ctxt->om, last_ota_stat_json_s, slen);
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
return 0;
}
return BLE_ATT_ERR_UNLIKELY;
}
static void ble_heartbeat_task(void *param)
{
(void)param;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(BLE_HEART_PERIOD_MS));
if (!ble_link_up_s || hb_chr_val_handle_s == 0U) {
continue;
}
char dev[32] = {0};
(void)read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
char *json = heart_payload_json_malloc(dev, NULL, false);
if (json == NULL) {
continue;
}
size_t jlen = strlen(json);
if (jlen >= sizeof(last_hb_buf_s)) {
jlen = sizeof(last_hb_buf_s) - 1U;
}
(void)ble_hb_notify_json(json);
free(json);
}
}
/** BLE OTA(0xFFE2):使用通用 OTA 二进制流模块
* 首字节 opcode:0x01 开始(后跟 uint32_t LE 固件总长度),0x02 数据(后跟原始字节),0x03 结束并校验长度后重启。
*/
/* OTA 状态回调:通过 0xFFE4 Notify 发送 OTA 状态 */
static void ble_ota_status_callback(const char *json)
{
if (json == NULL || ota_stat_chr_val_handle_s == 0 || !ble_link_up_s) {
return;
}
size_t n = strlen(json);
if (n == 0 || n > 240) {
return;
}
struct os_mbuf *om = ble_hs_mbuf_from_flat(json, (uint16_t)n);
if (om != NULL) {
ble_gatts_notify_custom(ble_conn_handle_s, ota_stat_chr_val_handle_s, om);
}
}
/* BLE OTA 初始化 */
static int ble_ota_init(void)
{
/* 初始化通用 OTA 模块,传入 BLE 传输类型和状态回调 */
esp_err_t err = ota_binary_stream_init(OTA_TRANSPORT_BLE, ble_ota_status_callback, NULL);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "BLE OTA init failed: %s", esp_err_to_name(err));
return -1;
}
return 0;
}
static int gatt_svr_chr_ota_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
(void)conn_handle;
(void)attr_handle;
(void)arg;
#if !CONFIG_APP_BLE_OTA
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
#endif
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
return BLE_ATT_ERR_UNLIKELY;
}
uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
if (len < 1) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
uint8_t buf[512];
uint16_t copy_len = 0;
int rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &copy_len);
if (rc != 0) {
return BLE_ATT_ERR_UNLIKELY;
}
#if !BUILD_IS_RELEASE
log_ble_rx_payload("FFE2", buf, copy_len);
#endif
/* 使用通用 OTA 二进制流模块处理数据包 */
int result = ota_binary_stream_process_packet(buf, copy_len);
/* 映射返回码为 BLE ATT 错误码 */
if (result == 0) {
return 0; /* 成功 */
} else if (result == -1) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
} else if (result == -2) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
} else {
return BLE_ATT_ERR_UNLIKELY;
}
}
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = BLE_UUID16_DECLARE(0xFFE0),
.characteristics =
(struct ble_gatt_chr_def[]){
{
.uuid = BLE_UUID16_DECLARE(0xFFE1),
.access_cb = gatt_svr_chr_access,
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
},
{
.uuid = BLE_UUID16_DECLARE(0xFFE2),
.access_cb = gatt_svr_chr_ota_access,
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
},
{
.uuid = BLE_UUID16_DECLARE(0xFFE3),
.access_cb = gatt_svr_chr_hb_access,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
},
{
.uuid = BLE_UUID16_DECLARE(0xFFE4),
.access_cb = gatt_svr_chr_ota_stat_access,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
},
{
0,
},
},
},
{
0,
},
};
static int gatt_svr_chr_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
(void)conn_handle;
(void)attr_handle;
(void)arg;
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
if (len > REMOTE_CTRL_JSON_MAX_BYTES) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
uint16_t copy_len = 0;
int rc = ble_hs_mbuf_to_flat(ctxt->om, s_ffe1_json_buf_s, REMOTE_CTRL_JSON_MAX_BYTES, &copy_len);
if (rc != 0) {
return BLE_ATT_ERR_UNLIKELY;
}
s_ffe1_json_buf_s[copy_len] = '\0';
#if !BUILD_IS_RELEASE
log_ble_rx_payload("FFE1", (const uint8_t *)s_ffe1_json_buf_s, copy_len);
ESP_LOGI(tag_s, "FFE1 JSON dispatch start");
#endif
remote_control_apply_json(s_ffe1_json_buf_s);
#if !BUILD_IS_RELEASE
ESP_LOGI(tag_s, "FFE1 JSON dispatch done");
#endif
return 0;
}
return BLE_ATT_ERR_UNLIKELY;
}
static int gap_event(struct ble_gap_event *event, void *arg)
{
(void)arg;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
ESP_LOGI(tag_s, "GAP connect status=%d", event->connect.status);
if (event->connect.status == 0) {
ble_conn_handle_s = event->connect.conn_handle;
ble_link_up_s = true;
/* 连接建立时启动遥控超时守护 */
if (remote_control_watchdog_start() != ESP_OK) {
ESP_LOGW(tag_s, "Failed to start RC watchdog");
}
} else {
ble_link_up_s = false;
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params_s, gap_event, NULL);
}
break;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(tag_s, "GAP disconnect reason=%d", event->disconnect.reason);
ble_link_up_s = false;
remote_control_watchdog_stop(); /* 断开时停止超时守护 */
rc_vehicle_stop(); /* 蓝牙断开时立即停止车辆 */
ESP_LOGI(tag_s, "BLE disconnected, vehicle stopped");
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params_s, gap_event, NULL);
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI(tag_s, "ADV complete reason=%d", event->adv_complete.reason);
break;
default:
break;
}
return 0;
}
static void ble_on_reset(int reason)
{
ESP_LOGE(tag_s, "ble reset reason=%d", reason);
}
static void ble_on_sync(void)
{
int rc = ble_hs_util_ensure_addr(0);
if (rc != 0) {
ESP_LOGE(tag_s, "ble_hs_util_ensure_addr rc=%d", rc);
return;
}
uint8_t own_addr_type = BLE_OWN_ADDR_PUBLIC;
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(tag_s, "ble_hs_id_infer_auto rc=%d", rc);
return;
}
char name[32] = {0};
if (read_from_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, name, sizeof(name)) != ESP_OK || strlen(name) == 0) {
strncpy(name, "ESP32-BLE", sizeof(name) - 1);
}
ble_adv_name_apply_mp_suffix(name, sizeof(name));
ble_svc_gap_device_name_set(name);
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.name = (uint8_t *)name;
fields.name_len = (uint8_t)strlen(name);
fields.name_is_complete = 1;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
ESP_LOGE(tag_s, "adv_set_fields rc=%d", rc);
return;
}
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params_s, gap_event, NULL);
if (rc != 0) {
ESP_LOGE(tag_s, "adv_start rc=%d", rc);
} else {
ESP_LOGI(tag_s, "BLE 广播已启动: %s", name);
}
}
static void ble_host_task(void *param)
{
(void)param;
nimble_port_run();
}
void link_ble_stop(void)
{
/* 简化:不在此版本做完整 teardown */
}
esp_err_t link_ble_start(void)
{
/* 初始化 BLE OTA 模块 */
ble_ota_init();
/* ESP-IDF 5.x:控制器与 VHCI 由 nimble_port_init() 内部完成,勿再调用已移除的 esp_nimble_hci_and_controller_init */
esp_err_t err = nimble_port_init();
if (err != ESP_OK) {
ESP_LOGE(tag_s, "nimble_port_init: %s", esp_err_to_name(err));
return err;
}
ble_hs_cfg.reset_cb = ble_on_reset;
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.gatts_register_cb = link_ble_gatt_register_cb;
#if BUILD_IS_RELEASE
ble_log_hook_install_if_needed();
#endif
ble_svc_gap_init();
ble_svc_gatt_init();
int rc = ble_gatts_count_cfg(gatt_svr_svcs);
if (rc != 0) {
ESP_LOGE(tag_s, "gatts_count_cfg %d", rc);
return ESP_FAIL;
}
rc = ble_gatts_add_svcs(gatt_svr_svcs);
if (rc != 0) {
ESP_LOGE(tag_s, "gatts_add_svcs %d", rc);
return ESP_FAIL;
}
if (app_task_start(APP_TASK_BLE_HEARTBEAT, ble_heartbeat_task, NULL, NULL) != ESP_OK) {
ESP_LOGE(tag_s, "ble heartbeat task create failed");
return ESP_FAIL;
}
if (app_task_start_nimble_host(ble_host_task) != ESP_OK) {
ESP_LOGE(tag_s, "ble host task create failed");
return ESP_FAIL;
}
return ESP_OK;
}
/**
* NimBLE:GAP 广播名来自 NVS `ble_adv_name`,固件会自动追加后缀 **MP**(便于 App 扫描过滤)。
* GATT:Primary 16-bit UUID 0xFFE0
* - 0xFFE1:UTF-8 JSON 遥控(与 MQTT payload 相同结构)
* - 0xFFE2:固件 OTA 二进制流(首字节 opcode:0x01+uint32 LE 总长,0x02+固件数据,0x03 结束并重启;受 menuconfig「APP_BLE_OTA」控制)
* - 0xFFE3:心跳 / 告警 / 错误 JSON,Read + Notify(心跳默认 3s;W/E 出现时即时另发一条)
* - 0xFFE4:OTA 过程状态 JSON,Read(最近一次)+ Notify(每步应答;需 enableNotification)
*/
#ifndef LINK_BLE_H
#define LINK_BLE_H
#include "esp_err.h"
esp_err_t link_ble_start(void);
void link_ble_stop(void);
#endif
#include "link_uart.h"
#include "remote_control.h"
#include "wifidevnum_config.h"
#include "device_nvs.h"
#include "rc_pwm_control.h"
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include "sdkconfig.h"
#include "core/build_config.h"
#include "task_manager.h"
#include "heart_payload.h"
/* OTA 二进制流模块 */
#include "ota_binary_stream.h"
static const char *tag_s = "LINK_UART";
/* UART 配置 */
#define UART_LINK_PORT UART_NUM_1
#define UART_LINK_BUF_SIZE 2048
#define UART_LINK_QUEUE_SIZE 16
#define UART_LINK_RX_TIMEOUT_MS 100
/* JSON 接收缓冲 */
#define UART_JSON_MAX_BYTES REMOTE_CTRL_JSON_MAX_BYTES
/* 心跳周期 */
#ifndef UART_HEART_PERIOD_MS
#define UART_HEART_PERIOD_MS 3000
#endif
/* 协议帧分隔符(使用换行符作为 JSON 分隔) */
#define UART_FRAME_DELIMITER '\n'
/* 函数前向声明 */
static int uart_link_send_json_line(const char *json);
static TaskHandle_t rx_task_handle_s = NULL;
static TaskHandle_t hb_task_handle_s = NULL;
static QueueHandle_t uart_queue_s = NULL;
static volatile bool link_up_s = false;
static volatile uint32_t last_rx_time_ms = 0;
/* 发送互斥 */
static SemaphoreHandle_t tx_mutex_s = NULL;
/* 接收 JSON 缓冲区 */
static char rx_json_buf_s[UART_JSON_MAX_BYTES + 1];
static size_t rx_buf_pos_s = 0;
/* OTA 状态缓冲 */
static char last_ota_stat_json_s[192];
/* OTA 专用接收缓冲 */
static uint8_t ota_rx_buf_s[512];
static size_t ota_rx_pos_s = 0;
static bool ota_rx_mode_s = false;
static uint32_t get_current_time_ms(void)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
/* UART OTA 状态回调:通过 UART 发送 OTA 状态 JSON */
static void uart_ota_status_callback(const char *json)
{
if (json == NULL) {
return;
}
strncpy(last_ota_stat_json_s, json, sizeof(last_ota_stat_json_s) - 1);
last_ota_stat_json_s[sizeof(last_ota_stat_json_s) - 1] = '\0';
/* 直接通过 UART 发送 OTA 状态 */
uart_link_send_json_line(json);
}
/* UART OTA 初始化 */
static void uart_ota_init(void)
{
/* 初始化通用 OTA 模块 */
esp_err_t err = ota_binary_stream_init(OTA_TRANSPORT_UART, uart_ota_status_callback, NULL);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART OTA init failed: %s", esp_err_to_name(err));
} else {
ESP_LOGI(tag_s, "UART OTA initialized");
}
}
static int uart_link_send_internal(const uint8_t *data, size_t len)
{
if (!data || len == 0) {
return -1;
}
int written = uart_write_bytes(UART_LINK_PORT, data, len);
return written;
}
int link_uart_send_raw(const uint8_t *data, size_t len)
{
if (tx_mutex_s == NULL) {
return -1;
}
if (xSemaphoreTake(tx_mutex_s, pdMS_TO_TICKS(100)) != pdTRUE) {
return -1;
}
int ret = uart_link_send_internal(data, len);
xSemaphoreGive(tx_mutex_s);
return ret;
}
static int uart_link_send_json_line(const char *json)
{
if (!json) {
return -1;
}
size_t len = strlen(json);
if (len == 0 || len > 1024) {
return -1;
}
/* 发送 JSON 加换行符 */
if (xSemaphoreTake(tx_mutex_s, pdMS_TO_TICKS(100)) != pdTRUE) {
return -1;
}
int ret = uart_link_send_internal((const uint8_t *)json, len);
if (ret > 0) {
ret += uart_link_send_internal((const uint8_t *)"\n", 1);
}
xSemaphoreGive(tx_mutex_s);
return ret;
}
int link_uart_send_heartbeat(const char *json)
{
return uart_link_send_json_line(json);
}
int link_uart_send_log(const char *json)
{
return uart_link_send_json_line(json);
}
bool link_uart_is_connected(void)
{
/* UART 有最近接收数据即认为在线 */
uint32_t now = get_current_time_ms();
uint32_t last = last_rx_time_ms;
/* 5秒内有数据交互认为在线 */
return link_up_s && (now - last) < 5000;
}
/* 处理接收到的 JSON 数据 */
static void process_received_json(const char *json)
{
if (!json || strlen(json) == 0) {
return;
}
#if !BUILD_IS_RELEASE
ESP_LOGI(tag_s, "RX JSON: %s", json);
#endif
/* 更新最后接收时间 */
last_rx_time_ms = get_current_time_ms();
link_up_s = true;
/* 调用遥控协议处理 JSON */
remote_control_apply_json(json);
}
/* 处理 OTA 数据包(二进制格式) */
static void process_ota_packet(const uint8_t *data, uint16_t len)
{
if (!data || len == 0) {
return;
}
/* 通过通用 OTA 模块处理 */
ota_binary_stream_process_packet(data, len);
/* OTA 数据包也更新连接状态 */
last_rx_time_ms = get_current_time_ms();
link_up_s = true;
}
/* 检查数据是否为 OTA 命令(以 0x01/0x02/0x03 开头) */
static bool is_ota_command(uint8_t first_byte)
{
return (first_byte == 0x01 || first_byte == 0x02 || first_byte == 0x03);
}
static void uart_rx_task(void *param)
{
(void)param;
uint8_t *data = (uint8_t *)malloc(UART_LINK_BUF_SIZE);
if (!data) {
ESP_LOGE(tag_s, "RX buffer alloc failed");
vTaskDelete(NULL);
return;
}
rx_buf_pos_s = 0;
ota_rx_pos_s = 0;
ota_rx_mode_s = false;
while (1) {
int len = uart_read_bytes(UART_LINK_PORT, data, UART_LINK_BUF_SIZE - 1,
pdMS_TO_TICKS(UART_LINK_RX_TIMEOUT_MS));
if (len > 0) {
last_rx_time_ms = get_current_time_ms();
/* 检查是否为 OTA 命令(首字节为 0x01/0x02/0x03) */
if (len > 0 && is_ota_command(data[0])) {
/* OTA 二进制数据模式 */
process_ota_packet(data, len);
continue;
}
/* JSON 文本模式 - 处理接收到的数据,按行分隔 */
for (int i = 0; i < len; i++) {
uint8_t ch = data[i];
/* 如果遇到 OTA 命令字节,切换模式 */
if (is_ota_command(ch) && i == 0 && rx_buf_pos_s == 0) {
/* 复制剩余数据到 OTA 缓冲区 */
uint16_t remaining = len - i;
if (remaining > sizeof(ota_rx_buf_s)) {
remaining = sizeof(ota_rx_buf_s);
}
memcpy(ota_rx_buf_s, data + i, remaining);
process_ota_packet(ota_rx_buf_s, remaining);
break;
}
if (ch == UART_FRAME_DELIMITER || ch == '\r') {
/* 找到行尾,处理 JSON */
if (rx_buf_pos_s > 0) {
rx_json_buf_s[rx_buf_pos_s] = '\0';
process_received_json(rx_json_buf_s);
rx_buf_pos_s = 0;
}
} else {
/* 累积字符 */
if (rx_buf_pos_s < UART_JSON_MAX_BYTES) {
rx_json_buf_s[rx_buf_pos_s++] = ch;
} else {
/* 缓冲区溢出,重置 */
rx_buf_pos_s = 0;
}
}
}
}
/* 检查连接状态 */
uint32_t now = get_current_time_ms();
if (last_rx_time_ms > 0 && (now - last_rx_time_ms) > 5000) {
link_up_s = false;
}
}
free(data);
vTaskDelete(NULL);
}
static void uart_heartbeat_task(void *param)
{
(void)param;
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
while (1) {
vTaskDelay(pdMS_TO_TICKS(UART_HEART_PERIOD_MS));
/* 生成心跳 JSON(与 BLE 0xFFE3 相同结构) */
char *heartbeat = heart_payload_json_malloc(devid, NULL, false);
if (heartbeat) {
link_uart_send_heartbeat(heartbeat);
free(heartbeat);
}
}
}
static esp_err_t uart_link_init_hardware(void)
{
uart_config_t uart_cfg = {
.baud_rate = CONFIG_APP_UART_LINK_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_param_config(UART_LINK_PORT, &uart_cfg);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART param config failed: %d", err);
return err;
}
/* 设置引脚:GPIO17 TX, GPIO18 RX */
int tx_gpio = CONFIG_APP_UART_LINK_TX_GPIO;
int rx_gpio = CONFIG_APP_UART_LINK_RX_GPIO;
if (tx_gpio < 0 || rx_gpio < 0) {
ESP_LOGE(tag_s, "Invalid UART GPIO config: TX=%d, RX=%d", tx_gpio, rx_gpio);
return ESP_ERR_INVALID_ARG;
}
err = uart_set_pin(UART_LINK_PORT, tx_gpio, rx_gpio,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART set pin failed: %d", err);
return err;
}
err = uart_driver_install(UART_LINK_PORT, UART_LINK_BUF_SIZE * 2,
UART_LINK_BUF_SIZE * 2, UART_LINK_QUEUE_SIZE,
&uart_queue_s, 0);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART driver install failed: %d", err);
return err;
}
return ESP_OK;
}
esp_err_t link_uart_start(void)
{
ESP_LOGI(tag_s, "启动 UART 链路 (GPIO%d TX / GPIO%d RX, %d baud)",
CONFIG_APP_UART_LINK_TX_GPIO, CONFIG_APP_UART_LINK_RX_GPIO,
CONFIG_APP_UART_LINK_BAUDRATE);
/* 初始化 UART OTA 模块 */
uart_ota_init();
/* 初始化 UART 硬件 */
esp_err_t err = uart_link_init_hardware();
if (err != ESP_OK) {
return err;
}
/* 创建发送互斥锁 */
tx_mutex_s = xSemaphoreCreateMutex();
if (!tx_mutex_s) {
ESP_LOGE(tag_s, "TX mutex create failed");
uart_driver_delete(UART_LINK_PORT);
return ESP_ERR_NO_MEM;
}
/* 启动遥控看门狗(与 BLE 模式相同) */
remote_control_watchdog_start();
/* 启动接收任务 */
BaseType_t ret = xTaskCreate(uart_rx_task, "uart_rx",
4096, NULL, 5, &rx_task_handle_s);
if (ret != pdPASS) {
ESP_LOGE(tag_s, "RX task create failed");
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
uart_driver_delete(UART_LINK_PORT);
return ESP_ERR_NO_MEM;
}
/* 启动心跳任务 */
ret = xTaskCreate(uart_heartbeat_task, "uart_hb",
4096, NULL, 5, &hb_task_handle_s);
if (ret != pdPASS) {
ESP_LOGE(tag_s, "Heartbeat task create failed");
vTaskDelete(rx_task_handle_s);
rx_task_handle_s = NULL;
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
uart_driver_delete(UART_LINK_PORT);
return ESP_ERR_NO_MEM;
}
link_up_s = true;
last_rx_time_ms = get_current_time_ms();
ESP_LOGI(tag_s, "UART 链路启动成功");
return ESP_OK;
}
void link_uart_stop(void)
{
/* 停止心跳任务 */
if (hb_task_handle_s) {
vTaskDelete(hb_task_handle_s);
hb_task_handle_s = NULL;
}
/* 停止接收任务 */
if (rx_task_handle_s) {
vTaskDelete(rx_task_handle_s);
rx_task_handle_s = NULL;
}
/* 停止遥控看门狗 */
remote_control_watchdog_stop();
/* 删除互斥锁 */
if (tx_mutex_s) {
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
}
/* 卸载 UART 驱动 */
uart_driver_delete(UART_LINK_PORT);
link_up_s = false;
ESP_LOGI(tag_s, "UART 链路已停止");
}
/**
* UART 链路通信模块
* 用于 UART 链路模式下与安卓进行串口通信
* 使用 UART1 (GPIO17 TX / GPIO18 RX)
*
* 协议格式:
* - 遥控指令:UTF-8 JSON(与 MQTT/BLE payload 相同结构)
* - 心跳:周期 JSON,包含 device_id、状态等信息
* - 日志:W/E 日志即时推送
*/
#ifndef LINK_UART_H
#define LINK_UART_H
#include "esp_err.h"
#include <stdbool.h>
#include <stddef.h>
/**
* @brief 启动 UART 链路通信
* 初始化 UART1 (GPIO17 TX / GPIO18 RX),启动接收和发送任务
* @return ESP_OK 成功,其他错误码失败
*/
esp_err_t link_uart_start(void);
/**
* @brief 停止 UART 链路通信
* 停止 UART 任务,释放资源
*/
void link_uart_stop(void);
/**
* @brief 发送心跳 JSON 数据
* @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败
*/
int link_uart_send_heartbeat(const char *json);
/**
* @brief 发送日志/告警 JSON 数据
* @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败
*/
int link_uart_send_log(const char *json);
/**
* @brief 检查 UART 链路是否已连接(有数据交互即认为在线)
* @return true 在线,false 离线
*/
bool link_uart_is_connected(void);
/**
* @brief 发送原始数据到 UART
* @param data 数据指针
* @param len 数据长度
* @return 实际发送字节数,-1 失败
*/
int link_uart_send_raw(const uint8_t *data, size_t len);
#endif /* LINK_UART_H */
#include "mqttconf_commun.h"
#include "wifidevnum_config.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_http_client.h"
#include "mqtt_client.h"
#include "esp_crt_bundle.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "driver/gpio.h"
#include "cJSON.h"
#include "sdkconfig.h"
#include "esp_heap_caps.h"
#include "esp_ota_ops.h"
#include "esp_app_format.h"
#include "rc_pwm_control.h"
#include "betteryread.h"
#include "ota.h"
#include "remote_control.h"
#include "heart_payload.h"
#include "task_manager.h"
static const char *tag_s = "MQTT_MGR";
#define DEFAULT_MQTT_HOST "119.45.167.177"
// 最多支持同时连接的服务器数量
#define MAX_MQTT_CLIENTS 10
typedef struct http_response_s {
char *buffer;
int len;
} http_response_t;
char g_device_id[32] = {0};
static char g_sta_ip[16] = "0.0.0.0";
// 存储所有活跃的 MQTT 客户端句柄,用于心跳群发
static esp_mqtt_client_handle_t client_list[MAX_MQTT_CLIENTS] = {NULL};
static int client_count = 0;
static TaskHandle_t g_hb_task = NULL;
static volatile bool g_hb_task_running = false;
static int mqttabnormal_exit_count = 0;
int mqtt_send_deviceota_info(void);
/**
* @brief 构造心跳包内容
*/
static char* generate_hb_payload(void)
{
return heart_payload_json_malloc(g_device_id, g_sta_ip, true);
}
static void mqtt_heartbeat_send_once(void) {
char *json = generate_hb_payload();
if (!json) return;
//for (int i = 0; i < client_count; i++) {
//if (client_list[i] != NULL) {
if (strlen(g_device_id) == 0) {
esp_mqtt_client_publish(client_list[0], "000000000", json, 0, 1, 0);
esp_mqtt_client_publish(client_list[0], "dev2app/000000000", json, 0, 1, 0);
free(json);
return;
}
// 向每个服务器发送心跳,确保 App 在每个 Broker 都能看到设备在线
esp_mqtt_client_publish(client_list[0], g_device_id, json, 0, 1, 0);
char t2[64];
snprintf(t2, sizeof(t2), "dev2app/%s", g_device_id);
esp_mqtt_client_publish(client_list[0], t2, json, 0, 1, 0);
// }
//}
free(json);
}
static void mqtt_heartbeat_task(void *arg)
{
(void)arg;
while (g_hb_task_running) {
vTaskDelay(pdMS_TO_TICKS(3000));
if (!g_hb_task_running) {
break;
}
mqtt_heartbeat_send_once();
}
vTaskDelete(NULL);
}
// 定时器句柄
TimerHandle_t ota_msg_timer;
// 定时器触发后的回调函数
void ota_msg_send_task(void *pvParameters) {
// 将传入的参数转回延迟秒数
int delay_seconds = (int)(intptr_t)pvParameters;
// 在任务内部进行延迟,不阻塞其他程序
vTaskDelay(pdMS_TO_TICKS(delay_seconds * 1000));
ESP_LOGI("OTA_TASK", "延迟结束,正在发送 MQTT OTA 信息...");
// 执行你的 MQTT 发送逻辑
mqtt_send_deviceota_info();
// 2. 关键:发送完毕后,任务自行销毁,实现“自动取消”和内存释放
ESP_LOGI("OTA_TASK", "发送完成,任务已自动退出并销毁");
vTaskDelete(NULL);
}
/**
* @brief 启动延迟发送
* @param delay_seconds 延迟几秒发送
*/
void start_ota_report_timer(int delay_seconds) {
ESP_LOGI("OTA_TASK", "创建一次性任务,将在 %d 秒后执行", delay_seconds);
(void)app_task_start(APP_TASK_MQTT_OTA_REPORT, ota_msg_send_task, (void *)(intptr_t)delay_seconds, NULL);
}
/**
* @brief MQTT 事件回调
*/
static void common_mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
esp_mqtt_event_handle_t event = event_data;
const char *ip = (const char *)handler_args;
switch (event->event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(tag_s, "[%s] 已连接", ip);
if(strcmp(ip,DEFAULT_MQTT_HOST)==0){
start_ota_report_timer(5);
}
char s1[64], s2[64];
snprintf(s1, sizeof(s1), "app2dev/%s", g_device_id);
snprintf(s2, sizeof(s2), "ser2dev/%s", g_device_id);
esp_mqtt_client_subscribe(event->client, s1, 1);
esp_mqtt_client_subscribe(event->client, s2, 1);
break;
case MQTT_EVENT_DATA:
if (event->data_len > 0) {
char *tmp = malloc(event->data_len + 1);
if (tmp) {
mqttabnormal_exit_count = 0;
memcpy(tmp, event->data, event->data_len);
tmp[event->data_len] = '\0';
remote_control_apply_json(tmp);
free(tmp);
}
}
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(tag_s, "[%s] 连接断开", ip);
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(tag_s, "[%s] 发生错误", ip);
break;
default: break;
}
}
/**
* @brief 初始化并启动一个 MQTT 客户端
*/
static void start_mqtt_client(const char *ip) {
if (!ip || client_count >= MAX_MQTT_CLIENTS) return;
char *persist_ip = strdup(ip);
char uri[128];
snprintf(uri, sizeof(uri), "mqtt://%s:1883", persist_ip);
ESP_LOGI(tag_s, "启动并发连接: %s", uri);
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = uri,
.credentials.username = "admin",
.credentials.authentication.password = "admin",
.session.keepalive = 60,
.network.timeout_ms = 20000,
.buffer.size = 2048,
.task.stack_size = 6144,
};
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
if (client) {
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, common_mqtt_event_handler, (void*)persist_ip);
esp_mqtt_client_start(client);
client_list[client_count++] = client; // 加入心跳群发列表
}
}
esp_err_t _http_event_handler(esp_http_client_event_t *evt) {
http_response_t *response = (http_response_t *)evt->user_data;
if (evt->event_id == HTTP_EVENT_ON_DATA) {
char *new_buf = realloc(response->buffer, response->len + evt->data_len + 1);
if (new_buf) {
response->buffer = new_buf;
memcpy(response->buffer + response->len, evt->data, evt->data_len);
response->len += evt->data_len;
response->buffer[response->len] = '\0';
}
}
return ESP_OK;
}
/**
* @brief 初始化任务:拉取列表并同时启动所有连接
*/
static void config_pull_and_init_task(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(5000));
esp_netif_ip_info_t ip_info;
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
esp_ip4addr_ntoa(&ip_info.ip, g_sta_ip, sizeof(g_sta_ip));
}
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, g_device_id, sizeof(g_device_id)-1);
rc_pwm_control_set_drive_from_device_id(g_device_id);
if (strlen(g_device_id) == 0) {
start_mqtt_client(DEFAULT_MQTT_HOST);
goto start_timer;
}
char url[256];
snprintf(url, sizeof(url), "%s%s", CONFIG_ROBOIOT_MQTT_URL, g_device_id);
http_response_t resp = { .buffer = NULL, .len = 0 };
esp_http_client_config_t http_cfg = {
.url = url,
.event_handler = _http_event_handler,
.user_data = &resp,
.crt_bundle_attach = esp_crt_bundle_attach,
.timeout_ms = 8000,
};
esp_http_client_handle_t http_client = esp_http_client_init(&http_cfg);
bool dynamic_ok = false;
if (esp_http_client_perform(http_client) == ESP_OK && resp.buffer) {
cJSON *root = cJSON_Parse(resp.buffer);
if (root) {
cJSON *data_node = cJSON_GetObjectItem(root, "data");
if (data_node) {
cJSON *mqtt_list = cJSON_GetObjectItem(data_node, "mqtt");
if (cJSON_IsArray(mqtt_list)) {
dynamic_ok = true;
int sz = cJSON_GetArraySize(mqtt_list);
for (int i = 0; i < sz; i++) {
cJSON *item = cJSON_GetArrayItem(mqtt_list, i);
if (cJSON_IsString(item)) {
start_mqtt_client(item->valuestring); // 同时启动
}
}
}
}
cJSON_Delete(root);
}
}
if (resp.buffer) free(resp.buffer);
esp_http_client_cleanup(http_client);
if (!dynamic_ok) {
start_mqtt_client(DEFAULT_MQTT_HOST);
}
start_timer:
// 统一心跳任务(每3秒群发一次),支持绑核
if (g_hb_task == NULL) {
g_hb_task_running = true;
if (app_task_start(APP_TASK_MQTT_HEARTBEAT, mqtt_heartbeat_task, NULL, &g_hb_task) != ESP_OK) {
g_hb_task_running = false;
ESP_LOGE(tag_s, "heart task create failed");
} else {
ESP_LOGI(tag_s, "并发心跳群发任务已启动(core=auto)");
}
}
vTaskDelete(NULL);
}
// 新增停止函数
void mqtt_manager_stop(void) {
ESP_LOGW(tag_s, "正在停止 MQTT 服务...");
// 1. 停止心跳任务
if (g_hb_task != NULL) {
g_hb_task_running = false;
app_task_stop(&g_hb_task);
}
// 2. 销毁所有 MQTT 客户端
for (int i = 0; i < client_count; i++) {
if (client_list[i] != NULL) {
esp_mqtt_client_stop(client_list[i]);
esp_mqtt_client_disconnect(client_list[i]);
esp_mqtt_client_destroy(client_list[i]);
client_list[i] = NULL;
}
}
client_count = 0;
ESP_LOGI(tag_s, "MQTT 服务已完全停止");
}
/**
* @brief 发送设备信息到 MQTT(JSON 格式)
* @return 0 成功,-1 失败
*/
int mqtt_send_deviceota_info(void)
{
cJSON *root = NULL;
char *json_str = NULL;
int ret = -1;
/* 1. 创建 JSON 对象 */
root = cJSON_CreateObject();
if (!root) {
return -1;
}
/* 2. 添加字段(直接读取全局变量) */
cJSON_AddStringToObject(root, "device_id", g_device_id);
const esp_app_desc_t *app_desc = esp_app_get_description();
const char* running_version = app_desc->version;
cJSON_AddStringToObject(root, "version", running_version);
/* 3. JSON 转字符串(紧凑格式,适合 MQTT) */
json_str = cJSON_PrintUnformatted(root);
if (!json_str) {
goto exit;
}
/* 4. MQTT 发送 */
esp_mqtt_client_publish(client_list[0], "esp32updata",json_str, 0, 1, 0);
exit:
if (json_str) {
free(json_str);
}
if (root) {
cJSON_Delete(root);
}
return ret;
}
/*
* @brief MQTT 异常任务
*/
void mqtt_abnormal_exittask(void *pvParameters)
{
vTaskDelay(pdMS_TO_TICKS(3000));
ESP_LOGI(tag_s, "MQTT异常任务启动");
while(1){
if(client_list[0] == NULL) {
vTaskDelay(pdMS_TO_TICKS(1000));
continue;
}
if(mqttabnormal_exit_count>5){
rc_vehicle_stop();
mqttabnormal_exit_count=6;
}
mqttabnormal_exit_count++;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/* MQTT 异常任务初始化*/
void mqtt_abnormal_exittask_init(void) {
(void)app_task_start(APP_TASK_MQTT_EXIT, mqtt_abnormal_exittask, NULL, NULL);
}
/**
* @brief MQTT 初始化任务
*/
void mqtt_manager_init_sequence(void) {
(void)app_task_start(APP_TASK_MQTT_INIT, config_pull_and_init_task, NULL, NULL);
}
\ No newline at end of file
#ifndef __MQTTCONF_COMMUN_H__
#define __MQTTCONF_COMMUN_H__
#include "esp_err.h"
#define MQTT_SERVICE_NUM_MAX 10
// 启动所有 MQTT 相关逻辑
void mqtt_manager_init_sequence(void);
void mqtt_manager_stop(void);
#endif
\ No newline at end of file
#include "app_run.h"
void app_main(void)
{
app_run();
}
#include "ota.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_app_format.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_crt_bundle.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "wifidevnum_config.h" // 假设你有封装好的 read_from_nvs 和 save_to_nvs
#include "esp_spiffs.h"
#include <cJSON.h>
#include <string.h>
static const char *tag_s = "OTA_UPDATE";
/**
* @brief HTTP 事件处理
*/
esp_err_t _ota_http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ERROR: ESP_LOGE(tag_s, "HTTP_EVENT_ERROR"); break;
case HTTP_EVENT_ON_CONNECTED: ESP_LOGI(tag_s, "已连接到服务器"); break;
case HTTP_EVENT_ON_FINISH: ESP_LOGI(tag_s, "数据传输完成"); break;
default: break;
}
return ESP_OK;
}
/**
* @brief 启动 OTA
*/
void start_ota_update(const char* url) {
ESP_LOGI(tag_s, "正在从 URL 启动 OTA: %s", url);
esp_http_client_config_t config = {
.url = url,
.crt_bundle_attach = esp_crt_bundle_attach,
.event_handler = _ota_http_event_handler,
.keep_alive_enable = true,
.timeout_ms = 10000,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(tag_s, "正在下载并写入固件... 请勿断电");
esp_err_t ret = esp_https_ota(&ota_config);
// 在这里获取更新后的分区指针
const esp_partition_t *updated_partition = esp_ota_get_next_update_partition(NULL);
if (ret == ESP_OK) {
ESP_LOGI(tag_s, "OTA 升级成功!即将重启...");
ESP_LOGI(tag_s, "找到更新分区: %s (偏移: 0x%08x)",
updated_partition->label, updated_partition->address);
esp_err_t set_err = esp_ota_set_boot_partition(updated_partition);
if (set_err == ESP_OK) {
ESP_LOGI(tag_s, "启动分区设置成功!");
// 验证设置结果
const esp_partition_t *boot_partition = esp_ota_get_boot_partition();
const esp_partition_t *running_partition = esp_ota_get_running_partition();
ESP_LOGI(tag_s, "当前运行分区: %s (0x%08x)",
running_partition->label, running_partition->address);
ESP_LOGI(tag_s, "下次启动分区: %s (0x%08x)",
boot_partition->label, boot_partition->address);
// 等待设置完成
vTaskDelay(2000 / portTICK_PERIOD_MS);
esp_restart();
}else {
ESP_LOGE(tag_s, "设置启动分区失败: %s", esp_err_to_name(set_err));
}
} else {
ESP_LOGE(tag_s, "OTA 升级失败: %s", esp_err_to_name(ret));
}
}
/**
* @brief MQTT 消息处理函数:解析 JSON 并判断是否需要更新
*/
int ota_update_recmqtt(const char *json_data) {
if (json_data == NULL) return -1;
cJSON *root = cJSON_Parse(json_data);
if (root == NULL) return -1;
ESP_LOGI(tag_s, "当前固件运行版本: %s", CONFIG_MY_APP_VERSION);
// 2. 解析云端发来的新版本号
cJSON *new_version_obj = cJSON_GetObjectItem(root, "new_version");
if (!cJSON_IsString(new_version_obj) || (new_version_obj->valuestring == NULL)) {
cJSON_Delete(root);
return -1;
}
const char* cloud_version = new_version_obj->valuestring;
// 3. 版本对比
if (strcmp(CONFIG_MY_APP_VERSION, cloud_version) == 0) {
ESP_LOGI(tag_s, "当前已是最新版本 (%s),跳过更新。", CONFIG_MY_APP_VERSION);
cJSON_Delete(root);
return 0;
}
ESP_LOGI(tag_s, "检测到新版本: %s -> %s",CONFIG_MY_APP_VERSION, cloud_version);
// 5. 解析下载地址并启动更新
cJSON *esp32_url = cJSON_GetObjectItem(root, "esp32_url");
if (cJSON_IsString(esp32_url) && (esp32_url->valuestring != NULL)) {
start_ota_update(esp32_url->valuestring);
} else {
ESP_LOGE(tag_s, "JSON 中未找到有效的 esp32_url");
}
cJSON_Delete(root);
return 0;
}
#ifndef __OTA_H__
#define __OTA_H__
#include "esp_err.h"
int ota_update_recmqtt(const char *json_data);
#endif
\ No newline at end of file
/*
* OTA 二进制流接收模块实现
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
*/
#include "ota_binary_stream.h"
#include "ota_manager.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
static const char *tag_s = "OTA_STREAM";
/* OTA 会话状态 */
typedef struct {
bool active;
esp_ota_handle_t handle;
const esp_partition_t *partition;
size_t expected_size;
size_t written_size;
ota_transport_type_t transport;
ota_binary_stream_status_callback_t status_callback;
} ota_stream_session_t;
static ota_stream_session_t session_s = {0};
/* 格式化并发送状态回调 */
static void ota_stream_send_status_fmt(const char *fmt, ...)
{
if (session_s.status_callback == NULL) {
return;
}
char buf[192];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
session_s.status_callback(buf);
}
/* 发送错误状态 */
static void ota_stream_send_error(const char *step, const char *err)
{
ESP_LOGE(tag_s, "OTA error at %s: %s", step, err);
ota_stream_send_status_fmt("{\"ota\":\"%s\",\"ok\":0,\"err\":\"%s\"}", step, err);
}
/* 中止 OTA 会话 */
static void ota_stream_abort_internal(void)
{
if (session_s.active && session_s.handle != 0) {
esp_ota_abort(session_s.handle);
ESP_LOGW(tag_s, "OTA session aborted");
}
session_s.active = false;
session_s.handle = 0;
session_s.partition = NULL;
session_s.expected_size = 0;
session_s.written_size = 0;
}
void ota_binary_stream_abort(void)
{
ota_stream_abort_internal();
}
bool ota_binary_stream_is_active(void)
{
return session_s.active;
}
esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
ota_binary_stream_status_callback_t status_callback,
const char *target_version)
{
if (session_s.active) {
ESP_LOGW(tag_s, "OTA already active");
return ESP_ERR_INVALID_STATE;
}
session_s.transport = transport;
session_s.status_callback = status_callback;
/* 通过OTA管理器开始会话 */
ota_type_t type = (transport == OTA_TRANSPORT_BLE) ? OTA_TYPE_BLE : OTA_TYPE_UART;
esp_err_t err = ota_manager_begin_session(type, target_version ? target_version : "unknown");
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to begin OTA session in manager: %s", esp_err_to_name(err));
}
ESP_LOGI(tag_s, "OTA stream init: transport=%d", transport);
return ESP_OK;
}
void ota_binary_stream_deinit(void)
{
if (session_s.active) {
ota_stream_abort_internal();
}
session_s.status_callback = NULL;
}
/* 处理 OTA 开始命令 */
static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
{
if (len < 5) {
/* 需要至少 1字节opcode + 4字节长度 */
ota_stream_send_error("begin", "size");
return -1;
}
/* 解析固件长度(小端序 uint32) */
uint32_t image_size = ((uint32_t)data[1]) |
((uint32_t)data[2] << 8) |
((uint32_t)data[3] << 16) |
((uint32_t)data[4] << 24);
if (image_size == 0 || image_size > (6 * 1024 * 1024)) {
/* 固件大小不合理(最大6MB) */
ota_stream_send_error("begin", "size_invalid");
return -2;
}
ESP_LOGI(tag_s, "OTA begin: size=%lu bytes", (unsigned long)image_size);
/* 查找 OTA 更新分区 */
session_s.partition = esp_ota_get_next_update_partition(NULL);
if (session_s.partition == NULL) {
ota_stream_send_error("begin", "no_partition");
return -3;
}
ESP_LOGI(tag_s, "OTA partition: %s at 0x%08x",
session_s.partition->label,
(unsigned)session_s.partition->address);
/* 开始 OTA */
esp_err_t err = esp_ota_begin(session_s.partition, image_size, &session_s.handle);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_begin failed: %s", esp_err_to_name(err));
ota_stream_send_error("begin", "esp_ota_begin");
session_s.handle = 0;
session_s.partition = NULL;
return -4;
}
session_s.expected_size = image_size;
session_s.written_size = 0;
session_s.active = true;
ota_stream_send_status_fmt("{\"ota\":\"begin\",\"ok\":1,\"expect\":%lu}",
(unsigned long)image_size);
return 0;
}
/* 处理 OTA 数据包 */
static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
{
if (!session_s.active) {
ota_stream_send_error("chunk", "no_session");
return -1;
}
if (len < 1) {
/* 空数据包 */
return 0;
}
/* data[0] 是 opcode,后面是实际数据 */
const uint8_t *payload = data + 1;
uint16_t payload_len = len - 1;
if (payload_len == 0) {
return 0;
}
/* 检查是否超出预期大小 */
if (session_s.written_size + payload_len > session_s.expected_size) {
ESP_LOGE(tag_s, "OTA overflow: %u + %u > %u",
(unsigned)session_s.written_size,
(unsigned)payload_len,
(unsigned)session_s.expected_size);
ota_stream_send_error("chunk", "overflow");
ota_stream_abort_internal();
return -2;
}
/* 写入数据 */
esp_err_t err = esp_ota_write(session_s.handle, payload, payload_len);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_write failed: %s", esp_err_to_name(err));
ota_stream_send_error("chunk", "write");
ota_stream_abort_internal();
return -3;
}
session_s.written_size += payload_len;
/* 每 8192 字节发送一次进度,或最后一次 */
static size_t last_reported = 0;
if (session_s.written_size - last_reported >= 8192 ||
session_s.written_size >= session_s.expected_size) {
ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size);
last_reported = session_s.written_size;
}
return 0;
}
/* 处理 OTA 结束命令 */
static int ota_stream_handle_end(void)
{
if (!session_s.active) {
ota_stream_send_error("end", "no_session");
return -1;
}
/* 检查写入大小是否匹配 */
if (session_s.written_size != session_s.expected_size) {
ESP_LOGE(tag_s, "OTA incomplete: %u/%u",
(unsigned)session_s.written_size,
(unsigned)session_s.expected_size);
ota_stream_send_error("end", "incomplete");
ota_stream_abort_internal();
return -2;
}
/* 结束 OTA 写入 */
esp_err_t err = esp_ota_end(session_s.handle);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_end failed: %s", esp_err_to_name(err));
ota_stream_send_error("end", "esp_ota_end");
ota_stream_abort_internal();
return -3;
}
session_s.handle = 0;
session_s.active = false;
/* 设置启动分区 */
err = esp_ota_set_boot_partition(session_s.partition);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
ota_stream_send_error("end", "set_boot");
return -4;
}
ESP_LOGI(tag_s, "OTA successful, written %u bytes",
(unsigned)session_s.written_size);
/* 通过OTA管理器结束会话 */
ota_manager_end_session();
ota_stream_send_status_fmt("{\"ota\":\"end\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size);
/* 延时后重启 */
ESP_LOGI(tag_s, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
return 0;
}
int ota_binary_stream_process_packet(const uint8_t *data, uint16_t len)
{
if (data == NULL || len == 0) {
return -1;
}
uint8_t opcode = data[0];
switch (opcode) {
case OTA_OP_BEGIN:
return ota_stream_handle_begin(data, len);
case OTA_OP_DATA:
return ota_stream_handle_data(data, len);
case OTA_OP_END:
return ota_stream_handle_end();
default:
ESP_LOGW(tag_s, "Unknown OTA opcode: 0x%02X", opcode);
ota_stream_send_error("unknown", "invalid_opcode");
return -10;
}
}
esp_err_t ota_binary_stream_get_status_json(char *buf, size_t len)
{
if (buf == NULL || len == 0) {
return ESP_ERR_INVALID_ARG;
}
if (!session_s.active) {
strncpy(buf, "{\"ota\":\"idle\"}", len);
return ESP_OK;
}
snprintf(buf, len,
"{\"ota\":\"active\",\"written\":%u,\"expect\":%u,\"transport\":%d}",
(unsigned)session_s.written_size,
(unsigned)session_s.expected_size,
session_s.transport);
return ESP_OK;
}
/*
* OTA 二进制流接收模块
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
* 协议:首字节 opcode + 数据
* 0x01 + uint32_t LE(4字节) = 开始OTA,指定固件总长度
* 0x02 + 数据 = 固件数据块
* 0x03 = 结束OTA,触发校验和重启
*/
#ifndef OTA_BINARY_STREAM_H
#define OTA_BINARY_STREAM_H
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* OTA 状态回调类型 */
typedef void (*ota_binary_stream_status_callback_t)(const char *json);
/* OTA 传输类型 */
typedef enum {
OTA_TRANSPORT_BLE = 0, /* BLE GATT OTA */
OTA_TRANSPORT_UART, /* UART 串口 OTA */
} ota_transport_type_t;
/**
* @brief 初始化 OTA 二进制流接收器
* @param transport 传输类型(BLE 或 UART)
* @param status_callback 状态回调函数,用于发送 OTA 状态 JSON
* @param target_version 目标版本号(可选,用于OTA管理器)
* @return ESP_OK 成功,其他错误码
*/
esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
ota_binary_stream_status_callback_t status_callback,
const char *target_version);
/**
* @brief 反初始化 OTA 二进制流接收器
* 如果 OTA 进行中,会自动中止
*/
void ota_binary_stream_deinit(void);
/**
* @brief 处理接收到的 OTA 数据包
* 由传输层(BLE/UART)调用
* @param data 数据指针
* @param len 数据长度
* @return 0 成功处理,负数错误码(传输层可映射为相应错误)
*/
int ota_binary_stream_process_packet(const uint8_t *data, uint16_t len);
/**
* @brief 检查 OTA 是否进行中
* @return true OTA进行中,false 空闲
*/
bool ota_binary_stream_is_active(void);
/**
* @brief 中止当前 OTA 会话
* 清理资源,擦除不完整的固件写入
*/
void ota_binary_stream_abort(void);
/**
* @brief 获取 OTA 状态 JSON 字符串
* 用于主动查询OTA状态
* @param buf 输出缓冲区
* @param len 缓冲区长度
* @return ESP_OK 成功
*/
esp_err_t ota_binary_stream_get_status_json(char *buf, size_t len);
/* OTA 操作码定义(与数据包格式一致) */
#define OTA_OP_BEGIN 0x01 /* 开始OTA会话 */
#define OTA_OP_DATA 0x02 /* 传输固件数据 */
#define OTA_OP_END 0x03 /* 结束OTA会话 */
#ifdef __cplusplus
}
#endif
#endif /* OTA_BINARY_STREAM_H */
/*
* OTA 状态管理器实现
* 管理OTA更新状态,支持失败回退到上一版本
*/
#include "ota_manager.h"
#include "device_nvs.h"
#include "nvs.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_app_desc.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include <string.h>
#include <stdio.h>
static const char *tag_s = "OTA_MGR";
#define OTA_MGR_NVS_NAMESPACE "ota_mgr"
#define OTA_MGR_KEY_STATE "state"
#define OTA_MGR_KEY_TYPE "type"
#define OTA_MGR_KEY_TARGET_VER "tgt_ver"
#define OTA_MGR_KEY_SOURCE_VER "src_ver"
#define OTA_MGR_KEY_TIMESTAMP "timestamp"
#define OTA_MGR_KEY_BOOT_COUNT "boot_cnt"
#define OTA_MGR_KEY_MAX_ATTEMPTS "max_retry"
#define OTA_MGR_KEY_LAST_SUCCESS_VER "last_ok_ver"
#define OTA_MAX_BOOT_ATTEMPTS 3 /* 最大启动尝试次数 */
#define OTA_SESSION_MAGIC 0x4F544100u /* "OTA\0" */
static ota_session_info_t current_session_s;
static bool session_loaded_s = false;
/**
* @brief 从NVS读取会话信息
*/
static esp_err_t load_session_from_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(OTA_MGR_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) {
return err;
}
uint32_t magic = 0;
err = nvs_get_u32(handle, "magic", &magic);
if (err != ESP_OK || magic != OTA_SESSION_MAGIC) {
nvs_close(handle);
memset(&current_session_s, 0, sizeof(current_session_s));
session_loaded_s = false;
return ESP_ERR_NOT_FOUND;
}
uint8_t state = 0;
nvs_get_u8(handle, OTA_MGR_KEY_STATE, &state);
current_session_s.state = (ota_state_t)state;
uint8_t type = 0;
nvs_get_u8(handle, OTA_MGR_KEY_TYPE, &type);
current_session_s.type = (ota_type_t)type;
size_t len = sizeof(current_session_s.target_version);
nvs_get_str(handle, OTA_MGR_KEY_TARGET_VER, current_session_s.target_version, &len);
len = sizeof(current_session_s.source_version);
nvs_get_str(handle, OTA_MGR_KEY_SOURCE_VER, current_session_s.source_version, &len);
nvs_get_u32(handle, OTA_MGR_KEY_TIMESTAMP, &current_session_s.timestamp);
nvs_get_u8(handle, OTA_MGR_KEY_BOOT_COUNT, &current_session_s.boot_count);
nvs_get_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, &current_session_s.max_boot_attempts);
nvs_close(handle);
session_loaded_s = true;
ESP_LOGI(tag_s, "Loaded session: state=%d, type=%d, target=%s, source=%s, boot_cnt=%d",
current_session_s.state, current_session_s.type,
current_session_s.target_version, current_session_s.source_version,
current_session_s.boot_count);
return ESP_OK;
}
/**
* @brief 保存会话信息到NVS
*/
static esp_err_t save_session_to_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(OTA_MGR_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to open NVS: %s", esp_err_to_name(err));
return err;
}
nvs_set_u32(handle, "magic", OTA_SESSION_MAGIC);
nvs_set_u8(handle, OTA_MGR_KEY_STATE, (uint8_t)current_session_s.state);
nvs_set_u8(handle, OTA_MGR_KEY_TYPE, (uint8_t)current_session_s.type);
nvs_set_str(handle, OTA_MGR_KEY_TARGET_VER, current_session_s.target_version);
nvs_set_str(handle, OTA_MGR_KEY_SOURCE_VER, current_session_s.source_version);
nvs_set_u32(handle, OTA_MGR_KEY_TIMESTAMP, current_session_s.timestamp);
nvs_set_u8(handle, OTA_MGR_KEY_BOOT_COUNT, current_session_s.boot_count);
nvs_set_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, current_session_s.max_boot_attempts);
nvs_commit(handle);
nvs_close(handle);
return ESP_OK;
}
/**
* @brief 清除会话信息
*/
static esp_err_t clear_session_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(OTA_MGR_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err;
}
nvs_erase_all(handle);
nvs_commit(handle);
nvs_close(handle);
memset(&current_session_s, 0, sizeof(current_session_s));
session_loaded_s = false;
ESP_LOGI(tag_s, "Session cleared");
return ESP_OK;
}
/**
* @brief 检查当前运行的分区是否是通过OTA更新的分区
*/
static bool is_running_ota_partition(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
if (running == NULL) {
return false;
}
return (strcmp(running->label, "ota_0") == 0 ||
strcmp(running->label, "ota_1") == 0);
}
esp_err_t ota_manager_init(void)
{
ESP_LOGI(tag_s, "OTA Manager initializing...");
/* 获取当前固件信息 */
const esp_partition_t *running = esp_ota_get_running_partition();
const esp_app_desc_t *app_desc = esp_app_get_description();
if (running) {
ESP_LOGI(tag_s, "Running partition: %s", running->label);
}
if (app_desc) {
ESP_LOGI(tag_s, "Running version: %s", app_desc->version);
}
/* 从NVS加载会话信息 */
esp_err_t err = load_session_from_nvs();
if (err != ESP_OK) {
/* 没有OTA会话,正常启动 */
ESP_LOGI(tag_s, "No pending OTA session, normal boot");
/* 如果运行的是OTA分区,说明是首次烧录或已稳定运行,保存版本信息 */
if (is_running_ota_partition() && app_desc) {
ota_manager_update_stored_version(app_desc->version);
}
return ESP_OK;
}
/* 有OTA会话,检查状态 */
if (current_session_s.state == OTA_STATE_PENDING_VERIFY) {
/* OTA已完成,这是新固件第一次启动 */
ESP_LOGI(tag_s, "New firmware boot detected, verifying...");
/* 增加启动计数 */
current_session_s.boot_count++;
save_session_to_nvs();
/* 检查版本是否匹配 */
if (app_desc && strcmp(app_desc->version, current_session_s.target_version) == 0) {
ESP_LOGI(tag_s, "Version matches: %s", app_desc->version);
/* 标记为成功 */
current_session_s.state = OTA_STATE_SUCCESS;
save_session_to_nvs();
ota_manager_update_stored_version(app_desc->version);
/* 延迟一段时间后清除会话(给应用层确认的时间) */
/* 应用层需要调用 ota_manager_mark_success() 来最终确认 */
} else {
ESP_LOGW(tag_s, "Version mismatch! Expected: %s, Got: %s",
current_session_s.target_version,
app_desc ? app_desc->version : "unknown");
/* 版本不匹配,可能是回退或其他问题,清除会话 */
clear_session_nvs();
}
} else if (current_session_s.state == OTA_STATE_IN_PROGRESS) {
/* OTA进行中但设备重启了,可能是断电或崩溃 */
ESP_LOGW(tag_s, "OTA was in progress but interrupted");
/* 标记为失败 */
current_session_s.state = OTA_STATE_FAILED;
save_session_to_nvs();
/* 尝试回退 */
if (current_session_s.boot_count >= 1) {
ESP_LOGW(tag_s, "OTA interrupted, triggering rollback");
ota_manager_rollback();
return ESP_FAIL; /* 设备将重启 */
}
} else if (current_session_s.state == OTA_STATE_SUCCESS) {
/* 新固件已启动但未收到应用层确认,重复重启则回退 */
current_session_s.boot_count++;
save_session_to_nvs();
ESP_LOGI(tag_s, "OTA success pending confirm, boot %u/%u",
current_session_s.boot_count,
current_session_s.max_boot_attempts);
if (current_session_s.boot_count >= current_session_s.max_boot_attempts) {
ESP_LOGW(tag_s, "OTA not confirmed, rolling back");
ota_manager_rollback();
return ESP_FAIL;
}
} else if (current_session_s.state == OTA_STATE_FAILED) {
/* 上次OTA已标记为失败 */
ESP_LOGW(tag_s, "Previous OTA marked as failed");
/* 检查是否应该回退 */
if (is_running_ota_partition()) {
ESP_LOGW(tag_s, "Triggering rollback to previous version");
ota_manager_rollback();
return ESP_FAIL; /* 设备将重启 */
}
}
return ESP_OK;
}
esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version)
{
if (target_version == NULL || strlen(target_version) == 0) {
return ESP_ERR_INVALID_ARG;
}
const esp_app_desc_t *app_desc = esp_app_get_description();
memset(&current_session_s, 0, sizeof(current_session_s));
current_session_s.state = OTA_STATE_IN_PROGRESS;
current_session_s.type = type;
strncpy(current_session_s.target_version, target_version, sizeof(current_session_s.target_version) - 1);
if (app_desc) {
strncpy(current_session_s.source_version, app_desc->version, sizeof(current_session_s.source_version) - 1);
}
current_session_s.timestamp = (uint32_t)(esp_timer_get_time() / 1000000ULL);
current_session_s.boot_count = 0;
current_session_s.max_boot_attempts = OTA_MAX_BOOT_ATTEMPTS;
session_loaded_s = true;
ESP_LOGI(tag_s, "OTA session started: type=%d, %s -> %s",
type, current_session_s.source_version, target_version);
return save_session_to_nvs();
}
esp_err_t ota_manager_end_session(void)
{
if (!session_loaded_s || current_session_s.state != OTA_STATE_IN_PROGRESS) {
return ESP_ERR_INVALID_STATE;
}
current_session_s.state = OTA_STATE_PENDING_VERIFY;
ESP_LOGI(tag_s, "OTA session ended, waiting for verification");
return save_session_to_nvs();
}
esp_err_t ota_manager_mark_success(void)
{
if (!session_loaded_s) {
return ESP_OK; /* 没有会话,不需要标记 */
}
/* 清除会话,OTA完全成功 */
const esp_app_desc_t *app_desc = esp_app_get_description();
if (app_desc) {
ota_manager_update_stored_version(app_desc->version);
}
clear_session_nvs();
ESP_LOGI(tag_s, "OTA marked as successful");
return ESP_OK;
}
esp_err_t ota_manager_mark_failed_and_rollback(const char *reason)
{
if (!session_loaded_s) {
/* 没有会话,直接回退 */
return ota_manager_rollback();
}
current_session_s.state = OTA_STATE_FAILED;
save_session_to_nvs();
ESP_LOGE(tag_s, "OTA marked as failed: %s", reason ? reason : "unknown");
return ota_manager_rollback();
}
bool ota_manager_should_rollback(void)
{
if (!session_loaded_s) {
return false;
}
if (current_session_s.state != OTA_STATE_PENDING_VERIFY) {
return false;
}
/* 增加启动计数 */
current_session_s.boot_count++;
save_session_to_nvs();
ESP_LOGW(tag_s, "Boot count: %d/%d",
current_session_s.boot_count,
current_session_s.max_boot_attempts);
/* 如果启动次数超过阈值,应该回退 */
if (current_session_s.boot_count >= current_session_s.max_boot_attempts) {
ESP_LOGE(tag_s, "Max boot attempts reached, rollback required");
return true;
}
return false;
}
esp_err_t ota_manager_rollback(void)
{
ESP_LOGW(tag_s, "Performing rollback...");
/* 获取另一个OTA分区(上一版本) */
const esp_partition_t *prev_partition = esp_ota_get_next_update_partition(NULL);
if (prev_partition == NULL) {
ESP_LOGE(tag_s, "No previous partition found, cannot rollback");
return ESP_ERR_NOT_FOUND;
}
ESP_LOGI(tag_s, "Rollback to partition: %s", prev_partition->label);
/* 设置启动分区为上一版本 */
esp_err_t err = esp_ota_set_boot_partition(prev_partition);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to set boot partition: %s", esp_err_to_name(err));
return err;
}
/* 更新会话状态 */
if (session_loaded_s) {
current_session_s.state = OTA_STATE_ROLLBACK;
save_session_to_nvs();
}
ESP_LOGI(tag_s, "Rollback set, rebooting...");
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK; /* 实际上不会执行到这里 */
}
ota_state_t ota_manager_get_state(void)
{
if (!session_loaded_s) {
return OTA_STATE_IDLE;
}
return current_session_s.state;
}
esp_err_t ota_manager_get_session_info(ota_session_info_t *info)
{
if (!session_loaded_s || info == NULL) {
return ESP_ERR_INVALID_STATE;
}
memcpy(info, &current_session_s, sizeof(ota_session_info_t));
return ESP_OK;
}
const char* ota_manager_get_current_partition_label(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
if (running == NULL) {
return NULL;
}
return running->label;
}
esp_err_t ota_manager_get_stored_version(char *buf, size_t len)
{
if (buf == NULL || len == 0) {
return ESP_ERR_INVALID_ARG;
}
nvs_handle_t handle;
esp_err_t err = nvs_open(OTA_MGR_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) {
return err;
}
size_t required_len = len;
err = nvs_get_str(handle, OTA_MGR_KEY_LAST_SUCCESS_VER, buf, &required_len);
nvs_close(handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
buf[0] = '\0';
return ESP_OK;
}
return err;
}
esp_err_t ota_manager_update_stored_version(const char *version)
{
if (version == NULL) {
return ESP_ERR_INVALID_ARG;
}
nvs_handle_t handle;
esp_err_t err = nvs_open(OTA_MGR_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err;
}
err = nvs_set_str(handle, OTA_MGR_KEY_LAST_SUCCESS_VER, version);
if (err == ESP_OK) {
nvs_commit(handle);
}
nvs_close(handle);
ESP_LOGI(tag_s, "Stored version updated: %s", version);
return err;
}
/*
* OTA 状态管理器
* 管理OTA更新状态,支持失败回退到上一版本
* 适用于 WiFi/HTTP OTA、BLE OTA、UART OTA
*/
#ifndef OTA_MANAGER_H
#define OTA_MANAGER_H
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* OTA 状态 */
typedef enum {
OTA_STATE_IDLE = 0, /* 空闲,无OTA进行中 */
OTA_STATE_IN_PROGRESS, /* OTA进行中 */
OTA_STATE_PENDING_VERIFY, /* OTA完成,等待验证 */
OTA_STATE_SUCCESS, /* OTA验证成功 */
OTA_STATE_FAILED, /* OTA失败,已回退或需要回退 */
OTA_STATE_ROLLBACK, /* 已回退到上一版本 */
} ota_state_t;
/* OTA 类型 */
typedef enum {
OTA_TYPE_HTTP = 0, /* WiFi/HTTP OTA */
OTA_TYPE_BLE, /* BLE OTA */
OTA_TYPE_UART, /* UART OTA */
} ota_type_t;
/* OTA 会话信息(存储于NVS) */
typedef struct {
ota_state_t state; /* 当前状态 */
ota_type_t type; /* OTA类型 */
char target_version[32]; /* 目标版本号 */
char source_version[32]; /* 升级前版本号 */
uint32_t timestamp; /* OTA开始时间戳 */
uint8_t boot_count; /* 新固件启动计数 */
uint8_t max_boot_attempts; /* 最大尝试次数 */
} ota_session_info_t;
/**
* @brief 初始化OTA管理器
* 在系统启动时调用,检查OTA状态并决定是否回退
* @return ESP_OK 正常启动,ESP_FAIL 已触发回退
*/
esp_err_t ota_manager_init(void);
/**
* @brief 开始OTA会话
* @param type OTA类型
* @param target_version 目标版本号
* @return ESP_OK 成功,其他错误码
*/
esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version);
/**
* @brief 结束OTA会话(标记为等待验证)
* 新固件写入完成后调用,标记状态为PENDING_VERIFY
* @return ESP_OK 成功
*/
esp_err_t ota_manager_end_session(void);
/**
* @brief 验证OTA成功
* 应用层确认固件运行正常后调用
* @return ESP_OK 成功
*/
esp_err_t ota_manager_mark_success(void);
/**
* @brief 标记OTA失败并触发回退
* 在确认新固件有问题时调用,立即回退到上一版本
* @param reason 失败原因
* @return ESP_OK 已触发回退,设备将重启
*/
esp_err_t ota_manager_mark_failed_and_rollback(const char *reason);
/**
* @brief 获取当前OTA状态
* @return 当前OTA状态
*/
ota_state_t ota_manager_get_state(void);
/**
* @brief 获取OTA会话信息
* @param info 输出结构体
* @return ESP_OK 成功,ESP_ERR_NOT_FOUND 无会话信息
*/
esp_err_t ota_manager_get_session_info(ota_session_info_t *info);
/**
* @brief 检查是否需要回退
* 在应用层检测到异常时调用,增加启动计数并决定是否回退
* @return true 需要回退,false 正常
*/
bool ota_manager_should_rollback(void);
/**
* @brief 执行回退到上一版本
* 切换到上一个OTA分区并重启
* @return ESP_OK 已设置回退,设备将重启
*/
esp_err_t ota_manager_rollback(void);
/**
* @brief 获取当前运行的OTA分区标签
* @return "ota_0" 或 "ota_1",失败返回NULL
*/
const char* ota_manager_get_current_partition_label(void);
/**
* @brief 获取当前运行的固件版本号(从NVS读取最后一次成功版本)
* @param buf 输出缓冲区
* @param len 缓冲区长度
* @return ESP_OK 成功
*/
esp_err_t ota_manager_get_stored_version(char *buf, size_t len);
/**
* @brief 更新存储的版本号(OTA成功后调用)
* @param version 版本号字符串
* @return ESP_OK 成功
*/
esp_err_t ota_manager_update_stored_version(const char *version);
#ifdef __cplusplus
}
#endif
#endif /* OTA_MANAGER_H */
#include "heart_payload.h"
#include "betteryread.h"
#include "cJSON.h"
#include "sdkconfig.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *heart_payload_build(int message_type, cJSON *body)
{
if (!body) {
return NULL;
}
cJSON *root = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
if (!root || !head) {
cJSON_Delete(root);
cJSON_Delete(head);
cJSON_Delete(body);
return NULL;
}
cJSON_AddNumberToObject(head, "message_type", message_type);
cJSON_AddItemToObject(root, "head", head);
cJSON_AddItemToObject(root, "body", body);
char *out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool include_ip)
{
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
if (include_ip) {
const char *ip = (sta_ip != NULL && sta_ip[0] != '\0') ? sta_ip : "0.0.0.0";
cJSON_AddStringToObject(body, "ip", ip);
}
char id_full[64];
const char *dev = (device_id != NULL) ? device_id : "";
snprintf(id_full, sizeof(id_full), "app2dev/%s", dev);
char voltage_s[16];
snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v());
cJSON_AddStringToObject(body, "voltage", voltage_s);
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
return heart_payload_build(HEART_MSG_TYPE_HEARTBEAT, body);
}
char *heart_payload_alert_json_malloc(int message_type, const char *msg)
{
if (msg == NULL) {
return NULL;
}
if (message_type != HEART_MSG_TYPE_LOG_WARN && message_type != HEART_MSG_TYPE_LOG_ERROR) {
return NULL;
}
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
cJSON_AddStringToObject(body, "msg", msg);
return heart_payload_build(message_type, body);
}
#ifndef HEART_PAYLOAD_H
#define HEART_PAYLOAD_H
#include <stdbool.h>
/** 设备 → 手机,经 BLE 0xFFE3 / MQTT 下发的 message_type */
#define HEART_MSG_TYPE_HEARTBEAT 1
/** 告警日志(W),body.msg 为文本 */
#define HEART_MSG_TYPE_LOG_WARN 4
/** 错误日志(E),body.msg 为文本 */
#define HEART_MSG_TYPE_LOG_ERROR 5
/**
* 构造心跳 JSON(head.message_type = HEART_MSG_TYPE_HEARTBEAT)。
*
* body:device_ID、version、voltage;include_ip 为 true 时带 ip。
*/
char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool include_ip);
/**
* 构造告警/错误 JSON,经 0xFFE3 Notify 即时推送(与周期心跳同特征值)。
*
* @param message_type HEART_MSG_TYPE_LOG_WARN 或 HEART_MSG_TYPE_LOG_ERROR
* @param msg 日志正文(可为 ESP_LOG 行内冒号后的内容)
*/
char *heart_payload_alert_json_malloc(int message_type, const char *msg);
#endif
#include "sdkconfig.h"
#include "remote_control.h"
#include "rc_pwm_control.h"
#include "task_manager.h"
#if CONFIG_APP_LINK_WIFI
#include "ota.h"
#endif
#include "cJSON.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *tag_s = "REMOTE_CTRL";
/* 超时保护:500ms 未收到命令则自动停止 */
#define RC_CMD_TIMEOUT_MS 500
#define RC_WATCHDOG_PERIOD_MS 100 /* 检查周期 100ms */
static volatile uint32_t s_last_cmd_time_ms = 0;
static volatile bool s_watchdog_running = false;
static TaskHandle_t s_watchdog_handle = NULL;
static uint32_t get_current_time_ms(void)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
static void rc_watchdog_task(void *param)
{
(void)param;
s_watchdog_running = true;
while (s_watchdog_running) {
vTaskDelay(pdMS_TO_TICKS(RC_WATCHDOG_PERIOD_MS));
uint32_t now = get_current_time_ms();
uint32_t last = s_last_cmd_time_ms;
/* 如果已经初始化过且超过 500ms 未收到命令 */
if (last != 0 && (now - last) > RC_CMD_TIMEOUT_MS) {
rc_vehicle_stop();
ESP_LOGW(tag_s, "RC timeout: no cmd for %d ms, vehicle stopped", (int)(now - last));
/* 重置时间戳,避免重复停止 */
s_last_cmd_time_ms = now;
}
}
s_watchdog_handle = NULL;
vTaskDelete(NULL);
}
esp_err_t remote_control_watchdog_start(void)
{
if (s_watchdog_handle != NULL) {
return ESP_OK; /* 已经在运行 */
}
s_last_cmd_time_ms = get_current_time_ms(); /* 初始化时间戳 */
return app_task_start(APP_TASK_RC_WATCHDOG, rc_watchdog_task, NULL, &s_watchdog_handle);
}
void remote_control_watchdog_stop(void)
{
s_watchdog_running = false;
/* task 会自行删除,句柄会在任务函数中设为 NULL */
}
static void update_last_cmd_time(void)
{
s_last_cmd_time_ms = get_current_time_ms();
}
void remote_control_apply_json(const char *json)
{
/* 更新上次接收命令的时间戳 */
update_last_cmd_time();
if (!json) {
return;
}
cJSON *root = cJSON_Parse(json);
if (!root) {
return;
}
cJSON *head = cJSON_GetObjectItem(root, "head");
if (!head || !cJSON_IsObject(head)) {
#if CONFIG_APP_LINK_WIFI
/* 仅 WiFi+MQTT 构建:云端 JSON 触发 HTTPS OTA */
ota_update_recmqtt(json);
#endif
cJSON_Delete(root);
return;
}
cJSON *m_type = cJSON_GetObjectItem(head, "message_type");
if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == 1) {
ESP_LOGW(tag_s, "message_type=1: reboot (all devices)");
cJSON_Delete(root);
esp_restart();
return;
}
cJSON *body = cJSON_GetObjectItem(root, "body");
if (m_type && cJSON_IsNumber(m_type) && body) {
if (m_type->valueint == 4) {
cJSON *pin_ctrl = cJSON_GetObjectItem(body, "pin_setctrl");
if (pin_ctrl && cJSON_IsObject(pin_ctrl)) {
cJSON *jp = cJSON_GetObjectItem(pin_ctrl, "pin");
cJSON *jv = cJSON_GetObjectItem(pin_ctrl, "val");
if (jp && jv && cJSON_IsNumber(jp) && cJSON_IsNumber(jv)) {
int pin = jp->valueint;
int val = jv->valueint;
ESP_LOGI(tag_s, "GPIO: pin %d -> %d", pin, val);
rc_vehicle_shot(pin, val);
}
}
} else if (m_type->valueint == 3) {
cJSON *pwm_ctrl = cJSON_GetObjectItem(body, "pwm_ctrl");
if (pwm_ctrl && cJSON_IsObject(pwm_ctrl)) {
cJSON *jm = cJSON_GetObjectItem(pwm_ctrl, "mode");
cJSON *jt = cJSON_GetObjectItem(pwm_ctrl, "type");
cJSON *jv = cJSON_GetObjectItem(pwm_ctrl, "val");
if (jm && jv && cJSON_IsNumber(jm) && cJSON_IsNumber(jv)) {
int mode = jm->valueint;
int type = jt && cJSON_IsNumber(jt) ? jt->valueint : 0;
int val = jv->valueint;
ESP_LOGI(tag_s, "PWM: mode %d type %d val %d", mode, type, val);
rc_vehicle_control(mode, val);
}
}
}
}
cJSON_Delete(root);
}
#ifndef REMOTE_CONTROL_H
#define REMOTE_CONTROL_H
#include "esp_err.h"
/** BLE FFE1 / 遥控 JSON:单帧 UTF-8 最大字节数(不含结尾 NUL) */
#define REMOTE_CTRL_JSON_MAX_BYTES 2048
/** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */
void remote_control_apply_json(const char *json);
/**
* @brief 启动遥控超时守护任务
* 500ms 未收到控制命令将自动停止车辆
*/
esp_err_t remote_control_watchdog_start(void);
/**
* @brief 停止遥控超时守护任务
*/
void remote_control_watchdog_stop(void);
#endif
#include "wifidevnum_config.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_http_server.h"
#include "driver/gpio.h"
#include "lwip/sockets.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_spiffs.h"
#include "sdkconfig.h"
#if CONFIG_APP_LINK_WIFI
#include "mqttconf_commun.h"
#endif
#include "task_manager.h"
static const char *tag_s = "WIFI_CFG";
#if CONFIG_APP_LINK_WIFI
static bool wifi_editable_s = false;
#endif
static bool devid_editable_s = false;
esp_err_t init_spiffs(void)
{
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = "storage",
.max_files = 5,
.format_if_mount_failed = true
};
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
ESP_LOGE(tag_s, "SPIFFS 挂载失败: %s", esp_err_to_name(ret));
return ret;
}
return ESP_OK;
}
void url_decode(char *dst, const char *src)
{
char a, b;
while (*src) {
if ((*src == '%') && ((a = src[1]) && (b = src[2])) && (isxdigit(a) && isxdigit(b))) {
a = toupper((unsigned char)a);
b = toupper((unsigned char)b);
a -= (a >= 'A') ? ('A' - 10) : '0';
b -= (b >= 'A') ? ('A' - 10) : '0';
*dst++ = (char)((a << 4) | b);
src += 3;
} else if (*src == '+') {
*dst++ = ' ';
src++;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
}
void save_to_nvs(const char *key, const char *value)
{
nvs_handle_t handle;
if (nvs_open(DEVICE_CFG_NVS_NAMESPACE, NVS_READWRITE, &handle) == ESP_OK) {
nvs_set_str(handle, key, value);
nvs_commit(handle);
nvs_close(handle);
ESP_LOGI(tag_s, "NVS 写入成功 [%s]", key);
}
}
esp_err_t read_from_nvs(const char *key, char *buf, size_t len)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(DEVICE_CFG_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err == ESP_OK) {
err = nvs_get_str(handle, key, buf, &len);
nvs_close(handle);
}
return err;
}
static esp_err_t get_handler(httpd_req_t *req)
{
#if CONFIG_APP_LINK_BLE
char devid_status[128], ble_status[128];
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
sprintf(ble_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index_ble.html", "r");
#elif CONFIG_APP_LINK_UART
char devid_status[128];
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index_uart.html", "r");
#else
char wifi_status[128], devid_status[128];
sprintf(wifi_status, wifi_editable_s ? "" : "disabled style='background:#eee'");
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index.html", "r");
#endif
if (!f) {
httpd_resp_send_404(req);
return ESP_FAIL;
}
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
char *template = malloc(fsize + 1);
fread(template, 1, fsize, f);
fclose(f);
template[fsize] = 0;
char *final_html = malloc(fsize + 1024);
#if CONFIG_APP_LINK_BLE
snprintf(final_html, fsize + 1024, template,
devid_editable_s ? "🔓" : "🔒", devid_status,
devid_editable_s ? "🔓" : "🔒", ble_status);
#elif CONFIG_APP_LINK_UART
snprintf(final_html, fsize + 1024, template, devid_editable_s ? "🔓" : "🔒", devid_status);
#else
snprintf(final_html, fsize + 1024, template,
wifi_editable_s ? "🔓" : "🔒", wifi_status, wifi_status,
devid_editable_s ? "🔓" : "🔒", devid_status);
#endif
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, final_html, HTTPD_RESP_USE_STRLEN);
free(template);
free(final_html);
return ESP_OK;
}
static esp_err_t post_handler(httpd_req_t *req)
{
char buf[512];
int cl = req->content_len;
if (cl < 0) {
cl = 0;
}
size_t max_recv = sizeof(buf) - 1;
if ((size_t)cl < max_recv) {
max_recv = (size_t)cl;
}
int ret = httpd_req_recv(req, buf, max_recv);
if (ret > 0) {
buf[ret] = '\0';
char *token, *saveptr;
token = strtok_r(buf, "&", &saveptr);
while (token != NULL) {
char *key = token;
char *val = strchr(token, '=');
if (val) {
*val = '\0';
val++;
char decoded_val[64];
url_decode(decoded_val, val);
#if CONFIG_APP_LINK_BLE
if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
} else if (strcmp(key, "ble_name") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, decoded_val);
}
#elif CONFIG_APP_LINK_UART
if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
}
#else
if (strcmp(key, "ssid") == 0 && wifi_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_WIFI_SSID, decoded_val);
} else if (strcmp(key, "pass") == 0 && wifi_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_WIFI_PASS, decoded_val);
} else if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
}
#endif
}
token = strtok_r(NULL, "&", &saveptr);
}
}
httpd_resp_send(req, "<h1>Success</h1><p>Restarting...</p>", HTTPD_RESP_USE_STRLEN);
vTaskDelay(pdMS_TO_TICKS(1500));
esp_restart();
return ESP_OK;
}
void dns_server_task(void *pvParameters)
{
enum { DNS_RX_BUF_SIZE = 256 };
static const uint8_t dns_answer[] = {
0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 192, 168, 4, 1};
const size_t dns_answer_len = sizeof(dns_answer);
uint8_t rx_buffer[DNS_RX_BUF_SIZE];
struct sockaddr_in dest_addr;
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(53);
bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
while (1) {
struct sockaddr_in source_addr;
socklen_t socklen = sizeof(source_addr);
/* 预留应答尾部长度,避免 memcpy(rx_buffer + len, answer, ...) 越界 */
int len = recvfrom(sock, rx_buffer, DNS_RX_BUF_SIZE - dns_answer_len, 0,
(struct sockaddr *)&source_addr, &socklen);
if (len > 12 && (size_t)len + dns_answer_len <= DNS_RX_BUF_SIZE) {
rx_buffer[2] |= 0x80;
rx_buffer[3] |= 0x80;
rx_buffer[7] = 1;
memcpy(rx_buffer + len, dns_answer, dns_answer_len);
sendto(sock, rx_buffer, len + dns_answer_len, 0, (struct sockaddr *)&source_addr,
sizeof(source_addr));
}
}
}
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
(void)err;
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "/");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
void start_config_web(void)
{
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_err_t e = esp_wifi_init(&cfg);
if (e != ESP_OK && e != ESP_ERR_INVALID_STATE) {
ESP_LOGE(tag_s, "wifi_init 失败: %s", esp_err_to_name(e));
return;
}
(void)esp_wifi_stop();
esp_netif_create_default_wifi_ap();
wifi_config_t ap_cfg = {
.ap = {.ssid = CONFIG_ROBIOT_WIFI_SSID, .max_connection = 4, .authmode = WIFI_AUTH_OPEN}
};
esp_wifi_set_mode(WIFI_MODE_AP);
esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
esp_wifi_start();
(void)app_task_start(APP_TASK_DNS_SERVER, dns_server_task, NULL, NULL);
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
if (httpd_start(&server, &config) == ESP_OK) {
httpd_uri_t get_uri = {.uri = "/", .method = HTTP_GET, .handler = get_handler};
httpd_register_uri_handler(server, &get_uri);
httpd_uri_t post_uri = {.uri = "/save", .method = HTTP_POST, .handler = post_handler};
httpd_register_uri_handler(server, &post_uri);
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
}
void nowifidata_start_config_web(void)
{
#if CONFIG_APP_LINK_WIFI
wifi_editable_s = true;
devid_editable_s = true;
#else
devid_editable_s = true;
#endif
start_config_web();
}
void button_monitor_task(void *arg)
{
(void)arg;
gpio_config_t io_conf = {
#if CONFIG_APP_LINK_UART
/* UART 模式:仅使用设备号重置按键 (GPIO4) */
.pin_bit_mask = (1ULL << BTN_DEVID_IO),
#else
/* WiFi/BLE 模式:使用两个按键 */
.pin_bit_mask = (1ULL << BTN_WIFI_IO) | (1ULL << BTN_DEVID_IO),
#endif
.mode = GPIO_MODE_INPUT,
.pull_up_en = 1
};
gpio_config(&io_conf);
#if CONFIG_APP_LINK_UART
int dev_cnt = 0;
#else
int wifi_cnt = 0, dev_cnt = 0;
#endif
bool web_started = false;
while (1) {
#if !CONFIG_APP_LINK_UART
/* WiFi/BLE 模式:处理 WiFi 配置按键 (GPIO0) */
if (gpio_get_level(BTN_WIFI_IO) == 0) {
#if CONFIG_APP_LINK_BLE
if (!devid_editable_s) {
}
if (++wifi_cnt == 20) {
devid_editable_s = true;
if (!web_started) {
start_config_web();
web_started = true;
}
}
#else
if (!wifi_editable_s) {
}
if (++wifi_cnt == 20) {
wifi_editable_s = true;
if (!web_started) {
mqtt_manager_stop();
start_config_web();
web_started = true;
}
}
#endif
} else {
wifi_cnt = 0;
}
#endif /* !CONFIG_APP_LINK_UART */
/* 所有模式:处理设备号重置按键 (GPIO4) */
if (gpio_get_level(BTN_DEVID_IO) == 0) {
if (!devid_editable_s) {
}
if (++dev_cnt == 20) {
devid_editable_s = true;
if (!web_started) {
#if CONFIG_APP_LINK_WIFI
mqtt_manager_stop();
#endif
#if CONFIG_APP_LINK_UART
/* UART 模式:停止 UART 通信,进入配网模式 */
extern void link_uart_stop(void);
link_uart_stop();
#endif
start_config_web();
web_started = true;
}
}
} else {
dev_cnt = 0;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void button_monitor_task_init(void)
{
(void)app_task_start(APP_TASK_BUTTON_MONITOR, button_monitor_task, NULL, NULL);
}
#ifndef __WIFI_DEV_NUM_CONFIG_H__
#define __WIFI_DEV_NUM_CONFIG_H__
#include "esp_err.h"
#include <stddef.h>
#include "device_nvs.h"
// 引脚定义 (ESP32-S3 默认 BOOT 键一般是 0)
#define BTN_WIFI_IO 0
#define BTN_DEVID_IO 4
void save_to_nvs(const char *key, const char *value);
esp_err_t read_from_nvs(const char *key, char *buf, size_t len);
void start_config_web(void);
esp_err_t init_spiffs(void);
void nowifidata_start_config_web(void);
void button_monitor_task_init(void);
#endif
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
ota_0, app, ota_0, , 0x600000,
ota_1, app, ota_1, , 0x600000,
storage, data, spiffs, , 0x50000,
\ No newline at end of file
@echo off
setlocal EnableExtensions EnableDelayedExpansion
goto :Main
:Require
if not exist "%~1" (
echo.
echo ERROR: missing file:
echo %~1
echo Run: idf.py build
echo.
pause
exit /b 1
)
exit /b 0
:ReadVersion
for /f "usebackq tokens=2 delims==" %%A in (`findstr /b "CONFIG_MY_APP_VERSION" "%~1" 2^>nul`) do (
set "VER=%%~A"
)
exit /b 0
:HashFile
set "HASH="
for /f "skip=1 usebackq delims=" %%H in (`certutil -hashfile "%~1" SHA256 2^>nul`) do (
set "HASH=%%H"
goto :HashDone
)
:HashDone
set "HASH=!HASH: =!"
set "%~2=!HASH!"
exit /b 0
:AppendJsonFile
call :HashFile "%~1" H
for %%A in ("%~1") do (
set "FN=%%~nxA"
set "SZ=%%~zA"
)
if "%~2"=="last" (
>>"%REL%\manifest.json" echo {"name":"!FN!","bytes":!SZ!,"sha256":"!H!"}
) else (
>>"%REL%\manifest.json" echo {"name":"!FN!","bytes":!SZ!,"sha256":"!H!"},
)
exit /b 0
:Main
REM Copy idf.py build -> firmware/release/ (double-click OK)
cd /d "%~dp0"
cd /d ".."
set "ROOT=%CD%"
set "BUILD=%ROOT%\build"
set "REL=%ROOT%\firmware\release"
set "OTA=%REL%\ota"
set "FAC=%REL%\factory"
call :Require "%BUILD%\ESPRCCar.bin"
call :Require "%BUILD%\bootloader\bootloader.bin"
call :Require "%BUILD%\partition_table\partition-table.bin"
call :Require "%BUILD%\ota_data_initial.bin"
call :Require "%BUILD%\storage.bin"
if not exist "%OTA%" mkdir "%OTA%"
if not exist "%FAC%" mkdir "%FAC%"
echo [1/4] Copy binaries ...
copy /Y "%BUILD%\ESPRCCar.bin" "%OTA%\ESPRCCar.bin" >nul
copy /Y "%BUILD%\ESPRCCar.bin" "%FAC%\ESPRCCar.bin" >nul
copy /Y "%BUILD%\bootloader\bootloader.bin" "%FAC%\bootloader.bin" >nul
copy /Y "%BUILD%\partition_table\partition-table.bin" "%FAC%\partition-table.bin" >nul
copy /Y "%BUILD%\ota_data_initial.bin" "%FAC%\ota_data_initial.bin" >nul
copy /Y "%BUILD%\storage.bin" "%FAC%\storage.bin" >nul
if exist "%BUILD%\flash_args" copy /Y "%BUILD%\flash_args" "%FAC%\flash_args.txt" >nul
echo [2/4] Read version ...
set "VER=unknown"
if exist "%ROOT%\sdkconfig" call :ReadVersion "%ROOT%\sdkconfig"
if "!VER!"=="unknown" if exist "%ROOT%\sdkconfig.defaults" call :ReadVersion "%ROOT%\sdkconfig.defaults"
set "UTC=unknown"
for /f "delims=" %%T in ('powershell -NoProfile -Command "(Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')" 2^>nul') do set "UTC=%%T"
set "LOCAL=%date% %time%"
echo [3/4] Write manifest.json ...
> "%REL%\manifest.json" echo {
>>"%REL%\manifest.json" echo "project": "ESPRCCar",
>>"%REL%\manifest.json" echo "chip": "esp32s3",
>>"%REL%\manifest.json" echo "flash_mode": "dio",
>>"%REL%\manifest.json" echo "flash_freq": "80m",
>>"%REL%\manifest.json" echo "flash_size": "16MB",
>>"%REL%\manifest.json" echo "version": "!VER!",
>>"%REL%\manifest.json" echo "generated_utc": "!UTC!",
>>"%REL%\manifest.json" echo "ota_image": "release/ota/ESPRCCar.bin",
>>"%REL%\manifest.json" echo "factory_dir": "release/factory",
>>"%REL%\manifest.json" echo "files": [
call :AppendJsonFile "%OTA%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\bootloader.bin"
call :AppendJsonFile "%FAC%\partition-table.bin"
call :AppendJsonFile "%FAC%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\ota_data_initial.bin"
call :AppendJsonFile "%FAC%\storage.bin" last
>>"%REL%\manifest.json" echo ]
>>"%REL%\manifest.json" echo }
echo [4/5] Copy Android device doc ...
set "DOC_SRC=%ROOT%\docs\Android端设备对接文档_v!VER!.md"
if exist "!DOC_SRC!" (
copy /Y "!DOC_SRC!" "%REL%\Android端设备对接文档_v!VER!.md" >nul
echo docs -^> release\Android端设备对接文档_v!VER!.md
) else (
echo WARN: missing !DOC_SRC! (create docs\Android端设备对接文档_v!VER!.md before handoff)
)
echo [5/5] Write VERSION.txt ...
call :HashFile "%OTA%\ESPRCCar.bin" OTAHASH
for %%A in ("%OTA%\ESPRCCar.bin") do set "OTASZ=%%~zA"
> "%REL%\VERSION.txt" (
echo ESPRCCar firmware release
echo =========================
echo version: !VER!
echo project: ESPRCCar
echo chip: esp32s3
echo built_utc: !UTC!
echo built_local: !LOCAL!
echo.
echo OTA:
echo path: firmware/release/ota/ESPRCCar.bin
echo bytes: !OTASZ!
echo sha256: !OTAHASH!
echo.
echo Factory: firmware/release/factory/
echo tool: Espressif Flash Download Tools
echo addrs: see factory/flash_args.txt
echo.
echo machine_readable: manifest.json
echo.
echo android_doc: firmware/release/Android端设备对接文档_v!VER!.md
)
echo.
echo Done: firmware/release/ version=!VER!
echo VERSION.txt + manifest.json updated
echo.
echo Note: run after idf.py build; build does not auto-copy here.
echo.
pause
exit /b 0
# Custom partition table (dual OTA + SPIFFS); needs >=16MB flash per partitions.csv
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
# Link mode (one of: WIFI / BLE / UART)
# CONFIG_APP_LINK_WIFI is not set
# CONFIG_APP_LINK_BLE is not set
CONFIG_APP_LINK_UART=y
# CONFIG_APP_BLE_OTA is not set
# UART link: GPIO17 TX / GPIO18 RX
CONFIG_APP_UART_LINK_BAUDRATE=115200
CONFIG_APP_UART_LINK_TX_GPIO=17
CONFIG_APP_UART_LINK_RX_GPIO=18
CONFIG_APP_PWM_IO15_SERVO=y
# CONFIG_APP_PWM_IO15_ESC is not set
# GPIO16: 1102 steering servo uses this pin. Must be SERVO. If dual ESC uses 15/16, set CONFIG_APP_PWM_IO16_ESC=y
CONFIG_APP_PWM_IO16_SERVO=y
# CONFIG_APP_PWM_IO16_ESC is not set
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLUEDROID_ENABLED is not set
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_CONTROLLER_ENABLED=y
# UART0 (GPIO43 TX / GPIO44 RX): debug + optional uart_comm in Release
CONFIG_APP_DEBUG_UART_NUM=0
CONFIG_APP_DEBUG_UART_TX_GPIO=-1
CONFIG_APP_DEBUG_UART_RX_GPIO=-1
# ESP-IDF console on default UART0
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
# Release defaults: -Os, no console UART, BLE log via 0xFFE3
# Build: idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
# CONFIG_COMPILER_OPTIMIZATION_DEBUG is not set
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# CONFIG_COMPILER_OPTIMIZATION_DEFAULT is not set
# CONFIG_COMPILER_OPTIMIZATION_PERF is not set
# CONFIG_COMPILER_OPTIMIZATION_NONE is not set
# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE is not set
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y
# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set
# CONFIG_ESP_COREDUMP_ENABLE is not set
# CONFIG_LOG_DEFAULT_LEVEL_INFO is not set
CONFIG_LOG_DEFAULT_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL=2
# Release: no UART console (W/E via BLE 0xFFE3 notify)
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
CONFIG_ESP_CONSOLE_NONE=y
# When APP_UART_MODE_DEBUG=y: uart_comm uses UART from APP_DEBUG_UART_*; no ESP_LOG
CONFIG_ROBO_APP_FW_RELEASE=y
# CONFIG_ROBO_APP_FW_DEBUG is not set
CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y
CONFIG_LOG_BOOTLOADER_LEVEL=2
# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_LOG_LEVEL=2
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #28a745; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
</style>
</head>
<body>
<div class="card">
<h1>🦉 飞驰设备配置中心</h1>
<form action='/save' method='POST'>
<b>WiFi 配置 %s</b>
<input name='ssid' placeholder='WiFi名称' %s>
<input name='pass' type='password' placeholder='WiFi密码' %s>
<br>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<button type='submit' class='btn'>保存并重启设备</button>
</form>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
</style>
</head>
<body>
<div class="card">
<h1>BLE 设备配置</h1>
<p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='设备号' %s>
<b>蓝牙广播名 %s</b>
<input name='ble_name' placeholder='手机扫描看到的名称' %s>
<button type='submit' class='btn'>保存并重启</button>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
</style>
</head>
<body>
<div class="card">
<h1>串口设备配置</h1>
<p style="color:#666;font-size:14px;">UART 模式不保存 WiFi,仅需填写设备号</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<button type='submit' class='btn'>保存并重启</button>
</form>
</div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment