/** * V2X-Sniffer Firmware für ESP32-C5 (T-Dongle-C5) * * 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/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" #include "v2x_db.h" static const char *TAG = "V2X-Sniffer"; /* ====== 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 */ }; /* 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 /* Frame Queue */ static QueueHandle_t s_frame_queue; /* ====== 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; /* 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; } /* ====== 802.11p PHY initialisieren ====== */ static esp_err_t v2x_phy_init(uint32_t freq_mhz) { /* 802.11p PHY aktivieren */ phy_11p_set(1, 0); /* Kanal schalten */ phy_change_channel(freq_mhz, 1, 0, 0); ESP_LOGI(TAG, "PHY aktiviert: %lu MHz", (unsigned long)freq_mhz); return ESP_OK; } /* ====== 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); /* 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(); } } } /* ====== 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); /* 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_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, "======================================="); }