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

Skip to content

Commit ce17169

Browse files
authored
fix: rate-limit CSI sends and add ENOMEM backoff to prevent crash (ruvnet#132)
The CSI callback fires for every WiFi frame in promiscuous mode (100-500+ fps). Each call invoked sendto() synchronously, exhausting lwIP packet buffers (errno 12 = ENOMEM). The rapid-fire failures cascaded into a LoadProhibited guru meditation crash. Two fixes: 1. csi_collector.c: Rate-limit UDP sends to 50 Hz (20ms interval). CSI frames arriving between sends are dropped — the sensing pipeline only needs 20-50 Hz. 2. stream_sender.c: When sendto fails with ENOMEM, suppress further sends for 100ms to let lwIP reclaim buffers. Logs the backoff event and resumes automatically. Closes ruvnet#127
1 parent b544545 commit ce17169

2 files changed

Lines changed: 66 additions & 8 deletions

File tree

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ static uint32_t s_sequence = 0;
2727
static uint32_t s_cb_count = 0;
2828
static uint32_t s_send_ok = 0;
2929
static uint32_t s_send_fail = 0;
30+
static uint32_t s_rate_skip = 0;
31+
32+
/**
33+
* Minimum interval between UDP sends in microseconds.
34+
* CSI callbacks can fire hundreds of times per second in promiscuous mode.
35+
* We cap the send rate to avoid exhausting lwIP packet buffers (ENOMEM).
36+
* Default: 20 ms = 50 Hz max send rate.
37+
*/
38+
#define CSI_MIN_SEND_INTERVAL_US (20 * 1000)
39+
static int64_t s_last_send_us = 0;
3040

3141
/* ---- ADR-029: Channel-hop state ---- */
3242

@@ -143,14 +153,23 @@ static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info)
143153
size_t frame_len = csi_serialize_frame(info, frame_buf, sizeof(frame_buf));
144154

145155
if (frame_len > 0) {
146-
int ret = stream_sender_send(frame_buf, frame_len);
147-
if (ret > 0) {
148-
s_send_ok++;
149-
} else {
150-
s_send_fail++;
151-
if (s_send_fail <= 5) {
152-
ESP_LOGW(TAG, "sendto failed (fail #%lu)", (unsigned long)s_send_fail);
156+
/* Rate-limit UDP sends to avoid ENOMEM from lwIP pbuf exhaustion.
157+
* In promiscuous mode, CSI callbacks can fire 100-500+ times/sec.
158+
* We only need 20-50 Hz for the sensing pipeline. */
159+
int64_t now = esp_timer_get_time();
160+
if ((now - s_last_send_us) >= CSI_MIN_SEND_INTERVAL_US) {
161+
int ret = stream_sender_send(frame_buf, frame_len);
162+
if (ret > 0) {
163+
s_send_ok++;
164+
s_last_send_us = now;
165+
} else {
166+
s_send_fail++;
167+
if (s_send_fail <= 5) {
168+
ESP_LOGW(TAG, "sendto failed (fail #%lu)", (unsigned long)s_send_fail);
169+
}
153170
}
171+
} else {
172+
s_rate_skip++;
154173
}
155174
}
156175

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <string.h>
1111
#include "esp_log.h"
12+
#include "esp_timer.h"
1213
#include "lwip/sockets.h"
1314
#include "lwip/netdb.h"
1415
#include "sdkconfig.h"
@@ -18,6 +19,17 @@ static const char *TAG = "stream_sender";
1819
static int s_sock = -1;
1920
static struct sockaddr_in s_dest_addr;
2021

22+
/**
23+
* ENOMEM backoff state.
24+
* When sendto fails with ENOMEM (errno 12), we suppress further sends for
25+
* a cooldown period to let lwIP reclaim packet buffers. Without this,
26+
* rapid-fire CSI callbacks can exhaust the pbuf pool and crash the device.
27+
*/
28+
static int64_t s_backoff_until_us = 0; /* esp_timer timestamp to resume */
29+
#define ENOMEM_COOLDOWN_MS 100 /* suppress sends for 100 ms */
30+
#define ENOMEM_LOG_INTERVAL 50 /* log every Nth suppressed send */
31+
static uint32_t s_enomem_suppressed = 0;
32+
2133
static int sender_init_internal(const char *ip, uint16_t port)
2234
{
2335
s_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
@@ -57,10 +69,37 @@ int stream_sender_send(const uint8_t *data, size_t len)
5769
return -1;
5870
}
5971

72+
/* ENOMEM backoff: if we recently exhausted lwIP buffers, skip sends
73+
* until the cooldown expires. This prevents the cascade of failed
74+
* sendto calls that leads to a guru meditation crash. */
75+
if (s_backoff_until_us > 0) {
76+
int64_t now = esp_timer_get_time();
77+
if (now < s_backoff_until_us) {
78+
s_enomem_suppressed++;
79+
if ((s_enomem_suppressed % ENOMEM_LOG_INTERVAL) == 1) {
80+
ESP_LOGW(TAG, "sendto suppressed (ENOMEM backoff, %lu dropped)",
81+
(unsigned long)s_enomem_suppressed);
82+
}
83+
return -1;
84+
}
85+
/* Cooldown expired — resume sending */
86+
ESP_LOGI(TAG, "ENOMEM backoff expired, resuming sends (%lu were suppressed)",
87+
(unsigned long)s_enomem_suppressed);
88+
s_backoff_until_us = 0;
89+
s_enomem_suppressed = 0;
90+
}
91+
6092
int sent = sendto(s_sock, data, len, 0,
6193
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
6294
if (sent < 0) {
63-
ESP_LOGW(TAG, "sendto failed: errno %d", errno);
95+
if (errno == ENOMEM) {
96+
/* Start backoff to let lwIP reclaim buffers */
97+
s_backoff_until_us = esp_timer_get_time() +
98+
(int64_t)ENOMEM_COOLDOWN_MS * 1000;
99+
ESP_LOGW(TAG, "sendto ENOMEM — backing off for %d ms", ENOMEM_COOLDOWN_MS);
100+
} else {
101+
ESP_LOGW(TAG, "sendto failed: errno %d", errno);
102+
}
64103
return -1;
65104
}
66105

0 commit comments

Comments
 (0)