Commit 8f3a151f authored by 957dd's avatar 957dd

Initial commit

parents
CompileFlags:
Remove: [-f*, -m*]
# =============================================================================
# ESP32-S3 + ESP-IDF 固件工程(ESPRCCar)
# =============================================================================
# 【建议提交】
# main/ docs/ www/ partitions.csv CMakeLists.txt
# sdkconfig.defaults sdkconfig.defaults.release main/idf_component.yml
# firmware/release/ ← 量产烧录 + OTA 用 .bin(见 firmware/README.md)
# 【不要提交】
# build/ managed_components/ dependencies.lock sdkconfig .devcontainer/
# =============================================================================
# --- ESP-IDF 构建输出(本地 idf.py build,勿入库)---
build/
**/build/
sdkconfig.old
*.map
*.elf
flasher_args.json
flash_project_args
flash_app_args
project_description.json
config.env
.gdbinit/
*.o
*.a
*.out
# 固件 .bin:默认忽略;仅 firmware/ 下发布包可提交(供 OTA / 产线下载)
*.bin
!firmware/
!firmware/**
# --- Component Manager(克隆后 idf.py build 会自动拉取)---
managed_components/
dependencies.lock
# --- Dev Container(仅 VS Code/Cursor 用 Docker 编译时可选,非必须)---
.devcontainer/
# --- 本地 ESP-IDF 工具/环境 ---
.espressif/
.idf/
# --- menuconfig 本地生成 ---
sdkconfig
# --- CMake / Ninja ---
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
install_manifest.txt
CTestTestfile.cmake
CMakeUserPresets.json
.ninja_deps
.ninja_log
# --- Python ---
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
.eggs/
venv/
.venv/
env/
.pytest_cache/
.coverage
htmlcov/
# --- IDE / 语言服务 ---
.vscode/
.idea/
*.iml
.clangd/
.ccls-cache/
compile_commands.json
.cursor/
# --- 日志与临时文件 ---
*.log
*~
*.swp
*.swo
*.bak
*.tmp
# --- 操作系统 ---
.DS_Store
.AppleDouble
.LSOverride
.directory
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.user
*.workspace
*.suo
*.sln.docstates
*.exe
.cache/
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESPRCCar)
This diff is collapsed.
# 文档已更名
# 文档已更名
Android 对接说明已合并为 **三种链路模式**(WiFi + MQTT / BLE / UART),请使用:
**[Android端设备对接文档_v1.0.1.md](./Android端设备对接文档_v1.0.1.md)**
旧文件名 `Android端蓝牙对接文档_v1.0.1.md` 仅保留 BLE 章节含义,已不再单独维护。
This diff is collapsed.
# 安装与配网指南
# 安装与配网指南
本文说明如何在本机编译、烧录 ESP32-S3 固件,以及三种链路模式下的**首次配网****重新配网**。Android 协议细节见 [`Android端设备对接文档_v1.0.1.md`](Android端设备对接文档_v1.0.1.md)
---
## 1. 环境准备
| 项 | 要求 |
|----|------|
| 芯片 | ESP32-S3 |
| ESP-IDF | **v5.5.x**(与团队 `.vscode/settings.json``idf.espIdfPathWin` 一致) |
| Flash | **16MB**(见 `partitions.csv`) |
| 系统 | Windows 10/11(下文以 PowerShell/CMD 为例) |
### 1.1 配置 ESP-IDF 环境
1. 安装 [ESP-IDF Windows 工具安装器](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/get-started/windows-setup.html),或确认本机已有 `esp\v5.5.x\esp-idf`
2. 将用户环境变量 **`IDF_PATH`** 指向真实的 `esp-idf` 根目录(勿使用已删除的旧路径)。
3. 每次编译前激活环境(二选一):
- 开始菜单 → **「ESP-IDF 5.5 PowerShell」** / **CMD**
- 项目目录执行:`call <IDF_PATH>\export.bat`(路径按本机修改)
若提示找不到 `idf.py`,参见 [`编译类型与UART模式配置指南.md`](编译类型与UART模式配置指南.md) §2.0。
### 1.2 克隆与进入工程
```bat
cd /d d:\myproject\esp32project\Android_control_driver
```
首次构建会由 `sdkconfig.defaults` 生成 `sdkconfig`;已存在 `sdkconfig` 时以本地保存的配置为准。
---
## 2. 选择链路模式(编译期三选一)
**`idf.py menuconfig`****`esp32s3_MYSDK`****链路模式** 中三选一。**同一时间仅一种模式编入固件**
| 模式 | menuconfig | 与 Android 通信 | 仓库默认(`sdkconfig.defaults`) |
|------|------------|-----------------|----------------------------------|
| WiFi + MQTT | `APP_LINK_WIFI` | 路由器 + MQTT | 否 |
| BLE | `APP_LINK_BLE` | NimBLE GATT | 否 |
| **UART 串口** | `APP_LINK_UART` | UART1:GPIO17=TX、GPIO18=RX | **是** |
切换模式后建议:
```bat
idf.py fullclean
idf.py build
```
> **注意**:`APP_LINK_UART`(业务串口 GPIO17/18)与 **UART0 调试口**(GPIO43/44,下载/日志)不是同一路。调试口说明见 [`编译类型与UART模式配置指南.md`](编译类型与UART模式配置指南.md)。
---
## 3. 编译与开发烧录
### 3.1 编译
```bat
cd /d d:\myproject\esp32project\Android_control_driver
idf.py build
```
**Release 量产构建**(可选):
```bat
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
### 3.2 USB 串口烧录(开发推荐)
```bat
idf.py -p COM16 flash monitor
```
| 命令 | 说明 |
|------|------|
| `idf.py -p COM16 flash` | 烧录 bootloader + 分区表 + 应用 + **SPIFFS(配网页)** |
| `idf.py -p COM16 flash_app` | 仅烧应用(**不更新** `www` 配网页,改 HTML 后勿单独用这个) |
| `idf.py -p COM16 erase_flash` | 擦除整片(清空 NVS,会重新进配网) |
**进下载模式**:按住 **BOOT** → 按 **RESET** → 松开 **BOOT**;部分开发板可自动进入。
烧录失败时请检查:数据线、COM 口、驱动、是否被占用。
### 3.3 修改配网页后
配网 UI 来自 `www/`,经 CMake 打入 SPIFFS 分区 `storage``main/CMakeLists.txt``spiffs_create_partition_image`)。
修改 `www/index*.html` 后须 **完整 `idf.py build` + `flash`**(或至少包含 `storage.bin` 的烧录),仅 `flash_app` **不会**更新配网页。
---
## 4. 配网(SoftAP + 网页)
三种模式在缺少必填 NVS 时,都会开启 **SoftAP 热点**(SSID 默认 `esp32-apconfig`,可在 menuconfig **`ROBITO WIFI SSID`** 修改)。手机连接热点后浏览器访问:
**http://192.168.4.1**
(设备侧 DNS 会劫持域名,直接输 IP 即可。)
### 4.1 各模式配网页与必填项
| 链路模式 | SPIFFS 页面 | 页面上需填写 | 写入 NVS |
|----------|-------------|--------------|----------|
| **WiFi** | `www/index.html` | WiFi 名称、WiFi 密码、设备 ID | `wifi_ssid``wifi_pass``device_id` |
| **BLE** | `www/index_ble.html` | 设备 ID、蓝牙广播名 | `device_id``ble_adv_name` |
| **UART** | `www/index_uart.html` | **仅设备 ID** | `device_id` |
**UART / BLE 模式说明**
- 热点仅用于**写入 NVS**,不是让设备长期连路由器。
- **UART 模式**配网页**不出现** WiFi 名称/密码;正常运行时不启 BLE、不连 STA。
- 提交表单后设备约 1.5s 后自动重启。
### 4.2 何时自动进入配网
| 模式 | 条件 |
|------|------|
| WiFi | NVS 无 `wifi_ssid` |
| BLE | `device_id``ble_adv_name` 为空 |
| UART | `device_id` 为空 |
### 4.3 运行时再次进入配网(按键)
| 模式 | 操作 |
|------|------|
| **UART** | **GPIO4** 长按约 **2 秒** → 开热点,仅可改 **设备 ID** |
| **WiFi** | **GPIO0** 长按约 2 秒 → 可改 WiFi + 设备 ID;**GPIO4** → 仅设备 ID |
| **BLE** | **GPIO0****GPIO4** 长按约 2 秒 → 可改设备 ID / 蓝牙名(逻辑见 `wifidevnum_config.c`) |
UART 模式下进入配网时会暂停 UART1 业务通信,配网完成后重启恢复。
---
## 5. 量产烧录(无需本地编译)
产线或测试人员可直接使用仓库内已编译包,无需安装 ESP-IDF:
1. 打开 [`firmware/README.md`](../firmware/README.md)
2. 下载 `firmware/release/factory/` 下全部 `.bin`
3. 使用 **乐鑫 Flash Download Tools**,按 `flash_args.txt` 地址烧录(含 `storage.bin` 配网页)。
维护者更新发布包:
```bat
idf.py build
scripts\copy_firmware_release.bat
```
核对 `firmware/release/VERSION.txt` 后再提交 Git。
---
## 6. 烧录后自检
| 模式 | 建议检查 |
|------|----------|
| **UART** | 串口 115200/8/N/1;GPIO17→模块 RX、GPIO18→模块 TX;能收到心跳 JSON;`device_id` 与 App 一致 |
| **BLE** | 能扫描到 `ble_adv_name` + 后缀 `MP`;GATT 0xFFE1 可写 JSON |
| **WiFi** | 串口日志见 STA 获 IP;MQTT 订阅成功 |
协议与引脚详见 [`Android端设备对接文档_v1.0.1.md`](Android端设备对接文档_v1.0.1.md)
---
## 7. 常见问题
| 现象 | 处理 |
|------|------|
| UART 模式热点里仍有 WiFi 输入框 | 固件未按 `APP_LINK_UART` 编译,或 SPIFFS 未更新 → `menuconfig` 选 UART 后 `fullclean` + `build` + `flash` |
| 改了 `www` 但网页不变 | 勿只用 `flash_app`;需完整烧录含 `storage` 的镜像 |
| `idf.py` 找不到 | 检查 `IDF_PATH``export.bat` |
| 切换链路模式后行为异常 | `idf.py fullclean` 后重新 `build` |
---
**文档版本**:1.1
**最后更新**:2026年5月
# 编译类型与 UART 模式配置指南
# 编译类型与 UART 模式配置指南
本文档说明如何使用项目的 **Release/Debug 构建类型****调试串口复用** 功能。
> **安装、烧录、三种链路模式选择与 SoftAP 配网**(含 UART 仅填设备号)见 **[`安装与配网指南.md`](安装与配网指南.md)**。
> 本文中的「UART」若未特别说明,指 **UART0 调试口(GPIO43/44)**;与业务链路 **`APP_LINK_UART`(UART1,GPIO17/18)** 不是同一路。
---
## 0. 串口是什么?和板子上的 RX1、TX1 是什么关系?
日常说的 **「串口」** 一般指 **UART**(异步串行口):用一根 **TX(发)** 和一根 **RX(收)** 加上地线,按约定波特率(如 115200)收发字节流。
### 本项目使用 UART0(默认调试串口)
| 项目 | 值 | 说明 |
|------|-----|------|
| **UART** | UART0 | ESP32-S3 默认下载/调试串口 |
| **TX** | GPIO43 | 固定引脚,不可更改 |
| **RX** | GPIO44 | 固定引脚,不可更改 |
| **用途** | 调试打印 / 普通通信 | Release 模式下可切换 |
**UART0 是 ESP32-S3 的默认串口**,用于:
- 固件下载(boot 模式)
- `ESP_LOG` / `printf` 调试输出
- 本项目 Release 模式下可选为普通通信串口
---
## 1. 功能概述
通过 `menuconfig` 可以选择:
1. **Debug 构建**(默认):-Og 优化,完整调试日志输出
2. **Release 构建**:-Os 优化,最小代码体积,可选将 **指定 UART** 用作通信串口
---
## 2. 配置方法
### 2.0 PowerShell 提示找不到 `...\esp\esp-idf\tools\idf.py`
说明当前终端里 **`IDF_PATH` 或 `idf.py` 启动方式仍指向不存在的目录**(常见为旧路径 `C:\Users\<你>\esp\esp-idf`),而本机 ESP-IDF 实际在 **`...\esp\v5.5.1\esp-idf`**(与 `.vscode/settings.json`**`idf.espIdfPathWin`** 一致)。
**处理**:在 Windows **用户环境变量**中将 **`IDF_PATH`** 改为上述真实 **`esp-idf` 根目录**;打开「开始菜单」里的 **「ESP-IDF PowerShell / CMD」**(随 ESP-IDF 安装生成)再执行 `idf.py`,或删除 PATH 里指向旧 `esp-idf` 的重复项后重开终端。Cursor/VS Code 也可用扩展命令 **「ESP-IDF: Open ESP-IDF Terminal」**
### 2.1 使用 Menuconfig(推荐开发时使用)
```bash
idf.py menuconfig
# → robot-esp32s3
# → 编译优化级别
# - Debug(Og 优化,调试)
# - Release(Os 优化,最小体积)
#
# 选择 Release 后会出现额外选项:
# → 调试打印串口作为普通通信串口使用(仅 Release 有效)
```
### 2.2 使用 SDKCONFIG 覆盖文件(推荐 CI/CD 使用)
**Debug 版本:**
```bash
# 使用默认 sdkconfig.defaults
idf.py build
```
**Release 版本:**
```bash
# 方式1:在 menuconfig 中选择 Release 后保存
idf.py build
# 方式2:使用覆盖文件(自动化构建)
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
---
## 3. Release 模式下 UART 串口两种工作模式
### 3.1 模式 A:Release 默认(无 UART 日志,BLE 转发 W/E)
**配置:**
- `ROBO_APP_FW_RELEASE=y`
- 使用 `sdkconfig.defaults.release`(含 **`CONFIG_ESP_CONSOLE_NONE=y`**
- `APP_UART_MODE_DEBUG=n`(默认)
**行为:**
- **不向串口输出** `ESP_LOG`(避免无人读取时阻塞 BLE)
- `ESP_LOGW` / `ESP_LOGE` 在 BLE 已连接时经 **0xFFE3 Notify** 推送(`message_type` 4/5,见 `docs/Android端设备对接文档_v1.0.1.md` §3.5-B)
- 周期心跳仍为 **0xFFE3**,约 3s,`message_type` 1
### 3.2 模式 B:UART0 转为普通通信串口
**配置:**
- `ROBO_APP_FW_RELEASE=y`(menuconfig 中「编译优化级别」选 Release)
- `APP_UART_MODE_DEBUG=y`(启用此选项)
**行为:**
- **UART0**`uart_comm` 接管做普通收发(GPIO43/44)
- `ESP_LOG` / `printf` 等日志输出不可用(没有日志输出)
- 代码中使用 `uart_comm.h` 接口进行数据传输
- 固件下载仍使用 UART0(bootrom 固定)
**完整配置步骤:**
```bash
idf.py menuconfig
# 1. robot-esp32s3 → 编译优化级别 → Release
# 2. robot-esp32s3 → 调试打印串口作为普通通信串口使用 → 启用
# 3. Component config → ESP System Settings → Channel for console output → None
# 保存并退出
```
---
## 4. 代码中的条件编译
项目提供了 `build_config.h` 头文件,包含预定义的宏:
### 4.1 头文件位置
```c
#include "core/build_config.h"
```
### 4.2 可用宏定义
| 宏 | 含义 |
|----|------|
| `BUILD_IS_RELEASE` | 1=Release 构建,0=Debug 构建 |
| `BUILD_IS_DEBUG` | 1=Debug 构建,0=Release 构建 |
| `UART_MODE_COMMUNICATION` | 1=UART 作为通信串口,0=默认 |
| `UART_MODE_DEBUG` | 1=调试串口模式,0=通信模式 |
| `BUILD_SERIAL_LOG_ENABLED` | 1=可向 UART 打日志,0=Release 默认关闭 |
### 4.3 条件日志宏
无论何种模式,都可以使用统一的日志宏:
```c
LOG_I(TAG, "信息日志"); // Info 级别
LOG_W(TAG, "警告日志"); // Warning 级别
LOG_E(TAG, "错误日志"); // Error 级别
LOG_D(TAG, "调试日志"); // Debug 级别
```
**Release + UART 通信模式** 下,这些宏自动展开为空操作(不输出任何内容)。
在其他模式下,这些宏等价于 `ESP_LOGI` / `ESP_LOGW` / `ESP_LOGE` / `ESP_LOGD`
### 4.4 UART 通信 API(仅在 Release+通信模式下有效)
```c
#include "drivers/uart_comm/uart_comm.h"
/* 初始化(波特率,0 表示使用默认值 115200) */
esp_err_t err = uart_comm_init(115200);
/* 发送数据 */
uart_comm_send((uint8_t*)data, len);
/* 发送字符串 */
uart_comm_send_string("Hello\r\n");
/* 接收数据(非阻塞,timeout_ms 超时) */
uint8_t buf[256];
int len = uart_comm_receive(buf, sizeof(buf), 100); // 100ms 超时
/* 反初始化 */
uart_comm_deinit();
```
在非 Release+通信模式下,这些函数返回 `ESP_ERR_NOT_SUPPORTED` 或 -1。
---
## 5. 完整示例代码
### 5.1 条件编译示例
```c
#include "core/build_config.h"
#include "drivers/uart_comm/uart_comm.h"
void my_init(void)
{
#if BUILD_IS_RELEASE
#if UART_MODE_COMMUNICATION
/* Release + 通信模式:初始化 UART 通信 */
uart_comm_init(115200);
uart_comm_send_string("System started\r\n");
#else
/* Release + 调试模式:减少日志输出 */
ESP_LOGI(TAG, "Release build started");
#endif
#else
/* Debug 模式:完整日志 */
ESP_LOGI(TAG, "Debug build started");
#endif
}
```
### 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.1 Debug 构建
```
[1/1] Linking project.elf
构建完成,固件大小约:
- .bin 文件:~2.5MB
- 内存占用:~180KB
```
### 6.2 Release 构建
```
[1/1] Linking project.elf
构建完成,固件大小约:
- .bin 文件:~2.1MB(减少约 15-20%)
- 内存占用:~150KB
```
---
## 7. 注意事项
### 7.1 从 Debug 切换到 Release 时
1. 执行完整清理:
```bash
idf.py fullclean
```
2. 如果使用 `sdkconfig.defaults.release`,确保重新生成 `sdkconfig`
```bash
rm sdkconfig
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
```
### 7.2 UART 通信模式下的调试
启用 `APP_UART_MODE_DEBUG` 后:
- **无法使用 `idf.py monitor` 查看日志**(因为没有日志输出)
- **烧录时仍然使用 UART0**,请确保外部设备在烧录时不会干扰
- 建议在开发阶段先用 **模式 A(调试打印)** 验证功能,再切换到 **模式 B(通信)**
### 7.3 推荐的开发流程
```
阶段1:功能开发
→ 使用 Debug 构建,完整日志输出
阶段2:性能优化
→ 使用 Release + 调试打印模式
→ 验证功能正确性,检查是否有 Release 模式特有 bug
阶段3:量产准备
→ 使用 Release + UART 通信模式(如需要)
→ 最终验证,关闭所有调试功能
```
---
## 8. 故障排除
### Q: 切换到 Release 后代码运行异常?
A: 检查是否有以下问题:
- 断言被禁用导致错误未捕获(`CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y`
- 代码依赖 Debug 模式下的未初始化变量清零(Debug 模式通常内存初始化为 0)
- 优化导致的时序问题(如未加 `volatile` 的硬件寄存器访问)
### Q: 启用 UART 通信模式后无法烧录?
A: 正常现象。启用 `CONFIG_ESP_CONSOLE_UART_NONE` 后固件启动时不会占用 UART0,但 **esptool 烧录过程不受影响**。烧录时确保:
- 外部设备未连接 UART0,或已断开
- 正确进入下载模式(BOOT 按钮)
### Q: 如何临时恢复日志输出进行调试?
A: 在 `menuconfig` 中:
1. 关闭 **调试打印串口作为普通通信串口使用**
2. 或重新选择 **Debug** 构建类型
---
**文档版本**:1.1
**最后更新**:2026年5月
# 固件发布包(Git 可下载)
本目录存放 **准备提交到 Git 的已编译二进制**,与本地 `build/` 分离:
| 目录 | 进 Git? | 何时更新 |
|------|----------|----------|
| `build/` | 否 | 每次 `idf.py build` 自动覆盖 |
| `firmware/release/` | **是** | **仅**在你手动运行复制脚本时更新(见下) |
> **`idf.py build` 不会自动往 `firmware/` 里拷文件。**
> 日常编译只用 `build/`;要发版、给产线/安卓下载时,编译成功后执行一次脚本即可。
## 版本信息(每次运行脚本会刷新)
| 文件 | 说明 |
|------|------|
| **`release/VERSION.txt`** | 人类可读:版本号、编译时间、OTA 文件大小与 SHA256 |
| **`release/manifest.json`** | 机器可读:同上 + 全部 factory 文件校验 |
| **`release/Android端设备对接文档_v<版本>.md`** | 与固件同版本的 Android 对接说明(WiFi/BLE/UART,脚本从 `docs/` 复制) |
版本号来自 menuconfig 项 **`CONFIG_MY_APP_VERSION`**`main/Kconfig.projbuild`,默认在 `sdkconfig.defaults` 里为 `1.0.1`)。发版前请先改版本再 `idf.py build`
## 目录说明
| 路径 | 内容 |
|------|------|
| `release/ota/ESPRCCar.bin` | **BLE OTA / Android 升级**(仅应用镜像) |
| `release/factory/*.bin` | **量产烧录**(乐鑫 Flash Download Tools 加载) |
| `release/factory/flash_args.txt` | 烧录地址摘要(与 `build/flash_args` 一致) |
## 烧录地址(ESP32-S3,16MB Flash)
| 文件 | 偏移 |
|------|------|
| `bootloader.bin` | `0x0` |
| `partition-table.bin` | `0x8000` |
| `ota_data_initial.bin` | `0xf000` |
| `ESPRCCar.bin` | `0x20000` |
| `storage.bin` | `0xc20000` |
Flash:`dio``80m``16MB`。产线请使用 **乐鑫 Flash Download Tools**,按上表地址加载 `factory/` 内各文件。
## 更新发布包(维护者)
```bat
idf.py build
scripts\copy_firmware_release.bat
```
脚本会刷新 **`VERSION.txt`****`manifest.json`**。打开 `VERSION.txt` 核对版本与时间;**Git 提交由你自行操作**,脚本不会执行任何 git 命令。
## 他人使用
- **OTA**:下载 `release/ota/ESPRCCar.bin`(版本看 `VERSION.txt`)。
- **Android 联调**:同目录下的 **`Android端设备对接文档_v<版本>.md`** + `VERSION.txt` / `manifest.json`
- **量产**:下载 `release/factory/` 全部 bin,用 Flash Download Tools 烧录。
- **安装与配网**(环境、三种链路、热点填什么):见仓库 [`docs/安装与配网指南.md`](../docs/安装与配网指南.md)
ESPRCCar firmware release
=========================
version: 1.0.1
project: ESPRCCar
chip: esp32s3
built_utc: 2026-05-19T07:53:23Z
built_local: 2026/05/19 ܶ 15:53:23.22
git_revision: (no git)
OTA:
path: firmware/release/ota/ESPRCCar.bin
bytes: 1115760
sha256: 724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6
Factory: firmware/release/factory/
tool: Espressif Flash Download Tools
addrs: see factory/flash_args.txt
machine_readable: manifest.json
--flash_mode dio --flash_freq 80m --flash_size 16MB
0x0 bootloader/bootloader.bin
0x20000 ESPRCCar.bin
0x8000 partition_table/partition-table.bin
0xf000 ota_data_initial.bin
0xc20000 storage.bin
{
"project": "ESPRCCar",
"chip": "esp32s3",
"flash_mode": "dio",
"flash_freq": "80m",
"flash_size": "16MB",
"version": "1.0.1",
"git_revision": "",
"generated_utc": "2026-05-19T07:53:23Z",
"ota_image": "release/ota/ESPRCCar.bin",
"factory_dir": "release/factory",
"files": [
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"},
{"name":"bootloader.bin","bytes":21056,"sha256":"740f1b3c78ca63acc0a5e3c788bf18f77585a28504e5d8722ac253991c6f1f7a"},
{"name":"partition-table.bin","bytes":3072,"sha256":"c400c9ed7d2eb335cd057036a54335938084f5120b4d2c2ca176419dfde69124"},
{"name":"ESPRCCar.bin","bytes":1115760,"sha256":"724e749fb1567e7f29a8f950ee29c2d4545af63497569a36e5bb638133cf6df6"},
{"name":"ota_data_initial.bin","bytes":8192,"sha256":"7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f"},
{"name":"storage.bin","bytes":327680,"sha256":"05be75d1159d4b3043e4b3847f7d8307b4a4955ae30c1baf93ce42eafb1308cf"}
]
}
set(SOURCES
"core/system_init.c"
"core/task_manager.c"
"provision/wifidevnum_config.c"
"device/device_model.c"
"protocol/remote_control.c"
"protocol/heart_payload.c"
"drivers/driver_manager.c"
"drivers/gpiotrol/rc_pwm_control.c"
"drivers/gpiotrol/devices/device_1201.c"
"drivers/gpiotrol/devices/device_1101.c"
"drivers/gpiotrol/devices/device_1102.c"
"drivers/gpiotrol/betteryread.c"
"drivers/uart_comm/uart_comm.c"
"app/app_run.c"
"app/example_uart_comm_usage.c"
"main.c"
"ota_manager.c"
"ota_binary_stream.c")
if(CONFIG_APP_LINK_BLE)
list(APPEND SOURCES "link_ble/link_ble.c")
elseif(CONFIG_APP_LINK_UART)
list(APPEND SOURCES "link_uart/link_uart.c")
else()
list(APPEND SOURCES "link_wifi/mqttconf_commun.c" "ota.c")
endif()
set(INCLUDE_DIRS
"."
"core"
"device"
"provision"
"protocol"
"app"
"drivers"
"drivers/gpiotrol"
"drivers/gpiotrol/devices"
"drivers/uart_comm")
if(CONFIG_APP_LINK_BLE)
list(APPEND INCLUDE_DIRS "link_ble")
elseif(CONFIG_APP_LINK_UART)
list(APPEND INCLUDE_DIRS "link_uart")
else()
list(APPEND INCLUDE_DIRS "link_wifi")
endif()
set(MAIN_REQUIRES
driver
app_update
nvs_flash
esp_http_server
spiffs
esp-tls
esp_netif
esp_event
esp_wifi
esp_adc
json)
set(MAIN_PRIV_REQUIRES)
# 避免配置切换/缓存导致 main 编译 link_ble.c 时丢失 bt 头文件路径,
# 这里始终将 bt 放入 main 的私有依赖。
list(APPEND MAIN_PRIV_REQUIRES bt)
if(CONFIG_APP_LINK_WIFI)
list(APPEND MAIN_REQUIRES mqtt esp_https_ota esp_http_client)
endif()
idf_component_register(SRCS ${SOURCES}
INCLUDE_DIRS ${INCLUDE_DIRS}
REQUIRES ${MAIN_REQUIRES}
PRIV_REQUIRES ${MAIN_PRIV_REQUIRES})
spiffs_create_partition_image(storage ../www FLASH_IN_PROJECT)
menu "esp32s3_MYSDK"
config ROBIOT_WIFI_SSID
string "ROBITO WIFI SSID"
default "esp32-apconfig"
help
Hotspot Name Settings.
config ROBOIOT_MQTT_URL
string "MQTT URL"
default "https://fcrs-api.yd-ss.com/device/getConfig?deviceNo="
help
MQTT Config URL
config MY_APP_VERSION
string "Project Version"
default "1.0.1"
help
The version number of this firmware.
config APP_DEBUG_UART_NUM
int "调试与通信 UART 外设编号"
default 0
range 0 2
help
ESP32-S3 片上有多路 UART:UART0、UART1、UART2。
UART0 默认引脚 GPIO43(TX)/GPIO44(RX),用于下载和调试打印。
UART1 默认引脚 GPIO17(TX)/GPIO18(RX),在 UART 链路模式下用于与安卓通信。
本项目使用 UART0 作为调试/通信串口,Release 下可切换为普通通信模式。
ESP_LOG / printf 走哪一路 UART,由 menuconfig「Component config → ESP System Settings」
里控制台 UART 编号决定;请与本项保持一致。
config APP_DEBUG_UART_TX_GPIO
int "调试串口 TX GPIO 编号(UART0 无需设置,保持 -1)"
default -1
range -1 48
help
UART0 固定使用 GPIO43(TX)/GPIO44(RX),无需设置。
UART1 模式下使用 GPIO17(TX)。
此项保留用于兼容性,请保持为 -1。
config APP_DEBUG_UART_RX_GPIO
int "调试串口 RX GPIO 编号(UART0 无需设置,保持 -1)"
default -1
range -1 48
help
UART0 固定使用 GPIO43(TX)/GPIO44(RX),无需设置。
UART1 模式下使用 GPIO18(RX)。
此项保留用于兼容性,请保持为 -1。
config APP_UART_LINK_BAUDRATE
int "UART 链路模式波特率"
default 115200
range 9600 921600
help
UART 链路模式下的通信波特率。默认 115200。
适用于 APP_LINK_UART 模式下的 UART1 通信。
config APP_UART_LINK_TX_GPIO
int "UART 链路 TX GPIO 编号"
default 17
range -1 48
help
UART 链路模式下 TX 引脚。UART1 默认使用 GPIO17。
ESP32-S3 UART1 默认 TX 为 GPIO17。
config APP_UART_LINK_RX_GPIO
int "UART 链路 RX GPIO 编号"
default 18
range -1 48
help
UART 链路模式下 RX 引脚。UART1 默认使用 GPIO18。
ESP32-S3 UART1 默认 RX 为 GPIO18。
choice APP_LINK_MODE
prompt "链路模式(编译期三选一)"
default APP_LINK_WIFI
help
WiFi:STA + MQTT。BLE:仅 NimBLE 与安卓 JSON,不走 STA/MQTT;配网页仅设备号与蓝牙广播名。
UART:使用串口1 (GPIO17 TX / GPIO18 RX) 与安卓通信,无需蓝牙或WiFi。
config APP_LINK_WIFI
bool "WiFi + MQTT"
help
保存 WiFi 后联网,MQTT 控制。
config APP_LINK_BLE
bool "BLE(NimBLE)"
help
热点网页仅写入 device_id 与 ble_adv_name;正常运行时不连 WiFi、不启 MQTT。
config APP_LINK_UART
bool "UART 串口(GPIO17 TX / GPIO18 RX)"
help
使用 UART1 (GPIO17=TX, GPIO18=RX) 与安卓进行串口通信。
无需蓝牙或WiFi,适合有线连接场景。
长按设备重置按键可进入配网模式重置设备号。
endchoice
config APP_BLE_OTA
bool "启用 BLE GATT OTA(0xFFE2)与状态 Notify(0xFFE4)"
depends on APP_LINK_BLE
default y
help
允许手机通过 0xFFE2 写入固件分包升级;每步成功后可通过 0xFFE4 Notify 收到 JSON 应答摘要。
关闭后向 0xFFE2 写入将返回 Write Not Permitted;0xFFE4 仍可发现,但无 OTA 过程推送。
choice APP_PWM_IO15_ROLE
prompt "GPIO15 PWM 角色"
default APP_PWM_IO15_SERVO
help
选择 GPIO15 在 50Hz PWM 下的用途。
config APP_PWM_IO15_SERVO
bool "舵机(初始化 90 度 / 1500us)"
config APP_PWM_IO15_ESC
bool "电调 ESC(初始化 1500us)"
endchoice
choice APP_PWM_IO16_ROLE
prompt "GPIO16 PWM 角色"
default APP_PWM_IO16_ESC
help
选择 GPIO16 在 50Hz PWM 下的用途。
config APP_PWM_IO16_SERVO
bool "舵机(初始化 90 度 / 1500us)"
config APP_PWM_IO16_ESC
bool "电调 ESC(初始化 1500us)"
endchoice
choice ROBO_APP_FW_BUILD
prompt "编译优化级别(固件 Debug/Release)"
default ROBO_APP_FW_DEBUG
help
注意:勿使用名称 APP_BUILD_TYPE,与 ESP-IDF 全局 Kconfig 冲突。
Debug:-Og;Release:-Os(另见 sdkconfig.defaults.release 或 menuconfig 编译器选项)。
config ROBO_APP_FW_DEBUG
bool "Debug(Og 优化,调试)"
help
优化级别 -Og,保留调试符号,适合开发和调试。
config ROBO_APP_FW_RELEASE
bool "Release(Os 优化,最小体积)"
help
优化级别 -Os,最小代码体积,适合量产发布。
默认关闭 UART 控制台(见 sdkconfig.defaults.release),W/E 日志经 BLE 0xFFE3 Notify 推送。
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
#include "app_run.h"
#include "sdkconfig.h"
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "core/system_init.h"
#include "core/build_config.h"
#include "wifidevnum_config.h"
#include "device_nvs.h"
#include "driver_manager.h"
#include "ota_manager.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define OTA_CONFIRM_DELAY_MS 30000
#if CONFIG_APP_LINK_WIFI
#include "mqttconf_commun.h"
#endif
#if CONFIG_APP_LINK_BLE
#include "link_ble.h"
#endif
#if CONFIG_APP_LINK_UART
#include "link_uart.h"
#endif
extern void example_uart_comm_start(void);
static const char *tag_s = "APP";
#if CONFIG_APP_LINK_WIFI
static bool init_task_triggered_s = false;
static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)data;
ESP_LOGI(tag_s, "获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
char devid_check[32] = {0};
if (read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid_check, sizeof(devid_check)) == ESP_OK &&
strlen(devid_check) > 0) {
if (!init_task_triggered_s) {
ESP_LOGI(tag_s, "启动 MQTT");
mqtt_manager_init_sequence();
init_task_triggered_s = true;
}
} else {
ESP_LOGE(tag_s, "device_id 为空,无法启动 MQTT");
}
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGW(tag_s, "WiFi 断开,重连...");
esp_wifi_connect();
}
}
#endif
#if CONFIG_APP_LINK_WIFI
static void run_wifi_mode(void)
{
char ssid[32] = {0}, pass[64] = {0};
esp_err_t wifi_ret = read_from_nvs(DEVICE_CFG_KEY_WIFI_SSID, ssid, sizeof(ssid));
if (wifi_ret == ESP_OK) {
read_from_nvs(DEVICE_CFG_KEY_WIFI_PASS, pass, sizeof(pass));
esp_netif_create_default_wifi_sta();
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL);
esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &wifi_event_handler, NULL,
NULL);
button_monitor_task_init();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
wifi_config_t sta_cfg = {
.sta =
{
.scan_method = WIFI_ALL_CHANNEL_SCAN,
.sort_method = WIFI_CONNECT_AP_BY_SIGNAL,
},
};
strncpy((char *)sta_cfg.sta.ssid, ssid, sizeof(sta_cfg.sta.ssid));
strncpy((char *)sta_cfg.sta.password, pass, sizeof(sta_cfg.sta.password));
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &sta_cfg);
esp_wifi_start();
esp_wifi_connect();
} else {
ESP_LOGI(tag_s, "无 WiFi 配置,启动配网页");
nowifidata_start_config_web();
}
}
#endif
#if CONFIG_APP_LINK_BLE
static void run_ble_mode(void)
{
char dev[32] = {0};
char ble[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
read_from_nvs(DEVICE_CFG_KEY_BLE_ADV_NAME, ble, sizeof(ble));
if (strlen(dev) == 0 || strlen(ble) == 0) {
ESP_LOGI(tag_s, "需配置 device_id 与蓝牙名,启动热点配网");
nowifidata_start_config_web();
return;
}
button_monitor_task_init();
if (link_ble_start() != ESP_OK) {
ESP_LOGE(tag_s, "BLE 启动失败");
}
}
#endif
#if CONFIG_APP_LINK_UART
static void run_uart_mode(void)
{
char dev[32] = {0};
read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, dev, sizeof(dev));
if (strlen(dev) == 0) {
ESP_LOGI(tag_s, "UART 模式: 需配置 device_id,启动配网热点");
nowifidata_start_config_web();
return;
}
ESP_LOGI(tag_s, "UART 模式: device_id=%s", dev);
/* 启动按键监控任务(长按重置设备号) */
button_monitor_task_init();
/* 启动 UART 链路通信 */
if (link_uart_start() != ESP_OK) {
ESP_LOGE(tag_s, "UART 链路启动失败");
}
}
#endif
static void ota_confirm_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(OTA_CONFIRM_DELAY_MS));
if (ota_manager_get_state() == OTA_STATE_SUCCESS) {
ESP_LOGI(tag_s, "OTA 稳定运行,确认新固件");
ota_manager_mark_success();
}
vTaskDelete(NULL);
}
static void schedule_ota_confirm_if_needed(void)
{
if (ota_manager_get_state() == OTA_STATE_SUCCESS) {
xTaskCreate(ota_confirm_task, "ota_confirm", 3072, NULL, 1, NULL);
}
}
void app_run(void)
{
ESP_ERROR_CHECK(system_core_init());
ESP_ERROR_CHECK(driver_manager_init_from_nvs());
#if BUILD_SERIAL_LOG_ENABLED
#if BUILD_IS_RELEASE
ESP_LOGI(tag_s, "构建类型: Release (Os优化)");
#if UART_MODE_COMMUNICATION
ESP_LOGI(tag_s, "UART模式: 普通通信串口");
#else
ESP_LOGI(tag_s, "UART模式: 调试打印");
#endif
#else
ESP_LOGI(tag_s, "构建类型: Debug (Og优化)");
ESP_LOGI(tag_s, "UART模式: 调试打印");
#endif
#endif
#if CONFIG_APP_LINK_WIFI
run_wifi_mode();
#elif CONFIG_APP_LINK_BLE
run_ble_mode();
#elif CONFIG_APP_LINK_UART
run_uart_mode();
#else
#error "请在 menuconfig 中选择 APP_LINK_WIFI / APP_LINK_BLE / APP_LINK_UART"
#endif
#if BUILD_IS_RELEASE && UART_MODE_COMMUNICATION
example_uart_comm_start();
#endif
schedule_ota_confirm_if_needed();
}
#ifndef APP_RUN_H
#define APP_RUN_H
void app_run(void);
#endif
/*
* 示例:Release 模式下将「APP_DEBUG_UART_NUM」这一路 UART 作为普通通信串口
*
* 编译条件:
* - CONFIG_ROBO_APP_FW_RELEASE=y
* - CONFIG_APP_UART_MODE_DEBUG=y
*
* 使用场景:
* - 通过板载 RX1/TX1(UART1)等与外部 MCU 透明传输
* - 量产固件不需要日志输出
*/
#include "core/build_config.h"
#include "task_manager.h"
#include "drivers/uart_comm/uart_comm.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/* 仅在 Release + UART 通信模式下编译此示例 */
#if BUILD_IS_RELEASE && UART_MODE_COMMUNICATION
static const char *TEST_MSG = "Hello from ESP32 UART communication mode!\r\n";
void example_uart_comm_task(void *pvParameters)
{
(void)pvParameters;
/* 初始化指定 UART 作为通信串口(115200 波特率,引脚已在 menuconfig 配置) */
esp_err_t err = uart_comm_init(115200);
if (err != ESP_OK) {
/* 初始化失败(无日志输出时无法打印错误) */
vTaskDelete(NULL);
return;
}
/* 发送测试消息 */
uart_comm_send_string(TEST_MSG);
uint8_t rx_buf[256];
while (1) {
/* 非阻塞接收(100ms 超时) */
int len = uart_comm_receive(rx_buf, sizeof(rx_buf), 100);
if (len > 0) {
/* 回传接收到的数据(Echo) */
uart_comm_send(rx_buf, len);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
/* 启动示例任务的入口函数 */
void example_uart_comm_start(void)
{
(void)app_task_start(APP_TASK_UART_COMM_EXAMPLE, example_uart_comm_task, NULL, NULL);
}
#else /* Debug 模式或 UART 作为调试模式 */
/* 空实现,避免编译错误 */
void example_uart_comm_start(void)
{
/* 在非 Release+通信模式下,此功能不可用 */
}
#endif
/*
* UART 链路模式示例使用文件
* 演示如何使用 link_uart 模块与安卓进行串口通信
*
* 硬件连接:
* - ESP32-S3 GPIO17 (U1TXD) -> 安卓设备 RX
* - ESP32-S3 GPIO18 (U1RXD) -> 安卓设备 TX
* - GND 共地
*
* 通信协议:
* - 波特率:115200 (可在 menuconfig 中配置)
* - 数据格式:8N1
* - 帧格式:JSON + 换行符 (\\n)
*
* 协议示例:
* 安卓发送:{"head":{"message_type":1},"body":{"throttle":50,"steer":30}}
* 设备回复:{"head":{"message_type":1},"body":{"device_ID":"110201","voltage":12.5,"percent":80}}
*/
#include "link_uart.h"
#include "sdkconfig.h"
#if CONFIG_APP_LINK_UART
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *tag_s = "UART_LINK_EX";
/**
* @brief 示例:直接发送原始数据
* 可用于发送自定义协议数据
*/
void example_uart_link_send_raw(void)
{
const char *test_data = "{\"test\":\"hello from esp32\"}\n";
int ret = link_uart_send_raw((const uint8_t *)test_data, strlen(test_data));
if (ret > 0) {
ESP_LOGI(tag_s, "原始数据发送成功: %d bytes", ret);
} else {
ESP_LOGE(tag_s, "原始数据发送失败");
}
}
/**
* @brief 示例:发送心跳
* 实际使用中由 link_uart.c 中的心跳任务自动发送
*/
void example_uart_link_send_heartbeat(void)
{
const char *heartbeat = "{\"head\":{\"message_type\":1},\"body\":{\"device_ID\":\"110201\"}}";
int ret = link_uart_send_heartbeat(heartbeat);
if (ret > 0) {
ESP_LOGI(tag_s, "心跳发送成功");
} else {
ESP_LOGE(tag_s, "心跳发送失败");
}
}
/**
* @brief 示例:发送日志/告警
*/
void example_uart_link_send_log(void)
{
const char *log_json = "{\"head\":{\"message_type\":4},\"body\":{\"msg\":\"系统运行正常\"}}";
int ret = link_uart_send_log(log_json);
if (ret > 0) {
ESP_LOGI(tag_s, "日志发送成功");
} else {
ESP_LOGE(tag_s, "日志发送失败");
}
}
/**
* @brief 示例:检查连接状态
*/
void example_uart_link_check_connection(void)
{
if (link_uart_is_connected()) {
ESP_LOGI(tag_s, "UART 链路已连接");
} else {
ESP_LOGW(tag_s, "UART 链路未连接");
}
}
/**
* @brief 启动 UART 链路模式示例任务
* 演示在应用层如何使用 link_uart 接口
*/
void example_uart_link_start(void)
{
ESP_LOGI(tag_s, "启动 UART 链路示例任务");
ESP_LOGI(tag_s, "UART 配置: GPIO%d TX / GPIO%d RX, 波特率 %d",
CONFIG_APP_UART_LINK_TX_GPIO,
CONFIG_APP_UART_LINK_RX_GPIO,
CONFIG_APP_UART_LINK_BAUDRATE);
/* 注意:link_uart_start() 已在 app_run.c 中调用
* 这里仅演示如何使用 link_uart 提供的接口 */
/* 示例:发送一条测试数据 */
example_uart_link_send_raw();
}
#endif /* CONFIG_APP_LINK_UART */
/*
* 构建配置检测头文件
* 用于在代码中区分 Debug/Release 构建和串口使用模式
*/
#ifndef BUILD_CONFIG_H
#define BUILD_CONFIG_H
#include "sdkconfig.h"
/* ========================
* 编译优化级别检测
* ======================== */
#if defined(CONFIG_ROBO_APP_FW_RELEASE)
#define BUILD_IS_RELEASE 1
#define BUILD_IS_DEBUG 0
#define BUILD_OPTIMIZATION "Os"
#else
#define BUILD_IS_RELEASE 0
#define BUILD_IS_DEBUG 1
#define BUILD_OPTIMIZATION "Og"
#endif
/* ========================
* 串口 / 日志输出
* ======================== */
/*
* 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_DEBUG 0
#define BUILD_SERIAL_LOG_ENABLED 0
#else
#define UART_MODE_COMMUNICATION 0
#define UART_MODE_DEBUG 1
#if BUILD_IS_RELEASE
#define BUILD_SERIAL_LOG_ENABLED 0
#else
#define BUILD_SERIAL_LOG_ENABLED 1
#endif
#endif
/* ========================
* 条件日志宏(Release 或无串口时禁用 LOG_* 宏)
* ======================== */
#if !BUILD_SERIAL_LOG_ENABLED
#define BUILD_LOG_LEVEL_NONE 1
#define LOG_I(tag, fmt, ...) ((void)0)
#define LOG_W(tag, fmt, ...) ((void)0)
#define LOG_E(tag, fmt, ...) ((void)0)
#define LOG_D(tag, fmt, ...) ((void)0)
#else
#include "esp_log.h"
#define BUILD_LOG_LEVEL_NONE 0
#define LOG_I(tag, fmt, ...) ESP_LOGI(tag, fmt, ##__VA_ARGS__)
#define LOG_W(tag, fmt, ...) ESP_LOGW(tag, fmt, ##__VA_ARGS__)
#define LOG_E(tag, fmt, ...) ESP_LOGE(tag, fmt, ##__VA_ARGS__)
#define LOG_D(tag, fmt, ...) ESP_LOGD(tag, fmt, ##__VA_ARGS__)
#endif
#ifdef __cplusplus
extern "C" {
#endif
static inline const char* build_get_type(void) {
return BUILD_IS_RELEASE ? "Release" : "Debug";
}
static inline const char* build_get_uart_mode(void) {
return UART_MODE_COMMUNICATION ? "Communication" : "Debug";
}
static inline int build_can_log(void) {
return BUILD_SERIAL_LOG_ENABLED;
}
#ifdef __cplusplus
}
#endif
#endif /* BUILD_CONFIG_H */
#include "system_init.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "wifidevnum_config.h"
#include "ota_manager.h"
static const char *tag_s = "SYS_INIT";
esp_err_t system_core_init(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* 初始化 OTA 管理器(检查上次OTA状态,如有必要则回退) */
ret = ota_manager_init();
if (ret != ESP_OK) {
ESP_LOGW(tag_s, "OTA manager triggered rollback, restarting...");
/* ota_manager_init 内部会调用 esp_restart() */
/* 如果能走到这里,说明没有触发回退,继续启动 */
}
ESP_ERROR_CHECK(init_spiffs());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
return ESP_OK;
}
#ifndef SYSTEM_INIT_H
#define SYSTEM_INIT_H
#include "esp_err.h"
esp_err_t system_core_init(void);
#endif
#include "task_manager.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include <stddef.h>
#if CONFIG_BT_NIMBLE_ENABLED
#include "nimble/nimble_port_freertos.h"
#endif
static const char *tag_s = "TASK_MGR";
typedef struct {
const char *name;
uint32_t stack_depth;
UBaseType_t priority;
int core_id;
} app_task_config_t;
static const app_task_config_t task_cfg_s[APP_TASK_COUNT] = {
[APP_TASK_DNS_SERVER] = {
.name = "dns",
.stack_depth = 3072,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_BUTTON_MONITOR] = {
.name = "btn_task",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_BLE_HEARTBEAT] = {
.name = "ble_hb",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_HEARTBEAT] = {
.name = "mqtt_hb",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_OTA_REPORT] = {
.name = "ota_msg_task",
.stack_depth = 4096,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_EXIT] = {
.name = "mqtt_exit",
.stack_depth = 512,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_MQTT_INIT] = {
.name = "mqtt_init",
.stack_depth = 10240,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
[APP_TASK_UART_COMM_EXAMPLE] = {
.name = "uart_comm",
.stack_depth = 2048,
.priority = 5,
.core_id = APP_TASK_CORE_NO_AFFINITY,
},
};
static esp_err_t app_task_create(const char *name,
TaskFunction_t entry,
uint32_t stack_depth,
void *arg,
UBaseType_t priority,
int core_id,
TaskHandle_t *out_handle)
{
BaseType_t rc = pdFAIL;
if (core_id == APP_TASK_CORE_NO_AFFINITY) {
rc = xTaskCreate(entry, name, stack_depth, arg, priority, out_handle);
} else {
rc = xTaskCreatePinnedToCore(entry, name, stack_depth, arg, priority, out_handle, core_id);
}
if (rc != pdPASS) {
ESP_LOGE(tag_s, "create task fail: %s core=%d", name ? name : "unknown", core_id);
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t app_task_start(app_task_id_t id,
TaskFunction_t entry,
void *arg,
TaskHandle_t *out_handle)
{
if (id < 0 || id >= APP_TASK_COUNT || entry == NULL) {
ESP_LOGE(tag_s, "invalid task id=%d", (int)id);
return ESP_ERR_INVALID_ARG;
}
const app_task_config_t *cfg = &task_cfg_s[id];
return app_task_create(cfg->name, entry, cfg->stack_depth, arg, cfg->priority, cfg->core_id, out_handle);
}
void app_task_stop(TaskHandle_t *handle)
{
if (handle == NULL || *handle == NULL) {
return;
}
vTaskDelete(*handle);
*handle = NULL;
}
esp_err_t app_task_start_nimble_host(TaskFunction_t entry)
{
if (entry == NULL) {
return ESP_ERR_INVALID_ARG;
}
#if CONFIG_BT_NIMBLE_ENABLED
nimble_port_freertos_init(entry);
return ESP_OK;
#else
ESP_LOGE(tag_s, "NimBLE is disabled");
return ESP_ERR_NOT_SUPPORTED;
#endif
}
#ifndef TASK_MANAGER_H
#define TASK_MANAGER_H
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define APP_TASK_CORE_NO_AFFINITY (-1)
typedef enum {
APP_TASK_DNS_SERVER = 0,
APP_TASK_BUTTON_MONITOR,
APP_TASK_BLE_HEARTBEAT,
APP_TASK_MQTT_HEARTBEAT,
APP_TASK_MQTT_OTA_REPORT,
APP_TASK_MQTT_EXIT,
APP_TASK_MQTT_INIT,
APP_TASK_UART_COMM_EXAMPLE,
APP_TASK_RC_WATCHDOG, /* 遥控超时守护任务 */
APP_TASK_COUNT,
} app_task_id_t;
esp_err_t app_task_start(app_task_id_t id,
TaskFunction_t entry,
void *arg,
TaskHandle_t *out_handle);
void app_task_stop(TaskHandle_t *handle);
esp_err_t app_task_start_nimble_host(TaskFunction_t entry);
#endif
#include "device_model.h"
#include <string.h>
#include "esp_log.h"
static const char *tag_s = "DEVICE_MODEL";
device_model_t device_model_from_full_id(const char *device_id)
{
if (!device_id || strlen(device_id) < (size_t)(DEVICE_MODEL_SLICE_0BASE_START + DEVICE_MODEL_SLICE_CHAR_LEN)) {
ESP_LOGW(tag_s, "device_id 为空或长度不足,按 1101");
return DEVICE_MODEL_1101;
}
const char *slice = device_id + DEVICE_MODEL_SLICE_0BASE_START;
if (strncmp(slice, "1201", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1201 → 型号 1201");
return DEVICE_MODEL_1201;
}
if (strncmp(slice, "1101", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1101 → 型号 1101");
return DEVICE_MODEL_1101;
}
if (strncmp(slice, "1102", DEVICE_MODEL_SLICE_CHAR_LEN) == 0) {
ESP_LOGI(tag_s, "片段 [3..6]=1102 → 型号 1102");
return DEVICE_MODEL_1102;
}
ESP_LOGW(tag_s, "片段 [3..6]=\"%.4s\" 未识别,默认 1101", slice);
return DEVICE_MODEL_1101;
}
#ifndef DEVICE_MODEL_H
#define DEVICE_MODEL_H
#include <stdbool.h>
/**
* 由完整 device_id 解析出的设备子型号(用于选择控制/急停等策略)。
* 规则:取 device_id 第 3~第 6 个字符(从第 1 个字符起算,共 4 字符),
* 即 C 字符串下标 [2..5],与 "1201" / "1101" 比较。
*/
typedef enum {
DEVICE_MODEL_1201 = 0,
DEVICE_MODEL_1101,
DEVICE_MODEL_1102,
} device_model_t;
#define DEVICE_MODEL_SLICE_0BASE_START 2
#define DEVICE_MODEL_SLICE_CHAR_LEN 4
device_model_t device_model_from_full_id(const char *device_id);
static inline bool device_model_is_1101(device_model_t m)
{
return m == DEVICE_MODEL_1101;
}
static inline bool device_model_is_1102(device_model_t m)
{
return m == DEVICE_MODEL_1102;
}
#endif
#ifndef DEVICE_NVS_H
#define DEVICE_NVS_H
/** NVS 命名空间(与网页配网写入一致) */
#define DEVICE_CFG_NVS_NAMESPACE "storage"
#define DEVICE_CFG_KEY_WIFI_SSID "wifi_ssid"
#define DEVICE_CFG_KEY_WIFI_PASS "wifi_pass"
#define DEVICE_CFG_KEY_DEVICE_ID "device_id"
#define DEVICE_CFG_KEY_BLE_ADV_NAME "ble_adv_name"
#endif
#include "driver_manager.h"
#include <string.h>
#include "betteryread.h"
#include "device_nvs.h"
#include "esp_log.h"
#include "rc_pwm_control.h"
#include "wifidevnum_config.h"
static const char *tag_s = "DRV_MGR";
esp_err_t driver_manager_init_from_nvs(void)
{
char devid[32] = {0};
if (read_from_nvs(DEVICE_CFG_KEY_DEVICE_ID, devid, sizeof(devid)) == ESP_OK && strlen(devid) > 0) {
rc_pwm_control_set_drive_from_device_id(devid);
} else {
rc_pwm_control_set_drive_from_device_id(NULL);
}
ESP_ERROR_CHECK(board_adc_init());
ESP_ERROR_CHECK(rc_pwm_control_init());
ESP_LOGI(tag_s, "drivers init done");
return ESP_OK;
}
#ifndef DRIVER_MANAGER_H
#define DRIVER_MANAGER_H
#include "esp_err.h"
esp_err_t driver_manager_init_from_nvs(void);
#endif
#include "betteryread.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <stdbool.h>
static const char *tag_s = "BATT_ADC";
static adc_oneshot_unit_handle_t adc_handle_s;
static adc_cali_handle_t adc_cali_s;
static adc_channel_t adc_channel_s;
static adc_unit_t adc_unit_s;
static bool adc_ready_s;
static bool adc_cali_ready_s;
static float voltage_cache_v_s;
static int64_t voltage_cache_ts_us_s;
static bool voltage_cache_valid_s;
#define BATT_READ_MIN_INTERVAL_US (2500LL * 1000LL)
#define BATT_FILTER_ALPHA (0.35f)
esp_err_t board_adc_init(void)
{
esp_err_t err = adc_oneshot_io_to_channel(ADC_GET_VOLTAGE_GPIO, &adc_unit_s, &adc_channel_s);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "gpio%d no ADC channel: %s", ADC_GET_VOLTAGE_GPIO, esp_err_to_name(err));
return err;
}
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = adc_unit_s,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
err = adc_oneshot_new_unit(&unit_cfg, &adc_handle_s);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "adc new unit fail: %s", esp_err_to_name(err));
return err;
}
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
err = adc_oneshot_config_channel(adc_handle_s, adc_channel_s, &chan_cfg);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "adc cfg channel fail: %s", esp_err_to_name(err));
return err;
}
adc_cali_curve_fitting_config_t cali_cfg = {
.unit_id = adc_unit_s,
.atten = chan_cfg.atten,
.bitwidth = chan_cfg.bitwidth,
};
err = adc_cali_create_scheme_curve_fitting(&cali_cfg, &adc_cali_s);
if (err == ESP_OK) {
adc_cali_ready_s = true;
} else {
adc_cali_ready_s = false;
ESP_LOGW(tag_s, "adc calibration unavailable, fallback raw");
}
adc_ready_s = true;
ESP_LOGI(tag_s, "ADC ready gpio=%d unit=%d ch=%d", ADC_GET_VOLTAGE_GPIO, adc_unit_s, adc_channel_s);
return ESP_OK;
}
float board_get_voltage_v(void)
{
if (!adc_ready_s) {
return 0.0f;
}
int64_t now_us = esp_timer_get_time();
if (voltage_cache_valid_s && (now_us - voltage_cache_ts_us_s) < BATT_READ_MIN_INTERVAL_US) {
return voltage_cache_v_s;
}
const int sample_count = 16;
int raw_sum = 0;
int valid_count = 0;
for (int i = 0; i < sample_count; ++i) {
int raw = 0;
if (adc_oneshot_read(adc_handle_s, adc_channel_s, &raw) != ESP_OK) {
continue;
}
raw_sum += raw;
++valid_count;
}
if (valid_count == 0) {
return 0.0f;
}
int raw_avg = raw_sum / valid_count;
int adc_mv = 0;
if (adc_cali_ready_s) {
if (adc_cali_raw_to_voltage(adc_cali_s, raw_avg, &adc_mv) != ESP_OK) {
adc_mv = (raw_avg * 3300) / 4095;
}
} else {
adc_mv = (raw_avg * 3300) / 4095;
}
const float v_adc = (float)adc_mv / 1000.0f;
/* 分压系数 = (R上拉 + R下拉) / R下拉 = (100k + 10k) / 10k = 11 */
float measured_v = v_adc * BOARD_VOLTAGE_DIVIDER_GAIN;
if (!voltage_cache_valid_s) {
voltage_cache_v_s = measured_v;
voltage_cache_valid_s = true;
} else {
voltage_cache_v_s = (BATT_FILTER_ALPHA * measured_v) +
((1.0f - BATT_FILTER_ALPHA) * voltage_cache_v_s);
}
voltage_cache_ts_us_s = now_us;
return voltage_cache_v_s;
}
\ No newline at end of file
#ifndef BETTERYREAD_H
#define BETTERYREAD_H
#include "esp_err.h"
#define ADC_GET_VOLTAGE_GPIO 5
/* 分压:上拉 100k 到电池正极、下拉 10k 到 GND,采样点接 GPIO5
* 分压系数 = (R上拉 + R下拉) / R下拉 = (100 + 10) / 10 = 11
* 所以电压 = ADC电压 × 11
*/
#define BOARD_VOLTAGE_DIVIDER_R_TOP_KOHM 100.0f
#define BOARD_VOLTAGE_DIVIDER_R_BOTTOM_KOHM 10.0f
#define BOARD_VOLTAGE_DIVIDER_GAIN 11.0f
float board_get_voltage_v(void);
/**
* @brief 电压采样初始化(GPIO5 ADC)
*/
esp_err_t board_adc_init(void);
#define get_voltage_v() board_get_voltage_v()
#define get_temperature() (0.0f)
#endif
\ No newline at end of file
#ifndef DEVICE_DRIVE_H
#define DEVICE_DRIVE_H
typedef struct device_drive_ops_s {
const char *name;
void (*stop)(void);
void (*control)(int mode, int val);
void (*shot)(int pin, int val);
} device_drive_ops_t;
#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;
}
#ifndef DEVICE_1101_H
#define DEVICE_1101_H
#include "device_drive.h"
const device_drive_ops_t *device_1101_get_ops(void);
#endif
#include "device_1102.h"
#include "rc_pwm_control.h"
#define DEV1102_VAL_MAX 200
#define DEV1102_DRIVE_START_TH 50
/* 转向:App 侧 val 在 [47,62] 线性对应角度;中位 90°,左右各 ±30° */
#define DEV1102_STEER_VAL_LO 47
#define DEV1102_STEER_VAL_HI 62
#define DEV1102_STEER_CENTER_ANG 90
#define DEV1102_STEER_DELTA_ANG 30
#define DEV1102_STEER_LEFT_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)
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 drive_percent_from_val_200(int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_DRIVE_START_TH) {
return 0;
}
/* 50~200 线性映射到 0~100% */
return (uint32_t)(((v - DEV1102_DRIVE_START_TH) * 100) /
(DEV1102_VAL_MAX - DEV1102_DRIVE_START_TH));
}
static uint32_t steer_left_angle_from_val(int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_STEER_VAL_LO) {
return DEV1102_STEER_CENTER_ANG;
}
if (v >= DEV1102_STEER_VAL_HI) {
return DEV1102_STEER_LEFT_MAX;
}
return (uint32_t)(DEV1102_STEER_CENTER_ANG +
(uint32_t)(v - DEV1102_STEER_VAL_LO) * DEV1102_STEER_DELTA_ANG /
(uint32_t)DEV1102_STEER_VAL_SPAN);
}
static uint32_t steer_right_angle_from_val(int val)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
if (v <= DEV1102_STEER_VAL_LO) {
return DEV1102_STEER_CENTER_ANG;
}
if (v >= DEV1102_STEER_VAL_HI) {
return DEV1102_STEER_RIGHT_MAX;
}
return (uint32_t)(DEV1102_STEER_CENTER_ANG -
(uint32_t)(v - DEV1102_STEER_VAL_LO) * DEV1102_STEER_DELTA_ANG /
(uint32_t)DEV1102_STEER_VAL_SPAN);
}
static void device_1102_stop(void)
{
/* 1102: 10 前进,21 后退;全部拉低并将 IO16 回中 */
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
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)
{
int v = clamp_int(val, 0, DEV1102_VAL_MAX);
switch (mode) {
case 1: { /* 前进:IO10 */
uint32_t pct = drive_percent_from_val_200(v);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, pct);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, 0);
break;
}
case 2: { /* 后退:IO21 */
uint32_t pct = drive_percent_from_val_200(v);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_A, 0);
rc_pwm_set_drive_percent(RC_PWM_PIN_DRV1_B, pct);
break;
}
case 3: { /* 左转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_left_angle_from_val(v));
break;
}
case 4: { /* 右转:IO16 舵机 */
rc_pwm_set_aux_servo_angle_deg(RC_PWM_PIN_AUX_16, steer_right_angle_from_val(v));
break;
}
default:
break;
}
}
static void device_1102_shot(int pin, int val)
{
(void)pin;
(void)val;
}
static const device_drive_ops_t ops_s = {
.name = "1102",
.stop = device_1102_stop,
.control = device_1102_control,
.shot = device_1102_shot,
};
const device_drive_ops_t *device_1102_get_ops(void)
{
return &ops_s;
}
#ifndef DEVICE_1102_H
#define DEVICE_1102_H
#include "device_drive.h"
const device_drive_ops_t *device_1102_get_ops(void);
#endif
#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
This diff is collapsed.
#ifndef RC_PWM_CONTROL_H
#define RC_PWM_CONTROL_H
#include "driver/ledc.h"
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/* RC 车统一 PWM 引脚定义(6 路均为 50Hz) */
#define RC_PWM_PIN_DRV1_A 10 /* 驱动芯片1 */
#define RC_PWM_PIN_DRV1_B 21 /* 驱动芯片1 */
#define RC_PWM_PIN_DRV2_A 11 /* 驱动芯片2 */
#define RC_PWM_PIN_DRV2_B 12 /* 驱动芯片2 */
#define RC_PWM_PIN_AUX_15 15 /* 舵机/电调(Kconfig 选择) */
#define RC_PWM_PIN_AUX_16 16 /* 舵机/电调(Kconfig 选择) */
#define RC_PWM_LEDC_TIMER LEDC_TIMER_0
#define RC_PWM_LEDC_MODE LEDC_LOW_SPEED_MODE
#define RC_PWM_DUTY_RES LEDC_TIMER_13_BIT
#define RC_PWM_FREQ_HZ 50
/* 50Hz + 13bit 下的典型占空比(20ms 周期) */
#define RC_PWM_DUTY_500US 205U /* 舵机 0 度 */
#define RC_PWM_DUTY_1500US 614U /* 舵机 90 度 / ESC 中位 */
#define RC_PWM_DUTY_2500US 1024U /* 舵机 180 度 */
#define RC_ESC_DUTY_MIN 410U /* 1000us */
#define RC_ESC_DUTY_MID RC_PWM_DUTY_1500US
#define RC_ESC_DUTY_MAX 819U /* 2000us */
typedef enum {
RC_AUX_ROLE_SERVO = 0,
RC_AUX_ROLE_ESC = 1,
} rc_aux_role_t;
/**
* @brief 初始化全部 6 路 PWM 输出
*
* IO15/16 初始值根据 Kconfig 选择:
* - 舵机:90 度(1500us)
* - 电调:1500us 中位
*/
esp_err_t rc_pwm_control_init(void);
/**
* @brief 根据 device_id 选择设备控制策略
*/
void rc_pwm_control_set_drive_from_device_id(const char *device_id);
void rc_vehicle_stop(void);
void rc_vehicle_shot(int pin, int val);
void rc_vehicle_control(int mode, int val);
/**
* @brief 设置驱动芯片控制引脚百分比(支持 10/21/11/12)
*/
void rc_pwm_set_drive_percent(int gpio, uint32_t duty_percent);
/**
* @brief 停止驱动芯片相关输出(10/21/11/12)
*/
void rc_pwm_stop_all_drive_outputs(void);
/**
* @brief 将指定 AUX 引脚按舵机角度输出(仅当该引脚被配置为 SERVO)
*/
void rc_pwm_set_aux_servo_angle_deg(int gpio, uint32_t angle);
/**
* @brief 将指定 AUX 引脚按电调油门百分比输出(仅当该引脚被配置为 ESC)
*/
void rc_pwm_set_aux_esc_percent(int gpio, uint32_t percent);
/**
* @brief 双电调同步输出(会对配置为 ESC 的 AUX 引脚生效)
*/
void rc_pwm_set_dual_esc_percent(uint32_t percent);
/**
* @brief 给“默认转向舵机”输出角度(优先 IO15,其次 IO16)
*/
void rc_pwm_set_steering_angle_deg(uint32_t angle);
/* ======================== PID 接口 ======================== */
typedef enum {
RC_PID_ACT_ESC_AUX_BOTH = 0, /* 对 IO15/16 中被配置为 ESC 的引脚生效 */
RC_PID_ACT_DRV1_A, /* IO10 */
RC_PID_ACT_DRV1_B, /* IO21 */
RC_PID_ACT_DRV2_A, /* IO11 */
RC_PID_ACT_DRV2_B, /* IO12 */
RC_PID_ACT_STEERING_SERVO, /* 默认舵机(IO15 优先) */
} rc_pid_actuator_t;
typedef enum {
RC_PID_CH_0 = 0,
RC_PID_CH_1,
RC_PID_CH_2,
RC_PID_CH_3,
RC_PID_CH_MAX
} rc_pid_channel_t;
typedef struct {
float kp;
float ki;
float kd;
float out_min;
float out_max;
float i_min;
float i_max;
rc_pid_actuator_t actuator;
bool enabled;
} rc_pid_config_t;
esp_err_t rc_pid_config(rc_pid_channel_t ch, const rc_pid_config_t *cfg);
void rc_pid_reset(rc_pid_channel_t ch);
void rc_pid_enable(rc_pid_channel_t ch, bool enabled);
float rc_pid_step(rc_pid_channel_t ch,
float target,
float feedback,
float dt_s,
bool apply_output);
#endif
/*
* UART 通信驱动实现
* 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口
*/
#include "uart_comm.h"
#include "core/build_config.h"
#if defined(CONFIG_APP_UART_MODE_DEBUG) && defined(CONFIG_ROBO_APP_FW_RELEASE)
#include "driver/uart.h"
#include <string.h>
#define UART_COMM_PORT ((uart_port_t)CONFIG_APP_DEBUG_UART_NUM)
static int initialized_s = 0;
esp_err_t uart_comm_init(int baudrate)
{
if (initialized_s) {
return ESP_OK;
}
if (baudrate <= 0) {
baudrate = UART_COMM_DEFAULT_BAUDRATE;
}
uart_config_t uart_cfg = {
.baud_rate = baudrate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_param_config(UART_COMM_PORT, &uart_cfg);
if (err != ESP_OK) {
return err;
}
/* 设置 TX/RX 引脚(UART1/UART2 必须显式设置,UART0 有默认 GPIO43/44) */
int tx_gpio = CONFIG_APP_DEBUG_UART_TX_GPIO;
int rx_gpio = CONFIG_APP_DEBUG_UART_RX_GPIO;
if (tx_gpio >= 0 && rx_gpio >= 0) {
err = uart_set_pin(UART_COMM_PORT, tx_gpio, rx_gpio,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
return err;
}
}
err = uart_driver_install(UART_COMM_PORT, UART_COMM_BUFFER_SIZE,
UART_COMM_BUFFER_SIZE, UART_COMM_QUEUE_SIZE,
NULL, 0);
if (err != ESP_OK) {
return err;
}
initialized_s = 1;
return ESP_OK;
}
void uart_comm_deinit(void)
{
if (!initialized_s) {
return;
}
uart_driver_delete(UART_COMM_PORT);
initialized_s = 0;
}
int uart_comm_send(const uint8_t *data, size_t len)
{
if (!initialized_s || !data || len == 0) {
return -1;
}
int written = uart_write_bytes(UART_COMM_PORT, data, len);
return written;
}
int uart_comm_send_string(const char *str)
{
if (!str) {
return -1;
}
return uart_comm_send((const uint8_t *)str, strlen(str));
}
int uart_comm_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms)
{
if (!initialized_s || !buf || buf_size == 0) {
return -1;
}
int len = uart_read_bytes(UART_COMM_PORT, buf, buf_size - 1,
pdMS_TO_TICKS(timeout_ms));
if (len > 0) {
buf[len] = '\0';
}
return len;
}
int uart_comm_is_initialized(void)
{
return initialized_s;
}
void uart_comm_flush(void)
{
if (!initialized_s) {
return;
}
uart_flush(UART_COMM_PORT);
}
#else /* !(CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE) */
/* 当功能未启用时,提供空实现(避免编译错误) */
esp_err_t uart_comm_init(int baudrate)
{
(void)baudrate;
return ESP_ERR_NOT_SUPPORTED;
}
void uart_comm_deinit(void) {}
int uart_comm_send(const uint8_t *data, size_t len)
{
(void)data;
(void)len;
return -1;
}
int uart_comm_send_string(const char *str)
{
(void)str;
return -1;
}
int uart_comm_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms)
{
(void)buf;
(void)buf_size;
(void)timeout_ms;
return -1;
}
int uart_comm_is_initialized(void)
{
return 0;
}
void uart_comm_flush(void) {}
#endif /* CONFIG_APP_UART_MODE_DEBUG && CONFIG_ROBO_APP_FW_RELEASE */
/*
* UART 通信驱动
* 用于 Release 模式下接管「APP_DEBUG_UART_NUM」对应 UART 作为普通通信串口
* 注意:启用此功能后,ESP_LOG/printf 将不可用
*/
#ifndef UART_COMM_H
#define UART_COMM_H
#include "esp_err.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* UART 配置参数 */
#define UART_COMM_DEFAULT_BAUDRATE 115200
#define UART_COMM_BUFFER_SIZE 1024
#define UART_COMM_QUEUE_SIZE 8
/* 初始化 UART 作为普通通信串口
* 仅在 APP_UART_MODE_DEBUG=y 时有效
* 返回 ESP_OK 成功,其他错误码失败
*/
esp_err_t uart_comm_init(int baudrate);
/* 反初始化,恢复默认状态 */
void uart_comm_deinit(void);
/* 发送数据 */
int uart_comm_send(const uint8_t *data, size_t len);
int uart_comm_send_string(const char *str);
/* 接收数据(非阻塞) */
int uart_comm_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms);
/* 检查是否已初始化 */
int uart_comm_is_initialized(void);
/* 清空接收缓冲区 */
void uart_comm_flush(void);
#ifdef __cplusplus
}
#endif
#endif /* UART_COMM_H */
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/cJSON: '*'
\ No newline at end of file
This diff is collapsed.
/**
* NimBLE:GAP 广播名来自 NVS `ble_adv_name`,固件会自动追加后缀 **MP**(便于 App 扫描过滤)。
* GATT:Primary 16-bit UUID 0xFFE0
* - 0xFFE1:UTF-8 JSON 遥控(与 MQTT payload 相同结构)
* - 0xFFE2:固件 OTA 二进制流(首字节 opcode:0x01+uint32 LE 总长,0x02+固件数据,0x03 结束并重启;受 menuconfig「APP_BLE_OTA」控制)
* - 0xFFE3:心跳 / 告警 / 错误 JSON,Read + Notify(心跳默认 3s;W/E 出现时即时另发一条)
* - 0xFFE4:OTA 过程状态 JSON,Read(最近一次)+ Notify(每步应答;需 enableNotification)
*/
#ifndef LINK_BLE_H
#define LINK_BLE_H
#include "esp_err.h"
esp_err_t link_ble_start(void);
void link_ble_stop(void);
#endif
This diff is collapsed.
/**
* UART 链路通信模块
* 用于 UART 链路模式下与安卓进行串口通信
* 使用 UART1 (GPIO17 TX / GPIO18 RX)
*
* 协议格式:
* - 遥控指令:UTF-8 JSON(与 MQTT/BLE payload 相同结构)
* - 心跳:周期 JSON,包含 device_id、状态等信息
* - 日志:W/E 日志即时推送
*/
#ifndef LINK_UART_H
#define LINK_UART_H
#include "esp_err.h"
#include <stdbool.h>
#include <stddef.h>
/**
* @brief 启动 UART 链路通信
* 初始化 UART1 (GPIO17 TX / GPIO18 RX),启动接收和发送任务
* @return ESP_OK 成功,其他错误码失败
*/
esp_err_t link_uart_start(void);
/**
* @brief 停止 UART 链路通信
* 停止 UART 任务,释放资源
*/
void link_uart_stop(void);
/**
* @brief 发送心跳 JSON 数据
* @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败
*/
int link_uart_send_heartbeat(const char *json);
/**
* @brief 发送日志/告警 JSON 数据
* @param json 要发送的 JSON 字符串
* @return 0 成功,其他失败
*/
int link_uart_send_log(const char *json);
/**
* @brief 检查 UART 链路是否已连接(有数据交互即认为在线)
* @return true 在线,false 离线
*/
bool link_uart_is_connected(void);
/**
* @brief 发送原始数据到 UART
* @param data 数据指针
* @param len 数据长度
* @return 实际发送字节数,-1 失败
*/
int link_uart_send_raw(const uint8_t *data, size_t len);
#endif /* LINK_UART_H */
This diff is collapsed.
#ifndef __MQTTCONF_COMMUN_H__
#define __MQTTCONF_COMMUN_H__
#include "esp_err.h"
#define MQTT_SERVICE_NUM_MAX 10
// 启动所有 MQTT 相关逻辑
void mqtt_manager_init_sequence(void);
void mqtt_manager_stop(void);
#endif
\ No newline at end of file
#include "app_run.h"
void app_main(void)
{
app_run();
}
#include "ota.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_app_format.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_crt_bundle.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "wifidevnum_config.h" // 假设你有封装好的 read_from_nvs 和 save_to_nvs
#include "esp_spiffs.h"
#include <cJSON.h>
#include <string.h>
static const char *tag_s = "OTA_UPDATE";
/**
* @brief HTTP 事件处理
*/
esp_err_t _ota_http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ERROR: ESP_LOGE(tag_s, "HTTP_EVENT_ERROR"); break;
case HTTP_EVENT_ON_CONNECTED: ESP_LOGI(tag_s, "已连接到服务器"); break;
case HTTP_EVENT_ON_FINISH: ESP_LOGI(tag_s, "数据传输完成"); break;
default: break;
}
return ESP_OK;
}
/**
* @brief 启动 OTA
*/
void start_ota_update(const char* url) {
ESP_LOGI(tag_s, "正在从 URL 启动 OTA: %s", url);
esp_http_client_config_t config = {
.url = url,
.crt_bundle_attach = esp_crt_bundle_attach,
.event_handler = _ota_http_event_handler,
.keep_alive_enable = true,
.timeout_ms = 10000,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(tag_s, "正在下载并写入固件... 请勿断电");
esp_err_t ret = esp_https_ota(&ota_config);
// 在这里获取更新后的分区指针
const esp_partition_t *updated_partition = esp_ota_get_next_update_partition(NULL);
if (ret == ESP_OK) {
ESP_LOGI(tag_s, "OTA 升级成功!即将重启...");
ESP_LOGI(tag_s, "找到更新分区: %s (偏移: 0x%08x)",
updated_partition->label, updated_partition->address);
esp_err_t set_err = esp_ota_set_boot_partition(updated_partition);
if (set_err == ESP_OK) {
ESP_LOGI(tag_s, "启动分区设置成功!");
// 验证设置结果
const esp_partition_t *boot_partition = esp_ota_get_boot_partition();
const esp_partition_t *running_partition = esp_ota_get_running_partition();
ESP_LOGI(tag_s, "当前运行分区: %s (0x%08x)",
running_partition->label, running_partition->address);
ESP_LOGI(tag_s, "下次启动分区: %s (0x%08x)",
boot_partition->label, boot_partition->address);
// 等待设置完成
vTaskDelay(2000 / portTICK_PERIOD_MS);
esp_restart();
}else {
ESP_LOGE(tag_s, "设置启动分区失败: %s", esp_err_to_name(set_err));
}
} else {
ESP_LOGE(tag_s, "OTA 升级失败: %s", esp_err_to_name(ret));
}
}
/**
* @brief MQTT 消息处理函数:解析 JSON 并判断是否需要更新
*/
int ota_update_recmqtt(const char *json_data) {
if (json_data == NULL) return -1;
cJSON *root = cJSON_Parse(json_data);
if (root == NULL) return -1;
ESP_LOGI(tag_s, "当前固件运行版本: %s", CONFIG_MY_APP_VERSION);
// 2. 解析云端发来的新版本号
cJSON *new_version_obj = cJSON_GetObjectItem(root, "new_version");
if (!cJSON_IsString(new_version_obj) || (new_version_obj->valuestring == NULL)) {
cJSON_Delete(root);
return -1;
}
const char* cloud_version = new_version_obj->valuestring;
// 3. 版本对比
if (strcmp(CONFIG_MY_APP_VERSION, cloud_version) == 0) {
ESP_LOGI(tag_s, "当前已是最新版本 (%s),跳过更新。", CONFIG_MY_APP_VERSION);
cJSON_Delete(root);
return 0;
}
ESP_LOGI(tag_s, "检测到新版本: %s -> %s",CONFIG_MY_APP_VERSION, cloud_version);
// 5. 解析下载地址并启动更新
cJSON *esp32_url = cJSON_GetObjectItem(root, "esp32_url");
if (cJSON_IsString(esp32_url) && (esp32_url->valuestring != NULL)) {
start_ota_update(esp32_url->valuestring);
} else {
ESP_LOGE(tag_s, "JSON 中未找到有效的 esp32_url");
}
cJSON_Delete(root);
return 0;
}
#ifndef __OTA_H__
#define __OTA_H__
#include "esp_err.h"
int ota_update_recmqtt(const char *json_data);
#endif
\ No newline at end of file
/*
* OTA 二进制流接收模块实现
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
*/
#include "ota_binary_stream.h"
#include "ota_manager.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
static const char *tag_s = "OTA_STREAM";
/* OTA 会话状态 */
typedef struct {
bool active;
esp_ota_handle_t handle;
const esp_partition_t *partition;
size_t expected_size;
size_t written_size;
ota_transport_type_t transport;
ota_binary_stream_status_callback_t status_callback;
} ota_stream_session_t;
static ota_stream_session_t session_s = {0};
/* 格式化并发送状态回调 */
static void ota_stream_send_status_fmt(const char *fmt, ...)
{
if (session_s.status_callback == NULL) {
return;
}
char buf[192];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
session_s.status_callback(buf);
}
/* 发送错误状态 */
static void ota_stream_send_error(const char *step, const char *err)
{
ESP_LOGE(tag_s, "OTA error at %s: %s", step, err);
ota_stream_send_status_fmt("{\"ota\":\"%s\",\"ok\":0,\"err\":\"%s\"}", step, err);
}
/* 中止 OTA 会话 */
static void ota_stream_abort_internal(void)
{
if (session_s.active && session_s.handle != 0) {
esp_ota_abort(session_s.handle);
ESP_LOGW(tag_s, "OTA session aborted");
}
session_s.active = false;
session_s.handle = 0;
session_s.partition = NULL;
session_s.expected_size = 0;
session_s.written_size = 0;
}
void ota_binary_stream_abort(void)
{
ota_stream_abort_internal();
}
bool ota_binary_stream_is_active(void)
{
return session_s.active;
}
esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
ota_binary_stream_status_callback_t status_callback,
const char *target_version)
{
if (session_s.active) {
ESP_LOGW(tag_s, "OTA already active");
return ESP_ERR_INVALID_STATE;
}
session_s.transport = transport;
session_s.status_callback = status_callback;
/* 通过OTA管理器开始会话 */
ota_type_t type = (transport == OTA_TRANSPORT_BLE) ? OTA_TYPE_BLE : OTA_TYPE_UART;
esp_err_t err = ota_manager_begin_session(type, target_version ? target_version : "unknown");
if (err != ESP_OK) {
ESP_LOGE(tag_s, "Failed to begin OTA session in manager: %s", esp_err_to_name(err));
}
ESP_LOGI(tag_s, "OTA stream init: transport=%d", transport);
return ESP_OK;
}
void ota_binary_stream_deinit(void)
{
if (session_s.active) {
ota_stream_abort_internal();
}
session_s.status_callback = NULL;
}
/* 处理 OTA 开始命令 */
static int ota_stream_handle_begin(const uint8_t *data, uint16_t len)
{
if (len < 5) {
/* 需要至少 1字节opcode + 4字节长度 */
ota_stream_send_error("begin", "size");
return -1;
}
/* 解析固件长度(小端序 uint32) */
uint32_t image_size = ((uint32_t)data[1]) |
((uint32_t)data[2] << 8) |
((uint32_t)data[3] << 16) |
((uint32_t)data[4] << 24);
if (image_size == 0 || image_size > (6 * 1024 * 1024)) {
/* 固件大小不合理(最大6MB) */
ota_stream_send_error("begin", "size_invalid");
return -2;
}
ESP_LOGI(tag_s, "OTA begin: size=%lu bytes", (unsigned long)image_size);
/* 查找 OTA 更新分区 */
session_s.partition = esp_ota_get_next_update_partition(NULL);
if (session_s.partition == NULL) {
ota_stream_send_error("begin", "no_partition");
return -3;
}
ESP_LOGI(tag_s, "OTA partition: %s at 0x%08x",
session_s.partition->label,
(unsigned)session_s.partition->address);
/* 开始 OTA */
esp_err_t err = esp_ota_begin(session_s.partition, image_size, &session_s.handle);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_begin failed: %s", esp_err_to_name(err));
ota_stream_send_error("begin", "esp_ota_begin");
session_s.handle = 0;
session_s.partition = NULL;
return -4;
}
session_s.expected_size = image_size;
session_s.written_size = 0;
session_s.active = true;
ota_stream_send_status_fmt("{\"ota\":\"begin\",\"ok\":1,\"expect\":%lu}",
(unsigned long)image_size);
return 0;
}
/* 处理 OTA 数据包 */
static int ota_stream_handle_data(const uint8_t *data, uint16_t len)
{
if (!session_s.active) {
ota_stream_send_error("chunk", "no_session");
return -1;
}
if (len < 1) {
/* 空数据包 */
return 0;
}
/* data[0] 是 opcode,后面是实际数据 */
const uint8_t *payload = data + 1;
uint16_t payload_len = len - 1;
if (payload_len == 0) {
return 0;
}
/* 检查是否超出预期大小 */
if (session_s.written_size + payload_len > session_s.expected_size) {
ESP_LOGE(tag_s, "OTA overflow: %u + %u > %u",
(unsigned)session_s.written_size,
(unsigned)payload_len,
(unsigned)session_s.expected_size);
ota_stream_send_error("chunk", "overflow");
ota_stream_abort_internal();
return -2;
}
/* 写入数据 */
esp_err_t err = esp_ota_write(session_s.handle, payload, payload_len);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_write failed: %s", esp_err_to_name(err));
ota_stream_send_error("chunk", "write");
ota_stream_abort_internal();
return -3;
}
session_s.written_size += payload_len;
/* 每 8192 字节发送一次进度,或最后一次 */
static size_t last_reported = 0;
if (session_s.written_size - last_reported >= 8192 ||
session_s.written_size >= session_s.expected_size) {
ota_stream_send_status_fmt("{\"ota\":\"chunk\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size);
last_reported = session_s.written_size;
}
return 0;
}
/* 处理 OTA 结束命令 */
static int ota_stream_handle_end(void)
{
if (!session_s.active) {
ota_stream_send_error("end", "no_session");
return -1;
}
/* 检查写入大小是否匹配 */
if (session_s.written_size != session_s.expected_size) {
ESP_LOGE(tag_s, "OTA incomplete: %u/%u",
(unsigned)session_s.written_size,
(unsigned)session_s.expected_size);
ota_stream_send_error("end", "incomplete");
ota_stream_abort_internal();
return -2;
}
/* 结束 OTA 写入 */
esp_err_t err = esp_ota_end(session_s.handle);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_end failed: %s", esp_err_to_name(err));
ota_stream_send_error("end", "esp_ota_end");
ota_stream_abort_internal();
return -3;
}
session_s.handle = 0;
session_s.active = false;
/* 设置启动分区 */
err = esp_ota_set_boot_partition(session_s.partition);
if (err != ESP_OK) {
ESP_LOGE(tag_s, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
ota_stream_send_error("end", "set_boot");
return -4;
}
ESP_LOGI(tag_s, "OTA successful, written %u bytes",
(unsigned)session_s.written_size);
/* 通过OTA管理器结束会话 */
ota_manager_end_session();
ota_stream_send_status_fmt("{\"ota\":\"end\",\"ok\":1,\"written\":%u}",
(unsigned)session_s.written_size);
/* 延时后重启 */
ESP_LOGI(tag_s, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
return 0;
}
int ota_binary_stream_process_packet(const uint8_t *data, uint16_t len)
{
if (data == NULL || len == 0) {
return -1;
}
uint8_t opcode = data[0];
switch (opcode) {
case OTA_OP_BEGIN:
return ota_stream_handle_begin(data, len);
case OTA_OP_DATA:
return ota_stream_handle_data(data, len);
case OTA_OP_END:
return ota_stream_handle_end();
default:
ESP_LOGW(tag_s, "Unknown OTA opcode: 0x%02X", opcode);
ota_stream_send_error("unknown", "invalid_opcode");
return -10;
}
}
esp_err_t ota_binary_stream_get_status_json(char *buf, size_t len)
{
if (buf == NULL || len == 0) {
return ESP_ERR_INVALID_ARG;
}
if (!session_s.active) {
strncpy(buf, "{\"ota\":\"idle\"}", len);
return ESP_OK;
}
snprintf(buf, len,
"{\"ota\":\"active\",\"written\":%u,\"expect\":%u,\"transport\":%d}",
(unsigned)session_s.written_size,
(unsigned)session_s.expected_size,
session_s.transport);
return ESP_OK;
}
/*
* OTA 二进制流接收模块
* 通用 OTA 实现,支持 BLE 和 UART 两种传输方式
* 协议:首字节 opcode + 数据
* 0x01 + uint32_t LE(4字节) = 开始OTA,指定固件总长度
* 0x02 + 数据 = 固件数据块
* 0x03 = 结束OTA,触发校验和重启
*/
#ifndef OTA_BINARY_STREAM_H
#define OTA_BINARY_STREAM_H
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* OTA 状态回调类型 */
typedef void (*ota_binary_stream_status_callback_t)(const char *json);
/* OTA 传输类型 */
typedef enum {
OTA_TRANSPORT_BLE = 0, /* BLE GATT OTA */
OTA_TRANSPORT_UART, /* UART 串口 OTA */
} ota_transport_type_t;
/**
* @brief 初始化 OTA 二进制流接收器
* @param transport 传输类型(BLE 或 UART)
* @param status_callback 状态回调函数,用于发送 OTA 状态 JSON
* @param target_version 目标版本号(可选,用于OTA管理器)
* @return ESP_OK 成功,其他错误码
*/
esp_err_t ota_binary_stream_init(ota_transport_type_t transport,
ota_binary_stream_status_callback_t status_callback,
const char *target_version);
/**
* @brief 反初始化 OTA 二进制流接收器
* 如果 OTA 进行中,会自动中止
*/
void ota_binary_stream_deinit(void);
/**
* @brief 处理接收到的 OTA 数据包
* 由传输层(BLE/UART)调用
* @param data 数据指针
* @param len 数据长度
* @return 0 成功处理,负数错误码(传输层可映射为相应错误)
*/
int ota_binary_stream_process_packet(const uint8_t *data, uint16_t len);
/**
* @brief 检查 OTA 是否进行中
* @return true OTA进行中,false 空闲
*/
bool ota_binary_stream_is_active(void);
/**
* @brief 中止当前 OTA 会话
* 清理资源,擦除不完整的固件写入
*/
void ota_binary_stream_abort(void);
/**
* @brief 获取 OTA 状态 JSON 字符串
* 用于主动查询OTA状态
* @param buf 输出缓冲区
* @param len 缓冲区长度
* @return ESP_OK 成功
*/
esp_err_t ota_binary_stream_get_status_json(char *buf, size_t len);
/* OTA 操作码定义(与数据包格式一致) */
#define OTA_OP_BEGIN 0x01 /* 开始OTA会话 */
#define OTA_OP_DATA 0x02 /* 传输固件数据 */
#define OTA_OP_END 0x03 /* 结束OTA会话 */
#ifdef __cplusplus
}
#endif
#endif /* OTA_BINARY_STREAM_H */
This diff is collapsed.
/*
* OTA 状态管理器
* 管理OTA更新状态,支持失败回退到上一版本
* 适用于 WiFi/HTTP OTA、BLE OTA、UART OTA
*/
#ifndef OTA_MANAGER_H
#define OTA_MANAGER_H
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* OTA 状态 */
typedef enum {
OTA_STATE_IDLE = 0, /* 空闲,无OTA进行中 */
OTA_STATE_IN_PROGRESS, /* OTA进行中 */
OTA_STATE_PENDING_VERIFY, /* OTA完成,等待验证 */
OTA_STATE_SUCCESS, /* OTA验证成功 */
OTA_STATE_FAILED, /* OTA失败,已回退或需要回退 */
OTA_STATE_ROLLBACK, /* 已回退到上一版本 */
} ota_state_t;
/* OTA 类型 */
typedef enum {
OTA_TYPE_HTTP = 0, /* WiFi/HTTP OTA */
OTA_TYPE_BLE, /* BLE OTA */
OTA_TYPE_UART, /* UART OTA */
} ota_type_t;
/* OTA 会话信息(存储于NVS) */
typedef struct {
ota_state_t state; /* 当前状态 */
ota_type_t type; /* OTA类型 */
char target_version[32]; /* 目标版本号 */
char source_version[32]; /* 升级前版本号 */
uint32_t timestamp; /* OTA开始时间戳 */
uint8_t boot_count; /* 新固件启动计数 */
uint8_t max_boot_attempts; /* 最大尝试次数 */
} ota_session_info_t;
/**
* @brief 初始化OTA管理器
* 在系统启动时调用,检查OTA状态并决定是否回退
* @return ESP_OK 正常启动,ESP_FAIL 已触发回退
*/
esp_err_t ota_manager_init(void);
/**
* @brief 开始OTA会话
* @param type OTA类型
* @param target_version 目标版本号
* @return ESP_OK 成功,其他错误码
*/
esp_err_t ota_manager_begin_session(ota_type_t type, const char *target_version);
/**
* @brief 结束OTA会话(标记为等待验证)
* 新固件写入完成后调用,标记状态为PENDING_VERIFY
* @return ESP_OK 成功
*/
esp_err_t ota_manager_end_session(void);
/**
* @brief 验证OTA成功
* 应用层确认固件运行正常后调用
* @return ESP_OK 成功
*/
esp_err_t ota_manager_mark_success(void);
/**
* @brief 标记OTA失败并触发回退
* 在确认新固件有问题时调用,立即回退到上一版本
* @param reason 失败原因
* @return ESP_OK 已触发回退,设备将重启
*/
esp_err_t ota_manager_mark_failed_and_rollback(const char *reason);
/**
* @brief 获取当前OTA状态
* @return 当前OTA状态
*/
ota_state_t ota_manager_get_state(void);
/**
* @brief 获取OTA会话信息
* @param info 输出结构体
* @return ESP_OK 成功,ESP_ERR_NOT_FOUND 无会话信息
*/
esp_err_t ota_manager_get_session_info(ota_session_info_t *info);
/**
* @brief 检查是否需要回退
* 在应用层检测到异常时调用,增加启动计数并决定是否回退
* @return true 需要回退,false 正常
*/
bool ota_manager_should_rollback(void);
/**
* @brief 执行回退到上一版本
* 切换到上一个OTA分区并重启
* @return ESP_OK 已设置回退,设备将重启
*/
esp_err_t ota_manager_rollback(void);
/**
* @brief 获取当前运行的OTA分区标签
* @return "ota_0" 或 "ota_1",失败返回NULL
*/
const char* ota_manager_get_current_partition_label(void);
/**
* @brief 获取当前运行的固件版本号(从NVS读取最后一次成功版本)
* @param buf 输出缓冲区
* @param len 缓冲区长度
* @return ESP_OK 成功
*/
esp_err_t ota_manager_get_stored_version(char *buf, size_t len);
/**
* @brief 更新存储的版本号(OTA成功后调用)
* @param version 版本号字符串
* @return ESP_OK 成功
*/
esp_err_t ota_manager_update_stored_version(const char *version);
#ifdef __cplusplus
}
#endif
#endif /* OTA_MANAGER_H */
#include "heart_payload.h"
#include "betteryread.h"
#include "cJSON.h"
#include "sdkconfig.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *heart_payload_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 *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool include_ip)
{
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
if (include_ip) {
const char *ip = (sta_ip != NULL && sta_ip[0] != '\0') ? sta_ip : "0.0.0.0";
cJSON_AddStringToObject(body, "ip", ip);
}
char id_full[64];
const char *dev = (device_id != NULL) ? device_id : "";
snprintf(id_full, sizeof(id_full), "app2dev/%s", dev);
char voltage_s[16];
snprintf(voltage_s, sizeof(voltage_s), "%.2f", get_voltage_v());
cJSON_AddStringToObject(body, "voltage", voltage_s);
cJSON_AddStringToObject(body, "device_ID", id_full);
cJSON_AddStringToObject(body, "version", CONFIG_MY_APP_VERSION);
return heart_payload_build(HEART_MSG_TYPE_HEARTBEAT, body);
}
char *heart_payload_alert_json_malloc(int message_type, const char *msg)
{
if (msg == NULL) {
return NULL;
}
if (message_type != HEART_MSG_TYPE_LOG_WARN && message_type != HEART_MSG_TYPE_LOG_ERROR) {
return NULL;
}
cJSON *body = cJSON_CreateObject();
if (!body) {
return NULL;
}
cJSON_AddStringToObject(body, "msg", msg);
return heart_payload_build(message_type, body);
}
#ifndef HEART_PAYLOAD_H
#define HEART_PAYLOAD_H
#include <stdbool.h>
/** 设备 → 手机,经 BLE 0xFFE3 / MQTT 下发的 message_type */
#define HEART_MSG_TYPE_HEARTBEAT 1
/** 告警日志(W),body.msg 为文本 */
#define HEART_MSG_TYPE_LOG_WARN 4
/** 错误日志(E),body.msg 为文本 */
#define HEART_MSG_TYPE_LOG_ERROR 5
/**
* 构造心跳 JSON(head.message_type = HEART_MSG_TYPE_HEARTBEAT)。
*
* body:device_ID、version、voltage;include_ip 为 true 时带 ip。
*/
char *heart_payload_json_malloc(const char *device_id, const char *sta_ip, bool include_ip);
/**
* 构造告警/错误 JSON,经 0xFFE3 Notify 即时推送(与周期心跳同特征值)。
*
* @param message_type HEART_MSG_TYPE_LOG_WARN 或 HEART_MSG_TYPE_LOG_ERROR
* @param msg 日志正文(可为 ESP_LOG 行内冒号后的内容)
*/
char *heart_payload_alert_json_malloc(int message_type, const char *msg);
#endif
#include "sdkconfig.h"
#include "remote_control.h"
#include "rc_pwm_control.h"
#include "task_manager.h"
#if CONFIG_APP_LINK_WIFI
#include "ota.h"
#endif
#include "cJSON.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *tag_s = "REMOTE_CTRL";
/* 超时保护:500ms 未收到命令则自动停止 */
#define RC_CMD_TIMEOUT_MS 500
#define RC_WATCHDOG_PERIOD_MS 100 /* 检查周期 100ms */
static volatile uint32_t s_last_cmd_time_ms = 0;
static volatile bool s_watchdog_running = false;
static TaskHandle_t s_watchdog_handle = NULL;
static uint32_t get_current_time_ms(void)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
static void rc_watchdog_task(void *param)
{
(void)param;
s_watchdog_running = true;
while (s_watchdog_running) {
vTaskDelay(pdMS_TO_TICKS(RC_WATCHDOG_PERIOD_MS));
uint32_t now = get_current_time_ms();
uint32_t last = s_last_cmd_time_ms;
/* 如果已经初始化过且超过 500ms 未收到命令 */
if (last != 0 && (now - last) > RC_CMD_TIMEOUT_MS) {
rc_vehicle_stop();
ESP_LOGW(tag_s, "RC timeout: no cmd for %d ms, vehicle stopped", (int)(now - last));
/* 重置时间戳,避免重复停止 */
s_last_cmd_time_ms = now;
}
}
s_watchdog_handle = NULL;
vTaskDelete(NULL);
}
esp_err_t remote_control_watchdog_start(void)
{
if (s_watchdog_handle != NULL) {
return ESP_OK; /* 已经在运行 */
}
s_last_cmd_time_ms = get_current_time_ms(); /* 初始化时间戳 */
return app_task_start(APP_TASK_RC_WATCHDOG, rc_watchdog_task, NULL, &s_watchdog_handle);
}
void remote_control_watchdog_stop(void)
{
s_watchdog_running = false;
/* task 会自行删除,句柄会在任务函数中设为 NULL */
}
static void update_last_cmd_time(void)
{
s_last_cmd_time_ms = get_current_time_ms();
}
void remote_control_apply_json(const char *json)
{
/* 更新上次接收命令的时间戳 */
update_last_cmd_time();
if (!json) {
return;
}
cJSON *root = cJSON_Parse(json);
if (!root) {
return;
}
cJSON *head = cJSON_GetObjectItem(root, "head");
if (!head || !cJSON_IsObject(head)) {
#if CONFIG_APP_LINK_WIFI
/* 仅 WiFi+MQTT 构建:云端 JSON 触发 HTTPS OTA */
ota_update_recmqtt(json);
#endif
cJSON_Delete(root);
return;
}
cJSON *m_type = cJSON_GetObjectItem(head, "message_type");
if (m_type && cJSON_IsNumber(m_type) && m_type->valueint == 1) {
ESP_LOGW(tag_s, "message_type=1: reboot (all devices)");
cJSON_Delete(root);
esp_restart();
return;
}
cJSON *body = cJSON_GetObjectItem(root, "body");
if (m_type && cJSON_IsNumber(m_type) && body) {
if (m_type->valueint == 4) {
cJSON *pin_ctrl = cJSON_GetObjectItem(body, "pin_setctrl");
if (pin_ctrl && cJSON_IsObject(pin_ctrl)) {
cJSON *jp = cJSON_GetObjectItem(pin_ctrl, "pin");
cJSON *jv = cJSON_GetObjectItem(pin_ctrl, "val");
if (jp && jv && cJSON_IsNumber(jp) && cJSON_IsNumber(jv)) {
int pin = jp->valueint;
int val = jv->valueint;
ESP_LOGI(tag_s, "GPIO: pin %d -> %d", pin, val);
rc_vehicle_shot(pin, val);
}
}
} else if (m_type->valueint == 3) {
cJSON *pwm_ctrl = cJSON_GetObjectItem(body, "pwm_ctrl");
if (pwm_ctrl && cJSON_IsObject(pwm_ctrl)) {
cJSON *jm = cJSON_GetObjectItem(pwm_ctrl, "mode");
cJSON *jt = cJSON_GetObjectItem(pwm_ctrl, "type");
cJSON *jv = cJSON_GetObjectItem(pwm_ctrl, "val");
if (jm && jv && cJSON_IsNumber(jm) && cJSON_IsNumber(jv)) {
int mode = jm->valueint;
int type = jt && cJSON_IsNumber(jt) ? jt->valueint : 0;
int val = jv->valueint;
ESP_LOGI(tag_s, "PWM: mode %d type %d val %d", mode, type, val);
rc_vehicle_control(mode, val);
}
}
}
}
cJSON_Delete(root);
}
#ifndef REMOTE_CONTROL_H
#define REMOTE_CONTROL_H
#include "esp_err.h"
/** BLE FFE1 / 遥控 JSON:单帧 UTF-8 最大字节数(不含结尾 NUL) */
#define REMOTE_CTRL_JSON_MAX_BYTES 2048
/** 解析 MQTT/BLE 下发的 JSON,执行 OTA、重启、PWM/GPIO 控制 */
void remote_control_apply_json(const char *json);
/**
* @brief 启动遥控超时守护任务
* 500ms 未收到控制命令将自动停止车辆
*/
esp_err_t remote_control_watchdog_start(void);
/**
* @brief 停止遥控超时守护任务
*/
void remote_control_watchdog_stop(void);
#endif
This diff is collapsed.
#ifndef __WIFI_DEV_NUM_CONFIG_H__
#define __WIFI_DEV_NUM_CONFIG_H__
#include "esp_err.h"
#include <stddef.h>
#include "device_nvs.h"
// 引脚定义 (ESP32-S3 默认 BOOT 键一般是 0)
#define BTN_WIFI_IO 0
#define BTN_DEVID_IO 4
void save_to_nvs(const char *key, const char *value);
esp_err_t read_from_nvs(const char *key, char *buf, size_t len);
void start_config_web(void);
esp_err_t init_spiffs(void);
void nowifidata_start_config_web(void);
void button_monitor_task_init(void);
#endif
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
ota_0, app, ota_0, , 0x600000,
ota_1, app, ota_1, , 0x600000,
storage, data, spiffs, , 0x50000,
\ No newline at end of file
@echo off
setlocal EnableExtensions EnableDelayedExpansion
goto :Main
:Require
if not exist "%~1" (
echo.
echo ERROR: missing file:
echo %~1
echo Run: idf.py build
echo.
pause
exit /b 1
)
exit /b 0
:ReadVersion
for /f "usebackq tokens=2 delims==" %%A in (`findstr /b "CONFIG_MY_APP_VERSION" "%~1" 2^>nul`) do (
set "VER=%%~A"
)
exit /b 0
:HashFile
set "HASH="
for /f "skip=1 usebackq delims=" %%H in (`certutil -hashfile "%~1" SHA256 2^>nul`) do (
set "HASH=%%H"
goto :HashDone
)
:HashDone
set "HASH=!HASH: =!"
set "%~2=!HASH!"
exit /b 0
:AppendJsonFile
call :HashFile "%~1" H
for %%A in ("%~1") do (
set "FN=%%~nxA"
set "SZ=%%~zA"
)
if "%~2"=="last" (
>>"%REL%\manifest.json" echo {"name":"!FN!","bytes":!SZ!,"sha256":"!H!"}
) else (
>>"%REL%\manifest.json" echo {"name":"!FN!","bytes":!SZ!,"sha256":"!H!"},
)
exit /b 0
:Main
REM Copy idf.py build -> firmware/release/ (double-click OK)
cd /d "%~dp0"
cd /d ".."
set "ROOT=%CD%"
set "BUILD=%ROOT%\build"
set "REL=%ROOT%\firmware\release"
set "OTA=%REL%\ota"
set "FAC=%REL%\factory"
call :Require "%BUILD%\ESPRCCar.bin"
call :Require "%BUILD%\bootloader\bootloader.bin"
call :Require "%BUILD%\partition_table\partition-table.bin"
call :Require "%BUILD%\ota_data_initial.bin"
call :Require "%BUILD%\storage.bin"
if not exist "%OTA%" mkdir "%OTA%"
if not exist "%FAC%" mkdir "%FAC%"
echo [1/4] Copy binaries ...
copy /Y "%BUILD%\ESPRCCar.bin" "%OTA%\ESPRCCar.bin" >nul
copy /Y "%BUILD%\ESPRCCar.bin" "%FAC%\ESPRCCar.bin" >nul
copy /Y "%BUILD%\bootloader\bootloader.bin" "%FAC%\bootloader.bin" >nul
copy /Y "%BUILD%\partition_table\partition-table.bin" "%FAC%\partition-table.bin" >nul
copy /Y "%BUILD%\ota_data_initial.bin" "%FAC%\ota_data_initial.bin" >nul
copy /Y "%BUILD%\storage.bin" "%FAC%\storage.bin" >nul
if exist "%BUILD%\flash_args" copy /Y "%BUILD%\flash_args" "%FAC%\flash_args.txt" >nul
echo [2/4] Read version ...
set "VER=unknown"
if exist "%ROOT%\sdkconfig" call :ReadVersion "%ROOT%\sdkconfig"
if "!VER!"=="unknown" if exist "%ROOT%\sdkconfig.defaults" call :ReadVersion "%ROOT%\sdkconfig.defaults"
set "UTC=unknown"
for /f "delims=" %%T in ('powershell -NoProfile -Command "(Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')" 2^>nul') do set "UTC=%%T"
set "LOCAL=%date% %time%"
echo [3/4] Write manifest.json ...
> "%REL%\manifest.json" echo {
>>"%REL%\manifest.json" echo "project": "ESPRCCar",
>>"%REL%\manifest.json" echo "chip": "esp32s3",
>>"%REL%\manifest.json" echo "flash_mode": "dio",
>>"%REL%\manifest.json" echo "flash_freq": "80m",
>>"%REL%\manifest.json" echo "flash_size": "16MB",
>>"%REL%\manifest.json" echo "version": "!VER!",
>>"%REL%\manifest.json" echo "generated_utc": "!UTC!",
>>"%REL%\manifest.json" echo "ota_image": "release/ota/ESPRCCar.bin",
>>"%REL%\manifest.json" echo "factory_dir": "release/factory",
>>"%REL%\manifest.json" echo "files": [
call :AppendJsonFile "%OTA%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\bootloader.bin"
call :AppendJsonFile "%FAC%\partition-table.bin"
call :AppendJsonFile "%FAC%\ESPRCCar.bin"
call :AppendJsonFile "%FAC%\ota_data_initial.bin"
call :AppendJsonFile "%FAC%\storage.bin" last
>>"%REL%\manifest.json" echo ]
>>"%REL%\manifest.json" echo }
echo [4/5] Copy Android device doc ...
set "DOC_SRC=%ROOT%\docs\Android端设备对接文档_v!VER!.md"
if exist "!DOC_SRC!" (
copy /Y "!DOC_SRC!" "%REL%\Android端设备对接文档_v!VER!.md" >nul
echo docs -^> release\Android端设备对接文档_v!VER!.md
) else (
echo WARN: missing !DOC_SRC! (create docs\Android端设备对接文档_v!VER!.md before handoff)
)
echo [5/5] Write VERSION.txt ...
call :HashFile "%OTA%\ESPRCCar.bin" OTAHASH
for %%A in ("%OTA%\ESPRCCar.bin") do set "OTASZ=%%~zA"
> "%REL%\VERSION.txt" (
echo ESPRCCar firmware release
echo =========================
echo version: !VER!
echo project: ESPRCCar
echo chip: esp32s3
echo built_utc: !UTC!
echo built_local: !LOCAL!
echo.
echo OTA:
echo path: firmware/release/ota/ESPRCCar.bin
echo bytes: !OTASZ!
echo sha256: !OTAHASH!
echo.
echo Factory: firmware/release/factory/
echo tool: Espressif Flash Download Tools
echo addrs: see factory/flash_args.txt
echo.
echo machine_readable: manifest.json
echo.
echo android_doc: firmware/release/Android端设备对接文档_v!VER!.md
)
echo.
echo Done: firmware/release/ version=!VER!
echo VERSION.txt + manifest.json updated
echo.
echo Note: run after idf.py build; build does not auto-copy here.
echo.
pause
exit /b 0
# Custom partition table (dual OTA + SPIFFS); needs >=16MB flash per partitions.csv
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
# Link mode (one of: WIFI / BLE / UART)
# CONFIG_APP_LINK_WIFI is not set
# CONFIG_APP_LINK_BLE is not set
CONFIG_APP_LINK_UART=y
# CONFIG_APP_BLE_OTA is not set
# UART link: GPIO17 TX / GPIO18 RX
CONFIG_APP_UART_LINK_BAUDRATE=115200
CONFIG_APP_UART_LINK_TX_GPIO=17
CONFIG_APP_UART_LINK_RX_GPIO=18
CONFIG_APP_PWM_IO15_SERVO=y
# CONFIG_APP_PWM_IO15_ESC is not set
# GPIO16: 1102 steering servo uses this pin. Must be SERVO. If dual ESC uses 15/16, set CONFIG_APP_PWM_IO16_ESC=y
CONFIG_APP_PWM_IO16_SERVO=y
# CONFIG_APP_PWM_IO16_ESC is not set
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLUEDROID_ENABLED is not set
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_CONTROLLER_ENABLED=y
# UART0 (GPIO43 TX / GPIO44 RX): debug + optional uart_comm in Release
CONFIG_APP_DEBUG_UART_NUM=0
CONFIG_APP_DEBUG_UART_TX_GPIO=-1
CONFIG_APP_DEBUG_UART_RX_GPIO=-1
# ESP-IDF console on default UART0
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
# Release defaults: -Os, no console UART, BLE log via 0xFFE3
# Build: idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" build
# CONFIG_COMPILER_OPTIMIZATION_DEBUG is not set
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# CONFIG_COMPILER_OPTIMIZATION_DEFAULT is not set
# CONFIG_COMPILER_OPTIMIZATION_PERF is not set
# CONFIG_COMPILER_OPTIMIZATION_NONE is not set
# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE is not set
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y
# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set
# CONFIG_ESP_COREDUMP_ENABLE is not set
# CONFIG_LOG_DEFAULT_LEVEL_INFO is not set
CONFIG_LOG_DEFAULT_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL=2
# Release: no UART console (W/E via BLE 0xFFE3 notify)
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
CONFIG_ESP_CONSOLE_NONE=y
# When APP_UART_MODE_DEBUG=y: uart_comm uses UART from APP_DEBUG_UART_*; no ESP_LOG
CONFIG_ROBO_APP_FW_RELEASE=y
# CONFIG_ROBO_APP_FW_DEBUG is not set
CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y
CONFIG_LOG_BOOTLOADER_LEVEL=2
# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_LOG_LEVEL=2
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #28a745; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
</style>
</head>
<body>
<div class="card">
<h1>🦉 飞驰设备配置中心</h1>
<form action='/save' method='POST'>
<b>WiFi 配置 %s</b>
<input name='ssid' placeholder='WiFi名称' %s>
<input name='pass' type='password' placeholder='WiFi密码' %s>
<br>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<button type='submit' class='btn'>保存并重启设备</button>
</form>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
</style>
</head>
<body>
<div class="card">
<h1>BLE 设备配置</h1>
<p style="color:#666;font-size:14px;">本模式不保存 WiFi,仅设备号与蓝牙广播名</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='设备号' %s>
<b>蓝牙广播名 %s</b>
<input name='ble_name' placeholder='手机扫描看到的名称' %s>
<button type='submit' class='btn'>保存并重启</button>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; background: #f0f2f5; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 380px; margin: auto; }
input { width: 90%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #ddd; }
.btn { width: 95%; padding: 15px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
b { display: block; text-align: left; margin-left: 5%; margin-top: 10px; color: #555; }
</style>
</head>
<body>
<div class="card">
<h1>串口设备配置</h1>
<p style="color:#666;font-size:14px;">UART 模式不保存 WiFi,仅需填写设备号</p>
<form action='/save' method='POST'>
<b>设备 ID %s</b>
<input name='devid' placeholder='请输入设备号' %s>
<button type='submit' class='btn'>保存并重启</button>
</form>
</div>
</body>
</html>
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