Compare commits

..

No commits in common. "07b328351856ef370785bc43d95187dfa5183dd2" and "a4e81c9852e1e173da867c6310b6f4ac99335dd1" have entirely different histories.

21 changed files with 7 additions and 1902 deletions

View File

@ -10,7 +10,7 @@ idf_component_register(
"cmd_ip.c" "cmd_ip.c"
"cmd_sdcard.c" "cmd_sdcard.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES console wifi_cfg sd_card sdcard_http REQUIRES console wifi_cfg sd_card
wifi_controller iperf status_led gps_sync wifi_controller iperf status_led gps_sync
esp_wifi esp_netif nvs_flash spi_flash esp_wifi esp_netif nvs_flash spi_flash
) )

View File

@ -33,29 +33,12 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <inttypes.h>
#include "esp_console.h" #include "esp_console.h"
#include "argtable3/argtable3.h" #include "argtable3/argtable3.h"
#include "app_console.h" #include "app_console.h"
#include "sd_card.h" #include "sd_card.h"
#include "sdcard_http.h"
#define SDCARD_READ_BUF_SIZE 4096 #define SDCARD_READ_BUF_SIZE 4096
#define SDCARD_SEND_CHUNK 256
#define SDCARD_SEND_MAX (512 * 1024) /* 512 KB max over serial */
/* Format bytes as human-readable (e.g. 1.2K, 4.5M). Writes into buf, max len chars. */
static void fmt_size_human(size_t bytes, char *buf, size_t len) {
if (bytes < 1024) {
snprintf(buf, len, "%zu B", bytes);
} else if (bytes < 1024 * 1024) {
snprintf(buf, len, "%.1f K", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024 * 1024) {
snprintf(buf, len, "%.1f M", bytes / (1024.0 * 1024.0));
} else {
snprintf(buf, len, "%.1f G", bytes / (1024.0 * 1024.0 * 1024.0));
}
}
static int do_sdcard_status(int argc, char **argv) { static int do_sdcard_status(int argc, char **argv) {
(void)argc; (void)argc;
@ -82,23 +65,6 @@ static int do_sdcard_status(int argc, char **argv) {
printf(" Total: %.2f MB\n", total / (1024.0 * 1024.0)); printf(" Total: %.2f MB\n", total / (1024.0 * 1024.0));
printf(" Free: %.2f MB\n", free_bytes / (1024.0 * 1024.0)); printf(" Free: %.2f MB\n", free_bytes / (1024.0 * 1024.0));
} }
if (sd_card_file_exists("fiwi-telemetry")) {
size_t sz = 0;
if (sd_card_get_file_size("fiwi-telemetry", &sz) == 0) {
char hr[16];
fmt_size_human(sz, hr, sizeof(hr));
printf(" fiwi-telemetry: yes, %s (%zu bytes)\n", hr, sz);
} else {
printf(" fiwi-telemetry: yes, ?\n");
}
} else {
printf(" fiwi-telemetry: none\n");
}
uint32_t attempts = 0, downloads = 0;
sdcard_http_get_telemetry_stats(&attempts, &downloads);
printf(" telemetry HTTP: %" PRIu32 " attempts, %" PRIu32 " downloads\n", attempts, downloads);
printf(" telemetry-status: %s (timestamps + bytes per download)\n",
sd_card_file_exists("telemetry-status") ? "yes" : "none");
} }
return 0; return 0;
} }
@ -170,133 +136,23 @@ static int do_sdcard_read(int argc, char **argv) {
return 0; return 0;
} }
/* Serial file transfer: output hex-encoded file for host script (e.g. sdcard_recv.py) */
static int do_sdcard_send(int argc, char **argv) {
if (argc < 2) {
printf("Usage: sdcard send <file>\n");
printf(" Streams file over serial (hex). Use tools/sdcard_recv.py on host to receive.\n");
return 1;
}
const char *filename = argv[1];
if (!sd_card_is_ready()) {
printf("Error: SD card not mounted\n");
return 1;
}
if (!sd_card_file_exists(filename)) {
printf("Error: File not found: %s\n", filename);
return 1;
}
size_t file_size = 0;
if (sd_card_get_file_size(filename, &file_size) != 0) {
printf("Error: Could not get file size\n");
return 1;
}
if (file_size > SDCARD_SEND_MAX) {
printf("Error: File too large for serial transfer (max %u KB)\n", (unsigned)(SDCARD_SEND_MAX / 1024));
return 1;
}
/* Protocol: ---SDFILE--- \n filename \n SIZE: N \n ---HEX--- \n <hex lines> ---END SDFILE--- */
printf("---SDFILE---\n%s\nSIZE:%zu\n---HEX---\n", filename, file_size);
fflush(stdout);
static uint8_t chunk[SDCARD_SEND_CHUNK];
size_t offset = 0;
while (offset < file_size) {
size_t to_read = file_size - offset;
if (to_read > sizeof(chunk)) {
to_read = sizeof(chunk);
}
size_t n = 0;
if (sd_card_read_file_at(filename, offset, chunk, to_read, &n) != 0 || n == 0) {
printf("\nError: Read failed at offset %zu\n", offset);
return 1;
}
for (size_t i = 0; i < n; i++) {
printf("%02x", (unsigned char)chunk[i]);
}
printf("\n");
fflush(stdout);
offset += n;
}
printf("---END SDFILE---\n");
fflush(stdout);
return 0;
}
static int do_sdcard_list(int argc, char **argv) {
const char *path = (argc >= 2) ? argv[1] : "";
if (!sd_card_is_ready()) {
printf("Error: SD card not mounted\n");
return 1;
}
printf("SD card: %s\n", path[0] ? path : "/");
esp_err_t ret = sd_card_list_dir(path);
if (ret != ESP_OK) {
printf("Error: Cannot list directory: %s\n", esp_err_to_name(ret));
return 1;
}
return 0;
}
static int do_sdcard_delete(int argc, char **argv) {
if (argc < 2) {
printf("Usage: sdcard delete <file>\n");
return 1;
}
const char *filename = argv[1];
if (!sd_card_is_ready()) {
printf("Error: SD card not mounted\n");
return 1;
}
if (!sd_card_file_exists(filename)) {
printf("Error: File not found: %s\n", filename);
return 1;
}
esp_err_t ret = sd_card_delete_file(filename);
if (ret != ESP_OK) {
printf("Error: Delete failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("Deleted: %s\n", filename);
return 0;
}
static int cmd_sdcard(int argc, char **argv) { static int cmd_sdcard(int argc, char **argv) {
if (argc < 2) { if (argc < 2) {
printf("Usage: sdcard <status|list|write|read|send|delete> [args]\n"); printf("Usage: sdcard <status|write|read> [args]\n");
printf(" status - Show CD, mounted, capacity\n"); printf(" status - Show CD, mounted, capacity\n");
printf(" list [path] - List files (path optional, default root)\n");
printf(" write <f> <t> - Write text to file\n"); printf(" write <f> <t> - Write text to file\n");
printf(" read <f> - Read and print file\n"); printf(" read <f> - Read and print file\n");
printf(" send <f> - Stream file over serial (use tools/sdcard_recv.py)\n");
printf(" delete <f> - Delete a file\n");
return 0; return 0;
} }
if (strcmp(argv[1], "status") == 0) { if (strcmp(argv[1], "status") == 0) {
return do_sdcard_status(argc - 1, &argv[1]); return do_sdcard_status(argc - 1, &argv[1]);
} }
if (strcmp(argv[1], "list") == 0 || strcmp(argv[1], "ls") == 0) {
return do_sdcard_list(argc - 1, &argv[1]);
}
if (strcmp(argv[1], "write") == 0) { if (strcmp(argv[1], "write") == 0) {
return do_sdcard_write(argc - 1, &argv[1]); return do_sdcard_write(argc - 1, &argv[1]);
} }
if (strcmp(argv[1], "read") == 0) { if (strcmp(argv[1], "read") == 0) {
return do_sdcard_read(argc - 1, &argv[1]); return do_sdcard_read(argc - 1, &argv[1]);
} }
if (strcmp(argv[1], "send") == 0) {
return do_sdcard_send(argc - 1, &argv[1]);
}
if (strcmp(argv[1], "delete") == 0 || strcmp(argv[1], "rm") == 0) {
return do_sdcard_delete(argc - 1, &argv[1]);
}
printf("Unknown subcommand '%s'\n", argv[1]); printf("Unknown subcommand '%s'\n", argv[1]);
return 1; return 1;
} }
@ -304,8 +160,8 @@ static int cmd_sdcard(int argc, char **argv) {
void register_sdcard_cmd(void) { void register_sdcard_cmd(void) {
const esp_console_cmd_t cmd = { const esp_console_cmd_t cmd = {
.command = "sdcard", .command = "sdcard",
.help = "SD card: status, list [path], write, read, send, delete <file>", .help = "SD card: status (CD, capacity), write <file> <text>, read <file>",
.hint = "<status|list|write|read|send|delete>", .hint = "<status|write|read>",
.func = &cmd_sdcard, .func = &cmd_sdcard,
}; };
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));

View File

@ -1,6 +0,0 @@
idf_component_register(
SRCS "mcs_telemetry.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi esp_timer wifi_monitor
)

View File

@ -1,371 +0,0 @@
/*
* mcs_telemetry.c
*
* Copyright (c) 2026 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "mcs_telemetry.h"
#include "wifi_monitor.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <string.h>
#include <time.h>
static const char *TAG = "MCS_Telemetry";
static bool s_telemetry_running = false;
static mcs_telemetry_cb_t s_user_callback = NULL;
static mcs_telemetry_stats_t s_stats = {0};
// 802.11ax MCS rates for 20MHz, 800ns GI, 1 Spatial Stream
// MCS 0-11: Standard HE rates
static const uint32_t PHY_RATES_20MHZ_1SS_800NS[] = {
8650, // MCS 0: BPSK 1/2
17200, // MCS 1: QPSK 1/2
25800, // MCS 2: QPSK 3/4
34400, // MCS 3: 16-QAM 1/2
51600, // MCS 4: 16-QAM 3/4
68800, // MCS 5: 64-QAM 2/3
77400, // MCS 6: 64-QAM 3/4
86000, // MCS 7: 64-QAM 5/6
103200, // MCS 8: 256-QAM 3/4
114700, // MCS 9: 256-QAM 5/6
129000, // MCS 10: 1024-QAM 3/4
143400 // MCS 11: 1024-QAM 5/6
};
// 802.11ax MCS rates for 20MHz, 400ns GI, 1 Spatial Stream
static const uint32_t PHY_RATES_20MHZ_1SS_400NS[] = {
9600, // MCS 0
19200, // MCS 1
28800, // MCS 2
38400, // MCS 3
57600, // MCS 4
76800, // MCS 5
86400, // MCS 6
96000, // MCS 7
115200, // MCS 8
128100, // MCS 9
144100, // MCS 10
160200 // MCS 11
};
// 802.11ax MCS rates for 40MHz, 800ns GI, 1 Spatial Stream
static const uint32_t PHY_RATES_40MHZ_1SS_800NS[] = {
17200, // MCS 0
34400, // MCS 1
51600, // MCS 2
68800, // MCS 3
103200, // MCS 4
137600, // MCS 5
154900, // MCS 6
172100, // MCS 7
206500, // MCS 8
229400, // MCS 9
258100, // MCS 10
286800 // MCS 11
};
// 802.11ax MCS rates for 40MHz, 400ns GI, 1 Spatial Stream
static const uint32_t PHY_RATES_40MHZ_1SS_400NS[] = {
19200, // MCS 0
38400, // MCS 1
57600, // MCS 2
76800, // MCS 3
115200, // MCS 4
153600, // MCS 5
172800, // MCS 6
192000, // MCS 7
230400, // MCS 8
256200, // MCS 9
288200, // MCS 10
320300 // MCS 11
};
/**
* @brief Get device index by MAC address, or create new entry
*/
static int mcs_get_device_index(const uint8_t *mac) {
int empty_slot = -1;
for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES; i++) {
if (memcmp(s_stats.devices[i].mac, mac, 6) == 0) {
return i; // Found existing device
}
if (empty_slot < 0 && s_stats.devices[i].sample_count == 0) {
empty_slot = i; // Found empty slot
}
}
// Use empty slot or create new entry
if (empty_slot >= 0) {
memcpy(s_stats.devices[empty_slot].mac, mac, 6);
memset(&s_stats.devices[empty_slot], 0, sizeof(mcs_device_telemetry_t));
memcpy(s_stats.devices[empty_slot].mac, mac, 6);
s_stats.total_devices++;
return empty_slot;
}
// No space - return oldest device (simple round-robin replacement)
return 0;
}
/**
* @brief Update device telemetry with new sample
*/
static void mcs_update_device_telemetry(int dev_idx, const mcs_sample_t *sample) {
if (dev_idx < 0 || dev_idx >= MCS_TELEMETRY_MAX_DEVICES) {
return;
}
mcs_device_telemetry_t *dev = &s_stats.devices[dev_idx];
// Update sample buffer (sliding window)
dev->samples[dev->sample_idx] = *sample;
dev->sample_idx = (dev->sample_idx + 1) % 16;
// Update counters
dev->sample_count++;
dev->total_frames++;
dev->total_bytes += sample->frame_len;
dev->last_update_ms = sample->timestamp_ms;
if (sample->is_retry) {
dev->retry_frames++;
}
// Update MCS distribution
if (sample->mcs <= MCS_TELEMETRY_MAX_MCS) {
dev->mcs_count[sample->mcs]++;
// Update dominant MCS (most frequent in recent samples)
uint32_t max_count = 0;
for (int i = 0; i <= MCS_TELEMETRY_MAX_MCS; i++) {
if (dev->mcs_count[i] > max_count) {
max_count = dev->mcs_count[i];
dev->current_mcs = i;
}
}
}
// Update SS distribution
if (sample->ss >= 1 && sample->ss <= MCS_TELEMETRY_MAX_SS) {
dev->ss_count[sample->ss]++;
// Update dominant SS
uint32_t max_count = 0;
for (int i = 1; i <= MCS_TELEMETRY_MAX_SS; i++) {
if (dev->ss_count[i] > max_count) {
max_count = dev->ss_count[i];
dev->current_ss = i;
}
}
}
// Update RSSI statistics
if (dev->sample_count == 1) {
dev->avg_rssi = sample->rssi;
dev->min_rssi = sample->rssi;
dev->max_rssi = sample->rssi;
} else {
// Running average
dev->avg_rssi = ((int16_t)dev->avg_rssi * (dev->sample_count - 1) + sample->rssi) / dev->sample_count;
if (sample->rssi < dev->min_rssi) dev->min_rssi = sample->rssi;
if (sample->rssi > dev->max_rssi) dev->max_rssi = sample->rssi;
}
// Update PHY rate statistics
uint32_t total_rate = dev->avg_phy_rate_kbps * (dev->sample_count - 1) + sample->phy_rate_kbps;
dev->avg_phy_rate_kbps = total_rate / dev->sample_count;
if (sample->phy_rate_kbps > dev->max_phy_rate_kbps) {
dev->max_phy_rate_kbps = sample->phy_rate_kbps;
}
}
esp_err_t mcs_telemetry_init(mcs_telemetry_cb_t callback) {
ESP_LOGI(TAG, "Initializing MCS telemetry");
s_user_callback = callback;
s_telemetry_running = false;
memset(&s_stats, 0, sizeof(mcs_telemetry_stats_t));
return ESP_OK;
}
esp_err_t mcs_telemetry_start(void) {
ESP_LOGI(TAG, "Starting MCS telemetry capture");
s_telemetry_running = true;
s_stats.window_start_ms = esp_timer_get_time() / 1000;
return ESP_OK;
}
esp_err_t mcs_telemetry_stop(void) {
ESP_LOGI(TAG, "Stopping MCS telemetry capture");
s_telemetry_running = false;
s_stats.window_end_ms = esp_timer_get_time() / 1000;
return ESP_OK;
}
esp_err_t mcs_telemetry_process_frame(const wifi_frame_info_t *frame_info, const wifi_pkt_rx_ctrl_t *rx_ctrl) {
(void)rx_ctrl; /* Optional: not used, frame_info contains RX metadata */
if (!s_telemetry_running || !frame_info) {
return ESP_ERR_INVALID_ARG;
}
// Get device index by MAC address (Addr2 = transmitter)
int dev_idx = mcs_get_device_index(frame_info->addr2);
if (dev_idx < 0) {
return ESP_ERR_NO_MEM;
}
// Create sample from frame info
mcs_sample_t sample = {0};
sample.timestamp_ms = esp_timer_get_time() / 1000;
sample.mcs = frame_info->mcs;
sample.ss = 1; // TODO: Extract from HT/VHT/HE headers
sample.rssi = frame_info->rssi;
sample.channel = frame_info->channel;
sample.bandwidth = (frame_info->bandwidth == 0) ? MCS_BW_20MHZ :
(frame_info->bandwidth == 1) ? MCS_BW_40MHZ : MCS_BW_20MHZ;
sample.frame_len = frame_info->frame_len;
sample.is_retry = frame_info->retry;
sample.sig_mode = frame_info->sig_mode;
// Calculate PHY rate if we have MCS info
if (sample.mcs <= MCS_TELEMETRY_MAX_MCS && sample.ss >= 1 && sample.ss <= MCS_TELEMETRY_MAX_SS) {
sample.phy_rate_kbps = mcs_calculate_phy_rate_ax(sample.mcs, sample.ss, sample.bandwidth, frame_info->sgi);
} else {
// Fallback to frame_info's calculated rate or estimate from rate index
sample.phy_rate_kbps = frame_info->phy_rate_kbps;
}
// Update device telemetry
mcs_update_device_telemetry(dev_idx, &sample);
s_stats.total_frames_captured++;
// TODO: Parse HT/VHT/HE headers to extract actual SS count
// This requires parsing the PLCP/HT Control/VHT Control/HE Control fields
// which follow the MAC header in HT/VHT/HE frames
return ESP_OK;
}
esp_err_t mcs_telemetry_get_stats(mcs_telemetry_stats_t *stats) {
if (!stats) {
return ESP_ERR_INVALID_ARG;
}
memcpy(stats, &s_stats, sizeof(mcs_telemetry_stats_t));
return ESP_OK;
}
void mcs_telemetry_reset(void) {
ESP_LOGI(TAG, "Resetting telemetry statistics");
memset(&s_stats, 0, sizeof(mcs_telemetry_stats_t));
}
esp_err_t mcs_telemetry_to_json(char *json_buffer, size_t buffer_len, const char *device_id) {
if (!json_buffer || buffer_len == 0) {
return ESP_ERR_INVALID_ARG;
}
uint32_t now_ms = esp_timer_get_time() / 1000;
int written = snprintf(json_buffer, buffer_len,
"{\"device_id\":\"%s\",\"timestamp\":%lu,\"total_frames\":%lu,\"devices\":[",
device_id ? device_id : "unknown", now_ms, s_stats.total_frames_captured);
if (written < 0 || written >= buffer_len) {
return ESP_ERR_NO_MEM;
}
int offset = written;
bool first = true;
for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES && offset < buffer_len - 100; i++) {
mcs_device_telemetry_t *dev = &s_stats.devices[i];
if (dev->sample_count == 0) continue;
if (!first) {
written = snprintf(json_buffer + offset, buffer_len - offset, ",");
if (written < 0) break;
offset += written;
}
first = false;
written = snprintf(json_buffer + offset, buffer_len - offset,
"{\"mac\":\"%02x:%02x:%02x:%02x:%02x:%02x\","
"\"mcs\":%u,\"ss\":%u,\"rssi\":%d,"
"\"channel\":%u,\"bandwidth\":%u,"
"\"frames\":%lu,\"retries\":%lu,"
"\"phy_rate_kbps\":%lu}",
dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4], dev->mac[5],
dev->current_mcs, dev->current_ss, dev->avg_rssi,
dev->samples[0].channel, dev->samples[0].bandwidth,
dev->total_frames, dev->retry_frames,
dev->avg_phy_rate_kbps);
if (written < 0) break;
offset += written;
}
written = snprintf(json_buffer + offset, buffer_len - offset, "]}");
if (written < 0) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
uint32_t mcs_calculate_phy_rate_ax(uint8_t mcs, uint8_t ss, mcs_bandwidth_t bandwidth, bool sgi) {
if (mcs > MCS_TELEMETRY_MAX_MCS || ss < 1 || ss > MCS_TELEMETRY_MAX_SS) {
return 0;
}
const uint32_t *rate_table = NULL;
if (bandwidth == MCS_BW_20MHZ) {
rate_table = sgi ? PHY_RATES_20MHZ_1SS_400NS : PHY_RATES_20MHZ_1SS_800NS;
} else if (bandwidth == MCS_BW_40MHZ) {
rate_table = sgi ? PHY_RATES_40MHZ_1SS_400NS : PHY_RATES_40MHZ_1SS_800NS;
} else {
return 0;
}
// PHY rate = base rate (1SS) * spatial streams
uint32_t base_rate = rate_table[mcs];
return base_rate * ss;
}

View File

@ -1,211 +0,0 @@
/*
* mcs_telemetry.h
*
* Copyright (c) 2026 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef MCS_TELEMETRY_H
#define MCS_TELEMETRY_H
#include "esp_wifi.h"
#include "esp_wifi_types.h"
#include <stdbool.h>
#include <stdint.h>
// Include wifi_monitor.h to get wifi_frame_info_t definition
#include "wifi_monitor.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Maximum MCS index for 802.11ax (0-11)
*/
#define MCS_TELEMETRY_MAX_MCS 11
/**
* @brief Maximum Spatial Streams (1-4 for ESP32-C5)
*/
#define MCS_TELEMETRY_MAX_SS 4
/**
* @brief Maximum device entries to track
*/
#define MCS_TELEMETRY_MAX_DEVICES 16
/**
* @brief Telemetry aggregation window in milliseconds
*/
#define MCS_TELEMETRY_WINDOW_MS 1000
/**
* @brief 802.11ax Bandwidth types (ESP32-C5 supports 20MHz and 40MHz)
*/
typedef enum {
MCS_BW_20MHZ = 0,
MCS_BW_40MHZ = 1
} mcs_bandwidth_t;
/**
* @brief Single telemetry sample
*/
typedef struct {
uint32_t timestamp_ms; // Timestamp in milliseconds
uint8_t mcs; // MCS index (0-11)
uint8_t ss; // Spatial Streams (1-4)
int8_t rssi; // RSSI in dBm
uint8_t channel; // WiFi channel
mcs_bandwidth_t bandwidth; // Bandwidth (20MHz or 40MHz)
uint32_t phy_rate_kbps; // PHY rate in Kbps
uint16_t frame_len; // Frame length in bytes
bool is_retry; // Retry flag
uint8_t sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT, 4=HE)
} mcs_sample_t;
/**
* @brief Aggregated telemetry per device (MAC address)
*/
typedef struct {
uint8_t mac[6]; // MAC address of the device
uint32_t sample_count; // Number of samples in this window
uint32_t last_update_ms; // Last update timestamp
// Aggregated statistics
uint8_t current_mcs; // Current/dominant MCS
uint8_t current_ss; // Current/dominant SS
int8_t avg_rssi; // Average RSSI
int8_t min_rssi; // Minimum RSSI
int8_t max_rssi; // Maximum RSSI
uint32_t total_bytes; // Total bytes transmitted
uint32_t total_frames; // Total frame count
uint32_t retry_frames; // Retry frame count
uint32_t avg_phy_rate_kbps; // Average PHY rate
uint32_t max_phy_rate_kbps; // Maximum PHY rate
// MCS distribution (how many frames per MCS)
uint32_t mcs_count[MCS_TELEMETRY_MAX_MCS + 1];
// SS distribution (how many frames per SS)
uint32_t ss_count[MCS_TELEMETRY_MAX_SS + 1];
// Time series for recent samples (sliding window)
mcs_sample_t samples[16]; // Last 16 samples
uint8_t sample_idx; // Current sample index
} mcs_device_telemetry_t;
/**
* @brief Global telemetry statistics
*/
typedef struct {
uint32_t total_frames_captured;
uint32_t total_devices;
uint32_t window_start_ms;
uint32_t window_end_ms;
mcs_device_telemetry_t devices[MCS_TELEMETRY_MAX_DEVICES];
} mcs_telemetry_stats_t;
/**
* @brief Callback function type for telemetry updates
*
* @param stats Telemetry statistics
*/
typedef void (*mcs_telemetry_cb_t)(const mcs_telemetry_stats_t *stats);
/**
* @brief Initialize MCS telemetry capture
*
* @param callback Optional callback for telemetry updates (can be NULL)
* @return esp_err_t ESP_OK on success
*/
esp_err_t mcs_telemetry_init(mcs_telemetry_cb_t callback);
/**
* @brief Start MCS telemetry capture
*
* @return esp_err_t ESP_OK on success
*/
esp_err_t mcs_telemetry_start(void);
/**
* @brief Stop MCS telemetry capture
*
* @return esp_err_t ESP_OK on success
*/
esp_err_t mcs_telemetry_stop(void);
/**
* @brief Process a captured 802.11 frame
*
* @param frame_info Parsed frame information (from wifi_monitor)
* @param rx_ctrl RX control information
* @return esp_err_t ESP_OK on success
*/
esp_err_t mcs_telemetry_process_frame(const wifi_frame_info_t *frame_info, const wifi_pkt_rx_ctrl_t *rx_ctrl);
/**
* @brief Get current telemetry statistics
*
* @param stats Output: telemetry statistics
* @return esp_err_t ESP_OK on success
*/
esp_err_t mcs_telemetry_get_stats(mcs_telemetry_stats_t *stats);
/**
* @brief Reset telemetry statistics
*/
void mcs_telemetry_reset(void);
/**
* @brief Get telemetry as JSON string (for HTTP POST)
*
* @param json_buffer Output buffer for JSON string
* @param buffer_len Buffer length
* @param device_id Device identifier string
* @return esp_err_t ESP_OK on success
*/
esp_err_t mcs_telemetry_to_json(char *json_buffer, size_t buffer_len, const char *device_id);
/**
* @brief Calculate PHY rate from MCS, SS, and bandwidth (802.11ax)
*
* @param mcs MCS index (0-11)
* @param ss Spatial Streams (1-4)
* @param bandwidth Bandwidth (20MHz or 40MHz)
* @param sgi Short Guard Interval (true = 400ns, false = 800ns)
* @return uint32_t PHY rate in Kbps, 0 if invalid
*/
uint32_t mcs_calculate_phy_rate_ax(uint8_t mcs, uint8_t ss, mcs_bandwidth_t bandwidth, bool sgi);
#ifdef __cplusplus
}
#endif
#endif // MCS_TELEMETRY_H

View File

@ -300,51 +300,6 @@ esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t
return ESP_OK; return ESP_OK;
} }
esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) {
if (!s_sd_card_mounted || size_bytes == NULL) {
return ESP_ERR_INVALID_STATE;
}
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
struct stat st;
if (stat(full_path, &st) != 0) {
return ESP_FAIL;
}
if (!S_ISREG(st.st_mode)) {
return ESP_ERR_INVALID_ARG;
}
*size_bytes = (size_t)st.st_size;
return ESP_OK;
}
esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, size_t len, size_t *bytes_read) {
if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE;
}
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "rb");
if (f == NULL) {
return ESP_FAIL;
}
if (fseek(f, (long)offset, SEEK_SET) != 0) {
fclose(f);
return ESP_FAIL;
}
size_t n = fread(data, 1, len, f);
fclose(f);
if (bytes_read) {
*bytes_read = n;
}
return ESP_OK;
}
bool sd_card_file_exists(const char *filename) { bool sd_card_file_exists(const char *filename) {
if (!s_sd_card_mounted) { if (!s_sd_card_mounted) {
return false; return false;
@ -358,49 +313,6 @@ bool sd_card_file_exists(const char *filename) {
return (stat(full_path, &st) == 0); return (stat(full_path, &st) == 0);
} }
esp_err_t sd_card_list_dir(const char *path) {
if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE;
}
char full_path[128];
if (!path || path[0] == '\0') {
snprintf(full_path, sizeof(full_path), "%s", s_mount_point);
} else {
snprintf(full_path, sizeof(full_path), "%s/%s", s_mount_point,
(path[0] == '/') ? path + 1 : path);
}
DIR *d = opendir(full_path);
if (!d) {
return ESP_FAIL;
}
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') {
continue;
}
char entry_path[384]; /* full_path(128) + "/" + d_name(255) */
int n = snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, e->d_name);
if (n < 0 || n >= (int)sizeof(entry_path)) {
continue; /* path too long, skip */
}
struct stat st;
if (stat(entry_path, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
printf(" %-32s <DIR>\n", e->d_name);
} else {
printf(" %-32s %10zu bytes\n", e->d_name, (size_t)st.st_size);
}
} else {
printf(" %-32s ?\n", e->d_name);
}
}
closedir(d);
return ESP_OK;
}
esp_err_t sd_card_delete_file(const char *filename) { esp_err_t sd_card_delete_file(const char *filename) {
if (!s_sd_card_mounted) { if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;

View File

@ -110,27 +110,6 @@ esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len,
*/ */
esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read); esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read);
/**
* @brief Get size of a file in bytes
*
* @param filename File path
* @param size_bytes Output parameter for file size
* @return ESP_OK on success
*/
esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes);
/**
* @brief Read a chunk of a file at given offset (for streaming)
*
* @param filename File path
* @param offset Byte offset from start
* @param data Buffer to read into
* @param len Maximum length to read
* @param bytes_read Output parameter for actual bytes read
* @return ESP_OK on success
*/
esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, size_t len, size_t *bytes_read);
/** /**
* @brief Check if a file exists on the SD card * @brief Check if a file exists on the SD card
* *
@ -139,14 +118,6 @@ esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data,
*/ */
bool sd_card_file_exists(const char *filename); bool sd_card_file_exists(const char *filename);
/**
* @brief List files in a directory on the SD card
*
* @param path Directory path (empty or "/" for root)
* @return ESP_OK on success
*/
esp_err_t sd_card_list_dir(const char *path);
/** /**
* @brief Delete a file from the SD card * @brief Delete a file from the SD card
* *

View File

@ -1,5 +0,0 @@
idf_component_register(
SRCS "sdcard_http.c"
INCLUDE_DIRS "."
REQUIRES sd_card esp_http_server
)

View File

@ -1,265 +0,0 @@
/*
* sdcard_http.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* SPDX-License-Identifier: BSD-3-Clause
*
* Serves files from the SD card via HTTP GET /sdcard/<path>.
* Use: wget http://<device-ip>:8080/sdcard/myfile.txt
*
* Telemetry download stats are persisted to telemetry-status on the SD card
* and survive reboots. Format: attempts=N, downloads=M, and per-download history
* with timestamp and bytes.
*/
#include "sdcard_http.h"
#include "sd_card.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <sys/stat.h>
#include <time.h>
static const char *TAG = "sdcard_http";
static httpd_handle_t s_server = NULL;
static uint32_t s_telemetry_attempts = 0;
static uint32_t s_telemetry_downloads = 0;
#define SDCARD_HTTP_CHUNK 2048
#define TELEMETRY_FILE "fiwi-telemetry"
#define TELEMETRY_STATUS_FILE "telemetry-status"
#define SDCARD_URI_PREFIX "/sdcard"
#define STATUS_MAX_SIZE 4096
#define MAX_HISTORY_LINES 50
#if !defined(CONFIG_TELEMETRY_AUTO_DELETE_AFTER_DOWNLOAD)
#define CONFIG_TELEMETRY_AUTO_DELETE_AFTER_DOWNLOAD 0
#endif
/* Reject path if it contains ".." to avoid traversal */
static bool path_is_safe(const char *path) {
if (!path || path[0] == '\0') return false;
const char *p = path;
while (*p) {
if (p[0] == '.' && p[1] == '.') return false;
p++;
}
return true;
}
/* Load telemetry status from SD card. Format:
* attempts=N
* downloads=M
* ---
* timestamp bytes
* ...
*/
static void load_telemetry_status(void) {
if (!sd_card_is_ready()) return;
if (!sd_card_file_exists(TELEMETRY_STATUS_FILE)) return;
static char buf[STATUS_MAX_SIZE];
size_t n = 0;
if (sd_card_read_file(TELEMETRY_STATUS_FILE, buf, sizeof(buf) - 1, &n) != ESP_OK || n == 0) return;
buf[n] = '\0';
char *p = buf;
while (*p) {
if (strncmp(p, "attempts=", 9) == 0) {
s_telemetry_attempts = (uint32_t)strtoul(p + 9, NULL, 10);
} else if (strncmp(p, "downloads=", 10) == 0) {
s_telemetry_downloads = (uint32_t)strtoul(p + 10, NULL, 10);
} else if (strncmp(p, "---", 3) == 0) {
break; /* rest is history, ignore for counts */
}
p = strchr(p, '\n');
if (!p) break;
p++;
}
}
/* Save telemetry status: attempts, downloads, and history (timestamp bytes per line).
* Appends new entry and trims history to MAX_HISTORY_LINES.
*/
static void save_telemetry_status(size_t bytes_sent) {
if (!sd_card_is_ready()) return;
time_t ts = time(NULL);
if (ts < 0) ts = 0;
/* Read existing file to get history */
static char buf[STATUS_MAX_SIZE];
char *history_start = NULL;
size_t history_count = 0;
if (sd_card_file_exists(TELEMETRY_STATUS_FILE)) {
size_t n = 0;
if (sd_card_read_file(TELEMETRY_STATUS_FILE, buf, sizeof(buf) - 1, &n) == ESP_OK && n > 0) {
buf[n] = '\0';
history_start = strstr(buf, "---\n");
if (history_start) {
history_start += 4;
char *line = history_start;
while (*line && history_count < MAX_HISTORY_LINES) {
if (line[0] && line[0] != '\n') history_count++;
line = strchr(line, '\n');
if (!line) break;
line++;
}
}
}
}
/* Build new content: header + trimmed history + new entry */
static char out[STATUS_MAX_SIZE];
int len = snprintf(out, sizeof(out), "attempts=%" PRIu32 "\ndownloads=%" PRIu32 "\n---\n",
s_telemetry_attempts, s_telemetry_downloads);
if (len < 0 || len >= (int)sizeof(out)) return;
/* Append existing history (skip oldest if we're at max) */
if (history_start && history_count > 0) {
char *line = history_start;
size_t skip = (history_count >= MAX_HISTORY_LINES) ? 1 : 0;
size_t kept = 0;
while (*line && len < (int)sizeof(out) - 64) {
if (line[0] && line[0] != '\n') {
if (skip > 0) {
skip--;
} else {
char *end = strchr(line, '\n');
size_t line_len = end ? (size_t)(end - line) + 1 : strlen(line);
if (len + line_len >= sizeof(out)) break;
memcpy(out + len, line, line_len);
len += (int)line_len;
kept++;
if (kept >= MAX_HISTORY_LINES - 1) break;
}
}
line = strchr(line, '\n');
if (!line) break;
line++;
}
}
/* Append new entry */
int n = snprintf(out + len, sizeof(out) - (size_t)len, "%ld %zu\n", (long)ts, bytes_sent);
if (n > 0) len += n;
sd_card_write_file(TELEMETRY_STATUS_FILE, out, (size_t)len, false);
}
static esp_err_t sdcard_file_handler(httpd_req_t *req) {
if (!sd_card_is_ready()) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "SD card not mounted");
return ESP_OK;
}
/* URI is /sdcard/foo/bar -> path = foo/bar */
const char *uri = req->uri;
if (strncmp(uri, SDCARD_URI_PREFIX, strlen(SDCARD_URI_PREFIX)) != 0) {
return ESP_FAIL;
}
const char *path = uri + strlen(SDCARD_URI_PREFIX);
if (path[0] == '/') path++;
if (path[0] == '\0') {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing path");
return ESP_OK;
}
if (!path_is_safe(path)) {
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Invalid path");
return ESP_OK;
}
bool is_telemetry = (strcmp(path, TELEMETRY_FILE) == 0);
if (is_telemetry) {
s_telemetry_attempts++;
}
size_t file_size = 0;
if (sd_card_get_file_size(path, &file_size) != ESP_OK) {
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
return ESP_OK;
}
httpd_resp_set_type(req, "application/octet-stream");
httpd_resp_set_hdr(req, "Content-Disposition", "attachment");
static uint8_t buf[SDCARD_HTTP_CHUNK];
size_t offset = 0;
while (offset < file_size) {
size_t to_read = file_size - offset;
if (to_read > sizeof(buf)) to_read = sizeof(buf);
size_t n = 0;
if (sd_card_read_file_at(path, offset, buf, to_read, &n) != ESP_OK || n == 0) {
break;
}
if (httpd_resp_send_chunk(req, (char *)buf, n) != ESP_OK) {
return ESP_FAIL;
}
offset += n;
}
if (httpd_resp_send_chunk(req, NULL, 0) != ESP_OK) {
return ESP_FAIL;
}
if (is_telemetry) {
s_telemetry_downloads++;
save_telemetry_status(file_size);
if (CONFIG_TELEMETRY_AUTO_DELETE_AFTER_DOWNLOAD) {
sd_card_delete_file(TELEMETRY_FILE);
}
}
return ESP_OK;
}
esp_err_t sdcard_http_start(void) {
if (s_server != NULL) {
return ESP_OK;
}
load_telemetry_status();
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 8080;
config.max_uri_handlers = 8;
config.max_open_sockets = 4;
config.uri_match_fn = httpd_uri_match_wildcard;
if (httpd_start(&s_server, &config) != ESP_OK) {
ESP_LOGE(TAG, "Failed to start HTTP server");
return ESP_FAIL;
}
httpd_uri_t sdcard_uri = {
.uri = "/sdcard/*",
.method = HTTP_GET,
.handler = sdcard_file_handler,
.user_ctx = NULL,
};
if (httpd_register_uri_handler(s_server, &sdcard_uri) != ESP_OK) {
httpd_stop(s_server);
s_server = NULL;
ESP_LOGE(TAG, "Failed to register /sdcard/* handler");
return ESP_FAIL;
}
ESP_LOGI(TAG, "HTTP server on port 8080: GET http://<ip>:8080/sdcard/<path>");
return ESP_OK;
}
void sdcard_http_get_telemetry_stats(uint32_t *attempts, uint32_t *downloads) {
if (attempts) *attempts = s_telemetry_attempts;
if (downloads) *downloads = s_telemetry_downloads;
}
void sdcard_http_stop(void) {
if (s_server) {
httpd_stop(s_server);
s_server = NULL;
ESP_LOGI(TAG, "HTTP server stopped");
}
}

View File

@ -1,32 +0,0 @@
/*
* sdcard_http.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef SDCARD_HTTP_H
#define SDCARD_HTTP_H
#include "esp_err.h"
/**
* @brief Start HTTP server that serves files from SD card at GET /sdcard/<path>
* Listens on port 8080. Call once after WiFi and SD card init.
* @return ESP_OK on success
*/
esp_err_t sdcard_http_start(void);
/**
* @brief Stop the SD card HTTP server (optional)
*/
void sdcard_http_stop(void);
/**
* @brief Get HTTP download stats for fiwi-telemetry
* @param attempts Output: total download attempts (may be NULL)
* @param downloads Output: successful downloads (may be NULL)
*/
void sdcard_http_get_telemetry_stats(uint32_t *attempts, uint32_t *downloads);
#endif /* SDCARD_HTTP_H */

View File

@ -1,5 +1,4 @@
idf_component_register(SRCS "wifi_controller.c" idf_component_register(SRCS "wifi_controller.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES esp_wifi freertos REQUIRES esp_wifi freertos
PRIV_REQUIRES csi_manager iperf status_led wifi_monitor wifi_cfg gps_sync log esp_netif PRIV_REQUIRES csi_manager iperf status_led wifi_monitor wifi_cfg gps_sync log esp_netif)
mcs_telemetry sd_card)

View File

@ -38,7 +38,6 @@
#include "esp_event.h" #include "esp_event.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "inttypes.h" #include "inttypes.h"
#include <string.h>
#include "wifi_cfg.h" #include "wifi_cfg.h"
// Dependencies // Dependencies
@ -51,12 +50,6 @@
#include "csi_manager.h" #include "csi_manager.h"
#endif #endif
#include "mcs_telemetry.h"
#include "sd_card.h"
#define FIWI_TELEMETRY_FILE "fiwi-telemetry"
#define FIWI_TELEMETRY_JSON_BUF_SIZE 4096
static const char *TAG = "WIFI_CTL"; static const char *TAG = "WIFI_CTL";
static wifi_ctl_mode_t s_current_mode = WIFI_CTL_MODE_STA; static wifi_ctl_mode_t s_current_mode = WIFI_CTL_MODE_STA;
@ -88,20 +81,13 @@ static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) {
} }
static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t *payload, uint16_t len) { static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t *payload, uint16_t len) {
(void)payload;
(void)len;
s_monitor_frame_count++; s_monitor_frame_count++;
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);
} }
/* MCS telemetry: feed frames to fiwi-telemetry (default on monitor start) */
mcs_telemetry_process_frame(frame, NULL);
} }
static void monitor_stats_task(void *arg) { static void monitor_stats_task(void *arg) {
(void)arg;
static char json_buf[FIWI_TELEMETRY_JSON_BUF_SIZE];
uint32_t flush_count = 0;
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(10000)); vTaskDelay(pdMS_TO_TICKS(10000));
wifi_collapse_stats_t stats; wifi_collapse_stats_t stats;
@ -110,14 +96,6 @@ static void monitor_stats_task(void *arg) {
(unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav); (unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav);
if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️"); if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️");
} }
/* 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) {
size_t len = strlen(json_buf);
if (len > 0 && sd_card_write_file(FIWI_TELEMETRY_FILE, json_buf, len, false) == ESP_OK) {
flush_count++;
ESP_LOGD(TAG, "fiwi-telemetry flushed (#%lu)", (unsigned long)flush_count);
}
}
} }
} }
@ -217,13 +195,6 @@ esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
return ESP_FAIL; return ESP_FAIL;
} }
/* MCS telemetry -> fiwi-telemetry on SD (default on monitor start) */
if (mcs_telemetry_init(NULL) != ESP_OK) {
ESP_LOGW(TAG, "MCS telemetry init failed");
} else if (mcs_telemetry_start() != ESP_OK) {
ESP_LOGW(TAG, "MCS telemetry start failed");
}
esp_wifi_set_bandwidth(WIFI_IF_STA, bw); esp_wifi_set_bandwidth(WIFI_IF_STA, bw);
if (wifi_monitor_start() != ESP_OK) { if (wifi_monitor_start() != ESP_OK) {
@ -257,7 +228,6 @@ esp_err_t wifi_ctl_switch_to_sta(void) {
} }
if (s_monitor_enabled) { if (s_monitor_enabled) {
mcs_telemetry_stop();
wifi_monitor_stop(); wifi_monitor_stop();
s_monitor_enabled = false; s_monitor_enabled = false;
vTaskDelay(pdMS_TO_TICKS(500)); vTaskDelay(pdMS_TO_TICKS(500));

View File

@ -157,96 +157,6 @@ If the card is not detected, check:
3. Card is formatted (FAT32) 3. Card is formatted (FAT32)
4. Power supply is stable (3.3V) 4. Power supply is stable (3.3V)
## Downloading files from the SD card
You can copy files off the SD card in two ways:
### Over WiFi (HTTP)
When the device is on a network, an HTTP server listens on port **8080**. Request a file with:
- **URL:** `http://<device-ip>:8080/sdcard/<path>`
- Example: `http://192.168.1.100:8080/sdcard/log/data.txt`
Use a browser or `curl`/`wget` to download. Paths are relative to the SD root; `..` is not allowed.
### Over serial (USB)
From the device console, run:
```text
sdcard send <filename>
```
The firmware streams the file in a hex-encoded protocol. On the host, use the provided script (requires Python 3 and `pyserial`):
```bash
pip install pyserial
python3 tools/sdcard_recv.py -p /dev/ttyUSB0 -f myfile.txt -o saved.txt
```
- `-p` / `--port`: serial port
- `-f` / `--remote`: path of the file on the SD card
- `-o` / `--output`: local path to save (default: basename of remote file)
Serial transfer is limited to 512 KB per file.
## Device discovery (broadcast beacon)
When connected to WiFi, the device sends periodic UDP broadcast packets on port **5555** so a laptop on the same LAN can discover it. The payload is JSON:
```json
{"ip":"192.168.1.73","mask":"255.255.255.0","gw":"192.168.1.1","dhcp":"OFF","mac":"3c:dc:75:82:2a:a8","fiwi_telemetry":true}
```
- **mac**: Unique identifier per device (use for deduplication when many devices are present).
- **fiwi_telemetry**: `true` if file `fiwi-telemetry` exists on the SD card, `false` otherwise.
Beacon interval is configurable in `idf.py menuconfig`**ESP32 iperf Configuration****Device discovery****Broadcast beacon interval** (default 5 seconds).
On a Linux laptop:
**Listen only (no download):**
```bash
python3 tools/beacon_listen.py
```
**Listen and download fiwi-telemetry from each device** (files saved as `fiwi-telemetry_<mac>` in `./telemetry/`):
```bash
python3 tools/beacon_listen.py --download --output-dir ./telemetry
```
**Re-download every 60 seconds:**
```bash
python3 tools/beacon_listen.py --download --output-dir ./telemetry --refresh 60
```
Devices are tracked by MAC address; each telemetry file is uniquely named.
## Telemetry status file
Download stats (attempts, successful downloads) are written to `telemetry-status` on the SD card and persist across reboots. The file format:
```
attempts=10
downloads=8
---
1738857600 1234
1738857701 1235
```
Lines after `---` are `timestamp bytes` (Unix epoch and bytes downloaded). View with:
```text
sdcard read telemetry-status
```
**Auto-delete option:** In `idf.py menuconfig`**ESP32 iperf Configuration****Telemetry** → enable **Auto-delete fiwi-telemetry file after successful HTTP download** to remove the telemetry file after each successful download.
## fiwi-telemetry: monitor mode default
When you run **monitor start**, the firmware automatically writes MCS telemetry (frame rates, MCS indices, RSSI, PHY rates per device) to the `fiwi-telemetry` file on the SD card. The file is updated every 10 seconds while monitor mode is active. No extra configuration is needed—telemetry logging to `fiwi-telemetry` is the default behavior when monitor mode is running.
## Troubleshooting ## Troubleshooting
**Card not detected:** **Card not detected:**

View File

@ -1,100 +0,0 @@
# Telemetry capture
How to enable and capture MCS telemetry (frame rates, MCS indices, RSSI, PHY rates per device) from the ESP32.
## Prerequisites
- SD card inserted and mounted (see [SD_CARD_WIRING.md](SD_CARD_WIRING.md))
- Python 3 on the laptop (for host scripts)
## 1. Enable telemetry on the device
Connect to the device over serial and run:
```text
monitor start [-c <channel>]
```
This puts the device in WiFi monitor mode and **automatically** writes MCS telemetry to `fiwi-telemetry` on the SD card every 10 seconds. No extra configuration needed.
> **Note:** Monitor mode disconnects WiFi. The device will not be on the network while capturing. Use **serial** to capture telemetry in this case. To capture over **WiFi**, run `monitor stop` first so the device rejoins the network, then download the telemetry file.
Check telemetry status:
```text
sdcard status
```
Shows `fiwi-telemetry: yes, <size>` when telemetry exists.
## 2. Capture telemetry on a laptop
### Option A: Serial (device connected by USB)
Use when the device is in monitor mode (no WiFi) or for any direct USB connection.
**On the laptop** (device must be reachable over serial; you may need to exit the REPL or connect in a second terminal):
```bash
pip install pyserial
python3 tools/sdcard_recv.py -p /dev/ttyUSB0 -f fiwi-telemetry -o fiwi-telemetry.json
```
The script sends `sdcard send fiwi-telemetry` to the device and captures the hex-encoded response. No need to type the command on the device.
- `-p` / `--port`: serial port (e.g. `/dev/ttyUSB0` on Linux, `COM3` on Windows)
- `-f` / `--remote`: file path on the SD card
- `-o` / `--output`: local path to save (default: basename of remote file)
Serial transfer is limited to 512 KB per file.
---
### Option B: WiFi + beacon discovery (device on network)
Use when the device is in STA mode and connected to the same LAN as the laptop. The device advertises itself via UDP broadcast on port 5555.
**1. On the device:** Run `monitor stop` so it reconnects to WiFi. Telemetry is already on the SD card from when monitor mode was running.
**2. On the laptop:**
```bash
# Listen only (see devices, no download)
python3 tools/beacon_listen.py
# Listen and download fiwi-telemetry from each device
python3 tools/beacon_listen.py --download --output-dir ./telemetry
# Re-download every 60 seconds
python3 tools/beacon_listen.py --download --output-dir ./telemetry --refresh 60
```
Files are saved as `fiwi-telemetry_<mac>` in the output directory. Uses standard library only (no pip install).
---
### Option C: WiFi + direct HTTP (you know the IP)
If the device is on the network and you know its IP:
```bash
curl -o fiwi-telemetry.json "http://<device-ip>:8080/sdcard/fiwi-telemetry"
```
Or in a browser: `http://<device-ip>:8080/sdcard/fiwi-telemetry`
## Telemetry format
`fiwi-telemetry` is JSON:
```json
{"device_id":"esp32","timestamp":12345,"total_frames":1000,"devices":[{"mac":"aa:bb:cc:dd:ee:ff","mcs":5,"ss":1,"rssi":-45,"channel":6,"bandwidth":0,"frames":500,"retries":2,"phy_rate_kbps":68800}]}
```
## Quick reference
| Method | When to use | Laptop command |
|-------------|---------------------------|---------------------------------------------------------------------------------|
| Serial | Monitor mode, USB only | `python3 tools/sdcard_recv.py -p /dev/ttyUSB0 -f fiwi-telemetry -o out.json` |
| Beacon | Device on LAN, discover | `python3 tools/beacon_listen.py --download --output-dir ./telemetry` |
| HTTP | Device on LAN, known IP | `curl -o out.json "http://<ip>:8080/sdcard/fiwi-telemetry"` |

View File

@ -1,3 +1,3 @@
idf_component_register(SRCS "main.c" "broadcast_beacon.c" idf_component_register(SRCS "main.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console sd_card sdcard_http) PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console sd_card)

View File

@ -45,24 +45,6 @@ menu "ESP32 iperf Configuration"
help help
Netmask for the network. Netmask for the network.
menu "Device discovery (broadcast beacon)"
config BEACON_INTERVAL_SEC
int "Broadcast beacon interval (seconds)"
default 5
range 1 60
help
Interval in seconds between UDP broadcast beacons for laptop discovery.
endmenu
menu "Telemetry (fiwi-telemetry)"
config TELEMETRY_AUTO_DELETE_AFTER_DOWNLOAD
bool "Auto-delete fiwi-telemetry file after successful HTTP download"
default n
help
When enabled, the fiwi-telemetry file is automatically deleted
from the SD card after it has been successfully downloaded via HTTP.
endmenu
menu "SD Card" menu "SD Card"
config SD_CD_GPIO config SD_CD_GPIO
int "Card Detect GPIO (-1 to disable)" int "Card Detect GPIO (-1 to disable)"

View File

@ -1,175 +0,0 @@
/*
* broadcast_beacon.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted under the terms of the BSD 3-Clause License.
*
* Sends periodic UDP broadcast so a Linux laptop can detect the device.
* Advertises: IP, mask, gw, dhcp, MAC, and whether fiwi-telemetry file exists on SD.
*/
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "sd_card.h"
#include "sdkconfig.h"
#define BEACON_PORT 5555
#ifndef CONFIG_BEACON_INTERVAL_SEC
#define CONFIG_BEACON_INTERVAL_SEC 5
#endif
#define BEACON_PAYLOAD_MAX 256
static const char *TAG = "BEACON";
static esp_event_handler_instance_t s_instance_got_ip = NULL;
static esp_event_handler_instance_t s_instance_disconnected = NULL;
static TaskHandle_t s_beacon_task = NULL;
static EventGroupHandle_t s_beacon_events = NULL;
#define BEACON_IP_READY_BIT (1 << 0)
#define BEACON_STOP_BIT (1 << 1)
static void beacon_task(void *arg) {
(void)arg;
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
ESP_LOGE(TAG, "socket: %s", strerror(errno));
vTaskDelete(NULL);
return;
}
int enable = 1;
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(enable)) != 0) {
ESP_LOGW(TAG, "setsockopt SO_BROADCAST: %s", strerror(errno));
}
struct sockaddr_in dst = {0};
dst.sin_family = AF_INET;
dst.sin_port = htons(BEACON_PORT);
dst.sin_addr.s_addr = htonl(INADDR_BROADCAST);
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif) {
ESP_LOGE(TAG, "no WIFI_STA netif");
close(sock);
vTaskDelete(NULL);
return;
}
char payload[BEACON_PAYLOAD_MAX];
while (1) {
EventBits_t bits = xEventGroupWaitBits(s_beacon_events,
BEACON_STOP_BIT, pdTRUE, pdFALSE,
pdMS_TO_TICKS(CONFIG_BEACON_INTERVAL_SEC * 1000));
if (bits & BEACON_STOP_BIT) {
break;
}
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || ip_info.ip.addr == 0) {
continue;
}
esp_netif_dhcp_status_t dhcp_status;
esp_netif_dhcpc_get_status(netif, &dhcp_status);
bool dhcp_on = (dhcp_status == ESP_NETIF_DHCP_STARTED);
uint8_t mac[6] = {0};
esp_netif_get_mac(netif, mac);
bool fiwi_telemetry = sd_card_is_ready() && sd_card_file_exists("fiwi-telemetry");
int n = snprintf(payload, sizeof(payload),
"{\"ip\":\"" IPSTR "\",\"mask\":\"" IPSTR "\",\"gw\":\"" IPSTR "\","
"\"dhcp\":\"%s\",\"mac\":\"%02x:%02x:%02x:%02x:%02x:%02x\","
"\"fiwi_telemetry\":%s}\n",
IP2STR(&ip_info.ip), IP2STR(&ip_info.netmask), IP2STR(&ip_info.gw),
dhcp_on ? "ON" : "OFF",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
fiwi_telemetry ? "true" : "false");
if (n < 0 || n >= (int)sizeof(payload)) {
continue;
}
ssize_t sent = sendto(sock, payload, (size_t)n, 0,
(struct sockaddr *)&dst, sizeof(dst));
if (sent < 0) {
ESP_LOGD(TAG, "sendto: %s", strerror(errno));
}
}
close(sock);
s_beacon_task = NULL;
vTaskDelete(NULL);
}
static void beacon_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
(void)arg;
(void)event_data;
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
xEventGroupClearBits(s_beacon_events, BEACON_STOP_BIT);
xEventGroupSetBits(s_beacon_events, BEACON_IP_READY_BIT);
if (s_beacon_task == NULL) {
xTaskCreate(beacon_task, "beacon", 4096, NULL, 5, &s_beacon_task);
}
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupClearBits(s_beacon_events, BEACON_IP_READY_BIT);
xEventGroupSetBits(s_beacon_events, BEACON_STOP_BIT);
/* task will exit and set s_beacon_task = NULL */
}
}
esp_err_t broadcast_beacon_init(void) {
if (s_beacon_events == NULL) {
s_beacon_events = xEventGroupCreate();
if (s_beacon_events == NULL) {
return ESP_ERR_NO_MEM;
}
}
esp_err_t err;
err = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&beacon_event_handler, NULL, &s_instance_got_ip);
if (err != ESP_OK) return err;
err = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&beacon_event_handler, NULL, &s_instance_disconnected);
if (err != ESP_OK) {
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, s_instance_got_ip);
return err;
}
/* If already connected, start beacon immediately */
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
xEventGroupSetBits(s_beacon_events, BEACON_IP_READY_BIT);
if (s_beacon_task == NULL) {
xTaskCreate(beacon_task, "beacon", 4096, NULL, 5, &s_beacon_task);
}
}
}
ESP_LOGI(TAG, "Beacon init: UDP broadcast port %d, interval %d s", BEACON_PORT, CONFIG_BEACON_INTERVAL_SEC);
return ESP_OK;
}

View File

@ -1,28 +0,0 @@
/*
* broadcast_beacon.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted under the terms of the BSD 3-Clause License.
*/
#ifndef BROADCAST_BEACON_H
#define BROADCAST_BEACON_H
#include "esp_err.h"
/**
* @brief Initialize the broadcast beacon (registers event handlers).
*
* On IP_EVENT_STA_GOT_IP, starts periodic UDP broadcast of device info.
* On WIFI_EVENT_STA_DISCONNECTED, stops the beacon.
*
* Beacon payload (JSON) includes: ip, mask, gw, dhcp, mac, fiwi_telemetry.
* fiwi_telemetry is true if SD card has file "fiwi-telemetry".
* Listen on UDP port 5555 for broadcast from 255.255.255.255.
*/
esp_err_t broadcast_beacon_init(void);
#endif /* BROADCAST_BEACON_H */

View File

@ -59,8 +59,6 @@
#include "iperf.h" #include "iperf.h"
#include "sd_card.h" #include "sd_card.h"
#include "sdcard_http.h"
#include "broadcast_beacon.h"
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED #ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_log.h" #include "csi_log.h"
#include "csi_manager.h" #include "csi_manager.h"
@ -194,11 +192,6 @@ void app_main(void) {
ESP_LOGW(TAG, "SD card initialization failed (card may not be present): %s", esp_err_to_name(sd_ret)); ESP_LOGW(TAG, "SD card initialization failed (card may not be present): %s", esp_err_to_name(sd_ret));
} }
/* Start HTTP server for SD file download (GET /sdcard/<path> on port 8080) */
if (sd_ret == ESP_OK && sdcard_http_start() != ESP_OK) {
ESP_LOGW(TAG, "SD card HTTP server failed to start");
}
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED #ifdef CONFIG_ESP_WIFI_CSI_ENABLED
ESP_ERROR_CHECK(csi_log_init()); ESP_ERROR_CHECK(csi_log_init());
csi_mgr_init(); csi_mgr_init();
@ -208,9 +201,6 @@ void app_main(void) {
wifi_ctl_init(); wifi_ctl_init();
iperf_param_init(); iperf_param_init();
/* Broadcast beacon: advertise device (IP, MAC, fiwi-telemetry) for laptop discovery */
broadcast_beacon_init();
// 6. Initialize Console (REPL) // 6. Initialize Console (REPL)
ESP_LOGI(TAG, "Initializing console REPL..."); ESP_LOGI(TAG, "Initializing console REPL...");
esp_console_repl_t *repl = NULL; esp_console_repl_t *repl = NULL;

View File

@ -1,178 +0,0 @@
#!/usr/bin/env python3
"""
Listen for ESP32 broadcast beacons and optionally download fiwi-telemetry from each device.
The device sends UDP broadcast packets (port 5555) with JSON:
{"ip":"...","mask":"...","gw":"...","dhcp":"ON|OFF","mac":"...","fiwi_telemetry":true|false}
Devices are tracked by MAC address (unique per device). For each device with fiwi_telemetry=true,
the script downloads http://<ip>:8080/sdcard/fiwi-telemetry and saves to the output directory
with a filename including the MAC for uniqueness.
Usage:
python3 tools/beacon_listen.py [options]
# Listen only (no download):
python3 tools/beacon_listen.py
# Listen and download telemetry to ./telemetry/:
python3 tools/beacon_listen.py --download --output-dir ./telemetry
# Refresh downloads every 60 seconds:
python3 tools/beacon_listen.py --download --output-dir ./telemetry --refresh 60
Requires: standard library only (socket, json, urllib)
"""
import argparse
import json
import os
import socket
import sys
import time
import urllib.error
import urllib.request
BEACON_PORT = 5555
HTTP_PORT = 8080
TELEMETRY_PATH = "fiwi-telemetry"
def mac_to_filename(mac: str) -> str:
"""Convert MAC like 3c:dc:75:82:2a:a8 to safe filename suffix 3cdc75822aa8."""
return mac.replace(":", "").replace("-", "").lower()
def download_telemetry(ip: str, mac: str, output_dir: str) -> bool:
"""Download fiwi-telemetry from device at ip, save to output_dir/fiwi-telemetry_<mac>."""
url = f"http://{ip}:{HTTP_PORT}/sdcard/{TELEMETRY_PATH}"
suffix = mac_to_filename(mac)
out_path = os.path.join(output_dir, f"fiwi-telemetry_{suffix}")
try:
with urllib.request.urlopen(url, timeout=10) as resp:
data = resp.read()
with open(out_path, "wb") as f:
f.write(data)
return True
except (urllib.error.URLError, OSError, TimeoutError) as e:
sys.stderr.write(f"Download failed {url}: {e}\n")
return False
def main():
ap = argparse.ArgumentParser(
description="Listen for ESP32 broadcast beacons and download fiwi-telemetry"
)
ap.add_argument(
"-p", "--port", type=int, default=BEACON_PORT,
help=f"UDP beacon port (default {BEACON_PORT})",
)
ap.add_argument(
"-d", "--download", action="store_true",
help="Download fiwi-telemetry from devices that advertise it",
)
ap.add_argument(
"-o", "--output-dir", default="./telemetry",
help="Directory to save telemetry files (default ./telemetry)",
)
ap.add_argument(
"-r", "--refresh", type=float, default=0,
help="Re-download telemetry every N seconds (0 = once per device, default)",
)
ap.add_argument(
"-q", "--quiet", action="store_true",
help="Less output; only print downloads and errors",
)
args = ap.parse_args()
if args.download:
os.makedirs(args.output_dir, exist_ok=True)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
except Exception:
pass
sock.bind(("", args.port))
sock.settimeout(1.0)
# Track devices by MAC: {mac: {"ip": ..., "last_download": timestamp, ...}}
devices = {}
if not args.quiet:
print(f"Listening for beacons on UDP port {args.port}...")
if args.download:
print(f"Downloading telemetry to {args.output_dir}/")
print("Ctrl+C to stop.\n")
while True:
try:
data, addr = sock.recvfrom(1024)
except socket.timeout:
# Periodic refresh: re-download devices with fiwi_telemetry if --refresh set
if args.download and args.refresh > 0 and devices:
now = time.time()
for mac, info in list(devices.items()):
if not info.get("fiwi_telemetry"):
continue
last = info.get("last_download", 0)
if now - last >= args.refresh:
if download_telemetry(info["ip"], mac, args.output_dir):
info["last_download"] = now
if not args.quiet:
print(f"Refresh: {mac} -> fiwi-telemetry_{mac_to_filename(mac)}")
continue
except KeyboardInterrupt:
break
try:
obj = json.loads(data.decode().strip())
except (json.JSONDecodeError, UnicodeDecodeError) as e:
if not args.quiet:
print(f"[{addr[0]}] Invalid JSON: {e}", file=sys.stderr)
continue
ip = obj.get("ip", "?")
mac = obj.get("mac", "?")
dhcp = obj.get("dhcp", "?")
fiwi = obj.get("fiwi_telemetry", False)
if mac == "?":
continue
is_new = mac not in devices
devices[mac] = {
"ip": ip,
"dhcp": dhcp,
"fiwi_telemetry": fiwi,
"last_download": devices.get(mac, {}).get("last_download", 0),
}
if not args.quiet or (args.download and fiwi and is_new):
print(f"Device: {ip} | MAC: {mac} | DHCP: {dhcp} | fiwi-telemetry: {fiwi}")
if args.download and fiwi:
should_download = is_new or (
args.refresh > 0
and (time.time() - devices[mac]["last_download"]) >= args.refresh
)
if should_download:
if download_telemetry(ip, mac, args.output_dir):
devices[mac]["last_download"] = time.time()
print(f" Downloaded -> {args.output_dir}/fiwi-telemetry_{mac_to_filename(mac)}")
else:
print(f" Download failed for {ip}", file=sys.stderr)
if not args.quiet and is_new:
print()
sock.close()
if not args.quiet:
print(f"\nSeen {len(devices)} unique device(s).")
if __name__ == "__main__":
main()

View File

@ -1,114 +0,0 @@
#!/usr/bin/env python3
"""
Receive a file from the ESP32 SD card over serial.
The device must be running the sdcard send command; this script sends the
command and captures the hex-encoded output, then decodes and saves to a file.
Usage:
python3 tools/sdcard_recv.py -p /dev/ttyUSB0 -f myfile.txt [-o output.bin]
python3 tools/sdcard_recv.py --port /dev/ttyUSB0 --remote myfile.txt
Requires: pyserial (pip install pyserial)
"""
import argparse
import re
import sys
import time
try:
import serial
except ImportError:
print("Error: pyserial required. Run: pip install pyserial", file=sys.stderr)
sys.exit(1)
def main():
ap = argparse.ArgumentParser(description="Receive file from ESP32 SD card over serial")
ap.add_argument("-p", "--port", required=True, help="Serial port (e.g. /dev/ttyUSB0)")
ap.add_argument("-b", "--baud", type=int, default=115200, help="Baud rate (default 115200)")
ap.add_argument("-f", "--remote", "--file", dest="remote", required=True,
help="Path of file on the SD card (e.g. myfile.txt or log/data.bin)")
ap.add_argument("-o", "--output", help="Local output path (default: basename of remote file)")
ap.add_argument("-t", "--timeout", type=float, default=60.0,
help="Timeout in seconds for transfer (default 60)")
args = ap.parse_args()
out_path = args.output
if not out_path:
out_path = args.remote.split("/")[-1].split("\\")[-1] or "received.bin"
ser = serial.Serial(args.port, args.baud, timeout=1.0)
# Drain any pending input
ser.reset_input_buffer()
# Send: sdcard send <path>\r\n
cmd = f"sdcard send {args.remote}\r\n"
ser.write(cmd.encode("ascii"))
ser.flush()
# Wait for ---SDFILE---
marker_start = b"---SDFILE---"
marker_end = b"---END SDFILE---"
line_buf = b""
state = "wait_start"
remote_name = None
size_val = None
hex_buf = []
deadline = time.time() + args.timeout
while True:
if time.time() > deadline:
print("Timeout waiting for transfer", file=sys.stderr)
sys.exit(1)
c = ser.read(1)
if not c:
continue
line_buf += c
if c != b"\n" and c != b"\r":
if len(line_buf) > 2048:
line_buf = line_buf[-1024:]
continue
line = line_buf.decode("ascii", errors="ignore").strip()
line_buf = b""
if state == "wait_start":
if marker_start.decode() in line or line == "---SDFILE---":
state = "read_meta"
continue
if state == "read_meta":
if line.startswith("SIZE:"):
try:
size_val = int(line.split(":", 1)[1].strip())
except ValueError:
size_val = 0
state = "wait_hex"
elif line and not line.startswith("---"):
remote_name = line
continue
if state == "wait_hex":
if "---HEX---" in line:
state = "read_hex"
continue
if state == "read_hex":
if marker_end.decode() in line or line == "---END SDFILE---":
break
# Strip non-hex and decode
hex_part = re.sub(r"[^0-9a-fA-F]", "", line)
if hex_part:
hex_buf.append(hex_part)
continue
# Decode hex and write
raw = bytes.fromhex("".join(hex_buf))
if size_val is not None and len(raw) != size_val:
print(f"Warning: size mismatch (expected {size_val}, got {len(raw)})", file=sys.stderr)
with open(out_path, "wb") as f:
f.write(raw)
print(f"Saved {len(raw)} bytes to {out_path}")
ser.close()
if __name__ == "__main__":
main()