Compare commits
7 Commits
master
...
feature/ge
| Author | SHA1 | Date |
|---|---|---|
|
|
fdc39bbd2a | |
|
|
21439cab8d | |
|
|
a34cafc034 | |
|
|
78a98d8ca6 | |
|
|
886cbf291a | |
|
|
404b7e3ad7 | |
|
|
1a6ab77b20 |
62
README.md
62
README.md
|
|
@ -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
|
||||
**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
|
||||
- **GPS Synchronization**: PPS signal support and NMEA parsing for timestamp synchronization
|
||||
- **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
|
||||
- **Mass Deployment**: Python scripts for flashing and configuring multiple devices
|
||||
|
||||
|
|
@ -96,11 +95,6 @@ iperf start
|
|||
|
||||
# Check status
|
||||
iperf status
|
||||
|
||||
# Wi‑Fi telemetry (monitor mode writes to fiwi-telemetry on SD; disconnects WiFi)
|
||||
monitor start 6
|
||||
sdcard status
|
||||
sdcard read fiwi-telemetry
|
||||
```
|
||||
|
||||
## Console Commands
|
||||
|
|
@ -136,29 +130,11 @@ sdcard read fiwi-telemetry
|
|||
- `gps status` - Show GPS synchronization status
|
||||
|
||||
### 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 channel <channel>` - Set monitor channel
|
||||
- `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).
|
||||
|
||||
## Mass Deployment
|
||||
|
|
@ -215,33 +191,15 @@ The deployment script (`esp32_deploy.py`) can use these stable symlinks when `--
|
|||
```
|
||||
├── main/ # Main application code
|
||||
│ ├── main.c # Entry point and console initialization
|
||||
│ ├── broadcast_beacon.c/h # UDP broadcast for laptop discovery
|
||||
│ └── board_config.h # Hardware pin definitions
|
||||
├── components/
|
||||
│ ├── 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
|
||||
│ ├── wifi_controller/ # WiFi management and monitor mode
|
||||
│ ├── wifi_monitor/ # 802.11 frame capture and collapse detection
|
||||
│ ├── wifi_cfg/ # WiFi and IP configuration storage
|
||||
│ ├── gps_sync/ # GPS PPS and NMEA parsing
|
||||
│ ├── 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
|
||||
├── gen_udev_rules.py # USB port mapping utility
|
||||
└── doc/ # Additional documentation
|
||||
|
|
@ -249,13 +207,11 @@ The deployment script (`esp32_deploy.py`) can use these stable symlinks when `--
|
|||
|
||||
## Documentation
|
||||
|
||||
- [Quick Start Guide](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/QUICK_START.md)
|
||||
- [Quick Reference](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/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
|
||||
- [Telemetry Capture](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/TELEMETRY_CAPTURE.md) - Enable and capture fiwi-telemetry
|
||||
- [Deployment Guide](https://git.umbernetworks.com/Umber/ESP32/src/branch/master/doc/DEPLOYMENT_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)
|
||||
- [Quick Start Guide](doc/QUICK_START.md)
|
||||
- [Quick Reference](doc/QUICK_REFERENCE.md)
|
||||
- [Deployment Guide](doc/DEPLOYMENT_GUIDE.md)
|
||||
- [Mass Deployment](doc/MASS_DEPLOY.md)
|
||||
- [GDB Debugging Guide (ESP32-C5)](doc/ESP32-C5_GDB_Debugging_Guide.md)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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)
|
||||
|
|
@ -8,9 +8,8 @@ idf_component_register(
|
|||
"cmd_gps.c"
|
||||
"cmd_ping.c"
|
||||
"cmd_ip.c"
|
||||
"cmd_sdcard.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES console wifi_cfg sd_card sdcard_http
|
||||
REQUIRES console wifi_cfg
|
||||
wifi_controller iperf status_led gps_sync
|
||||
esp_wifi esp_netif nvs_flash spi_flash
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,5 +45,4 @@ void app_console_register_commands(void) {
|
|||
register_ping_cmd();
|
||||
register_monitor_cmd();
|
||||
register_ip_cmd();
|
||||
register_sdcard_cmd();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ void register_gps_cmd(void);
|
|||
void register_ping_cmd(void);
|
||||
void register_monitor_cmd(void);
|
||||
void register_ip_cmd(void);
|
||||
void register_sdcard_cmd(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@
|
|||
|
||||
// --- Forward Declarations ---
|
||||
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)
|
||||
|
|
@ -54,8 +52,6 @@ static void print_gps_usage(void) {
|
|||
printf("Usage: gps <subcommand> [args]\n");
|
||||
printf("Subcommands:\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");
|
||||
}
|
||||
|
||||
|
|
@ -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], "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]);
|
||||
print_gps_usage();
|
||||
|
|
@ -135,77 +129,6 @@ static int gps_do_status(int argc, char **argv) {
|
|||
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
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -213,7 +136,7 @@ static int gps_do_pps_scan(int argc, char **argv) {
|
|||
void register_gps_cmd(void) {
|
||||
const esp_console_cmd_t cmd = {
|
||||
.command = "gps",
|
||||
.help = "GPS Tool: status, pps-test, pps-scan",
|
||||
.help = "GPS Tool: status",
|
||||
.hint = "<subcommand>",
|
||||
.func = &cmd_gps,
|
||||
.argtable = NULL
|
||||
|
|
|
|||
|
|
@ -49,16 +49,6 @@ static struct {
|
|||
struct arg_end *end;
|
||||
} 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) {
|
||||
printf("Usage: monitor <subcommand> [args]\n");
|
||||
printf("Subcommands:\n");
|
||||
|
|
@ -66,8 +56,6 @@ static void print_monitor_usage(void) {
|
|||
printf(" stop Stop Monitor Mode\n");
|
||||
printf(" status Show current status\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(" reload Reload config from NVS\n");
|
||||
printf(" clear Clear NVS config\n");
|
||||
|
|
@ -111,107 +99,6 @@ static int do_monitor_channel(int argc, char **argv) {
|
|||
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) {
|
||||
if (argc < 2) {
|
||||
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], "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) {
|
||||
print_monitor_usage();
|
||||
|
|
@ -246,12 +131,6 @@ void register_monitor_cmd(void) {
|
|||
channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel");
|
||||
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 = {
|
||||
.command = "monitor",
|
||||
.help = "Monitor Mode: start, stop, channel, status",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -226,7 +226,7 @@ void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) {
|
|||
}
|
||||
|
||||
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.mode = GPIO_MODE_INPUT;
|
||||
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);
|
||||
|
||||
ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d %s)", s_cfg.uart_port, s_cfg.pps_pin,
|
||||
s_cfg.pps_active_low ? "falling-edge" : "rising-edge");
|
||||
ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d)", s_cfg.uart_port, s_cfg.pps_pin);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ typedef struct {
|
|||
gpio_num_t tx_pin;
|
||||
gpio_num_t rx_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;
|
||||
|
||||
// --- Timestamp Struct ---
|
||||
|
|
@ -72,12 +71,6 @@ int64_t gps_get_pps_age_ms(void);
|
|||
// Copies the last received NMEA line into buffer (Diagnostic)
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
idf_component_register(
|
||||
SRCS "mcs_telemetry.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_wifi esp_timer wifi_monitor
|
||||
)
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
idf_component_register(
|
||||
SRCS "sd_card.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES driver fatfs esp_driver_sdspi sdmmc esp_timer
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
idf_component_register(
|
||||
SRCS "sdcard_http.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES sd_card esp_http_server
|
||||
)
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
@ -43,7 +43,6 @@ static led_strip_handle_t s_led_strip = NULL;
|
|||
static bool s_is_rgb = false;
|
||||
static int s_gpio_pin = -1;
|
||||
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) {
|
||||
if (s_is_rgb && s_led_strip) {
|
||||
|
|
@ -70,13 +69,8 @@ static void led_task(void *arg) {
|
|||
case LED_STATE_CONNECTED:
|
||||
set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
break;
|
||||
case LED_STATE_MONITORING: /* Blink blue only when frames being captured */
|
||||
if (s_capture_active) {
|
||||
set_color(0, 0, toggle ? 50 : 0); toggle = !toggle;
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
} else {
|
||||
set_color(0, 0, 10); vTaskDelay(pdMS_TO_TICKS(1000)); /* Dim solid: monitor on, no capture */
|
||||
}
|
||||
case LED_STATE_MONITORING:
|
||||
set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
break;
|
||||
case LED_STATE_TRANSMITTING:
|
||||
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);
|
||||
}
|
||||
|
||||
// ... Setters/Getters ...
|
||||
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; }
|
||||
void status_led_set_capture_active(bool active) { s_capture_active = active; }
|
||||
|
|
|
|||
|
|
@ -70,12 +70,6 @@ void status_led_set_state(led_state_t state);
|
|||
*/
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -141,26 +141,6 @@ bool wifi_cfg_set_monitor_channel(uint8_t channel) {
|
|||
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) {
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) == ESP_OK) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ bool wifi_cfg_set_password(const char *password);
|
|||
|
||||
// Monitor Specific
|
||||
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);
|
||||
bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
idf_component_register(SRCS "wifi_controller.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_wifi freertos
|
||||
PRIV_REQUIRES csi_manager iperf status_led wifi_monitor wifi_cfg gps_sync log esp_netif
|
||||
mcs_telemetry sd_card)
|
||||
PRIV_REQUIRES csi_manager iperf status_led wifi_monitor wifi_cfg gps_sync log esp_netif)
|
||||
|
|
|
|||
|
|
@ -33,14 +33,11 @@
|
|||
#include "wifi_controller.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_timer.h"
|
||||
#include "inttypes.h"
|
||||
#include <string.h>
|
||||
#include "wifi_cfg.h"
|
||||
|
||||
// Dependencies
|
||||
|
|
@ -53,21 +50,6 @@
|
|||
#include "csi_manager.h"
|
||||
#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 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 uint32_t s_monitor_frame_count = 0;
|
||||
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 ---
|
||||
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] ...
|
||||
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();
|
||||
int64_t now_ms = ts.gps_us / 1000;
|
||||
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);
|
||||
}
|
||||
gps_timestamp_t ts = gps_get_timestamp();
|
||||
int64_t now_ms = ts.gps_us / 1000;
|
||||
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);
|
||||
}
|
||||
|
||||
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++;
|
||||
status_led_set_capture_active(true);
|
||||
if (frame->retry && frame->duration_id > 5000) {
|
||||
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) {
|
||||
(void)arg;
|
||||
static char json_buf[TELEMETRY_JSON_BUF_SIZE];
|
||||
uint32_t flush_count = 0;
|
||||
uint32_t last_frame_count = 0;
|
||||
uint64_t last_batch_flush_ms = 0;
|
||||
uint64_t last_stats_log_ms = 0;
|
||||
bool task_running = true;
|
||||
|
||||
/* 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;
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
wifi_collapse_stats_t stats;
|
||||
if (wifi_monitor_get_stats(&stats) == ESP_OK) {
|
||||
ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
|
||||
(unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav);
|
||||
if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -376,24 +102,23 @@ static void monitor_stats_task(void *arg) {
|
|||
// --- Helper to apply IP settings ---
|
||||
static void apply_ip_settings(void) {
|
||||
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (!netif) return;
|
||||
|
||||
if (netif != NULL) {
|
||||
if (wifi_cfg_get_dhcp()) {
|
||||
esp_netif_dhcpc_start(netif);
|
||||
} else {
|
||||
esp_netif_dhcpc_stop(netif);
|
||||
if (wifi_cfg_get_dhcp()) {
|
||||
esp_netif_dhcpc_start(netif);
|
||||
} else {
|
||||
esp_netif_dhcpc_stop(netif);
|
||||
|
||||
char ip[16], mask[16], gw[16];
|
||||
if (wifi_cfg_get_ipv4(ip, mask, gw)) {
|
||||
esp_netif_ip_info_t info = {0};
|
||||
// API Fix: esp_ip4addr_aton returns uint32_t
|
||||
info.ip.addr = esp_ip4addr_aton(ip);
|
||||
info.netmask.addr = esp_ip4addr_aton(mask);
|
||||
info.gw.addr = esp_ip4addr_aton(gw);
|
||||
char ip[16], mask[16], gw[16];
|
||||
if (wifi_cfg_get_ipv4(ip, mask, gw)) {
|
||||
esp_netif_ip_info_t info = {0};
|
||||
// API Fix: esp_ip4addr_aton returns uint32_t
|
||||
info.ip.addr = esp_ip4addr_aton(ip);
|
||||
info.netmask.addr = esp_ip4addr_aton(mask);
|
||||
info.gw.addr = esp_ip4addr_aton(gw);
|
||||
|
||||
esp_netif_set_ip_info(netif, &info);
|
||||
ESP_LOGI(TAG, "Static IP applied: %s", ip);
|
||||
}
|
||||
esp_netif_set_ip_info(netif, &info);
|
||||
ESP_LOGI(TAG, "Static IP applied: %s", ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -436,151 +161,88 @@ void wifi_ctl_init(void) {
|
|||
esp_wifi_connect();
|
||||
}
|
||||
|
||||
// Load Staging and Active Params from NVS
|
||||
// Load Staging Params
|
||||
char mode_ignored[16];
|
||||
wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging);
|
||||
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) ---
|
||||
|
||||
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
|
||||
esp_err_t result = ESP_OK;
|
||||
|
||||
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 (channel == 0) channel = s_monitor_channel_staging;
|
||||
|
||||
if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) {
|
||||
ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", channel);
|
||||
result = 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 ESP_OK;
|
||||
}
|
||||
|
||||
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 result = ESP_OK;
|
||||
|
||||
if (s_current_mode == WIFI_CTL_MODE_STA) {
|
||||
ESP_LOGI(TAG, "Already in STA mode");
|
||||
result = 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 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 ---
|
||||
|
|
@ -615,39 +277,9 @@ void wifi_ctl_set_channel(int channel) {
|
|||
ESP_LOGI(TAG, "Switching live channel to %d", channel);
|
||||
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
|
||||
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) {
|
||||
const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" :
|
||||
(s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION";
|
||||
|
|
@ -655,132 +287,10 @@ void wifi_ctl_status(void) {
|
|||
printf("WiFi Status:\n");
|
||||
printf(" Mode: %s\n", mode_str);
|
||||
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
|
||||
uint8_t channel = s_monitor_channel_active;
|
||||
uint32_t freq_mhz = wifi_channel_to_frequency(channel);
|
||||
|
||||
/* 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(" Channel: %d\n", s_monitor_channel_active);
|
||||
printf(" Frames: %lu\n", (unsigned long)s_monitor_frame_count);
|
||||
}
|
||||
printf(" Staging Ch: %d\n", s_monitor_channel_staging);
|
||||
}
|
||||
|
||||
// --- Params (NVS) ---
|
||||
|
|
@ -791,15 +301,8 @@ bool wifi_ctl_param_is_unsaved(void) {
|
|||
|
||||
void wifi_ctl_param_save(const char *dummy) {
|
||||
(void)dummy;
|
||||
/* If monitor mode is running, save the active channel; otherwise save staging */
|
||||
uint8_t channel_to_save = (s_current_mode == WIFI_CTL_MODE_MONITOR) ?
|
||||
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;
|
||||
}
|
||||
if (wifi_cfg_set_monitor_channel(s_monitor_channel_staging)) {
|
||||
ESP_LOGI(TAG, "Monitor channel (%d) saved to NVS", s_monitor_channel_staging);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "No changes to save.");
|
||||
}
|
||||
|
|
@ -810,17 +313,7 @@ void wifi_ctl_param_init(void) {
|
|||
uint8_t ch = 0;
|
||||
wifi_cfg_get_mode(mode_ignored, &ch);
|
||||
if (ch > 0) s_monitor_channel_staging = ch;
|
||||
|
||||
// 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);
|
||||
ESP_LOGI(TAG, "Reloaded monitor channel: %d", s_monitor_channel_staging);
|
||||
}
|
||||
|
||||
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; }
|
||||
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 ---
|
||||
static void auto_monitor_task_func(void *arg) {
|
||||
uint8_t channel = (uint8_t)(uintptr_t)arg;
|
||||
|
|
|
|||
|
|
@ -74,18 +74,6 @@ void wifi_ctl_param_clear(void);
|
|||
wifi_ctl_mode_t wifi_ctl_get_mode(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
|
||||
void wifi_ctl_auto_monitor_start(uint8_t channel);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
idf_component_register(
|
||||
SRCS "wifi_monitor.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_wifi nvs_flash esp_timer
|
||||
REQUIRES esp_wifi nvs_flash
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@
|
|||
#include "wifi_monitor.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_timer.h"
|
||||
#include "string.h"
|
||||
|
||||
static const char *TAG = "WiFi_Monitor";
|
||||
|
|
@ -57,195 +56,9 @@ uint32_t threshold_duration_multiplier = 2; // NAV > expected * this = mism
|
|||
// Logging control
|
||||
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 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
|
||||
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
|
||||
|
|
@ -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)
|
||||
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) {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
// Extract PHY rate info from RX control
|
||||
frame_info.mcs = 0;
|
||||
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
|
||||
// ESP-IDF encoding: rate >= 128 encodes MCS for HT/VHT/HE frames
|
||||
// HT: MCS 0-31, VHT: MCS 0-9, HE: MCS 0-11
|
||||
if (frame_info.sig_mode > 0 && frame_info.mcs == 0 && rx_ctrl->rate >= 128) {
|
||||
uint8_t extracted_mcs = rx_ctrl->rate - 128;
|
||||
// Estimate PHY rate from rate index (rough approximation)
|
||||
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
|
||||
};
|
||||
|
||||
// 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
|
||||
// Otherwise fall back to rate table estimation
|
||||
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
|
||||
}
|
||||
if (rx_ctrl->rate < sizeof(rate_table) / sizeof(rate_table[0])) {
|
||||
frame_info.phy_rate_kbps = rate_table[rx_ctrl->rate];
|
||||
} else {
|
||||
// Estimate PHY rate from rate index (rough approximation for legacy frames)
|
||||
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
|
||||
}
|
||||
frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default
|
||||
}
|
||||
|
||||
// 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) {
|
||||
s_mismatch_log_counter++;
|
||||
if (s_monitor_debug && (s_mismatch_log_counter % log_every_n_mismatches) == 0) {
|
||||
/* Check MAC filter before logging */
|
||||
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
|
||||
ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
|
||||
wifi_frame_type_str(frame_info.type, frame_info.subtype),
|
||||
frame_info.frame_len, phy_rate_mbps);
|
||||
if ((s_mismatch_log_counter % log_every_n_mismatches) == 0) {
|
||||
ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
|
||||
wifi_frame_type_str(frame_info.type, frame_info.subtype),
|
||||
frame_info.frame_len, phy_rate_mbps);
|
||||
|
||||
// NEW: Log the Source MAC (Addr2)
|
||||
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[3], frame_info.addr2[4], frame_info.addr2[5]);
|
||||
// NEW: Log the Source MAC (Addr2)
|
||||
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[3], frame_info.addr2[4], frame_info.addr2[5]);
|
||||
|
||||
ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)",
|
||||
expected_duration, frame_info.duration_id,
|
||||
frame_info.duration_id - expected_duration);
|
||||
ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm",
|
||||
frame_info.retry ? "YES" : "no", frame_info.rssi);
|
||||
}
|
||||
ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)",
|
||||
expected_duration, frame_info.duration_id,
|
||||
frame_info.duration_id - expected_duration);
|
||||
ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm",
|
||||
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 &&
|
||||
phy_rate_mbps < threshold_phy_rate_fallback_mbps) {
|
||||
if (s_monitor_debug) {
|
||||
/* Check MAC filter before logging */
|
||||
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
|
||||
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
|
||||
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
|
||||
|
||||
// NEW: Log the Attacker MAC
|
||||
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[3], frame_info.addr2[4], frame_info.addr2[5]);
|
||||
// NEW: Log the Attacker MAC
|
||||
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[3], frame_info.addr2[4], frame_info.addr2[5]);
|
||||
|
||||
ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps",
|
||||
wifi_frame_type_str(frame_info.type, frame_info.subtype),
|
||||
frame_info.frame_len, phy_rate_mbps);
|
||||
ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES",
|
||||
frame_info.duration_id, expected_duration);
|
||||
}
|
||||
}
|
||||
ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps",
|
||||
wifi_frame_type_str(frame_info.type, frame_info.subtype),
|
||||
frame_info.frame_len, phy_rate_mbps);
|
||||
ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES",
|
||||
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:
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -807,67 +477,3 @@ const char* wifi_frame_type_str(uint8_t type, uint8_t subtype) {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,17 +168,13 @@ typedef struct {
|
|||
uint32_t timestamp;
|
||||
|
||||
// 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 sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT, 4=HE)
|
||||
uint8_t spatial_streams; // Number of spatial streams (NSS) - 1-8
|
||||
uint8_t sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT)
|
||||
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)
|
||||
|
||||
// Aggregation info
|
||||
uint8_t ampdu_count; // Number of aggregated MPDUs in A-MPDU (1 = not aggregated)
|
||||
|
||||
// Frame size
|
||||
uint16_t frame_len;
|
||||
} wifi_frame_info_t;
|
||||
|
|
@ -208,11 +204,6 @@ typedef struct {
|
|||
uint16_t avg_phy_rate_mbps; // Average PHY rate
|
||||
uint16_t min_phy_rate_mbps; // Minimum 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;
|
||||
|
||||
/**
|
||||
|
|
@ -296,32 +287,6 @@ bool wifi_monitor_is_collapsed(void);
|
|||
*/
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ mode_monitor 149/80
|
|||
|
||||
## 🔍 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
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -608,7 +608,7 @@ idf.py -p $PORT monitor | tee device_90.log
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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 1–2 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 module’s antenna connector matches. Outdoor or window placement helps.
|
||||
- **Startup delay** — Allow 1–2 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 |
|
||||
|
|
@ -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 daisy‑chain 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 doesn’t support CD:** Some SparkFun-style breakouts have a CD pad or through-hole that is **not** connected to the slot’s 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 Wi‑Fi (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 Wi‑Fi, 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**.
|
||||
|
|
@ -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"` |
|
||||
|
|
@ -249,12 +249,12 @@
|
|||
<body>
|
||||
<header>
|
||||
<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>
|
||||
|
||||
<div class="section">
|
||||
<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">
|
||||
<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.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>
|
||||
</div>
|
||||
|
||||
|
|
@ -923,7 +923,7 @@ echo "All devices flashed!"</code></pre>
|
|||
|
||||
<footer>
|
||||
<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>
|
||||
</footer>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
idf_component_register(SRCS "main.c" "broadcast_beacon.c"
|
||||
idf_component_register(SRCS "main.c"
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -45,37 +45,4 @@ menu "ESP32 iperf Configuration"
|
|||
help
|
||||
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
|
||||
|
|
|
|||
|
|
@ -46,14 +46,7 @@
|
|||
#define HAS_RGB_LED 1
|
||||
#define GPS_TX_PIN GPIO_NUM_24
|
||||
#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_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
|
||||
#define GPS_PPS_PIN GPIO_NUM_25
|
||||
#elif defined (CONFIG_IDF_TARGET_ESP32S3)
|
||||
// ============================================================================
|
||||
// ESP32-S3 (DevKitC-1)
|
||||
|
|
@ -65,7 +58,6 @@
|
|||
#define GPS_TX_PIN GPIO_NUM_5
|
||||
#define GPS_RX_PIN GPIO_NUM_4
|
||||
#define GPS_PPS_PIN GPIO_NUM_6
|
||||
#define GPS_PPS_ACTIVE_LOW 1
|
||||
#elif defined (CONFIG_IDF_TARGET_ESP32)
|
||||
// ============================================================================
|
||||
// ESP32 (Original / Standard)
|
||||
|
|
@ -77,7 +69,6 @@
|
|||
#define GPS_TX_PIN GPIO_NUM_17
|
||||
#define GPS_RX_PIN GPIO_NUM_16
|
||||
#define GPS_PPS_PIN GPIO_NUM_4
|
||||
#define GPS_PPS_ACTIVE_LOW 1
|
||||
#else
|
||||
// Fallback
|
||||
#define RGB_LED_GPIO 8
|
||||
|
|
@ -85,7 +76,6 @@
|
|||
#define GPS_TX_PIN GPIO_NUM_1
|
||||
#define GPS_RX_PIN GPIO_NUM_3
|
||||
#define GPS_PPS_PIN GPIO_NUM_5
|
||||
#define GPS_PPS_ACTIVE_LOW 1
|
||||
#endif
|
||||
|
||||
#endif // BOARD_CONFIG_H
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 */
|
||||
25
main/main.c
25
main/main.c
|
|
@ -58,9 +58,6 @@
|
|||
#include "app_console.h"
|
||||
#include "iperf.h"
|
||||
|
||||
#include "sd_card.h"
|
||||
#include "sdcard_http.h"
|
||||
#include "broadcast_beacon.h"
|
||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||
#include "csi_log.h"
|
||||
#include "csi_manager.h"
|
||||
|
|
@ -171,7 +168,6 @@ void app_main(void) {
|
|||
.tx_pin = GPS_TX_PIN,
|
||||
.rx_pin = GPS_RX_PIN,
|
||||
.pps_pin = GPS_PPS_PIN,
|
||||
.pps_active_low = GPS_PPS_ACTIVE_LOW,
|
||||
};
|
||||
gps_sync_init(&gps_cfg, true);
|
||||
} else {
|
||||
|
|
@ -181,24 +177,6 @@ void app_main(void) {
|
|||
// Hardware Init
|
||||
status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
|
||||
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
|
||||
ESP_ERROR_CHECK(csi_log_init());
|
||||
csi_mgr_init();
|
||||
|
|
@ -208,9 +186,6 @@ void app_main(void) {
|
|||
wifi_ctl_init();
|
||||
iperf_param_init();
|
||||
|
||||
/* Broadcast beacon: advertise device (IP, MAC, fiwi-telemetry) for laptop discovery */
|
||||
broadcast_beacon_init();
|
||||
|
||||
// 6. Initialize Console (REPL)
|
||||
ESP_LOGI(TAG, "Initializing console REPL...");
|
||||
esp_console_repl_t *repl = NULL;
|
||||
|
|
|
|||
713
php/mcs_fiwi.php
713
php/mcs_fiwi.php
|
|
@ -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) →</div>
|
||||
<div style="width:100%; border-top:1px solid #333; margin-top:5px; padding-top:2px;">
|
||||
<div class="math-row">
|
||||
<span>Eigenvalues:</span>
|
||||
<span>λ<sub>1</sub>:<span class="math-val" id="eig1-${dev.id}">1.0</span> λ<sub>2</sub>:<span class="math-val" id="eig2-${dev.id}">0.5</span></span>
|
||||
</div>
|
||||
<div class="math-row">
|
||||
<span>Condition (κ):</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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
|
|
@ -7,6 +7,3 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
|
|||
CONFIG_ESP_WIFI_RX_BA_WIN=16
|
||||
CONFIG_ESP_WIFI_AMPDU_RX_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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue