Commit 775a3e61 authored by 957dd's avatar 957dd

Merge branch 'feature/android_initial_version' into 'master'

Feature/android initial version See merge request !1
parents 8f3a151f 76e8da9d
......@@ -78,6 +78,9 @@ htmlcov/
compile_commands.json
.cursor/
# --- 本地 Agent 技能(pua 等,勿入库)---
pua/
# --- 日志与临时文件 ---
*.log
*~
......
......@@ -2,5 +2,24 @@
# CMakeLists in this exact order for cmake to work correctly
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)
project(ESPRCCar)
# Release 模式:强制全局静默,编译期删除所有 ESP_LOG (I/W/E)
# menuconfig 选择 Release 时自动生效,无需其他操作
if(CONFIG_ROBO_APP_FW_RELEASE)
idf_build_set_property(COMPILE_DEFINITIONS "LOG_LOCAL_LEVEL=ESP_LOG_NONE" APPEND)
endif()
......@@ -44,10 +44,9 @@
其中 `gpiotrol` 已拆分为“通用 PWM 底座 + 设备策略”:
- `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/devices/device_1201.c`:1201 设备控制映射。
- `main/drivers/gpiotrol/devices/device_1101.c`:1101 设备控制映射。
- `main/drivers/gpiotrol/devices/1101/``devices/1102/`:各车型控制映射。
当前统一引脚约定(均为 50Hz):
- 驱动芯片1:`IO10``IO21`
......
This source diff could not be displayed because it is too large. You can view the blob instead.
# 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
## 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**
(设备侧 DNS 会劫持域名,直接输 IP 即可。)
- 连接热点后,系统应 **自动弹出**「需登录网络 / 认证」页(Captive Portal);固件对 `generate_204` 等检测 URL 返回 **302** 到配网页(**勿再返回 204**,否则 Android 认为已联网不弹窗)。
- 若未自动弹出:关移动数据,浏览器手动打开 **http://192.168.4.1**(设备 DNS 劫持任意域名到 `192.168.4.1`)。
### 4.1 各模式配网页与必填项
......@@ -172,6 +171,7 @@ scripts\copy_firmware_release.bat
|------|------|
| UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` |
| 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 |
| 连热点不自动弹配网页 | 刷含 captive 302 修复的固件;关移动数据;仍不行则手动 **http://192.168.4.1** |
| `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` |
| 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` |
......
# 编译类型与 UART 模式配置指南
# 编译类型与 UART 模式配置指南
......@@ -76,38 +76,30 @@ idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" buil
---
## 3. Release 模式下 UART 串口两种工作模式
## 3. Release 模式下 UART 串口工作模式(已简化)
### 3.1 模式 A:Release 默认(无 UART 日志,BLE 转发 W/E
### 3.1 Release 默认(无 UART 日志,UART 可做普通通信
**配置:**
- `ROBO_APP_FW_RELEASE=y`
- 使用 `sdkconfig.defaults.release`(含 **`CONFIG_ESP_CONSOLE_NONE=y`**
- `APP_UART_MODE_DEBUG=n`(默认)
**行为:**
- **不向串口输出** `ESP_LOG`(避免无人读取时阻塞 BLE)
- `ESP_LOGW` / `ESP_LOGE` 在 BLE 已连接时经 **0xFFE3 Notify** 推送(`message_type` 4/5,见 `docs/Android端设备对接文档_v1.0.1.md` §3.5-B)
- 周期心跳仍为 **0xFFE3**,约 3s,`message_type` 1
### 3.2 模式 B:UART0 转为普通通信串口
### 3.2 说明
**配置:**
- `ROBO_APP_FW_RELEASE=y`(menuconfig 中「编译优化级别」选 Release)
- `APP_UART_MODE_DEBUG=y`(启用此选项)
**行为:**
- **UART0**`uart_comm` 接管做普通收发(GPIO43/44)
- `ESP_LOG` / `printf` 等日志输出不可用(没有日志输出)
- 代码中使用 `uart_comm.h` 接口进行数据传输
- 固件下载仍使用 UART0(bootrom 固定)
- 现在不再提供 `APP_UART_MODE_DEBUG` 手动切换。
- 选择 Release 后即默认无控制台日志输出;`APP_DEBUG_UART_*` 对应 UART 可由 `uart_comm` 做普通收发。
- 固件下载仍使用 UART0(bootrom 固定)。
**完整配置步骤:**
```bash
idf.py menuconfig
# 1. robot-esp32s3 → 编译优化级别 → Release
# 2. robot-esp32s3 → 调试打印串口作为普通通信串口使用 → 启用
# 3. Component config → ESP System Settings → Channel for console output → None
# 2. Component config → ESP System Settings → Channel for console output → None
# 保存并退出
```
......@@ -131,22 +123,16 @@ idf.py menuconfig
| `BUILD_IS_DEBUG` | 1=Debug 构建,0=Release 构建 |
| `UART_MODE_COMMUNICATION` | 1=UART 作为通信串口,0=默认 |
| `UART_MODE_DEBUG` | 1=调试串口模式,0=通信模式 |
| `BUILD_SERIAL_LOG_ENABLED` | 1=可向 UART 打日志,0=Release 默认关闭 |
### 4.3 条件日志宏
无论何种模式,都可以使用统一的日志宏:
### 4.3 日志(直接用 ESP-IDF)
```c
LOG_I(TAG, "信息日志"); // Info 级别
LOG_W(TAG, "警告日志"); // Warning 级别
LOG_E(TAG, "错误日志"); // Error 级别
LOG_D(TAG, "调试日志"); // Debug 级别
```
Release 不在 `build_config.h` 里再包 `LOG_*` 宏,由 **`sdkconfig`** 统一控制:
**Release + UART 通信模式** 下,这些宏自动展开为空操作(不输出任何内容)。
- `CONFIG_LOG_DEFAULT_LEVEL_WARN``ESP_LOGI` / `ESP_LOGD` **编译期删除**
- `CONFIG_ESP_CONSOLE_NONE` → 无 UART 控制台
- BLE Release:`ESP_LOGW` / `ESP_LOGE``link_ble.c` 钩子 → **0xFFE3**
在其他模式下,这些宏等价于 `ESP_LOGI` / `ESP_LOGW` / `ESP_LOGE` / `ESP_LOGD`
业务代码直接写 `ESP_LOGI` / `ESP_LOGW` 即可;仅 Debug 专属的启动横幅可用 `#if BUILD_IS_DEBUG`
### 4.4 UART 通信 API(仅在 Release+通信模式下有效)
......@@ -200,20 +186,6 @@ void my_init(void)
}
```
### 5.2 使用统一日志宏(推荐)
```c
#include "core/build_config.h"
void my_function(void)
{
/* 在任何构建类型下都可以使用,自动适配 */
LOG_I(TAG, "This is info message");
LOG_W(TAG, "This is warning message");
LOG_E(TAG, "This is error message");
}
```
---
## 6. 构建输出对比
......@@ -255,10 +227,9 @@ void my_function(void)
### 7.2 UART 通信模式下的调试
启用 `APP_UART_MODE_DEBUG` 后:
- **无法使用 `idf.py monitor` 查看日志**(因为没有日志输出)
- **烧录时仍然使用 UART0**,请确保外部设备在烧录时不会干扰
- 建议在开发阶段先用 **模式 A(调试打印)** 验证功能,再切换到 **模式 B(通信)**
- Release 下默认无法使用 `idf.py monitor` 查看日志(控制台关闭)。
- 烧录时仍然使用 UART0,请确保外部设备在烧录时不会干扰。
- 建议开发阶段使用 Debug 验证,量产切 Release。
### 7.3 推荐的开发流程
......@@ -275,6 +246,12 @@ void my_function(void)
→ 最终验证,关闭所有调试功能
```
### 7.4 UART Debug 日志策略(自动)
- 链路模式为 `APP_LINK_UART` 且构建为 Debug 时,`LINK_UART` 自动打印 JSON 级日志。
- RAW 字节日志默认关闭(避免刷屏),仅保留 JSON 文本便于联调。
- Release 仍按 `LOG_DEFAULT_LEVEL_WARN``ESP_CONSOLE_NONE` 静默。
---
## 8. 故障排除
......
......@@ -15,6 +15,7 @@
| 文件 | 说明 |
|------|------|
| **`release/VERSION.txt`** | 人类可读:版本号、编译时间、OTA 文件大小与 SHA256 |
| **`release/ota_manifest.json`** | Android OTA 机器可读版本 JSON:版本、固件大小、SHA256 |
| **`release/manifest.json`** | 机器可读:同上 + 全部 factory 文件校验 |
| **`release/Android端设备对接文档_v<版本>.md`** | 与固件同版本的 Android 对接说明(WiFi/BLE/UART,脚本从 `docs/` 复制) |
......@@ -25,6 +26,7 @@
| 路径 | 内容 |
|------|------|
| `release/ota/ESPRCCar.bin` | **BLE OTA / Android 升级**(仅应用镜像) |
| `release/ota_manifest.json` | **Android OTA 版本 JSON**(与 `ESPRCCar.bin` 配套) |
| `release/factory/*.bin` | **量产烧录**(乐鑫 Flash Download Tools 加载) |
| `release/factory/flash_args.txt` | 烧录地址摘要(与 `build/flash_args` 一致) |
......@@ -47,11 +49,11 @@ idf.py build
scripts\copy_firmware_release.bat
```
脚本会刷新 **`VERSION.txt`****`manifest.json`**。打开 `VERSION.txt` 核对版本与时间**Git 提交由你自行操作**,脚本不会执行任何 git 命令。
脚本会刷新 **`VERSION.txt`****`ota_manifest.json`****`manifest.json`**。打开 `ota_manifest.json` 核对 Android OTA 版本、大小与 SHA256**Git 提交由你自行操作**,脚本不会执行任何 git 命令。
## 他人使用
- **OTA**:下载 `release/ota/ESPRCCar.bin`(版本看 `VERSION.txt`
- **Android 联调**:同目录下的 **`Android端设备对接文档_v<版本>.md`** + `VERSION.txt` / `manifest.json`
- **OTA**:下载 **两个文件**`release/ota/ESPRCCar.bin` + `release/ota_manifest.json`
- **Android 联调**:同目录下的 **`Android端设备对接文档_v<版本>.md`** + `release/ota/ESPRCCar.bin` + `release/ota_manifest.json`
- **量产**:下载 `release/factory/` 全部 bin,用 Flash Download Tools 烧录。
- **安装与配网**(环境、三种链路、热点填什么):见仓库 [`docs/安装与配网指南.md`](../docs/安装与配网指南.md)
This source diff could not be displayed because it is too large. You can view the blob instead.
# 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` 为准。
ESPRCCar firmware release
=========================
version: 1.0.1
version: 1.0.2
project: ESPRCCar
chip: esp32s3
built_utc: 2026-05-19T07:53:23Z
built_local: 2026/05/19 ܶ 15:53:23.22
git_revision: (no git)
built_utc: 2026-06-11T08:29:54Z
built_local: 2026/06/11 16:29:54.96
OTA:
path: firmware/release/ota/ESPRCCar.bin
bytes: 1115760
sha256: 724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6
bytes: 889552
sha256: 065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977
Factory: firmware/release/factory/
tool: Espressif Flash Download Tools
addrs: see factory/flash_args.txt
machine_readable: manifest.json
machine_readable: ota_manifest.json
full_release_manifest: manifest.json
android_doc: firmware/release/Android*_v1.0.2.md (see release folder
......@@ -4,17 +4,15 @@
"flash_mode": "dio",
"flash_freq": "80m",
"flash_size": "16MB",
"version": "1.0.1",
"git_revision": "",
"generated_utc": "2026-05-19T07:53:23Z",
"version": "1.0.2",
"generated_utc": "2026-06-11T08:29:54Z",
"ota_image": "release/ota/ESPRCCar.bin",
"factory_dir": "release/factory",
"files": [
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"},
{"name":"bootloader.bin","bytes":21056,"sha256":"740f1b3c78ca63acc0a5e3c788bf18f77585a28504e5d8722ac253991c6f1f7a"},
{"name":"ESPRCCar.bin","bytes":889552,"sha256":"065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"},
{"name":"bootloader.bin","bytes":13808,"sha256":"21a5becf42c59120c3c4322a04ca684f86a496dfb770575ebbe6d6ec739b7199"},
{"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"},
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"},
{"name":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"},
{"name":"storage.bin","bytes":327680,"sha256":"05be75d1159d4b3043e4b3847f7d8307b4a4955ae30c1baf93ce42eafb1308cf"}
{"name":"storage.bin","bytes":327680,"sha256":"6d2f4c246d34ebf80e3d32ac4873aa84d29f68ee799214b123bdd998a8b2d3b2"}
]
}
{
"project": "ESPRCCar",
"chip": "esp32s3",
"version": "1.0.2",
"generated_utc": "2026-06-11T08:29:54Z",
"image": "ESPRCCar.bin",
"bytes": 889552,
"sha256": "065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"
}
......@@ -5,11 +5,14 @@ set(SOURCES
"device/device_model.c"
"protocol/remote_control.c"
"protocol/heart_payload.c"
"drivers/driver_manager.c"
"protocol/ota_offer_protocol.c"
"protocol/ota_frame_protocol.c"
"protocol/ota_uart_tune.c"
"drivers/driver_manager/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/devices/device_1201.c"
"drivers/gpiotrol/devices/device_1101.c"
"drivers/gpiotrol/devices/device_1102.c"
"drivers/gpiotrol/devices/1101/device_1101.c"
"drivers/gpiotrol/devices/1102/device_1102.c"
"drivers/gpiotrol/devices/1102/device_1102_brake.c"
"drivers/gpiotrol/betteryread.c"
"drivers/uart_comm/uart_comm.c"
"app/app_run.c"
......@@ -19,23 +22,27 @@ set(SOURCES
"ota_binary_stream.c")
if(CONFIG_APP_LINK_BLE)
list(APPEND SOURCES "link_ble/link_ble.c")
list(APPEND SOURCES "link_ble/link_ble.c" "link_common/link_dma_ble.c")
elseif(CONFIG_APP_LINK_UART)
list(APPEND SOURCES "link_uart/link_uart.c")
list(APPEND SOURCES "link_uart/link_uart.c" "link_common/link_dma_uart.c")
else()
list(APPEND SOURCES "link_wifi/mqttconf_commun.c" "ota.c")
endif()
set(INCLUDE_DIRS
"."
"link_common"
"core"
"device"
"provision"
"protocol"
"app"
"drivers"
"drivers/driver_manager"
"drivers/gpiotrol"
"drivers/gpiotrol/devices"
"drivers/gpiotrol/devices/1101"
"drivers/gpiotrol/devices/1102"
"drivers/uart_comm")
if(CONFIG_APP_LINK_BLE)
......
......@@ -8,7 +8,7 @@ config ROBIOT_WIFI_SSID
config ROBOIOT_MQTT_URL
string "MQTT URL"
default "https://fcrs-api.yd-ss.com/device/getConfig?deviceNo="
default "0.0.0.0"
help
MQTT Config URL
......@@ -48,13 +48,48 @@ config APP_DEBUG_UART_RX_GPIO
UART1 模式下使用 GPIO18(RX)。
此项保留用于兼容性,请保持为 -1。
choice APP_UART_LINK_BAUDRATE_PRESET
prompt "UART 链路模式波特率"
depends on APP_LINK_UART
default APP_UART_BAUD_115200
help
UART 链路模式下可选波特率档位。
Android 端需与本项保持一致。
config APP_UART_BAUD_9600
bool "9600"
config APP_UART_BAUD_115200
bool "115200"
config APP_UART_BAUD_230400
bool "230400"
config APP_UART_BAUD_460800
bool "460800"
config APP_UART_BAUD_921600
bool "921600"
endchoice
config APP_UART_LINK_BAUDRATE
int "UART 链路模式波特率"
default 115200
range 9600 921600
int
default 9600 if APP_UART_BAUD_9600
default 115200 if APP_UART_BAUD_115200
default 230400 if APP_UART_BAUD_230400
default 460800 if APP_UART_BAUD_460800
default 921600 if APP_UART_BAUD_921600
help
当前 UART 链路模式生效波特率(由上方档位自动生成)。
config APP_OTA_UART_MAX_CHUNK
int "OTA DATA 单帧最大载荷 (字节)"
depends on APP_LINK_UART
default 1024
range 240 1024
help
UART 链路模式下的通信波特率。默认 115200。
适用于 APP_LINK_UART 模式下的 UART1 通信
UART OTA 每帧最多携带的固件字节数。越大往返越少、越快;
须与 Android OtaFrame 发送缓冲一致。推荐 1024(460800 下 6MB 约 1~3 分钟)
config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号"
......@@ -103,14 +138,46 @@ config APP_BLE_OTA
depends on APP_LINK_BLE
default y
help
允许手机通过 0xFFE2 写入固件分包升级;每步成功后可通过 0xFFE4 Notify 收到 JSON 应答摘要。
关闭后向 0xFFE2 写入将返回 Write Not Permitted;0xFFE4 仍可发现,但无 OTA 过程推送。
允许手机在回复 message_type=0 后,通过 0xFFE2 写入固件分包升级;
固件数据经 0xFFE4 ACK/NAK 帧。询问/结果 JSON 经 0xFFE3(message_type=1002);询问在手机先发 JSON 后回复。
关闭后向 0xFFE2 写入将返回 Write Not Permitted。
choice APP_BLE_TX_POWER
prompt "BLE 发射功率(贴机/降干扰)"
depends on APP_LINK_BLE
default APP_BLE_TX_POWER_N24
help
仅降低 ESP32 **发射**功率,减轻贴主板时对手机的干扰。
接收灵敏度由芯片射频决定,固件无法单独把「接收功率」调大;
若手机发过来的包收不好,请让 Android 侧保持正常发射功率/靠近天线。
同时在 menuconfig → Bluetooth Controller → BLE default Tx power
建议选 -24dBm 与该项一致。
config APP_BLE_TX_POWER_N24
bool "-24 dBm(贴机推荐,最低发射)"
config APP_BLE_TX_POWER_N21
bool "-21 dBm"
config APP_BLE_TX_POWER_N18
bool "-18 dBm"
config APP_BLE_TX_POWER_N15
bool "-15 dBm"
config APP_BLE_TX_POWER_N12
bool "-12 dBm"
config APP_BLE_TX_POWER_N9
bool "-9 dBm"
endchoice
choice APP_PWM_IO15_ROLE
prompt "GPIO15 PWM 角色"
default APP_PWM_IO15_SERVO
default APP_PWM_IO15_ESC
help
选择 GPIO15 在 50Hz PWM 下的用途。
默认:电调 ESC(1101 外接油门 / 备用电调口)。
config APP_PWM_IO15_SERVO
bool "舵机(初始化 90 度 / 1500us)"
......@@ -121,9 +188,10 @@ endchoice
choice APP_PWM_IO16_ROLE
prompt "GPIO16 PWM 角色"
default APP_PWM_IO16_ESC
default APP_PWM_IO16_SERVO
help
选择 GPIO16 在 50Hz PWM 下的用途。
默认:舵机(1102 转向固定使用 IO16,须为 SERVO)。
config APP_PWM_IO16_SERVO
bool "舵机(初始化 90 度 / 1500us)"
......@@ -136,30 +204,39 @@ choice ROBO_APP_FW_BUILD
prompt "编译优化级别(固件 Debug/Release)"
default ROBO_APP_FW_DEBUG
help
注意:勿使用名称 APP_BUILD_TYPE,与 ESP-IDF 全局 Kconfig 冲突。
Debug:-Og;Release:-Os(另见 sdkconfig.defaults.release 或 menuconfig 编译器选项)。
首次切换 Release 时,若日志级别未变,请删除 sdkconfig 后重新配置。
Debug:-Og 优化,UART0 输出所有日志。
Release:-Os 优化,UART0 完全静默,日志级别 WARN,代码体积最小。
config ROBO_APP_FW_DEBUG
bool "Debug(Og 优化,调试)"
bool "Debug(Og 优化,UART0 输出日志)"
select COMPILER_OPTIMIZATION_DEBUG
select COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE
select LOG_DEFAULT_LEVEL_INFO
select ESP_CONSOLE_UART_DEFAULT
help
优化级别 -Og,保留调试符号,适合开发和调试。
开发调试版本:
- 优化级别 -Og,保留调试符号
- 日志级别 Info,所有 ESP_LOGI 输出到 UART0
- UART0 作为控制台,printf 可用
config ROBO_APP_FW_RELEASE
bool "Release(Os 优化,最小体积)"
help
优化级别 -Os,最小代码体积,适合量产发布。
默认关闭 UART 控制台(见 sdkconfig.defaults.release),W/E 日志经 BLE 0xFFE3 Notify 推送。
bool "Release(Os 优化,UART0 完全静默)"
select COMPILER_OPTIMIZATION_SIZE
select COMPILER_OPTIMIZATION_ASSERTIONS_SILENT
select LOG_DEFAULT_LEVEL_WARN
select BOOTLOADER_LOG_LEVEL_WARN
select ESP_CONSOLE_NONE
help
量产发布版本:
- 优化级别 -Os,代码体积最小
- 日志级别 WARN,ESP_LOGI/ESP_LOGD 编译期删除
- UART0 控制台关闭,printf/ESP_LOG 无输出
- 错误/警告通过业务通道(BLE 0xFFE3 或 UART1 JSON)上报
注意:从 Debug 首次切换到 Release,若 sdkconfig 中日志级别仍为 INFO,
请删除 sdkconfig 文件后重新运行 menuconfig,让 select 生效。
endchoice
config APP_UART_MODE_DEBUG
bool "调试打印串口作为普通通信串口使用(仅 Release 有效)"
depends on ROBO_APP_FW_RELEASE
default n
help
仅在 Release 版本下可用。启用后,由「调试与通信 UART 外设编号」指定的那一路 UART
不再作为日志输出,可由 uart_comm 做普通收发。禁用则保持调试打印。
注意:启用后 ESP_LOG / printf 等输出将不可用(建议同时关闭控制台串口)。
endmenu
......@@ -230,7 +230,7 @@ static void run_ble_mode(void)
button_monitor_task_init();
provision_wifi_shutdown();
if (link_ble_start() != ESP_OK) {
......@@ -258,7 +258,7 @@ static void run_uart_mode(void)
if (strlen(dev) == 0) {
ESP_LOGI(tag_s, "UART 模式: 需配置 device_id,启动配网热点");
ESP_LOGI(tag_s, "UART 模式: 未选择设备型号,启动配网热点");
nowifidata_start_config_web();
......@@ -276,7 +276,7 @@ static void run_uart_mode(void)
button_monitor_task_init();
provision_wifi_shutdown();
/* 启动 UART 链路通信 */
......@@ -338,23 +338,7 @@ void app_run(void)
#if BUILD_SERIAL_LOG_ENABLED
#if BUILD_IS_RELEASE
ESP_LOGI(tag_s, "构建类型: Release (Os优化)");
#if UART_MODE_COMMUNICATION
ESP_LOGI(tag_s, "UART模式: 普通通信串口");
#else
ESP_LOGI(tag_s, "UART模式: 调试打印");
#endif
#else
#if BUILD_IS_DEBUG
ESP_LOGI(tag_s, "构建类型: Debug (Og优化)");
......@@ -362,8 +346,6 @@ void app_run(void)
#endif
#endif
#if CONFIG_APP_LINK_WIFI
......
......@@ -3,7 +3,6 @@
*
* 编译条件:
* - CONFIG_ROBO_APP_FW_RELEASE=y
* - CONFIG_APP_UART_MODE_DEBUG=y
*
* 使用场景:
* - 通过板载 RX1/TX1(UART1)等与外部 MCU 透明传输
......
/*
* 构建配置检测头文件
* 用于在代码中区分 Debug/Release 构建和串口使用模式
*
* 日志:Release 由 menuconfig(LOG_DEFAULT_LEVEL_WARN、ESP_CONSOLE_NONE)裁掉 ESP_LOGI/D;
* BLE Release 的 W/E 转发见 link_ble.c。勿在本头文件再包一层 LOG_* 宏。
*/
#ifndef BUILD_CONFIG_H
......@@ -23,64 +26,30 @@
#endif
/* ========================
* 串口 / 日志输出
* 串口模式(精简:Release 走通信,Debug 走调试)
* ======================== */
/*
* Release:关闭串口调试打印(sdkconfig 建议 CONFIG_ESP_CONSOLE_NONE),
* W/E 经 BLE 0xFFE3 Notify 推送(见 link_ble.c)。
*
* Release + APP_UART_MODE_DEBUG:指定 UART 给 uart_comm,同样无 ESP_LOG。
*/
#if defined(CONFIG_APP_UART_MODE_DEBUG) && defined(CONFIG_ROBO_APP_FW_RELEASE)
#if BUILD_IS_RELEASE
#define UART_MODE_COMMUNICATION 1
#define UART_MODE_DEBUG 0
#define BUILD_SERIAL_LOG_ENABLED 0
#else
#define UART_MODE_COMMUNICATION 0
#define UART_MODE_DEBUG 1
#if BUILD_IS_RELEASE
#define BUILD_SERIAL_LOG_ENABLED 0
#else
#define BUILD_SERIAL_LOG_ENABLED 1
#endif
#endif
/* ========================
* 条件日志宏(Release 或无串口时禁用 LOG_* 宏)
* ======================== */
#if !BUILD_SERIAL_LOG_ENABLED
#define BUILD_LOG_LEVEL_NONE 1
#define LOG_I(tag, fmt, ...) ((void)0)
#define LOG_W(tag, fmt, ...) ((void)0)
#define LOG_E(tag, fmt, ...) ((void)0)
#define LOG_D(tag, fmt, ...) ((void)0)
#else
#include "esp_log.h"
#define BUILD_LOG_LEVEL_NONE 0
#define LOG_I(tag, fmt, ...) ESP_LOGI(tag, fmt, ##__VA_ARGS__)
#define LOG_W(tag, fmt, ...) ESP_LOGW(tag, fmt, ##__VA_ARGS__)
#define LOG_E(tag, fmt, ...) ESP_LOGE(tag, fmt, ##__VA_ARGS__)
#define LOG_D(tag, fmt, ...) ESP_LOGD(tag, fmt, ##__VA_ARGS__)
#endif
#ifdef __cplusplus
extern "C" {
#endif
static inline const char* build_get_type(void) {
static inline const char *build_get_type(void)
{
return BUILD_IS_RELEASE ? "Release" : "Debug";
}
static inline const char* build_get_uart_mode(void) {
static inline const char *build_get_uart_mode(void)
{
return UART_MODE_COMMUNICATION ? "Communication" : "Debug";
}
static inline int build_can_log(void) {
return BUILD_SERIAL_LOG_ENABLED;
}
#ifdef __cplusplus
}
#endif
......
......@@ -66,6 +66,18 @@ static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = {
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_RC_WATCHDOG] = {
.name = "rc_wdog",
.stack_depth = 3072,
.priority = 5,
.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,
......
......@@ -17,6 +17,7 @@ typedef enum {
APP_TASK_MQTT_INIT,
APP_TASK_UART_COMM_EXAMPLE,
APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */
APP_TASK_DEV1102_BRAKE, /* 1102 板载驱动 PID 制动 */
APP_TASK_COUNT,
} app_task_id_t;
......
......@@ -6,17 +6,30 @@ static const char *tag_s = "DEVICE_MODEL";
device_model_t device_model_from_full_id(const char *device_id)
{
if (!device_id || strlen(device_id) < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN)) {
ESP_LOGW(tag_s, "device_id 为空或长度不足,按 1101");
if (!device_id || device_id[0] == '\0') {
ESP_LOGW(tag_s, "device_id 为空,按 1101");
return DEVICE_MODEL_1101;
}
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START;
size_t len = strlen(device_id);
if (len == DEVICE_MODEL_SLICE_CHAR_LEN) {
if (strncmp(device_id, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1101");
return DEVICE_MODEL_1101;
}
if (strncmp(device_id, "1102", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1102");
return DEVICE_MODEL_1102;
}
}
if (strncmp(slice, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1201 → 型号 1201");
return DEVICE_MODEL_1201;
if (len < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN)) {
ESP_LOGW(tag_s, "device_id 长度不足,按 1101");
return DEVICE_MODEL_1101;
}
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START;
if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101");
return DEVICE_MODEL_1101;
......
......@@ -5,12 +5,11 @@
/**
* 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。
* 规则:取 device_id 第 3~第 6 个字符(从第 1 个字符起算,共 4 字符),
* 即 C 字符串下标 [2..5],与 "1201" / "1101" 比较
* 规则:完整设备号如 CN110200000001(无 app2dev/ 前缀);
* 取第 3~第 6 位(1 起算)共 4 字符 → C 下标 [2..5],如 "1102"
*/
typedef enum {
DEVICE_MODEL_1201 = 0,
DEVICE_MODEL_1101,
DEVICE_MODEL_1101 = 0,
DEVICE_MODEL_1102,
} device_model_t;
......
#ifndef DEVICE_NVS_H
#define DEVICE_NVS_H
#include <string.h>
/** NVS 命名空间(与网页配网写入一致) */
#define DEVICE_CFG_NVS_NAMESPACE "storage"
......@@ -9,4 +11,13 @@
#define DEVICE_CFG_KEY_DEVICE_ID "device_id"
#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
......@@ -4,7 +4,8 @@
typedef struct device_drive_ops_s {
const char *name;
void (*stop)(void);
void (*control)(int mode, int val);
/** mode 1~4:仅用 speed_val;mode 5~8(1102):speed_val + steer_val */
void (*control)(int mode, int speed_val, int steer_val);
void (*shot)(int pin, int val);
} device_drive_ops_t;
......
#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_brake.h"
#include "rc_pwm_control.h"
#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° */
#define DEV1102_STEER_VAL_LO 47
#define DEV1102_STEER_VAL_HI 62
......@@ -12,6 +17,9 @@
#define DEV1102_STEER_RIGHT_MAX (DEV1102_STEER_CENTER_ANG - DEV1102_STEER_DELTA_ANG)
#define DEV1102_STEER_VAL_SPAN (DEV1102_STEER_VAL_HI - DEV1102_STEER_VAL_LO)
static uint32_t s_last_drive_pct;
static bool s_brake_inited;
static int clamp_int(int v, int lo, int hi)
{
if (v < lo) {
......@@ -23,15 +31,34 @@ static int clamp_int(int v, int lo, int hi)
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)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_DRIVE_START_TH) {
if (v <= DEV1102_DRIVE_STOP_TH) {
return 0;
}
/* 50~200 线性映射到 0~100% */
return (uint32_t)(((v - DEV1102_DRIVE_START_TH) * 100) /
(DEV1102_VAL_MAX - DEV1102_DRIVE_START_TH));
if (v >= DEV1102_VAL_MAX) {
return DEV1102_DRIVE_PWM_MAX;
}
/* 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)
......@@ -64,36 +91,75 @@ static uint32_t steer_right_angle_from_val(int val)
static void device_1102_stop(void)
{
/* 1102: 10 前进,21 后退;全部拉低并将 IO16 回中 */
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
device_1102_request_brake();
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
}
static void device_1102_control(int mode, int val)
static void device_1102_drive_forward(uint32_t pct)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
switch (mode) {
case 1: { /* 前进:IO10 */
uint32_t pct = drive_percent_from_val_200(v);
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_B, 0);
break;
}
static void device_1102_drive_backward(uint32_t pct)
{
if (pct == 0U) {
device_1102_request_brake();
return;
}
case 2: { /* 后退:IO21 */
uint32_t pct = drive_percent_from_val_200(v);
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_B, pct);
}
static void device_1102_control(int mode, int speed_val, int steer_val)
{
int speed = clamp_int(speed_val, 0, DEV1102_VAL_MAX);
int steer = clamp_int(steer_val, 0, DEV1102_VAL_MAX);
switch (mode) {
case 1: {
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);
device_1102_drive_forward(pct);
break;
}
case 3: { /* 左转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(v));
case 2: {
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);
device_1102_drive_backward(pct);
break;
}
case 4: { /* 右转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(v));
case 3:
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(speed));
break;
case 4:
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(speed));
break;
case 5:
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));
break;
case 6:
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));
break;
case 7:
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));
break;
case 8:
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));
break;
}
default:
break;
}
......@@ -114,5 +180,6 @@ static const device_drive_ops_t ops_s = {
const device_drive_ops_t *device_1102_get_ops(void)
{
device_1102_ensure_brake_task();
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 val)
{
if (mode == 1) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1101_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1101",
.stop = device_1101_stop,
.control = device_1101_control,
.shot = device_1101_shot,
};
const device_drive_ops_t *device_1101_get_ops(void)
{
return &ops_s;
}
#include "device_1201.h"
#include "rc_pwm_control.h"
static void device_1201_stop(void)
{
rc_pwm_stop_all_drive_outputs();
}
static void device_1201_control(int mode, int val)
{
if (mode == 1) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1201_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1201",
.stop = device_1201_stop,
.control = device_1201_control,
.shot = device_1201_shot,
};
const device_drive_ops_t *device_1201_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1201_H
#define DEVICE_1201_H
#include "device_drive.h"
const device_drive_ops_t *device_1201_get_ops(void);
#endif
......@@ -2,10 +2,12 @@
#include "device_drive.h"
#include "device_model.h"
#include "devices/device_1101.h"
#include "devices/device_1102.h"
#include "devices/device_1201.h"
#include "device_1101.h"
#include "device_1102.h"
#include "device_1102_brake.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include <stdbool.h>
......@@ -159,7 +161,7 @@ esp_err_t rc_pwm_control_init(void)
}
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",
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)
drive_ops_s = device_1101_get_ops();
} else if (device_model_is_1102(m)) {
drive_ops_s = device_1102_get_ops();
ESP_LOGI(tag_s, "1102 制动任务: %s",
device_1102_brake_task_ready() ? "已启动" : "未启动");
#if CONFIG_APP_PWM_IO16_ESC
ESP_LOGW(tag_s, "1102 需要 IO16 作为舵机,请在 menuconfig 将 GPIO16 角色改为 SERVO");
#endif
} else {
drive_ops_s = device_1201_get_ops();
drive_ops_s = device_1101_get_ops();
}
ESP_LOGI(tag_s, "当前设备策略: %s", drive_ops_s->name);
}
......@@ -198,10 +202,10 @@ void rc_vehicle_shot(int pin, int val)
}
}
void rc_vehicle_control(int mode, int val)
void rc_vehicle_control(int mode, int speed_val, int steer_val)
{
if (drive_ops_s && drive_ops_s->control) {
drive_ops_s->control(mode, val);
drive_ops_s->control(mode, speed_val, steer_val);
}
}
......@@ -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)
{
if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO) {
......
......@@ -49,7 +49,7 @@ void rc_pwm_control_set_drive_from_device_id(const char *device_id);
void rc_vehicle_stop(void);
void rc_vehicle_shot(int pin, int val);
void rc_vehicle_control(int mode, int val);
void rc_vehicle_control(int mode, int speed_val, int steer_val);
/**
* @brief 设置驱动芯片控制引脚百分比(支持 10/21/11/12)
......@@ -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);
/** @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)
*/
......
......@@ -6,7 +6,7 @@
#include "uart_comm.h"
#include "core/build_config.h"
#if defined(CONFIG_APP_UART_MODE_DEBUG) && defined(CONFIG_ROBO_APP_FW_RELEASE)
#if defined(CONFIG_ROBO_APP_FW_RELEASE)
#include "driver/uart.h"
#include <string.h>
......@@ -113,7 +113,7 @@ void uart_comm_flush(void)
uart_flush(UART_COMM_PORT);
}
#else /* !(CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE) */
#else /* !CONFIG_ROBO_APP_FW_RELEASE */
/* 当功能未启用时,提供空实现(避免编译错误) */
......@@ -153,4 +153,4 @@ int uart_comm_is_initialized(void)
void uart_comm_flush(void) {}
#endif /* CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE */
#endif /* CONFIG_ROBO_APP_FW_RELEASE */
/*
* UART 通信驱动
* 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口
* 注意:启用此功能后,ESP_LOG/printf 将不可用
* Release 默认无控制台输出,UART 引脚用于业务通信
*/
#ifndef UART_COMM_H
......@@ -20,10 +20,7 @@ extern "C" {
#define UART_COMM_BUFFER_SIZE 1024
#define UART_COMM_QUEUE_SIZE 8
/* 初始化 UART 作为普通通信串口
* 仅在 APP_UART_MODE_DEBUG=y 时有效
* 返回 ESP_OK 成功,其他错误码失败
*/
/* 初始化 UART 作为普通通信串口(仅 Release 有效) */
esp_err_t uart_comm_init(int baudrate);
/* 反初始化,恢复默认状态 */
......
......@@ -12,6 +12,7 @@
#include <stdlib.h>
#include <string.h>
#include "nimble/nimble_port.h"
#include "esp_bt.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "host/ble_uuid.h"
......@@ -30,12 +31,32 @@
#include "core/build_config.h"
#include "task_manager.h"
#include "heart_payload.h"
#include "ota_offer_protocol.h"
#include "ota_frame_protocol.h"
/* OTA 二进制流模块(BLE和UART共用) */
#include "ota_binary_stream.h"
#include "cJSON.h"
#include "link_dma_ble.h"
static const char *tag_s = "LINK_BLE";
#if CONFIG_APP_BLE_TX_POWER_N24
#define LINK_BLE_TX_PWR ESP_PWR_LVL_N24
#elif CONFIG_APP_BLE_TX_POWER_N21
#define LINK_BLE_TX_PWR ESP_PWR_LVL_N21
#elif CONFIG_APP_BLE_TX_POWER_N18
#define LINK_BLE_TX_PWR ESP_PWR_LVL_N18
#elif CONFIG_APP_BLE_TX_POWER_N15
#define LINK_BLE_TX_PWR ESP_PWR_LVL_N15
#elif CONFIG_APP_BLE_TX_POWER_N12
#define LINK_BLE_TX_PWR ESP_PWR_LVL_N12
#else
#define LINK_BLE_TX_PWR ESP_PWR_LVL_N9
#endif
static bool s_ble_stack_running_s;
/** 0xFFE1 JSON 接收缓冲(避免 GATT 回调栈上分配 2KB) */
static char s_ffe1_json_buf_s[REMOTE_CTRL_JSON_MAX_BYTES + 1];
......@@ -74,6 +95,17 @@ static int gatt_svr_chr_ota_stat_access(uint16_t conn_handle, uint16_t attr_hand
static void link_ble_gatt_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
static int ble_hb_notify_json(const char *json);
static void ble_heartbeat_task(void *param);
static bool ble_dispatch_json_line(const char *json);
static void ble_dma_rx_dispatch(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len, void *ctx);
static int ble_dma_tx_notify(uint16_t attr_handle, const uint8_t *data, uint16_t len, void *ctx);
static int ble_dma_process_ota_payload(const uint8_t *buf, uint16_t copy_len);
static void ble_request_low_duty_conn(uint16_t conn_handle);
static void link_ble_apply_tx_power(void);
static void ble_send_heartbeat_once(void);
static void ble_ota_notify_payload(const uint8_t *payload, uint16_t payload_len);
static void ble_ota_feed_raw_bytes(const uint8_t *data, uint16_t len);
static int ble_ota_process_packet(const uint8_t *data, uint16_t len);
void link_ble_session_reset(void);
/** GATT 0xFFE3:与 MQTT 同结构心跳 JSON,Notify 周期推送;可读为最近一次内容 */
static uint16_t hb_chr_val_handle_s;
......@@ -92,7 +124,7 @@ static volatile bool log_forward_busy_s;
#endif
#ifndef BLE_HEART_PERIOD_MS
#define BLE_HEART_PERIOD_MS 3000
#define BLE_HEART_PERIOD_MS 10000
#endif
static void ble_adv_name_apply_mp_suffix(char *name, size_t cap)
......@@ -129,15 +161,10 @@ static int ble_hb_notify_json(const char *json)
last_hb_buf_s[jlen] = '\0';
last_hb_len_s = (uint16_t)jlen;
struct os_mbuf *om = ble_hs_mbuf_from_flat(last_hb_buf_s, last_hb_len_s);
if (om == NULL) {
if (!link_dma_ble_post_tx(hb_chr_val_handle_s, last_hb_buf_s, last_hb_len_s)) {
return -1;
}
int nrc = ble_gatts_notify_custom(ble_conn_handle_s, hb_chr_val_handle_s, om);
if (nrc != 0) {
os_mbuf_free_chain(om);
}
return nrc;
return 0;
}
#if BUILD_IS_RELEASE
......@@ -255,60 +282,178 @@ static int gatt_svr_chr_ota_stat_access(uint16_t conn_handle, uint16_t attr_hand
return BLE_ATT_ERR_UNLIKELY;
}
static void ble_heartbeat_task(void *param)
static void ble_send_heartbeat_once(void)
{
(void)param;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(BLE_HEART_PERIOD_MS));
if (!ble_link_up_s || hb_chr_val_handle_s == 0U) {
continue;
return;
}
if (ota_offer_session_busy()) {
return;
}
char dev[32] = {0};
(void)read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
char *json = heart_payload_json_malloc(dev, NULL, false);
if (json == NULL) {
continue;
}
size_t jlen = strlen(json);
if (jlen >= sizeof(last_hb_buf_s)) {
jlen = sizeof(last_hb_buf_s) - 1U;
return;
}
(void)ble_hb_notify_json(json);
free(json);
}
static void ble_heartbeat_task(void *param)
{
(void)param;
for (;;) {
if (ble_link_up_s && hb_chr_val_handle_s != 0U) {
ble_send_heartbeat_once();
}
vTaskDelay(pdMS_TO_TICKS(BLE_HEART_PERIOD_MS));
}
}
/** BLE OTA(0xFFE2):OTA 帧协议见 ota_frame_protocol.h;0xFFE4 回传整帧 ACK/NAK */
static bool ble_is_ota_opcode(uint8_t b)
{
return (b == OTA_OP_BEGIN || b == OTA_OP_DATA || b == OTA_OP_END);
}
/** BLE OTA(0xFFE2):使用通用 OTA 二进制流模块
* 首字节 opcode:0x01 开始(后跟 uint32_t LE 固件总长度),0x02 数据(后跟原始字节),0x03 结束并校验长度后重启。
*/
static void ble_ota_tx_frame(const uint8_t *frame, uint16_t len, void *ctx)
{
(void)ctx;
ble_ota_notify_payload(frame, len);
}
/* OTA 状态回调:通过 0xFFE4 Notify 发送 OTA 状态 */
/* OTA 状态:仅缓存;进度用 1002 JSON(FFE3),固件字节确认走 FFE4 单字节 Notify */
static void ble_ota_status_callback(const char *json)
{
if (json == NULL || ota_stat_chr_val_handle_s == 0 || !ble_link_up_s) {
if (json == NULL) {
return;
}
strncpy(last_ota_stat_json_s, json, sizeof(last_ota_stat_json_s) - 1);
last_ota_stat_json_s[sizeof(last_ota_stat_json_s) - 1] = '\0';
}
static void ble_ota_notify_payload(const uint8_t *payload, uint16_t payload_len)
{
if (!ble_link_up_s || ota_stat_chr_val_handle_s == 0U || payload == NULL || payload_len == 0U) {
return;
}
(void)link_dma_ble_post_tx(ota_stat_chr_val_handle_s, payload, payload_len);
}
static int ble_dma_tx_notify(uint16_t attr_handle, const uint8_t *data, uint16_t len, void *ctx)
{
(void)ctx;
if (!ble_link_up_s || ble_conn_handle_s == 0U || data == NULL || len == 0U) {
return -1;
}
struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len);
if (om == NULL) {
return -1;
}
int nrc = ble_gatts_notify_custom(ble_conn_handle_s, attr_handle, om);
if (nrc != 0) {
os_mbuf_free_chain(om);
}
return nrc;
}
static int ble_dma_process_ota_payload(const uint8_t *buf, uint16_t copy_len)
{
if (!buf || copy_len < 1) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
#if !BUILD_IS_RELEASE
log_ble_rx_payload("FFE2", buf, copy_len);
#endif
if (!ota_offer_is_armed() && !ota_binary_stream_is_active()) {
ESP_LOGW(tag_s, "FFE2 忽略 OTA:尚未收到手机 message_type=0");
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
}
if (ota_frame_rx_looks_framed(buf, copy_len)) {
ota_frame_rx_feed(buf, copy_len);
return 0;
}
if (copy_len >= 1U && !ble_is_ota_opcode(buf[0]) &&
(ota_binary_stream_is_active() || ota_offer_is_armed())) {
ble_ota_feed_raw_bytes(buf, copy_len);
return 0;
}
int result = ble_ota_process_packet(buf, copy_len);
if (result == 0) {
return 0;
}
if (result == -1 || result == -2) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
return BLE_ATT_ERR_UNLIKELY;
}
static void ble_dma_rx_dispatch(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len, void *ctx)
{
(void)ctx;
if (!data || len == 0U) {
return;
}
if (ch == LINK_DMA_BLE_CH_JSON) {
if (len > REMOTE_CTRL_JSON_MAX_BYTES) {
return;
}
size_t n = strlen(json);
if (n == 0 || n > 240) {
memcpy(s_ffe1_json_buf_s, data, len);
s_ffe1_json_buf_s[len] = '\0';
#if !BUILD_IS_RELEASE
log_ble_rx_payload("FFE1", (const uint8_t *)s_ffe1_json_buf_s, len);
ESP_LOGI(tag_s, "FFE1 JSON dispatch start");
#endif
(void)ble_dispatch_json_line(s_ffe1_json_buf_s);
#if !BUILD_IS_RELEASE
ESP_LOGI(tag_s, "FFE1 JSON dispatch done");
#endif
return;
}
struct os_mbuf *om = ble_hs_mbuf_from_flat(json, (uint16_t)n);
if (om != NULL) {
ble_gatts_notify_custom(ble_conn_handle_s, ota_stat_chr_val_handle_s, om);
(void)ble_dma_process_ota_payload(data, len);
}
static void ble_ota_feed_raw_bytes(const uint8_t *data, uint16_t len)
{
for (uint16_t i = 0; i < len; i++) {
uint8_t pkt[2] = { OTA_OP_DATA, data[i] };
if (ota_binary_stream_process_packet(pkt, 2) == 0) {
ble_ota_notify_payload(&data[i], 1);
}
}
}
static int ble_ota_process_packet(const uint8_t *data, uint16_t len)
{
if (!data || len == 0U) {
return -1;
}
uint8_t op = data[0];
int rc = ota_binary_stream_process_packet(data, len);
if (rc == 0 && op == OTA_OP_DATA && len > 1U) {
ble_ota_notify_payload(data + 1, (uint16_t)(len - 1U));
}
return rc;
}
/* BLE OTA 初始化 */
static int ble_ota_init(void)
{
/* 初始化通用 OTA 模块,传入 BLE 传输类型和状态回调 */
esp_err_t err = ota_binary_stream_init(OTA_TRANSPORT_BLE, ble_ota_status_callback, NULL);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "BLE OTA init failed: %s", esp_err_to_name(err));
return -1;
}
ota_frame_protocol_init(ble_ota_tx_frame, NULL);
return 0;
}
......@@ -335,23 +480,10 @@ static int gatt_svr_chr_ota_access(uint16_t conn_handle, uint16_t attr_handle,
return BLE_ATT_ERR_UNLIKELY;
}
#if !BUILD_IS_RELEASE
log_ble_rx_payload("FFE2", buf, copy_len);
#endif
/* 使用通用 OTA 二进制流模块处理数据包 */
int result = ota_binary_stream_process_packet(buf, copy_len);
/* 映射返回码为 BLE ATT 错误码 */
if (result == 0) {
return 0; /* 成功 */
} else if (result == -1) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
} else if (result == -2) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
} else {
return BLE_ATT_ERR_UNLIKELY;
if (!link_dma_ble_post_rx(LINK_DMA_BLE_CH_OTA, buf, copy_len)) {
return BLE_ATT_ERR_INSUFFICIENT_RES;
}
return 0;
}
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
......@@ -406,21 +538,109 @@ static int gatt_svr_chr_access(uint16_t conn_handle, uint16_t attr_handle,
if (rc != 0) {
return BLE_ATT_ERR_UNLIKELY;
}
s_ffe1_json_buf_s[copy_len] = '\0';
#if !BUILD_IS_RELEASE
log_ble_rx_payload("FFE1", (const uint8_t *)s_ffe1_json_buf_s, copy_len);
ESP_LOGI(tag_s, "FFE1 JSON dispatch start");
#endif
remote_control_apply_json(s_ffe1_json_buf_s);
#if !BUILD_IS_RELEASE
ESP_LOGI(tag_s, "FFE1 JSON dispatch done");
#endif
if (!link_dma_ble_post_rx(LINK_DMA_BLE_CH_JSON, (const uint8_t *)s_ffe1_json_buf_s, copy_len)) {
return BLE_ATT_ERR_INSUFFICIENT_RES;
}
return 0;
}
return BLE_ATT_ERR_UNLIKELY;
}
static void ble_ota_done_report(bool success, uint32_t bytes_written, const char *err_step)
{
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step);
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);
ESP_LOGI(tag_s, "BLE TX OTA 进行中 (1002/active): %s", json);
free(json);
}
}
/** 手机 message_type=6:回复 OTA 询问(每次连接后 App 发 6 即回 1002) */
static void ble_send_ota_query_on_link_ready(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_query_json_malloc(devid);
if (json) {
(void)ble_hb_notify_json(json);
ESP_LOGI(tag_s, "BLE TX OTA 询问 (1002,已收 link_ready=6): %s", json);
free(json);
}
}
static bool ble_dispatch_json_line(const char *json)
{
if (!json || json[0] == '\0') {
return false;
}
cJSON *root = cJSON_Parse(json);
if (!root) {
remote_control_apply_json(json);
return true;
}
cJSON *head = cJSON_GetObjectItem(root, "head");
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) {
link_ble_session_reset_ex("reset");
ESP_LOGI(tag_s, "收到 message_type=2,已清空 BLE 会话");
cJSON_Delete(root);
return true;
}
if (m_type && cJSON_IsNumber(m_type) && (int)m_type->valuedouble == REMOTE_CTRL_MSG_LINK_READY) {
ble_send_ota_query_on_link_ready();
cJSON_Delete(root);
return true;
}
if (ota_offer_parse_phone_response(root)) {
ESP_LOGI(tag_s, "手机同意 OTA (head.message_type=0),可写 0xFFE2");
ble_send_ota_active_notice();
cJSON_Delete(root);
return true;
}
remote_control_apply_json_root(root, json);
cJSON_Delete(root);
return true;
}
static int gap_event(struct ble_gap_event *event, void *arg)
{
(void)arg;
......@@ -429,11 +649,15 @@ static int gap_event(struct ble_gap_event *event, void *arg)
ESP_LOGI(tag_s, "GAP connect status=%d", event->connect.status);
if (event->connect.status == 0) {
ble_conn_handle_s = event->connect.conn_handle;
link_dma_ble_set_conn_handle(event->connect.conn_handle);
ble_link_up_s = true;
/* 连接建立时启动遥控超时守护 */
link_ble_session_reset();
link_ble_apply_tx_power();
ble_request_low_duty_conn(event->connect.conn_handle);
if (remote_control_watchdog_start() != ESP_OK) {
ESP_LOGW(tag_s, "Failed to start RC watchdog");
}
ble_send_heartbeat_once();
} else {
ble_link_up_s = false;
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params_s, gap_event, NULL);
......@@ -442,9 +666,11 @@ static int gap_event(struct ble_gap_event *event, void *arg)
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(tag_s, "GAP disconnect reason=%d", event->disconnect.reason);
ble_link_up_s = false;
remote_control_watchdog_stop(); /* 断开时停止超时守护 */
rc_vehicle_stop(); /* 蓝牙断开时立即停止车辆 */
ESP_LOGI(tag_s, "BLE disconnected, vehicle stopped");
ble_conn_handle_s = 0U;
link_dma_ble_set_conn_handle(0);
remote_control_watchdog_stop();
link_ble_session_reset_ex("disconnect");
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);
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
......@@ -510,22 +736,112 @@ static void ble_host_task(void *param)
nimble_port_run();
}
static void link_ble_apply_tx_power(void)
{
static const esp_ble_power_type_t k_pwr_types[] = {
ESP_BLE_PWR_TYPE_DEFAULT,
ESP_BLE_PWR_TYPE_ADV,
ESP_BLE_PWR_TYPE_SCAN,
ESP_BLE_PWR_TYPE_CONN_HDL0,
ESP_BLE_PWR_TYPE_CONN_HDL1,
ESP_BLE_PWR_TYPE_CONN_HDL2,
ESP_BLE_PWR_TYPE_CONN_HDL3,
ESP_BLE_PWR_TYPE_CONN_HDL4,
ESP_BLE_PWR_TYPE_CONN_HDL5,
ESP_BLE_PWR_TYPE_CONN_HDL6,
ESP_BLE_PWR_TYPE_CONN_HDL7,
ESP_BLE_PWR_TYPE_CONN_HDL8,
};
for (size_t i = 0; i < sizeof(k_pwr_types) / sizeof(k_pwr_types[0]); i++) {
esp_err_t e = esp_ble_tx_power_set(k_pwr_types[i], LINK_BLE_TX_PWR);
if (e != ESP_OK) {
ESP_LOGW(tag_s, "tx_power type=%d: %s", (int)k_pwr_types[i], esp_err_to_name(e));
}
}
ESP_LOGI(tag_s, "BLE 发射功率已设为 %d(贴机降干扰)", (int)LINK_BLE_TX_PWR);
}
static void ble_request_low_duty_conn(uint16_t conn_handle)
{
struct ble_gap_upd_params params = {
.itvl_min = BLE_GAP_CONN_ITVL_MS(45),
.itvl_max = BLE_GAP_CONN_ITVL_MS(60),
.latency = 6,
.supervision_timeout = 400,
};
int rc = ble_gap_update_params(conn_handle, &params);
if (rc != 0) {
ESP_LOGW(tag_s, "conn param update rc=%d", rc);
}
}
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();
remote_control_link_reset();
memset(s_ffe1_json_buf_s, 0, sizeof(s_ffe1_json_buf_s));
last_hb_len_s = 0;
last_hb_buf_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)
{
/* 简化:不在此版本做完整 teardown */
if (!s_ble_stack_running_s) {
return;
}
link_dma_ble_stop();
ble_link_up_s = false;
remote_control_watchdog_stop();
link_ble_session_reset();
(void)ble_gap_adv_stop();
if (ble_conn_handle_s != 0U) {
(void)ble_gap_terminate(ble_conn_handle_s, BLE_ERR_REM_USER_CONN_TERM);
ble_conn_handle_s = 0U;
}
int rc = nimble_port_stop();
if (rc == 0) {
nimble_port_deinit();
} else {
ESP_LOGW(tag_s, "nimble_port_stop rc=%d", rc);
}
s_ble_stack_running_s = false;
ESP_LOGI(tag_s, "BLE 已停止(配网/关 WiFi 时使用)");
}
esp_err_t link_ble_start(void)
{
if (s_ble_stack_running_s) {
return ESP_OK;
}
ota_offer_reset_boot();
ota_offer_set_done_callback(ble_ota_done_report);
/* 初始化 BLE OTA 模块 */
ble_ota_init();
esp_err_t dma_err = link_dma_ble_start(ble_dma_rx_dispatch, NULL, ble_dma_tx_notify, NULL);
if (dma_err != ESP_OK) {
ESP_LOGE(tag_s, "BLE DMA queue init failed: %s", esp_err_to_name(dma_err));
return dma_err;
}
/* ESP-IDF 5.x:控制器与 VHCI 由 nimble_port_init() 内部完成,勿再调用已移除的 esp_nimble_hci_and_controller_init */
esp_err_t err = nimble_port_init();
if (err != ESP_OK) {
ESP_LOGE(tag_s, "nimble_port_init: %s", esp_err_to_name(err));
return err;
}
link_ble_apply_tx_power();
s_ble_stack_running_s = true;
ble_hs_cfg.reset_cb = ble_on_reset;
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.gatts_register_cb = link_ble_gatt_register_cb;
......@@ -539,21 +855,25 @@ esp_err_t link_ble_start(void)
int rc = ble_gatts_count_cfg(gatt_svr_svcs);
if (rc != 0) {
ESP_LOGE(tag_s, "gatts_count_cfg %d", rc);
link_ble_stop();
return ESP_FAIL;
}
rc = ble_gatts_add_svcs(gatt_svr_svcs);
if (rc != 0) {
ESP_LOGE(tag_s, "gatts_add_svcs %d", rc);
link_ble_stop();
return ESP_FAIL;
}
if (app_task_start(APP_TASK_BLE_HEARTBEAT, ble_heartbeat_task, NULL, NULL) != ESP_OK) {
ESP_LOGE(tag_s, "ble heartbeat task create failed");
link_ble_stop();
return ESP_FAIL;
}
if (app_task_start_nimble_host(ble_host_task) != ESP_OK) {
ESP_LOGE(tag_s, "ble host task create failed");
link_ble_stop();
return ESP_FAIL;
}
return ESP_OK;
......
......@@ -8,7 +8,7 @@
* - 0xFFE2:固件 OTA 二进制流(首字节 opcode:0x01+uint32 LE 总长,0x02+固件数据,0x03 结束并重启;受 menuconfig「APP_BLE_OTA」控制)
* - 0xFFE3:心跳 / 告警 / 错误 JSON,Read + Notify(心跳默认 3s;W/E 出现时即时另发一条
* - 0xFFE3:心跳 / 告警 / 错误 JSON,Read + Notify(心跳默认 10s,连接成功立即首条;W/E 即时另发
* - 0xFFE4:OTA 过程状态 JSON,Read(最近一次)+ Notify(每步应答;需 enableNotification)
......@@ -28,6 +28,9 @@ esp_err_t link_ble_start(void);
void link_ble_stop(void);
/** 清空 BLE 侧缓冲并中止 OTA/遥控会话(断开或 message_type=2 时) */
void link_ble_session_reset(void);
#endif
......
#include "link_dma_ble.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <stdlib.h>
#include <string.h>
static const char *tag_s = "LINK_DMA_BLE";
#define BLE_DMA_RX_QUEUE_DEPTH 24
#define BLE_DMA_TX_QUEUE_DEPTH 48
#define BLE_DMA_RX_MAX_BYTES 512
#define BLE_DMA_TX_MAX_BYTES 256
typedef struct {
link_dma_ble_ch_t ch;
uint16_t len;
uint8_t data[BLE_DMA_RX_MAX_BYTES];
} ble_dma_rx_item_t;
typedef struct {
uint16_t attr_handle;
uint16_t len;
uint8_t *data;
} ble_dma_tx_item_t;
static QueueHandle_t rx_q_s;
static QueueHandle_t tx_q_s;
static TaskHandle_t rx_task_s;
static TaskHandle_t tx_task_s;
static link_dma_ble_rx_cb_t rx_cb_s;
static void *rx_ctx_s;
static link_dma_ble_tx_fn_t tx_fn_s;
static void *tx_ctx_s;
static volatile uint16_t conn_handle_s;
static volatile bool running_s;
static void ble_dma_rx_task(void *param)
{
(void)param;
ble_dma_rx_item_t item;
while (running_s) {
if (xQueueReceive(rx_q_s, &item, pdMS_TO_TICKS(100)) == pdTRUE) {
if (rx_cb_s && item.len > 0) {
rx_cb_s(item.ch, item.data, item.len, rx_ctx_s);
}
}
}
vTaskDelete(NULL);
}
static void ble_dma_tx_task(void *param)
{
(void)param;
ble_dma_tx_item_t item;
while (running_s) {
if (xQueueReceive(tx_q_s, &item, pdMS_TO_TICKS(100)) != pdTRUE) {
continue;
}
if (!tx_fn_s || conn_handle_s == 0U || item.data == NULL || item.len == 0) {
free(item.data);
continue;
}
int rc = tx_fn_s(item.attr_handle, item.data, item.len, tx_ctx_s);
if (rc != 0) {
vTaskDelay(pdMS_TO_TICKS(2));
(void)tx_fn_s(item.attr_handle, item.data, item.len, tx_ctx_s);
}
free(item.data);
}
vTaskDelete(NULL);
}
esp_err_t link_dma_ble_start(link_dma_ble_rx_cb_t rx_cb, void *rx_ctx,
link_dma_ble_tx_fn_t tx_fn, void *tx_ctx)
{
if (running_s) {
return ESP_OK;
}
if (!rx_cb || !tx_fn) {
return ESP_ERR_INVALID_ARG;
}
rx_cb_s = rx_cb;
rx_ctx_s = rx_ctx;
tx_fn_s = tx_fn;
tx_ctx_s = tx_ctx;
rx_q_s = xQueueCreate(BLE_DMA_RX_QUEUE_DEPTH, sizeof(ble_dma_rx_item_t));
tx_q_s = xQueueCreate(BLE_DMA_TX_QUEUE_DEPTH, sizeof(ble_dma_tx_item_t));
if (!rx_q_s || !tx_q_s) {
link_dma_ble_stop();
return ESP_ERR_NO_MEM;
}
running_s = true;
if (xTaskCreate(ble_dma_rx_task, "ble_dma_rx", 4096, NULL, 6, &rx_task_s) != pdPASS ||
xTaskCreate(ble_dma_tx_task, "ble_dma_tx", 4096, NULL, 5, &tx_task_s) != pdPASS) {
running_s = false;
link_dma_ble_stop();
return ESP_ERR_NO_MEM;
}
ESP_LOGI(tag_s, "BLE 异步收发队列已启动 (RX depth=%d, TX depth=%d)",
BLE_DMA_RX_QUEUE_DEPTH, BLE_DMA_TX_QUEUE_DEPTH);
return ESP_OK;
}
void link_dma_ble_stop(void)
{
running_s = false;
if (rx_task_s) {
vTaskDelete(rx_task_s);
rx_task_s = NULL;
}
if (tx_task_s) {
vTaskDelete(tx_task_s);
tx_task_s = NULL;
}
if (rx_q_s) {
vQueueDelete(rx_q_s);
rx_q_s = NULL;
}
if (tx_q_s) {
ble_dma_tx_item_t item;
while (xQueueReceive(tx_q_s, &item, 0) == pdTRUE) {
free(item.data);
}
vQueueDelete(tx_q_s);
tx_q_s = NULL;
}
rx_cb_s = NULL;
tx_fn_s = NULL;
conn_handle_s = 0;
}
bool link_dma_ble_post_rx(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len)
{
if (!running_s || !data || len == 0 || len > BLE_DMA_RX_MAX_BYTES) {
return false;
}
ble_dma_rx_item_t item = {
.ch = ch,
.len = len,
};
memcpy(item.data, data, len);
if (xQueueSend(rx_q_s, &item, 0) != pdTRUE) {
ESP_LOGW(tag_s, "BLE RX 队列满,丢弃 %u 字节", (unsigned)len);
return false;
}
return true;
}
bool link_dma_ble_post_tx(uint16_t attr_handle, const uint8_t *data, uint16_t len)
{
if (!running_s || !data || len == 0 || len > BLE_DMA_TX_MAX_BYTES) {
return false;
}
uint8_t *copy = (uint8_t *)malloc(len);
if (!copy) {
return false;
}
memcpy(copy, data, len);
ble_dma_tx_item_t item = {
.attr_handle = attr_handle,
.len = len,
.data = copy,
};
if (xQueueSend(tx_q_s, &item, 0) != pdTRUE) {
ESP_LOGW(tag_s, "BLE TX 队列满,丢弃 %u 字节", (unsigned)len);
free(copy);
return false;
}
return true;
}
void link_dma_ble_set_conn_handle(uint16_t conn_handle)
{
conn_handle_s = conn_handle;
}
/**
* BLE 链路异步收发缓冲
* NimBLE 与 UHCI 共用 HCI 硬件,无法用 UART DMA;此处用环形队列模拟 DMA 双缓冲,防止 GATT 回调阻塞导致溢出。
*/
#ifndef LINK_DMA_BLE_H
#define LINK_DMA_BLE_H
#include "esp_err.h"
#include <stddef.h>
#include <stdint.h>
typedef enum {
LINK_DMA_BLE_CH_JSON = 0,
LINK_DMA_BLE_CH_OTA = 1,
} link_dma_ble_ch_t;
typedef void (*link_dma_ble_rx_cb_t)(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len, void *ctx);
/** payload 由队列持有,发送完成后自动释放 */
typedef int (*link_dma_ble_tx_fn_t)(uint16_t attr_handle, const uint8_t *data, uint16_t len, void *ctx);
esp_err_t link_dma_ble_start(link_dma_ble_rx_cb_t rx_cb, void *rx_ctx,
link_dma_ble_tx_fn_t tx_fn, void *tx_ctx);
void link_dma_ble_stop(void);
/** GATT 写回调中调用:拷贝入队后立即返回 */
bool link_dma_ble_post_rx(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len);
/** 排队 Notify/Write;conn 与 handle 由 tx_fn 使用 */
bool link_dma_ble_post_tx(uint16_t attr_handle, const uint8_t *data, uint16_t len);
void link_dma_ble_set_conn_handle(uint16_t conn_handle);
#endif /* LINK_DMA_BLE_H */
#include "link_dma_uart.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include <stdlib.h>
#include <string.h>
static const char *tag_s = "LINK_DMA_UART";
#define UART_BUF_SIZE 4096
#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 TaskHandle_t rx_task_s;
static link_dma_uart_rx_cb_t rx_cb_s;
static void *rx_ctx_s;
static uart_port_t uart_port_s;
static volatile bool running_s;
static SemaphoreHandle_t tx_mutex_s;
static void uart_evt_rx_task(void *param)
{
(void)param;
uint8_t *buf = (uint8_t *)malloc(UART_BUF_SIZE);
if (!buf) {
vTaskDelete(NULL);
return;
}
uart_event_t evt;
while (running_s) {
if (xQueueReceive(uart_evt_q_s, &evt, pdMS_TO_TICKS(50)) != pdTRUE) {
continue;
}
switch (evt.type) {
case UART_DATA:
case UART_BUFFER_FULL:
case UART_FIFO_OVF:
if (evt.type == UART_FIFO_OVF) {
ESP_LOGW(tag_s, "UART FIFO overflow, flushing");
uart_flush_input(uart_port_s);
}
for (;;) {
int n = uart_read_bytes(uart_port_s, buf, UART_BUF_SIZE,
(evt.type == UART_DATA) ? 0 : pdMS_TO_TICKS(10));
if (n <= 0) {
break;
}
if (rx_cb_s) {
rx_cb_s(buf, (size_t)n, rx_ctx_s);
}
}
break;
default:
break;
}
}
free(buf);
vTaskDelete(NULL);
}
esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int rx_gpio,
link_dma_uart_rx_cb_t rx_cb, void *rx_ctx)
{
if (running_s) {
return ESP_OK;
}
if (tx_gpio < 0 || rx_gpio < 0 || rx_cb == NULL) {
return ESP_ERR_INVALID_ARG;
}
uart_port_s = port;
rx_cb_s = rx_cb;
rx_ctx_s = rx_ctx;
uart_config_t uart_cfg = {
.baud_rate = baud_rate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
gpio_reset_pin((gpio_num_t)tx_gpio);
gpio_reset_pin((gpio_num_t)rx_gpio);
esp_err_t err = uart_param_config(port, &uart_cfg);
if (err != ESP_OK) {
return err;
}
err = uart_set_pin(port, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
return err;
}
err = uart_driver_install(port, UART_BUF_SIZE, UART_BUF_SIZE, UART_EVT_QUEUE_LEN,
&uart_evt_q_s, 0);
if (err != ESP_OK) {
return err;
}
uart_set_rx_full_threshold(port, 64);
uart_set_rx_timeout(port, 10);
gpio_set_pull_mode((gpio_num_t)rx_gpio, GPIO_PULLUP_ONLY);
uart_flush(port);
tx_mutex_s = xSemaphoreCreateMutex();
if (!tx_mutex_s) {
link_dma_uart_stop();
return ESP_ERR_NO_MEM;
}
running_s = true;
if (xTaskCreatePinnedToCore(uart_evt_rx_task, "uart_evt_rx", 6144, NULL, 6,
&rx_task_s, UART_RX_TASK_CORE) != pdPASS) {
running_s = false;
link_dma_uart_stop();
return ESP_ERR_NO_MEM;
}
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, UART_RX_TASK_CORE);
return ESP_OK;
}
void link_dma_uart_stop(void)
{
running_s = false;
if (rx_task_s) {
vTaskDelete(rx_task_s);
rx_task_s = NULL;
}
if (uart_port_s >= 0 && uart_evt_q_s != NULL) {
uart_driver_delete(uart_port_s);
}
uart_evt_q_s = NULL;
if (tx_mutex_s) {
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
}
rx_cb_s = NULL;
rx_ctx_s = NULL;
}
int link_dma_uart_send(const uint8_t *data, size_t len, int timeout_ms)
{
if (!running_s || !data || len == 0) {
return -1;
}
int wait_ms = (timeout_ms > 0) ? timeout_ms : 500;
if (tx_mutex_s == NULL || xSemaphoreTake(tx_mutex_s, pdMS_TO_TICKS(wait_ms)) != pdTRUE) {
ESP_LOGW(tag_s, "UART TX mutex timeout");
return -2;
}
int n = uart_write_bytes(uart_port_s, data, len);
if (n > 0) {
esp_err_t err = uart_wait_tx_done(uart_port_s, pdMS_TO_TICKS(wait_ms));
if (err != ESP_OK) {
ESP_LOGW(tag_s, "uart_wait_tx_done: %s", esp_err_to_name(err));
n = -2;
}
} else {
ESP_LOGW(tag_s, "uart_write_bytes failed: %d", n);
}
(void)xSemaphoreGive(tx_mutex_s);
return n;
}
bool link_dma_uart_is_running(void)
{
return running_s;
}
/**
* UART 链路事件驱动收发
* uart_driver_install + UART_DATA 事件立即读空,降低 FIFO 溢出风险。
*/
#ifndef LINK_DMA_UART_H
#define LINK_DMA_UART_H
#include "esp_err.h"
#include "driver/uart.h"
#include <stddef.h>
#include <stdint.h>
typedef void (*link_dma_uart_rx_cb_t)(const uint8_t *data, size_t len, void *ctx);
esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int rx_gpio,
link_dma_uart_rx_cb_t rx_cb, void *rx_ctx);
void link_dma_uart_stop(void);
/** 阻塞直到本批 DMA 发送完成;timeout_ms<0 表示一直等 */
int link_dma_uart_send(const uint8_t *data, size_t len, int timeout_ms);
bool link_dma_uart_is_running(void);
#endif /* LINK_DMA_UART_H */
......@@ -7,9 +7,9 @@
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "link_dma_uart.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
......@@ -19,18 +19,32 @@
#include "core/build_config.h"
#include "task_manager.h"
#if BUILD_IS_DEBUG
/* Debug 固定仅打印 JSON 级日志;RAW 日志关闭,避免刷屏 */
#define UART_LINK_LOG_TX_JSON 1
#define UART_LINK_LOG_TX_RAW 0
#define UART_LINK_LOG_RX_JSON 1
#define UART_LINK_LOG_RX_RAW 0
#else
#define UART_LINK_LOG_TX_JSON 0
#define UART_LINK_LOG_TX_RAW 0
#define UART_LINK_LOG_RX_JSON 0
#define UART_LINK_LOG_RX_RAW 0
#endif
#include "heart_payload.h"
#include "ota_offer_protocol.h"
#include "ota_frame_protocol.h"
#include "ota_uart_tune.h"
/* OTA 二进制流模块 */
#include "ota_binary_stream.h"
#include "cJSON.h"
static const char *tag_s = "LINK_UART";
/* UART 配置 */
#define UART_LINK_PORT UART_NUM_1
#define UART_LINK_BUF_SIZE 2048
#define UART_LINK_QUEUE_SIZE 16
#define UART_LINK_RX_TIMEOUT_MS 100
/* JSON 接收缓冲 */
#define UART_JSON_MAX_BYTES REMOTE_CTRL_JSON_MAX_BYTES
......@@ -40,17 +54,50 @@ static const char *tag_s = "LINK_UART";
#define UART_HEART_PERIOD_MS 3000
#endif
/* 心跳:对端曾 RX 且在此窗口内(允许 App 只收不发) */
#define UART_LINK_ALIVE_MS (UART_HEART_PERIOD_MS + 5000)
/* 拔线/无字节:超过此时间无 RX 即停停车推送 */
#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 分隔) */
#define UART_FRAME_DELIMITER '\n'
/* 函数前向声明 */
static int uart_link_send_json_line(const char *json);
static void uart_send_heartbeat_once(void);
static TaskHandle_t rx_task_handle_s = NULL;
static TaskHandle_t hb_task_handle_s = NULL;
static QueueHandle_t uart_queue_s = NULL;
static TaskHandle_t uart_rx_worker_handle_s = NULL;
static volatile bool link_up_s = false;
static volatile uint32_t last_rx_time_ms = 0;
/* OTA 期间动态栈管理 */
static volatile bool uart_rx_worker_is_ota_stack_s = false;
static SemaphoreHandle_t uart_rx_worker_mutex_s = NULL;
/** 上次从 UART1 RX 收到对端数据的时刻;0=从未收到 */
static volatile uint32_t last_peer_rx_ms = 0;
/* 发送互斥 */
static SemaphoreHandle_t tx_mutex_s = NULL;
......@@ -61,18 +108,19 @@ static size_t rx_buf_pos_s = 0;
/* OTA 状态缓冲 */
static char last_ota_stat_json_s[192];
static bool uart_ota_framed_mode_s = false;
/* OTA 专用接收缓冲 */
static uint8_t ota_rx_buf_s[512];
static size_t ota_rx_pos_s = 0;
static bool ota_rx_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)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
/* UART OTA 状态回调:通过 UART 发送 OTA 状态 JSON */
/* UART OTA 状态:仅缓存供调试,不向串口发 JSON(固件数据按字节回传) */
static void uart_ota_status_callback(const char *json)
{
if (json == NULL) {
......@@ -80,9 +128,6 @@ static void uart_ota_status_callback(const char *json)
}
strncpy(last_ota_stat_json_s, json, sizeof(last_ota_stat_json_s) - 1);
last_ota_stat_json_s[sizeof(last_ota_stat_json_s) - 1] = '\0';
/* 直接通过 UART 发送 OTA 状态 */
uart_link_send_json_line(json);
}
/* UART OTA 初始化 */
......@@ -97,32 +142,126 @@ static void uart_ota_init(void)
}
}
#if UART_LINK_LOG_TX_JSON
static const char *uart_tx_json_kind_hint(const char *json)
{
if (!json) {
return "JSON";
}
if (strstr(json, "\"message_type\":1002") != NULL) {
return "OTA(1002)";
}
if (strstr(json, "\"message_type\":1}") != NULL ||
strstr(json, "\"message_type\": 1}") != NULL ||
strstr(json, "\"message_type\":1,") != NULL ||
strstr(json, "\"message_type\": 1,") != NULL) {
return "心跳(1)";
}
if (strstr(json, "\"message_type\":3") != NULL) {
return "停车/状态(3)";
}
if (strstr(json, "\"message_type\":4") != NULL ||
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";
}
#endif
static void uart_link_log_tx_json(const char *json)
{
#if UART_LINK_LOG_TX_JSON
if (json) {
ESP_LOGI(tag_s, "UART1 TX %s: %s", uart_tx_json_kind_hint(json), json);
}
#else
(void)json;
#endif
}
static void uart_link_log_tx_raw(const uint8_t *data, size_t len)
{
#if UART_LINK_LOG_TX_RAW
if (!data || len == 0) {
return;
}
/* 仅打印长度和 ASCII 预览,省略 hex dump 便于阅读 */
char ascii[65];
size_t n = len > sizeof(ascii) - 1 ? sizeof(ascii) - 1 : len;
for (size_t i = 0; i < n; i++) {
uint8_t c = data[i];
ascii[i] = (c >= 32 && c < 127) ? (char)c : '.';
}
ascii[n] = '\0';
if (len > n) {
ESP_LOGI(tag_s, "UART1 TX %u bytes: \"%s\"...", (unsigned)len, ascii);
} else {
ESP_LOGI(tag_s, "UART1 TX %u bytes: \"%s\"", (unsigned)len, ascii);
}
#else
(void)data;
(void)len;
#endif
}
static int uart_link_send_internal(const uint8_t *data, size_t len)
{
if (!data || len == 0) {
return -1;
}
int written = uart_write_bytes(UART_LINK_PORT, data, len);
return written;
return link_dma_uart_send(data, len, 200);
}
int link_uart_send_raw(const uint8_t *data, size_t len)
static bool uart_link_tx_acquire(TickType_t timeout)
{
if (tx_mutex_s == NULL) {
return -1;
return false;
}
if (xSemaphoreTake(tx_mutex_s, timeout) != pdTRUE) {
ESP_LOGW(tag_s, "UART1 TX 互斥等待超时");
return false;
}
return true;
}
if (xSemaphoreTake(tx_mutex_s, pdMS_TO_TICKS(100)) != pdTRUE) {
return -1;
static void uart_link_tx_release(void)
{
if (tx_mutex_s != NULL) {
xSemaphoreGive(tx_mutex_s);
}
}
static int uart_link_tx(const uint8_t *data, size_t len, TickType_t mutex_timeout)
{
if (!data || len == 0) {
return -1;
}
if (!uart_link_tx_acquire(mutex_timeout)) {
return -2;
}
int ret = uart_link_send_internal(data, len);
xSemaphoreGive(tx_mutex_s);
if (ret > 0) {
uart_link_log_tx_raw(data, len);
}
uart_link_tx_release();
return ret;
}
static void uart_link_note_peer_rx(void)
{
last_peer_rx_ms = get_current_time_ms();
link_up_s = true;
}
int link_uart_send_raw(const uint8_t *data, size_t len)
{
return uart_link_tx(data, len, portMAX_DELAY);
}
static int uart_link_send_json_line(const char *json)
{
if (!json) {
......@@ -134,17 +273,17 @@ static int uart_link_send_json_line(const char *json)
return -1;
}
/* 发送 JSON 加换行符 */
if (xSemaphoreTake(tx_mutex_s, pdMS_TO_TICKS(100)) != pdTRUE) {
return -1;
if (!uart_link_tx_acquire(portMAX_DELAY)) {
return -2;
}
int ret = uart_link_send_internal((const uint8_t *)json, len);
if (ret > 0) {
ret += uart_link_send_internal((const uint8_t *)"\n", 1);
uart_link_log_tx_json(json);
}
xSemaphoreGive(tx_mutex_s);
uart_link_tx_release();
return ret;
}
......@@ -158,48 +297,260 @@ int link_uart_send_log(const char *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)
{
uint32_t last = last_peer_rx_ms;
if (last == 0) {
return false;
}
return (get_current_time_ms() - last) < UART_PEER_UNPLUG_MS;
}
bool link_uart_has_peer_seen(void)
{
return last_peer_rx_ms != 0;
}
bool link_uart_is_connected(void)
{
/* UART 有最近接收数据即认为在线 */
uint32_t now = get_current_time_ms();
uint32_t last = last_rx_time_ms;
uint32_t last = last_peer_rx_ms;
if (last == 0) {
return false;
}
return (get_current_time_ms() - last) < UART_LINK_ALIVE_MS;
}
/* 5秒内有数据交互认为在线 */
return link_up_s && (now - last) < 5000;
static void uart_send_ota_offer_json(const char *json)
{
if (!json) {
return;
}
(void)uart_link_send_json_line(json);
}
/* 处理接收到的 JSON 数据 */
static void process_received_json(const char *json)
static void uart_ota_done_report(bool success, uint32_t bytes_written, const char *err_step)
{
if (!json || strlen(json) == 0) {
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step);
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);
free(json);
}
}
/** 手机 message_type=6:回复 OTA 询问(每次重连串口可各发一次) */
static void uart_send_ota_query_on_link_ready(void)
{
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
char *json = ota_offer_query_json_malloc(devid);
if (json) {
uart_send_ota_offer_json(json);
free(json);
}
}
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;
uart_ota_framed_mode_s = false;
ota_frame_protocol_reset();
remote_control_link_reset();
}
/* 处理接收到的 JSON(root 已由调用方解析) */
static void process_received_json(cJSON *root, const char *json)
{
if (!root || !json || json[0] == '\0') {
return;
}
#if !BUILD_IS_RELEASE
ESP_LOGI(tag_s, "RX JSON: %s", json);
#if UART_LINK_LOG_RX_JSON
ESP_LOGI(tag_s, "UART1 RX JSON line: %s", json);
#endif
/* 更新最后接收时间 */
last_rx_time_ms = get_current_time_ms();
link_up_s = true;
cJSON *head = cJSON_GetObjectItem(root, "head");
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) {
link_uart_session_reset_ex("reset");
ESP_LOGI(tag_s, "收到 message_type=2,已清空 UART 会话");
return;
}
if (m_type && cJSON_IsNumber(m_type) && (int)m_type->valuedouble == REMOTE_CTRL_MSG_LINK_READY) {
ESP_LOGI(tag_s, "UART1 收到 link_ready(6),回复 OTA 询问 1002/query + 首条心跳");
uart_send_ota_query_on_link_ready();
uart_send_heartbeat_once();
return;
}
if (ota_offer_parse_phone_response(root)) {
ESP_LOGI(tag_s, "手机同意 OTA (head.message_type=0),可发 OT 帧");
uart_send_ota_active_notice();
return;
}
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)
{
(void)ctx;
if (frame == NULL || len == 0) {
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);
}
/* 调用遥控协议处理 JSON */
remote_control_apply_json(json);
/* 旧版裸字节回显(无 OT 帧头时兼容) */
static void uart_link_ota_ack_payload(const uint8_t *payload, uint16_t payload_len)
{
for (uint16_t i = 0; i < payload_len; i++) {
uint8_t b = payload[i];
(void)uart_link_tx(&b, 1, portMAX_DELAY);
}
}
/* 处理 OTA 数据包(二进制格式) */
/* 处理 OTA 数据包(0x01/0x02/0x03);仅 0x02 固件载荷按字节回传 */
static void process_ota_packet(const uint8_t *data, uint16_t len)
{
if (!data || len == 0) {
return;
}
/* 通过通用 OTA 模块处理 */
ota_binary_stream_process_packet(data, len);
if (ota_frame_rx_looks_framed(data, len)) {
ota_frame_rx_feed(data, len);
return;
}
/* OTA 数据包也更新连接状态 */
last_rx_time_ms = get_current_time_ms();
link_up_s = true;
uint8_t op = data[0];
int rc = ota_binary_stream_process_packet(data, len);
if (rc == 0 && op == OTA_OP_DATA && len > 1) {
uart_link_ota_ack_payload(data + 1, (uint16_t)(len - 1));
}
}
/* 检查数据是否为 OTA 命令(以 0x01/0x02/0x03 开头) */
......@@ -208,118 +559,488 @@ static bool is_ota_command(uint8_t first_byte)
return (first_byte == 0x01 || first_byte == 0x02 || first_byte == 0x03);
}
static void uart_rx_task(void *param)
static bool uart_rx_json_balanced(const char *s)
{
(void)param;
int depth = 0;
if (!s || s[0] != '{') {
return false;
}
uint8_t *data = (uint8_t *)malloc(UART_LINK_BUF_SIZE);
if (!data) {
ESP_LOGE(tag_s, "RX buffer alloc failed");
vTaskDelete(NULL);
for (; *s != '\0'; s++) {
if (*s == '{') {
depth++;
} else if (*s == '}') {
depth--;
if (depth < 0) {
return false;
}
}
}
return depth == 0;
}
static void uart_rx_try_finish_json_line(void)
{
if (rx_buf_pos_s == 0) {
return;
}
rx_json_buf_s[rx_buf_pos_s] = '\0';
if (rx_json_buf_s[0] != '{') {
ESP_LOGW(tag_s, "UART1 RX 行结束但非 JSON (len=%u)", (unsigned)rx_buf_pos_s);
rx_buf_pos_s = 0;
ota_rx_pos_s = 0;
ota_rx_mode_s = false;
return;
}
while (1) {
int len = uart_read_bytes(UART_LINK_PORT, data, UART_LINK_BUF_SIZE - 1,
pdMS_TO_TICKS(UART_LINK_RX_TIMEOUT_MS));
cJSON *root = cJSON_Parse(rx_json_buf_s);
if (!root) {
if (!uart_rx_json_balanced(rx_json_buf_s)) {
/* 多行/分段 JSON 尚未收齐,继续累积 */
return;
}
ESP_LOGW(tag_s, "UART1 RX JSON 无效 (len=%u)", (unsigned)rx_buf_pos_s);
rx_buf_pos_s = 0;
return;
}
if (len > 0) {
last_rx_time_ms = get_current_time_ms();
process_received_json(root, rx_json_buf_s);
cJSON_Delete(root);
rx_buf_pos_s = 0;
}
/* 检查是否为 OTA 命令(首字节为 0x01/0x02/0x03) */
if (len > 0 && is_ota_command(data[0])) {
/* OTA 二进制数据模式 */
/* 原始接收调试:本函数用 ESP_LOG 打印;输出在「控制台 UART」(默认 UART0/USB),与 UART1 业务引脚可能相同,见文档 §1.5 */
static void log_uart1_rx_raw(const uint8_t *data, int len)
{
#if UART_LINK_LOG_RX_RAW
if (!data || len <= 0) {
return;
}
/* 仅打印长度和 ASCII 内容,省略 hex dump 便于阅读 JSON */
char ascii[129];
int n = len > (int)sizeof(ascii) - 1 ? (int)sizeof(ascii) - 1 : len;
for (int i = 0; i < n; i++) {
uint8_t c = data[i];
ascii[i] = (c >= 32 && c < 127) ? (char)c : '.';
}
ascii[n] = '\0';
if (len > n) {
ESP_LOGI(tag_s, "UART1 RX %d bytes: \"%s\"...", len, ascii);
} else {
ESP_LOGI(tag_s, "UART1 RX %d bytes: \"%s\"", len, ascii);
}
#else
(void)data;
(void)len;
#endif
}
/** 处理 JSON 行后的 OT 帧 / 裸 OTA 字节(同一 uart_read 内粘包) */
static void link_uart_feed_rx_binary_chunk(const uint8_t *data, int len)
{
if (!data || len <= 0) {
return;
}
bool frame_parser_active = ota_frame_rx_is_active();
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 (!ota_offer_is_armed() && !ota_binary_stream_is_active()) {
ESP_LOGW(tag_s, "UART1 OT 帧在未 armed 时到达,将回 NAK(须先发 message_type=0)");
}
if (uart_ota_framed_mode_s || frame_parser_active || frame_header_complete) {
uart_ota_framed_mode_s = true;
}
ESP_LOGD(tag_s, "UART1 OTA frame bytes (%d bytes)", len);
ota_frame_rx_feed(data, (uint16_t)len);
return;
}
if (is_ota_command(data[0])) {
if (!ota_offer_is_armed() && !ota_binary_stream_is_active()) {
ESP_LOGW(tag_s, "UART1 忽略 OTA 二进制:尚未收到手机 message_type=0");
return;
}
ESP_LOGI(tag_s, "UART1 legacy OTA packet (%d bytes)", len);
process_ota_packet(data, len);
continue;
return;
}
/* 未识别为 OT 帧的杂字节在 OTA 期间丢弃,禁止走裸字节写入 flash */
if (ota_binary_stream_is_active() || ota_offer_is_armed()) {
ESP_LOGW(tag_s, "UART1 丢弃 OTA 期间非 OT 帧数据 (%d bytes)", len);
}
}
static void uart_skip_json_delimiters_before_ota(const uint8_t **data, int *len)
{
while (*len > 0 && ((*data)[0] == '\r' || (*data)[0] == '\n')) {
(*data)++;
(*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)
{
if (!data || len <= 0) {
return;
}
uart_link_note_peer_rx();
log_uart1_rx_raw(data, len);
/*
* OTA 期间 UART1 只走 OT 二进制解析。
* 固件载荷里常有 0x7B('{'):若走 JSON 逐字节路径会误解析并破坏 OT 帧同步。
*/
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);
if (len <= 0) {
return;
}
}
link_uart_feed_rx_binary_chunk(data, len);
return;
}
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);
return;
}
/* JSON 文本模式 - 处理接收到的数据,按行分隔 */
for (int i = 0; i < len; i++) {
uint8_t ch = data[i];
/* 如果遇到 OTA 命令字节,切换模式 */
if (is_ota_command(ch) && i == 0 && rx_buf_pos_s == 0) {
/* 复制剩余数据到 OTA 缓冲区 */
uint16_t remaining = len - i;
if (remaining > sizeof(ota_rx_buf_s)) {
remaining = sizeof(ota_rx_buf_s);
}
memcpy(ota_rx_buf_s, data + i, remaining);
process_ota_packet(ota_rx_buf_s, remaining);
link_uart_feed_rx_binary_chunk(data + i, len - i);
break;
}
if (ch == UART_FRAME_DELIMITER || ch == '\r') {
/* 找到行尾,处理 JSON */
if (rx_buf_pos_s > 0) {
rx_json_buf_s[rx_buf_pos_s] = '\0';
process_received_json(rx_json_buf_s);
rx_buf_pos_s = 0;
uart_rx_try_finish_json_line();
/* 同一 read 内:{"...":0}\nOT... 须在 JSON 行后立刻吃二进制尾 */
int tail = i + 1;
if (tail < len) {
const uint8_t *tail_data = data + tail;
int tail_len = len - tail;
uart_skip_json_delimiters_before_ota(&tail_data, &tail_len);
link_uart_feed_rx_binary_chunk(tail_data, tail_len);
return;
}
} else {
/* 累积字符 */
if (rx_buf_pos_s < UART_JSON_MAX_BYTES) {
} else if (rx_buf_pos_s < UART_JSON_MAX_BYTES) {
rx_json_buf_s[rx_buf_pos_s++] = ch;
} else {
/* 缓冲区溢出,重置 */
ESP_LOGW(tag_s, "UART1 RX JSON buffer overflow");
rx_buf_pos_s = 0;
}
}
}
/* 环形缓冲区实例(静态分配,避免运行时 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);
}
/* 检查连接状态 */
uint32_t now = get_current_time_ms();
if (last_rx_time_ms > 0 && (now - last_rx_time_ms) > 5000) {
link_up_s = false;
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; /* 满,丢包 */
}
free(data);
vTaskDelete(NULL);
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_heartbeat_task(void *param)
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;
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
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) {
vTaskDelay(pdMS_TO_TICKS(UART_HEART_PERIOD_MS));
/* 等待数据信号量,或 10ms 超时轮询 */
if (uart_rx_data_sem_s) {
xSemaphoreTake(uart_rx_data_sem_s, pdMS_TO_TICKS(10));
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
/* 生成心跳 JSON(与 BLE 0xFFE3 相同结构) */
char *heartbeat = heart_payload_json_malloc(devid, NULL, false);
if (heartbeat) {
link_uart_send_heartbeat(heartbeat);
free(heartbeat);
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 esp_err_t uart_link_init_hardware(void)
static void uart_dma_rx_cb(const uint8_t *data, size_t len, void *ctx)
{
uart_config_t uart_cfg = {
.baud_rate = CONFIG_APP_UART_LINK_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
(void)ctx;
if (!data || len == 0) {
return;
}
if (len > 65535U) {
ESP_LOGW(tag_s, "UART1 RX 单次过长 (%u),截断", (unsigned)len);
len = 65535U;
}
esp_err_t err = uart_param_config(UART_LINK_PORT, &uart_cfg);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART param config failed: %d", err);
return err;
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)
{
(void)param;
ESP_LOGI(tag_s, "UART1 DMA RX 监视任务 (port=%d, %d baud, TX=GPIO%d RX=GPIO%d)",
(int)UART_LINK_PORT, CONFIG_APP_UART_LINK_BAUDRATE,
CONFIG_APP_UART_LINK_TX_GPIO, CONFIG_APP_UART_LINK_RX_GPIO);
bool was_ota_mode = false;
while (1) {
vTaskDelay(pdMS_TO_TICKS(200));
uint32_t now = get_current_time_ms();
uint32_t last_rx = last_peer_rx_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) {
link_up_s = false;
remote_control_stop_notify_end();
if (ota_session_reset || rx_buf_pos_s > 0) {
link_uart_session_reset_ex(ota_session_reset ? "timeout" : NULL);
ESP_LOGW(tag_s, "UART1 RX 超时(%lu ms),已清空未完成 OTA/JSON 会话",
(unsigned long)idle_limit_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)
{
if (!link_uart_has_peer_seen()) {
return;
}
if (uart_ota_tx_busy()) {
return;
}
char devid[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid));
char *heartbeat = heart_payload_json_malloc(devid, NULL, false);
if (!heartbeat) {
return;
}
(void)link_uart_send_heartbeat(heartbeat);
free(heartbeat);
}
static void uart_stop_notify_cb(const char *json)
{
if (json == NULL || !uart_peer_rx_recent()) {
return;
}
(void)uart_link_send_json_line(json);
}
static void uart_heartbeat_task(void *param)
{
(void)param;
ESP_LOGI(tag_s, "UART1 心跳任务已启动 (周期 %d ms)", UART_HEART_PERIOD_MS);
for (uint32_t beat_count = 1;; beat_count++) {
uart_send_heartbeat_once();
ESP_LOGD(tag_s, "UART1 心跳 #%lu", (unsigned long)beat_count);
vTaskDelay(pdMS_TO_TICKS(UART_HEART_PERIOD_MS));
}
}
/* 设置引脚:GPIO17 TX, GPIO18 RX */
static esp_err_t uart_link_init_hardware(void)
{
int tx_gpio = CONFIG_APP_UART_LINK_TX_GPIO;
int rx_gpio = CONFIG_APP_UART_LINK_RX_GPIO;
......@@ -328,21 +1049,17 @@ static esp_err_t uart_link_init_hardware(void)
return ESP_ERR_INVALID_ARG;
}
err = uart_set_pin(UART_LINK_PORT, tx_gpio, rx_gpio,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART set pin failed: %d", err);
return err;
}
rx_buf_pos_s = 0;
err = uart_driver_install(UART_LINK_PORT, UART_LINK_BUF_SIZE * 2,
UART_LINK_BUF_SIZE * 2, UART_LINK_QUEUE_SIZE,
&uart_queue_s, 0);
esp_err_t err = link_dma_uart_start(UART_LINK_PORT, CONFIG_APP_UART_LINK_BAUDRATE,
tx_gpio, rx_gpio, uart_dma_rx_cb, NULL);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "UART driver install failed: %d", err);
ESP_LOGE(tag_s, "UART DMA init failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(tag_s, "UART1 DMA 就绪: UART_NUM_%d TX=GPIO%d RX=GPIO%d %d baud",
(int)UART_LINK_PORT, tx_gpio, rx_gpio, CONFIG_APP_UART_LINK_BAUDRATE);
return ESP_OK;
}
......@@ -352,12 +1069,77 @@ esp_err_t link_uart_start(void)
CONFIG_APP_UART_LINK_TX_GPIO, CONFIG_APP_UART_LINK_RX_GPIO,
CONFIG_APP_UART_LINK_BAUDRATE);
ota_offer_reset_boot();
uart_ota_framed_mode_s = false;
ota_offer_set_done_callback(uart_ota_done_report);
remote_control_set_stop_notify_cb(uart_stop_notify_cb);
ota_frame_protocol_init(uart_ota_tx_frame, NULL);
/* 初始化 UART OTA 模块 */
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 硬件 */
esp_err_t err = uart_link_init_hardware();
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;
}
......@@ -365,21 +1147,45 @@ esp_err_t link_uart_start(void)
tx_mutex_s = xSemaphoreCreateMutex();
if (!tx_mutex_s) {
ESP_LOGE(tag_s, "TX mutex create failed");
uart_driver_delete(UART_LINK_PORT);
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;
}
/* 启动遥控看门狗(与 BLE 模式相同) */
remote_control_watchdog_start();
/* 启动接收任务 */
BaseType_t ret = xTaskCreate(uart_rx_task, "uart_rx",
4096, NULL, 5, &rx_task_handle_s);
/* DMA 接收由 link_dma_uart 驱动;独立任务仅做对端离线检测 */
BaseType_t ret = xTaskCreate(uart_link_watchdog_task, "uart_wd",
3072, NULL, 5, &rx_task_handle_s);
if (ret != pdPASS) {
ESP_LOGE(tag_s, "RX task create failed");
ESP_LOGE(tag_s, "UART watchdog task create failed");
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
uart_driver_delete(UART_LINK_PORT);
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;
}
......@@ -392,19 +1198,63 @@ esp_err_t link_uart_start(void)
rx_task_handle_s = NULL;
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
uart_driver_delete(UART_LINK_PORT);
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;
}
link_up_s = true;
last_rx_time_ms = get_current_time_ms();
link_up_s = false;
last_peer_rx_ms = 0;
ESP_LOGI(tag_s, "UART 链路启动成功");
ESP_LOGI(tag_s, "UART 链路启动成功 (事件驱动收发;无新日志=无收发)");
ESP_LOGI(tag_s, "握手: 设备不主动发 1002;须手机经 UART1 发 message_type=6 后才回 1002/query");
#if BUILD_IS_DEBUG
ESP_LOGI(tag_s, "Debug: UART1 收发 JSON/二进制均有 I 级日志 (监视器 COM=UART0,数据口=GPIO17/18)");
#endif
ESP_LOGI(tag_s, "串口工具应接 UART1: 手机_TX->ESP_GPIO%d(RX), 手机_RX<-ESP_GPIO%d(TX), %d baud",
CONFIG_APP_UART_LINK_RX_GPIO, CONFIG_APP_UART_LINK_TX_GPIO,
CONFIG_APP_UART_LINK_BAUDRATE);
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;
}
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) {
vTaskDelete(hb_task_handle_s);
......@@ -426,9 +1276,6 @@ void link_uart_stop(void)
tx_mutex_s = NULL;
}
/* 卸载 UART 驱动 */
uart_driver_delete(UART_LINK_PORT);
link_up_s = false;
ESP_LOGI(tag_s, "UART 链路已停止");
......
......@@ -37,15 +37,19 @@ void link_uart_stop(void);
int link_uart_send_heartbeat(const char *json);
/**
* @brief 发送日志/告警 JSON 数据
* @brief 发送日志/告警 JSON(message_type=4/5;Release 下 ESP_LOGW/E 亦经此上送)
* @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败
*/
int link_uart_send_log(const char *json);
/**
* @brief 检查 UART 链路是否已连接(有数据交互即认为在线)
* @return true 在线,false 离线
* @brief 是否曾收到对端 UART1 数据(发过 6 或控制帧后为 true)
*/
bool link_uart_has_peer_seen(void);
/**
* @brief 对端会话有效(曾 RX 且在 UART_LINK_ALIVE_MS 内,供心跳)
*/
bool link_uart_is_connected(void);
......
......@@ -51,6 +51,16 @@ static int mqttabnormal_exit_count = 0;
int mqtt_send_deviceota_info(void);
static bool mqtt_topic_is_server_cmd(const char *topic)
{
if (topic == NULL || g_device_id[0] == '\0') {
return false;
}
char expect[64];
snprintf(expect, sizeof(expect), "ser2dev/%s", g_device_id);
return strcmp(topic, expect) == 0;
}
/**
* @brief 构造心跳包内容
*/
......@@ -153,7 +163,32 @@ static void common_mqtt_event_handler(void *handler_args, esp_event_base_t base,
mqttabnormal_exit_count = 0;
memcpy(tmp, event->data, event->data_len);
tmp[event->data_len] = '\0';
char *topic = NULL;
if (event->topic && event->topic_len > 0) {
topic = malloc(event->topic_len + 1);
if (topic) {
memcpy(topic, event->topic, event->topic_len);
topic[event->topic_len] = '\0';
}
}
/*
* 安全策略:
* 1) OTA JSON(无 head)仅允许来自服务端 ser2dev/<device_id> 主题;
* 2) 其它消息统一按 remote_control 协议(必须包含 head)处理。
*/
bool ota_handled = false;
if (topic && mqtt_topic_is_server_cmd(topic)) {
ota_handled = (ota_update_recmqtt(tmp) == 0);
}
if (!ota_handled) {
remote_control_apply_json(tmp);
}
if (topic) {
free(topic);
}
free(tmp);
}
......
......@@ -3,7 +3,9 @@
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
*/
#include "sdkconfig.h"
#include "ota_binary_stream.h"
#include "ota_offer_protocol.h"
#include "ota_manager.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
......@@ -23,6 +25,7 @@ typedef struct {
const esp_partition_t *partition;
size_t expected_size;
size_t written_size;
size_t last_reported_size;
ota_transport_type_t transport;
ota_binary_stream_status_callback_t status_callback;
} ota_stream_session_t;
......@@ -45,11 +48,34 @@ static void ota_stream_send_status_fmt(const char *fmt, ...)
session_s.status_callback(buf);
}
/* 发送错误状态 */
/* 发送错误状态并结束 OTA 会话(仅用于致命错误,须已 armed) */
static void ota_stream_send_error(const char *step, const char *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_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 会话 */
......@@ -60,11 +86,17 @@ static void ota_stream_abort_internal(void)
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.handle = 0;
session_s.partition = NULL;
session_s.expected_size = 0;
session_s.written_size = 0;
session_s.last_reported_size = 0;
}
void ota_binary_stream_abort(void)
......@@ -77,6 +109,11 @@ bool ota_binary_stream_is_active(void)
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,
ota_binary_stream_status_callback_t status_callback,
const char *target_version)
......@@ -88,15 +125,9 @@ esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
session_s.transport = transport;
session_s.status_callback = status_callback;
(void)target_version;
/* 通过OTA管理器开始会话 */
ota_type_t type = (transport == OTA_TRANSPORT_BLE) ? OTA_TYPE_BLE : OTA_TYPE_UART;
esp_err_t err = ota_manager_begin_session(type, target_version ? target_version : "unknown");
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to begin OTA session in manager: %s", esp_err_to_name(err));
}
ESP_LOGI(tag_s, "OTA stream init: transport=%d", transport);
ESP_LOGI(tag_s, "OTA stream init: transport=%d (mgr session starts at BEGIN/0x01)", transport);
return ESP_OK;
}
......@@ -111,6 +142,11 @@ void ota_binary_stream_deinit(void)
/* 处理 OTA 开始命令 */
static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
{
if (session_s.active) {
ESP_LOGW(tag_s, "OTA begin while active, abort previous session");
ota_stream_abort_internal();
}
if (len < 5) {
/* 需要至少 1字节opcode + 4字节长度 */
ota_stream_send_error("begin", "size");
......@@ -152,8 +188,20 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
return -4;
}
ota_type_t type = (session_s.transport == OTA_TRANSPORT_BLE) ? OTA_TYPE_BLE : OTA_TYPE_UART;
esp_err_t mgr_err = ota_manager_begin_session(type, CONFIG_MY_APP_VERSION);
if (mgr_err != ESP_OK) {
ESP_LOGE(tag_s, "ota_manager_begin_session failed: %s", esp_err_to_name(mgr_err));
esp_ota_abort(session_s.handle);
session_s.handle = 0;
session_s.partition = NULL;
ota_stream_send_error("begin", "mgr_session");
return -5;
}
session_s.expected_size = image_size;
session_s.written_size = 0;
session_s.last_reported_size = 0;
session_s.active = true;
ota_stream_send_status_fmt("{\"ota\":\"begin\",\"ok\":1,\"expect\":%lu}",
......@@ -166,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)
{
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;
}
......@@ -205,13 +255,14 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
session_s.written_size += payload_len;
/* 每 8192 字节发送一次进度,或最后一次 */
static size_t last_reported = 0;
if (session_s.written_size - last_reported >= 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 ||
session_s.written_size >= session_s.expected_size) {
ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size);
last_reported = session_s.written_size;
session_s.last_reported_size = session_s.written_size;
}
}
return 0;
......@@ -221,7 +272,7 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
static int ota_stream_handle_end(void)
{
if (!session_s.active) {
ota_stream_send_error("end", "no_session");
ESP_LOGW(tag_s, "OTA END without active session");
return -1;
}
......@@ -255,8 +306,8 @@ static int ota_stream_handle_end(void)
return -4;
}
ESP_LOGI(tag_s, "OTA successful, written %u bytes",
(unsigned)session_s.written_size);
ESP_LOGI(tag_s, "OTA successful, written %u bytes, next boot from %s",
(unsigned)session_s.written_size, session_s.partition->label);
/* 通过OTA管理器结束会话 */
ota_manager_end_session();
......@@ -264,9 +315,11 @@ static int ota_stream_handle_end(void)
ota_stream_send_status_fmt("{\"ota\":\"end\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size);
/* 延时后重启 */
ESP_LOGI(tag_s, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
ota_offer_notify_done(true, (uint32_t)session_s.written_size, NULL);
/* notify_done 内已连发 OTA_DONE_REPORT_COUNT 次 1002/done,再等 DMA 发完后重启切分区 */
vTaskDelay(pdMS_TO_TICKS(OTA_DONE_REBOOT_DELAY_MS));
ESP_LOGI(tag_s, "Rebooting to %s...", session_s.partition->label);
esp_restart();
return 0;
......
/*
* OTA 二进制流接收模块
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
* 协议:首字节 opcode + 数据
* 0x01 + uint32_t LE(4字节) = 开始OTA,指定固件总长度
* 0x02 + 数据 = 固件数据块
* 0x03 = 结束OTA,触发校验和重启
* 内部载荷:0x01/0x02/0x03(由 ota_frame_protocol 解帧后调用)
* 对外传输:见 protocol/ota_frame_protocol.h(OT 包头 + CRC8 + 包尾 0x16)
*/
#ifndef OTA_BINARY_STREAM_H
......@@ -65,6 +63,9 @@ bool ota_binary_stream_is_active(void);
*/
void ota_binary_stream_abort(void);
/** 当前会话已写入字节数(未 begin 时为 0) */
uint32_t ota_binary_stream_get_written_bytes(void);
/**
* @brief 获取 OTA 状态 JSON 字符串
* 用于主动查询OTA状态
......
......@@ -9,6 +9,7 @@
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_app_desc.h"
#include "esp_partition.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
......@@ -22,6 +23,7 @@ static const char *tag_s = "OTA_MGR";
#define OTA_MGR_KEY_TYPE "type"
#define OTA_MGR_KEY_TARGET_VER "tgt_ver"
#define OTA_MGR_KEY_SOURCE_VER "src_ver"
#define OTA_MGR_KEY_SOURCE_PART "src_part"
#define OTA_MGR_KEY_TIMESTAMP "timestamp"
#define OTA_MGR_KEY_BOOT_COUNT "boot_cnt"
#define OTA_MGR_KEY_MAX_ATTEMPTS "max_retry"
......@@ -67,6 +69,11 @@ static esp_err_t load_session_from_nvs(void)
len = sizeof(current_session_s.source_version);
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_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);
......@@ -99,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_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_PART, current_session_s.source_partition);
nvs_set_u32(handle, OTA_MGR_KEY_TIMESTAMP, current_session_s.timestamp);
nvs_set_u8(handle, OTA_MGR_KEY_BOOT_COUNT, current_session_s.boot_count);
nvs_set_u8(handle, OTA_MGR_KEY_MAX_ATTEMPTS, current_session_s.max_boot_attempts);
......@@ -144,6 +152,24 @@ static bool is_running_ota_partition(void)
strcmp(running->label, "ota_1") == 0);
}
static bool partition_is_same(const esp_partition_t *a, const esp_partition_t *b)
{
if (a == NULL || b == NULL) {
return false;
}
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)
{
......@@ -176,46 +202,69 @@ esp_err_t ota_manager_init(void)
/* 有OTA会话,检查状态 */
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++;
save_session_to_nvs();
/* 检查版本是否匹配 */
if (app_desc && strcmp(app_desc->version, current_session_s.target_version) == 0) {
ESP_LOGI(tag_s, "Version matches: %s", app_desc->version);
bool switched = false;
if (running && current_session_s.source_partition[0] != '\0') {
switched = (strcmp(running->label, current_session_s.source_partition) != 0);
} else {
/* 旧会话无 source_partition:bootloader 能加载即视为成功 */
switched = (running != NULL);
}
if (switched) {
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;
save_session_to_nvs();
if (app_desc) {
ota_manager_update_stored_version(app_desc->version);
/* 延迟一段时间后清除会话(给应用层确认的时间) */
/* 应用层需要调用 ota_manager_mark_success() 来最终确认 */
}
} else {
ESP_LOGW(tag_s, "Version mismatch! Expected: %s, Got: %s",
current_session_s.target_version,
app_desc ? app_desc->version : "unknown");
/* 版本不匹配,可能是回退或其他问题,清除会话 */
ESP_LOGW(tag_s, "OTA verify failed: still on %s",
running ? running->label : "unknown");
clear_session_nvs();
}
} else if (current_session_s.state == OTA_STATE_IN_PROGRESS) {
/*
* 旧版固件在 UART/BLE 链路 init 时误调 begin_session("unknown") 会留下假会话;
* 无实际写入时 target=unknown 且 boot_cnt=0,直接清除,避免每次上电 WARN。
*/
if (current_session_s.boot_count == 0 &&
strcmp(current_session_s.target_version, "unknown") == 0) {
ESP_LOGI(tag_s, "Cleared stale OTA session (no transfer; re-flash or erase NVS if WARN persists)");
clear_session_nvs();
return ESP_OK;
}
/* OTA进行中但设备重启了,可能是断电或崩溃 */
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;
save_session_to_nvs();
/* 尝试回退 */
if (current_session_s.boot_count >= 1) {
ESP_LOGW(tag_s, "OTA interrupted, triggering rollback");
ota_manager_rollback();
return ESP_FAIL; /* 设备将重启 */
return ESP_FAIL;
}
} else if (current_session_s.state == OTA_STATE_SUCCESS) {
......@@ -234,15 +283,25 @@ esp_err_t ota_manager_init(void)
}
} else if (current_session_s.state == OTA_STATE_FAILED) {
/* 上次OTA已标记为失败 */
ESP_LOGW(tag_s, "Previous OTA marked as failed");
/* 检查是否应该回退 */
if (is_running_ota_partition()) {
ESP_LOGW(tag_s, "Triggering rollback to previous version");
if (still_on_source_partition()) {
ESP_LOGI(tag_s, "Still on pre-OTA partition %s, clear failed session",
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();
return ESP_FAIL; /* 设备将重启 */
return ESP_FAIL;
}
ESP_LOGI(tag_s, "No rollback needed, clear failed session");
clear_session_nvs();
}
return ESP_OK;
......@@ -255,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_partition_t *running = esp_ota_get_running_partition();
memset(&current_session_s, 0, sizeof(current_session_s));
current_session_s.state = OTA_STATE_IN_PROGRESS;
......@@ -263,14 +323,19 @@ esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version)
if (app_desc) {
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.boot_count = 0;
current_session_s.max_boot_attempts = OTA_MAX_BOOT_ATTEMPTS;
session_loaded_s = true;
ESP_LOGI(tag_s, "OTA session started: type=%d, %s -> %s",
type, current_session_s.source_version, target_version);
ESP_LOGI(tag_s, "OTA session started: type=%d, %s@%s -> %s",
type, current_session_s.source_version,
current_session_s.source_partition, target_version);
return save_session_to_nvs();
}
......@@ -351,20 +416,54 @@ esp_err_t ota_manager_rollback(void)
{
ESP_LOGW(tag_s, "Performing rollback...");
/* 获取另一个OTA分区(上一版本) */
const esp_partition_t *prev_partition = esp_ota_get_next_update_partition(NULL);
/*
* 优先使用 ESP-IDF 原生回退流程,避免手工选错分区导致不可启动。
* 该 API 会在可回退场景下自动标记当前 app 无效并回退到上一个可用镜像。
*/
esp_err_t rb_err = esp_ota_mark_app_invalid_rollback_and_reboot();
if (rb_err == ESP_OK) {
return ESP_OK; /* 不会执行到这里 */
}
ESP_LOGW(tag_s, "native rollback unavailable: %s, fallback to manual partition selection",
esp_err_to_name(rb_err));
const esp_partition_t *running = esp_ota_get_running_partition();
if (running == NULL) {
ESP_LOGE(tag_s, "Cannot determine running partition");
return ESP_ERR_INVALID_STATE;
}
/*
* 仅作为兜底:若 boot 分区已不是 running,优先回到该分区;
* 否则在 ota_0/ota_1 之间切换,且明确不能回到当前 running 分区。
*/
const esp_partition_t *prev_partition = NULL;
const esp_partition_t *boot = esp_ota_get_boot_partition();
if (boot && !partition_is_same(boot, running) && boot->type == ESP_PARTITION_TYPE_APP) {
prev_partition = boot;
} else if (strcmp(running->label, "ota_0") == 0) {
prev_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
} else if (strcmp(running->label, "ota_1") == 0) {
prev_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
}
if (prev_partition == NULL) {
ESP_LOGE(tag_s, "No previous partition found, cannot rollback");
clear_session_nvs();
return ESP_ERR_NOT_FOUND;
}
if (partition_is_same(prev_partition, running)) {
ESP_LOGE(tag_s, "Rollback target equals running partition (%s)", running->label);
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(tag_s, "Rollback to partition: %s", prev_partition->label);
/* 设置启动分区为上一版本 */
esp_err_t err = esp_ota_set_boot_partition(prev_partition);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to set boot partition: %s", esp_err_to_name(err));
ESP_LOGE(tag_s, "Failed to set boot partition: %s, clear session", esp_err_to_name(err));
clear_session_nvs();
return err;
}
......
......@@ -38,6 +38,7 @@ typedef struct {
ota_type_t type; /* OTA类型 */
char target_version[32]; /* 目标版本号 */
char source_version[32]; /* 升级前版本号 */
char source_partition[16]; /* 升级前运行分区标签,如 ota_0 */
uint32_t timestamp; /* OTA开始时间戳 */
uint8_t boot_count; /* 新固件启动计数 */
uint8_t max_boot_attempts; /* 最大尝试次数 */
......
#include "heart_payload.h"
#include "device_nvs.h"
#include "betteryread.h"
#include "cJSON.h"
#include "sdkconfig.h"
......@@ -42,14 +43,12 @@ char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool
cJSON_AddStringToObject(body, "ip", ip);
}
char id_full[64];
const char *dev = (device_id != NULL) ? device_id : "";
snprintf(id_full, sizeof(id_full), "app2dev/%s", dev);
const char *dev = device_id_plain(device_id);
char voltage_s[16];
snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v());
cJSON_AddStringToObject(body, "voltage", voltage_s);
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "device_ID", dev);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
return heart_payload_build(HEART_MSG_TYPE_HEARTBEAT, body);
......
#include "ota_frame_protocol.h"
#include "ota_binary_stream.h"
#include "ota_offer_protocol.h"
#include "esp_log.h"
#include <string.h>
static const char *tag_s = "OTA_FRAME";
static ota_frame_tx_fn s_tx_fn;
static void *s_tx_ctx;
typedef enum {
PS_SYNC0,
PS_SYNC1,
PS_VER,
PS_OP,
PS_SEQ0,
PS_SEQ1,
PS_LEN0,
PS_LEN1,
PS_PAYLOAD,
PS_CRC,
PS_TAIL,
} parser_state_t;
static struct {
parser_state_t state;
uint8_t ver;
uint8_t op;
uint16_t seq;
uint16_t payload_len;
uint16_t payload_pos;
uint8_t payload[OTA_FRAME_MAX_PAYLOAD];
uint8_t crc_rx;
} s_parser;
static uint8_t ota_frame_crc8(const uint8_t *data, uint16_t len)
{
uint8_t c = 0;
for (uint16_t i = 0; i < len; i++) {
c ^= data[i];
}
return c;
}
void ota_frame_protocol_init(ota_frame_tx_fn tx, void *ctx)
{
s_tx_fn = tx;
s_tx_ctx = ctx;
ota_frame_protocol_reset();
}
void ota_frame_protocol_reset(void)
{
memset(&s_parser, 0, sizeof(s_parser));
s_parser.state = PS_SYNC0;
}
bool ota_frame_rx_looks_framed(const uint8_t *data, uint16_t len)
{
return (data != NULL && len >= 1 && data[0] == OTA_FRAME_MAGIC0 &&
(len == 1 || data[1] == OTA_FRAME_MAGIC1));
}
bool ota_frame_rx_is_active(void)
{
return s_parser.state != PS_SYNC0;
}
int ota_frame_build(uint8_t op, uint16_t seq, const uint8_t *payload, uint16_t payload_len,
uint8_t *out, uint16_t out_cap, uint16_t *out_len)
{
if (out == NULL || out_len == NULL || payload_len > OTA_FRAME_MAX_PAYLOAD) {
return -1;
}
uint16_t need = (uint16_t)(8 + payload_len + 2);
if (out_cap < need) {
return -2;
}
out[0] = OTA_FRAME_MAGIC0;
out[1] = OTA_FRAME_MAGIC1;
out[2] = OTA_FRAME_VERSION;
out[3] = op;
out[4] = (uint8_t)(seq & 0xFF);
out[5] = (uint8_t)((seq >> 8) & 0xFF);
out[6] = (uint8_t)(payload_len & 0xFF);
out[7] = (uint8_t)((payload_len >> 8) & 0xFF);
if (payload_len > 0 && payload != NULL) {
memcpy(out + 8, payload, payload_len);
}
out[8 + payload_len] = ota_frame_crc8(out + 2, (uint16_t)(6 + payload_len));
out[8 + payload_len + 1] = OTA_FRAME_TAIL_BYTE;
*out_len = need;
return 0;
}
static void ota_frame_tx_bytes(const uint8_t *frame, uint16_t len)
{
if (s_tx_fn != NULL && frame != NULL && len > 0) {
s_tx_fn(frame, len, s_tx_ctx);
}
}
static void ota_frame_send_simple(uint8_t op, uint16_t seq, const uint8_t *payload, uint16_t plen)
{
uint8_t buf[OTA_FRAME_BUF_SIZE];
uint16_t 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);
}
}
static void ota_frame_send_ack(uint16_t seq, uint8_t data_byte, uint8_t status)
{
uint8_t pl[2] = { data_byte, status };
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)
{
ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, &status, 1);
}
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);
}
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)
{
if (rc == 0) {
return OTA_ACK_OK;
}
if (rc == -2) {
return OTA_ACK_ERR_OVERFLOW;
}
if (rc == -1) {
return OTA_ACK_ERR_NO_SESSION;
}
return OTA_ACK_ERR_FLASH;
}
static void handle_complete_frame(void)
{
if (!ota_offer_is_armed() && !ota_binary_stream_is_active()) {
ESP_LOGW(tag_s, "忽略 OTA 帧:未 armed");
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_NO_SESSION);
return;
}
const uint8_t *p = s_parser.payload;
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) {
case OTA_FRAME_OP_BEGIN: {
if (plen != 4) {
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_LEN);
return;
}
uint8_t pkt[5] = { OTA_OP_BEGIN, p[0], p[1], p[2], p[3] };
int rc = ota_binary_stream_process_packet(pkt, 5);
ota_frame_send_status_ack(s_parser.seq, map_stream_rc_to_ack(rc));
break;
}
case OTA_FRAME_OP_DATA: {
if (plen < 1 || plen > OTA_FRAME_MAX_PAYLOAD) {
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_LEN);
return;
}
uint8_t pkt[1 + OTA_FRAME_MAX_PAYLOAD];
pkt[0] = OTA_OP_DATA;
memcpy(pkt + 1, p, plen);
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;
}
case OTA_FRAME_OP_END: {
if (plen != 0) {
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_LEN);
return;
}
uint8_t pkt[1] = { OTA_OP_END };
int rc = ota_binary_stream_process_packet(pkt, 1);
ota_frame_send_status_ack(s_parser.seq, map_stream_rc_to_ack(rc));
break;
}
default:
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_OP);
break;
}
}
static void parser_push_byte(uint8_t b)
{
switch (s_parser.state) {
case PS_SYNC0:
if (b == OTA_FRAME_MAGIC0) {
s_parser.state = PS_SYNC1;
}
break;
case PS_SYNC1:
if (b == OTA_FRAME_MAGIC1) {
s_parser.state = PS_VER;
} else {
s_parser.state = PS_SYNC0;
if (b == OTA_FRAME_MAGIC0) {
s_parser.state = PS_SYNC1;
}
}
break;
case PS_VER:
s_parser.ver = b;
s_parser.state = PS_OP;
break;
case PS_OP:
s_parser.op = b;
s_parser.state = PS_SEQ0;
break;
case PS_SEQ0:
s_parser.seq = b;
s_parser.state = PS_SEQ1;
break;
case PS_SEQ1:
s_parser.seq |= (uint16_t)b << 8;
s_parser.state = PS_LEN0;
break;
case PS_LEN0:
s_parser.payload_len = b;
s_parser.state = PS_LEN1;
break;
case PS_LEN1:
s_parser.payload_len |= (uint16_t)b << 8;
s_parser.payload_pos = 0;
if (s_parser.payload_len > OTA_FRAME_MAX_PAYLOAD) {
s_parser.state = PS_SYNC0;
ota_frame_send_nak(0, OTA_ACK_ERR_BAD_LEN);
return;
}
if (s_parser.payload_len == 0) {
s_parser.state = PS_CRC;
} else {
s_parser.state = PS_PAYLOAD;
}
break;
case PS_PAYLOAD:
s_parser.payload[s_parser.payload_pos++] = b;
if (s_parser.payload_pos >= s_parser.payload_len) {
s_parser.state = PS_CRC;
}
break;
case PS_CRC:
s_parser.crc_rx = b;
s_parser.state = PS_TAIL;
break;
case PS_TAIL:
if (b != OTA_FRAME_TAIL_BYTE) {
s_parser.state = PS_SYNC0;
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_CRC);
return;
}
if (s_parser.ver != OTA_FRAME_VERSION) {
s_parser.state = PS_SYNC0;
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_OP);
return;
}
{
uint8_t hdr[6 + OTA_FRAME_MAX_PAYLOAD];
hdr[0] = s_parser.ver;
hdr[1] = s_parser.op;
hdr[2] = (uint8_t)(s_parser.seq & 0xFF);
hdr[3] = (uint8_t)((s_parser.seq >> 8) & 0xFF);
hdr[4] = (uint8_t)(s_parser.payload_len & 0xFF);
hdr[5] = (uint8_t)((s_parser.payload_len >> 8) & 0xFF);
if (s_parser.payload_len > 0) {
memcpy(hdr + 6, s_parser.payload, s_parser.payload_len);
}
uint8_t crc_calc = ota_frame_crc8(hdr, (uint16_t)(6 + s_parser.payload_len));
if (crc_calc != s_parser.crc_rx) {
s_parser.state = PS_SYNC0;
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_CRC);
return;
}
handle_complete_frame();
}
s_parser.state = PS_SYNC0;
break;
default:
s_parser.state = PS_SYNC0;
break;
}
}
void ota_frame_rx_feed(const uint8_t *data, uint16_t len)
{
if (data == NULL || len == 0) {
return;
}
for (uint16_t i = 0; i < len; i++) {
parser_push_byte(data[i]);
}
}
/**
* OTA 二进制帧协议(BLE 0xFFE2 / UART)
*
* 帧格式:[HEAD0][HEAD1][VER][OP][SEQ_L][SEQ_H][LEN_L][LEN_H][PAYLOAD...][CRC8][TAIL]
* - HEAD: 0x4F 0x54 ('O','T')
* - VER: 0x02
* - CRC8: 从 VER 到 PAYLOAD 末字节逐字节 XOR
* - TAIL: 0x16
*
* OP_DATA 载荷 1~MAX 字节固件数据(UART 最大见 menuconfig);设备回 OP_ACK:
* - 1 字节载荷:ACK [echo_byte][status]
* - 多块载荷:ACK [len_lo][len_hi][last_byte][status]
* status=0 成功,非 0 须重传同一 SEQ 帧。
*/
#ifndef OTA_FRAME_PROTOCOL_H
#define OTA_FRAME_PROTOCOL_H
#include "sdkconfig.h"
#include <stdint.h>
#include <stdbool.h>
#define OTA_FRAME_MAGIC0 0x4F
#define OTA_FRAME_MAGIC1 0x54
#define OTA_FRAME_VERSION 0x02
#define OTA_FRAME_TAIL_BYTE 0x16
#define OTA_FRAME_OP_BEGIN 0x01
#define OTA_FRAME_OP_DATA 0x02
#define OTA_FRAME_OP_END 0x03
#define OTA_FRAME_OP_ACK 0x80
#define OTA_FRAME_OP_NAK 0x81
#define OTA_ACK_OK 0x00
#define OTA_ACK_ERR_FLASH 0x01
#define OTA_ACK_ERR_OVERFLOW 0x02
#define OTA_ACK_ERR_NO_SESSION 0x03
#define OTA_ACK_ERR_CRC 0x04
#define OTA_ACK_ERR_BAD_LEN 0x05
#define OTA_ACK_ERR_BAD_OP 0x06
#if CONFIG_APP_LINK_BLE
#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)
typedef void (*ota_frame_tx_fn)(const uint8_t *frame, uint16_t len, void *ctx);
void ota_frame_protocol_init(ota_frame_tx_fn tx, void *ctx);
void ota_frame_protocol_reset(void);
/** 喂入 RX 字节流(可含多帧);内部组帧并应答 */
void ota_frame_rx_feed(const uint8_t *data, uint16_t len);
/** 帧解析器是否正在等待当前帧的后续字节 */
bool ota_frame_rx_is_active(void);
/** 是否像帧协议('O')或旧版裸 opcode */
bool ota_frame_rx_looks_framed(const uint8_t *data, uint16_t len);
int ota_frame_build(uint8_t op, uint16_t seq, const uint8_t *payload, uint16_t payload_len,
uint8_t *out, uint16_t out_cap, uint16_t *out_len);
#endif
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
static volatile bool s_query_sent_boot_s;
static volatile bool s_ota_armed_s;
static ota_offer_done_cb_t s_done_cb_s;
void ota_offer_reset_boot(void)
{
s_query_sent_boot_s = false;
s_ota_armed_s = false;
}
bool ota_offer_is_armed(void)
{
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)
{
s_ota_armed_s = false;
}
static void ota_offer_format_device_id(char *out, size_t cap, const char *device_id)
{
snprintf(out, cap, "%s", device_id_plain(device_id));
}
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)
{
if (!body) {
return NULL;
}
cJSON *root = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
if (!root || !head) {
cJSON_Delete(root);
cJSON_Delete(head);
cJSON_Delete(body);
return NULL;
}
cJSON_AddNumberToObject(head, "message_type", message_type);
cJSON_AddItemToObject(root, "head", head);
cJSON_AddItemToObject(root, "body", body);
char *out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
char *ota_offer_query_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", "query");
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);
}
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);
}
char *ota_offer_result_json_malloc(const char *device_id,
bool success,
uint32_t bytes_written,
const char *err_step)
{
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", "done");
cJSON_AddNumberToObject(body, "ok", success ? 1 : 0);
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
cJSON_AddNumberToObject(body, "written", (double)bytes_written);
if (!success && err_step != NULL && err_step[0] != '\0') {
cJSON_AddStringToObject(body, "err", err_step);
} else {
cJSON_AddStringToObject(body, "err", "");
}
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
}
bool ota_offer_parse_phone_response(cJSON *root)
{
if (!root) {
return false;
}
cJSON *head = cJSON_GetObjectItem(root, "head");
if (!head || !cJSON_IsObject(head)) {
return false;
}
cJSON *m_type = cJSON_GetObjectItem(head, "message_type");
if (!m_type || !cJSON_IsNumber(m_type)) {
return false;
}
if ((int)m_type->valuedouble == OTA_MSG_PHONE_OTA_START) {
s_ota_armed_s = true;
return true;
}
return false;
}
bool ota_offer_mark_query_sent(void)
{
if (s_query_sent_boot_s) {
return false;
}
s_query_sent_boot_s = true;
return true;
}
void ota_offer_reset_query_sent(void)
{
s_query_sent_boot_s = false;
}
void ota_offer_set_done_callback(ota_offer_done_cb_t cb)
{
s_done_cb_s = cb;
}
void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err_step)
{
s_ota_armed_s = false;
if (s_done_cb_s) {
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);
}
#ifndef OTA_OFFER_PROTOCOL_H
#define OTA_OFFER_PROTOCOL_H
#include <stdbool.h>
#include <stdint.h>
#include "cJSON.h"
/** 设备 → 手机:询问是否 OTA(手机 message_type=6 后回复)及 OTA 结束汇报 */
#define OTA_MSG_DEVICE_OFFER 1002
/** 手机 → 设备:同意开始二进制 OTA(随后发 0x01/0x02/0x03) */
#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);
/** 上电/链路启动时调用,允许再次发送询问 JSON */
void ota_offer_reset_boot(void);
/** @deprecated 已由 message_type=6 触发;保留供兼容 */
bool ota_offer_mark_query_sent(void);
/** @deprecated */
void ota_offer_reset_query_sent(void);
/** OTA 二进制结束/失败时由 ota_binary_stream 调用 */
void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err_step);
/** 是否已允许接收 OTA 二进制(手机已回复 message_type=0) */
bool ota_offer_is_armed(void);
/** 取消 OTA 武装(链路清空/卡顿恢复时调用,不重置开机询问标记) */
void ota_offer_disarm(void);
/**
* 解析手机 JSON;head.message_type==0 时置位 armed 并返回 true
*/
bool ota_offer_parse_phone_response(cJSON *root);
/** 构造询问 JSON:head.message_type=1002, body.phase=query */
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 */
char *ota_offer_result_json_malloc(const char *device_id,
bool success,
uint32_t bytes_written,
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);
#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
#include "sdkconfig.h"
#include "remote_control.h"
#if CONFIG_APP_LINK_UART
#include "link_uart.h"
#endif
#include "rc_pwm_control.h"
#include "task_manager.h"
#if CONFIG_APP_LINK_WIFI
#include "ota.h"
#endif
#include "ota_binary_stream.h"
#include "ota_offer_protocol.h"
#include "cJSON.h"
#include "esp_log.h"
#include "esp_system.h"
......@@ -13,19 +15,224 @@
static const char *tag_s = "REMOTE_CTRL";
/* 超时保护:500ms 未收到命令则自动停止 */
#define RC_CMD_TIMEOUT_MS 500
#define RC_WATCHDOG_PERIOD_MS 100 /* 检查周期 100ms */
#define RC_WATCHDOG_PERIOD_MS 100
static volatile uint32_t s_last_cmd_time_ms = 0;
static volatile bool s_watchdog_running = false;
static volatile bool s_timeout_stopped = false;
static TaskHandle_t s_watchdog_handle = NULL;
static TaskHandle_t s_stop_notify_handle = NULL;
static volatile bool s_stop_notify_active = false;
static remote_control_stop_notify_cb_t s_stop_notify_cb = NULL;
static uint32_t get_current_time_ms(void)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
char *remote_control_stop_json_malloc(void)
{
cJSON *body = cJSON_CreateObject();
cJSON *pwm = cJSON_CreateObject();
if (!body || !pwm) {
cJSON_Delete(body);
cJSON_Delete(pwm);
return NULL;
}
cJSON_AddNumberToObject(pwm, "speed_mode", 0);
cJSON_AddNumberToObject(pwm, "speed_val", 0);
cJSON_AddNumberToObject(pwm, "steer_mode", 0);
cJSON_AddNumberToObject(pwm, "steer_val", 0);
cJSON_AddItemToObject(body, "pwm_ctrl", pwm);
cJSON *root = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
if (!root || !head) {
cJSON_Delete(root);
cJSON_Delete(head);
cJSON_Delete(body);
return NULL;
}
cJSON_AddNumberToObject(head, "message_type", 3);
cJSON_AddItemToObject(root, "head", head);
cJSON_AddItemToObject(root, "body", body);
char *out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
void remote_control_set_stop_notify_cb(remote_control_stop_notify_cb_t cb)
{
s_stop_notify_cb = cb;
}
static void remote_control_stop_notify_push_once(void)
{
if (s_stop_notify_cb == NULL) {
return;
}
char *json = remote_control_stop_json_malloc();
if (json == NULL) {
return;
}
s_stop_notify_cb(json);
free(json);
}
static void remote_control_stop_notify_task(void *param)
{
(void)param;
for (int i = 0; i < RC_STOP_NOTIFY_BURST_COUNT; i++) {
if (!s_stop_notify_active || !s_timeout_stopped) {
break;
}
remote_control_stop_notify_push_once();
if (i + 1 < RC_STOP_NOTIFY_BURST_COUNT) {
vTaskDelay(pdMS_TO_TICKS(RC_STOP_NOTIFY_BURST_INTERVAL_MS));
}
}
s_stop_notify_active = false;
s_stop_notify_handle = NULL;
vTaskDelete(NULL);
}
static void remote_control_stop_notify_begin(void)
{
if (s_stop_notify_handle != NULL) {
return;
}
#if CONFIG_APP_LINK_UART
if (!link_uart_has_peer_seen()) {
return;
}
#endif
s_stop_notify_active = true;
/* 禁止在 rc_wdog 小栈任务里同步 cJSON+BLE Notify;由 rc_stop_ntf 任务发送 */
if (xTaskCreate(remote_control_stop_notify_task, "rc_stop_ntf", 4096, NULL, 5,
&s_stop_notify_handle) != pdPASS) {
s_stop_notify_active = false;
s_stop_notify_handle = NULL;
ESP_LOGW(tag_s, "停车 Notify 任务创建失败");
}
}
void remote_control_stop_notify_end(void)
{
s_stop_notify_active = false;
}
static bool remote_control_is_stop_pwm_json(cJSON *pwm_ctrl)
{
if (!pwm_ctrl || !cJSON_IsObject(pwm_ctrl)) {
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 *direction = cJSON_GetObjectItem(pwm_ctrl, "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;
}
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)
{
(void)param;
......@@ -37,12 +244,15 @@ static void rc_watchdog_task(void *param)
uint32_t now = get_current_time_ms();
uint32_t last = s_last_cmd_time_ms;
/* 如果已经初始化过且超过 500ms 未收到命令 */
/* 已初始化且超过 500ms 未收到命令:仅首次进入超时态时停车 */
if (last != 0 && (now - last) > RC_CMD_TIMEOUT_MS) {
if (!s_timeout_stopped) {
rc_vehicle_stop();
ESP_LOGW(tag_s, "RC timeout: no cmd for %d ms, vehicle stopped", (int)(now - last));
/* 重置时间戳,避免重复停止 */
s_last_cmd_time_ms = now;
s_timeout_stopped = true;
remote_control_stop_notify_begin();
ESP_LOGI(tag_s, "RC timeout: no cmd for %d ms, vehicle stopped",
(int)(now - last));
}
}
}
......@@ -56,7 +266,9 @@ esp_err_t remote_control_watchdog_start(void)
return ESP_OK; /* 已经在运行 */
}
s_last_cmd_time_ms = get_current_time_ms(); /* 初始化时间戳 */
/* 0 = 尚未收到控制 JSON,避免上电数百毫秒误触发超时停车 */
s_last_cmd_time_ms = 0;
s_timeout_stopped = false;
return app_task_start(APP_TASK_RC_WATCHDOG, rc_watchdog_task, NULL, &s_watchdog_handle);
}
......@@ -64,76 +276,148 @@ esp_err_t remote_control_watchdog_start(void)
void remote_control_watchdog_stop(void)
{
s_watchdog_running = false;
/* task 会自行删除,句柄会在任务函数中设为 NULL */
remote_control_stop_notify_end();
}
static void update_last_cmd_time(void)
{
if (s_timeout_stopped) {
remote_control_stop_notify_end();
}
s_timeout_stopped = false;
s_last_cmd_time_ms = get_current_time_ms();
}
void remote_control_apply_json(const char *json)
void remote_control_note_rx_activity(void)
{
/* 更新上次接收命令的时间戳 */
update_last_cmd_time();
/*
* UART/USB 可能把一条 JSON 拆成多段。已有控制会话时,收到后续片段也刷新
* 看门狗时间,避免“第二段刚到但尚未 parse 完整”时误停车。
*/
if (s_last_cmd_time_ms != 0 && !s_timeout_stopped) {
s_last_cmd_time_ms = get_current_time_ms();
}
}
if (!json) {
return;
void remote_control_link_reset(void)
{
remote_control_stop_notify_end();
if (ota_binary_stream_is_active()) {
ota_binary_stream_abort();
}
ota_offer_disarm();
s_last_cmd_time_ms = 0;
s_timeout_stopped = true;
rc_vehicle_stop();
ESP_LOGI(tag_s, "链路会话已清空(OTA 中止、停车)");
}
cJSON *root = cJSON_Parse(json);
void remote_control_apply_json_root(cJSON *root, const char *json_raw)
{
if (!root) {
return;
}
cJSON *head = cJSON_GetObjectItem(root, "head");
if (!head || !cJSON_IsObject(head)) {
#if CONFIG_APP_LINK_WIFI
/* 仅 WiFi+MQTT 构建:云端 JSON 触发 HTTPS OTA */
ota_update_recmqtt(json);
#endif
cJSON_Delete(root);
/* 安全收敛:无 head 的 JSON 不再进入 OTA 分支,避免误触发升级。 */
(void)json_raw;
return;
}
cJSON *m_type = cJSON_GetObjectItem(head, "message_type");
if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == REMOTE_CTRL_MSG_LINK_RESET) {
remote_control_link_reset();
return;
}
update_last_cmd_time();
if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == 1) {
ESP_LOGW(tag_s, "message_type=1: reboot (all devices)");
cJSON_Delete(root);
esp_restart();
return;
}
cJSON *body = cJSON_GetObjectItem(root, "body");
if (m_type && cJSON_IsNumber(m_type) && body) {
if (m_type->valueint == 4) {
if (m_type->valueint == 3 || m_type->valueint == 4) {
cJSON *pin_ctrl = cJSON_GetObjectItem(body, "pin_setctrl");
if (pin_ctrl && cJSON_IsObject(pin_ctrl)) {
cJSON *jp = cJSON_GetObjectItem(pin_ctrl, "pin");
cJSON *jv = cJSON_GetObjectItem(pin_ctrl, "val");
if (jp && jv && cJSON_IsNumber(jp) && cJSON_IsNumber(jv)) {
int pin = jp->valueint;
int val = jv->valueint;
ESP_LOGI(tag_s, "GPIO: pin %d -> %d", pin, val);
rc_vehicle_shot(pin, val);
}
}
} else if (m_type->valueint == 3) {
cJSON *pwm_ctrl = cJSON_GetObjectItem(body, "pwm_ctrl");
bool has_any_ctrl = false;
if (m_type->valueint == 4) {
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)) {
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 *jt = cJSON_GetObjectItem(pwm_ctrl, "type");
cJSON *jv = cJSON_GetObjectItem(pwm_ctrl, "val");
cJSON *js = cJSON_GetObjectItem(pwm_ctrl, "speed");
cJSON *jd = cJSON_GetObjectItem(pwm_ctrl, "direction");
if (jm && jv && cJSON_IsNumber(jm) && cJSON_IsNumber(jv)) {
int mode = jm->valueint;
int type = jt && cJSON_IsNumber(jt) ? jt->valueint : 0;
int val = jv->valueint;
ESP_LOGI(tag_s, "PWM: mode %d type %d val %d", mode, type, val);
rc_vehicle_control(mode, val);
int speed = js && cJSON_IsNumber(js) ? js->valueint : val;
int steer = jd && cJSON_IsNumber(jd) ? jd->valueint : ((mode >= 5) ? val : 0);
if (mode == 0 && val == 0) {
rc_vehicle_stop();
ESP_LOGD(tag_s, "PWM mode=0 val=0 -> stop");
} else {
ESP_LOGI(tag_s, "PWM legacy: mode %d type %d speed %d steer %d",
mode, type, 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");
}
}
}
}
void remote_control_apply_json(const char *json)
{
if (!json) {
return;
}
cJSON *root = cJSON_Parse(json);
if (!root) {
ESP_LOGW(tag_s, "JSON parse failed (len=%u)", (unsigned)strlen(json));
return;
}
remote_control_apply_json_root(root, json);
cJSON_Delete(root);
}
......@@ -2,13 +2,46 @@
#define REMOTE_CONTROL_H
#include "esp_err.h"
#include "cJSON.h"
/** BLE FFE1 / 遥控 JSON:单帧 UTF-8 最大字节数(不含结尾 NUL) */
#define REMOTE_CTRL_JSON_MAX_BYTES 2048
/** 手机 → 设备:清空链路会话(中止 OTA、停车、丢弃武装态) */
#define REMOTE_CTRL_MSG_LINK_RESET 2
/** 手机 → 设备:串口/BLE 链路已连接(App 侧 open 成功);设备回 OTA 询问 JSON(1002) */
#define REMOTE_CTRL_MSG_LINK_READY 6
/** 遥控超时:超过该时间未收到控制 JSON 则停车(ms) */
#define RC_CMD_TIMEOUT_MS 500
/** 固件超时停车后,经链路推送行走全停 JSON:共 3 条,条间间隔(ms) */
#define RC_STOP_NOTIFY_BURST_COUNT 3
#define RC_STOP_NOTIFY_BURST_INTERVAL_MS 100
/** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */
void remote_control_apply_json(const char *json);
/** 中止 OTA 并复位遥控超时状态(BLE 断开/卡顿恢复) */
void remote_control_link_reset(void);
/** 链路正在接收一条控制 JSON 的片段,用于避免分片期间误触发超时停车 */
void remote_control_note_rx_activity(void);
/** 构造停车专用 JSON(message_type=3,speed_val/steer_val 均为 0) */
char *remote_control_stop_json_malloc(void);
/** 注册停车状态 Notify 回调(BLE 0xFFE3 / UART 行);json 由回调方发送后勿 free */
typedef void (*remote_control_stop_notify_cb_t)(const char *json);
void remote_control_set_stop_notify_cb(remote_control_stop_notify_cb_t cb);
/** 停止周期停车 JSON 推送任务(UART 对端离线等) */
void remote_control_stop_notify_end(void);
/** 已解析的 JSON 根节点,json_raw 用于无 head 时的 OTA 等需原文场景 */
void remote_control_apply_json_root(cJSON *root, const char *json_raw);
/**
* @brief 启动遥控超时守护任务
* 500ms 未收到控制命令将自动停止车辆
......
......@@ -7,6 +7,7 @@
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_http_server.h"
#include "driver/gpio.h"
#include "lwip/sockets.h"
......@@ -18,13 +19,119 @@
#include "mqttconf_commun.h"
#endif
#include "task_manager.h"
#if CONFIG_APP_LINK_BLE
#include "link_ble.h"
#endif
#include "device_model.h"
static const char *tag_s = "WIFI_CFG";
static httpd_handle_t config_httpd_s = NULL;
static volatile bool s_prov_restart_scheduled_s = false;
#if CONFIG_APP_LINK_WIFI
static bool wifi_editable_s = false;
#endif
static bool devid_editable_s = false;
static void provision_restart_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(400));
ESP_LOGI(tag_s, "配网已保存,关闭热点并重启");
#if !CONFIG_APP_LINK_WIFI
provision_wifi_shutdown();
#endif
esp_restart();
}
static void provision_schedule_restart(void)
{
if (s_prov_restart_scheduled_s) {
return;
}
s_prov_restart_scheduled_s = true;
if (xTaskCreate(provision_restart_task, "prov_restart", 3072, NULL, 10, NULL) != pdPASS) {
s_prov_restart_scheduled_s = false;
ESP_LOGE(tag_s, "创建重启任务失败,立即重启");
provision_wifi_shutdown();
esp_restart();
}
}
/**
* 强制门户:须返回非 204(如 302),Android/iOS 才会弹出「需登录网络」。
* 旧实现 generate_204 回 204,系统会认为已联网,故不会自动弹网页。
*/
static esp_err_t captive_portal_redirect(httpd_req_t *req)
{
(void)req;
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);
return ESP_OK;
}
static void register_captive_redirect_uri(httpd_handle_t server, const char *uri)
{
httpd_uri_t u = {
.uri = uri,
.method = HTTP_GET,
.handler = captive_portal_redirect,
};
(void)httpd_register_uri_handler(server, &u);
}
static const char *device_model_to_str(device_model_t model)
{
switch (model) {
case DEVICE_MODEL_1102:
return "1102";
default:
return "1101";
}
}
/** 完整设备号:如 CN110200000001;第 3~6 位须为 1101/1102 */
static bool device_full_id_is_valid(const char *val)
{
if (val == NULL || val[0] == '\0') {
return false;
}
if (strstr(val, "app2dev/") != NULL || strchr(val, '/') != NULL) {
return false;
}
size_t len = strlen(val);
if (len < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN) || len >= 32) {
return false;
}
for (size_t i = 0; i < len; i++) {
if (!isalnum((unsigned char)val[i])) {
return false;
}
}
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)));
}
esp_err_t init_spiffs(void)
{
esp_vfs_spiffs_conf_t conf = {
......@@ -41,25 +148,34 @@ esp_err_t init_spiffs(void)
return ESP_OK;
}
void url_decode(char *dst, const char *src)
static bool url_decode_safe(char *dst, size_t dst_cap, const char *src)
{
char a, b;
while (*src) {
size_t out = 0;
if (dst == NULL || src == NULL || dst_cap == 0) {
return false;
}
while (*src && (out + 1 < dst_cap)) {
if ((*src == '%') && ((a = src[1]) && (b = src[2])) && (isxdigit(a) && isxdigit(b))) {
a = toupper((unsigned char)a);
b = toupper((unsigned char)b);
a -= (a >= 'A') ? ('A' - 10) : '0';
b -= (b >= 'A') ? ('A' - 10) : '0';
*dst++ = (char)((a << 4) | b);
dst[out++] = (char)((a << 4) | b);
src += 3;
} else if (*src == '+') {
*dst++ = ' ';
dst[out++] = ' ';
src++;
} else {
*dst++ = *src++;
dst[out++] = *src++;
}
}
*dst = '\0';
dst[out] = '\0';
/* src 未消费完说明被截断,调用方应拒绝本次写入 */
return (*src == '\0');
}
void save_to_nvs(const char *key, const char *value)
......@@ -86,19 +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)
{
#if CONFIG_APP_LINK_BLE
char devid_status[128], ble_status[128];
char devid_current[32] = {0};
char devid_status[128];
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid_current, sizeof(devid_current));
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
char devid_status[128];
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index_uart.html", "r");
#else
char wifi_status[128], devid_status[128];
char wifi_status[128];
sprintf(wifi_status, wifi_editable_s ? "" : "disabled style='background:#eee'");
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index.html", "r");
#endif
if (!f) {
......@@ -117,14 +235,15 @@ static esp_err_t get_handler(httpd_req_t *req)
char *final_html = malloc(fsize + 1024);
#if CONFIG_APP_LINK_BLE
snprintf(final_html, fsize + 1024, template,
devid_editable_s ? "🔓" : "🔒", devid_status,
devid_editable_s ? "🔓" : "🔒", devid_current, devid_status,
devid_editable_s ? "🔓" : "🔒", ble_status);
#elif CONFIG_APP_LINK_UART
snprintf(final_html, fsize + 1024, template, devid_editable_s ? "🔓" : "🔒", devid_status);
snprintf(final_html, fsize + 1024, template,
devid_editable_s ? "🔓" : "🔒", devid_current, devid_status);
#else
snprintf(final_html, fsize + 1024, template,
wifi_editable_s ? "🔓" : "🔒", wifi_status, wifi_status,
devid_editable_s ? "🔓" : "🔒", devid_status);
devid_editable_s ? "🔓" : "🔒", devid_current, devid_status);
#endif
httpd_resp_set_type(req, "text/html");
......@@ -158,33 +277,35 @@ static esp_err_t post_handler(httpd_req_t *req)
*val = '\0';
val++;
char decoded_val[64];
url_decode(decoded_val, val);
#if CONFIG_APP_LINK_BLE
if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
} else if (strcmp(key, "ble_name") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, decoded_val);
if (!url_decode_safe(decoded_val, sizeof(decoded_val), val)) {
ESP_LOGW(tag_s, "表单字段过长或非法编码,已拒绝 key=%s", key);
token = strtok_r(NULL, "&", &saveptr);
continue;
}
#elif CONFIG_APP_LINK_UART
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, "ssid") == 0 && wifi_editable_s) {
#if CONFIG_APP_LINK_BLE
else if (strcmp(key, "ble_name") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, decoded_val);
}
#endif
#if CONFIG_APP_LINK_WIFI
else if (strcmp(key, "ssid") == 0 && wifi_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_WIFI_SSID, decoded_val);
} else if (strcmp(key, "pass") == 0 && wifi_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_WIFI_PASS, decoded_val);
} else if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
}
#endif
}
token = strtok_r(NULL, "&", &saveptr);
}
}
provision_schedule_restart();
httpd_resp_set_status(req, "200 OK");
httpd_resp_set_hdr(req, "Connection", "close");
httpd_resp_set_type(req, "text/html; charset=utf-8");
httpd_resp_send(req, "<h1>Success</h1><p>Restarting...</p>", HTTPD_RESP_USE_STRLEN);
vTaskDelay(pdMS_TO_TICKS(1500));
esp_restart();
return ESP_OK;
}
......@@ -228,8 +349,31 @@ esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
return ESP_OK;
}
void provision_wifi_shutdown(void)
{
#if !CONFIG_APP_LINK_WIFI
if (config_httpd_s != NULL) {
(void)httpd_stop(config_httpd_s);
config_httpd_s = NULL;
}
esp_err_t e = esp_wifi_stop();
if (e != ESP_OK && e != ESP_ERR_WIFI_NOT_INIT) {
ESP_LOGD(tag_s, "wifi_stop: %s", esp_err_to_name(e));
}
e = esp_wifi_deinit();
if (e != ESP_OK && e != ESP_ERR_WIFI_NOT_INIT) {
ESP_LOGD(tag_s, "wifi_deinit: %s", esp_err_to_name(e));
}
#endif
}
void start_config_web(void)
{
#if CONFIG_APP_LINK_BLE
link_ble_stop();
#endif
provision_wifi_shutdown();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_err_t e = esp_wifi_init(&cfg);
if (e != ESP_OK && e != ESP_ERR_INVALID_STATE) {
......@@ -247,13 +391,31 @@ void start_config_web(void)
(void)app_task_start(APP_TASK_DNS_SERVER, dns_server_task, NULL, NULL);
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
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) {
config_httpd_s = server;
httpd_uri_t get_uri = {.uri = "/", .method = HTTP_GET, .handler = get_handler};
httpd_register_uri_handler(server, &get_uri);
httpd_uri_t post_uri = {.uri = "/save", .method = HTTP_POST, .handler = post_handler};
httpd_register_uri_handler(server, &post_uri);
/* 各系统连网检测 URL(DNS 劫持到本机后命中)→ 302 触发自动弹窗 */
register_captive_redirect_uri(server, "/generate_204");
register_captive_redirect_uri(server, "/gen_204");
register_captive_redirect_uri(server, "/hotspot-detect.html");
register_captive_redirect_uri(server, "/library/test/success.html");
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);
ESP_LOGI(tag_s, "配网门户已启动: http://192.168.4.1 (连网检测→302 弹窗)");
}
s_prov_restart_scheduled_s = false;
}
void nowifidata_start_config_web(void)
......
......@@ -17,4 +17,10 @@ esp_err_t init_spiffs(void);
void nowifidata_start_config_web(void);
void button_monitor_task_init(void);
/**
* @brief 关闭配网 SoftAP 与 WiFi 驱动(BLE/UART 正常运行前调用)
* WiFi+MQTT 模式不调用;配网保存后 esp_restart 亦会清空状态。
*/
void provision_wifi_shutdown(void);
#endif
# Copy Android device handoff doc (Unicode-safe; no Chinese in .bat)
param(
[Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)][string]$Ver
)
$docs = Join-Path $Root 'docs'
$rel = Join-Path $Root 'firmware\release'
if (-not (Test-Path -LiteralPath $docs)) {
Write-Host " WARN: missing docs folder"
exit 1
}
# Match: Android*_vX.Y.Z.md (ASCII regex only; avoids .bat GBK / .ps1 encoding issues)
$all = Get-ChildItem -LiteralPath $docs -Filter '*.md' -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^Android.+_v[0-9]+(\.[0-9]+)*\.md$' }
if (-not $all) {
Write-Host " WARN: no Android device doc in docs (Android*_v*.md)"
exit 1
}
$exact = $all | Where-Object { $_.Name -like "*_v$Ver.md" } | Select-Object -First 1
if (-not $exact) {
$src = $all | Sort-Object Name -Descending | Select-Object -First 1
Write-Host " NOTE: no *_v$Ver.md, using $($src.Name)"
} else {
$src = $exact
}
Copy-Item -LiteralPath $src.FullName (Join-Path $rel $src.Name) -Force
Write-Host " docs -> release\$($src.Name)"
exit 0
......@@ -81,8 +81,10 @@ if "!VER!"=="unknown" if exist "%ROOT%\sdkconfig.defaults" call :ReadVersion "%R
set "UTC=unknown"
for /f "delims=" %%T in ('powershell -NoProfile -Command "(Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')" 2^>nul') do set "UTC=%%T"
set "LOCAL=%date% %time%"
call :HashFile "%OTA%\ESPRCCar.bin" OTAHASH
for %%A in ("%OTA%\ESPRCCar.bin") do set "OTASZ=%%~zA"
echo [3/4] Write manifest.json ...
echo [3/5] Write manifest.json ...
> "%REL%\manifest.json" echo {
>>"%REL%\manifest.json" echo "project": "ESPRCCar",
>>"%REL%\manifest.json" echo "chip": "esp32s3",
......@@ -97,24 +99,26 @@ echo [3/4] Write manifest.json ...
call :AppendJsonFile "%OTA%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\bootloader.bin"
call :AppendJsonFile "%FAC%\partition-table.bin"
call :AppendJsonFile "%FAC%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\ota_data_initial.bin"
call :AppendJsonFile "%FAC%\storage.bin" last
>>"%REL%\manifest.json" echo ]
>>"%REL%\manifest.json" echo }
> "%REL%\ota_manifest.json" echo {
>>"%REL%\ota_manifest.json" echo "project": "ESPRCCar",
>>"%REL%\ota_manifest.json" echo "chip": "esp32s3",
>>"%REL%\ota_manifest.json" echo "version": "!VER!",
>>"%REL%\ota_manifest.json" echo "generated_utc": "!UTC!",
>>"%REL%\ota_manifest.json" echo "image": "ESPRCCar.bin",
>>"%REL%\ota_manifest.json" echo "bytes": !OTASZ!,
>>"%REL%\ota_manifest.json" echo "sha256": "!OTAHASH!"
>>"%REL%\ota_manifest.json" echo }
echo [4/5] Copy Android device doc ...
set "DOC_SRC=%ROOT%\docs\Android端设备对接文档_v!VER!.md"
if exist "!DOC_SRC!" (
copy /Y "!DOC_SRC!" "%REL%\Android端设备对接文档_v!VER!.md" >nul
echo docs -^> release\Android端设备对接文档_v!VER!.md
) else (
echo WARN: missing !DOC_SRC! (create docs\Android端设备对接文档_v!VER!.md before handoff)
)
REM Chinese paths: use PowerShell (CMD GBK cannot open UTF-8 filenames)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0copy_firmware_doc.ps1" "%ROOT%" "!VER!"
echo [5/5] Write VERSION.txt ...
call :HashFile "%OTA%\ESPRCCar.bin" OTAHASH
for %%A in ("%OTA%\ESPRCCar.bin") do set "OTASZ=%%~zA"
> "%REL%\VERSION.txt" (
echo ESPRCCar firmware release
......@@ -134,16 +138,18 @@ echo Factory: firmware/release/factory/
echo tool: Espressif Flash Download Tools
echo addrs: see factory/flash_args.txt
echo.
echo machine_readable: manifest.json
echo machine_readable: ota_manifest.json
echo full_release_manifest: manifest.json
echo.
echo android_doc: firmware/release/Android端设备对接文档_v!VER!.md
echo android_doc: firmware/release/Android*_v!VER!.md (see release folder)
)
echo.
echo Done: firmware/release/ version=!VER!
echo VERSION.txt + manifest.json updated
echo VERSION.txt + manifest.json + ota_manifest.json updated
echo.
echo Note: run after idf.py build; build does not auto-copy here.
echo.
pause
exit /b 0
......@@ -4,6 +4,9 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
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)
# CONFIG_APP_LINK_WIFI is not set
# CONFIG_APP_LINK_BLE is not set
......@@ -14,9 +17,9 @@ CONFIG_APP_LINK_UART=y
CONFIG_APP_UART_LINK_BAUDRATE=115200
CONFIG_APP_UART_LINK_TX_GPIO=17
CONFIG_APP_UART_LINK_RX_GPIO=18
CONFIG_APP_PWM_IO15_SERVO=y
# CONFIG_APP_PWM_IO15_ESC is not set
# GPIO16: 1102 steering servo uses this pin. Must be SERVO. If dual ESC uses 15/16, set CONFIG_APP_PWM_IO16_ESC=y
# Default AUX: IO15=ESC (throttle), IO16=SERVO (steering). 1102 drive: IO10/21.
# CONFIG_APP_PWM_IO15_SERVO is not set
CONFIG_APP_PWM_IO15_ESC=y
CONFIG_APP_PWM_IO16_SERVO=y
# CONFIG_APP_PWM_IO16_ESC is not set
CONFIG_BT_ENABLED=y
......
# BLE 链路示例 defaults(与 CONFIG_APP_LINK_BLE=y 一并使用)
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.ble" build
CONFIG_APP_LINK_BLE=y
# CONFIG_APP_LINK_WIFI is not set
# CONFIG_APP_LINK_UART is not set
CONFIG_APP_BLE_OTA=y
CONFIG_APP_BLE_TX_POWER_N24=y
# 控制器上电默认发射也压低(与 APP_BLE_TX_POWER 一致)
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N24=y
......@@ -21,13 +21,12 @@ CONFIG_LOG_DEFAULT_LEVEL=2
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
CONFIG_ESP_CONSOLE_NONE=y
# When APP_UART_MODE_DEBUG=y: uart_comm uses UART from APP_DEBUG_UART_*; no ESP_LOG
# Release: uart_comm can use UART from APP_DEBUG_UART_*; no ESP_LOG
CONFIG_ROBO_APP_FW_RELEASE=y
# CONFIG_ROBO_APP_FW_DEBUG is not set
CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y
CONFIG_LOG_BOOTLOADER_LEVEL=2
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
......
......@@ -20,7 +20,7 @@
<input name='pass' type='password' placeholder='WiFi密码' %s>
<br>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<input name='devid' placeholder='CN110200000001' value='%s' %s>
<button type='submit' class='btn'>保存并重启设备</button>
</form>
</div>
......
......@@ -17,7 +17,7 @@
<p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='设备号' %s>
<input name='devid' placeholder='CN110200000001' value='%s' %s>
<b>蓝牙广播名 %s</b>
<input name='ble_name' placeholder='手机扫描看到的名称' %s>
<button type='submit' class='btn'>保存并重启</button>
......
......@@ -2,22 +2,25 @@
<html>
<head>
<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>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
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; }
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>
</head>
<body>
<div class="card">
<h1>串口设备配置</h1>
<p style="color:#666;font-size:14px;">UART 模式不保存 WiFi,仅需填写设备号</p>
<p style="color:#666;font-size:14px;">UART 模式无需 WiFi,请填写完整设备号</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<b>设备号 %s</b>
<input name='devid' placeholder='CN110200000001' value='%s' %s>
<p class="hint">格式示例:<code>CN110200000001</code>(无 <code>app2dev/</code> 前缀)<br>
型号由第 3~6 位自动识别,如 <strong>1102</strong> → 1102 车型控制策略</p>
<button type='submit' class='btn'>保存并重启</button>
</form>
</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