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:
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user