firmware/esp32-csi-node: IDF 6 build, HE CSI config, unicore DSP, provision chip detect (#522)

* firmware/esp32-csi-node: fix IDF 6 build (PSA SHA-256, explicit REQUIRES)

- rvf_parser: use psa_hash_* / psa_hash_compute; mbedTLS 4 has no public
  mbedtls/sha256.h on the IDF include path.
- main/CMakeLists: declare REQUIRES for WiFi, netif, HTTP, OTA, drivers, lwip,
  mbedtls per ESP-IDF v6 component dependency checks; optional wasm3 when
  CONFIG_WASM_ENABLE.

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* firmware/esp32-csi-node: fix CSI config for Wi-Fi 6 (ESP32-C6)

When CONFIG_SOC_WIFI_HE_SUPPORT is set, wifi_csi_config_t is the
wifi_csi_acquire_config_t bitfield layout. The legacy bool fields
(lltf_en, htltf_en, ...) only apply to ESP32-S3-class targets.

Initialize acquire fields for HE targets; add MAC v3-only members when
CONFIG_SOC_WIFI_MAC_VERSION_NUM >= 3.

Verified: idf.py build for esp32c6 and esp32s3 (ESP-IDF v6.1).

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* firmware/esp32-csi-node: pin edge DSP task for unicore (ESP32-C6)

edge_processing_init used xTaskCreatePinnedToCore(..., core 1). ESP32-C6
runs FreeRTOS unicore (portNUM_PROCESSORS == 1), so core 1 trips the
xTaskCreatePinnedToCore range assert right after CSI init.

Use core 1 only when SMP is available; otherwise pin to core 0.

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* firmware/esp32-csi-node: provision NVS with chip auto-detect

provision.py always passed --chip esp32s3 to esptool, so flashing NVS on
ESP32-C6 failed. Default --chip to auto (esptool v5) and add an explicit
--chip override. Use write-flash instead of deprecated write_flash.

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Chaitanya Tata
2026-05-18 03:30:40 +05:30
committed by GitHub
parent f853c74563
commit cee414f3c0
5 changed files with 105 additions and 43 deletions
+25 -2
View File
@@ -11,7 +11,26 @@ set(SRCS
"adaptive_controller.c"
)
set(REQUIRES "")
# ESP-IDF v6+: headers must resolve via explicit REQUIRES (no implicit deps).
set(REQUIRES
esp_wifi
esp_netif
esp_event
nvs_flash
app_update
esp_http_server
esp_http_client
esp_app_format
esp_timer
esp_pm
esp_driver_uart
esp_driver_gpio
esp_driver_spi
esp_driver_i2c
driver
lwip
mbedtls
)
# ADR-061: Mock CSI generator for QEMU testing + ADR-081 mock radio binding
if(CONFIG_CSI_MOCK_ENABLED)
@@ -21,7 +40,11 @@ endif()
# ADR-045: AMOLED display support (compile-time optional)
if(CONFIG_DISPLAY_ENABLE)
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
set(REQUIRES esp_lcd esp_lcd_touch lvgl)
list(APPEND REQUIRES esp_lcd esp_lcd_touch lvgl)
endif()
if(CONFIG_WASM_ENABLE)
list(APPEND REQUIRES wasm3)
endif()
idf_component_register(
@@ -371,6 +371,30 @@ void csi_collector_init(void)
ESP_LOGI(TAG, "Promiscuous mode enabled (MGMT-only, RuView#396)");
#if CONFIG_SOC_WIFI_HE_SUPPORT
/* Wi-Fi 6 targets (e.g. ESP32-C6): wifi_csi_config_t is wifi_csi_acquire_config_t
* (bitfields), not the legacy 802.11n bool layout used on ESP32-S3. */
wifi_csi_config_t csi_config;
memset(&csi_config, 0, sizeof(csi_config));
csi_config.enable = 1U;
csi_config.acquire_csi_legacy = 1U;
csi_config.acquire_csi_ht20 = 1U;
csi_config.acquire_csi_ht40 = 1U;
csi_config.acquire_csi_su = 1U;
csi_config.acquire_csi_mu = 1U;
csi_config.acquire_csi_dcm = 1U;
csi_config.acquire_csi_beamformed = 1U;
#if CONFIG_SOC_WIFI_MAC_VERSION_NUM >= 3
csi_config.acquire_csi_force_lltf = 1U;
csi_config.acquire_csi_vht = 1U;
csi_config.acquire_csi_he_stbc_mode = ESP_CSI_ACQUIRE_STBC_SAMPLE_HELTFS;
csi_config.val_scale_cfg = 0U;
#else
csi_config.acquire_csi_he_stbc = ESP_CSI_ACQUIRE_STBC_SAMPLE_HELTFS;
csi_config.val_scale_cfg = 0U;
#endif
csi_config.dump_ack_en = 0U;
#else
wifi_csi_config_t csi_config = {
.lltf_en = true,
.htltf_en = true,
@@ -380,6 +404,7 @@ void csi_collector_init(void)
.manu_scale = false,
.shift = false,
};
#endif
ESP_ERROR_CHECK(esp_wifi_set_csi_config(&csi_config));
ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(wifi_csi_callback, NULL));
@@ -2,8 +2,9 @@
* @file edge_processing.c
* @brief ADR-039 Edge Intelligence — dual-core CSI processing pipeline.
*
* Core 0 (WiFi task): Pushes raw CSI frames into lock-free SPSC ring buffer.
* Core 1 (DSP task): Pops frames, runs signal processing pipeline:
* Core 0 (WiFi path): Pushes raw CSI frames into lock-free SPSC ring buffer.
* Second core when present (DSP task): pops frames, runs signal processing pipeline.
* On unicore targets (e.g. ESP32-C6), the DSP task is pinned to core 0.
* 1. Phase extraction from I/Q pairs
* 2. Phase unwrapping (continuous phase)
* 3. Welford variance tracking per subcarrier
@@ -1050,7 +1051,9 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
return ESP_OK;
}
/* Start DSP task on Core 1. */
/* Pin DSP off WiFi's preferred core when SMP; else core 0 only (ESP32-C6). */
const BaseType_t dsp_core = (portNUM_PROCESSORS > 1) ? (BaseType_t)1 : (BaseType_t)0;
BaseType_t ret = xTaskCreatePinnedToCore(
edge_task,
"edge_dsp",
@@ -1058,14 +1061,14 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
NULL,
5, /* Priority 5 — above idle, below WiFi. */
NULL,
1 /* Pin to Core 1. */
);
dsp_core);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create edge DSP task");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Edge DSP task created on Core 1 (stack=8192, priority=5)");
ESP_LOGI(TAG, "Edge DSP task created on core %d (stack=8192, priority=5)",
(int)dsp_core);
return ESP_OK;
}
+25 -23
View File
@@ -10,7 +10,7 @@
#include <string.h>
#include "esp_log.h"
#include "mbedtls/sha256.h"
#include "psa/crypto.h"
static const char *TAG = "rvf";
@@ -125,9 +125,13 @@ esp_err_t rvf_parse(const uint8_t *data, uint32_t data_len, rvf_parsed_t *out)
/* ---- Verify build hash (SHA-256 of WASM payload) ---- */
uint8_t computed_hash[32];
int ret = mbedtls_sha256(wasm_data, hdr->wasm_len, computed_hash, 0);
if (ret != 0) {
ESP_LOGE(TAG, "SHA-256 computation failed: %d", ret);
size_t hash_len = 0;
psa_status_t psa_st = psa_hash_compute(PSA_ALG_SHA_256, wasm_data,
hdr->wasm_len, computed_hash,
sizeof(computed_hash), &hash_len);
if (psa_st != PSA_SUCCESS || hash_len != 32) {
ESP_LOGE(TAG, "SHA-256 computation failed: psa=%d len=%u",
(int)psa_st, (unsigned)hash_len);
return ESP_FAIL;
}
@@ -186,8 +190,7 @@ esp_err_t rvf_verify_signature(const rvf_parsed_t *parsed, const uint8_t *data,
/*
* Ed25519 verification.
*
* ESP-IDF v5.2 mbedtls does NOT include Ed25519 (Curve25519 is
* for ECDH/X25519 only). We use a SHA-256-HMAC integrity check:
* Legacy mbedtls Ed25519 is optional. We use a SHA-256 keyed digest:
*
* expected = SHA-256(pubkey || signed_region)
*
@@ -196,35 +199,34 @@ esp_err_t rvf_verify_signature(const rvf_parsed_t *parsed, const uint8_t *data,
* pubkey produces a different expected hash, so unauthorized
* publishers cannot forge a valid signature.
*
* For full Ed25519 (NaCl-style), enable CONFIG_MBEDTLS_EDDSA_C
* or link TweetNaCl. The RVF builder should match this scheme.
* For full Ed25519, enable CONFIG_MBEDTLS_EDDSA_C or equivalent.
* The RVF builder should match this scheme.
*/
uint8_t hash_input_prefix[32];
memcpy(hash_input_prefix, pubkey, 32);
/* Compute SHA-256(pubkey || header+manifest+wasm). */
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
int ret = mbedtls_sha256_starts(&ctx, 0);
if (ret != 0) {
mbedtls_sha256_free(&ctx);
/* Compute SHA-256(pubkey || header+manifest+wasm) via PSA Crypto. */
psa_hash_operation_t op = PSA_HASH_OPERATION_INIT;
psa_status_t st = psa_hash_setup(&op, PSA_ALG_SHA_256);
if (st != PSA_SUCCESS) {
return ESP_FAIL;
}
ret = mbedtls_sha256_update(&ctx, hash_input_prefix, 32);
if (ret != 0) {
mbedtls_sha256_free(&ctx);
st = psa_hash_update(&op, hash_input_prefix, 32);
if (st != PSA_SUCCESS) {
(void)psa_hash_abort(&op);
return ESP_FAIL;
}
ret = mbedtls_sha256_update(&ctx, data, signed_len);
if (ret != 0) {
mbedtls_sha256_free(&ctx);
st = psa_hash_update(&op, data, signed_len);
if (st != PSA_SUCCESS) {
(void)psa_hash_abort(&op);
return ESP_FAIL;
}
uint8_t expected[32];
ret = mbedtls_sha256_finish(&ctx, expected);
mbedtls_sha256_free(&ctx);
if (ret != 0) {
size_t out_len = 0;
st = psa_hash_finish(&op, expected, sizeof(expected), &out_len);
if (st != PSA_SUCCESS || out_len != 32) {
(void)psa_hash_abort(&op);
return ESP_FAIL;
}
+21 -12
View File
@@ -1,12 +1,14 @@
#!/usr/bin/env python3
"""
ESP32-S3 CSI Node Provisioning Script
ESP32 CSI node provisioning (ESP32-S3, ESP32-C6, other targets).
Writes WiFi credentials and aggregator target to the ESP32's NVS partition
so users can configure a pre-built firmware binary without recompiling.
Usage:
python provision.py --port COM7 --ssid "MyWiFi" --password "secret" --target-ip 192.168.1.20
python provision.py --port /dev/ttyUSB0 --chip esp32c6 --ssid "..." \\
--password "..." --target-ip 192.168.1.20
Requirements:
pip install 'esptool>=5.0' nvs-partition-gen
@@ -143,7 +145,7 @@ def generate_nvs_binary(csv_content, size):
os.unlink(p)
def flash_nvs(port, baud, nvs_bin):
def flash_nvs(port, baud, nvs_bin, chip):
"""Flash the NVS partition binary to the ESP32."""
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
f.write(nvs_bin)
@@ -152,16 +154,13 @@ def flash_nvs(port, baud, nvs_bin):
try:
cmd = [
sys.executable, "-m", "esptool",
"--chip", "esp32s3",
"--chip", chip,
"--port", port,
"--baud", str(baud),
# Keep underscore form — ESP-IDF v5.4 bundles esptool 4.10.0 which only
# accepts "write_flash". pip's esptool >=5.x accepts both (hyphenated
# form preferred) but keeps underscore working. Do not "correct" this.
"write_flash",
"write-flash",
hex(NVS_PARTITION_OFFSET), bin_path,
]
print(f"Flashing NVS partition ({len(nvs_bin)} bytes) to {port}...")
print(f"Flashing NVS partition ({len(nvs_bin)} bytes) to {port} (chip={chip})...")
subprocess.check_call(cmd)
print("NVS provisioning complete!")
finally:
@@ -170,10 +169,20 @@ def flash_nvs(port, baud, nvs_bin):
def main():
parser = argparse.ArgumentParser(
description="Provision ESP32-S3 CSI Node with WiFi and aggregator settings",
epilog="Example: python provision.py --port COM7 --ssid MyWiFi --password secret --target-ip 192.168.1.20",
description="Provision CSI node NVS (WiFi + aggregator); works on S3, C6, etc.",
epilog=(
"Example: python provision.py --port COM7 --ssid MyWiFi --password secret "
"--target-ip 192.168.1.20\n"
"ESP32-C6: same, or pass --chip esp32c6 if auto-detect fails "
"(default chip is auto for esptool v5+)."
),
)
parser.add_argument("--port", required=True, help="Serial port (e.g. COM7, /dev/ttyUSB0)")
parser.add_argument(
"--chip",
default="auto",
help="esptool target: auto (default), esp32s3, esp32c6, ... (must match connected chip)",
)
parser.add_argument("--baud", type=int, default=460800, help="Flash baud rate (default: 460800)")
parser.add_argument("--ssid", help="WiFi SSID")
parser.add_argument("--password", help="WiFi password")
@@ -337,11 +346,11 @@ def main():
with open(out, "wb") as f:
f.write(nvs_bin)
print(f"NVS binary saved to {out} ({len(nvs_bin)} bytes)")
print(f"Flash manually: python -m esptool --chip esp32s3 --port {args.port} "
print(f"Flash manually: python -m esptool --chip {args.chip} --port {args.port} "
f"write-flash 0x9000 {out}")
return
flash_nvs(args.port, args.baud, nvs_bin)
flash_nvs(args.port, args.baud, nvs_bin, args.chip)
if __name__ == "__main__":