From fd71b83e3ac5db3bd9311130a7bf6a1506eccac9 Mon Sep 17 00:00:00 2001 From: Horldsence Date: Wed, 14 Oct 2026 19:45:15 +0800 Subject: [PATCH] =?UTF-8?q?reference:=20=E6=97=A9=E5=85=88=E9=82=A3?= =?UTF-8?q?=E7=89=88=20C=20=E5=86=99=E7=9A=84=E8=A3=85=E7=94=B2=E6=9D=BF?= =?UTF-8?q?=E5=9B=BA=E4=BB=B6=EF=BC=88ESP-IDF=20=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `armor/` 是它的 Rust 改写版,这一份留着当参考:ADC 阈值的 2000 就是从它标定出来的 (`.env.example` 里那句 "same as the reference firmware" 指的就是它),而且它的 README 记着实测设备与 MAC,换硬件时是有用的现场记录。 只收了 20 个源文件。它自带的 `.gitignore` 已经排掉 `build/`(211 MB 的 ESP-IDF 构建产物) 与生成的 `sdkconfig`,所以仓库里不会多出这些东西。 两处顺带处理: - `reference/armor/.git` 是一个**空的**仓库壳(`git init` 过,零提交、零 remote、零 ref), 而外层 git 正因为它的存在而拒绝索引 `reference/` 下的任何文件 (`error: 'reference/armor/' does not have a commit checked out`)。它被挪到 `desktop/.dev/backup/reference-armor-git`(可逆,且该目录已被忽略),不是删除。 - 它的 README 指着 `../armor-platformio-backup/`,那份备份并不存在。改成明说"没有进仓库", 免得以后有人照着找一个不存在的目录。 另外把 `/docs/` 加进 .gitignore:那是工作区里另起的一个 Astro 文档站,与本仓库无关。 --- .gitignore | 4 + reference/armor/.clangd | 2 + reference/armor/.gitattributes | 106 +++++++++++ reference/armor/.gitignore | 209 +++++++++++++++++++++ reference/armor/CMakeLists.txt | 8 + reference/armor/README.md | 205 +++++++++++++++++++++ reference/armor/idf.ps1 | 83 +++++++++ reference/armor/main/CMakeLists.txt | 21 +++ reference/armor/main/air.cpp | 222 +++++++++++++++++++++++ reference/armor/main/air.hpp | 57 ++++++ reference/armor/main/armor_main.cpp | 215 ++++++++++++++++++++++ reference/armor/main/armor_time.hpp | 16 ++ reference/armor/main/button.cpp | 126 +++++++++++++ reference/armor/main/button.hpp | 78 ++++++++ reference/armor/main/hit.cpp | 192 ++++++++++++++++++++ reference/armor/main/hit.hpp | 34 ++++ reference/armor/main/led_strip_encoder.c | 126 +++++++++++++ reference/armor/main/led_strip_encoder.h | 36 ++++ reference/armor/main/storage.cpp | 92 ++++++++++ reference/armor/main/storage.hpp | 36 ++++ reference/armor/partitions.csv | 11 ++ reference/armor/sdkconfig.defaults | 7 + 22 files changed, 1886 insertions(+) create mode 100644 reference/armor/.clangd create mode 100644 reference/armor/.gitattributes create mode 100644 reference/armor/.gitignore create mode 100644 reference/armor/CMakeLists.txt create mode 100644 reference/armor/README.md create mode 100644 reference/armor/idf.ps1 create mode 100644 reference/armor/main/CMakeLists.txt create mode 100644 reference/armor/main/air.cpp create mode 100644 reference/armor/main/air.hpp create mode 100644 reference/armor/main/armor_main.cpp create mode 100644 reference/armor/main/armor_time.hpp create mode 100644 reference/armor/main/button.cpp create mode 100644 reference/armor/main/button.hpp create mode 100644 reference/armor/main/hit.cpp create mode 100644 reference/armor/main/hit.hpp create mode 100644 reference/armor/main/led_strip_encoder.c create mode 100644 reference/armor/main/led_strip_encoder.h create mode 100644 reference/armor/main/storage.cpp create mode 100644 reference/armor/main/storage.hpp create mode 100644 reference/armor/partitions.csv create mode 100644 reference/armor/sdkconfig.defaults diff --git a/.gitignore b/.gitignore index 8a22b3c..3e8ecc1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ # macOS 的目录元数据。不锚定,子目录里的也一起忽略。 .DS_Store + +# `docs/simba_docs`:工作区里另起的一个 Astro 文档站,与本仓库的目录结构无关, +# 也不属于本项目的产物。留着不跟踪,免得以后 `git add -A` 顺手把它带进来。 +/docs/ diff --git a/reference/armor/.clangd b/reference/armor/.clangd new file mode 100644 index 0000000..437f255 --- /dev/null +++ b/reference/armor/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/reference/armor/.gitattributes b/reference/armor/.gitattributes new file mode 100644 index 0000000..aed7efd --- /dev/null +++ b/reference/armor/.gitattributes @@ -0,0 +1,106 @@ +# ============================================================================= +# armor — 文本/二进制归属与行尾(CRLF/LF)策略 +# ----------------------------------------------------------------------------- +# 规则:仓库里一律存 LF;工作区只在 Windows 原生脚本上使用 CRLF。 +# 目的: +# * 避免不同机器(core.autocrlf 开着/关着)之间产生"整文件假改动"; +# * 让 diff / blame / 合并只在真正改动的行上出现; +# * .ps1/.bat/.cmd 保持 CRLF,兼容 cmd.exe 与习惯 Windows 的工具链。 +# +# 注意:.gitattributes 的 text/eol 属性优先级高于 core.autocrlf。 +# 本文件用 "* text=auto eol=lf" 覆盖了所有路径,所以无论别人本机 +# core.autocrlf 是 true / false / 未设置,入库结果都完全一致(已实测)。 +# 若想改成"工作区也全用 LF"(含 .ps1),只需把第 1 节的 +# eol=crlf 全部改成 eol=lf,其余规则不用动。 +# +# 核对命令: +# git check-attr -a -- <路径> 查看某路径最终生效的属性 +# git ls-files --eol 查看已入库文件的 i/(索引) w/(工作区) 行尾 +# ============================================================================= + +# 默认:内容自动判定是否为文本;文本一律在索引与工作区使用 LF +* text=auto eol=lf + + +# ============================================================================= +# 1. Windows 原生脚本 —— 工作区保持 CRLF +# ============================================================================= + +# cmd.exe 对纯 LF 的 .bat/.cmd 处理有坑(尤其是带 label/goto 的脚本), +# 必须 CRLF;.ps1/.psm1 按 Windows 惯例也保持 CRLF。 +*.ps1 text eol=crlf +*.psm1 text eol=crlf +*.bat text eol=crlf +*.cmd text eol=crlf + + +# ============================================================================= +# 2. 源码与文本配置 —— 显式声明为文本,工作区 LF +# ============================================================================= + +# 固件源码(main/ 下的 C/C++ 与官方 RMT 编码器) +*.c text eol=lf +*.h text eol=lf +*.cpp text eol=lf +*.hpp text eol=lf +*.cc text eol=lf +*.hh text eol=lf + +# 构建系统 +CMakeLists.txt text eol=lf +*.cmake text eol=lf +Kconfig text eol=lf +Kconfig.projbuild text eol=lf + +# ESP-IDF 配置:sdkconfig 由工具生成(已忽略),defaults 是入库的那份 +sdkconfig text eol=lf +sdkconfig.old text eol=lf +sdkconfig.defaults text eol=lf +sdkconfig.ci* text eol=lf + +# 文档与数据 +*.md text eol=lf +*.rst text eol=lf +*.txt text eol=lf +*.csv text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +# 本仓库自身的元文件与工程级工具配置 +.gitignore text eol=lf +.gitattributes text eol=lf +.clangd text eol=lf + + +# ============================================================================= +# 3. 二进制 —— 禁止任何行尾转换与文本合并 +# ============================================================================= + +# 固件/构建产物(已被 .gitignore 排除,这里再兜一层,防止被强制入库时被破坏) +*.bin binary +*.elf binary +*.hex binary +*.a binary +*.o binary + +# 图片与压缩包 +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.bmp binary +*.svg binary +*.zip binary +*.gz binary +*.tar binary +*.7z binary + +# 密钥与证书:绝不能因为行尾转换而改变字节 +*.pem binary +*.key binary +*.der binary +*.crt binary +*.p12 binary +*.pfx binary diff --git a/reference/armor/.gitignore b/reference/armor/.gitignore new file mode 100644 index 0000000..fd38e7b --- /dev/null +++ b/reference/armor/.gitignore @@ -0,0 +1,209 @@ +# ============================================================================= +# armor — ESP32-C3 / ESP-IDF v6.1 工程专用 .gitignore +# ----------------------------------------------------------------------------- +# 取舍原则 +# 1) 只忽略「本机生成物 + 本机专有配置 + 编辑器/系统杂物」; +# 源码、构建脚本、分区表、可复现配置一律入库。 +# 2) 能用 "/" 锚定就用锚定模式,避免将来新增同名目录时被误伤。 +# 3) 每段都写明理由,后人可据此判断能否删改。 +# 4) 语法提醒:.gitignore 的 "#" 注释必须独占一行,不能写在模式后面; +# 行尾空格会被静默丢弃(或被当成模式的一部分),也不要加。 +# +# 自检 +# git status --short # 期望只列出应该入库的源文件 +# git check-ignore -v <路径> # 查看某路径被哪条规则命中 +# ============================================================================= + + +# ============================================================================= +# 1. ESP-IDF 构建输出(体积最大,本工作区约 17 MB,必须忽略) +# ============================================================================= + +# idf.py 默认在工程根目录生成 build/:CMake/Ninja 缓存、.o、静态库、固件 +# 镜像、flasher_args.json、compile_commands.json 全在里面。 +# 本工程路径是纯 ASCII,不会触发 idf.ps1 的「非 ASCII 路径 -> 外置构建目录」 +# 逻辑,所以 build/ 就落在工程内。 +/build/ + +# 其它常见构建目录命名:多配置构建、CMake 预设、CLion +/build-*/ +/cmake-build-*/ + +# 仅当 IDF_BUILD_DIR 被指向工程内部时才会出现 +# (idf.ps1 的默认兜底是 <盘符>:\esp-idf-build\armor,位于仓库之外) +/esp-idf-build/ + +# clangd --background-index 的索引缓存(.vscode 里启用了该选项) +/.cache/ + +# 根目录下零散的 CMake/Ninja 缓存(正常应在 build/ 内,保险起见) +/.ninja_deps +/.ninja_log +/CMakeCache.txt +/CMakeFiles/ + + +# ============================================================================= +# 2. ESP-IDF 生成的本机配置 +# ============================================================================= + +# sdkconfig : menuconfig/构建时自动写出,代表「本机当前状态」,每台机器都可能不同 +# sdkconfig.old : 上一次配置的备份 +# 可复现的那份是 sdkconfig.defaults(已生成,见第 9 节),所以这两个都忽略。 +/sdkconfig +/sdkconfig.old + +# 提示:ESP-IDF 工具在 Windows 上写 sdkconfig / sdkconfig.defaults 用的是 CRLF, +# 这是工具行为、无需干预 —— 入库行尾由 .gitattributes 统一成 LF, +# 本地暂时出现 CRLF 不会让 git status 变脏(已实测)。 + + +# ============================================================================= +# 3. IDF Component Manager(引入 idf_component.yml 后才会出现) +# ============================================================================= + +# managed_components/ 是下载到本地的第三方组件副本,官方文档明确说它 +# 「通常不纳入版本控制」;dependencies.lock 由工具自动求解生成、禁止手改。 +# 若团队要锁定依赖版本,可注释掉 dependencies.lock 并把它提交入库。 +/managed_components/ +/dependencies.lock + +# 工具导出的编译数据库副本(真正那份在 build/ 里,已随 build/ 忽略) +/compile_commands.json + + +# ============================================================================= +# 4. 固件产物与运行日志 +# ============================================================================= + +# 只锚定根目录,避免误伤将来放进 assets/ 之类的素材 +/*.bin +/*.elf +/*.map +/*.hex + +# README 里 monitor 打印的 CSV,属于采集到的运行数据,不入库 +/logs/ +*.log + + +# ============================================================================= +# 5. 密钥类文件 —— 提交即泄密,且很难从历史里挽回 +# ============================================================================= + +# 安全启动签名私钥 / Flash 加密密钥 / 证书私钥。 +# 若仓库确实需要存放公开证书,请把下面的通配收窄成具体文件名。 +*.pem +*.key +*.p12 +*.pfx + + +# ============================================================================= +# 6. 编辑器 / IDE(本机专有,往往含绝对路径) +# ============================================================================= + +# VS Code + ESP-IDF 扩展自动生成。当前 .vscode/settings.json 里是 +# C:\esp\v6.1\esp-idf、d:\宣筹\...\build、COM10 这类本机绝对路径, +# 提交上去只会误导别人。 +/.vscode/* +# 想共享「推荐扩展列表」时,取消下一行注释(上一行的 /.vscode/* 必须保留) +!/.vscode/extensions.json + +/.idea/ +/.vs/ +*.code-workspace +*.sublime-project +*.sublime-workspace + +# 编辑器临时/交换文件 +*.swp +*.swo +*~ +.#* +\#*# + + +# ============================================================================= +# 7. Python 与测试/覆盖率工具缓存(ESP-IDF 工具链脚本产物) +# ============================================================================= +__pycache__/ +*.py[cod] +.venv/ +venv/ +/.mypy_cache/ +/.pytest_cache/ +/.ruff_cache/ + +# pytest-embedded、单元测试与尺寸报告(ESP-IDF 自身仓库也忽略这些) +pytest-embedded/ +pytest_embedded_log/ +TEST_LOGS/ +XUNIT_RESULT*.xml +build_summary_*.xml +app_info_*.txt +size_info*.txt +.manifest_sha + +# 覆盖率 +*.gcda +*.gcno +*.gcov +coverage.info +coverage_report/ + + +# ============================================================================= +# 8. 操作系统杂物 +# ============================================================================= +Thumbs.db +ehthumbs.db +desktop.ini +$RECYCLE.BIN/ +.DS_Store +.AppleDouble +._* +*.lnk + + +# ============================================================================= +# 9. 必须入库的文件(切勿加入忽略列表) +# ============================================================================= +# CMakeLists.txt / main/CMakeLists.txt +# main/*.cpp main/*.hpp main/*.c main/*.h +# idf.ps1 构建入口(含非 ASCII 路径 workaround) +# partitions.csv 自定义分区表 +# README.md 工程文档(移植要点与踩坑记录) +# .clangd 工程级 clangd 配置 +# sdkconfig.defaults 可复现配置来源(由 idf.py save-defconfig 生成) +# .gitattributes 行尾(CRLF/LF)与文本/二进制归属策略 +# +# 提醒:ESP-IDF 自身仓库的 .gitignore 会忽略 .clangd 和 .vscode/; +# 本工程故意保留 .clangd(它是为 clangd 去掉 -f*/-m* 的项目配置), +# 不要照抄 IDF 仓库的规则。 + + +# ============================================================================= +# 10. sdkconfig.defaults 与 sdkconfig 的关系(2026-10-14 核对并修复) +# ============================================================================= +# 忽略 sdkconfig 的前提是 sdkconfig.defaults 存在且正确。核对时发现: +# * README「目录结构」里列的 sdkconfig.defaults 当时并不存在; +# * 当时的 sdkconfig(以及 build/config/sdkconfig.h)是 +# CONFIG_PARTITION_TABLE_SINGLE_APP=y、CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y, +# 即用的是默认 single-app 分区表,partitions.csv 根本没有生效; +# * 而 README「实测验证结果」的 boot log 却是 4 MB flash + 2 MB factory 分区。 +# 已按文档修正(4 MB flash + CONFIG_PARTITION_TABLE_CUSTOM=y)重新配置, +# 并用 `idf.py save-defconfig` 生成 sdkconfig.defaults,内容就是最小差异: +# CONFIG_IDF_TARGET="esp32c3" +# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# CONFIG_PARTITION_TABLE_CUSTOM=y +# (CONFIG_PARTITION_TABLE_CUSTOM_FILENAME 的 Kconfig 默认值本来就是 +# "partitions.csv",所以 save-defconfig 不会重复写入。) +# +# 维护规则: +# * 改配置请改 sdkconfig.defaults(或 menuconfig 后重跑 save-defconfig), +# 不要把 sdkconfig 提交进来; +# * 想确认「相对 IDF 默认值改了什么」:重跑 idf.py save-defconfig 再看 diff; +# * 构建日志里分区表应为 nvs 0x9000 / phy_init 0xf000 / factory 0x10000 2M, +# 否则说明 defaults 没生效或 partitions.csv 没被采用。 +# ============================================================================= diff --git a/reference/armor/CMakeLists.txt b/reference/armor/CMakeLists.txt new file mode 100644 index 0000000..82c6112 --- /dev/null +++ b/reference/armor/CMakeLists.txt @@ -0,0 +1,8 @@ +# ESP-IDF project for the "armor" hit-detection node (ESP32-C3). +# +# Converted from a PlatformIO / Arduino project; see README.md for the mapping +# between the original Arduino APIs and the ESP-IDF ones used here. +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(armor) diff --git a/reference/armor/README.md b/reference/armor/README.md new file mode 100644 index 0000000..b2bfc36 --- /dev/null +++ b/reference/armor/README.md @@ -0,0 +1,205 @@ +# armor (ESP32-C3) — ESP-IDF 工程 + +本目录原为一个 PlatformIO / Arduino 工程,现已改造为 ESP-IDF 工程。 +原始 PlatformIO 工程曾备份在同级的 `../armor-platformio-backup/`——**那份备份没有进仓库**, +这里的 `reference/armor/` 只保留改造后的 ESP-IDF 工程。 + +- 目标芯片:**ESP32-C3**(实测设备 `ESP32-C3 (QFN32) rev v0.4`,USB-Serial/JTAG,MAC `98:a3:16:4c:67:c8`) +- ESP-IDF 版本:**v6.1.0** +- 状态:**已构建并烧录成功,实测稳定运行** + +## 目录结构 + +``` +armor/ +├── CMakeLists.txt # ESP-IDF 顶层工程文件 +├── sdkconfig.defaults # 默认配置(替代 platformio.ini 的 board/build_flags) +│ # 由 idf.py save-defconfig 生成:IDF_TARGET / 4MB flash / 自定义分区表 +├── partitions.csv # 自定义分区表(2 MB factory 应用分区) +├── idf.ps1 # 便捷脚本:自动激活 ESP-IDF 后转发给 idf.py +├── .gitignore # 忽略 build/、sdkconfig、IDE 与本机生成物 +├── .gitattributes # 行尾策略:仓库统一 LF,.ps1/.bat/.cmd 在工作区用 CRLF +└── main/ + ├── CMakeLists.txt + ├── armor_main.cpp # app_main:NVS / 串口 / 按键 / 业务任务(原 main.cpp) + ├── air.hpp / air.cpp # ESP-NOW 对拷链路 + ├── hit.hpp / hit.cpp # 压电片击打检测 + WS2812 + ├── button.hpp/.cpp # OneButton 2.6.1 状态机移植 + ├── storage.hpp/.cpp # NVS 封装(替代 Arduino Preferences) + ├── armor_time.hpp # millis() + └── led_strip_encoder.* # 官方 WS2812 RMT 编码器(取自 IDF 示例) +``` + +## 与 PlatformIO 工程的对应关系 + +| PlatformIO / Arduino | ESP-IDF | +| --- | --- | +| `platform = espressif32`, `framework = arduino` | ESP-IDF v6.1 原生工程 | +| `board = esp32-c3-devkitm-1` | `CONFIG_IDF_TARGET="esp32c3"` | +| `lib_deps = Adafruit NeoPixel` | `led_strip_encoder.c` + RMT TX 驱动 | +| `lib_deps = OneButton` | `main/button.cpp`(逐行移植其状态机) | +| `Preferences` | `storage.cpp`(NVS) | +| `ARDUINO_USB_CDC_ON_BOOT=1`(`Serial`) | USB Serial/JTAG 控制台 + `usb_serial_jtag_read_bytes()` | +| `Serial0`(GPIO20/21) | `uart_driver_install(UART_NUM_0)`,TX=21 / RX=20 | +| `setup()` / `loop()` / `loop2()` | `app_main()` / `hit_task` / `loop2_task` | +| `gen_build.cmd`, `upload.cmd` | `idf.ps1`(见下) | + +## 移植要点 + +### 1. ADC:旧驱动在新版 IDF 中已被移除 + +原代码使用 `driver/adc.h` 的 legacy DMA 接口(`adc_digi_initialize` / +`adc_digi_controller_configure` / `adc_digi_read_bytes`),该驱动在 ESP-IDF v6 +中已删除。现改用 `esp_adc/adc_continuous.h`,采样参数保持一致: +总采样率 40 kHz、4 通道、12 bit、`ADC_ATTEN_DB_0`,每帧 10 ms(1600 字节)。 +解析方式仍是直接读取 `adc_digi_output_data_t::type2`(C3 的 12 bit 布局), +因此击打判定阈值 `HIT_THRESHOLD = 2000` 的语义不变。 + +### 2. WS2812:软件位翻转 → RMT 硬件驱动 + +Adafruit_NeoPixel 是软件位翻转驱动,这里换成 RMT + 官方 `led_strip_encoder`。 +字节序保持 Adafruit 的 `NEO_GRB`(GRB + MSB first)。 + +**ESP32-C3 的 RMT 资源限制(踩坑记录)**:C3 的 RMT 只有一个 block,总计 +**96 个 symbol 的通道内存**,按 `SOC_RMT_MEM_WORDS_PER_CHANNEL = 48` 为单位分配给 +两个 TX 通道。因此: + +- `mem_block_symbols = 64` 会被向上取整到 96,即**一个通道吃光全部内存**, + 导致第二个 `rmt_new_tx_channel()` 失败: + `E rmt: rmt_tx_register_to_group(168): no free tx channels` → + `ESP_ERROR_CHECK` 触发 `abort()` → 芯片反复重启。 +- 4 条灯带在固件中始终显示同一颜色,所以这里只用**一个** RMT TX 通道, + 令其独占全部 96 symbol(足够一次装下 3 颗灯珠的整帧:3×24 bit + reset = 73 + symbol),再用 `rmt_tx_switch_gpio()` 依次把该通道切到 4 个 GPIO 上发送。 + 每条灯带约 140 µs,10 ms 周期内开销可忽略。 + +### 3. 按键 + +按 OneButton 2.6.1 的 `_fsm()` / `debounce()` 逐行移植,默认值与调用方式 +(`attachClick` / `attachLongPressStart` / `setClickMs(50)` / `setPressMs(500)` / +每 10 ms `tick()`)完全一致。未使用的双击、多击、idle 等功能没有移植。 + +### 4. 控制台 + +控制台走 USB Serial/JTAG,等价于原来的 `ARDUINO_USB_CDC_ON_BOOT=1`, +同时让 UART0(GPIO20/21)腾出来给 DBUS 数据,与原 `Serial0` 一致。 +USB CDC 的 10 字节 DBUS 帧由独立的 `cdc_task` 接收。 + +### 5. NVS + +命名空间仍是 `armor`,键名 `color`、`peer_mac` 保持原样。 + +### 与原始固件的差异(有意为之) + +- `Air::onRecv()` 现在直接用 `esp_now_recv_info_t::rx_ctrl->rssi` 记录信号强度。 + 原代码依赖混杂模式回调“恰好先于” ESP-NOW 回调执行,存在竞态;新写法在配对 + 比较时能拿到当前包的准确 RSSI。 +- `my_packet.dbus` 的写入/清零加了临界区保护(原代码在 CDC 回调与业务线程之间 + 无保护)。 +- ESP-NOW 负载长度由 `alignas(1)` 改为 `__attribute__((packed))`, + 两者都让 `sizeof(packet_t) == 19`,但后者是可移植写法(C++ 标准不允许 + `alignas` 比成员自然对齐更宽松)。 +- `hit_cnt++` 改为 `hit_cnt = hit_cnt + 1`:C++20 起对 `volatile` 使用 `++` 已废弃, + 而 IDF 默认开启 `-Werror`。 +- 分区表使用 2 MB 的 factory 分区(默认的 1 MB 对 WiFi + ESP-NOW 固件偏紧; + 当前固件 0xcfbc0 ≈ 852 KB)。 + +其余业务逻辑(颜色切换、配对流程、串口协议、超时清零)与原实现逐条对应。 + +## ⚠️ 非 ASCII 路径问题(本工程必须注意) + +本工程所在路径包含中文(`D:\宣筹\校内赛\...`)。**ESP-IDF v6.1 在 Windows 上无法 +在非 ASCII 路径下编译**,原因是 `components/esp_libc/project_include.cmake` 会把 +编译器的 `-specs=/specs/picolibc.specs` 参数写进 +`build/toolchain/cflags`,而读写该文件用的 `tools/cmake/toolchain_flags.cmake` +走的是 CMake `file(STRINGS)`;未指定 `ENCODING` 时它按 ASCII 解析,会**静默丢弃 +所有非 ASCII 字节**,于是 `-specs=` 指向的文件名被破坏,每个编译单元都报: + +``` +riscv32-esp-elf-gcc.exe: fatal error: cannot read spec file + 'D:/\x0a/\x0a/2026/SmallMaster-master/code/armor/build/specs/picolibc.specs': Invalid argument +``` + +实测字节级证据(`file(STRINGS)` 往返): + +| | 字节 | +| --- | --- | +| 输入 `D:/宣筹/校内赛/...` | `... 2F, E5,AE,A3, E7,AD,B9, 2F, E6,A0,A1, E5,86,85, E8,B5,9B, 2F ...` | +| `file(READ)` 直读(正常) | 同上 | +| `file(STRINGS)` 往返后 | `... 2F, 0D,0A, 2F, 0D,0A, 2F ...` | + +**解决办法**:把 build 目录放到纯 ASCII 路径下(源码可以留在中文路径), +即 `idf.py -B `。本仓库的 `idf.ps1` 已自动处理: + +``` +检测到工程路径含非 ASCII 字符时,自动改用 <盘符>:\esp-idf-build\armor 作为 build 目录 +``` + +可用环境变量 `IDF_BUILD_DIR` 覆盖该位置;若想改回在工程内构建, +把工程移到纯 ASCII 路径即可(此时脚本不会加 `-B`)。 + +> 注意:用目录联接(junction)指向中文路径**无效**——PowerShell 的 +> `Set-Location` 会把联接解析回真实路径,CMake 仍然看到中文路径。 + +## 构建与烧录 + +```powershell +# 构建(自动处理非 ASCII 路径) +.\idf.ps1 build + +# 烧录 +.\idf.ps1 -p COM9 flash + +# 烧录并监视(Ctrl+] 退出) +.\idf.ps1 -p COM9 flash monitor + +# 擦除整片 flash +.\idf.ps1 erase-flash +``` + +`idf.ps1` 会在需要时通过 `eim_idf.json` 自动找到并激活本机的 ESP-IDF。 +如果你已经激活过 ESP-IDF(`idf.py` 可用),也可以直接: + +```powershell +idf.py -B build +idf.py -B -p COM9 flash monitor +``` + +串口参数与原先一致:UART0 = 115200 8N1(DBUS),USB CDC 输出 +`color,hit_cnt,peer_rssi,my_rssi,latency_ms` 的 CSV 行。 + +## 实测验证结果(COM9,ESP32-C3 rev v0.4) + +烧录后 boot log: + +``` +I (24) boot: ESP-IDF v6.1 2nd stage bootloader +I (25) boot.esp32c3: SPI Speed : 80MHz / Mode : DIO / Flash Size : 4MB +I (26) boot: 2 factory factory app 00 00 00010000 00200000 +I (175) boot: Loaded app from partition at offset 0x10000 +I (185) app_init: Project name: armor +I (185) app_init: ESP-IDF: v6.1 +I (190) main_task: Calling app_main() +I (245) wifi:mode : sta (98:a3:16:4c:67:c8) +I (247) wifi:(2.4G)set country: cc=JP schan=1 nchan=14 policy=1 +I (250) wifi:ic_enable_sniffer <- 混杂模式 +I (250) ESPNOW: espnow [version: 2.0] init +I (251) armor: armor node started +I (252) main_task: Returned from app_main() +16711680,0,0,0,65 +16711680,0,0,0,75 +... +``` + +- 无重启、无 panic、无 abort;连续 6 秒稳定输出 555 行 CSV(约 11 ms 一条,符合 10 ms 循环)。 +- `color = 16711680 = 0xFF0000`,即 NVS 为空时的默认红色,与 `Hit::RED` 一致。 +- WiFi 国家码 `JP`、信道 1–14、混杂模式、ESP-NOW v2.0 全部按原逻辑生效。 + +## 硬件引脚(未改动) + +| 功能 | GPIO | +| --- | --- | +| 按键(BOOT,低有效) | 9 | +| WS2812 前 / 后 / 左 / 右 | 10 / 5 / 6 / 7 | +| 压电片 ADC 前 / 后 / 左 / 右(ADC1_CH0/4/3/1) | 0 / 4 / 3 / 1 | +| DBUS UART0 TX / RX | 21 / 20 | diff --git a/reference/armor/idf.ps1 b/reference/armor/idf.ps1 new file mode 100644 index 0000000..e75e7b3 --- /dev/null +++ b/reference/armor/idf.ps1 @@ -0,0 +1,83 @@ +<# +.SYNOPSIS + Run idf.py for this project with a local ESP-IDF environment activated. + +.DESCRIPTION + Activates ESP-IDF (located through the Espressif Installation Manager's + eim_idf.json, unless IDF_PATH is already set) and forwards every remaining + argument to idf.py. + + Non-ASCII project path workaround + --------------------------------- + ESP-IDF v6.1 rewrites the compiler's "-specs=..." flag from + components/esp_libc/project_include.cmake through CMake's file(STRINGS), + which silently drops every non-ASCII byte. When this project lives under a + path containing non-ASCII characters (e.g. "D:\宣筹\..."), the rewritten + flag points at a nonexistent file and every compile fails with: + + riscv32-esp-elf-gcc.exe: fatal error: cannot read spec file ... + + This script detects that case and builds out-of-tree into an ASCII build + directory instead, which keeps the flag (and every generated path) ASCII. + Override the location with the IDF_BUILD_DIR environment variable. + +.EXAMPLE + .\idf.ps1 build + .\idf.ps1 menuconfig + .\idf.ps1 -p COM9 flash monitor + .\idf.ps1 erase-flash +#> +# NOTE: deliberately no param() block. An advanced/declared parameter list makes +# PowerShell try to bind idf.py's own short options to its common parameters +# ("-p" is ambiguous with -ProgressAction / -PipelineVariable). A simple script +# passes every unbound token through $args verbatim. +$IdfArgs = @($args) + +$ErrorActionPreference = 'Stop' + +function Find-IdfActivation { + $candidates = @( + (Join-Path $env:USERPROFILE '.espressif\eim_idf.json'), + 'C:\Espressif\tools\eim_idf.json', + (Join-Path $env:LOCALAPPDATA 'Espressif\eim_idf.json') + ) + foreach ($path in $candidates) { + if (-not (Test-Path $path)) { continue } + $cfg = Get-Content $path -Raw | ConvertFrom-Json + $installed = $cfg.idfInstalled | Where-Object { $_.id -eq $cfg.idfSelectedId } | Select-Object -First 1 + if (-not $installed) { $installed = $cfg.idfInstalled | Select-Object -First 1 } + if ($installed -and (Test-Path $installed.activationScript)) { + return $installed.activationScript + } + } + return $null +} + +if (-not $env:IDF_PATH) { + $activation = Find-IdfActivation + if (-not $activation) { + throw 'ESP-IDF not found. Activate your ESP-IDF environment first (export.ps1 / eim).' + } + . $activation *> $null +} + +if (-not $IdfArgs -or $IdfArgs.Count -eq 0) { $IdfArgs = @('build') } + +# --- non-ASCII project path workaround ------------------------------------- +$projectDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } +$userPickedBuildDir = $IdfArgs -contains '-B' -or $IdfArgs -contains '--build-dir' + +if ($projectDir -notmatch '^[\x20-\x7E]+$' -and -not $userPickedBuildDir) { + if ($env:IDF_BUILD_DIR) { + $buildDir = $env:IDF_BUILD_DIR + } else { + $qualifier = Split-Path -Qualifier $projectDir # e.g. "D:" + $buildDir = Join-Path $qualifier 'esp-idf-build\armor' + } + Write-Host "Non-ASCII project path detected; building out-of-tree in '$buildDir'." -ForegroundColor Yellow + Write-Host 'Set IDF_BUILD_DIR to use a different build directory.' -ForegroundColor Yellow + $IdfArgs = @('-B', $buildDir) + $IdfArgs +} + +& idf.py @IdfArgs +exit $LASTEXITCODE diff --git a/reference/armor/main/CMakeLists.txt b/reference/armor/main/CMakeLists.txt new file mode 100644 index 0000000..42a38a5 --- /dev/null +++ b/reference/armor/main/CMakeLists.txt @@ -0,0 +1,21 @@ +idf_component_register( + SRCS + "armor_main.cpp" + "air.cpp" + "hit.cpp" + "button.cpp" + "storage.cpp" + "led_strip_encoder.c" + INCLUDE_DIRS "." + REQUIRES + esp_wifi + esp_netif + esp_event + esp_timer + nvs_flash + esp_adc + esp_driver_gpio + esp_driver_rmt + esp_driver_uart + esp_driver_usb_serial_jtag +) diff --git a/reference/armor/main/air.cpp b/reference/armor/main/air.cpp new file mode 100644 index 0000000..543a863 --- /dev/null +++ b/reference/armor/main/air.cpp @@ -0,0 +1,222 @@ +#include "air.hpp" + +#include + +#include "armor_time.hpp" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "storage.hpp" + +namespace { + +constexpr const char *TAG = "air"; + +portMUX_TYPE s_dbus_lock = portMUX_INITIALIZER_UNLOCKED; + +// ESP-NOW payloads are 802.11 frames; the promiscuous callback is used to keep +// track of the peer's signal strength. +typedef struct { + unsigned frame_ctrl : 16; + unsigned duration_id : 16; + uint8_t addr1[6]; /* receiver address */ + uint8_t addr2[6]; /* sender address */ + uint8_t addr3[6]; /* filtering address */ + unsigned sequence_ctrl : 16; + uint8_t addr4[6]; /* optional */ +} wifi_ieee80211_mac_hdr_t; + +typedef struct { + wifi_ieee80211_mac_hdr_t hdr; + uint8_t payload[0]; /* network data ended with 4 bytes csum (CRC32) */ +} wifi_ieee80211_packet_t; + +} // namespace + +// public +Air::packet_t Air::my_packet, Air::peer_packet; +volatile uint32_t Air::last_air_ms = 0; +volatile bool Air::is_pairing = false; +volatile bool Air::pairing_found = false; +// private +const uint8_t Air::empty_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const uint8_t Air::pairing_data[4] = {0x11, 0x45, 0x14, 0x00}; +esp_now_peer_info_t Air::broadcast_peer; +esp_now_peer_info_t Air::peer; +volatile int8_t Air::pairing_best_rssi = -128; + +void Air::begin() { + // ESP-NOW needs the WiFi stack up on the station interface. + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg)); + // The peer MAC lives in our own NVS namespace; don't let the WiFi driver + // write extra calibration data it does not need here. + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + + // 使用 WiFi 14 信道 + wifi_country_t country = {}; + memcpy(country.cc, "JP", sizeof(country.cc)); + country.schan = 1; + country.nchan = 14; + country.policy = WIFI_COUNTRY_POLICY_MANUAL; + ESP_ERROR_CHECK(esp_wifi_set_country(&country)); + ESP_ERROR_CHECK(esp_wifi_set_channel(14, WIFI_SECOND_CHAN_NONE)); + + // 20dBm 功率(84 = 21dBm;C3 上限为 20dBm,失败时退回 80) + esp_err_t power_err = esp_wifi_set_max_tx_power(84); + if (power_err != ESP_OK) { + ESP_LOGW(TAG, "set_max_tx_power(84) failed: %s, retrying with 80", + esp_err_to_name(power_err)); + power_err = esp_wifi_set_max_tx_power(80); + if (power_err != ESP_OK) { + ESP_LOGW(TAG, "set_max_tx_power(80) failed: %s", esp_err_to_name(power_err)); + } + } + + // 启用混杂模式,以获取发送方 rssi + wifi_promiscuous_filter_t filter = {}; + filter.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT; + ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter)); + ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(promiscuous_rx_cb)); + ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); + + // 初始化 ESP-NOW + ESP_ERROR_CHECK(esp_now_init()); + ESP_ERROR_CHECK(esp_now_register_recv_cb(onRecv)); + + // 添加广播 peer + memset(&broadcast_peer, 0, sizeof(broadcast_peer)); + memset(broadcast_peer.peer_addr, 0xFF, sizeof(broadcast_peer.peer_addr)); + broadcast_peer.channel = 0; + broadcast_peer.ifidx = WIFI_IF_STA; + broadcast_peer.encrypt = false; + ESP_ERROR_CHECK(esp_now_add_peer(&broadcast_peer)); + + memset(&peer, 0, sizeof(peer)); + peer.channel = 0; + peer.ifidx = WIFI_IF_STA; + peer.encrypt = false; + + if (Storage::hasKey("peer_mac")) { + Storage::getBytes("peer_mac", peer.peer_addr, sizeof(peer.peer_addr)); + if (memcmp(peer.peer_addr, empty_mac, sizeof(empty_mac)) == 0) { + ESP_LOGI(TAG, "stored peer MAC is empty, staying unpaired"); + } else { + esp_err_t err = esp_now_add_peer(&peer); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_now_add_peer(stored) failed: %s", esp_err_to_name(err)); + } + } + } else { + memset(peer.peer_addr, 0x00, sizeof(peer.peer_addr)); + } +} + +void Air::entryPairing() { + // 清除之前的配对 + esp_now_del_peer(peer.peer_addr); + + is_pairing = true; + pairing_found = false; + + // 用于只配对最强的信号 + my_packet.rssi = -128; + pairing_best_rssi = -128; +} + +void Air::savePairing() { + if (pairing_found) { + // 添加 peer + esp_err_t err = esp_now_add_peer(&peer); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_now_add_peer(pairing) failed: %s", esp_err_to_name(err)); + } + } else { + // 配对失败,清空设备 mac + memset(peer.peer_addr, 0x00, sizeof(peer.peer_addr)); + } + + // 写入 flash + Storage::putBytes("peer_mac", peer.peer_addr, sizeof(peer.peer_addr)); + + is_pairing = false; +} + +void Air::onLoop() { + if (is_pairing) { + // 发送配对请求 + esp_now_send(broadcast_peer.peer_addr, pairing_data, sizeof(pairing_data)); + } else if (memcmp(peer.peer_addr, empty_mac, sizeof(empty_mac)) != 0) { + // 已有配对设备 + esp_now_send(peer.peer_addr, reinterpret_cast(&my_packet), sizeof(packet_t)); + } +} + +void Air::dbusSet(const uint8_t *src, size_t len) { + portENTER_CRITICAL(&s_dbus_lock); + memcpy(my_packet.dbus, src, len); + portEXIT_CRITICAL(&s_dbus_lock); +} + +void Air::dbusClear() { + portENTER_CRITICAL(&s_dbus_lock); + memset(my_packet.dbus, 0, sizeof(my_packet.dbus)); + portEXIT_CRITICAL(&s_dbus_lock); +} + +void Air::onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int data_len) { + if (info == nullptr || info->src_addr == nullptr || data == nullptr) { + return; + } + + // The original relied on the promiscuous callback to have recorded the RSSI + // of this frame beforehand; rx_ctrl carries the same value synchronously + // with the packet we are handling. + if (info->rx_ctrl != nullptr) { + my_packet.rssi = info->rx_ctrl->rssi; + } + + if (is_pairing) { + // 配对 magic 正确 + if (data_len == static_cast(sizeof(pairing_data)) && + memcmp(pairing_data, data, sizeof(pairing_data)) == 0) { + // 只配对最强的信号 + if (my_packet.rssi > pairing_best_rssi) { + pairing_best_rssi = my_packet.rssi; + memcpy(peer.peer_addr, info->src_addr, sizeof(peer.peer_addr)); + pairing_found = true; + } + } + } else if (memcmp(peer.peer_addr, info->src_addr, sizeof(peer.peer_addr)) == 0) { + // 是从配对设备发过来的,且数据长度正确 + if (data_len == static_cast(sizeof(packet_t))) { + memcpy(&peer_packet, data, sizeof(packet_t)); + last_air_ms = millis(); + } + } +} + +void Air::promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) { + return; + } + + const auto *ppkt = static_cast(buf); + const auto *ipkt = reinterpret_cast(ppkt->payload); + const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr; + + if (is_pairing) { + // 配对模式 + my_packet.rssi = ppkt->rx_ctrl.rssi; + } else if (memcmp(hdr->addr2, peer.peer_addr, sizeof(peer.peer_addr)) == 0) { + my_packet.rssi = ppkt->rx_ctrl.rssi; + } +} diff --git a/reference/armor/main/air.hpp b/reference/armor/main/air.hpp new file mode 100644 index 0000000..6476bf5 --- /dev/null +++ b/reference/armor/main/air.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include "esp_now.h" +#include "esp_wifi.h" + +/** + * @brief ESP-NOW peer link (ported from the Arduino/ESP-NOW version). + * + * Runs ESP-NOW on the station interface of channel 14 with a broadcast peer + * used for pairing requests, plus a stored unicast peer used for the normal + * game traffic. + */ +class Air { + public: + /** + * Over-the-air packet. The original declared it `alignas(1)`; `packed` + * expresses the same intent (sizeof == 19, no trailing padding) in a way + * that is portable C++. + */ + struct __attribute__((packed)) packet_t { + uint32_t color = 0; + uint32_t hit_cnt = 0; + uint8_t dbus[10] = {0}; + int8_t rssi = 0; + }; + + static packet_t my_packet, peer_packet; + static volatile uint32_t last_air_ms; + + static volatile bool is_pairing; + static volatile bool pairing_found; + + static void begin(); + static void entryPairing(); + static void savePairing(); + static void onLoop(); + + // The USB-CDC task writes my_packet.dbus while the application task reads + // it, so the copy/clear are serialised here instead of being done inline. + static void dbusSet(const uint8_t *src, size_t len); + static void dbusClear(); + + private: + static const uint8_t empty_mac[6]; + static const uint8_t pairing_data[4]; + + static esp_now_peer_info_t broadcast_peer; + static esp_now_peer_info_t peer; + + static volatile int8_t pairing_best_rssi; + + static void onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int data_len); + static void promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type); +}; diff --git a/reference/armor/main/armor_main.cpp b/reference/armor/main/armor_main.cpp new file mode 100644 index 0000000..e1e5b1c --- /dev/null +++ b/reference/armor/main/armor_main.cpp @@ -0,0 +1,215 @@ +/* + * ESP-IDF entry point for the "armor" hit-detection node (ESP32-C3). + * + * This is the ESP-IDF counterpart of the original Arduino `main.cpp`: + * setup() -> app_main() + * loop() -> hit_task() (only ran Hit::onLoop()) + * loop2() -> loop2_task() (button, ESP-NOW and serial bridging) + * Serial -> USB Serial/JTAG console + driver (CDC protocol) + * Serial0 -> UART0 on GPIO20/21 (DBUS bridge) + */ + +#include +#include + +#include "driver/uart.h" +#include "driver/usb_serial_jtag.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nvs_flash.h" + +#include "air.hpp" +#include "armor_time.hpp" +#include "button.hpp" +#include "hit.hpp" +#include "storage.hpp" + +namespace { + +constexpr const char *TAG = "armor"; + +constexpr uint32_t TIMEOUT_MS = 100; + +// GPIO9 is the on-board BOOT button of the ESP32-C3 DevKitM-1, active low. +// Original: `OneButton button(9, true);` +Button button(9, true); + +volatile bool g_click = false; +volatile bool g_long_click = false; +volatile uint32_t g_last_cdc_receive_ms = 0; + +void handleClick() { + g_click = true; +} + +void handleLongPressStart() { + g_long_click = true; +} + +// --------------------------------------------------------------- UART0 ----- +// Original: `Serial0.begin(115200, SERIAL_8N1, 20, 21);` (rx=20, tx=21) +constexpr uart_port_t DBUS_UART = UART_NUM_0; +constexpr int DBUS_UART_RX = 20; +constexpr int DBUS_UART_TX = 21; + +void serial0_begin() { + uart_config_t cfg = {}; + cfg.baud_rate = 115200; + cfg.data_bits = UART_DATA_8_BITS; + cfg.parity = UART_PARITY_DISABLE; + cfg.stop_bits = UART_STOP_BITS_1; + cfg.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + cfg.source_clk = UART_SCLK_DEFAULT; + + ESP_ERROR_CHECK(uart_driver_install(DBUS_UART, 256, 256, 0, nullptr, 0)); + ESP_ERROR_CHECK(uart_param_config(DBUS_UART, &cfg)); + ESP_ERROR_CHECK(uart_set_pin(DBUS_UART, DBUS_UART_TX, DBUS_UART_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); +} + +// ----------------------------------------------------------- USB CDC ------- +// Replaces Arduino's `serialEvent()`: 10-byte DBUS frames arrive over the USB +// CDC port, and the original truncated/committed them in 10-byte units. +void cdc_task(void *) { + uint8_t chunk[32]; + uint8_t frame[10]; + size_t frame_len = 0; + + while (true) { + const int n = usb_serial_jtag_read_bytes(chunk, sizeof(chunk), pdMS_TO_TICKS(20)); + for (int i = 0; i < n; i++) { + frame[frame_len++] = chunk[i]; + if (frame_len == sizeof(frame)) { + Air::dbusSet(frame, sizeof(frame)); + g_last_cdc_receive_ms = millis(); + frame_len = 0; + } + } + } +} + +// ------------------------------------------------------------ business ----- +void handle_air() { + static Air::packet_t peer_packet_last; + + // 处理收到的数据包 + // 颜色变化 + if (Air::peer_packet.color != peer_packet_last.color && Air::peer_packet.color != 0) { + Hit::color = Air::peer_packet.color; + Storage::putUInt("color", Hit::color); + } + // 击打次数变化 + if (Air::peer_packet.hit_cnt != peer_packet_last.hit_cnt) { + Hit::last_hit_ms = millis(); + } + peer_packet_last = Air::peer_packet; + + // 发送自己的数据包 + Air::my_packet.color = Hit::color; + Air::my_packet.hit_cnt = Hit::hit_cnt; + if (millis() - g_last_cdc_receive_ms > TIMEOUT_MS) { // 超时数据清零 + Air::dbusClear(); + } +} + +void handle_serial() { + // 串口发送 DBUS 数据 + if (millis() - Air::last_air_ms > TIMEOUT_MS) { // 超时数据清零 + memset(Air::peer_packet.dbus, 0, sizeof(Air::peer_packet.dbus)); + } + uart_write_bytes(DBUS_UART, Air::peer_packet.dbus, sizeof(Air::peer_packet.dbus)); + uart_wait_tx_done(DBUS_UART, pdMS_TO_TICKS(20)); // Serial0.flush() + + // CDC发送:颜色、击打次数、信号强度(tx, rx)、距离最后一次收到消息的毫秒数 + const uint32_t latency_ms = millis() - Air::last_air_ms; + printf("%d,%d,%d,%d,%d\n", + static_cast(Air::my_packet.color), + static_cast(Air::peer_packet.hit_cnt), + static_cast(Air::peer_packet.rssi), + static_cast(Air::my_packet.rssi), + static_cast(latency_ms)); +} + +// ------------------------------------------------------------- tasks ------- +// Equivalent of the original Arduino `loop2()`. +void loop2_task(void *) { + while (true) { + button.tick(); + Air::onLoop(); + + if (!Air::is_pairing) { // 不在配对中 + if (g_long_click) { // 长按配对 + Air::entryPairing(); + Hit::detect_hit = false; + } else if (g_click) { // 短按切换颜色 + Hit::color = (Hit::color == Hit::RED) ? Hit::BLUE : Hit::RED; + Storage::putUInt("color", Hit::color); + } else { // 正常运行业务逻辑 + handle_air(); + handle_serial(); + } + } else { // 在配对中 + if (g_click) { // 短按保存配对 + Air::savePairing(); + Hit::color = Hit::RED; + Storage::putUInt("color", Hit::color); + Hit::detect_hit = true; + } else { + Hit::color = Air::pairing_found ? Hit::GREEN : Hit::YELLOW; + } + } + + g_click = false; + g_long_click = false; + + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +// Equivalent of the original Arduino `loop()`. +void hit_task(void *) { + while (true) { + Hit::onLoop(); + } +} + +} // namespace + +extern "C" void app_main(void) { + // 初始化 NVS(原 Preferences 底层就是 NVS) + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + ESP_ERROR_CHECK(Storage::begin("armor")); + + // With CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG the console initialisation has + // already installed the USB Serial/JTAG driver, so only install it when + // that did not happen; reading raw CDC bytes then works either way. + if (!usb_serial_jtag_is_driver_installed()) { + usb_serial_jtag_driver_config_t usb_cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(); + usb_cfg.rx_buffer_size = 256; + if (usb_serial_jtag_driver_install(&usb_cfg) != ESP_OK) { + ESP_LOGW(TAG, "USB Serial/JTAG driver unavailable, CDC protocol disabled"); + } + } + + serial0_begin(); + + // 绑定回调函数 + button.attachClick(handleClick); + button.attachLongPressStart(handleLongPressStart); + button.setClickMs(50); // 短按时间(毫秒) + button.setPressMs(500); // 长按时间(毫秒) + + Hit::begin(); + Air::begin(); + + ESP_LOGI(TAG, "armor node started"); + + xTaskCreate(cdc_task, "cdc", 3072, nullptr, 5, nullptr); + xTaskCreate(loop2_task, "loop2", 8192, nullptr, 5, nullptr); + xTaskCreate(hit_task, "hit", 4096, nullptr, 5, nullptr); +} diff --git a/reference/armor/main/armor_time.hpp b/reference/armor/main/armor_time.hpp new file mode 100644 index 0000000..c661dcf --- /dev/null +++ b/reference/armor/main/armor_time.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "esp_timer.h" + +/** + * @brief Arduino-compatible millisecond counter. + * + * The original firmware used the Arduino `millis()` helper; ESP-IDF offers the + * same value through esp_timer. The result is truncated to 32 bits on purpose + * so that the "elapsed = now - then" idiom keeps working across the wrap. + */ +inline uint32_t millis() { + return static_cast(esp_timer_get_time() / 1000); +} diff --git a/reference/armor/main/button.cpp b/reference/armor/main/button.cpp new file mode 100644 index 0000000..068fdfa --- /dev/null +++ b/reference/armor/main/button.cpp @@ -0,0 +1,126 @@ +#include "button.hpp" + +#include + +#include "armor_time.hpp" +#include "driver/gpio.h" + +Button::Button(int gpio, bool active_low) : _pin(gpio), _active_low(active_low) { + gpio_config_t cfg = {}; + cfg.pin_bit_mask = 1ULL << gpio; + cfg.mode = GPIO_MODE_INPUT; + cfg.pull_up_en = active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + cfg.pull_down_en = active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE; + cfg.intr_type = GPIO_INTR_DISABLE; + gpio_config(&cfg); +} + +void Button::attachClick(callback_t cb) { + _clickFunc = cb; +} + +void Button::attachLongPressStart(callback_t cb) { + _longPressStartFunc = cb; +} + +void Button::setClickMs(uint32_t ms) { + _click_ms = ms; +} + +void Button::setPressMs(uint32_t ms) { + _press_ms = ms; +} + +// Port of OneButton::debounce(): a level only becomes the debounced level once +// it has been stable for _debounce_ms. +bool Button::debounce(bool value) { + now = millis(); + + if (_lastDebounceLevel == value) { + if (now - _lastDebounceTime >= static_cast(std::abs(_debounce_ms))) { + debouncedLevel = value; + } + } else { + _lastDebounceTime = now; + _lastDebounceLevel = value; + } + return debouncedLevel; +} + +void Button::tick() { + if (_pin < 0) { + return; + } + const int level = gpio_get_level(static_cast(_pin)); + const bool active_level = _active_low ? (level == 0) : (level != 0); + fsm(debounce(active_level)); +} + +void Button::reset() { + _state = OCS_INIT; + _nClicks = 0; + _startTime = millis(); +} + +// Port of OneButton::_fsm() (single click + long press start only). +void Button::fsm(bool active_level) { + const unsigned long waitTime = now - _startTime; + + switch (_state) { + case OCS_INIT: + // Waiting for the level to become active. + if (active_level) { + _state = OCS_DOWN; + _startTime = now; + _nClicks = 0; + } + break; + + case OCS_DOWN: + // Waiting for the level to become inactive again... or a long press. + if (!active_level) { + _state = OCS_UP; + _startTime = now; + } else if (waitTime > _press_ms) { + if (_longPressStartFunc) { + _longPressStartFunc(); + } + _state = OCS_PRESS; + } + break; + + case OCS_UP: + // Level is inactive: count as a short button down. + _nClicks++; + _state = OCS_COUNT; + break; + + case OCS_COUNT: + if (active_level) { + // Button is down again. + _state = OCS_DOWN; + _startTime = now; + } else if ((waitTime >= _click_ms) || (_nClicks == _maxClicks)) { + if (_nClicks == 1 && _clickFunc) { + _clickFunc(); + } + reset(); + } + break; + + case OCS_PRESS: + // Waiting for the pin to be released after a long press. + if (!active_level) { + _state = OCS_PRESSEND; + } + break; + + case OCS_PRESSEND: + reset(); + break; + + default: + _state = OCS_INIT; + break; + } +} diff --git a/reference/armor/main/button.hpp b/reference/armor/main/button.hpp new file mode 100644 index 0000000..2dc4808 --- /dev/null +++ b/reference/armor/main/button.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include + +/** + * @brief Button gesture detector, ported from OneButton 2.6.1. + * + * Only the parts the firmware actually uses are kept (single click and long + * press start); the state machine, the debounce rule and the default timings + * are a line-by-line port of `OneButton::_fsm()` from + * https://github.com/mathertel/OneButton (BSD-3-Clause) so gesture behaviour + * matches the original Arduino build exactly. + * + * The unused OneButton features (double click, multi click, press, long press + * stop/during, idle) were never attached in the original firmware and are + * therefore not reproduced here. + */ +class Button { + public: + using callback_t = void (*)(); + + /** + * @param gpio GPIO the button is attached to. + * @param active_low true when pressing connects the pin to GND + * (the original used `OneButton button(9, true)`). + */ + Button(int gpio, bool active_low); + + void attachClick(callback_t cb); + void attachLongPressStart(callback_t cb); + + /** Milliseconds before a click is considered stable (OneButton setClickMs). */ + void setClickMs(uint32_t ms); + + /** Milliseconds before the press counts as a long press (setPressMs). */ + void setPressMs(uint32_t ms); + + /** Poll the pin and advance the state machine. Call periodically (10 ms). */ + void tick(); + + private: + enum state_t { + OCS_INIT = 0, + OCS_DOWN = 1, + OCS_UP = 2, + OCS_COUNT = 3, + OCS_PRESS = 6, + OCS_PRESSEND = 7, + }; + + bool debounce(bool value); + void fsm(bool active_level); + void reset(); + + int _pin; + bool _active_low; + + state_t _state = OCS_INIT; + int _nClicks = 0; + + // Debounce state (OneButton: _debounce_ms defaults to 50 ms). + int _debounce_ms = 50; + bool debouncedLevel = false; + bool _lastDebounceLevel = false; + unsigned long _lastDebounceTime = 0; + unsigned long now = 0; + + unsigned long _startTime = 0; + + // `setClickMs(50)` / `setPressMs(500)` are applied by the application; + // these are the OneButton defaults. + unsigned int _click_ms = 400; + unsigned int _press_ms = 800; + int _maxClicks = 1; + + callback_t _clickFunc = nullptr; + callback_t _longPressStartFunc = nullptr; +}; diff --git a/reference/armor/main/hit.cpp b/reference/armor/main/hit.cpp new file mode 100644 index 0000000..aa9ab9f --- /dev/null +++ b/reference/armor/main/hit.cpp @@ -0,0 +1,192 @@ +#include "hit.hpp" + +#include + +#include "armor_time.hpp" +#include "driver/gpio.h" +#include "driver/rmt_tx.h" +#include "esp_adc/adc_continuous.h" +#include "esp_log.h" +#include "hal/adc_types.h" +#include "led_strip_encoder.h" +#include "soc/soc_caps.h" +#include "storage.hpp" + +namespace { + +constexpr const char *TAG = "hit"; + +// 这是总轮询采样率,每个通道的采样率只有 1/4 +constexpr uint32_t SAMPLE_FREQ_HZ = 40000; +// 10ms 的采样数据(字节数,每个采样点 4 字节) +constexpr uint32_t SAMPLE_LEN = static_cast(10e-3 * SAMPLE_FREQ_HZ * 4); + +// 压电片 ADC 输入:front, back, left, right -> GPIO0 / GPIO4 / GPIO3 / GPIO1 +constexpr int ADC_CHANNEL_COUNT = 4; +constexpr adc_channel_t ADC_CHANNELS[ADC_CHANNEL_COUNT] = {ADC_CHANNEL_0, ADC_CHANNEL_4, ADC_CHANNEL_3, + ADC_CHANNEL_1}; + +// WS2812 数据脚:front, back, left, right +constexpr int STRIP_COUNT = 4; +constexpr int STRIP_LEDS = 3; +constexpr gpio_num_t STRIP_GPIOS[STRIP_COUNT] = {GPIO_NUM_10, GPIO_NUM_5, GPIO_NUM_6, GPIO_NUM_7}; + +// WS2812 bit timing needs a fine resolution: 10MHz -> 1 tick = 0.1us +constexpr uint32_t RMT_RESOLUTION_HZ = 10000000; +// The ESP32-C3 RMT block owns 96 symbols of channel memory in total, shared by +// its two TX channels (each channel's memory is allocated in multiples of +// SOC_RMT_MEM_WORDS_PER_CHANNEL = 48 symbols). A 64-symbol block size would +// round up to 96 and therefore consume the whole block for a single channel, +// making the second `rmt_new_tx_channel()` fail with +// "rmt_tx_register_to_group: no free tx channels". +// +// All four strips always display the same colour, so a single channel that owns +// the full 96-symbol memory is enough: it holds a complete 3-LED frame +// (3 * 24 bits + reset = 73 symbols) in one shot and is parked on a different +// GPIO for each strip. +constexpr int RMT_CHANNEL_COUNT = 1; +constexpr uint32_t RMT_MEM_BLOCK_SYMBOLS = 96; + +adc_continuous_handle_t s_adc = nullptr; + +rmt_channel_handle_t s_rmt_channels[RMT_CHANNEL_COUNT] = {}; +rmt_encoder_handle_t s_rmt_encoders[RMT_CHANNEL_COUNT] = {}; + +// Raw ADC DMA frame (10 ms worth of samples). 4-byte aligned so the result +// records can be read through `adc_digi_output_data_t`. +alignas(4) uint8_t s_adc_buf[SAMPLE_LEN]; + +// GRB pixel buffer; all four strips always display the same colour, so a single +// buffer is reused for each of them. +uint8_t s_pixels[STRIP_LEDS * 3]; + +void continuous_adc_init() { + // ADC 配置 + adc_continuous_handle_cfg_t adc_dma_config = {}; + adc_dma_config.max_store_buf_size = SAMPLE_LEN * 2; // 两倍缓冲区 + adc_dma_config.conv_frame_size = SAMPLE_LEN; + ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_dma_config, &s_adc)); + + // 通道配置 + adc_digi_pattern_config_t adc_pattern[ADC_CHANNEL_COUNT] = {}; + for (int i = 0; i < ADC_CHANNEL_COUNT; i++) { + adc_pattern[i].atten = ADC_ATTEN_DB_0; + adc_pattern[i].channel = ADC_CHANNELS[i] & 0x7; + adc_pattern[i].unit = 0; + adc_pattern[i].bit_width = SOC_ADC_DIGI_MAX_BITWIDTH; + } + + adc_continuous_config_t dig_cfg = {}; + dig_cfg.pattern_num = ADC_CHANNEL_COUNT; + dig_cfg.adc_pattern = adc_pattern; + dig_cfg.sample_freq_hz = SAMPLE_FREQ_HZ; + dig_cfg.conv_mode = ADC_CONV_SINGLE_UNIT_1; + ESP_ERROR_CHECK(adc_continuous_config(s_adc, &dig_cfg)); +} + +void rmt_led_init() { + for (int i = 0; i < RMT_CHANNEL_COUNT; i++) { + rmt_tx_channel_config_t tx_chan_config = {}; + tx_chan_config.gpio_num = STRIP_GPIOS[i]; + tx_chan_config.clk_src = RMT_CLK_SRC_DEFAULT; + tx_chan_config.resolution_hz = RMT_RESOLUTION_HZ; + tx_chan_config.mem_block_symbols = RMT_MEM_BLOCK_SYMBOLS; + tx_chan_config.trans_queue_depth = 4; + ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &s_rmt_channels[i])); + + led_strip_encoder_config_t encoder_config = {}; + encoder_config.resolution = RMT_RESOLUTION_HZ; + ESP_ERROR_CHECK(rmt_new_led_strip_encoder(&encoder_config, &s_rmt_encoders[i])); + + ESP_ERROR_CHECK(rmt_enable(s_rmt_channels[i])); + } +} + +// Push the shared pixel buffer to one strip. A single RMT TX channel can only +// drive one pin at a time, and rmt_tx_switch_gpio() only works on a disabled +// channel, so the channel is briefly parked on the target GPIO before each frame. +void strip_show(int strip) { + const int index = strip % RMT_CHANNEL_COUNT; + rmt_channel_handle_t chan = s_rmt_channels[index]; + + esp_err_t err = rmt_disable(chan); + if (err == ESP_OK) { + err = rmt_tx_switch_gpio(chan, STRIP_GPIOS[strip], false); + } + if (err == ESP_OK) { + err = rmt_enable(chan); + } + + rmt_transmit_config_t tx_config = {}; + tx_config.loop_count = 0; // no transfer loop + if (err == ESP_OK) { + err = rmt_transmit(chan, s_rmt_encoders[index], s_pixels, sizeof(s_pixels), &tx_config); + } + if (err == ESP_OK) { + err = rmt_tx_wait_all_done(chan, 1000); + } + if (err != ESP_OK) { + ESP_LOGW(TAG, "strip %d refresh failed: %s", strip, esp_err_to_name(err)); + } +} + +void refresh_strips() { + const bool blink = Hit::detect_hit && (millis() - Hit::last_hit_ms < Hit::HIT_BLINK_MS); + if (blink) { + memset(s_pixels, 0, sizeof(s_pixels)); + } else { + // WS2812 wants GRB byte order, MSB first, matching Adafruit_NeoPixel's + // NEO_GRB setting. + const uint32_t rgb = Hit::color; + for (int i = 0; i < STRIP_LEDS; i++) { + s_pixels[i * 3 + 0] = static_cast((rgb >> 8) & 0xFF); // G + s_pixels[i * 3 + 1] = static_cast((rgb >> 16) & 0xFF); // R + s_pixels[i * 3 + 2] = static_cast((rgb >> 0) & 0xFF); // B + } + } + + for (int i = 0; i < STRIP_COUNT; i++) { + strip_show(i); + } +} + +} // namespace + +// public +volatile uint32_t Hit::color = 0; +volatile bool Hit::detect_hit = true; +volatile uint32_t Hit::hit_cnt = 0; +volatile uint32_t Hit::last_hit_ms = 0; + +void Hit::begin() { + // 从 flash 读取颜色 + color = Storage::getUInt("color", RED); + + rmt_led_init(); + continuous_adc_init(); + ESP_ERROR_CHECK(adc_continuous_start(s_adc)); +} + +void Hit::onLoop() { + // 击打检测 + uint32_t out_length = 0; + esp_err_t err = adc_continuous_read(s_adc, s_adc_buf, SAMPLE_LEN, &out_length, 1000); + if (err == ESP_OK) { + for (uint32_t i = 0; i + SOC_ADC_DIGI_RESULT_BYTES <= out_length; i += SOC_ADC_DIGI_RESULT_BYTES) { + const auto *p = reinterpret_cast(&s_adc_buf[i]); + const uint32_t data = p->type2.data; + if (data > HIT_THRESHOLD) { // 检测到击打 + if (detect_hit && (millis() - last_hit_ms) > HIT_BLINK_MS) { // 不在上一次击打冷却期内,新的击打 + // Note: '++' on a volatile is deprecated since C++20 (and IDF + // builds with -Werror), so use an explicit read-modify-write. + hit_cnt = hit_cnt + 1; + last_hit_ms = millis(); + } + } + } + } else if (err != ESP_ERR_TIMEOUT) { + ESP_LOGW(TAG, "adc_continuous_read failed: %s", esp_err_to_name(err)); + } + + refresh_strips(); +} diff --git a/reference/armor/main/hit.hpp b/reference/armor/main/hit.hpp new file mode 100644 index 0000000..e3f40de --- /dev/null +++ b/reference/armor/main/hit.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +/** + * @brief Piezo hit detection + WS2812 status LEDs. + * + * Ported from the Arduino version: + * * the legacy ADC DMA driver (`driver/adc.h`, removed in ESP-IDF v6) is + * replaced by the `adc_continuous` driver with the same 40 kHz aggregate + * sampling rate, 0 dB attenuation and 12-bit results; + * * Adafruit_NeoPixel (bit-banged) is replaced by an RMT WS2812 encoder. + */ +class Hit { + public: + // Same values as Adafruit_NeoPixel::Color(r, g, b) -> 0x00RRGGBB. + static constexpr uint32_t RED = 0xFF0000; + static constexpr uint32_t BLUE = 0x0000FF; + static constexpr uint32_t GREEN = 0x00FF00; + static constexpr uint32_t YELLOW = 0xFFFF00; + + /** Blink duration after a hit, and the cooldown that suppresses repeats. */ + static constexpr uint32_t HIT_BLINK_MS = 50; + /** Raw 12-bit ADC level above which a sample counts as a hit. */ + static constexpr uint32_t HIT_THRESHOLD = 2000; + + static volatile uint32_t color; + static volatile bool detect_hit; + static volatile uint32_t hit_cnt; + static volatile uint32_t last_hit_ms; + + static void begin(); + static void onLoop(); +}; diff --git a/reference/armor/main/led_strip_encoder.c b/reference/armor/main/led_strip_encoder.c new file mode 100644 index 0000000..243c427 --- /dev/null +++ b/reference/armor/main/led_strip_encoder.c @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_check.h" +#include "led_strip_encoder.h" + +static const char *TAG = "led_encoder"; + +typedef struct { + rmt_encoder_t base; + rmt_encoder_t *bytes_encoder; + rmt_encoder_t *copy_encoder; + int state; + rmt_symbol_word_t reset_code; +} rmt_led_strip_encoder_t; + +RMT_ENCODER_FUNC_ATTR +static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state) +{ + rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); + rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder; + rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder; + rmt_encode_state_t session_state = RMT_ENCODING_RESET; + rmt_encode_state_t state = RMT_ENCODING_RESET; + size_t encoded_symbols = 0; + switch (led_encoder->state) { + case 0: // send RGB data + encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state); + if (session_state & RMT_ENCODING_COMPLETE) { + led_encoder->state = 1; // switch to next state when current encoding session finished + } + if (session_state & RMT_ENCODING_MEM_FULL) { + state |= RMT_ENCODING_MEM_FULL; + goto out; // yield if there's no free space for encoding artifacts + } + // fall-through + case 1: // send reset code + encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code, + sizeof(led_encoder->reset_code), &session_state); + if (session_state & RMT_ENCODING_COMPLETE) { + led_encoder->state = RMT_ENCODING_RESET; // back to the initial encoding session + state |= RMT_ENCODING_COMPLETE; + } + if (session_state & RMT_ENCODING_MEM_FULL) { + state |= RMT_ENCODING_MEM_FULL; + goto out; // yield if there's no free space for encoding artifacts + } + } +out: + *ret_state = state; + return encoded_symbols; +} + +static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder) +{ + rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); + rmt_del_encoder(led_encoder->bytes_encoder); + rmt_del_encoder(led_encoder->copy_encoder); + free(led_encoder); + return ESP_OK; +} + +RMT_ENCODER_FUNC_ATTR +static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder) +{ + rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); + rmt_encoder_reset(led_encoder->bytes_encoder); + rmt_encoder_reset(led_encoder->copy_encoder); + led_encoder->state = RMT_ENCODING_RESET; + return ESP_OK; +} + +esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder) +{ + esp_err_t ret = ESP_OK; + rmt_led_strip_encoder_t *led_encoder = NULL; + ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + led_encoder = rmt_alloc_encoder_mem(sizeof(rmt_led_strip_encoder_t)); + ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder"); + led_encoder->base.encode = rmt_encode_led_strip; + led_encoder->base.del = rmt_del_led_strip_encoder; + led_encoder->base.reset = rmt_led_strip_encoder_reset; + // different led strip might have its own timing requirements, following parameter is for WS2812 + rmt_bytes_encoder_config_t bytes_encoder_config = { + .bit0 = { + .level0 = 1, + .duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us + .level1 = 0, + .duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us + }, + .bit1 = { + .level0 = 1, + .duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us + .level1 = 0, + .duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us + }, + .flags.msb_first = 1 // WS2812 transfer bit order: G7...G0R7...R0B7...B0 + }; + ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed"); + rmt_copy_encoder_config_t copy_encoder_config = {}; + ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(©_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed"); + + uint32_t reset_ticks = config->resolution / 1000000 * 50 / 2; // reset code duration defaults to 50us + led_encoder->reset_code = (rmt_symbol_word_t) { + .level0 = 0, + .duration0 = reset_ticks, + .level1 = 0, + .duration1 = reset_ticks, + }; + *ret_encoder = &led_encoder->base; + return ESP_OK; +err: + if (led_encoder) { + if (led_encoder->bytes_encoder) { + rmt_del_encoder(led_encoder->bytes_encoder); + } + if (led_encoder->copy_encoder) { + rmt_del_encoder(led_encoder->copy_encoder); + } + free(led_encoder); + } + return ret; +} diff --git a/reference/armor/main/led_strip_encoder.h b/reference/armor/main/led_strip_encoder.h new file mode 100644 index 0000000..db5ef07 --- /dev/null +++ b/reference/armor/main/led_strip_encoder.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "driver/rmt_encoder.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Type of led strip encoder configuration + */ +typedef struct { + uint32_t resolution; /*!< Encoder resolution, in Hz */ +} led_strip_encoder_config_t; + +/** + * @brief Create RMT encoder for encoding LED strip pixels into RMT symbols + * + * @param[in] config Encoder configuration + * @param[out] ret_encoder Returned encoder handle + * @return + * - ESP_ERR_INVALID_ARG for any invalid arguments + * - ESP_ERR_NO_MEM out of memory when creating led strip encoder + * - ESP_OK if creating encoder successfully + */ +esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder); + +#ifdef __cplusplus +} +#endif diff --git a/reference/armor/main/storage.cpp b/reference/armor/main/storage.cpp new file mode 100644 index 0000000..eb3d326 --- /dev/null +++ b/reference/armor/main/storage.cpp @@ -0,0 +1,92 @@ +#include "storage.hpp" + +#include + +#include "esp_log.h" +#include "nvs.h" + +namespace { + +constexpr const char *TAG = "storage"; + +nvs_handle_t s_handle = 0; +bool s_open = false; + +} // namespace + +namespace Storage { + +esp_err_t begin(const char *name_space) { + esp_err_t err = nvs_open(name_space, NVS_READWRITE, &s_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open(%s) failed: %s", name_space, esp_err_to_name(err)); + return err; + } + s_open = true; + return ESP_OK; +} + +bool hasKey(const char *key) { + if (!s_open) { + return false; + } + // nvs_get_u32() requires a real output pointer, so probe with a scratch value. + uint32_t scratch = 0; + if (nvs_get_u32(s_handle, key, &scratch) == ESP_OK) { + return true; + } + size_t len = 0; + return nvs_get_blob(s_handle, key, nullptr, &len) == ESP_OK; +} + +uint32_t getUInt(const char *key, uint32_t default_value) { + if (!s_open) { + return default_value; + } + uint32_t value = default_value; + if (nvs_get_u32(s_handle, key, &value) != ESP_OK) { + return default_value; + } + return value; +} + +size_t getBytes(const char *key, void *out, size_t max_len) { + if (!s_open || out == nullptr) { + return 0; + } + size_t len = max_len; + if (nvs_get_blob(s_handle, key, out, &len) != ESP_OK) { + return 0; + } + return len; +} + +esp_err_t putUInt(const char *key, uint32_t value) { + if (!s_open) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t err = nvs_set_u32(s_handle, key, value); + if (err == ESP_OK) { + err = nvs_commit(s_handle); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "putUInt(%s) failed: %s", key, esp_err_to_name(err)); + } + return err; +} + +esp_err_t putBytes(const char *key, const void *data, size_t len) { + if (!s_open) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t err = nvs_set_blob(s_handle, key, data, len); + if (err == ESP_OK) { + err = nvs_commit(s_handle); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "putBytes(%s) failed: %s", key, esp_err_to_name(err)); + } + return err; +} + +} // namespace Storage diff --git a/reference/armor/main/storage.hpp b/reference/armor/main/storage.hpp new file mode 100644 index 0000000..ae05770 --- /dev/null +++ b/reference/armor/main/storage.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "esp_err.h" + +/** + * @brief Small NVS wrapper standing in for the Arduino `Preferences` class. + * + * The original code used `preferences.begin("armor", false)`, `putUInt`, + * `getUInt`, `isKey`, `putBytes` and `getBytes`. Those map directly onto the + * ESP-IDF NVS API; the handle is kept open for the lifetime of the program + * exactly like `Preferences` did. + */ +namespace Storage { + +/** Open (creating if needed) the NVS namespace used by this application. */ +esp_err_t begin(const char *name_space); + +/** True when @p key exists as either a u32 or a blob. */ +bool hasKey(const char *key); + +/** Read a u32, returning @p default_value when the key is absent. */ +uint32_t getUInt(const char *key, uint32_t default_value); + +/** Read a blob, returning the number of bytes read (0 when absent). */ +size_t getBytes(const char *key, void *out, size_t max_len); + +/** Store a u32 and commit it. */ +esp_err_t putUInt(const char *key, uint32_t value); + +/** Store a blob and commit it. */ +esp_err_t putBytes(const char *key, const void *data, size_t len); + +} // namespace Storage diff --git a/reference/armor/partitions.csv b/reference/armor/partitions.csv new file mode 100644 index 0000000..afa707e --- /dev/null +++ b/reference/armor/partitions.csv @@ -0,0 +1,11 @@ +# Custom partition table for the armor node (4 MB flash). +# +# nvs : application settings (color, peer MAC) + WiFi calibration +# phy_init : RF calibration data +# factory : the single application image (2 MB headroom; the WiFi + ESP-NOW +# build is well under 1 MB but the default 1 MB single-app slot is +# uncomfortably tight) +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x200000, diff --git a/reference/armor/sdkconfig.defaults b/reference/armor/sdkconfig.defaults new file mode 100644 index 0000000..82bcb66 --- /dev/null +++ b/reference/armor/sdkconfig.defaults @@ -0,0 +1,7 @@ +# This file was generated using idf.py save-defconfig or menuconfig [D] key. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 6.1.0 Project Minimal Configuration +# +# default: +CONFIG_IDF_TARGET="esp32c3" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y