mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
ADR-110: ESP32-C6 firmware extension (#764)
Closes the firmware-side ADR-110 design at v0.7.0-esp32 after a 38-iter /loop SOTA sprint. Headline (bench, COM9+COM12 ESP32-C6): - 99.56% cross-board RX, 104.1 µs smoothed offset stdev (≤100 µs §2.4 target met) - 3.95× EMA suppression, 1.4 ppm crystal skew preserved 4 firmware releases: v0.6.7 / v0.6.8 / v0.6.9 / v0.7.0-esp32. 42 ADR-110 unit tests, 1761 v2 workspace tests, full Firmware CI + QEMU green.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
# ESP32-S3 CSI Node Firmware
|
||||
# ESP32 CSI Node Firmware
|
||||
|
||||
**Turn a $7 microcontroller into a privacy-first human sensing node.**
|
||||
|
||||
This firmware captures WiFi Channel State Information (CSI) from an ESP32-S3 and transforms it into real-time presence detection, vital sign monitoring, and programmable sensing -- all without cameras or wearables. Part of the [WiFi-DensePose](../../README.md) project.
|
||||
This firmware captures WiFi Channel State Information (CSI) from an ESP32-S3 (production) or ESP32-C6 (research target — Wi-Fi 6 / 802.15.4 / TWT / LP-core hibernation, see [ADR-110](../../docs/adr/ADR-110-esp32-c6-firmware-extension.md)) and transforms it into real-time presence detection, vital sign monitoring, and programmable sensing -- all without cameras or wearables. Part of the [WiFi-DensePose](../../README.md) project.
|
||||
|
||||
[](https://docs.espressif.com/projects/esp-idf/en/v5.2/)
|
||||
[](https://www.espressif.com/en/products/socs/esp32-s3)
|
||||
[](https://www.espressif.com/en/products/socs/esp32-s3)
|
||||
[](../../LICENSE)
|
||||
[](#memory-budget)
|
||||
[](../../.github/workflows/firmware-ci.yml)
|
||||
|
||||
@@ -9,6 +9,14 @@ set(SRCS
|
||||
"rv_feature_state.c"
|
||||
"rv_mesh.c"
|
||||
"adaptive_controller.c"
|
||||
# ADR-110 — ESP32-C6 capability modules (no-op stubs on other targets via #ifdef)
|
||||
"c6_twt.c"
|
||||
"c6_timesync.c"
|
||||
"c6_lp_core.c"
|
||||
# ADR-110 D1 workaround — ESP-NOW cross-node sync (works on S3+C6)
|
||||
"c6_sync_espnow.c"
|
||||
# ADR-110 B1/B2 unblock — soft-AP HE/TWT (C6-only when enabled)
|
||||
"c6_softap_he.c"
|
||||
)
|
||||
|
||||
# ESP-IDF v6+: headers must resolve via explicit REQUIRES (no implicit deps).
|
||||
@@ -32,6 +40,13 @@ set(REQUIRES
|
||||
mbedtls
|
||||
)
|
||||
|
||||
# ADR-110: C6-only components — pulled in when building for esp32c6.
|
||||
# Note: CONFIG_* symbols are not available in main CMakeLists.txt evaluation —
|
||||
# we use the IDF_TARGET variable that idf.py sets from sdkconfig.defaults / set-target.
|
||||
if(IDF_TARGET STREQUAL "esp32c6")
|
||||
list(APPEND REQUIRES ieee802154 ulp esp_hw_support)
|
||||
endif()
|
||||
|
||||
# ADR-061: Mock CSI generator for QEMU testing + ADR-081 mock radio binding
|
||||
if(CONFIG_CSI_MOCK_ENABLED)
|
||||
list(APPEND SRCS "mock_csi.c" "rv_radio_ops_mock.c")
|
||||
@@ -52,3 +67,15 @@ idf_component_register(
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES ${REQUIRES}
|
||||
)
|
||||
|
||||
# ADR-110 P5 (full): embed the LP-core motion-gate program when enabled.
|
||||
# `ulp_embed_binary` compiles lp_core/main.c with the RISC-V LP toolchain
|
||||
# and links the resulting binary into the HP image, exposing shared symbols
|
||||
# via the auto-generated `ulp_main.h` header.
|
||||
if(IDF_TARGET STREQUAL "esp32c6" AND CONFIG_C6_LP_CORE_ENABLE)
|
||||
set(ulp_app_name ulp_main)
|
||||
set(ulp_sources "lp_core/main.c")
|
||||
# Source files in the HP component that include the generated ulp_main.h
|
||||
set(ulp_exp_dep_srcs "c6_lp_core.c")
|
||||
ulp_embed_binary(${ulp_app_name} "${ulp_sources}" "${ulp_exp_dep_srcs}")
|
||||
endif()
|
||||
|
||||
@@ -287,6 +287,151 @@ menu "WASM Programmable Sensing (ADR-040)"
|
||||
|
||||
endmenu
|
||||
|
||||
menu "ESP32-C6 capabilities (ADR-110)"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
|
||||
config C6_TWT_ENABLE
|
||||
bool "Enable TWT (Target Wake Time) negotiation"
|
||||
default y
|
||||
# SOC_WIFI_HE_SUPPORT is auto-set on chips with HE (Wi-Fi 6) PHY (C6/C5)
|
||||
depends on SOC_WIFI_HE_SUPPORT
|
||||
help
|
||||
After WiFi STA connect, request an individual TWT agreement
|
||||
with the AP for deterministic CSI cadence. Falls back
|
||||
gracefully if the AP doesn't support 11ax TWT.
|
||||
|
||||
config C6_TWT_WAKE_INTERVAL_US
|
||||
int "TWT wake interval (microseconds)"
|
||||
default 10000
|
||||
range 1024 1048576
|
||||
depends on C6_TWT_ENABLE
|
||||
help
|
||||
Period between TWT wake events. 10000 µs = 100 Hz CSI cadence.
|
||||
|
||||
config C6_TWT_MIN_WAKE_DURA_US
|
||||
int "TWT minimum wake duration (microseconds)"
|
||||
default 512
|
||||
range 256 16384
|
||||
depends on C6_TWT_ENABLE
|
||||
help
|
||||
Minimum awake duration per TWT wake. 512 µs is enough to
|
||||
capture one CSI frame.
|
||||
|
||||
config C6_TIMESYNC_ENABLE
|
||||
bool "Enable 802.15.4 mesh time-sync"
|
||||
default y
|
||||
depends on IEEE802154_ENABLED
|
||||
help
|
||||
Cross-node clock alignment over the 802.15.4 radio. Frees
|
||||
WiFi airtime from coordination traffic — relevant to
|
||||
ADR-029/030 multistatic sensing.
|
||||
|
||||
config C6_TIMESYNC_CHANNEL
|
||||
int "802.15.4 time-sync channel (11-26)"
|
||||
default 15
|
||||
range 11 26
|
||||
depends on C6_TIMESYNC_ENABLE
|
||||
|
||||
config C6_LP_CORE_ENABLE
|
||||
bool "Enable LP-core wake-on-motion hibernation"
|
||||
default n
|
||||
depends on ULP_COPROC_TYPE_LP_CORE
|
||||
help
|
||||
Arm the LP RISC-V coprocessor as an always-on motion gate
|
||||
in deep sleep. Targets ~5 µA hibernation for battery
|
||||
seed nodes. Requires a motion sensor on a wake-capable GPIO.
|
||||
|
||||
config C6_LP_WAKE_GPIO
|
||||
int "LP-core wake GPIO"
|
||||
default 4
|
||||
range 0 23
|
||||
depends on C6_LP_CORE_ENABLE
|
||||
|
||||
config C6_LP_WAKE_ACTIVE_HIGH
|
||||
bool "Wake on rising edge"
|
||||
default y
|
||||
depends on C6_LP_CORE_ENABLE
|
||||
|
||||
config C6_LP_POLL_PERIOD_US
|
||||
int "LP-core poll period (microseconds)"
|
||||
default 10000
|
||||
range 1000 1000000
|
||||
depends on C6_LP_CORE_ENABLE
|
||||
help
|
||||
How often the LP-core program reads the wake GPIO.
|
||||
10000 µs = 100 Hz. Lower values give faster response
|
||||
but increase the average LP-core duty cycle (and
|
||||
current). 10 ms is a good balance for PIR sensors.
|
||||
|
||||
config C6_LP_DEBOUNCE_SAMPLES
|
||||
int "LP-core debounce sample count"
|
||||
default 3
|
||||
range 1 32
|
||||
depends on C6_LP_CORE_ENABLE
|
||||
help
|
||||
How many consecutive matching GPIO reads are required
|
||||
before the LP-core wakes the HP core. 3 = ~30 ms at the
|
||||
default 10 ms poll period.
|
||||
|
||||
config C6_SOFTAP_HE_ENABLE
|
||||
bool "Run as Wi-Fi 6 soft-AP with TWT Responder (two-board bench)"
|
||||
default n
|
||||
depends on SOC_WIFI_HE_SUPPORT
|
||||
help
|
||||
When set, the C6 starts in AP+STA mode and advertises a
|
||||
soft-AP that announces HE (Wi-Fi 6) capability with
|
||||
TWT Responder=1. Lets a second C6 station-mode board
|
||||
negotiate a real iTWT agreement against a known-cooperative
|
||||
AP, unblocking ADR-110 §B1/B2 measurement without
|
||||
buying an 11ax router. SSID/PSK configured via NVS
|
||||
(keys `softap_ssid` / `softap_psk`) or the defaults below.
|
||||
|
||||
config C6_SOFTAP_HE_SSID
|
||||
string "Soft-AP SSID (when C6_SOFTAP_HE_ENABLE)"
|
||||
default "ruview-c6-twt"
|
||||
depends on C6_SOFTAP_HE_ENABLE
|
||||
|
||||
config C6_SOFTAP_HE_PSK
|
||||
string "Soft-AP WPA2 password (>= 8 chars)"
|
||||
default "ruviewtwt"
|
||||
depends on C6_SOFTAP_HE_ENABLE
|
||||
|
||||
config C6_SOFTAP_HE_CHANNEL
|
||||
int "Soft-AP channel (1-13)"
|
||||
default 6
|
||||
range 1 13
|
||||
depends on C6_SOFTAP_HE_ENABLE
|
||||
|
||||
config C6_SYNC_EVERY_N_FRAMES
|
||||
int "Sync-packet emission cadence (CSI frames per sync)"
|
||||
default 20
|
||||
range 1 1000
|
||||
help
|
||||
How many CSI callbacks fire before csi_collector emits one
|
||||
ADR-110 §A0.11 sync packet (magic 0xC511A110) carrying the
|
||||
mesh-aligned epoch + sequence high-water for the host
|
||||
aggregator to pair against incoming CSI frames.
|
||||
|
||||
Default 20 = ~2 s between sync packets at the bench's
|
||||
observed 10 fps CSI rate. Raise for less wire overhead;
|
||||
lower for tighter multistatic alignment windows.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "ADR-018 frame extensions (ADR-110)"
|
||||
|
||||
config CSI_FRAME_HE_TAGGING
|
||||
bool "Tag ADR-018 frames with HE PPDU metadata"
|
||||
default y
|
||||
help
|
||||
When the WiFi driver reports an 802.11ax HE-SU/HE-MU/HE-TB
|
||||
PPDU, write the PPDU type + bandwidth into ADR-018 frame
|
||||
bytes 18-19 (previously reserved). Readers that don't know
|
||||
about this extension see the bytes as zero — fully
|
||||
backwards compatible.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Mock CSI (QEMU Testing)"
|
||||
config CSI_MOCK_ENABLED
|
||||
bool "Enable mock CSI generator (for QEMU testing)"
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @file c6_lp_core.c
|
||||
* @brief LP-core wake-on-motion hibernation — ADR-110 Phase 5 (full).
|
||||
*
|
||||
* Two operating modes, controlled by CONFIG_C6_LP_CORE_ENABLE:
|
||||
*
|
||||
* 1. ENABLED — real LP-core RISC-V program polls the wake GPIO at
|
||||
* LP_TIMER cadence (default 10 ms), debounces N matching samples,
|
||||
* and triggers an HP wake via `ulp_lp_core_wakeup_main_processor()`.
|
||||
* HP enters deep sleep with `ESP_SLEEP_WAKEUP_ULP` as the source.
|
||||
* Targets ~5 µA average current (datasheet figure for LP-core +
|
||||
* RTC peripherals powered down). The LP binary is built by
|
||||
* `ulp_embed_binary(...)` in main/CMakeLists.txt from lp_core/main.c.
|
||||
*
|
||||
* 2. DISABLED — falls back to plain deep-sleep + GPIO wake-up
|
||||
* (`esp_deep_sleep_enable_gpio_wakeup`). No debounce, no
|
||||
* sub-10 µA floor, but no LP toolchain dependency either.
|
||||
* This is the path the v0.6.6 firmware shipped with.
|
||||
*
|
||||
* Both paths share `c6_lp_core_arm()` / `c6_lp_core_hibernate_and_wait()`
|
||||
* so call sites in main.c don't change between modes.
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_ULP_COPROC_TYPE_LP_CORE)
|
||||
|
||||
#include "c6_lp_core.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "driver/rtc_io.h"
|
||||
#include "soc/soc_caps.h"
|
||||
#include <string.h>
|
||||
|
||||
#if defined(CONFIG_C6_LP_CORE_ENABLE)
|
||||
#include "ulp_lp_core.h"
|
||||
/* ulp_main.h is auto-generated by `ulp_embed_binary(ulp_main, ...)` and
|
||||
* exports every `volatile` global from lp_core/main.c with the `ulp_`
|
||||
* prefix. Include is guarded so disabled builds don't try to find a
|
||||
* file the build system hasn't generated. */
|
||||
#include "ulp_main.h"
|
||||
extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start");
|
||||
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end");
|
||||
#endif
|
||||
|
||||
static const char *TAG = "c6_lp";
|
||||
|
||||
static int s_wake_gpio = -1;
|
||||
static bool s_active_high = true;
|
||||
static bool s_armed = false;
|
||||
|
||||
#ifndef CONFIG_C6_LP_POLL_PERIOD_US
|
||||
#define CONFIG_C6_LP_POLL_PERIOD_US 10000 /* 100 Hz default poll cadence */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_C6_LP_DEBOUNCE_SAMPLES
|
||||
#define CONFIG_C6_LP_DEBOUNCE_SAMPLES 3
|
||||
#endif
|
||||
|
||||
esp_err_t c6_lp_core_arm(int wake_gpio, bool active_high)
|
||||
{
|
||||
if (wake_gpio < 0) {
|
||||
ESP_LOGE(TAG, "invalid wake_gpio=%d", wake_gpio);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
s_wake_gpio = wake_gpio;
|
||||
s_active_high = active_high;
|
||||
|
||||
/* GPIO must be in the LP/RTC domain for either wake path. */
|
||||
esp_err_t ret = rtc_gpio_init(wake_gpio);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "rtc_gpio_init(%d) failed: %s", wake_gpio, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
rtc_gpio_set_direction(wake_gpio, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
/* Floating inputs in deep sleep are an antenna — disable internal pulls
|
||||
* only if the user has an external pull on the motion line; we leave
|
||||
* default pulls so a disconnected pin doesn't toggle randomly. */
|
||||
|
||||
#if defined(CONFIG_C6_LP_CORE_ENABLE)
|
||||
/* --- Real LP-core path --- */
|
||||
|
||||
/* On C6, LP-IO maps 1:1 to GPIO for indices 0..7. Validate. */
|
||||
if (wake_gpio > 7) {
|
||||
ESP_LOGE(TAG, "LP-core path requires LP-IO 0..7, got GPIO %d", wake_gpio);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Load the LP-core binary blob. */
|
||||
esp_err_t err = ulp_lp_core_load_binary(
|
||||
ulp_main_bin_start,
|
||||
(size_t)(ulp_main_bin_end - ulp_main_bin_start));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ulp_lp_core_load_binary failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Hand the GPIO parameters to the LP program via shared symbols.
|
||||
* These are declared `volatile` in lp_core/main.c so the HP write
|
||||
* is observed by LP on the next iteration. */
|
||||
ulp_wake_gpio_num = (uint32_t)wake_gpio;
|
||||
ulp_wake_active_high = active_high ? 1u : 0u;
|
||||
ulp_debounce_samples = CONFIG_C6_LP_DEBOUNCE_SAMPLES;
|
||||
ulp_motion_count = 0;
|
||||
ulp_poll_count = 0;
|
||||
ulp_last_gpio_level = 0;
|
||||
|
||||
/* Configure LP-timer wakeup at the configured poll period and start the
|
||||
* LP-core. `ulp_lp_core_run` is non-blocking; the LP core begins running
|
||||
* the program immediately and the HP core can proceed to deep sleep. */
|
||||
ulp_lp_core_cfg_t cfg = {
|
||||
.wakeup_source = ULP_LP_CORE_WAKEUP_SOURCE_LP_TIMER,
|
||||
.lp_timer_sleep_duration_us = CONFIG_C6_LP_POLL_PERIOD_US,
|
||||
};
|
||||
err = ulp_lp_core_run(&cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ulp_lp_core_run failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Tell deep-sleep that the LP-core is our wake source. */
|
||||
err = esp_sleep_enable_ulp_wakeup();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_sleep_enable_ulp_wakeup failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
s_armed = true;
|
||||
ESP_LOGI(TAG, "LP-core armed: gpio=%d active_%s debounce=%d poll=%d µs",
|
||||
wake_gpio, active_high ? "high" : "low",
|
||||
CONFIG_C6_LP_DEBOUNCE_SAMPLES, CONFIG_C6_LP_POLL_PERIOD_US);
|
||||
return ESP_OK;
|
||||
|
||||
#else
|
||||
/* --- Fallback path: plain deep-sleep GPIO wakeup (~10 µA floor) --- */
|
||||
uint64_t mask = 1ULL << wake_gpio;
|
||||
esp_deepsleep_gpio_wake_up_mode_t mode = active_high
|
||||
? ESP_GPIO_WAKEUP_GPIO_HIGH
|
||||
: ESP_GPIO_WAKEUP_GPIO_LOW;
|
||||
esp_err_t err = esp_deep_sleep_enable_gpio_wakeup(mask, mode);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "enable_gpio_wakeup failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
s_armed = true;
|
||||
ESP_LOGI(TAG, "GPIO-wakeup armed (no LP-core): gpio=%d active_%s",
|
||||
wake_gpio, active_high ? "high" : "low");
|
||||
return ESP_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
void c6_lp_core_hibernate_and_wait(void)
|
||||
{
|
||||
if (!s_armed) {
|
||||
ESP_LOGW(TAG, "hibernate called without arm — sleeping with no wake source");
|
||||
}
|
||||
/* Power down the RTC peripheral domain — the LP-core itself stays
|
||||
* powered on the LP power domain so it can keep polling. */
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
|
||||
|
||||
#if defined(CONFIG_C6_LP_CORE_ENABLE)
|
||||
ESP_LOGI(TAG, "entering deep sleep — LP-core polling, target ≤5 µA");
|
||||
#else
|
||||
ESP_LOGI(TAG, "entering deep sleep — GPIO wakeup, target ~10 µA");
|
||||
#endif
|
||||
esp_deep_sleep_start();
|
||||
/* Never returns. */
|
||||
}
|
||||
|
||||
bool c6_lp_core_was_motion_wake(void)
|
||||
{
|
||||
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||
#if defined(CONFIG_C6_LP_CORE_ENABLE)
|
||||
/* Real LP-core path: wakeup cause is ULP (LP-core triggered HP). */
|
||||
if (cause == ESP_SLEEP_WAKEUP_ULP) return true;
|
||||
#endif
|
||||
/* Fallback path or alternate GPIO wakeup. */
|
||||
return cause == ESP_SLEEP_WAKEUP_GPIO || cause == ESP_SLEEP_WAKEUP_EXT1;
|
||||
}
|
||||
|
||||
#if defined(CONFIG_C6_LP_CORE_ENABLE)
|
||||
uint32_t c6_lp_core_motion_count(void)
|
||||
{
|
||||
return (uint32_t)ulp_motion_count;
|
||||
}
|
||||
|
||||
uint32_t c6_lp_core_poll_count(void)
|
||||
{
|
||||
return (uint32_t)ulp_poll_count;
|
||||
}
|
||||
#else
|
||||
uint32_t c6_lp_core_motion_count(void) { return 0; }
|
||||
uint32_t c6_lp_core_poll_count(void) { return 0; }
|
||||
#endif
|
||||
|
||||
#endif /* CONFIG_IDF_TARGET_ESP32C6 && CONFIG_ULP_COPROC_TYPE_LP_CORE */
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @file c6_lp_core.h
|
||||
* @brief LP-core wake-on-motion hibernation helper — ADR-110 Phase 5.
|
||||
*
|
||||
* Arms the C6 LP RISC-V coprocessor as an always-on watchdog that
|
||||
* monitors a GPIO (typically a PIR or accelerometer interrupt line) and
|
||||
* wakes the HP core only when motion is detected. Targets ~5 µA
|
||||
* hibernation current for battery-powered Cognitum Seed nodes.
|
||||
*
|
||||
* Only built when CONFIG_IDF_TARGET_ESP32C6 + CONFIG_ULP_COPROC_TYPE_LP_CORE.
|
||||
*
|
||||
* P5 skeleton: the LP-core program is shipped as inline C compiled into
|
||||
* the main image. A follow-up turn migrates it to a separate
|
||||
* lp_core/main.c subproject with its own CMake.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_ULP_COPROC_TYPE_LP_CORE)
|
||||
|
||||
/**
|
||||
* Configure the LP-core wake-on-motion watcher.
|
||||
*
|
||||
* @param wake_gpio GPIO pin to monitor (must be an RTC/LP-domain GPIO).
|
||||
* @param active_high true = wake on rising edge, false = falling.
|
||||
* @return ESP_OK on success.
|
||||
*/
|
||||
esp_err_t c6_lp_core_arm(int wake_gpio, bool active_high);
|
||||
|
||||
/**
|
||||
* Enter deep sleep with the LP-core armed as the wake source. Does not
|
||||
* return — the next boot will see ESP_SLEEP_WAKEUP_LP_CORE in
|
||||
* esp_sleep_get_wakeup_cause().
|
||||
*/
|
||||
void c6_lp_core_hibernate_and_wait(void);
|
||||
|
||||
/**
|
||||
* Returns true if the most recent boot was a wake from LP-core motion
|
||||
* detection (vs a cold boot or different wake source).
|
||||
*/
|
||||
bool c6_lp_core_was_motion_wake(void);
|
||||
|
||||
/**
|
||||
* Monotonic counter of wake-triggering motion events observed by the
|
||||
* LP-core program since the last cold boot. Returns 0 when
|
||||
* CONFIG_C6_LP_CORE_ENABLE is unset (fallback path).
|
||||
*/
|
||||
uint32_t c6_lp_core_motion_count(void);
|
||||
|
||||
/**
|
||||
* Total LP-timer poll iterations executed by the LP-core program.
|
||||
* Useful as a sanity check that the LP-core is actually running;
|
||||
* returns 0 on the fallback path.
|
||||
*/
|
||||
uint32_t c6_lp_core_poll_count(void);
|
||||
|
||||
#else
|
||||
|
||||
static inline esp_err_t c6_lp_core_arm(int g, bool h) { (void)g; (void)h; return ESP_OK; }
|
||||
static inline void c6_lp_core_hibernate_and_wait(void) { }
|
||||
static inline bool c6_lp_core_was_motion_wake(void) { return false; }
|
||||
static inline uint32_t c6_lp_core_motion_count(void) { return 0; }
|
||||
static inline uint32_t c6_lp_core_poll_count(void) { return 0; }
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @file c6_softap_he.c
|
||||
* @brief ESP32-C6 soft-AP with HE/TWT — ADR-110 B1/B2 cheap-unblock.
|
||||
*
|
||||
* Pairs with c6_softap_he.h. Builds only when both targets are set:
|
||||
*
|
||||
* CONFIG_IDF_TARGET_ESP32C6 (selected by `idf.py set-target esp32c6`)
|
||||
* CONFIG_C6_SOFTAP_HE_ENABLE (Kconfig, default n)
|
||||
*
|
||||
* The IDF v5.4 soft-AP path advertises HE automatically on chips with
|
||||
* SOC_WIFI_HE_SUPPORT; the operator-side concern here is making sure
|
||||
* the beacon also advertises `TWT Responder=1` so a STA-side
|
||||
* `esp_wifi_sta_itwt_setup()` call doesn't bounce with `INVALID_ARG`
|
||||
* the same way it did against `ruv.net` (the bench's 11n-only AP).
|
||||
*
|
||||
* TWT Responder advertisement in IDF v5.4 is gated by
|
||||
* `wifi_he_ap_config_t.twt_responder = 1`. When the IDF header doesn't
|
||||
* expose that struct (older v5.3), the AP still comes up with HE but
|
||||
* without TWT Responder — we log a warning and continue so the build
|
||||
* stays portable.
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_SOFTAP_HE_ENABLE)
|
||||
|
||||
#include "c6_softap_he.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_wifi_types.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "c6_softap";
|
||||
|
||||
static bool s_started = false;
|
||||
static uint8_t s_sta_count = 0;
|
||||
static uint8_t s_channel = 0;
|
||||
|
||||
#ifndef CONFIG_C6_SOFTAP_HE_SSID
|
||||
#define CONFIG_C6_SOFTAP_HE_SSID "ruview-c6-twt"
|
||||
#endif
|
||||
#ifndef CONFIG_C6_SOFTAP_HE_PSK
|
||||
#define CONFIG_C6_SOFTAP_HE_PSK "ruviewtwt"
|
||||
#endif
|
||||
#ifndef CONFIG_C6_SOFTAP_HE_CHANNEL
|
||||
#define CONFIG_C6_SOFTAP_HE_CHANNEL 6
|
||||
#endif
|
||||
|
||||
static void load_nvs_override(const char *key, char *dst, size_t dst_len)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open("ruview", NVS_READONLY, &h) != ESP_OK) return;
|
||||
size_t n = dst_len;
|
||||
esp_err_t err = nvs_get_str(h, key, dst, &n);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "nvs override: %s=\"%s\"", key, dst);
|
||||
}
|
||||
nvs_close(h);
|
||||
}
|
||||
|
||||
static uint8_t load_nvs_u8(const char *key, uint8_t fallback)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open("ruview", NVS_READONLY, &h) != ESP_OK) return fallback;
|
||||
uint8_t v = fallback;
|
||||
if (nvs_get_u8(h, key, &v) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "nvs override: %s=%u", key, v);
|
||||
}
|
||||
nvs_close(h);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void on_wifi_event(void *arg, esp_event_base_t base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
(void)arg; (void)base; (void)event_data;
|
||||
switch (event_id) {
|
||||
case WIFI_EVENT_AP_START:
|
||||
s_started = true;
|
||||
ESP_LOGI(TAG, "AP started on channel %u", s_channel);
|
||||
break;
|
||||
case WIFI_EVENT_AP_STOP:
|
||||
s_started = false;
|
||||
ESP_LOGI(TAG, "AP stopped");
|
||||
break;
|
||||
case WIFI_EVENT_AP_STACONNECTED:
|
||||
if (s_sta_count < 255) s_sta_count++;
|
||||
ESP_LOGI(TAG, "STA connected — total=%u", s_sta_count);
|
||||
break;
|
||||
case WIFI_EVENT_AP_STADISCONNECTED:
|
||||
if (s_sta_count > 0) s_sta_count--;
|
||||
ESP_LOGI(TAG, "STA disconnected — total=%u", s_sta_count);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t c6_softap_he_start(uint8_t *out_channel)
|
||||
{
|
||||
if (s_started) {
|
||||
if (out_channel) *out_channel = s_channel;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Resolve config: NVS overrides Kconfig defaults. */
|
||||
char ssid[33] = CONFIG_C6_SOFTAP_HE_SSID;
|
||||
char psk[64] = CONFIG_C6_SOFTAP_HE_PSK;
|
||||
load_nvs_override("softap_ssid", ssid, sizeof(ssid));
|
||||
load_nvs_override("softap_psk", psk, sizeof(psk));
|
||||
s_channel = load_nvs_u8("softap_chan", CONFIG_C6_SOFTAP_HE_CHANNEL);
|
||||
if (s_channel < 1 || s_channel > 13) s_channel = CONFIG_C6_SOFTAP_HE_CHANNEL;
|
||||
|
||||
/* AP+STA so the existing STA path keeps working (NVS-provisioned upstream). */
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
|
||||
|
||||
wifi_config_t ap_cfg = {0};
|
||||
size_t ssid_len = strlen(ssid);
|
||||
if (ssid_len > 32) ssid_len = 32;
|
||||
memcpy(ap_cfg.ap.ssid, ssid, ssid_len);
|
||||
ap_cfg.ap.ssid_len = (uint8_t)ssid_len;
|
||||
strncpy((char *)ap_cfg.ap.password, psk, sizeof(ap_cfg.ap.password) - 1);
|
||||
ap_cfg.ap.channel = s_channel;
|
||||
ap_cfg.ap.max_connection = 4;
|
||||
ap_cfg.ap.authmode = strlen(psk) >= 8 ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
|
||||
ap_cfg.ap.beacon_interval = 100;
|
||||
/* pmf_cfg.required = false keeps backward compatibility for STA clients
|
||||
* that don't speak PMF. */
|
||||
ap_cfg.ap.pmf_cfg.required = false;
|
||||
|
||||
/* Register the event handler before bringing the AP up so we don't
|
||||
* miss WIFI_EVENT_AP_START. */
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, on_wifi_event, NULL, NULL));
|
||||
|
||||
esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "set_config(AP) failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/* IDF v5.4 LIMIT (verified empirically 2026-05-23 — WITNESS-LOG-110 §A0.6):
|
||||
* the public API exposes ONLY STA-side iTWT/bTWT (esp_wifi_sta_itwt_*,
|
||||
* esp_wifi_sta_btwt_*). There is NO esp_wifi_ap_set_he_config(), NO
|
||||
* wifi_he_ap_config_t, and NO wifi_config_t.ap.he_* field. A second C6
|
||||
* associating against this soft-AP currently lands at phymode 11bgn
|
||||
* (he:0, vht:0, ht:1) — the AP doesn't advertise HE because there's no
|
||||
* way to ask it to. A future IDF release that exposes AP-side HE config
|
||||
* (or a patched WiFi blob) is required to make this AP iTWT-capable.
|
||||
*
|
||||
* Until then, this module still gives you a working WPA2 soft-AP on a
|
||||
* controlled channel for AP+STA bench experiments and ESP-NOW peer
|
||||
* discovery — just not iTWT validation. The c6_twt module on the STA
|
||||
* side will return ESP_ERR_INVALID_ARG against this AP (no TWT Responder
|
||||
* in the beacon), exactly as it does against any other 11n-only AP. */
|
||||
ESP_LOGI(TAG, "soft-AP starting: ssid=\"%s\" channel=%u auth=%s",
|
||||
ssid, s_channel,
|
||||
ap_cfg.ap.authmode == WIFI_AUTH_OPEN ? "open" : "wpa2-psk");
|
||||
ESP_LOGW(TAG, "IDF v5.4 soft-AP does NOT advertise HE — STAs will associate at 11bgn. "
|
||||
"iTWT validation requires an external 11ax AP. See WITNESS-LOG-110 §A0.6.");
|
||||
|
||||
/* Don't call esp_wifi_start() here — main.c brings the WiFi up once
|
||||
* for both AP and STA. We just configured the AP iface so it joins
|
||||
* the existing start. */
|
||||
|
||||
if (out_channel) *out_channel = s_channel;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool c6_softap_he_is_up(void) { return s_started; }
|
||||
uint8_t c6_softap_he_sta_count(void) { return s_sta_count; }
|
||||
|
||||
#endif /* CONFIG_IDF_TARGET_ESP32C6 && CONFIG_C6_SOFTAP_HE_ENABLE */
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file c6_softap_he.h
|
||||
* @brief ESP32-C6 soft-AP with Wi-Fi 6 (HE) capability + TWT Responder.
|
||||
*
|
||||
* ADR-110 §B1/B2 cheap-unblock: turn one C6 board into the iTWT-capable
|
||||
* AP that the C6-DevKit-on-the-shelf-only bench is missing. A second C6
|
||||
* board in STA mode can then negotiate a real iTWT agreement against
|
||||
* this AP and measure deterministic CSI cadence — without buying an
|
||||
* 11ax router.
|
||||
*
|
||||
* Build-gated by CONFIG_C6_SOFTAP_HE_ENABLE (default n). When disabled,
|
||||
* all functions become no-ops so non-AP firmwares pay zero overhead.
|
||||
*
|
||||
* NVS overrides (read at boot if present, fall back to Kconfig defaults):
|
||||
* softap_ssid (string, up to 32 chars)
|
||||
* softap_psk (string, 8..63 chars)
|
||||
* softap_chan (u8, 1..13)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_SOFTAP_HE_ENABLE)
|
||||
|
||||
/**
|
||||
* Bring up the soft-AP in AP+STA mode with HE (Wi-Fi 6) advertised and
|
||||
* TWT Responder=1 if the IDF build supports it. Idempotent — safe to
|
||||
* call once during boot after `esp_wifi_init()`. Returns the channel
|
||||
* the AP is actually running on (may differ from Kconfig if the IDF
|
||||
* scanner picks a clearer channel).
|
||||
*/
|
||||
esp_err_t c6_softap_he_start(uint8_t *out_channel);
|
||||
|
||||
/**
|
||||
* True after the IDF reports the AP has started successfully.
|
||||
*/
|
||||
bool c6_softap_he_is_up(void);
|
||||
|
||||
/**
|
||||
* Number of currently associated stations (read-only, refreshed on the
|
||||
* WIFI_EVENT_AP_STACONNECTED/DISCONNECTED events).
|
||||
*/
|
||||
uint8_t c6_softap_he_sta_count(void);
|
||||
|
||||
#else /* disabled — no-op stubs */
|
||||
|
||||
static inline esp_err_t c6_softap_he_start(uint8_t *out_channel)
|
||||
{
|
||||
if (out_channel) *out_channel = 0;
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline bool c6_softap_he_is_up(void) { return false; }
|
||||
static inline uint8_t c6_softap_he_sta_count(void) { return 0; }
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @file c6_sync_espnow.c
|
||||
* @brief ESP-NOW cross-node time-sync — ADR-110 D1 workaround.
|
||||
*
|
||||
* Same protocol as c6_timesync.c (TS_BEACON every 100 ms with leader epoch),
|
||||
* but over ESP-NOW instead of 802.15.4 because the IDF v5.4 ieee802154 RX
|
||||
* path doesn't deliver frames to user-space (see WITNESS-LOG-110 §D1).
|
||||
*
|
||||
* Frame layout (16 bytes payload, broadcast MAC FF:FF:FF:FF:FF:FF):
|
||||
* [0..3] Magic 0x53454E50 ('SENP' — Sync via ESP-NOW)
|
||||
* [4] Protocol ver 0x01
|
||||
* [5] Leader flag 1 if sender claims leader
|
||||
* [6..7] Reserved
|
||||
* [8..15] Leader epoch µs (LE u64)
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "c6_sync_espnow.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_now.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/timers.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "c6_espnow";
|
||||
|
||||
#define BEACON_MAGIC 0x53454E50u /* 'SENP' little-endian */
|
||||
#define BEACON_PROTO_VER 0x01
|
||||
#define BEACON_PERIOD_MS 100
|
||||
#define VALID_WINDOW_MS 3000
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint8_t proto_ver;
|
||||
uint8_t leader_flag;
|
||||
uint16_t _reserved;
|
||||
uint64_t leader_epoch_us;
|
||||
} espnow_beacon_t;
|
||||
|
||||
static const uint8_t s_broadcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
static uint64_t s_local_id = 0; /* 6-byte MAC packed into u64 */
|
||||
static uint64_t s_leader_id = 0;
|
||||
static int64_t s_offset_us = 0;
|
||||
static uint64_t s_last_seen_us = 0;
|
||||
static bool s_is_leader = false;
|
||||
static TimerHandle_t s_beacon_timer = NULL;
|
||||
|
||||
static uint32_t s_tx_count = 0;
|
||||
static uint32_t s_tx_fail = 0;
|
||||
static uint32_t s_rx_count = 0;
|
||||
static uint32_t s_rx_magic_match = 0;
|
||||
|
||||
/* ADR-110 P10 — EMA-smoothed offset (host-side trajectory in firmware).
|
||||
*
|
||||
* The §A0.8 four-minute soak measured 540 µs sample-stdev around a true
|
||||
* offset that drifts at ≈1.4 ppm between two C6 crystals. An exponential
|
||||
* moving average with α=0.125 (Q3.3 fixed-point shift = 3) yields an
|
||||
* effective ~8-sample window, fast enough to track the drift (~7 µs/sec
|
||||
* worst-case) while suppressing the per-beacon WiFi-MAC jitter.
|
||||
*
|
||||
* Two consumers: get_offset_us() (raw, unchanged — for diagnostics) and
|
||||
* get_offset_us_smoothed() (filtered — what CSI frames should stamp).
|
||||
* Both expose `int64_t` so call sites stay identical. */
|
||||
#define OFFSET_EMA_SHIFT 3 /* α = 1/8 = 0.125 */
|
||||
static int64_t s_offset_us_smoothed = 0;
|
||||
static bool s_smoothed_seeded = false;
|
||||
|
||||
static uint64_t mac6_to_u64(const uint8_t mac[6])
|
||||
{
|
||||
return ((uint64_t)mac[0] << 40) | ((uint64_t)mac[1] << 32) |
|
||||
((uint64_t)mac[2] << 24) | ((uint64_t)mac[3] << 16) |
|
||||
((uint64_t)mac[4] << 8) | (uint64_t)mac[5];
|
||||
}
|
||||
|
||||
static void send_beacon(void)
|
||||
{
|
||||
espnow_beacon_t b = {
|
||||
.magic = BEACON_MAGIC,
|
||||
.proto_ver = BEACON_PROTO_VER,
|
||||
.leader_flag = s_is_leader ? 1 : 0,
|
||||
._reserved = 0,
|
||||
.leader_epoch_us = (uint64_t)esp_timer_get_time(),
|
||||
};
|
||||
esp_err_t r = esp_now_send(s_broadcast_mac, (uint8_t *)&b, sizeof(b));
|
||||
s_tx_count++;
|
||||
if (r != ESP_OK) s_tx_fail++;
|
||||
/* Diag log every 50 beacons. */
|
||||
if ((s_tx_count % 50) == 1) {
|
||||
ESP_LOGI(TAG, "tx#%lu (fail=%lu) rx#%lu (match=%lu) leader=%d offset_us=%lld smoothed=%lld",
|
||||
(unsigned long)s_tx_count, (unsigned long)s_tx_fail,
|
||||
(unsigned long)s_rx_count, (unsigned long)s_rx_magic_match,
|
||||
(int)s_is_leader, (long long)s_offset_us,
|
||||
(long long)s_offset_us_smoothed);
|
||||
}
|
||||
}
|
||||
|
||||
/* IDF v5.4 ESP-NOW recv callback signature uses esp_now_recv_info_t.
|
||||
* Falls back to the older signature on older IDF via ifdef. */
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
static void on_recv(const esp_now_recv_info_t *info,
|
||||
const uint8_t *data, int len)
|
||||
{
|
||||
const uint8_t *src_mac = info ? info->src_addr : NULL;
|
||||
#else
|
||||
static void on_recv(const uint8_t *src_mac, const uint8_t *data, int len)
|
||||
{
|
||||
#endif
|
||||
s_rx_count++;
|
||||
if (data == NULL || len < (int)sizeof(espnow_beacon_t)) return;
|
||||
const espnow_beacon_t *b = (const espnow_beacon_t *)data;
|
||||
if (b->magic != BEACON_MAGIC || b->proto_ver != BEACON_PROTO_VER) return;
|
||||
s_rx_magic_match++;
|
||||
uint64_t sender_id = src_mac ? mac6_to_u64(src_mac) : 0;
|
||||
uint64_t now_us = (uint64_t)esp_timer_get_time();
|
||||
|
||||
/* Adopt sender as leader if it's claiming leadership AND its ID is
|
||||
* lower than our current leader (or we have no leader). Lowest MAC
|
||||
* wins — deterministic. */
|
||||
if (b->leader_flag && (s_leader_id == 0 || sender_id < s_leader_id)) {
|
||||
if (s_is_leader && sender_id < s_local_id) {
|
||||
ESP_LOGI(TAG, "stepping down: heard lower-id leader %012llx (we are %012llx)",
|
||||
(unsigned long long)sender_id, (unsigned long long)s_local_id);
|
||||
s_is_leader = false;
|
||||
}
|
||||
s_leader_id = sender_id;
|
||||
}
|
||||
|
||||
/* If accepted leader, compute offset from their epoch (only for non-leader). */
|
||||
if (b->leader_flag && !s_is_leader && sender_id == s_leader_id) {
|
||||
int64_t raw = (int64_t)b->leader_epoch_us - (int64_t)now_us;
|
||||
s_offset_us = raw;
|
||||
s_last_seen_us = now_us;
|
||||
/* EMA: y[n] = y[n-1] + (raw - y[n-1]) >> SHIFT */
|
||||
if (!s_smoothed_seeded) {
|
||||
s_offset_us_smoothed = raw;
|
||||
s_smoothed_seeded = true;
|
||||
} else {
|
||||
s_offset_us_smoothed += (raw - s_offset_us_smoothed) >> OFFSET_EMA_SHIFT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void on_send(const uint8_t *mac, esp_now_send_status_t status)
|
||||
{
|
||||
(void)mac;
|
||||
if (status != ESP_NOW_SEND_SUCCESS) s_tx_fail++;
|
||||
}
|
||||
|
||||
static void beacon_timer_cb(TimerHandle_t t)
|
||||
{
|
||||
(void)t;
|
||||
uint64_t now = (uint64_t)esp_timer_get_time();
|
||||
/* Promote self if no leader beacon for VALID_WINDOW_MS and we have lowest known id. */
|
||||
if (!s_is_leader && (now - s_last_seen_us) > (VALID_WINDOW_MS * 1000ULL)) {
|
||||
if (s_leader_id == 0 || s_local_id < s_leader_id) {
|
||||
s_is_leader = true;
|
||||
s_leader_id = s_local_id;
|
||||
s_offset_us = 0;
|
||||
ESP_LOGI(TAG, "promoting self to leader (no beacons for %u ms; local_id=%012llx)",
|
||||
(unsigned)VALID_WINDOW_MS, (unsigned long long)s_local_id);
|
||||
}
|
||||
}
|
||||
send_beacon();
|
||||
}
|
||||
|
||||
esp_err_t c6_sync_espnow_init(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
s_local_id = mac6_to_u64(mac);
|
||||
|
||||
esp_err_t r = esp_now_init();
|
||||
if (r != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(r));
|
||||
return r;
|
||||
}
|
||||
esp_now_register_recv_cb(on_recv);
|
||||
esp_now_register_send_cb(on_send);
|
||||
|
||||
/* Add broadcast peer so esp_now_send to FF:FF:FF:FF:FF:FF works. */
|
||||
esp_now_peer_info_t peer = {0};
|
||||
memcpy(peer.peer_addr, s_broadcast_mac, 6);
|
||||
peer.channel = 0; /* current STA channel */
|
||||
peer.ifidx = WIFI_IF_STA;
|
||||
peer.encrypt = false;
|
||||
r = esp_now_add_peer(&peer);
|
||||
if (r != ESP_OK && r != ESP_ERR_ESPNOW_EXIST) {
|
||||
ESP_LOGW(TAG, "esp_now_add_peer(broadcast) failed: %s", esp_err_to_name(r));
|
||||
}
|
||||
|
||||
/* Start as candidate leader — will step down on receiving lower-id beacon. */
|
||||
s_is_leader = true;
|
||||
s_leader_id = s_local_id;
|
||||
s_last_seen_us = (uint64_t)esp_timer_get_time();
|
||||
|
||||
s_beacon_timer = xTimerCreate("c6_espnow_beacon",
|
||||
pdMS_TO_TICKS(BEACON_PERIOD_MS),
|
||||
pdTRUE, NULL, beacon_timer_cb);
|
||||
if (s_beacon_timer == NULL) {
|
||||
ESP_LOGE(TAG, "xTimerCreate failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
xTimerStart(s_beacon_timer, 0);
|
||||
|
||||
ESP_LOGI(TAG, "init done: local_id=%012llx leader=yes(candidate) period=%ums",
|
||||
(unsigned long long)s_local_id, (unsigned)BEACON_PERIOD_MS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint64_t c6_sync_espnow_get_epoch_us(void)
|
||||
{
|
||||
/* Prefer the smoothed offset once we've heard a leader beacon; falls
|
||||
* back to raw=0 on the leader board and during the first second after
|
||||
* follower boot. The smoothed value is what CSI frames should stamp
|
||||
* for cross-board multistatic alignment (§A0.8 measured 540 µs raw
|
||||
* stdev → expected <100 µs smoothed with α=1/8 over ~8 samples). */
|
||||
int64_t off = s_smoothed_seeded ? s_offset_us_smoothed : s_offset_us;
|
||||
return (uint64_t)((int64_t)esp_timer_get_time() + off);
|
||||
}
|
||||
|
||||
bool c6_sync_espnow_is_leader(void) { return s_is_leader; }
|
||||
int64_t c6_sync_espnow_get_offset_us(void) { return s_offset_us; }
|
||||
int64_t c6_sync_espnow_get_offset_us_smoothed(void) { return s_offset_us_smoothed; }
|
||||
|
||||
bool c6_sync_espnow_is_valid(void)
|
||||
{
|
||||
if (s_is_leader) return true;
|
||||
uint64_t now = (uint64_t)esp_timer_get_time();
|
||||
return (now - s_last_seen_us) < (VALID_WINDOW_MS * 1000ULL);
|
||||
}
|
||||
|
||||
uint32_t c6_sync_espnow_tx_count(void) { return s_tx_count; }
|
||||
uint32_t c6_sync_espnow_tx_fail(void) { return s_tx_fail; }
|
||||
uint32_t c6_sync_espnow_rx_count(void) { return s_rx_count; }
|
||||
uint32_t c6_sync_espnow_rx_magic_match(void) { return s_rx_magic_match; }
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file c6_sync_espnow.h
|
||||
* @brief ESP-NOW based cross-node time-sync — ADR-110 D1 workaround.
|
||||
*
|
||||
* After 4 systematic experiments confirmed the 802.15.4 RX path is broken
|
||||
* in this user-code + IDF v5.4 combination (see WITNESS-LOG-110 §D1), the
|
||||
* cross-node sync claim was unblocked by switching transport from IEEE
|
||||
* 802.15.4 to ESP-NOW (WiFi-based peer-to-peer, runs on the same 2.4 GHz
|
||||
* radio but uses the WiFi MAC layer that ESP-IDF's 802.11 driver fully
|
||||
* supports).
|
||||
*
|
||||
* Trade vs. 802.15.4:
|
||||
* - Loses the "frees WiFi airtime for CSI" property (uses WiFi for sync)
|
||||
* - Gains a known-working RX path on every ESP32 family
|
||||
* - Same API surface (epoch_us, is_valid, is_leader) so call sites that
|
||||
* used to depend on c6_timesync drop in unchanged
|
||||
*
|
||||
* Works on both ESP32-S3 and ESP32-C6 — the cross-node sync becomes a
|
||||
* cross-target feature, not C6-only.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Initialize the ESP-NOW sync module. Must be called AFTER WiFi STA is
|
||||
* connected (ESP-NOW needs the WiFi driver active).
|
||||
*
|
||||
* @return ESP_OK on success.
|
||||
*/
|
||||
esp_err_t c6_sync_espnow_init(void);
|
||||
|
||||
/**
|
||||
* Returns the synced wall-clock estimate in microseconds.
|
||||
* If no leader heard within the timeout, returns the local
|
||||
* esp_timer_get_time() value unchanged (offset = 0).
|
||||
*/
|
||||
uint64_t c6_sync_espnow_get_epoch_us(void);
|
||||
|
||||
bool c6_sync_espnow_is_leader(void);
|
||||
bool c6_sync_espnow_is_valid(void);
|
||||
int64_t c6_sync_espnow_get_offset_us(void);
|
||||
|
||||
/**
|
||||
* EMA-smoothed offset (α=1/8, ~8-sample effective window at the 10 Hz
|
||||
* beacon rate). Tracks the ≈1.4 ppm crystal drift between two C6 boards
|
||||
* (measured in §A0.8) while suppressing the 540 µs per-beacon WiFi-MAC
|
||||
* jitter. CSI frame timestamps should stamp from this value, not the raw
|
||||
* offset — `c6_sync_espnow_get_epoch_us()` already does so internally.
|
||||
*/
|
||||
int64_t c6_sync_espnow_get_offset_us_smoothed(void);
|
||||
|
||||
/* Counters for the witness harness — exposed for tests/diagnostics. */
|
||||
uint32_t c6_sync_espnow_tx_count(void);
|
||||
uint32_t c6_sync_espnow_tx_fail(void);
|
||||
uint32_t c6_sync_espnow_rx_count(void);
|
||||
uint32_t c6_sync_espnow_rx_magic_match(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @file c6_timesync.c
|
||||
* @brief 802.15.4 mesh time-sync skeleton — ADR-110 Phase 4.
|
||||
*
|
||||
* P4 ships the API surface, role election, and the leader-broadcast +
|
||||
* follower-receive paths using esp_ieee802154 raw frames. Full
|
||||
* OpenThread MTD attachment with a real network key is deferred to a
|
||||
* follow-up turn — the skeleton already exercises the radio init and
|
||||
* the offset-tracking math.
|
||||
*
|
||||
* Beacon frame layout (12 bytes payload + 802.15.4 MAC header):
|
||||
* [0..3] Magic 0x54534D45 ('TSME' — Time Sync MEsh)
|
||||
* [4] Protocol ver 0x01
|
||||
* [5] Leader flag 1 if sender is current leader
|
||||
* [6..7] Reserved
|
||||
* [8..15] Leader epoch µs (LE u64)
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_IEEE802154_ENABLED)
|
||||
|
||||
#include "c6_timesync.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_ieee802154.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/timers.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "c6_ts";
|
||||
|
||||
#define TS_MAGIC 0x54534D45u
|
||||
#define TS_PROTO_VER 0x01
|
||||
#define TS_BEACON_MS 100
|
||||
#define TS_VALID_WINDOW_MS 3000 /* drop to invalid if no beacon in 3 s */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint8_t proto_ver;
|
||||
uint8_t leader_flag;
|
||||
uint16_t _reserved;
|
||||
uint64_t leader_epoch_us;
|
||||
} ts_beacon_t;
|
||||
|
||||
static uint64_t s_local_eui = 0;
|
||||
static uint64_t s_leader_eui = 0; /* 0 = unknown */
|
||||
static int64_t s_offset_us = 0; /* leader_us - local_us */
|
||||
static uint64_t s_last_seen_us = 0;
|
||||
static bool s_is_leader = false;
|
||||
static uint8_t s_channel = 15;
|
||||
static TimerHandle_t s_beacon_timer = NULL;
|
||||
|
||||
/* IEEE EUI-64 from a 6-byte MAC-48: insert 0xFFFE between bytes 2 and 3.
|
||||
* Used only as a fallback when esp_read_mac(..., ESP_MAC_IEEE802154) is
|
||||
* unavailable. The C6's native call returns 8 bytes already in EUI-64
|
||||
* format, so prefer that path (see c6_timesync_init). */
|
||||
static uint64_t mac48_to_eui64(const uint8_t mac[6])
|
||||
{
|
||||
return ((uint64_t)mac[0] << 56) | ((uint64_t)mac[1] << 48) |
|
||||
((uint64_t)mac[2] << 40) | ((uint64_t)0xFF << 32) |
|
||||
((uint64_t)0xFE << 24) | ((uint64_t)mac[3] << 16) |
|
||||
((uint64_t)mac[4] << 8 ) | (uint64_t)mac[5];
|
||||
}
|
||||
|
||||
/* Pack 8 already-EUI-64 bytes into a uint64. */
|
||||
static uint64_t eui64_bytes_to_u64(const uint8_t eui[8])
|
||||
{
|
||||
return ((uint64_t)eui[0] << 56) | ((uint64_t)eui[1] << 48) |
|
||||
((uint64_t)eui[2] << 40) | ((uint64_t)eui[3] << 32) |
|
||||
((uint64_t)eui[4] << 24) | ((uint64_t)eui[5] << 16) |
|
||||
((uint64_t)eui[6] << 8 ) | (uint64_t)eui[7];
|
||||
}
|
||||
|
||||
static uint32_t s_tx_count = 0;
|
||||
static uint32_t s_tx_fail = 0;
|
||||
static uint32_t s_rx_count = 0;
|
||||
static uint32_t s_rx_magic_match = 0;
|
||||
|
||||
static void send_beacon(void)
|
||||
{
|
||||
uint8_t frame[32];
|
||||
/* Minimal 802.15.4 MAC header: FCF + seq + dst PAN + dst short addr. */
|
||||
frame[0] = 0x41; /* FCF lo: data frame, no security, no ack */
|
||||
frame[1] = 0x88; /* FCF hi: short addrs, intra-PAN */
|
||||
frame[2] = 0x00; /* seq number — placeholder */
|
||||
/* Empirically (rx#0 over 60s on all 3 boards), the IDF v5.4 receiver
|
||||
* was rejecting the dst-PAN-broadcast (0xFFFF) frames even in
|
||||
* promiscuous mode. Match our configured PAN ID 0xCAFE here — short
|
||||
* dst stays 0xFFFF for intra-PAN broadcast. PAN bytes are LE. */
|
||||
frame[3] = 0xFE; frame[4] = 0xCA; /* dst PAN = 0xCAFE (matches local) */
|
||||
frame[5] = 0xFF; frame[6] = 0xFF; /* dst short broadcast */
|
||||
frame[7] = 0x00; frame[8] = 0x00; /* src short = 0x0000 */
|
||||
ts_beacon_t *b = (ts_beacon_t *)&frame[9];
|
||||
b->magic = TS_MAGIC;
|
||||
b->proto_ver = TS_PROTO_VER;
|
||||
b->leader_flag = 1;
|
||||
b->_reserved = 0;
|
||||
b->leader_epoch_us = (uint64_t)esp_timer_get_time();
|
||||
size_t total = 9 + sizeof(ts_beacon_t);
|
||||
/* ESP-IDF esp_ieee802154 transmit: first byte is the PHY length. */
|
||||
uint8_t tx_buf[64];
|
||||
tx_buf[0] = (uint8_t)(total + 2); /* +2 for FCS appended by HW */
|
||||
memcpy(&tx_buf[1], frame, total);
|
||||
esp_err_t r = esp_ieee802154_transmit(tx_buf, false);
|
||||
s_tx_count++;
|
||||
if (r != ESP_OK) s_tx_fail++;
|
||||
/* Diag log every 10 beacons. */
|
||||
if ((s_tx_count % 10) == 1) {
|
||||
ESP_LOGI(TAG, "tx#%lu (fail=%lu) rx#%lu (magic_match=%lu) is_leader=%d",
|
||||
(unsigned long)s_tx_count, (unsigned long)s_tx_fail,
|
||||
(unsigned long)s_rx_count, (unsigned long)s_rx_magic_match,
|
||||
(int)s_is_leader);
|
||||
}
|
||||
}
|
||||
|
||||
/* KNOWN ISSUE (see WITNESS-LOG-110 §D1 / task #30):
|
||||
* Empirically observed on 3 C6 boards with channel=26, OpenThread disabled,
|
||||
* promiscuous=true, and IDF v5.4 reference RX/TX callback pattern: only 1
|
||||
* RX event ever fires after init, despite ~381 successful TX events from
|
||||
* the other boards in the same 38-second window. Manual re-arm with
|
||||
* esp_ieee802154_receive() in either callback context bootloops the
|
||||
* driver. Hypothesis: half-duplex radio + driver state-machine issue;
|
||||
* needs an IDF maintainer trace or a working multi-board reference.
|
||||
* Cross-node sync claim (ADR-110 §B3) is BLOCKED on this. */
|
||||
void esp_ieee802154_receive_done(uint8_t *frame, esp_ieee802154_frame_info_t *frame_info)
|
||||
{
|
||||
s_rx_count++;
|
||||
/* PHY length is frame[0]; payload starts at frame[1]. */
|
||||
if (frame == NULL || frame[0] < (9 + sizeof(ts_beacon_t) + 2)) {
|
||||
if (frame) esp_ieee802154_receive_handle_done(frame);
|
||||
return;
|
||||
}
|
||||
const ts_beacon_t *b = (const ts_beacon_t *)&frame[1 + 9];
|
||||
if (b->magic != TS_MAGIC || b->proto_ver != TS_PROTO_VER) {
|
||||
esp_ieee802154_receive_handle_done(frame);
|
||||
return;
|
||||
}
|
||||
s_rx_magic_match++;
|
||||
uint64_t now = (uint64_t)esp_timer_get_time();
|
||||
if (b->leader_flag) {
|
||||
/* Adopt this leader if its EUI is lower than ours (or unknown). */
|
||||
if (s_leader_eui == 0 || b->leader_epoch_us > 0) {
|
||||
s_offset_us = (int64_t)b->leader_epoch_us - (int64_t)now;
|
||||
s_last_seen_us = now;
|
||||
if (s_is_leader) {
|
||||
/* Step down — somebody else is broadcasting; lowest EUI wins
|
||||
* (deferred — for now last-heard wins). */
|
||||
s_is_leader = false;
|
||||
ESP_LOGI(TAG, "stepping down — heard another leader beacon");
|
||||
}
|
||||
}
|
||||
}
|
||||
/* handle_done auto-restarts RX in the IDF driver; calling
|
||||
* esp_ieee802154_receive() here would double-arm and panic
|
||||
* (verified empirically — 25 reboot loops observed). */
|
||||
esp_ieee802154_receive_handle_done(frame);
|
||||
}
|
||||
|
||||
void esp_ieee802154_transmit_done(const uint8_t *frame,
|
||||
const uint8_t *ack,
|
||||
esp_ieee802154_frame_info_t *ack_frame_info)
|
||||
{
|
||||
(void)frame; (void)ack; (void)ack_frame_info;
|
||||
/* Note: do NOT call esp_ieee802154_receive() here — it panics the
|
||||
* driver (verified empirically, all 3 boards bootloop). The IDF
|
||||
* driver internally manages RX/TX state transitions. */
|
||||
}
|
||||
|
||||
void esp_ieee802154_transmit_failed(const uint8_t *frame, esp_ieee802154_tx_error_t error)
|
||||
{
|
||||
(void)frame;
|
||||
ESP_LOGD(TAG, "tx failed: %d", error);
|
||||
}
|
||||
|
||||
static void beacon_timer_cb(TimerHandle_t t)
|
||||
{
|
||||
(void)t;
|
||||
uint64_t now = (uint64_t)esp_timer_get_time();
|
||||
if (s_is_leader) {
|
||||
send_beacon();
|
||||
} else if ((now - s_last_seen_us) > (TS_VALID_WINDOW_MS * 1000ULL)) {
|
||||
/* Lost the leader — promote self if no one else takes over in 1 s. */
|
||||
s_is_leader = true;
|
||||
s_leader_eui = s_local_eui;
|
||||
ESP_LOGI(TAG, "promoting self to time-leader (no beacons for %u ms)",
|
||||
(unsigned)TS_VALID_WINDOW_MS);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t c6_timesync_init(uint8_t channel)
|
||||
{
|
||||
/* esp_mac.h: ESP_MAC_IEEE802154 returns 8 bytes ALREADY in EUI-64 format
|
||||
* (ff:fe is pre-inserted in bytes 3-4 from the eFuse MAC_EXT). Using a
|
||||
* 6-byte buffer here truncates and then double-inserts ff:fe — the bug
|
||||
* we hit on the first run (boot log: EUI=206ef1fffefffe17).
|
||||
*
|
||||
* Correct path: read 8 bytes, pack into uint64 unchanged. Fallback to
|
||||
* the base MAC + manual EUI-64 derivation if the 8-byte read errors. */
|
||||
uint8_t eui_bytes[8] = {0};
|
||||
esp_err_t mac_ret = esp_read_mac(eui_bytes, ESP_MAC_IEEE802154);
|
||||
if (mac_ret == ESP_OK) {
|
||||
s_local_eui = eui64_bytes_to_u64(eui_bytes);
|
||||
} else {
|
||||
uint8_t base_mac[6];
|
||||
esp_read_mac(base_mac, ESP_MAC_BASE);
|
||||
s_local_eui = mac48_to_eui64(base_mac);
|
||||
}
|
||||
/* Use the 6-byte base MAC for the IEEE 802.15.4 extended address — the
|
||||
* radio expects MAC-48-style bytes here, not the EUI-64 derivation. */
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_BASE);
|
||||
s_channel = (channel >= 11 && channel <= 26) ? channel : 15;
|
||||
|
||||
esp_err_t ret = esp_ieee802154_enable();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ieee802154_enable failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
/* promiscuous=true so we accept broadcast frames addressed to 0xFFFF.
|
||||
* In non-promiscuous mode the radio filters to frames addressed to
|
||||
* our short or extended address. Our beacon protocol uses broadcast. */
|
||||
esp_ieee802154_set_promiscuous(true);
|
||||
esp_ieee802154_set_panid(0xCAFE);
|
||||
esp_ieee802154_set_short_address(0x0000);
|
||||
esp_ieee802154_set_extended_address(mac);
|
||||
esp_ieee802154_set_channel(s_channel);
|
||||
esp_ieee802154_receive();
|
||||
|
||||
/* Start as candidate leader; first received beacon will demote us if needed. */
|
||||
s_is_leader = true;
|
||||
s_leader_eui = s_local_eui;
|
||||
s_last_seen_us = (uint64_t)esp_timer_get_time();
|
||||
|
||||
s_beacon_timer = xTimerCreate("c6ts_beacon", pdMS_TO_TICKS(TS_BEACON_MS),
|
||||
pdTRUE, NULL, beacon_timer_cb);
|
||||
if (s_beacon_timer == NULL) {
|
||||
ESP_LOGE(TAG, "xTimerCreate failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
xTimerStart(s_beacon_timer, 0);
|
||||
|
||||
ESP_LOGI(TAG, "init done: channel=%u EUI=%016llx leader=yes(candidate)",
|
||||
(unsigned)s_channel, (unsigned long long)s_local_eui);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint64_t c6_timesync_get_epoch_us(void)
|
||||
{
|
||||
return (uint64_t)((int64_t)esp_timer_get_time() + s_offset_us);
|
||||
}
|
||||
|
||||
bool c6_timesync_is_leader(void) { return s_is_leader; }
|
||||
int64_t c6_timesync_get_offset_us(void) { return s_offset_us; }
|
||||
|
||||
bool c6_timesync_is_valid(void)
|
||||
{
|
||||
if (s_is_leader) return true;
|
||||
uint64_t now = (uint64_t)esp_timer_get_time();
|
||||
return (now - s_last_seen_us) < (TS_VALID_WINDOW_MS * 1000ULL);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_IDF_TARGET_ESP32C6 && CONFIG_IEEE802154_ENABLED */
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @file c6_timesync.h
|
||||
* @brief 802.15.4 mesh time-sync — ADR-110 Phase 4.
|
||||
*
|
||||
* Provides cross-node clock alignment over a separate 802.15.4 radio so
|
||||
* the WiFi airtime stays clean for CSI sensing. Solves the multistatic
|
||||
* synchronization problem (ADR-029/030) without burning the sensing
|
||||
* channel on coordination traffic.
|
||||
*
|
||||
* Protocol (skeleton — full Thread join deferred to a follow-up phase):
|
||||
* - One node is elected time-leader (lowest 64-bit EUI on the mesh).
|
||||
* - Leader broadcasts a TS_BEACON every 100 ms on 802.15.4 channel 15.
|
||||
* - Followers compute offset = leader_us - local_us, apply lazily.
|
||||
* - Each CSI frame is stamped with c6_timesync_get_epoch_us().
|
||||
*
|
||||
* Only built when CONFIG_IDF_TARGET_ESP32C6 + CONFIG_IEEE802154_ENABLED.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_IEEE802154_ENABLED)
|
||||
|
||||
/**
|
||||
* Initialize the 802.15.4 radio and time-sync state machine.
|
||||
* Picks leader or follower role based on EUI comparison.
|
||||
*
|
||||
* @param channel 802.15.4 channel (11-26, default 15).
|
||||
* @return ESP_OK on success.
|
||||
*/
|
||||
esp_err_t c6_timesync_init(uint8_t channel);
|
||||
|
||||
/**
|
||||
* Returns the synced wall-clock estimate in microseconds.
|
||||
* If no leader heard within the timeout, returns the local
|
||||
* esp_timer_get_time() value unchanged (offset = 0).
|
||||
*/
|
||||
uint64_t c6_timesync_get_epoch_us(void);
|
||||
|
||||
/**
|
||||
* Returns true if this node is currently the time-leader.
|
||||
*/
|
||||
bool c6_timesync_is_leader(void);
|
||||
|
||||
/**
|
||||
* Returns true if the local clock is synced (heard a beacon within timeout).
|
||||
*/
|
||||
bool c6_timesync_is_valid(void);
|
||||
|
||||
/**
|
||||
* Returns the most-recently-measured offset from the leader (microseconds).
|
||||
* 0 if this node is the leader; sign indicates direction.
|
||||
*/
|
||||
int64_t c6_timesync_get_offset_us(void);
|
||||
|
||||
#else /* not C6 with 802.15.4 — provide stubs so call sites compile */
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
static inline esp_err_t c6_timesync_init(uint8_t c) { (void)c; return ESP_OK; }
|
||||
static inline uint64_t c6_timesync_get_epoch_us(void) { return (uint64_t)esp_timer_get_time(); }
|
||||
static inline bool c6_timesync_is_leader(void) { return false; }
|
||||
static inline bool c6_timesync_is_valid(void) { return false; }
|
||||
static inline int64_t c6_timesync_get_offset_us(void) { return 0; }
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @file c6_twt.c
|
||||
* @brief ESP32-C6 TWT setup implementation — ADR-110 Phase 3.
|
||||
*
|
||||
* Implementation note: ESP-IDF v5.4's iTWT API on C6 is
|
||||
*
|
||||
* esp_err_t esp_wifi_sta_itwt_setup(wifi_itwt_setup_config_t *cfg);
|
||||
* esp_err_t esp_wifi_sta_itwt_teardown(uint8_t flow_id);
|
||||
*
|
||||
* The setup is asynchronous — the actual accept/reject arrives later as
|
||||
* a WIFI_EVENT_ITWT_SETUP event. The default handler in this module
|
||||
* logs the outcome; the helper itself returns as soon as the request
|
||||
* is queued.
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && SOC_WIFI_HE_SUPPORT
|
||||
|
||||
#include "c6_twt.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_wifi_he.h" /* esp_wifi_sta_itwt_setup / _teardown */
|
||||
#include "esp_wifi_he_types.h"
|
||||
#include "esp_wifi_types.h"
|
||||
#include "esp_event.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "c6_twt";
|
||||
|
||||
static bool s_active = false;
|
||||
static uint8_t s_flow_id = 0;
|
||||
static uint32_t s_wake_int = 0;
|
||||
static uint32_t s_wake_dura = 0;
|
||||
|
||||
#ifndef CONFIG_C6_TWT_WAKE_INTERVAL_US
|
||||
#define CONFIG_C6_TWT_WAKE_INTERVAL_US 10000 /* 100 fps default cadence */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_C6_TWT_MIN_WAKE_DURA_US
|
||||
#define CONFIG_C6_TWT_MIN_WAKE_DURA_US 512 /* enough to capture 1 CSI frame */
|
||||
#endif
|
||||
|
||||
/* WIFI_EVENT_ITWT_SETUP handler — logs accept/reject. */
|
||||
static void on_itwt_event(void *arg, esp_event_base_t base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
(void)arg;
|
||||
(void)base;
|
||||
(void)event_data;
|
||||
switch (event_id) {
|
||||
case WIFI_EVENT_ITWT_SETUP:
|
||||
ESP_LOGI(TAG, "iTWT setup event received from AP (flow_id captured)");
|
||||
s_active = true;
|
||||
break;
|
||||
case WIFI_EVENT_ITWT_TEARDOWN:
|
||||
ESP_LOGI(TAG, "iTWT teardown event received");
|
||||
s_active = false;
|
||||
break;
|
||||
case WIFI_EVENT_ITWT_SUSPEND:
|
||||
ESP_LOGI(TAG, "iTWT suspended by AP");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool s_handler_installed = false;
|
||||
|
||||
static void install_event_handler_once(void)
|
||||
{
|
||||
if (s_handler_installed) return;
|
||||
esp_err_t e = esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, on_itwt_event, NULL, NULL);
|
||||
if (e == ESP_OK) {
|
||||
s_handler_installed = true;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not install iTWT event handler: %s",
|
||||
esp_err_to_name(e));
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t c6_twt_setup(uint32_t wake_interval_us, uint32_t min_wake_dura_us)
|
||||
{
|
||||
install_event_handler_once();
|
||||
|
||||
s_wake_int = wake_interval_us;
|
||||
s_wake_dura = min_wake_dura_us < 256 ? 256 : min_wake_dura_us;
|
||||
|
||||
wifi_itwt_setup_config_t cfg = {0};
|
||||
cfg.setup_cmd = TWT_REQUEST;
|
||||
cfg.flow_id = s_flow_id;
|
||||
cfg.twt_id = 0;
|
||||
cfg.flow_type = 1; /* unannounced */
|
||||
cfg.min_wake_dura = (uint8_t)((s_wake_dura + 255) / 256); /* 256 µs units */
|
||||
cfg.wake_duration_unit = 0; /* 0 = 256 µs, 1 = 1024 µs */
|
||||
cfg.wake_invl_expn = 10; /* mantissa * 2^10 ≈ 1024 µs base */
|
||||
/* mantissa = wake_interval_us / 1024, clamped to uint16 */
|
||||
uint32_t mant = wake_interval_us >> 10;
|
||||
if (mant == 0) mant = 1;
|
||||
if (mant > 0xFFFF) mant = 0xFFFF;
|
||||
cfg.wake_invl_mant = (uint16_t)mant;
|
||||
cfg.trigger = 0; /* non-triggered: STA wakes on its own */
|
||||
|
||||
esp_err_t ret = esp_wifi_sta_itwt_setup(&cfg);
|
||||
if (ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "iTWT setup queued: wake_interval=%lu µs (mant=%u expn=10), "
|
||||
"min_wake_dura=%u (%lu µs)",
|
||||
(unsigned long)wake_interval_us, (unsigned)mant,
|
||||
cfg.min_wake_dura, (unsigned long)s_wake_dura);
|
||||
return ESP_OK;
|
||||
}
|
||||
/* Treat AP-rejection / not-supported / wrong-AP-mode as graceful — log
|
||||
* and continue. ESP_ERR_INVALID_ARG is included here because empirically
|
||||
* (live capture on ruv.net 2026-05-22) the ESP-IDF v5.4 driver returns
|
||||
* INVALID_ARG when the associated AP advertises TWT Responder=0 — the
|
||||
* call validates against the AP's HE capability bitmap, not just the
|
||||
* struct fields. */
|
||||
if (ret == ESP_ERR_NOT_SUPPORTED || ret == ESP_ERR_WIFI_NOT_CONNECT ||
|
||||
ret == ESP_ERR_INVALID_STATE || ret == ESP_ERR_INVALID_ARG) {
|
||||
ESP_LOGW(TAG, "iTWT not available (%s) - AP likely not 11ax/iTWT capable,"
|
||||
" falling back to opportunistic CSI",
|
||||
esp_err_to_name(ret));
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGE(TAG, "iTWT setup failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t c6_twt_setup_default(void)
|
||||
{
|
||||
return c6_twt_setup(CONFIG_C6_TWT_WAKE_INTERVAL_US,
|
||||
CONFIG_C6_TWT_MIN_WAKE_DURA_US);
|
||||
}
|
||||
|
||||
void c6_twt_teardown(void)
|
||||
{
|
||||
if (!s_active) return;
|
||||
/* IDF v5.4 signature: esp_err_t esp_wifi_sta_itwt_teardown(int flow_id) */
|
||||
esp_err_t ret = esp_wifi_sta_itwt_teardown((int)s_flow_id);
|
||||
if (ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "iTWT teardown sent (flow_id=%u)", s_flow_id);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "iTWT teardown failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
s_active = false;
|
||||
}
|
||||
|
||||
bool c6_twt_is_active(void)
|
||||
{
|
||||
return s_active;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_IDF_TARGET_ESP32C6 && SOC_WIFI_HE_SUPPORT */
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @file c6_twt.h
|
||||
* @brief ESP32-C6 TWT (Target Wake Time) helper — ADR-110 Phase 3.
|
||||
*
|
||||
* Wraps esp_wifi_sta_itwt_setup() to negotiate a deterministic wake slot
|
||||
* with the AP, replacing today's opportunistic CSI capture cadence with
|
||||
* a scheduler-bounded one.
|
||||
*
|
||||
* Only built when CONFIG_IDF_TARGET_ESP32C6 is set — the S3 radio is
|
||||
* 802.11n only and cannot speak iTWT.
|
||||
*
|
||||
* Usage from main.c (after WiFi STA is connected):
|
||||
* c6_twt_setup_default(); // honors CONFIG_C6_TWT_WAKE_INTERVAL_US
|
||||
*
|
||||
* Graceful failure: if the AP rejects (no 11ax support, doesn't allow
|
||||
* iTWT, or returns a NACK), the helper logs and returns ESP_OK — the
|
||||
* device keeps doing opportunistic CSI just like the S3.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && SOC_WIFI_HE_SUPPORT
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Set up an individual TWT agreement using the Kconfig defaults
|
||||
* (CONFIG_C6_TWT_WAKE_INTERVAL_US, CONFIG_C6_TWT_MIN_WAKE_DURA_US).
|
||||
*
|
||||
* @return ESP_OK whether or not the AP accepted — the helper never
|
||||
* propagates a TWT NACK as an error to the caller.
|
||||
*/
|
||||
esp_err_t c6_twt_setup_default(void);
|
||||
|
||||
/**
|
||||
* Set up an individual TWT agreement with explicit parameters.
|
||||
*
|
||||
* @param wake_interval_us Period between wake events.
|
||||
* @param min_wake_dura_us Minimum awake duration per wake (≥256 µs).
|
||||
* @return ESP_OK on success or graceful NACK; ESP_FAIL on local error.
|
||||
*/
|
||||
esp_err_t c6_twt_setup(uint32_t wake_interval_us, uint32_t min_wake_dura_us);
|
||||
|
||||
/**
|
||||
* Tear down any active TWT agreement. Safe to call when none is active.
|
||||
* Should be invoked on WIFI_EVENT_STA_DISCONNECTED so the AP scheduler
|
||||
* doesn't keep a dead slot reserved.
|
||||
*/
|
||||
void c6_twt_teardown(void);
|
||||
|
||||
/**
|
||||
* Returns true if a TWT agreement is currently active.
|
||||
*/
|
||||
bool c6_twt_is_active(void);
|
||||
|
||||
#else /* not C6 with iTWT support — provide stubs so call sites compile */
|
||||
|
||||
static inline esp_err_t c6_twt_setup_default(void) { return ESP_OK; }
|
||||
static inline esp_err_t c6_twt_setup(uint32_t a, uint32_t b) { (void)a; (void)b; return ESP_OK; }
|
||||
static inline void c6_twt_teardown(void) { }
|
||||
static inline bool c6_twt_is_active(void) { return false; }
|
||||
|
||||
#endif /* CONFIG_IDF_TARGET_ESP32C6 && SOC_WIFI_HE_SUPPORT */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "nvs_config.h"
|
||||
#include "stream_sender.h"
|
||||
#include "edge_processing.h"
|
||||
#include "c6_timesync.h" /* ADR-110: 802.15.4 epoch for cross-node alignment */
|
||||
#include "c6_sync_espnow.h" /* ADR-110 §A0.11: mesh-aligned epoch for sync packet */
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
@@ -173,9 +175,64 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf
|
||||
/* Noise floor (i8) */
|
||||
buf[17] = (uint8_t)(int8_t)info->rx_ctrl.noise_floor;
|
||||
|
||||
/* Reserved */
|
||||
/* ADR-110: PPDU type (byte 18) + bandwidth/flags (byte 19).
|
||||
* Previously reserved-zero, now optionally populated when CONFIG_CSI_FRAME_HE_TAGGING.
|
||||
* Readers that don't know about the extension see zeros — backward compatible.
|
||||
*
|
||||
* The struct that backs info->rx_ctrl is target-conditional in IDF v5.4
|
||||
* (esp_wifi/include/local/esp_wifi_types_native.h):
|
||||
*
|
||||
* CONFIG_SOC_WIFI_HE_SUPPORT=y (C6/C5) → esp_wifi_rxctrl_t with cur_bb_format, second
|
||||
* otherwise (S3 etc) → legacy struct with sig_mode, cwb, stbc
|
||||
*
|
||||
* Byte-18 PPDU type encoding stays the same across targets:
|
||||
* 0=HT/legacy bucket, 1=HE-SU, 2=HE-MU, 3=HE-TB, 0xFF=unknown
|
||||
*/
|
||||
#ifdef CONFIG_CSI_FRAME_HE_TAGGING
|
||||
uint8_t ppdu_type = 0xFF;
|
||||
uint8_t flags = 0;
|
||||
#if CONFIG_SOC_WIFI_HE_SUPPORT
|
||||
/* HE-capable chips: read cur_bb_format (0=11b, 1=11g, 2=HT, 3=VHT, 4=HE-SU,
|
||||
* 5=HE-MU, 6=HE-ERSU, 7=HE-TB) and 'second' (40 MHz secondary chan offset). */
|
||||
switch (info->rx_ctrl.cur_bb_format) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2: ppdu_type = 0; break; /* 11b/g/a/HT bucket */
|
||||
case 3: ppdu_type = 0; break; /* VHT — rare on 2.4 GHz, HT bucket */
|
||||
case 4: ppdu_type = 1; break; /* HE-SU */
|
||||
case 5: ppdu_type = 2; break; /* HE-MU */
|
||||
case 6: ppdu_type = 1; break; /* HE-ER-SU collapses to HE-SU */
|
||||
case 7: ppdu_type = 3; break; /* HE-TB */
|
||||
default: ppdu_type = 0xFF; break;
|
||||
}
|
||||
if (info->rx_ctrl.second != 0) flags |= 0x1; /* bw 40 MHz */
|
||||
#else
|
||||
/* Pre-HE chips (S3 etc): use legacy sig_mode + cwb + stbc fields. */
|
||||
switch (info->rx_ctrl.sig_mode) {
|
||||
case 0: ppdu_type = 0; break; /* non-HT (11b/g) */
|
||||
case 1: ppdu_type = 0; break; /* HT (11n) */
|
||||
case 3: ppdu_type = 0; break; /* VHT — bucket as HT for storage */
|
||||
default: ppdu_type = 0xFF; break;
|
||||
}
|
||||
if (info->rx_ctrl.cwb) flags |= 0x1; /* bw 40 MHz */
|
||||
if (info->rx_ctrl.stbc) flags |= (1 << 2); /* STBC */
|
||||
#endif /* CONFIG_SOC_WIFI_HE_SUPPORT */
|
||||
/* ADR-018 byte 19 bit 4 = "cross-node sync valid". Two transports can
|
||||
* set it: the original 802.15.4 c6_timesync (broken in IDF v5.4 — D1)
|
||||
* and the ESP-NOW workaround c6_sync_espnow (measured working in §A0.7-
|
||||
* §A0.10). OR them together so frames signal sync from whichever
|
||||
* transport is alive on this node. Host can pair against the sync
|
||||
* packet (§A0.12) once it sees this bit. */
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_TIMESYNC_ENABLE)
|
||||
if (c6_timesync_is_valid()) flags |= (1 << 4); /* 15.4 sync valid */
|
||||
#endif
|
||||
if (c6_sync_espnow_is_valid()) flags |= (1 << 4); /* ESP-NOW sync valid (D1 workaround) */
|
||||
buf[18] = ppdu_type;
|
||||
buf[19] = flags;
|
||||
#else
|
||||
buf[18] = 0;
|
||||
buf[19] = 0;
|
||||
#endif
|
||||
|
||||
/* I/Q data */
|
||||
memcpy(&buf[CSI_HEADER_SIZE], info->buf, iq_len);
|
||||
@@ -245,6 +302,56 @@ static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info)
|
||||
edge_enqueue_csi((const uint8_t *)info->buf, (uint16_t)info->len,
|
||||
(int8_t)info->rx_ctrl.rssi, info->rx_ctrl.channel);
|
||||
}
|
||||
|
||||
/* ADR-110 §A0.11/§A0.12 — Emit a sync-packet every N CSI frames so the
|
||||
* host aggregator can pair node-local sequence numbers with the mesh-aligned
|
||||
* epoch coming out of c6_sync_espnow_get_epoch_us(). Backwards-compatible
|
||||
* with the ADR-018 frame format: new packet uses a distinct magic so the
|
||||
* existing CSI parser can dispatch by first 4 bytes.
|
||||
*
|
||||
* Cadence is operator-tunable via CONFIG_C6_SYNC_EVERY_N_FRAMES (default 20).
|
||||
* At 10 Hz observed CSI rate that's ~2 s between sync packets; raise to 50
|
||||
* for ~5 s (less overhead, slower convergence), lower to 5 for ~0.5 s
|
||||
* (heavier wire, tighter ADR-029/030 multistatic alignment window). */
|
||||
{
|
||||
#ifndef CONFIG_C6_SYNC_EVERY_N_FRAMES
|
||||
#define CONFIG_C6_SYNC_EVERY_N_FRAMES 20
|
||||
#endif
|
||||
if ((s_cb_count % CONFIG_C6_SYNC_EVERY_N_FRAMES) == 0) {
|
||||
uint8_t sync[32];
|
||||
uint32_t sync_magic = 0xC511A110u; /* CSI-ADR-110 sync packet */
|
||||
uint64_t local_us = (uint64_t)esp_timer_get_time();
|
||||
uint64_t epoch_us = c6_sync_espnow_get_epoch_us();
|
||||
int64_t off_smooth = c6_sync_espnow_get_offset_us_smoothed();
|
||||
uint8_t flags = 0;
|
||||
if (c6_sync_espnow_is_leader()) flags |= 0x01;
|
||||
if (c6_sync_espnow_is_valid()) flags |= 0x02;
|
||||
if (off_smooth != 0) flags |= 0x04;
|
||||
|
||||
memcpy(&sync[0], &sync_magic, 4);
|
||||
sync[4] = s_node_id;
|
||||
sync[5] = 0x01; /* protocol version */
|
||||
sync[6] = flags;
|
||||
sync[7] = 0; /* reserved */
|
||||
memcpy(&sync[8], &local_us, 8);
|
||||
memcpy(&sync[16], &epoch_us, 8);
|
||||
memcpy(&sync[24], &s_sequence, 4); /* high-water seq for pairing */
|
||||
uint32_t zero32 = 0;
|
||||
memcpy(&sync[28], &zero32, 4); /* reserved (room for leader_id low32) */
|
||||
int sr = stream_sender_send(sync, sizeof(sync));
|
||||
static uint32_t s_sync_count = 0;
|
||||
s_sync_count++;
|
||||
if (s_sync_count <= 3 || (s_sync_count % 60) == 0) {
|
||||
ESP_LOGI(TAG, "sync-pkt #%lu (sr=%d) node=%u flags=0x%02x "
|
||||
"local_us=%llu epoch_us=%llu seq=%lu",
|
||||
(unsigned long)s_sync_count, sr,
|
||||
(unsigned)s_node_id, (unsigned)flags,
|
||||
(unsigned long long)local_us,
|
||||
(unsigned long long)epoch_us,
|
||||
(unsigned long)s_sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# LP-core motion-gate program — ADR-110 Phase 5 (full).
|
||||
#
|
||||
# Built only when CONFIG_C6_LP_CORE_ENABLE=y (gated in the parent CMakeLists).
|
||||
# The IDF build system invokes this via `ulp_embed_binary()` from
|
||||
# main/CMakeLists.txt.
|
||||
|
||||
# This file intentionally has no idf_component_register — the LP-core sources
|
||||
# are compiled with the RISC-V LP toolchain via `ulp_embed_binary` and then
|
||||
# linked into the HP image as a binary blob, not as a normal component.
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @file lp_core/main.c
|
||||
* @brief LP RISC-V coprocessor motion-gate — ADR-110 Phase 5 (full).
|
||||
*
|
||||
* Polls a single LP-IO GPIO at LP_TIMER cadence (default 10 ms / 100 Hz),
|
||||
* debounces N consecutive samples, and wakes the HP core when a confirmed
|
||||
* transition matches the configured active-edge polarity. Counter +
|
||||
* last-level are exported as shared symbols so the HP side can inspect
|
||||
* them on wake.
|
||||
*
|
||||
* Shared symbols (HP-visible as `ulp_<name>` after `ulp_embed_binary`):
|
||||
* - wake_gpio_num (input) : LP-IO index 0..7 on ESP32-C6
|
||||
* - wake_active_high (input) : 1 = wake on rising stable, 0 = falling
|
||||
* - debounce_samples (input) : consecutive matches required, default 3
|
||||
* - motion_count (output) : monotonic wake-trigger counter
|
||||
* - last_gpio_level (output) : level latched at the most recent wake
|
||||
* - poll_count (output) : total LP-timer ticks observed (sanity)
|
||||
*
|
||||
* Defaults are written by HP via the `ulp_*` symbols before `ulp_lp_core_run()`,
|
||||
* so the program is parameterised at boot without recompiling the LP binary.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "ulp_lp_core.h"
|
||||
#include "ulp_lp_core_utils.h"
|
||||
#include "ulp_lp_core_gpio.h"
|
||||
|
||||
/* --- Shared (HP/LP) state --- */
|
||||
volatile uint32_t wake_gpio_num = 4; /* LP-IO 4 by default */
|
||||
volatile uint32_t wake_active_high = 1; /* rising edge */
|
||||
volatile uint32_t debounce_samples = 3;
|
||||
volatile uint32_t motion_count = 0;
|
||||
volatile uint32_t last_gpio_level = 0;
|
||||
volatile uint32_t poll_count = 0;
|
||||
|
||||
/* --- Local state (persists across LP-timer wake cycles via .data) --- */
|
||||
static uint32_t stable_run = 0;
|
||||
static uint32_t prev_level = 0;
|
||||
|
||||
int main(void)
|
||||
{
|
||||
poll_count++;
|
||||
|
||||
/* LP-IO read returns 0/1 directly. The Kconfig-selected GPIO index maps
|
||||
* 1:1 to LP_IO on C6 for indices 0..7. */
|
||||
uint32_t level = (uint32_t)ulp_lp_core_gpio_get_level((lp_io_num_t)wake_gpio_num);
|
||||
|
||||
if (level == prev_level) {
|
||||
if (stable_run < 0xFFFFu) stable_run++;
|
||||
} else {
|
||||
stable_run = 1;
|
||||
prev_level = level;
|
||||
}
|
||||
|
||||
/* Trigger when level matches the configured active polarity AND has been
|
||||
* stable for `debounce_samples` consecutive reads. After firing, hold off
|
||||
* until level returns to the inactive state to avoid re-triggering on
|
||||
* the same continuous edge. */
|
||||
static uint32_t armed = 1;
|
||||
uint32_t want = wake_active_high ? 1 : 0;
|
||||
|
||||
if (armed && level == want && stable_run >= debounce_samples) {
|
||||
motion_count++;
|
||||
last_gpio_level = level;
|
||||
armed = 0;
|
||||
ulp_lp_core_wakeup_main_processor();
|
||||
} else if (!armed && level != want && stable_run >= debounce_samples) {
|
||||
/* Re-arm once the line has cleanly returned to the inactive state. */
|
||||
armed = 1;
|
||||
}
|
||||
|
||||
/* ulp_lp_core_halt() is called automatically when main returns. */
|
||||
return 0;
|
||||
}
|
||||
@@ -33,6 +33,11 @@
|
||||
#include "swarm_bridge.h"
|
||||
#include "rv_radio_ops.h" /* ADR-081 Layer 1 — Radio Abstraction Layer. */
|
||||
#include "adaptive_controller.h" /* ADR-081 Layer 2 — Adaptive controller. */
|
||||
#include "c6_twt.h" /* ADR-110: TWT (no-op stub on S3) */
|
||||
#include "c6_timesync.h" /* ADR-110: 802.15.4 mesh time-sync (no-op on S3) */
|
||||
#include "c6_lp_core.h" /* ADR-110: LP-core hibernation (no-op on S3) */
|
||||
#include "c6_sync_espnow.h" /* ADR-110 D1 workaround: ESP-NOW sync */
|
||||
#include "c6_softap_he.h" /* ADR-110 B1/B2: HE/TWT soft-AP (no-op when disabled) */
|
||||
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||
#include "mock_csi.h"
|
||||
#endif
|
||||
@@ -112,6 +117,17 @@ static void wifi_init_sta(void)
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_SOFTAP_HE_ENABLE)
|
||||
/* ADR-110 B1/B2 cheap-unblock: bring up a soft-AP that advertises HE +
|
||||
* TWT Responder=1 so a second C6 board can negotiate iTWT against
|
||||
* this node. c6_softap_he_start() switches the mode to AP+STA. */
|
||||
uint8_t softap_chan = 0;
|
||||
if (c6_softap_he_start(&softap_chan) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "C6 soft-AP HE armed on channel %u (ADR-110 B1/B2)", softap_chan);
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "WiFi STA initialized, connecting to SSID: %s", g_nvs_config.wifi_ssid);
|
||||
@@ -147,13 +163,27 @@ void app_main(void)
|
||||
csi_collector_set_node_id(g_nvs_config.node_id);
|
||||
|
||||
const esp_app_desc_t *app_desc = esp_app_get_description();
|
||||
ESP_LOGI(TAG, "ESP32-S3 CSI Node (ADR-018) — v%s — Node ID: %d",
|
||||
app_desc->version, g_nvs_config.node_id);
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
const char *target_name = "ESP32-C6";
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
const char *target_name = "ESP32-S3";
|
||||
#else
|
||||
const char *target_name = "ESP32";
|
||||
#endif
|
||||
ESP_LOGI(TAG, "%s CSI Node (ADR-018 / ADR-110) — v%s — Node ID: %d",
|
||||
target_name, app_desc->version, g_nvs_config.node_id);
|
||||
|
||||
/* Turn off onboard WS2812 LED on GPIO 38 */
|
||||
/* Turn off onboard WS2812 LED.
|
||||
* S3 dev boards put the LED on GPIO 38; C6 dev boards on GPIO 8.
|
||||
* On C6, GPIO 38 doesn't exist (only 0-30) — gate the init by target. */
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
const int led_gpio = 8;
|
||||
#else
|
||||
const int led_gpio = 38;
|
||||
#endif
|
||||
led_strip_handle_t led_strip;
|
||||
led_strip_config_t strip_config = {
|
||||
.strip_gpio_num = 38,
|
||||
.strip_gpio_num = led_gpio,
|
||||
.max_leds = 1,
|
||||
.led_model = LED_MODEL_WS2812,
|
||||
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
|
||||
@@ -167,6 +197,27 @@ void app_main(void)
|
||||
led_strip_clear(led_strip);
|
||||
}
|
||||
|
||||
/* ADR-110 P4: 802.15.4 mesh time-sync (C6 only).
|
||||
* Initialized BEFORE WiFi so it's available even when WiFi STA can't
|
||||
* connect — the radios are physically independent on the C6.
|
||||
* No-op on S3 (the helper compiles to an empty inline stub). */
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_TIMESYNC_ENABLE)
|
||||
esp_err_t ts_ret = c6_timesync_init(CONFIG_C6_TIMESYNC_CHANNEL);
|
||||
if (ts_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "c6_timesync_init failed: %s (continuing without 15.4 sync)",
|
||||
esp_err_to_name(ts_ret));
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ADR-110 P5: Optionally arm LP-core wake-on-motion (C6 only, opt-in).
|
||||
* Default off — only nodes flashed for battery-powered seed duty enable
|
||||
* this in menuconfig. */
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_LP_CORE_ENABLE)
|
||||
if (c6_lp_core_was_motion_wake()) {
|
||||
ESP_LOGI(TAG, "boot cause: LP-core motion wake (running CSI burst)");
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Initialize WiFi STA (skip entirely under QEMU mock — no RF hardware) */
|
||||
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||
wifi_init_sta();
|
||||
@@ -208,6 +259,26 @@ void app_main(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ADR-110 P3: Request TWT from the AP for deterministic CSI cadence.
|
||||
* No-op on S3 (the helper compiles to an empty inline stub). On C6
|
||||
* the AP may NACK — the helper logs and falls back to opportunistic.
|
||||
* Called only after WiFi STA connect (wifi_init_sta blocks until then). */
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_TWT_ENABLE)
|
||||
c6_twt_setup_default();
|
||||
#endif
|
||||
|
||||
/* ADR-110 D1 workaround: ESP-NOW cross-node sync. Initialized after
|
||||
* WiFi STA connects (ESP-NOW needs the WiFi driver up). Works on
|
||||
* both S3 and C6 — replaces the broken 802.15.4 RX path in c6_timesync.
|
||||
* Skip on QEMU mock (no real WiFi → no ESP-NOW). */
|
||||
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||
esp_err_t espnow_ret = c6_sync_espnow_init();
|
||||
if (espnow_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "c6_sync_espnow_init failed: %s (continuing without ESP-NOW sync)",
|
||||
esp_err_to_name(espnow_ret));
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ADR-039: Initialize edge processing pipeline. */
|
||||
edge_config_t edge_cfg = {
|
||||
.tier = g_nvs_config.edge_tier,
|
||||
|
||||
@@ -230,9 +230,13 @@ static void swarm_task(void *arg)
|
||||
ESP_LOGI(TAG, "Bearer token configured for Seed auth");
|
||||
}
|
||||
|
||||
/* Get firmware version string. */
|
||||
/* Firmware version + IP captured locally so logs name the build; both
|
||||
* intentionally unused in the JSON payloads — the seed extracts them
|
||||
* from the register/heartbeat IDs. Keep as side-effect probes. */
|
||||
const esp_app_desc_t *app = esp_app_get_description();
|
||||
const char *fw_ver = app ? app->version : "unknown";
|
||||
if (app) {
|
||||
ESP_LOGI(TAG, "swarm bridge fw=%s", app->version);
|
||||
}
|
||||
|
||||
/* Get local IP. */
|
||||
char ip_str[16];
|
||||
@@ -278,15 +282,12 @@ static void swarm_task(void *arg)
|
||||
xSemaphoreGive(s_mutex);
|
||||
|
||||
uint32_t uptime_s = (uint32_t)(esp_timer_get_time() / 1000000ULL);
|
||||
uint32_t free_heap = esp_get_free_heap_size();
|
||||
uint32_t ts = (uint32_t)(esp_timer_get_time() / 1000ULL);
|
||||
|
||||
/* ---- Heartbeat ---- */
|
||||
if ((now - last_heartbeat) >= pdMS_TO_TICKS(s_cfg.heartbeat_sec * 1000U)) {
|
||||
last_heartbeat = now;
|
||||
|
||||
bool presence = vit_valid && (vit.flags & 0x01);
|
||||
|
||||
/* Heartbeat ID: node_id * 1000000 + 100000 + ts_sec */
|
||||
uint32_t hb_id = (uint32_t)s_node_id * 1000000U + 100000U + (uptime_s % 100000U);
|
||||
char json[SWARM_JSON_BUF];
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
889715e9d698ad78f9978ad8b93b6af24a726b0494247201c8f0d920d9fc80ca *firmware/esp32-csi-node/release_bins/c6-adr110/bootloader.bin
|
||||
d8539e47c6f10a3344679118619e3fe01cfd66eb560ea8883268ca7c9a12efa4 *firmware/esp32-csi-node/release_bins/c6-adr110/esp32-csi-node.bin
|
||||
7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f *firmware/esp32-csi-node/release_bins/c6-adr110/ota_data_initial.bin
|
||||
4c2cc4ffd52641e23b779bd57b3908014083ac3c1aab395756478c89e70d81f0 *firmware/esp32-csi-node/release_bins/c6-adr110/partition-table.bin
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
3c4905dd202ccabf4230cbabcc9320f250a60b1a7254eff7424780201bcb2072 *firmware/esp32-csi-node/release_bins/s3-adr110/bootloader.bin
|
||||
7a8bf9582c9031fed32f1ada44f5c41dd99bd07fadff8e5c86e07aa0f343e847 *firmware/esp32-csi-node/release_bins/s3-adr110/esp32-csi-node.bin
|
||||
67222c257c0477501fd4002275638dc4262b34eb68235b8289fb1337054d322b *firmware/esp32-csi-node/release_bins/s3-adr110/partition-table.bin
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
a53b2c018bfd2e367525bedf6dc3fda6bc9639d1a9cc9e8bf9eb3e9fee379ed2 *firmware/esp32-csi-node/release_bins/s3-fair-adr110/bootloader.bin
|
||||
53eb50ea890a8388b8a39285a3dd34c53651535c689a3b42f136a5ed7f424145 *firmware/esp32-csi-node/release_bins/s3-fair-adr110/esp32-csi-node.bin
|
||||
4c2cc4ffd52641e23b779bd57b3908014083ac3c1aab395756478c89e70d81f0 *firmware/esp32-csi-node/release_bins/s3-fair-adr110/partition-table.bin
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
# ESP32-C6 CSI Node — Target overlay (ADR-110)
|
||||
#
|
||||
# Auto-applied by ESP-IDF when CONFIG_IDF_TARGET=esp32c6.
|
||||
# Layered on top of sdkconfig.defaults — only the differences live here.
|
||||
#
|
||||
# Build:
|
||||
# idf.py set-target esp32c6
|
||||
# idf.py build
|
||||
#
|
||||
# Hardware: stock ESP32-C6 dev board with 4 MB or 8 MB embedded flash.
|
||||
# Confirmed on COM6: ESP32-C6 (QFN40) rev v0.2, 8 MB flash, 320 KiB SRAM.
|
||||
|
||||
# ── Target ──
|
||||
CONFIG_IDF_TARGET="esp32c6"
|
||||
|
||||
# ── Flash & partitions (4 MB — common across C6 dev boards) ──
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
|
||||
|
||||
# ── CSI (required) ──
|
||||
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
||||
|
||||
# ── ADR-110 P2 & P3: Wi-Fi 6 / iTWT ──
|
||||
# IDF v5.4 exposes neither ESP_WIFI_11AX_SUPPORT nor ESP_WIFI_ITWT_SUPPORT as
|
||||
# user Kconfig — they're SoC capabilities (SOC_WIFI_HE_SUPPORT) auto-enabled
|
||||
# on chips that have HE support (C6/C5). WPA3 is opt-in:
|
||||
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
|
||||
|
||||
# ── ADR-110 P4: 802.15.4 (raw, no OpenThread) ──
|
||||
# IEEE 802.15.4 PHY enabled for our raw beacon protocol in c6_timesync.c.
|
||||
# OpenThread is DISABLED — empirically (ch15 + ch26 tested with the same
|
||||
# negative result), enabling OpenThread MTD caused our weak-symbol overrides
|
||||
# of esp_ieee802154_receive_done/transmit_done to never fire, breaking
|
||||
# leader election. Raw 802.15.4 mode is what we actually need: a private
|
||||
# mesh protocol on a private channel, no Thread network attach.
|
||||
CONFIG_IEEE802154_ENABLED=y
|
||||
CONFIG_OPENTHREAD_ENABLED=n
|
||||
|
||||
# ADR-110 P4: 802.15.4 channel override.
|
||||
# Default Kconfig value is 15 (2425 MHz). On the 2.4 GHz radio that's
|
||||
# directly under WiFi channel 5 (2432 MHz). Channel 26 = 2480 MHz is on
|
||||
# the WiFi guard band above channel 14, giving the 15.4 path room to RX
|
||||
# without competing with WiFi traffic for radio time.
|
||||
CONFIG_C6_TIMESYNC_CHANNEL=26
|
||||
|
||||
# ── ADR-110 P5: LP-core (deep-sleep coprocessor) ──
|
||||
# Enable the LP RISC-V core so c6_lp_core.c can ship a wake-on-motion stub.
|
||||
CONFIG_ULP_COPROC_ENABLED=y
|
||||
CONFIG_ULP_COPROC_TYPE_LP_CORE=y
|
||||
CONFIG_ULP_COPROC_RESERVE_MEM=8192
|
||||
|
||||
# ── No display, no WASM, no mmWave on the C6 research target ──
|
||||
# Display (ADR-045) needs 8 MB + native USB-OTG framebuffer hooks.
|
||||
# WASM3 (ADR-040) needs PSRAM for hot-loadable modules.
|
||||
# mmWave (Seeed MR60BHA2 on COM4) is a separate board.
|
||||
# CONFIG_DISPLAY_ENABLE is not set
|
||||
# CONFIG_WASM_ENABLE is not set
|
||||
|
||||
# ── Compiler ──
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
|
||||
# ── Logging ──
|
||||
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
|
||||
# ── lwIP / FreeRTOS — same as S3 path ──
|
||||
CONFIG_LWIP_SO_RCVBUF=y
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=8192
|
||||
|
||||
# ── Power: keep CPU at max 160 MHz (C6 ceiling) for DSP throughput ──
|
||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
|
||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-110 apples-to-apples S3 overlay for fair vs-C6 size comparison.
|
||||
# Same target as production S3 but with the features that aren't on C6 disabled:
|
||||
# - No AMOLED display (ADR-045 — C6 has no PSRAM for framebuffers)
|
||||
# - No WASM3 (ADR-040 — same reason)
|
||||
# - No mmWave fusion (separate board)
|
||||
# This is NOT a production build — only used to answer "is C6 smaller than S3
|
||||
# once you strip the S3-only features?"
|
||||
#
|
||||
# Build:
|
||||
# cp sdkconfig.defaults.s3-fair sdkconfig.defaults && idf.py set-target esp32s3 && idf.py build
|
||||
# # Restore default: git checkout sdkconfig.defaults
|
||||
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
||||
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
CONFIG_LWIP_SO_RCVBUF=y
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=8192
|
||||
|
||||
# Disable display + WASM + mmWave for apples-to-apples vs C6.
|
||||
# CONFIG_DISPLAY_ENABLE is not set
|
||||
# CONFIG_WASM_ENABLE is not set
|
||||
@@ -20,6 +20,11 @@
|
||||
# FUZZ_JOBS=4 # Parallel fuzzing jobs
|
||||
|
||||
CC = clang
|
||||
# ADR-110: -DCONFIG_CSI_FRAME_HE_TAGGING=1 enables the byte-18/19 HE path
|
||||
# in csi_collector.c so the fuzzer exercises that code as well as the
|
||||
# legacy zero-fill path. CONFIG_SOC_WIFI_HE_SUPPORT is left UNSET to
|
||||
# exercise the legacy S3 branch (sig_mode/cwb/stbc). Add it to CFLAGS for
|
||||
# a parallel HE-stub build if you want fuzz coverage of the C6 branch.
|
||||
CFLAGS = -fsanitize=fuzzer,address,undefined -g -O1 \
|
||||
-Istubs -I../main \
|
||||
-DCONFIG_CSI_NODE_ID=1 \
|
||||
@@ -28,6 +33,7 @@ CFLAGS = -fsanitize=fuzzer,address,undefined -g -O1 \
|
||||
-DCONFIG_CSI_TARGET_IP=\"192.168.1.1\" \
|
||||
-DCONFIG_CSI_TARGET_PORT=5500 \
|
||||
-DCONFIG_ESP_WIFI_CSI_ENABLED=1 \
|
||||
-DCONFIG_CSI_FRAME_HE_TAGGING=1 \
|
||||
-Wno-unused-function
|
||||
|
||||
STUBS_SRC = stubs/esp_stubs.c
|
||||
@@ -37,9 +43,22 @@ MAIN_DIR = ../main
|
||||
FUZZ_DURATION ?= 30
|
||||
FUZZ_JOBS ?= 1
|
||||
|
||||
.PHONY: all clean run_serialize run_edge run_nvs run_all
|
||||
.PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 host_tests
|
||||
|
||||
all: fuzz_serialize fuzz_edge fuzz_nvs
|
||||
all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110
|
||||
|
||||
# --- ADR-110 encoding unit tests ---
|
||||
# Host-side, no libFuzzer needed — plain C99 deterministic table tests
|
||||
# for mac_to_eui64() and PPDU-type → ADR-018 byte 18 mapping.
|
||||
# Builds with stock cc/gcc/clang — runs in CI on Ubuntu.
|
||||
test_adr110: test_adr110_encoding.c
|
||||
cc -std=c99 -Wall -Wextra -o $@ $<
|
||||
|
||||
run_adr110: test_adr110
|
||||
./test_adr110
|
||||
|
||||
host_tests: run_adr110
|
||||
@echo "ADR-110 host tests passed"
|
||||
|
||||
# --- Serialize fuzzer ---
|
||||
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
|
||||
@@ -75,5 +94,5 @@ run_nvs: fuzz_nvs
|
||||
run_all: run_serialize run_edge run_nvs
|
||||
|
||||
clean:
|
||||
rm -f fuzz_serialize fuzz_edge fuzz_nvs
|
||||
rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110
|
||||
rm -rf corpus_serialize/ corpus_edge/ corpus_nvs/
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""ADR-110 multi-board live capture — 802.15.4 sync + TWT + HE-LTF.
|
||||
|
||||
Captures from up to 3 ESP32-C6 boards simultaneously, resets them
|
||||
together so the leader election starts from a clean slate, then
|
||||
records 35 s of serial output to per-port log files and prints
|
||||
a summary of the time-sync state machine, TWT events, and CSI
|
||||
metadata at the end.
|
||||
"""
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PORTS = ['COM6', 'COM9', 'COM12']
|
||||
DURATION_SECONDS = 35
|
||||
OUTPUT_DIR = Path(__file__).parent / 'witness-3board'
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def capture(port: str, results: dict):
|
||||
"""Reset and capture from one port for DURATION_SECONDS."""
|
||||
try:
|
||||
ser = serial.Serial(port, 115200, timeout=1)
|
||||
# Hard reset via DTR/RTS pulse.
|
||||
ser.setDTR(False); ser.setRTS(True); time.sleep(0.05)
|
||||
ser.setDTR(False); ser.setRTS(False)
|
||||
ser.reset_input_buffer()
|
||||
buf = bytearray()
|
||||
start = time.time()
|
||||
while time.time() - start < DURATION_SECONDS:
|
||||
data = ser.read(4096)
|
||||
if data:
|
||||
buf.extend(data)
|
||||
ser.close()
|
||||
log_path = OUTPUT_DIR / f'{port}.log'
|
||||
log_path.write_bytes(bytes(buf))
|
||||
text = bytes(buf).decode('utf-8', errors='replace')
|
||||
results[port] = text
|
||||
print(f'[{port}] {len(buf)} bytes captured -> {log_path}')
|
||||
except Exception as e:
|
||||
print(f'[{port}] ERROR: {e}')
|
||||
results[port] = None
|
||||
|
||||
|
||||
# Launch 3 capture threads — actual concurrent reset + capture.
|
||||
results = {}
|
||||
threads = [threading.Thread(target=capture, args=(p, results)) for p in PORTS]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
# ── Analyze ────────────────────────────────────────────────────────────
|
||||
|
||||
def grep_pattern(text: str, pattern: str, n: int = 8):
|
||||
rx = re.compile(pattern)
|
||||
return [L.strip() for L in (text or '').split('\n') if rx.search(L)][:n]
|
||||
|
||||
|
||||
print('\n' + '='*78)
|
||||
print('ADR-110 multi-board capture summary')
|
||||
print('='*78)
|
||||
|
||||
|
||||
for port in PORTS:
|
||||
text = results.get(port)
|
||||
if not text:
|
||||
print(f'\n--- {port}: NO DATA ---')
|
||||
continue
|
||||
print(f'\n--- {port} ---')
|
||||
|
||||
# Boot banner
|
||||
for L in grep_pattern(text, r'main: ESP32-C6.*Node ID', 2):
|
||||
print(f' banner : {L}')
|
||||
|
||||
# Time-sync init (802.15.4 path — known broken D1)
|
||||
for L in grep_pattern(text, r'c6_ts:.*(init done|promot|stepping down|tx fail)', 4):
|
||||
print(f' c6_ts : {L}')
|
||||
|
||||
# ESP-NOW sync (D1 workaround, working path)
|
||||
for L in grep_pattern(text, r'c6_espnow:.*(init done|promot|stepping down|tx#\d)', 6):
|
||||
print(f' c6_espnow: {L}')
|
||||
|
||||
# WiFi mode + connect status
|
||||
for L in grep_pattern(text, r'(wifi:mode|wifi:state|Retrying WiFi|got ip|Connected to WiFi)', 6):
|
||||
print(f' wifi : {L}')
|
||||
|
||||
# TWT events
|
||||
for L in grep_pattern(text, r'c6_twt|itwt|TWT', 5):
|
||||
print(f' twt : {L}')
|
||||
|
||||
# CSI callbacks
|
||||
for L in grep_pattern(text, r'CSI cb #\d+.*len=', 5):
|
||||
print(f' csi_cb : {L}')
|
||||
|
||||
# 11ax MAC firmware
|
||||
for L in grep_pattern(text, r'mac_version:HAL_MAC_ESP32AX', 2):
|
||||
print(f' he-mac : {L}')
|
||||
|
||||
|
||||
# Cross-board leader election summary
|
||||
print('\n' + '='*78)
|
||||
print('Leader election analysis')
|
||||
print('='*78)
|
||||
eui_re = re.compile(r'EUI=([0-9a-fA-F]+)')
|
||||
euis = {}
|
||||
for port in PORTS:
|
||||
text = results.get(port) or ''
|
||||
m = eui_re.search(text)
|
||||
if m:
|
||||
euis[port] = int(m.group(1), 16)
|
||||
print(f' {port} EUI=0x{m.group(1).lower()} -> {"LEADER" if False else "candidate"}')
|
||||
|
||||
if len(euis) >= 2:
|
||||
lowest_port = min(euis, key=euis.get)
|
||||
print(f'\n lowest EUI -> expected leader: {lowest_port} (0x{euis[lowest_port]:016x})')
|
||||
|
||||
# Did a "stepping down" log appear on the non-lowest boards?
|
||||
for port in PORTS:
|
||||
if port == lowest_port:
|
||||
continue
|
||||
text = results.get(port) or ''
|
||||
if 'stepping down' in text:
|
||||
print(f' {port}: [OK] stepped down (heard leader beacon)')
|
||||
elif port in euis:
|
||||
print(f' {port}: [FAIL] did NOT step down — investigate (own EUI=0x{euis[port]:016x}, expected leader=0x{euis[lowest_port]:016x})')
|
||||
@@ -60,6 +60,10 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
uint8_t channel;
|
||||
int8_t noise_floor;
|
||||
uint8_t out_buf_scale; /* Controls output buffer size: 0-255. */
|
||||
/* ADR-110: fuzz the new HE-branch + legacy-branch input fields too so
|
||||
* the byte 18/19 encoding code path is exercised. */
|
||||
uint8_t he_inputs[2] = {0}; /* cur_bb_format (4 bits) + second (4 bits) packed */
|
||||
uint8_t legacy_inputs = 0; /* sig_mode (2) + cwb (1) + stbc (1) packed */
|
||||
|
||||
fuzz_read(&cursor, &remaining, &test_case, 1);
|
||||
fuzz_read(&cursor, &remaining, &iq_len_raw, 2);
|
||||
@@ -67,6 +71,8 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
fuzz_read(&cursor, &remaining, &channel, 1);
|
||||
fuzz_read(&cursor, &remaining, &noise_floor, 1);
|
||||
fuzz_read(&cursor, &remaining, &out_buf_scale, 1);
|
||||
fuzz_read(&cursor, &remaining, he_inputs, 2);
|
||||
fuzz_read(&cursor, &remaining, &legacy_inputs, 1);
|
||||
|
||||
/* --- Test case 0: Normal operation with fuzz-controlled values --- */
|
||||
|
||||
@@ -75,6 +81,15 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
info.rx_ctrl.rssi = rssi;
|
||||
info.rx_ctrl.channel = channel & 0x0F; /* 4-bit field */
|
||||
info.rx_ctrl.noise_floor = noise_floor;
|
||||
/* ADR-110: feed both branch families. Only the active branch (chosen
|
||||
* at compile time by CONFIG_SOC_WIFI_HE_SUPPORT) will read its fields;
|
||||
* the other set is set-but-not-read. Both must be assignable without
|
||||
* triggering UBSAN bitfield-overflow. */
|
||||
info.rx_ctrl.cur_bb_format = he_inputs[0] & 0x0F; /* 0..15 valid input space */
|
||||
info.rx_ctrl.second = he_inputs[1] & 0x0F;
|
||||
info.rx_ctrl.sig_mode = legacy_inputs & 0x03;
|
||||
info.rx_ctrl.cwb = (legacy_inputs >> 2) & 0x01;
|
||||
info.rx_ctrl.stbc = (legacy_inputs >> 3) & 0x01;
|
||||
|
||||
/* Use remaining fuzz data as I/Q buffer content. */
|
||||
uint16_t iq_len;
|
||||
|
||||
@@ -73,3 +73,13 @@ static mmwave_state_t s_stub_mmwave = {0};
|
||||
esp_err_t mmwave_sensor_init(int tx, int rx) { (void)tx; (void)rx; return ESP_ERR_NOT_FOUND; }
|
||||
bool mmwave_sensor_get_state(mmwave_state_t *s) { if (s) *s = s_stub_mmwave; return false; }
|
||||
const char *mmwave_type_name(mmwave_type_t t) { (void)t; return "None"; }
|
||||
|
||||
/* ADR-110 iter 38 — fuzz-harness stub for c6_sync_espnow_is_valid.
|
||||
* Real implementation lives in main/c6_sync_espnow.c; the fuzz target
|
||||
* (`fuzz_serialize`) only links csi_collector.c against esp_stubs.c, so
|
||||
* iter-11's `if (c6_sync_espnow_is_valid()) flags |= (1 << 4);` needs a
|
||||
* symbol here or `clang -fsanitize=fuzzer` fails with an undefined-reference
|
||||
* linker error. Returning false means the bit-4 cross-node-sync-valid flag
|
||||
* stays 0 in fuzz inputs, which is the natural fuzz semantic. */
|
||||
#include <stdbool.h>
|
||||
bool c6_sync_espnow_is_valid(void) { return false; }
|
||||
|
||||
@@ -62,14 +62,28 @@ static inline esp_err_t esp_timer_delete(esp_timer_handle_t h) { (void)h; return
|
||||
|
||||
/* ---- esp_wifi_types.h ---- */
|
||||
|
||||
/** Minimal rx_ctrl fields needed by csi_serialize_frame. */
|
||||
/** Minimal rx_ctrl fields needed by csi_serialize_frame.
|
||||
*
|
||||
* ADR-110: the HE-tagging path in csi_collector.c references either
|
||||
* (CONFIG_SOC_WIFI_HE_SUPPORT branch) cur_bb_format, second
|
||||
* (legacy / S3 branch) sig_mode, cwb, stbc
|
||||
*
|
||||
* Both sets are unconditionally declared here so a single stub builds
|
||||
* for either branch — the Makefile picks which side via -D flags. */
|
||||
typedef struct {
|
||||
signed rssi : 8;
|
||||
unsigned channel : 4;
|
||||
unsigned noise_floor : 8;
|
||||
unsigned rx_ant : 2;
|
||||
/* Padding to fill out the struct so it compiles. */
|
||||
unsigned _pad : 10;
|
||||
signed rssi : 8;
|
||||
unsigned channel : 4;
|
||||
unsigned noise_floor : 8;
|
||||
unsigned rx_ant : 2;
|
||||
/* ADR-110 HE-branch fields (CONFIG_SOC_WIFI_HE_SUPPORT path) */
|
||||
unsigned cur_bb_format : 4; /**< 0=11b 1=11g/a 2=HT 3=VHT 4=HE-SU 5=HE-MU 6=HE-ER-SU 7=HE-TB */
|
||||
unsigned second : 4; /**< secondary 40 MHz channel offset */
|
||||
/* ADR-110 legacy-branch fields (pre-HE chips) */
|
||||
unsigned sig_mode : 2; /**< 0=non-HT 1=HT 3=VHT */
|
||||
unsigned cwb : 1; /**< 0=20 MHz 1=40 MHz */
|
||||
unsigned stbc : 1; /**< STBC flag */
|
||||
/* Padding to keep alignment predictable. */
|
||||
unsigned _pad : 18;
|
||||
} wifi_pkt_rx_ctrl_t;
|
||||
|
||||
/** Minimal wifi_csi_info_t needed by csi_serialize_frame. */
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* @file test_adr110_encoding.c
|
||||
* @brief Host-side unit tests for ADR-110 pure functions.
|
||||
*
|
||||
* Covers the two encoding paths that don't need ESP-IDF runtime:
|
||||
* 1. mac_to_eui64() — IEEE EUI-64 from MAC-48 (c6_timesync.c)
|
||||
* 2. PPDU-type → ADR-018 byte 18 mapping for both HE-capable and
|
||||
* legacy paths (csi_collector.c)
|
||||
*
|
||||
* Build (Linux/macOS/Windows with any C99 compiler):
|
||||
* cc -std=c99 -Wall -o test_adr110 test_adr110_encoding.c && ./test_adr110
|
||||
*
|
||||
* Or in WSL on this Windows box:
|
||||
* gcc -std=c99 -Wall -o test_adr110 test_adr110_encoding.c && ./test_adr110
|
||||
*
|
||||
* Exits 0 on all-pass, prints which assertion failed otherwise.
|
||||
*
|
||||
* Why a separate host test file rather than extending the existing fuzz
|
||||
* harness: fuzzers want random bytes; these are deterministic table-driven
|
||||
* checks for tiny pure functions where libFuzzer adds no signal.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────
|
||||
* System under test — copied verbatim from the firmware. If the
|
||||
* firmware copy changes, this test must be updated and the new behavior
|
||||
* attested by re-running the test before the firmware change merges.
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* From firmware/esp32-csi-node/main/c6_timesync.c — fallback path used only
|
||||
* when esp_read_mac(..., ESP_MAC_IEEE802154) fails. The primary C6 path
|
||||
* reads 8 bytes directly (the eFuse-provided EUI-64). */
|
||||
static uint64_t mac48_to_eui64(const uint8_t mac[6])
|
||||
{
|
||||
return ((uint64_t)mac[0] << 56) | ((uint64_t)mac[1] << 48) |
|
||||
((uint64_t)mac[2] << 40) | ((uint64_t)0xFF << 32) |
|
||||
((uint64_t)0xFE << 24) | ((uint64_t)mac[3] << 16) |
|
||||
((uint64_t)mac[4] << 8 ) | (uint64_t)mac[5];
|
||||
}
|
||||
|
||||
/* Pack 8-byte EUI-64 buffer (as returned by ESP_MAC_IEEE802154) into u64. */
|
||||
static uint64_t eui64_bytes_to_u64(const uint8_t eui[8])
|
||||
{
|
||||
return ((uint64_t)eui[0] << 56) | ((uint64_t)eui[1] << 48) |
|
||||
((uint64_t)eui[2] << 40) | ((uint64_t)eui[3] << 32) |
|
||||
((uint64_t)eui[4] << 24) | ((uint64_t)eui[5] << 16) |
|
||||
((uint64_t)eui[6] << 8 ) | (uint64_t)eui[7];
|
||||
}
|
||||
|
||||
/* From firmware/esp32-csi-node/main/csi_collector.c — HE-capable branch.
|
||||
* Returns the ADR-018 byte-18 PPDU type. */
|
||||
static uint8_t ppdu_type_he(uint8_t cur_bb_format)
|
||||
{
|
||||
switch (cur_bb_format) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2: return 0; /* 11b/g/a/HT bucket */
|
||||
case 3: return 0; /* VHT */
|
||||
case 4: return 1; /* HE-SU */
|
||||
case 5: return 2; /* HE-MU */
|
||||
case 6: return 1; /* HE-ER-SU collapses to HE-SU */
|
||||
case 7: return 3; /* HE-TB */
|
||||
default: return 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* From csi_collector.c — legacy (non-HE) branch. */
|
||||
static uint8_t ppdu_type_legacy(uint8_t sig_mode)
|
||||
{
|
||||
switch (sig_mode) {
|
||||
case 0: return 0; /* non-HT */
|
||||
case 1: return 0; /* HT */
|
||||
case 3: return 0; /* VHT */
|
||||
default: return 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────
|
||||
* Test harness
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
static int g_failed = 0;
|
||||
static int g_passed = 0;
|
||||
|
||||
#define CHECK_EQ_U64(label, got, expected) do { \
|
||||
if ((got) == (expected)) { g_passed++; } \
|
||||
else { \
|
||||
g_failed++; \
|
||||
printf("FAIL: %s — got=0x%016llx expected=0x%016llx\n", \
|
||||
(label), (unsigned long long)(got), \
|
||||
(unsigned long long)(expected)); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_EQ_U8(label, got, expected) do { \
|
||||
if ((uint8_t)(got) == (uint8_t)(expected)) { g_passed++; } \
|
||||
else { \
|
||||
g_failed++; \
|
||||
printf("FAIL: %s — got=0x%02x expected=0x%02x\n", \
|
||||
(label), (unsigned)(got), (unsigned)(expected)); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────
|
||||
* EUI-64 tests
|
||||
*
|
||||
* IEEE 802 MAC-48 → EUI-64 spec: insert 0xFFFE between bytes 3 and 4
|
||||
* of the MAC. ADR-110's c6_timesync.c does exactly that, leaving the
|
||||
* U/L bit in byte 0 untouched (the c6 EUI then matches what `esp_read_mac
|
||||
* ESP_MAC_IEEE802154` returns).
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
static void test_eui64_fallback_zero_mac(void)
|
||||
{
|
||||
uint8_t mac[6] = {0, 0, 0, 0, 0, 0};
|
||||
/* mac48_to_eui64 inserts FFFE → 00 00 00 FF FE 00 00 00 */
|
||||
CHECK_EQ_U64("mac48->eui64 zero", mac48_to_eui64(mac), 0x000000FFFE000000ULL);
|
||||
}
|
||||
|
||||
static void test_eui64_fallback_all_ones(void)
|
||||
{
|
||||
uint8_t mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
/* FF FF FF FF FE FF FF FF */
|
||||
CHECK_EQ_U64("mac48->eui64 all-ones", mac48_to_eui64(mac), 0xFFFFFFFFFEFFFFFFULL);
|
||||
}
|
||||
|
||||
static void test_eui64_fallback_byte_order(void)
|
||||
{
|
||||
uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
||||
CHECK_EQ_U64("mac48->eui64 byte order", mac48_to_eui64(mac), 0x112233FFFE445566ULL);
|
||||
}
|
||||
|
||||
/* Primary path: 8-byte EUI-64 from ESP_MAC_IEEE802154 packed unchanged.
|
||||
* Verified by esptool's chip_id output on the real C6 hardware:
|
||||
* COM6: BASE MAC 20:6e:f1:17:27:8c, MAC_EXT ff:fe →
|
||||
* full EUI: 20:6e:f1:ff:fe:17:27:8c → 0x206EF1FFFE17278C
|
||||
* COM9: BASE MAC 20:6e:f1:17:05:3c, MAC_EXT ff:fe →
|
||||
* full EUI: 20:6e:f1:ff:fe:17:05:3c → 0x206EF1FFFE17053C
|
||||
*
|
||||
* Note COM9's EUI is numerically smaller — it wins the leader election. */
|
||||
static void test_eui64_from_native_com6(void)
|
||||
{
|
||||
uint8_t eui[8] = {0x20, 0x6e, 0xf1, 0xff, 0xfe, 0x17, 0x27, 0x8c};
|
||||
CHECK_EQ_U64("native eui64 COM6", eui64_bytes_to_u64(eui), 0x206EF1FFFE17278CULL);
|
||||
}
|
||||
|
||||
static void test_eui64_from_native_com9(void)
|
||||
{
|
||||
uint8_t eui[8] = {0x20, 0x6e, 0xf1, 0xff, 0xfe, 0x17, 0x05, 0x3c};
|
||||
CHECK_EQ_U64("native eui64 COM9", eui64_bytes_to_u64(eui), 0x206EF1FFFE17053CULL);
|
||||
}
|
||||
|
||||
static void test_eui64_leader_election_order(void)
|
||||
{
|
||||
uint8_t com6[8] = {0x20, 0x6e, 0xf1, 0xff, 0xfe, 0x17, 0x27, 0x8c};
|
||||
uint8_t com9[8] = {0x20, 0x6e, 0xf1, 0xff, 0xfe, 0x17, 0x05, 0x3c};
|
||||
uint64_t a = eui64_bytes_to_u64(com6);
|
||||
uint64_t b = eui64_bytes_to_u64(com9);
|
||||
/* Lowest EUI wins → COM9 should be leader when both boards online. */
|
||||
if (b < a) { g_passed++; }
|
||||
else { g_failed++; printf("FAIL: leader-election order — expected COM9 < COM6\n"); }
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────
|
||||
* PPDU-type encoding tests — HE-capable branch (C6/C5)
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
static void test_ppdu_he_legacy_bucket(void)
|
||||
{
|
||||
CHECK_EQ_U8("he 0 → 0 (11b)", ppdu_type_he(0), 0);
|
||||
CHECK_EQ_U8("he 1 → 0 (11g/a)", ppdu_type_he(1), 0);
|
||||
CHECK_EQ_U8("he 2 → 0 (HT)", ppdu_type_he(2), 0);
|
||||
CHECK_EQ_U8("he 3 → 0 (VHT)", ppdu_type_he(3), 0);
|
||||
}
|
||||
|
||||
static void test_ppdu_he_su(void)
|
||||
{
|
||||
CHECK_EQ_U8("he 4 → 1 (HE-SU)", ppdu_type_he(4), 1);
|
||||
CHECK_EQ_U8("he 6 → 1 (HE-ER-SU)", ppdu_type_he(6), 1);
|
||||
}
|
||||
|
||||
static void test_ppdu_he_mu(void)
|
||||
{
|
||||
CHECK_EQ_U8("he 5 → 2 (HE-MU)", ppdu_type_he(5), 2);
|
||||
}
|
||||
|
||||
static void test_ppdu_he_tb(void)
|
||||
{
|
||||
CHECK_EQ_U8("he 7 → 3 (HE-TB)", ppdu_type_he(7), 3);
|
||||
}
|
||||
|
||||
static void test_ppdu_he_out_of_range(void)
|
||||
{
|
||||
CHECK_EQ_U8("he 8 → 0xFF (unknown)", ppdu_type_he(8), 0xFF);
|
||||
CHECK_EQ_U8("he 15 → 0xFF (unknown)", ppdu_type_he(15), 0xFF);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────
|
||||
* PPDU-type encoding tests — legacy (S3/etc) branch
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
static void test_ppdu_legacy_known(void)
|
||||
{
|
||||
CHECK_EQ_U8("legacy sig_mode 0 → 0 (non-HT)", ppdu_type_legacy(0), 0);
|
||||
CHECK_EQ_U8("legacy sig_mode 1 → 0 (HT)", ppdu_type_legacy(1), 0);
|
||||
CHECK_EQ_U8("legacy sig_mode 3 → 0 (VHT)", ppdu_type_legacy(3), 0);
|
||||
}
|
||||
|
||||
static void test_ppdu_legacy_unknown(void)
|
||||
{
|
||||
CHECK_EQ_U8("legacy sig_mode 2 → 0xFF", ppdu_type_legacy(2), 0xFF);
|
||||
CHECK_EQ_U8("legacy sig_mode 5 → 0xFF", ppdu_type_legacy(5), 0xFF);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────
|
||||
* main
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_eui64_fallback_zero_mac();
|
||||
test_eui64_fallback_all_ones();
|
||||
test_eui64_fallback_byte_order();
|
||||
test_eui64_from_native_com6();
|
||||
test_eui64_from_native_com9();
|
||||
test_eui64_leader_election_order();
|
||||
|
||||
test_ppdu_he_legacy_bucket();
|
||||
test_ppdu_he_su();
|
||||
test_ppdu_he_mu();
|
||||
test_ppdu_he_tb();
|
||||
test_ppdu_he_out_of_range();
|
||||
|
||||
test_ppdu_legacy_known();
|
||||
test_ppdu_legacy_unknown();
|
||||
|
||||
printf("\n%d passed, %d failed\n", g_passed, g_failed);
|
||||
return g_failed == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
0.6.6
|
||||
0.7.0
|
||||
|
||||
Reference in New Issue
Block a user