Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3038712

Browse files
committed
feat: ADR-029/031 TDM sensing protocol, channel hopping, and NVS config
Implement the hardware and firmware portions of RuvSense (ADR-029) and RuView (ADR-031) for multistatic WiFi sensing: Rust (wifi-densepose-hardware): - TdmSchedule: uniform slot assignments with configurable cycle period, guard intervals, and processing window (default 4-node 20 Hz) - TdmCoordinator: manages sensing cycles, tracks per-slot completion, cumulative clock drift compensation (±10 ppm over 50 ms = 0.5 us) - SyncBeacon: 16-byte wire format for cycle synchronization with drift correction offsets - TdmSlotCompleted event for aggregator notification - 18 unit tests + 4 doctests, all passing Firmware (C, ESP32): - Channel-hop table in csi_collector.c (s_hop_channels, configurable via csi_collector_set_hop_table) - Timer-driven channel hopping via esp_timer at dwell intervals - NDP frame injection stub via esp_wifi_80211_tx() - Backward-compatible: hop_count=1 disables hopping entirely - NVS config extension: hop_count, chan_list, dwell_ms, tdm_slot, tdm_node_count with bounds validation and Kconfig fallback defaults Co-Authored-By: claude-flow <[email protected]>
1 parent b4f1e55 commit 3038712

7 files changed

Lines changed: 1124 additions & 0 deletions

File tree

firmware/esp32-csi-node/main/csi_collector.c

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
*
55
* Registers the ESP-IDF WiFi CSI callback and serializes incoming CSI data
66
* into the ADR-018 binary frame format for UDP transmission.
7+
*
8+
* ADR-029 extensions:
9+
* - Channel-hop table for multi-band sensing (channels 1/6/11 by default)
10+
* - Timer-driven channel hopping at configurable dwell intervals
11+
* - NDP frame injection stub for sensing-first TX
712
*/
813

914
#include "csi_collector.h"
@@ -12,6 +17,7 @@
1217
#include <string.h>
1318
#include "esp_log.h"
1419
#include "esp_wifi.h"
20+
#include "esp_timer.h"
1521
#include "sdkconfig.h"
1622

1723
static const char *TAG = "csi_collector";
@@ -21,6 +27,23 @@ static uint32_t s_cb_count = 0;
2127
static uint32_t s_send_ok = 0;
2228
static uint32_t s_send_fail = 0;
2329

30+
/* ---- ADR-029: Channel-hop state ---- */
31+
32+
/** Channel hop table (populated from NVS at boot or via set_hop_table). */
33+
static uint8_t s_hop_channels[CSI_HOP_CHANNELS_MAX] = {1, 6, 11, 36, 40, 44};
34+
35+
/** Number of active channels in the hop table. 1 = single-channel (no hop). */
36+
static uint8_t s_hop_count = 1;
37+
38+
/** Dwell time per channel in milliseconds. */
39+
static uint32_t s_dwell_ms = 50;
40+
41+
/** Current index into s_hop_channels. */
42+
static uint8_t s_hop_index = 0;
43+
44+
/** Handle for the periodic hop timer. NULL when timer is not running. */
45+
static esp_timer_handle_t s_hop_timer = NULL;
46+
2447
/**
2548
* Serialize CSI data into ADR-018 binary frame format.
2649
*
@@ -174,3 +197,146 @@ void csi_collector_init(void)
174197
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%d)",
175198
CONFIG_CSI_NODE_ID, CONFIG_CSI_WIFI_CHANNEL);
176199
}
200+
201+
/* ---- ADR-029: Channel hopping ---- */
202+
203+
void csi_collector_set_hop_table(const uint8_t *channels, uint8_t hop_count, uint32_t dwell_ms)
204+
{
205+
if (channels == NULL) {
206+
ESP_LOGW(TAG, "csi_collector_set_hop_table: channels is NULL");
207+
return;
208+
}
209+
if (hop_count == 0 || hop_count > CSI_HOP_CHANNELS_MAX) {
210+
ESP_LOGW(TAG, "csi_collector_set_hop_table: invalid hop_count=%u (max=%u)",
211+
(unsigned)hop_count, (unsigned)CSI_HOP_CHANNELS_MAX);
212+
return;
213+
}
214+
if (dwell_ms < 10) {
215+
ESP_LOGW(TAG, "csi_collector_set_hop_table: dwell_ms=%lu too small, clamping to 10",
216+
(unsigned long)dwell_ms);
217+
dwell_ms = 10;
218+
}
219+
220+
memcpy(s_hop_channels, channels, hop_count);
221+
s_hop_count = hop_count;
222+
s_dwell_ms = dwell_ms;
223+
s_hop_index = 0;
224+
225+
ESP_LOGI(TAG, "Hop table set: %u channels, dwell=%lu ms", (unsigned)hop_count,
226+
(unsigned long)dwell_ms);
227+
for (uint8_t i = 0; i < hop_count; i++) {
228+
ESP_LOGI(TAG, " hop[%u] = channel %u", (unsigned)i, (unsigned)channels[i]);
229+
}
230+
}
231+
232+
void csi_hop_next_channel(void)
233+
{
234+
if (s_hop_count <= 1) {
235+
/* Single-channel mode: no-op for backward compatibility. */
236+
return;
237+
}
238+
239+
s_hop_index = (s_hop_index + 1) % s_hop_count;
240+
uint8_t channel = s_hop_channels[s_hop_index];
241+
242+
/*
243+
* esp_wifi_set_channel() changes the primary channel.
244+
* The second parameter is the secondary channel offset for HT40;
245+
* we use HT20 (no secondary) for sensing.
246+
*/
247+
esp_err_t err = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
248+
if (err != ESP_OK) {
249+
ESP_LOGW(TAG, "Channel hop to %u failed: %s", (unsigned)channel, esp_err_to_name(err));
250+
} else if ((s_cb_count % 200) == 0) {
251+
/* Periodic log to confirm hopping is working (not every hop). */
252+
ESP_LOGI(TAG, "Hopped to channel %u (index %u/%u)",
253+
(unsigned)channel, (unsigned)s_hop_index, (unsigned)s_hop_count);
254+
}
255+
}
256+
257+
/**
258+
* Timer callback for channel hopping.
259+
* Called every s_dwell_ms milliseconds from the esp_timer context.
260+
*/
261+
static void hop_timer_cb(void *arg)
262+
{
263+
(void)arg;
264+
csi_hop_next_channel();
265+
}
266+
267+
void csi_collector_start_hop_timer(void)
268+
{
269+
if (s_hop_count <= 1) {
270+
ESP_LOGI(TAG, "Single-channel mode: hop timer not started");
271+
return;
272+
}
273+
274+
if (s_hop_timer != NULL) {
275+
ESP_LOGW(TAG, "Hop timer already running");
276+
return;
277+
}
278+
279+
esp_timer_create_args_t timer_args = {
280+
.callback = hop_timer_cb,
281+
.arg = NULL,
282+
.name = "csi_hop",
283+
};
284+
285+
esp_err_t err = esp_timer_create(&timer_args, &s_hop_timer);
286+
if (err != ESP_OK) {
287+
ESP_LOGE(TAG, "Failed to create hop timer: %s", esp_err_to_name(err));
288+
return;
289+
}
290+
291+
uint64_t period_us = (uint64_t)s_dwell_ms * 1000;
292+
err = esp_timer_start_periodic(s_hop_timer, period_us);
293+
if (err != ESP_OK) {
294+
ESP_LOGE(TAG, "Failed to start hop timer: %s", esp_err_to_name(err));
295+
esp_timer_delete(s_hop_timer);
296+
s_hop_timer = NULL;
297+
return;
298+
}
299+
300+
ESP_LOGI(TAG, "Hop timer started: period=%lu ms, channels=%u",
301+
(unsigned long)s_dwell_ms, (unsigned)s_hop_count);
302+
}
303+
304+
/* ---- ADR-029: NDP frame injection stub ---- */
305+
306+
esp_err_t csi_inject_ndp_frame(void)
307+
{
308+
/*
309+
* TODO: Construct a proper 802.11 Null Data Packet frame.
310+
*
311+
* A real NDP is preamble-only (~24 us airtime, no payload) and is the
312+
* sensing-first TX mechanism described in ADR-029. For now we send a
313+
* minimal null-data frame as a placeholder so the API is wired up.
314+
*
315+
* Frame structure (IEEE 802.11 Null Data):
316+
* FC (2) | Duration (2) | Addr1 (6) | Addr2 (6) | Addr3 (6) | SeqCtl (2)
317+
* = 24 bytes total, no body, no FCS (hardware appends FCS).
318+
*/
319+
uint8_t ndp_frame[24];
320+
memset(ndp_frame, 0, sizeof(ndp_frame));
321+
322+
/* Frame Control: Type=Data (0x02), Subtype=Null (0x04) -> 0x0048 */
323+
ndp_frame[0] = 0x48;
324+
ndp_frame[1] = 0x00;
325+
326+
/* Duration: 0 (let hardware fill) */
327+
328+
/* Addr1 (destination): broadcast */
329+
memset(&ndp_frame[4], 0xFF, 6);
330+
331+
/* Addr2 (source): will be overwritten by hardware with own MAC */
332+
333+
/* Addr3 (BSSID): broadcast */
334+
memset(&ndp_frame[16], 0xFF, 6);
335+
336+
esp_err_t err = esp_wifi_80211_tx(WIFI_IF_STA, ndp_frame, sizeof(ndp_frame), false);
337+
if (err != ESP_OK) {
338+
ESP_LOGW(TAG, "NDP inject failed: %s", esp_err_to_name(err));
339+
}
340+
341+
return err;
342+
}

firmware/esp32-csi-node/main/csi_collector.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
/** Maximum frame buffer size (header + 4 antennas * 256 subcarriers * 2 bytes). */
2020
#define CSI_MAX_FRAME_SIZE (CSI_HEADER_SIZE + 4 * 256 * 2)
2121

22+
/** Maximum number of channels in the hop table (ADR-029). */
23+
#define CSI_HOP_CHANNELS_MAX 6
24+
2225
/**
2326
* Initialize CSI collection.
2427
* Registers the WiFi CSI callback.
@@ -35,4 +38,47 @@ void csi_collector_init(void);
3538
*/
3639
size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf_len);
3740

41+
/**
42+
* Configure the channel-hop table for multi-band sensing (ADR-029).
43+
*
44+
* When hop_count == 1 the collector stays on the single configured channel
45+
* (backward-compatible with the original single-channel mode).
46+
*
47+
* @param channels Array of WiFi channel numbers (1-14 for 2.4 GHz, 36-177 for 5 GHz).
48+
* @param hop_count Number of entries in the channels array (1..CSI_HOP_CHANNELS_MAX).
49+
* @param dwell_ms Dwell time per channel in milliseconds (>= 10).
50+
*/
51+
void csi_collector_set_hop_table(const uint8_t *channels, uint8_t hop_count, uint32_t dwell_ms);
52+
53+
/**
54+
* Advance to the next channel in the hop table.
55+
*
56+
* Called by the hop timer callback. If hop_count <= 1 this is a no-op.
57+
* Calls esp_wifi_set_channel() internally.
58+
*/
59+
void csi_hop_next_channel(void);
60+
61+
/**
62+
* Start the channel-hop timer.
63+
*
64+
* Creates an esp_timer periodic callback that fires every dwell_ms
65+
* milliseconds, calling csi_hop_next_channel(). If hop_count <= 1
66+
* the timer is not started (single-channel backward-compatible mode).
67+
*/
68+
void csi_collector_start_hop_timer(void);
69+
70+
/**
71+
* Inject an NDP (Null Data Packet) frame for sensing.
72+
*
73+
* Uses esp_wifi_80211_tx() to send a preamble-only frame (~24 us airtime)
74+
* that triggers CSI measurement at all receivers. This is the "sensing-first"
75+
* TX mechanism described in ADR-029.
76+
*
77+
* @return ESP_OK on success, or an error code.
78+
*
79+
* @note TODO: Full NDP frame construction. Currently sends a minimal
80+
* null-data frame as a placeholder.
81+
*/
82+
esp_err_t csi_inject_ndp_frame(void);
83+
3884
#endif /* CSI_COLLECTOR_H */

firmware/esp32-csi-node/main/nvs_config.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ static const char *TAG = "nvs_config";
1818

1919
void nvs_config_load(nvs_config_t *cfg)
2020
{
21+
if (cfg == NULL) {
22+
ESP_LOGE(TAG, "nvs_config_load: cfg is NULL");
23+
return;
24+
}
25+
2126
/* Start with Kconfig compiled defaults */
2227
strncpy(cfg->wifi_ssid, CONFIG_CSI_WIFI_SSID, NVS_CFG_SSID_MAX - 1);
2328
cfg->wifi_ssid[NVS_CFG_SSID_MAX - 1] = '\0';
@@ -35,6 +40,17 @@ void nvs_config_load(nvs_config_t *cfg)
3540
cfg->target_port = (uint16_t)CONFIG_CSI_TARGET_PORT;
3641
cfg->node_id = (uint8_t)CONFIG_CSI_NODE_ID;
3742

43+
/* ADR-029: Defaults for channel hopping and TDM.
44+
* hop_count=1 means single-channel (backward-compatible). */
45+
cfg->channel_hop_count = 1;
46+
cfg->channel_list[0] = (uint8_t)CONFIG_CSI_WIFI_CHANNEL;
47+
for (uint8_t i = 1; i < NVS_CFG_HOP_MAX; i++) {
48+
cfg->channel_list[i] = 0;
49+
}
50+
cfg->dwell_ms = 50;
51+
cfg->tdm_slot_index = 0;
52+
cfg->tdm_node_count = 1;
53+
3854
/* Try to override from NVS */
3955
nvs_handle_t handle;
4056
esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle);
@@ -84,5 +100,64 @@ void nvs_config_load(nvs_config_t *cfg)
84100
ESP_LOGI(TAG, "NVS override: node_id=%u", cfg->node_id);
85101
}
86102

103+
/* ADR-029: Channel hop count */
104+
uint8_t hop_count_val;
105+
if (nvs_get_u8(handle, "hop_count", &hop_count_val) == ESP_OK) {
106+
if (hop_count_val >= 1 && hop_count_val <= NVS_CFG_HOP_MAX) {
107+
cfg->channel_hop_count = hop_count_val;
108+
ESP_LOGI(TAG, "NVS override: hop_count=%u", (unsigned)cfg->channel_hop_count);
109+
} else {
110+
ESP_LOGW(TAG, "NVS hop_count=%u out of range [1..%u], ignored",
111+
(unsigned)hop_count_val, (unsigned)NVS_CFG_HOP_MAX);
112+
}
113+
}
114+
115+
/* ADR-029: Channel list (stored as a blob of up to NVS_CFG_HOP_MAX bytes) */
116+
len = NVS_CFG_HOP_MAX;
117+
uint8_t ch_blob[NVS_CFG_HOP_MAX];
118+
if (nvs_get_blob(handle, "chan_list", ch_blob, &len) == ESP_OK && len > 0) {
119+
uint8_t count = (len < cfg->channel_hop_count) ? (uint8_t)len : cfg->channel_hop_count;
120+
for (uint8_t i = 0; i < count; i++) {
121+
cfg->channel_list[i] = ch_blob[i];
122+
}
123+
ESP_LOGI(TAG, "NVS override: chan_list loaded (%u channels)", (unsigned)count);
124+
}
125+
126+
/* ADR-029: Dwell time */
127+
uint32_t dwell_val;
128+
if (nvs_get_u32(handle, "dwell_ms", &dwell_val) == ESP_OK) {
129+
if (dwell_val >= 10) {
130+
cfg->dwell_ms = dwell_val;
131+
ESP_LOGI(TAG, "NVS override: dwell_ms=%lu", (unsigned long)cfg->dwell_ms);
132+
} else {
133+
ESP_LOGW(TAG, "NVS dwell_ms=%lu too small, ignored", (unsigned long)dwell_val);
134+
}
135+
}
136+
137+
/* ADR-029/031: TDM slot index */
138+
uint8_t slot_val;
139+
if (nvs_get_u8(handle, "tdm_slot", &slot_val) == ESP_OK) {
140+
cfg->tdm_slot_index = slot_val;
141+
ESP_LOGI(TAG, "NVS override: tdm_slot_index=%u", (unsigned)cfg->tdm_slot_index);
142+
}
143+
144+
/* ADR-029/031: TDM node count */
145+
uint8_t tdm_nodes_val;
146+
if (nvs_get_u8(handle, "tdm_nodes", &tdm_nodes_val) == ESP_OK) {
147+
if (tdm_nodes_val >= 1) {
148+
cfg->tdm_node_count = tdm_nodes_val;
149+
ESP_LOGI(TAG, "NVS override: tdm_node_count=%u", (unsigned)cfg->tdm_node_count);
150+
} else {
151+
ESP_LOGW(TAG, "NVS tdm_nodes=%u invalid, ignored", (unsigned)tdm_nodes_val);
152+
}
153+
}
154+
155+
/* Validate tdm_slot_index < tdm_node_count */
156+
if (cfg->tdm_slot_index >= cfg->tdm_node_count) {
157+
ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0",
158+
(unsigned)cfg->tdm_slot_index, (unsigned)cfg->tdm_node_count);
159+
cfg->tdm_slot_index = 0;
160+
}
161+
87162
nvs_close(handle);
88163
}

firmware/esp32-csi-node/main/nvs_config.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,23 @@
1818
#define NVS_CFG_PASS_MAX 65
1919
#define NVS_CFG_IP_MAX 16
2020

21+
/** Maximum channels in the hop list (must match CSI_HOP_CHANNELS_MAX). */
22+
#define NVS_CFG_HOP_MAX 6
23+
2124
/** Runtime configuration loaded from NVS or Kconfig defaults. */
2225
typedef struct {
2326
char wifi_ssid[NVS_CFG_SSID_MAX];
2427
char wifi_password[NVS_CFG_PASS_MAX];
2528
char target_ip[NVS_CFG_IP_MAX];
2629
uint16_t target_port;
2730
uint8_t node_id;
31+
32+
/* ADR-029: Channel hopping and TDM configuration */
33+
uint8_t channel_hop_count; /**< Number of channels to hop (1 = no hop). */
34+
uint8_t channel_list[NVS_CFG_HOP_MAX]; /**< Channel numbers for hopping. */
35+
uint32_t dwell_ms; /**< Dwell time per channel in ms. */
36+
uint8_t tdm_slot_index; /**< This node's TDM slot index (0-based). */
37+
uint8_t tdm_node_count; /**< Total nodes in the TDM schedule. */
2838
} nvs_config_t;
2939

3040
/**
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//! ESP32 hardware protocol modules.
2+
//!
3+
//! Implements sensing-first RF protocols for ESP32-S3 mesh nodes,
4+
//! including TDM (Time-Division Multiplexed) sensing schedules
5+
//! per ADR-029 (RuvSense) and ADR-031 (RuView).
6+
7+
pub mod tdm;
8+
9+
pub use tdm::{
10+
TdmSchedule, TdmCoordinator, TdmSlot, TdmSlotCompleted,
11+
SyncBeacon, TdmError,
12+
};

0 commit comments

Comments
 (0)