Fix telemetry: snprintf truncation, open/write for SD, append NDJSON, capture LED

- mcs_telemetry: fix snprintf truncation in to_json (advance offset by actual
  bytes written, reserve tail for closing brackets)
- sd_card: use open/write/close instead of fwrite to bypass stdio buffering
- wifi_controller: append telemetry as NDJSON (one object per line)
- status_led: blink blue only when frames captured in monitor mode

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Robert McMahon 2026-02-07 21:39:57 -08:00
parent 2b7ef9cb19
commit 74eb6cb553
5 changed files with 70 additions and 24 deletions

View File

@ -302,30 +302,40 @@ esp_err_t mcs_telemetry_to_json(char *json_buffer, size_t buffer_len, const char
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
#define MCS_JSON_TAIL_RESERVE 4 /* room for "]\0" and safety */
if (buffer_len <= MCS_JSON_TAIL_RESERVE) {
return ESP_ERR_NO_MEM;
}
uint32_t now_ms = esp_timer_get_time() / 1000; uint32_t now_ms = esp_timer_get_time() / 1000;
int written = snprintf(json_buffer, buffer_len, int written = snprintf(json_buffer, buffer_len,
"{\"device_id\":\"%s\",\"timestamp\":%lu,\"total_frames\":%lu,\"devices\":[", "{\"device_id\":\"%s\",\"timestamp\":%lu,\"total_frames\":%lu,\"devices\":[",
device_id ? device_id : "unknown", now_ms, s_stats.total_frames_captured); device_id ? device_id : "unknown", now_ms, s_stats.total_frames_captured);
if (written < 0 || written >= buffer_len) { if (written < 0 || (size_t)written >= buffer_len) {
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
int offset = written; size_t offset = (size_t)written;
bool first = true; bool first = true;
for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES && offset < buffer_len - 100; i++) { for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES; i++) {
mcs_device_telemetry_t *dev = &s_stats.devices[i]; mcs_device_telemetry_t *dev = &s_stats.devices[i];
if (dev->sample_count == 0) continue; if (dev->sample_count == 0) continue;
size_t space = buffer_len - offset - MCS_JSON_TAIL_RESERVE;
if (space < 2) break;
if (!first) { if (!first) {
written = snprintf(json_buffer + offset, buffer_len - offset, ","); written = snprintf(json_buffer + offset, space, ",");
if (written < 0) break; if (written < 0) break;
offset += written; offset += (size_t)((written < (int)space) ? written : (space - 1));
space = buffer_len - offset - MCS_JSON_TAIL_RESERVE;
if (space < 2) break;
} }
first = false; first = false;
written = snprintf(json_buffer + offset, buffer_len - offset, written = snprintf(json_buffer + offset, space,
"{\"mac\":\"%02x:%02x:%02x:%02x:%02x:%02x\"," "{\"mac\":\"%02x:%02x:%02x:%02x:%02x:%02x\","
"\"mcs\":%u,\"ss\":%u,\"rssi\":%d," "\"mcs\":%u,\"ss\":%u,\"rssi\":%d,"
"\"channel\":%u,\"bandwidth\":%u," "\"channel\":%u,\"bandwidth\":%u,"
@ -338,15 +348,22 @@ esp_err_t mcs_telemetry_to_json(char *json_buffer, size_t buffer_len, const char
dev->avg_phy_rate_kbps); dev->avg_phy_rate_kbps);
if (written < 0) break; if (written < 0) break;
offset += written; offset += (size_t)((written < (int)space) ? written : (space - 1));
} }
written = snprintf(json_buffer + offset, buffer_len - offset, "]}"); {
if (written < 0) { size_t tail_space = buffer_len - offset;
return ESP_ERR_NO_MEM; if (tail_space < 3) {
return ESP_ERR_NO_MEM;
}
written = snprintf(json_buffer + offset, tail_space, "]}");
if (written < 0 || (size_t)written >= tail_space) {
return ESP_ERR_NO_MEM;
}
} }
return ESP_OK; return ESP_OK;
#undef MCS_JSON_TAIL_RESERVE
} }
uint32_t mcs_calculate_phy_rate_ax(uint8_t mcs, uint8_t ss, mcs_bandwidth_t bandwidth, bool sgi) { uint32_t mcs_calculate_phy_rate_ax(uint8_t mcs, uint8_t ss, mcs_bandwidth_t bandwidth, bool sgi) {

View File

@ -42,6 +42,7 @@
#include <stdio.h> #include <stdio.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/unistd.h> #include <sys/unistd.h>
#include <fcntl.h>
#include <dirent.h> #include <dirent.h>
// Pin definitions for SparkFun microSD Transflash Breakout // Pin definitions for SparkFun microSD Transflash Breakout
@ -255,22 +256,27 @@ esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len,
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
const char *mode = append ? "a" : "w"; int flags = O_WRONLY | O_CREAT;
FILE *f = fopen(full_path, mode); if (append) {
if (f == NULL) { flags |= O_APPEND;
} else {
flags |= O_TRUNC;
}
int fd = open(full_path, flags, 0644);
if (fd < 0) {
ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path); ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path);
return ESP_FAIL; return ESP_FAIL;
} }
size_t written = fwrite(data, 1, len, f); ssize_t written = write(fd, data, len);
fclose(f); close(fd);
if (written != len) { if (written < 0 || (size_t)written != len) {
ESP_LOGE(TAG, "Failed to write all data: wrote %zu of %zu bytes", written, len); ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len);
return ESP_FAIL; return ESP_FAIL;
} }
ESP_LOGD(TAG, "Wrote %zu bytes to %s", written, full_path); ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path);
return ESP_OK; return ESP_OK;
} }

View File

@ -43,6 +43,7 @@ static led_strip_handle_t s_led_strip = NULL;
static bool s_is_rgb = false; static bool s_is_rgb = false;
static int s_gpio_pin = -1; static int s_gpio_pin = -1;
static volatile led_state_t s_current_state = LED_STATE_NO_CONFIG; static volatile led_state_t s_current_state = LED_STATE_NO_CONFIG;
static volatile bool s_capture_active = false;
static void set_color(uint8_t r, uint8_t g, uint8_t b) { static void set_color(uint8_t r, uint8_t g, uint8_t b) {
if (s_is_rgb && s_led_strip) { if (s_is_rgb && s_led_strip) {
@ -69,8 +70,13 @@ static void led_task(void *arg) {
case LED_STATE_CONNECTED: case LED_STATE_CONNECTED:
set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000)); set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000));
break; break;
case LED_STATE_MONITORING: case LED_STATE_MONITORING: /* Blink blue only when frames being captured */
set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000)); if (s_capture_active) {
set_color(0, 0, toggle ? 50 : 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(300));
} else {
set_color(0, 0, 10); vTaskDelay(pdMS_TO_TICKS(1000)); /* Dim solid: monitor on, no capture */
}
break; break;
case LED_STATE_TRANSMITTING: case LED_STATE_TRANSMITTING:
set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle; set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
@ -116,6 +122,6 @@ void status_led_init(int gpio_pin, bool is_rgb_strip) {
xTaskCreate(led_task, "led_task", 2048, NULL, 5, NULL); xTaskCreate(led_task, "led_task", 2048, NULL, 5, NULL);
} }
// ... Setters/Getters ...
void status_led_set_state(led_state_t state) { s_current_state = state; } void status_led_set_state(led_state_t state) { s_current_state = state; }
led_state_t status_led_get_state(void) { return s_current_state; } led_state_t status_led_get_state(void) { return s_current_state; }
void status_led_set_capture_active(bool active) { s_capture_active = active; }

View File

@ -70,6 +70,12 @@ void status_led_set_state(led_state_t state);
*/ */
led_state_t status_led_get_state(void); led_state_t status_led_get_state(void);
/**
* @brief Set capture-active flag (frames being captured in monitor mode)
* Used with LED_STATE_MONITORING: blink blue only when capture_active is true.
*/
void status_led_set_capture_active(bool active);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -91,6 +91,7 @@ static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t
(void)payload; (void)payload;
(void)len; (void)len;
s_monitor_frame_count++; s_monitor_frame_count++;
status_led_set_capture_active(true);
if (frame->retry && frame->duration_id > 5000) { if (frame->retry && frame->duration_id > 5000) {
log_collapse_event((float)frame->duration_id, frame->rssi, frame->retry); log_collapse_event((float)frame->duration_id, frame->rssi, frame->retry);
} }
@ -102,8 +103,13 @@ static void monitor_stats_task(void *arg) {
(void)arg; (void)arg;
static char json_buf[FIWI_TELEMETRY_JSON_BUF_SIZE]; static char json_buf[FIWI_TELEMETRY_JSON_BUF_SIZE];
uint32_t flush_count = 0; uint32_t flush_count = 0;
uint32_t last_frame_count = 0;
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(10000)); vTaskDelay(pdMS_TO_TICKS(10000));
if (s_monitor_frame_count == last_frame_count) {
status_led_set_capture_active(false);
}
last_frame_count = s_monitor_frame_count;
wifi_collapse_stats_t stats; wifi_collapse_stats_t stats;
if (wifi_monitor_get_stats(&stats) == ESP_OK) { if (wifi_monitor_get_stats(&stats) == ESP_OK) {
ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---", ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
@ -113,9 +119,12 @@ static void monitor_stats_task(void *arg) {
/* Write MCS telemetry to fiwi-telemetry on SD card (default on monitor start) */ /* Write MCS telemetry to fiwi-telemetry on SD card (default on monitor start) */
if (sd_card_is_ready() && mcs_telemetry_to_json(json_buf, sizeof(json_buf), "esp32") == ESP_OK) { if (sd_card_is_ready() && mcs_telemetry_to_json(json_buf, sizeof(json_buf), "esp32") == ESP_OK) {
size_t len = strlen(json_buf); size_t len = strlen(json_buf);
if (len > 0 && sd_card_write_file(FIWI_TELEMETRY_FILE, json_buf, len, false) == ESP_OK) { if (len > 0) {
flush_count++; json_buf[len] = '\n'; /* NDJSON: one object per line */
ESP_LOGD(TAG, "fiwi-telemetry flushed (#%lu)", (unsigned long)flush_count); if (sd_card_write_file(FIWI_TELEMETRY_FILE, json_buf, len + 1, true) == ESP_OK) {
flush_count++;
ESP_LOGD(TAG, "fiwi-telemetry flushed (#%lu)", (unsigned long)flush_count);
}
} }
} }
} }
@ -212,6 +221,7 @@ esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
vTaskDelay(pdMS_TO_TICKS(500)); vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_set_mode(WIFI_MODE_NULL); esp_wifi_set_mode(WIFI_MODE_NULL);
status_led_set_capture_active(false);
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) { if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
ESP_LOGE(TAG, "Failed to init monitor mode"); ESP_LOGE(TAG, "Failed to init monitor mode");
return ESP_FAIL; return ESP_FAIL;
@ -257,6 +267,7 @@ esp_err_t wifi_ctl_switch_to_sta(void) {
} }
if (s_monitor_enabled) { if (s_monitor_enabled) {
status_led_set_capture_active(false);
mcs_telemetry_stop(); mcs_telemetry_stop();
wifi_monitor_stop(); wifi_monitor_stop();
s_monitor_enabled = false; s_monitor_enabled = false;