SD card (SPI + CD), GPS PPS polarity/diagnostics, wiring docs

- Add sd_card component: SPI mode for ESP32-C5, optional CD (GPIO -1 to disable)
- Add sdcard console command; CD shift fix for CONFIG_SD_CD_GPIO=-1
- GPS: pps_active_low support, gps pps-test / pps-scan console commands
- board_config: ESP32-C5 SD pins (7,8,9,10,26), GPS PPS polarity
- doc/SD_CARD_WIRING.md, doc/GPS_WIRING.md: pin diagrams, Y-cable power note
- sdkconfig.defaults.esp32c5: SD CD GPIO 26
- php/mcs_fiwi.php: Fi-Wi demo page

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Robert McMahon 2026-02-07 11:42:03 -08:00
parent 4ff7ed53b1
commit a4e81c9852
18 changed files with 2009 additions and 6 deletions

View File

@ -8,8 +8,9 @@ idf_component_register(
"cmd_gps.c" "cmd_gps.c"
"cmd_ping.c" "cmd_ping.c"
"cmd_ip.c" "cmd_ip.c"
"cmd_sdcard.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES console wifi_cfg REQUIRES console wifi_cfg sd_card
wifi_controller iperf status_led gps_sync wifi_controller iperf status_led gps_sync
esp_wifi esp_netif nvs_flash spi_flash esp_wifi esp_netif nvs_flash spi_flash
) )

View File

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

View File

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

View File

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

View File

@ -0,0 +1,168 @@
/*
* 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 "esp_console.h"
#include "argtable3/argtable3.h"
#include "app_console.h"
#include "sd_card.h"
#define SDCARD_READ_BUF_SIZE 4096
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));
}
}
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;
}
static int cmd_sdcard(int argc, char **argv) {
if (argc < 2) {
printf("Usage: sdcard <status|write|read> [args]\n");
printf(" status - Show CD, mounted, capacity\n");
printf(" write <f> <t> - Write text to file\n");
printf(" read <f> - Read and print file\n");
return 0;
}
if (strcmp(argv[1], "status") == 0) {
return do_sdcard_status(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]);
}
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 (CD, capacity), write <file> <text>, read <file>",
.hint = "<status|write|read>",
.func = &cmd_sdcard,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,332 @@
/*
* 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 "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 <dirent.h>
// 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 void sd_card_cd_ensure_configured(void) {
if (s_cd_configured || SD_CD_PIN < 0) {
return;
}
/* 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) {
if (s_sd_card_mounted) {
ESP_LOGW(TAG, "SD card already initialized");
return ESP_OK;
}
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,
};
esp_err_t err = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err));
return err;
}
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
};
err = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card);
if (err != ESP_OK) {
if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false;
}
if (err == 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(err));
}
return err;
}
sdmmc_card_print_info(stdout, s_card);
s_sd_card_mounted = true;
ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point);
return ESP_OK;
}
esp_err_t sd_card_deinit(void) {
if (!s_sd_card_mounted) {
return ESP_OK;
}
ESP_LOGI(TAG, "Unmounting SD card...");
esp_err_t ret = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card);
if (ret == ESP_OK) {
s_sd_card_mounted = false;
s_card = NULL;
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(ret));
}
return ret;
}
bool sd_card_is_ready(void) {
return s_sd_card_mounted;
}
bool sd_card_cd_available(void) {
return (SD_CD_PIN >= 0);
}
bool sd_card_cd_is_inserted(void) {
if (SD_CD_PIN < 0) {
return false;
}
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)
return (level == 1); /* HIGH = inserted (inverted breakout) */
#else
return (level == 0); /* LOW = inserted (SparkFun default) */
#endif
}
int sd_card_cd_get_level(void) {
if (SD_CD_PIN < 0) {
return -1;
}
sd_card_cd_ensure_configured();
return gpio_get_level(SD_CD_PIN);
}
esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) {
if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE;
}
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) {
ESP_LOGE(TAG, "Failed to get free space: %d", res);
return ESP_FAIL;
}
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;
}
return ESP_OK;
}
esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) {
if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE;
}
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
const char *mode = append ? "a" : "w";
FILE *f = fopen(full_path, mode);
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path);
return ESP_FAIL;
}
size_t written = fwrite(data, 1, len, f);
fclose(f);
if (written != len) {
ESP_LOGE(TAG, "Failed to write all data: wrote %zu of %zu bytes", written, len);
return ESP_FAIL;
}
ESP_LOGD(TAG, "Wrote %zu bytes to %s", written, full_path);
return ESP_OK;
}
esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) {
if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE;
}
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path);
return ESP_FAIL;
}
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);
return ESP_OK;
}
bool sd_card_file_exists(const char *filename) {
if (!s_sd_card_mounted) {
return false;
}
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
struct stat st;
return (stat(full_path, &st) == 0);
}
esp_err_t sd_card_delete_file(const char *filename) {
if (!s_sd_card_mounted) {
return ESP_ERR_INVALID_STATE;
}
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
if (unlink(full_path) != 0) {
ESP_LOGE(TAG, "Failed to delete file: %s", full_path);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Deleted file: %s", full_path);
return ESP_OK;
}

View File

@ -0,0 +1,129 @@
/*
* 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 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 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

241
doc/GPS_WIRING.md Normal file
View File

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

180
doc/SD_CARD_WIRING.md Normal file
View File

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

View File

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

View File

@ -45,4 +45,19 @@ menu "ESP32 iperf Configuration"
help help
Netmask for the network. Netmask for the network.
menu "SD Card"
config SD_CD_GPIO
int "Card Detect GPIO (-1 to disable)"
default 26 if IDF_TARGET_ESP32C5
default -1
help
GPIO for SD card detect. -1 to disable. SparkFun breakout on ESP32-C5: GPIO26.
config SD_CD_ACTIVE_LOW
bool "Card Detect: LOW = inserted"
default y
help
When set (default), LOW on CD pin means card inserted (SparkFun). When unset, HIGH means inserted.
endmenu
endmenu endmenu

View File

@ -46,7 +46,14 @@
#define HAS_RGB_LED 1 #define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_24 #define GPS_TX_PIN GPIO_NUM_24
#define GPS_RX_PIN GPIO_NUM_23 #define GPS_RX_PIN GPIO_NUM_23
#define GPS_PPS_PIN GPIO_NUM_25 #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
#elif defined (CONFIG_IDF_TARGET_ESP32S3) #elif defined (CONFIG_IDF_TARGET_ESP32S3)
// ============================================================================ // ============================================================================
// ESP32-S3 (DevKitC-1) // ESP32-S3 (DevKitC-1)
@ -58,6 +65,7 @@
#define GPS_TX_PIN GPIO_NUM_5 #define GPS_TX_PIN GPIO_NUM_5
#define GPS_RX_PIN GPIO_NUM_4 #define GPS_RX_PIN GPIO_NUM_4
#define GPS_PPS_PIN GPIO_NUM_6 #define GPS_PPS_PIN GPIO_NUM_6
#define GPS_PPS_ACTIVE_LOW 1
#elif defined (CONFIG_IDF_TARGET_ESP32) #elif defined (CONFIG_IDF_TARGET_ESP32)
// ============================================================================ // ============================================================================
// ESP32 (Original / Standard) // ESP32 (Original / Standard)
@ -69,6 +77,7 @@
#define GPS_TX_PIN GPIO_NUM_17 #define GPS_TX_PIN GPIO_NUM_17
#define GPS_RX_PIN GPIO_NUM_16 #define GPS_RX_PIN GPIO_NUM_16
#define GPS_PPS_PIN GPIO_NUM_4 #define GPS_PPS_PIN GPIO_NUM_4
#define GPS_PPS_ACTIVE_LOW 1
#else #else
// Fallback // Fallback
#define RGB_LED_GPIO 8 #define RGB_LED_GPIO 8
@ -76,6 +85,7 @@
#define GPS_TX_PIN GPIO_NUM_1 #define GPS_TX_PIN GPIO_NUM_1
#define GPS_RX_PIN GPIO_NUM_3 #define GPS_RX_PIN GPIO_NUM_3
#define GPS_PPS_PIN GPIO_NUM_5 #define GPS_PPS_PIN GPIO_NUM_5
#define GPS_PPS_ACTIVE_LOW 1
#endif #endif
#endif // BOARD_CONFIG_H #endif // BOARD_CONFIG_H

View File

@ -58,6 +58,7 @@
#include "app_console.h" #include "app_console.h"
#include "iperf.h" #include "iperf.h"
#include "sd_card.h"
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED #ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_log.h" #include "csi_log.h"
#include "csi_manager.h" #include "csi_manager.h"
@ -168,6 +169,7 @@ void app_main(void) {
.tx_pin = GPS_TX_PIN, .tx_pin = GPS_TX_PIN,
.rx_pin = GPS_RX_PIN, .rx_pin = GPS_RX_PIN,
.pps_pin = GPS_PPS_PIN, .pps_pin = GPS_PPS_PIN,
.pps_active_low = GPS_PPS_ACTIVE_LOW,
}; };
gps_sync_init(&gps_cfg, true); gps_sync_init(&gps_cfg, true);
} else { } else {
@ -177,6 +179,19 @@ void app_main(void) {
// Hardware Init // Hardware Init
status_led_init(RGB_LED_GPIO, HAS_RGB_LED); status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
status_led_set_state(LED_STATE_FAILED); // Force Red Blink status_led_set_state(LED_STATE_FAILED); // Force Red Blink
// Initialize SD card (optional - will fail gracefully if not present)
esp_err_t sd_ret = sd_card_init();
if (sd_ret == ESP_OK) {
uint64_t total_bytes = 0, free_bytes = 0;
if (sd_card_get_info(&total_bytes, &free_bytes) == ESP_OK) {
ESP_LOGI(TAG, "SD card ready: %.2f MB total, %.2f MB free",
total_bytes / (1024.0 * 1024.0), free_bytes / (1024.0 * 1024.0));
}
} else {
ESP_LOGW(TAG, "SD card initialization failed (card may not be present): %s", esp_err_to_name(sd_ret));
}
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED #ifdef CONFIG_ESP_WIFI_CSI_ENABLED
ESP_ERROR_CHECK(csi_log_init()); ESP_ERROR_CHECK(csi_log_init());
csi_mgr_init(); csi_mgr_init();

713
php/mcs_fiwi.php Normal file
View File

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

View File

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