reference: 早先那版 C 写的装甲板固件(ESP-IDF 工程)

`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 文档站,与本仓库无关。
This commit is contained in:
2026-10-14 19:45:15 +08:00
parent a30eb3200a
commit fd71b83e3a
22 changed files with 1886 additions and 0 deletions
+21
View File
@@ -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
)
+222
View File
@@ -0,0 +1,222 @@
#include "air.hpp"
#include <cstring>
#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<const uint8_t *>(&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<int>(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<int>(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<const wifi_promiscuous_pkt_t *>(buf);
const auto *ipkt = reinterpret_cast<const wifi_ieee80211_packet_t *>(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;
}
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <cstddef>
#include <cstdint>
#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);
};
+215
View File
@@ -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 <cstdio>
#include <cstring>
#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<int>(Air::my_packet.color),
static_cast<int>(Air::peer_packet.hit_cnt),
static_cast<int>(Air::peer_packet.rssi),
static_cast<int>(Air::my_packet.rssi),
static_cast<int>(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);
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#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<uint32_t>(esp_timer_get_time() / 1000);
}
+126
View File
@@ -0,0 +1,126 @@
#include "button.hpp"
#include <cstdlib>
#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<unsigned long>(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<gpio_num_t>(_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;
}
}
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#include <cstdint>
/**
* @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;
};
+192
View File
@@ -0,0 +1,192 @@
#include "hit.hpp"
#include <cstring>
#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<uint32_t>(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<uint8_t>((rgb >> 8) & 0xFF); // G
s_pixels[i * 3 + 1] = static_cast<uint8_t>((rgb >> 16) & 0xFF); // R
s_pixels[i * 3 + 2] = static_cast<uint8_t>((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<const adc_digi_output_data_t *>(&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();
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <cstdint>
/**
* @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();
};
+126
View File
@@ -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(&copy_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;
}
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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
+92
View File
@@ -0,0 +1,92 @@
#include "storage.hpp"
#include <cstring>
#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
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstddef>
#include <cstdint>
#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