SD-Karte: V2X-Frames direkt auf SD speichern (CSV, autonom)

- NVS-Flash (4MB) durch SD-Karte via SDMMC ersetzt
- GPIO: SD_CMD=2, SD_CLK=6, SD_D0=7, SD_CS=23
- CSV-Ausgabe mit Rohdaten-Hex: timestamp, msg_type, channel, RSSI, MAC, raw_hex
- Channel-Hopping über alle 10 V2X-Kanäle (5845-5925 MHz)
- CSV-Auto-Rotation bei >50MB
- v2x_log.txt mit periodischen Statistiken
- Frame Queue (64 Frames) zwischen Promisc-CB und Writer Task
- Kanal-Hopper, Stats-Task, Frame-Writer als eigene Tasks
- README mit SD-Vorbereitung und Analyse-Hinweisen
This commit is contained in:
Clawdia
2026-05-09 08:44:24 +02:00
parent fdb4cca5d8
commit 874dd969b6
6 changed files with 755 additions and 146 deletions
+260 -3
View File
@@ -1,9 +1,266 @@
/**
* V2X-SD Driver - SD-Karte via SDMMC (ESP32-C5 native) + CSV-Ausgabe
*
* T-Dongle-C5 SD-Karte:
* SD_CMD = GPIO 2
* SD_CLK = GPIO 6
* SD_D0 = GPIO 7
* SD_CS = GPIO 23
*
* 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 "v2x_db.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <inttypes.h>
static const char *TAG = "v2x_driver";
static const char *TAG = "v2x-sd";
esp_err_t v2x_driver_init(void) {
ESP_LOGI(TAG, "V2X Driver initialisiert");
/* Globale Variablen */
static v2x_stats_t s_stats;
static FILE *s_log_fp = NULL;
static bool s_initialized = false;
/* ====== SD-Karte initialisieren (ESP-IDF v5.x SDMMC, 1-Bit) ====== */
esp_err_t v2x_sd_init(void)
{
esp_err_t ret;
sdmmc_card_t *card = NULL;
FILE *fp;
ESP_LOGI(TAG, "Initialisiere SD-Karte via SDMMC (1-Bit)...");
/* SDMMC Host */
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.flags = SDMMC_HOST_FLAG_1BIT;
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
/* Slot konfigurieren (ESP32-C5 native SDMMC) */
sdmmc_slot_config_t slot = SDMMC_SLOT_DEFAULT_CONFIG();
slot.width = 1;
slot.clk = GPIO_NUM_6;
slot.cmd = GPIO_NUM_2;
slot.d0 = GPIO_NUM_7;
slot.cd = -1; /* Card detect nicht verwendet */
slot.wr = -1; /* Write protect nicht verwendet */
/* FATFS mounten */
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 4,
.allocation_unit_size = 16 * 1024, /* 16 KB Blöcke */
};
ret = esp_vfs_fat_sdmmc_mount(SD_CARD_MOUNT_POINT, &host, &slot, &mount_config, &card);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SD mount fehlgeschlagen: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "SD-Karte erkannt: %lu MB (%s)",
(unsigned long)(card->card_size / (1024 * 1024)),
card->type == SDMMC_TYPE_SDHC ? "SDHC" : "SD");
/* Schreibtest */
fp = fopen(SD_CARD_MOUNT_POINT "/test.txt", "w");
if (fp) {
fprintf(fp, "OK\n");
fclose(fp);
remove(SD_CARD_MOUNT_POINT "/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: %lu MB\n", (unsigned long)(card->card_size / (1024 * 1024)));
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: %s", SD_CARD_MOUNT_POINT);
return ESP_OK;
}
/* ====== 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(SD_CSV_FILE, "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; }
void v2x_sd_reset_stats(void)
{
memset(&s_stats, 0, sizeof(s_stats));
s_stats.start_timestamp_us = esp_timer_get_time();
}
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(SD_CSV_FILE, "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: %s -> %s", SD_CSV_FILE, rotated);
rename(SD_CSV_FILE, rotated);
f = fopen(SD_CSV_FILE, "w");
if (f) {
fprintf(f, "%s\n", CSV_HEADER);
fclose(f);
}
v2x_sd_log_write("Neue CSV: %s", SD_CSV_FILE);
}
}
v2x_sd_log_write("--- Ende ---\n");
return ESP_OK;
}