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

bug修改完,串口通信

parent 5eb7707c
......@@ -78,6 +78,9 @@ htmlcov/
compile_commands.json
.cursor/
# --- 本地 Agent 技能(pua 等,勿入库)---
pua/
# --- 日志与临时文件 ---
*.log
*~
......
......@@ -2,6 +2,19 @@
# 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)
......
......@@ -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`
......
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
......@@ -62,7 +62,7 @@ Android 做 BLE / UART OTA **只需要两个文件**(运行 `scripts\copy_firm
- **OTA 固件确认**:设备回 **完整 ACK/NAK 帧**(BLE 经 **0xFFE4** 按帧字节 Notify;UART 按帧写回);`status≠0` 时 App **原 SEQ 重传**
- **控制 JSON**:手机下发后 **无 `OK` 文本回复**
- **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。
- **心跳仍在 `0xFFE3` / UART 串口**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**,字段见 §3.5-A
- **心跳**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**(§3.5-A)。**BLE****0xFFE3**,与 **0xFFE4** OTA ACK **分特征值**,OTA 期间心跳照常。**UART** 与 OT ACK **共用 UART1**:手机发 `0` 武装后至 `1002/done` 或会话中止前,设备 **暂停 UART 心跳**,线上仅 OT ACK/NAK + `1002` JSON,避免与逐帧确认混线
- **命令下发间隔**(BLE/UART 共用):手机→设备 **任意相邻两条 JSON** 间隔 **≥100ms**;摇杆控制建议 **100~200ms** 一发,且须 **≤500ms** 以免固件超时停车(§5.0.2、§7.2.3)。
- **停车专用 JSON**(§4.4.1、§4.4.2):松手/停止时发 **全位 0**`speed/direction/mode/val` 均为 0);**前进+左转等多键同时抬起** 也必须用同一条 JSON,**每 300ms** 重复写。
- **UART 波特率固定 115200**:App **设置页**应有「串口波特率」项,但当前固件只支持 **115200**;每次 `open` 后必须 `setParameters(115200, 8, N, 1)`(§7.2.0)。
......@@ -539,7 +539,19 @@ flowchart TD
**BEGIN/END 应答**:PAYLOAD **`[status]`** 仅 1 字节。
**NAK**`OP=0x81`):CRC/长度错误等。
**Android**:从 **0xFFE4** 组满一整帧再校验;`status!=0``data_byte` 不符 → **重传**
**整帧长度(VER=0x02,便于日志对照)**
| OP | 方向 | 典型总长 |
|----|------|----------|
| BEGIN `0x01` | 手机→设备 | **14**(8 头 + 4B 镜像长度 + CRC + TAIL) |
| DATA `0x02` | 手机→设备 | **11**(8 头 + 1B 固件字节 + CRC + TAIL) |
| END `0x03` | 手机→设备 | **10**(8 头 + 空 payload + CRC + TAIL) |
| ACK `0x80` | 设备→手机 | BEGIN/END:**11**;DATA:**12**(含回显字节) |
| NAK `0x81` | 设备→手机 | **11** |
设备 **每收到一帧完整 OT 请求即在同链路回一帧 ACK/NAK**(UART 写回 UART1;BLE Notify **0xFFE4**)。App **必须等 ACK `status==0` 再发下一帧**,禁止无节流连发 DATA。
**Android**:从 **0xFFE4**(或 UART 读流)组满一整帧再校验;`status!=0``data_byte` 不符 → **原 SEQ 重传**
#### 3.4.3 旧版裸 opcode(兼容)
......@@ -1788,7 +1800,8 @@ Android App 内置或下载 OTA 包时只需要两个文件:
|------|-----|-------------------|
| 询问 / 结果 JSON | **0xFFE3** Notify | UART1 文本行 + `\n` |
| 同意 OTA | Write **0xFFE1** | JSON 行 + `\n` |
| 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧) |
| 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧);**每 RX 一帧即 TX 一回 ACK** |
| OTA 期间心跳 | **0xFFE3** 照常(与 ACK 分通道) | **暂停**(避免与 ACK 混线) |
| 固件数据确认(旧版兼容) | 旧固件可能出现单字节回显 | 旧固件可能出现同线单字节回显 |
| 控制 JSON 后 | 无 ACK | **无** `OK` |
| USB 调试口(UART0) | Release 下 **无日志**(§1.5) | 同左 |
......@@ -1798,7 +1811,7 @@ Android App 内置或下载 OTA 包时只需要两个文件:
1. **OT 帧(推荐)**:按 §3.4.2 发送 BEGIN/DATA/END,等待 ACK/NAK 帧;`status!=0` 按原 SEQ 重传。
2. **裸 `0x01/0x02/0x03`(兼容旧流程)**:仍可用,但新 App 不建议继续依赖该模式。
**UART 读线程必须做双路解复用**:同一 UART1 RX 流里既可能有 JSON 行(`{...}\n`,如心跳/1002),也可能有 OT ACK/NAK 二进制帧(`0x4F 0x54 ... 0x16`)。OTA 阶段读线程应优先组 OT 帧;遇到 `{` 再按换行组 JSON。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。
**UART 读线程必须做双路解复用(App 读设备下行)**:手机 `read()` 到的字节流里,**非 OTA 阶段**可能有 JSON 行(`{...}\n`,心跳/`1002`)。**OTA 二进制阶段**(已发 `message_type=0`)设备 **暂停 UART 心跳**,线上以 **OT ACK/NAK 帧**`0x4F 0x54 ... 0x16`)为主,结束时再收 `1002/done` JSON。读线程应 **按字节组 OT 帧**;仅当缓冲以 `{` 开头且无完整 OT 魔数时再按 `\n` 组 JSON。**禁止**把整个 `read()` 缓冲当 UTF-8 文本解析(会破坏 ACK 二进制)。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。
**JSON 分片/粘包必读(对应你看到的日志现象)**
......@@ -1814,12 +1827,14 @@ Android App 内置或下载 OTA 包时只需要两个文件:
3. 解析完从缓冲中移除该行,继续处理剩余内容(可能是下一条 JSON 头部)。
4. 没有 `\n` 的残留内容继续保留,等待下一次回调拼接。
**推荐参考实现(串口文本流)**
**推荐参考实现(握手/遥控阶段:仅 JSON 行)**
> 下列代码 **仅适用于未进入 OTA 二进制阶段**(尚未发 `message_type=0`)。OTA 推包时必须改用下方 **字节流 + OT 帧解析**,不能把 `read()` 当 `String`。
```kotlin
private val rxTextBuffer = StringBuilder()
fun onSerialBytes(bytes: ByteArray) {
fun onSerialBytesForJsonOnly(bytes: ByteArray) {
rxTextBuffer.append(bytes.toString(Charsets.UTF_8))
while (true) {
......@@ -1830,7 +1845,7 @@ fun onSerialBytes(bytes: ByteArray) {
rxTextBuffer.delete(0, idx + 1)
if (line.isEmpty()) continue
if (!line.startsWith("{")) continue // 过滤日志行或非 JSON 文本
if (!line.startsWith("{")) continue
try {
handleJson(JSONObject(line))
......@@ -1841,6 +1856,23 @@ fun onSerialBytes(bytes: ByteArray) {
}
```
**OTA 阶段读 ACK(与 JSON 解耦,伪代码)**
```kotlin
private val rxOtaBuf = ByteArray(512)
private var rxOtaLen = 0
fun onSerialBytesDuringOta(chunk: ByteArray) {
// 追加到字节缓冲,循环尝试 parseOtFrame();完整 ACK 后再发下一 DATA
append(rxOtaBuf, chunk)
while (true) {
val frame = tryParseOtFrame(rxOtaBuf) ?: break
consume(rxOtaBuf, frame.size)
otaAckQueue.offer(frame)
}
}
```
**遥控连续发送与超时停车(必读)**
- 固件遥控看门狗按“最近一次收到命令字节”计时,间隔过长会触发自动停车。
......
This diff is collapsed.
# 安装与配网指南
# 安装与配网指南
......@@ -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` |
......
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
# Android 端设备对接文档 · 固件 v1.0.1(三链路模式)
......@@ -62,7 +62,7 @@ Android 做 BLE / UART OTA **只需要两个文件**(运行 `scripts\copy_firm
- **OTA 固件确认**:设备回 **完整 ACK/NAK 帧**(BLE 经 **0xFFE4** 按帧字节 Notify;UART 按帧写回);`status≠0` 时 App **原 SEQ 重传**
- **控制 JSON**:手机下发后 **无 `OK` 文本回复**
- **删除 GATT `0xFFE5`**:告警/错误改由 **`0xFFE3` Notify** 推送,`head.message_type` **4**=告警、**5**=错误,`body.msg` 为文本。
- **心跳仍在 `0xFFE3` / UART 串口**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**,字段见 §3.5-A
- **心跳**`message_type` **1****连接成功立即 1 条**,之后周期 **UART 3s / BLE 10s**(§3.5-A)。**BLE****0xFFE3**,与 **0xFFE4** OTA ACK **分特征值**,OTA 期间心跳照常。**UART** 与 OT ACK **共用 UART1**:手机发 `0` 武装后至 `1002/done` 或会话中止前,设备 **暂停 UART 心跳**,线上仅 OT ACK/NAK + `1002` JSON,避免与逐帧确认混线
- **命令下发间隔**(BLE/UART 共用):手机→设备 **任意相邻两条 JSON** 间隔 **≥100ms**;摇杆控制建议 **100~200ms** 一发,且须 **≤500ms** 以免固件超时停车(§5.0.2、§7.2.3)。
- **停车专用 JSON**(§4.4.1、§4.4.2):松手/停止时发 **全位 0**`speed/direction/mode/val` 均为 0);**前进+左转等多键同时抬起** 也必须用同一条 JSON,**每 300ms** 重复写。
- **UART 波特率固定 115200**:App **设置页**应有「串口波特率」项,但当前固件只支持 **115200**;每次 `open` 后必须 `setParameters(115200, 8, N, 1)`(§7.2.0)。
......@@ -539,7 +539,19 @@ flowchart TD
**BEGIN/END 应答**:PAYLOAD **`[status]`** 仅 1 字节。
**NAK**`OP=0x81`):CRC/长度错误等。
**Android**:从 **0xFFE4** 组满一整帧再校验;`status!=0``data_byte` 不符 → **重传**
**整帧长度(VER=0x02,便于日志对照)**
| OP | 方向 | 典型总长 |
|----|------|----------|
| BEGIN `0x01` | 手机→设备 | **14**(8 头 + 4B 镜像长度 + CRC + TAIL) |
| DATA `0x02` | 手机→设备 | **11**(8 头 + 1B 固件字节 + CRC + TAIL) |
| END `0x03` | 手机→设备 | **10**(8 头 + 空 payload + CRC + TAIL) |
| ACK `0x80` | 设备→手机 | BEGIN/END:**11**;DATA:**12**(含回显字节) |
| NAK `0x81` | 设备→手机 | **11** |
设备 **每收到一帧完整 OT 请求即在同链路回一帧 ACK/NAK**(UART 写回 UART1;BLE Notify **0xFFE4**)。App **必须等 ACK `status==0` 再发下一帧**,禁止无节流连发 DATA。
**Android**:从 **0xFFE4**(或 UART 读流)组满一整帧再校验;`status!=0``data_byte` 不符 → **原 SEQ 重传**
#### 3.4.3 旧版裸 opcode(兼容)
......@@ -1788,7 +1800,8 @@ Android App 内置或下载 OTA 包时只需要两个文件:
|------|-----|-------------------|
| 询问 / 结果 JSON | **0xFFE3** Notify | UART1 文本行 + `\n` |
| 同意 OTA | Write **0xFFE1** | JSON 行 + `\n` |
| 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧) |
| 固件数据确认(推荐 OT 帧) | **0xFFE4** ACK/NAK 帧(需组帧) | UART1 ACK/NAK 帧(需组帧);**每 RX 一帧即 TX 一回 ACK** |
| OTA 期间心跳 | **0xFFE3** 照常(与 ACK 分通道) | **暂停**(避免与 ACK 混线) |
| 固件数据确认(旧版兼容) | 旧固件可能出现单字节回显 | 旧固件可能出现同线单字节回显 |
| 控制 JSON 后 | 无 ACK | **无** `OK` |
| USB 调试口(UART0) | Release 下 **无日志**(§1.5) | 同左 |
......@@ -1798,7 +1811,7 @@ Android App 内置或下载 OTA 包时只需要两个文件:
1. **OT 帧(推荐)**:按 §3.4.2 发送 BEGIN/DATA/END,等待 ACK/NAK 帧;`status!=0` 按原 SEQ 重传。
2. **裸 `0x01/0x02/0x03`(兼容旧流程)**:仍可用,但新 App 不建议继续依赖该模式。
**UART 读线程必须做双路解复用**:同一 UART1 RX 流里既可能有 JSON 行(`{...}\n`,如心跳/1002),也可能有 OT ACK/NAK 二进制帧(`0x4F 0x54 ... 0x16`)。OTA 阶段读线程应优先组 OT 帧;遇到 `{` 再按换行组 JSON。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。
**UART 读线程必须做双路解复用(App 读设备下行)**:手机 `read()` 到的字节流里,**非 OTA 阶段**可能有 JSON 行(`{...}\n`,心跳/`1002`)。**OTA 二进制阶段**(已发 `message_type=0`)设备 **暂停 UART 心跳**,线上以 **OT ACK/NAK 帧**`0x4F 0x54 ... 0x16`)为主,结束时再收 `1002/done` JSON。读线程应 **按字节组 OT 帧**;仅当缓冲以 `{` 开头且无完整 OT 魔数时再按 `\n` 组 JSON。**禁止**把整个 `read()` 缓冲当 UTF-8 文本解析(会破坏 ACK 二进制)。不要假设一次 `read()` 就是一整帧,USB 串口库可能把一个 OT 帧拆成多次回调。
**JSON 分片/粘包必读(对应你看到的日志现象)**
......@@ -1814,12 +1827,14 @@ Android App 内置或下载 OTA 包时只需要两个文件:
3. 解析完从缓冲中移除该行,继续处理剩余内容(可能是下一条 JSON 头部)。
4. 没有 `\n` 的残留内容继续保留,等待下一次回调拼接。
**推荐参考实现(串口文本流)**
**推荐参考实现(握手/遥控阶段:仅 JSON 行)**
> 下列代码 **仅适用于未进入 OTA 二进制阶段**(尚未发 `message_type=0`)。OTA 推包时必须改用下方 **字节流 + OT 帧解析**,不能把 `read()` 当 `String`。
```kotlin
private val rxTextBuffer = StringBuilder()
fun onSerialBytes(bytes: ByteArray) {
fun onSerialBytesForJsonOnly(bytes: ByteArray) {
rxTextBuffer.append(bytes.toString(Charsets.UTF_8))
while (true) {
......@@ -1830,7 +1845,7 @@ fun onSerialBytes(bytes: ByteArray) {
rxTextBuffer.delete(0, idx + 1)
if (line.isEmpty()) continue
if (!line.startsWith("{")) continue // 过滤日志行或非 JSON 文本
if (!line.startsWith("{")) continue
try {
handleJson(JSONObject(line))
......@@ -1841,6 +1856,23 @@ fun onSerialBytes(bytes: ByteArray) {
}
```
**OTA 阶段读 ACK(与 JSON 解耦,伪代码)**
```kotlin
private val rxOtaBuf = ByteArray(512)
private var rxOtaLen = 0
fun onSerialBytesDuringOta(chunk: ByteArray) {
// 追加到字节缓冲,循环尝试 parseOtFrame();完整 ACK 后再发下一 DATA
append(rxOtaBuf, chunk)
while (true) {
val frame = tryParseOtFrame(rxOtaBuf) ?: break
consume(rxOtaBuf, frame.size)
otaAckQueue.offer(frame)
}
}
```
**遥控连续发送与超时停车(必读)**
- 固件遥控看门狗按“最近一次收到命令字节”计时,间隔过长会触发自动停车。
......
This diff is collapsed.
......@@ -3,13 +3,13 @@ ESPRCCar firmware release
version: 1.0.2
project: ESPRCCar
chip: esp32s3
built_utc: 2026-06-06T06:42:37Z
built_local: 2026/06/06 14:42:37.77
built_utc: 2026-06-11T08:29:54Z
built_local: 2026/06/11 16:29:54.96
OTA:
path: firmware/release/ota/ESPRCCar.bin
bytes: 889072
sha256: 9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef
bytes: 889552
sha256: 065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977
Factory: firmware/release/factory/
tool: Espressif Flash Download Tools
......
......@@ -5,14 +5,14 @@
"flash_freq": "80m",
"flash_size": "16MB",
"version": "1.0.2",
"generated_utc": "2026-06-06T06:42:37Z",
"generated_utc": "2026-06-11T08:29:54Z",
"ota_image": "release/ota/ESPRCCar.bin",
"factory_dir": "release/factory",
"files": [
{"name":"ESPRCCar.bin","bytes":889072,"sha256":"9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef"},
{"name":"bootloader.bin","bytes":13808,"sha256":"1bfe318a61972c3f601272c318091b06e62d0076360433f5e608c03064c639b2"},
{"name":"ESPRCCar.bin","bytes":889552,"sha256":"065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"},
{"name":"bootloader.bin","bytes":13808,"sha256":"21a5becf42c59120c3c4322a04ca684f86a496dfb770575ebbe6d6ec739b7199"},
{"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"},
{"name":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"},
{"name":"storage.bin","bytes":327680,"sha256":"dc9e166779acf5a7a960cee93e4d778646e0fb7b110a4e1dbc9ea04031da361c"}
{"name":"storage.bin","bytes":327680,"sha256":"6d2f4c246d34ebf80e3d32ac4873aa84d29f68ee799214b123bdd998a8b2d3b2"}
]
}
......@@ -2,8 +2,8 @@
"project": "ESPRCCar",
"chip": "esp32s3",
"version": "1.0.2",
"generated_utc": "2026-06-06T06:42:37Z",
"generated_utc": "2026-06-11T08:29:54Z",
"image": "ESPRCCar.bin",
"bytes": 889072,
"sha256": "9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef"
"bytes": 889552,
"sha256": "065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"
}
......@@ -7,11 +7,12 @@ set(SOURCES
"protocol/heart_payload.c"
"protocol/ota_offer_protocol.c"
"protocol/ota_frame_protocol.c"
"drivers/driver_manager.c"
"protocol/ota_uart_tune.c"
"drivers/driver_manager/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/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"
......@@ -37,8 +38,11 @@ set(INCLUDE_DIRS
"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)
......@@ -77,10 +81,4 @@ idf_component_register(SRCS ${SOURCES}
REQUIRES ${MAIN_REQUIRES}
PRIV_REQUIRES ${MAIN_PRIV_REQUIRES})
# menuconfig 仅勾选 ROBO_APP_FW_RELEASE 时,LOG/控制台 select 可能未写入 sdkconfig;
# 在 main 组件编译期强制 WARN,去掉 ESP_LOGI/LOGD(与 sdkconfig.defaults.release 一致)。
if(CONFIG_ROBO_APP_FW_RELEASE)
target_compile_definitions(${COMPONENT_LIB} PRIVATE LOG_LOCAL_LEVEL=ESP_LOG_WARN)
endif()
spiffs_create_partition_image(storage ../www FLASH_IN_PROJECT)
......@@ -82,6 +82,15 @@ config APP_UART_LINK_BAUDRATE
help
当前 UART 链路模式生效波特率(由上方档位自动生成)。
config APP_OTA_UART_MAX_CHUNK
int "OTA DATA 单帧最大载荷 (字节)"
depends on APP_LINK_UART
default 1024
range 240 1024
help
UART OTA 每帧最多携带的固件字节数。越大往返越少、越快;
须与 Android OtaFrame 发送缓冲一致。推荐 1024(460800 下 6MB 约 1~3 分钟)。
config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号"
default 17
......@@ -165,9 +174,10 @@ 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)"
......@@ -178,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)"
......
......@@ -72,6 +72,12 @@ static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = {
.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;
......
......@@ -13,10 +13,6 @@ device_model_t device_model_from_full_id(const char *device_id)
size_t len = strlen(device_id);
if (len == DEVICE_MODEL_SLICE_CHAR_LEN) {
if (strncmp(device_id, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1201");
return DEVICE_MODEL_1201;
}
if (strncmp(device_id, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1101");
return DEVICE_MODEL_1101;
......@@ -34,10 +30,6 @@ device_model_t device_model_from_full_id(const char *device_id)
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START;
if (strncmp(slice, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1201 → 型号 1201");
return DEVICE_MODEL_1201;
}
if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101");
return DEVICE_MODEL_1101;
......
......@@ -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
#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,20 +91,32 @@ 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_drive_forward(uint32_t pct)
{
if (pct == 0U) {
device_1102_request_brake();
return;
}
device_1102_ensure_brake_task();
device_1102_brake_abort();
s_last_drive_pct = pct;
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
}
static void device_1102_drive_backward(uint32_t pct)
{
if (pct == 0U) {
device_1102_request_brake();
return;
}
device_1102_ensure_brake_task();
device_1102_brake_abort();
s_last_drive_pct = pct;
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct);
}
......@@ -87,46 +126,40 @@ static void device_1102_control(int mode, int speed_val, int steer_val)
int speed = clamp_int(speed_val, 0, DEV1102_VAL_MAX);
int steer = clamp_int(steer_val, 0, DEV1102_VAL_MAX);
switch (mode) {
case 1: { /* 前进:IO10,转向回中 */
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 2: { /* 后退:IO21,转向回中 */
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 3: { /* 左转:IO16 舵机 */
case 3:
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(speed));
break;
}
case 4: { /* 右转:IO16 舵机 */
case 4:
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(speed));
break;
}
case 5: { /* 前左:前进 + 左转 */
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: { /* 前右:前进 + 右转 */
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: { /* 后左:后退 + 左转 */
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: { /* 后右:后退 + 右转 */
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;
}
......@@ -147,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 speed_val, int steer_val)
{
(void)steer_val;
if (mode == 1) {
if (speed_val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, speed_val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (speed_val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, speed_val / 2 - 10);
}
} else if (mode == 3) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(50 + speed_val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(130 - speed_val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1101_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1101",
.stop = device_1101_stop,
.control = device_1101_control,
.shot = device_1101_shot,
};
const device_drive_ops_t *device_1101_get_ops(void)
{
return &ops_s;
}
#include "device_1201.h"
#include "rc_pwm_control.h"
static void device_1201_stop(void)
{
rc_pwm_stop_all_drive_outputs();
}
static void device_1201_control(int mode, int speed_val, int steer_val)
{
(void)steer_val;
if (mode == 1) {
if (speed_val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, speed_val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (speed_val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, speed_val / 2 - 10);
}
} else if (mode == 3) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(50 + speed_val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(130 - speed_val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1201_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1201",
.stop = device_1201_stop,
.control = device_1201_control,
.shot = device_1201_shot,
};
const device_drive_ops_t *device_1201_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1201_H
#define DEVICE_1201_H
#include "device_drive.h"
const device_drive_ops_t *device_1201_get_ops(void);
#endif
......@@ -2,10 +2,12 @@
#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);
}
......@@ -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) {
......
......@@ -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)
*/
......
......@@ -287,6 +287,9 @@ static void ble_send_heartbeat_once(void)
if (!ble_link_up_s || hb_chr_val_handle_s == 0U) {
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);
......@@ -550,8 +553,33 @@ static void ble_ota_done_report(bool success, uint32_t bytes_written, const char
char *json = ota_offer_result_json_malloc(devid, success, bytes_written, err_step);
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): %s", json);
ESP_LOGI(tag_s, "BLE TX OTA 进行中 (1002/active): %s", json);
free(json);
}
}
......@@ -589,7 +617,7 @@ static bool ble_dispatch_json_line(const char *json)
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();
link_ble_session_reset_ex("reset");
ESP_LOGI(tag_s, "收到 message_type=2,已清空 BLE 会话");
cJSON_Delete(root);
return true;
......@@ -603,6 +631,7 @@ static bool ble_dispatch_json_line(const char *json)
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;
}
......@@ -640,7 +669,7 @@ static int gap_event(struct ble_gap_event *event, void *arg)
ble_conn_handle_s = 0U;
link_dma_ble_set_conn_handle(0);
remote_control_watchdog_stop();
link_ble_session_reset();
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;
......@@ -746,8 +775,11 @@ static void ble_request_low_duty_conn(uint16_t conn_handle)
}
}
void link_ble_session_reset(void)
static void link_ble_session_reset_ex(const char *ota_abort_err)
{
if (ota_abort_err != NULL) {
ota_offer_report_abort(ota_abort_err);
}
ota_frame_protocol_reset();
remote_control_link_reset();
memset(s_ffe1_json_buf_s, 0, sizeof(s_ffe1_json_buf_s));
......@@ -756,6 +788,11 @@ void link_ble_session_reset(void)
last_ota_stat_json_s[0] = '\0';
}
void link_ble_session_reset(void)
{
link_ble_session_reset_ex(NULL);
}
void link_ble_stop(void)
{
if (!s_ble_stack_running_s) {
......
......@@ -15,6 +15,11 @@ 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;
......@@ -121,14 +126,15 @@ esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int
}
running_s = true;
if (xTaskCreate(uart_evt_rx_task, "uart_evt_rx", 4096, NULL, 6, &rx_task_s) != pdPASS) {
if (xTaskCreatePinnedToCore(uart_evt_rx_task, "uart_evt_rx", 6144, NULL, 6,
&rx_task_s, UART_RX_TASK_CORE) != pdPASS) {
running_s = false;
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)",
(int)port, baud_rate, tx_gpio, rx_gpio, UART_BUF_SIZE);
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;
}
......
This diff is collapsed.
......@@ -37,7 +37,7 @@ 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 成功,其他失败
*/
......
......@@ -48,12 +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);
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);
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 会话 */
......@@ -64,6 +86,11 @@ 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;
......@@ -82,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)
......@@ -182,7 +214,9 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
{
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;
}
......@@ -221,12 +255,14 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
session_s.written_size += payload_len;
/* 每 8192 字节发送一次进度,或最后一次 */
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);
session_s.last_reported_size = session_s.written_size;
/* 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);
session_s.last_reported_size = session_s.written_size;
}
}
return 0;
......@@ -236,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;
}
......@@ -270,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();
......@@ -281,9 +317,9 @@ static int ota_stream_handle_end(void)
ota_offer_notify_done(true, (uint32_t)session_s.written_size, NULL);
/* 留时间把 message_type=1002 结果 JSON 发完再重启 */
ESP_LOGI(tag_s, "Rebooting in 1 second...");
vTaskDelay(pdMS_TO_TICKS(1000));
/* 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;
......
......@@ -63,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状态
......
......@@ -23,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"
......@@ -68,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);
......@@ -100,6 +106,7 @@ static esp_err_t save_session_to_nvs(void)
nvs_set_u8(handle, OTA_MGR_KEY_TYPE, (uint8_t)current_session_s.type);
nvs_set_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);
......@@ -153,6 +160,16 @@ static bool partition_is_same(const esp_partition_t *a, const esp_partition_t *b
return (a->address == b->address) && (a->size == b->size);
}
/** 传输失败或中断后仍停留在升级前分区(无需切 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)
{
......@@ -185,45 +202,37 @@ 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();
/*
* 检查版本是否匹配:
* 1) 目标版本与当前运行版本一致(强校验);
* 2) 若目标版本未知/旧流程未携带目标版本,只要当前版本已不同于 source_version,
* 也视为新固件已生效(避免 BLE/UART OTA 成功后误判为 mismatch)。
*/
bool version_match = false;
if (app_desc) {
if (strcmp(app_desc->version, current_session_s.target_version) == 0) {
version_match = true;
} else if (current_session_s.source_version[0] != '\0' &&
strcmp(app_desc->version, current_session_s.source_version) != 0) {
version_match = true;
}
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 (version_match) {
ESP_LOGI(tag_s, "Version matches: %s", app_desc->version);
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();
ota_manager_update_stored_version(app_desc->version);
/* 延迟一段时间后清除会话(给应用层确认的时间) */
/* 应用层需要调用 ota_manager_mark_success() 来最终确认 */
if (app_desc) {
ota_manager_update_stored_version(app_desc->version);
}
} 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();
}
......@@ -242,15 +251,20 @@ esp_err_t ota_manager_init(void)
/* 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) {
......@@ -269,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;
......@@ -290,6 +314,7 @@ esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version)
}
const esp_app_desc_t *app_desc = esp_app_get_description();
const esp_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;
......@@ -298,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();
}
......@@ -419,6 +449,7 @@ esp_err_t ota_manager_rollback(void)
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)) {
......@@ -431,7 +462,8 @@ esp_err_t ota_manager_rollback(void)
/* 设置启动分区为上一版本 */
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,7 +43,7 @@ char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool
cJSON_AddStringToObject(body, "ip", ip);
}
const char *dev = (device_id != NULL) ? device_id : "";
const char *dev = device_id_plain(device_id);
char voltage_s[16];
snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v());
......
......@@ -107,6 +107,15 @@ static void ota_frame_send_simple(uint8_t op, uint16_t seq, const uint8_t *paylo
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);
}
}
......@@ -117,6 +126,18 @@ static void ota_frame_send_ack(uint16_t seq, uint8_t data_byte, uint8_t status)
ota_frame_send_simple(OTA_FRAME_OP_ACK, seq, pl, 2);
}
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);
......@@ -127,6 +148,24 @@ static void ota_frame_send_nak(uint16_t seq, uint8_t err_code)
ota_frame_send_simple(OTA_FRAME_OP_NAK, seq, &err_code, 1);
}
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) {
......@@ -152,6 +191,14 @@ static void handle_complete_frame(void)
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) {
......@@ -164,14 +211,20 @@ static void handle_complete_frame(void)
break;
}
case OTA_FRAME_OP_DATA: {
if (plen != 1) {
if (plen < 1 || plen > OTA_FRAME_MAX_PAYLOAD) {
ota_frame_send_nak(s_parser.seq, OTA_ACK_ERR_BAD_LEN);
return;
}
uint8_t b = p[0];
uint8_t pkt[2] = { OTA_OP_DATA, b };
int rc = ota_binary_stream_process_packet(pkt, 2);
ota_frame_send_ack(s_parser.seq, b, (uint8_t)map_stream_rc_to_ack(rc));
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: {
......
......@@ -7,12 +7,15 @@
* - CRC8: 从 VER 到 PAYLOAD 末字节逐字节 XOR
* - TAIL: 0x16
*
* 数据 OP_DATA 建议每帧 1 字节固件数据;设备回 OP_ACK,载荷 [data][status],
* status=0 成功,非 0 需重传同一 SEQ 帧。
* 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>
......@@ -35,8 +38,15 @@
#define OTA_ACK_ERR_BAD_LEN 0x05
#define OTA_ACK_ERR_BAD_OP 0x06
#define OTA_FRAME_MAX_PAYLOAD 240
#define OTA_FRAME_BUF_SIZE (8 + OTA_FRAME_MAX_PAYLOAD + 2)
#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);
......
#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>
......@@ -20,6 +24,11 @@ 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;
......@@ -27,8 +36,25 @@ void ota_offer_disarm(void)
static void ota_offer_format_device_id(char *out, size_t cap, const char *device_id)
{
const char *dev = (device_id != NULL) ? device_id : "";
snprintf(out, cap, "%s", dev);
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)
......@@ -68,6 +94,25 @@ char *ota_offer_query_json_malloc(const char *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);
}
......@@ -92,6 +137,8 @@ char *ota_offer_result_json_malloc(const char *device_id,
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);
......@@ -147,3 +194,16 @@ void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err
s_done_cb_s(success, bytes_written, err_step);
}
}
void ota_offer_report_abort(const char *err_step)
{
if (!ota_offer_session_busy()) {
return;
}
uint32_t written = ota_binary_stream_get_written_bytes();
if (ota_binary_stream_is_active()) {
ota_binary_stream_abort();
}
ota_offer_notify_done(false, written, err_step);
}
......@@ -11,6 +11,13 @@
/** 手机 → 设备:同意开始二进制 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 */
......@@ -39,12 +46,24 @@ 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
This diff is collapsed.
......@@ -16,8 +16,9 @@
/** 遥控超时:超过该时间未收到控制 JSON 则停车(ms) */
#define RC_CMD_TIMEOUT_MS 500
/** 固件超时停车后,经链路 Notify 推送停车 JSON 的周期(ms) */
#define RC_STOP_NOTIFY_PERIOD_MS 300
/** 固件超时停车后,经链路推送行走全停 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);
......@@ -28,7 +29,7 @@ void remote_control_link_reset(void);
/** 链路正在接收一条控制 JSON 的片段,用于避免分片期间误触发超时停车 */
void remote_control_note_rx_activity(void);
/** 构造停车专用 JSON(message_type=3,speed/direction/mode/val 均为 0) */
/** 构造停车专用 JSON(message_type=3,speed_val/steer_val 均为 0) */
char *remote_control_stop_json_malloc(void);
/** 注册停车状态 Notify 回调(BLE 0xFFE3 / UART 行);json 由回调方发送后勿 free */
......
This diff is collapsed.
......@@ -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
......
......@@ -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,24 +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; }
select { width: 95%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; font-size: 16px; background: white; }
input { width: 95%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; font-size: 16px; box-sizing: border-box; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; margin-top: 10px; }
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,请选择设备型号;心跳将上报所选型号(如 1101、1102)</p>
<p style="color:#666;font-size:14px;">UART 模式无需 WiFi,请填写完整设备号</p>
<form action='/save' method='POST'>
<b>设备号 %s</b>
<select name='devid' %s>
%s
</select>
<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