Compare commits

..

7 Commits

Author SHA1 Message Date
Robert McMahon fdc39bbd2a update README 2025-12-31 08:16:59 -08:00
Robert McMahon 21439cab8d Fix documentation links: convert inline doc references to proper markdown links 2025-12-28 18:04:14 -08:00
Robert McMahon a34cafc034 Add note that get_idf must be run before all idf.py commands 2025-12-28 18:00:15 -08:00
Robert McMahon 78a98d8ca6 Add ESP-IDF v6 setup instructions with get_idf() function 2025-12-28 17:58:16 -08:00
Robert McMahon 886cbf291a Add USB port mapping instructions to README.md
- Introduced a new section detailing the `gen_udev_rules.py` script for stable USB port mapping.
- Explained features, usage, and installation steps for generated udev rules to ensure consistent device identification across reboots.
2025-12-28 17:53:13 -08:00
Robert McMahon 404b7e3ad7 Update README.md to correct deployment script parameter from --target-ip to --start-ip 2025-12-28 17:49:33 -08:00
Robert McMahon 1a6ab77b20 Move README.md to root and update with comprehensive project documentation 2025-12-27 19:47:26 -08:00
49 changed files with 179 additions and 6156 deletions

View File

@ -1,6 +1,6 @@
# ESP32 Wi-Fi Telemetry Capture / iPerf Load Generator # ESP32 Wi-Fi Collapse Detector / iPerf Load Generator
High-performance Wi-Fi monitor and iperf 2 based traffic generator firmware for ESP32 devices with interactive console interface, GPS synchronization, and Wi-Fi telemetry capture (MCS, RSSI, frame stats, collapse detection, and more). High-performance Wi-Fi monitor and iperf 2 based traffic generator firmware for ESP32 devices with interactive console interface, GPS synchronization, and WiFi collapse detection capabilities.
**Version:** 2.1.0 **Version:** 2.1.0
**ESP-IDF:** 6.0+ **ESP-IDF:** 6.0+
@ -12,7 +12,6 @@ High-performance Wi-Fi monitor and iperf 2 based traffic generator firmware for
- **WiFi Management**: Station mode with static IP support, DHCP, and monitor mode - **WiFi Management**: Station mode with static IP support, DHCP, and monitor mode
- **GPS Synchronization**: PPS signal support and NMEA parsing for timestamp synchronization - **GPS Synchronization**: PPS signal support and NMEA parsing for timestamp synchronization
- **Persistent Configuration**: NVS storage for WiFi credentials, IP settings, and iPerf parameters - **Persistent Configuration**: NVS storage for WiFi credentials, IP settings, and iPerf parameters
- **SD Card**: Telemetry storage (fiwi-telemetry), HTTP file download (port 8080), serial transfer, broadcast beacon for laptop discovery
- **Status LED**: Visual feedback for connection state and system status - **Status LED**: Visual feedback for connection state and system status
- **Mass Deployment**: Python scripts for flashing and configuring multiple devices - **Mass Deployment**: Python scripts for flashing and configuring multiple devices
@ -96,11 +95,6 @@ iperf start
# Check status # Check status
iperf status iperf status
# WiFi telemetry (monitor mode writes to fiwi-telemetry on SD; disconnects WiFi)
monitor start 6
sdcard status
sdcard read fiwi-telemetry
``` ```
## Console Commands ## Console Commands
@ -136,29 +130,11 @@ sdcard read fiwi-telemetry
- `gps status` - Show GPS synchronization status - `gps status` - Show GPS synchronization status
### Monitor Mode ### Monitor Mode
- `monitor start [channel]` - Start WiFi monitor mode (writes MCS telemetry to `fiwi-telemetry` on SD every 10s) - `monitor start [channel]` - Start WiFi monitor mode
- `monitor stop` - Stop monitor mode - `monitor stop` - Stop monitor mode
- `monitor channel <channel>` - Set monitor channel - `monitor channel <channel>` - Set monitor channel
- `monitor status` - Show monitor status - `monitor status` - Show monitor status
### SD Card Commands
Telemetry from monitor mode is stored in `fiwi-telemetry` (NDJSON, appended each flush). Use SD commands to inspect or transfer it.
- `sdcard status` - Show CD, mounted, capacity, and fiwi-telemetry file info
- `sdcard list [path]` - List files (path optional, default root)
- `sdcard read <file>` - Read and print file (e.g. `sdcard read fiwi-telemetry`)
- `sdcard write <file> <text>` - Write text to file
- `sdcard send <file>` - Stream file over serial (use `tools/sdcard_recv.py`)
- `sdcard delete <file>` - Delete a file
**Example (telemetry capture):**
```text
monitor start 6
sdcard status
sdcard list
sdcard read fiwi-telemetry
```
For detailed command documentation, see [Quick Reference](doc/QUICK_REFERENCE.md). For detailed command documentation, see [Quick Reference](doc/QUICK_REFERENCE.md).
## Mass Deployment ## Mass Deployment
@ -215,33 +191,15 @@ The deployment script (`esp32_deploy.py`) can use these stable symlinks when `--
``` ```
├── main/ # Main application code ├── main/ # Main application code
│ ├── main.c # Entry point and console initialization │ ├── main.c # Entry point and console initialization
│ ├── broadcast_beacon.c/h # UDP broadcast for laptop discovery
│ └── board_config.h # Hardware pin definitions │ └── board_config.h # Hardware pin definitions
├── components/ ├── components/
│ ├── app_console/ # Console command implementations │ ├── app_console/ # Console command implementations
│ │ ├── cmd_wifi.c # WiFi commands (connect, scan, status, mode, power)
│ │ ├── cmd_ip.c # IP configuration (addr, set, dhcp)
│ │ ├── cmd_iperf.c # iPerf commands (start, stop, status, set, save, reload, clear)
│ │ ├── cmd_monitor.c # Monitor mode (start, stop, channel, status)
│ │ ├── cmd_sdcard.c # SD card commands (status, list, read, write, send, delete)
│ │ ├── cmd_gps.c # GPS commands (status)
│ │ ├── cmd_system.c # System commands (reset, version, info, heap)
│ │ ├── cmd_ping.c # Ping command
│ │ └── cmd_nvs.c # NVS management commands
│ ├── iperf/ # iPerf traffic generator core │ ├── iperf/ # iPerf traffic generator core
│ ├── wifi_controller/ # WiFi management and monitor mode │ ├── wifi_controller/ # WiFi management and monitor mode
│ ├── wifi_monitor/ # 802.11 frame capture and collapse detection
│ ├── wifi_cfg/ # WiFi and IP configuration storage │ ├── wifi_cfg/ # WiFi and IP configuration storage
│ ├── gps_sync/ # GPS PPS and NMEA parsing │ ├── gps_sync/ # GPS PPS and NMEA parsing
│ ├── status_led/ # LED status indication │ ├── status_led/ # LED status indication
│ ├── sd_card/ # SD card SPI mount and file I/O │ └── ...
│ ├── sdcard_http/ # HTTP server for SD file download (port 8080)
│ ├── mcs_telemetry/ # MCS/RSSI telemetry -> fiwi-telemetry
│ ├── csi_log/ # CSI logging (when CSI enabled)
│ └── csi_manager/ # CSI configuration (when CSI enabled)
├── tools/
│ ├── beacon_listen.py # Listen for beacons, download fiwi-telemetry
│ └── sdcard_recv.py # Receive file over serial from device
├── esp32_deploy.py # Mass deployment script ├── esp32_deploy.py # Mass deployment script
├── gen_udev_rules.py # USB port mapping utility ├── gen_udev_rules.py # USB port mapping utility
└── doc/ # Additional documentation └── doc/ # Additional documentation
@ -249,13 +207,11 @@ The deployment script (`esp32_deploy.py`) can use these stable symlinks when `--
## Documentation ## Documentation
- [Quick Start Guide](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/QUICK_START.md) - [Quick Start Guide](doc/QUICK_START.md)
- [Quick Reference](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/QUICK_REFERENCE.md) - [Quick Reference](doc/QUICK_REFERENCE.md)
- [SD Card Wiring](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/SD_CARD_WIRING.md) - Hardware, file transfer, beacon discovery - [Deployment Guide](doc/DEPLOYMENT_GUIDE.md)
- [Telemetry Capture](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/TELEMETRY_CAPTURE.md) - Enable and capture fiwi-telemetry - [Mass Deployment](doc/MASS_DEPLOY.md)
- [Deployment Guide](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/DEPLOYMENT_GUIDE.md) - [GDB Debugging Guide (ESP32-C5)](doc/ESP32-C5_GDB_Debugging_Guide.md)
- [Mass Deployment](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/MASS_DEPLOY.md)
- [GDB Debugging Guide (ESP32-C5)](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/ESP32-C5_GDB_Debugging_Guide.md)
## License ## License

View File

@ -1,40 +0,0 @@
# Installing Scapy on Raspberry Pi 5
## Option 1: Install via apt (Recommended)
```bash
sudo apt-get install python3-scapy
```
This is the safest method and integrates with the system package manager.
## Option 2: Use --break-system-packages (For system tools)
Since this is a system-level monitoring tool that runs as root, you can use:
```bash
sudo pip3 install --break-system-packages scapy
```
## Option 3: Virtual Environment (Not recommended for root tools)
If you prefer a virtual environment (though less convenient for root tools):
```bash
python3 -m venv ~/scapy-env
source ~/scapy-env/bin/activate
pip install scapy
```
Then run the script with:
```bash
sudo ~/scapy-env/bin/python3 rpi_capture_ra_ta_python.py 11
```
## Quick Install Command
For this use case (system monitoring tool), Option 2 is acceptable:
```bash
sudo pip3 install --break-system-packages scapy
```

View File

@ -1,216 +0,0 @@
# Raspberry Pi 5 WiFi Monitor Mode Guide
## Quick Setup
1. **Copy the setup script to your Raspberry Pi:**
```bash
scp rpi_monitor_setup.sh pi@your-pi-ip:~/
```
2. **SSH into your Raspberry Pi and run:**
```bash
sudo ./rpi_monitor_setup.sh [channel]
```
Example for channel 11:
```bash
sudo ./rpi_monitor_setup.sh 11
```
## Manual Setup (Alternative)
If you prefer to run commands manually:
```bash
# 1. Check available interfaces
iw dev
# 2. Bring down the interface
sudo ip link set wlan0 down
# 3. Set to monitor mode
sudo iw dev wlan0 set type monitor
# 4. Bring up the interface
sudo ip link set wlan0 up
# 5. Set channel (e.g., channel 11)
sudo iw dev wlan0 set channel 11
# 6. Verify monitor mode
iw dev wlan0 info
```
## Capturing Packets
Once monitor mode is active, you can capture packets:
### Using tcpdump (simple)
```bash
# View packets in real-time
sudo tcpdump -i wlan0 -n
# Save to file
sudo tcpdump -i wlan0 -w capture.pcap
# Filter by MAC address (e.g., your Pi's MAC)
sudo tcpdump -i wlan0 -n ether host 80:84:89:93:c4:b6
# Filter by channel (if using multiple interfaces)
sudo tcpdump -i wlan0 -n -c 100 # Capture 100 packets
```
### Using airodump-ng (advanced, requires aircrack-ng)
```bash
# Install aircrack-ng if needed
sudo apt-get update
sudo apt-get install aircrack-ng
# Capture on specific channel
sudo airodump-ng wlan0 -c 11
# Save to file
sudo airodump-ng wlan0 -c 11 -w capture
```
### Using Wireshark (GUI)
```bash
# Install wireshark if needed
sudo apt-get install wireshark
# Run wireshark (may need to add user to wireshark group)
sudo wireshark -i wlan0
```
## Capturing RA/TA Addresses
### Quick Capture Script (Recommended)
Use the provided Python script for best results:
```bash
# Install scapy if needed
sudo apt-get install python3-pip
sudo pip3 install scapy
# Capture on channel 11 (shows all frames with RA/TA)
sudo python3 rpi_capture_ra_ta_python.py 11
# Capture and filter by specific MAC address
sudo python3 rpi_capture_ra_ta_python.py 11 80:84:89:93:c4:b6
```
The script will:
- Automatically set monitor mode
- Parse 802.11 frames correctly
- Display RA (Receiver Address) and TA (Transmitter Address)
- Show frame type, RSSI, length, and QoS info
- Provide statistics when stopped (Ctrl+C)
### Alternative: Bash Script
For a simpler bash-based solution:
```bash
# Capture on channel 11
sudo ./rpi_capture_ra_ta.sh 11
# Capture and filter by MAC
sudo ./rpi_capture_ra_ta.sh 11 80:84:89:93:c4:b6
```
## Monitoring Specific Traffic
### Filter by MAC address (TA/RA)
```bash
# Capture frames from specific transmitter (TA)
sudo tcpdump -i wlan0 -n ether src 80:84:89:93:c4:b6
# Capture frames to specific receiver (RA)
sudo tcpdump -i wlan0 -n ether dst e0:46:ee:07:df:e1
# Capture frames involving either address
sudo tcpdump -i wlan0 -n "ether host 80:84:89:93:c4:b6 or ether host e0:46:ee:07:df:e1"
```
### Filter by frame type
```bash
# Data frames only
sudo tcpdump -i wlan0 -n "type wlan type data"
# Management frames (beacons, probes, etc.)
sudo tcpdump -i wlan0 -n "type wlan type mgt"
# Control frames (RTS, CTS, ACK)
sudo tcpdump -i wlan0 -n "type wlan type ctl"
```
## Restoring Normal WiFi
To restore normal WiFi operation:
```bash
# Bring down interface
sudo ip link set wlan0 down
# Set back to managed mode
sudo iw dev wlan0 set type managed
# Bring up interface
sudo ip link set wlan0 up
# Reconnect to your network (use NetworkManager, wpa_supplicant, etc.)
sudo nmcli device wifi connect "YourSSID" password "YourPassword"
# OR
sudo wpa_supplicant -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf &
sudo dhclient wlan0
```
## Troubleshooting
### Interface not found
```bash
# List all network interfaces
ip link show
# Check WiFi interfaces specifically
iw dev
```
### Permission denied
- Make sure you're using `sudo` for all monitor mode commands
- Some distributions require adding your user to specific groups
### Can't set monitor mode
- Some WiFi adapters don't support monitor mode
- Check adapter capabilities: `iw phy | grep -A 10 "Supported interface modes"`
- Raspberry Pi 5 built-in WiFi should support monitor mode
### Channel not changing
- Make sure the interface is up: `sudo ip link set wlan0 up`
- Try bringing it down first, then setting channel, then bringing it up
## Useful Commands
```bash
# Check current interface status
iw dev wlan0 info
# Scan for networks (won't work in monitor mode, but useful before switching)
iw dev wlan0 scan
# Check signal strength and link info (before switching to monitor mode)
iw dev wlan0 link
# Monitor channel activity
watch -n 1 "iw dev wlan0 info | grep channel"
```
## Comparing with ESP32 Monitor
When comparing captures between your ESP32 and Raspberry Pi:
1. **Ensure same channel**: Both devices must monitor the same channel
2. **Time sync**: Consider using NTP for accurate timestamp comparison
3. **MAC filtering**: Use tcpdump filters to match your ESP32's filter settings
4. **Frame types**: Both should capture the same frame types (data, management, control)

View File

@ -8,9 +8,8 @@ idf_component_register(
"cmd_gps.c" "cmd_gps.c"
"cmd_ping.c" "cmd_ping.c"
"cmd_ip.c" "cmd_ip.c"
"cmd_sdcard.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES console wifi_cfg sd_card sdcard_http REQUIRES console wifi_cfg
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

@ -45,5 +45,4 @@ void app_console_register_commands(void) {
register_ping_cmd(); register_ping_cmd();
register_monitor_cmd(); register_monitor_cmd();
register_ip_cmd(); register_ip_cmd();
register_sdcard_cmd();
} }

View File

@ -51,7 +51,6 @@ void register_gps_cmd(void);
void register_ping_cmd(void); void register_ping_cmd(void);
void register_monitor_cmd(void); void register_monitor_cmd(void);
void register_ip_cmd(void); void register_ip_cmd(void);
void register_sdcard_cmd(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@ -43,8 +43,6 @@
// --- Forward Declarations --- // --- Forward Declarations ---
static int gps_do_status(int argc, char **argv); static int gps_do_status(int argc, char **argv);
static int gps_do_pps_test(int argc, char **argv);
static int gps_do_pps_scan(int argc, char **argv);
// ============================================================================ // ============================================================================
// COMMAND: gps (Dispatcher) // COMMAND: gps (Dispatcher)
@ -54,8 +52,6 @@ static void print_gps_usage(void) {
printf("Usage: gps <subcommand> [args]\n"); printf("Usage: gps <subcommand> [args]\n");
printf("Subcommands:\n"); printf("Subcommands:\n");
printf(" status Show GPS lock status, time, and last NMEA message\n"); printf(" status Show GPS lock status, time, and last NMEA message\n");
printf(" pps-test Poll PPS GPIO to verify signal (default 5 sec)\n");
printf(" pps-scan Scan GPIO 1, 6, 25 to find which has PPS signal\n");
printf("\nType 'gps <subcommand> --help' for details.\n"); printf("\nType 'gps <subcommand> --help' for details.\n");
} }
@ -67,8 +63,6 @@ static int cmd_gps(int argc, char **argv) {
if (strcmp(argv[1], "status") == 0) return gps_do_status(argc - 1, &argv[1]); if (strcmp(argv[1], "status") == 0) return gps_do_status(argc - 1, &argv[1]);
if (strcmp(argv[1], "info") == 0) return gps_do_status(argc - 1, &argv[1]); // Alias if (strcmp(argv[1], "info") == 0) return gps_do_status(argc - 1, &argv[1]); // Alias
if (strcmp(argv[1], "pps-test") == 0) return gps_do_pps_test(argc - 1, &argv[1]);
if (strcmp(argv[1], "pps-scan") == 0) return gps_do_pps_scan(argc - 1, &argv[1]);
printf("Unknown subcommand '%s'.\n", argv[1]); printf("Unknown subcommand '%s'.\n", argv[1]);
print_gps_usage(); print_gps_usage();
@ -135,77 +129,6 @@ static int gps_do_status(int argc, char **argv) {
return 0; return 0;
} }
// ----------------------------------------------------------------------------
// Sub-command: pps-test
// ----------------------------------------------------------------------------
static struct {
struct arg_int *duration;
struct arg_lit *help;
struct arg_end *end;
} pps_test_args;
static int gps_do_pps_test(int argc, char **argv) {
pps_test_args.duration = arg_int0("d", "duration", "<sec>", "Seconds to poll (1-30, default 5)");
pps_test_args.help = arg_lit0("h", "help", "Help");
pps_test_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&pps_test_args);
if (nerrors > 0) {
arg_print_errors(stderr, pps_test_args.end, argv[0]);
return 1;
}
if (pps_test_args.help->count > 0) {
printf("Usage: gps pps-test [-d <sec>]\n");
printf("Polls the PPS GPIO to verify the signal is reaching the ESP32.\n");
return 0;
}
int duration = 5;
if (pps_test_args.duration->count > 0) {
duration = pps_test_args.duration->ival[0];
}
gps_pps_diagnostic(duration);
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: pps-scan
// ----------------------------------------------------------------------------
static struct {
struct arg_int *duration;
struct arg_lit *help;
struct arg_end *end;
} pps_scan_args;
static int gps_do_pps_scan(int argc, char **argv) {
pps_scan_args.duration = arg_int0("d", "duration", "<sec>", "Seconds per pin (1-10, default 3)");
pps_scan_args.help = arg_lit0("h", "help", "Help");
pps_scan_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&pps_scan_args);
if (nerrors > 0) {
arg_print_errors(stderr, pps_scan_args.end, argv[0]);
return 1;
}
if (pps_scan_args.help->count > 0) {
printf("Usage: gps pps-scan [-d <sec>]\n");
printf("Scans GPIO 1, 6, 25 to find which pin has the PPS signal (~1 Hz).\n");
printf("Use this if pps-test shows no edges on the configured pin.\n");
return 0;
}
int duration = 3;
if (pps_scan_args.duration->count > 0) {
duration = pps_scan_args.duration->ival[0];
}
gps_pps_scan(duration);
return 0;
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Registration // Registration
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -213,7 +136,7 @@ static int gps_do_pps_scan(int argc, char **argv) {
void register_gps_cmd(void) { void register_gps_cmd(void) {
const esp_console_cmd_t cmd = { const esp_console_cmd_t cmd = {
.command = "gps", .command = "gps",
.help = "GPS Tool: status, pps-test, pps-scan", .help = "GPS Tool: status",
.hint = "<subcommand>", .hint = "<subcommand>",
.func = &cmd_gps, .func = &cmd_gps,
.argtable = NULL .argtable = NULL

View File

@ -49,16 +49,6 @@ static struct {
struct arg_end *end; struct arg_end *end;
} channel_args; } channel_args;
static struct {
struct arg_str *enable;
struct arg_end *end;
} debug_args;
static struct {
struct arg_str *mac;
struct arg_end *end;
} filter_args;
static void print_monitor_usage(void) { static void print_monitor_usage(void) {
printf("Usage: monitor <subcommand> [args]\n"); printf("Usage: monitor <subcommand> [args]\n");
printf("Subcommands:\n"); printf("Subcommands:\n");
@ -66,8 +56,6 @@ static void print_monitor_usage(void) {
printf(" stop Stop Monitor Mode\n"); printf(" stop Stop Monitor Mode\n");
printf(" status Show current status\n"); printf(" status Show current status\n");
printf(" channel <n> Switch channel (while running)\n"); printf(" channel <n> Switch channel (while running)\n");
printf(" debug [on|off] Enable/disable debug logging (default: show status)\n");
printf(" filter [mac] Set MAC address filter for debug (e.g., 80:84:89:93:c4:b6), or 'clear' to disable\n");
printf(" save Save current config to NVS\n"); printf(" save Save current config to NVS\n");
printf(" reload Reload config from NVS\n"); printf(" reload Reload config from NVS\n");
printf(" clear Clear NVS config\n"); printf(" clear Clear NVS config\n");
@ -111,107 +99,6 @@ static int do_monitor_channel(int argc, char **argv) {
return 0; return 0;
} }
static int do_monitor_debug(int argc, char **argv) {
debug_args.enable = arg_str0(NULL, NULL, "<on|off>", "Enable (on) or disable (off) debug logging");
debug_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&debug_args);
if (nerrors > 0) {
arg_print_errors(stderr, debug_args.end, argv[0]);
return 1;
}
if (debug_args.enable->count > 0) {
const char *value = debug_args.enable->sval[0];
bool enable = false;
if (strcmp(value, "on") == 0 || strcmp(value, "1") == 0 || strcmp(value, "true") == 0) {
enable = true;
} else if (strcmp(value, "off") == 0 || strcmp(value, "0") == 0 || strcmp(value, "false") == 0) {
enable = false;
} else {
printf("Invalid value '%s'. Use 'on' or 'off'.\n", value);
return 1;
}
wifi_ctl_set_monitor_debug(enable);
printf("Debug mode %s\n", enable ? "enabled" : "disabled");
} else {
/* No argument: show current status */
bool enabled = wifi_ctl_get_monitor_debug();
printf("Debug mode: %s\n", enabled ? "enabled" : "disabled");
}
return 0;
}
static int do_monitor_filter(int argc, char **argv) {
filter_args.mac = arg_str0(NULL, NULL, "<mac|clear>", "MAC address (XX:XX:XX:XX:XX:XX) or 'clear' to disable filter");
filter_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&filter_args);
if (nerrors > 0) {
arg_print_errors(stderr, filter_args.end, argv[0]);
return 1;
}
if (filter_args.mac->count > 0) {
const char *mac_str = filter_args.mac->sval[0];
if (strcmp(mac_str, "clear") == 0) {
wifi_ctl_set_monitor_debug_filter(NULL);
printf("Debug filter cleared\n");
} else {
/* Parse MAC address string (format: XX:XX:XX:XX:XX:XX) */
uint8_t mac[6];
int values[6];
int count = sscanf(mac_str, "%x:%x:%x:%x:%x:%x",
&values[0], &values[1], &values[2],
&values[3], &values[4], &values[5]);
if (count == 6) {
/* Validate values are in range */
bool valid = true;
for (int i = 0; i < 6; i++) {
if (values[i] < 0 || values[i] > 255) {
valid = false;
break;
}
mac[i] = (uint8_t)values[i];
}
if (valid) {
if (wifi_ctl_set_monitor_debug_filter(mac) == ESP_OK) {
printf("Debug filter set to: %02x:%02x:%02x:%02x:%02x:%02x\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
printf("Failed to set debug filter\n");
return 1;
}
} else {
printf("Invalid MAC address: values must be 0-255\n");
return 1;
}
} else {
printf("Invalid MAC address format. Use XX:XX:XX:XX:XX:XX or 'clear'\n");
return 1;
}
}
} else {
/* No argument: show current filter */
uint8_t mac[6];
bool enabled = wifi_ctl_get_monitor_debug_filter(mac);
if (enabled) {
printf("Debug filter: %02x:%02x:%02x:%02x:%02x:%02x\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
printf("Debug filter: disabled\n");
}
}
return 0;
}
static int cmd_monitor(int argc, char **argv) { static int cmd_monitor(int argc, char **argv) {
if (argc < 2) { if (argc < 2) {
print_monitor_usage(); print_monitor_usage();
@ -226,8 +113,6 @@ static int cmd_monitor(int argc, char **argv) {
if (strcmp(argv[1], "clear") == 0) { wifi_ctl_param_clear(); printf("Cleared.\n"); return 0; } if (strcmp(argv[1], "clear") == 0) { wifi_ctl_param_clear(); printf("Cleared.\n"); return 0; }
if (strcmp(argv[1], "channel") == 0) return do_monitor_channel(argc - 1, &argv[1]); if (strcmp(argv[1], "channel") == 0) return do_monitor_channel(argc - 1, &argv[1]);
if (strcmp(argv[1], "debug") == 0) return do_monitor_debug(argc - 1, &argv[1]);
if (strcmp(argv[1], "filter") == 0) return do_monitor_filter(argc - 1, &argv[1]);
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) { if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) {
print_monitor_usage(); print_monitor_usage();
@ -246,12 +131,6 @@ void register_monitor_cmd(void) {
channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel"); channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel");
channel_args.end = arg_end(1); channel_args.end = arg_end(1);
debug_args.enable = arg_str0(NULL, NULL, "<on|off>", "Enable or disable debug logging");
debug_args.end = arg_end(1);
filter_args.mac = arg_str0(NULL, NULL, "<mac|clear>", "MAC address filter or 'clear'");
filter_args.end = arg_end(1);
const esp_console_cmd_t cmd = { const esp_console_cmd_t cmd = {
.command = "monitor", .command = "monitor",
.help = "Monitor Mode: start, stop, channel, status", .help = "Monitor Mode: start, stop, channel, status",

View File

@ -1,312 +0,0 @@
/*
* cmd_sdcard.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 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
* 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 specific 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 <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "esp_console.h"
#include "argtable3/argtable3.h"
#include "app_console.h"
#include "sd_card.h"
#include "sdcard_http.h"
#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) {
(void)argc;
(void)argv;
printf("SD Card Status:\n");
printf(" CD (Card Detect): ");
if (sd_card_cd_available()) {
int level = sd_card_cd_get_level();
bool inserted = sd_card_cd_is_inserted();
printf("%s (GPIO=%s)\n", inserted ? "INSERTED" : "REMOVED",
level >= 0 ? (level ? "HIGH" : "LOW") : "?");
if (!inserted && sd_card_is_ready()) {
printf(" (Card works but CD says REMOVED: wire CD to J1 Pin 12, or menuconfig -> SD Card -> uncheck 'LOW = inserted')\n");
}
} else {
printf("N/A (pin not configured)\n");
}
printf(" Mounted: %s\n", sd_card_is_ready() ? "yes" : "no");
if (sd_card_is_ready()) {
uint64_t total = 0, free_bytes = 0;
if (sd_card_get_info(&total, &free_bytes) == 0) {
printf(" Total: %.2f MB\n", total / (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;
}
static int do_sdcard_write(int argc, char **argv) {
if (argc < 3) {
printf("Usage: sdcard write <file> <text...>\n");
return 1;
}
const char *filename = argv[1];
if (!sd_card_is_ready()) {
printf("Error: SD card not mounted\n");
return 1;
}
/* Join argv[2]..argv[argc-1] with spaces for multi-word text */
static char text_buf[512];
size_t off = 0;
for (int i = 2; i < argc && off < sizeof(text_buf) - 2; i++) {
if (i > 2) {
text_buf[off++] = ' ';
}
size_t len = strlen(argv[i]);
if (off + len >= sizeof(text_buf)) {
len = sizeof(text_buf) - off - 1;
}
memcpy(text_buf + off, argv[i], len);
off += len;
}
text_buf[off] = '\0';
esp_err_t ret = sd_card_write_file(filename, text_buf, off, false);
if (ret != 0) {
printf("Write failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("Wrote %zu bytes to %s\n", off, filename);
return 0;
}
static int do_sdcard_read(int argc, char **argv) {
if (argc < 2) {
printf("Usage: sdcard read <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;
}
static uint8_t buf[SDCARD_READ_BUF_SIZE];
size_t bytes_read = 0;
esp_err_t ret = sd_card_read_file(filename, buf, sizeof(buf) - 1, &bytes_read);
if (ret != 0) {
printf("Read failed: %s\n", esp_err_to_name(ret));
return 1;
}
buf[bytes_read] = '\0';
printf("Read %zu bytes from %s:\n", bytes_read, filename);
printf("---\n%s\n---\n", (char *)buf);
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) {
if (argc < 2) {
printf("Usage: sdcard <status|list|write|read|send|delete> [args]\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(" 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;
}
if (strcmp(argv[1], "status") == 0) {
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) {
return do_sdcard_write(argc - 1, &argv[1]);
}
if (strcmp(argv[1], "read") == 0) {
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]);
return 1;
}
void register_sdcard_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "sdcard",
.help = "SD card: status, list [path], write, read, send, delete <file>",
.hint = "<status|list|write|read|send|delete>",
.func = &cmd_sdcard,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -226,7 +226,7 @@ void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) {
} }
gpio_config_t io_conf = {}; gpio_config_t io_conf = {};
io_conf.intr_type = s_cfg.pps_active_low ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; io_conf.intr_type = GPIO_INTR_POSEDGE;
io_conf.pin_bit_mask = (1ULL << s_cfg.pps_pin); io_conf.pin_bit_mask = (1ULL << s_cfg.pps_pin);
io_conf.mode = GPIO_MODE_INPUT; io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = 1; io_conf.pull_up_en = 1;
@ -251,8 +251,7 @@ void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) {
xTaskCreate(gps_task, "gps_task", 4096, NULL, 5, NULL); xTaskCreate(gps_task, "gps_task", 4096, NULL, 5, NULL);
ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d %s)", s_cfg.uart_port, s_cfg.pps_pin, ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d)", s_cfg.uart_port, s_cfg.pps_pin);
s_cfg.pps_active_low ? "falling-edge" : "rising-edge");
} }
gps_timestamp_t gps_get_timestamp(void) { gps_timestamp_t gps_get_timestamp(void) {
@ -281,107 +280,3 @@ void gps_get_last_nmea(char *buf, size_t buf_len) {
strlcpy(buf, s_last_nmea_msg, buf_len); strlcpy(buf, s_last_nmea_msg, buf_len);
} }
} }
void gps_pps_diagnostic(int duration_sec) {
if (duration_sec < 1) duration_sec = 1;
if (duration_sec > 30) duration_sec = 30;
const int interval_ms = 50;
int samples = (duration_sec * 1000) / interval_ms;
int prev_level = -1;
int rising = 0, falling = 0;
printf("PPS diagnostic: polling GPIO%d for %d sec (interval %d ms)...\n",
s_cfg.pps_pin, duration_sec, interval_ms);
for (int i = 0; i < samples; i++) {
int level = gpio_get_level(s_cfg.pps_pin);
if (prev_level >= 0) {
if (level > prev_level) rising++;
else if (level < prev_level) falling++;
}
prev_level = level;
vTaskDelay(pdMS_TO_TICKS(interval_ms));
}
int total = rising + falling;
printf(" Samples: %d, Rising edges: %d, Falling edges: %d\n", samples, rising, falling);
/* 1 Hz pulse gives ~2 edges/sec (rising+falling) */
int lo = 2 * (duration_sec - 1);
int hi = 2 * (duration_sec + 1);
if (total >= lo && total <= hi) {
printf(" Signal: DETECTED (~1 Hz)\n");
printf(" Try opposite polarity: set GPS_PPS_ACTIVE_LOW=%d in board_config.h\n",
falling > rising ? 0 : 1);
} else if (total == 0) {
printf(" Signal: NOT DETECTED (no edges)\n");
printf(" Check: PPS wire, GPS PPS pin, UBX-CFG-TP to enable timepulse\n");
} else {
printf(" Signal: UNCLEAR (%d edges in %ds, expected ~%d)\n", total, duration_sec, 2 * duration_sec);
}
}
// Candidate PPS pins for ESP32-C5 (J1: 6=GPIO1, 7=GPIO6, 13=GPIO25)
static const int s_pps_scan_pins[] = { 1, 6, 25 };
#define PPS_SCAN_PIN_COUNT (sizeof(s_pps_scan_pins) / sizeof(s_pps_scan_pins[0]))
static void poll_pin_edges(int pin, int duration_sec, int interval_ms,
int *rising, int *falling) {
gpio_config_t io = {
.pin_bit_mask = (1ULL << pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = 1,
.pull_down_en = 0,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io);
int samples = (duration_sec * 1000) / interval_ms;
int prev = -1;
*rising = *falling = 0;
for (int i = 0; i < samples; i++) {
int lvl = gpio_get_level(pin);
if (prev >= 0) {
if (lvl > prev) (*rising)++;
else if (lvl < prev) (*falling)++;
}
prev = lvl;
vTaskDelay(pdMS_TO_TICKS(interval_ms));
}
}
void gps_pps_scan(int duration_sec) {
if (duration_sec < 1) duration_sec = 1;
if (duration_sec > 10) duration_sec = 10;
const int interval_ms = 50;
printf("PPS scan: polling GPIO 1, 6, 25 for %d sec each...\n", duration_sec);
printf("(J1 Pin 6=GPIO1, Pin 7=GPIO6, Pin 13=GPIO25)\n\n");
for (size_t i = 0; i < PPS_SCAN_PIN_COUNT; i++) {
int pin = s_pps_scan_pins[i];
int rising, falling;
poll_pin_edges(pin, duration_sec, interval_ms, &rising, &falling);
int total = rising + falling;
/* 1 Hz pulse gives ~2 edges/sec (rising+falling) */
int lo = 2 * (duration_sec - 1);
int hi = 2 * (duration_sec + 1);
const char *result;
if (total >= lo && total <= hi) {
result = "DETECTED (~1 Hz)";
} else if (total == 0) {
result = "no signal";
} else {
result = "unclear";
}
printf(" GPIO%-2d (J1 P%2d): rising=%d falling=%d -> %s\n",
pin, (pin == 1) ? 6 : (pin == 6) ? 7 : 13, rising, falling, result);
if (total >= lo && total <= hi) {
printf(" Set GPS_PPS_PIN to GPIO_NUM_%d in board_config.h\n", pin);
printf(" Polarity: set GPS_PPS_ACTIVE_LOW=%d\n", falling > rising ? 1 : 0);
}
}
}

View File

@ -49,7 +49,6 @@ typedef struct {
gpio_num_t tx_pin; gpio_num_t tx_pin;
gpio_num_t rx_pin; gpio_num_t rx_pin;
gpio_num_t pps_pin; gpio_num_t pps_pin;
bool pps_active_low; // true = trigger on falling edge (common for u-blox GT-U7/NEO-6M)
} gps_sync_config_t; } gps_sync_config_t;
// --- Timestamp Struct --- // --- Timestamp Struct ---
@ -72,12 +71,6 @@ int64_t gps_get_pps_age_ms(void);
// Copies the last received NMEA line into buffer (Diagnostic) // Copies the last received NMEA line into buffer (Diagnostic)
void gps_get_last_nmea(char *buf, size_t buf_len); void gps_get_last_nmea(char *buf, size_t buf_len);
// PPS diagnostic: poll GPIO for duration_sec, count edges (call from console)
void gps_pps_diagnostic(int duration_sec);
// PPS scan: poll multiple candidate pins to find which has ~1 Hz signal
void gps_pps_scan(int duration_sec);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

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,424 +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
};
// 802.11ax MCS rates for 80MHz, 800ns GI, 1 Spatial Stream
// (80MHz rates are approximately 2x 40MHz rates)
static const uint32_t PHY_RATES_80MHZ_1SS_800NS[] = {
34400, // MCS 0
68800, // MCS 1
103200, // MCS 2
137600, // MCS 3
206500, // MCS 4
275200, // MCS 5
309800, // MCS 6
344200, // MCS 7
413000, // MCS 8
458800, // MCS 9
516200, // MCS 10
573600 // MCS 11
};
// 802.11ax MCS rates for 80MHz, 400ns GI, 1 Spatial Stream
static const uint32_t PHY_RATES_80MHZ_1SS_400NS[] = {
38400, // MCS 0
76800, // MCS 1
115200, // MCS 2
153600, // MCS 3
230400, // MCS 4
307200, // MCS 5
345600, // MCS 6
384000, // MCS 7
460800, // MCS 8
512400, // MCS 9
576400, // MCS 10
640600 // 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 = frame_info->spatial_streams; // Now extracted 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 :
(frame_info->bandwidth == 2) ? MCS_BW_80MHZ : MCS_BW_20MHZ;
sample.frame_len = frame_info->frame_len;
sample.is_retry = frame_info->retry;
sample.sig_mode = frame_info->sig_mode;
// Ensure spatial streams is valid (1-8)
if (sample.ss < 1) sample.ss = 1;
if (sample.ss > MCS_TELEMETRY_MAX_SS) sample.ss = MCS_TELEMETRY_MAX_SS;
// 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++;
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;
}
#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;
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 || (size_t)written >= buffer_len) {
return ESP_ERR_NO_MEM;
}
size_t offset = (size_t)written;
bool first = true;
for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES; i++) {
mcs_device_telemetry_t *dev = &s_stats.devices[i];
if (dev->sample_count == 0) continue;
size_t space = buffer_len - offset - MCS_JSON_TAIL_RESERVE;
if (space < 2) break;
if (!first) {
written = snprintf(json_buffer + offset, space, ",");
if (written < 0) break;
offset += (size_t)((written < (int)space) ? written : (space - 1));
space = buffer_len - offset - MCS_JSON_TAIL_RESERVE;
if (space < 2) break;
}
first = false;
written = snprintf(json_buffer + offset, space,
"{\"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 += (size_t)((written < (int)space) ? written : (space - 1));
}
{
size_t tail_space = buffer_len - offset;
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;
#undef MCS_JSON_TAIL_RESERVE
}
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 if (bandwidth == MCS_BW_80MHZ) {
rate_table = sgi ? PHY_RATES_80MHZ_1SS_400NS : PHY_RATES_80MHZ_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,212 +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, but we track 80MHz for compatibility)
*/
typedef enum {
MCS_BW_20MHZ = 0,
MCS_BW_40MHZ = 1,
MCS_BW_80MHZ = 2
} 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

@ -1,5 +0,0 @@
idf_component_register(
SRCS "sd_card.c"
INCLUDE_DIRS "."
REQUIRES driver fatfs esp_driver_sdspi sdmmc esp_timer
)

View File

@ -1,517 +0,0 @@
/*
* sd_card.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 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 specific 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 "sd_card.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
#include "esp_vfs_fat.h"
#include "ff.h"
#include "sdmmc_cmd.h"
#include <string.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <fcntl.h>
#include <dirent.h>
/* 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));
}
}
// Pin definitions for SparkFun microSD Transflash Breakout
// ESP32-C5: no SDMMC host, use SD SPI mode
// SparkFun in SPI: CLK, MOSI(DI), MISO(DO), CS, CD(optional)
// CONFIG_SD_CD_GPIO / CONFIG_SD_CD_ACTIVE_LOW from Kconfig (main/Kconfig.projbuild)
// CONFIG_SD_CD_ACTIVE_LOW is only defined when y; when n it is omitted from sdkconfig.h
#ifndef CONFIG_SD_CD_GPIO
#define CONFIG_SD_CD_GPIO (-1)
#endif
#if defined(CONFIG_IDF_TARGET_ESP32C5)
#define SDSPI_CLK_PIN GPIO_NUM_9
#define SDSPI_MOSI_PIN GPIO_NUM_10
#define SDSPI_MISO_PIN GPIO_NUM_8
#define SDSPI_CS_PIN GPIO_NUM_7
#define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1))
#define SDSPI_HOST_ID SPI2_HOST
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
#define SDSPI_CLK_PIN GPIO_NUM_14
#define SDSPI_MOSI_PIN GPIO_NUM_15
#define SDSPI_MISO_PIN GPIO_NUM_2
#define SDSPI_CS_PIN GPIO_NUM_13
#define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1))
#define SDSPI_HOST_ID SPI2_HOST
#else
#define SDSPI_CLK_PIN GPIO_NUM_14
#define SDSPI_MOSI_PIN GPIO_NUM_15
#define SDSPI_MISO_PIN GPIO_NUM_2
#define SDSPI_CS_PIN GPIO_NUM_13
#define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1))
#define SDSPI_HOST_ID SPI2_HOST
#endif
#include "driver/sdspi_host.h"
#include "driver/spi_common.h"
static const char *TAG = "sd_card";
static bool s_sd_card_mounted = false;
static bool s_cd_configured = false;
static bool s_spi_bus_inited = false;
static sdmmc_card_t *s_card = NULL;
static const char *s_mount_point = "/sdcard";
static uint64_t s_last_status_check_ms = 0;
static bool s_last_status_result = false;
#define SD_STATUS_CHECK_INTERVAL_MS 1000 /* Check card status at most once per second */
static void sd_card_cd_ensure_configured(void) {
if (!s_cd_configured && SD_CD_PIN >= 0) {
/* Limit shift to 0..63 so compiler does not warn; valid GPIOs are 0..48 */
const unsigned int cd_pin = (unsigned int)SD_CD_PIN & 0x3Fu;
gpio_config_t io = {
.pin_bit_mask = (1ULL << cd_pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&io) == ESP_OK) {
s_cd_configured = true;
}
}
}
esp_err_t sd_card_init(void) {
esp_err_t result = ESP_OK;
if (s_sd_card_mounted) {
ESP_LOGW(TAG, "SD card already initialized");
result = ESP_OK;
} else {
ESP_LOGI(TAG, "Initializing SD card via SPI...");
// Initialize SPI bus (required before sdspi mount)
spi_bus_config_t bus_cfg = {
.mosi_io_num = SDSPI_MOSI_PIN,
.miso_io_num = SDSPI_MISO_PIN,
.sclk_io_num = SDSPI_CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
result = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO);
if (result == ESP_OK) {
s_spi_bus_inited = true;
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SDSPI_HOST_ID;
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SDSPI_CS_PIN;
slot_config.host_id = SDSPI_HOST_ID;
// Do not pass gpio_cd to driver: ESP-IDF expects LOW=inserted and blocks init if CD says no card.
// We use CD only for status (sd_card_cd_is_inserted) with configurable polarity.
slot_config.gpio_cd = SDSPI_SLOT_NO_CD;
slot_config.gpio_wp = SDSPI_SLOT_NO_WP;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
result = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card);
if (result != ESP_OK) {
if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false;
}
/* Reset status cache on init failure */
s_last_status_result = false;
s_last_status_check_ms = 0;
if (result == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem. "
"If you want the card to be formatted, set format_if_mount_failed = true.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card is inserted and wiring is correct.", esp_err_to_name(result));
}
} else {
sdmmc_card_print_info(stdout, s_card);
s_sd_card_mounted = true;
/* Reset status cache on successful mount */
s_last_status_result = true;
s_last_status_check_ms = esp_timer_get_time() / 1000;
ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point);
}
} else {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(result));
}
}
return result;
}
esp_err_t sd_card_deinit(void) {
esp_err_t result = ESP_OK;
if (s_sd_card_mounted) {
ESP_LOGI(TAG, "Unmounting SD card...");
result = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card);
if (result == ESP_OK) {
s_sd_card_mounted = false;
s_card = NULL;
/* Reset status cache on unmount */
s_last_status_result = false;
s_last_status_check_ms = 0;
if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false;
}
ESP_LOGI(TAG, "SD card unmounted successfully");
} else {
ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(result));
}
}
return result;
}
bool sd_card_is_ready(void) {
bool result = false;
/* If mounted, assume card is ready (since CD pin doesn't work) */
/* Status check is used to detect removal, but we're lenient about failures */
if (s_sd_card_mounted && s_card != NULL) {
/* Since CD pin doesn't work, probe the card periodically to detect removal */
/* Cache the result to avoid checking too frequently (SDMMC commands have overhead) */
uint64_t now_ms = esp_timer_get_time() / 1000;
uint64_t time_since_check = now_ms - s_last_status_check_ms;
if (time_since_check >= SD_STATUS_CHECK_INTERVAL_MS) {
/* Use CMD13 (SEND_STATUS) to check if card is actually responsive */
/* This is a lightweight operation that will fail if card is removed */
esp_err_t err = sdmmc_get_status(s_card);
if (err == ESP_OK) {
/* Status check passed - card is definitely ready */
s_last_status_result = true;
} else {
/* Status check failed - but be lenient: only mark as not ready if we've */
/* had multiple consecutive failures (to avoid false negatives) */
/* For now, if mounted, assume ready - status check failures might be transient */
/* Only log a warning, don't block writes */
ESP_LOGW(TAG, "SD card status check failed (but assuming ready): %s", esp_err_to_name(err));
/* Keep s_last_status_result as true - trust mount status over status check */
/* This handles cases where sdmmc_get_status() fails but card is still functional */
}
s_last_status_check_ms = now_ms;
}
/* If mounted, always return true (optimistic) - let actual write operations fail if card is gone */
result = true;
} else {
s_last_status_result = false;
result = false;
}
return result;
}
bool sd_card_cd_available(void) {
return (SD_CD_PIN >= 0);
}
bool sd_card_cd_is_inserted(void) {
bool result = false;
if (SD_CD_PIN >= 0) {
sd_card_cd_ensure_configured();
int level = gpio_get_level(SD_CD_PIN);
#if defined(CONFIG_SD_CD_ACTIVE_LOW) && !(CONFIG_SD_CD_ACTIVE_LOW)
result = (level == 1); /* HIGH = inserted (inverted breakout) */
#else
result = (level == 0); /* LOW = inserted (SparkFun default) */
#endif
}
return result;
}
int sd_card_cd_get_level(void) {
int result = -1;
if (SD_CD_PIN >= 0) {
sd_card_cd_ensure_configured();
result = gpio_get_level(SD_CD_PIN);
}
return result;
}
esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
FATFS *fs;
DWORD fre_clust, fre_sect, tot_sect;
char path[32];
snprintf(path, sizeof(path), "%s", s_mount_point);
FRESULT res = f_getfree(path, &fre_clust, &fs);
if (res == FR_OK) {
tot_sect = (fs->n_fatent - 2) * fs->csize;
fre_sect = fre_clust * fs->csize;
if (total_bytes) {
*total_bytes = (uint64_t)tot_sect * 512;
}
if (free_bytes) {
*free_bytes = (uint64_t)fre_sect * 512;
}
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to get free space: %d", res);
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
int flags = O_WRONLY | O_CREAT;
if (append) {
flags |= O_APPEND;
} else {
flags |= O_TRUNC;
}
int fd = open(full_path, flags, 0644);
if (fd >= 0) {
ssize_t written = write(fd, data, len);
close(fd);
if (written >= 0 && (size_t)written == len) {
ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path);
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len);
result = ESP_FAIL;
}
} else {
ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path);
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "r");
if (f != NULL) {
size_t read = fread(data, 1, len, f);
fclose(f);
if (bytes_read) {
*bytes_read = read;
}
ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path);
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path);
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted && size_bytes != NULL) {
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) {
if (S_ISREG(st.st_mode)) {
*size_bytes = (size_t)st.st_size;
result = ESP_OK;
} else {
result = ESP_ERR_INVALID_ARG;
}
} else {
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, size_t len, size_t *bytes_read) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
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) {
if (fseek(f, (long)offset, SEEK_SET) == 0) {
size_t n = fread(data, 1, len, f);
fclose(f);
if (bytes_read) {
*bytes_read = n;
}
result = ESP_OK;
} else {
fclose(f);
result = ESP_FAIL;
}
} else {
result = ESP_FAIL;
}
}
return result;
}
bool sd_card_file_exists(const char *filename) {
bool result = false;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
struct stat st;
result = (stat(full_path, &st) == 0);
}
return result;
}
esp_err_t sd_card_list_dir(const char *path) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
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 != NULL) {
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 {
char hr_size[16];
fmt_size_human((size_t)st.st_size, hr_size, sizeof(hr_size));
printf(" %-32s %10zu bytes (%s)\n", e->d_name, (size_t)st.st_size, hr_size);
}
} else {
printf(" %-32s ?\n", e->d_name);
}
}
closedir(d);
result = ESP_OK;
} else {
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_delete_file(const char *filename) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
if (unlink(full_path) == 0) {
ESP_LOGI(TAG, "Deleted file: %s", full_path);
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to delete file: %s", full_path);
result = ESP_FAIL;
}
}
return result;
}

View File

@ -1,158 +0,0 @@
/*
* sd_card.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 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 specific 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 SD_CARD_H
#define SD_CARD_H
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Initialize SD card using SDIO interface
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t sd_card_init(void);
/**
* @brief Deinitialize SD card
*
* @return ESP_OK on success
*/
esp_err_t sd_card_deinit(void);
/**
* @brief Check if SD card is mounted and ready
*
* @return true if SD card is ready, false otherwise
*/
bool sd_card_is_ready(void);
/**
* @brief Check if Card Detect (CD) pin is configured and wired
*
* @return true if CD pin is available, false otherwise
*/
bool sd_card_cd_available(void);
/**
* @brief Check if a card is physically inserted (CD pin, polarity from Kconfig)
* Only meaningful when sd_card_cd_available() is true.
*
* @return true if CD indicates card inserted, false if removed or N/A
*/
bool sd_card_cd_is_inserted(void);
/**
* @brief Get raw CD pin level for diagnostics (0=LOW, 1=HIGH, -1=not configured)
*/
int sd_card_cd_get_level(void);
/**
* @brief Get SD card capacity information
*
* @param total_bytes Output parameter for total capacity in bytes
* @param free_bytes Output parameter for free space in bytes
* @return ESP_OK on success
*/
esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes);
/**
* @brief Write data to a file on the SD card
*
* @param filename File path (e.g., "/sdcard/telemetry.json")
* @param data Data to write
* @param len Length of data in bytes
* @param append If true, append to file; if false, overwrite
* @return ESP_OK on success
*/
esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append);
/**
* @brief Read data from a file on the SD card
*
* @param filename File path
* @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(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
*
* @param filename File path
* @return true if file exists, false otherwise
*/
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
*
* @param filename File path
* @return ESP_OK on success
*/
esp_err_t sd_card_delete_file(const char *filename);
#endif // SD_CARD_H

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

@ -43,7 +43,6 @@ 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) {
@ -70,13 +69,8 @@ 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: /* Blink blue only when frames being captured */ case LED_STATE_MONITORING:
if (s_capture_active) { set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
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;
@ -122,6 +116,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,12 +70,6 @@ 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

@ -141,26 +141,6 @@ bool wifi_cfg_set_monitor_channel(uint8_t channel) {
return (err == ESP_OK); return (err == ESP_OK);
} }
bool wifi_cfg_get_monitor_channel(uint8_t *channel_out) {
bool result = false;
if (channel_out != NULL) {
nvs_handle_t h;
uint8_t saved_val = DEFAULT_MONITOR_CHANNEL;
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
esp_err_t err = nvs_get_u8(h, "mon_chan", &saved_val);
nvs_close(h);
if (err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND) {
*channel_out = saved_val;
result = true;
}
}
}
return result;
}
void wifi_cfg_clear_monitor_channel(void) { void wifi_cfg_clear_monitor_channel(void) {
nvs_handle_t h; nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) == ESP_OK) { if (nvs_open(NVS_NS, NVS_READWRITE, &h) == ESP_OK) {

View File

@ -55,7 +55,6 @@ bool wifi_cfg_set_password(const char *password);
// Monitor Specific // Monitor Specific
bool wifi_cfg_set_monitor_channel(uint8_t channel); bool wifi_cfg_set_monitor_channel(uint8_t channel);
bool wifi_cfg_get_monitor_channel(uint8_t *channel_out);
void wifi_cfg_clear_monitor_channel(void); void wifi_cfg_clear_monitor_channel(void);
bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val); bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val);

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

@ -33,14 +33,11 @@
#include "wifi_controller.h" #include "wifi_controller.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "esp_timer.h"
#include "inttypes.h" #include "inttypes.h"
#include <string.h>
#include "wifi_cfg.h" #include "wifi_cfg.h"
// Dependencies // Dependencies
@ -53,21 +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 TELEMETRY_JSON_BUF_SIZE 4096
/* SD card write optimization:
* - 16KB batch size: Optimal for high-frequency telemetry (4KB-64KB range, sweet spot 4-16KB)
* - Multiple of 512 bytes (SD sector size): 16KB = 32 sectors, ensures good alignment
* - Balances performance vs RAM usage for embedded systems
* - For ESP32: 16KB is reasonable RAM usage and provides excellent write efficiency
*/
#define TELEMETRY_BATCH_SIZE (16 * 1024) /* 16KB batch buffer for SD card writes */
#define TELEMETRY_BATCH_FLUSH_INTERVAL_MS 5000 /* Flush batch every 5 seconds or when 80% full */
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;
@ -77,29 +59,6 @@ static uint8_t s_monitor_channel_staging = 6;
static bool s_monitor_enabled = false; static bool s_monitor_enabled = false;
static uint32_t s_monitor_frame_count = 0; static uint32_t s_monitor_frame_count = 0;
static TaskHandle_t s_monitor_stats_task_handle = NULL; static TaskHandle_t s_monitor_stats_task_handle = NULL;
static bool s_monitor_debug = false; /* Debug mode: enable serial logging */
/* Telemetry rate tracking (Welford algorithm) */
typedef struct {
uint64_t total_bytes; /* Total bytes generated/written */
double mean_rate_bps; /* Mean rate in bytes per second */
double m2; /* Sum of squares of differences (for variance) */
uint32_t sample_count; /* Number of samples */
uint64_t last_update_ms; /* Last update timestamp */
} rate_tracker_t;
static rate_tracker_t s_telemetry_gen_rate = {0};
static rate_tracker_t s_sd_write_rate = {0};
static rate_tracker_t s_frame_rate = {0};
static uint32_t s_last_frame_count_for_rate = 0;
/* Batch buffer for telemetry (shared between task and flush function) */
static char s_batch_buf[TELEMETRY_BATCH_SIZE];
static size_t s_batch_offset = 0;
static SemaphoreHandle_t s_batch_mutex = NULL;
/* Static flush buffer to avoid large stack allocations (16KB) */
static char s_flush_buf[TELEMETRY_BATCH_SIZE];
// --- Event Handler --- // --- Event Handler ---
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
@ -115,260 +74,27 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t e
// ... [Log Collapse / Monitor Callback Logic] ... // ... [Log Collapse / Monitor Callback Logic] ...
static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) { static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) {
if (s_monitor_debug) { /* Only log in debug mode */ gps_timestamp_t ts = gps_get_timestamp();
gps_timestamp_t ts = gps_get_timestamp(); int64_t now_ms = ts.gps_us / 1000;
int64_t now_ms = ts.gps_us / 1000; ESP_LOGI(TAG, "COLLAPSE: Time=%" PRId64 "ms, Sync=%d, Dur=%lu us, RSSI=%d, Retry=%d",
ESP_LOGI(TAG, "COLLAPSE: Time=%" PRId64 "ms, Sync=%d, Dur=%lu us, RSSI=%d, Retry=%d", now_ms, ts.synced ? 1 : 0, nav_duration_us, rssi, retry);
now_ms, ts.synced ? 1 : 0, nav_duration_us, rssi, 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++;
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);
} }
/* MCS telemetry: feed frames to fiwi-telemetry (default on monitor start) */
mcs_telemetry_process_frame(frame, NULL);
}
/**
* @brief Update rate tracker using Welford's online algorithm
* @param tracker Rate tracker to update
* @param bytes Bytes generated/written in this interval
* @param interval_ms Time interval in milliseconds
*/
static void update_rate_tracker(rate_tracker_t *tracker, size_t bytes, uint32_t interval_ms) {
if (interval_ms > 0) {
double rate_bps = (bytes * 1000.0) / interval_ms; /* Convert to bytes per second */
tracker->total_bytes += bytes;
tracker->sample_count++;
/* Welford's online algorithm for running mean and variance */
double delta = rate_bps - tracker->mean_rate_bps;
tracker->mean_rate_bps += delta / tracker->sample_count;
double delta2 = rate_bps - tracker->mean_rate_bps;
tracker->m2 += delta * delta2;
}
}
/**
* @brief Flush pending telemetry batch to SD card
*/
static void flush_telemetry_batch(void) {
size_t offset = 0;
if (s_batch_mutex != NULL) {
if (xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
offset = s_batch_offset;
xSemaphoreGive(s_batch_mutex);
}
if (offset > 0 && sd_card_is_ready()) {
size_t flush_size = 0;
if (xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
/* Copy batch to static buffer for safe write */
memcpy(s_flush_buf, s_batch_buf, offset);
flush_size = offset;
s_batch_offset = 0;
xSemaphoreGive(s_batch_mutex);
}
if (flush_size > 0) {
if (sd_card_write_file(FIWI_TELEMETRY_FILE, s_flush_buf, flush_size, true) == ESP_OK) {
uint64_t now_ms = esp_timer_get_time() / 1000;
uint32_t write_interval_ms = (uint32_t)(now_ms - s_sd_write_rate.last_update_ms);
if (write_interval_ms > 0) {
update_rate_tracker(&s_sd_write_rate, flush_size, write_interval_ms);
s_sd_write_rate.last_update_ms = now_ms;
}
if (s_monitor_debug) {
ESP_LOGD(TAG, "Batch flushed on stop: %zu bytes", flush_size);
}
}
}
}
}
} }
static void monitor_stats_task(void *arg) { static void monitor_stats_task(void *arg) {
(void)arg; while (1) {
static char json_buf[TELEMETRY_JSON_BUF_SIZE]; vTaskDelay(pdMS_TO_TICKS(10000));
uint32_t flush_count = 0; wifi_collapse_stats_t stats;
uint32_t last_frame_count = 0; if (wifi_monitor_get_stats(&stats) == ESP_OK) {
uint64_t last_batch_flush_ms = 0; ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
uint64_t last_stats_log_ms = 0; (unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav);
bool task_running = true; if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️");
/* Initialize rate trackers */
uint64_t start_ms = esp_timer_get_time() / 1000;
s_telemetry_gen_rate.last_update_ms = start_ms;
s_sd_write_rate.last_update_ms = start_ms;
s_frame_rate.last_update_ms = start_ms;
s_last_frame_count_for_rate = s_monitor_frame_count;
last_batch_flush_ms = start_ms;
last_stats_log_ms = start_ms;
/* Batch mutex should already be created by switch_to_monitor */
if (s_batch_mutex == NULL) {
ESP_LOGE(TAG, "Batch mutex not initialized");
task_running = false;
}
while (task_running && s_batch_mutex != NULL) {
vTaskDelay(pdMS_TO_TICKS(1000)); /* Check every 1 second for batching */
uint64_t now_ms = esp_timer_get_time() / 1000;
if (s_monitor_frame_count == last_frame_count) {
status_led_set_capture_active(false);
}
/* Update frame rate tracker */
uint32_t frames_delta = s_monitor_frame_count - s_last_frame_count_for_rate;
uint32_t frame_interval_ms = (uint32_t)(now_ms - s_frame_rate.last_update_ms);
if (frame_interval_ms > 0) {
update_rate_tracker(&s_frame_rate, frames_delta, frame_interval_ms);
s_frame_rate.last_update_ms = now_ms;
s_last_frame_count_for_rate = s_monitor_frame_count;
}
last_frame_count = s_monitor_frame_count;
/* Generate telemetry JSON (regardless of SD card status) */
if (mcs_telemetry_to_json(json_buf, sizeof(json_buf), "esp32") == ESP_OK) {
size_t len = strlen(json_buf);
if (len > 0) {
json_buf[len] = '\n'; /* NDJSON: one object per line */
size_t total_len = len + 1;
/* Update telemetry generation rate (always track generation) */
uint32_t interval_ms = (uint32_t)(now_ms - s_telemetry_gen_rate.last_update_ms);
if (interval_ms > 0) {
update_rate_tracker(&s_telemetry_gen_rate, total_len, interval_ms);
s_telemetry_gen_rate.last_update_ms = now_ms;
}
/* Only write to SD card if ready */
bool card_ready = sd_card_is_ready();
if (!card_ready && s_monitor_debug) {
/* Log when card is not ready (only in debug mode to avoid spam) */
static uint64_t last_not_ready_log_ms = 0;
uint64_t now_ms_check = esp_timer_get_time() / 1000;
if (now_ms_check - last_not_ready_log_ms > 10000) { /* Log at most every 10 seconds */
ESP_LOGW(TAG, "SD card not ready, telemetry not being written");
last_not_ready_log_ms = now_ms_check;
}
}
if (card_ready) {
/* Add to batch buffer (with mutex protection) */
if (s_batch_mutex && xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) {
if (s_batch_offset + total_len < TELEMETRY_BATCH_SIZE) {
memcpy(s_batch_buf + s_batch_offset, json_buf, total_len);
s_batch_offset += total_len;
xSemaphoreGive(s_batch_mutex);
} else {
/* Batch buffer full, flush immediately */
size_t flush_size = s_batch_offset;
if (flush_size > 0) {
memcpy(s_flush_buf, s_batch_buf, flush_size);
s_batch_offset = 0;
xSemaphoreGive(s_batch_mutex);
esp_err_t write_err = sd_card_write_file(FIWI_TELEMETRY_FILE, s_flush_buf, flush_size, true);
if (write_err == ESP_OK) {
flush_count++;
uint32_t write_interval_ms = (uint32_t)(now_ms - s_sd_write_rate.last_update_ms);
if (write_interval_ms > 0) {
update_rate_tracker(&s_sd_write_rate, flush_size, write_interval_ms);
s_sd_write_rate.last_update_ms = now_ms;
}
if (s_monitor_debug) {
ESP_LOGD(TAG, "Batch flushed: %zu bytes (#%lu)", flush_size, (unsigned long)flush_count);
}
} else {
/* Log write failures - this helps diagnose SD card issues */
ESP_LOGE(TAG, "Failed to write telemetry batch (buffer full): %s (%zu bytes)", esp_err_to_name(write_err), flush_size);
}
} else {
xSemaphoreGive(s_batch_mutex);
}
/* Add current JSON to fresh batch */
if (total_len < TELEMETRY_BATCH_SIZE) {
if (xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) {
memcpy(s_batch_buf, json_buf, total_len);
s_batch_offset = total_len;
xSemaphoreGive(s_batch_mutex);
}
}
}
}
}
}
}
/* Flush batch periodically or if buffer is getting full */
size_t current_offset = 0;
if (s_batch_mutex && xSemaphoreTake(s_batch_mutex, 0) == pdTRUE) {
current_offset = s_batch_offset;
xSemaphoreGive(s_batch_mutex);
}
uint32_t time_since_flush_ms = (uint32_t)(now_ms - last_batch_flush_ms);
bool should_flush = (time_since_flush_ms >= TELEMETRY_BATCH_FLUSH_INTERVAL_MS) ||
(current_offset > TELEMETRY_BATCH_SIZE * 0.8); /* Flush at 80% full */
if (should_flush && current_offset > 0 && s_batch_mutex && sd_card_is_ready()) {
size_t flush_size = 0;
if (xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) {
memcpy(s_flush_buf, s_batch_buf, current_offset);
flush_size = current_offset;
s_batch_offset = 0;
xSemaphoreGive(s_batch_mutex);
}
if (flush_size > 0) {
esp_err_t write_err = sd_card_write_file(FIWI_TELEMETRY_FILE, s_flush_buf, flush_size, true);
if (write_err == ESP_OK) {
flush_count++;
uint32_t write_interval_ms = (uint32_t)(now_ms - s_sd_write_rate.last_update_ms);
if (write_interval_ms > 0) {
update_rate_tracker(&s_sd_write_rate, flush_size, write_interval_ms);
s_sd_write_rate.last_update_ms = now_ms;
}
if (s_monitor_debug) {
ESP_LOGD(TAG, "Batch flushed: %zu bytes (#%lu, gen: %.1f B/s, write: %.1f B/s)",
flush_size, (unsigned long)flush_count,
s_telemetry_gen_rate.mean_rate_bps, s_sd_write_rate.mean_rate_bps);
}
last_batch_flush_ms = now_ms;
} else {
/* Log write failures - this helps diagnose SD card issues */
ESP_LOGE(TAG, "Failed to write telemetry batch: %s (%zu bytes)", esp_err_to_name(write_err), flush_size);
}
}
}
/* Log stats periodically (only in debug mode) */
uint32_t time_since_log_ms = (uint32_t)(now_ms - last_stats_log_ms);
if (s_monitor_debug && time_since_log_ms >= 10000) {
wifi_collapse_stats_t stats;
if (wifi_monitor_get_stats(&stats) == ESP_OK) {
ESP_LOGD("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
(unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav);
ESP_LOGD("MONITOR", "Telemetry: gen=%.1f B/s (total=%llu), write=%.1f B/s (total=%llu)",
s_telemetry_gen_rate.mean_rate_bps, (unsigned long long)s_telemetry_gen_rate.total_bytes,
s_sd_write_rate.mean_rate_bps, (unsigned long long)s_sd_write_rate.total_bytes);
if (wifi_monitor_is_collapsed()) {
ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️");
}
}
last_stats_log_ms = now_ms;
} }
} }
} }
@ -376,24 +102,23 @@ static void monitor_stats_task(void *arg) {
// --- Helper to apply IP settings --- // --- Helper to apply IP settings ---
static void apply_ip_settings(void) { static void apply_ip_settings(void) {
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif) return;
if (netif != NULL) { if (wifi_cfg_get_dhcp()) {
if (wifi_cfg_get_dhcp()) { esp_netif_dhcpc_start(netif);
esp_netif_dhcpc_start(netif); } else {
} else { esp_netif_dhcpc_stop(netif);
esp_netif_dhcpc_stop(netif);
char ip[16], mask[16], gw[16]; char ip[16], mask[16], gw[16];
if (wifi_cfg_get_ipv4(ip, mask, gw)) { if (wifi_cfg_get_ipv4(ip, mask, gw)) {
esp_netif_ip_info_t info = {0}; esp_netif_ip_info_t info = {0};
// API Fix: esp_ip4addr_aton returns uint32_t // API Fix: esp_ip4addr_aton returns uint32_t
info.ip.addr = esp_ip4addr_aton(ip); info.ip.addr = esp_ip4addr_aton(ip);
info.netmask.addr = esp_ip4addr_aton(mask); info.netmask.addr = esp_ip4addr_aton(mask);
info.gw.addr = esp_ip4addr_aton(gw); info.gw.addr = esp_ip4addr_aton(gw);
esp_netif_set_ip_info(netif, &info); esp_netif_set_ip_info(netif, &info);
ESP_LOGI(TAG, "Static IP applied: %s", ip); ESP_LOGI(TAG, "Static IP applied: %s", ip);
}
} }
} }
} }
@ -436,151 +161,88 @@ void wifi_ctl_init(void) {
esp_wifi_connect(); esp_wifi_connect();
} }
// Load Staging and Active Params from NVS // Load Staging Params
char mode_ignored[16]; char mode_ignored[16];
wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging); wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging);
if (s_monitor_channel_staging == 0) s_monitor_channel_staging = 6; if (s_monitor_channel_staging == 0) s_monitor_channel_staging = 6;
// Load active channel from NVS (persists across reboots)
uint8_t saved_active_channel = 0;
if (wifi_cfg_get_monitor_channel(&saved_active_channel) && saved_active_channel > 0 && saved_active_channel <= 14) {
s_monitor_channel_active = saved_active_channel;
// Also update staging to match active if staging wasn't set
if (s_monitor_channel_staging == 6) {
s_monitor_channel_staging = saved_active_channel;
}
}
} }
// --- Mode Control (Core) --- // --- Mode Control (Core) ---
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) { esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
esp_err_t result = ESP_OK; if (channel == 0) channel = s_monitor_channel_staging;
if (channel == 0) {
// Use active channel if set, otherwise fall back to staging
channel = (s_monitor_channel_active > 0) ? s_monitor_channel_active : s_monitor_channel_staging;
}
if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) { if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) {
ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", channel); ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", channel);
result = ESP_OK; return ESP_OK;
} else {
ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel);
iperf_stop();
vTaskDelay(pdMS_TO_TICKS(500));
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
csi_mgr_disable();
#endif
esp_wifi_disconnect();
esp_wifi_stop();
vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_set_mode(WIFI_MODE_NULL);
status_led_set_capture_active(false);
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
ESP_LOGE(TAG, "Failed to init monitor mode");
result = ESP_FAIL;
} else {
/* 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);
if (wifi_monitor_start() != ESP_OK) {
ESP_LOGE(TAG, "Failed to start monitor mode");
result = ESP_FAIL;
} else {
s_monitor_enabled = true;
s_current_mode = WIFI_CTL_MODE_MONITOR;
s_monitor_channel_active = channel;
/* Save active channel to NVS so it persists across reboots */
wifi_cfg_set_monitor_channel(channel);
status_led_set_state(LED_STATE_MONITORING);
/* Reset rate trackers when starting monitor */
uint64_t start_ms = esp_timer_get_time() / 1000;
memset(&s_telemetry_gen_rate, 0, sizeof(rate_tracker_t));
memset(&s_sd_write_rate, 0, sizeof(rate_tracker_t));
memset(&s_frame_rate, 0, sizeof(rate_tracker_t));
s_telemetry_gen_rate.last_update_ms = start_ms;
s_sd_write_rate.last_update_ms = start_ms;
s_frame_rate.last_update_ms = start_ms;
s_last_frame_count_for_rate = s_monitor_frame_count;
/* Initialize batch buffer and mutex */
if (s_batch_mutex == NULL) {
s_batch_mutex = xSemaphoreCreateMutex();
if (s_batch_mutex == NULL) {
ESP_LOGE(TAG, "Failed to create batch mutex");
result = ESP_FAIL;
}
}
if (result == ESP_OK && s_batch_mutex != NULL) {
if (xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) {
s_batch_offset = 0;
xSemaphoreGive(s_batch_mutex);
}
}
if (result == ESP_OK && s_monitor_stats_task_handle == NULL) {
/* Task stack size: 12KB needed for:
* - 4KB static json_buf (TELEMETRY_JSON_BUF_SIZE)
* - Local variables and function call frames
* - ESP-IDF API call overhead (esp_timer_get_time, sd_card_write_file, etc.)
* - Mutex operations and nested function calls
*/
xTaskCreate(monitor_stats_task, "monitor_stats", 12 * 1024, NULL, 5, &s_monitor_stats_task_handle);
}
}
}
} }
return result; ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel);
iperf_stop();
vTaskDelay(pdMS_TO_TICKS(500));
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
csi_mgr_disable();
#endif
esp_wifi_disconnect();
esp_wifi_stop();
vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_set_mode(WIFI_MODE_NULL);
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
ESP_LOGE(TAG, "Failed to init monitor mode");
return ESP_FAIL;
}
esp_wifi_set_bandwidth(WIFI_IF_STA, bw);
if (wifi_monitor_start() != ESP_OK) {
ESP_LOGE(TAG, "Failed to start monitor mode");
return ESP_FAIL;
}
s_monitor_enabled = true;
s_current_mode = WIFI_CTL_MODE_MONITOR;
s_monitor_channel_active = channel;
status_led_set_state(LED_STATE_MONITORING);
if (s_monitor_stats_task_handle == NULL) {
xTaskCreate(monitor_stats_task, "monitor_stats", 4096, NULL, 5, &s_monitor_stats_task_handle);
}
return ESP_OK;
} }
esp_err_t wifi_ctl_switch_to_sta(void) { esp_err_t wifi_ctl_switch_to_sta(void) {
esp_err_t result = ESP_OK;
if (s_current_mode == WIFI_CTL_MODE_STA) { if (s_current_mode == WIFI_CTL_MODE_STA) {
ESP_LOGI(TAG, "Already in STA mode"); ESP_LOGI(TAG, "Already in STA mode");
result = ESP_OK; return ESP_OK;
} else {
ESP_LOGI(TAG, "Switching to STA MODE");
if (s_monitor_stats_task_handle != NULL) {
vTaskDelete(s_monitor_stats_task_handle);
s_monitor_stats_task_handle = NULL;
}
if (s_monitor_enabled) {
status_led_set_capture_active(false);
/* Flush any pending telemetry before stopping */
flush_telemetry_batch();
mcs_telemetry_stop();
wifi_monitor_stop();
s_monitor_enabled = false;
vTaskDelay(pdMS_TO_TICKS(500));
}
esp_wifi_set_mode(WIFI_MODE_STA);
vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_start();
esp_wifi_connect();
s_current_mode = WIFI_CTL_MODE_STA;
status_led_set_state(LED_STATE_WAITING);
result = ESP_OK;
} }
return result; ESP_LOGI(TAG, "Switching to STA MODE");
if (s_monitor_stats_task_handle != NULL) {
vTaskDelete(s_monitor_stats_task_handle);
s_monitor_stats_task_handle = NULL;
}
if (s_monitor_enabled) {
wifi_monitor_stop();
s_monitor_enabled = false;
vTaskDelay(pdMS_TO_TICKS(500));
}
esp_wifi_set_mode(WIFI_MODE_STA);
vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_start();
esp_wifi_connect();
s_current_mode = WIFI_CTL_MODE_STA;
status_led_set_state(LED_STATE_WAITING);
return ESP_OK;
} }
// --- Wrappers for cmd_monitor.c --- // --- Wrappers for cmd_monitor.c ---
@ -615,39 +277,9 @@ void wifi_ctl_set_channel(int channel) {
ESP_LOGI(TAG, "Switching live channel to %d", channel); ESP_LOGI(TAG, "Switching live channel to %d", channel);
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
s_monitor_channel_active = (uint8_t)channel; s_monitor_channel_active = (uint8_t)channel;
/* Save active channel to NVS so it persists across reboots */
wifi_cfg_set_monitor_channel(channel);
} }
} }
/**
* @brief Calculate WiFi channel frequency in MHz
* @param channel Channel number (1-14 for 2.4GHz, 36+ for 5GHz)
* @return Frequency in MHz, or 0 if invalid channel
*/
static uint32_t wifi_channel_to_frequency(uint8_t channel) {
uint32_t freq = 0;
if (channel >= 1 && channel <= 14) {
/* 2.4 GHz band: 2407 + (channel * 5) MHz */
freq = 2407 + (channel * 5);
} else if (channel >= 36 && channel <= 64) {
/* 5 GHz UNII-1/2A: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
} else if (channel >= 100 && channel <= 144) {
/* 5 GHz UNII-2C: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
} else if (channel >= 149 && channel <= 165) {
/* 5 GHz UNII-3: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
} else if (channel >= 169 && channel <= 177) {
/* 5 GHz UNII-4: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
}
return freq;
}
void wifi_ctl_status(void) { void wifi_ctl_status(void) {
const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" : const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" :
(s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION"; (s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION";
@ -655,132 +287,10 @@ void wifi_ctl_status(void) {
printf("WiFi Status:\n"); printf("WiFi Status:\n");
printf(" Mode: %s\n", mode_str); printf(" Mode: %s\n", mode_str);
if (s_current_mode == WIFI_CTL_MODE_MONITOR) { if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
uint8_t channel = s_monitor_channel_active; printf(" Channel: %d\n", s_monitor_channel_active);
uint32_t freq_mhz = wifi_channel_to_frequency(channel); printf(" Frames: %lu\n", (unsigned long)s_monitor_frame_count);
/* Display channel info: channel N (FREQ MHz), width: 20 MHz, center1: FREQ MHz */
/* Note: Monitor mode currently uses single channel (20 MHz width) */
if (freq_mhz > 0) {
printf(" Channel: %d (%" PRIu32 " MHz), width: 20 MHz, center1: %" PRIu32 " MHz\n",
channel, freq_mhz, freq_mhz);
} else {
printf(" Channel: %d\n", channel);
}
/* Get frame rate (frames per second) */
double frame_rate_fps = 0.0;
if (s_frame_rate.sample_count > 0) {
frame_rate_fps = s_frame_rate.mean_rate_bps; /* Already in per-second units for frames */
}
printf(" Frames: %lu, %.1f fps\n", (unsigned long)s_monitor_frame_count, frame_rate_fps);
/* Show frame type breakdown */
wifi_collapse_stats_t monitor_stats;
if (wifi_monitor_get_stats(&monitor_stats) == ESP_OK) {
printf(" Frame Types: Data=%lu, Mgmt=%lu, Ctrl=%lu (RTS=%lu, CTS=%lu, ACK=%lu)\n",
(unsigned long)monitor_stats.data_frames,
(unsigned long)monitor_stats.mgmt_frames,
(unsigned long)(monitor_stats.rts_frames + monitor_stats.cts_frames + monitor_stats.ack_frames),
(unsigned long)monitor_stats.rts_frames,
(unsigned long)monitor_stats.cts_frames,
(unsigned long)monitor_stats.ack_frames);
/* Show histograms */
// AMPDU histogram
uint32_t total_ampdu = 0;
for (int i = 0; i < 8; i++) {
total_ampdu += monitor_stats.ampdu_hist[i];
}
if (total_ampdu > 0) {
printf(" AMPDU Aggregation: ");
const char *ampdu_labels[] = {"1", "2-4", "5-8", "9-16", "17-32", "33-48", "49-64", "65+"};
for (int i = 0; i < 8; i++) {
if (monitor_stats.ampdu_hist[i] > 0) {
printf("%s=%lu ", ampdu_labels[i], (unsigned long)monitor_stats.ampdu_hist[i]);
}
}
printf("\n");
}
// MCS histogram (show non-zero entries)
uint32_t total_mcs = 0;
for (int i = 0; i < 32; i++) {
total_mcs += monitor_stats.mcs_hist[i];
}
if (total_mcs > 0) {
printf(" MCS Distribution: ");
int printed = 0;
for (int i = 0; i < 32; i++) {
if (monitor_stats.mcs_hist[i] > 0) {
if (printed > 0) printf(", ");
printf("MCS%d=%lu", i, (unsigned long)monitor_stats.mcs_hist[i]);
printed++;
if (printed >= 10) { // Limit output length
printf(" ...");
break;
}
}
}
printf("\n");
}
// Spatial streams histogram
uint32_t total_ss = 0;
for (int i = 0; i < 8; i++) {
total_ss += monitor_stats.ss_hist[i];
}
if (total_ss > 0) {
printf(" Spatial Streams: ");
for (int i = 0; i < 8; i++) {
if (monitor_stats.ss_hist[i] > 0) {
printf("%dSS=%lu ", i + 1, (unsigned long)monitor_stats.ss_hist[i]);
}
}
printf("\n");
}
}
/* Show debug and filter status */
bool debug_enabled = wifi_ctl_get_monitor_debug();
uint8_t filter_mac[6];
bool filter_enabled = wifi_ctl_get_monitor_debug_filter(filter_mac);
printf(" Debug: %s\n", debug_enabled ? "enabled" : "disabled");
if (filter_enabled) {
printf(" Filter: %02x:%02x:%02x:%02x:%02x:%02x\n",
filter_mac[0], filter_mac[1], filter_mac[2],
filter_mac[3], filter_mac[4], filter_mac[5]);
} else {
printf(" Filter: disabled (showing all frames)\n");
}
/* Show telemetry rate statistics */
uint64_t gen_bytes = 0, write_bytes = 0;
double gen_rate = 0.0, write_rate = 0.0;
wifi_ctl_get_telemetry_gen_stats(&gen_bytes, &gen_rate);
wifi_ctl_get_sd_write_stats(&write_bytes, &write_rate);
/* Get bytes pending in batch buffer (awaiting flush) */
size_t bytes_in_batch = 0;
if (s_batch_mutex != NULL && xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
bytes_in_batch = s_batch_offset;
xSemaphoreGive(s_batch_mutex);
}
printf(" Telemetry (SD Card = %s):\n", sd_card_is_ready() ? "ready" : "not ready");
printf(" Generated: %llu bytes, %.1f B/s\n", (unsigned long long)gen_bytes, gen_rate);
printf(" Written: %llu bytes, %.1f B/s\n", (unsigned long long)write_bytes, write_rate);
if (gen_bytes > write_bytes) {
/* Account for bytes in batch buffer - they're not dropped, just pending */
uint64_t pending = (uint64_t)bytes_in_batch;
uint64_t dropped = (gen_bytes > write_bytes + pending) ? (gen_bytes - write_bytes - pending) : 0;
if (dropped > 0) {
printf(" Dropped: %llu bytes (%.1f%%)\n", (unsigned long long)dropped,
(dropped * 100.0) / gen_bytes);
}
if (pending > 0) {
printf(" Pending: %zu bytes (in batch buffer)\n", bytes_in_batch);
}
}
} }
printf(" Staging Ch: %d\n", s_monitor_channel_staging);
} }
// --- Params (NVS) --- // --- Params (NVS) ---
@ -791,15 +301,8 @@ bool wifi_ctl_param_is_unsaved(void) {
void wifi_ctl_param_save(const char *dummy) { void wifi_ctl_param_save(const char *dummy) {
(void)dummy; (void)dummy;
/* If monitor mode is running, save the active channel; otherwise save staging */ if (wifi_cfg_set_monitor_channel(s_monitor_channel_staging)) {
uint8_t channel_to_save = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? ESP_LOGI(TAG, "Monitor channel (%d) saved to NVS", s_monitor_channel_staging);
s_monitor_channel_active : s_monitor_channel_staging;
if (wifi_cfg_set_monitor_channel(channel_to_save)) {
ESP_LOGI(TAG, "Monitor channel (%d) saved to NVS", channel_to_save);
/* Update staging to match if we saved active */
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
s_monitor_channel_staging = channel_to_save;
}
} else { } else {
ESP_LOGI(TAG, "No changes to save."); ESP_LOGI(TAG, "No changes to save.");
} }
@ -810,17 +313,7 @@ void wifi_ctl_param_init(void) {
uint8_t ch = 0; uint8_t ch = 0;
wifi_cfg_get_mode(mode_ignored, &ch); wifi_cfg_get_mode(mode_ignored, &ch);
if (ch > 0) s_monitor_channel_staging = ch; if (ch > 0) s_monitor_channel_staging = ch;
ESP_LOGI(TAG, "Reloaded monitor channel: %d", s_monitor_channel_staging);
// Reload active channel from NVS
uint8_t saved_active_channel = 0;
if (wifi_cfg_get_monitor_channel(&saved_active_channel) && saved_active_channel > 0 && saved_active_channel <= 14) {
s_monitor_channel_active = saved_active_channel;
// Update staging to match active if staging wasn't set
if (s_monitor_channel_staging == 6 && ch == 0) {
s_monitor_channel_staging = saved_active_channel;
}
}
ESP_LOGI(TAG, "Reloaded monitor channel: active=%d, staging=%d", s_monitor_channel_active, s_monitor_channel_staging);
} }
void wifi_ctl_param_clear(void) { void wifi_ctl_param_clear(void) {
@ -834,83 +327,6 @@ void wifi_ctl_param_clear(void) {
wifi_ctl_mode_t wifi_ctl_get_mode(void) { return s_current_mode; } wifi_ctl_mode_t wifi_ctl_get_mode(void) { return s_current_mode; }
int wifi_ctl_get_channel(void) { return s_monitor_channel_active; } int wifi_ctl_get_channel(void) { return s_monitor_channel_active; }
/**
* @brief Get telemetry generation rate statistics
* @param total_bytes Total bytes generated (output)
* @param rate_bps Mean generation rate in bytes per second (output)
* @return ESP_OK on success
*/
esp_err_t wifi_ctl_get_telemetry_gen_stats(uint64_t *total_bytes, double *rate_bps) {
esp_err_t result = ESP_OK;
if (total_bytes == NULL || rate_bps == NULL) {
result = ESP_ERR_INVALID_ARG;
} else {
if (s_batch_mutex != NULL && xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
*total_bytes = s_telemetry_gen_rate.total_bytes;
*rate_bps = s_telemetry_gen_rate.mean_rate_bps;
xSemaphoreGive(s_batch_mutex);
} else {
*total_bytes = s_telemetry_gen_rate.total_bytes;
*rate_bps = s_telemetry_gen_rate.mean_rate_bps;
}
result = ESP_OK;
}
return result;
}
/**
* @brief Get SD card write rate statistics
* @param total_bytes Total bytes written (output)
* @param rate_bps Mean write rate in bytes per second (output)
* @return ESP_OK on success
*/
esp_err_t wifi_ctl_get_sd_write_stats(uint64_t *total_bytes, double *rate_bps) {
esp_err_t result = ESP_OK;
if (total_bytes == NULL || rate_bps == NULL) {
result = ESP_ERR_INVALID_ARG;
} else {
if (s_batch_mutex != NULL && xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
*total_bytes = s_sd_write_rate.total_bytes;
*rate_bps = s_sd_write_rate.mean_rate_bps;
xSemaphoreGive(s_batch_mutex);
} else {
*total_bytes = s_sd_write_rate.total_bytes;
*rate_bps = s_sd_write_rate.mean_rate_bps;
}
result = ESP_OK;
}
return result;
}
/**
* @brief Enable/disable monitor debug mode (serial logging)
* @param enable true to enable debug logging, false to disable
*/
void wifi_ctl_set_monitor_debug(bool enable) {
s_monitor_debug = enable;
wifi_monitor_set_debug(enable); /* Also set debug mode in wifi_monitor */
}
/**
* @brief Get monitor debug mode status
* @return true if debug mode enabled, false otherwise
*/
bool wifi_ctl_get_monitor_debug(void) {
return s_monitor_debug;
}
esp_err_t wifi_ctl_set_monitor_debug_filter(const uint8_t *mac) {
return wifi_monitor_set_debug_filter(mac);
}
bool wifi_ctl_get_monitor_debug_filter(uint8_t *mac_out) {
return wifi_monitor_get_debug_filter(mac_out);
}
// --- Deprecated --- // --- Deprecated ---
static void auto_monitor_task_func(void *arg) { static void auto_monitor_task_func(void *arg) {
uint8_t channel = (uint8_t)(uintptr_t)arg; uint8_t channel = (uint8_t)(uintptr_t)arg;

View File

@ -74,18 +74,6 @@ void wifi_ctl_param_clear(void);
wifi_ctl_mode_t wifi_ctl_get_mode(void); wifi_ctl_mode_t wifi_ctl_get_mode(void);
int wifi_ctl_get_channel(void); int wifi_ctl_get_channel(void);
// Telemetry rate statistics
esp_err_t wifi_ctl_get_telemetry_gen_stats(uint64_t *total_bytes, double *rate_bps);
esp_err_t wifi_ctl_get_sd_write_stats(uint64_t *total_bytes, double *rate_bps);
// Debug mode control
void wifi_ctl_set_monitor_debug(bool enable);
bool wifi_ctl_get_monitor_debug(void);
// Debug MAC filter control
esp_err_t wifi_ctl_set_monitor_debug_filter(const uint8_t *mac);
bool wifi_ctl_get_monitor_debug_filter(uint8_t *mac_out);
// Deprecated / Compatibility // Deprecated / Compatibility
void wifi_ctl_auto_monitor_start(uint8_t channel); void wifi_ctl_auto_monitor_start(uint8_t channel);

View File

@ -1,5 +1,5 @@
idf_component_register( idf_component_register(
SRCS "wifi_monitor.c" SRCS "wifi_monitor.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES esp_wifi nvs_flash esp_timer REQUIRES esp_wifi nvs_flash
) )

View File

@ -34,7 +34,6 @@
#include "wifi_monitor.h" #include "wifi_monitor.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_timer.h"
#include "string.h" #include "string.h"
static const char *TAG = "WiFi_Monitor"; static const char *TAG = "WiFi_Monitor";
@ -57,195 +56,9 @@ uint32_t threshold_duration_multiplier = 2; // NAV > expected * this = mism
// Logging control // Logging control
uint32_t log_every_n_mismatches = 1; // Log every Nth mismatch (1 = all, 10 = every 10th) uint32_t log_every_n_mismatches = 1; // Log every Nth mismatch (1 = all, 10 = every 10th)
static uint32_t s_mismatch_log_counter = 0; static uint32_t s_mismatch_log_counter = 0;
static bool s_monitor_debug = false; // Debug mode: enable serial logging
static uint8_t s_monitor_debug_filter_mac[6] = {0}; // MAC address filter (all zeros = no filter)
static bool s_monitor_debug_filter_enabled = false; // Whether MAC filtering is enabled
// Forward declarations // Forward declarations
static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type); static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type);
static bool wifi_monitor_debug_filter_match(const uint8_t *mac);
/**
* @brief Parse HT/VHT/HE headers to extract PHY parameters
* @param payload Raw frame payload
* @param len Total frame length
* @param mac_hdr_len Length of MAC header (24 or 30 bytes)
* @param frame_info Frame info structure to populate
*/
static void wifi_parse_phy_headers(const uint8_t *payload, uint16_t len, uint16_t mac_hdr_len, wifi_frame_info_t *frame_info) {
if (len < mac_hdr_len + 4) {
return; // Not enough data for any PHY headers
}
uint16_t offset = mac_hdr_len;
// Check for QoS Control field (4 bytes) - present in QoS data frames
bool has_qos = (frame_info->type == FRAME_TYPE_DATA &&
(frame_info->subtype == DATA_QOS_DATA ||
frame_info->subtype == DATA_QOS_DATA_CF_ACK ||
frame_info->subtype == DATA_QOS_DATA_CF_POLL ||
frame_info->subtype == DATA_QOS_DATA_CF_ACK_POLL ||
frame_info->subtype == DATA_QOS_NULL ||
frame_info->subtype == DATA_QOS_CF_POLL ||
frame_info->subtype == DATA_QOS_CF_ACK_POLL));
if (has_qos && len >= offset + 4) {
offset += 4; // Skip QoS Control field
}
// Check for HT Control field (4 bytes) - present if Order bit set
// HT Control field format (IEEE 802.11-2016):
// Bit 0-3: Control ID (0=HT, 1-3=VHT, 4+=HE)
// For VHT/HE: Additional fields for MCS, bandwidth, SGI, NSS
bool has_ht_ctrl = frame_info->order;
if (has_ht_ctrl && len >= offset + 4) {
// HT Control field is little-endian
uint32_t ht_ctrl = payload[offset] | (payload[offset + 1] << 8) |
(payload[offset + 2] << 16) | (payload[offset + 3] << 24);
// Control ID is in bits 0-3
uint8_t ctrl_id = ht_ctrl & 0x0F;
if (ctrl_id == 0) {
// HT Control (802.11n) - MCS info is typically in PLCP header (HT-SIG)
// which ESP-IDF provides in rx_ctrl->rate, not in HT Control field
frame_info->sig_mode = 1; // HT
// Note: MCS will be extracted from rx_ctrl->rate in the callback
// For HT, spatial streams can be inferred from MCS index (MCS 0-7 = 1 SS, 8-15 = 2 SS, etc.)
} else if (ctrl_id >= 1 && ctrl_id <= 3) {
// VHT Control (802.11ac) - bits layout varies by variant
frame_info->sig_mode = 3; // VHT
// VHT Control variant 1: MCS in bits 4-7, bandwidth in 8-9, SGI in 10, NSS in 12-14
if (ctrl_id == 1) {
frame_info->mcs = (ht_ctrl >> 4) & 0x0F;
frame_info->bandwidth = (ht_ctrl >> 8) & 0x03;
frame_info->sgi = ((ht_ctrl >> 10) & 0x01) != 0;
uint8_t nss = (ht_ctrl >> 12) & 0x07;
frame_info->spatial_streams = (nss == 0) ? 1 : nss; // NSS encoding varies
}
} else if (ctrl_id >= 4) {
// HE Control (802.11ax)
frame_info->sig_mode = 4; // HE
// HE Control: Similar structure to VHT
frame_info->mcs = (ht_ctrl >> 4) & 0x0F;
frame_info->bandwidth = (ht_ctrl >> 8) & 0x03;
frame_info->sgi = ((ht_ctrl >> 10) & 0x01) != 0;
uint8_t nss = (ht_ctrl >> 12) & 0x07;
frame_info->spatial_streams = (nss == 0) ? 1 : nss;
}
}
// For HT frames without HT Control, try to infer from rate/MCS
// MCS index encoding: MCS 0-7 = 1 SS, 8-15 = 2 SS, 16-23 = 3 SS, 24-31 = 4 SS
if (frame_info->sig_mode == 1 && frame_info->mcs > 0) {
if (frame_info->mcs < 8) {
frame_info->spatial_streams = 1;
} else if (frame_info->mcs < 16) {
frame_info->spatial_streams = 2;
} else if (frame_info->mcs < 24) {
frame_info->spatial_streams = 3;
} else {
frame_info->spatial_streams = 4;
}
}
}
/**
* @brief Parse A-MPDU aggregation count from frame payload
* @param payload Raw frame payload
* @param len Total frame length
* @param mac_hdr_len Length of MAC header (24 or 30 bytes)
* @return Number of aggregated MPDUs (1 if not aggregated)
*/
static uint8_t wifi_parse_ampdu_count(const uint8_t *payload, uint16_t len, uint16_t mac_hdr_len) {
uint8_t count = 1; // Default: not aggregated
uint16_t offset = mac_hdr_len;
// Check if this is a QoS data frame (required for A-MPDU)
uint8_t frame_type = (payload[0] >> 2) & 0x03;
uint8_t frame_subtype = (payload[0] >> 4) & 0x0F;
if (frame_type != FRAME_TYPE_DATA) {
return count; // Not a data frame
}
// Check if it's a QoS subtype
bool is_qos = (frame_subtype == DATA_QOS_DATA ||
frame_subtype == DATA_QOS_DATA_CF_ACK ||
frame_subtype == DATA_QOS_DATA_CF_POLL ||
frame_subtype == DATA_QOS_DATA_CF_ACK_POLL ||
frame_subtype == DATA_QOS_NULL ||
frame_subtype == DATA_QOS_CF_POLL ||
frame_subtype == DATA_QOS_CF_ACK_POLL);
if (!is_qos || len < offset + 4) {
return count; // Not QoS or too short
}
// Skip QoS Control field (4 bytes)
offset += 4;
// Check for HT Control field (4 bytes) - present if Order bit set
// ORDER bit is 0x8000 (bit 15), which is in payload[1], bit 7
bool has_ht_ctrl = (payload[1] & 0x80) != 0;
if (has_ht_ctrl && len >= offset + 4) {
offset += 4; // Skip HT Control field
}
// A-MPDU delimiter format (IEEE 802.11-2016):
// Each delimiter is 4 bytes: [Reserved(4) | MPDU Length(12) | CRC(8) | Delimiter Signature(8)]
// Delimiter signature is 0x4E (ASCII 'N')
// We count delimiters until we find the end delimiter (length = 0) or run out of data
uint16_t remaining = len - offset;
if (remaining < 4) {
return count; // Not enough data for even one delimiter
}
// Count A-MPDU subframes by parsing delimiters
uint16_t pos = offset;
uint16_t delimiter_count = 0; // Use uint16_t to support up to 256 delimiters
while (pos + 4 <= len && delimiter_count < 256) { // Support up to 256 for HE (though typically 64)
// Read delimiter (little-endian)
uint16_t delimiter = payload[pos] | (payload[pos + 1] << 8);
uint8_t sig = payload[pos + 3];
// Check delimiter signature (should be 0x4E)
if (sig != 0x4E) {
break; // Not a valid delimiter
}
// Extract MPDU length (bits 4-15)
uint16_t mpdu_len = (delimiter >> 4) & 0x0FFF;
if (mpdu_len == 0) {
break; // End delimiter
}
delimiter_count++;
// Move to next delimiter (skip this MPDU)
pos += 4 + mpdu_len; // Delimiter (4) + MPDU length
// Align to 4-byte boundary
pos = (pos + 3) & ~3;
if (pos >= len) {
break; // Reached end of frame
}
}
// If we found multiple delimiters, this is an A-MPDU
// Cap count at 255 (max value for uint8_t return type)
if (delimiter_count > 1) {
count = (delimiter_count > 255) ? 255 : (uint8_t)delimiter_count;
}
return count;
}
/** /**
* @brief Parse 802.11 MAC header * @brief Parse 802.11 MAC header
@ -290,24 +103,10 @@ esp_err_t wifi_parse_frame(const uint8_t *payload, uint16_t len, wifi_frame_info
// Check for Address 4 (only present if To DS and From DS both set) // Check for Address 4 (only present if To DS and From DS both set)
frame_info->has_addr4 = frame_info->to_ds && frame_info->from_ds; frame_info->has_addr4 = frame_info->to_ds && frame_info->from_ds;
uint16_t mac_hdr_len = frame_info->has_addr4 ? 30 : 24;
if (frame_info->has_addr4 && len >= 30) { if (frame_info->has_addr4 && len >= 30) {
memcpy(frame_info->addr4, &payload[24], 6); memcpy(frame_info->addr4, &payload[24], 6);
} }
// Initialize PHY parameters
frame_info->spatial_streams = 1; // Default to 1 SS
frame_info->mcs = 0;
frame_info->sig_mode = 0;
frame_info->sgi = false;
frame_info->bandwidth = 0;
// Parse HT/VHT/HE headers to extract PHY parameters
wifi_parse_phy_headers(payload, len, mac_hdr_len, frame_info);
// Parse A-MPDU aggregation count
frame_info->ampdu_count = wifi_parse_ampdu_count(payload, len, mac_hdr_len);
frame_info->frame_len = len; frame_info->frame_len = len;
return ESP_OK; return ESP_OK;
@ -336,56 +135,23 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
frame_info.timestamp = rx_ctrl->timestamp; frame_info.timestamp = rx_ctrl->timestamp;
// Extract PHY rate info from RX control // Extract PHY rate info from RX control
frame_info.mcs = 0;
frame_info.rate = rx_ctrl->rate; // This is the rate index frame_info.rate = rx_ctrl->rate; // This is the rate index
frame_info.sig_mode = 0;
frame_info.sgi = false;
frame_info.bandwidth = 0;
// If MCS wasn't parsed from headers but we have HT/VHT/HE mode, try to extract from rate // Estimate PHY rate from rate index (rough approximation)
// ESP-IDF encoding: rate >= 128 encodes MCS for HT/VHT/HE frames static const uint32_t rate_table[] = {
// HT: MCS 0-31, VHT: MCS 0-9, HE: MCS 0-11 1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS)
if (frame_info.sig_mode > 0 && frame_info.mcs == 0 && rx_ctrl->rate >= 128) { 6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates
uint8_t extracted_mcs = rx_ctrl->rate - 128; 65000, 130000, 195000, 260000 // Rough HT estimates
};
// Cap MCS based on signal mode
if (frame_info.sig_mode == 1) {
// HT (802.11n): MCS 0-31
if (extracted_mcs > 31) extracted_mcs = 31;
frame_info.mcs = extracted_mcs;
} else if (frame_info.sig_mode == 3) {
// VHT (802.11ac): MCS 0-9
if (extracted_mcs > 9) extracted_mcs = 9;
frame_info.mcs = extracted_mcs;
} else if (frame_info.sig_mode == 4) {
// HE (802.11ax): MCS 0-11
if (extracted_mcs > 11) extracted_mcs = 11;
frame_info.mcs = extracted_mcs;
} else {
// Unknown mode, use extracted value but cap at 31 (max HT)
if (extracted_mcs > 31) extracted_mcs = 31;
frame_info.mcs = extracted_mcs;
}
}
// Calculate PHY rate using parsed MCS/spatial streams if available if (rx_ctrl->rate < sizeof(rate_table) / sizeof(rate_table[0])) {
// Otherwise fall back to rate table estimation frame_info.phy_rate_kbps = rate_table[rx_ctrl->rate];
if (frame_info.sig_mode > 0 && frame_info.mcs > 0 && frame_info.spatial_streams > 0) {
// Use parsed MCS/SS info - rate will be calculated accurately in mcs_telemetry
// For monitor stats, keep the parsed value (may be 0 if not calculated yet)
if (frame_info.phy_rate_kbps == 0) {
// Fallback estimate if not set by parsing
frame_info.phy_rate_kbps = 100000; // Default estimate
}
} else { } else {
// Estimate PHY rate from rate index (rough approximation for legacy frames) frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default
static const uint32_t rate_table[] = {
1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS)
6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates
65000, 130000, 195000, 260000 // Rough HT estimates
};
if (rx_ctrl->rate < sizeof(rate_table) / sizeof(rate_table[0])) {
frame_info.phy_rate_kbps = rate_table[rx_ctrl->rate];
} else {
frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default
}
} }
// Update statistics // Update statistics
@ -437,24 +203,21 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
if (frame_info.duration_id > threshold_duration_mismatch_us) { if (frame_info.duration_id > threshold_duration_mismatch_us) {
s_mismatch_log_counter++; s_mismatch_log_counter++;
if (s_monitor_debug && (s_mismatch_log_counter % log_every_n_mismatches) == 0) { if ((s_mismatch_log_counter % log_every_n_mismatches) == 0) {
/* Check MAC filter before logging */ ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
if (wifi_monitor_debug_filter_match(frame_info.addr2)) { wifi_frame_type_str(frame_info.type, frame_info.subtype),
ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps", frame_info.frame_len, phy_rate_mbps);
wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps);
// NEW: Log the Source MAC (Addr2) // NEW: Log the Source MAC (Addr2)
ESP_LOGW("MONITOR", " Source MAC: %02x:%02x:%02x:%02x:%02x:%02x", ESP_LOGW("MONITOR", " Source MAC: %02x:%02x:%02x:%02x:%02x:%02x",
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2], frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]); frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)", ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)",
expected_duration, frame_info.duration_id, expected_duration, frame_info.duration_id,
frame_info.duration_id - expected_duration); frame_info.duration_id - expected_duration);
ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm", ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm",
frame_info.retry ? "YES" : "no", frame_info.rssi); frame_info.retry ? "YES" : "no", frame_info.rssi);
}
} }
} }
} }
@ -464,23 +227,18 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
// --------------------------------------------------------- // ---------------------------------------------------------
if (frame_info.retry && frame_info.duration_id > threshold_high_nav_us && if (frame_info.retry && frame_info.duration_id > threshold_high_nav_us &&
phy_rate_mbps < threshold_phy_rate_fallback_mbps) { phy_rate_mbps < threshold_phy_rate_fallback_mbps) {
if (s_monitor_debug) { ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
/* Check MAC filter before logging */
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
// NEW: Log the Attacker MAC // NEW: Log the Attacker MAC
ESP_LOGW("MONITOR", " Attacker MAC: %02x:%02x:%02x:%02x:%02x:%02x", ESP_LOGW("MONITOR", " Attacker MAC: %02x:%02x:%02x:%02x:%02x:%02x",
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2], frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]); frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps", ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps",
wifi_frame_type_str(frame_info.type, frame_info.subtype), wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps); frame_info.frame_len, phy_rate_mbps);
ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES", ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES",
frame_info.duration_id, expected_duration); frame_info.duration_id, expected_duration);
}
}
} }
} }
@ -502,94 +260,6 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
case FRAME_TYPE_DATA: case FRAME_TYPE_DATA:
stats.data_frames++; stats.data_frames++;
/* Periodic diagnostic summary (even when debug is off) */
static uint32_t data_frame_diag_counter = 0;
static uint64_t last_diag_log_ms = 0;
data_frame_diag_counter++;
uint64_t now_ms = esp_timer_get_time() / 1000;
if (now_ms - last_diag_log_ms > 10000) { /* Log every 10 seconds */
ESP_LOGI("MONITOR", "Data frames: %lu total, last TA=%02x:%02x:%02x:%02x:%02x:%02x, "
"debug=%s, filter=%s",
(unsigned long)stats.data_frames,
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5],
s_monitor_debug ? "on" : "off",
s_monitor_debug_filter_enabled ? "on" : "off");
data_frame_diag_counter = 0;
last_diag_log_ms = now_ms;
}
/* Update histograms for data frames */
// AMPDU histogram: [1, 2-4, 5-8, 9-16, 17-32, 33-48, 49-64, 65+]
uint8_t ampdu_count = frame_info.ampdu_count;
if (ampdu_count == 1) {
stats.ampdu_hist[0]++;
} else if (ampdu_count >= 2 && ampdu_count <= 4) {
stats.ampdu_hist[1]++;
} else if (ampdu_count >= 5 && ampdu_count <= 8) {
stats.ampdu_hist[2]++;
} else if (ampdu_count >= 9 && ampdu_count <= 16) {
stats.ampdu_hist[3]++;
} else if (ampdu_count >= 17 && ampdu_count <= 32) {
stats.ampdu_hist[4]++;
} else if (ampdu_count >= 33 && ampdu_count <= 48) {
stats.ampdu_hist[5]++;
} else if (ampdu_count >= 49 && ampdu_count <= 64) {
stats.ampdu_hist[6]++;
} else if (ampdu_count >= 65) {
stats.ampdu_hist[7]++;
}
// MCS histogram: [0-31]
if (frame_info.mcs < 32) {
stats.mcs_hist[frame_info.mcs]++;
}
// Spatial streams histogram: [1-8]
uint8_t ss = frame_info.spatial_streams;
if (ss >= 1 && ss <= 8) {
stats.ss_hist[ss - 1]++; // Convert to 0-indexed
}
/* Debug logging for data frames (especially QoS data used by iperf) */
if (s_monitor_debug) {
bool filter_match = wifi_monitor_debug_filter_match(frame_info.addr2);
uint16_t phy_rate_mbps = frame_info.phy_rate_kbps / 1000;
const char *frame_type_name = wifi_frame_type_str(frame_info.type, frame_info.subtype);
if (filter_match) {
/* Log all data frames (QoS and non-QoS) when filter matches or filter is disabled */
ESP_LOGI("MONITOR", "DATA: %s, TA=%02x:%02x:%02x:%02x:%02x:%02x, RA=%02x:%02x:%02x:%02x:%02x:%02x, "
"Size=%u bytes, Rate=%u Mbps, MCS=%u, SS=%u, BW=%u MHz, RSSI=%d dBm, Retry=%s",
frame_type_name,
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5],
frame_info.addr1[0], frame_info.addr1[1], frame_info.addr1[2],
frame_info.addr1[3], frame_info.addr1[4], frame_info.addr1[5],
frame_info.frame_len, phy_rate_mbps, frame_info.mcs,
frame_info.spatial_streams,
(frame_info.bandwidth == 0) ? 20 : (frame_info.bandwidth == 1) ? 40 : 80,
frame_info.rssi, frame_info.retry ? "YES" : "no");
} else {
/* Diagnostic: log when data frames are filtered out (throttled, but more visible) */
static uint32_t filtered_data_count = 0;
static uint64_t last_filtered_log_ms = 0;
static uint8_t last_filtered_ta[6] = {0};
filtered_data_count++;
uint64_t now_ms = esp_timer_get_time() / 1000;
memcpy(last_filtered_ta, frame_info.addr2, 6);
if (now_ms - last_filtered_log_ms > 5000) { /* Log every 5 seconds */
ESP_LOGW("MONITOR", "Filtered out %lu data frames (last TA=%02x:%02x:%02x:%02x:%02x:%02x, filter=%s)",
(unsigned long)filtered_data_count,
last_filtered_ta[0], last_filtered_ta[1], last_filtered_ta[2],
last_filtered_ta[3], last_filtered_ta[4], last_filtered_ta[5],
s_monitor_debug_filter_enabled ? "enabled" : "disabled");
filtered_data_count = 0;
last_filtered_log_ms = now_ms;
}
}
}
break; break;
} }
@ -807,67 +477,3 @@ const char* wifi_frame_type_str(uint8_t type, uint8_t subtype) {
return "UNKNOWN"; return "UNKNOWN";
} }
/**
* @brief Enable/disable monitor debug mode (serial logging)
* @param enable true to enable debug logging, false to disable
*/
void wifi_monitor_set_debug(bool enable) {
s_monitor_debug = enable;
}
/**
* @brief Get monitor debug mode status
* @return true if debug mode enabled, false otherwise
*/
bool wifi_monitor_get_debug(void) {
return s_monitor_debug;
}
/**
* @brief Check if a MAC address matches the debug filter
* @param mac MAC address to check (6 bytes)
* @return true if filter is disabled or MAC matches filter, false otherwise
*/
static bool wifi_monitor_debug_filter_match(const uint8_t *mac) {
if (!s_monitor_debug_filter_enabled || mac == NULL) {
return true; /* No filter or invalid MAC - allow all */
}
/* Compare MAC addresses byte by byte */
for (int i = 0; i < 6; i++) {
if (mac[i] != s_monitor_debug_filter_mac[i]) {
return false; /* MAC doesn't match filter */
}
}
return true; /* MAC matches filter */
}
/**
* @brief Set MAC address filter for debug logging
* @param mac MAC address to filter on (6 bytes), or NULL to disable filter
* @return ESP_OK on success, ESP_ERR_INVALID_ARG if mac is invalid
*/
esp_err_t wifi_monitor_set_debug_filter(const uint8_t *mac) {
if (mac == NULL) {
/* Disable filter */
s_monitor_debug_filter_enabled = false;
memset(s_monitor_debug_filter_mac, 0, 6);
return ESP_OK;
}
/* Enable filter and copy MAC address */
memcpy(s_monitor_debug_filter_mac, mac, 6);
s_monitor_debug_filter_enabled = true;
return ESP_OK;
}
/**
* @brief Get current MAC address filter for debug logging
* @param mac_out Buffer to store MAC address (6 bytes), or NULL to just check if filter is enabled
* @return true if filter is enabled, false otherwise
*/
bool wifi_monitor_get_debug_filter(uint8_t *mac_out) {
if (mac_out != NULL && s_monitor_debug_filter_enabled) {
memcpy(mac_out, s_monitor_debug_filter_mac, 6);
}
return s_monitor_debug_filter_enabled;
}

View File

@ -144,41 +144,37 @@ typedef struct {
bool more_data; bool more_data;
bool protected_frame; bool protected_frame;
bool order; bool order;
// Duration/ID (NAV) // Duration/ID (NAV)
uint16_t duration_id; uint16_t duration_id;
// MAC Addresses // MAC Addresses
uint8_t addr1[6]; // Receiver address uint8_t addr1[6]; // Receiver address
uint8_t addr2[6]; // Transmitter address uint8_t addr2[6]; // Transmitter address
uint8_t addr3[6]; // Filtering address (BSSID/SA/DA) uint8_t addr3[6]; // Filtering address (BSSID/SA/DA)
// Sequence Control // Sequence Control
uint16_t seq_ctrl; uint16_t seq_ctrl;
uint16_t fragment_num; uint16_t fragment_num;
uint16_t sequence_num; uint16_t sequence_num;
// Optional: Address 4 (if To DS and From DS both set) // Optional: Address 4 (if To DS and From DS both set)
uint8_t addr4[6]; uint8_t addr4[6];
bool has_addr4; bool has_addr4;
// RX Info // RX Info
int8_t rssi; int8_t rssi;
uint8_t channel; uint8_t channel;
uint32_t timestamp; uint32_t timestamp;
// PHY rate info // PHY rate info
uint8_t mcs; // MCS index (for HT/VHT/HE frames) uint8_t mcs; // MCS index (for HT/VHT frames)
uint8_t rate; // Legacy rate or rate index uint8_t rate; // Legacy rate or rate index
uint8_t sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT, 4=HE) uint8_t sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT)
uint8_t spatial_streams; // Number of spatial streams (NSS) - 1-8
bool sgi; // Short Guard Interval bool sgi; // Short Guard Interval
uint8_t bandwidth; // 0=20MHz, 1=40MHz, 2=80MHz, 3=160MHz uint8_t bandwidth; // 0=20MHz, 1=40MHz, 2=80MHz
uint32_t phy_rate_kbps; // Calculated PHY rate in Kbps (uint32_t to handle >65 Mbps) uint32_t phy_rate_kbps; // Calculated PHY rate in Kbps (uint32_t to handle >65 Mbps)
// Aggregation info
uint8_t ampdu_count; // Number of aggregated MPDUs in A-MPDU (1 = not aggregated)
// Frame size // Frame size
uint16_t frame_len; uint16_t frame_len;
} wifi_frame_info_t; } wifi_frame_info_t;
@ -195,40 +191,35 @@ typedef struct {
uint32_t ack_frames; uint32_t ack_frames;
uint32_t data_frames; uint32_t data_frames;
uint32_t mgmt_frames; uint32_t mgmt_frames;
// Collapse indicators // Collapse indicators
float retry_rate; // Percentage of retried frames float retry_rate; // Percentage of retried frames
uint16_t avg_nav; // Average NAV duration uint16_t avg_nav; // Average NAV duration
uint16_t max_nav; // Maximum NAV seen uint16_t max_nav; // Maximum NAV seen
uint32_t collision_events; // Estimated collision count uint32_t collision_events; // Estimated collision count
// Duration analysis // Duration analysis
uint32_t duration_mismatch_frames; // NAV >> expected uint32_t duration_mismatch_frames; // NAV >> expected
uint32_t rate_fallback_frames; // PHY rate < 100 Mbps uint32_t rate_fallback_frames; // PHY rate < 100 Mbps
uint16_t avg_phy_rate_mbps; // Average PHY rate uint16_t avg_phy_rate_mbps; // Average PHY rate
uint16_t min_phy_rate_mbps; // Minimum PHY rate seen uint16_t min_phy_rate_mbps; // Minimum PHY rate seen
uint16_t max_phy_rate_mbps; // Maximum PHY rate seen uint16_t max_phy_rate_mbps; // Maximum PHY rate seen
// Histograms
uint32_t ampdu_hist[8]; // AMPDU aggregation: [1, 2-4, 5-8, 9-16, 17-32, 33-48, 49-64, 65+]
uint32_t mcs_hist[32]; // MCS index: [0-31]
uint32_t ss_hist[8]; // Spatial streams: [1-8]
} wifi_collapse_stats_t; } wifi_collapse_stats_t;
/** /**
* @brief Callback function type for frame capture * @brief Callback function type for frame capture
* *
* @param frame_info Parsed frame information * @param frame_info Parsed frame information
* @param payload Raw frame payload (starts with MAC header) * @param payload Raw frame payload (starts with MAC header)
* @param len Frame length * @param len Frame length
*/ */
typedef void (*wifi_monitor_cb_t)(const wifi_frame_info_t *frame_info, typedef void (*wifi_monitor_cb_t)(const wifi_frame_info_t *frame_info,
const uint8_t *payload, const uint8_t *payload,
uint16_t len); uint16_t len);
/** /**
* @brief Initialize WiFi monitor mode * @brief Initialize WiFi monitor mode
* *
* @param channel WiFi channel to monitor (1-14 for 2.4GHz, 36+ for 5GHz) * @param channel WiFi channel to monitor (1-14 for 2.4GHz, 36+ for 5GHz)
* @param callback Callback function for captured frames * @param callback Callback function for captured frames
* @return esp_err_t ESP_OK on success * @return esp_err_t ESP_OK on success
@ -237,21 +228,21 @@ esp_err_t wifi_monitor_init(uint8_t channel, wifi_monitor_cb_t callback);
/** /**
* @brief Start WiFi monitoring * @brief Start WiFi monitoring
* *
* @return esp_err_t ESP_OK on success * @return esp_err_t ESP_OK on success
*/ */
esp_err_t wifi_monitor_start(void); esp_err_t wifi_monitor_start(void);
/** /**
* @brief Stop WiFi monitoring * @brief Stop WiFi monitoring
* *
* @return esp_err_t ESP_OK on success * @return esp_err_t ESP_OK on success
*/ */
esp_err_t wifi_monitor_stop(void); esp_err_t wifi_monitor_stop(void);
/** /**
* @brief Set WiFi channel for monitoring * @brief Set WiFi channel for monitoring
* *
* @param channel WiFi channel (1-14 for 2.4GHz, 36+ for 5GHz) * @param channel WiFi channel (1-14 for 2.4GHz, 36+ for 5GHz)
* @return esp_err_t ESP_OK on success * @return esp_err_t ESP_OK on success
*/ */
@ -259,7 +250,7 @@ esp_err_t wifi_monitor_set_channel(uint8_t channel);
/** /**
* @brief Parse 802.11 frame header * @brief Parse 802.11 frame header
* *
* @param payload Raw frame data * @param payload Raw frame data
* @param len Frame length * @param len Frame length
* @param frame_info Output: parsed frame information * @param frame_info Output: parsed frame information
@ -269,7 +260,7 @@ esp_err_t wifi_parse_frame(const uint8_t *payload, uint16_t len, wifi_frame_info
/** /**
* @brief Get WiFi collapse detection statistics * @brief Get WiFi collapse detection statistics
* *
* @param stats Output: statistics structure * @param stats Output: statistics structure
* @return esp_err_t ESP_OK on success * @return esp_err_t ESP_OK on success
*/ */
@ -282,46 +273,20 @@ void wifi_monitor_reset_stats(void);
/** /**
* @brief Check if current conditions indicate WiFi collapse * @brief Check if current conditions indicate WiFi collapse
* *
* @return true if collapse is detected, false otherwise * @return true if collapse is detected, false otherwise
*/ */
bool wifi_monitor_is_collapsed(void); bool wifi_monitor_is_collapsed(void);
/** /**
* @brief Get string representation of frame type * @brief Get string representation of frame type
* *
* @param type Frame type * @param type Frame type
* @param subtype Frame subtype * @param subtype Frame subtype
* @return const char* String description * @return const char* String description
*/ */
const char* wifi_frame_type_str(uint8_t type, uint8_t subtype); const char* wifi_frame_type_str(uint8_t type, uint8_t subtype);
/**
* @brief Enable/disable monitor debug mode (serial logging)
* @param enable true to enable debug logging, false to disable
*/
void wifi_monitor_set_debug(bool enable);
/**
* @brief Get monitor debug mode status
* @return true if debug mode enabled, false otherwise
*/
bool wifi_monitor_get_debug(void);
/**
* @brief Set MAC address filter for debug logging
* @param mac MAC address to filter on (6 bytes), or NULL to disable filter
* @return ESP_OK on success, ESP_ERR_INVALID_ARG if mac is invalid
*/
esp_err_t wifi_monitor_set_debug_filter(const uint8_t *mac);
/**
* @brief Get current MAC address filter for debug logging
* @param mac_out Buffer to store MAC address (6 bytes), or NULL to just check if filter is enabled
* @return true if filter is enabled, false otherwise
*/
bool wifi_monitor_get_debug_filter(uint8_t *mac_out);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -257,7 +257,7 @@ mode_monitor 149/80
## 🔍 Real-World Scenarios ## 🔍 Real-World Scenarios
### Scenario 1: Office Wi-Fi Telemetry Capture ### Scenario 1: Office WiFi Collapse Detection
**Goal:** Monitor office WiFi on 5GHz with WiFi 6 **Goal:** Monitor office WiFi on 5GHz with WiFi 6
```bash ```bash

View File

@ -608,7 +608,7 @@ idf.py -p $PORT monitor | tee device_90.log
10. COMMON DEPLOYMENT SCENARIOS 10. COMMON DEPLOYMENT SCENARIOS
================================================================================ ================================================================================
SCENARIO 1: WIFI TELEMETRY CAPTURE (32 devices) SCENARIO 1: WIFI COLLAPSE DETECTION (32 devices)
------------------------------------------------- -------------------------------------------------
Goal: Detect WiFi collapse events and correlate with iperf degradation Goal: Detect WiFi collapse events and correlate with iperf degradation

View File

@ -1,241 +0,0 @@
# GPS Module Wiring Guide
## Overview
This guide shows how to connect a GPS module (e.g., MakerFocus GT-U7, NEO-6M) to the ESP32 boards supported by this firmware. The GPS provides NMEA sentences for position/time and an optional PPS (pulse-per-second) signal for precise timing.
## Supported GPS Modules
- **MakerFocus GT-U7** (recommended) — 3.3V compatible, NMEA output, PPS, 9600 baud default
- **NEO-6M** — Same pinout and NMEA format
## Connections Required
| GPS Pin | ESP32 Side | Function |
|---------|------------|----------|
| VCC | 3.3V | Power (3.3V only) |
| GND | GND | Ground |
| TXD | ESP32 RX | NMEA data from GPS → ESP32 |
| RXD | ESP32 TX | Optional: config/commands to GPS |
| PPS | GPIO input | Pulse-per-second (optional but recommended) |
**Note:** TX and RX are crossed: GPS TXD → ESP32 RX, GPS RXD → ESP32 TX.
**Power with SD card:** If you also use the SparkFun microSD breakout, **J1 Pin 1 (3.3V)** and **J1 Pin 15 (GND)** must use **Y-cables**: one branch to the GPS, one to the SD breakout, so both devices are powered from the same 3.3V and GND. See [SD_CARD_WIRING.md](SD_CARD_WIRING.md) for the combined pin diagram.
---
## ESP32-C5 (DevKitC-1)
### Wiring Table
| GPS Module Pin | ESP32-C5 GPIO | Header Location | Function |
|----------------|---------------|-----------------|----------|
| **VCC** | 3V3 | J1 Pin 1 (Left) | Power |
| **GND** | GND | J1 Pin 15 (Left) | Ground |
| **TXD** (NMEA out) | GPIO23 (RX) | J3 Pin 5 (Right) | GPS → ESP32 |
| **RXD** (config in) | GPIO24 (TX) | J3 Pin 4 (Right) | ESP32 → GPS (optional) |
| **PPS** | GPIO1 | J1 Pin 6 (Left) | Pulse per second |
**Note:** GPIO25 (J1 Pin 13) is a strapping pin and can interfere with PPS input; the firmware uses GPIO1 (J1 Pin 6) instead.
### Quick Wiring (5 wires)
```
GT-U7 VCC → J1 Pin 1 (3V3) [use Y-cable if also powering SD breakout]
GT-U7 GND → J1 Pin 15 (GND) [use Y-cable if also powering SD breakout]
GT-U7 TXD → J3 Pin 5 (GPIO23) — ESP32 RX
GT-U7 RXD → J3 Pin 4 (GPIO24) — ESP32 TX
GT-U7 PPS → J1 Pin 6 (GPIO1)
```
### Minimal Wiring (4 wires, no config)
```
GT-U7 VCC → J1 Pin 1 (3V3)
GT-U7 GND → J1 Pin 15 (GND)
GT-U7 TXD → J3 Pin 5 (GPIO23)
GT-U7 PPS → J1 Pin 6 (GPIO1)
```
### Pin Locations (J1 / J3)
| J1 (Left) | Pin | J3 (Right) | Pin |
|-----------|---|------------|-----|
| 3V3 | 1 | GND | 1 |
| RST | 2 | TX (UART0) | 2 |
| ... | ... | RX (UART0) | 3 |
| ... | ... | GPIO24 | 4 ← GPS RXD |
| ... | ... | GPIO23 | 5 ← GPS TXD |
| GPIO1 | 6 ← PPS | ... | ... |
| ... | ... | ... | ... |
| GPIO25 | 13 | ... | ... (strapping, avoid for PPS) |
| 5V | 14 (do not use) | ... | ... |
| GND | 15 | ... | ... |
---
## ESP32-S3 (DevKitC-1)
| GPS Module Pin | ESP32-S3 GPIO | Function |
|----------------|---------------|----------|
| VCC | 3V3 | Power |
| GND | GND | Ground |
| TXD | GPIO4 (RX) | GPS → ESP32 |
| RXD | GPIO5 (TX) | ESP32 → GPS (optional) |
| PPS | GPIO6 | Pulse per second |
### Quick Wiring
```
GT-U7 VCC → 3V3
GT-U7 GND → GND
GT-U7 TXD → GPIO4 (RX)
GT-U7 RXD → GPIO5 (TX)
GT-U7 PPS → GPIO6
```
---
## ESP32 (Original / Standard)
| GPS Module Pin | ESP32 GPIO | Function |
|----------------|------------|----------|
| VCC | 3V3 | Power |
| GND | GND | Ground |
| TXD | GPIO16 (RX) | GPS → ESP32 |
| RXD | GPIO17 (TX) | ESP32 → GPS (optional) |
| PPS | GPIO4 | Pulse per second |
### Quick Wiring
```
GT-U7 VCC → 3V3
GT-U7 GND → GND
GT-U7 TXD → GPIO16 (RX)
GT-U7 RXD → GPIO17 (TX)
GT-U7 PPS → GPIO4
```
---
## PPS Polarity
The firmware supports both **rising-edge** and **falling-edge** (active-low) PPS. Many u-blox GT-U7/NEO-6M modules use **active-low** PPS (line pulls low for ~100 ms at each second). The default is `GPS_PPS_ACTIVE_LOW=1` in `board_config.h`. If PPS Lock shows NO despite correct wiring, try flipping this (set to 0 for rising-edge).
## Important Notes
⚠️ **Warnings:**
- **Use 3.3V only** — Do not connect GPS VCC to 5V; many modules are 3.3V logic.
- **TX ↔ RX crossover** — GPS TXD connects to ESP32 RX; GPS RXD connects to ESP32 TX.
- Avoid strapping pins (e.g., GPIO2, GPIO3 on ESP32-C5) for general I/O.
## Testing
After wiring, use the `gps` console command to check NMEA output and fix status. Example:
```
gps status
gps nmea
```
## Troubleshooting
### NMEA Valid: NO
- **TX/RX swapped** — GPS TXD must go to ESP32 RX (GPIO23 on C5). If you swapped them, NMEA won't parse.
- **Wrong pins** — Double-check GPIO numbers. Older guides (e.g. `esp32-c5-gps-sync-guide.html`) use GPIO4/5 for UART; the firmware expects GPIO23/24.
- **No satellite fix** — Move the antenna near a window or outdoors. Cold start can take 12 minutes.
- **Power** — Ensure VCC is 3.3V. Some modules draw more current during acquisition.
### PPS Locked: NO (but NMEA Valid: YES)
- **Verify signal** — Run `gps pps-test` (or `gps pps-test -d 5`) to poll the PPS GPIO. If no edges are detected, the signal isn't reaching the ESP32.
- **Wrong PPS pin** — The firmware expects PPS on GPIO1 (J1 Pin 6). GPIO25 (Pin 13) is a strapping pin and can interfere; use Pin 6 instead.
- **PPS polarity** — Many u-blox modules use active-low PPS. The default `GPS_PPS_ACTIVE_LOW=1` in `board_config.h` triggers on falling edge. If it still fails, try setting `GPS_PPS_ACTIVE_LOW=0` (rising-edge).
- **PPS not enabled** — Some modules need UBX configuration to output PPS. GT-U7 usually outputs it by default once a fix is acquired.
- **Loose connection** — PPS is a 1 Hz signal; a bad connection will cause missed edges.
### No Time / No Fix
- **Antenna** — Active antennas need power; ensure the modules antenna connector matches. Outdoor or window placement helps.
- **Startup delay** — Allow 12 minutes for cold start. Check `gps nmea` for $GPGGA with "A" (valid) vs "V" (invalid).
### Documentation Mismatch
If you followed `doc/esp32-c5-gps-sync-guide.html`, note its pinout differs from the current firmware:
| Signal | Old guide | Current firmware |
|--------|-----------|------------------|
| RX (GPS TXD) | GPIO4, J3 Pin 8 | GPIO23, J3 Pin 5 |
| TX (GPS RXD) | GPIO5, J3 Pin 9 | GPIO24, J3 Pin 4 |
| PPS | GPIO1, J1 Pin 6 | GPIO1, J1 Pin 6 ✓ (same) |
Use the pinout in this document for compatibility with the firmware.
## Configuration Source
Pin assignments are defined in `main/board_config.h`. To change pins for a custom board, edit the appropriate `#elif defined (CONFIG_IDF_TARGET_...)` block.
---
## Complete Pin Reference (ESP32-C5)
The following table shows all pins for both the SparkFun microSD breakout and the GPS module (MakerFocus GT-U7 / NEO-6M), and how they map to the ESP32-C5 DevKit.
### SparkFun microSD Transflash Breakout — All Pins
| SparkFun Pin | Label | ESP32-C5 | J1 Pin | Function |
|--------------|-------|----------|--------|----------|
| 1 | VCC | 3V3 | 1 | Power (3.3V only) |
| 2 | GND | GND | 15 | Ground |
| 3 | SCK | GPIO9 | 10 | SPI clock |
| 4 | MOSI / DI | GPIO10 | 11 | Data in to card |
| 5 | MISO / DO | GPIO8 | 9 | Data out from card |
| 6 | CS | GPIO7 | 8 | Chip select |
| 7 | CD | GPIO26 | 12 | Card detect (LOW = inserted) |
**Note:** Pin numbering may vary by SparkFun breakout revision. Some boards use DI/DO labels; DI = MOSI, DO = MISO.
### GPS Module (GT-U7 / NEO-6M) — All Pins
| GPS Pin | Label | ESP32-C5 | J1/J3 | Function |
|---------|-------|----------|-------|----------|
| 1 | VCC | 3V3 | J1 Pin 1 | Power (3.3V only) |
| 2 | GND | GND | J1 Pin 15 | Ground |
| 3 | TXD | GPIO23 (RX) | J3 Pin 5 | NMEA data out → ESP32 RX |
| 4 | RXD | GPIO24 (TX) | J3 Pin 4 | Config in ← ESP32 TX (optional) |
| 5 | PPS | GPIO1 | J1 Pin 6 | Pulse per second |
**Note:** GPS and ESP32 may share VCC/GND with the SD card if powered from the same 3V3 rail.
### Combined ESP32-C5 J1 Header (Left Side)
| Pin | Signal | SparkFun | GPS | Notes |
|-----|--------|----------|-----|-------|
| 1 | 3V3 | VCC | VCC | Shared power |
| 2 | RST | — | — | Do not connect |
| 3 | GPIO2 | — | — | Strapping pin |
| 4 | GPIO3 | — | — | Strapping pin |
| 5 | GPIO0 | — | — | Boot mode |
| 6 | GPIO1 | — | PPS | GPS pulse |
| 7 | GPIO6 | — | — | — |
| 8 | GPIO7 | CS | — | SD chip select |
| 9 | GPIO8 | MISO (DO) | — | SD data out |
| 10 | GPIO9 | CLK (SCK) | — | SD clock |
| 11 | GPIO10 | MOSI (DI) | — | SD data in |
| 12 | GPIO26 | CD | — | SD card detect |
| 13 | GPIO25 | — | — | Strapping (avoid for PPS) |
| 14 | 5V | — | — | Do not use |
| 15 | GND | GND | GND | Shared ground |
| 16 | NC | — | — | No connection |
### Combined ESP32-C5 J3 Header (Right Side)
| Pin | Signal | SparkFun | GPS | Notes |
|-----|--------|----------|-----|-------|
| 1 | GND | — | — | — |
| 2 | TX (UART0) | — | — | USB bridge |
| 3 | RX (UART0) | — | — | USB bridge |
| 4 | GPIO24 | — | RXD | ESP32 TX → GPS RXD |
| 5 | GPIO23 | — | TXD | GPS TXD → ESP32 RX |

View File

@ -1,270 +0,0 @@
# SparkFun microSD Transflash Breakout - ESP32-C5 Wiring Guide
## Overview
This guide shows how to connect the SparkFun microSD Transflash Breakout board to the ESP32-C5 DevKit. **ESP32-C5 does not support SDMMC/SDIO host**, so the firmware uses **SPI mode** for SD card access.
**If you also have a GPS module (e.g. GT-U7):** J1 Pin 1 (3.3V) and Pin 15 (GND) must each use a **Y-cable** (or splice) so one leg goes to the SD breakout and one to the GPS — both devices share the same 3.3V and GND from the ESP32-C5.
## Required Connections (SPI mode)
- VCC (3.3V)
- GND (Ground)
- CLK (SPI SCLK)
- DI (SPI MOSI - Data In to card)
- DO (SPI MISO - Data Out from card)
- CS (Chip Select)
- CD (Card Detect, optional)
## Wiring Table (SPI Mode)
| SparkFun Label | ESP32-C5 Pin | Header Location | GPIO Number | Function |
|----------------|--------------|-----------------|-------------|----------|
| **VCC** | Pin 1 | J1 (Left Side) | 3V3 | Power |
| **GND** | Pin 15 | J1 (Left Side) | GND | Ground |
| **SCK** | Pin 10 | J1 (Left Side) | GPIO9 | SPI_CLK |
| **DI** | Pin 11 | J1 (Left Side) | GPIO10 | SPI_MOSI |
| **DO** | Pin 9 | J1 (Left Side) | GPIO8 | SPI_MISO |
| **CS** | Pin 8 | J1 (Left Side) | GPIO7 | SPI_CS |
| **CD** | Pin 12 | J1 (Left Side) | GPIO26 | Card Detect (Input Pull-up) |
Note: If your SparkFun breakout does not expose CS, some breakouts tie CS to GND (always selected). The firmware expects CS on GPIO7.
## Wiring Diagram (SD only)
```
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ SparkFun microSD Breakout │ │ ESP32-C5 DevKit (J1) │
│ │ │ │
│ VCC ──────────────────────┼─────────┼─► Pin 1 (3V3) │
│ GND ──────────────────────┼─────────┼─► Pin 15 (GND) │
│ SCK ──────────────────────┼─────────┼─► Pin 10 (GPIO9) CLK │
│ DI ──────────────────────┼─────────┼─► Pin 11 (GPIO10) MOSI │
│ DO ──────────────────────┼─────────┼─► Pin 9 (GPIO8) MISO │
│ CS ──────────────────────┼─────────┼─► Pin 8 (GPIO7) Chip Select│
│ CD ──────────────────────┼─────────┼─► Pin 12 (GPIO26) Card Detect│
│ │ │ (optional) │
└─────────────────────────────┘ └──────────────────────────────┘
```
## Combined Pin Diagram (SD Card + GPS)
When using **both** the SparkFun microSD breakout and a GPS module (e.g. GT-U7 / NEO-6M), power and ground are shared:
- **J1 Pin 1 (3.3V)** and **J1 Pin 15 (GND)** each need a **Y-cable** (or one 3.3V and one GND split): one branch to the SD breakout, one branch to the GPS. Do **not** rely on a single wire to the first device and then daisychain to the second; use a proper Y (or star) so both get a direct connection to 3.3V and GND.
```
ESP32-C5 DevKit J1 (left) + J3 (right)
┌─────────────────────────────────────────────────────────────────────────┐
│ Pin 1: 3V3 ◄────── Y-cable ──────┬────► SD Breakout VCC │
│ └────► GPS VCC │
│ Pin 2: RST (do not connect) │
│ Pin 3: GPIO2 (strapping do not use) │
│ Pin 4: GPIO3 (strapping do not use) │
│ Pin 5: GPIO0 │
│ Pin 6: GPIO1 ◄──────────────────────── GPS PPS (optional) │
│ Pin 7: GPIO6 │
│ Pin 8: GPIO7 ◄──────────────────────── SD CS │
│ Pin 9: GPIO8 ◄──────────────────────── SD MISO (DO) │
│ Pin 10: GPIO9 ◄──────────────────────── SD CLK (SCK) │
│ Pin 11: GPIO10 ◄──────────────────────── SD MOSI (DI) │
│ Pin 12: GPIO26 ◄──────────────────────── SD CD (optional) │
│ Pin 13: GPIO25 ◄──────────────────────── GPS PPS (alt; if not using 6) │
│ Pin 14: 5V (do NOT use would damage SD/GPS) │
│ Pin 15: GND ◄────── Y-cable ──────┬────► SD Breakout GND │
│ └────► GPS GND │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ J3 (right): Pin 4 = GPIO24 (TX) ◄────── GPS RXD (optional) │
│ Pin 5 = GPIO23 (RX) ◄────── GPS TXD (NMEA from GPS) │
└─────────────────────────────────────────────────────────────────────────┘
```
**Summary:** Use Y-cables (or equivalent) on **3.3V (Pin 1)** and **GND (Pin 15)** so the SD card and GPS each have their own lead from the ESP32-C5. Same 3.3V and GND rail, two physical wires per net.
## Quick Wiring Reference
```
SparkFun microSD Breakout → ESP32-C5 DevKit
1. VCC → J1 Pin 1 (3V3) [Left side, top; use Y-cable if also powering GPS]
2. GND → J1 Pin 15 (GND) [Left side, bottom; use Y-cable if also powering GPS]
3. SCK → J1 Pin 10 (GPIO9) SPI_CLK
4. DI → J1 Pin 11 (GPIO10) SPI_MOSI
5. DO → J1 Pin 9 (GPIO8) SPI_MISO
6. CS → J1 Pin 8 (GPIO7) SPI_CS
7. CD → J1 Pin 12 (GPIO26) Card Detect (optional)
```
## Card Detect (CD) Logic
- **LOW (0V):** Card is **Inserted** (default: `CONFIG_SD_CD_ACTIVE_LOW=y`)
- **HIGH (3.3V):** Card is **Removed** (pin floats high with pull-up)
If your breakout uses inverted logic (HIGH = inserted), run `idf.py menuconfig`**ESP32 iperf Configuration****SD Card** → uncheck **Card Detect: LOW = inserted**.
**Breakout doesnt support CD:** Some SparkFun-style breakouts have a CD pad or through-hole that is **not** connected to the slots card-detect switch (the slot may not have a CD pin). If wiring is correct but status always shows REMOVED (or CD never changes), disable CD: `idf.py menuconfig`**ESP32 iperf Configuration****SD Card** → set **Card Detect GPIO** to **-1**. The card will still mount and work; only insert/remove detection is disabled.
## Pin Locations on ESP32-C5 DevKit
### J1 (LEFT SIDE - Top to Bottom):
```
Pin 1: 3V3 ← VCC
Pin 2: RST (Reset - do not connect)
Pin 3: GPIO2 (Strapping - do not use)
Pin 4: GPIO3 (Strapping - do not use)
Pin 5: GPIO0
Pin 6: GPIO1 (GPS PPS - if used)
Pin 7: GPIO6
Pin 8: GPIO7 ← CS
Pin 9: GPIO8 ← MISO (DO)
Pin 10: GPIO9 ← CLK (SCK)
Pin 11: GPIO10 ← MOSI (DI)
Pin 12: GPIO26 ← CD (optional)
Pin 13: GPIO25 (GPS PPS - if used)
Pin 14: 5V (do not use - too high voltage)
Pin 15: GND ← GND
Pin 16: NC (No connection)
```
## Important Notes
⚠️ **Warnings:**
- **Use 3.3V ONLY** - Do NOT connect to 5V (Pin 14) - this will damage the microSD card!
- ESP32-C5 does not have SDMMC host; SPI mode is required
- CD is optional: LOW = inserted, HIGH = removed (with pull-up)
## Current Configuration
The firmware is configured for **SPI mode** using:
- CLK: GPIO9
- MOSI: GPIO10
- MISO: GPIO8
- CS: GPIO7
- CD: GPIO26 (optional)
## Testing
After wiring, the firmware will automatically detect the SD card on boot. You should see:
```
I (xxx) sd_card: Initializing SD card via SPI...
I (xxx) sd_card: SD card mounted successfully at /sdcard
I (xxx) sd_card: SD card ready: XX.XX MB total, XX.XX MB free
```
If the card is not detected, check:
1. All connections are secure
2. Card is properly inserted in the breakout board
3. Card is formatted (FAT32)
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
**Card not detected:**
- Verify VCC is connected to 3V3 (Pin 1), NOT 5V
- Check GND connection
- Ensure card is properly seated in the breakout board
- Try a different microSD card
**Initialization fails:**
- Card may need formatting (FAT32)
- Check for loose connections
- Verify GPIO pins are correct
- Some cards may not work in SPI mode - try a different brand
**File system errors:**
- Card may be corrupted - reformat as FAT32
- Check card capacity (very large cards may have issues)
- Ensure proper power supply
**CD always shows REMOVED but card works:**
- Your breakout or slot may not support Card Detect (CD pad may be unconnected). Disable CD: menuconfig → ESP32 iperf Configuration → SD Card → Card Detect GPIO = **-1**.

View File

@ -1,98 +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** (Option A) to capture while in monitor mode. Use **WiFi** (Option B) when the device is back on the network—e.g. after reboot, or after running `monitor stop` if you have serial access.
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 (no device interaction)
Use when the device is on the network (STA mode) and connected to the same LAN as the laptop. **No serial or device interaction required**—the device advertises itself via UDP broadcast on port 5555. Telemetry must already be on the SD card (from a previous monitor session, e.g. device ran monitor mode in the field, or rebooted back to STA).
**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

@ -249,12 +249,12 @@
<body> <body>
<header> <header>
<h1>ESP32-C5 GPS Synchronization Guide</h1> <h1>ESP32-C5 GPS Synchronization Guide</h1>
<div class="subtitle">Precision Timing for Wi-Fi Telemetry Capture with iperf2 Correlation</div> <div class="subtitle">Precision Timing for WiFi Collapse Detection with iperf2 Correlation</div>
</header> </header>
<div class="section"> <div class="section">
<h2>Overview</h2> <h2>Overview</h2>
<p>This guide demonstrates how to synchronize an ESP32-C5-DevKitC-1-N8R4 to GPS time using a GPS module with PPS (Pulse Per Second) output. This enables precise timestamp correlation between Wi-Fi telemetry capture events and iperf2 latency measurements running on a GPS-synced Raspberry Pi 5.</p> <p>This guide demonstrates how to synchronize an ESP32-C5-DevKitC-1-N8R4 to GPS time using a GPS module with PPS (Pulse Per Second) output. This enables precise timestamp correlation between WiFi collapse detector events and iperf2 latency measurements running on a GPS-synced Raspberry Pi 5.</p>
<div class="info-box"> <div class="info-box">
<strong>Key Features:</strong> <strong>Key Features:</strong>
@ -773,7 +773,7 @@ collapse_times = merged[merged['event'] == 'COLLAPSE']['timestamp']
ax2.scatter(collapse_times, [1]*len(collapse_times), color='r', marker='x', s=100, label='Collapse') ax2.scatter(collapse_times, [1]*len(collapse_times), color='r', marker='x', s=100, label='Collapse')
ax2.set_ylabel('Collapse Events', color='r') ax2.set_ylabel('Collapse Events', color='r')
plt.title('WiFi Latency vs Telemetry Capture Events') plt.title('WiFi Latency vs Collapse Detection Events')
plt.show()</code></pre> plt.show()</code></pre>
</div> </div>
@ -923,7 +923,7 @@ echo &quot;All devices flashed!&quot;</code></pre>
<footer> <footer>
<p><strong>ESP32-C5 GPS Synchronization Guide</strong></p> <p><strong>ESP32-C5 GPS Synchronization Guide</strong></p>
<p>For Wi-Fi Telemetry Capture and iperf2 Latency Correlation</p> <p>For WiFi Collapse Detection and iperf2 Latency Correlation</p>
<p>December 2025</p> <p>December 2025</p>
</footer> </footer>
</body> </body>

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)

View File

@ -45,37 +45,4 @@ 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"
config SD_CD_GPIO
int "Card Detect GPIO (-1 to disable)"
default 26 if IDF_TARGET_ESP32C5
default -1
help
GPIO for SD card detect. -1 to disable. SparkFun breakout on ESP32-C5: GPIO26.
config SD_CD_ACTIVE_LOW
bool "Card Detect: LOW = inserted"
default y
help
When set (default), LOW on CD pin means card inserted (SparkFun). When unset, HIGH means inserted.
endmenu
endmenu endmenu

View File

@ -46,14 +46,7 @@
#define HAS_RGB_LED 1 #define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_24 #define GPS_TX_PIN GPIO_NUM_24
#define GPS_RX_PIN GPIO_NUM_23 #define GPS_RX_PIN GPIO_NUM_23
#define GPS_PPS_PIN GPIO_NUM_25 // J1 Pin 13; PPS works here with SD on 7,8,9,10,26 #define GPS_PPS_PIN GPIO_NUM_25
#define GPS_PPS_ACTIVE_LOW 0 // rising-edge PPS (use 1 if pps-scan suggests falling > rising)
// SD SPI pins for SparkFun microSD Transflash Breakout (ESP32-C5 has no SDMMC host)
#define SDSPI_CLK_PIN GPIO_NUM_9
#define SDSPI_MOSI_PIN GPIO_NUM_10
#define SDSPI_MISO_PIN GPIO_NUM_8
#define SDSPI_CS_PIN GPIO_NUM_7
#define SD_CD_PIN GPIO_NUM_26 // Card Detect: LOW=inserted
#elif defined (CONFIG_IDF_TARGET_ESP32S3) #elif defined (CONFIG_IDF_TARGET_ESP32S3)
// ============================================================================ // ============================================================================
// ESP32-S3 (DevKitC-1) // ESP32-S3 (DevKitC-1)
@ -65,7 +58,6 @@
#define GPS_TX_PIN GPIO_NUM_5 #define GPS_TX_PIN GPIO_NUM_5
#define GPS_RX_PIN GPIO_NUM_4 #define GPS_RX_PIN GPIO_NUM_4
#define GPS_PPS_PIN GPIO_NUM_6 #define GPS_PPS_PIN GPIO_NUM_6
#define GPS_PPS_ACTIVE_LOW 1
#elif defined (CONFIG_IDF_TARGET_ESP32) #elif defined (CONFIG_IDF_TARGET_ESP32)
// ============================================================================ // ============================================================================
// ESP32 (Original / Standard) // ESP32 (Original / Standard)
@ -77,7 +69,6 @@
#define GPS_TX_PIN GPIO_NUM_17 #define GPS_TX_PIN GPIO_NUM_17
#define GPS_RX_PIN GPIO_NUM_16 #define GPS_RX_PIN GPIO_NUM_16
#define GPS_PPS_PIN GPIO_NUM_4 #define GPS_PPS_PIN GPIO_NUM_4
#define GPS_PPS_ACTIVE_LOW 1
#else #else
// Fallback // Fallback
#define RGB_LED_GPIO 8 #define RGB_LED_GPIO 8
@ -85,7 +76,6 @@
#define GPS_TX_PIN GPIO_NUM_1 #define GPS_TX_PIN GPIO_NUM_1
#define GPS_RX_PIN GPIO_NUM_3 #define GPS_RX_PIN GPIO_NUM_3
#define GPS_PPS_PIN GPIO_NUM_5 #define GPS_PPS_PIN GPIO_NUM_5
#define GPS_PPS_ACTIVE_LOW 1
#endif #endif
#endif // BOARD_CONFIG_H #endif // BOARD_CONFIG_H

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

@ -58,9 +58,6 @@
#include "app_console.h" #include "app_console.h"
#include "iperf.h" #include "iperf.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"
@ -171,7 +168,6 @@ void app_main(void) {
.tx_pin = GPS_TX_PIN, .tx_pin = GPS_TX_PIN,
.rx_pin = GPS_RX_PIN, .rx_pin = GPS_RX_PIN,
.pps_pin = GPS_PPS_PIN, .pps_pin = GPS_PPS_PIN,
.pps_active_low = GPS_PPS_ACTIVE_LOW,
}; };
gps_sync_init(&gps_cfg, true); gps_sync_init(&gps_cfg, true);
} else { } else {
@ -181,24 +177,6 @@ void app_main(void) {
// Hardware Init // Hardware Init
status_led_init(RGB_LED_GPIO, HAS_RGB_LED); status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
status_led_set_state(LED_STATE_FAILED); // Force Red Blink status_led_set_state(LED_STATE_FAILED); // Force Red Blink
// Initialize SD card (optional - will fail gracefully if not present)
esp_err_t sd_ret = sd_card_init();
if (sd_ret == ESP_OK) {
uint64_t total_bytes = 0, free_bytes = 0;
if (sd_card_get_info(&total_bytes, &free_bytes) == ESP_OK) {
ESP_LOGI(TAG, "SD card ready: %.2f MB total, %.2f MB free",
total_bytes / (1024.0 * 1024.0), free_bytes / (1024.0 * 1024.0));
}
} else {
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 +186,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,713 +0,0 @@
<?php
/**
* Umber Fi-Wi: Deterministic Scaling (v34 - Mu-MIMO Logic)
* © 2026 Umber Networks, Inc.
*/
// Handle telemetry POST requests (for future measured data)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$telemetryFile = __DIR__ . '/telemetry.json';
$data = json_decode(file_get_contents('php://input'), true);
if ($data && isset($data['device_id']) && isset($data['timestamp'])) {
$telemetry = [];
if (file_exists($telemetryFile)) {
$telemetry = json_decode(file_get_contents($telemetryFile), true) ?: [];
}
$deviceId = $data['device_id'];
if (!isset($telemetry[$deviceId])) {
$telemetry[$deviceId] = [];
}
$telemetry[$deviceId][] = array_merge($data, ['received_at' => time()]);
// Keep only last 1000 samples per device
if (count($telemetry[$deviceId]) > 1000) {
$telemetry[$deviceId] = array_slice($telemetry[$deviceId], -1000);
}
file_put_contents($telemetryFile, json_encode($telemetry, JSON_PRETTY_PRINT));
echo json_encode(['status' => 'success']);
exit;
}
echo json_encode(['status' => 'error', 'message' => 'Invalid data']);
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Umber Fi-Wi: Deterministic Scaling & Mu-MIMO</title>
<style>
:root {
--node-size: 14px;
--gap: 5px;
--fiwi-color: #00cc99; /* Green */
--auto-color: #ffaa00; /* Orange */
--mesh-color: #ff5500; /* Red-Orange */
--chaos-color: #ff3333; /* Red */
--wan-color: #cc66ff; /* Purple */
--mu-color: #0088ff; /* Blue for Mu-MIMO */
--bg-color: #0d0d0d;
--panel-bg: #151515;
--text-color: #eee;
}
body {
font-family: 'Segoe UI', monospace, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 { margin-top: 15px; font-size: 1.4rem; color: #fff; text-transform: uppercase; letter-spacing: 2px; border-bottom: 1px solid #333; padding-bottom: 5px; }
/* Global Stats */
.global-stats {
display: flex; gap: 20px; background: #000; padding: 15px 30px; border-radius: 8px; margin-bottom: 15px; border: 1px solid #333;
box-shadow: 0 4px 10px rgba(0,0,0,0.5); align-items: center; justify-content: center; width: 95%; max-width: 1000px;
}
.g-stat-item { display: flex; flex-direction: column; align-items: center; min-width: 120px; }
.g-label { font-size: 0.7rem; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 5px; }
.g-value { font-family: 'Consolas', monospace; font-size: 1.4rem; font-weight: bold; color: #fff; }
.g-sub { font-size: 0.65rem; margin-top: 2px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; }
.val-good { color: var(--fiwi-color); }
.val-mid { color: var(--auto-color); }
.val-bad { color: var(--chaos-color); }
.val-wan { color: var(--wan-color); text-shadow: 0 0 10px rgba(204,102,255,0.4); }
/* Scenarios Bar */
.scenario-bar { display: flex; gap: 10px; margin-bottom: 15px; }
.scene-btn {
background: #222; border: 1px solid #444; color: #aaa; padding: 5px 15px; border-radius: 4px; cursor: pointer; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s;
}
.scene-btn:hover { background: #333; color: #fff; }
.scene-btn:active { background: #444; }
/* Controls Layout */
.controls-wrapper { display: flex; gap: 15px; align-items: stretch; margin-bottom: 15px; flex-wrap: wrap; justify-content: center; }
.ctrl-box {
background: #222; padding: 8px 12px; border-radius: 8px; border: 1px solid #444;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
/* Algo Toggle */
.switch-container { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
.switch-label { font-weight: bold; font-size: 0.75rem; color: #666; }
.switch-label.active-chaos { color: var(--chaos-color); }
.switch-label.active-l4s { color: #00aaff; }
.toggle-checkbox {
position: relative; width: 40px; height: 20px; appearance: none; background: #000;
border-radius: 30px; cursor: pointer; border: 1px solid #444; outline: none;
}
.toggle-checkbox::after {
content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
border-radius: 50%; background: #666; transition: all 0.2s;
}
.toggle-checkbox:checked::after { left: 22px; background: #00aaff; }
.toggle-checkbox:not(:checked)::after { background: var(--chaos-color); }
/* Sliders */
.slider-group { width: 120px; display: flex; flex-direction: column; gap: 2px; }
.slider-label { display: flex; justify-content: space-between; font-size: 0.65rem; color: #aaa; }
.slider-val { font-weight: bold; color: #fff; font-size: 0.8rem; }
input[type=range] { width: 100%; cursor: pointer; accent-color: #00cc99; height: 4px; }
/* Topo Buttons */
.topo-btns { display: flex; gap: 5px; }
.mode-btn {
background: transparent; border: 1px solid transparent; color: #666; font-weight: bold; padding: 4px 10px; cursor: pointer;
border-radius: 20px; transition: all 0.3s; font-size: 0.75rem;
}
.mode-btn:hover { color: #fff; border-color: #444; }
.mode-btn.active-fiwi { background: var(--fiwi-color); color: #000; box-shadow: 0 0 10px var(--fiwi-color); }
.mode-btn.active-auto { background: var(--auto-color); color: #000; box-shadow: 0 0 10px var(--auto-color); }
.mode-btn.active-mesh { background: var(--mesh-color); color: #000; box-shadow: 0 0 10px var(--mesh-color); }
/* Grid */
.quad-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; max-width: 1200px; margin-bottom: 20px; }
.device-card {
background: var(--panel-bg); padding: 12px; border-radius: 8px; border: 1px solid #333;
display: flex; flex-direction: column; align-items: center; position: relative;
}
.hop-badge { position: absolute; top: 8px; right: 8px; background: #333; color: #aaa; font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; display: none; }
.device-header { width: 100%; display: flex; justify-content: space-between; margin-bottom: 8px; border-bottom: 1px solid #333; padding-bottom: 4px; }
.dev-title { font-weight: bold; color: #888; font-size: 0.85rem; }
.dev-status { font-family: 'Consolas', monospace; font-size: 0.7rem; font-weight: bold; letter-spacing: -0.5px; }
.grid-wrapper { position: relative; }
.mumimo-label { position: absolute; left: -18px; top: 20%; transform: rotate(-90deg); font-size: 0.55rem; color: var(--mu-color); letter-spacing: 1px; opacity: 0.5; }
.mimo-label { position: absolute; left: -12px; bottom: 20%; transform: rotate(-90deg); font-size: 0.55rem; color: #666; letter-spacing: 1px; }
.mini-grid-row { display: flex; align-items: center; margin-bottom: var(--gap); }
.mini-row-label { width: 25px; text-align: right; padding-right: 6px; font-size: 0.55rem; color: #555; }
.mini-node {
width: var(--node-size); height: var(--node-size); border-radius: 50%; background-color: #2a2a2a; margin-right: var(--gap);
border: 1px solid #333; transition: background-color 0.1s;
}
.mini-node.active.fiwi { background-color: var(--fiwi-color); box-shadow: 0 0 6px var(--fiwi-color); border-color: #fff; transform: scale(1.1); }
.mini-node.active.auto { background-color: var(--auto-color); box-shadow: 0 0 6px var(--auto-color); border-color: #fff; transform: scale(1.1); }
.mini-node.active.mesh { background-color: var(--mesh-color); box-shadow: 0 0 6px var(--mesh-color); border-color: #fff; transform: scale(1.1); }
.mini-node.active.chaos { background-color: var(--chaos-color); box-shadow: 0 0 8px var(--chaos-color); border-color: #fff; transform: scale(1.2); }
.mini-node.active.mumimo { background-color: var(--mu-color); box-shadow: 0 0 8px var(--mu-color); border-color: #fff; transform: scale(1.1); }
@keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 51, 51, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(255, 51, 51, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 51, 51, 0); } }
.mini-node.collision { animation: pulse-red 1s infinite; border-color: #ff3333 !important; background-color: #500 !important; }
.mini-node.ghost { border: 1px dashed #666; background-color: rgba(255,255,255,0.05); }
.axis-label { width: 100%; text-align: right; font-size: 0.55rem; color: #555; margin-top: 2px; letter-spacing: 1px; font-family: monospace; }
.dev-telemetry { margin-top: 8px; width: 100%; display: flex; justify-content: space-between; align-items: flex-end; font-size: 0.7rem; color: #666; font-family: monospace; border-top: 1px solid #333; padding-top: 5px; }
.math-row { width: 100%; display: flex; justify-content: space-between; font-size: 0.65rem; color: #555; font-family: 'Courier New', monospace; margin-top: 2px; }
.math-val { color: #888; font-weight: bold; }
.math-bad { color: var(--chaos-color); } .math-ok { color: var(--fiwi-color); }
.t-bad { color: var(--chaos-color); } .t-warn { color: var(--mesh-color); } .t-good { color: var(--fiwi-color); } .t-mid { color: var(--auto-color); } .t-wan { color: var(--wan-color); }
.footer { margin-top: 10px; font-size: 0.75rem; color: #666; padding-bottom: 20px; }
.footer a { color: var(--fiwi-color); text-decoration: none; border-bottom: 1px dotted var(--fiwi-color); }
</style>
</head>
<body>
<h1>Umber Fi-Wi: Deterministic Scaling & Mu-MIMO</h1>
<div class="scenario-bar">
<button class="scene-btn" onclick="setScenario('light')">Light Load</button>
<button class="scene-btn" onclick="setScenario('typical')">Typical Office</button>
<button class="scene-btn" onclick="setScenario('dense')">Dense (12 Cli)</button>
<button class="scene-btn" onclick="setScenario('stress')">Stress Test</button>
</div>
<div class="global-stats">
<div class="g-stat-item">
<span class="g-label">Throughput</span>
<span class="g-value" id="glob-tput">--</span>
<span class="g-sub" id="glob-bottleneck">--</span>
</div>
<div class="g-stat-item">
<span class="g-label">P99 Latency</span>
<span class="g-value" id="glob-lat">--</span>
</div>
<div class="g-stat-item">
<span class="g-label">Packet Error Rate</span>
<span class="g-value" id="glob-per">--</span>
</div>
<div class="g-stat-item" style="border-left:1px solid #333; padding-left:20px;">
<span class="g-label">Collision Prob</span>
<span class="g-value" id="glob-bday">--</span>
</div>
</div>
<div class="controls-wrapper">
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Active Rooms</span><span id="val-aps" class="slider-val">4</span></div>
<input type="range" id="aps-slider" min="1" max="4" step="1" value="4" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Total Clients</span><span id="val-clients" class="slider-val">12</span></div>
<input type="range" id="clients-slider" min="1" max="50" step="1" value="12" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Packet Pressure</span><span id="val-pps" class="slider-val">5000 PPS</span></div>
<input type="range" id="pps-slider" min="100" max="20000" step="100" value="5000" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="switch-container">
<span id="lbl-chaos" class="switch-label active-chaos">Greedy</span>
<input type="checkbox" id="algo-toggle" class="toggle-checkbox" onchange="updateSim()">
<span id="lbl-l4s" class="switch-label">L4S</span>
</div>
<div style="font-size:0.6rem; color:#666;">Rate Control</div>
</div>
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Aggregation (MPDU)</span><span id="val-ampdu" class="slider-val">12</span></div>
<input type="range" id="ampdu-slider" min="1" max="64" step="1" value="12" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="topo-btns">
<button class="mode-btn active-fiwi" onclick="setTopo('fiwi')" id="btn-fiwi">Fi-Wi</button>
<button class="mode-btn" onclick="setTopo('auto')" id="btn-auto">Auton AP</button>
<button class="mode-btn" onclick="setTopo('mesh')" id="btn-mesh">Mesh</button>
</div>
</div>
</div>
<div style="margin-bottom:10px; font-size: 0.85rem; color: #aaa; height: 18px;">
<span id="status-text">...</span>
</div>
<div class="quad-grid" id="quad-container"></div>
<div class="footer">
Reference: <a href="https://mcsindex.com/" target="_blank">MCS Index Table (80 MHz)</a> | Shared WAN Cap: 1000 Mbps
</div>
<script>
const CONFIG = { mcs_max: 11, ss_max: 4 };
let topo = 'fiwi';
let algo = 'greedy';
let ampdu = 12;
let globalPPS = 5000;
let totalClients = 12;
let apCount = 4;
let latSamples = [];
// Physics Constants
const PHYSICS = {
TX_PROB_CSMA: 0.05,
RX_TURNAROUND: 0.04,
WAN_CAP: 1000
};
const devices = [
{ id: 0, name: "Room A", state: {mcs:6, ss:2}, ghost: {mcs:9, ss:4}, pps: 0, hops: 0, clients: 0 },
{ id: 1, name: "Room B", state: {mcs:5, ss:2}, ghost: {mcs:8, ss:3}, pps: 0, hops: 1, clients: 0 },
{ id: 2, name: "Room C", state: {mcs:4, ss:1}, ghost: {mcs:7, ss:2}, pps: 0, hops: 2, clients: 0 },
{ id: 3, name: "Room D", state: {mcs:7, ss:2}, ghost: {mcs:10,ss:3}, pps: 0, hops: 3, clients: 0 }
];
// 802.11ac VHT MCS rates for 80 MHz, 800ns GI, 1 Spatial Stream (from mcsindex.com)
// MCS 0-9: Standard VHT rates
// MCS 10-11: Extended rates (may vary by implementation)
const PHY_RATES_1SS = [
32.5, // MCS 0: BPSK 1/2
65, // MCS 1: QPSK 1/2
97.5, // MCS 2: QPSK 3/4
130, // MCS 3: 16-QAM 1/2
195, // MCS 4: 16-QAM 3/4
260, // MCS 5: 64-QAM 2/3
292.5, // MCS 6: 64-QAM 3/4
325, // MCS 7: 64-QAM 5/6
390, // MCS 8: 256-QAM 3/4
433.3, // MCS 9: 256-QAM 5/6
433.3, // MCS 10: 256-QAM 5/6 (same as MCS 9 in some implementations)
520 // MCS 11: 256-QAM 5/6 with 400ns GI (if supported, else falls back)
];
const PRESETS = {
'light': { aps: 2, clients: 4, pps: 1000, ampdu: 32 },
'typical': { aps: 4, clients: 8, pps: 3000, ampdu: 24 },
'dense': { aps: 4, clients: 12, pps: 5000, ampdu: 12 },
'stress': { aps: 4, clients: 40, pps: 15000, ampdu: 4 }
};
function init() {
const container = document.getElementById('quad-container');
container.innerHTML = '';
devices.forEach(dev => {
const card = document.createElement('div');
card.className = 'device-card';
card.id = `card-${dev.id}`;
card.innerHTML = `
<div class="hop-badge" id="hop-${dev.id}">Inactive</div>
<div class="device-header">
<span class="dev-title">${dev.name}</span>
<span class="dev-status" id="status-${dev.id}">INIT</span>
</div>
<div class="grid-wrapper">
<div class="mumimo-label">Mu-MIMO</div>
<div class="mimo-label">2x2 Client</div>
<div id="grid-${dev.id}"></div>
</div>
<div class="axis-label">MCS Index (0 - 11) &rarr;</div>
<div style="width:100%; border-top:1px solid #333; margin-top:5px; padding-top:2px;">
<div class="math-row">
<span>Eigenvalues:</span>
<span>&lambda;<sub>1</sub>:<span class="math-val" id="eig1-${dev.id}">1.0</span> &nbsp; &lambda;<sub>2</sub>:<span class="math-val" id="eig2-${dev.id}">0.5</span></span>
</div>
<div class="math-row">
<span>Condition (&kappa;):</span>
<span class="math-val" id="cond-${dev.id}">3.0 dB</span>
</div>
</div>
<div class="dev-telemetry">
<div style="display:flex; flex-direction:column;">
<span id="tput-${dev.id}" style="font-weight:bold;">0 Mbps</span>
<span id="state-${dev.id}" style="font-size:0.65rem; color:#666;">MCS -- / -- SS</span>
</div>
<span id="lat-${dev.id}">Lat: --</span>
</div>
`;
container.appendChild(card);
const gridBox = card.querySelector(`#grid-${dev.id}`);
// Render 4 Rows (4 SS down to 1 SS)
for (let ss = CONFIG.ss_max; ss >= 1; ss--) {
const row = document.createElement('div');
row.className = 'mini-grid-row';
const lbl = document.createElement('div');
lbl.className = 'mini-row-label'; lbl.innerText = `${ss}SS`;
row.appendChild(lbl);
for (let mcs = 0; mcs <= CONFIG.mcs_max; mcs++) {
const n = document.createElement('div');
n.className = 'mini-node'; n.id = `d${dev.id}-n-${ss}-${mcs}`;
row.appendChild(n);
}
gridBox.appendChild(row);
}
});
updateSliders();
updateSim();
setInterval(loop, 100);
}
function setScenario(name) {
const s = PRESETS[name];
if(!s) return;
document.getElementById('aps-slider').value = s.aps;
document.getElementById('clients-slider').value = s.clients;
document.getElementById('pps-slider').value = s.pps;
document.getElementById('ampdu-slider').value = s.ampdu;
updateSliders();
}
function updateSliders() {
ampdu = parseInt(document.getElementById('ampdu-slider').value);
document.getElementById('val-ampdu').innerText = ampdu;
globalPPS = parseInt(document.getElementById('pps-slider').value);
document.getElementById('val-pps').innerText = globalPPS + " PPS";
totalClients = parseInt(document.getElementById('clients-slider').value);
document.getElementById('val-clients').innerText = totalClients;
apCount = parseInt(document.getElementById('aps-slider').value);
document.getElementById('val-aps').innerText = apCount;
let base = Math.floor(totalClients / apCount);
let remainder = totalClients % apCount;
devices.forEach((dev, idx) => {
const card = document.getElementById(`card-${dev.id}`);
if (idx < apCount) {
card.style.opacity = '1';
card.style.filter = 'none';
dev.clients = base + (idx < remainder ? 1 : 0);
} else {
card.style.opacity = '0.3';
card.style.filter = 'grayscale(100%)';
dev.clients = 0;
}
});
}
function setTopo(t) { topo = t; updateSim(); }
function updateSim() {
const isL4S = document.getElementById('algo-toggle').checked;
algo = isL4S ? 'l4s' : 'greedy';
document.getElementById('lbl-chaos').className = !isL4S ? 'switch-label active-chaos' : 'switch-label';
document.getElementById('lbl-l4s').className = isL4S ? 'switch-label active-l4s' : 'switch-label';
['fiwi', 'auto', 'mesh'].forEach(m => {
const btn = document.getElementById(`btn-${m}`);
if (m === topo) btn.classList.add(`active-${m}`);
else btn.classList.remove(`active-fiwi`, `active-auto`, `active-mesh`);
});
devices.forEach((d, idx) => {
const badge = document.getElementById(`hop-${d.id}`);
if (idx >= apCount) { badge.style.display = 'none'; return; }
badge.style.display = 'block';
let role = (topo === 'fiwi') ? "Radio Head" : ((topo === 'auto') ? "AP" : "Node");
if(topo === 'mesh' && d.hops === 0) role = "Gateway";
badge.innerText = `${role} (${d.clients} Cli)`;
});
latSamples = [];
const txt = document.getElementById('status-text');
if (topo === 'fiwi') {
txt.innerText = (algo==='l4s')
? "Fi-Wi L4S: Deterministic scheduling enables stable Mu-MIMO (3-4 SS)."
: "Fi-Wi Greedy: Centralized control, but queues fill.";
} else if (topo === 'auto') {
txt.innerText = (algo==='l4s')
? "Autonomous L4S: Drift hampers Mu-MIMO coordination."
: "Autonomous Greedy: CSMA CHAOS. Collisions kill Mu-MIMO gains.";
} else if (topo === 'mesh') {
txt.innerText = "Mesh: Wireless Backhaul + Multi-Client = Latency Wall.";
}
}
function loop() {
let globalFailures = 0;
let globalPotentialTput = 0;
let bottleneck = "AIRTIME";
let globalBirthdayProb = 0;
let activeClientTotal = 0;
devices.forEach((d, idx) => {
if (idx < apCount && d.clients > 0) activeClientTotal += d.clients;
});
devices.forEach((d, idx) => {
if (idx < apCount && d.clients > 0) {
d.pps = (globalPPS * (d.clients / activeClientTotal));
} else { d.pps = 0; }
});
const results = devices.map((dev, idx) => {
if (idx >= apCount) return null;
return calculateDevicePhysics(dev);
});
results.forEach(res => { if(res) globalPotentialTput += res.airTput; });
let wanFactor = 1.0;
if (globalPotentialTput > PHYSICS.WAN_CAP) {
wanFactor = PHYSICS.WAN_CAP / globalPotentialTput;
bottleneck = "WAN LINK";
}
results.forEach((res, index) => {
if (!res) return;
const dev = devices[index];
globalFailures += res.per * dev.pps;
if (dev.clients > 0) {
latSamples.push(res.lat);
latSamples.push(res.lat);
}
if(res.bottleneck !== "AIRTIME") bottleneck = res.bottleneck;
if(res.birthdayProb > globalBirthdayProb) globalBirthdayProb = res.birthdayProb;
const finalTput = res.airTput * wanFactor;
updateDeviceUI(dev, res, finalTput, wanFactor < 1.0);
});
if (latSamples.length > 200) latSamples = latSamples.slice(-200);
const totalPPS = devices.reduce((sum, d) => sum + d.pps, 0);
const avgPer = (totalPPS > 0) ? (globalFailures / totalPPS) * 100 : 0;
const elPer = document.getElementById('glob-per');
elPer.innerText = avgPer.toFixed(2) + "%";
elPer.className = avgPer < 0.2 ? "g-value val-good" : (avgPer < 4 ? "g-value val-mid" : "g-value val-bad");
latSamples.sort((a,b) => a - b);
const p99Index = Math.floor(latSamples.length * 0.99);
const p99 = latSamples.length > 0 ? latSamples[p99Index] : 0;
const elLat = document.getElementById('glob-lat');
elLat.innerText = Math.round(p99) + "ms";
elLat.className = p99 < 8 ? "g-value val-good" : (p99 < 60 ? "g-value val-mid" : "g-value val-bad");
const elTput = document.getElementById('glob-tput');
const totalActualTput = Math.min(globalPotentialTput, PHYSICS.WAN_CAP);
if (totalActualTput > 1000) elTput.innerText = (totalActualTput/1000).toFixed(1) + " Gbps";
else elTput.innerText = Math.round(totalActualTput) + " Mbps";
const elBN = document.getElementById('glob-bottleneck');
elBN.innerText = "LIMIT: " + bottleneck;
if(bottleneck === "WAN LINK") { elTput.className = "g-value val-wan"; elBN.style.color = "#cc66ff"; }
else if(bottleneck === "AIRTIME") { elTput.className = "g-value val-mid"; elBN.style.color = "#ffaa00"; }
else { elTput.className = "g-value val-bad"; elBN.style.color = "#ff3333"; }
const elBday = document.getElementById('glob-bday');
elBday.innerText = (globalBirthdayProb * 100).toFixed(1) + "%";
elBday.className = globalBirthdayProb < 0.05 ? "g-value val-good" : (globalBirthdayProb < 0.3 ? "g-value val-mid" : "g-value val-bad");
}
function calculateDevicePhysics(dev) {
if (dev.clients === 0) {
return {
airTput: 0, per: 0, lat: 0, birthdayProb: 0,
visualClass: '', statusText: 'IDLE', statusClass: 'dev-status', bottleneck: 'NONE',
e1: 0, e2: 0, kappa: 0, ss: 0, mcs: 0
};
}
// Client Hardware Cap
let clientCap = (dev.clients > 1) ? 4 : 2; // MuMIMO requires >1 client
if (topo === 'mesh' && dev.hops > 0) clientCap = 2; // Mesh backhaul limit
let birthdayProb = 0;
let collisionPenalty = 0;
let latencyAdd = 0;
// --- PHYSICS: The Birthday Paradox ---
if (topo === 'fiwi') {
birthdayProb = 0.001 * dev.clients;
latencyAdd = 0.5 * dev.clients;
} else {
let n = dev.clients;
if (n < 2) birthdayProb = 0.01;
else birthdayProb = 1 - Math.pow(1 - PHYSICS.TX_PROB_CSMA, n * (n - 1));
if (apCount > 1) birthdayProb += 0.1 * (apCount - 1);
if(birthdayProb > 0.98) birthdayProb = 0.98;
collisionPenalty = birthdayProb;
latencyAdd = 5 * Math.pow(n, 1.5);
}
// Matrix Physics
let e1 = 1.0, e2 = 0.5, kappa = 6.0;
if (topo === 'fiwi') {
e1 = 0.95 + Math.random()*0.05; e2 = 0.6 + Math.random()*0.1;
kappa = 20 * Math.log10(e1/e2);
} else {
e1 = 0.9 + Math.random()*0.1;
let collapse = Math.max(0.001, 1.0 - birthdayProb);
e2 = 0.5 * collapse + (Math.random()*0.02);
kappa = 20 * Math.log10(e1/e2);
}
// Ghost Physics (Active Variation)
// Ghost can float up to Client Cap
if(Math.random() > 0.9) dev.ghost.ss = Math.min(clientCap, dev.ghost.ss + 1);
else if(Math.random() < 0.1) dev.ghost.ss = Math.max(1, dev.ghost.ss - 1);
if(Math.random() > 0.8) dev.ghost.mcs = Math.min(11, Math.max(4, dev.ghost.mcs + (Math.random()>0.5?1:-1)));
let per = 0;
let lat = 0;
let visualClass = '';
let statusText = '';
let statusClass = '';
let bottleneck = "AIRTIME";
const load = dev.pps / 10000;
// --- ALGORITHM LOGIC ---
if (algo === 'l4s') {
// L4S: Conservative SS
const safeSS = Math.min(clientCap, dev.ghost.ss); // Respect hardware cap
if (dev.state.ss > safeSS) dev.state.ss--;
else if (dev.state.ss < safeSS) dev.state.ss++;
const targetMcs = Math.max(0, dev.ghost.mcs - 2);
if (dev.state.mcs < targetMcs) dev.state.mcs++;
else if (dev.state.mcs > targetMcs) dev.state.mcs--;
if (topo === 'fiwi') {
per = 0.001; lat = 2 + latencyAdd;
visualClass = 'active fiwi';
if (dev.state.ss > 2) visualClass += ' mumimo';
statusText = `SCHEDULED (${dev.clients})`; statusClass = "dev-status t-good";
} else {
per = 0.01 + birthdayProb * 0.2; lat = 10 + latencyAdd;
visualClass = (topo==='mesh') ? 'active mesh' : 'active auto';
statusText = (topo==='mesh') ? "RELAYING" : `DRIFTING (${(birthdayProb*100).toFixed(0)}%)`;
statusClass = "dev-status t-mid";
}
} else {
// GREEDY: Aggressive SS
let targetSS = Math.min(clientCap, dev.ghost.ss); // Even Greedy respects physics
if (dev.state.ss < targetSS) dev.state.ss++;
else if (dev.state.ss > targetSS) dev.state.ss--;
if (dev.state.mcs < dev.ghost.mcs) {
if (Math.random() < 0.6) dev.state.mcs++;
} else if (dev.state.mcs > dev.ghost.mcs) {
dev.state.mcs = dev.ghost.mcs - 1; per = 0.5;
} else per = 0.1;
if (topo === 'fiwi') {
per = Math.min(per, 0.05); lat = 20 + latencyAdd;
visualClass = 'active fiwi';
if (dev.state.ss > 2) visualClass += ' mumimo';
statusText = "BUFFERBLOAT"; statusClass = "dev-status t-warn";
} else {
per = Math.max(per, 0.20 + birthdayProb); lat = 30 + latencyAdd * 3;
visualClass = 'active chaos';
if(birthdayProb > 0.3) visualClass += ' collision';
statusText = `COLLISION (${(birthdayProb*100).toFixed(0)}%)`;
statusClass = "dev-status t-bad";
}
}
let rawPhyRate = PHY_RATES_1SS[dev.state.mcs] * dev.state.ss;
let aggEff = Math.min(0.95, 0.3 + (0.65 * Math.log(ampdu + 1) / Math.log(65)));
let meshPenalty = (topo === 'mesh' && dev.hops > 0) ? Math.pow(0.5, dev.hops) : 1.0;
let safeCollisionPenalty = Math.min(0.99, Math.max(0, collisionPenalty));
let airTput = rawPhyRate * aggEff * (1 - per) * meshPenalty * (1 - safeCollisionPenalty);
if (airTput < 0) airTput = 0;
if (birthdayProb > 0.5) bottleneck = "BDAY PARADOX";
else if (per > 0.1) bottleneck = "RE-TX / NOISE";
return {
airTput: airTput, per: per, lat: lat, birthdayProb: birthdayProb,
visualClass: visualClass, statusText: statusText, statusClass: statusClass, bottleneck: bottleneck,
ss: dev.state.ss, mcs: dev.state.mcs, e1: e1, e2: e2, kappa: kappa
};
}
function updateDeviceUI(dev, res, finalTput, isWanLimited) {
document.getElementById(`eig1-${dev.id}`).innerText = res ? res.e1.toFixed(2) : "0.00";
document.getElementById(`eig2-${dev.id}`).innerText = res ? res.e2.toFixed(3) : "0.00";
const kEl = document.getElementById(`cond-${dev.id}`);
if(res) {
kEl.innerText = res.kappa.toFixed(1) + " dB";
if (res.kappa > 12) kEl.className = "math-val math-bad"; else kEl.className = "math-val math-ok";
} else { kEl.innerText = "--"; }
if (dev.clients === 0) {
document.getElementById(`status-${dev.id}`).innerText = "IDLE";
document.getElementById(`status-${dev.id}`).className = "dev-status";
document.getElementById(`lat-${dev.id}`).innerText = "--";
document.getElementById(`tput-${dev.id}`).innerText = "0 Mbps";
document.getElementById(`state-${dev.id}`).innerText = "Inactive";
for(let ss=1; ss<=4; ss++){ for(let m=0; m<=11; m++){ const n = document.getElementById(`d${dev.id}-n-${ss}-${m}`); if(n) n.className = 'mini-node'; } }
return;
}
for(let ss=1; ss<=4; ss++){
for(let m=0; m<=11; m++){
const n = document.getElementById(`d${dev.id}-n-${ss}-${m}`);
if(n) n.className = 'mini-node';
}
}
const g = document.getElementById(`d${dev.id}-n-${dev.ghost.ss}-${dev.ghost.mcs}`);
if(g) g.classList.add('ghost');
const a = document.getElementById(`d${dev.id}-n-${dev.state.ss}-${dev.state.mcs}`);
if(a) a.className = `mini-node ${res.visualClass}`;
const statEl = document.getElementById(`status-${dev.id}`);
const latEl = document.getElementById(`lat-${dev.id}`);
const tputEl = document.getElementById(`tput-${dev.id}`);
const stateEl = document.getElementById(`state-${dev.id}`);
let ssLabel = (res.ss > 2) ? `${res.ss} SS (Mu)` : `${res.ss} SS`;
stateEl.innerText = `MCS ${res.mcs} / ${ssLabel}`;
if (isWanLimited && res.per < 0.1) {
statEl.innerText = "WAN LIMITED"; statEl.className = "dev-status val-wan";
} else {
statEl.innerText = res.statusText; statEl.className = res.statusClass;
}
latEl.innerText = `Lat: ${Math.round(res.lat)}ms`;
if (res.lat < 10) latEl.className = "t-good"; else if (res.lat < 50) latEl.className = "t-mid"; else latEl.className = "t-bad";
tputEl.innerText = Math.round(finalTput) + " Mbps";
if (isWanLimited) tputEl.className = "t-wan"; else tputEl.className = (finalTput > 500) ? "t-good" : "t-mid";
}
init();
</script>
</body>
</html>

View File

@ -1,101 +0,0 @@
#!/bin/bash
# Raspberry Pi 5 WiFi Monitor Mode - Capture RA/TA Addresses
# This script sets up monitor mode and captures 802.11 frames showing RA and TA
set -e
WIFI_INTERFACE="wlan0"
CHANNEL="${1:-11}" # Default to channel 11, or pass as argument
FILTER_MAC="${2:-}" # Optional: filter by MAC address (e.g., 80:84:89:93:c4:b6)
echo "=== Raspberry Pi 5 WiFi Monitor - RA/TA Capture ==="
echo "Interface: $WIFI_INTERFACE"
echo "Channel: $CHANNEL"
if [ -n "$FILTER_MAC" ]; then
echo "Filter: $FILTER_MAC"
fi
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root (use sudo)"
exit 1
fi
# Check if interface exists
if ! ip link show "$WIFI_INTERFACE" &>/dev/null; then
echo "Error: Interface $WIFI_INTERFACE not found"
exit 1
fi
# Check current mode
CURRENT_MODE=$(iw dev "$WIFI_INTERFACE" info 2>/dev/null | grep "type" | awk '{print $2}' || echo "unknown")
if [ "$CURRENT_MODE" != "monitor" ]; then
echo "Setting $WIFI_INTERFACE to monitor mode..."
ip link set "$WIFI_INTERFACE" down
iw dev "$WIFI_INTERFACE" set type monitor
ip link set "$WIFI_INTERFACE" up
iw dev "$WIFI_INTERFACE" set channel "$CHANNEL"
echo "Monitor mode activated on channel $CHANNEL"
else
echo "Already in monitor mode, setting channel to $CHANNEL..."
iw dev "$WIFI_INTERFACE" set channel "$CHANNEL"
fi
echo ""
echo "=== Starting Capture (showing RA/TA addresses) ==="
echo "Press Ctrl+C to stop"
echo ""
# Build tcpdump filter
TCPDUMP_FILTER=""
if [ -n "$FILTER_MAC" ]; then
# Remove colons from MAC for tcpdump
MAC_CLEAN=$(echo "$FILTER_MAC" | tr -d ':')
TCPDUMP_FILTER="ether host $FILTER_MAC"
fi
# Use tcpdump with verbose output to show MAC addresses
# -e shows link-level headers (includes MAC addresses)
# -n prevents DNS resolution
# -v increases verbosity
if [ -n "$TCPDUMP_FILTER" ]; then
tcpdump -i "$WIFI_INTERFACE" -e -n -v "$TCPDUMP_FILTER" 2>&1 | \
grep -E "(ether|RA|TA|SA|DA|BSSID)" | \
awk '
{
# Extract MAC addresses from tcpdump output
# tcpdump shows: ether src/dst MAC
if (match($0, /ether (src|dst) ([0-9a-f:]{17})/, arr)) {
direction = arr[1]
mac = arr[2]
print "[" direction "] " mac
}
# Also show full frame info
print $0
}'
else
# Show all frames with MAC address extraction
tcpdump -i "$WIFI_INTERFACE" -e -n -v 2>&1 | \
while IFS= read -r line; do
# Extract and highlight MAC addresses
if echo "$line" | grep -q "ether"; then
# Extract source MAC (TA in 802.11)
if echo "$line" | grep -q "ether src"; then
TA=$(echo "$line" | grep -oP 'ether src \K[0-9a-f:]{17}' || echo "")
if [ -n "$TA" ]; then
echo "TA (Transmitter): $TA"
fi
fi
# Extract destination MAC (RA in 802.11)
if echo "$line" | grep -q "ether dst"; then
RA=$(echo "$line" | grep -oP 'ether dst \K[0-9a-f:]{17}' || echo "")
if [ -n "$RA" ]; then
echo "RA (Receiver): $RA"
fi
fi
fi
echo "$line"
done
fi

View File

@ -1,307 +0,0 @@
#!/usr/bin/env python3
"""
Raspberry Pi 5 WiFi Monitor - Capture RA/TA Addresses
Uses scapy to parse 802.11 frames and display RA/TA addresses clearly
Requirements:
sudo apt-get install python3-pip
sudo pip3 install scapy
Usage:
sudo python3 rpi_capture_ra_ta_python.py [channel] [filter_mac]
Example:
sudo python3 rpi_capture_ra_ta_python.py 11
sudo python3 rpi_capture_ra_ta_python.py 11 80:84:89:93:c4:b6
"""
import sys
import os
import subprocess
import signal
from datetime import datetime
from scapy.all import *
from scapy.layers.dot11 import Dot11, Dot11QoS
# Configuration
WIFI_INTERFACE = "wlan0"
CHANNEL = int(sys.argv[1]) if len(sys.argv) > 1 else 11
FILTER_MAC = sys.argv[2] if len(sys.argv) > 2 else None
def setup_monitor_mode():
"""Set WiFi interface to monitor mode"""
print(f"=== Setting up monitor mode on {WIFI_INTERFACE} ===")
# Check current mode
try:
result = subprocess.run(
["iw", "dev", WIFI_INTERFACE, "info"],
capture_output=True,
text=True,
check=True
)
if "type monitor" in result.stdout:
print(f"Already in monitor mode")
else:
print(f"Setting {WIFI_INTERFACE} to monitor mode...")
subprocess.run(["ip", "link", "set", WIFI_INTERFACE, "down"], check=True)
subprocess.run(["iw", "dev", WIFI_INTERFACE, "set", "type", "monitor"], check=True)
subprocess.run(["ip", "link", "set", WIFI_INTERFACE, "up"], check=True)
print("Monitor mode activated")
except subprocess.CalledProcessError as e:
print(f"Error setting monitor mode: {e}")
sys.exit(1)
# Set channel
try:
subprocess.run(["iw", "dev", WIFI_INTERFACE, "set", "channel", str(CHANNEL)], check=True)
print(f"Channel set to {CHANNEL}")
except subprocess.CalledProcessError as e:
print(f"Error setting channel: {e}")
sys.exit(1)
def get_frame_type_name(dot11):
"""Get human-readable frame type name"""
type_names = {
0: { # Management
0: "Association Request",
1: "Association Response",
2: "Reassociation Request",
3: "Reassociation Response",
4: "Probe Request",
5: "Probe Response",
8: "Beacon",
10: "Disassociation",
11: "Authentication",
12: "Deauthentication"
},
1: { # Control
10: "RTS",
11: "CTS",
12: "ACK",
13: "CF-End",
14: "CF-End+CF-Ack"
},
2: { # Data
0: "Data",
1: "Data+CF-Ack",
2: "Data+CF-Poll",
3: "Data+CF-Ack+CF-Poll",
4: "Null",
8: "QoS Data",
9: "QoS Data+CF-Ack",
10: "QoS Data+CF-Poll",
11: "QoS Data+CF-Ack+CF-Poll",
12: "QoS Null"
}
}
return type_names.get(dot11.type, {}).get(dot11.subtype,
f"Type{dot11.type}/Subtype{dot11.subtype}")
def parse_80211_frame(pkt):
"""Parse 802.11 frame and extract RA/TA addresses"""
if not pkt.haslayer(Dot11):
return None
dot11 = pkt[Dot11]
# 802.11 addressing:
# For Data frames (To DS=1, From DS=1):
# addr1 = RA (Receiver Address) = Next hop destination
# addr2 = TA (Transmitter Address) = Transmitting station
# addr3 = DA (Destination Address) = Final destination
# addr4 = SA (Source Address) = Original source
# For Data frames (To DS=0, From DS=1): AP to STA
# addr1 = RA = DA (Destination STA)
# addr2 = TA = SA (Source AP)
# addr3 = BSSID
# For Data frames (To DS=1, From DS=0): STA to AP
# addr1 = RA = BSSID (AP)
# addr2 = TA = SA (Source STA)
# addr3 = DA (Destination)
frame_type = dot11.type
frame_subtype = dot11.subtype
# Get addresses
addr1 = dot11.addr1 if dot11.addr1 else "N/A"
addr2 = dot11.addr2 if dot11.addr2 else "N/A"
addr3 = dot11.addr3 if dot11.addr3 else "N/A"
addr4 = dot11.addr4 if hasattr(dot11, 'addr4') and dot11.addr4 else None
# Extract To DS and From DS flags
to_ds = dot11.FCfield & 0x1
from_ds = (dot11.FCfield >> 1) & 0x1
# Determine RA and TA based on frame type
if frame_type == 2: # Data frame
# For data frames:
# addr1 is always RA (receiver)
# addr2 is always TA (transmitter)
ra = addr1
ta = addr2
# Additional context based on To DS / From DS
if to_ds and from_ds:
# WDS frame: addr3=DA, addr4=SA
context = f"WDS: DA={addr3}, SA={addr4 if addr4 else 'N/A'}"
elif to_ds and not from_ds:
# STA to AP: addr3=DA
context = f"STA→AP: DA={addr3}"
elif not to_ds and from_ds:
# AP to STA: addr3=BSSID
context = f"AP→STA: BSSID={addr3}"
else:
# Ad-hoc: addr3=BSSID
context = f"Ad-hoc: BSSID={addr3}"
else:
# For management/control frames, addr1=DA, addr2=SA
ra = addr1 # Receiver/Destination
ta = addr2 # Transmitter/Source
context = f"BSSID={addr3}" if addr3 != "N/A" else ""
# Get RSSI if available
rssi = "N/A"
if hasattr(pkt, "dBm_AntSignal"):
rssi = f"{pkt.dBm_AntSignal} dBm"
elif hasattr(pkt, "notdecoded"):
# Try to extract from radiotap header if present
pass
# Check for QoS data
is_qos = pkt.haslayer(Dot11QoS)
qos_info = ""
if is_qos:
qos = pkt[Dot11QoS]
tid = qos.TID if hasattr(qos, 'TID') else "N/A"
qos_info = f", TID={tid}"
return {
"type": frame_type,
"subtype": frame_subtype,
"name": get_frame_type_name(dot11),
"ra": ra,
"ta": ta,
"bssid": addr3,
"context": context,
"rssi": rssi,
"len": len(pkt),
"qos": is_qos,
"qos_info": qos_info,
"retry": bool(dot11.FCfield & 0x8) if hasattr(dot11, 'FCfield') else False
}
# Statistics
stats = {
"total": 0,
"by_ta": {},
"by_ra": {},
"by_type": {}
}
def packet_handler(pkt):
"""Handle captured packets"""
frame_info = parse_80211_frame(pkt)
if not frame_info:
return
# Apply MAC filter if specified
if FILTER_MAC:
filter_mac_clean = FILTER_MAC.lower().replace(":", "").replace("-", "")
ta_clean = frame_info["ta"].replace(":", "").replace("-", "").lower() if frame_info["ta"] != "N/A" else ""
ra_clean = frame_info["ra"].replace(":", "").replace("-", "").lower() if frame_info["ra"] != "N/A" else ""
if filter_mac_clean.lower() not in ta_clean and filter_mac_clean.lower() not in ra_clean:
return # Skip this frame
# Update statistics
stats["total"] += 1
if frame_info["ta"] != "N/A":
stats["by_ta"][frame_info["ta"]] = stats["by_ta"].get(frame_info["ta"], 0) + 1
if frame_info["ra"] != "N/A":
stats["by_ra"][frame_info["ra"]] = stats["by_ra"].get(frame_info["ra"], 0) + 1
frame_type_key = f"{frame_info['name']}"
stats["by_type"][frame_type_key] = stats["by_type"].get(frame_type_key, 0) + 1
# Print frame information
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
retry_str = " [RETRY]" if frame_info["retry"] else ""
print(f"\n[{timestamp}] {frame_info['name']}{retry_str}")
print(f" RA (Receiver): {frame_info['ra']}")
print(f" TA (Transmitter): {frame_info['ta']}")
if frame_info['bssid'] != "N/A":
print(f" BSSID: {frame_info['bssid']}")
if frame_info['context']:
print(f" Context: {frame_info['context']}")
print(f" RSSI: {frame_info['rssi']}")
print(f" Length: {frame_info['len']} bytes{frame_info['qos_info']}")
def signal_handler(sig, frame):
"""Handle Ctrl+C gracefully"""
print("\n\nStopping capture...")
sys.exit(0)
def main():
"""Main function"""
print(f"=== Raspberry Pi 5 WiFi Monitor - RA/TA Capture ===")
print(f"Interface: {WIFI_INTERFACE}")
print(f"Channel: {CHANNEL}")
if FILTER_MAC:
print(f"Filter: {FILTER_MAC}")
print("")
# Check if running as root
if os.geteuid() != 0:
print("Error: This script must be run as root (use sudo)")
sys.exit(1)
# Setup monitor mode
setup_monitor_mode()
# Register signal handler
signal.signal(signal.SIGINT, signal_handler)
print("\n=== Starting Capture (showing RA/TA addresses) ===")
print("Press Ctrl+C to stop\n")
# Build filter
bpf_filter = None
if FILTER_MAC:
bpf_filter = f"ether host {FILTER_MAC}"
# Start capturing
try:
sniff(
iface=WIFI_INTERFACE,
prn=packet_handler,
filter=bpf_filter,
store=False
)
except KeyboardInterrupt:
print("\n\n" + "="*60)
print("Capture stopped by user")
print("="*60)
print(f"\nStatistics:")
print(f" Total frames captured: {stats['total']}")
print(f"\n Top 5 TAs (Transmitters):")
sorted_tas = sorted(stats['by_ta'].items(), key=lambda x: x[1], reverse=True)[:5]
for ta, count in sorted_tas:
print(f" {ta}: {count} frames")
print(f"\n Top 5 RAs (Receivers):")
sorted_ras = sorted(stats['by_ra'].items(), key=lambda x: x[1], reverse=True)[:5]
for ra, count in sorted_ras:
print(f" {ra}: {count} frames")
print(f"\n Frame types:")
for ftype, count in sorted(stats['by_type'].items(), key=lambda x: x[1], reverse=True):
print(f" {ftype}: {count}")
except Exception as e:
print(f"\nError during capture: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,73 +0,0 @@
#!/bin/bash
# Raspberry Pi 5 WiFi Monitor Mode Setup Script
# Run this script with sudo on your Raspberry Pi
set -e
# Configuration
WIFI_INTERFACE="wlan0" # Change if your interface is different
MONITOR_INTERFACE="mon0" # Monitor mode interface name
CHANNEL="${1:-11}" # Default to channel 11, or pass as argument: ./rpi_monitor_setup.sh 36
echo "=== Raspberry Pi 5 WiFi Monitor Mode Setup ==="
echo "Interface: $WIFI_INTERFACE"
echo "Monitor interface: $MONITOR_INTERFACE"
echo "Channel: $CHANNEL"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root (use sudo)"
exit 1
fi
# Check if interface exists
if ! ip link show "$WIFI_INTERFACE" &>/dev/null; then
echo "Error: Interface $WIFI_INTERFACE not found"
echo "Available interfaces:"
ip link show | grep -E "^[0-9]+:" | awk '{print $2}' | sed 's/://'
exit 1
fi
# Check if monitor interface already exists
if ip link show "$MONITOR_INTERFACE" &>/dev/null; then
echo "Monitor interface $MONITOR_INTERFACE already exists. Removing..."
iw dev "$MONITOR_INTERFACE" del
fi
# Bring down the interface
echo "Bringing down $WIFI_INTERFACE..."
ip link set "$WIFI_INTERFACE" down
# Set monitor mode
echo "Setting $WIFI_INTERFACE to monitor mode..."
iw dev "$WIFI_INTERFACE" set type monitor
# Bring up the monitor interface
echo "Bringing up monitor interface..."
ip link set "$WIFI_INTERFACE" up
# Set channel
echo "Setting channel to $CHANNEL..."
iw dev "$WIFI_INTERFACE" set channel "$CHANNEL"
# Verify monitor mode
echo ""
echo "=== Verification ==="
iw dev "$WIFI_INTERFACE" info
echo ""
echo "=== Monitor mode is now active! ==="
echo "Interface: $WIFI_INTERFACE"
echo "Channel: $CHANNEL"
echo ""
echo "To capture packets, you can use:"
echo " sudo tcpdump -i $WIFI_INTERFACE -n"
echo " sudo tcpdump -i $WIFI_INTERFACE -w capture.pcap"
echo " sudo wireshark -i $WIFI_INTERFACE"
echo ""
echo "To stop monitor mode and restore normal WiFi:"
echo " sudo ip link set $WIFI_INTERFACE down"
echo " sudo iw dev $WIFI_INTERFACE set type managed"
echo " sudo ip link set $WIFI_INTERFACE up"
echo " # Then reconnect to your network"

View File

@ -7,6 +7,3 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP_WIFI_RX_BA_WIN=16 CONFIG_ESP_WIFI_RX_BA_WIN=16
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y
# SD Card Detect: GPIO26, LOW=inserted (SparkFun breakout)
CONFIG_SD_CD_GPIO=26
CONFIG_SD_CD_ACTIVE_LOW=y

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()