/** * V2X-SD Driver - SD-Karte via SDSPI (ESP32-C5 kompatibel) + CSV-Ausgabe * * T-Dongle-C5 SD-Karte �ber SPI (SDSPI): * SD_CLK = GPIO 4 * SD_MOSI = GPIO 5 * SD_MISO = GPIO 0 * SD_CS = GPIO 1 * * Format v2x_frames.csv: * timestamp_us,msg_type,channel,frequency_mhz,rssi_dbm,noise_dbm,rate_index,frame_len,mac_addr,bssid,raw_hex * * Format v2x_log.txt: * Start-Info, periodische Stats, Fehler, CSV-Rotation * * Rotation: CSV-Dateien > 50MB werden in v2x_frames_YYYYMMDD_HHMMSS.csv umbenannt */ #include "esp_log.h" #include "esp_timer.h" #include #include "esp_vfs_fat.h" #include "sdmmc_cmd.h" #include "driver/sdmmc_host.h" #include "driver/sdspi_host.h" #include "driver/spi_master.h" #include #include #include #include #include "v2x_display.h" /* Typ-Definitonen: v2x_frame_t, v2x_stats_t, v2x_msg_type_t, Konstanten */ static const char *TAG = "v2x-sd"; /* Globale Variablen */ static v2x_stats_t s_stats; static FILE *s_log_fp = NULL; static bool s_initialized = false; static sdmmc_card_t *s_sd_card = NULL; /* ====== SD-Karte initialisieren (SDSPI-Modus f�r ESP32-C5) ====== */ esp_err_t v2x_sd_init(void) { esp_err_t ret; sdmmc_card_t *card = NULL; ESP_LOGI(TAG, "Initialisiere SD-Karte �ber SDSPI (ESP32-C5)..."); /* SPI-Bus konfigurieren */ spi_bus_config_t bus_cfg = { .mosi_io_num = 5, // SD_MOSI .miso_io_num = 0, // SD_MISO .sclk_io_num = 4, // SD_CLK .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 4000, }; ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO); if (ret != ESP_OK) { ESP_LOGE(TAG, "SPI-Bus initialisieren fehlgeschlagen: %s", esp_err_to_name(ret)); return ret; } /* SDSPI-Ger�t konfigurieren */ sdspi_device_config_t device_cfg = { .host_id = SPI2_HOST, .gpio_cs = GPIO_NUM_1, // SD_CS .gpio_cd = GPIO_NUM_NC, // SD Card Detect .gpio_wp = GPIO_NUM_NC, // SD Write Protect .gpio_int = GPIO_NUM_NC, .gpio_wp_polarity = false, .duty_cycle_pos = 0, }; /* SDSPI-Host konfigurieren */ sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.slot = SPI2_HOST; host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; /* FATFS-Mount-Konfiguration (korrekter Typ: esp_vfs_fat_mount_config_t) */ esp_vfs_fat_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 4, .allocation_unit_size = 16 * 1024, }; /* FATFS mounten (SDSPI-Variante) */ ret = esp_vfs_fat_sdspi_mount("/sdcard", &host, &device_cfg, &mount_config, &card); if (ret != ESP_OK) { ESP_LOGE(TAG, "SD-FATFS mount fehlgeschlagen: %s", esp_err_to_name(ret)); return ret; } s_sd_card = card; /* SD-Karten-Info ausgeben */ ESP_LOGI(TAG, "SD-Karte erkannt: %lu MB", (unsigned long)(card->csd.capacity * card->csd.sector_size / (1024 * 1024))); /* Schreibtest */ FILE *fp = fopen("/sdcard/test.txt", "w"); if (fp) { fprintf(fp, "OK\n"); fclose(fp); remove("/sdcard/test.txt"); ESP_LOGI(TAG, "SD-Schreibtest OK"); } else { ESP_LOGW(TAG, "SD-Schreibtest fehlgeschlagen"); } /* Log-Datei erstellen */ s_log_fp = fopen(SD_LOG_FILE, "w"); if (s_log_fp) { time_t now = time(NULL); char tbuf[64]; struct tm *tm_info = localtime(&now); strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", tm_info); fprintf(s_log_fp, "= V2X-Sniffer Log =\n"); fprintf(s_log_fp, "Start: %s\n", tbuf); fprintf(s_log_fp, "SD-Karte: SDSPI-Modus (/sdcard)\n"); fprintf(s_log_fp, "CSV: %s\n", SD_CSV_FILE); fprintf(s_log_fp, "==============\n\n"); fflush(s_log_fp); } s_initialized = true; s_stats.start_timestamp_us = esp_timer_get_time(); ESP_LOGI(TAG, "SD-Karte eingebunden: /sdcard"); return ESP_OK; } /* ====== SD-Karte aush�ngen ====== */ void v2x_sd_deinit(void) { if (!s_initialized) return; ESP_LOGI(TAG, "SD-Karte aush�ngen..."); if (s_log_fp) { fprintf(s_log_fp, "\n===== Shutdown =====\n"); fflush(s_log_fp); fclose(s_log_fp); s_log_fp = NULL; } /* SD-Karte unmounten */ if (s_sd_card) { esp_vfs_fat_sdcard_unmount("/sdcard", s_sd_card); s_sd_card = NULL; } s_initialized = false; ESP_LOGI(TAG, "SD-Karte entfernt"); } /* ====== MAC-Adresse als Hex-String ====== */ static void mac_to_str(const uint8_t *mac, char *out) { snprintf(out, 18, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } /* ====== Rohdaten als Hex-String ====== */ static void raw_to_hex(const uint8_t *raw, uint16_t len, char *out, size_t out_len) { size_t pos = 0; for (uint16_t i = 0; i < len && pos < out_len - 2; i++) { int n = snprintf(out + pos, out_len - pos, "%02X", raw[i]); if (n < 0 || (size_t)n >= out_len - pos) break; pos += (size_t)n; } out[pos] = '\0'; } /* ====== Frame in CSV-Zeile konvertieren ====== */ void frame_to_csv_line(const v2x_frame_t *frame, char *out, size_t out_len) { char mac_hex[18], bssid_hex[18], raw_hex[RAW_HEX_BUF]; mac_to_str(frame->mac_addr, mac_hex); mac_to_str(frame->bssid, bssid_hex); raw_to_hex(frame->raw_data, frame->raw_len, raw_hex, sizeof(raw_hex)); snprintf(out, out_len, "%" PRIu64 ",%d,%d,%.1f,%d,%d,%d,%d,%s,%s,%s", frame->timestamp_us, frame->msg_type, frame->channel, frame->frequency_mhz, frame->rssi_dbm, frame->noise_dbm, frame->rate_index, frame->frame_len, mac_hex, bssid_hex, raw_hex); } /* ====== Frame auf SD schreiben ====== */ esp_err_t v2x_sd_write_frame(const v2x_frame_t *frame) { if (!s_initialized) { return ESP_ERR_INVALID_STATE; } /* Stats updaten */ s_stats.total_frames++; switch (frame->msg_type) { case V2X_MSG_BSM: s_stats.bsm_count++; break; case V2X_MSG_CAM: s_stats.cam_count++; break; case V2X_MSG_DENM: s_stats.denm_count++; break; default: s_stats.unknown_count++; break; } /* CSV-Zeile */ char line[RAW_HEX_BUF + 128]; frame_to_csv_line(frame, line, sizeof(line)); /* An CSV anh�ngen */ FILE *fp = fopen("/sdcard/v2x_frames.csv", "a+"); if (!fp) { s_stats.sd_write_errors++; ESP_LOGE(TAG, "CSV-Datei kann nicht ge�ffnet werden"); return ESP_FAIL; } fprintf(fp, "%s\n", line); fclose(fp); return ESP_OK; } /* ====== Log schreiben ====== */ esp_err_t v2x_sd_log_write(const char *fmt, ...) { if (!s_initialized || !s_log_fp) { return ESP_ERR_INVALID_STATE; } va_list args; va_start(args, fmt); vfprintf(s_log_fp, fmt, args); va_end(args); fprintf(s_log_fp, "\n"); fflush(s_log_fp); return ESP_OK; } /* ====== Statistik ====== */ const v2x_stats_t* v2x_sd_get_stats(void) { return &s_stats; } esp_err_t v2x_sd_reset_stats(void) { memset(&s_stats, 0, sizeof(s_stats)); s_stats.start_timestamp_us = esp_timer_get_time(); return ESP_OK; } esp_err_t v2x_sd_dump_stats(void) { if (!s_initialized) return ESP_ERR_INVALID_STATE; const v2x_stats_t *stats = v2x_sd_get_stats(); time_t now = time(NULL); struct tm *tm_info = localtime(&now); char tbuf[64]; strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", tm_info); uint64_t uptime_s = (esp_timer_get_time() - s_stats.start_timestamp_us) / 1000000; v2x_sd_log_write("--- Statistik ---"); v2x_sd_log_write("Zeit: %s", tbuf); v2x_sd_log_write("Uptime: %lu s", (unsigned long)uptime_s); v2x_sd_log_write("Frames: %lu Gesamt", (unsigned long)stats->total_frames); v2x_sd_log_write(" BSM: %lu | CAM: %lu | DENM: %lu | Sonst: %lu", (unsigned long)stats->bsm_count, (unsigned long)stats->cam_count, (unsigned long)stats->denm_count, (unsigned long)stats->unknown_count); v2x_sd_log_write("SD-Fehler: %lu", (unsigned long)stats->sd_write_errors); FILE *f = fopen("/sdcard/v2x_frames.csv", "r"); if (f) { fseek(f, 0, SEEK_END); long fsize = ftell(f); fclose(f); v2x_sd_log_write("CSV: %lu KB", (unsigned long)(fsize / 1024)); /* Rotation bei > 50 MB */ if (fsize > 50 * 1024 * 1024) { char rotated[128]; strftime(tbuf, sizeof(tbuf), "%Y%m%d_%H%M%S", tm_info); snprintf(rotated, sizeof(rotated), "/sdcard/v2x_frames_%s.csv", tbuf); ESP_LOGW(TAG, "CSV rotieren: v2x_frames.csv -> %s", rotated); rename("/sdcard/v2x_frames.csv", rotated); f = fopen("/sdcard/v2x_frames.csv", "w"); if (f) { fprintf(f, "timestamp_us,msg_type,channel,frequency_mhz,rssi_dbm,noise_dbm,rate_index,frame_len,mac_addr,bssid,raw_hex\n"); fclose(f); } v2x_sd_log_write("Neue CSV: /sdcard/v2x_frames.csv"); } } v2x_sd_log_write("--- Ende ---\n"); return ESP_OK; }