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

Merge branch 'feature/android_initial_version' into 'master'

Feature/android initial version See merge request !1
parents 8f3a151f 76e8da9d
...@@ -78,6 +78,9 @@ htmlcov/ ...@@ -78,6 +78,9 @@ htmlcov/
compile_commands.json compile_commands.json
.cursor/ .cursor/
# --- 本地 Agent 技能(pua 等,勿入库)---
pua/
# --- 日志与临时文件 --- # --- 日志与临时文件 ---
*.log *.log
*~ *~
......
...@@ -2,5 +2,24 @@ ...@@ -2,5 +2,24 @@
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# esp_app_desc.version 与 menuconfig CONFIG_MY_APP_VERSION 保持一致
set(_proj_ver "1.0.1")
foreach(_cfg IN ITEMS sdkconfig.defaults sdkconfig)
set(_cfg_path "${CMAKE_CURRENT_SOURCE_DIR}/${_cfg}")
if(EXISTS "${_cfg_path}")
file(STRINGS "${_cfg_path}" _ver_line REGEX "^CONFIG_MY_APP_VERSION=")
if(_ver_line)
string(REGEX REPLACE "^CONFIG_MY_APP_VERSION=\"(.*)\"$" "\\1" _proj_ver "${_ver_line}")
endif()
endif()
endforeach()
set(PROJECT_VER "${_proj_ver}")
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESPRCCar) project(ESPRCCar)
# Release 模式:强制全局静默,编译期删除所有 ESP_LOG (I/W/E)
# menuconfig 选择 Release 时自动生效,无需其他操作
if(CONFIG_ROBO_APP_FW_RELEASE)
idf_build_set_property(COMPILE_DEFINITIONS "LOG_LOCAL_LEVEL=ESP_LOG_NONE" APPEND)
endif()
...@@ -44,10 +44,9 @@ ...@@ -44,10 +44,9 @@
其中 `gpiotrol` 已拆分为“通用 PWM 底座 + 设备策略”: 其中 `gpiotrol` 已拆分为“通用 PWM 底座 + 设备策略”:
- `main/drivers/gpiotrol/rc_pwm_control.c`:统一 RC 车 PWM 控制(含 6 路 50Hz 初始化、AUX 角色选择、PID 接口、策略分发)。 - `main/drivers/gpiotrol/rc_pwm_control.c`:统一 RC 车 PWM 控制(含 6 路 50Hz 初始化、AUX 角色选择、PID 接口、策略分发)。
- `main/drivers/driver_manager.c`:统一初始化并绑定设备策略(业务层不直接碰具体驱动细节)。 - `main/drivers/driver_manager/`:统一初始化并绑定设备策略(业务层不直接碰具体驱动细节)。
- `main/drivers/gpiotrol/device_drive.h`:设备策略接口定义(`stop/control/shot`)。 - `main/drivers/gpiotrol/device_drive.h`:设备策略接口定义(`stop/control/shot`)。
- `main/drivers/gpiotrol/devices/device_1201.c`:1201 设备控制映射。 - `main/drivers/gpiotrol/devices/1101/``devices/1102/`:各车型控制映射。
- `main/drivers/gpiotrol/devices/device_1101.c`:1101 设备控制映射。
当前统一引脚约定(均为 50Hz): 当前统一引脚约定(均为 50Hz):
- 驱动芯片1:`IO10``IO21` - 驱动芯片1:`IO10``IO21`
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
# 安装与配网指南 # 安装与配网指南
...@@ -95,11 +95,10 @@ idf.py -p COM16 flash monitor ...@@ -95,11 +95,10 @@ idf.py -p COM16 flash monitor
## 4. 配网(SoftAP + 网页) ## 4. 配网(SoftAP + 网页)
三种模式在缺少必填 NVS 时,都会开启 **SoftAP 热点**(SSID 默认 `esp32-apconfig`,可在 menuconfig **`ROBITO WIFI SSID`** 修改)。手机连接热点后浏览器访问: 三种模式在缺少必填 NVS 时,都会开启 **SoftAP 热点**(SSID 默认 `esp32-apconfig`,可在 menuconfig **`ROBIOT WIFI SSID`** 修改)。
**http://192.168.4.1** - 连接热点后,系统应 **自动弹出**「需登录网络 / 认证」页(Captive Portal);固件对 `generate_204` 等检测 URL 返回 **302** 到配网页(**勿再返回 204**,否则 Android 认为已联网不弹窗)。
- 若未自动弹出:关移动数据,浏览器手动打开 **http://192.168.4.1**(设备 DNS 劫持任意域名到 `192.168.4.1`)。
(设备侧 DNS 会劫持域名,直接输 IP 即可。)
### 4.1 各模式配网页与必填项 ### 4.1 各模式配网页与必填项
...@@ -172,6 +171,7 @@ scripts\copy_firmware_release.bat ...@@ -172,6 +171,7 @@ scripts\copy_firmware_release.bat
|------|------| |------|------|
| UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` | | UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` |
| 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 | | 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 |
| 连热点不自动弹配网页 | 刷含 captive 302 修复的固件;关移动数据;仍不行则手动 **http://192.168.4.1** |
| `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` | | `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` |
| 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` | | 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` |
......
# 编译类型与 UART 模式配置指南 # 编译类型与 UART 模式配置指南
...@@ -76,38 +76,30 @@ idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" buil ...@@ -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` - `ROBO_APP_FW_RELEASE=y`
- 使用 `sdkconfig.defaults.release`(含 **`CONFIG_ESP_CONSOLE_NONE=y`** - 使用 `sdkconfig.defaults.release`(含 **`CONFIG_ESP_CONSOLE_NONE=y`**
- `APP_UART_MODE_DEBUG=n`(默认)
**行为:** **行为:**
- **不向串口输出** `ESP_LOG`(避免无人读取时阻塞 BLE) - **不向串口输出** `ESP_LOG`(避免无人读取时阻塞 BLE)
- `ESP_LOGW` / `ESP_LOGE` 在 BLE 已连接时经 **0xFFE3 Notify** 推送(`message_type` 4/5,见 `docs/Android端设备对接文档_v1.0.1.md` §3.5-B) - `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 - 周期心跳仍为 **0xFFE3**,约 3s,`message_type` 1
### 3.2 模式 B:UART0 转为普通通信串口 ### 3.2 说明
**配置:** - 现在不再提供 `APP_UART_MODE_DEBUG` 手动切换。
- `ROBO_APP_FW_RELEASE=y`(menuconfig 中「编译优化级别」选 Release) - 选择 Release 后即默认无控制台日志输出;`APP_DEBUG_UART_*` 对应 UART 可由 `uart_comm` 做普通收发。
- `APP_UART_MODE_DEBUG=y`(启用此选项) - 固件下载仍使用 UART0(bootrom 固定)。
**行为:**
- **UART0**`uart_comm` 接管做普通收发(GPIO43/44)
- `ESP_LOG` / `printf` 等日志输出不可用(没有日志输出)
- 代码中使用 `uart_comm.h` 接口进行数据传输
- 固件下载仍使用 UART0(bootrom 固定)
**完整配置步骤:** **完整配置步骤:**
```bash ```bash
idf.py menuconfig idf.py menuconfig
# 1. robot-esp32s3 → 编译优化级别 → Release # 1. robot-esp32s3 → 编译优化级别 → Release
# 2. robot-esp32s3 → 调试打印串口作为普通通信串口使用 → 启用 # 2. Component config → ESP System Settings → Channel for console output → None
# 3. Component config → ESP System Settings → Channel for console output → None
# 保存并退出 # 保存并退出
``` ```
...@@ -131,22 +123,16 @@ idf.py menuconfig ...@@ -131,22 +123,16 @@ idf.py menuconfig
| `BUILD_IS_DEBUG` | 1=Debug 构建,0=Release 构建 | | `BUILD_IS_DEBUG` | 1=Debug 构建,0=Release 构建 |
| `UART_MODE_COMMUNICATION` | 1=UART 作为通信串口,0=默认 | | `UART_MODE_COMMUNICATION` | 1=UART 作为通信串口,0=默认 |
| `UART_MODE_DEBUG` | 1=调试串口模式,0=通信模式 | | `UART_MODE_DEBUG` | 1=调试串口模式,0=通信模式 |
| `BUILD_SERIAL_LOG_ENABLED` | 1=可向 UART 打日志,0=Release 默认关闭 |
### 4.3 条件日志宏
无论何种模式,都可以使用统一的日志宏: ### 4.3 日志(直接用 ESP-IDF)
```c Release 不在 `build_config.h` 里再包 `LOG_*` 宏,由 **`sdkconfig`** 统一控制:
LOG_I(TAG, "信息日志"); // Info 级别
LOG_W(TAG, "警告日志"); // Warning 级别
LOG_E(TAG, "错误日志"); // Error 级别
LOG_D(TAG, "调试日志"); // Debug 级别
```
**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+通信模式下有效) ### 4.4 UART 通信 API(仅在 Release+通信模式下有效)
...@@ -200,20 +186,6 @@ void my_init(void) ...@@ -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. 构建输出对比 ## 6. 构建输出对比
...@@ -255,10 +227,9 @@ void my_function(void) ...@@ -255,10 +227,9 @@ void my_function(void)
### 7.2 UART 通信模式下的调试 ### 7.2 UART 通信模式下的调试
启用 `APP_UART_MODE_DEBUG` 后: - Release 下默认无法使用 `idf.py monitor` 查看日志(控制台关闭)。
- **无法使用 `idf.py monitor` 查看日志**(因为没有日志输出) - 烧录时仍然使用 UART0,请确保外部设备在烧录时不会干扰。
- **烧录时仍然使用 UART0**,请确保外部设备在烧录时不会干扰 - 建议开发阶段使用 Debug 验证,量产切 Release。
- 建议在开发阶段先用 **模式 A(调试打印)** 验证功能,再切换到 **模式 B(通信)**
### 7.3 推荐的开发流程 ### 7.3 推荐的开发流程
...@@ -275,6 +246,12 @@ void my_function(void) ...@@ -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. 故障排除 ## 8. 故障排除
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
| 文件 | 说明 | | 文件 | 说明 |
|------|------| |------|------|
| **`release/VERSION.txt`** | 人类可读:版本号、编译时间、OTA 文件大小与 SHA256 | | **`release/VERSION.txt`** | 人类可读:版本号、编译时间、OTA 文件大小与 SHA256 |
| **`release/ota_manifest.json`** | Android OTA 机器可读版本 JSON:版本、固件大小、SHA256 |
| **`release/manifest.json`** | 机器可读:同上 + 全部 factory 文件校验 | | **`release/manifest.json`** | 机器可读:同上 + 全部 factory 文件校验 |
| **`release/Android端设备对接文档_v<版本>.md`** | 与固件同版本的 Android 对接说明(WiFi/BLE/UART,脚本从 `docs/` 复制) | | **`release/Android端设备对接文档_v<版本>.md`** | 与固件同版本的 Android 对接说明(WiFi/BLE/UART,脚本从 `docs/` 复制) |
...@@ -25,6 +26,7 @@ ...@@ -25,6 +26,7 @@
| 路径 | 内容 | | 路径 | 内容 |
|------|------| |------|------|
| `release/ota/ESPRCCar.bin` | **BLE OTA / Android 升级**(仅应用镜像) | | `release/ota/ESPRCCar.bin` | **BLE OTA / Android 升级**(仅应用镜像) |
| `release/ota_manifest.json` | **Android OTA 版本 JSON**(与 `ESPRCCar.bin` 配套) |
| `release/factory/*.bin` | **量产烧录**(乐鑫 Flash Download Tools 加载) | | `release/factory/*.bin` | **量产烧录**(乐鑫 Flash Download Tools 加载) |
| `release/factory/flash_args.txt` | 烧录地址摘要(与 `build/flash_args` 一致) | | `release/factory/flash_args.txt` | 烧录地址摘要(与 `build/flash_args` 一致) |
...@@ -47,11 +49,11 @@ idf.py build ...@@ -47,11 +49,11 @@ idf.py build
scripts\copy_firmware_release.bat 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` - **OTA**:下载 **两个文件**`release/ota/ESPRCCar.bin` + `release/ota_manifest.json`
- **Android 联调**:同目录下的 **`Android端设备对接文档_v<版本>.md`** + `VERSION.txt` / `manifest.json` - **Android 联调**:同目录下的 **`Android端设备对接文档_v<版本>.md`** + `release/ota/ESPRCCar.bin` + `release/ota_manifest.json`
- **量产**:下载 `release/factory/` 全部 bin,用 Flash Download Tools 烧录。 - **量产**:下载 `release/factory/` 全部 bin,用 Flash Download Tools 烧录。
- **安装与配网**(环境、三种链路、热点填什么):见仓库 [`docs/安装与配网指南.md`](../docs/安装与配网指南.md) - **安装与配网**(环境、三种链路、热点填什么):见仓库 [`docs/安装与配网指南.md`](../docs/安装与配网指南.md)
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
ESPRCCar firmware release ESPRCCar firmware release
========================= =========================
version: 1.0.1 version: 1.0.2
project: ESPRCCar project: ESPRCCar
chip: esp32s3 chip: esp32s3
built_utc: 2026-05-19T07:53:23Z built_utc: 2026-06-11T08:29:54Z
built_local: 2026/05/19 ܶ 15:53:23.22 built_local: 2026/06/11 16:29:54.96
git_revision: (no git)
OTA: OTA:
path: firmware/release/ota/ESPRCCar.bin path: firmware/release/ota/ESPRCCar.bin
bytes: 1115760 bytes: 889552
sha256: 724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6 sha256: 065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977
Factory: firmware/release/factory/ Factory: firmware/release/factory/
tool: Espressif Flash Download Tools tool: Espressif Flash Download Tools
addrs: see factory/flash_args.txt 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 @@ ...@@ -4,17 +4,15 @@
"flash_mode": "dio", "flash_mode": "dio",
"flash_freq": "80m", "flash_freq": "80m",
"flash_size": "16MB", "flash_size": "16MB",
"version": "1.0.1", "version": "1.0.2",
"git_revision": "", "generated_utc": "2026-06-11T08:29:54Z",
"generated_utc": "2026-05-19T07:53:23Z",
"ota_image": "release/ota/ESPRCCar.bin", "ota_image": "release/ota/ESPRCCar.bin",
"factory_dir": "release/factory", "factory_dir": "release/factory",
"files": [ "files": [
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"}, {"name":"ESPRCCar.bin","bytes":889552,"sha256":"065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"},
{"name":"bootloader.bin","bytes":21056,"sha256":"740f1b3c78ca63acc0a5e3c788bf18f77585a28504e5d8722ac253991c6f1f7a"}, {"name":"bootloader.bin","bytes":13808,"sha256":"21a5becf42c59120c3c4322a04ca684f86a496dfb770575ebbe6d6ec739b7199"},
{"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"}, {"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":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"},
{"name":"storage.bin","bytes":327680,"sha256":"05be75d1159d4b3043e4b3847f7d8307b4a4955ae30c1baf93ce42eafb1308cf"} {"name":"storage.bin","bytes":327680,"sha256":"6d2f4c246d34ebf80e3d32ac4873aa84d29f68ee799214b123bdd998a8b2d3b2"}
] ]
} }
{
"project": "ESPRCCar",
"chip": "esp32s3",
"version": "1.0.2",
"generated_utc": "2026-06-11T08:29:54Z",
"image": "ESPRCCar.bin",
"bytes": 889552,
"sha256": "065afe0640decd02c377b43c57901f7484f293b0f44666fc71fbd441e4f7c977"
}
...@@ -5,11 +5,14 @@ set(SOURCES ...@@ -5,11 +5,14 @@ set(SOURCES
"device/device_model.c" "device/device_model.c"
"protocol/remote_control.c" "protocol/remote_control.c"
"protocol/heart_payload.c" "protocol/heart_payload.c"
"drivers/driver_manager.c" "protocol/ota_offer_protocol.c"
"protocol/ota_frame_protocol.c"
"protocol/ota_uart_tune.c"
"drivers/driver_manager/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c" "drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/devices/device_1201.c" "drivers/gpiotrol/devices/1101/device_1101.c"
"drivers/gpiotrol/devices/device_1101.c" "drivers/gpiotrol/devices/1102/device_1102.c"
"drivers/gpiotrol/devices/device_1102.c" "drivers/gpiotrol/devices/1102/device_1102_brake.c"
"drivers/gpiotrol/betteryread.c" "drivers/gpiotrol/betteryread.c"
"drivers/uart_comm/uart_comm.c" "drivers/uart_comm/uart_comm.c"
"app/app_run.c" "app/app_run.c"
...@@ -19,23 +22,27 @@ set(SOURCES ...@@ -19,23 +22,27 @@ set(SOURCES
"ota_binary_stream.c") "ota_binary_stream.c")
if(CONFIG_APP_LINK_BLE) 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) 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() else()
list(APPEND SOURCES "link_wifi/mqttconf_commun.c" "ota.c") list(APPEND SOURCES "link_wifi/mqttconf_commun.c" "ota.c")
endif() endif()
set(INCLUDE_DIRS set(INCLUDE_DIRS
"." "."
"link_common"
"core" "core"
"device" "device"
"provision" "provision"
"protocol" "protocol"
"app" "app"
"drivers" "drivers"
"drivers/driver_manager"
"drivers/gpiotrol" "drivers/gpiotrol"
"drivers/gpiotrol/devices" "drivers/gpiotrol/devices"
"drivers/gpiotrol/devices/1101"
"drivers/gpiotrol/devices/1102"
"drivers/uart_comm") "drivers/uart_comm")
if(CONFIG_APP_LINK_BLE) if(CONFIG_APP_LINK_BLE)
......
...@@ -8,7 +8,7 @@ config ROBIOT_WIFI_SSID ...@@ -8,7 +8,7 @@ config ROBIOT_WIFI_SSID
config ROBOIOT_MQTT_URL config ROBOIOT_MQTT_URL
string "MQTT URL" string "MQTT URL"
default "https://fcrs-api.yd-ss.com/device/getConfig?deviceNo=" default "0.0.0.0"
help help
MQTT Config URL MQTT Config URL
...@@ -48,13 +48,48 @@ config APP_DEBUG_UART_RX_GPIO ...@@ -48,13 +48,48 @@ config APP_DEBUG_UART_RX_GPIO
UART1 模式下使用 GPIO18(RX)。 UART1 模式下使用 GPIO18(RX)。
此项保留用于兼容性,请保持为 -1。 此项保留用于兼容性,请保持为 -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 config APP_UART_LINK_BAUDRATE
int "UART 链路模式波特率" int
default 115200 default 9600 if APP_UART_BAUD_9600
range 9600 921600 default 115200 if APP_UART_BAUD_115200
default 230400 if APP_UART_BAUD_230400
default 460800 if APP_UART_BAUD_460800
default 921600 if APP_UART_BAUD_921600
help
当前 UART 链路模式生效波特率(由上方档位自动生成)。
config APP_OTA_UART_MAX_CHUNK
int "OTA DATA 单帧最大载荷 (字节)"
depends on APP_LINK_UART
default 1024
range 240 1024
help help
UART 链路模式下的通信波特率。默认 115200。 UART OTA 每帧最多携带的固件字节数。越大往返越少、越快;
适用于 APP_LINK_UART 模式下的 UART1 通信 须与 Android OtaFrame 发送缓冲一致。推荐 1024(460800 下 6MB 约 1~3 分钟)
config APP_UART_LINK_TX_GPIO config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号" int "UART 链路 TX GPIO 编号"
...@@ -103,14 +138,46 @@ config APP_BLE_OTA ...@@ -103,14 +138,46 @@ config APP_BLE_OTA
depends on APP_LINK_BLE depends on APP_LINK_BLE
default y default y
help help
允许手机通过 0xFFE2 写入固件分包升级;每步成功后可通过 0xFFE4 Notify 收到 JSON 应答摘要。 允许手机在回复 message_type=0 后,通过 0xFFE2 写入固件分包升级;
关闭后向 0xFFE2 写入将返回 Write Not Permitted;0xFFE4 仍可发现,但无 OTA 过程推送。 固件数据经 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 choice APP_PWM_IO15_ROLE
prompt "GPIO15 PWM 角色" prompt "GPIO15 PWM 角色"
default APP_PWM_IO15_SERVO default APP_PWM_IO15_ESC
help help
选择 GPIO15 在 50Hz PWM 下的用途。 选择 GPIO15 在 50Hz PWM 下的用途。
默认:电调 ESC(1101 外接油门 / 备用电调口)。
config APP_PWM_IO15_SERVO config APP_PWM_IO15_SERVO
bool "舵机(初始化 90 度 / 1500us)" bool "舵机(初始化 90 度 / 1500us)"
...@@ -121,9 +188,10 @@ endchoice ...@@ -121,9 +188,10 @@ endchoice
choice APP_PWM_IO16_ROLE choice APP_PWM_IO16_ROLE
prompt "GPIO16 PWM 角色" prompt "GPIO16 PWM 角色"
default APP_PWM_IO16_ESC default APP_PWM_IO16_SERVO
help help
选择 GPIO16 在 50Hz PWM 下的用途。 选择 GPIO16 在 50Hz PWM 下的用途。
默认:舵机(1102 转向固定使用 IO16,须为 SERVO)。
config APP_PWM_IO16_SERVO config APP_PWM_IO16_SERVO
bool "舵机(初始化 90 度 / 1500us)" bool "舵机(初始化 90 度 / 1500us)"
...@@ -136,30 +204,39 @@ choice ROBO_APP_FW_BUILD ...@@ -136,30 +204,39 @@ choice ROBO_APP_FW_BUILD
prompt "编译优化级别(固件 Debug/Release)" prompt "编译优化级别(固件 Debug/Release)"
default ROBO_APP_FW_DEBUG default ROBO_APP_FW_DEBUG
help help
注意:勿使用名称 APP_BUILD_TYPE,与 ESP-IDF 全局 Kconfig 冲突。 首次切换 Release 时,若日志级别未变,请删除 sdkconfig 后重新配置。
Debug:-Og;Release:-Os(另见 sdkconfig.defaults.release 或 menuconfig 编译器选项)。 Debug:-Og 优化,UART0 输出所有日志。
Release:-Os 优化,UART0 完全静默,日志级别 WARN,代码体积最小。
config ROBO_APP_FW_DEBUG 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 help
优化级别 -Og,保留调试符号,适合开发和调试。 开发调试版本:
- 优化级别 -Og,保留调试符号
- 日志级别 Info,所有 ESP_LOGI 输出到 UART0
- UART0 作为控制台,printf 可用
config ROBO_APP_FW_RELEASE 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 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 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 endmenu
...@@ -230,7 +230,7 @@ static void run_ble_mode(void) ...@@ -230,7 +230,7 @@ static void run_ble_mode(void)
button_monitor_task_init(); button_monitor_task_init();
provision_wifi_shutdown();
if (link_ble_start() != ESP_OK) { if (link_ble_start() != ESP_OK) {
...@@ -258,7 +258,7 @@ static void run_uart_mode(void) ...@@ -258,7 +258,7 @@ static void run_uart_mode(void)
if (strlen(dev) == 0) { if (strlen(dev) == 0) {
ESP_LOGI(tag_s, "UART 模式: 需配置 device_id,启动配网热点"); ESP_LOGI(tag_s, "UART 模式: 未选择设备型号,启动配网热点");
nowifidata_start_config_web(); nowifidata_start_config_web();
...@@ -276,7 +276,7 @@ static void run_uart_mode(void) ...@@ -276,7 +276,7 @@ static void run_uart_mode(void)
button_monitor_task_init(); button_monitor_task_init();
provision_wifi_shutdown();
/* 启动 UART 链路通信 */ /* 启动 UART 链路通信 */
...@@ -338,23 +338,7 @@ void app_run(void) ...@@ -338,23 +338,7 @@ void app_run(void)
#if BUILD_SERIAL_LOG_ENABLED #if BUILD_IS_DEBUG
#if BUILD_IS_RELEASE
ESP_LOGI(tag_s, "构建类型: Release (Os优化)");
#if UART_MODE_COMMUNICATION
ESP_LOGI(tag_s, "UART模式: 普通通信串口");
#else
ESP_LOGI(tag_s, "UART模式: 调试打印");
#endif
#else
ESP_LOGI(tag_s, "构建类型: Debug (Og优化)"); ESP_LOGI(tag_s, "构建类型: Debug (Og优化)");
...@@ -362,8 +346,6 @@ void app_run(void) ...@@ -362,8 +346,6 @@ void app_run(void)
#endif #endif
#endif
#if CONFIG_APP_LINK_WIFI #if CONFIG_APP_LINK_WIFI
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
* *
* 编译条件: * 编译条件:
* - CONFIG_ROBO_APP_FW_RELEASE=y * - CONFIG_ROBO_APP_FW_RELEASE=y
* - CONFIG_APP_UART_MODE_DEBUG=y
* *
* 使用场景: * 使用场景:
* - 通过板载 RX1/TX1(UART1)等与外部 MCU 透明传输 * - 通过板载 RX1/TX1(UART1)等与外部 MCU 透明传输
......
/* /*
* 构建配置检测头文件 * 构建配置检测头文件
* 用于在代码中区分 Debug/Release 构建和串口使用模式 * 用于在代码中区分 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 #ifndef BUILD_CONFIG_H
...@@ -23,64 +26,30 @@ ...@@ -23,64 +26,30 @@
#endif #endif
/* ======================== /* ========================
* 串口 / 日志输出 * 串口模式(精简:Release 走通信,Debug 走调试)
* ======================== */ * ======================== */
#if BUILD_IS_RELEASE
/*
* Release:关闭串口调试打印(sdkconfig 建议 CONFIG_ESP_CONSOLE_NONE),
* W/E 经 BLE 0xFFE3 Notify 推送(见 link_ble.c)。
*
* Release + APP_UART_MODE_DEBUG:指定 UART 给 uart_comm,同样无 ESP_LOG。
*/
#if defined(CONFIG_APP_UART_MODE_DEBUG) && defined(CONFIG_ROBO_APP_FW_RELEASE)
#define UART_MODE_COMMUNICATION 1 #define UART_MODE_COMMUNICATION 1
#define UART_MODE_DEBUG 0 #define UART_MODE_DEBUG 0
#define BUILD_SERIAL_LOG_ENABLED 0
#else #else
#define UART_MODE_COMMUNICATION 0 #define UART_MODE_COMMUNICATION 0
#define UART_MODE_DEBUG 1 #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 #endif
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
static inline const char* build_get_type(void) { static inline const char *build_get_type(void)
{
return BUILD_IS_RELEASE ? "Release" : "Debug"; 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"; return UART_MODE_COMMUNICATION ? "Communication" : "Debug";
} }
static inline int build_can_log(void) {
return BUILD_SERIAL_LOG_ENABLED;
}
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
......
...@@ -66,6 +66,18 @@ static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = { ...@@ -66,6 +66,18 @@ static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = {
.priority = 5, .priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY, .core_id = APP_TASK_CORE_NO_AFFINITY,
}, },
[APP_TASK_RC_WATCHDOG] = {
.name = "rc_wdog",
.stack_depth = 3072,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_DEV1102_BRAKE] = {
.name = "1102_brk",
.stack_depth = 3072,
.priority = 6,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
}; };
static esp_err_t app_task_create(const char *name, static esp_err_t app_task_create(const char *name,
......
...@@ -17,6 +17,7 @@ typedef enum { ...@@ -17,6 +17,7 @@ typedef enum {
APP_TASK_MQTT_INIT, APP_TASK_MQTT_INIT,
APP_TASK_UART_COMM_EXAMPLE, APP_TASK_UART_COMM_EXAMPLE,
APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */ APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */
APP_TASK_DEV1102_BRAKE, /* 1102 板载驱动 PID 制动 */
APP_TASK_COUNT, APP_TASK_COUNT,
} app_task_id_t; } app_task_id_t;
......
...@@ -6,17 +6,30 @@ static const char *tag_s = "DEVICE_MODEL"; ...@@ -6,17 +6,30 @@ static const char *tag_s = "DEVICE_MODEL";
device_model_t device_model_from_full_id(const char *device_id) 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)) { if (!device_id || device_id[0] == '\0') {
ESP_LOGW(tag_s, "device_id 为空或长度不足,按 1101"); ESP_LOGW(tag_s, "device_id 为空,按 1101");
return DEVICE_MODEL_1101; return DEVICE_MODEL_1101;
} }
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START; size_t len = strlen(device_id);
if (len == DEVICE_MODEL_SLICE_CHAR_LEN) {
if (strncmp(device_id, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1101");
return DEVICE_MODEL_1101;
}
if (strncmp(device_id, "1102", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "短型号 1102");
return DEVICE_MODEL_1102;
}
}
if (strncmp(slice, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) { if (len < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN)) {
ESP_LOGI(tag_s, "片段 [3..6]=1201 → 型号 1201"); ESP_LOGW(tag_s, "device_id 长度不足,按 1101");
return DEVICE_MODEL_1201; return DEVICE_MODEL_1101;
} }
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START;
if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) { if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101"); ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101");
return DEVICE_MODEL_1101; return DEVICE_MODEL_1101;
......
...@@ -5,12 +5,11 @@ ...@@ -5,12 +5,11 @@
/** /**
* 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。 * 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。
* 规则:取 device_id 第 3~第 6 个字符(从第 1 个字符起算,共 4 字符), * 规则:完整设备号如 CN110200000001(无 app2dev/ 前缀);
* 即 C 字符串下标 [2..5],与 "1201" / "1101" 比较 * 取第 3~第 6 位(1 起算)共 4 字符 → C 下标 [2..5],如 "1102"
*/ */
typedef enum { typedef enum {
DEVICE_MODEL_1201 = 0, DEVICE_MODEL_1101 = 0,
DEVICE_MODEL_1101,
DEVICE_MODEL_1102, DEVICE_MODEL_1102,
} device_model_t; } device_model_t;
......
#ifndef DEVICE_NVS_H #ifndef DEVICE_NVS_H
#define DEVICE_NVS_H #define DEVICE_NVS_H
#include <string.h>
/** NVS 命名空间(与网页配网写入一致) */ /** NVS 命名空间(与网页配网写入一致) */
#define DEVICE_CFG_NVS_NAMESPACE "storage" #define DEVICE_CFG_NVS_NAMESPACE "storage"
...@@ -9,4 +11,13 @@ ...@@ -9,4 +11,13 @@
#define DEVICE_CFG_KEY_DEVICE_ID "device_id" #define DEVICE_CFG_KEY_DEVICE_ID "device_id"
#define DEVICE_CFG_KEY_BLE_ADV_NAME "ble_adv_name" #define DEVICE_CFG_KEY_BLE_ADV_NAME "ble_adv_name"
/** 去掉历史 MQTT 前缀,输出纯设备号(如 CN110200000001) */
static inline const char *device_id_plain(const char *device_id)
{
if (device_id != NULL && strncmp(device_id, "app2dev/", 8) == 0) {
return device_id + 8;
}
return (device_id != NULL) ? device_id : "";
}
#endif #endif
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
typedef struct device_drive_ops_s { typedef struct device_drive_ops_s {
const char *name; const char *name;
void (*stop)(void); 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); void (*shot)(int pin, int val);
} device_drive_ops_t; } device_drive_ops_t;
......
#include "device_1101.h"
#include "rc_pwm_control.h"
/*
* 1101: throttle on IO15 (ESC); steering on IO16 (SERVO, via rc_pwm_set_steering_angle_deg).
* No onboard driver IC (IO10/21/11/12 unused). Default menuconfig: IO15=ESC, IO16=SERVO.
*/
#define DEV1101_VAL_MAX 200
#define DEV1101_DRIVE_START_TH 50
#define DEV1101_ESC_NEUTRAL_PCT 50U /* 1500us 中位 */
#define DEV1101_STEER_VAL_LO 45
#define DEV1101_STEER_VAL_HI 70
#define DEV1101_STEER_VAL_SPAN (69 - DEV1101_STEER_VAL_LO)
#define DEV1101_STEER_CENTER_ANG 90
#define DEV1101_STEER_DELTA_ANG 30
#define DEV1101_STEER_START_DELTA 12
#define DEV1101_STEER_LEFT_MAX (DEV1101_STEER_CENTER_ANG + DEV1101_STEER_DELTA_ANG)
#define DEV1101_STEER_RIGHT_MAX (DEV1101_STEER_CENTER_ANG - DEV1101_STEER_DELTA_ANG)
static int clamp_int(int v, int lo, int hi)
{
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static uint32_t esc_forward_percent_from_val(int val)
{
int v = clamp_int(val, 0, DEV1101_VAL_MAX);
if (v <= DEV1101_DRIVE_START_TH) {
return DEV1101_ESC_NEUTRAL_PCT;
}
/* 50~200 → 中位~满前进(50%~100% → 1500~2000us) */
return 50U + (uint32_t)(v - DEV1101_DRIVE_START_TH) * 50U /
(uint32_t)(DEV1101_VAL_MAX - DEV1101_DRIVE_START_TH);
}
static uint32_t esc_backward_percent_from_val(int val)
{
int v = clamp_int(val, 0, DEV1101_VAL_MAX);
if (v <= DEV1101_DRIVE_START_TH) {
return DEV1101_ESC_NEUTRAL_PCT;
}
/* 50~200 → 中位~满后退(50%~0% → 1500~1000us) */
return 50U - (uint32_t)(v - DEV1101_DRIVE_START_TH) * 50U /
(uint32_t)(DEV1101_VAL_MAX - DEV1101_DRIVE_START_TH);
}
static void device_1101_stop(void)
{
rc_pwm_set_esc_neutral();
rc_pwm_set_steering_angle_deg(DEV1101_STEER_CENTER_ANG);
}
static void device_1101_drive_forward(int speed_val)
{
rc_pwm_set_dual_esc_percent(esc_forward_percent_from_val(speed_val));
}
static void device_1101_drive_backward(int speed_val)
{
rc_pwm_set_dual_esc_percent(esc_backward_percent_from_val(speed_val));
}
static void device_1101_steer_left(int speed_val)
{
if (speed_val < DEV1101_STEER_VAL_LO) {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_CENTER_ANG);
} else if (speed_val < DEV1101_STEER_VAL_HI) {
uint32_t ang = (uint32_t)(DEV1101_STEER_CENTER_ANG + DEV1101_STEER_START_DELTA +
(uint32_t)(speed_val - DEV1101_STEER_VAL_LO) *
(DEV1101_STEER_DELTA_ANG - DEV1101_STEER_START_DELTA) /
(uint32_t)DEV1101_STEER_VAL_SPAN);
rc_pwm_set_steering_angle_deg(ang);
} else {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_LEFT_MAX);
}
}
static void device_1101_steer_right(int speed_val)
{
if (speed_val < DEV1101_STEER_VAL_LO) {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_CENTER_ANG);
} else if (speed_val < DEV1101_STEER_VAL_HI) {
uint32_t ang = (uint32_t)(DEV1101_STEER_CENTER_ANG - DEV1101_STEER_START_DELTA -
(uint32_t)(speed_val - DEV1101_STEER_VAL_LO) *
(DEV1101_STEER_DELTA_ANG - DEV1101_STEER_START_DELTA) /
(uint32_t)DEV1101_STEER_VAL_SPAN);
rc_pwm_set_steering_angle_deg(ang);
} else {
rc_pwm_set_steering_angle_deg(DEV1101_STEER_RIGHT_MAX);
}
}
static void device_1101_control(int mode, int speed_val, int steer_val)
{
switch (mode) {
case 1:
device_1101_drive_forward(speed_val);
break;
case 2:
device_1101_drive_backward(speed_val);
break;
case 3:
device_1101_steer_left(speed_val);
break;
case 4:
device_1101_steer_right(speed_val);
break;
case 5:
device_1101_drive_forward(speed_val);
device_1101_steer_left(steer_val);
break;
case 6:
device_1101_drive_forward(speed_val);
device_1101_steer_right(steer_val);
break;
case 7:
device_1101_drive_backward(speed_val);
device_1101_steer_left(steer_val);
break;
case 8:
device_1101_drive_backward(speed_val);
device_1101_steer_right(steer_val);
break;
default:
break;
}
}
static void device_1101_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1101",
.stop = device_1101_stop,
.control = device_1101_control,
.shot = device_1101_shot,
};
const device_drive_ops_t *device_1101_get_ops(void)
{
return &ops_s;
}
#include "device_1102.h" #include "device_1102.h"
#include "device_1102_brake.h"
#include "rc_pwm_control.h" #include "rc_pwm_control.h"
#define DEV1102_VAL_MAX 200 #define DEV1102_VAL_MAX 200
#define DEV1102_DRIVE_START_TH 50 #define DEV1102_DRIVE_STOP_TH 50
#define DEV1102_DRIVE_VAL_MIN 51
#define DEV1102_DRIVE_PWM_MIN 28U
#define DEV1102_DRIVE_PWM_MAX 100U
#define DEV1102_DRIVE_VAL_SPAN (DEV1102_VAL_MAX - DEV1102_DRIVE_VAL_MIN)
/* 转向:App 侧 val 在 [47,62] 线性对应角度;中位 90°,左右各 ±30° */ /* 转向:App 侧 val 在 [47,62] 线性对应角度;中位 90°,左右各 ±30° */
#define DEV1102_STEER_VAL_LO 47 #define DEV1102_STEER_VAL_LO 47
#define DEV1102_STEER_VAL_HI 62 #define DEV1102_STEER_VAL_HI 62
...@@ -12,6 +17,9 @@ ...@@ -12,6 +17,9 @@
#define DEV1102_STEER_RIGHT_MAX (DEV1102_STEER_CENTER_ANG - DEV1102_STEER_DELTA_ANG) #define DEV1102_STEER_RIGHT_MAX (DEV1102_STEER_CENTER_ANG - DEV1102_STEER_DELTA_ANG)
#define DEV1102_STEER_VAL_SPAN (DEV1102_STEER_VAL_HI - DEV1102_STEER_VAL_LO) #define DEV1102_STEER_VAL_SPAN (DEV1102_STEER_VAL_HI - DEV1102_STEER_VAL_LO)
static uint32_t s_last_drive_pct;
static bool s_brake_inited;
static int clamp_int(int v, int lo, int hi) static int clamp_int(int v, int lo, int hi)
{ {
if (v < lo) { if (v < lo) {
...@@ -23,15 +31,34 @@ static int clamp_int(int v, int lo, int hi) ...@@ -23,15 +31,34 @@ static int clamp_int(int v, int lo, int hi)
return v; return v;
} }
static void device_1102_ensure_brake_task(void)
{
if (!s_brake_inited) {
if (device_1102_brake_init() == ESP_OK) {
s_brake_inited = true;
}
}
}
static uint32_t drive_percent_from_val_200(int val) static uint32_t drive_percent_from_val_200(int val)
{ {
int v = clamp_int(val, 0, DEV1102_VAL_MAX); int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_DRIVE_START_TH) { if (v <= DEV1102_DRIVE_STOP_TH) {
return 0; return 0;
} }
/* 50~200 线性映射到 0~100% */ if (v >= DEV1102_VAL_MAX) {
return (uint32_t)(((v - DEV1102_DRIVE_START_TH) * 100) / return DEV1102_DRIVE_PWM_MAX;
(DEV1102_VAL_MAX - DEV1102_DRIVE_START_TH)); }
/* 51~200 线性映射到 28%~100% 驱动占空比 */
return DEV1102_DRIVE_PWM_MIN +
(uint32_t)(v - DEV1102_DRIVE_VAL_MIN) * (DEV1102_DRIVE_PWM_MAX - DEV1102_DRIVE_PWM_MIN) /
(uint32_t)DEV1102_DRIVE_VAL_SPAN;
}
static void device_1102_request_brake(void)
{
device_1102_ensure_brake_task();
device_1102_brake_request(s_last_drive_pct);
} }
static uint32_t steer_left_angle_from_val(int val) static uint32_t steer_left_angle_from_val(int val)
...@@ -64,36 +91,75 @@ static uint32_t steer_right_angle_from_val(int val) ...@@ -64,36 +91,75 @@ static uint32_t steer_right_angle_from_val(int val)
static void device_1102_stop(void) static void device_1102_stop(void)
{ {
/* 1102: 10 前进,21 后退;全部拉低并将 IO16 回中 */ device_1102_request_brake();
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
} }
static void device_1102_control(int mode, int val) static void device_1102_drive_forward(uint32_t pct)
{ {
int v = clamp_int(val, 0, DEV1102_VAL_MAX); 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);
}
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) { switch (mode) {
case 1: { /* 前进:IO10 */ case 1: {
uint32_t pct = drive_percent_from_val_200(v); uint32_t pct = drive_percent_from_val_200(speed);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0); device_1102_drive_forward(pct);
break; break;
} }
case 2: { /* 后退:IO21 */ case 2: {
uint32_t pct = drive_percent_from_val_200(v); uint32_t pct = drive_percent_from_val_200(speed);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, DEV1102_STEER_CENTER_ANG);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct); device_1102_drive_backward(pct);
break; break;
} }
case 3: { /* 左转:IO16 舵机 */ case 3:
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(v)); rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(speed));
break; break;
} case 4:
case 4: { /* 右转:IO16 舵机 */ rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(speed));
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(v)); 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; break;
}
default: default:
break; break;
} }
...@@ -114,5 +180,6 @@ static const device_drive_ops_t ops_s = { ...@@ -114,5 +180,6 @@ static const device_drive_ops_t ops_s = {
const device_drive_ops_t *device_1102_get_ops(void) const device_drive_ops_t *device_1102_get_ops(void)
{ {
device_1102_ensure_brake_task();
return &ops_s; return &ops_s;
} }
#include "device_1102_brake.h"
#include "core/task_manager.h"
#include "rc_pwm_control.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <stdbool.h>
static const char *tag_s = "DEV1102_BRK";
#define BRAKE_QUEUE_LEN 1
#define BRAKE_TICK_MS 6U
#define BRAKE_STOP_SPEED_TH 3.0f
#define BRAKE_DRIVE_PWM_MIN 28U
#define BRAKE_SPEED_LOW_TH 45U
#define BRAKE_SPEED_HIGH_TH 70U
typedef struct {
float est_speed;
float integral;
float prev_error;
} dev1102_brake_pid_t;
typedef struct {
uint32_t kick_duty_min;
uint32_t kick_duty_max;
uint32_t kick_ms_min;
uint32_t kick_ms_max;
float duty_max;
float duty_floor_ratio;
float pid_kp;
float pid_ki;
float pid_kd;
uint32_t max_loop_ms;
} dev1102_brake_profile_t;
static QueueHandle_t s_brake_q;
static TaskHandle_t s_brake_task;
static volatile bool s_abort;
static void device_1102_brake_both(uint32_t duty_pct)
{
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, duty_pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, duty_pct);
}
static float dev1102_clampf(float v, float lo, float hi)
{
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
static void dev1102_brake_profile(uint32_t start_pct, dev1102_brake_profile_t *out)
{
if (start_pct <= BRAKE_SPEED_LOW_TH) {
/* 低速:强抱闸、短周期,尽快停住 */
*out = (dev1102_brake_profile_t){
.kick_duty_min = 72U,
.kick_duty_max = 88U,
.kick_ms_min = 35U,
.kick_ms_max = 50U,
.duty_max = 85.0f,
.duty_floor_ratio = 0.72f,
.pid_kp = 1.05f,
.pid_ki = 0.22f,
.pid_kd = 0.12f,
.max_loop_ms = 220U,
};
return;
}
if (start_pct <= BRAKE_SPEED_HIGH_TH) {
*out = (dev1102_brake_profile_t){
.kick_duty_min = 62U,
.kick_duty_max = 78U,
.kick_ms_min = 45U,
.kick_ms_max = 65U,
.duty_max = 78.0f,
.duty_floor_ratio = 0.62f,
.pid_kp = 0.92f,
.pid_ki = 0.18f,
.pid_kd = 0.10f,
.max_loop_ms = 320U,
};
return;
}
/* 高速:抱闸略柔和但 PID 持续施压,避免长距离滑行 */
*out = (dev1102_brake_profile_t){
.kick_duty_min = 58U,
.kick_duty_max = 76U,
.kick_ms_min = 60U,
.kick_ms_max = 95U,
.duty_max = 72.0f,
.duty_floor_ratio = 0.50f,
.pid_kp = 0.78f,
.pid_ki = 0.14f,
.pid_kd = 0.08f,
.max_loop_ms = 450U,
};
}
static uint32_t dev1102_brake_kick_duty(uint32_t start_pct, const dev1102_brake_profile_t *prof)
{
if (start_pct == 0U) {
return 55U;
}
if (start_pct <= BRAKE_DRIVE_PWM_MIN) {
return prof->kick_duty_min;
}
return prof->kick_duty_min +
(start_pct - BRAKE_DRIVE_PWM_MIN) * (prof->kick_duty_max - prof->kick_duty_min) /
(100U - BRAKE_DRIVE_PWM_MIN);
}
static uint32_t dev1102_brake_kick_ms(uint32_t start_pct, const dev1102_brake_profile_t *prof)
{
if (start_pct == 0U) {
return 45U;
}
if (start_pct <= BRAKE_DRIVE_PWM_MIN) {
return prof->kick_ms_min;
}
return prof->kick_ms_min +
(start_pct - BRAKE_DRIVE_PWM_MIN) * (prof->kick_ms_max - prof->kick_ms_min) /
(100U - BRAKE_DRIVE_PWM_MIN);
}
static float dev1102_brake_pid_step(dev1102_brake_pid_t *pid,
float target,
float dt_s,
const dev1102_brake_profile_t *prof)
{
/* 剩余速度越大,制动输出越大(此前符号反了导致 PID 阶段几乎不刹车) */
const float error = pid->est_speed - target;
pid->integral += error * dt_s;
pid->integral = dev1102_clampf(pid->integral, -30.0f, 30.0f);
const float derivative = (error - pid->prev_error) / dt_s;
pid->prev_error = error;
float out = prof->pid_kp * error + prof->pid_ki * pid->integral + prof->pid_kd * derivative;
const float floor = pid->est_speed * prof->duty_floor_ratio;
if (out < floor) {
out = floor;
}
return dev1102_clampf(out, 0.0f, prof->duty_max);
}
static void dev1102_brake_model_step(dev1102_brake_pid_t *pid, float brake_duty_pct, float dt_s)
{
const float b = brake_duty_pct / 100.0f;
const float decel = (150.0f * b + 6.0f * (pid->est_speed / 100.0f)) * dt_s;
pid->est_speed -= decel;
if (pid->est_speed < 0.0f) {
pid->est_speed = 0.0f;
}
}
static void device_1102_brake_run(uint32_t start_pct)
{
dev1102_brake_profile_t prof;
dev1102_brake_profile(start_pct, &prof);
const uint32_t kick_duty = dev1102_brake_kick_duty(start_pct, &prof);
const uint32_t kick_ms = dev1102_brake_kick_ms(start_pct, &prof);
const float dt_s = (float)BRAKE_TICK_MS / 1000.0f;
dev1102_brake_pid_t pid = {
.est_speed = (float)(start_pct == 0U ? 40U : start_pct),
.integral = 0.0f,
.prev_error = 0.0f,
};
ESP_LOGW(tag_s, "brake run start=%u kick=%u%% %ums",
(unsigned)start_pct, (unsigned)kick_duty, (unsigned)kick_ms);
device_1102_brake_both(kick_duty);
uint32_t kick_elapsed = 0U;
while (kick_elapsed < kick_ms) {
if (s_abort) {
return;
}
dev1102_brake_model_step(&pid, (float)kick_duty, dt_s);
vTaskDelay(pdMS_TO_TICKS(BRAKE_TICK_MS));
kick_elapsed += BRAKE_TICK_MS;
}
const TickType_t loop_start = xTaskGetTickCount();
while (pid.est_speed > BRAKE_STOP_SPEED_TH) {
if (s_abort) {
return;
}
if ((xTaskGetTickCount() - loop_start) > pdMS_TO_TICKS(prof.max_loop_ms)) {
ESP_LOGW(tag_s, "brake timeout est=%.1f start=%u", pid.est_speed, (unsigned)start_pct);
device_1102_brake_both((uint32_t)(prof.duty_max + 0.5f));
vTaskDelay(pdMS_TO_TICKS(30U));
break;
}
const float brake_duty = dev1102_brake_pid_step(&pid, 0.0f, dt_s, &prof);
device_1102_brake_both((uint32_t)(brake_duty + 0.5f));
dev1102_brake_model_step(&pid, brake_duty, dt_s);
vTaskDelay(pdMS_TO_TICKS(BRAKE_TICK_MS));
}
device_1102_brake_both(0U);
ESP_LOGI(tag_s, "brake done start=%u", (unsigned)start_pct);
}
static void device_1102_brake_task(void *param)
{
(void)param;
uint32_t start_pct = 0U;
ESP_LOGI(tag_s, "brake task running");
for (;;) {
if (xQueueReceive(s_brake_q, &start_pct, portMAX_DELAY) != pdTRUE) {
continue;
}
s_abort = false;
device_1102_brake_run(start_pct);
}
}
esp_err_t device_1102_brake_init(void)
{
if (s_brake_q != NULL) {
return ESP_OK;
}
s_brake_q = xQueueCreate(BRAKE_QUEUE_LEN, sizeof(uint32_t));
if (s_brake_q == NULL) {
ESP_LOGE(tag_s, "queue create fail");
return ESP_ERR_NO_MEM;
}
esp_err_t err = app_task_start(APP_TASK_DEV1102_BRAKE, device_1102_brake_task, NULL, &s_brake_task);
if (err != ESP_OK) {
vQueueDelete(s_brake_q);
s_brake_q = NULL;
ESP_LOGE(tag_s, "task start fail err=%d", (int)err);
return err;
}
ESP_LOGI(tag_s, "brake task ready name=1102_brk handle=%p", (void *)s_brake_task);
return ESP_OK;
}
void device_1102_brake_request(uint32_t start_speed_pct)
{
if (start_speed_pct > 100U) {
start_speed_pct = 100U;
}
dev1102_brake_profile_t prof;
dev1102_brake_profile(start_speed_pct, &prof);
const uint32_t kick_duty = dev1102_brake_kick_duty(start_speed_pct, &prof);
device_1102_brake_both(kick_duty);
if (s_brake_q == NULL) {
ESP_LOGW(tag_s, "brake request without task kick=%u%%", (unsigned)kick_duty);
return;
}
s_abort = true;
(void)xQueueOverwrite(s_brake_q, &start_speed_pct);
}
void device_1102_brake_abort(void)
{
s_abort = true;
device_1102_brake_both(0U);
}
bool device_1102_brake_task_ready(void)
{
return s_brake_task != NULL;
}
#ifndef DEVICE_1102_BRAKE_H
#define DEVICE_1102_BRAKE_H
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/**
* 1102 板载驱动制动:独立 FreeRTOS 任务 + PID(开环速度估计作反馈)。
* 无编码器时用「上次油门 + 制动模型」估计车速;停车时先同步抱闸再 PID 收尾。
*/
esp_err_t device_1102_brake_init(void);
/** 异步请求制动;start_speed_pct 为停车前驱动占空比 0~100 */
void device_1102_brake_request(uint32_t start_speed_pct);
/** 新行驶指令时中止制动(立即松闸) */
void device_1102_brake_abort(void);
/** 制动任务是否已创建(调试/自检) */
bool device_1102_brake_task_ready(void);
#endif
#include "device_1101.h"
#include "rc_pwm_control.h"
static void device_1101_stop(void)
{
rc_pwm_stop_all_drive_outputs();
}
/*
* 当前 1101 先复用 1201 的控制映射,后续若协议或硬件差异扩大,
* 只需在本文件内调整映射,不影响上层协议处理。
*/
static void device_1101_control(int mode, int val)
{
if (mode == 1) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1101_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1101",
.stop = device_1101_stop,
.control = device_1101_control,
.shot = device_1101_shot,
};
const device_drive_ops_t *device_1101_get_ops(void)
{
return &ops_s;
}
#include "device_1201.h"
#include "rc_pwm_control.h"
static void device_1201_stop(void)
{
rc_pwm_stop_all_drive_outputs();
}
static void device_1201_control(int mode, int val)
{
if (mode == 1) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, val / 2 - 10);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
}
} else if (mode == 2) {
if (val < 50) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, 0);
} else {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV2_A, val / 2 - 10);
}
} else if (mode == 3) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(50 + val + 7);
} else {
rc_pwm_set_steering_angle_deg(135);
}
} else if (mode == 4) {
if (val < 45) {
rc_pwm_set_steering_angle_deg(90);
} else if (val < 70) {
rc_pwm_set_steering_angle_deg(130 - val - 7);
} else {
rc_pwm_set_steering_angle_deg(45);
}
}
}
static void device_1201_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1201",
.stop = device_1201_stop,
.control = device_1201_control,
.shot = device_1201_shot,
};
const device_drive_ops_t *device_1201_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1201_H
#define DEVICE_1201_H
#include "device_drive.h"
const device_drive_ops_t *device_1201_get_ops(void);
#endif
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
#include "device_drive.h" #include "device_drive.h"
#include "device_model.h" #include "device_model.h"
#include "devices/device_1101.h" #include "device_1101.h"
#include "devices/device_1102.h" #include "device_1102.h"
#include "devices/device_1201.h" #include "device_1102_brake.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include <stdbool.h> #include <stdbool.h>
...@@ -159,7 +161,7 @@ esp_err_t rc_pwm_control_init(void) ...@@ -159,7 +161,7 @@ esp_err_t rc_pwm_control_init(void)
} }
if (!drive_ops_s) { if (!drive_ops_s) {
drive_ops_s = device_1201_get_ops(); drive_ops_s = device_1101_get_ops();
} }
ESP_LOGI(tag_s, "PWM init done: D1(10/21) D2(11/12) AUX(15:%s 16:%s) 50Hz profile=%s", ESP_LOGI(tag_s, "PWM init done: D1(10/21) D2(11/12) AUX(15:%s 16:%s) 50Hz profile=%s",
rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO ? "servo" : "esc", rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO ? "servo" : "esc",
...@@ -175,11 +177,13 @@ void rc_pwm_control_set_drive_from_device_id(const char *device_id) ...@@ -175,11 +177,13 @@ void rc_pwm_control_set_drive_from_device_id(const char *device_id)
drive_ops_s = device_1101_get_ops(); drive_ops_s = device_1101_get_ops();
} else if (device_model_is_1102(m)) { } else if (device_model_is_1102(m)) {
drive_ops_s = device_1102_get_ops(); drive_ops_s = device_1102_get_ops();
ESP_LOGI(tag_s, "1102 制动任务: %s",
device_1102_brake_task_ready() ? "已启动" : "未启动");
#if CONFIG_APP_PWM_IO16_ESC #if CONFIG_APP_PWM_IO16_ESC
ESP_LOGW(tag_s, "1102 需要 IO16 作为舵机,请在 menuconfig 将 GPIO16 角色改为 SERVO"); ESP_LOGW(tag_s, "1102 需要 IO16 作为舵机,请在 menuconfig 将 GPIO16 角色改为 SERVO");
#endif #endif
} else { } else {
drive_ops_s = device_1201_get_ops(); drive_ops_s = device_1101_get_ops();
} }
ESP_LOGI(tag_s, "当前设备策略: %s", drive_ops_s->name); ESP_LOGI(tag_s, "当前设备策略: %s", drive_ops_s->name);
} }
...@@ -198,10 +202,10 @@ void rc_vehicle_shot(int pin, int val) ...@@ -198,10 +202,10 @@ void rc_vehicle_shot(int pin, int val)
} }
} }
void rc_vehicle_control(int mode, int val) void rc_vehicle_control(int mode, int speed_val, int steer_val)
{ {
if (drive_ops_s && drive_ops_s->control) { if (drive_ops_s && drive_ops_s->control) {
drive_ops_s->control(mode, val); drive_ops_s->control(mode, speed_val, steer_val);
} }
} }
...@@ -258,6 +262,28 @@ void rc_pwm_set_dual_esc_percent(uint32_t percent) ...@@ -258,6 +262,28 @@ void rc_pwm_set_dual_esc_percent(uint32_t percent)
} }
} }
void rc_pwm_set_esc_neutral(void)
{
rc_pwm_set_dual_esc_percent(50U);
}
void rc_pwm_brake_drv1_pair(uint32_t brake_duty_pct, uint32_t hold_ms)
{
if (brake_duty_pct == 0U || hold_ms == 0U) {
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0U);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0U);
return;
}
if (brake_duty_pct > 100U) {
brake_duty_pct = 100U;
}
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, brake_duty_pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, brake_duty_pct);
vTaskDelay(pdMS_TO_TICKS(hold_ms));
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0U);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0U);
}
void rc_pwm_set_steering_angle_deg(uint32_t angle) void rc_pwm_set_steering_angle_deg(uint32_t angle)
{ {
if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO) { if (rc_pwm_aux_role_of_pin(RC_PWM_PIN_AUX_15) == RC_AUX_ROLE_SERVO) {
......
...@@ -49,7 +49,7 @@ void rc_pwm_control_set_drive_from_device_id(const char *device_id); ...@@ -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_stop(void);
void rc_vehicle_shot(int pin, int val); 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) * @brief 设置驱动芯片控制引脚百分比(支持 10/21/11/12)
...@@ -76,6 +76,16 @@ void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent); ...@@ -76,6 +76,16 @@ void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent);
*/ */
void rc_pwm_set_dual_esc_percent(uint32_t percent); void rc_pwm_set_dual_esc_percent(uint32_t percent);
/** @brief 电调回中位(1500us,percent=50) */
void rc_pwm_set_esc_neutral(void);
/**
* @brief 驱动芯片1 双引脚(IO10+IO21)动态刹车:同占空比短路制动后回 0
* @param brake_duty_pct 制动占空比(建议 30~60,勿用 100 以免过猛)
* @param hold_ms 制动保持时间(ms)
*/
void rc_pwm_brake_drv1_pair(uint32_t brake_duty_pct, uint32_t hold_ms);
/** /**
* @brief 给“默认转向舵机”输出角度(优先 IO15,其次 IO16) * @brief 给“默认转向舵机”输出角度(优先 IO15,其次 IO16)
*/ */
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
#include "uart_comm.h" #include "uart_comm.h"
#include "core/build_config.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 "driver/uart.h"
#include <string.h> #include <string.h>
...@@ -113,7 +113,7 @@ void uart_comm_flush(void) ...@@ -113,7 +113,7 @@ void uart_comm_flush(void)
uart_flush(UART_COMM_PORT); 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) ...@@ -153,4 +153,4 @@ int uart_comm_is_initialized(void)
void uart_comm_flush(void) {} void uart_comm_flush(void) {}
#endif /* CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE */ #endif /* CONFIG_ROBO_APP_FW_RELEASE */
/* /*
* UART 通信驱动 * UART 通信驱动
* 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口 * 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口
* 注意:启用此功能后,ESP_LOG/printf 将不可用 * Release 默认无控制台输出,UART 引脚用于业务通信
*/ */
#ifndef UART_COMM_H #ifndef UART_COMM_H
...@@ -20,10 +20,7 @@ extern "C" { ...@@ -20,10 +20,7 @@ extern "C" {
#define UART_COMM_BUFFER_SIZE 1024 #define UART_COMM_BUFFER_SIZE 1024
#define UART_COMM_QUEUE_SIZE 8 #define UART_COMM_QUEUE_SIZE 8
/* 初始化 UART 作为普通通信串口 /* 初始化 UART 作为普通通信串口(仅 Release 有效) */
* 仅在 APP_UART_MODE_DEBUG=y 时有效
* 返回 ESP_OK 成功,其他错误码失败
*/
esp_err_t uart_comm_init(int baudrate); esp_err_t uart_comm_init(int baudrate);
/* 反初始化,恢复默认状态 */ /* 反初始化,恢复默认状态 */
......
This diff is collapsed.
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* - 0xFFE2:固件 OTA 二进制流(首字节 opcode:0x01+uint32 LE 总长,0x02+固件数据,0x03 结束并重启;受 menuconfig「APP_BLE_OTA」控制) * - 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) * - 0xFFE4:OTA 过程状态 JSON,Read(最近一次)+ Notify(每步应答;需 enableNotification)
...@@ -28,6 +28,9 @@ esp_err_t link_ble_start(void); ...@@ -28,6 +28,9 @@ esp_err_t link_ble_start(void);
void link_ble_stop(void); void link_ble_stop(void);
/** 清空 BLE 侧缓冲并中止 OTA/遥控会话(断开或 message_type=2 时) */
void link_ble_session_reset(void);
#endif #endif
......
#include "link_dma_ble.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <stdlib.h>
#include <string.h>
static const char *tag_s = "LINK_DMA_BLE";
#define BLE_DMA_RX_QUEUE_DEPTH 24
#define BLE_DMA_TX_QUEUE_DEPTH 48
#define BLE_DMA_RX_MAX_BYTES 512
#define BLE_DMA_TX_MAX_BYTES 256
typedef struct {
link_dma_ble_ch_t ch;
uint16_t len;
uint8_t data[BLE_DMA_RX_MAX_BYTES];
} ble_dma_rx_item_t;
typedef struct {
uint16_t attr_handle;
uint16_t len;
uint8_t *data;
} ble_dma_tx_item_t;
static QueueHandle_t rx_q_s;
static QueueHandle_t tx_q_s;
static TaskHandle_t rx_task_s;
static TaskHandle_t tx_task_s;
static link_dma_ble_rx_cb_t rx_cb_s;
static void *rx_ctx_s;
static link_dma_ble_tx_fn_t tx_fn_s;
static void *tx_ctx_s;
static volatile uint16_t conn_handle_s;
static volatile bool running_s;
static void ble_dma_rx_task(void *param)
{
(void)param;
ble_dma_rx_item_t item;
while (running_s) {
if (xQueueReceive(rx_q_s, &item, pdMS_TO_TICKS(100)) == pdTRUE) {
if (rx_cb_s && item.len > 0) {
rx_cb_s(item.ch, item.data, item.len, rx_ctx_s);
}
}
}
vTaskDelete(NULL);
}
static void ble_dma_tx_task(void *param)
{
(void)param;
ble_dma_tx_item_t item;
while (running_s) {
if (xQueueReceive(tx_q_s, &item, pdMS_TO_TICKS(100)) != pdTRUE) {
continue;
}
if (!tx_fn_s || conn_handle_s == 0U || item.data == NULL || item.len == 0) {
free(item.data);
continue;
}
int rc = tx_fn_s(item.attr_handle, item.data, item.len, tx_ctx_s);
if (rc != 0) {
vTaskDelay(pdMS_TO_TICKS(2));
(void)tx_fn_s(item.attr_handle, item.data, item.len, tx_ctx_s);
}
free(item.data);
}
vTaskDelete(NULL);
}
esp_err_t link_dma_ble_start(link_dma_ble_rx_cb_t rx_cb, void *rx_ctx,
link_dma_ble_tx_fn_t tx_fn, void *tx_ctx)
{
if (running_s) {
return ESP_OK;
}
if (!rx_cb || !tx_fn) {
return ESP_ERR_INVALID_ARG;
}
rx_cb_s = rx_cb;
rx_ctx_s = rx_ctx;
tx_fn_s = tx_fn;
tx_ctx_s = tx_ctx;
rx_q_s = xQueueCreate(BLE_DMA_RX_QUEUE_DEPTH, sizeof(ble_dma_rx_item_t));
tx_q_s = xQueueCreate(BLE_DMA_TX_QUEUE_DEPTH, sizeof(ble_dma_tx_item_t));
if (!rx_q_s || !tx_q_s) {
link_dma_ble_stop();
return ESP_ERR_NO_MEM;
}
running_s = true;
if (xTaskCreate(ble_dma_rx_task, "ble_dma_rx", 4096, NULL, 6, &rx_task_s) != pdPASS ||
xTaskCreate(ble_dma_tx_task, "ble_dma_tx", 4096, NULL, 5, &tx_task_s) != pdPASS) {
running_s = false;
link_dma_ble_stop();
return ESP_ERR_NO_MEM;
}
ESP_LOGI(tag_s, "BLE 异步收发队列已启动 (RX depth=%d, TX depth=%d)",
BLE_DMA_RX_QUEUE_DEPTH, BLE_DMA_TX_QUEUE_DEPTH);
return ESP_OK;
}
void link_dma_ble_stop(void)
{
running_s = false;
if (rx_task_s) {
vTaskDelete(rx_task_s);
rx_task_s = NULL;
}
if (tx_task_s) {
vTaskDelete(tx_task_s);
tx_task_s = NULL;
}
if (rx_q_s) {
vQueueDelete(rx_q_s);
rx_q_s = NULL;
}
if (tx_q_s) {
ble_dma_tx_item_t item;
while (xQueueReceive(tx_q_s, &item, 0) == pdTRUE) {
free(item.data);
}
vQueueDelete(tx_q_s);
tx_q_s = NULL;
}
rx_cb_s = NULL;
tx_fn_s = NULL;
conn_handle_s = 0;
}
bool link_dma_ble_post_rx(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len)
{
if (!running_s || !data || len == 0 || len > BLE_DMA_RX_MAX_BYTES) {
return false;
}
ble_dma_rx_item_t item = {
.ch = ch,
.len = len,
};
memcpy(item.data, data, len);
if (xQueueSend(rx_q_s, &item, 0) != pdTRUE) {
ESP_LOGW(tag_s, "BLE RX 队列满,丢弃 %u 字节", (unsigned)len);
return false;
}
return true;
}
bool link_dma_ble_post_tx(uint16_t attr_handle, const uint8_t *data, uint16_t len)
{
if (!running_s || !data || len == 0 || len > BLE_DMA_TX_MAX_BYTES) {
return false;
}
uint8_t *copy = (uint8_t *)malloc(len);
if (!copy) {
return false;
}
memcpy(copy, data, len);
ble_dma_tx_item_t item = {
.attr_handle = attr_handle,
.len = len,
.data = copy,
};
if (xQueueSend(tx_q_s, &item, 0) != pdTRUE) {
ESP_LOGW(tag_s, "BLE TX 队列满,丢弃 %u 字节", (unsigned)len);
free(copy);
return false;
}
return true;
}
void link_dma_ble_set_conn_handle(uint16_t conn_handle)
{
conn_handle_s = conn_handle;
}
/**
* BLE 链路异步收发缓冲
* NimBLE 与 UHCI 共用 HCI 硬件,无法用 UART DMA;此处用环形队列模拟 DMA 双缓冲,防止 GATT 回调阻塞导致溢出。
*/
#ifndef LINK_DMA_BLE_H
#define LINK_DMA_BLE_H
#include "esp_err.h"
#include <stddef.h>
#include <stdint.h>
typedef enum {
LINK_DMA_BLE_CH_JSON = 0,
LINK_DMA_BLE_CH_OTA = 1,
} link_dma_ble_ch_t;
typedef void (*link_dma_ble_rx_cb_t)(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len, void *ctx);
/** payload 由队列持有,发送完成后自动释放 */
typedef int (*link_dma_ble_tx_fn_t)(uint16_t attr_handle, const uint8_t *data, uint16_t len, void *ctx);
esp_err_t link_dma_ble_start(link_dma_ble_rx_cb_t rx_cb, void *rx_ctx,
link_dma_ble_tx_fn_t tx_fn, void *tx_ctx);
void link_dma_ble_stop(void);
/** GATT 写回调中调用:拷贝入队后立即返回 */
bool link_dma_ble_post_rx(link_dma_ble_ch_t ch, const uint8_t *data, uint16_t len);
/** 排队 Notify/Write;conn 与 handle 由 tx_fn 使用 */
bool link_dma_ble_post_tx(uint16_t attr_handle, const uint8_t *data, uint16_t len);
void link_dma_ble_set_conn_handle(uint16_t conn_handle);
#endif /* LINK_DMA_BLE_H */
#include "link_dma_uart.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include <stdlib.h>
#include <string.h>
static const char *tag_s = "LINK_DMA_UART";
#define UART_BUF_SIZE 4096
#define UART_EVT_QUEUE_LEN 16
#if configNUMBER_OF_CORES > 1
#define UART_RX_TASK_CORE 1
#else
#define UART_RX_TASK_CORE 0
#endif
static QueueHandle_t uart_evt_q_s;
static TaskHandle_t rx_task_s;
static link_dma_uart_rx_cb_t rx_cb_s;
static void *rx_ctx_s;
static uart_port_t uart_port_s;
static volatile bool running_s;
static SemaphoreHandle_t tx_mutex_s;
static void uart_evt_rx_task(void *param)
{
(void)param;
uint8_t *buf = (uint8_t *)malloc(UART_BUF_SIZE);
if (!buf) {
vTaskDelete(NULL);
return;
}
uart_event_t evt;
while (running_s) {
if (xQueueReceive(uart_evt_q_s, &evt, pdMS_TO_TICKS(50)) != pdTRUE) {
continue;
}
switch (evt.type) {
case UART_DATA:
case UART_BUFFER_FULL:
case UART_FIFO_OVF:
if (evt.type == UART_FIFO_OVF) {
ESP_LOGW(tag_s, "UART FIFO overflow, flushing");
uart_flush_input(uart_port_s);
}
for (;;) {
int n = uart_read_bytes(uart_port_s, buf, UART_BUF_SIZE,
(evt.type == UART_DATA) ? 0 : pdMS_TO_TICKS(10));
if (n <= 0) {
break;
}
if (rx_cb_s) {
rx_cb_s(buf, (size_t)n, rx_ctx_s);
}
}
break;
default:
break;
}
}
free(buf);
vTaskDelete(NULL);
}
esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int rx_gpio,
link_dma_uart_rx_cb_t rx_cb, void *rx_ctx)
{
if (running_s) {
return ESP_OK;
}
if (tx_gpio < 0 || rx_gpio < 0 || rx_cb == NULL) {
return ESP_ERR_INVALID_ARG;
}
uart_port_s = port;
rx_cb_s = rx_cb;
rx_ctx_s = rx_ctx;
uart_config_t uart_cfg = {
.baud_rate = baud_rate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
gpio_reset_pin((gpio_num_t)tx_gpio);
gpio_reset_pin((gpio_num_t)rx_gpio);
esp_err_t err = uart_param_config(port, &uart_cfg);
if (err != ESP_OK) {
return err;
}
err = uart_set_pin(port, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
return err;
}
err = uart_driver_install(port, UART_BUF_SIZE, UART_BUF_SIZE, UART_EVT_QUEUE_LEN,
&uart_evt_q_s, 0);
if (err != ESP_OK) {
return err;
}
uart_set_rx_full_threshold(port, 64);
uart_set_rx_timeout(port, 10);
gpio_set_pull_mode((gpio_num_t)rx_gpio, GPIO_PULLUP_ONLY);
uart_flush(port);
tx_mutex_s = xSemaphoreCreateMutex();
if (!tx_mutex_s) {
link_dma_uart_stop();
return ESP_ERR_NO_MEM;
}
running_s = true;
if (xTaskCreatePinnedToCore(uart_evt_rx_task, "uart_evt_rx", 6144, NULL, 6,
&rx_task_s, UART_RX_TASK_CORE) != pdPASS) {
running_s = false;
link_dma_uart_stop();
return ESP_ERR_NO_MEM;
}
ESP_LOGI(tag_s, "UART 事件驱动收发已启动 (port=%d, %d baud, TX=GPIO%d RX=GPIO%d, buf=%d, core=%d)",
(int)port, baud_rate, tx_gpio, rx_gpio, UART_BUF_SIZE, UART_RX_TASK_CORE);
return ESP_OK;
}
void link_dma_uart_stop(void)
{
running_s = false;
if (rx_task_s) {
vTaskDelete(rx_task_s);
rx_task_s = NULL;
}
if (uart_port_s >= 0 && uart_evt_q_s != NULL) {
uart_driver_delete(uart_port_s);
}
uart_evt_q_s = NULL;
if (tx_mutex_s) {
vSemaphoreDelete(tx_mutex_s);
tx_mutex_s = NULL;
}
rx_cb_s = NULL;
rx_ctx_s = NULL;
}
int link_dma_uart_send(const uint8_t *data, size_t len, int timeout_ms)
{
if (!running_s || !data || len == 0) {
return -1;
}
int wait_ms = (timeout_ms > 0) ? timeout_ms : 500;
if (tx_mutex_s == NULL || xSemaphoreTake(tx_mutex_s, pdMS_TO_TICKS(wait_ms)) != pdTRUE) {
ESP_LOGW(tag_s, "UART TX mutex timeout");
return -2;
}
int n = uart_write_bytes(uart_port_s, data, len);
if (n > 0) {
esp_err_t err = uart_wait_tx_done(uart_port_s, pdMS_TO_TICKS(wait_ms));
if (err != ESP_OK) {
ESP_LOGW(tag_s, "uart_wait_tx_done: %s", esp_err_to_name(err));
n = -2;
}
} else {
ESP_LOGW(tag_s, "uart_write_bytes failed: %d", n);
}
(void)xSemaphoreGive(tx_mutex_s);
return n;
}
bool link_dma_uart_is_running(void)
{
return running_s;
}
/**
* UART 链路事件驱动收发
* uart_driver_install + UART_DATA 事件立即读空,降低 FIFO 溢出风险。
*/
#ifndef LINK_DMA_UART_H
#define LINK_DMA_UART_H
#include "esp_err.h"
#include "driver/uart.h"
#include <stddef.h>
#include <stdint.h>
typedef void (*link_dma_uart_rx_cb_t)(const uint8_t *data, size_t len, void *ctx);
esp_err_t link_dma_uart_start(uart_port_t port, int baud_rate, int tx_gpio, int rx_gpio,
link_dma_uart_rx_cb_t rx_cb, void *rx_ctx);
void link_dma_uart_stop(void);
/** 阻塞直到本批 DMA 发送完成;timeout_ms<0 表示一直等 */
int link_dma_uart_send(const uint8_t *data, size_t len, int timeout_ms);
bool link_dma_uart_is_running(void);
#endif /* LINK_DMA_UART_H */
This diff is collapsed.
...@@ -37,15 +37,19 @@ void link_uart_stop(void); ...@@ -37,15 +37,19 @@ void link_uart_stop(void);
int link_uart_send_heartbeat(const char *json); int link_uart_send_heartbeat(const char *json);
/** /**
* @brief 发送日志/告警 JSON 数据 * @brief 发送日志/告警 JSON(message_type=4/5;Release 下 ESP_LOGW/E 亦经此上送)
* @param json 要发送的 JSON 字符串 * @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败 * @return 0 成功,其他失败
*/ */
int link_uart_send_log(const char *json); int link_uart_send_log(const char *json);
/** /**
* @brief 检查 UART 链路是否已连接(有数据交互即认为在线) * @brief 是否曾收到对端 UART1 数据(发过 6 或控制帧后为 true)
* @return true 在线,false 离线 */
bool link_uart_has_peer_seen(void);
/**
* @brief 对端会话有效(曾 RX 且在 UART_LINK_ALIVE_MS 内,供心跳)
*/ */
bool link_uart_is_connected(void); bool link_uart_is_connected(void);
......
...@@ -51,6 +51,16 @@ static int mqttabnormal_exit_count = 0; ...@@ -51,6 +51,16 @@ static int mqttabnormal_exit_count = 0;
int mqtt_send_deviceota_info(void); 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 构造心跳包内容 * @brief 构造心跳包内容
*/ */
...@@ -153,7 +163,32 @@ static void common_mqtt_event_handler(void *handler_args, esp_event_base_t base, ...@@ -153,7 +163,32 @@ static void common_mqtt_event_handler(void *handler_args, esp_event_base_t base,
mqttabnormal_exit_count = 0; mqttabnormal_exit_count = 0;
memcpy(tmp, event->data, event->data_len); memcpy(tmp, event->data, event->data_len);
tmp[event->data_len] = '\0'; 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); free(tmp);
} }
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式 * 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
*/ */
#include "sdkconfig.h"
#include "ota_binary_stream.h" #include "ota_binary_stream.h"
#include "ota_offer_protocol.h"
#include "ota_manager.h" #include "ota_manager.h"
#include "esp_ota_ops.h" #include "esp_ota_ops.h"
#include "esp_partition.h" #include "esp_partition.h"
...@@ -23,6 +25,7 @@ typedef struct { ...@@ -23,6 +25,7 @@ typedef struct {
const esp_partition_t *partition; const esp_partition_t *partition;
size_t expected_size; size_t expected_size;
size_t written_size; size_t written_size;
size_t last_reported_size;
ota_transport_type_t transport; ota_transport_type_t transport;
ota_binary_stream_status_callback_t status_callback; ota_binary_stream_status_callback_t status_callback;
} ota_stream_session_t; } ota_stream_session_t;
...@@ -45,11 +48,34 @@ static void ota_stream_send_status_fmt(const char *fmt, ...) ...@@ -45,11 +48,34 @@ static void ota_stream_send_status_fmt(const char *fmt, ...)
session_s.status_callback(buf); session_s.status_callback(buf);
} }
/* 发送错误状态 */ /* 发送错误状态并结束 OTA 会话(仅用于致命错误,须已 armed) */
static void ota_stream_send_error(const char *step, const char *err) static void ota_stream_send_error(const char *step, const char *err)
{ {
ESP_LOGE(tag_s, "OTA error at %s: %s", step, err); ESP_LOGE(tag_s, "OTA error at %s: %s", step, err);
ota_stream_send_status_fmt("{\"ota\":\"%s\",\"ok\":0,\"err\":\"%s\"}", step, err); if (ota_offer_is_armed() || session_s.active) {
ota_stream_send_status_fmt("{\"ota\":\"%s\",\"ok\":0,\"err\":\"%s\"}", step, err);
ota_offer_notify_done(false, (uint32_t)session_s.written_size, step);
}
}
/**
* @brief 擦除 OTA 目标分区(失败清理,防止下次启动误用损坏固件)
*/
static void ota_stream_erase_partition(void)
{
if (session_s.partition == NULL) {
return;
}
ESP_LOGW(tag_s, "OTA 失败:擦除分区 %s 防止误启动", session_s.partition->label);
const esp_partition_t *part = session_s.partition;
esp_err_t err = esp_partition_erase_range(part, 0, part->size);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "擦除分区失败: %s", esp_err_to_name(err));
} else {
ESP_LOGI(tag_s, "分区 %s 已擦除 (%u bytes)", part->label, (unsigned)part->size);
}
} }
/* 中止 OTA 会话 */ /* 中止 OTA 会话 */
...@@ -60,11 +86,17 @@ static void ota_stream_abort_internal(void) ...@@ -60,11 +86,17 @@ static void ota_stream_abort_internal(void)
ESP_LOGW(tag_s, "OTA session aborted"); ESP_LOGW(tag_s, "OTA session aborted");
} }
/* 如果已写入部分数据(>4KB),擦除分区避免启动误用 */
if (session_s.written_size > 4096 && session_s.partition != NULL) {
ota_stream_erase_partition();
}
session_s.active = false; session_s.active = false;
session_s.handle = 0; session_s.handle = 0;
session_s.partition = NULL; session_s.partition = NULL;
session_s.expected_size = 0; session_s.expected_size = 0;
session_s.written_size = 0; session_s.written_size = 0;
session_s.last_reported_size = 0;
} }
void ota_binary_stream_abort(void) void ota_binary_stream_abort(void)
...@@ -77,6 +109,11 @@ bool ota_binary_stream_is_active(void) ...@@ -77,6 +109,11 @@ bool ota_binary_stream_is_active(void)
return session_s.active; return session_s.active;
} }
uint32_t ota_binary_stream_get_written_bytes(void)
{
return (uint32_t)session_s.written_size;
}
esp_err_t ota_binary_stream_init(ota_transport_type_t transport, esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
ota_binary_stream_status_callback_t status_callback, ota_binary_stream_status_callback_t status_callback,
const char *target_version) const char *target_version)
...@@ -88,15 +125,9 @@ esp_err_t ota_binary_stream_init(ota_transport_type_t transport, ...@@ -88,15 +125,9 @@ esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
session_s.transport = transport; session_s.transport = transport;
session_s.status_callback = status_callback; session_s.status_callback = status_callback;
(void)target_version;
/* 通过OTA管理器开始会话 */ ESP_LOGI(tag_s, "OTA stream init: transport=%d (mgr session starts at BEGIN/0x01)", transport);
ota_type_t type = (transport == OTA_TRANSPORT_BLE) ? OTA_TYPE_BLE : OTA_TYPE_UART;
esp_err_t err = ota_manager_begin_session(type, target_version ? target_version : "unknown");
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to begin OTA session in manager: %s", esp_err_to_name(err));
}
ESP_LOGI(tag_s, "OTA stream init: transport=%d", transport);
return ESP_OK; return ESP_OK;
} }
...@@ -111,6 +142,11 @@ void ota_binary_stream_deinit(void) ...@@ -111,6 +142,11 @@ void ota_binary_stream_deinit(void)
/* 处理 OTA 开始命令 */ /* 处理 OTA 开始命令 */
static int ota_stream_handle_begin(const uint8_t *data, uint16_t len) 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) { if (len < 5) {
/* 需要至少 1字节opcode + 4字节长度 */ /* 需要至少 1字节opcode + 4字节长度 */
ota_stream_send_error("begin", "size"); ota_stream_send_error("begin", "size");
...@@ -152,8 +188,20 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len) ...@@ -152,8 +188,20 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
return -4; 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.expected_size = image_size;
session_s.written_size = 0; session_s.written_size = 0;
session_s.last_reported_size = 0;
session_s.active = true; session_s.active = true;
ota_stream_send_status_fmt("{\"ota\":\"begin\",\"ok\":1,\"expect\":%lu}", ota_stream_send_status_fmt("{\"ota\":\"begin\",\"ok\":1,\"expect\":%lu}",
...@@ -166,7 +214,9 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len) ...@@ -166,7 +214,9 @@ static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
static int ota_stream_handle_data(const uint8_t *data, uint16_t len) static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
{ {
if (!session_s.active) { if (!session_s.active) {
ota_stream_send_error("chunk", "no_session"); /* 仅 NAK,不结束 OTA:可能 BEGIN 未到或 App 重传;避免 done 风暴 */
ESP_LOGW(tag_s, "OTA DATA without active session (armed=%d)",
(int)ota_offer_is_armed());
return -1; return -1;
} }
...@@ -205,13 +255,14 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len) ...@@ -205,13 +255,14 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
session_s.written_size += payload_len; session_s.written_size += payload_len;
/* 每 8192 字节发送一次进度,或最后一次 */ /* BLE 调试 JSON;UART OTA 期间 UART1 只走 OT 帧,不发 chunk 状态 JSON */
static size_t last_reported = 0; if (session_s.transport != OTA_TRANSPORT_UART) {
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) { session_s.written_size >= session_s.expected_size) {
ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}", ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size); (unsigned)session_s.written_size);
last_reported = session_s.written_size; session_s.last_reported_size = session_s.written_size;
}
} }
return 0; return 0;
...@@ -221,7 +272,7 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len) ...@@ -221,7 +272,7 @@ static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
static int ota_stream_handle_end(void) static int ota_stream_handle_end(void)
{ {
if (!session_s.active) { if (!session_s.active) {
ota_stream_send_error("end", "no_session"); ESP_LOGW(tag_s, "OTA END without active session");
return -1; return -1;
} }
...@@ -255,8 +306,8 @@ static int ota_stream_handle_end(void) ...@@ -255,8 +306,8 @@ static int ota_stream_handle_end(void)
return -4; return -4;
} }
ESP_LOGI(tag_s, "OTA successful, written %u bytes", ESP_LOGI(tag_s, "OTA successful, written %u bytes, next boot from %s",
(unsigned)session_s.written_size); (unsigned)session_s.written_size, session_s.partition->label);
/* 通过OTA管理器结束会话 */ /* 通过OTA管理器结束会话 */
ota_manager_end_session(); ota_manager_end_session();
...@@ -264,9 +315,11 @@ static int ota_stream_handle_end(void) ...@@ -264,9 +315,11 @@ static int ota_stream_handle_end(void)
ota_stream_send_status_fmt("{\"ota\":\"end\",\"ok\":1,\"written\":%u}", ota_stream_send_status_fmt("{\"ota\":\"end\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size); (unsigned)session_s.written_size);
/* 延时后重启 */ ota_offer_notify_done(true, (uint32_t)session_s.written_size, NULL);
ESP_LOGI(tag_s, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000)); /* 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(); esp_restart();
return 0; return 0;
......
/* /*
* OTA 二进制流接收模块 * OTA 二进制流接收模块
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式 * 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
* 协议:首字节 opcode + 数据 * 内部载荷:0x01/0x02/0x03(由 ota_frame_protocol 解帧后调用)
* 0x01 + uint32_t LE(4字节) = 开始OTA,指定固件总长度 * 对外传输:见 protocol/ota_frame_protocol.h(OT 包头 + CRC8 + 包尾 0x16)
* 0x02 + 数据 = 固件数据块
* 0x03 = 结束OTA,触发校验和重启
*/ */
#ifndef OTA_BINARY_STREAM_H #ifndef OTA_BINARY_STREAM_H
...@@ -65,6 +63,9 @@ bool ota_binary_stream_is_active(void); ...@@ -65,6 +63,9 @@ bool ota_binary_stream_is_active(void);
*/ */
void ota_binary_stream_abort(void); void ota_binary_stream_abort(void);
/** 当前会话已写入字节数(未 begin 时为 0) */
uint32_t ota_binary_stream_get_written_bytes(void);
/** /**
* @brief 获取 OTA 状态 JSON 字符串 * @brief 获取 OTA 状态 JSON 字符串
* 用于主动查询OTA状态 * 用于主动查询OTA状态
......
This diff is collapsed.
...@@ -38,6 +38,7 @@ typedef struct { ...@@ -38,6 +38,7 @@ typedef struct {
ota_type_t type; /* OTA类型 */ ota_type_t type; /* OTA类型 */
char target_version[32]; /* 目标版本号 */ char target_version[32]; /* 目标版本号 */
char source_version[32]; /* 升级前版本号 */ char source_version[32]; /* 升级前版本号 */
char source_partition[16]; /* 升级前运行分区标签,如 ota_0 */
uint32_t timestamp; /* OTA开始时间戳 */ uint32_t timestamp; /* OTA开始时间戳 */
uint8_t boot_count; /* 新固件启动计数 */ uint8_t boot_count; /* 新固件启动计数 */
uint8_t max_boot_attempts; /* 最大尝试次数 */ uint8_t max_boot_attempts; /* 最大尝试次数 */
......
#include "heart_payload.h" #include "heart_payload.h"
#include "device_nvs.h"
#include "betteryread.h" #include "betteryread.h"
#include "cJSON.h" #include "cJSON.h"
#include "sdkconfig.h" #include "sdkconfig.h"
...@@ -42,14 +43,12 @@ char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool ...@@ -42,14 +43,12 @@ char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool
cJSON_AddStringToObject(body, "ip", ip); cJSON_AddStringToObject(body, "ip", ip);
} }
char id_full[64]; const char *dev = device_id_plain(device_id);
const char *dev = (device_id != NULL) ? device_id : "";
snprintf(id_full, sizeof(id_full), "app2dev/%s", dev);
char voltage_s[16]; char voltage_s[16];
snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v()); snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v());
cJSON_AddStringToObject(body, "voltage", voltage_s); 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); cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
return heart_payload_build(HEART_MSG_TYPE_HEARTBEAT, body); return heart_payload_build(HEART_MSG_TYPE_HEARTBEAT, body);
......
This diff is collapsed.
/**
* OTA 二进制帧协议(BLE 0xFFE2 / UART)
*
* 帧格式:[HEAD0][HEAD1][VER][OP][SEQ_L][SEQ_H][LEN_L][LEN_H][PAYLOAD...][CRC8][TAIL]
* - HEAD: 0x4F 0x54 ('O','T')
* - VER: 0x02
* - CRC8: 从 VER 到 PAYLOAD 末字节逐字节 XOR
* - TAIL: 0x16
*
* OP_DATA 载荷 1~MAX 字节固件数据(UART 最大见 menuconfig);设备回 OP_ACK:
* - 1 字节载荷:ACK [echo_byte][status]
* - 多块载荷:ACK [len_lo][len_hi][last_byte][status]
* status=0 成功,非 0 须重传同一 SEQ 帧。
*/
#ifndef OTA_FRAME_PROTOCOL_H
#define OTA_FRAME_PROTOCOL_H
#include "sdkconfig.h"
#include <stdint.h>
#include <stdbool.h>
#define OTA_FRAME_MAGIC0 0x4F
#define OTA_FRAME_MAGIC1 0x54
#define OTA_FRAME_VERSION 0x02
#define OTA_FRAME_TAIL_BYTE 0x16
#define OTA_FRAME_OP_BEGIN 0x01
#define OTA_FRAME_OP_DATA 0x02
#define OTA_FRAME_OP_END 0x03
#define OTA_FRAME_OP_ACK 0x80
#define OTA_FRAME_OP_NAK 0x81
#define OTA_ACK_OK 0x00
#define OTA_ACK_ERR_FLASH 0x01
#define OTA_ACK_ERR_OVERFLOW 0x02
#define OTA_ACK_ERR_NO_SESSION 0x03
#define OTA_ACK_ERR_CRC 0x04
#define OTA_ACK_ERR_BAD_LEN 0x05
#define OTA_ACK_ERR_BAD_OP 0x06
#if CONFIG_APP_LINK_BLE
#define OTA_FRAME_MAX_PAYLOAD 240
#elif defined(CONFIG_APP_OTA_UART_MAX_CHUNK)
#define OTA_FRAME_MAX_PAYLOAD CONFIG_APP_OTA_UART_MAX_CHUNK
#else
#define OTA_FRAME_MAX_PAYLOAD 1024
#endif
#define OTA_FRAME_DATA_CHUNK_RECOMMEND 128
#define OTA_FRAME_BUF_SIZE (8 + OTA_FRAME_MAX_PAYLOAD + 2)
typedef void (*ota_frame_tx_fn)(const uint8_t *frame, uint16_t len, void *ctx);
void ota_frame_protocol_init(ota_frame_tx_fn tx, void *ctx);
void ota_frame_protocol_reset(void);
/** 喂入 RX 字节流(可含多帧);内部组帧并应答 */
void ota_frame_rx_feed(const uint8_t *data, uint16_t len);
/** 帧解析器是否正在等待当前帧的后续字节 */
bool ota_frame_rx_is_active(void);
/** 是否像帧协议('O')或旧版裸 opcode */
bool ota_frame_rx_looks_framed(const uint8_t *data, uint16_t len);
int ota_frame_build(uint8_t op, uint16_t seq, const uint8_t *payload, uint16_t payload_len,
uint8_t *out, uint16_t out_cap, uint16_t *out_len);
#endif
#include "ota_offer_protocol.h"
#include "ota_binary_stream.h"
#include "ota_uart_tune.h"
#include "ota_frame_protocol.h"
#include "device_nvs.h"
#include "sdkconfig.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static volatile bool s_query_sent_boot_s;
static volatile bool s_ota_armed_s;
static ota_offer_done_cb_t s_done_cb_s;
void ota_offer_reset_boot(void)
{
s_query_sent_boot_s = false;
s_ota_armed_s = false;
}
bool ota_offer_is_armed(void)
{
return s_ota_armed_s;
}
bool ota_offer_session_busy(void)
{
return s_ota_armed_s || ota_binary_stream_is_active();
}
void ota_offer_disarm(void)
{
s_ota_armed_s = false;
}
static void ota_offer_format_device_id(char *out, size_t cap, const char *device_id)
{
snprintf(out, cap, "%s", device_id_plain(device_id));
}
static void ota_offer_body_add_ota_tune(cJSON *body)
{
#if CONFIG_APP_LINK_UART
int baud = CONFIG_APP_UART_LINK_BAUDRATE;
#else
int baud = 0;
#endif
uint16_t chunk = ota_uart_tune_chunk_for_baud(baud);
uint32_t ack_ms = ota_uart_tune_ack_timeout_ms(baud > 0 ? baud : 115200, chunk);
cJSON_AddNumberToObject(body, "ota_chunk", chunk);
cJSON_AddNumberToObject(body, "ota_chunk_max", OTA_FRAME_MAX_PAYLOAD);
cJSON_AddNumberToObject(body, "ota_ack_timeout_ms", (double)ack_ms);
#if CONFIG_APP_LINK_UART
cJSON_AddNumberToObject(body, "uart_baud", baud);
#endif
}
static char *ota_offer_build(int message_type, cJSON *body)
{
if (!body) {
return NULL;
}
cJSON *root = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
if (!root || !head) {
cJSON_Delete(root);
cJSON_Delete(head);
cJSON_Delete(body);
return NULL;
}
cJSON_AddNumberToObject(head, "message_type", message_type);
cJSON_AddItemToObject(root, "head", head);
cJSON_AddItemToObject(root, "body", body);
char *out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
char *ota_offer_query_json_malloc(const char *device_id)
{
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
char id_full[64];
ota_offer_format_device_id(id_full, sizeof(id_full), device_id);
cJSON_AddStringToObject(body, "phase", "query");
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
ota_offer_body_add_ota_tune(body);
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
}
char *ota_offer_active_json_malloc(const char *device_id)
{
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
char id_full[64];
ota_offer_format_device_id(id_full, sizeof(id_full), device_id);
cJSON_AddStringToObject(body, "phase", "active");
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
ota_offer_body_add_ota_tune(body);
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
}
char *ota_offer_result_json_malloc(const char *device_id,
bool success,
uint32_t bytes_written,
const char *err_step)
{
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
char id_full[64];
ota_offer_format_device_id(id_full, sizeof(id_full), device_id);
cJSON_AddStringToObject(body, "phase", "done");
cJSON_AddNumberToObject(body, "ok", success ? 1 : 0);
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
cJSON_AddNumberToObject(body, "written", (double)bytes_written);
if (!success && err_step != NULL && err_step[0] != '\0') {
cJSON_AddStringToObject(body, "err", err_step);
} else {
cJSON_AddStringToObject(body, "err", "");
}
return ota_offer_build(OTA_MSG_DEVICE_OFFER, body);
}
bool ota_offer_parse_phone_response(cJSON *root)
{
if (!root) {
return false;
}
cJSON *head = cJSON_GetObjectItem(root, "head");
if (!head || !cJSON_IsObject(head)) {
return false;
}
cJSON *m_type = cJSON_GetObjectItem(head, "message_type");
if (!m_type || !cJSON_IsNumber(m_type)) {
return false;
}
if ((int)m_type->valuedouble == OTA_MSG_PHONE_OTA_START) {
s_ota_armed_s = true;
return true;
}
return false;
}
bool ota_offer_mark_query_sent(void)
{
if (s_query_sent_boot_s) {
return false;
}
s_query_sent_boot_s = true;
return true;
}
void ota_offer_reset_query_sent(void)
{
s_query_sent_boot_s = false;
}
void ota_offer_set_done_callback(ota_offer_done_cb_t cb)
{
s_done_cb_s = cb;
}
void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err_step)
{
s_ota_armed_s = false;
if (s_done_cb_s) {
s_done_cb_s(success, bytes_written, err_step);
}
}
void ota_offer_report_abort(const char *err_step)
{
if (!ota_offer_session_busy()) {
return;
}
uint32_t written = ota_binary_stream_get_written_bytes();
if (ota_binary_stream_is_active()) {
ota_binary_stream_abort();
}
ota_offer_notify_done(false, written, err_step);
}
#ifndef OTA_OFFER_PROTOCOL_H
#define OTA_OFFER_PROTOCOL_H
#include <stdbool.h>
#include <stdint.h>
#include "cJSON.h"
/** 设备 → 手机:询问是否 OTA(手机 message_type=6 后回复)及 OTA 结束汇报 */
#define OTA_MSG_DEVICE_OFFER 1002
/** 手机 → 设备:同意开始二进制 OTA(随后发 0x01/0x02/0x03) */
#define OTA_MSG_PHONE_OTA_START 0
/** OTA 结束 1002/done 重复发送次数(提高手机收包成功率后再重启切分区) */
#define OTA_DONE_REPORT_COUNT 3
/** 各次 done JSON 之间的间隔(ms) */
#define OTA_DONE_REPORT_GAP_MS 100
/** 发完 done 重复包后、重启前的额外等待(ms,留给 UART DMA 发完) */
#define OTA_DONE_REBOOT_DELAY_MS 400
typedef void (*ota_offer_done_cb_t)(bool success, uint32_t bytes_written, const char *err_step);
/** 上电/链路启动时调用,允许再次发送询问 JSON */
void ota_offer_reset_boot(void);
/** @deprecated 已由 message_type=6 触发;保留供兼容 */
bool ota_offer_mark_query_sent(void);
/** @deprecated */
void ota_offer_reset_query_sent(void);
/** OTA 二进制结束/失败时由 ota_binary_stream 调用 */
void ota_offer_notify_done(bool success, uint32_t bytes_written, const char *err_step);
/** 是否已允许接收 OTA 二进制(手机已回复 message_type=0) */
bool ota_offer_is_armed(void);
/** 取消 OTA 武装(链路清空/卡顿恢复时调用,不重置开机询问标记) */
void ota_offer_disarm(void);
/**
* 解析手机 JSON;head.message_type==0 时置位 armed 并返回 true
*/
bool ota_offer_parse_phone_response(cJSON *root);
/** 构造询问 JSON:head.message_type=1002, body.phase=query */
char *ota_offer_query_json_malloc(const char *device_id);
/** 构造 OTA 进行中 JSON:head.message_type=1002, body.phase=active(替代心跳) */
char *ota_offer_active_json_malloc(const char *device_id);
/** 构造结果 JSON:head.message_type=1002, body.phase=done */
char *ota_offer_result_json_malloc(const char *device_id,
bool success,
uint32_t bytes_written,
const char *err_step);
/** OTA 是否进行中(已武装或正在写 Flash) */
bool ota_offer_session_busy(void);
/**
* 链路异常中止:若 OTA 进行中则上报 1002/done(ok=0) 并解除武装。
* @param err_step 错误码,如 timeout / reset / disconnect
*/
void ota_offer_report_abort(const char *err_step);
void ota_offer_set_done_callback(ota_offer_done_cb_t cb);
#endif
#include "ota_uart_tune.h"
uint16_t ota_uart_tune_chunk_for_baud(int baud)
{
if (baud <= 0) {
return OTA_FRAME_DATA_CHUNK_RECOMMEND;
}
/*
* chunk ≈ baud/512 - wire_overhead:高波特率大块,减少 USB 往返次数。
* 115200→~201B,460800→~876B,921600→顶满 MAX(通常 1024)。
*/
int64_t chunk = (int64_t)baud / 512 - OTA_UART_WIRE_OVERHEAD_BYTES;
if (chunk < 32) {
chunk = 32;
}
if (chunk > OTA_FRAME_MAX_PAYLOAD) {
chunk = OTA_FRAME_MAX_PAYLOAD;
}
return (uint16_t)chunk;
}
uint32_t ota_uart_tune_ack_timeout_ms(int baud, uint16_t chunk)
{
if (baud <= 0) {
baud = 115200;
}
uint32_t wire_ms = (uint32_t)(((uint64_t)(chunk + OTA_UART_WIRE_OVERHEAD_BYTES) * 10 * 1000) /
(uint32_t)baud);
uint32_t t = wire_ms * 3 + 800;
if (t < 1200) {
t = 1200;
}
if (t > 8000) {
t = 8000;
}
return t;
}
uint32_t ota_uart_tune_transfer_idle_ms(int baud)
{
uint16_t chunk = ota_uart_tune_chunk_for_baud(baud);
uint32_t t = ota_uart_tune_ack_timeout_ms(baud, chunk) + 1000;
if (t < 2000) {
t = 2000;
}
if (t > 12000) {
t = 12000;
}
return t;
}
#ifndef OTA_UART_TUNE_H
#define OTA_UART_TUNE_H
#include <stdint.h>
#include "ota_frame_protocol.h"
/** OT 帧线传固定开销:DATA 帧 10B + 多块 ACK 14B */
#define OTA_UART_WIRE_OVERHEAD_BYTES 24
/**
* 按波特率计算推荐 DATA 块大小(32~OTA_FRAME_MAX_PAYLOAD,UART 默认最大 1024)。
* baud<=0(BLE)时返回 OTA_FRAME_DATA_CHUNK_RECOMMEND。
*/
uint16_t ota_uart_tune_chunk_for_baud(int baud);
/** App 等 ACK 建议超时(ms),与块大小、波特率对齐 */
uint32_t ota_uart_tune_ack_timeout_ms(int baud, uint16_t chunk);
/** 固件 OTA 传输阶段无手机 RX 超时(ms),应 ≥ App ack 超时 + 余量 */
uint32_t ota_uart_tune_transfer_idle_ms(int baud);
#endif
This diff is collapsed.
...@@ -2,13 +2,46 @@ ...@@ -2,13 +2,46 @@
#define REMOTE_CONTROL_H #define REMOTE_CONTROL_H
#include "esp_err.h" #include "esp_err.h"
#include "cJSON.h"
/** BLE FFE1 / 遥控 JSON:单帧 UTF-8 最大字节数(不含结尾 NUL) */ /** BLE FFE1 / 遥控 JSON:单帧 UTF-8 最大字节数(不含结尾 NUL) */
#define REMOTE_CTRL_JSON_MAX_BYTES 2048 #define REMOTE_CTRL_JSON_MAX_BYTES 2048
/** 手机 → 设备:清空链路会话(中止 OTA、停车、丢弃武装态) */
#define REMOTE_CTRL_MSG_LINK_RESET 2
/** 手机 → 设备:串口/BLE 链路已连接(App 侧 open 成功);设备回 OTA 询问 JSON(1002) */
#define REMOTE_CTRL_MSG_LINK_READY 6
/** 遥控超时:超过该时间未收到控制 JSON 则停车(ms) */
#define RC_CMD_TIMEOUT_MS 500
/** 固件超时停车后,经链路推送行走全停 JSON:共 3 条,条间间隔(ms) */
#define RC_STOP_NOTIFY_BURST_COUNT 3
#define RC_STOP_NOTIFY_BURST_INTERVAL_MS 100
/** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */ /** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */
void remote_control_apply_json(const char *json); void remote_control_apply_json(const char *json);
/** 中止 OTA 并复位遥控超时状态(BLE 断开/卡顿恢复) */
void remote_control_link_reset(void);
/** 链路正在接收一条控制 JSON 的片段,用于避免分片期间误触发超时停车 */
void remote_control_note_rx_activity(void);
/** 构造停车专用 JSON(message_type=3,speed_val/steer_val 均为 0) */
char *remote_control_stop_json_malloc(void);
/** 注册停车状态 Notify 回调(BLE 0xFFE3 / UART 行);json 由回调方发送后勿 free */
typedef void (*remote_control_stop_notify_cb_t)(const char *json);
void remote_control_set_stop_notify_cb(remote_control_stop_notify_cb_t cb);
/** 停止周期停车 JSON 推送任务(UART 对端离线等) */
void remote_control_stop_notify_end(void);
/** 已解析的 JSON 根节点,json_raw 用于无 head 时的 OTA 等需原文场景 */
void remote_control_apply_json_root(cJSON *root, const char *json_raw);
/** /**
* @brief 启动遥控超时守护任务 * @brief 启动遥控超时守护任务
* 500ms 未收到控制命令将自动停止车辆 * 500ms 未收到控制命令将自动停止车辆
......
This diff is collapsed.
...@@ -17,4 +17,10 @@ esp_err_t init_spiffs(void); ...@@ -17,4 +17,10 @@ esp_err_t init_spiffs(void);
void nowifidata_start_config_web(void); void nowifidata_start_config_web(void);
void button_monitor_task_init(void); void button_monitor_task_init(void);
/**
* @brief 关闭配网 SoftAP 与 WiFi 驱动(BLE/UART 正常运行前调用)
* WiFi+MQTT 模式不调用;配网保存后 esp_restart 亦会清空状态。
*/
void provision_wifi_shutdown(void);
#endif #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 ...@@ -81,8 +81,10 @@ if "!VER!"=="unknown" if exist "%ROOT%\sdkconfig.defaults" call :ReadVersion "%R
set "UTC=unknown" 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" 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%" 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 {
>>"%REL%\manifest.json" echo "project": "ESPRCCar", >>"%REL%\manifest.json" echo "project": "ESPRCCar",
>>"%REL%\manifest.json" echo "chip": "esp32s3", >>"%REL%\manifest.json" echo "chip": "esp32s3",
...@@ -97,24 +99,26 @@ echo [3/4] Write manifest.json ... ...@@ -97,24 +99,26 @@ echo [3/4] Write manifest.json ...
call :AppendJsonFile "%OTA%\ESPRCCar.bin" call :AppendJsonFile "%OTA%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\bootloader.bin" call :AppendJsonFile "%FAC%\bootloader.bin"
call :AppendJsonFile "%FAC%\partition-table.bin" call :AppendJsonFile "%FAC%\partition-table.bin"
call :AppendJsonFile "%FAC%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\ota_data_initial.bin" call :AppendJsonFile "%FAC%\ota_data_initial.bin"
call :AppendJsonFile "%FAC%\storage.bin" last call :AppendJsonFile "%FAC%\storage.bin" last
>>"%REL%\manifest.json" echo ] >>"%REL%\manifest.json" echo ]
>>"%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 ... echo [4/5] Copy Android device doc ...
set "DOC_SRC=%ROOT%\docs\Android端设备对接文档_v!VER!.md" REM Chinese paths: use PowerShell (CMD GBK cannot open UTF-8 filenames)
if exist "!DOC_SRC!" ( powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0copy_firmware_doc.ps1" "%ROOT%" "!VER!"
copy /Y "!DOC_SRC!" "%REL%\Android端设备对接文档_v!VER!.md" >nul
echo docs -^> release\Android端设备对接文档_v!VER!.md
) else (
echo WARN: missing !DOC_SRC! (create docs\Android端设备对接文档_v!VER!.md before handoff)
)
echo [5/5] Write VERSION.txt ... 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" ( > "%REL%\VERSION.txt" (
echo ESPRCCar firmware release echo ESPRCCar firmware release
...@@ -134,16 +138,18 @@ echo Factory: firmware/release/factory/ ...@@ -134,16 +138,18 @@ echo Factory: firmware/release/factory/
echo tool: Espressif Flash Download Tools echo tool: Espressif Flash Download Tools
echo addrs: see factory/flash_args.txt echo addrs: see factory/flash_args.txt
echo. echo.
echo machine_readable: manifest.json echo machine_readable: ota_manifest.json
echo full_release_manifest: manifest.json
echo. echo.
echo android_doc: firmware/release/Android端设备对接文档_v!VER!.md echo android_doc: firmware/release/Android*_v!VER!.md (see release folder)
) )
echo. echo.
echo Done: firmware/release/ version=!VER! echo Done: firmware/release/ version=!VER!
echo VERSION.txt + manifest.json updated echo VERSION.txt + manifest.json + ota_manifest.json updated
echo. echo.
echo Note: run after idf.py build; build does not auto-copy here. echo Note: run after idf.py build; build does not auto-copy here.
echo. echo.
pause pause
exit /b 0 exit /b 0
...@@ -4,6 +4,9 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" ...@@ -4,6 +4,9 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB" CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
# Semantic version: OTA manifest / JSON body.version / esp_app_desc.version
CONFIG_MY_APP_VERSION="1.0.2"
# Link mode (one of: WIFI / BLE / UART) # Link mode (one of: WIFI / BLE / UART)
# CONFIG_APP_LINK_WIFI is not set # CONFIG_APP_LINK_WIFI is not set
# CONFIG_APP_LINK_BLE is not set # CONFIG_APP_LINK_BLE is not set
...@@ -14,9 +17,9 @@ CONFIG_APP_LINK_UART=y ...@@ -14,9 +17,9 @@ CONFIG_APP_LINK_UART=y
CONFIG_APP_UART_LINK_BAUDRATE=115200 CONFIG_APP_UART_LINK_BAUDRATE=115200
CONFIG_APP_UART_LINK_TX_GPIO=17 CONFIG_APP_UART_LINK_TX_GPIO=17
CONFIG_APP_UART_LINK_RX_GPIO=18 CONFIG_APP_UART_LINK_RX_GPIO=18
CONFIG_APP_PWM_IO15_SERVO=y # Default AUX: IO15=ESC (throttle), IO16=SERVO (steering). 1102 drive: IO10/21.
# CONFIG_APP_PWM_IO15_ESC is not set # CONFIG_APP_PWM_IO15_SERVO is not set
# GPIO16: 1102 steering servo uses this pin. Must be SERVO. If dual ESC uses 15/16, set CONFIG_APP_PWM_IO16_ESC=y CONFIG_APP_PWM_IO15_ESC=y
CONFIG_APP_PWM_IO16_SERVO=y CONFIG_APP_PWM_IO16_SERVO=y
# CONFIG_APP_PWM_IO16_ESC is not set # CONFIG_APP_PWM_IO16_ESC is not set
CONFIG_BT_ENABLED=y CONFIG_BT_ENABLED=y
......
# 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 ...@@ -21,13 +21,12 @@ CONFIG_LOG_DEFAULT_LEVEL=2
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set # CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
CONFIG_ESP_CONSOLE_NONE=y 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_RELEASE=y
# CONFIG_ROBO_APP_FW_DEBUG is not set # CONFIG_ROBO_APP_FW_DEBUG is not set
CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_BOOTLOADER_LEVEL=2
# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set # CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<input name='pass' type='password' placeholder='WiFi密码' %s> <input name='pass' type='password' placeholder='WiFi密码' %s>
<br> <br>
<b>设备 ID %s</b> <b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s> <input name='devid' placeholder='CN110200000001' value='%s' %s>
<button type='submit' class='btn'>保存并重启设备</button> <button type='submit' class='btn'>保存并重启设备</button>
</form> </form>
</div> </div>
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p> <p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p>
<form action='/save' method='POST'> <form action='/save' method='POST'>
<b>设备 ID %s</b> <b>设备 ID %s</b>
<input name='devid' placeholder='设备号' %s> <input name='devid' placeholder='CN110200000001' value='%s' %s>
<b>蓝牙广播名 %s</b> <b>蓝牙广播名 %s</b>
<input name='ble_name' placeholder='手机扫描看到的名称' %s> <input name='ble_name' placeholder='手机扫描看到的名称' %s>
<button type='submit' class='btn'>保存并重启</button> <button type='submit' class='btn'>保存并重启</button>
......
...@@ -2,22 +2,25 @@ ...@@ -2,22 +2,25 @@
<html> <html>
<head> <head>
<meta charset='UTF-8'> <meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<style> <style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; } body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; } .card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; } 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; } .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: 5%; margin-top: 10px; color: #555; } b { display: block; text-align: left; margin-left: 2.5%; margin-top: 10px; color: #555; }
.hint { text-align: left; margin: 8px 2.5% 0; font-size: 13px; color: #666; line-height: 1.5; }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<h1>串口设备配置</h1> <h1>串口设备配置</h1>
<p style="color:#666;font-size:14px;">UART 模式不保存 WiFi,仅需填写设备号</p> <p style="color:#666;font-size:14px;">UART 模式无需 WiFi,请填写完整设备号</p>
<form action='/save' method='POST'> <form action='/save' method='POST'>
<b>设备 ID %s</b> <b>设备号 %s</b>
<input name='devid' placeholder='请输入设备号' %s> <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> <button type='submit' class='btn'>保存并重启</button>
</form> </form>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment