Commit 5eb7707c authored by 957dd's avatar 957dd

第二次提交修改bug,第一版问题还在

parent 8f3a151f
......@@ -4,3 +4,9 @@ cmake_minimum_required(VERSION 3.16)
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()
This source diff could not be displayed because it is too large. You can view the blob instead.
# 编译类型与 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.
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-06T06:42:37Z
built_local: 2026/06/06 14:42:37.77
OTA:
path: firmware/release/ota/ESPRCCar.bin
bytes: 1115760
sha256: 724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6
bytes: 889072
sha256: 9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef
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-06T06:42:37Z",
"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":889072,"sha256":"9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef"},
{"name":"bootloader.bin","bytes":13808,"sha256":"1bfe318a61972c3f601272c318091b06e62d0076360433f5e608c03064c639b2"},
{"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":"dc9e166779acf5a7a960cee93e4d778646e0fb7b110a4e1dbc9ea04031da361c"}
]
}
{
"project": "ESPRCCar",
"chip": "esp32s3",
"version": "1.0.2",
"generated_utc": "2026-06-06T06:42:37Z",
"image": "ESPRCCar.bin",
"bytes": 889072,
"sha256": "9f4e722697948ea5cc73025fd44ef131f260d0efbb2ec875c13ed571f6707fef"
}
......@@ -5,6 +5,8 @@ set(SOURCES
"device/device_model.c"
"protocol/remote_control.c"
"protocol/heart_payload.c"
"protocol/ota_offer_protocol.c"
"protocol/ota_frame_protocol.c"
"drivers/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/devices/device_1201.c"
......@@ -19,15 +21,16 @@ 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"
......@@ -74,4 +77,10 @@ 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)
......@@ -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,39 @@ 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 链路模式下的通信波特率。默认 115200。
适用于 APP_LINK_UART 模式下的 UART1 通信。
当前 UART 链路模式生效波特率(由上方档位自动生成)。
config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号"
......@@ -103,8 +129,39 @@ 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 角色"
......@@ -136,30 +193,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 优化,最小体积)"
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,最小代码体积,适合量产发布。
默认关闭 UART 控制台(见 sdkconfig.defaults.release),W/E 日志经 BLE 0xFFE3 Notify 推送。
量产发布版本:
- 优化级别 -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,12 @@ 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,
},
};
static esp_err_t app_task_create(const char *name,
......
......@@ -6,8 +6,29 @@ 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;
}
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;
}
if (strncmp(device_id, "1102", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1102");
return DEVICE_MODEL_1102;
}
}
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;
}
......
......@@ -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;
......
......@@ -10,37 +10,38 @@ static void device_1101_stop(void)
* 当前 1101 先复用 1201 的控制映射,后续若协议或硬件差异扩大,
* 只需在本文件内调整映射,不影响上层协议处理。
*/
static void device_1101_control(int mode, int val)
static void device_1101_control(int mode, int speed_val, int steer_val)
{
(void)steer_val;
if (mode == 1) {
if (val < 50) {
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, val / 2 - 10);
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 (val < 50) {
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, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, speed_val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} 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 (val < 45) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(130 - speed_val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
......
......@@ -70,28 +70,61 @@ static void device_1102_stop(void)
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);
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)
{
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: { /* 前进:IO10 */
uint32_t pct = drive_percent_from_val_200(v);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
case 1: { /* 前进:IO10,转向回中 */
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 */
uint32_t pct = drive_percent_from_val_200(v);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct);
case 2: { /* 后退:IO21,转向回中 */
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 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(v));
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(speed));
break;
}
case 4: { /* 右转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(v));
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:
......
......@@ -6,37 +6,38 @@ static void device_1201_stop(void)
rc_pwm_stop_all_drive_outputs();
}
static void device_1201_control(int mode, int val)
static void device_1201_control(int mode, int speed_val, int steer_val)
{
(void)steer_val;
if (mode == 1) {
if (val < 50) {
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, val / 2 - 10);
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 (val < 50) {
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, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, speed_val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} 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 (val < 45) {
if (speed_val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else if (speed_val < 70) {
rc_pwm_set_steering_angle_deg(130 - speed_val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
......
......@@ -198,10 +198,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);
}
}
......
......@@ -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)
......
......@@ -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);
/* 反初始化,恢复默认状态 */
......
This diff is collapsed.
......@@ -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
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 (xTaskCreate(uart_evt_rx_task, "uart_evt_rx", 4096, NULL, 6, &rx_task_s) != 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);
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 */
This diff is collapsed.
......@@ -44,8 +44,12 @@ int link_uart_send_heartbeat(const char *json);
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';
remote_control_apply_json(tmp);
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;
......@@ -50,6 +53,7 @@ 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);
}
/* 中止 OTA 会话 */
......@@ -65,6 +69,7 @@ static void ota_stream_abort_internal(void)
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)
......@@ -88,15 +93,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 +110,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 +156,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}",
......@@ -206,12 +222,11 @@ 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 ||
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;
......@@ -264,9 +279,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);
/* 留时间把 message_type=1002 结果 JSON 发完再重启 */
ESP_LOGI(tag_s, "Rebooting in 1 second...");
vTaskDelay(pdMS_TO_TICKS(1000));
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
......
......@@ -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"
......@@ -144,6 +145,14 @@ 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);
}
esp_err_t ota_manager_init(void)
{
......@@ -183,8 +192,23 @@ esp_err_t ota_manager_init(void)
current_session_s.boot_count++;
save_session_to_nvs();
/* 检查版本是否匹配 */
if (app_desc && strcmp(app_desc->version, current_session_s.target_version) == 0) {
/*
* 检查版本是否匹配:
* 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;
}
}
if (version_match) {
ESP_LOGI(tag_s, "Version matches: %s", app_desc->version);
/* 标记为成功 */
......@@ -204,6 +228,17 @@ esp_err_t ota_manager_init(void)
}
} 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");
......@@ -351,13 +386,45 @@ 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");
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);
......
......@@ -42,14 +42,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);
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) {
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_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 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;
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) {
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));
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 字节固件数据;设备回 OP_ACK,载荷 [data][status],
* status=0 成功,非 0 需重传同一 SEQ 帧。
*/
#ifndef OTA_FRAME_PROTOCOL_H
#define OTA_FRAME_PROTOCOL_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
#define OTA_FRAME_MAX_PAYLOAD 240
#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 "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;
}
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)
{
const char *dev = (device_id != NULL) ? device_id : "";
snprintf(out, cap, "%s", dev);
}
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);
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);
}
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);
}
}
#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
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);
/** 构造结果 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);
void ota_offer_set_done_callback(ota_offer_done_cb_t cb);
#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,121 @@
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", 0);
cJSON_AddNumberToObject(pwm, "direction", 0);
cJSON_AddNumberToObject(pwm, "mode", 0);
cJSON_AddNumberToObject(pwm, "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;
while (s_stop_notify_active) {
if (s_timeout_stopped) {
remote_control_stop_notify_push_once();
}
vTaskDelay(pdMS_TO_TICKS(RC_STOP_NOTIFY_PERIOD_MS));
}
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 = cJSON_GetObjectItem(pwm_ctrl, "speed");
cJSON *direction = cJSON_GetObjectItem(pwm_ctrl, "direction");
if (!speed || !direction || !cJSON_IsNumber(speed) || !cJSON_IsNumber(direction)) {
return false;
}
return (speed->valueint == 0 && direction->valueint == 0);
}
static void rc_watchdog_task(void *param)
{
(void)param;
......@@ -37,12 +141,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) {
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;
if (!s_timeout_stopped) {
rc_vehicle_stop();
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,50 +163,75 @@ 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);
}
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();
if (!json) {
return;
/*
* UART/USB 可能把一条 JSON 拆成多段。已有控制会话时,收到后续片段也刷新
* 看门狗时间,避免“第二段刚到但尚未 parse 完整”时误停车。
*/
if (s_last_cmd_time_ms != 0 && !s_timeout_stopped) {
s_last_cmd_time_ms = get_current_time_ms();
}
}
cJSON *root = cJSON_Parse(json);
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 中止、停车)");
}
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;
}
......@@ -120,20 +252,48 @@ void remote_control_apply_json(const char *json)
}
} else if (m_type->valueint == 3) {
cJSON *pwm_ctrl = cJSON_GetObjectItem(body, "pwm_ctrl");
if (remote_control_is_stop_pwm_json(pwm_ctrl)) {
rc_vehicle_stop();
ESP_LOGD(tag_s, "停车 JSON (speed=0,direction=0)");
return;
}
if (pwm_ctrl && cJSON_IsObject(pwm_ctrl)) {
cJSON *jm = cJSON_GetObjectItem(pwm_ctrl, "mode");
cJSON *jt = cJSON_GetObjectItem(pwm_ctrl, "type");
cJSON *jv = cJSON_GetObjectItem(pwm_ctrl, "val");
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");
return;
}
ESP_LOGI(tag_s, "PWM: mode %d type %d speed %d steer %d", mode, type, speed, steer);
rc_vehicle_control(mode, speed, steer);
}
}
}
}
}
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,45 @@
#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
/** 固件超时停车后,经链路 Notify 推送停车 JSON 的周期(ms) */
#define RC_STOP_NOTIFY_PERIOD_MS 300
/** 解析 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/direction/mode/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,102 @@
#include "mqttconf_commun.h"
#endif
#include "task_manager.h"
#if CONFIG_APP_LINK_BLE
#include "link_ble.h"
#endif
#if CONFIG_APP_LINK_UART
#include "device_model.h"
#endif
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();
}
}
static esp_err_t captive_no_content_handler(httpd_req_t *req)
{
(void)req;
httpd_resp_set_status(req, "204 No Content");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
#if CONFIG_APP_LINK_UART
static const char *uart_model_options_s[] = { "1101", "1102", "1201" };
#define UART_MODEL_OPTION_COUNT ((int)(sizeof(uart_model_options_s) / sizeof(uart_model_options_s[0])))
static bool uart_devid_is_valid(const char *val)
{
if (val == NULL) {
return false;
}
for (int i = 0; i < UART_MODEL_OPTION_COUNT; i++) {
if (strcmp(val, uart_model_options_s[i]) == 0) {
return true;
}
}
return false;
}
static const char *device_model_to_str(device_model_t model)
{
switch (model) {
case DEVICE_MODEL_1102:
return "1102";
case DEVICE_MODEL_1201:
return "1201";
default:
return "1101";
}
}
static void build_uart_model_select_options(char *buf, size_t buf_len, const char *current_model)
{
size_t pos = 0;
buf[0] = '\0';
for (int i = 0; i < UART_MODEL_OPTION_COUNT; i++) {
const char *model = uart_model_options_s[i];
const char *selected = (strcmp(current_model, model) == 0) ? " selected" : "";
int n = snprintf(buf + pos, (pos < buf_len) ? (buf_len - pos) : 0,
" <option value='%s'%s>%s 设备</option>\n",
model, selected, model);
if (n < 0 || (size_t)n >= buf_len - pos) {
break;
}
pos += (size_t)n;
}
}
#endif
esp_err_t init_spiffs(void)
{
esp_vfs_spiffs_conf_t conf = {
......@@ -41,25 +131,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)
......@@ -93,6 +192,18 @@ static esp_err_t get_handler(httpd_req_t *req)
FILE *f = fopen("/spiffs/index_ble.html", "r");
#elif CONFIG_APP_LINK_UART
char devid_status[128];
char devid_current[32] = {0};
char model_options[384];
char current_model[8] = "1101";
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid_current, sizeof(devid_current));
if (uart_devid_is_valid(devid_current)) {
strncpy(current_model, devid_current, sizeof(current_model) - 1);
} else if (devid_current[0] != '\0') {
strncpy(current_model, device_model_to_str(device_model_from_full_id(devid_current)),
sizeof(current_model) - 1);
}
build_uart_model_select_options(model_options, sizeof(model_options), current_model);
sprintf(devid_status, devid_editable_s ? "" : "disabled style='background:#eee'");
FILE *f = fopen("/spiffs/index_uart.html", "r");
#else
......@@ -120,7 +231,8 @@ static esp_err_t get_handler(httpd_req_t *req)
devid_editable_s ? "🔓" : "🔒", devid_status,
devid_editable_s ? "🔓" : "🔒", ble_status);
#elif CONFIG_APP_LINK_UART
snprintf(final_html, fsize + 1024, template, devid_editable_s ? "🔓" : "🔒", devid_status);
snprintf(final_html, fsize + 1024, template,
devid_editable_s ? "🔓" : "🔒", devid_status, model_options);
#else
snprintf(final_html, fsize + 1024, template,
wifi_editable_s ? "🔓" : "🔒", wifi_status, wifi_status,
......@@ -158,7 +270,11 @@ static esp_err_t post_handler(httpd_req_t *req)
*val = '\0';
val++;
char decoded_val[64];
url_decode(decoded_val, val);
if (!url_decode_safe(decoded_val, sizeof(decoded_val), val)) {
ESP_LOGW(tag_s, "表单字段过长或非法编码,已拒绝 key=%s", key);
token = strtok_r(NULL, "&", &saveptr);
continue;
}
#if CONFIG_APP_LINK_BLE
if (strcmp(key, "devid") == 0 && devid_editable_s) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
......@@ -166,7 +282,7 @@ static esp_err_t post_handler(httpd_req_t *req)
save_to_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, decoded_val);
}
#elif CONFIG_APP_LINK_UART
if (strcmp(key, "devid") == 0 && devid_editable_s) {
if (strcmp(key, "devid") == 0 && devid_editable_s && uart_devid_is_valid(decoded_val)) {
save_to_nvs(DEVICE_CFG_KEY_DEVICE_ID, decoded_val);
}
#else
......@@ -182,9 +298,11 @@ static esp_err_t post_handler(httpd_req_t *req)
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 +346,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) {
......@@ -248,12 +389,20 @@ void start_config_web(void)
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
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);
httpd_uri_t cap204 = {.uri = "/generate_204", .method = HTTP_GET, .handler = captive_no_content_handler};
httpd_register_uri_handler(server, &cap204);
httpd_uri_t cap204b = {.uri = "/gen_204", .method = HTTP_GET, .handler = captive_no_content_handler};
httpd_register_uri_handler(server, &cap204b);
httpd_uri_t favicon = {.uri = "/favicon.ico", .method = HTTP_GET, .handler = captive_no_content_handler};
httpd_register_uri_handler(server, &favicon);
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
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
# 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
......
......@@ -6,18 +6,20 @@
<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; }
select { width: 95%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; font-size: 16px; background: white; }
.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; }
</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,请选择设备型号;心跳将上报所选型号(如 1101、1102)</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<b>设备型号 %s</b>
<select name='devid' %s>
%s
</select>
<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