Commit 76e8da9d authored by 957dd's avatar 957dd

bug修改完,串口通信

parent 5eb7707c
...@@ -78,6 +78,9 @@ htmlcov/ ...@@ -78,6 +78,9 @@ htmlcov/
compile_commands.json compile_commands.json
.cursor/ .cursor/
# --- 本地 Agent 技能(pua 等,勿入库)---
pua/
# --- 日志与临时文件 --- # --- 日志与临时文件 ---
*.log *.log
*~ *~
......
...@@ -2,6 +2,19 @@ ...@@ -2,6 +2,19 @@
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# esp_app_desc.version 与 menuconfig CONFIG_MY_APP_VERSION 保持一致
set(_proj_ver "1.0.1")
foreach(_cfg IN ITEMS sdkconfig.defaults sdkconfig)
set(_cfg_path "${CMAKE_CURRENT_SOURCE_DIR}/${_cfg}")
if(EXISTS "${_cfg_path}")
file(STRINGS "${_cfg_path}" _ver_line REGEX "^CONFIG_MY_APP_VERSION=")
if(_ver_line)
string(REGEX REPLACE "^CONFIG_MY_APP_VERSION=\"(.*)\"$" "\\1" _proj_ver "${_ver_line}")
endif()
endif()
endforeach()
set(PROJECT_VER "${_proj_ver}")
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESPRCCar) project(ESPRCCar)
......
...@@ -44,10 +44,9 @@ ...@@ -44,10 +44,9 @@
其中 `gpiotrol` 已拆分为“通用 PWM 底座 + 设备策略”: 其中 `gpiotrol` 已拆分为“通用 PWM 底座 + 设备策略”:
- `main/drivers/gpiotrol/rc_pwm_control.c`:统一 RC 车 PWM 控制(含 6 路 50Hz 初始化、AUX 角色选择、PID 接口、策略分发)。 - `main/drivers/gpiotrol/rc_pwm_control.c`:统一 RC 车 PWM 控制(含 6 路 50Hz 初始化、AUX 角色选择、PID 接口、策略分发)。
- `main/drivers/driver_manager.c`:统一初始化并绑定设备策略(业务层不直接碰具体驱动细节)。 - `main/drivers/driver_manager/`:统一初始化并绑定设备策略(业务层不直接碰具体驱动细节)。
- `main/drivers/gpiotrol/device_drive.h`:设备策略接口定义(`stop/control/shot`)。 - `main/drivers/gpiotrol/device_drive.h`:设备策略接口定义(`stop/control/shot`)。
- `main/drivers/gpiotrol/devices/device_1201.c`:1201 设备控制映射。 - `main/drivers/gpiotrol/devices/1101/``devices/1102/`:各车型控制映射。
- `main/drivers/gpiotrol/devices/device_1101.c`:1101 设备控制映射。
当前统一引脚约定(均为 50Hz): 当前统一引脚约定(均为 50Hz):
- 驱动芯片1:`IO10``IO21` - 驱动芯片1:`IO10``IO21`
......
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式) # Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
...@@ -62,7 +62,7 @@ Android 做 BLE / UART OTA **只需要两个文件**(运行 `scripts\copy_firm ...@@ -62,7 +62,7 @@ Android 做 BLE / UART OTA **只需要两个文件**(运行 `scripts\copy_firm
- **OTA 固件确认**:设备回 **完整 ACK/NAK 帧**(BLE 经 **0xFFE4** 按帧字节 Notify;UART 按帧写回);`status≠0` 时 App **原 SEQ 重传** - **OTA 固件确认**:设备回 **完整 ACK/NAK 帧**(BLE 经 **0xFFE4** 按帧字节 Notify;UART 按帧写回);`status≠0` 时 App **原 SEQ 重传**
- **控制 JSON**:手机下发后 **无 `OK` 文本回复** - **控制 JSON**:手机下发后 **无 `OK` 文本回复**
- **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。 - **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。
- **心跳仍在 `0xFFE3` / UART 串口**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**,字段见 §3.5-A - **心跳**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**(§3.5-A)。**BLE****0xFFE3**,与 **0xFFE4** OTA ACK **分特征值**,OTA 期间心跳照常。**UART** 与 OT ACK **共用 UART1**:手机发 `0` 武装后至 `1002/done` 或会话中止前,设备 **暂停 UART 心跳**,线上仅 OT ACK/NAK + `1002` JSON,避免与逐帧确认混线
- **命令下发间隔**(BLE/UART 共用):手机→设备 **任意相邻两条 JSON** 间隔 **≥100ms**;摇杆控制建议 **100~200ms** 一发,且须 **≤500ms** 以免固件超时停车(§5.0.2、§7.2.3)。 - **命令下发间隔**(BLE/UART 共用):手机→设备 **任意相邻两条 JSON** 间隔 **≥100ms**;摇杆控制建议 **100~200ms** 一发,且须 **≤500ms** 以免固件超时停车(§5.0.2、§7.2.3)。
- **停车专用 JSON**(§4.4.1、§4.4.2):松手/停止时发 **全位 0**`speed/direction/mode/val` 均为 0);**前进+左转等多键同时抬起** 也必须用同一条 JSON,**每 300ms** 重复写。 - **停车专用 JSON**(§4.4.1、§4.4.2):松手/停止时发 **全位 0**`speed/direction/mode/val` 均为 0);**前进+左转等多键同时抬起** 也必须用同一条 JSON,**每 300ms** 重复写。
- **UART 波特率固定 115200**:App **设置页**应有「串口波特率」项,但当前固件只支持 **115200**;每次 `open` 后必须 `setParameters(115200, 8, N, 1)`(§7.2.0)。 - **UART 波特率固定 115200**:App **设置页**应有「串口波特率」项,但当前固件只支持 **115200**;每次 `open` 后必须 `setParameters(115200, 8, N, 1)`(§7.2.0)。
...@@ -539,7 +539,19 @@ flowchart TD ...@@ -539,7 +539,19 @@ flowchart TD
**BEGIN/END 应答**:PAYLOAD **`[status]`** 仅 1 字节。 **BEGIN/END 应答**:PAYLOAD **`[status]`** 仅 1 字节。
**NAK**`OP=0x81`):CRC/长度错误等。 **NAK**`OP=0x81`):CRC/长度错误等。
**Android**:从 **0xFFE4** 组满一整帧再校验;`status!=0``data_byte` 不符 → **重传** **整帧长度(VER=0x02,便于日志对照)**
| OP | 方向 | 典型总长 |
|----|------|----------|
| BEGIN `0x01` | 手机→设备 | **14**(8 头 + 4B 镜像长度 + CRC + TAIL) |
| DATA `0x02` | 手机→设备 | **11**(8 头 + 1B 固件字节 + CRC + TAIL) |
| END `0x03` | 手机→设备 | **10**(8 头 + 空 payload + CRC + TAIL) |
| ACK `0x80` | 设备→手机 | BEGIN/END:**11**;DATA:**12**(含回显字节) |
| NAK `0x81` | 设备→手机 | **11** |
设备 **每收到一帧完整 OT 请求即在同链路回一帧 ACK/NAK**(UART 写回 UART1;BLE Notify **0xFFE4**)。App **必须等 ACK `status==0` 再发下一帧**,禁止无节流连发 DATA。
**Android**:从 **0xFFE4**(或 UART 读流)组满一整帧再校验;`status!=0``data_byte` 不符 → **原 SEQ 重传**
#### 3.4.3 旧版裸 opcode(兼容) #### 3.4.3 旧版裸 opcode(兼容)
...@@ -1788,7 +1800,8 @@ Android App 内置或下载 OTA 包时只需要两个文件: ...@@ -1788,7 +1800,8 @@ Android App 内置或下载 OTA 包时只需要两个文件:
|------|-----|-------------------| |------|-----|-------------------|
| 询问 / 结果 JSON | **0xFFE3** Notify | UART1 文本行 + `\n` | | 询问 / 结果 JSON | **0xFFE3** Notify | UART1 文本行 + `\n` |
| 同意 OTA | Write **0xFFE1** | JSON 行 + `\n` | | 同意 OTA | Write **0xFFE1** | JSON 行 + `\n` |
| 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧) | | 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧);**每 RX 一帧即 TX 一回 ACK** |
| OTA 期间心跳 | **0xFFE3** 照常(与 ACK 分通道) | **暂停**(避免与 ACK 混线) |
| 固件数据确认(旧版兼容) | 旧固件可能出现单字节回显 | 旧固件可能出现同线单字节回显 | | 固件数据确认(旧版兼容) | 旧固件可能出现单字节回显 | 旧固件可能出现同线单字节回显 |
| 控制 JSON 后 | 无 ACK | **无** `OK` | | 控制 JSON 后 | 无 ACK | **无** `OK` |
| USB 调试口(UART0) | Release 下 **无日志**(§1.5) | 同左 | | USB 调试口(UART0) | Release 下 **无日志**(§1.5) | 同左 |
...@@ -1798,7 +1811,7 @@ Android App 内置或下载 OTA 包时只需要两个文件: ...@@ -1798,7 +1811,7 @@ Android App 内置或下载 OTA 包时只需要两个文件:
1. **OT 帧(推荐)**:按 §3.4.2 发送 BEGIN/DATA/END,等待 ACK/NAK 帧;`status!=0` 按原 SEQ 重传。 1. **OT 帧(推荐)**:按 §3.4.2 发送 BEGIN/DATA/END,等待 ACK/NAK 帧;`status!=0` 按原 SEQ 重传。
2. **裸 `0x01/0x02/0x03`(兼容旧流程)**:仍可用,但新 App 不建议继续依赖该模式。 2. **裸 `0x01/0x02/0x03`(兼容旧流程)**:仍可用,但新 App 不建议继续依赖该模式。
**UART 读线程必须做双路解复用**:同一 UART1 RX 流里既可能有 JSON 行(`{...}\n`,如心跳/1002),也可能有 OT ACK/NAK 二进制帧(`0x4F 0x54 ... 0x16`)。OTA 阶段读线程应优先组 OT 帧;遇到 `{` 再按换行组 JSON。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。 **UART 读线程必须做双路解复用(App 读设备下行)**:手机 `read()` 到的字节流里,**非 OTA 阶段**可能有 JSON 行(`{...}\n`,心跳/`1002`)。**OTA 二进制阶段**(已发 `message_type=0`)设备 **暂停 UART 心跳**,线上以 **OT ACK/NAK 帧**`0x4F 0x54 ... 0x16`)为主,结束时再收 `1002/done` JSON。读线程应 **按字节组 OT 帧**;仅当缓冲以 `{` 开头且无完整 OT 魔数时再按 `\n` 组 JSON。**禁止**把整个 `read()` 缓冲当 UTF-8 文本解析(会破坏 ACK 二进制)。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。
**JSON 分片/粘包必读(对应你看到的日志现象)** **JSON 分片/粘包必读(对应你看到的日志现象)**
...@@ -1814,12 +1827,14 @@ Android App 内置或下载 OTA 包时只需要两个文件: ...@@ -1814,12 +1827,14 @@ Android App 内置或下载 OTA 包时只需要两个文件:
3. 解析完从缓冲中移除该行,继续处理剩余内容(可能是下一条 JSON 头部)。 3. 解析完从缓冲中移除该行,继续处理剩余内容(可能是下一条 JSON 头部)。
4. 没有 `\n` 的残留内容继续保留,等待下一次回调拼接。 4. 没有 `\n` 的残留内容继续保留,等待下一次回调拼接。
**推荐参考实现(串口文本流)** **推荐参考实现(握手/遥控阶段:仅 JSON 行)**
> 下列代码 **仅适用于未进入 OTA 二进制阶段**(尚未发 `message_type=0`)。OTA 推包时必须改用下方 **字节流 + OT 帧解析**,不能把 `read()` 当 `String`。
```kotlin ```kotlin
private val rxTextBuffer = StringBuilder() private val rxTextBuffer = StringBuilder()
fun onSerialBytes(bytes: ByteArray) { fun onSerialBytesForJsonOnly(bytes: ByteArray) {
rxTextBuffer.append(bytes.toString(Charsets.UTF_8)) rxTextBuffer.append(bytes.toString(Charsets.UTF_8))
while (true) { while (true) {
...@@ -1830,7 +1845,7 @@ fun onSerialBytes(bytes: ByteArray) { ...@@ -1830,7 +1845,7 @@ fun onSerialBytes(bytes: ByteArray) {
rxTextBuffer.delete(0, idx + 1) rxTextBuffer.delete(0, idx + 1)
if (line.isEmpty()) continue if (line.isEmpty()) continue
if (!line.startsWith("{")) continue // 过滤日志行或非 JSON 文本 if (!line.startsWith("{")) continue
try { try {
handleJson(JSONObject(line)) handleJson(JSONObject(line))
...@@ -1841,6 +1856,23 @@ fun onSerialBytes(bytes: ByteArray) { ...@@ -1841,6 +1856,23 @@ fun onSerialBytes(bytes: ByteArray) {
} }
``` ```
**OTA 阶段读 ACK(与 JSON 解耦,伪代码)**
```kotlin
private val rxOtaBuf = ByteArray(512)
private var rxOtaLen = 0
fun onSerialBytesDuringOta(chunk: ByteArray) {
// 追加到字节缓冲,循环尝试 parseOtFrame();完整 ACK 后再发下一 DATA
append(rxOtaBuf, chunk)
while (true) {
val frame = tryParseOtFrame(rxOtaBuf) ?: break
consume(rxOtaBuf, frame.size)
otaAckQueue.offer(frame)
}
}
```
**遥控连续发送与超时停车(必读)** **遥控连续发送与超时停车(必读)**
- 固件遥控看门狗按“最近一次收到命令字节”计时,间隔过长会触发自动停车。 - 固件遥控看门狗按“最近一次收到命令字节”计时,间隔过长会触发自动停车。
......
# Android 端设备对接文档 · 固件 v1.0.2
# Android 端设备对接文档 · 固件 v1.0.2
> **读者**:Android 开发。
> **控制 JSON 在 UART、BLE、MQTT 上格式完全相同**;差异仅在传输层,组包规则一致。
---
## 0. 三十秒看懂
| 项目 | 说明 |
|------|------|
| 适用链路 | **UART / BLE / MQTT** 下发控制时,JSON 结构相同 |
| 控制入口 | `head.message_type = 3``message_type=4` 已废弃) |
| **行走 / 转向** | 只有 `body.pwm_ctrl`(前后左右 + 力度,**无 GPIO 号**) |
| **GPIO / 发射** | 只有 `body.pin_setctrl``pin` + `val`) |
| **铁律** | **凡行走相关 JSON(控制 / 松手 / 全停),UART·BLE·MQTT 一律不得带 `pin_setctrl`** |
| 无 ACK | 控制 JSON 发出后不要等待 `OK` |
| 发送节奏 | 控制中 100~200ms 一条;间隔不得超过 500ms |
| UART 行尾 | 串口每条 JSON 以 `\n` 结尾 |
| 行走全停 | 仅 `pwm_ctrl` 四字段全 `0`;连发 **3 条**(0 / +100ms / +100ms)后停 |
| GPIO | 单独 `pin_setctrl`;与行走 JSON **分包**(主动同包见 §3.4) |
---
## 1. 本版相对 v1.0.1 的变更
1. **融合控制**`message_type=3` 统一入口;行走用 `pwm_ctrl`,GPIO 用 `pin_setctrl`
2. **行走新格式**`speed_mode/speed_val` + `steer_mode/steer_val`,一条 JSON 可「前进 + 左转」。
3. **行走与 GPIO 解耦**:停车 / 松手 / 方向控制 **不带 pin**;GPIO 另发。
4. **`message_type=4` 废弃**
5. **OTA 专题**(§6):OT **分块 DATA**(按波特率自适应,UART 单帧最大 **1024B**);`1002` 下发 `ota_chunk` / `ota_chunk_max`;460800 下 **6MB 典型 ~4min**
6. **日志上送**(§4.1):Release 下 `ESP_LOGW`/`ESP_LOGE``message_type=4/5` JSON(UART/BLE)。
7. **设备号**(§2.2):配网填完整号 `CN110200000001`(无 `app2dev/`);第 3~6 位选车型驱动。
8. **App 待改**(§8):固件 v1.0.2 已就绪项 vs 建议下一版 App 修改清单。
---
## 2. 链路基础(UART / BLE / MQTT)
控制 payload **同一份 JSON**,固件均走 `remote_control_apply_json()`
| 链路 | App 侧要点 |
|------|------------|
| **UART** | 8N1;波特率 **须与固件 menuconfig 一致**(本仓库常用 **460800**;可选 115200/230400/921600);每条 JSON 末尾 `\n`;须按行组包(§6) |
| **BLE** | 写 **0xFFE1**;Notify **0xFFE3** 收心跳/停车推送;连接后发 `message_type=6` |
| **MQTT** | 订阅设备下行主题;发布控制 JSON 到设备上行主题;**组包与 UART 相同** |
### 2.1 连接与心跳
- UART / BLE:open 后发 `message_type=6`,收到 `1002/query` 后再认为已连接。
- 心跳 `message_type=1`:UART 约 3s,BLE 约 10s,MQTT 由固件周期上报。
- **OTA 进行中**(已发 `message_type=0``1002/done` 或失败中止):设备 **暂停心跳**,改发 **`1002` / `phase=active`** 识别 JSON(仅一次,见 §6.2)。
- OTA **失败**后设备发 `1002/done(ok=0)`,随后 **恢复周期心跳**。OTA **成功**约 1s 后重启,重连后再收心跳。
- OTA 完整流程见 **§6**
### 2.2 设备号与配网(UART / BLE / WiFi 通用)
| 项 | 说明 |
|----|------|
| 格式 | 纯设备号,如 **`CN110200000001`****不要** `app2dev/` 前缀 |
| 配网 | 长按 GPIO4 → 连热点 `192.168.4.1` → 填写完整设备号(UART 无 WiFi 下拉选型号) |
| 车型 | 取第 **3~6** 位(`1101` / `1102` / `1201`)自动选控制策略,无需单独选型号 |
| 上报 | 心跳、`1002``body.device_ID` 均为纯号;固件会剥掉历史 `app2dev/` 前缀 |
---
## 3. 控制协议(核心)
### 3.0 两类控制
```
message_type = 3
├── pwm_ctrl 行走:前后左右 + 力度(无 pin 字段)
└── pin_setctrl GPIO:pin + val(仅此处出现引脚号)
```
| 对象 | 含义 |
|------|------|
| `pwm_ctrl` | 前进 / 后退 / 左转 / 右转及力度 |
| `pin_setctrl` | 某 GPIO 拉高或拉低(发射、继电器等) |
固件先处理 `pwm_ctrl`,再处理 `pin_setctrl`
**App 组包必须分场景,不能把 GPIO 塞进行走 JSON。**
---
### 3.0.1 铁律:行走 JSON 禁止带 `pin`(UART / BLE / MQTT 通用)
> **只要是行走相关下发(含 MQTT),`body` 里只能有 `pwm_ctrl`,不能有 `pin_setctrl`。**
适用:**UART、BLE、MQTT 任意链路**,规则相同。
| 场景 | 能否带 `pin_setctrl` | `body` 内容 |
|------|---------------------|-------------|
| 控制中:前进 / 后退 / 转向 | ❌ **禁止** | 仅 `pwm_ctrl` |
| 只松前后 | ❌ **禁止** | 仅 `pwm_ctrl` |
| 只松左右 | ❌ **禁止** | 仅 `pwm_ctrl` |
| 前后 + 转向全停(§3.2.2) | ❌ **禁止** | 仅 `pwm_ctrl` 四字段全 `0` |
| 需要关 GPIO / 发射 | ❌ 不与行走同包 | **另发一条**`pin_setctrl` |
| 主动:边走边控 GPIO(§3.4) | ✅ 可同包 | `pwm_ctrl` + `pin_setctrl` |
**错误(MQTT / 串口 / BLE 都不要这样发):**
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0},"pin_setctrl":{"pin":16,"val":0}}}
```
**正确——行走与 GPIO 分两路:**
```text
# 行走全停 ×3(只有 pwm_ctrl)
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0}}}
# 需要关 GPIO 时,MQTT/串口/BLE 另发一条(只有 pin_setctrl)
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":0}}}
```
---
### 3.0.2 MQTT 行走控制专则
MQTT 与 UART/BLE **共用同一套 JSON**,无额外字段。
| MQTT 场景 | 发什么 | 禁止 |
|-----------|--------|------|
| 遥控前进 / 转向 | 仅 `pwm_ctrl` | ❌ 不要带 `pin_setctrl` |
| 单轴松手 | 仅 `pwm_ctrl`(该轴置 0) | ❌ 不要带 `pin_setctrl` |
| 全停 3 条 | 仅 `pwm_ctrl` 全 0 ×3 | ❌ **禁止**在全停里夹 `pin` |
| 开发射 / 关发射 | 仅 `pin_setctrl` | 不要混进全停 JSON |
| 边走边发射 | §3.4 同包 | 仅主动控制时允许 |
MQTT 发布行走 JSON 时,检查 `body`**若存在 `pin_setctrl` 且当前意图是行走控制/停车,则为错误组包。**
---
### 3.1 `pwm_ctrl` — 行走与转向
> 以下示例 **全部只有 `pwm_ctrl`**,适用于 UART / BLE / MQTT。
#### 字段
| 字段 | 含义 | 取值 |
|------|------|------|
| `speed_mode` | 前后 | `0` 无;`1` 前;`2` 后 |
| `speed_val` | 前后力度 | `0~200` |
| `steer_mode` | 左右 | `0` 无;`1` 左;`3` 右 |
| `steer_val` | 左右力度 | `0~200` |
#### 方向互斥
- 前后:`speed_mode` 只能 `0` / `1` / `2` 之一,不能同时前+后。
- 左右:`steer_mode` 只能 `0` / `1` / `3` 之一,不能同时左+右。
- 可组合:前+左、前+右、后+左、后+右。
#### 示例
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":1,"speed_val":80,"steer_mode":0,"steer_val":0}}}
```
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":1,"speed_val":80,"steer_mode":1,"steer_val":50}}}
```
---
### 3.2 停车与松手(只有 `pwm_ctrl`)
**UART / BLE / MQTT 全停、松手 JSON 均禁止出现 `pin_setctrl`。**
#### 3.2.1 单轴松手
| 操作 | 写法 |
|------|------|
| 只松前后 | `speed_mode=0, speed_val=0` |
| 只松左右 | `steer_mode=0, steer_val=0` |
| 全松 | §3.2.2 |
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":1,"steer_val":50}}}
```
#### 3.2.2 行走全停(3 条,仅 `pwm_ctrl`)
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0}}}
```
| 条次 | 时机 |
|------|------|
| 第 1 条 | 全 0 后立刻 |
| 第 2 条 | +100ms |
| 第 3 条 | +100ms |
发完 3 条结束,不要循环发送。
**3 条内容相同,且都不要带 `pin_setctrl`。**
#### 3.2.3 固件超时停车
500ms 无 JSON、链路清空、断连等:固件只停行走,**不会**自动拉低 GPIO。
App 需关 GPIO 时,MQTT/串口/BLE **另发** `pin_setctrl.val=0`
---
### 3.3 `pin_setctrl` — GPIO(独立 JSON)
**只有控制 GPIO 时才发此对象。** 与行走分包。
```json
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":1}}}
```
```json
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":0}}}
```
- 关 GPIO 必须显式 `val=0`
- 行走全停 / 松手:**禁止**`pwm_ctrl` 同包。
-`message_type=4` 改为 `message_type=3` + 仅 `pin_setctrl`
---
### 3.4 唯一允许同包的场景:主动边走边控 GPIO
仅用户操控中需要同时行走 + GPIO 时:
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":1,"speed_val":80,"steer_mode":1,"steer_val":50},"pin_setctrl":{"pin":16,"val":1}}}
```
松手仍分两段:
```text
# ① 行走全停 ×3(只有 pwm)
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0}}}
# ② 关 GPIO(只有 pin,另发)
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":0}}}
```
---
### 3.5 发送节奏
| 场景 | 间隔 / 次数 |
|------|-------------|
| 行走控制中 | 100~200ms 一条 |
| 最大间隔 | ≤ 500ms |
| 行走全停 | 3 条:0 / +100ms / +100ms,**仅 pwm_ctrl** |
| 单轴松手 | 1 条 `pwm_ctrl`,不带 pin |
---
### 3.6 Kotlin 组包(UART / BLE / MQTT 共用)
```kotlin
data class WalkCtrl(val speedMode: Int, val speedVal: Int, val steerMode: Int, val steerVal: Int)
/** 行走:只有 pwm_ctrl — 控制/松手/全停一律用这个 */
fun buildWalkJson(walk: WalkCtrl): String =
"""{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":${walk.speedMode},"speed_val":${walk.speedVal},"steer_mode":${walk.steerMode},"steer_val":${walk.steerVal}}}}"""
/** GPIO:只有 pin_setctrl — MQTT/串口/BLE 另发 */
fun buildPinJson(pin: Int, val: Int): String =
"""{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":$pin,"val":$val}}}"""
/** 仅 §3.4 主动同包;禁止用于停车 */
fun buildWalkAndPinJson(walk: WalkCtrl, pin: Int, pinVal: Int): String =
"""{"head":{"message_type":3},"body":{"pwm_ctrl":{...},"pin_setctrl":{"pin":$pin,"val":$pinVal}}}"""
val STOP_WALK_JSON = buildWalkJson(WalkCtrl(0, 0, 0, 0))
fun sendWalkStopBurst(publish: (String) -> Unit) {
publish(STOP_WALK_JSON) // 无 pin
postDelayed(100) { publish(STOP_WALK_JSON) }
postDelayed(200) { publish(STOP_WALK_JSON) }
}
// MQTT 行走:mqttPublish(buildWalkJson(...))
// MQTT 关 GPIO:mqttPublish(buildPinJson(16, 0)) // 与全停分开
```
组包前校验:
```kotlin
fun assertWalkOnly(json: String) {
val body = JSONObject(json).getJSONObject("body")
check(!body.has("pin_setctrl")) { "行走 JSON 禁止带 pin_setctrl" }
check(body.has("pwm_ctrl")) { "行走 JSON 必须有 pwm_ctrl" }
}
```
---
## 4. 设备 → 手机(解析)
| `message_type` | 含义 |
|----------------|------|
| `1` | 心跳(OTA 进行中 **暂停**,见 §6.2) |
| `3` | 行走停车推送(仅 `pwm_ctrl` 全 0,最多 3 条) |
| `4` | **告警**`ESP_LOGW`,Release 固件即时推送) |
| `5` | **错误**`ESP_LOGE`,Release 固件即时推送) |
| `1002` | OTA(`phase=query` / `active` / `done`,见 §6) |
收到停车推送:UI 同步行走停止;GPIO 不变,要关则 App 另发 `buildPinJson`
### 4.1 告警 / 错误日志 JSON(W / E → 手机)
**Release 固件**在链路已建立后,将 `ESP_LOGW` / `ESP_LOGE` **即时**打成 JSON 发给手机(与心跳无关,**出现一条推一条**):
| 链路 | 通道 | 条件 |
|------|------|------|
| **BLE** | Notify **0xFFE3** | GATT 已连接 |
| **UART** | UART1 文本行 + `\n` | 曾收到对端 RX(发过 `6` 等) |
- `head.message_type` = **4**(Warning)或 **5**(Error)
- `body`**仅** `msg`(字符串,通常为 `TAG: 说明`,已去掉行首 `W/E` 与级别前缀)
- **不缓存**:未连接 / 未握手期间产生的日志 **不会** 补发。
- **OTA 进行中**:告警/错误 JSON **仍会发送**(与 OT ACK 二进制并存;读线程遇 `{` 按行解析 JSON)。
**告警示例(`message_type = 4`):**
```json
{"head":{"message_type":4},"body":{"msg":"OTA_STREAM: OTA session aborted"}}
```
**错误示例(`message_type = 5`):**
```json
{"head":{"message_type":5},"body":{"msg":"OTA_STREAM: OTA error at chunk: overflow"}}
```
> **方向勿混**:**手机 → 设备** 的 `message_type=3` 里 `pin_setctrl` 是 GPIO 控制;**设备 → 手机** 的 `4`/`5` 是日志,二者方向与含义均不同。
**App 解析(UART / BLE 相同):**
```kotlin
when (messageType) {
1 -> onHeartbeat(body)
4, 5 -> onDeviceLog(messageType, body.getString("msg")) // 告警/错误
1002 -> onOtaPhase(body.getString("phase"), body)
}
```
---
## 5. 串口接收组包(遥控 / 握手阶段)
> **仅适用于未进入 OTA 二进制阶段**(尚未发 `message_type=0`)。OTA 推包时读线程规则见 **§6.5**。
```kotlin
private val rxTextBuffer = StringBuilder()
fun onSerialBytesForJsonOnly(bytes: ByteArray) {
rxTextBuffer.append(bytes.toString(Charsets.UTF_8))
while (true) {
val idx = rxTextBuffer.indexOf("\n")
if (idx < 0) break
val line = rxTextBuffer.substring(0, idx).trim('\r', '\n', ' ')
rxTextBuffer.delete(0, idx + 1)
if (line.isEmpty() || !line.startsWith("{")) continue
runCatching { JSONObject(line) }.onSuccess { handleJson(it) }
}
}
```
MQTT / BLE 按各自 SDK 收完整 JSON 字符串后,解析逻辑与上相同。
---
## 6. OTA 专题(UART / BLE)
### 6.1 文件与分区
| 项 | 说明 |
|----|------|
| 固件镜像 | `firmware/release/ota/ESPRCCar.bin` 或本地 `build/ESPRCCar.bin` |
| 版本清单 | 同目录 `ota_manifest.json``version` 须高于设备当前版) |
| 大小上限 | ≤ **6 MiB**`ota_0` / `ota_1` 分区) |
### 6.2 JSON 阶段(握手 + 状态识别)
**`message_type=1002` 的 `body.phase` 三态:**
| phase | 方向 | 何时 | 含义 |
|-------|------|------|------|
| `query` | 设备→手机 | 收到 `6` 后 | 握手 + OTA 参数(`device_ID``version``uart_baud``ota_chunk` 等,§6.2.1) |
| `active` | 设备→手机 | 收到 `0`**立即一条** | **OTA 识别 JSON**:已进入升级,**暂停心跳**(含同套 OTA 参数) |
| `done` | 设备→手机 | OTA 结束或失败 | `ok=1` 成功将重启;`ok=0` 失败(含 `err``written`),随后 **恢复心跳** |
**时序:**
| 步骤 | 方向 | 内容 |
|------|------|------|
| 1 | 手机→设备 | `message_type=6` + `\n`(每次 open 串口 / BLE 连接后) |
| 2 | 设备→手机 | `1002` / `phase=query` |
| 3 | 手机→设备 | **仅当** `manifest.version` **>** `query.body.version``message_type=0` + `\n` |
| 4 | 设备→手机 | `1002` / `phase=active`**替代心跳**,告知 App 进入 OTA) |
| 5 | 手机→设备 | OT 帧 BEGIN → DATA(分块)→ END(§6.3,**每块等 ACK 再发下一块**) |
| 6 | 设备→手机 | `1002` / `phase=done` |
| 7 | 设备 | `ok=1` 时约 **1s** 后重启;`ok=0`**恢复周期心跳**,可重试 OTA |
#### 6.2.1 `1002` 体字段(`query` / `active` 均可能携带)
| 字段 | 类型 | 说明 |
|------|------|------|
| `phase` | string | `query` / `active` / `done` |
| `device_ID` | string | 纯设备号 |
| `version` | string | 固件版本 |
| `uart_baud` | number | **UART 模式**:固件当前波特率(如 **460800**);BLE 无此字段 |
| `ota_chunk` | number | 设备建议 **DATA 块字节数**(固件按波特率计算,32~240) |
| `ota_chunk_max` | number | 固定 **240** |
| `ota_ack_timeout_ms` | number | 建议 App 等 ACK 超时(ms) |
**示例 — 握手(步骤 2,460800):**
```json
{"head":{"message_type":1002},"body":{"phase":"query","device_ID":"CN110200000001","version":"1.0.2","uart_baud":460800,"ota_chunk":876,"ota_chunk_max":1024,"ota_ack_timeout_ms":1200}}
```
**示例 — 识别 JSON(步骤 4):**
```json
{"head":{"message_type":1002},"body":{"phase":"active","device_ID":"CN110200000001","version":"1.0.2","uart_baud":460800,"ota_chunk":876,"ota_chunk_max":1024,"ota_ack_timeout_ms":1200}}
```
> **App 优先使用 `query`/`active` 里的 `ota_chunk` 与 `ota_ack_timeout_ms`**;若缺失则用 §6.3.3 本地公式(`open` 波特率须与 `uart_baud` 一致)。
**示例 — 成功汇报(步骤 6):**
```json
{"head":{"message_type":1002},"body":{"phase":"done","ok":1,"device_ID":"CN110200000001","version":"1.0.2","written":889072,"err":""}}
```
**示例 — 失败汇报(步骤 6,超时):**
```json
{"head":{"message_type":1002},"body":{"phase":"done","ok":0,"device_ID":"CN110200000001","version":"1.0.2","written":4096,"err":"timeout"}}
```
常见 `err``timeout` | `reset` | `disconnect` | `incomplete` | `overflow` | `write`(成功时 `err` 为空字符串 `""`)。
App 收到 `phase=active` 后:**不要等心跳**,UI 显示「升级中」;读线程以 **OT ACK 帧** 为主(§6.5)。
收到 `phase=done``ok=0` 后:**恢复等心跳**,可提示失败并允许用户重试(重新 `6→0→BEGIN`)。
### 6.3 OT 二进制帧(分块确认)
帧格式:`[4F 54][02][OP][SEQ×2][LEN×2][PAYLOAD…][CRC8][16]`
| OP | 方向 | 典型总长 | 说明 |
|----|------|----------|------|
| `0x01` BEGIN | 手机→设备 | **14** | PAYLOAD=4B 镜像大小(小端) |
| `0x02` DATA | 手机→设备 | **10+plen** | PAYLOAD=**1~1024B**(UART,`ota_chunk_max` 由固件下发;**用 `ota_chunk`**) |
| `0x03` END | 手机→设备 | **10** | PAYLOAD 空 |
| `0x80` ACK | 设备→手机 | BEGIN/END:**11** | 载荷仅 `[status]` |
| `0x80` ACK | 设备→手机 | DATA 1B:**12** | `[echo_byte][status]`(兼容旧版逐字节) |
| `0x80` ACK | 设备→手机 | DATA 多块:**14** | `[len_lo][len_hi][last_byte][status]` |
| `0x81` NAK | 设备→手机 | **11** | CRC/长度等错误 |
**分块参数(v1.0.2 起 · 波特率自适应):**
| 项 | 说明 |
|----|------|
| `ota_chunk` | **以设备 `1002` JSON 为准**;无则按 §6.3.3 用 `open` 波特率计算 |
| `ota_chunk_max` | **1024**(UART 单帧上限,menuconfig 可配 240~1024) |
| `ota_ack_timeout_ms` | **以设备 JSON 为准**;无则按 §6.3.3 计算 |
| `MAX_RETRIES_PER_CHUNK` | **3**(同 SEQ 重传) |
| 串口 `open` | **必须与 `uart_baud` 相同**,否则 CRC/速度均异常 |
**铁律:**
1. 必须先 `message_type=0`,再发 OT 帧。
2. 顺序:BEGIN 成功 → 若干 DATA 块 → END。
3. **每发一块须等设备 ACK**`status==0` 且长度/末字节校验通过)**再发下一块**;禁止在 ACK 前连发多块(日志里成片 `11 bytes` 为旧版逐字节或灌包过快)。
4. JSON 行与 OT 帧 **不要拼在同一行**`0` 后须 `\n`,再发二进制。
5. **仍须停等 ACK**(一块一确认);不能在未收 ACK 时流水线发下一块。
#### 6.3.1 固件按波特率推荐块大小与用时(`ota_chunk_max=1024`)
固件 `ota_chunk` 由 §6.3.2 公式算出;下表为 **分块停等 ACK** + USB 往返(**+15ms 典型** / **+30ms 保守**)。
**1MB:**
| `uart_baud` | `ota_chunk` | 典型吞吐 | **1MB 典型** | **1MB 保守** |
|-------------|-------------|----------|--------------|--------------|
| **115200** | **201** | ~5.7 KB/s | **~180s(3min)** | **~258s(4.3min)** |
| **230400** | **426** | ~12 KB/s | **~85s** | **~122s** |
| **460800** | **876** | ~25 KB/s | **~41s** | **~59s** |
| **921600** | **1024** | ~38 KB/s | **~27s** | **~42s** |
**6MB(分区上限):**
| `uart_baud` | `ota_chunk` | **6MB 典型** | **6MB 保守** |
|-------------|-------------|--------------|--------------|
| **115200** | 201 | **~18min** | **~26min** |
| **230400** | 426 | **~8.5min** | **~12min** |
| **460800** | 876 | **~4.1min(~248s)** | **~6min(~356s)** |
| **921600** | 1024 | **~2.7min(~162s)** | **~4.2min(~254s)** |
对比:
| 模式 | 6MB 约 |
|------|--------|
| 旧:逐字节 1B/帧 | **~17 小时** |
| 分块 max=240 @ 460800 | **典型 ~9min,保守 ~16min** |
| **分块 max=1024 @ 460800** | **典型 ~4min,保守 ~6min** |
> 再快需要 **改协议**(流水线多帧再批量 ACK / WiFi HTTP OTA),当前 UART 停等模型下 **460800+1024 块** 已接近 USB 线 practical 上限。
> 想压到 **6MB &lt;3min**:用 **921600** 波特率 + 确保 App 读线程够快(RTT≈15ms)。
#### 6.3.2 固件计算公式(与 `ota_uart_tune.c` 一致,App 可本地复算)
```text
wire_overhead = 24
ota_chunk_max = 1024 // menuconfig APP_OTA_UART_MAX_CHUNK
ota_chunk = clamp(baud / 512 - wire_overhead, 32, ota_chunk_max)
ota_ack_timeout_ms = clamp((ota_chunk + wire_overhead) * 10 * 1000 / baud * 3 + 800, 1200, 8000)
```
#### 6.3.3 App 分块发送示例(读 `1002` 参数)
```kotlin
object OtaUartTune {
private const val WIRE_OVERHEAD = 24
private const val CHUNK_MIN = 32
private const val CHUNK_MAX_DEFAULT = 1024
fun chunkForBaud(baud: Int, chunkMax: Int = CHUNK_MAX_DEFAULT): Int {
if (baud <= 0) return 128
return (baud / 512 - WIRE_OVERHEAD).coerceIn(CHUNK_MIN, chunkMax)
}
fun ackTimeoutMs(baud: Int, chunk: Int): Long {
val b = if (baud > 0) baud else 115200
val wireMs = (chunk + WIRE_OVERHEAD) * 10L * 1000L / b
return (wireMs * 3 + 800).coerceIn(1200, 8000)
}
fun fromQueryBody(body: JSONObject, openBaud: Int): Pair<Int, Long> {
val chunkMax = body.optInt("ota_chunk_max", CHUNK_MAX_DEFAULT)
val baud = body.optInt("uart_baud", openBaud)
val chunk = body.optInt("ota_chunk", 0).takeIf { it in CHUNK_MIN..chunkMax }
?: chunkForBaud(baud, chunkMax)
val ackMs = body.optLong("ota_ack_timeout_ms", 0L).takeIf { it > 0 }
?: ackTimeoutMs(baud, chunk)
return chunk to ackMs
}
}
const val MAX_RETRIES_PER_CHUNK = 3
data class DataAck(val chunkLen: Int, val lastByte: Byte, val status: Int)
/** DATA ACK:1B 载荷 → [echo][status];多块 → [len_lo][len_hi][last][status] */
fun OtaFrame.parseDataAck(frame: ByteArray): DataAck? {
if (frame.size < 10 || frame[3] != OP_ACK) return null
val plen = (frame[6].toInt() and 0xFF) or ((frame[7].toInt() and 0xFF) shl 8)
return when (plen) {
2 -> DataAck(1, frame[8], frame[9].toInt() and 0xFF)
4 -> DataAck(
(frame[8].toInt() and 0xFF) or ((frame[9].toInt() and 0xFF) shl 8),
frame[10], frame[11].toInt() and 0xFF
)
else -> null
}
}
fun buildDataChunk(seq: Int, chunk: ByteArray) = build(OP_DATA, seq, chunk)
fun uartOtaWithChunks(
port: UsbSerialPort,
firmware: ByteArray,
chunkSize: Int, // 来自 1002.body.ota_chunk
ackTimeoutMs: Long // 来自 1002.body.ota_ack_timeout_ms
) {
var seq = 0
port.write(OtaFrame.buildBegin(firmware.size), 3000)
require(waitStatusAck(seq, ackTimeoutMs) == 0)
seq = 1
var offset = 0
while (offset < firmware.size) {
val n = minOf(chunkSize, firmware.size - offset)
val chunk = firmware.copyOfRange(offset, offset + n)
var ok = false
repeat(MAX_RETRIES_PER_CHUNK) {
port.write(OtaFrame.buildDataChunk(seq, chunk), 5000)
val ack = waitAckFrame(seq, ackTimeoutMs) ?: return@repeat
val r = OtaFrame.parseDataAck(ack) ?: return@repeat
if (r.status == 0 && r.chunkLen == n && r.lastByte == chunk.last()) {
ok = true
return@repeat
}
}
check(ok) { "DATA chunk failed at offset=$offset seq=$seq" }
offset += n
seq++
}
port.write(OtaFrame.buildEnd(seq), 3000)
require(waitStatusAck(seq, ackTimeoutMs) == 0)
}
```
**`UartOtaManager.kt` 接入要点:**
1. `UsbSerialPort` **open 波特率 = `query.body.uart_baud`**(如 **460800**)。
2. 收到 `1002/query` 后:`val (chunk, ackMs) = OtaUartTune.fromQueryBody(body, openBaud)`
3. 收到 `1002/active` 后开始 OTA,用 `chunk` / `ackMs` 调用 `uartOtaWithChunks`
4.`openBaud != body.uart_baud`**拒绝 OTA** 并提示波特率不一致。
> `BleOtaManager.kt`:无 `uart_baud`,用 `ota_chunk`(缺省 128)与 `ota_ack_timeout_ms`。
### 6.4 UART 与 BLE 通道对照
| 项目 | BLE | UART |
|------|-----|------|
| 握手 / `0` | Write **0xFFE1** | JSON 行 + `\n` |
| `1002` JSON | Notify **0xFFE3** | UART1 文本行 + `\n` |
| 固件 OT 帧 | Write **0xFFE2** | UART1 二进制写 |
| ACK/NAK | Notify **0xFFE4** | UART1 读回(与 JSON 同线) |
| OTA 期间心跳 | **暂停**(改 `1002/active`) | **暂停**(改 `1002/active`) |
| OTA 期间告警 | Notify **0xFFE3** 仍可能推 `4`/`5` | 同 UART1 JSON 行 |
| 失败后心跳 | **恢复**`1002/done` 之后) | **恢复** |
### 6.5 UART RX 超时(固件侧,与 App 对齐)
| 阶段 | 无 RX 超时 | 设备行为 |
|------|------------|----------|
| 正常遥控 | **1.2s** | 停停车推送;非 OTA 会话 |
| 已发 `0`、等待 BEGIN | **15s** | 允许 App 在 `0` 后等待 `active`/组包再发 BEGIN |
| OT 传输中(BEGIN~END) | **`ota_ack_timeout_ms + 800ms`**(按波特率,约 1.2~6s) | 中止 OTA → `done(ok=0,err=timeout)` |
> App **`ota_ack_timeout_ms` 须小于固件传输无 RX 超时**(设备侧 = App 值 + 约 800ms)。`0` 后发 BEGIN 前走 **15s** 武装超时。
### 6.6 UART 读线程(OTA 阶段)
手机 `read()` 下行字节流:
- **OTA 进行中**:以 **OT ACK/NAK**(魔数 `4F 54`)为主;**不要**把整个缓冲当 UTF-8 解析。
- **非 OTA**:按 `\n` 组 JSON 行(§5)。
- 一次 `read()` 可能只含半帧;须字节缓冲组满再校验 CRC。
- `1002/done` 仍为 JSON 行,在 OT 帧解析器之外按行处理。
### 6.7 BEGIN ACK 超时与设备重启(联调实录)
手机报 **BEGIN ACK 超时** 时,若设备 monitor 出现:
```text
OTA_FRAME: RX BEGIN seq=0 plen=4
OTA_STREAM: OTA begin: size=... bytes
OTA_STREAM: OTA partition: ota_1 at ...
***ERROR*** A stack overflow in task uart_evt_rx has been detected.
```
说明 **BEGIN 已收到**,但在 `esp_ota_begin` 阶段 **UART RX 任务栈溢出重启**,ACK 来不及发出。
| 侧 | 处理 |
|----|------|
| **固件(必须)** | UART RX 经**指针队列**`uart_rx_work`(≥12KB 栈)处理;禁止在 `uart_evt_rx` 栈上放大块缓冲或同步 `esp_ota_begin`;队列满会导致 CRC NAK |
| **App(建议)** | BEGIN ACK 超时时若串口已断开,提示「设备异常重启,请更新固件」而非仅提示握手问题 |
修复后正常日志应含:`OTA_FRAME: TX ACK seq=0 status=0`
### 6.8 联调检查(OTA · 固件侧)
- [ ] 每次 open 先发 `6`,收到 `1002/query` 再显示已连接
- [ ] 版本更高才发 `0`;收到 `1002/active` 后 UI 切升级态、**不等心跳**
- [ ] 串口波特率与 `query.uart_baud` 一致(如 **460800**
- [ ] BEGIN **14B** → 等 ACK → 再发 DATA **分块(`ota_chunk`)**
- [ ] 每 DATA 块等 ACK:`status==0``chunkLen``last_byte` 与发送块一致
- [ ] 失败收 `1002/done(ok=0)`,之后应再收到周期心跳
- [ ] 成功收 `1002/done(ok=1)`,约 1s 重启,重连后 `query.body.version` 更新
---
## 7. 联调检查清单(App + 固件)
- [ ] UART / BLE / MQTT 控制 JSON **格式一致**
- [ ] **铁律**:行走相关(控制/松手/全停)**只有 `pwm_ctrl`,无 `pin_setctrl`**(含 MQTT)
- [ ] 全停 3 条仅 `pwm_ctrl`,不发带 pin 的包
- [ ] GPIO 用 `buildPinJson` **单独**发(MQTT 同规则)
- [ ] 同包融合**仅** §3.4 主动控制
- [ ] 前后/左右互斥;控制 100~200ms,≤500ms
- [ ] UART 行尾 `\n`;串口按行组包
- [ ] 握手 `6→1002/query`;控制不等 ACK
- [ ] OTA:`0` 后收 `1002/active`;分块 DATA、每块等 ACK;失败收 `done(ok=0)` 后恢复心跳(§6)
---
## 8. App 修改清单(配合固件 v1.0.2 · 波特率自适应分块 OTA)
> **不改成 §6.3 分块时仍可逐字节 OTA(极慢)**。实用速度须:**刷新版固件 + 按下列改 App + 串口波特率与固件一致(如 460800)**。
### 8.1 必须修改(P0)
| # | 文件 | 改什么 |
|---|------|--------|
| 1 | `UartRobotManager.kt`(或串口配置处) | `UsbSerialPort` **open 波特率 = 460800**(或与固件 menuconfig 一致);**不要再用 115200 除非固件也是 115200** |
| 2 | `OtaUartTune.kt`(新建) | 实现 §6.3.2 `OtaUartTune``chunkForBaud` / `ackTimeoutMs` / `fromQueryBody` |
| 3 | `OtaFrame.kt` | `buildDataChunk(seq, ByteArray)``parseDataAck` 支持 2B / 4B ACK |
| 4 | `UartOtaManager.kt` | 解析 `1002/query``uart_baud``ota_chunk``ota_ack_timeout_ms`**校验 open 波特率一致**;分块循环(§6.3.3);**收到 ACK 再发下一块** |
| 5 | `UartOtaManager.kt` | 未收到 `1002/active` → FAILED;BEGIN ACK 失败 → FAILED |
| 6 | `UartRobotManager.kt` | OTA 读流:组 OT 帧 + JSON 行(`4`/`5`/`1002`) |
| 7 | `BleOtaManager.kt` | 分块 OTA;`ota_chunk` / `ota_ack_timeout_ms``1002` 读取(无 `uart_baud`) |
### 8.2 建议优化(P1)
| # | 文件 | 建议 |
|---|------|------|
| 8 | `UartOtaManager.kt` | `MAX_RETRIES_PER_CHUNK = 3`;进度 `offset/size` |
| 9 | `UartDebugViewModel.kt` | 联调页显示 `uart_baud` / `ota_chunk`;失败展示 `done.err` |
| 10 | `OtaProtocol.kt` | 常量旁注释:块大小以 `1002` 为准,勿写死 |
| 11 | `OTA对比文档.md` | 同步 §6.2.1、§6.3.1 波特率表、§6.3.2 公式 |
### 8.3 固件 v1.0.2 已实现(App 可依赖)
- `6``1002/query`(含 `uart_baud``ota_chunk``ota_chunk_max``ota_ack_timeout_ms`
- `0``1002/active`(含同套 OTA 参数),暂停心跳
- OT DATA **1~1024B/帧**`APP_OTA_UART_MAX_CHUNK`,默认 1024)
- 460800 时 `ota_chunk` 通常为 **876**;921600 → **1024**
- DATA 多块 ACK:`[len_lo][len_hi][last][status]`
- OTA 传输无 RX 超时 = `ota_ack_timeout_ms + 800ms`(随波特率)
- 成功 `done(ok=1)` 后约 1s 重启;失败恢复心跳
- Release:`ESP_LOGW`/`E``message_type=4/5`
### 8.4 参考代码位置(App)
| 模块 | 路径 |
|------|------|
| OTA 状态机 | `com/fcrs2025/arflycar/uart/UartOtaManager.kt` |
| 波特率/块大小 | **新建** `com/fcrs2025/arflycar/ota/OtaUartTune.kt`(§6.3.2) |
| 串口 open 波特率 | `com/fcrs2025/arflycar/uart/UartRobotManager.kt`(须与 `uart_baud` 一致) |
| OT 帧 | `com/fcrs2025/arflycar/ble/OtaFrame.kt` |
| 协议版本 | `com/fcrs2025/arflycar/ota/OtaProtocol.kt``DOC_VERSION = "1.0.2"`) |
---
## 附录 A · 日志
| 场景 | 关键字 |
|------|--------|
| 行走 | `PWM fused:` |
| GPIO | `GPIO: pin N ->` |
| 全停 | `停车 JSON` |
| 超时 | `RC timeout` |
| OTA 识别 | `1002/active` |
| OTA ACK | `UART1 TX OTA ACK` |
| OTA 失败 | `1002/done` + `ok:0`;或 `message_type=5` + `msg` |
| 告警推手机 | `message_type=4` + `body.msg` |
---
如与 `v1.0.1` 冲突,以本文 `v1.0.2` 为准。
# 安装与配网指南 # 安装与配网指南
...@@ -95,11 +95,10 @@ idf.py -p COM16 flash monitor ...@@ -95,11 +95,10 @@ idf.py -p COM16 flash monitor
## 4. 配网(SoftAP + 网页) ## 4. 配网(SoftAP + 网页)
三种模式在缺少必填 NVS 时,都会开启 **SoftAP 热点**(SSID 默认 `esp32-apconfig`,可在 menuconfig **`ROBITO WIFI SSID`** 修改)。手机连接热点后浏览器访问: 三种模式在缺少必填 NVS 时,都会开启 **SoftAP 热点**(SSID 默认 `esp32-apconfig`,可在 menuconfig **`ROBIOT WIFI SSID`** 修改)。
**http://192.168.4.1** - 连接热点后,系统应 **自动弹出**「需登录网络 / 认证」页(Captive Portal);固件对 `generate_204` 等检测 URL 返回 **302** 到配网页(**勿再返回 204**,否则 Android 认为已联网不弹窗)。
- 若未自动弹出:关移动数据,浏览器手动打开 **http://192.168.4.1**(设备 DNS 劫持任意域名到 `192.168.4.1`)。
(设备侧 DNS 会劫持域名,直接输 IP 即可。)
### 4.1 各模式配网页与必填项 ### 4.1 各模式配网页与必填项
...@@ -172,6 +171,7 @@ scripts\copy_firmware_release.bat ...@@ -172,6 +171,7 @@ scripts\copy_firmware_release.bat
|------|------| |------|------|
| UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` | | UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` |
| 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 | | 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 |
| 连热点不自动弹配网页 | 刷含 captive 302 修复的固件;关移动数据;仍不行则手动 **http://192.168.4.1** |
| `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` | | `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` |
| 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` | | 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` |
......
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式) # Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
...@@ -62,7 +62,7 @@ Android 做 BLE / UART OTA **只需要两个文件**(运行 `scripts\copy_firm ...@@ -62,7 +62,7 @@ Android 做 BLE / UART OTA **只需要两个文件**(运行 `scripts\copy_firm
- **OTA 固件确认**:设备回 **完整 ACK/NAK 帧**(BLE 经 **0xFFE4** 按帧字节 Notify;UART 按帧写回);`status≠0` 时 App **原 SEQ 重传** - **OTA 固件确认**:设备回 **完整 ACK/NAK 帧**(BLE 经 **0xFFE4** 按帧字节 Notify;UART 按帧写回);`status≠0` 时 App **原 SEQ 重传**
- **控制 JSON**:手机下发后 **无 `OK` 文本回复** - **控制 JSON**:手机下发后 **无 `OK` 文本回复**
- **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。 - **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。
- **心跳仍在 `0xFFE3` / UART 串口**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**,字段见 §3.5-A - **心跳**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**(§3.5-A)。**BLE****0xFFE3**,与 **0xFFE4** OTA ACK **分特征值**,OTA 期间心跳照常。**UART** 与 OT ACK **共用 UART1**:手机发 `0` 武装后至 `1002/done` 或会话中止前,设备 **暂停 UART 心跳**,线上仅 OT ACK/NAK + `1002` JSON,避免与逐帧确认混线
- **命令下发间隔**(BLE/UART 共用):手机→设备 **任意相邻两条 JSON** 间隔 **≥100ms**;摇杆控制建议 **100~200ms** 一发,且须 **≤500ms** 以免固件超时停车(§5.0.2、§7.2.3)。 - **命令下发间隔**(BLE/UART 共用):手机→设备 **任意相邻两条 JSON** 间隔 **≥100ms**;摇杆控制建议 **100~200ms** 一发,且须 **≤500ms** 以免固件超时停车(§5.0.2、§7.2.3)。
- **停车专用 JSON**(§4.4.1、§4.4.2):松手/停止时发 **全位 0**`speed/direction/mode/val` 均为 0);**前进+左转等多键同时抬起** 也必须用同一条 JSON,**每 300ms** 重复写。 - **停车专用 JSON**(§4.4.1、§4.4.2):松手/停止时发 **全位 0**`speed/direction/mode/val` 均为 0);**前进+左转等多键同时抬起** 也必须用同一条 JSON,**每 300ms** 重复写。
- **UART 波特率固定 115200**:App **设置页**应有「串口波特率」项,但当前固件只支持 **115200**;每次 `open` 后必须 `setParameters(115200, 8, N, 1)`(§7.2.0)。 - **UART 波特率固定 115200**:App **设置页**应有「串口波特率」项,但当前固件只支持 **115200**;每次 `open` 后必须 `setParameters(115200, 8, N, 1)`(§7.2.0)。
...@@ -539,7 +539,19 @@ flowchart TD ...@@ -539,7 +539,19 @@ flowchart TD
**BEGIN/END 应答**:PAYLOAD **`[status]`** 仅 1 字节。 **BEGIN/END 应答**:PAYLOAD **`[status]`** 仅 1 字节。
**NAK**`OP=0x81`):CRC/长度错误等。 **NAK**`OP=0x81`):CRC/长度错误等。
**Android**:从 **0xFFE4** 组满一整帧再校验;`status!=0``data_byte` 不符 → **重传** **整帧长度(VER=0x02,便于日志对照)**
| OP | 方向 | 典型总长 |
|----|------|----------|
| BEGIN `0x01` | 手机→设备 | **14**(8 头 + 4B 镜像长度 + CRC + TAIL) |
| DATA `0x02` | 手机→设备 | **11**(8 头 + 1B 固件字节 + CRC + TAIL) |
| END `0x03` | 手机→设备 | **10**(8 头 + 空 payload + CRC + TAIL) |
| ACK `0x80` | 设备→手机 | BEGIN/END:**11**;DATA:**12**(含回显字节) |
| NAK `0x81` | 设备→手机 | **11** |
设备 **每收到一帧完整 OT 请求即在同链路回一帧 ACK/NAK**(UART 写回 UART1;BLE Notify **0xFFE4**)。App **必须等 ACK `status==0` 再发下一帧**,禁止无节流连发 DATA。
**Android**:从 **0xFFE4**(或 UART 读流)组满一整帧再校验;`status!=0``data_byte` 不符 → **原 SEQ 重传**
#### 3.4.3 旧版裸 opcode(兼容) #### 3.4.3 旧版裸 opcode(兼容)
...@@ -1788,7 +1800,8 @@ Android App 内置或下载 OTA 包时只需要两个文件: ...@@ -1788,7 +1800,8 @@ Android App 内置或下载 OTA 包时只需要两个文件:
|------|-----|-------------------| |------|-----|-------------------|
| 询问 / 结果 JSON | **0xFFE3** Notify | UART1 文本行 + `\n` | | 询问 / 结果 JSON | **0xFFE3** Notify | UART1 文本行 + `\n` |
| 同意 OTA | Write **0xFFE1** | JSON 行 + `\n` | | 同意 OTA | Write **0xFFE1** | JSON 行 + `\n` |
| 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧) | | 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧);**每 RX 一帧即 TX 一回 ACK** |
| OTA 期间心跳 | **0xFFE3** 照常(与 ACK 分通道) | **暂停**(避免与 ACK 混线) |
| 固件数据确认(旧版兼容) | 旧固件可能出现单字节回显 | 旧固件可能出现同线单字节回显 | | 固件数据确认(旧版兼容) | 旧固件可能出现单字节回显 | 旧固件可能出现同线单字节回显 |
| 控制 JSON 后 | 无 ACK | **无** `OK` | | 控制 JSON 后 | 无 ACK | **无** `OK` |
| USB 调试口(UART0) | Release 下 **无日志**(§1.5) | 同左 | | USB 调试口(UART0) | Release 下 **无日志**(§1.5) | 同左 |
...@@ -1798,7 +1811,7 @@ Android App 内置或下载 OTA 包时只需要两个文件: ...@@ -1798,7 +1811,7 @@ Android App 内置或下载 OTA 包时只需要两个文件:
1. **OT 帧(推荐)**:按 §3.4.2 发送 BEGIN/DATA/END,等待 ACK/NAK 帧;`status!=0` 按原 SEQ 重传。 1. **OT 帧(推荐)**:按 §3.4.2 发送 BEGIN/DATA/END,等待 ACK/NAK 帧;`status!=0` 按原 SEQ 重传。
2. **裸 `0x01/0x02/0x03`(兼容旧流程)**:仍可用,但新 App 不建议继续依赖该模式。 2. **裸 `0x01/0x02/0x03`(兼容旧流程)**:仍可用,但新 App 不建议继续依赖该模式。
**UART 读线程必须做双路解复用**:同一 UART1 RX 流里既可能有 JSON 行(`{...}\n`,如心跳/1002),也可能有 OT ACK/NAK 二进制帧(`0x4F 0x54 ... 0x16`)。OTA 阶段读线程应优先组 OT 帧;遇到 `{` 再按换行组 JSON。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。 **UART 读线程必须做双路解复用(App 读设备下行)**:手机 `read()` 到的字节流里,**非 OTA 阶段**可能有 JSON 行(`{...}\n`,心跳/`1002`)。**OTA 二进制阶段**(已发 `message_type=0`)设备 **暂停 UART 心跳**,线上以 **OT ACK/NAK 帧**`0x4F 0x54 ... 0x16`)为主,结束时再收 `1002/done` JSON。读线程应 **按字节组 OT 帧**;仅当缓冲以 `{` 开头且无完整 OT 魔数时再按 `\n` 组 JSON。**禁止**把整个 `read()` 缓冲当 UTF-8 文本解析(会破坏 ACK 二进制)。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。
**JSON 分片/粘包必读(对应你看到的日志现象)** **JSON 分片/粘包必读(对应你看到的日志现象)**
...@@ -1814,12 +1827,14 @@ Android App 内置或下载 OTA 包时只需要两个文件: ...@@ -1814,12 +1827,14 @@ Android App 内置或下载 OTA 包时只需要两个文件:
3. 解析完从缓冲中移除该行,继续处理剩余内容(可能是下一条 JSON 头部)。 3. 解析完从缓冲中移除该行,继续处理剩余内容(可能是下一条 JSON 头部)。
4. 没有 `\n` 的残留内容继续保留,等待下一次回调拼接。 4. 没有 `\n` 的残留内容继续保留,等待下一次回调拼接。
**推荐参考实现(串口文本流)** **推荐参考实现(握手/遥控阶段:仅 JSON 行)**
> 下列代码 **仅适用于未进入 OTA 二进制阶段**(尚未发 `message_type=0`)。OTA 推包时必须改用下方 **字节流 + OT 帧解析**,不能把 `read()` 当 `String`。
```kotlin ```kotlin
private val rxTextBuffer = StringBuilder() private val rxTextBuffer = StringBuilder()
fun onSerialBytes(bytes: ByteArray) { fun onSerialBytesForJsonOnly(bytes: ByteArray) {
rxTextBuffer.append(bytes.toString(Charsets.UTF_8)) rxTextBuffer.append(bytes.toString(Charsets.UTF_8))
while (true) { while (true) {
...@@ -1830,7 +1845,7 @@ fun onSerialBytes(bytes: ByteArray) { ...@@ -1830,7 +1845,7 @@ fun onSerialBytes(bytes: ByteArray) {
rxTextBuffer.delete(0, idx + 1) rxTextBuffer.delete(0, idx + 1)
if (line.isEmpty()) continue if (line.isEmpty()) continue
if (!line.startsWith("{")) continue // 过滤日志行或非 JSON 文本 if (!line.startsWith("{")) continue
try { try {
handleJson(JSONObject(line)) handleJson(JSONObject(line))
...@@ -1841,6 +1856,23 @@ fun onSerialBytes(bytes: ByteArray) { ...@@ -1841,6 +1856,23 @@ fun onSerialBytes(bytes: ByteArray) {
} }
``` ```
**OTA 阶段读 ACK(与 JSON 解耦,伪代码)**
```kotlin
private val rxOtaBuf = ByteArray(512)
private var rxOtaLen = 0
fun onSerialBytesDuringOta(chunk: ByteArray) {
// 追加到字节缓冲,循环尝试 parseOtFrame();完整 ACK 后再发下一 DATA
append(rxOtaBuf, chunk)
while (true) {
val frame = tryParseOtFrame(rxOtaBuf) ?: break
consume(rxOtaBuf, frame.size)
otaAckQueue.offer(frame)
}
}
```
**遥控连续发送与超时停车(必读)** **遥控连续发送与超时停车(必读)**
- 固件遥控看门狗按“最近一次收到命令字节”计时,间隔过长会触发自动停车。 - 固件遥控看门狗按“最近一次收到命令字节”计时,间隔过长会触发自动停车。
......
# Android 端设备对接文档 · 固件 v1.0.2
# Android 端设备对接文档 · 固件 v1.0.2
> **读者**:Android 开发。
> **控制 JSON 在 UART、BLE、MQTT 上格式完全相同**;差异仅在传输层,组包规则一致。
---
## 0. 三十秒看懂
| 项目 | 说明 |
|------|------|
| 适用链路 | **UART / BLE / MQTT** 下发控制时,JSON 结构相同 |
| 控制入口 | `head.message_type = 3``message_type=4` 已废弃) |
| **行走 / 转向** | 只有 `body.pwm_ctrl`(前后左右 + 力度,**无 GPIO 号**) |
| **GPIO / 发射** | 只有 `body.pin_setctrl``pin` + `val`) |
| **铁律** | **凡行走相关 JSON(控制 / 松手 / 全停),UART·BLE·MQTT 一律不得带 `pin_setctrl`** |
| 无 ACK | 控制 JSON 发出后不要等待 `OK` |
| 发送节奏 | 控制中 100~200ms 一条;间隔不得超过 500ms |
| UART 行尾 | 串口每条 JSON 以 `\n` 结尾 |
| 行走全停 | 仅 `pwm_ctrl` 四字段全 `0`;连发 **3 条**(0 / +100ms / +100ms)后停 |
| GPIO | 单独 `pin_setctrl`;与行走 JSON **分包**(主动同包见 §3.4) |
---
## 1. 本版相对 v1.0.1 的变更
1. **融合控制**`message_type=3` 统一入口;行走用 `pwm_ctrl`,GPIO 用 `pin_setctrl`
2. **行走新格式**`speed_mode/speed_val` + `steer_mode/steer_val`,一条 JSON 可「前进 + 左转」。
3. **行走与 GPIO 解耦**:停车 / 松手 / 方向控制 **不带 pin**;GPIO 另发。
4. **`message_type=4` 废弃**
5. **OTA 专题**(§6):OT **分块 DATA**(按波特率自适应,UART 单帧最大 **1024B**);`1002` 下发 `ota_chunk` / `ota_chunk_max`;460800 下 **6MB 典型 ~4min**
6. **日志上送**(§4.1):Release 下 `ESP_LOGW`/`ESP_LOGE``message_type=4/5` JSON(UART/BLE)。
7. **设备号**(§2.2):配网填完整号 `CN110200000001`(无 `app2dev/`);第 3~6 位选车型驱动。
8. **App 待改**(§8):固件 v1.0.2 已就绪项 vs 建议下一版 App 修改清单。
---
## 2. 链路基础(UART / BLE / MQTT)
控制 payload **同一份 JSON**,固件均走 `remote_control_apply_json()`
| 链路 | App 侧要点 |
|------|------------|
| **UART** | 8N1;波特率 **须与固件 menuconfig 一致**(本仓库常用 **460800**;可选 115200/230400/921600);每条 JSON 末尾 `\n`;须按行组包(§6) |
| **BLE** | 写 **0xFFE1**;Notify **0xFFE3** 收心跳/停车推送;连接后发 `message_type=6` |
| **MQTT** | 订阅设备下行主题;发布控制 JSON 到设备上行主题;**组包与 UART 相同** |
### 2.1 连接与心跳
- UART / BLE:open 后发 `message_type=6`,收到 `1002/query` 后再认为已连接。
- 心跳 `message_type=1`:UART 约 3s,BLE 约 10s,MQTT 由固件周期上报。
- **OTA 进行中**(已发 `message_type=0``1002/done` 或失败中止):设备 **暂停心跳**,改发 **`1002` / `phase=active`** 识别 JSON(仅一次,见 §6.2)。
- OTA **失败**后设备发 `1002/done(ok=0)`,随后 **恢复周期心跳**。OTA **成功**约 1s 后重启,重连后再收心跳。
- OTA 完整流程见 **§6**
### 2.2 设备号与配网(UART / BLE / WiFi 通用)
| 项 | 说明 |
|----|------|
| 格式 | 纯设备号,如 **`CN110200000001`****不要** `app2dev/` 前缀 |
| 配网 | 长按 GPIO4 → 连热点 `192.168.4.1` → 填写完整设备号(UART 无 WiFi 下拉选型号) |
| 车型 | 取第 **3~6** 位(`1101` / `1102` / `1201`)自动选控制策略,无需单独选型号 |
| 上报 | 心跳、`1002``body.device_ID` 均为纯号;固件会剥掉历史 `app2dev/` 前缀 |
---
## 3. 控制协议(核心)
### 3.0 两类控制
```
message_type = 3
├── pwm_ctrl 行走:前后左右 + 力度(无 pin 字段)
└── pin_setctrl GPIO:pin + val(仅此处出现引脚号)
```
| 对象 | 含义 |
|------|------|
| `pwm_ctrl` | 前进 / 后退 / 左转 / 右转及力度 |
| `pin_setctrl` | 某 GPIO 拉高或拉低(发射、继电器等) |
固件先处理 `pwm_ctrl`,再处理 `pin_setctrl`
**App 组包必须分场景,不能把 GPIO 塞进行走 JSON。**
---
### 3.0.1 铁律:行走 JSON 禁止带 `pin`(UART / BLE / MQTT 通用)
> **只要是行走相关下发(含 MQTT),`body` 里只能有 `pwm_ctrl`,不能有 `pin_setctrl`。**
适用:**UART、BLE、MQTT 任意链路**,规则相同。
| 场景 | 能否带 `pin_setctrl` | `body` 内容 |
|------|---------------------|-------------|
| 控制中:前进 / 后退 / 转向 | ❌ **禁止** | 仅 `pwm_ctrl` |
| 只松前后 | ❌ **禁止** | 仅 `pwm_ctrl` |
| 只松左右 | ❌ **禁止** | 仅 `pwm_ctrl` |
| 前后 + 转向全停(§3.2.2) | ❌ **禁止** | 仅 `pwm_ctrl` 四字段全 `0` |
| 需要关 GPIO / 发射 | ❌ 不与行走同包 | **另发一条**`pin_setctrl` |
| 主动:边走边控 GPIO(§3.4) | ✅ 可同包 | `pwm_ctrl` + `pin_setctrl` |
**错误(MQTT / 串口 / BLE 都不要这样发):**
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0},"pin_setctrl":{"pin":16,"val":0}}}
```
**正确——行走与 GPIO 分两路:**
```text
# 行走全停 ×3(只有 pwm_ctrl)
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0}}}
# 需要关 GPIO 时,MQTT/串口/BLE 另发一条(只有 pin_setctrl)
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":0}}}
```
---
### 3.0.2 MQTT 行走控制专则
MQTT 与 UART/BLE **共用同一套 JSON**,无额外字段。
| MQTT 场景 | 发什么 | 禁止 |
|-----------|--------|------|
| 遥控前进 / 转向 | 仅 `pwm_ctrl` | ❌ 不要带 `pin_setctrl` |
| 单轴松手 | 仅 `pwm_ctrl`(该轴置 0) | ❌ 不要带 `pin_setctrl` |
| 全停 3 条 | 仅 `pwm_ctrl` 全 0 ×3 | ❌ **禁止**在全停里夹 `pin` |
| 开发射 / 关发射 | 仅 `pin_setctrl` | 不要混进全停 JSON |
| 边走边发射 | §3.4 同包 | 仅主动控制时允许 |
MQTT 发布行走 JSON 时,检查 `body`**若存在 `pin_setctrl` 且当前意图是行走控制/停车,则为错误组包。**
---
### 3.1 `pwm_ctrl` — 行走与转向
> 以下示例 **全部只有 `pwm_ctrl`**,适用于 UART / BLE / MQTT。
#### 字段
| 字段 | 含义 | 取值 |
|------|------|------|
| `speed_mode` | 前后 | `0` 无;`1` 前;`2` 后 |
| `speed_val` | 前后力度 | `0~200` |
| `steer_mode` | 左右 | `0` 无;`1` 左;`3` 右 |
| `steer_val` | 左右力度 | `0~200` |
#### 方向互斥
- 前后:`speed_mode` 只能 `0` / `1` / `2` 之一,不能同时前+后。
- 左右:`steer_mode` 只能 `0` / `1` / `3` 之一,不能同时左+右。
- 可组合:前+左、前+右、后+左、后+右。
#### 示例
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":1,"speed_val":80,"steer_mode":0,"steer_val":0}}}
```
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":1,"speed_val":80,"steer_mode":1,"steer_val":50}}}
```
---
### 3.2 停车与松手(只有 `pwm_ctrl`)
**UART / BLE / MQTT 全停、松手 JSON 均禁止出现 `pin_setctrl`。**
#### 3.2.1 单轴松手
| 操作 | 写法 |
|------|------|
| 只松前后 | `speed_mode=0, speed_val=0` |
| 只松左右 | `steer_mode=0, steer_val=0` |
| 全松 | §3.2.2 |
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":1,"steer_val":50}}}
```
#### 3.2.2 行走全停(3 条,仅 `pwm_ctrl`)
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0}}}
```
| 条次 | 时机 |
|------|------|
| 第 1 条 | 全 0 后立刻 |
| 第 2 条 | +100ms |
| 第 3 条 | +100ms |
发完 3 条结束,不要循环发送。
**3 条内容相同,且都不要带 `pin_setctrl`。**
#### 3.2.3 固件超时停车
500ms 无 JSON、链路清空、断连等:固件只停行走,**不会**自动拉低 GPIO。
App 需关 GPIO 时,MQTT/串口/BLE **另发** `pin_setctrl.val=0`
---
### 3.3 `pin_setctrl` — GPIO(独立 JSON)
**只有控制 GPIO 时才发此对象。** 与行走分包。
```json
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":1}}}
```
```json
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":0}}}
```
- 关 GPIO 必须显式 `val=0`
- 行走全停 / 松手:**禁止**`pwm_ctrl` 同包。
-`message_type=4` 改为 `message_type=3` + 仅 `pin_setctrl`
---
### 3.4 唯一允许同包的场景:主动边走边控 GPIO
仅用户操控中需要同时行走 + GPIO 时:
```json
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":1,"speed_val":80,"steer_mode":1,"steer_val":50},"pin_setctrl":{"pin":16,"val":1}}}
```
松手仍分两段:
```text
# ① 行走全停 ×3(只有 pwm)
{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":0,"speed_val":0,"steer_mode":0,"steer_val":0}}}
# ② 关 GPIO(只有 pin,另发)
{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":16,"val":0}}}
```
---
### 3.5 发送节奏
| 场景 | 间隔 / 次数 |
|------|-------------|
| 行走控制中 | 100~200ms 一条 |
| 最大间隔 | ≤ 500ms |
| 行走全停 | 3 条:0 / +100ms / +100ms,**仅 pwm_ctrl** |
| 单轴松手 | 1 条 `pwm_ctrl`,不带 pin |
---
### 3.6 Kotlin 组包(UART / BLE / MQTT 共用)
```kotlin
data class WalkCtrl(val speedMode: Int, val speedVal: Int, val steerMode: Int, val steerVal: Int)
/** 行走:只有 pwm_ctrl — 控制/松手/全停一律用这个 */
fun buildWalkJson(walk: WalkCtrl): String =
"""{"head":{"message_type":3},"body":{"pwm_ctrl":{"speed_mode":${walk.speedMode},"speed_val":${walk.speedVal},"steer_mode":${walk.steerMode},"steer_val":${walk.steerVal}}}}"""
/** GPIO:只有 pin_setctrl — MQTT/串口/BLE 另发 */
fun buildPinJson(pin: Int, val: Int): String =
"""{"head":{"message_type":3},"body":{"pin_setctrl":{"pin":$pin,"val":$val}}}"""
/** 仅 §3.4 主动同包;禁止用于停车 */
fun buildWalkAndPinJson(walk: WalkCtrl, pin: Int, pinVal: Int): String =
"""{"head":{"message_type":3},"body":{"pwm_ctrl":{...},"pin_setctrl":{"pin":$pin,"val":$pinVal}}}"""
val STOP_WALK_JSON = buildWalkJson(WalkCtrl(0, 0, 0, 0))
fun sendWalkStopBurst(publish: (String) -> Unit) {
publish(STOP_WALK_JSON) // 无 pin
postDelayed(100) { publish(STOP_WALK_JSON) }
postDelayed(200) { publish(STOP_WALK_JSON) }
}
// MQTT 行走:mqttPublish(buildWalkJson(...))
// MQTT 关 GPIO:mqttPublish(buildPinJson(16, 0)) // 与全停分开
```
组包前校验:
```kotlin
fun assertWalkOnly(json: String) {
val body = JSONObject(json).getJSONObject("body")
check(!body.has("pin_setctrl")) { "行走 JSON 禁止带 pin_setctrl" }
check(body.has("pwm_ctrl")) { "行走 JSON 必须有 pwm_ctrl" }
}
```
---
## 4. 设备 → 手机(解析)
| `message_type` | 含义 |
|----------------|------|
| `1` | 心跳(OTA 进行中 **暂停**,见 §6.2) |
| `3` | 行走停车推送(仅 `pwm_ctrl` 全 0,最多 3 条) |
| `4` | **告警**`ESP_LOGW`,Release 固件即时推送) |
| `5` | **错误**`ESP_LOGE`,Release 固件即时推送) |
| `1002` | OTA(`phase=query` / `active` / `done`,见 §6) |
收到停车推送:UI 同步行走停止;GPIO 不变,要关则 App 另发 `buildPinJson`
### 4.1 告警 / 错误日志 JSON(W / E → 手机)
**Release 固件**在链路已建立后,将 `ESP_LOGW` / `ESP_LOGE` **即时**打成 JSON 发给手机(与心跳无关,**出现一条推一条**):
| 链路 | 通道 | 条件 |
|------|------|------|
| **BLE** | Notify **0xFFE3** | GATT 已连接 |
| **UART** | UART1 文本行 + `\n` | 曾收到对端 RX(发过 `6` 等) |
- `head.message_type` = **4**(Warning)或 **5**(Error)
- `body`**仅** `msg`(字符串,通常为 `TAG: 说明`,已去掉行首 `W/E` 与级别前缀)
- **不缓存**:未连接 / 未握手期间产生的日志 **不会** 补发。
- **OTA 进行中**:告警/错误 JSON **仍会发送**(与 OT ACK 二进制并存;读线程遇 `{` 按行解析 JSON)。
**告警示例(`message_type = 4`):**
```json
{"head":{"message_type":4},"body":{"msg":"OTA_STREAM: OTA session aborted"}}
```
**错误示例(`message_type = 5`):**
```json
{"head":{"message_type":5},"body":{"msg":"OTA_STREAM: OTA error at chunk: overflow"}}
```
> **方向勿混**:**手机 → 设备** 的 `message_type=3` 里 `pin_setctrl` 是 GPIO 控制;**设备 → 手机** 的 `4`/`5` 是日志,二者方向与含义均不同。
**App 解析(UART / BLE 相同):**
```kotlin
when (messageType) {
1 -> onHeartbeat(body)
4, 5 -> onDeviceLog(messageType, body.getString("msg")) // 告警/错误
1002 -> onOtaPhase(body.getString("phase"), body)
}
```
---
## 5. 串口接收组包(遥控 / 握手阶段)
> **仅适用于未进入 OTA 二进制阶段**(尚未发 `message_type=0`)。OTA 推包时读线程规则见 **§6.5**。
```kotlin
private val rxTextBuffer = StringBuilder()
fun onSerialBytesForJsonOnly(bytes: ByteArray) {
rxTextBuffer.append(bytes.toString(Charsets.UTF_8))
while (true) {
val idx = rxTextBuffer.indexOf("\n")
if (idx < 0) break
val line = rxTextBuffer.substring(0, idx).trim('\r', '\n', ' ')
rxTextBuffer.delete(0, idx + 1)
if (line.isEmpty() || !line.startsWith("{")) continue
runCatching { JSONObject(line) }.onSuccess { handleJson(it) }
}
}
```
MQTT / BLE 按各自 SDK 收完整 JSON 字符串后,解析逻辑与上相同。
---
## 6. OTA 专题(UART / BLE)
### 6.1 文件与分区
| 项 | 说明 |
|----|------|
| 固件镜像 | `firmware/release/ota/ESPRCCar.bin` 或本地 `build/ESPRCCar.bin` |
| 版本清单 | 同目录 `ota_manifest.json``version` 须高于设备当前版) |
| 大小上限 | ≤ **6 MiB**`ota_0` / `ota_1` 分区) |
### 6.2 JSON 阶段(握手 + 状态识别)
**`message_type=1002` 的 `body.phase` 三态:**
| phase | 方向 | 何时 | 含义 |
|-------|------|------|------|
| `query` | 设备→手机 | 收到 `6` 后 | 握手 + OTA 参数(`device_ID``version``uart_baud``ota_chunk` 等,§6.2.1) |
| `active` | 设备→手机 | 收到 `0`**立即一条** | **OTA 识别 JSON**:已进入升级,**暂停心跳**(含同套 OTA 参数) |
| `done` | 设备→手机 | OTA 结束或失败 | `ok=1` 成功将重启;`ok=0` 失败(含 `err``written`),随后 **恢复心跳** |
**时序:**
| 步骤 | 方向 | 内容 |
|------|------|------|
| 1 | 手机→设备 | `message_type=6` + `\n`(每次 open 串口 / BLE 连接后) |
| 2 | 设备→手机 | `1002` / `phase=query` |
| 3 | 手机→设备 | **仅当** `manifest.version` **>** `query.body.version``message_type=0` + `\n` |
| 4 | 设备→手机 | `1002` / `phase=active`**替代心跳**,告知 App 进入 OTA) |
| 5 | 手机→设备 | OT 帧 BEGIN → DATA(分块)→ END(§6.3,**每块等 ACK 再发下一块**) |
| 6 | 设备→手机 | `1002` / `phase=done` |
| 7 | 设备 | `ok=1` 时约 **1s** 后重启;`ok=0`**恢复周期心跳**,可重试 OTA |
#### 6.2.1 `1002` 体字段(`query` / `active` 均可能携带)
| 字段 | 类型 | 说明 |
|------|------|------|
| `phase` | string | `query` / `active` / `done` |
| `device_ID` | string | 纯设备号 |
| `version` | string | 固件版本 |
| `uart_baud` | number | **UART 模式**:固件当前波特率(如 **460800**);BLE 无此字段 |
| `ota_chunk` | number | 设备建议 **DATA 块字节数**(固件按波特率计算,32~240) |
| `ota_chunk_max` | number | 固定 **240** |
| `ota_ack_timeout_ms` | number | 建议 App 等 ACK 超时(ms) |
**示例 — 握手(步骤 2,460800):**
```json
{"head":{"message_type":1002},"body":{"phase":"query","device_ID":"CN110200000001","version":"1.0.2","uart_baud":460800,"ota_chunk":876,"ota_chunk_max":1024,"ota_ack_timeout_ms":1200}}
```
**示例 — 识别 JSON(步骤 4):**
```json
{"head":{"message_type":1002},"body":{"phase":"active","device_ID":"CN110200000001","version":"1.0.2","uart_baud":460800,"ota_chunk":876,"ota_chunk_max":1024,"ota_ack_timeout_ms":1200}}
```
> **App 优先使用 `query`/`active` 里的 `ota_chunk` 与 `ota_ack_timeout_ms`**;若缺失则用 §6.3.3 本地公式(`open` 波特率须与 `uart_baud` 一致)。
**示例 — 成功汇报(步骤 6):**
```json
{"head":{"message_type":1002},"body":{"phase":"done","ok":1,"device_ID":"CN110200000001","version":"1.0.2","written":889072,"err":""}}
```
**示例 — 失败汇报(步骤 6,超时):**
```json
{"head":{"message_type":1002},"body":{"phase":"done","ok":0,"device_ID":"CN110200000001","version":"1.0.2","written":4096,"err":"timeout"}}
```
常见 `err``timeout` | `reset` | `disconnect` | `incomplete` | `overflow` | `write`(成功时 `err` 为空字符串 `""`)。
App 收到 `phase=active` 后:**不要等心跳**,UI 显示「升级中」;读线程以 **OT ACK 帧** 为主(§6.5)。
收到 `phase=done``ok=0` 后:**恢复等心跳**,可提示失败并允许用户重试(重新 `6→0→BEGIN`)。
### 6.3 OT 二进制帧(分块确认)
帧格式:`[4F 54][02][OP][SEQ×2][LEN×2][PAYLOAD…][CRC8][16]`
| OP | 方向 | 典型总长 | 说明 |
|----|------|----------|------|
| `0x01` BEGIN | 手机→设备 | **14** | PAYLOAD=4B 镜像大小(小端) |
| `0x02` DATA | 手机→设备 | **10+plen** | PAYLOAD=**1~1024B**(UART,`ota_chunk_max` 由固件下发;**用 `ota_chunk`**) |
| `0x03` END | 手机→设备 | **10** | PAYLOAD 空 |
| `0x80` ACK | 设备→手机 | BEGIN/END:**11** | 载荷仅 `[status]` |
| `0x80` ACK | 设备→手机 | DATA 1B:**12** | `[echo_byte][status]`(兼容旧版逐字节) |
| `0x80` ACK | 设备→手机 | DATA 多块:**14** | `[len_lo][len_hi][last_byte][status]` |
| `0x81` NAK | 设备→手机 | **11** | CRC/长度等错误 |
**分块参数(v1.0.2 起 · 波特率自适应):**
| 项 | 说明 |
|----|------|
| `ota_chunk` | **以设备 `1002` JSON 为准**;无则按 §6.3.3 用 `open` 波特率计算 |
| `ota_chunk_max` | **1024**(UART 单帧上限,menuconfig 可配 240~1024) |
| `ota_ack_timeout_ms` | **以设备 JSON 为准**;无则按 §6.3.3 计算 |
| `MAX_RETRIES_PER_CHUNK` | **3**(同 SEQ 重传) |
| 串口 `open` | **必须与 `uart_baud` 相同**,否则 CRC/速度均异常 |
**铁律:**
1. 必须先 `message_type=0`,再发 OT 帧。
2. 顺序:BEGIN 成功 → 若干 DATA 块 → END。
3. **每发一块须等设备 ACK**`status==0` 且长度/末字节校验通过)**再发下一块**;禁止在 ACK 前连发多块(日志里成片 `11 bytes` 为旧版逐字节或灌包过快)。
4. JSON 行与 OT 帧 **不要拼在同一行**`0` 后须 `\n`,再发二进制。
5. **仍须停等 ACK**(一块一确认);不能在未收 ACK 时流水线发下一块。
#### 6.3.1 固件按波特率推荐块大小与用时(`ota_chunk_max=1024`)
固件 `ota_chunk` 由 §6.3.2 公式算出;下表为 **分块停等 ACK** + USB 往返(**+15ms 典型** / **+30ms 保守**)。
**1MB:**
| `uart_baud` | `ota_chunk` | 典型吞吐 | **1MB 典型** | **1MB 保守** |
|-------------|-------------|----------|--------------|--------------|
| **115200** | **201** | ~5.7 KB/s | **~180s(3min)** | **~258s(4.3min)** |
| **230400** | **426** | ~12 KB/s | **~85s** | **~122s** |
| **460800** | **876** | ~25 KB/s | **~41s** | **~59s** |
| **921600** | **1024** | ~38 KB/s | **~27s** | **~42s** |
**6MB(分区上限):**
| `uart_baud` | `ota_chunk` | **6MB 典型** | **6MB 保守** |
|-------------|-------------|--------------|--------------|
| **115200** | 201 | **~18min** | **~26min** |
| **230400** | 426 | **~8.5min** | **~12min** |
| **460800** | 876 | **~4.1min(~248s)** | **~6min(~356s)** |
| **921600** | 1024 | **~2.7min(~162s)** | **~4.2min(~254s)** |
对比:
| 模式 | 6MB 约 |
|------|--------|
| 旧:逐字节 1B/帧 | **~17 小时** |
| 分块 max=240 @ 460800 | **典型 ~9min,保守 ~16min** |
| **分块 max=1024 @ 460800** | **典型 ~4min,保守 ~6min** |
> 再快需要 **改协议**(流水线多帧再批量 ACK / WiFi HTTP OTA),当前 UART 停等模型下 **460800+1024 块** 已接近 USB 线 practical 上限。
> 想压到 **6MB &lt;3min**:用 **921600** 波特率 + 确保 App 读线程够快(RTT≈15ms)。
#### 6.3.2 固件计算公式(与 `ota_uart_tune.c` 一致,App 可本地复算)
```text
wire_overhead = 24
ota_chunk_max = 1024 // menuconfig APP_OTA_UART_MAX_CHUNK
ota_chunk = clamp(baud / 512 - wire_overhead, 32, ota_chunk_max)
ota_ack_timeout_ms = clamp((ota_chunk + wire_overhead) * 10 * 1000 / baud * 3 + 800, 1200, 8000)
```
#### 6.3.3 App 分块发送示例(读 `1002` 参数)
```kotlin
object OtaUartTune {
private const val WIRE_OVERHEAD = 24
private const val CHUNK_MIN = 32
private const val CHUNK_MAX_DEFAULT = 1024
fun chunkForBaud(baud: Int, chunkMax: Int = CHUNK_MAX_DEFAULT): Int {
if (baud <= 0) return 128
return (baud / 512 - WIRE_OVERHEAD).coerceIn(CHUNK_MIN, chunkMax)
}
fun ackTimeoutMs(baud: Int, chunk: Int): Long {
val b = if (baud > 0) baud else 115200
val wireMs = (chunk + WIRE_OVERHEAD) * 10L * 1000L / b
return (wireMs * 3 + 800).coerceIn(1200, 8000)
}
fun fromQueryBody(body: JSONObject, openBaud: Int): Pair<Int, Long> {
val chunkMax = body.optInt("ota_chunk_max", CHUNK_MAX_DEFAULT)
val baud = body.optInt("uart_baud", openBaud)
val chunk = body.optInt("ota_chunk", 0).takeIf { it in CHUNK_MIN..chunkMax }
?: chunkForBaud(baud, chunkMax)
val ackMs = body.optLong("ota_ack_timeout_ms", 0L).takeIf { it > 0 }
?: ackTimeoutMs(baud, chunk)
return chunk to ackMs
}
}
const val MAX_RETRIES_PER_CHUNK = 3
data class DataAck(val chunkLen: Int, val lastByte: Byte, val status: Int)
/** DATA ACK:1B 载荷 → [echo][status];多块 → [len_lo][len_hi][last][status] */
fun OtaFrame.parseDataAck(frame: ByteArray): DataAck? {
if (frame.size < 10 || frame[3] != OP_ACK) return null
val plen = (frame[6].toInt() and 0xFF) or ((frame[7].toInt() and 0xFF) shl 8)
return when (plen) {
2 -> DataAck(1, frame[8], frame[9].toInt() and 0xFF)
4 -> DataAck(
(frame[8].toInt() and 0xFF) or ((frame[9].toInt() and 0xFF) shl 8),
frame[10], frame[11].toInt() and 0xFF
)
else -> null
}
}
fun buildDataChunk(seq: Int, chunk: ByteArray) = build(OP_DATA, seq, chunk)
fun uartOtaWithChunks(
port: UsbSerialPort,
firmware: ByteArray,
chunkSize: Int, // 来自 1002.body.ota_chunk
ackTimeoutMs: Long // 来自 1002.body.ota_ack_timeout_ms
) {
var seq = 0
port.write(OtaFrame.buildBegin(firmware.size), 3000)
require(waitStatusAck(seq, ackTimeoutMs) == 0)
seq = 1
var offset = 0
while (offset < firmware.size) {
val n = minOf(chunkSize, firmware.size - offset)
val chunk = firmware.copyOfRange(offset, offset + n)
var ok = false
repeat(MAX_RETRIES_PER_CHUNK) {
port.write(OtaFrame.buildDataChunk(seq, chunk), 5000)
val ack = waitAckFrame(seq, ackTimeoutMs) ?: return@repeat
val r = OtaFrame.parseDataAck(ack) ?: return@repeat
if (r.status == 0 && r.chunkLen == n && r.lastByte == chunk.last()) {
ok = true
return@repeat
}
}
check(ok) { "DATA chunk failed at offset=$offset seq=$seq" }
offset += n
seq++
}
port.write(OtaFrame.buildEnd(seq), 3000)
require(waitStatusAck(seq, ackTimeoutMs) == 0)
}
```
**`UartOtaManager.kt` 接入要点:**
1. `UsbSerialPort` **open 波特率 = `query.body.uart_baud`**(如 **460800**)。
2. 收到 `1002/query` 后:`val (chunk, ackMs) = OtaUartTune.fromQueryBody(body, openBaud)`
3. 收到 `1002/active` 后开始 OTA,用 `chunk` / `ackMs` 调用 `uartOtaWithChunks`
4.`openBaud != body.uart_baud`**拒绝 OTA** 并提示波特率不一致。
> `BleOtaManager.kt`:无 `uart_baud`,用 `ota_chunk`(缺省 128)与 `ota_ack_timeout_ms`。
### 6.4 UART 与 BLE 通道对照
| 项目 | BLE | UART |
|------|-----|------|
| 握手 / `0` | Write **0xFFE1** | JSON 行 + `\n` |
| `1002` JSON | Notify **0xFFE3** | UART1 文本行 + `\n` |
| 固件 OT 帧 | Write **0xFFE2** | UART1 二进制写 |
| ACK/NAK | Notify **0xFFE4** | UART1 读回(与 JSON 同线) |
| OTA 期间心跳 | **暂停**(改 `1002/active`) | **暂停**(改 `1002/active`) |
| OTA 期间告警 | Notify **0xFFE3** 仍可能推 `4`/`5` | 同 UART1 JSON 行 |
| 失败后心跳 | **恢复**`1002/done` 之后) | **恢复** |
### 6.5 UART RX 超时(固件侧,与 App 对齐)
| 阶段 | 无 RX 超时 | 设备行为 |
|------|------------|----------|
| 正常遥控 | **1.2s** | 停停车推送;非 OTA 会话 |
| 已发 `0`、等待 BEGIN | **15s** | 允许 App 在 `0` 后等待 `active`/组包再发 BEGIN |
| OT 传输中(BEGIN~END) | **`ota_ack_timeout_ms + 800ms`**(按波特率,约 1.2~6s) | 中止 OTA → `done(ok=0,err=timeout)` |
> App **`ota_ack_timeout_ms` 须小于固件传输无 RX 超时**(设备侧 = App 值 + 约 800ms)。`0` 后发 BEGIN 前走 **15s** 武装超时。
### 6.6 UART 读线程(OTA 阶段)
手机 `read()` 下行字节流:
- **OTA 进行中**:以 **OT ACK/NAK**(魔数 `4F 54`)为主;**不要**把整个缓冲当 UTF-8 解析。
- **非 OTA**:按 `\n` 组 JSON 行(§5)。
- 一次 `read()` 可能只含半帧;须字节缓冲组满再校验 CRC。
- `1002/done` 仍为 JSON 行,在 OT 帧解析器之外按行处理。
### 6.7 BEGIN ACK 超时与设备重启(联调实录)
手机报 **BEGIN ACK 超时** 时,若设备 monitor 出现:
```text
OTA_FRAME: RX BEGIN seq=0 plen=4
OTA_STREAM: OTA begin: size=... bytes
OTA_STREAM: OTA partition: ota_1 at ...
***ERROR*** A stack overflow in task uart_evt_rx has been detected.
```
说明 **BEGIN 已收到**,但在 `esp_ota_begin` 阶段 **UART RX 任务栈溢出重启**,ACK 来不及发出。
| 侧 | 处理 |
|----|------|
| **固件(必须)** | UART RX 经**指针队列**`uart_rx_work`(≥12KB 栈)处理;禁止在 `uart_evt_rx` 栈上放大块缓冲或同步 `esp_ota_begin`;队列满会导致 CRC NAK |
| **App(建议)** | BEGIN ACK 超时时若串口已断开,提示「设备异常重启,请更新固件」而非仅提示握手问题 |
修复后正常日志应含:`OTA_FRAME: TX ACK seq=0 status=0`
### 6.8 联调检查(OTA · 固件侧)
- [ ] 每次 open 先发 `6`,收到 `1002/query` 再显示已连接
- [ ] 版本更高才发 `0`;收到 `1002/active` 后 UI 切升级态、**不等心跳**
- [ ] 串口波特率与 `query.uart_baud` 一致(如 **460800**
- [ ] BEGIN **14B** → 等 ACK → 再发 DATA **分块(`ota_chunk`)**
- [ ] 每 DATA 块等 ACK:`status==0``chunkLen``last_byte` 与发送块一致
- [ ] 失败收 `1002/done(ok=0)`,之后应再收到周期心跳
- [ ] 成功收 `1002/done(ok=1)`,约 1s 重启,重连后 `query.body.version` 更新
---
## 7. 联调检查清单(App + 固件)
- [ ] UART / BLE / MQTT 控制 JSON **格式一致**
- [ ] **铁律**:行走相关(控制/松手/全停)**只有 `pwm_ctrl`,无 `pin_setctrl`**(含 MQTT)
- [ ] 全停 3 条仅 `pwm_ctrl`,不发带 pin 的包
- [ ] GPIO 用 `buildPinJson` **单独**发(MQTT 同规则)
- [ ] 同包融合**仅** §3.4 主动控制
- [ ] 前后/左右互斥;控制 100~200ms,≤500ms
- [ ] UART 行尾 `\n`;串口按行组包
- [ ] 握手 `6→1002/query`;控制不等 ACK
- [ ] OTA:`0` 后收 `1002/active`;分块 DATA、每块等 ACK;失败收 `done(ok=0)` 后恢复心跳(§6)
---
## 8. App 修改清单(配合固件 v1.0.2 · 波特率自适应分块 OTA)
> **不改成 §6.3 分块时仍可逐字节 OTA(极慢)**。实用速度须:**刷新版固件 + 按下列改 App + 串口波特率与固件一致(如 460800)**。
### 8.1 必须修改(P0)
| # | 文件 | 改什么 |
|---|------|--------|
| 1 | `UartRobotManager.kt`(或串口配置处) | `UsbSerialPort` **open 波特率 = 460800**(或与固件 menuconfig 一致);**不要再用 115200 除非固件也是 115200** |
| 2 | `OtaUartTune.kt`(新建) | 实现 §6.3.2 `OtaUartTune``chunkForBaud` / `ackTimeoutMs` / `fromQueryBody` |
| 3 | `OtaFrame.kt` | `buildDataChunk(seq, ByteArray)``parseDataAck` 支持 2B / 4B ACK |
| 4 | `UartOtaManager.kt` | 解析 `1002/query``uart_baud``ota_chunk``ota_ack_timeout_ms`**校验 open 波特率一致**;分块循环(§6.3.3);**收到 ACK 再发下一块** |
| 5 | `UartOtaManager.kt` | 未收到 `1002/active` → FAILED;BEGIN ACK 失败 → FAILED |
| 6 | `UartRobotManager.kt` | OTA 读流:组 OT 帧 + JSON 行(`4`/`5`/`1002`) |
| 7 | `BleOtaManager.kt` | 分块 OTA;`ota_chunk` / `ota_ack_timeout_ms``1002` 读取(无 `uart_baud`) |
### 8.2 建议优化(P1)
| # | 文件 | 建议 |
|---|------|------|
| 8 | `UartOtaManager.kt` | `MAX_RETRIES_PER_CHUNK = 3`;进度 `offset/size` |
| 9 | `UartDebugViewModel.kt` | 联调页显示 `uart_baud` / `ota_chunk`;失败展示 `done.err` |
| 10 | `OtaProtocol.kt` | 常量旁注释:块大小以 `1002` 为准,勿写死 |
| 11 | `OTA对比文档.md` | 同步 §6.2.1、§6.3.1 波特率表、§6.3.2 公式 |
### 8.3 固件 v1.0.2 已实现(App 可依赖)
- `6``1002/query`(含 `uart_baud``ota_chunk``ota_chunk_max``ota_ack_timeout_ms`
- `0``1002/active`(含同套 OTA 参数),暂停心跳
- OT DATA **1~1024B/帧**`APP_OTA_UART_MAX_CHUNK`,默认 1024)
- 460800 时 `ota_chunk` 通常为 **876**;921600 → **1024**
- DATA 多块 ACK:`[len_lo][len_hi][last][status]`
- OTA 传输无 RX 超时 = `ota_ack_timeout_ms + 800ms`(随波特率)
- 成功 `done(ok=1)` 后约 1s 重启;失败恢复心跳
- Release:`ESP_LOGW`/`E``message_type=4/5`
### 8.4 参考代码位置(App)
| 模块 | 路径 |
|------|------|
| OTA 状态机 | `com/fcrs2025/arflycar/uart/UartOtaManager.kt` |
| 波特率/块大小 | **新建** `com/fcrs2025/arflycar/ota/OtaUartTune.kt`(§6.3.2) |
| 串口 open 波特率 | `com/fcrs2025/arflycar/uart/UartRobotManager.kt`(须与 `uart_baud` 一致) |
| OT 帧 | `com/fcrs2025/arflycar/ble/OtaFrame.kt` |
| 协议版本 | `com/fcrs2025/arflycar/ota/OtaProtocol.kt``DOC_VERSION = "1.0.2"`) |
---
## 附录 A · 日志
| 场景 | 关键字 |
|------|--------|
| 行走 | `PWM fused:` |
| GPIO | `GPIO: pin N ->` |
| 全停 | `停车 JSON` |
| 超时 | `RC timeout` |
| OTA 识别 | `1002/active` |
| OTA ACK | `UART1 TX OTA ACK` |
| OTA 失败 | `1002/done` + `ok:0`;或 `message_type=5` + `msg` |
| 告警推手机 | `message_type=4` + `body.msg` |
---
如与 `v1.0.1` 冲突,以本文 `v1.0.2` 为准。
...@@ -3,13 +3,13 @@ ESPRCCar firmware release ...@@ -3,13 +3,13 @@ ESPRCCar firmware release
version: 1.0.2 version: 1.0.2
project: ESPRCCar project: ESPRCCar
chip: esp32s3 chip: esp32s3
built_utc: 2026-06-06T06:42:37Z built_utc: 2026-06-11T08:29:54Z
built_local: 2026/06/06 14:42:37.77 built_local: 2026/06/11 16:29:54.96
OTA: OTA:
path: firmware/release/ota/ESPRCCar.bin path: firmware/release/ota/ESPRCCar.bin
bytes: 889072 bytes: 889552
sha256: 9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef sha256: 065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977
Factory: firmware/release/factory/ Factory: firmware/release/factory/
tool: Espressif Flash Download Tools tool: Espressif Flash Download Tools
......
...@@ -5,14 +5,14 @@ ...@@ -5,14 +5,14 @@
"flash_freq": "80m", "flash_freq": "80m",
"flash_size": "16MB", "flash_size": "16MB",
"version": "1.0.2", "version": "1.0.2",
"generated_utc": "2026-06-06T06:42:37Z", "generated_utc": "2026-06-11T08:29:54Z",
"ota_image": "release/ota/ESPRCCar.bin", "ota_image": "release/ota/ESPRCCar.bin",
"factory_dir": "release/factory", "factory_dir": "release/factory",
"files": [ "files": [
{"name":"ESPRCCar.bin","bytes":889072,"sha256":"9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef"}, {"name":"ESPRCCar.bin","bytes":889552,"sha256":"065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"},
{"name":"bootloader.bin","bytes":13808,"sha256":"1bfe318a61972c3f601272c318091b06e62d0076360433f5e608c03064c639b2"}, {"name":"bootloader.bin","bytes":13808,"sha256":"21a5becf42c59120c3c4322a04ca684f86a496dfb770575ebbe6d6ec739b7199"},
{"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"}, {"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"},
{"name":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"}, {"name":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"},
{"name":"storage.bin","bytes":327680,"sha256":"dc9e166779acf5a7a960cee93e4d778646e0fb7b110a4e1dbc9ea04031da361c"} {"name":"storage.bin","bytes":327680,"sha256":"6d2f4c246d34ebf80e3d32ac4873aa84d29f68ee799214b123bdd998a8b2d3b2"}
] ]
} }
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
"project": "ESPRCCar", "project": "ESPRCCar",
"chip": "esp32s3", "chip": "esp32s3",
"version": "1.0.2", "version": "1.0.2",
"generated_utc": "2026-06-06T06:42:37Z", "generated_utc": "2026-06-11T08:29:54Z",
"image": "ESPRCCar.bin", "image": "ESPRCCar.bin",
"bytes": 889072, "bytes": 889552,
"sha256": "9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef" "sha256": "065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"
} }
...@@ -7,11 +7,12 @@ set(SOURCES ...@@ -7,11 +7,12 @@ set(SOURCES
"protocol/heart_payload.c" "protocol/heart_payload.c"
"protocol/ota_offer_protocol.c" "protocol/ota_offer_protocol.c"
"protocol/ota_frame_protocol.c" "protocol/ota_frame_protocol.c"
"drivers/driver_manager.c" "protocol/ota_uart_tune.c"
"drivers/driver_manager/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c" "drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/devices/device_1201.c" "drivers/gpiotrol/devices/1101/device_1101.c"
"drivers/gpiotrol/devices/device_1101.c" "drivers/gpiotrol/devices/1102/device_1102.c"
"drivers/gpiotrol/devices/device_1102.c" "drivers/gpiotrol/devices/1102/device_1102_brake.c"
"drivers/gpiotrol/betteryread.c" "drivers/gpiotrol/betteryread.c"
"drivers/uart_comm/uart_comm.c" "drivers/uart_comm/uart_comm.c"
"app/app_run.c" "app/app_run.c"
...@@ -37,8 +38,11 @@ set(INCLUDE_DIRS ...@@ -37,8 +38,11 @@ set(INCLUDE_DIRS
"protocol" "protocol"
"app" "app"
"drivers" "drivers"
"drivers/driver_manager"
"drivers/gpiotrol" "drivers/gpiotrol"
"drivers/gpiotrol/devices" "drivers/gpiotrol/devices"
"drivers/gpiotrol/devices/1101"
"drivers/gpiotrol/devices/1102"
"drivers/uart_comm") "drivers/uart_comm")
if(CONFIG_APP_LINK_BLE) if(CONFIG_APP_LINK_BLE)
...@@ -77,10 +81,4 @@ idf_component_register(SRCS ${SOURCES} ...@@ -77,10 +81,4 @@ idf_component_register(SRCS ${SOURCES}
REQUIRES ${MAIN_REQUIRES} REQUIRES ${MAIN_REQUIRES}
PRIV_REQUIRES ${MAIN_PRIV_REQUIRES}) PRIV_REQUIRES ${MAIN_PRIV_REQUIRES})
# menuconfig 仅勾选 ROBO_APP_FW_RELEASE 时,LOG/控制台 select 可能未写入 sdkconfig;
# 在 main 组件编译期强制 WARN,去掉 ESP_LOGI/LOGD(与 sdkconfig.defaults.release 一致)。
if(CONFIG_ROBO_APP_FW_RELEASE)
target_compile_definitions(${COMPONENT_LIB} PRIVATE LOG_LOCAL_LEVEL=ESP_LOG_WARN)
endif()
spiffs_create_partition_image(storage ../www FLASH_IN_PROJECT) spiffs_create_partition_image(storage ../www FLASH_IN_PROJECT)
...@@ -82,6 +82,15 @@ config APP_UART_LINK_BAUDRATE ...@@ -82,6 +82,15 @@ config APP_UART_LINK_BAUDRATE
help help
当前 UART 链路模式生效波特率(由上方档位自动生成)。 当前 UART 链路模式生效波特率(由上方档位自动生成)。
config APP_OTA_UART_MAX_CHUNK
int "OTA DATA 单帧最大载荷 (字节)"
depends on APP_LINK_UART
default 1024
range 240 1024
help
UART OTA 每帧最多携带的固件字节数。越大往返越少、越快;
须与 Android OtaFrame 发送缓冲一致。推荐 1024(460800 下 6MB 约 1~3 分钟)。
config APP_UART_LINK_TX_GPIO config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号" int "UART 链路 TX GPIO 编号"
default 17 default 17
...@@ -165,9 +174,10 @@ endchoice ...@@ -165,9 +174,10 @@ endchoice
choice APP_PWM_IO15_ROLE choice APP_PWM_IO15_ROLE
prompt "GPIO15 PWM 角色" prompt "GPIO15 PWM 角色"
default APP_PWM_IO15_SERVO default APP_PWM_IO15_ESC
help help
选择 GPIO15 在 50Hz PWM 下的用途。 选择 GPIO15 在 50Hz PWM 下的用途。
默认:电调 ESC(1101 外接油门 / 备用电调口)。
config APP_PWM_IO15_SERVO config APP_PWM_IO15_SERVO
bool "舵机(初始化 90 度 / 1500us)" bool "舵机(初始化 90 度 / 1500us)"
...@@ -178,9 +188,10 @@ endchoice ...@@ -178,9 +188,10 @@ endchoice
choice APP_PWM_IO16_ROLE choice APP_PWM_IO16_ROLE
prompt "GPIO16 PWM 角色" prompt "GPIO16 PWM 角色"
default APP_PWM_IO16_ESC default APP_PWM_IO16_SERVO
help help
选择 GPIO16 在 50Hz PWM 下的用途。 选择 GPIO16 在 50Hz PWM 下的用途。
默认:舵机(1102 转向固定使用 IO16,须为 SERVO)。
config APP_PWM_IO16_SERVO config APP_PWM_IO16_SERVO
bool "舵机(初始化 90 度 / 1500us)" bool "舵机(初始化 90 度 / 1500us)"
......
...@@ -72,6 +72,12 @@ static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = { ...@@ -72,6 +72,12 @@ static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = {
.priority = 5, .priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY, .core_id = APP_TASK_CORE_NO_AFFINITY,
}, },
[APP_TASK_DEV1102_BRAKE] = {
.name = "1102_brk",
.stack_depth = 3072,
.priority = 6,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
}; };
static esp_err_t app_task_create(const char *name, static esp_err_t app_task_create(const char *name,
......
...@@ -17,6 +17,7 @@ typedef enum { ...@@ -17,6 +17,7 @@ typedef enum {
APP_TASK_MQTT_INIT, APP_TASK_MQTT_INIT,
APP_TASK_UART_COMM_EXAMPLE, APP_TASK_UART_COMM_EXAMPLE,
APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */ APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */
APP_TASK_DEV1102_BRAKE, /* 1102 板载驱动 PID 制动 */
APP_TASK_COUNT, APP_TASK_COUNT,
} app_task_id_t; } app_task_id_t;
......
...@@ -13,10 +13,6 @@ device_model_t device_model_from_full_id(const char *device_id) ...@@ -13,10 +13,6 @@ device_model_t device_model_from_full_id(const char *device_id)
size_t len = strlen(device_id); size_t len = strlen(device_id);
if (len == DEVICE_MODEL_SLICE_CHAR_LEN) { if (len == DEVICE_MODEL_SLICE_CHAR_LEN) {
if (strncmp(device_id, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1201");
return DEVICE_MODEL_1201;
}
if (strncmp(device_id, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) { if (strncmp(device_id, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1101"); ESP_LOGI(tag_s, "短型号 1101");
return DEVICE_MODEL_1101; return DEVICE_MODEL_1101;
...@@ -34,10 +30,6 @@ device_model_t device_model_from_full_id(const char *device_id) ...@@ -34,10 +30,6 @@ device_model_t device_model_from_full_id(const char *device_id)
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START; 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) { if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101"); ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101");
return DEVICE_MODEL_1101; return DEVICE_MODEL_1101;
......
...@@ -5,12 +5,11 @@ ...@@ -5,12 +5,11 @@
/** /**
* 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。 * 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。
* 规则:取 device_id 第 3~第 6 个字符(从第 1 个字符起算,共 4 字符), * 规则:完整设备号如 CN110200000001(无 app2dev/ 前缀);
* 即 C 字符串下标 [2..5],与 "1201" / "1101" 比较 * 取第 3~第 6 位(1 起算)共 4 字符 → C 下标 [2..5],如 "1102"
*/ */
typedef enum { typedef enum {
DEVICE_MODEL_1201 = 0, DEVICE_MODEL_1101 = 0,
DEVICE_MODEL_1101,
DEVICE_MODEL_1102, DEVICE_MODEL_1102,
} device_model_t; } device_model_t;
......
#ifndef DEVICE_NVS_H #ifndef DEVICE_NVS_H
#define DEVICE_NVS_H #define DEVICE_NVS_H
#include <string.h>
/** NVS 命名空间(与网页配网写入一致) */ /** NVS 命名空间(与网页配网写入一致) */
#define DEVICE_CFG_NVS_NAMESPACE "storage" #define DEVICE_CFG_NVS_NAMESPACE "storage"
...@@ -9,4 +11,13 @@ ...@@ -9,4 +11,13 @@
#define DEVICE_CFG_KEY_DEVICE_ID "device_id" #define DEVICE_CFG_KEY_DEVICE_ID "device_id"
#define DEVICE_CFG_KEY_BLE_ADV_NAME "ble_adv_name" #define DEVICE_CFG_KEY_BLE_ADV_NAME "ble_adv_name"
/** 去掉历史 MQTT 前缀,输出纯设备号(如 CN110200000001) */
static inline const char *device_id_plain(const char *device_id)
{
if (device_id != NULL && strncmp(device_id, "app2dev/", 8) == 0) {
return device_id + 8;
}
return (device_id != NULL) ? device_id : "";
}
#endif #endif
#include "device_1101.h"
#include "rc_pwm_control.h"
/*
* 1101: throttle on IO15 (ESC); steering on IO16 (SERVO, via rc_pwm_set_steering_angle_deg).
* No onboard driver IC (IO10/21/11/12 unused). Default menuconfig: IO15=ESC, IO16=SERVO.
*/
#define DEV1101_VAL_MAX 200
#define DEV1101_DRIVE_START_TH 50
#define DEV1101_ESC_NEUTRAL_PCT 50U /* 1500us 中位 */
#define DEV1101_STEER_VAL_LO 45
#define DEV1101_STEER_VAL_HI 70
#define DEV1101_STEER_VAL_SPAN (69 - DEV1101_STEER_VAL_LO)
#define DEV1101_STEER_CENTER_ANG 90
#define DEV1101_STEER_DELTA_ANG 30
#define DEV1101_STEER_START_DELTA 12
#define DEV1101_STEER_LEFT_MAX (DEV1101_STEER_CENTER_ANG + DEV1101_STEER_DELTA_ANG)
#define DEV1101_STEER_RIGHT_MAX (DEV1101_STEER_CENTER_ANG - DEV1101_STEER_DELTA_ANG)
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 esc_forward_percent_from_val(int val)
{
int v = clamp_int(val, 0, DEV1101_VAL_MAX);
if (v <= DEV1101_DRIVE_START_TH) {
return DEV1101_ESC_NEUTRAL_PCT;
}
/* 50~200 → 中位~满前进(50%~100% → 1500~2000us) */
return 50U + (uint32_t)(v - DEV1101_DRIVE_START_TH) * 50U /
(uint32_t)(DEV1101_VAL_MAX - DEV1101_DRIVE_START_TH);
}
static uint32_t esc_backward_percent_from_val(int val)
{
int v = clamp_int(val, 0, DEV1101_VAL_MAX);
if (v <= DEV1101_DRIVE_START_TH) {
return DEV1101_ESC_NEUTRAL_PCT;
}
/* 50~200 → 中位~满后退(50%~0% → 1500~1000us) */
return 50U - (uint32_t)(v - DEV1101_DRIVE_START_TH) * 50U /
(uint32_t)(DEV1101_VAL_MAX - DEV1101_DRIVE_START_TH);
}
static void device_1101_stop(void)
{
rc_pwm_set_esc_neutral();
rc_pwm_set_steering_angle_deg(DEV1101_STEER_CENTER_ANG);
}
static void device_1101_drive_forward(int speed_val)
{
rc_pwm_set_dual_esc_percent(esc_forward_percent_from_val(speed_val));
}
static void device_1101_drive_backward(int speed_val)
{
rc_pwm_set_dual_esc_percent(esc_backward_percent_from_val(speed_val));
}
static void device_1101_steer_left(int speed_val)
{
if (speed_val < DEV1101_STEER_VAL_LO) {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_CENTER_ANG);
} else if (speed_val < DEV1101_STEER_VAL_HI) {
uint32_t ang = (uint32_t)(DEV1101_STEER_CENTER_ANG + DEV1101_STEER_START_DELTA +
(uint32_t)(speed_val - DEV1101_STEER_VAL_LO) *
(DEV1101_STEER_DELTA_ANG - DEV1101_STEER_START_DELTA) /
(uint32_t)DEV1101_STEER_VAL_SPAN);
rc_pwm_set_steering_angle_deg(ang);
} else {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_LEFT_MAX);
}
}
static void device_1101_steer_right(int speed_val)
{
if (speed_val < DEV1101_STEER_VAL_LO) {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_CENTER_ANG);
} else if (speed_val < DEV1101_STEER_VAL_HI) {
uint32_t ang = (uint32_t)(DEV1101_STEER_CENTER_ANG - DEV1101_STEER_START_DELTA -
(uint32_t)(speed_val - DEV1101_STEER_VAL_LO) *
(DEV1101_STEER_DELTA_ANG - DEV1101_STEER_START_DELTA) /
(uint32_t)DEV1101_STEER_VAL_SPAN);
rc_pwm_set_steering_angle_deg(ang);
} else {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_RIGHT_MAX);
}
}
static void device_1101_control(int mode, int speed_val, int steer_val)
{
switch (mode) {
case 1:
device_1101_drive_forward(speed_val);
break;
case 2:
device_1101_drive_backward(speed_val);
break;
case 3:
device_1101_steer_left(speed_val);
break;
case 4:
device_1101_steer_right(speed_val);
break;
case 5:
device_1101_drive_forward(speed_val);
device_1101_steer_left(steer_val);
break;
case 6:
device_1101_drive_forward(speed_val);
device_1101_steer_right(steer_val);
break;
case 7:
device_1101_drive_backward(speed_val);
device_1101_steer_left(steer_val);
break;
case 8:
device_1101_drive_backward(speed_val);
device_1101_steer_right(steer_val);
break;
default:
break;
}
}
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;
}
#include "device_1102.h" #include "device_1102.h"
#include "device_1102_brake.h"
#include "rc_pwm_control.h" #include "rc_pwm_control.h"
#define DEV1102_VAL_MAX 200 #define DEV1102_VAL_MAX 200
#define DEV1102_DRIVE_START_TH 50 #define DEV1102_DRIVE_STOP_TH 50
#define DEV1102_DRIVE_VAL_MIN 51
#define DEV1102_DRIVE_PWM_MIN 28U
#define DEV1102_DRIVE_PWM_MAX 100U
#define DEV1102_DRIVE_VAL_SPAN (DEV1102_VAL_MAX - DEV1102_DRIVE_VAL_MIN)
/* 转向:App 侧 val 在 [47,62] 线性对应角度;中位 90°,左右各 ±30° */ /* 转向:App 侧 val 在 [47,62] 线性对应角度;中位 90°,左右各 ±30° */
#define DEV1102_STEER_VAL_LO 47 #define DEV1102_STEER_VAL_LO 47
#define DEV1102_STEER_VAL_HI 62 #define DEV1102_STEER_VAL_HI 62
...@@ -12,6 +17,9 @@ ...@@ -12,6 +17,9 @@
#define DEV1102_STEER_RIGHT_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) #define DEV1102_STEER_VAL_SPAN (DEV1102_STEER_VAL_HI - DEV1102_STEER_VAL_LO)
static uint32_t s_last_drive_pct;
static bool s_brake_inited;
static int clamp_int(int v, int lo, int hi) static int clamp_int(int v, int lo, int hi)
{ {
if (v < lo) { if (v < lo) {
...@@ -23,15 +31,34 @@ static int clamp_int(int v, int lo, int hi) ...@@ -23,15 +31,34 @@ static int clamp_int(int v, int lo, int hi)
return v; return v;
} }
static void device_1102_ensure_brake_task(void)
{
if (!s_brake_inited) {
if (device_1102_brake_init() == ESP_OK) {
s_brake_inited = true;
}
}
}
static uint32_t drive_percent_from_val_200(int val) static uint32_t drive_percent_from_val_200(int val)
{ {
int v = clamp_int(val, 0, DEV1102_VAL_MAX); int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_DRIVE_START_TH) { if (v <= DEV1102_DRIVE_STOP_TH) {
return 0; return 0;
} }
/* 50~200 线性映射到 0~100% */ if (v >= DEV1102_VAL_MAX) {
return (uint32_t)(((v - DEV1102_DRIVE_START_TH) * 100) / return DEV1102_DRIVE_PWM_MAX;
(DEV1102_VAL_MAX - DEV1102_DRIVE_START_TH)); }
/* 51~200 线性映射到 28%~100% 驱动占空比 */
return DEV1102_DRIVE_PWM_MIN +
(uint32_t)(v - DEV1102_DRIVE_VAL_MIN) * (DEV1102_DRIVE_PWM_MAX - DEV1102_DRIVE_PWM_MIN) /
(uint32_t)DEV1102_DRIVE_VAL_SPAN;
}
static void device_1102_request_brake(void)
{
device_1102_ensure_brake_task();
device_1102_brake_request(s_last_drive_pct);
} }
static uint32_t steer_left_angle_from_val(int val) static uint32_t steer_left_angle_from_val(int val)
...@@ -64,20 +91,32 @@ static uint32_t steer_right_angle_from_val(int val) ...@@ -64,20 +91,32 @@ static uint32_t steer_right_angle_from_val(int val)
static void device_1102_stop(void) static void device_1102_stop(void)
{ {
/* 1102: 10 前进,21 后退;全部拉低并将 IO16 回中 */ device_1102_request_brake();
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); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
} }
static void device_1102_drive_forward(uint32_t pct) static void device_1102_drive_forward(uint32_t pct)
{ {
if (pct == 0U) {
device_1102_request_brake();
return;
}
device_1102_ensure_brake_task();
device_1102_brake_abort();
s_last_drive_pct = pct;
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct); rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0); rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
} }
static void device_1102_drive_backward(uint32_t pct) static void device_1102_drive_backward(uint32_t pct)
{ {
if (pct == 0U) {
device_1102_request_brake();
return;
}
device_1102_ensure_brake_task();
device_1102_brake_abort();
s_last_drive_pct = pct;
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0); rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct); rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct);
} }
...@@ -87,46 +126,40 @@ static void device_1102_control(int mode, int speed_val, int steer_val) ...@@ -87,46 +126,40 @@ static void device_1102_control(int mode, int speed_val, int steer_val)
int speed = clamp_int(speed_val, 0, DEV1102_VAL_MAX); int speed = clamp_int(speed_val, 0, DEV1102_VAL_MAX);
int steer = clamp_int(steer_val, 0, DEV1102_VAL_MAX); int steer = clamp_int(steer_val, 0, DEV1102_VAL_MAX);
switch (mode) { switch (mode) {
case 1: { /* 前进:IO10,转向回中 */ case 1: {
uint32_t pct = drive_percent_from_val_200(speed); uint32_t pct = drive_percent_from_val_200(speed);
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
device_1102_drive_forward(pct); device_1102_drive_forward(pct);
break; break;
} }
case 2: { /* 后退:IO21,转向回中 */ case 2: {
uint32_t pct = drive_percent_from_val_200(speed); uint32_t pct = drive_percent_from_val_200(speed);
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
device_1102_drive_backward(pct); device_1102_drive_backward(pct);
break; break;
} }
case 3: { /* 左转:IO16 舵机 */ case 3:
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(speed)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(speed));
break; break;
} case 4:
case 4: { /* 右转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(speed)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(speed));
break; break;
} case 5:
case 5: { /* 前左:前进 + 左转 */
device_1102_drive_forward(drive_percent_from_val_200(speed)); device_1102_drive_forward(drive_percent_from_val_200(speed));
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(steer)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(steer));
break; break;
} case 6:
case 6: { /* 前右:前进 + 右转 */
device_1102_drive_forward(drive_percent_from_val_200(speed)); device_1102_drive_forward(drive_percent_from_val_200(speed));
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(steer)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(steer));
break; break;
} case 7:
case 7: { /* 后左:后退 + 左转 */
device_1102_drive_backward(drive_percent_from_val_200(speed)); device_1102_drive_backward(drive_percent_from_val_200(speed));
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(steer)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(steer));
break; break;
} case 8:
case 8: { /* 后右:后退 + 右转 */
device_1102_drive_backward(drive_percent_from_val_200(speed)); device_1102_drive_backward(drive_percent_from_val_200(speed));
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(steer)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(steer));
break; break;
}
default: default:
break; break;
} }
...@@ -147,5 +180,6 @@ static const device_drive_ops_t ops_s = { ...@@ -147,5 +180,6 @@ static const device_drive_ops_t ops_s = {
const device_drive_ops_t *device_1102_get_ops(void) const device_drive_ops_t *device_1102_get_ops(void)
{ {
device_1102_ensure_brake_task();
return &ops_s; return &ops_s;
} }
#include "device_1102_brake.h"
#include "core/task_manager.h"
#include "rc_pwm_control.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <stdbool.h>
static const char *tag_s = "DEV1102_BRK";
#define BRAKE_QUEUE_LEN 1
#define BRAKE_TICK_MS 6U
#define BRAKE_STOP_SPEED_TH 3.0f
#define BRAKE_DRIVE_PWM_MIN 28U
#define BRAKE_SPEED_LOW_TH 45U
#define BRAKE_SPEED_HIGH_TH 70U
typedef struct {
float est_speed;
float integral;
float prev_error;
} dev1102_brake_pid_t;
typedef struct {
uint32_t kick_duty_min;
uint32_t kick_duty_max;
uint32_t kick_ms_min;
uint32_t kick_ms_max;
float duty_max;
float duty_floor_ratio;
float pid_kp;
float pid_ki;
float pid_kd;
uint32_t max_loop_ms;
} dev1102_brake_profile_t;
static QueueHandle_t s_brake_q;
static TaskHandle_t s_brake_task;
static volatile bool s_abort;
static void device_1102_brake_both(uint32_t duty_pct)
{
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, duty_pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, duty_pct);
}
static float dev1102_clampf(float v, float lo, float hi)
{
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static void dev1102_brake_profile(uint32_t start_pct, dev1102_brake_profile_t *out)
{
if (start_pct <= BRAKE_SPEED_LOW_TH) {
/* 低速:强抱闸、短周期,尽快停住 */
*out = (dev1102_brake_profile_t){
.kick_duty_min = 72U,
.kick_duty_max = 88U,
.kick_ms_min = 35U,
.kick_ms_max = 50U,
.duty_max = 85.0f,
.duty_floor_ratio = 0.72f,
.pid_kp = 1.05f,
.pid_ki = 0.22f,
.pid_kd = 0.12f,
.max_loop_ms = 220U,
};
return;
}
if (start_pct <= BRAKE_SPEED_HIGH_TH) {
*out = (dev1102_brake_profile_t){
.kick_duty_min = 62U,
.kick_duty_max = 78U,
.kick_ms_min = 45U,
.kick_ms_max = 65U,
.duty_max = 78.0f,
.duty_floor_ratio = 0.62f,
.pid_kp = 0.92f,
.pid_ki = 0.18f,
.pid_kd = 0.10f,
.max_loop_ms = 320U,
};
return;
}
/* 高速:抱闸略柔和但 PID 持续施压,避免长距离滑行 */
*out = (dev1102_brake_profile_t){
.kick_duty_min = 58U,
.kick_duty_max = 76U,
.kick_ms_min = 60U,
.kick_ms_max = 95U,
.duty_max = 72.0f,
.duty_floor_ratio = 0.50f,
.pid_kp = 0.78f,
.pid_ki = 0.14f,
.pid_kd = 0.08f,
.max_loop_ms = 450U,
};
}
static uint32_t dev1102_brake_kick_duty(uint32_t start_pct, const dev1102_brake_profile_t *prof)
{
if (start_pct == 0U) {
return 55U;
}
if (start_pct <= BRAKE_DRIVE_PWM_MIN) {
return prof->kick_duty_min;
}
return prof->kick_duty_min +
(start_pct - BRAKE_DRIVE_PWM_MIN) * (prof->kick_duty_max - prof->kick_duty_min) /
(100U - BRAKE_DRIVE_PWM_MIN);
}
static uint32_t dev1102_brake_kick_ms(uint32_t start_pct, const dev1102_brake_profile_t *prof)
{
if (start_pct == 0U) {
return 45U;
}
if (start_pct <= BRAKE_DRIVE_PWM_MIN) {
return prof->kick_ms_min;
}
return prof->kick_ms_min +
(start_pct - BRAKE_DRIVE_PWM_MIN) * (prof->kick_ms_max - prof->kick_ms_min) /
(100U - BRAKE_DRIVE_PWM_MIN);
}
static float dev1102_brake_pid_step(dev1102_brake_pid_t *pid,
float target,
float dt_s,
const dev1102_brake_profile_t *prof)
{
/* 剩余速度越大,制动输出越大(此前符号反了导致 PID 阶段几乎不刹车) */
const float error = pid->est_speed - target;
pid->integral += error * dt_s;
pid->integral = dev1102_clampf(pid->integral, -30.0f, 30.0f);
const float derivative = (error - pid->prev_error) / dt_s;
pid->prev_error = error;
float out = prof->pid_kp * error + prof->pid_ki * pid->integral + prof->pid_kd * derivative;
const float floor = pid->est_speed * prof->duty_floor_ratio;
if (out < floor) {
out = floor;
}
return dev1102_clampf(out, 0.0f, prof->duty_max);
}
static void dev1102_brake_model_step(dev1102_brake_pid_t *pid, float brake_duty_pct, float dt_s)
{
const float b = brake_duty_pct / 100.0f;
const float decel = (150.0f * b + 6.0f * (pid->est_speed / 100.0f)) * dt_s;
pid->est_speed -= decel;
if (pid->est_speed < 0.0f) {
pid->est_speed = 0.0f;
}
}
static void device_1102_brake_run(uint32_t start_pct)
{
dev1102_brake_profile_t prof;
dev1102_brake_profile(start_pct, &prof);
const uint32_t kick_duty = dev1102_brake_kick_duty(start_pct, &prof);
const uint32_t kick_ms = dev1102_brake_kick_ms(start_pct, &prof);
const float dt_s = (float)BRAKE_TICK_MS / 1000.0f;
dev1102_brake_pid_t pid = {
.est_speed = (float)(start_pct == 0U ? 40U : start_pct),
.integral = 0.0f,
.prev_error = 0.0f,
};
ESP_LOGW(tag_s, "brake run start=%u kick=%u%% %ums",
(unsigned)start_pct, (unsigned)kick_duty, (unsigned)kick_ms);
device_1102_brake_both(kick_duty);
uint32_t kick_elapsed = 0U;
while (kick_elapsed < kick_ms) {
if (s_abort) {
return;
}
dev1102_brake_model_step(&pid, (float)kick_duty, dt_s);
vTaskDelay(pdMS_TO_TICKS(BRAKE_TICK_MS));
kick_elapsed += BRAKE_TICK_MS;
}
const TickType_t loop_start = xTaskGetTickCount();
while (pid.est_speed > BRAKE_STOP_SPEED_TH) {
if (s_abort) {
return;
}
if ((xTaskGetTickCount() - loop_start) > pdMS_TO_TICKS(prof.max_loop_ms)) {
ESP_LOGW(tag_s, "brake timeout est=%.1f start=%u", pid.est_speed, (unsigned)start_pct);
device_1102_brake_both((uint32_t)(prof.duty_max + 0.5f));
vTaskDelay(pdMS_TO_TICKS(30U));
break;
}
const float brake_duty = dev1102_brake_pid_step(&pid, 0.0f, dt_s, &prof);
device_1102_brake_both((uint32_t)(brake_duty + 0.5f));
dev1102_brake_model_step(&pid, brake_duty, dt_s);
vTaskDelay(pdMS_TO_TICKS(BRAKE_TICK_MS));
}
device_1102_brake_both(0U);
ESP_LOGI(tag_s, "brake done start=%u", (unsigned)start_pct);
}
static void device_1102_brake_task(void *param)
{
(void)param;
uint32_t start_pct = 0U;
ESP_LOGI(tag_s, "brake task running");
for (;;) {
if (xQueueReceive(s_brake_q, &start_pct, portMAX_DELAY) != pdTRUE) {
continue;
}
s_abort = false;
device_1102_brake_run(start_pct);
}
}
esp_err_t device_1102_brake_init(void)
{
if (s_brake_q != NULL) {
return ESP_OK;
}
s_brake_q = xQueueCreate(BRAKE_QUEUE_LEN, sizeof(uint32_t));
if (s_brake_q == NULL) {
ESP_LOGE(tag_s, "queue create fail");
return ESP_ERR_NO_MEM;
}
esp_err_t err = app_task_start(APP_TASK_DEV1102_BRAKE, device_1102_brake_task, NULL, &s_brake_task);
if (err != ESP_OK) {
vQueueDelete(s_brake_q);
s_brake_q = NULL;
ESP_LOGE(tag_s, "task start fail err=%d", (int)err);
return err;
}
ESP_LOGI(tag_s, "brake task ready name=1102_brk handle=%p", (void *)s_brake_task);
return ESP_OK;
}
void device_1102_brake_request(uint32_t start_speed_pct)
{
if (start_speed_pct > 100U) {
start_speed_pct = 100U;
}
dev1102_brake_profile_t prof;
dev1102_brake_profile(start_speed_pct, &prof);
const uint32_t kick_duty = dev1102_brake_kick_duty(start_speed_pct, &prof);
device_1102_brake_both(kick_duty);
if (s_brake_q == NULL) {
ESP_LOGW(tag_s, "brake request without task kick=%u%%", (unsigned)kick_duty);
return;
}
s_abort = true;
(void)xQueueOverwrite(s_brake_q, &start_speed_pct);
}
void device_1102_brake_abort(void)
{
s_abort = true;
device_1102_brake_both(0U);
}
bool device_1102_brake_task_ready(void)
{
return s_brake_task != NULL;
}
#ifndef DEVICE_1102_BRAKE_H
#define DEVICE_1102_BRAKE_H
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/**
* 1102 板载驱动制动:独立 FreeRTOS 任务 + PID(开环速度估计作反馈)。
* 无编码器时用「上次油门 + 制动模型」估计车速;停车时先同步抱闸再 PID 收尾。
*/
esp_err_t device_1102_brake_init(void);
/** 异步请求制动;start_speed_pct 为停车前驱动占空比 0~100 */
void device_1102_brake_request(uint32_t start_speed_pct);
/** 新行驶指令时中止制动(立即松闸) */
void device_1102_brake_abort(void);
/** 制动任务是否已创建(调试/自检) */
bool device_1102_brake_task_ready(void);
#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 speed_val, int steer_val)
{
(void)steer_val;
if (mode == 1) {
if (speed_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, speed_val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (speed_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, speed_val / 2 - 10);
}
} else if (mode == 3) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(50 + speed_val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(130 - speed_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;
}
#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 speed_val, int steer_val)
{
(void)steer_val;
if (mode == 1) {
if (speed_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, speed_val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (speed_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, speed_val / 2 - 10);
}
} else if (mode == 3) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(50 + speed_val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(130 - speed_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
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
#include "device_drive.h" #include "device_drive.h"
#include "device_model.h" #include "device_model.h"
#include "devices/device_1101.h" #include "device_1101.h"
#include "devices/device_1102.h" #include "device_1102.h"
#include "devices/device_1201.h" #include "device_1102_brake.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include <stdbool.h> #include <stdbool.h>
...@@ -159,7 +161,7 @@ esp_err_t rc_pwm_control_init(void) ...@@ -159,7 +161,7 @@ esp_err_t rc_pwm_control_init(void)
} }
if (!drive_ops_s) { if (!drive_ops_s) {
drive_ops_s = device_1201_get_ops(); drive_ops_s = device_1101_get_ops();
} }
ESP_LOGI(tag_s, "PWM init done: D1(10/21) D2(11/12) AUX(15:%s 16:%s) 50Hz profile=%s", 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_15) == RC_AUX_ROLE_SERVO ? "servo" : "esc",
...@@ -175,11 +177,13 @@ void rc_pwm_control_set_drive_from_device_id(const char *device_id) ...@@ -175,11 +177,13 @@ void rc_pwm_control_set_drive_from_device_id(const char *device_id)
drive_ops_s = device_1101_get_ops(); drive_ops_s = device_1101_get_ops();
} else if (device_model_is_1102(m)) { } else if (device_model_is_1102(m)) {
drive_ops_s = device_1102_get_ops(); drive_ops_s = device_1102_get_ops();
ESP_LOGI(tag_s, "1102 制动任务: %s",
device_1102_brake_task_ready() ? "已启动" : "未启动");
#if CONFIG_APP_PWM_IO16_ESC #if CONFIG_APP_PWM_IO16_ESC
ESP_LOGW(tag_s, "1102 需要 IO16 作为舵机,请在 menuconfig 将 GPIO16 角色改为 SERVO"); ESP_LOGW(tag_s, "1102 需要 IO16 作为舵机,请在 menuconfig 将 GPIO16 角色改为 SERVO");
#endif #endif
} else { } else {
drive_ops_s = device_1201_get_ops(); drive_ops_s = device_1101_get_ops();
} }
ESP_LOGI(tag_s, "当前设备策略: %s", drive_ops_s->name); ESP_LOGI(tag_s, "当前设备策略: %s", drive_ops_s->name);
} }
...@@ -258,6 +262,28 @@ void rc_pwm_set_dual_esc_percent(uint32_t percent) ...@@ -258,6 +262,28 @@ void rc_pwm_set_dual_esc_percent(uint32_t percent)
} }
} }
void rc_pwm_set_esc_neutral(void)
{
rc_pwm_set_dual_esc_percent(50U);
}
void rc_pwm_brake_drv1_pair(uint32_t brake_duty_pct, uint32_t hold_ms)
{
if (brake_duty_pct == 0U || hold_ms == 0U) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0U);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0U);
return;
}
if (brake_duty_pct > 100U) {
brake_duty_pct = 100U;
}
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, brake_duty_pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, brake_duty_pct);
vTaskDelay(pdMS_TO_TICKS(hold_ms));
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0U);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0U);
}
void rc_pwm_set_steering_angle_deg(uint32_t angle) 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) { if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO) {
......
...@@ -76,6 +76,16 @@ void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent); ...@@ -76,6 +76,16 @@ void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent);
*/ */
void rc_pwm_set_dual_esc_percent(uint32_t percent); void rc_pwm_set_dual_esc_percent(uint32_t percent);
/** @brief 电调回中位(1500us,percent=50) */
void rc_pwm_set_esc_neutral(void);
/**
* @brief 驱动芯片1 双引脚(IO10+IO21)动态刹车:同占空比短路制动后回 0
* @param brake_duty_pct 制动占空比(建议 30~60,勿用 100 以免过猛)
* @param hold_ms 制动保持时间(ms)
*/
void rc_pwm_brake_drv1_pair(uint32_t brake_duty_pct, uint32_t hold_ms);
/** /**
* @brief 给“默认转向舵机”输出角度(优先 IO15,其次 IO16) * @brief 给“默认转向舵机”输出角度(优先 IO15,其次 IO16)
*/ */
......
...@@ -287,6 +287,9 @@ static void ble_send_heartbeat_once(void) ...@@ -287,6 +287,9 @@ static void ble_send_heartbeat_once(void)
if (!ble_link_up_s || hb_chr_val_handle_s == 0U) { if (!ble_link_up_s || hb_chr_val_handle_s == 0U) {
return; return;
} }
if (ota_offer_session_busy()) {
return;
}
char dev[32] = {0}; char dev[32] = {0};
(void)read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev)); (void)read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
char *json = heart_payload_json_malloc(dev, NULL, false); char *json = heart_payload_json_malloc(dev, NULL, false);
...@@ -550,8 +553,33 @@ static void ble_ota_done_report(bool success, uint32_t bytes_written, const char ...@@ -550,8 +553,33 @@ static void ble_ota_done_report(bool success, uint32_t bytes_written, const char
char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step); char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step);
if (json) { if (json) {
for (int i = 0; i < OTA_DONE_REPORT_COUNT; i++) {
(void)ble_hb_notify_json(json);
ESP_LOGI(tag_s, "BLE TX OTA done (1002) %d/%d", i + 1, OTA_DONE_REPORT_COUNT);
if (i + 1 < OTA_DONE_REPORT_COUNT) {
vTaskDelay(pdMS_TO_TICKS(OTA_DONE_REPORT_GAP_MS));
}
}
free(json);
}
if (!success) {
ble_send_heartbeat_once();
}
}
static void ble_send_ota_active_notice(void)
{
if (!ble_link_up_s || hb_chr_val_handle_s == 0U) {
return;
}
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
char *json = ota_offer_active_json_malloc(devid);
if (json) {
(void)ble_hb_notify_json(json); (void)ble_hb_notify_json(json);
ESP_LOGI(tag_s, "BLE TX OTA 结果 (1002): %s", json); ESP_LOGI(tag_s, "BLE TX OTA 进行中 (1002/active): %s", json);
free(json); free(json);
} }
} }
...@@ -589,7 +617,7 @@ static bool ble_dispatch_json_line(const char *json) ...@@ -589,7 +617,7 @@ static bool ble_dispatch_json_line(const char *json)
cJSON *head = cJSON_GetObjectItem(root, "head"); cJSON *head = cJSON_GetObjectItem(root, "head");
cJSON *m_type = (head && cJSON_IsObject(head)) ? cJSON_GetObjectItem(head, "message_type") : NULL; cJSON *m_type = (head && cJSON_IsObject(head)) ? cJSON_GetObjectItem(head, "message_type") : NULL;
if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == REMOTE_CTRL_MSG_LINK_RESET) { if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == REMOTE_CTRL_MSG_LINK_RESET) {
link_ble_session_reset(); link_ble_session_reset_ex("reset");
ESP_LOGI(tag_s, "收到 message_type=2,已清空 BLE 会话"); ESP_LOGI(tag_s, "收到 message_type=2,已清空 BLE 会话");
cJSON_Delete(root); cJSON_Delete(root);
return true; return true;
...@@ -603,6 +631,7 @@ static bool ble_dispatch_json_line(const char *json) ...@@ -603,6 +631,7 @@ static bool ble_dispatch_json_line(const char *json)
if (ota_offer_parse_phone_response(root)) { if (ota_offer_parse_phone_response(root)) {
ESP_LOGI(tag_s, "手机同意 OTA (head.message_type=0),可写 0xFFE2"); ESP_LOGI(tag_s, "手机同意 OTA (head.message_type=0),可写 0xFFE2");
ble_send_ota_active_notice();
cJSON_Delete(root); cJSON_Delete(root);
return true; return true;
} }
...@@ -640,7 +669,7 @@ static int gap_event(struct ble_gap_event *event, void *arg) ...@@ -640,7 +669,7 @@ static int gap_event(struct ble_gap_event *event, void *arg)
ble_conn_handle_s = 0U; ble_conn_handle_s = 0U;
link_dma_ble_set_conn_handle(0); link_dma_ble_set_conn_handle(0);
remote_control_watchdog_stop(); remote_control_watchdog_stop();
link_ble_session_reset(); link_ble_session_reset_ex("disconnect");
ESP_LOGI(tag_s, "BLE disconnected, session cleared"); ESP_LOGI(tag_s, "BLE disconnected, session cleared");
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params_s, gap_event, NULL); ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params_s, gap_event, NULL);
break; break;
...@@ -746,8 +775,11 @@ static void ble_request_low_duty_conn(uint16_t conn_handle) ...@@ -746,8 +775,11 @@ static void ble_request_low_duty_conn(uint16_t conn_handle)
} }
} }
void link_ble_session_reset(void) static void link_ble_session_reset_ex(const char *ota_abort_err)
{ {
if (ota_abort_err != NULL) {
ota_offer_report_abort(ota_abort_err);
}
ota_frame_protocol_reset(); ota_frame_protocol_reset();
remote_control_link_reset(); remote_control_link_reset();
memset(s_ffe1_json_buf_s, 0, sizeof(s_ffe1_json_buf_s)); memset(s_ffe1_json_buf_s, 0, sizeof(s_ffe1_json_buf_s));
...@@ -756,6 +788,11 @@ void link_ble_session_reset(void) ...@@ -756,6 +788,11 @@ void link_ble_session_reset(void)
last_ota_stat_json_s[0] = '\0'; last_ota_stat_json_s[0] = '\0';
} }
void link_ble_session_reset(void)
{
link_ble_session_reset_ex(NULL);
}
void link_ble_stop(void) void link_ble_stop(void)
{ {
if (!s_ble_stack_running_s) { if (!s_ble_stack_running_s) {
......
...@@ -15,6 +15,11 @@ static const char *tag_s = "LINK_DMA_UART"; ...@@ -15,6 +15,11 @@ static const char *tag_s = "LINK_DMA_UART";
#define UART_BUF_SIZE 4096 #define UART_BUF_SIZE 4096
#define UART_EVT_QUEUE_LEN 16 #define UART_EVT_QUEUE_LEN 16
#if configNUMBER_OF_CORES > 1
#define UART_RX_TASK_CORE 1
#else
#define UART_RX_TASK_CORE 0
#endif
static QueueHandle_t uart_evt_q_s; static QueueHandle_t uart_evt_q_s;
static TaskHandle_t rx_task_s; static TaskHandle_t rx_task_s;
...@@ -121,14 +126,15 @@ esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int ...@@ -121,14 +126,15 @@ esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int
} }
running_s = true; running_s = true;
if (xTaskCreate(uart_evt_rx_task, "uart_evt_rx", 4096, NULL, 6, &rx_task_s) != pdPASS) { if (xTaskCreatePinnedToCore(uart_evt_rx_task, "uart_evt_rx", 6144, NULL, 6,
&rx_task_s, UART_RX_TASK_CORE) != pdPASS) {
running_s = false; running_s = false;
link_dma_uart_stop(); link_dma_uart_stop();
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
ESP_LOGI(tag_s, "UART 事件驱动收发已启动 (port=%d, %d baud, TX=GPIO%d RX=GPIO%d, buf=%d)", ESP_LOGI(tag_s, "UART 事件驱动收发已启动 (port=%d, %d baud, TX=GPIO%d RX=GPIO%d, buf=%d, core=%d)",
(int)port, baud_rate, tx_gpio, rx_gpio, UART_BUF_SIZE); (int)port, baud_rate, tx_gpio, rx_gpio, UART_BUF_SIZE, UART_RX_TASK_CORE);
return ESP_OK; return ESP_OK;
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "freertos/queue.h"
#include "link_dma_uart.h" #include "link_dma_uart.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
...@@ -34,6 +35,7 @@ ...@@ -34,6 +35,7 @@
#include "heart_payload.h" #include "heart_payload.h"
#include "ota_offer_protocol.h" #include "ota_offer_protocol.h"
#include "ota_frame_protocol.h" #include "ota_frame_protocol.h"
#include "ota_uart_tune.h"
/* OTA 二进制流模块 */ /* OTA 二进制流模块 */
#include "ota_binary_stream.h" #include "ota_binary_stream.h"
...@@ -54,8 +56,30 @@ static const char *tag_s = "LINK_UART"; ...@@ -54,8 +56,30 @@ static const char *tag_s = "LINK_UART";
/* 心跳:对端曾 RX 且在此窗口内(允许 App 只收不发) */ /* 心跳:对端曾 RX 且在此窗口内(允许 App 只收不发) */
#define UART_LINK_ALIVE_MS (UART_HEART_PERIOD_MS + 5000) #define UART_LINK_ALIVE_MS (UART_HEART_PERIOD_MS + 5000)
/* 拔线/无字节:超过此时间无 RX 即停停车推送(约 3~4 条 @300ms) */ /* 拔线/无字节:超过此时间无 RX 即停停车推送 */
#define UART_PEER_UNPLUG_MS 1200 #define UART_PEER_UNPLUG_MS 1200
/* OTA 已武装、尚未 BEGIN:允许 App 在 0 后等待 active/BEGIN(对齐 App OTA_ACTIVE_WAIT_MS) */
#define UART_OTA_ARMED_IDLE_MS 15000
/* OTA 二进制传输中:无手机 RX 超时由 ota_uart_tune_transfer_idle_ms() 按波特率计算 */
/*
* UART RX 环形缓冲区:esp_ota_begin / flash 写不得在 uart_evt_rx(4KB 栈) 内同步执行。
* 每槽 1024B,可容纳一整帧 OT DATA(876B 载荷 + 帧头 ≈ 886B)。
* worker 按槽逐段喂入解析器,禁止合并截断(旧版 512B cap 会在 seq≈35 处 CRC 失败)。
* 动态栈:平时 8KB,OTA 切 16KB,结束切回。
*/
#define UART_RX_RING_SLOTS 32
#define UART_RX_SLOT_SIZE 1024
#define UART_RX_WORKER_STACK_NORMAL 8192
#define UART_RX_WORKER_STACK_OTA 16384
#define UART_RX_WORKER_PRIO 5
typedef struct {
volatile uint8_t write_idx; /* DMA 回调写入 */
volatile uint8_t read_idx; /* worker 读出 */
uint16_t len[UART_RX_RING_SLOTS];
uint8_t data[UART_RX_RING_SLOTS][UART_RX_SLOT_SIZE];
} uart_rx_ring_t;
/* 协议帧分隔符(使用换行符作为 JSON 分隔) */ /* 协议帧分隔符(使用换行符作为 JSON 分隔) */
#define UART_FRAME_DELIMITER '\n' #define UART_FRAME_DELIMITER '\n'
...@@ -66,7 +90,12 @@ static void uart_send_heartbeat_once(void); ...@@ -66,7 +90,12 @@ static void uart_send_heartbeat_once(void);
static TaskHandle_t rx_task_handle_s = NULL; static TaskHandle_t rx_task_handle_s = NULL;
static TaskHandle_t hb_task_handle_s = NULL; static TaskHandle_t hb_task_handle_s = NULL;
static TaskHandle_t uart_rx_worker_handle_s = NULL;
static volatile bool link_up_s = false; static volatile bool link_up_s = false;
/* OTA 期间动态栈管理 */
static volatile bool uart_rx_worker_is_ota_stack_s = false;
static SemaphoreHandle_t uart_rx_worker_mutex_s = NULL;
/** 上次从 UART1 RX 收到对端数据的时刻;0=从未收到 */ /** 上次从 UART1 RX 收到对端数据的时刻;0=从未收到 */
static volatile uint32_t last_peer_rx_ms = 0; static volatile uint32_t last_peer_rx_ms = 0;
...@@ -81,6 +110,11 @@ static size_t rx_buf_pos_s = 0; ...@@ -81,6 +110,11 @@ static size_t rx_buf_pos_s = 0;
static char last_ota_stat_json_s[192]; static char last_ota_stat_json_s[192];
static bool uart_ota_framed_mode_s = false; static bool uart_ota_framed_mode_s = false;
#if BUILD_IS_RELEASE
static bool uart_log_hook_installed_s;
static volatile bool uart_log_forward_busy_s;
#endif
static uint32_t get_current_time_ms(void) static uint32_t get_current_time_ms(void)
{ {
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
...@@ -108,6 +142,7 @@ static void uart_ota_init(void) ...@@ -108,6 +142,7 @@ static void uart_ota_init(void)
} }
} }
#if UART_LINK_LOG_TX_JSON
static const char *uart_tx_json_kind_hint(const char *json) static const char *uart_tx_json_kind_hint(const char *json)
{ {
if (!json) { if (!json) {
...@@ -125,11 +160,17 @@ static const char *uart_tx_json_kind_hint(const char *json) ...@@ -125,11 +160,17 @@ static const char *uart_tx_json_kind_hint(const char *json)
if (strstr(json, "\"message_type\":3") != NULL) { if (strstr(json, "\"message_type\":3") != NULL) {
return "停车/状态(3)"; return "停车/状态(3)";
} }
if (strstr(json, "\"message_type\":4") != NULL || strstr(json, "\"message_type\":5") != NULL) { if (strstr(json, "\"message_type\":4") != NULL ||
return "告警/错误(4/5)"; strstr(json, "\"message_type\": 4") != NULL) {
return "告警(4)";
}
if (strstr(json, "\"message_type\":5") != NULL ||
strstr(json, "\"message_type\": 5") != NULL) {
return "错误(5)";
} }
return "JSON"; return "JSON";
} }
#endif
static void uart_link_log_tx_json(const char *json) static void uart_link_log_tx_json(const char *json)
{ {
...@@ -256,6 +297,76 @@ int link_uart_send_log(const char *json) ...@@ -256,6 +297,76 @@ int link_uart_send_log(const char *json)
return uart_link_send_json_line(json); return uart_link_send_json_line(json);
} }
#if BUILD_IS_RELEASE
static void uart_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 uart_log_vprintf_hook(const char *fmt, va_list ap)
{
(void)fmt;
if (xPortInIsrContext() || uart_log_forward_busy_s || !link_uart_has_peer_seen()) {
return 0;
}
/* OTA 期间 UART1 只走 OT 二进制 ACK,禁止插入 JSON 日志行以免 App 组帧失败 */
if (ota_offer_session_busy()) {
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];
uart_log_line_to_msg(line, msg, sizeof(msg));
char *json = heart_payload_alert_json_malloc(msg_type, msg);
if (json == NULL) {
return 0;
}
uart_log_forward_busy_s = true;
(void)link_uart_send_log(json);
uart_log_forward_busy_s = false;
free(json);
return 0;
}
static void uart_log_hook_install_if_needed(void)
{
if (uart_log_hook_installed_s) {
return;
}
(void)esp_log_set_vprintf(uart_log_vprintf_hook);
uart_log_hook_installed_s = true;
}
#endif
static bool uart_peer_rx_recent(void) static bool uart_peer_rx_recent(void)
{ {
uint32_t last = last_peer_rx_ms; uint32_t last = last_peer_rx_ms;
...@@ -294,6 +405,28 @@ static void uart_ota_done_report(bool success, uint32_t bytes_written, const cha ...@@ -294,6 +405,28 @@ static void uart_ota_done_report(bool success, uint32_t bytes_written, const cha
char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step); char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step);
if (json) { if (json) {
for (int i = 0; i < OTA_DONE_REPORT_COUNT; i++) {
uart_send_ota_offer_json(json);
ESP_LOGI(tag_s, "UART1 TX OTA done (1002) %d/%d", i + 1, OTA_DONE_REPORT_COUNT);
if (i + 1 < OTA_DONE_REPORT_COUNT) {
vTaskDelay(pdMS_TO_TICKS(OTA_DONE_REPORT_GAP_MS));
}
}
free(json);
}
if (!success) {
uart_send_heartbeat_once();
}
}
static void uart_send_ota_active_notice(void)
{
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
char *json = ota_offer_active_json_malloc(devid);
if (json) {
ESP_LOGI(tag_s, "UART1 TX OTA 进行中 (1002/active)");
uart_send_ota_offer_json(json); uart_send_ota_offer_json(json);
free(json); free(json);
} }
...@@ -312,8 +445,11 @@ static void uart_send_ota_query_on_link_ready(void) ...@@ -312,8 +445,11 @@ static void uart_send_ota_query_on_link_ready(void)
} }
} }
static void link_uart_session_reset(void) static void link_uart_session_reset_ex(const char *ota_abort_err)
{ {
if (ota_abort_err != NULL) {
ota_offer_report_abort(ota_abort_err);
}
rx_buf_pos_s = 0; rx_buf_pos_s = 0;
uart_ota_framed_mode_s = false; uart_ota_framed_mode_s = false;
ota_frame_protocol_reset(); ota_frame_protocol_reset();
...@@ -335,7 +471,7 @@ static void process_received_json(cJSON *root, const char *json) ...@@ -335,7 +471,7 @@ static void process_received_json(cJSON *root, const char *json)
cJSON *m_type = (head && cJSON_IsObject(head)) ? cJSON_GetObjectItem(head, "message_type") : NULL; cJSON *m_type = (head && cJSON_IsObject(head)) ? cJSON_GetObjectItem(head, "message_type") : NULL;
if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == REMOTE_CTRL_MSG_LINK_RESET) { if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == REMOTE_CTRL_MSG_LINK_RESET) {
link_uart_session_reset(); link_uart_session_reset_ex("reset");
ESP_LOGI(tag_s, "收到 message_type=2,已清空 UART 会话"); ESP_LOGI(tag_s, "收到 message_type=2,已清空 UART 会话");
return; return;
} }
...@@ -348,19 +484,44 @@ static void process_received_json(cJSON *root, const char *json) ...@@ -348,19 +484,44 @@ static void process_received_json(cJSON *root, const char *json)
} }
if (ota_offer_parse_phone_response(root)) { if (ota_offer_parse_phone_response(root)) {
ESP_LOGI(tag_s, "手机同意 OTA (head.message_type=0),可发 0x01/固件字节"); ESP_LOGI(tag_s, "手机同意 OTA (head.message_type=0),可发 OT 帧");
uart_send_ota_active_notice();
return; return;
} }
remote_control_apply_json_root(root, json); remote_control_apply_json_root(root, json);
} }
static const char *uart_ota_frame_op_name(uint8_t op)
{
switch (op) {
case OTA_FRAME_OP_BEGIN:
return "BEGIN";
case OTA_FRAME_OP_DATA:
return "DATA";
case OTA_FRAME_OP_END:
return "END";
case OTA_FRAME_OP_ACK:
return "ACK";
case OTA_FRAME_OP_NAK:
return "NAK";
default:
return "OT";
}
}
static void uart_ota_tx_frame(const uint8_t *frame, uint16_t len, void *ctx) static void uart_ota_tx_frame(const uint8_t *frame, uint16_t len, void *ctx)
{ {
(void)ctx; (void)ctx;
if (frame != NULL && len > 0) { if (frame == NULL || len == 0) {
(void)uart_link_tx(frame, len, portMAX_DELAY); return;
}
if (len >= 4) {
ESP_LOGI(tag_s, "UART1 TX OTA %s (%u bytes, seq=%u)",
uart_ota_frame_op_name(frame[3]), (unsigned)len,
(unsigned)(frame[4] | ((uint16_t)frame[5] << 8)));
} }
(void)uart_link_tx(frame, len, portMAX_DELAY);
} }
/* 旧版裸字节回显(无 OT 帧头时兼容) */ /* 旧版裸字节回显(无 OT 帧头时兼容) */
...@@ -372,18 +533,6 @@ static void uart_link_ota_ack_payload(const uint8_t *payload, uint16_t payload_l ...@@ -372,18 +533,6 @@ static void uart_link_ota_ack_payload(const uint8_t *payload, uint16_t payload_l
} }
} }
/* 逐字节写入 OTA(单字节 0x02 包)并回传确认 */
static void uart_link_ota_feed_raw_bytes(const uint8_t *data, int len)
{
for (int i = 0; i < len; i++) {
uint8_t pkt[2] = { OTA_OP_DATA, data[i] };
if (ota_binary_stream_process_packet(pkt, 2) == 0) {
uint8_t b = data[i];
(void)uart_link_tx(&b, 1, portMAX_DELAY);
}
}
}
/* 处理 OTA 数据包(0x01/0x02/0x03);仅 0x02 固件载荷按字节回传 */ /* 处理 OTA 数据包(0x01/0x02/0x03);仅 0x02 固件载荷按字节回传 */
static void process_ota_packet(const uint8_t *data, uint16_t len) static void process_ota_packet(const uint8_t *data, uint16_t len)
{ {
...@@ -497,13 +646,12 @@ static void link_uart_feed_rx_binary_chunk(const uint8_t *data, int len) ...@@ -497,13 +646,12 @@ static void link_uart_feed_rx_binary_chunk(const uint8_t *data, int len)
bool frame_header_complete = (len >= 2 && data[0] == OTA_FRAME_MAGIC0 && data[1] == OTA_FRAME_MAGIC1); bool frame_header_complete = (len >= 2 && data[0] == OTA_FRAME_MAGIC0 && data[1] == OTA_FRAME_MAGIC1);
if (uart_ota_framed_mode_s || frame_parser_active || ota_frame_rx_looks_framed(data, len)) { if (uart_ota_framed_mode_s || frame_parser_active || ota_frame_rx_looks_framed(data, len)) {
if (!ota_offer_is_armed() && !ota_binary_stream_is_active()) { if (!ota_offer_is_armed() && !ota_binary_stream_is_active()) {
ESP_LOGW(tag_s, "UART1 忽略 OTA 二进制:尚未收到手机 message_type=0"); ESP_LOGW(tag_s, "UART1 OT 帧在未 armed 时到达,将回 NAK(须先发 message_type=0)");
return;
} }
if (uart_ota_framed_mode_s || frame_parser_active || frame_header_complete) { if (uart_ota_framed_mode_s || frame_parser_active || frame_header_complete) {
uart_ota_framed_mode_s = true; uart_ota_framed_mode_s = true;
} }
ESP_LOGI(tag_s, "UART1 OTA frame bytes (%d bytes)", len); ESP_LOGD(tag_s, "UART1 OTA frame bytes (%d bytes)", len);
ota_frame_rx_feed(data, (uint16_t)len); ota_frame_rx_feed(data, (uint16_t)len);
return; return;
} }
...@@ -518,8 +666,9 @@ static void link_uart_feed_rx_binary_chunk(const uint8_t *data, int len) ...@@ -518,8 +666,9 @@ static void link_uart_feed_rx_binary_chunk(const uint8_t *data, int len)
return; return;
} }
/* 未识别为 OT 帧的杂字节在 OTA 期间丢弃,禁止走裸字节写入 flash */
if (ota_binary_stream_is_active() || ota_offer_is_armed()) { if (ota_binary_stream_is_active() || ota_offer_is_armed()) {
uart_link_ota_feed_raw_bytes(data, len); ESP_LOGW(tag_s, "UART1 丢弃 OTA 期间非 OT 帧数据 (%d bytes)", len);
} }
} }
...@@ -531,6 +680,12 @@ static void uart_skip_json_delimiters_before_ota(const uint8_t **data, int *len) ...@@ -531,6 +680,12 @@ static void uart_skip_json_delimiters_before_ota(const uint8_t **data, int *len)
} }
} }
static bool uart_link_ota_rx_busy(void)
{
return uart_ota_framed_mode_s || ota_frame_rx_is_active() ||
ota_offer_is_armed() || ota_binary_stream_is_active();
}
static void link_uart_feed_rx_bytes(const uint8_t *data, int len) static void link_uart_feed_rx_bytes(const uint8_t *data, int len)
{ {
if (!data || len <= 0) { if (!data || len <= 0) {
...@@ -540,30 +695,32 @@ static void link_uart_feed_rx_bytes(const uint8_t *data, int len) ...@@ -540,30 +695,32 @@ static void link_uart_feed_rx_bytes(const uint8_t *data, int len)
uart_link_note_peer_rx(); uart_link_note_peer_rx();
log_uart1_rx_raw(data, len); log_uart1_rx_raw(data, len);
if (rx_buf_pos_s > 0 || data[0] == '{') {
remote_control_note_rx_activity();
}
/* /*
* Android/USB 串口库可能发送 "\r\n" 后紧跟 OT 帧,甚至把 '\r' 和 '\n' * OTA 期间 UART1 只走 OT 二进制解析。
* 拆成两次 read。OTA 尚未 begin 时忽略这些 JSON 行尾,避免把 '\n' * 固件载荷里常有 0x7B('{'):若走 JSON 逐字节路径会误解析并破坏 OT 帧同步。
* 当成第一字节固件数据写入 flash。
*/ */
if (rx_buf_pos_s == 0 && ota_offer_is_armed() && !ota_binary_stream_is_active()) { if (uart_link_ota_rx_busy()) {
remote_control_note_rx_activity();
if (rx_buf_pos_s > 0) {
rx_buf_pos_s = 0;
}
if (ota_offer_is_armed() && !ota_binary_stream_is_active()) {
uart_skip_json_delimiters_before_ota(&data, &len); uart_skip_json_delimiters_before_ota(&data, &len);
if (len <= 0) { if (len <= 0) {
return; return;
} }
} }
/* 整包即为 OT 帧或裸 0x01/0x02/0x03(不以 { 开头) */
if (data[0] != '{' &&
(ota_frame_rx_looks_framed(data, len) || is_ota_command(data[0]))) {
link_uart_feed_rx_binary_chunk(data, len); link_uart_feed_rx_binary_chunk(data, len);
return; return;
} }
if (data[0] != '{' && (ota_binary_stream_is_active() || ota_offer_is_armed())) { if (rx_buf_pos_s > 0 || data[0] == '{') {
remote_control_note_rx_activity();
}
/* 整包即为 OT 帧或裸 0x01/0x02/0x03(不以 { 开头) */
if (data[0] != '{' &&
(ota_frame_rx_looks_framed(data, len) || is_ota_command(data[0]))) {
link_uart_feed_rx_binary_chunk(data, len); link_uart_feed_rx_binary_chunk(data, len);
return; return;
} }
...@@ -596,10 +753,198 @@ static void link_uart_feed_rx_bytes(const uint8_t *data, int len) ...@@ -596,10 +753,198 @@ static void link_uart_feed_rx_bytes(const uint8_t *data, int len)
} }
} }
/* 环形缓冲区实例(静态分配,避免运行时 malloc) */
static uart_rx_ring_t uart_rx_ring_s;
static SemaphoreHandle_t uart_rx_data_sem_s = NULL; /* 数据可用信号量 */
static SemaphoreHandle_t uart_rx_ring_mutex_s = NULL;
static void uart_rx_ring_init(void)
{
memset(&uart_rx_ring_s, 0, sizeof(uart_rx_ring_s));
uart_rx_ring_s.write_idx = 0;
uart_rx_ring_s.read_idx = 0;
}
static inline bool uart_rx_ring_full(void)
{
uint8_t next = (uart_rx_ring_s.write_idx + 1) % UART_RX_RING_SLOTS;
return next == uart_rx_ring_s.read_idx;
}
static inline bool uart_rx_ring_empty(void)
{
return uart_rx_ring_s.write_idx == uart_rx_ring_s.read_idx;
}
/**
* @brief 向环形缓冲区写入数据(DMA 回调调用,须快速)
* @return true=成功,false=满(丢包)
*/
static bool uart_rx_ring_write(const uint8_t *data, size_t len)
{
if (len == 0) {
return true;
}
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreTake(uart_rx_ring_mutex_s, portMAX_DELAY);
}
size_t offset = 0;
while (offset < len) {
if (uart_rx_ring_full()) {
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreGive(uart_rx_ring_mutex_s);
}
return false; /* 满,丢包 */
}
uint8_t idx = uart_rx_ring_s.write_idx;
size_t chunk = len - offset;
if (chunk > UART_RX_SLOT_SIZE) {
chunk = UART_RX_SLOT_SIZE;
}
memcpy(uart_rx_ring_s.data[idx], data + offset, chunk);
uart_rx_ring_s.len[idx] = (uint16_t)chunk;
uart_rx_ring_s.write_idx = (idx + 1) % UART_RX_RING_SLOTS;
offset += chunk;
}
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreGive(uart_rx_ring_mutex_s);
}
/* 通知 worker */
if (uart_rx_data_sem_s) {
xSemaphoreGive(uart_rx_data_sem_s);
}
return true;
}
static void uart_rx_ring_drain(void)
{
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreTake(uart_rx_ring_mutex_s, portMAX_DELAY);
}
uart_rx_ring_s.read_idx = uart_rx_ring_s.write_idx;
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreGive(uart_rx_ring_mutex_s);
}
}
/** 按槽逐段读出并喂解析器,保持字节流连续、不截断(持锁仅拷贝,解析在锁外) */
static void uart_rx_ring_drain_to_parser(void)
{
uint8_t local[UART_RX_SLOT_SIZE];
for (;;) {
uint16_t n = 0;
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreTake(uart_rx_ring_mutex_s, portMAX_DELAY);
}
if (uart_rx_ring_empty()) {
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreGive(uart_rx_ring_mutex_s);
}
break;
}
uint8_t idx = uart_rx_ring_s.read_idx;
n = uart_rx_ring_s.len[idx];
memcpy(local, uart_rx_ring_s.data[idx], n);
uart_rx_ring_s.read_idx = (idx + 1) % UART_RX_RING_SLOTS;
if (uart_rx_ring_mutex_s != NULL) {
xSemaphoreGive(uart_rx_ring_mutex_s);
}
link_uart_feed_rx_bytes(local, (int)n);
}
}
static void uart_rx_worker_task(void *param)
{
(void)param;
const uint32_t stack_sz = (uint32_t)(uintptr_t)param;
ESP_LOGI(tag_s, "UART1 RX 工作任务已启动 (stack=%u, ring=%dx%dB=%dKB)",
(unsigned)stack_sz, UART_RX_RING_SLOTS, UART_RX_SLOT_SIZE,
(UART_RX_RING_SLOTS * UART_RX_SLOT_SIZE) / 1024);
while (1) {
/* 等待数据信号量,或 10ms 超时轮询 */
if (uart_rx_data_sem_s) {
xSemaphoreTake(uart_rx_data_sem_s, pdMS_TO_TICKS(10));
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
uart_rx_ring_drain_to_parser();
}
}
/**
* @brief 切换 RX worker 栈大小。OTA 期间用大栈(16KB),平时用小栈(8KB)。
* @param use_ota_stack true=切换到大栈,false=切换到小栈
*/
static void uart_rx_worker_switch_stack(bool use_ota_stack)
{
if (uart_rx_worker_mutex_s == NULL) {
return;
}
xSemaphoreTake(uart_rx_worker_mutex_s, portMAX_DELAY);
if (use_ota_stack == uart_rx_worker_is_ota_stack_s) {
xSemaphoreGive(uart_rx_worker_mutex_s);
return; /* 已是目标栈大小 */
}
/* 删除旧 worker(保留环形缓冲区内数据,避免切换时丢失 BEGIN/DATA) */
if (uart_rx_worker_handle_s) {
vTaskDelete(uart_rx_worker_handle_s);
uart_rx_worker_handle_s = NULL;
}
const uint32_t stack_sz = use_ota_stack ? UART_RX_WORKER_STACK_OTA : UART_RX_WORKER_STACK_NORMAL;
BaseType_t ret = xTaskCreate(uart_rx_worker_task,
use_ota_stack ? "uart_rx_ota" : "uart_rx_work",
(uint32_t)stack_sz,
(void *)(uintptr_t)stack_sz, /* 传递栈大小用于日志 */
UART_RX_WORKER_PRIO,
&uart_rx_worker_handle_s);
if (ret != pdPASS) {
ESP_LOGE(tag_s, "切换 RX worker 栈(%u)失败", (unsigned)stack_sz);
} else {
uart_rx_worker_is_ota_stack_s = use_ota_stack;
ESP_LOGI(tag_s, "RX worker 已切换为 %s 栈(%u)",
use_ota_stack ? "OTA(大)" : "NORMAL(小)", (unsigned)stack_sz);
/* 唤醒新 worker 处理切换前已入队的数据 */
if (uart_rx_data_sem_s) {
xSemaphoreGive(uart_rx_data_sem_s);
}
}
xSemaphoreGive(uart_rx_worker_mutex_s);
}
static void uart_dma_rx_cb(const uint8_t *data, size_t len, void *ctx) static void uart_dma_rx_cb(const uint8_t *data, size_t len, void *ctx)
{ {
(void)ctx; (void)ctx;
link_uart_feed_rx_bytes(data, (int)len); if (!data || len == 0) {
return;
}
if (len > 65535U) {
ESP_LOGW(tag_s, "UART1 RX 单次过长 (%u),截断", (unsigned)len);
len = 65535U;
}
if (!uart_rx_ring_write(data, len)) {
ESP_LOGW(tag_s, "UART1 RX 环形缓冲区满,丢弃 %u 字节(OTA 将 NAK 触发重传)", (unsigned)len);
}
} }
static void uart_link_watchdog_task(void *param) static void uart_link_watchdog_task(void *param)
...@@ -609,30 +954,61 @@ static void uart_link_watchdog_task(void *param) ...@@ -609,30 +954,61 @@ static void uart_link_watchdog_task(void *param)
(int)UART_LINK_PORT, CONFIG_APP_UART_LINK_BAUDRATE, (int)UART_LINK_PORT, CONFIG_APP_UART_LINK_BAUDRATE,
CONFIG_APP_UART_LINK_TX_GPIO, CONFIG_APP_UART_LINK_RX_GPIO); CONFIG_APP_UART_LINK_TX_GPIO, CONFIG_APP_UART_LINK_RX_GPIO);
bool was_ota_mode = false;
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(200)); vTaskDelay(pdMS_TO_TICKS(200));
uint32_t now = get_current_time_ms(); uint32_t now = get_current_time_ms();
uint32_t last_rx = last_peer_rx_ms; uint32_t last_rx = last_peer_rx_ms;
if (last_rx != 0 && (now - last_rx) > UART_PEER_UNPLUG_MS) { uint32_t idle_limit_ms = UART_PEER_UNPLUG_MS;
bool ota_session_reset = false;
bool in_ota = ota_binary_stream_is_active() || ota_offer_is_armed() || ota_frame_rx_is_active();
/* 动态栈切换:进入/退出 OTA 时调整 RX worker 栈大小 */
if (in_ota != was_ota_mode) {
uart_rx_worker_switch_stack(in_ota);
was_ota_mode = in_ota;
}
if (ota_binary_stream_is_active() || ota_frame_rx_is_active()) {
idle_limit_ms = ota_uart_tune_transfer_idle_ms(CONFIG_APP_UART_LINK_BAUDRATE);
ota_session_reset = true;
} else if (ota_offer_is_armed()) {
idle_limit_ms = UART_OTA_ARMED_IDLE_MS;
ota_session_reset = true;
}
if (last_rx != 0 && (now - last_rx) > idle_limit_ms) {
if (link_up_s) { if (link_up_s) {
link_up_s = false; link_up_s = false;
remote_control_stop_notify_end(); remote_control_stop_notify_end();
if (ota_offer_is_armed() || ota_binary_stream_is_active() || if (ota_session_reset || rx_buf_pos_s > 0) {
ota_frame_rx_is_active() || rx_buf_pos_s > 0) { link_uart_session_reset_ex(ota_session_reset ? "timeout" : NULL);
link_uart_session_reset(); ESP_LOGW(tag_s, "UART1 RX 超时(%lu ms),已清空未完成 OTA/JSON 会话",
ESP_LOGW(tag_s, "UART1 RX 超时,已清空未完成 OTA/JSON 会话"); (unsigned long)idle_limit_ms);
} }
ESP_LOGI(tag_s, "UART1 对端近期无 RX(%d ms),停止停车推送", UART_PEER_UNPLUG_MS); ESP_LOGI(tag_s, "UART1 对端近期无 RX(%lu ms),停止停车推送",
(unsigned long)idle_limit_ms);
} }
} }
} }
} }
static bool uart_ota_tx_busy(void)
{
/* UART 与 OT ACK 共用一线;OTA 期间不发心跳,改发 1002/active 识别 JSON */
return ota_offer_session_busy();
}
static void uart_send_heartbeat_once(void) static void uart_send_heartbeat_once(void)
{ {
if (!link_uart_has_peer_seen()) { if (!link_uart_has_peer_seen()) {
return; return;
} }
if (uart_ota_tx_busy()) {
return;
}
char devid[32] = {0}; char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid)); read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
...@@ -702,9 +1078,68 @@ esp_err_t link_uart_start(void) ...@@ -702,9 +1078,68 @@ esp_err_t link_uart_start(void)
/* 初始化 UART OTA 模块 */ /* 初始化 UART OTA 模块 */
uart_ota_init(); uart_ota_init();
/* 初始化环形缓冲区 */
uart_rx_ring_init();
uart_rx_ring_mutex_s = xSemaphoreCreateMutex();
if (uart_rx_ring_mutex_s == NULL) {
ESP_LOGE(tag_s, "UART RX ring mutex create failed");
return ESP_ERR_NO_MEM;
}
/* 创建数据可用信号量(二值信号量,非计数) */
uart_rx_data_sem_s = xSemaphoreCreateBinary();
if (uart_rx_data_sem_s == NULL) {
ESP_LOGE(tag_s, "UART RX data sem create failed");
vSemaphoreDelete(uart_rx_ring_mutex_s);
uart_rx_ring_mutex_s = NULL;
return ESP_ERR_NO_MEM;
}
uart_rx_worker_mutex_s = xSemaphoreCreateMutex();
if (uart_rx_worker_mutex_s == NULL) {
ESP_LOGE(tag_s, "UART RX worker mutex create failed");
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
vSemaphoreDelete(uart_rx_ring_mutex_s);
uart_rx_ring_mutex_s = NULL;
return ESP_ERR_NO_MEM;
}
/* 初始创建小栈(8KB)worker,OTA期间自动切换大栈(16KB) */
BaseType_t worker_ok = xTaskCreate(uart_rx_worker_task, "uart_rx_work",
UART_RX_WORKER_STACK_NORMAL,
(void *)(uintptr_t)UART_RX_WORKER_STACK_NORMAL,
UART_RX_WORKER_PRIO, &uart_rx_worker_handle_s);
if (worker_ok != pdPASS) {
ESP_LOGE(tag_s, "UART RX worker task create failed");
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
return ESP_ERR_NO_MEM;
}
uart_rx_worker_is_ota_stack_s = false;
/* 初始化 UART 硬件 */ /* 初始化 UART 硬件 */
esp_err_t err = uart_link_init_hardware(); esp_err_t err = uart_link_init_hardware();
if (err != ESP_OK) { if (err != ESP_OK) {
if (uart_rx_worker_handle_s) {
vTaskDelete(uart_rx_worker_handle_s);
uart_rx_worker_handle_s = NULL;
}
if (uart_rx_worker_mutex_s) {
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
}
if (uart_rx_data_sem_s) {
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
}
if (uart_rx_worker_mutex_s) {
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
}
return err; return err;
} }
...@@ -713,6 +1148,18 @@ esp_err_t link_uart_start(void) ...@@ -713,6 +1148,18 @@ esp_err_t link_uart_start(void)
if (!tx_mutex_s) { if (!tx_mutex_s) {
ESP_LOGE(tag_s, "TX mutex create failed"); ESP_LOGE(tag_s, "TX mutex create failed");
link_dma_uart_stop(); link_dma_uart_stop();
if (uart_rx_worker_handle_s) {
vTaskDelete(uart_rx_worker_handle_s);
uart_rx_worker_handle_s = NULL;
}
if (uart_rx_worker_mutex_s) {
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
}
if (uart_rx_data_sem_s) {
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
}
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
...@@ -727,6 +1174,18 @@ esp_err_t link_uart_start(void) ...@@ -727,6 +1174,18 @@ esp_err_t link_uart_start(void)
vSemaphoreDelete(tx_mutex_s); vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL; tx_mutex_s = NULL;
link_dma_uart_stop(); link_dma_uart_stop();
if (uart_rx_worker_handle_s) {
vTaskDelete(uart_rx_worker_handle_s);
uart_rx_worker_handle_s = NULL;
}
if (uart_rx_worker_mutex_s) {
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
}
if (uart_rx_data_sem_s) {
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
}
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
...@@ -740,6 +1199,18 @@ esp_err_t link_uart_start(void) ...@@ -740,6 +1199,18 @@ esp_err_t link_uart_start(void)
vSemaphoreDelete(tx_mutex_s); vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL; tx_mutex_s = NULL;
link_dma_uart_stop(); link_dma_uart_stop();
if (uart_rx_worker_handle_s) {
vTaskDelete(uart_rx_worker_handle_s);
uart_rx_worker_handle_s = NULL;
}
if (uart_rx_worker_mutex_s) {
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
}
if (uart_rx_data_sem_s) {
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
}
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
...@@ -755,11 +1226,35 @@ esp_err_t link_uart_start(void) ...@@ -755,11 +1226,35 @@ esp_err_t link_uart_start(void)
CONFIG_APP_UART_LINK_RX_GPIO, CONFIG_APP_UART_LINK_TX_GPIO, CONFIG_APP_UART_LINK_RX_GPIO, CONFIG_APP_UART_LINK_TX_GPIO,
CONFIG_APP_UART_LINK_BAUDRATE); CONFIG_APP_UART_LINK_BAUDRATE);
ESP_LOGI(tag_s, "手机发 link_ready(6) 后设备回 OTA 询问(1002);手机回 0 后发 OT 帧固件"); ESP_LOGI(tag_s, "手机发 link_ready(6) 后设备回 OTA 询问(1002);手机回 0 后发 OT 帧固件");
#if BUILD_IS_RELEASE
uart_log_hook_install_if_needed();
ESP_LOGI(tag_s, "Release: ESP_LOGW/ESP_LOGE 经 UART1 推送 message_type=4/5 JSON");
#endif
return ESP_OK; return ESP_OK;
} }
void link_uart_stop(void) void link_uart_stop(void)
{ {
link_dma_uart_stop();
if (uart_rx_worker_handle_s) {
vTaskDelete(uart_rx_worker_handle_s);
uart_rx_worker_handle_s = NULL;
}
uart_rx_ring_drain();
if (uart_rx_data_sem_s) {
vSemaphoreDelete(uart_rx_data_sem_s);
uart_rx_data_sem_s = NULL;
}
if (uart_rx_ring_mutex_s) {
vSemaphoreDelete(uart_rx_ring_mutex_s);
uart_rx_ring_mutex_s = NULL;
}
if (uart_rx_worker_mutex_s) {
vSemaphoreDelete(uart_rx_worker_mutex_s);
uart_rx_worker_mutex_s = NULL;
}
/* 停止心跳任务 */ /* 停止心跳任务 */
if (hb_task_handle_s) { if (hb_task_handle_s) {
vTaskDelete(hb_task_handle_s); vTaskDelete(hb_task_handle_s);
...@@ -781,8 +1276,6 @@ void link_uart_stop(void) ...@@ -781,8 +1276,6 @@ void link_uart_stop(void)
tx_mutex_s = NULL; tx_mutex_s = NULL;
} }
link_dma_uart_stop();
link_up_s = false; link_up_s = false;
ESP_LOGI(tag_s, "UART 链路已停止"); ESP_LOGI(tag_s, "UART 链路已停止");
......
...@@ -37,7 +37,7 @@ void link_uart_stop(void); ...@@ -37,7 +37,7 @@ void link_uart_stop(void);
int link_uart_send_heartbeat(const char *json); int link_uart_send_heartbeat(const char *json);
/** /**
* @brief 发送日志/告警 JSON 数据 * @brief 发送日志/告警 JSON(message_type=4/5;Release 下 ESP_LOGW/E 亦经此上送)
* @param json 要发送的 JSON 字符串 * @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败 * @return 0 成功,其他失败
*/ */
......
...@@ -48,12 +48,34 @@ static void ota_stream_send_status_fmt(const char *fmt, ...) ...@@ -48,12 +48,34 @@ static void ota_stream_send_status_fmt(const char *fmt, ...)
session_s.status_callback(buf); session_s.status_callback(buf);
} }
/* 发送错误状态 */ /* 发送错误状态并结束 OTA 会话(仅用于致命错误,须已 armed) */
static void ota_stream_send_error(const char *step, const char *err) static void ota_stream_send_error(const char *step, const char *err)
{ {
ESP_LOGE(tag_s, "OTA error at %s: %s", step, err); ESP_LOGE(tag_s, "OTA error at %s: %s", step, err);
if (ota_offer_is_armed() || session_s.active) {
ota_stream_send_status_fmt("{\"ota\":\"%s\",\"ok\":0,\"err\":\"%s\"}", step, err); ota_stream_send_status_fmt("{\"ota\":\"%s\",\"ok\":0,\"err\":\"%s\"}", step, err);
ota_offer_notify_done(false, (uint32_t)session_s.written_size, step); ota_offer_notify_done(false, (uint32_t)session_s.written_size, step);
}
}
/**
* @brief 擦除 OTA 目标分区(失败清理,防止下次启动误用损坏固件)
*/
static void ota_stream_erase_partition(void)
{
if (session_s.partition == NULL) {
return;
}
ESP_LOGW(tag_s, "OTA 失败:擦除分区 %s 防止误启动", session_s.partition->label);
const esp_partition_t *part = session_s.partition;
esp_err_t err = esp_partition_erase_range(part, 0, part->size);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "擦除分区失败: %s", esp_err_to_name(err));
} else {
ESP_LOGI(tag_s, "分区 %s 已擦除 (%u bytes)", part->label, (unsigned)part->size);
}
} }
/* 中止 OTA 会话 */ /* 中止 OTA 会话 */
...@@ -64,6 +86,11 @@ static void ota_stream_abort_internal(void) ...@@ -64,6 +86,11 @@ static void ota_stream_abort_internal(void)
ESP_LOGW(tag_s, "OTA session aborted"); ESP_LOGW(tag_s, "OTA session aborted");
} }
/* 如果已写入部分数据(>4KB),擦除分区避免启动误用 */
if (session_s.written_size > 4096 && session_s.partition != NULL) {
ota_stream_erase_partition();
}
session_s.active = false; session_s.active = false;
session_s.handle = 0; session_s.handle = 0;
session_s.partition = NULL; session_s.partition = NULL;
...@@ -82,6 +109,11 @@ bool ota_binary_stream_is_active(void) ...@@ -82,6 +109,11 @@ bool ota_binary_stream_is_active(void)
return session_s.active; return session_s.active;
} }
uint32_t ota_binary_stream_get_written_bytes(void)
{
return (uint32_t)session_s.written_size;
}
esp_err_t ota_binary_stream_init(ota_transport_type_t transport, esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
ota_binary_stream_status_callback_t status_callback, ota_binary_stream_status_callback_t status_callback,
const char *target_version) const char *target_version)
...@@ -182,7 +214,9 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len) ...@@ -182,7 +214,9 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
static int ota_stream_handle_data(const uint8_t *data, uint16_t len) static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
{ {
if (!session_s.active) { if (!session_s.active) {
ota_stream_send_error("chunk", "no_session"); /* 仅 NAK,不结束 OTA:可能 BEGIN 未到或 App 重传;避免 done 风暴 */
ESP_LOGW(tag_s, "OTA DATA without active session (armed=%d)",
(int)ota_offer_is_armed());
return -1; return -1;
} }
...@@ -221,13 +255,15 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len) ...@@ -221,13 +255,15 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
session_s.written_size += payload_len; session_s.written_size += payload_len;
/* 每 8192 字节发送一次进度,或最后一次 */ /* BLE 调试 JSON;UART OTA 期间 UART1 只走 OT 帧,不发 chunk 状态 JSON */
if (session_s.transport != OTA_TRANSPORT_UART) {
if (session_s.written_size - session_s.last_reported_size >= 8192 || if (session_s.written_size - session_s.last_reported_size >= 8192 ||
session_s.written_size >= session_s.expected_size) { session_s.written_size >= session_s.expected_size) {
ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}", ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size); (unsigned)session_s.written_size);
session_s.last_reported_size = session_s.written_size; session_s.last_reported_size = session_s.written_size;
} }
}
return 0; return 0;
} }
...@@ -236,7 +272,7 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len) ...@@ -236,7 +272,7 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
static int ota_stream_handle_end(void) static int ota_stream_handle_end(void)
{ {
if (!session_s.active) { if (!session_s.active) {
ota_stream_send_error("end", "no_session"); ESP_LOGW(tag_s, "OTA END without active session");
return -1; return -1;
} }
...@@ -270,8 +306,8 @@ static int ota_stream_handle_end(void) ...@@ -270,8 +306,8 @@ static int ota_stream_handle_end(void)
return -4; return -4;
} }
ESP_LOGI(tag_s, "OTA successful, written %u bytes", ESP_LOGI(tag_s, "OTA successful, written %u bytes, next boot from %s",
(unsigned)session_s.written_size); (unsigned)session_s.written_size, session_s.partition->label);
/* 通过OTA管理器结束会话 */ /* 通过OTA管理器结束会话 */
ota_manager_end_session(); ota_manager_end_session();
...@@ -281,9 +317,9 @@ static int ota_stream_handle_end(void) ...@@ -281,9 +317,9 @@ static int ota_stream_handle_end(void)
ota_offer_notify_done(true, (uint32_t)session_s.written_size, NULL); ota_offer_notify_done(true, (uint32_t)session_s.written_size, NULL);
/* 留时间把 message_type=1002 结果 JSON 发完再重启 */ /* notify_done 内已连发 OTA_DONE_REPORT_COUNT 次 1002/done,再等 DMA 发完后重启切分区 */
ESP_LOGI(tag_s, "Rebooting in 1 second..."); vTaskDelay(pdMS_TO_TICKS(OTA_DONE_REBOOT_DELAY_MS));
vTaskDelay(pdMS_TO_TICKS(1000)); ESP_LOGI(tag_s, "Rebooting to %s...", session_s.partition->label);
esp_restart(); esp_restart();
return 0; return 0;
......
...@@ -63,6 +63,9 @@ bool ota_binary_stream_is_active(void); ...@@ -63,6 +63,9 @@ bool ota_binary_stream_is_active(void);
*/ */
void ota_binary_stream_abort(void); void ota_binary_stream_abort(void);
/** 当前会话已写入字节数(未 begin 时为 0) */
uint32_t ota_binary_stream_get_written_bytes(void);
/** /**
* @brief 获取 OTA 状态 JSON 字符串 * @brief 获取 OTA 状态 JSON 字符串
* 用于主动查询OTA状态 * 用于主动查询OTA状态
......
...@@ -23,6 +23,7 @@ static const char *tag_s = "OTA_MGR"; ...@@ -23,6 +23,7 @@ static const char *tag_s = "OTA_MGR";
#define OTA_MGR_KEY_TYPE "type" #define OTA_MGR_KEY_TYPE "type"
#define OTA_MGR_KEY_TARGET_VER "tgt_ver" #define OTA_MGR_KEY_TARGET_VER "tgt_ver"
#define OTA_MGR_KEY_SOURCE_VER "src_ver" #define OTA_MGR_KEY_SOURCE_VER "src_ver"
#define OTA_MGR_KEY_SOURCE_PART "src_part"
#define OTA_MGR_KEY_TIMESTAMP "timestamp" #define OTA_MGR_KEY_TIMESTAMP "timestamp"
#define OTA_MGR_KEY_BOOT_COUNT "boot_cnt" #define OTA_MGR_KEY_BOOT_COUNT "boot_cnt"
#define OTA_MGR_KEY_MAX_ATTEMPTS "max_retry" #define OTA_MGR_KEY_MAX_ATTEMPTS "max_retry"
...@@ -68,6 +69,11 @@ static esp_err_t load_session_from_nvs(void) ...@@ -68,6 +69,11 @@ static esp_err_t load_session_from_nvs(void)
len = sizeof(current_session_s.source_version); len = sizeof(current_session_s.source_version);
nvs_get_str(handle, OTA_MGR_KEY_SOURCE_VER, current_session_s.source_version, &len); nvs_get_str(handle, OTA_MGR_KEY_SOURCE_VER, current_session_s.source_version, &len);
len = sizeof(current_session_s.source_partition);
if (nvs_get_str(handle, OTA_MGR_KEY_SOURCE_PART, current_session_s.source_partition, &len) != ESP_OK) {
current_session_s.source_partition[0] = '\0';
}
nvs_get_u32(handle, OTA_MGR_KEY_TIMESTAMP, &current_session_s.timestamp); 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_BOOT_COUNT, &current_session_s.boot_count);
nvs_get_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, &current_session_s.max_boot_attempts); nvs_get_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, &current_session_s.max_boot_attempts);
...@@ -100,6 +106,7 @@ static esp_err_t save_session_to_nvs(void) ...@@ -100,6 +106,7 @@ static esp_err_t save_session_to_nvs(void)
nvs_set_u8(handle, OTA_MGR_KEY_TYPE, (uint8_t)current_session_s.type); 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_TARGET_VER, current_session_s.target_version);
nvs_set_str(handle, OTA_MGR_KEY_SOURCE_VER, current_session_s.source_version); nvs_set_str(handle, OTA_MGR_KEY_SOURCE_VER, current_session_s.source_version);
nvs_set_str(handle, OTA_MGR_KEY_SOURCE_PART, current_session_s.source_partition);
nvs_set_u32(handle, OTA_MGR_KEY_TIMESTAMP, current_session_s.timestamp); 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_BOOT_COUNT, current_session_s.boot_count);
nvs_set_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, current_session_s.max_boot_attempts); nvs_set_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, current_session_s.max_boot_attempts);
...@@ -153,6 +160,16 @@ static bool partition_is_same(const esp_partition_t *a, const esp_partition_t *b ...@@ -153,6 +160,16 @@ static bool partition_is_same(const esp_partition_t *a, const esp_partition_t *b
return (a->address == b->address) && (a->size == b->size); return (a->address == b->address) && (a->size == b->size);
} }
/** 传输失败或中断后仍停留在升级前分区(无需切 boot) */
static bool still_on_source_partition(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
if (running == NULL || current_session_s.source_partition[0] == '\0') {
return false;
}
return strcmp(running->label, current_session_s.source_partition) == 0;
}
esp_err_t ota_manager_init(void) esp_err_t ota_manager_init(void)
{ {
...@@ -185,45 +202,37 @@ esp_err_t ota_manager_init(void) ...@@ -185,45 +202,37 @@ esp_err_t ota_manager_init(void)
/* 有OTA会话,检查状态 */ /* 有OTA会话,检查状态 */
if (current_session_s.state == OTA_STATE_PENDING_VERIFY) { if (current_session_s.state == OTA_STATE_PENDING_VERIFY) {
/* OTA已完成,这是新固件第一次启动 */ /*
ESP_LOGI(tag_s, "New firmware boot detected, verifying..."); * esp_ota_end 已校验镜像;能从此分区启动即包有效。
* 成功条件:运行分区与升级前不同(已从另一 OTA 槽启动)。
*/
ESP_LOGI(tag_s, "New firmware boot detected, verifying partition switch...");
/* 增加启动计数 */
current_session_s.boot_count++; current_session_s.boot_count++;
save_session_to_nvs(); save_session_to_nvs();
/* bool switched = false;
* 检查版本是否匹配: if (running && current_session_s.source_partition[0] != '\0') {
* 1) 目标版本与当前运行版本一致(强校验); switched = (strcmp(running->label, current_session_s.source_partition) != 0);
* 2) 若目标版本未知/旧流程未携带目标版本,只要当前版本已不同于 source_version, } else {
* 也视为新固件已生效(避免 BLE/UART OTA 成功后误判为 mismatch)。 /* 旧会话无 source_partition:bootloader 能加载即视为成功 */
*/ switched = (running != NULL);
bool version_match = false;
if (app_desc) {
if (strcmp(app_desc->version, current_session_s.target_version) == 0) {
version_match = true;
} else if (current_session_s.source_version[0] != '\0' &&
strcmp(app_desc->version, current_session_s.source_version) != 0) {
version_match = true;
}
} }
if (version_match) { if (switched) {
ESP_LOGI(tag_s, "Version matches: %s", app_desc->version); ESP_LOGI(tag_s, "OTA OK: boot from %s (was %s), version %s",
running ? running->label : "?",
current_session_s.source_partition[0] ? current_session_s.source_partition : "?",
app_desc ? app_desc->version : "?");
/* 标记为成功 */
current_session_s.state = OTA_STATE_SUCCESS; current_session_s.state = OTA_STATE_SUCCESS;
save_session_to_nvs(); save_session_to_nvs();
if (app_desc) {
ota_manager_update_stored_version(app_desc->version); ota_manager_update_stored_version(app_desc->version);
}
/* 延迟一段时间后清除会话(给应用层确认的时间) */
/* 应用层需要调用 ota_manager_mark_success() 来最终确认 */
} else { } else {
ESP_LOGW(tag_s, "Version mismatch! Expected: %s, Got: %s", ESP_LOGW(tag_s, "OTA verify failed: still on %s",
current_session_s.target_version, running ? running->label : "unknown");
app_desc ? app_desc->version : "unknown");
/* 版本不匹配,可能是回退或其他问题,清除会话 */
clear_session_nvs(); clear_session_nvs();
} }
...@@ -242,15 +251,20 @@ esp_err_t ota_manager_init(void) ...@@ -242,15 +251,20 @@ esp_err_t ota_manager_init(void)
/* OTA进行中但设备重启了,可能是断电或崩溃 */ /* OTA进行中但设备重启了,可能是断电或崩溃 */
ESP_LOGW(tag_s, "OTA was in progress but interrupted"); ESP_LOGW(tag_s, "OTA was in progress but interrupted");
/* 标记为失败 */ if (still_on_source_partition()) {
ESP_LOGI(tag_s, "Still on %s after interrupt, clear session",
current_session_s.source_partition);
clear_session_nvs();
return ESP_OK;
}
current_session_s.state = OTA_STATE_FAILED; current_session_s.state = OTA_STATE_FAILED;
save_session_to_nvs(); save_session_to_nvs();
/* 尝试回退 */
if (current_session_s.boot_count >= 1) { if (current_session_s.boot_count >= 1) {
ESP_LOGW(tag_s, "OTA interrupted, triggering rollback"); ESP_LOGW(tag_s, "OTA interrupted, triggering rollback");
ota_manager_rollback(); ota_manager_rollback();
return ESP_FAIL; /* 设备将重启 */ return ESP_FAIL;
} }
} else if (current_session_s.state == OTA_STATE_SUCCESS) { } else if (current_session_s.state == OTA_STATE_SUCCESS) {
...@@ -269,15 +283,25 @@ esp_err_t ota_manager_init(void) ...@@ -269,15 +283,25 @@ esp_err_t ota_manager_init(void)
} }
} else if (current_session_s.state == OTA_STATE_FAILED) { } else if (current_session_s.state == OTA_STATE_FAILED) {
/* 上次OTA已标记为失败 */
ESP_LOGW(tag_s, "Previous OTA marked as failed"); ESP_LOGW(tag_s, "Previous OTA marked as failed");
/* 检查是否应该回退 */ if (still_on_source_partition()) {
if (is_running_ota_partition()) { ESP_LOGI(tag_s, "Still on pre-OTA partition %s, clear failed session",
ESP_LOGW(tag_s, "Triggering rollback to previous version"); current_session_s.source_partition);
clear_session_nvs();
return ESP_OK;
}
const esp_partition_t *boot = esp_ota_get_boot_partition();
if (running && boot && !partition_is_same(boot, running)) {
ESP_LOGW(tag_s, "Boot partition %s != running %s, try rollback",
boot->label, running->label);
ota_manager_rollback(); ota_manager_rollback();
return ESP_FAIL; /* 设备将重启 */ return ESP_FAIL;
} }
ESP_LOGI(tag_s, "No rollback needed, clear failed session");
clear_session_nvs();
} }
return ESP_OK; return ESP_OK;
...@@ -290,6 +314,7 @@ esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version) ...@@ -290,6 +314,7 @@ esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version)
} }
const esp_app_desc_t *app_desc = esp_app_get_description(); const esp_app_desc_t *app_desc = esp_app_get_description();
const esp_partition_t *running = esp_ota_get_running_partition();
memset(&current_session_s, 0, sizeof(current_session_s)); memset(&current_session_s, 0, sizeof(current_session_s));
current_session_s.state = OTA_STATE_IN_PROGRESS; current_session_s.state = OTA_STATE_IN_PROGRESS;
...@@ -298,14 +323,19 @@ esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version) ...@@ -298,14 +323,19 @@ esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version)
if (app_desc) { if (app_desc) {
strncpy(current_session_s.source_version, app_desc->version, sizeof(current_session_s.source_version) - 1); strncpy(current_session_s.source_version, app_desc->version, sizeof(current_session_s.source_version) - 1);
} }
if (running) {
strncpy(current_session_s.source_partition, running->label,
sizeof(current_session_s.source_partition) - 1);
}
current_session_s.timestamp = (uint32_t)(esp_timer_get_time() / 1000000ULL); current_session_s.timestamp = (uint32_t)(esp_timer_get_time() / 1000000ULL);
current_session_s.boot_count = 0; current_session_s.boot_count = 0;
current_session_s.max_boot_attempts = OTA_MAX_BOOT_ATTEMPTS; current_session_s.max_boot_attempts = OTA_MAX_BOOT_ATTEMPTS;
session_loaded_s = true; session_loaded_s = true;
ESP_LOGI(tag_s, "OTA session started: type=%d, %s -> %s", ESP_LOGI(tag_s, "OTA session started: type=%d, %s@%s -> %s",
type, current_session_s.source_version, target_version); type, current_session_s.source_version,
current_session_s.source_partition, target_version);
return save_session_to_nvs(); return save_session_to_nvs();
} }
...@@ -419,6 +449,7 @@ esp_err_t ota_manager_rollback(void) ...@@ -419,6 +449,7 @@ esp_err_t ota_manager_rollback(void)
if (prev_partition == NULL) { if (prev_partition == NULL) {
ESP_LOGE(tag_s, "No previous partition found, cannot rollback"); ESP_LOGE(tag_s, "No previous partition found, cannot rollback");
clear_session_nvs();
return ESP_ERR_NOT_FOUND; return ESP_ERR_NOT_FOUND;
} }
if (partition_is_same(prev_partition, running)) { if (partition_is_same(prev_partition, running)) {
...@@ -431,7 +462,8 @@ esp_err_t ota_manager_rollback(void) ...@@ -431,7 +462,8 @@ esp_err_t ota_manager_rollback(void)
/* 设置启动分区为上一版本 */ /* 设置启动分区为上一版本 */
esp_err_t err = esp_ota_set_boot_partition(prev_partition); esp_err_t err = esp_ota_set_boot_partition(prev_partition);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to set boot partition: %s", esp_err_to_name(err)); ESP_LOGE(tag_s, "Failed to set boot partition: %s, clear session", esp_err_to_name(err));
clear_session_nvs();
return err; return err;
} }
......
...@@ -38,6 +38,7 @@ typedef struct { ...@@ -38,6 +38,7 @@ typedef struct {
ota_type_t type; /* OTA类型 */ ota_type_t type; /* OTA类型 */
char target_version[32]; /* 目标版本号 */ char target_version[32]; /* 目标版本号 */
char source_version[32]; /* 升级前版本号 */ char source_version[32]; /* 升级前版本号 */
char source_partition[16]; /* 升级前运行分区标签,如 ota_0 */
uint32_t timestamp; /* OTA开始时间戳 */ uint32_t timestamp; /* OTA开始时间戳 */
uint8_t boot_count; /* 新固件启动计数 */ uint8_t boot_count; /* 新固件启动计数 */
uint8_t max_boot_attempts; /* 最大尝试次数 */ uint8_t max_boot_attempts; /* 最大尝试次数 */
......
#include "heart_payload.h" #include "heart_payload.h"
#include "device_nvs.h"
#include "betteryread.h" #include "betteryread.h"
#include "cJSON.h" #include "cJSON.h"
#include "sdkconfig.h" #include "sdkconfig.h"
...@@ -42,7 +43,7 @@ char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool ...@@ -42,7 +43,7 @@ char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool
cJSON_AddStringToObject(body, "ip", ip); cJSON_AddStringToObject(body, "ip", ip);
} }
const char *dev = (device_id != NULL) ? device_id : ""; const char *dev = device_id_plain(device_id);
char voltage_s[16]; char voltage_s[16];
snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v()); snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v());
......
...@@ -107,6 +107,15 @@ static void ota_frame_send_simple(uint8_t op, uint16_t seq, const uint8_t *paylo ...@@ -107,6 +107,15 @@ static void ota_frame_send_simple(uint8_t op, uint16_t seq, const uint8_t *paylo
uint8_t buf[OTA_FRAME_BUF_SIZE]; uint8_t buf[OTA_FRAME_BUF_SIZE];
uint16_t out_len = 0; uint16_t out_len = 0;
if (ota_frame_build(op, seq, payload, plen, buf, sizeof(buf), &out_len) == 0) { if (ota_frame_build(op, seq, payload, plen, buf, sizeof(buf), &out_len) == 0) {
if (op == OTA_FRAME_OP_ACK && plen >= 2) {
ESP_LOGD(tag_s, "TX ACK seq=%u data=0x%02X status=%u",
(unsigned)seq, payload[0], (unsigned)payload[1]);
} else if (op == OTA_FRAME_OP_ACK && plen == 1) {
ESP_LOGI(tag_s, "TX ACK seq=%u status=%u", (unsigned)seq, (unsigned)payload[0]);
} else if (op == OTA_FRAME_OP_NAK) {
ESP_LOGW(tag_s, "TX NAK seq=%u err=%u", (unsigned)seq,
plen > 0 ? (unsigned)payload[0] : 0U);
}
ota_frame_tx_bytes(buf, out_len); ota_frame_tx_bytes(buf, out_len);
} }
} }
...@@ -117,6 +126,18 @@ static void ota_frame_send_ack(uint16_t seq, uint8_t data_byte, uint8_t status) ...@@ -117,6 +126,18 @@ static void ota_frame_send_ack(uint16_t seq, uint8_t data_byte, uint8_t status)
ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, pl, 2); ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, pl, 2);
} }
static void ota_frame_send_chunk_ack(uint16_t seq, uint16_t chunk_len,
uint8_t last_byte, uint8_t status)
{
uint8_t pl[4] = {
(uint8_t)(chunk_len & 0xFF),
(uint8_t)((chunk_len >> 8) & 0xFF),
last_byte,
status,
};
ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, pl, 4);
}
static void ota_frame_send_status_ack(uint16_t seq, uint8_t status) static void ota_frame_send_status_ack(uint16_t seq, uint8_t status)
{ {
ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, &status, 1); ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, &status, 1);
...@@ -127,6 +148,24 @@ static void ota_frame_send_nak(uint16_t seq, uint8_t err_code) ...@@ -127,6 +148,24 @@ static void ota_frame_send_nak(uint16_t seq, uint8_t err_code)
ota_frame_send_simple(OTA_FRAME_OP_NAK, seq, &err_code, 1); ota_frame_send_simple(OTA_FRAME_OP_NAK, seq, &err_code, 1);
} }
static const char *ota_frame_op_name(uint8_t op)
{
switch (op) {
case OTA_FRAME_OP_BEGIN:
return "BEGIN";
case OTA_FRAME_OP_DATA:
return "DATA";
case OTA_FRAME_OP_END:
return "END";
case OTA_FRAME_OP_ACK:
return "ACK";
case OTA_FRAME_OP_NAK:
return "NAK";
default:
return "UNKNOWN";
}
}
static int map_stream_rc_to_ack(int rc) static int map_stream_rc_to_ack(int rc)
{ {
if (rc == 0) { if (rc == 0) {
...@@ -152,6 +191,14 @@ static void handle_complete_frame(void) ...@@ -152,6 +191,14 @@ static void handle_complete_frame(void)
const uint8_t *p = s_parser.payload; const uint8_t *p = s_parser.payload;
uint16_t plen = s_parser.payload_len; uint16_t plen = s_parser.payload_len;
if (s_parser.op == OTA_FRAME_OP_DATA) {
ESP_LOGD(tag_s, "RX %s seq=%u plen=%u", ota_frame_op_name(s_parser.op),
(unsigned)s_parser.seq, (unsigned)plen);
} else {
ESP_LOGI(tag_s, "RX %s seq=%u plen=%u", ota_frame_op_name(s_parser.op),
(unsigned)s_parser.seq, (unsigned)plen);
}
switch (s_parser.op) { switch (s_parser.op) {
case OTA_FRAME_OP_BEGIN: { case OTA_FRAME_OP_BEGIN: {
if (plen != 4) { if (plen != 4) {
...@@ -164,14 +211,20 @@ static void handle_complete_frame(void) ...@@ -164,14 +211,20 @@ static void handle_complete_frame(void)
break; break;
} }
case OTA_FRAME_OP_DATA: { case OTA_FRAME_OP_DATA: {
if (plen != 1) { if (plen < 1 || plen > OTA_FRAME_MAX_PAYLOAD) {
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_LEN); ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_LEN);
return; return;
} }
uint8_t b = p[0]; uint8_t pkt[1 + OTA_FRAME_MAX_PAYLOAD];
uint8_t pkt[2] = { OTA_OP_DATA, b }; pkt[0] = OTA_OP_DATA;
int rc = ota_binary_stream_process_packet(pkt, 2); memcpy(pkt + 1, p, plen);
ota_frame_send_ack(s_parser.seq, b, (uint8_t)map_stream_rc_to_ack(rc)); int rc = ota_binary_stream_process_packet(pkt, (uint16_t)(1 + plen));
uint8_t ack_status = (uint8_t)map_stream_rc_to_ack(rc);
if (plen == 1) {
ota_frame_send_ack(s_parser.seq, p[0], ack_status);
} else {
ota_frame_send_chunk_ack(s_parser.seq, plen, p[plen - 1], ack_status);
}
break; break;
} }
case OTA_FRAME_OP_END: { case OTA_FRAME_OP_END: {
......
...@@ -7,12 +7,15 @@ ...@@ -7,12 +7,15 @@
* - CRC8: 从 VER 到 PAYLOAD 末字节逐字节 XOR * - CRC8: 从 VER 到 PAYLOAD 末字节逐字节 XOR
* - TAIL: 0x16 * - TAIL: 0x16
* *
* 数据 OP_DATA 建议每帧 1 字节固件数据;设备回 OP_ACK,载荷 [data][status], * OP_DATA 载荷 1~MAX 字节固件数据(UART 最大见 menuconfig);设备回 OP_ACK:
* status=0 成功,非 0 需重传同一 SEQ 帧。 * - 1 字节载荷:ACK [echo_byte][status]
* - 多块载荷:ACK [len_lo][len_hi][last_byte][status]
* status=0 成功,非 0 须重传同一 SEQ 帧。
*/ */
#ifndef OTA_FRAME_PROTOCOL_H #ifndef OTA_FRAME_PROTOCOL_H
#define OTA_FRAME_PROTOCOL_H #define OTA_FRAME_PROTOCOL_H
#include "sdkconfig.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
...@@ -35,7 +38,14 @@ ...@@ -35,7 +38,14 @@
#define OTA_ACK_ERR_BAD_LEN 0x05 #define OTA_ACK_ERR_BAD_LEN 0x05
#define OTA_ACK_ERR_BAD_OP 0x06 #define OTA_ACK_ERR_BAD_OP 0x06
#if CONFIG_APP_LINK_BLE
#define OTA_FRAME_MAX_PAYLOAD 240 #define OTA_FRAME_MAX_PAYLOAD 240
#elif defined(CONFIG_APP_OTA_UART_MAX_CHUNK)
#define OTA_FRAME_MAX_PAYLOAD CONFIG_APP_OTA_UART_MAX_CHUNK
#else
#define OTA_FRAME_MAX_PAYLOAD 1024
#endif
#define OTA_FRAME_DATA_CHUNK_RECOMMEND 128
#define OTA_FRAME_BUF_SIZE (8 + OTA_FRAME_MAX_PAYLOAD + 2) #define OTA_FRAME_BUF_SIZE (8 + OTA_FRAME_MAX_PAYLOAD + 2)
typedef void (*ota_frame_tx_fn)(const uint8_t *frame, uint16_t len, void *ctx); typedef void (*ota_frame_tx_fn)(const uint8_t *frame, uint16_t len, void *ctx);
......
#include "ota_offer_protocol.h" #include "ota_offer_protocol.h"
#include "ota_binary_stream.h"
#include "ota_uart_tune.h"
#include "ota_frame_protocol.h"
#include "device_nvs.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include <stdio.h> #include <stdio.h>
...@@ -20,6 +24,11 @@ bool ota_offer_is_armed(void) ...@@ -20,6 +24,11 @@ bool ota_offer_is_armed(void)
return s_ota_armed_s; return s_ota_armed_s;
} }
bool ota_offer_session_busy(void)
{
return s_ota_armed_s || ota_binary_stream_is_active();
}
void ota_offer_disarm(void) void ota_offer_disarm(void)
{ {
s_ota_armed_s = false; s_ota_armed_s = false;
...@@ -27,8 +36,25 @@ void ota_offer_disarm(void) ...@@ -27,8 +36,25 @@ void ota_offer_disarm(void)
static void ota_offer_format_device_id(char *out, size_t cap, const char *device_id) static void ota_offer_format_device_id(char *out, size_t cap, const char *device_id)
{ {
const char *dev = (device_id != NULL) ? device_id : ""; snprintf(out, cap, "%s", device_id_plain(device_id));
snprintf(out, cap, "%s", dev); }
static void ota_offer_body_add_ota_tune(cJSON *body)
{
#if CONFIG_APP_LINK_UART
int baud = CONFIG_APP_UART_LINK_BAUDRATE;
#else
int baud = 0;
#endif
uint16_t chunk = ota_uart_tune_chunk_for_baud(baud);
uint32_t ack_ms = ota_uart_tune_ack_timeout_ms(baud > 0 ? baud : 115200, chunk);
cJSON_AddNumberToObject(body, "ota_chunk", chunk);
cJSON_AddNumberToObject(body, "ota_chunk_max", OTA_FRAME_MAX_PAYLOAD);
cJSON_AddNumberToObject(body, "ota_ack_timeout_ms", (double)ack_ms);
#if CONFIG_APP_LINK_UART
cJSON_AddNumberToObject(body, "uart_baud", baud);
#endif
} }
static char *ota_offer_build(int message_type, cJSON *body) static char *ota_offer_build(int message_type, cJSON *body)
...@@ -68,6 +94,25 @@ char *ota_offer_query_json_malloc(const char *device_id) ...@@ -68,6 +94,25 @@ char *ota_offer_query_json_malloc(const char *device_id)
cJSON_AddStringToObject(body, "phase", "query"); cJSON_AddStringToObject(body, "phase", "query");
cJSON_AddStringToObject(body, "device_ID", id_full); cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION); cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
ota_offer_body_add_ota_tune(body);
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
}
char *ota_offer_active_json_malloc(const char *device_id)
{
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
char id_full[64];
ota_offer_format_device_id(id_full, sizeof(id_full), device_id);
cJSON_AddStringToObject(body, "phase", "active");
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
ota_offer_body_add_ota_tune(body);
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body); return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
} }
...@@ -92,6 +137,8 @@ char *ota_offer_result_json_malloc(const char *device_id, ...@@ -92,6 +137,8 @@ char *ota_offer_result_json_malloc(const char *device_id,
cJSON_AddNumberToObject(body, "written", (double)bytes_written); cJSON_AddNumberToObject(body, "written", (double)bytes_written);
if (!success && err_step != NULL && err_step[0] != '\0') { if (!success && err_step != NULL && err_step[0] != '\0') {
cJSON_AddStringToObject(body, "err", err_step); cJSON_AddStringToObject(body, "err", err_step);
} else {
cJSON_AddStringToObject(body, "err", "");
} }
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body); return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
...@@ -147,3 +194,16 @@ void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err ...@@ -147,3 +194,16 @@ void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err
s_done_cb_s(success, bytes_written, err_step); s_done_cb_s(success, bytes_written, err_step);
} }
} }
void ota_offer_report_abort(const char *err_step)
{
if (!ota_offer_session_busy()) {
return;
}
uint32_t written = ota_binary_stream_get_written_bytes();
if (ota_binary_stream_is_active()) {
ota_binary_stream_abort();
}
ota_offer_notify_done(false, written, err_step);
}
...@@ -11,6 +11,13 @@ ...@@ -11,6 +11,13 @@
/** 手机 → 设备:同意开始二进制 OTA(随后发 0x01/0x02/0x03) */ /** 手机 → 设备:同意开始二进制 OTA(随后发 0x01/0x02/0x03) */
#define OTA_MSG_PHONE_OTA_START 0 #define OTA_MSG_PHONE_OTA_START 0
/** OTA 结束 1002/done 重复发送次数(提高手机收包成功率后再重启切分区) */
#define OTA_DONE_REPORT_COUNT 3
/** 各次 done JSON 之间的间隔(ms) */
#define OTA_DONE_REPORT_GAP_MS 100
/** 发完 done 重复包后、重启前的额外等待(ms,留给 UART DMA 发完) */
#define OTA_DONE_REBOOT_DELAY_MS 400
typedef void (*ota_offer_done_cb_t)(bool success, uint32_t bytes_written, const char *err_step); typedef void (*ota_offer_done_cb_t)(bool success, uint32_t bytes_written, const char *err_step);
/** 上电/链路启动时调用,允许再次发送询问 JSON */ /** 上电/链路启动时调用,允许再次发送询问 JSON */
...@@ -39,12 +46,24 @@ bool ota_offer_parse_phone_response(cJSON *root); ...@@ -39,12 +46,24 @@ bool ota_offer_parse_phone_response(cJSON *root);
/** 构造询问 JSON:head.message_type=1002, body.phase=query */ /** 构造询问 JSON:head.message_type=1002, body.phase=query */
char *ota_offer_query_json_malloc(const char *device_id); char *ota_offer_query_json_malloc(const char *device_id);
/** 构造 OTA 进行中 JSON:head.message_type=1002, body.phase=active(替代心跳) */
char *ota_offer_active_json_malloc(const char *device_id);
/** 构造结果 JSON:head.message_type=1002, body.phase=done */ /** 构造结果 JSON:head.message_type=1002, body.phase=done */
char *ota_offer_result_json_malloc(const char *device_id, char *ota_offer_result_json_malloc(const char *device_id,
bool success, bool success,
uint32_t bytes_written, uint32_t bytes_written,
const char *err_step); const char *err_step);
/** OTA 是否进行中(已武装或正在写 Flash) */
bool ota_offer_session_busy(void);
/**
* 链路异常中止:若 OTA 进行中则上报 1002/done(ok=0) 并解除武装。
* @param err_step 错误码,如 timeout / reset / disconnect
*/
void ota_offer_report_abort(const char *err_step);
void ota_offer_set_done_callback(ota_offer_done_cb_t cb); void ota_offer_set_done_callback(ota_offer_done_cb_t cb);
#endif #endif
#include "ota_uart_tune.h"
uint16_t ota_uart_tune_chunk_for_baud(int baud)
{
if (baud <= 0) {
return OTA_FRAME_DATA_CHUNK_RECOMMEND;
}
/*
* chunk ≈ baud/512 - wire_overhead:高波特率大块,减少 USB 往返次数。
* 115200→~201B,460800→~876B,921600→顶满 MAX(通常 1024)。
*/
int64_t chunk = (int64_t)baud / 512 - OTA_UART_WIRE_OVERHEAD_BYTES;
if (chunk < 32) {
chunk = 32;
}
if (chunk > OTA_FRAME_MAX_PAYLOAD) {
chunk = OTA_FRAME_MAX_PAYLOAD;
}
return (uint16_t)chunk;
}
uint32_t ota_uart_tune_ack_timeout_ms(int baud, uint16_t chunk)
{
if (baud <= 0) {
baud = 115200;
}
uint32_t wire_ms = (uint32_t)(((uint64_t)(chunk + OTA_UART_WIRE_OVERHEAD_BYTES) * 10 * 1000) /
(uint32_t)baud);
uint32_t t = wire_ms * 3 + 800;
if (t < 1200) {
t = 1200;
}
if (t > 8000) {
t = 8000;
}
return t;
}
uint32_t ota_uart_tune_transfer_idle_ms(int baud)
{
uint16_t chunk = ota_uart_tune_chunk_for_baud(baud);
uint32_t t = ota_uart_tune_ack_timeout_ms(baud, chunk) + 1000;
if (t < 2000) {
t = 2000;
}
if (t > 12000) {
t = 12000;
}
return t;
}
#ifndef OTA_UART_TUNE_H
#define OTA_UART_TUNE_H
#include <stdint.h>
#include "ota_frame_protocol.h"
/** OT 帧线传固定开销:DATA 帧 10B + 多块 ACK 14B */
#define OTA_UART_WIRE_OVERHEAD_BYTES 24
/**
* 按波特率计算推荐 DATA 块大小(32~OTA_FRAME_MAX_PAYLOAD,UART 默认最大 1024)。
* baud<=0(BLE)时返回 OTA_FRAME_DATA_CHUNK_RECOMMEND。
*/
uint16_t ota_uart_tune_chunk_for_baud(int baud);
/** App 等 ACK 建议超时(ms),与块大小、波特率对齐 */
uint32_t ota_uart_tune_ack_timeout_ms(int baud, uint16_t chunk);
/** 固件 OTA 传输阶段无手机 RX 超时(ms),应 ≥ App ack 超时 + 余量 */
uint32_t ota_uart_tune_transfer_idle_ms(int baud);
#endif
...@@ -39,10 +39,10 @@ char *remote_control_stop_json_malloc(void) ...@@ -39,10 +39,10 @@ char *remote_control_stop_json_malloc(void)
cJSON_Delete(pwm); cJSON_Delete(pwm);
return NULL; return NULL;
} }
cJSON_AddNumberToObject(pwm, "speed", 0); cJSON_AddNumberToObject(pwm, "speed_mode", 0);
cJSON_AddNumberToObject(pwm, "direction", 0); cJSON_AddNumberToObject(pwm, "speed_val", 0);
cJSON_AddNumberToObject(pwm, "mode", 0); cJSON_AddNumberToObject(pwm, "steer_mode", 0);
cJSON_AddNumberToObject(pwm, "val", 0); cJSON_AddNumberToObject(pwm, "steer_val", 0);
cJSON_AddItemToObject(body, "pwm_ctrl", pwm); cJSON_AddItemToObject(body, "pwm_ctrl", pwm);
cJSON *root = cJSON_CreateObject(); cJSON *root = cJSON_CreateObject();
...@@ -82,12 +82,16 @@ static void remote_control_stop_notify_push_once(void) ...@@ -82,12 +82,16 @@ static void remote_control_stop_notify_push_once(void)
static void remote_control_stop_notify_task(void *param) static void remote_control_stop_notify_task(void *param)
{ {
(void)param; (void)param;
while (s_stop_notify_active) { for (int i = 0; i < RC_STOP_NOTIFY_BURST_COUNT; i++) {
if (s_timeout_stopped) { if (!s_stop_notify_active || !s_timeout_stopped) {
break;
}
remote_control_stop_notify_push_once(); remote_control_stop_notify_push_once();
if (i + 1 < RC_STOP_NOTIFY_BURST_COUNT) {
vTaskDelay(pdMS_TO_TICKS(RC_STOP_NOTIFY_BURST_INTERVAL_MS));
} }
vTaskDelay(pdMS_TO_TICKS(RC_STOP_NOTIFY_PERIOD_MS));
} }
s_stop_notify_active = false;
s_stop_notify_handle = NULL; s_stop_notify_handle = NULL;
vTaskDelete(NULL); vTaskDelete(NULL);
} }
...@@ -122,12 +126,111 @@ static bool remote_control_is_stop_pwm_json(cJSON *pwm_ctrl) ...@@ -122,12 +126,111 @@ static bool remote_control_is_stop_pwm_json(cJSON *pwm_ctrl)
if (!pwm_ctrl || !cJSON_IsObject(pwm_ctrl)) { if (!pwm_ctrl || !cJSON_IsObject(pwm_ctrl)) {
return false; return false;
} }
cJSON *speed_mode = cJSON_GetObjectItem(pwm_ctrl, "speed_mode");
cJSON *speed_val = cJSON_GetObjectItem(pwm_ctrl, "speed_val");
cJSON *steer_mode = cJSON_GetObjectItem(pwm_ctrl, "steer_mode");
cJSON *steer_val = cJSON_GetObjectItem(pwm_ctrl, "steer_val");
if (speed_mode && speed_val && steer_mode && steer_val &&
cJSON_IsNumber(speed_mode) && cJSON_IsNumber(speed_val) &&
cJSON_IsNumber(steer_mode) && cJSON_IsNumber(steer_val)) {
return (speed_mode->valueint == 0 && speed_val->valueint == 0 &&
steer_mode->valueint == 0 && steer_val->valueint == 0);
}
if (speed_val && steer_val && cJSON_IsNumber(speed_val) && cJSON_IsNumber(steer_val)) {
return (speed_val->valueint == 0 && steer_val->valueint == 0);
}
cJSON *speed = cJSON_GetObjectItem(pwm_ctrl, "speed"); cJSON *speed = cJSON_GetObjectItem(pwm_ctrl, "speed");
cJSON *direction = cJSON_GetObjectItem(pwm_ctrl, "direction"); cJSON *direction = cJSON_GetObjectItem(pwm_ctrl, "direction");
if (!speed || !direction || !cJSON_IsNumber(speed) || !cJSON_IsNumber(direction)) { if (speed && direction && cJSON_IsNumber(speed) && cJSON_IsNumber(direction)) {
return (speed->valueint == 0 && direction->valueint == 0);
}
cJSON *mode = cJSON_GetObjectItem(pwm_ctrl, "mode");
cJSON *val = cJSON_GetObjectItem(pwm_ctrl, "val");
if (mode && val && cJSON_IsNumber(mode) && cJSON_IsNumber(val)) {
return (mode->valueint == 0 && val->valueint == 0);
}
return false;
}
/** v1.0.2 融合 pwm_ctrl:speed_mode/speed_val + steer_mode/steer_val */
static bool remote_control_apply_pwm_fused(cJSON *pwm_ctrl)
{
cJSON *jsm = cJSON_GetObjectItem(pwm_ctrl, "speed_mode");
cJSON *jsv = cJSON_GetObjectItem(pwm_ctrl, "speed_val");
cJSON *jtm = cJSON_GetObjectItem(pwm_ctrl, "steer_mode");
cJSON *jtv = cJSON_GetObjectItem(pwm_ctrl, "steer_val");
if (!jsm || !jsv || !jtm || !jtv ||
!cJSON_IsNumber(jsm) || !cJSON_IsNumber(jsv) ||
!cJSON_IsNumber(jtm) || !cJSON_IsNumber(jtv)) {
return false; return false;
} }
return (speed->valueint == 0 && direction->valueint == 0);
int speed_mode = jsm->valueint;
int speed_val = jsv->valueint;
int steer_mode = jtm->valueint;
int steer_val = jtv->valueint;
/* 前后/左右互斥:每轴同一时刻至多一个方向 */
if (speed_mode != 0 && speed_mode != 1 && speed_mode != 2) {
ESP_LOGW(tag_s, "pwm_ctrl speed_mode=%d 非法(仅允许 0/1/2)", speed_mode);
return true;
}
if (steer_mode != 0 && steer_mode != 1 && steer_mode != 3) {
ESP_LOGW(tag_s, "pwm_ctrl steer_mode=%d 非法(仅允许 0/1/3)", steer_mode);
return true;
}
if (speed_val > 0 && speed_mode != 1 && speed_mode != 2) {
ESP_LOGW(tag_s, "pwm_ctrl 前后互斥:speed_val>0 时 speed_mode 须为 1(前) 或 2(后)");
return true;
}
if (steer_val > 0 && steer_mode != 1 && steer_mode != 3) {
ESP_LOGW(tag_s, "pwm_ctrl 左右互斥:steer_val>0 时 steer_mode 须为 1(左) 或 3(右)");
return true;
}
bool has_speed = (speed_val > 0) && (speed_mode == 1 || speed_mode == 2);
bool has_steer = (steer_val > 0) && (steer_mode == 1 || steer_mode == 3);
if (!has_speed && !has_steer) {
rc_vehicle_stop();
ESP_LOGD(tag_s, "PWM fused stop");
return true;
}
int mode;
int speed;
int steer;
if (has_speed && has_steer) {
if (speed_mode == 1 && steer_mode == 1) {
mode = 5;
} else if (speed_mode == 1 && steer_mode == 3) {
mode = 6;
} else if (speed_mode == 2 && steer_mode == 1) {
mode = 7;
} else if (speed_mode == 2 && steer_mode == 3) {
mode = 8;
} else {
ESP_LOGW(tag_s, "pwm_ctrl 无效 speed_mode/steer_mode 组合");
return true;
}
speed = speed_val;
steer = steer_val;
} else if (has_speed) {
mode = speed_mode;
speed = speed_val;
steer = 0;
} else {
mode = (steer_mode == 1) ? 3 : 4;
speed = steer_val;
steer = 0;
}
ESP_LOGI(tag_s, "PWM fused: sm %d sv %d tm %d tv %d -> mode %d speed %d steer %d",
speed_mode, speed_val, steer_mode, steer_val, mode, speed, steer);
rc_vehicle_control(mode, speed, steer);
return true;
} }
static void rc_watchdog_task(void *param) static void rc_watchdog_task(void *param)
...@@ -238,26 +341,27 @@ void remote_control_apply_json_root(cJSON *root, const char *json_raw) ...@@ -238,26 +341,27 @@ void remote_control_apply_json_root(cJSON *root, const char *json_raw)
cJSON *body = cJSON_GetObjectItem(root, "body"); cJSON *body = cJSON_GetObjectItem(root, "body");
if (m_type && cJSON_IsNumber(m_type) && body) { if (m_type && cJSON_IsNumber(m_type) && body) {
if (m_type->valueint == 4) { if (m_type->valueint == 3 || m_type->valueint == 4) {
cJSON *pin_ctrl = cJSON_GetObjectItem(body, "pin_setctrl"); 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"); cJSON *pwm_ctrl = cJSON_GetObjectItem(body, "pwm_ctrl");
if (remote_control_is_stop_pwm_json(pwm_ctrl)) { bool has_any_ctrl = false;
rc_vehicle_stop();
ESP_LOGD(tag_s, "停车 JSON (speed=0,direction=0)"); if (m_type->valueint == 4) {
return; ESP_LOGW(tag_s, "message_type=4 已废弃,请改用 message_type=3");
} }
/*
* pwm_ctrl(前后左右)与 pin_setctrl(GPIO)协议分离、低耦合:
* 先处理行走,再处理 pin;pin 号由 pin_setctrl 独立指定,不与行走字段混用。
*/
if (pwm_ctrl && cJSON_IsObject(pwm_ctrl)) { if (pwm_ctrl && cJSON_IsObject(pwm_ctrl)) {
has_any_ctrl = true;
if (remote_control_is_stop_pwm_json(pwm_ctrl)) {
rc_vehicle_stop();
ESP_LOGD(tag_s, "停车 JSON");
} else if (remote_control_apply_pwm_fused(pwm_ctrl)) {
/* 已按 v1.0.2 融合字段处理 */
} else {
cJSON *jm = cJSON_GetObjectItem(pwm_ctrl, "mode"); cJSON *jm = cJSON_GetObjectItem(pwm_ctrl, "mode");
cJSON *jt = cJSON_GetObjectItem(pwm_ctrl, "type"); cJSON *jt = cJSON_GetObjectItem(pwm_ctrl, "type");
cJSON *jv = cJSON_GetObjectItem(pwm_ctrl, "val"); cJSON *jv = cJSON_GetObjectItem(pwm_ctrl, "val");
...@@ -272,11 +376,31 @@ void remote_control_apply_json_root(cJSON *root, const char *json_raw) ...@@ -272,11 +376,31 @@ void remote_control_apply_json_root(cJSON *root, const char *json_raw)
if (mode == 0 && val == 0) { if (mode == 0 && val == 0) {
rc_vehicle_stop(); rc_vehicle_stop();
ESP_LOGD(tag_s, "PWM mode=0 val=0 -> stop"); ESP_LOGD(tag_s, "PWM mode=0 val=0 -> stop");
return; } else {
} ESP_LOGI(tag_s, "PWM legacy: mode %d type %d speed %d steer %d",
ESP_LOGI(tag_s, "PWM: mode %d type %d speed %d steer %d", mode, type, speed, steer); mode, type, speed, steer);
rc_vehicle_control(mode, speed, steer); rc_vehicle_control(mode, speed, steer);
} }
} else {
ESP_LOGW(tag_s, "pwm_ctrl 字段无效");
}
}
}
if (pin_ctrl && cJSON_IsObject(pin_ctrl)) {
cJSON *jp = cJSON_GetObjectItem(pin_ctrl, "pin");
cJSON *jpinv = cJSON_GetObjectItem(pin_ctrl, "val");
if (jp && jpinv && cJSON_IsNumber(jp) && cJSON_IsNumber(jpinv)) {
has_any_ctrl = true;
ESP_LOGI(tag_s, "GPIO: pin %d -> %d", jp->valueint, jpinv->valueint);
rc_vehicle_shot(jp->valueint, jpinv->valueint);
} else {
ESP_LOGW(tag_s, "pin_setctrl 字段无效");
}
}
if (!has_any_ctrl) {
ESP_LOGW(tag_s, "message_type=3 缺少 pwm_ctrl/pin_setctrl");
} }
} }
} }
......
...@@ -16,8 +16,9 @@ ...@@ -16,8 +16,9 @@
/** 遥控超时:超过该时间未收到控制 JSON 则停车(ms) */ /** 遥控超时:超过该时间未收到控制 JSON 则停车(ms) */
#define RC_CMD_TIMEOUT_MS 500 #define RC_CMD_TIMEOUT_MS 500
/** 固件超时停车后,经链路 Notify 推送停车 JSON 的周期(ms) */ /** 固件超时停车后,经链路推送行走全停 JSON:共 3 条,条间间隔(ms) */
#define RC_STOP_NOTIFY_PERIOD_MS 300 #define RC_STOP_NOTIFY_BURST_COUNT 3
#define RC_STOP_NOTIFY_BURST_INTERVAL_MS 100
/** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */ /** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */
void remote_control_apply_json(const char *json); void remote_control_apply_json(const char *json);
...@@ -28,7 +29,7 @@ void remote_control_link_reset(void); ...@@ -28,7 +29,7 @@ void remote_control_link_reset(void);
/** 链路正在接收一条控制 JSON 的片段,用于避免分片期间误触发超时停车 */ /** 链路正在接收一条控制 JSON 的片段,用于避免分片期间误触发超时停车 */
void remote_control_note_rx_activity(void); void remote_control_note_rx_activity(void);
/** 构造停车专用 JSON(message_type=3,speed/direction/mode/val 均为 0) */ /** 构造停车专用 JSON(message_type=3,speed_val/steer_val 均为 0) */
char *remote_control_stop_json_malloc(void); char *remote_control_stop_json_malloc(void);
/** 注册停车状态 Notify 回调(BLE 0xFFE3 / UART 行);json 由回调方发送后勿 free */ /** 注册停车状态 Notify 回调(BLE 0xFFE3 / UART 行);json 由回调方发送后勿 free */
......
...@@ -22,9 +22,7 @@ ...@@ -22,9 +22,7 @@
#if CONFIG_APP_LINK_BLE #if CONFIG_APP_LINK_BLE
#include "link_ble.h" #include "link_ble.h"
#endif #endif
#if CONFIG_APP_LINK_UART
#include "device_model.h" #include "device_model.h"
#endif
static const char *tag_s = "WIFI_CFG"; static const char *tag_s = "WIFI_CFG";
static httpd_handle_t config_httpd_s = NULL; static httpd_handle_t config_httpd_s = NULL;
...@@ -59,29 +57,29 @@ static void provision_schedule_restart(void) ...@@ -59,29 +57,29 @@ static void provision_schedule_restart(void)
} }
} }
static esp_err_t captive_no_content_handler(httpd_req_t *req) /**
* 强制门户:须返回非 204(如 302),Android/iOS 才会弹出「需登录网络」。
* 旧实现 generate_204 回 204,系统会认为已联网,故不会自动弹网页。
*/
static esp_err_t captive_portal_redirect(httpd_req_t *req)
{ {
(void)req; (void)req;
httpd_resp_set_status(req, "204 No Content"); httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate");
httpd_resp_set_hdr(req, "Connection", "close");
httpd_resp_send(req, NULL, 0); httpd_resp_send(req, NULL, 0);
return ESP_OK; return ESP_OK;
} }
#if CONFIG_APP_LINK_UART static void register_captive_redirect_uri(httpd_handle_t server, const char *uri)
static const char *uart_model_options_s[] = { "1101", "1102", "1201" };
#define UART_MODEL_OPTION_COUNT ((int)(sizeof(uart_model_options_s) / sizeof(uart_model_options_s[0])))
static bool uart_devid_is_valid(const char *val)
{ {
if (val == NULL) { httpd_uri_t u = {
return false; .uri = uri,
} .method = HTTP_GET,
for (int i = 0; i < UART_MODEL_OPTION_COUNT; i++) { .handler = captive_portal_redirect,
if (strcmp(val, uart_model_options_s[i]) == 0) { };
return true; (void)httpd_register_uri_handler(server, &u);
}
}
return false;
} }
static const char *device_model_to_str(device_model_t model) static const char *device_model_to_str(device_model_t model)
...@@ -89,31 +87,50 @@ static const char *device_model_to_str(device_model_t model) ...@@ -89,31 +87,50 @@ static const char *device_model_to_str(device_model_t model)
switch (model) { switch (model) {
case DEVICE_MODEL_1102: case DEVICE_MODEL_1102:
return "1102"; return "1102";
case DEVICE_MODEL_1201:
return "1201";
default: default:
return "1101"; return "1101";
} }
} }
static void build_uart_model_select_options(char *buf, size_t buf_len, const char *current_model) /** 完整设备号:如 CN110200000001;第 3~6 位须为 1101/1102 */
static bool device_full_id_is_valid(const char *val)
{ {
size_t pos = 0; if (val == NULL || val[0] == '\0') {
return false;
}
if (strstr(val, "app2dev/") != NULL || strchr(val, '/') != NULL) {
return false;
}
buf[0] = '\0'; size_t len = strlen(val);
for (int i = 0; i < UART_MODEL_OPTION_COUNT; i++) { if (len < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN) || len >= 32) {
const char *model = uart_model_options_s[i]; return false;
const char *selected = (strcmp(current_model, model) == 0) ? " selected" : ""; }
int n = snprintf(buf + pos, (pos < buf_len) ? (buf_len - pos) : 0,
" <option value='%s'%s>%s 设备</option>\n", for (size_t i = 0; i < len; i++) {
model, selected, model); if (!isalnum((unsigned char)val[i])) {
if (n < 0 || (size_t)n >= buf_len - pos) { return false;
break; }
} }
pos += (size_t)n;
const char *slice = val + DEVICE_MODEL_SLICE_0BASE_START;
if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0 ||
strncmp(slice, "1102", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
return true;
} }
return false;
}
static void device_id_save_if_valid(const char *decoded_val)
{
if (!device_full_id_is_valid(decoded_val)) {
ESP_LOGW(tag_s, "设备号格式无效(示例 CN110200000001): %s", decoded_val);
return;
}
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
ESP_LOGI(tag_s, "设备号已保存: %s → 型号 %s", decoded_val,
device_model_to_str(device_model_from_full_id(decoded_val)));
} }
#endif
esp_err_t init_spiffs(void) esp_err_t init_spiffs(void)
{ {
...@@ -185,31 +202,21 @@ esp_err_t read_from_nvs(const char *key, char *buf, size_t len) ...@@ -185,31 +202,21 @@ esp_err_t read_from_nvs(const char *key, char *buf, size_t len)
static esp_err_t get_handler(httpd_req_t *req) 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];
char devid_current[32] = {0}; char devid_current[32] = {0};
char model_options[384]; char devid_status[128];
char current_model[8] = "1101";
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid_current, sizeof(devid_current)); read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid_current, sizeof(devid_current));
if (uart_devid_is_valid(devid_current)) {
strncpy(current_model, devid_current, sizeof(current_model) - 1);
} else if (devid_current[0] != '\0') {
strncpy(current_model, device_model_to_str(device_model_from_full_id(devid_current)),
sizeof(current_model) - 1);
}
build_uart_model_select_options(model_options, sizeof(model_options), current_model);
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'"); sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
#if CONFIG_APP_LINK_BLE
char ble_status[128];
sprintf(ble_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index_ble.html", "r");
#elif CONFIG_APP_LINK_UART
FILE *f = fopen("/spiffs/index_uart.html", "r"); FILE *f = fopen("/spiffs/index_uart.html", "r");
#else #else
char wifi_status[128], devid_status[128]; char wifi_status[128];
sprintf(wifi_status, wifi_editable_s ? "" : "disabled style='background:#eee'"); 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"); FILE *f = fopen("/spiffs/index.html", "r");
#endif #endif
if (!f) { if (!f) {
...@@ -228,15 +235,15 @@ static esp_err_t get_handler(httpd_req_t *req) ...@@ -228,15 +235,15 @@ static esp_err_t get_handler(httpd_req_t *req)
char *final_html = malloc(fsize + 1024); char *final_html = malloc(fsize + 1024);
#if CONFIG_APP_LINK_BLE #if CONFIG_APP_LINK_BLE
snprintf(final_html, fsize + 1024, template, snprintf(final_html, fsize + 1024, template,
devid_editable_s ? "🔓" : "🔒", devid_status, devid_editable_s ? "🔓" : "🔒", devid_current, devid_status,
devid_editable_s ? "🔓" : "🔒", ble_status); devid_editable_s ? "🔓" : "🔒", ble_status);
#elif CONFIG_APP_LINK_UART #elif CONFIG_APP_LINK_UART
snprintf(final_html, fsize + 1024, template, snprintf(final_html, fsize + 1024, template,
devid_editable_s ? "🔓" : "🔒", devid_status, model_options); devid_editable_s ? "🔓" : "🔒", devid_current, devid_status);
#else #else
snprintf(final_html, fsize + 1024, template, snprintf(final_html, fsize + 1024, template,
wifi_editable_s ? "🔓" : "🔒", wifi_status, wifi_status, wifi_editable_s ? "🔓" : "🔒", wifi_status, wifi_status,
devid_editable_s ? "🔓" : "🔒", devid_status); devid_editable_s ? "🔓" : "🔒", devid_current, devid_status);
#endif #endif
httpd_resp_set_type(req, "text/html"); httpd_resp_set_type(req, "text/html");
...@@ -275,23 +282,19 @@ static esp_err_t post_handler(httpd_req_t *req) ...@@ -275,23 +282,19 @@ static esp_err_t post_handler(httpd_req_t *req)
token = strtok_r(NULL, "&", &saveptr); token = strtok_r(NULL, "&", &saveptr);
continue; continue;
} }
#if CONFIG_APP_LINK_BLE
if (strcmp(key, "devid") == 0 && devid_editable_s) { if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val); device_id_save_if_valid(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 CONFIG_APP_LINK_BLE
if (strcmp(key, "devid") == 0 && devid_editable_s && uart_devid_is_valid(decoded_val)) { else if (strcmp(key, "ble_name") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val); save_to_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, decoded_val);
} }
#else #endif
if (strcmp(key, "ssid") == 0 && wifi_editable_s) { #if CONFIG_APP_LINK_WIFI
else if (strcmp(key, "ssid") == 0 && wifi_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_WIFI_SSID, decoded_val); save_to_nvs(DEVICE_CFG_KEY_WIFI_SSID, decoded_val);
} else if (strcmp(key, "pass") == 0 && wifi_editable_s) { } else if (strcmp(key, "pass") == 0 && wifi_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_WIFI_PASS, decoded_val); 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 #endif
} }
...@@ -388,19 +391,29 @@ void start_config_web(void) ...@@ -388,19 +391,29 @@ void start_config_web(void)
(void)app_task_start(APP_TASK_DNS_SERVER, dns_server_task, NULL, NULL); (void)app_task_start(APP_TASK_DNS_SERVER, dns_server_task, NULL, NULL);
httpd_handle_t server = NULL; httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_match_fn = httpd_uri_match_wildcard;
config.max_open_sockets = 13;
config.lru_purge_enable = true;
if (httpd_start(&server, &config) == ESP_OK) { if (httpd_start(&server, &config) == ESP_OK) {
config_httpd_s = server; config_httpd_s = server;
httpd_uri_t get_uri = {.uri = "/", .method = HTTP_GET, .handler = get_handler}; httpd_uri_t get_uri = {.uri = "/", .method = HTTP_GET, .handler = get_handler};
httpd_register_uri_handler(server, &get_uri); httpd_register_uri_handler(server, &get_uri);
httpd_uri_t post_uri = {.uri = "/save", .method = HTTP_POST, .handler = post_handler}; httpd_uri_t post_uri = {.uri = "/save", .method = HTTP_POST, .handler = post_handler};
httpd_register_uri_handler(server, &post_uri); httpd_register_uri_handler(server, &post_uri);
httpd_uri_t cap204 = {.uri = "/generate_204", .method = HTTP_GET, .handler = captive_no_content_handler}; /* 各系统连网检测 URL(DNS 劫持到本机后命中)→ 302 触发自动弹窗 */
httpd_register_uri_handler(server, &cap204); register_captive_redirect_uri(server, "/generate_204");
httpd_uri_t cap204b = {.uri = "/gen_204", .method = HTTP_GET, .handler = captive_no_content_handler}; register_captive_redirect_uri(server, "/gen_204");
httpd_register_uri_handler(server, &cap204b); register_captive_redirect_uri(server, "/hotspot-detect.html");
httpd_uri_t favicon = {.uri = "/favicon.ico", .method = HTTP_GET, .handler = captive_no_content_handler}; register_captive_redirect_uri(server, "/library/test/success.html");
httpd_register_uri_handler(server, &favicon); register_captive_redirect_uri(server, "/connecttest.txt");
register_captive_redirect_uri(server, "/redirect");
register_captive_redirect_uri(server, "/success.txt");
register_captive_redirect_uri(server, "/canonical.html");
register_captive_redirect_uri(server, "/favicon.ico");
/* 微信等任意探测路径 */
register_captive_redirect_uri(server, "/*");
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler); httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
ESP_LOGI(tag_s, "配网门户已启动: http://192.168.4.1 (连网检测→302 弹窗)");
} }
s_prov_restart_scheduled_s = false; s_prov_restart_scheduled_s = false;
} }
......
...@@ -4,6 +4,9 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" ...@@ -4,6 +4,9 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB" CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
# Semantic version: OTA manifest / JSON body.version / esp_app_desc.version
CONFIG_MY_APP_VERSION="1.0.2"
# Link mode (one of: WIFI / BLE / UART) # Link mode (one of: WIFI / BLE / UART)
# CONFIG_APP_LINK_WIFI is not set # CONFIG_APP_LINK_WIFI is not set
# CONFIG_APP_LINK_BLE is not set # CONFIG_APP_LINK_BLE is not set
...@@ -14,9 +17,9 @@ CONFIG_APP_LINK_UART=y ...@@ -14,9 +17,9 @@ CONFIG_APP_LINK_UART=y
CONFIG_APP_UART_LINK_BAUDRATE=115200 CONFIG_APP_UART_LINK_BAUDRATE=115200
CONFIG_APP_UART_LINK_TX_GPIO=17 CONFIG_APP_UART_LINK_TX_GPIO=17
CONFIG_APP_UART_LINK_RX_GPIO=18 CONFIG_APP_UART_LINK_RX_GPIO=18
CONFIG_APP_PWM_IO15_SERVO=y # Default AUX: IO15=ESC (throttle), IO16=SERVO (steering). 1102 drive: IO10/21.
# CONFIG_APP_PWM_IO15_ESC is not set # CONFIG_APP_PWM_IO15_SERVO 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_IO15_ESC=y
CONFIG_APP_PWM_IO16_SERVO=y CONFIG_APP_PWM_IO16_SERVO=y
# CONFIG_APP_PWM_IO16_ESC is not set # CONFIG_APP_PWM_IO16_ESC is not set
CONFIG_BT_ENABLED=y CONFIG_BT_ENABLED=y
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<input name='pass' type='password' placeholder='WiFi密码' %s> <input name='pass' type='password' placeholder='WiFi密码' %s>
<br> <br>
<b>设备 ID %s</b> <b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s> <input name='devid' placeholder='CN110200000001' value='%s' %s>
<button type='submit' class='btn'>保存并重启设备</button> <button type='submit' class='btn'>保存并重启设备</button>
</form> </form>
</div> </div>
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p> <p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p>
<form action='/save' method='POST'> <form action='/save' method='POST'>
<b>设备 ID %s</b> <b>设备 ID %s</b>
<input name='devid' placeholder='设备号' %s> <input name='devid' placeholder='CN110200000001' value='%s' %s>
<b>蓝牙广播名 %s</b> <b>蓝牙广播名 %s</b>
<input name='ble_name' placeholder='手机扫描看到的名称' %s> <input name='ble_name' placeholder='手机扫描看到的名称' %s>
<button type='submit' class='btn'>保存并重启</button> <button type='submit' class='btn'>保存并重启</button>
......
...@@ -2,24 +2,25 @@ ...@@ -2,24 +2,25 @@
<html> <html>
<head> <head>
<meta charset='UTF-8'> <meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<style> <style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; } 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; } .card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
select { width: 95%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; font-size: 16px; background: white; } input { width: 95%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; font-size: 16px; box-sizing: border-box; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; margin-top: 10px; } .btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; margin-top: 10px; }
b { display: block; text-align: left; margin-left: 2.5%; margin-top: 10px; color: #555; } b { display: block; text-align: left; margin-left: 2.5%; margin-top: 10px; color: #555; }
.hint { text-align: left; margin: 8px 2.5% 0; font-size: 13px; color: #666; line-height: 1.5; }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<h1>串口设备配置</h1> <h1>串口设备配置</h1>
<p style="color:#666;font-size:14px;">UART 模式无需 WiFi,请选择设备型号;心跳将上报所选型号(如 1101、1102)</p> <p style="color:#666;font-size:14px;">UART 模式无需 WiFi,请填写完整设备号</p>
<form action='/save' method='POST'> <form action='/save' method='POST'>
<b>设备号 %s</b> <b>设备号 %s</b>
<select name='devid' %s> <input name='devid' placeholder='CN110200000001' value='%s' %s>
%s <p class="hint">格式示例:<code>CN110200000001</code>(无 <code>app2dev/</code> 前缀)<br>
</select> 型号由第 3~6 位自动识别,如 <strong>1102</strong> → 1102 车型控制策略</p>
<button type='submit' class='btn'>保存并重启</button> <button type='submit' class='btn'>保存并重启</button>
</form> </form>
</div> </div>
......
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