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:
parent
4ff7ed53b1
commit
a4e81c9852
|
|
@ -8,8 +8,9 @@ idf_component_register(
|
|||
"cmd_gps.c"
|
||||
"cmd_ping.c"
|
||||
"cmd_ip.c"
|
||||
"cmd_sdcard.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES console wifi_cfg
|
||||
REQUIRES console wifi_cfg sd_card
|
||||
wifi_controller iperf status_led gps_sync
|
||||
esp_wifi esp_netif nvs_flash spi_flash
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,4 +45,5 @@ void app_console_register_commands(void) {
|
|||
register_ping_cmd();
|
||||
register_monitor_cmd();
|
||||
register_ip_cmd();
|
||||
register_sdcard_cmd();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ 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,6 +43,8 @@
|
|||
|
||||
// --- 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)
|
||||
|
|
@ -52,6 +54,8 @@ 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");
|
||||
}
|
||||
|
||||
|
|
@ -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], "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();
|
||||
|
|
@ -129,6 +135,77 @@ 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
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -136,7 +213,7 @@ static int gps_do_status(int argc, char **argv) {
|
|||
void register_gps_cmd(void) {
|
||||
const esp_console_cmd_t cmd = {
|
||||
.command = "gps",
|
||||
.help = "GPS Tool: status",
|
||||
.help = "GPS Tool: status, pps-test, pps-scan",
|
||||
.hint = "<subcommand>",
|
||||
.func = &cmd_gps,
|
||||
.argtable = NULL
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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 = 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.mode = GPIO_MODE_INPUT;
|
||||
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);
|
||||
|
||||
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) {
|
||||
|
|
@ -280,3 +281,107 @@ 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,6 +49,7 @@ 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 ---
|
||||
|
|
@ -71,6 +72,12 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
idf_component_register(
|
||||
SRCS "sd_card.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES driver fatfs esp_driver_sdspi sdmmc
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 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 |
|
||||
|
|
@ -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 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)
|
||||
|
||||
## 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,3 +1,3 @@
|
|||
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)
|
||||
PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console sd_card)
|
||||
|
|
|
|||
|
|
@ -45,4 +45,19 @@ menu "ESP32 iperf Configuration"
|
|||
help
|
||||
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
|
||||
|
|
|
|||
|
|
@ -46,7 +46,14 @@
|
|||
#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
|
||||
#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)
|
||||
// ============================================================================
|
||||
// ESP32-S3 (DevKitC-1)
|
||||
|
|
@ -58,6 +65,7 @@
|
|||
#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)
|
||||
|
|
@ -69,6 +77,7 @@
|
|||
#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
|
||||
|
|
@ -76,6 +85,7 @@
|
|||
#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
|
||||
|
|
|
|||
15
main/main.c
15
main/main.c
|
|
@ -58,6 +58,7 @@
|
|||
#include "app_console.h"
|
||||
#include "iperf.h"
|
||||
|
||||
#include "sd_card.h"
|
||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||
#include "csi_log.h"
|
||||
#include "csi_manager.h"
|
||||
|
|
@ -168,6 +169,7 @@ 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 {
|
||||
|
|
@ -177,6 +179,19 @@ 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));
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||
ESP_ERROR_CHECK(csi_log_init());
|
||||
csi_mgr_init();
|
||||
|
|
|
|||
|
|
@ -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) →</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>
|
||||
|
|
@ -7,3 +7,6 @@ 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue