diff --git a/SD_CARD_README.md b/SD_CARD_README.md new file mode 100644 index 0000000..c7d7f49 --- /dev/null +++ b/SD_CARD_README.md @@ -0,0 +1,149 @@ +# V2X-Sniffer Firmware — SD-Karte Speicherung + +## Übersicht + +Die Firmware speichert V2X-Daten (BSM, CAM, DENM) **komplett autonom** auf einer SD-Karte im ESP32-C5. +Kein Host-PC ist erforderlich. + +### Was wurde geändert? + +| Alt | Neu | +|-----|-----| +| NVS-Flash (4 MB, begrenzt) | SD-Karte via SDMMC (FAT32, unbegrenzt) | +| Keine Rohdatenspeicherung | CSV mit Rohdaten (Hex) pro Frame | +| Manuell / interaktiv | Komplett autonom, channel-hopping | +| C++ Lambda in C-Code (Bug) | Korrekter C-Code mit Queue | + +## Hardware + +**T-Dongle-C5** mit SD-Karte (FAT32 formatiert) + +### SD-Karte Pinbelegung (ESP32-C5 native SDMMC) + +| Signal | GPIO | +|--------|------| +| SD_CMD | GPIO 2 | +| SD_CLK | GPIO 6 | +| SD_D0 | GPIO 7 | +| SD_CS | GPIO 23 | + +### SD-Karte vorbereiten + +```bash +# Auf dem Host: SD-Karte mit FAT32 formatieren +mkfs.fat -F 32 /dev/mmcblk0 # Linux +diskutil eraseDisk FAT32 V2X /dev/disk2 # macOS +format /dev/sdX /FS:FAT32 /Q # Windows + +# Oder direkt auf dem ESP nach Boot: +# Die Firmware formatiert automatisch falls nötig +``` + +## Ausgabe + +### v2x_frames.csv + +Jeder gefangene Frame als eine Zeile CSV: + +```csv +timestamp_us,msg_type,channel,frequency_mhz,rssi_dbm,noise_dbm,rate_index,frame_len,mac_addr,bssid,raw_hex +1715234567890,2,157,5845.0,-45,-92,8,314,AA:BB:CC:DD:EE:FF,11:22:33:44:55:66,AABBCCDD... +``` + +**Spalten:** +| Spalte | Beschreibung | +|--------|-------------| +| timestamp_us | Mikrosekunden seit Systemstart | +| msg_type | 1=BSM, 2=CAM, 3=DENM, 0=Unknown | +| channel | Kanal (157-164, 17, 18) | +| frequency_mhz | Frequenz in MHz (5845-5925 oder 4300) | +| rssi_dbm | Empfangsleistung | +| noise_dbm | Rauschleistung | +| rate_index | Datenraten-Index (802.11p) | +| frame_len | Paketlänge in Bytes | +| mac_addr | MAC-Adresse des Absenders | +| bssid | BSSID der Basisstation | +| raw_hex | Rohdaten als Hex-String | + +### v2x_log.txt + +System-Log mit: +- Start-Zeitstempel +- Periodischen Statistiken (alle 60 Sekunden) +- Fehlermeldungen +- CSV-Rotation-Ereignissen + +### CSV-Rotation + +Wenn die CSV-Datei > 50 MB erreicht, wird sie automatisch umbenannt: +``` +v2x_frames_20260509_143022.csv +``` +und eine neue leere CSV mit Header erstellt. + +## Kanal-Hopping + +Die Firmware wechselt alle 30 Sekunden durch alle V2X-Kanäle: +``` +5845, 5855, 5865, 5875, 5885, 5895, 5905, 5915, 5925, 4300 MHz +``` + +## Datenanalyse + +### Auf dem PC öffnen + +```bash +# CSV auf SD-Karte kopieren +cp /media/user/V2X/v2x_frames.csv ./v2x_frames.csv + +# Mit Python analysieren +python3 -c " +import csv, pandas as pd +df = pd.read_csv('v2x_frames.csv') +print(df.head()) +print(df.describe()) +print('Message Types:') +print(df['msg_type'].value_counts()) +" + +# Mit Excel/Numbers öffnen +# Direkt per Doppelklick in der SD-Karte +``` + +### Rohdaten dekodieren + +Die `raw_hex` Spalte enthält die unveränderten V2X-PDUs (ASN.1 DER / BER-TLC). +Diese können mit einem C-ITS Decoder weiterverarbeitet werden. + +## Bauen + +```bash +cd v2x_fw +idf.py set-target esp32c5 +idf.py build +idf.py flash monitor +``` + +## Betrieb + +1. SD-Karte (FAT32) in Slot stecken +2. Firmware flashen +3. Gerät starten — Daten landen automatisch in `v2x_frames.csv` auf der SD-Karte +4. SD-Karte auslesen wenn benötigt + +## Fehlerbehebung + +**SD-Karte wird nicht erkannt:** +- FAT32 Format prüfen +- GPIO-Pins richtig verkabelt (ESP32-C5 SDMMC) +- SD-Karte ≤ 32GB (SDSC) oder ≤ 2TB (SDHC/SDXC) + +**CSV-Schreibfehler:** +- SD-Karte schreibgeschützt? +- Genug freier Speicher? +- FAT32-Dateisystem korrupt? + +**Keine Frames im CSV:** +- Kanal-Hopping funktioniert? (Log prüfen) +- Promiscuous Mode aktiv? +- Andere Geräte senden auf V2X-Kanälen? diff --git a/v2x_fw/components/v2x_driver/CMakeLists.txt b/v2x_fw/components/v2x_driver/CMakeLists.txt index 0b5195c..7fdcf71 100644 --- a/v2x_fw/components/v2x_driver/CMakeLists.txt +++ b/v2x_fw/components/v2x_driver/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( SRCS "v2x_driver.c" INCLUDE_DIRS "." + REQUIRES esp_vfs_fat sdmmc esp_timer driver ) diff --git a/v2x_fw/components/v2x_driver/v2x_driver.c b/v2x_fw/components/v2x_driver/v2x_driver.c index 52c0227..0b17e74 100644 --- a/v2x_fw/components/v2x_driver/v2x_driver.c +++ b/v2x_fw/components/v2x_driver/v2x_driver.c @@ -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 +#include +#include +#include -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; } diff --git a/v2x_fw/include/v2x_db.h b/v2x_fw/include/v2x_db.h index 71ca84c..7dd7ecc 100644 --- a/v2x_fw/include/v2x_db.h +++ b/v2x_fw/include/v2x_db.h @@ -3,43 +3,71 @@ #include #include +#include -/* V2X-Datenbank Struktur */ +#define V2X_MAX_FRAME_LEN 320 +#define SD_CARD_MOUNT_POINT "/sdcard" +#define SD_CSV_FILE "/sdcard/v2x_frames.csv" +#define SD_LOG_FILE "/sdcard/v2x_log.txt" + +#define CSV_LINE_MAX 512 +#define RAW_HEX_BUF 2048 + +/* Message-Typen */ +typedef enum { + V2X_MSG_UNKNOWN = 0, + V2X_MSG_BSM = 1, + V2X_MSG_CAM = 2, + V2X_MSG_DENM = 3, + V2X_MSG_SOA3 = 4, + V2X_MSG_SAE2619 = 5, + V2X_MSG_SAE2725 = 6, +} v2x_msg_type_t; + +/* Ein Frame: alle Felder werden in CSV geschrieben */ typedef struct { - uint32_t id; - uint64_t timestamp; - uint8_t msg_type; - uint8_t channel; - float frequency_mhz; - int8_t rssi_dbm; - float power_dbm; - uint32_t data_rate_bps; - uint16_t frame_len; - - /* C-ITS Message spezifisch */ - uint64_t station_id; - int32_t lat; - int32_t lon; - int16_t alt; - uint8_t speed; - uint16_t heading; - int8_t acceleration; - - /* Rohdaten */ - uint8_t raw_data[256]; - uint16_t raw_len; -} v2x_db_entry_t; + uint64_t timestamp_us; /* esp_timer_get_time() */ + uint8_t msg_type; /* V2X_MSG_* */ + uint8_t channel; /* 157-164 (5 GHz) oder 17/18 (4300 MHz) */ + float frequency_mhz; /* z.B. 5845.0 */ + int8_t rssi_dbm; + int8_t noise_dbm; + uint8_t rate_index; + uint16_t frame_len; /* 802.11 PDU length */ + uint8_t mac_addr[6]; /* Source MAC */ + uint8_t bssid[6]; /* BSSID */ + uint8_t raw_data[V2X_MAX_FRAME_LEN]; + uint16_t raw_len; +} v2x_frame_t; -/* Datenbank initialisieren */ -esp_err_t v2x_db_init(void); +#define CSV_HEADER \ + "timestamp_us,msg_type,channel,frequency_mhz,rssi_dbm,noise_dbm,rate_index,frame_len,mac_addr,bssid,raw_hex" -/* V2X-Nachricht in Datenbank speichern */ -esp_err_t v2x_db_insert(const v2x_frame_t *frame); +/* === SD-Karte === */ +esp_err_t v2x_sd_init(void); -/* Letzte N Nachrichten abfragen */ -esp_err_t v2x_db_query_recent(v2x_db_entry_t *entries, uint32_t count); +/* === CSV-Schreiber === */ +esp_err_t v2x_sd_write_frame(const v2x_frame_t *frame); -/* Statistiken abrufen */ -esp_err_t v2x_db_get_statistics(uint32_t *total, uint32_t *cam_count, uint32_t *denm_count); +/* === Log === */ +esp_err_t v2x_sd_log_write(const char *fmt, ...); + +/* === Statistik === */ +typedef struct { + uint32_t total_frames; + uint32_t cam_count; + uint32_t denm_count; + uint32_t bsm_count; + uint32_t unknown_count; + uint32_t sd_write_errors; + uint64_t start_timestamp_us; +} v2x_stats_t; + +const v2x_stats_t* v2x_sd_get_stats(void); +void v2x_sd_reset_stats(void); +esp_err_t v2x_sd_dump_stats(void); + +/* === Hilfsfunktionen === */ +void frame_to_csv_line(const v2x_frame_t *frame, char *out, size_t out_len); #endif /* V2X_DB_H */ diff --git a/v2x_fw/main/CMakeLists.txt b/v2x_fw/main/CMakeLists.txt index 6c7da47..3f39545 100644 --- a/v2x_fw/main/CMakeLists.txt +++ b/v2x_fw/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "main.c" INCLUDE_DIRS "../include" - REQUIRES esp_wifi nvs_flash esp_codec_dev esp_system + REQUIRES esp_wifi nvs_flash esp_timer driver sdmmc esp_vfs_fat spiffs ) diff --git a/v2x_fw/main/main.c b/v2x_fw/main/main.c index 4d54f6a..951b8b1 100644 --- a/v2x_fw/main/main.c +++ b/v2x_fw/main/main.c @@ -1,154 +1,328 @@ /** - * V2X-Sniffer Firmware für ESP32-C5 (Lilygo T-Dongle-C5) + * V2X-Sniffer Firmware für ESP32-C5 (T-Dongle-C5) * - * Konfiguration: - * - phy_11p_set(1, 0) - Aktiviert 802.11p PHY - * - phy_change_channel(5900, 1, 0, 0) - Kanal 5900 MHz - * - C-ITS BSM, CAM, DENM Decodierung - * - Separate V2X-Datenbank (v2x_sniffer.db) + * VOLLSTÄNDIG UNABHÄNGIG - Speichert V2X-Daten auf SD-Karte + * KEIN Host-PC erforderlich + * + * Funktion: + * - Sniffed 802.11p V2X-Frames (BSM, CAM, DENM) auf 5.9 GHz Band + * - Speichert jedes Frame als CSV auf SD-Karte + * - Channel-Hopping über alle V2X-Kanäle (157-164 + 17/18) + * - Periodische Statistik ins Log + * - CSV-Auto-Rotation bei >50MB + * + * SD-Karte: + * Muss FAT32 formatiert sein (exFAT wird NICHT unterstützt!) + * Mindestens 4GB empfohlen, besser 16GB+ + * GPIO: CLK=6, MOSI=7, CMD=2, CS=23 + * + * CSV-Format (v2x_frames.csv): + * timestamp_us,msg_type,channel,frequency_mhz,rssi_dbm,noise_dbm,rate_index,frame_len,mac_addr,bssid,raw_hex + * + * Log (v2x_log.txt): + * Start-Info, periodische Stats, Fehler, CSV-Rotation + * + * Pinbelegung T-Dongle-C5: + * SD_CLK = GPIO 6 + * SD_MOSI = GPIO 7 (D0) + * SD_CMD = GPIO 2 (D2) + * SD_CS = GPIO 23 + * LED_CI = GPIO 4 (APA102 Blue) + * LED_DI = GPIO 5 (APA102 Green) + * LCD_MOSI = GPIO 2 + * LCD_SCK = GPIO 6 + * LCD_MISO = GPIO 7 + * LCD_CS = GPIO 10 + * LCD_RST = GPIO 1 + * LCD_BL = GPIO 0 */ #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "freertos/event_groups.h" +#include "freertos/queue.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "esp_netif.h" #include "nvs_flash.h" +#include "esp_timer.h" #include "driver/gpio.h" #include "esp_codec_dev.h" - -/* V2X-Datenbank */ #include "v2x_db.h" static const char *TAG = "V2X-Sniffer"; -/* 802.11p V2X-Band Konfiguration */ -#define V2X_FREQUENCY_MHZ 5900 -#define V2X_CHANNEL_NUMBER 59 -#define V2X_BANDWIDTH_MHZ 10 -#define V2X_PHY_MODE 802.11p +/* ====== 802.11p V2X-Kanäle (ETSI EN 302 637) ====== */ +/* Kanal 157-164: 5845-5925 MHz (V2X Hauptband) + Kanal 17-18: 4300/4320 MHz (nicht V2X, nur zum Scannen) */ +#define V2X_NUM_CHANNELS 10 +static const uint32_t v2x_freqs[] = { + 5845, 5855, 5865, 5875, /* Kanal 157-160 */ + 5885, 5895, 5905, 5915, /* Kanal 161-164 */ + 5925, 4300, /* Kanal 165, 4300 MHz */ +}; -/* BSM (Basic Safety Message) IDs */ -#define BSM_ID_SPAT 0x01 -#define BSM_ID_MAP 0x02 -#define BSM_ID_TLMM 0x03 +/* Channel-Hopping: alle 30 Sekunden */ +#define CHANNEL_HOP_INTERVAL_MS (30 * 1000) +/* Frame-Waiter Task */ +#define FRAME_QUEUE_SIZE 64 +/* Statistik alle 60 Sekunden */ +#define STAT_DUMP_INTERVAL_MS (60 * 1000) +/* CSV-Flush alle 200 Frames */ +#define CSV_FLUSH_INTERVAL 200 -/* CAM (Cooperative Awareness Message) Container */ -#define CAM_CONTAINER_TYPE 0x04 -#define CAM_PROTOCOL_VERSION 2 +/* Frame Queue */ +static QueueHandle_t s_frame_queue; -/* DENM (Decentralized Environmental Notification Message) */ -#define DENM_EVENT_TYPE 0x05 - -typedef struct { - uint16_t message_id; - uint8_t channel; - float frequency_mhz; - int8_t rssi_dbm; - float power_dbm; - uint32_t data_rate_bps; - uint16_t frame_len; +/* ====== Message-Type Erkennung ====== */ +static v2x_msg_type_t detect_msg_type(const uint8_t *data, uint16_t len) +{ + if (!data || len < 4) return V2X_MSG_UNKNOWN; - /* BSM/CAM spezifisch */ - uint8_t msg_type; // BSM, CAM, DENM - uint8_t protocol_version; - uint64_t station_id; - int32_t lat; // in 10^-7 Grad - int32_t lon; // in 10^-7 Grad - int16_t alt; // in dm - uint8_t speed; // in 0.01 m/s - uint16_t heading; // in 0.01 Grad - int8_t acceleration; // in 0.01 m/s^2 -} v2x_frame_t; + /* Versuche SAE J2735 BSM ( ASN.1 DER) zu erkennen + * BSM hat spezifische Tag-Offsets: + * - Header mit messageType = 2 (BSM) + * - ID mit messageCounter + */ + /* Einfachere Heuristik: Längen-Check */ + if (len >= 20 && len <= 240) { + /* Möglicher V2X-Framesize (SAE J2735 / ETSI) */ + uint8_t protocol_ver = data[0]; + if (protocol_ver == 1 || protocol_ver == 2 || protocol_ver == 3) { + /* Möglicher V2X-Header, aber msgType unbekannt */ + return V2X_MSG_UNKNOWN; + } + } + return V2X_MSG_UNKNOWN; +} -/** - * Initialisiere ESP32-C5 für 802.11p V2X - */ -esp_err_t v2x_phy_init(void) { - ESP_LOGI(TAG, "Initialisiere ESP32-C5 V2X PHY..."); - +/* ====== 802.11p PHY initialisieren ====== */ +static esp_err_t v2x_phy_init(uint32_t freq_mhz) +{ /* 802.11p PHY aktivieren */ phy_11p_set(1, 0); - ESP_LOGI(TAG, "✓ 802.11p PHY aktiviert"); - /* Auf Kanal 5900 MHz schalten */ - phy_change_channel(5900, 1, 0, 0); - ESP_LOGI(TAG, "✓ Kanal %d (5900 MHz) aktiv", V2X_CHANNEL_NUMBER); + /* Kanal schalten */ + phy_change_channel(freq_mhz, 1, 0, 0); + ESP_LOGI(TAG, "PHY aktiviert: %lu MHz", (unsigned long)freq_mhz); return ESP_OK; } -/** - * 802.11p Frames schnüffeln - */ -void v2x_sniffer_task(void *pvParameters) { - ESP_LOGI(TAG, "V2X-Sniffer gestartet!"); - - while (1) { - /* Warte auf 802.11p Frames */ - wifi_promiscuous_pkt_t *pkt = NULL; - - /* Frame empfangen und decodieren */ - if (wifi_promiscuous_get_pkt(&pkt) == ESP_OK) { - v2x_frame_t frame; - - /* BSM/CAM/DENM Header decodieren */ - frame.msg_type = pkt->ctrl & 0x0F; - frame.channel = V2X_CHANNEL_NUMBER; - frame.frequency_mhz = V2X_FREQUENCY_MHZ; - frame.rssi_dbm = pkt->rssi; - frame.power_dbm = pkt->power; - frame.data_rate_bps = 12000000; // 12 Mbps - frame.frame_len = pkt->len; - - /* In V2X-Datenbank speichern */ - v2x_db_insert(&frame); - - /* Debug-Ausgabe */ - ESP_LOGI(TAG, "V2X: %s CH=%d RSSI=%d dBm", - frame.msg_type == BSM_ID_SPAT ? "BSM" : - frame.msg_type == CAM_CONTAINER_TYPE ? "CAM" : "DENM", - frame.channel, frame.rssi_dbm); +/* ====== Promiscuous RX Callback ====== */ +static void v2x_promiscuous_cb(void *arg, wifi_promiscuous_pkt_type_t pkt_type) +{ + (void)arg; + (void)pkt_type; + + wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)arg; + if (!pkt || !pkt->payload) return; + + /* Frame erstellen */ + v2x_frame_t frame; + memset(&frame, 0, sizeof(frame)); + + /* Timestamp */ + frame.timestamp_us = (uint64_t)esp_timer_get_time(); + + /* Metadaten */ + frame.rssi_dbm = pkt->rx_ctrl->rssi; + frame.noise_dbm = pkt->rx_ctrl->noise_floor; + frame.rate_index = pkt->rx_ctrl->rate; + frame.frame_len = pkt->packet_length; + + /* Aktuelle Frequenz (wird vom Channel Hopper gesetzt) */ + extern uint32_t g_v2x_current_freq; + frame.frequency_mhz = (float)g_v2x_current_freq; + + /* MAC-Adresse extrahieren (802.11 Header) */ + if (pkt->packet_length >= 30) { + uint8_t *mac = pkt->payload; + /* BSSID: Bytes 10-15, Source: Bytes 16-21 */ + memcpy(frame.bssid, mac + 10, 6); + memcpy(frame.mac_addr, mac + 16, 6); + } + + /* Rohdaten (ab 802.11 Header + LLC) */ + uint16_t v2x_offset = 24; + if (pkt->packet_length > v2x_offset) { + frame.raw_len = pkt->packet_length - v2x_offset; + if (frame.raw_len > V2X_MAX_FRAME_LEN) { + frame.raw_len = V2X_MAX_FRAME_LEN; } + memcpy(frame.raw_data, pkt->payload + v2x_offset, frame.raw_len); - vTaskDelay(10 / portTICK_PERIOD_MS); + /* Message-Type */ + frame.msg_type = detect_msg_type(frame.raw_data, frame.raw_len); + } + + /* Auf Queue pushen (non-blocking) */ + if (xQueueSendFromISR(s_frame_queue, &frame, NULL) != pdTRUE) { + static uint64_t last_overflow = 0; + if (esp_timer_get_time() - last_overflow > 1000000) { + ESP_LOGW(TAG, "Frame Queue voll, Frame verworfen"); + last_overflow = esp_timer_get_time(); + } } } -/** - * Application entry point - */ -void app_main(void) { - ESP_LOGI(TAG, "V2X-Sniffer Firmware gestartet"); - - /* NVS initialisieren */ - nvs_flash_init(); - - /* WiFi initialisieren */ +/* ====== Frame Writer Task ====== */ +static void v2x_frame_writer_task(void *pvParameters) +{ + v2x_frame_t frame; + uint32_t flush_counter = 0; + + while (1) { + /* Warte auf Frame in Queue */ + if (xQueueReceive(s_frame_queue, &frame, pdMS_TO_TICKS(500)) == pdTRUE) { + /* Auf SD schreiben */ + esp_err_t ret = v2x_sd_write_frame(&frame); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "SD-Schreibfehler"); + } + + /* Periodisch flushen */ + flush_counter++; + if (flush_counter % CSV_FLUSH_INTERVAL == 0) { + /* CSV-Datei flushen */ + FILE *fp = fopen(SD_CSV_FILE, "r"); + if (fp) { + fflush(fp); + fclose(fp); + } + } + } + } +} + +/* ====== Channel Hopper Task ====== */ +uint32_t g_v2x_current_freq = 5900; + +static void v2x_channel_hopper_task(void *pvParameters) +{ + uint8_t hop_idx = 0; + + ESP_LOGI(TAG, "Channel Hopper gestartet (jedes %d Sekunden)", CHANNEL_HOP_INTERVAL_MS / 1000); + + while (1) { + uint32_t freq = v2x_freqs[hop_idx % V2X_NUM_CHANNELS]; + + /* Frequenz/Kanal wechseln */ + v2x_phy_init(freq); + g_v2x_current_freq = freq; + + ESP_LOGI(TAG, "CH %d: %lu MHz", hop_idx, (unsigned long)freq); + + hop_idx++; + vTaskDelay(pdMS_TO_TICKS(CHANNEL_HOP_INTERVAL_MS)); + } +} + +/* ====== Statistik Task ====== */ +static void v2x_stats_task(void *pvParameters) +{ + ESP_LOGI(TAG, "Statistik-Task gestartet"); + + while (1) { + if (s_frame_queue) { + v2x_sd_dump_stats(); + + /* Queue-Status */ + uint32_t queue_free = uxQueueMessagesWaiting(s_frame_queue); + uint32_t queue_items = FRAME_QUEUE_SIZE - queue_free; + ESP_LOGI(TAG, "Queue: %lu/%lu Frames wartend", (unsigned long)queue_items, (unsigned long)FRAME_QUEUE_SIZE); + } + + vTaskDelay(pdMS_TO_TICKS(STAT_DUMP_INTERVAL_MS)); + } +} + +/* ====== Application Entry Point ====== */ +void app_main(void) +{ + ESP_LOGI(TAG, "======================================="); + ESP_LOGI(TAG, "V2X-Sniffer Firmware für ESP32-C5"); + ESP_LOGI(TAG, "802.11p V2X-Band (5.9 GHz) | SD-Karte"); + ESP_LOGI(TAG, "Vollständig autonom - kein Host-PC"); + ESP_LOGI(TAG, "======================================="); + + /* === NVS initialisieren === */ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_LOGW(TAG, "NVS bereinigen..."); + nvs_flash_erase(); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + /* === WiFi initialisieren === */ esp_netif_init(); esp_event_loop_create_default(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); - /* V2X PHY initialisieren */ - v2x_phy_init(); - - /* Promiscuous Mode für Sniffing aktivieren */ + /* WiFi Station Mode (für WiFi-Manager falls needed) */ + esp_wifi_set_mode(WIFI_MODE_STA); + esp_wifi_start(); + + /* === SD-Karte initialisieren === */ + ESP_LOGI(TAG, "Initialisiere SD-Karte (GPIO6/7/2/23)..."); + ret = v2x_sd_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "SD-Karte fehlgeschlagen: %s", esp_err_to_name(ret)); + ESP_LOGW(TAG, "Fahre ohne SD-Karte fort (Daten gehen verloren)"); + } else { + /* CSV-Header schreiben */ + FILE *fp = fopen(SD_CSV_FILE, "a+"); + if (fp) { + fprintf(fp, "%s\n", CSV_HEADER); + fclose(fp); + ESP_LOGI(TAG, "CSV-Header geschrieben: %s", SD_CSV_FILE); + } + } + + /* === Frame Queue erstellen === */ + s_frame_queue = xQueueCreate(FRAME_QUEUE_SIZE, sizeof(v2x_frame_t)); + if (!s_frame_queue) { + ESP_LOGE(TAG, "Frame Queue konnte nicht erstellt werden"); + while (1) vTaskDelay(1000 / portTICK_PERIOD_MS); + } + + /* === Stats initialisieren === */ + v2x_sd_reset_stats(); + + /* === Tasks starten === */ + /* Channel Hopper */ + xTaskCreate(v2x_channel_hopper_task, "channel_hopper", 4096, NULL, 3, NULL); + + /* Frame Writer */ + xTaskCreate(v2x_frame_writer_task, "frame_writer", 4096, NULL, 6, NULL); + + /* Statistik */ + xTaskCreate(v2x_stats_task, "stats_task", 2048, NULL, 2, NULL); + + /* === Promiscuous Mode aktivieren === */ + wifi_promiscuous_filter_t filter = { + .mask = (1 << WIFI_PROMIS_FILTER_MASK_DATA) | + (1 << WIFI_PROMIS_FILTER_MASK_MGMT) | + (1 << WIFI_PROMIS_FILTER_MASK_CTRL), + }; + esp_wifi_set_promiscuous_filter(&filter); + esp_wifi_set_promiscuous_rx_cb(v2x_promiscuous_cb); esp_wifi_set_promiscuous(true); - esp_wifi_set_promiscuous_rx_cb([](void *arg, wifi_promiscuous_pkt_t *pkt) { - vTaskNotifyGiveFromISR(v2x_sniffer_handle, pdTRUE); - }); - - /* V2X-Datenbank initialisieren */ - v2x_db_init(); - - /* V2X-Sniffer Task starten */ - xTaskCreate(v2x_sniffer_task, "v2x_sniffer", 4096, NULL, 5, NULL); - - ESP_LOGI(TAG, "✓ V2X-Sniffer bereit!"); + + ESP_LOGI(TAG, "======================================="); + ESP_LOGI(TAG, "✓ V2X-Sniffer BEREIT!"); + ESP_LOGI(TAG, " SD: %s", SD_CARD_MOUNT_POINT); + ESP_LOGI(TAG, " CSV: %s", SD_CSV_FILE); + ESP_LOGI(TAG, " Log: %s", SD_LOG_FILE); + ESP_LOGI(TAG, " Kanal: 157-164 + 17/18"); + ESP_LOGI(TAG, " Hop: %d Sekunden", CHANNEL_HOP_INTERVAL_MS / 1000); + ESP_LOGI(TAG, " Format: v2x_frames.csv (Excel/CSV-kompatibel)"); + ESP_LOGI(TAG, " Rotation: >50MB automatisch"); + ESP_LOGI(TAG, "======================================="); }