From 240a37414854fa4106720ef1df890d8d20d51aec Mon Sep 17 00:00:00 2001 From: Clawdia Date: Sat, 9 May 2026 11:47:55 +0200 Subject: [PATCH] Display UI: ST7735 Treiber + Frame-Scroller + Speicheranzeige MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ST7735 SPI-Treiber für 0.96 Zoll Display (80x160 Pixel) - ASCII-Font 6x8 mit Text-Ausgabe - Frame-Counter (gross, rot) - SD-Speicher-Balken (grün/gelb/rot) - Frame-Scroller: rollende Liste der letzten 7 Frames - RSSI-Farbbalken (rot/gelb/grün) - Message-Type (B=BSM, C=CAM, D=DENM) - BSSID Hex + Kanal/RSSI - Display UI Task aktualisiert alle 2 Sekunden - Frame Writer sendet Frames an Scroller Queue - Channel Hopper aktualisiert g_v2x_channel_idx --- v2x_fw/components/v2x_driver/CMakeLists.txt | 7 +- v2x_fw/components/v2x_driver/v2x_display.c | 258 ++++++++++++++++++ .../v2x_driver/v2x_display_scroller.c | 175 ++++++++++++ .../components/v2x_driver/v2x_display_text.c | 192 +++++++++++++ v2x_fw/components/v2x_driver/v2x_display_ui.c | 90 ++++++ v2x_fw/include/v2x_display.h | 93 +++++++ v2x_fw/main/main.c | 84 +++++- 7 files changed, 896 insertions(+), 3 deletions(-) create mode 100644 v2x_fw/components/v2x_driver/v2x_display.c create mode 100644 v2x_fw/components/v2x_driver/v2x_display_scroller.c create mode 100644 v2x_fw/components/v2x_driver/v2x_display_text.c create mode 100644 v2x_fw/components/v2x_driver/v2x_display_ui.c create mode 100644 v2x_fw/include/v2x_display.h diff --git a/v2x_fw/components/v2x_driver/CMakeLists.txt b/v2x_fw/components/v2x_driver/CMakeLists.txt index 7fdcf71..8c9f5fa 100644 --- a/v2x_fw/components/v2x_driver/CMakeLists.txt +++ b/v2x_fw/components/v2x_driver/CMakeLists.txt @@ -1,5 +1,8 @@ idf_component_register( SRCS "v2x_driver.c" - INCLUDE_DIRS "." - REQUIRES esp_vfs_fat sdmmc esp_timer driver + "v2x_display.c" + "v2x_display_text.c" + "v2x_display_scroller.c" + INCLUDE_DIRS "." ".." + REQUIRES esp_vfs_fat sdmmc esp_timer driver esp_lcd esp_lcd_st7735 ) diff --git a/v2x_fw/components/v2x_driver/v2x_display.c b/v2x_fw/components/v2x_driver/v2x_display.c new file mode 100644 index 0000000..6f00363 --- /dev/null +++ b/v2x_fw/components/v2x_driver/v2x_display.c @@ -0,0 +1,258 @@ +/** + * V2X-Display Driver - ST7735 SPI (T-Dongle-C5) + * + * Display: ST7735, 80x160 Pixel + * SPI-Pins: CS=GPIO10, RST=GPIO1, SDA(MOSI)=GPIO2, SCK=GPIO6 + * LCD-Backlight: GPIO0 + * + * Statusanzeige: + * - Oben: SD-Speicher in %, Frame-Counter + * - Unten: rollende Liste der letzten X Frames (RSSI-Balken + msg_type) + */ + +#include "esp_log.h" +#include "v2x_display.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lvgl_port.h" +#include "driver/gpio.h" +#include + +static const char *TAG = "v2x-display"; + +/* ST7735 Kommandos */ +#define ST7735_NOP 0x00 +#define ST7735_SWRESET 0x01 +#define ST7735_SLPIN 0x10 +#define ST7735_SLPOUT 0x11 +#define ST7735_PTLON 0x12 +#define ST7735_NORON 0x13 +#define ST7735_INVOFF 0x20 +#define ST7735_INVON 0x21 +#define ST7735_DIS5520 0x29 +#define ST7735_MADCTL 0x36 +#define ST7735_COLMOD 0x3A +#define ST7735_CASET 0x2A +#define ST7735_RASET 0x2B +#define ST7735_RAMWR 0x2C + +static esp_lcd_panel_handle_t s_panel = NULL; +static bool s_initialized = false; + +/* ST7735 Init-Sequenz (TFT 0.96 Zoll 80x160, SPI, ROTG) */ +static const esp_lcd_panel_cmd_init_t st7735_init[] = { + {0x11, (uint8_t[]){0}, 0, 120}, /* SWRESET */ + {0x3A, (uint8_t[]){0x55}, 1, 0}, /* COLMOD=16bit */ + {0x36, (uint8_t[]){0x08}, 1, 0}, /* MADCTL (portrait) */ + {0x21, (uint8_t[]){}, 0, 0}, /* INVOFF */ + {0x29, (uint8_t[]){}, 0, 0}, /* DISON */ +}; + +esp_err_t v2x_display_init(void) +{ + esp_err_t ret; + + ESP_LOGI(TAG, "Initialisiere ST7735 Display..."); + + /* Backlight an */ + gpio_config_t bl_conf = { + .pin_bit_mask = (1ULL << DISP_GPIO_BL), + .mode = GPIO_MODE_OUTPUT, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .pull_up_en = GPIO_PULLUP_DISABLE, + }; + gpio_config(&bl_conf); + gpio_set_level(DISP_GPIO_BL, 1); + + /* Reset-Pin */ + gpio_config_t rst_conf = { + .pin_bit_mask = (1ULL << DISP_GPIO_RST), + .mode = GPIO_MODE_OUTPUT, + }; + gpio_config(&rst_conf); + + /* SPI für LCD */ + spi_bus_config_t bus_cfg = { + .mosi_io_num = DISP_GPIO_SDA, /* GPIO2 */ + .miso_io_num = -1, /* LCD braucht kein MISO */ + .sclk_io_num = DISP_GPIO_SCK, /* GPIO6 */ + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = DISP_W * DISP_H * sizeof(uint16_t), + }; + + ret = spi_bus_initialize(SPI3_HOST, &bus_cfg, 1); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "spi_bus_init fehlgeschlagen"); + return ret; + } + + /* LCD-Panel konfigurieren */ + esp_lcd_panel_dev_config_t panel_cfg = { + .reset_gpio_num = DISP_GPIO_RST, + .color_space = ESP_LCD_COLOR_SPACE_RGB, + .bits_per_pixel = 16, + }; + + ret = esp_lcd_new_panel_st7735(NULL, &panel_cfg, &s_panel); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "ST7735 Panel konnte nicht erstellt werden"); + return ret; + } + + /* Panel initialisieren */ + ret = esp_lcd_panel_reset(s_panel); + if (ret != ESP_OK) goto err; + + ret = esp_lcd_panel_init(s_panel); + if (ret != ESP_OK) goto err; + + ret = esp_lcd_panel_disp_on_off(s_panel, true); + if (ret != ESP_OK) goto err; + + /* Komplette Init-Sequenz (falls ST7735 nicht korrekt init) */ + ret = esp_lcd_panel_cmd_init(s_panel, st7735_init, sizeof(st7735_init)/sizeof(st7735_init[0])); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "ST7735 Init-Kommandos fehlgeschlagen: %s", esp_err_to_name(ret)); + } + + s_initialized = true; + g_disp_initialized = true; + g_disp_panel = s_panel; + ESP_LOGI(TAG, "ST7735 Display bereit (80x160, SPI)"); + return ESP_OK; + +err: + ESP_LOGE(TAG, "Display-Init fehlgeschlagen"); + return ret; +} + +esp_err_t v2x_display_clear(uint16_t color) +{ + if (!s_initialized) return ESP_ERR_INVALID_STATE; + + esp_lcd_panel_draw_bitmap(s_panel, 0, 0, DISP_W, DISP_H, &color); + return ESP_OK; +} + +esp_err_t v2x_display_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, bool filled) +{ + if (!s_initialized) return ESP_ERR_INVALID_STATE; + + uint8_t *buf = malloc(w * h * sizeof(uint16_t)); + if (!buf) return ESP_ERR_NO_MEM; + + for (int i = 0; i < w * h; i++) { + ((uint16_t *)buf)[i] = color; + } + + esp_lcd_panel_draw_bitmap(s_panel, x, y, x + w, y + h, buf); + free(buf); + return ESP_OK; +} + +esp_err_t v2x_display_line(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) +{ + /* Einfacher Bresenham-Linienzeiger für 1-Pixel Linien */ + if (!s_initialized) return ESP_ERR_INVALID_STATE; + + int dx = abs(x1 - x0); + int dy = abs(y1 - y0); + int sx = (x0 < x1) ? 1 : -1; + int sy = (y0 < y1) ? 1 : -1; + int err = dx - dy; + + while (true) { + esp_lcd_panel_draw_bitmap(s_panel, x0, y0, x0 + 1, y0 + 1, &color); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 > -dy) { err -= dy; x0 += sx; } + if (e2 < dx) { err += dx; y0 += sy; } + } + return ESP_OK; +} + +esp_err_t v2x_display_off(void) +{ + if (!s_initialized) return ESP_ERR_INVALID_STATE; + + gpio_set_level(DISP_GPIO_BL, 0); + if (s_panel) esp_lcd_panel_disp_on_off(s_panel, false); + return ESP_OK; +} + +/* Fortschrittsbalken: progress 0.0 - 1.0 */ +esp_err_t v2x_display_progress_bar(int16_t x, int16_t y, int16_t w, int16_t h, float progress, + uint16_t fg_color, uint16_t bg_color) +{ + progress = (progress < 0.0f) ? 0.0f : (progress > 1.0f) ? 1.0f : progress; + + /* Hintergrund */ + v2x_display_rect(x, y, w, h, bg_color, true); + + /* Vordergrund */ + int16_t fw = (int16_t)(w * progress); + if (fw > 0) { + v2x_display_rect(x, y, fw, h, fg_color, true); + } + + /* Rahmen */ + for (int i = 0; i < w; i++) { + v2x_display_line(x + i, y, x + i, y + h, DISP_COLOR_BLACK); + } + for (int i = 0; i < h; i++) { + v2x_display_line(x, y + i, x + w, y + i, DISP_COLOR_BLACK); + } + return ESP_OK; +} + +/* RSSI-Balken: -100 bis -20 dBm → 0-100% */ +esp_err_t v2x_display_rssi_gauge(int8_t rssi) +{ + if (rssi < -100) rssi = -100; + if (rssi > -20) rssi = -20; + + float pct = (float)(rssi + 100) / 80.0f; + + /* Farbe: rot → gelb → grün */ + uint16_t color = DISP_COLOR_RED; + if (pct > 0.6f) color = DISP_COLOR_GREEN; + else if (pct > 0.3f) color = DISP_COLOR_YELLOW; + + return v2x_display_progress_bar(0, 0, DISP_W, 3, pct, color, DISP_COLOR_BLACK); +} + +/* Frame-Info als Zeile rendern */ +esp_err_t v2x_display_render_frame_line(int16_t x, int16_t y, const disp_frame_info_t *info, + uint16_t bg_color) +{ + /* Background */ + v2x_display_rect(x, y, DISP_W, 12, bg_color, true); + + /* RSSI-Farbring links (5 Pixel breit) */ + if (info->rssi != 0) { + int8_t r = info->rssi; + if (r < -100) r = -100; + if (r > -20) r = -20; + float pct = (float)(r + 100) / 80.0f; + uint16_t bar_color = DISP_COLOR_RED; + if (pct > 0.6f) bar_color = DISP_COLOR_GREEN; + else if (pct > 0.3f) bar_color = DISP_COLOR_YELLOW; + v2x_display_rect(x, y + 1, 5, 10, bar_color, true); + } + + /* Message-Typ-Icon */ + char msg_char = '?'; + uint16_t msg_color = DISP_COLOR_GRAY; + switch (info->msg_type) { + case 1: msg_char = 'B'; msg_color = DISP_COLOR_GREEN; break; /* BSM */ + case 2: msg_char = 'C'; msg_color = DISP_COLOR_CYAN; break; /* CAM */ + case 3: msg_char = 'D'; msg_color = DISP_COLOR_RED; break; /* DENM */ + } + + /* Zeichen: msg_char */ + /* (Wird in Schritt 2: Font-Ausgabe implementiert) */ + (void)msg_color; (void)x; (void)y; + + return ESP_OK; +} diff --git a/v2x_fw/components/v2x_driver/v2x_display_scroller.c b/v2x_fw/components/v2x_driver/v2x_display_scroller.c new file mode 100644 index 0000000..477097f --- /dev/null +++ b/v2x_fw/components/v2x_driver/v2x_display_scroller.c @@ -0,0 +1,175 @@ +/** + * Frame-Scroller — rollende Liste der letzten Frames + * + * Zeigt bis zu 7 Frames an, jeder Frame = 1 Zeile (12px hoch): + * [RSSI-Balk] X #####-#####-##### CH:161 RSSI:-45 + * + * Der Scroller aktualisiert sich nur wenn sich ein Frame ändert. + */ + +#include "esp_log.h" +#include "v2x_display.h" +#include "esp_lcd_panel_ops.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" + +extern esp_lcd_panel_handle_t s_panel; +extern bool s_initialized; + +static const char *TAG = "v2x-scroller"; + +/* Scroller-Konfiguration */ +#define SCROLLER_FRAMES_VISIBLE 7 /* Frames sichtbar */ +#define SCROLLER_LINE_HEIGHT 13 /* Pixel pro Zeile */ +#define SCROLLER_START_Y 28 /* Erste Zeile beginnt bei Y=28 */ +#define SCROLLER_MAX_ENTRIES 64 /* Frames im Buffer */ + +/* Frame-Daten für den Scroller */ +typedef struct { + char msg_char; /* 'B', 'C', 'D', '?' */ + int8_t rssi; /* RSSI dBm */ + uint8_t ch; /* Kanal */ + char bssid_hex[9]; /* #####-#####-##### */ +} scroller_entry_t; + +static scroller_entry_t s_entries[SCROLLER_MAX_ENTRIES]; +static uint32_t s_entry_count = 0; + +/* RSSI-Balk als ASCII (20 Zeichen breit) */ +static void rssi_to_bar(int8_t rssi, char *bar, int len) +{ + if (rssi <= -100) rssi = -100; + if (rssi >= -20) rssi = -20; + + float pct = (float)(rssi + 100) / 80.0f; + int filled = (int)(pct * 20); + + for (int i = 0; i < len - 1; i++) { + bar[i] = (i < filled) ? '\x5C' : '\xC1'; /* ▌ und ░ (block chars) */ + } + bar[len - 1] = '\0'; +} + +/* BSSID als #####-#####-##### hex */ +static void bssid_to_str(const uint8_t *bssid, char *str) +{ + snprintf(str, 14, "%02X-%02X-%02X-%02X-%02X-%02X", + bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); +} + +/* Scroller-Eintrag hinzufügen */ +void scroller_add_entry(uint8_t msg_type, int8_t rssi, uint8_t ch, const uint8_t *bssid) +{ + scroller_entry_t *e = &s_entries[s_entry_count % SCROLLER_MAX_ENTRIES]; + + /* Msg-Type Character */ + switch (msg_type) { + case 1: e->msg_char = 'B'; break; /* BSM */ + case 2: e->msg_char = 'C'; break; /* CAM */ + case 3: e->msg_char = 'D'; break; /* DENM */ + default: e->msg_char = '?'; break; + } + + e->rssi = rssi; + e->ch = ch; + if (bssid) { + bssid_to_str(bssid, e->bssid_hex); + } else { + snprintf(e->bssid_hex, 14, "----------"); + } + + s_entry_count++; +} + +/* Scroller neu zeichnen (vollständig) */ +void scroller_redraw(void) +{ + if (!s_initialized) return; + + int32_t total = s_entry_count; + int start = (total > SCROLLER_FRAMES_VISIBLE) ? + (total - SCROLLER_FRAMES_VISIBLE) : 0; + + /* Hintergrund zeichnen */ + int total_h = SCROLLER_FRAMES_VISIBLE * SCROLLER_LINE_HEIGHT; + v2x_display_rect(0, SCROLLER_START_Y, DISP_W, total_h, DISP_COLOR_BLACK, true); + + /* Zeilenweise rendern (von oben nach unten = neueste Frames zuerst) */ + for (int i = SCROLLER_FRAMES_VISIBLE - 1; i >= 0; i--) { + int idx = (start + i) % SCROLLER_MAX_ENTRIES; + scroller_entry_t *e = &s_entries[idx]; + int y = SCROLLER_START_Y + (SCROLLER_FRAMES_VISIBLE - 1 - i) * SCROLLER_LINE_HEIGHT; + + /* RSSI-Farbbalk links (5px breit, 11px hoch) */ + uint16_t bar_color = DISP_COLOR_RED; + float pct = (float)(e->rssi + 100) / 80.0f; + if (pct > 0.6f) bar_color = DISP_COLOR_GREEN; + else if (pct > 0.3f) bar_color = DISP_COLOR_YELLOW; + + v2x_display_rect(2, y + 1, 5, 11, bar_color, true); + + /* Msg-Type Zeichen (grün/cyan/rot/gray) */ + uint16_t msg_color = DISP_COLOR_GRAY; + if (e->msg_char == 'B') msg_color = DISP_COLOR_GREEN; + else if (e->msg_char == 'C') msg_color = DISP_COLOR_CYAN; + else if (e->msg_char == 'D') msg_color = DISP_COLOR_RED; + + char msg_line[2] = { e->msg_char, '\0' }; + v2x_display_text(9, y + 1, msg_line, msg_color, FONT_6x8); + + /* BSSID als Hex (blau) */ + v2x_display_text(16, y + 1, e->bssid_hex, DISP_COLOR_BLUE, FONT_6x8); + + /* Kanal + RSSI (weiß) */ + char ch_buf[20]; + snprintf(ch_buf, sizeof(ch_buf), "CH:%d %3d", e->ch, e->rssi); + v2x_display_text(60, y + 1, ch_buf, DISP_COLOR_WHITE, FONT_6x8); + } +} + +/* Geänderte Zeile neu zeichnen (nur die betroffene Zeile — schnell) */ +void scroller_redraw_line(uint32_t entry_idx) +{ + if (!s_initialized) return; + + int32_t total = s_entry_count; + if (entry_idx < (total > SCROLLER_FRAMES_VISIBLE ? total - SCROLLER_FRAMES_VISIBLE : 0)) { + return; /* Zu alt, nicht sichtbar */ + } + + int start = (total > SCROLLER_FRAMES_VISIBLE) ? + (total - SCROLLER_FRAMES_VISIBLE) : 0; + + int line_idx = (int)(entry_idx - start); + if (line_idx < 0 || line_idx >= SCROLLER_FRAMES_VISIBLE) return; + + int idx = entry_idx % SCROLLER_MAX_ENTRIES; + scroller_entry_t *e = &s_entries[idx]; + int y = SCROLLER_START_Y + (SCROLLER_FRAMES_VISIBLE - 1 - line_idx) * SCROLLER_LINE_HEIGHT; + + /* Zeile löschen */ + v2x_display_rect(0, y, DISP_W, SCROLLER_LINE_HEIGHT, DISP_COLOR_BLACK, true); + + /* Neu zeichnen */ + uint16_t bar_color = DISP_COLOR_RED; + float pct = (float)(e->rssi + 100) / 80.0f; + if (pct > 0.6f) bar_color = DISP_COLOR_GREEN; + else if (pct > 0.3f) bar_color = DISP_COLOR_YELLOW; + + v2x_display_rect(2, y + 1, 5, 11, bar_color, true); + + uint16_t msg_color = DISP_COLOR_GRAY; + if (e->msg_char == 'B') msg_color = DISP_COLOR_GREEN; + else if (e->msg_char == 'C') msg_color = DISP_COLOR_CYAN; + else if (e->msg_char == 'D') msg_color = DISP_COLOR_RED; + + char msg_line[2] = { e->msg_char, '\0' }; + v2x_display_text(9, y + 1, msg_line, msg_color, FONT_6x8); + + v2x_display_text(16, y + 1, e->bssid_hex, DISP_COLOR_BLUE, FONT_6x8); + + char ch_buf[20]; + snprintf(ch_buf, sizeof(ch_buf), "CH:%d %3d", e->ch, e->rssi); + v2x_display_text(60, y + 1, ch_buf, DISP_COLOR_WHITE, FONT_6x8); +} diff --git a/v2x_fw/components/v2x_driver/v2x_display_text.c b/v2x_fw/components/v2x_driver/v2x_display_text.c new file mode 100644 index 0000000..e4f28c8 --- /dev/null +++ b/v2x_fw/components/v2x_driver/v2x_display_text.c @@ -0,0 +1,192 @@ +/** + * ASCII-Font (6x8 Pixel) + Text-Ausgabe + */ + +#include "esp_log.h" +#include "v2x_display.h" +#include "esp_lcd_panel_ops.h" + +extern esp_lcd_panel_handle_t s_panel; +extern bool s_initialized; + +static const uint8_t font6x8[][6] = { + {0x00,0x00,0x00,0x00,0x00,0x00}, // space + {0x00,0x00,0x5F,0x00,0x00,0x00}, // ! + {0x00,0x07,0x00,0x07,0x00,0x00}, // " + {0x14,0x7F,0x14,0x7F,0x14,0x00}, // # + {0x24,0x2A,0x7F,0x2A,0x12,0x00}, // $ + {0x23,0x13,0x08,0x64,0x62,0x00}, // % + {0x36,0x49,0x55,0x22,0x50,0x00}, // & + {0x00,0x05,0x03,0x00,0x00,0x00}, // ' + {0x00,0x1C,0x22,0x41,0x00,0x00}, // ( + {0x00,0x41,0x22,0x1C,0x00,0x00}, // ) + {0x14,0x08,0x3E,0x08,0x14,0x00}, // * + {0x08,0x08,0x3E,0x08,0x08,0x00}, // + + {0x00,0x50,0x30,0x00,0x00,0x00}, // , + {0x08,0x08,0x08,0x08,0x08,0x00}, // - + {0x00,0x60,0x60,0x00,0x00,0x00}, // . + {0x20,0x10,0x08,0x04,0x02,0x00}, // / + {0x3E,0x51,0x49,0x45,0x3E,0x00}, // 0 + {0x00,0x42,0x7F,0x40,0x00,0x00}, // 1 + {0x42,0x61,0x51,0x49,0x46,0x00}, // 2 + {0x21,0x41,0x45,0x4B,0x31,0x00}, // 3 + {0x18,0x14,0x12,0x7F,0x10,0x00}, // 4 + {0x27,0x45,0x45,0x45,0x39,0x00}, // 5 + {0x3C,0x4A,0x49,0x49,0x30,0x00}, // 6 + {0x01,0x71,0x09,0x05,0x03,0x00}, // 7 + {0x36,0x49,0x49,0x49,0x36,0x00}, // 8 + {0x06,0x49,0x49,0x29,0x1E,0x00}, // 9 + {0x00,0x36,0x36,0x00,0x00,0x00}, // : + {0x00,0x56,0x36,0x00,0x00,0x00}, // ; + {0x08,0x14,0x22,0x41,0x00,0x00}, // < + {0x14,0x14,0x14,0x14,0x14,0x00}, // = + {0x00,0x41,0x22,0x14,0x08,0x00}, // > + {0x02,0x01,0x51,0x09,0x06,0x00}, // ? + {0x3E,0x41,0x5D,0x55,0x1E,0x00}, // A + {0x7F,0x09,0x09,0x09,0x7E,0x00}, // B + {0x7F,0x49,0x49,0x49,0x36,0x00}, // C + {0x3E,0x41,0x41,0x21,0x1E,0x00}, // D + {0x7F,0x49,0x49,0x49,0x41,0x00}, // E + {0x7F,0x49,0x49,0x45,0x43,0x00}, // F + {0x7F,0x49,0x49,0x49,0x38,0x00}, // G + {0x7F,0x08,0x08,0x08,0x7F,0x00}, // H + {0x00,0x41,0x7F,0x41,0x00,0x00}, // I + {0x20,0x40,0x41,0x3F,0x01,0x00}, // J + {0x7F,0x08,0x14,0x22,0x41,0x00}, // K + {0x7F,0x40,0x40,0x40,0x40,0x00}, // L + {0x7F,0x02,0x0C,0x02,0x7F,0x00}, // M + {0x7F,0x04,0x08,0x10,0x7F,0x00}, // N + {0x3E,0x41,0x41,0x41,0x3E,0x00}, // O + {0x7F,0x09,0x09,0x09,0x06,0x00}, // P + {0x3E,0x41,0x51,0x21,0x5E,0x00}, // Q + {0x7F,0x09,0x19,0x29,0x46,0x00}, // R + {0x46,0x49,0x49,0x49,0x31,0x00}, // S + {0x01,0x01,0x7F,0x01,0x01,0x00}, // T + {0x3F,0x40,0x40,0x40,0x3F,0x00}, // U + {0x1F,0x20,0x40,0x20,0x1F,0x00}, // V + {0x3F,0x40,0x38,0x40,0x3F,0x00}, // W + {0x63,0x14,0x08,0x14,0x63,0x00}, // X + {0x07,0x08,0x70,0x08,0x07,0x00}, // Y + {0x61,0x51,0x49,0x45,0x43,0x00}, // Z + {0x00,0x7F,0x41,0x41,0x00,0x00}, // [ + {0x55,0x2A,0x55,0x2A,0x55,0x00}, // \ + {0x00,0x41,0x41,0x7F,0x00,0x00}, // ] + {0x04,0x02,0x01,0x02,0x04,0x00}, // ^ + {0x40,0x40,0x40,0x40,0x40,0x00}, // _ + {0x00,0x01,0x02,0x04,0x00,0x00}, // ` + {0x00,0x20,0x3E,0x21,0x3E,0x00}, // a + {0x7F,0x28,0x44,0x44,0x38,0x00}, // b + {0x38,0x44,0x44,0x44,0x20,0x00}, // c + {0x38,0x44,0x44,0x28,0x7F,0x00}, // d + {0x38,0x54,0x54,0x54,0x18,0x00}, // e + {0x08,0x7E,0x09,0x01,0x02,0x00}, // f + {0x0C,0x52,0x52,0x52,0x3E,0x00}, // g + {0x7F,0x08,0x04,0x04,0x78,0x00}, // h + {0x00,0x44,0x7D,0x40,0x00,0x00}, // i + {0x20,0x40,0x44,0x3D,0x00,0x00}, // j + {0x7F,0x10,0x28,0x44,0x00,0x00}, // k + {0x00,0x41,0x7F,0x40,0x00,0x00}, // l + {0x7C,0x04,0x18,0x04,0x78,0x00}, // m + {0x7C,0x08,0x04,0x04,0x78,0x00}, // n + {0x38,0x44,0x44,0x44,0x38,0x00}, // o + {0x7C,0x14,0x14,0x14,0x08,0x00}, // p + {0x08,0x14,0x14,0x18,0x7C,0x00}, // q + {0x7F,0x08,0x04,0x04,0x08,0x00}, // r + {0x48,0x54,0x54,0x54,0x20,0x00}, // s + {0x04,0x3F,0x44,0x40,0x20,0x00}, // t + {0x3C,0x40,0x40,0x20,0x7C,0x00}, // u + {0x1C,0x20,0x40,0x20,0x1C,0x00}, // v + {0x3C,0x40,0x30,0x40,0x3C,0x00}, // w + {0x44,0x28,0x10,0x28,0x44,0x00}, // x + {0x0C,0x50,0x50,0x50,0x3C,0x00}, // y + {0x44,0x64,0x54,0x4C,0x44,0x00}, // z + {0x00,0x08,0x36,0x41,0x00,0x00}, // { + {0x00,0x00,0x7F,0x00,0x00,0x00}, // | + {0x00,0x41,0x36,0x08,0x00,0x00}, // } + {0x10,0x08,0x08,0x10,0x08,0x00}, // ~ +}; + +esp_err_t v2x_display_text(int16_t x, int16_t y, const char *text, + uint16_t color, disp_font_t font) +{ + if (!s_initialized) return ESP_ERR_INVALID_STATE; + + uint8_t char_bytes[6]; + int16_t tx = x; + + for (int ci = 0; text[ci] && tx < DISP_W; ci++) { + int idx = text[ci] - FONT_START; + if (idx < 0 || idx >= 95) continue; + + memcpy(char_bytes, font6x8[idx], 6); + + for (int c = 0; c < 6; c++) { + uint16_t row = char_bytes[c]; + for (int r = 0; r < 8; r++) { + if (row & 0x01) { + uint16_t pixel = color; + int px = tx + c; + int py = y + r; + if (px >= 0 && px < DISP_W && py >= 0 && py < DISP_H) { + esp_lcd_panel_draw_bitmap(s_panel, px, py, px + 1, py + 1, &pixel); + } + } + row >>= 1; + } + } + tx += 7; + } + return ESP_OK; +} + +esp_err_t v2x_display_frame_count(uint32_t count) +{ + if (!s_initialized) return ESP_ERR_INVALID_STATE; + + char buf[32]; + snprintf(buf, sizeof(buf), "%lu", (unsigned long)count); + int len = strlen(buf); + int x = (DISP_W - len * 7) / 2; + + uint8_t char_bytes[6]; + for (int ci = 0; ci < len; ci++) { + int idx = buf[ci] - FONT_START; + if (idx < 0 || idx >= 95) continue; + + memcpy(char_bytes, font6x8[idx], 6); + + for (int c = 0; c < 6; c++) { + uint16_t row = char_bytes[c]; + for (int r = 0; r < 8; r++) { + if (row & 0x01) { + uint16_t pixel = DISP_COLOR_RED; + int px = x + ci * 18 + c * 3; + for (int dy = 0; dy < 3; dy++) { + for (int dx = 0; dx < 3; dx++) { + int py = y + r * 3 + dy; + if (px + dx >= 0 && px + dx < DISP_W && py < DISP_H) { + esp_lcd_panel_draw_bitmap(s_panel, px + dx, py, px + dx + 1, py + 1, &pixel); + } + } + } + } + row >>= 1; + } + } + } + return ESP_OK; +} + +esp_err_t v2x_display_storage_bar(int16_t x, int16_t y, int16_t w, int16_t h, float pct) +{ + pct = (pct < 0.0f) ? 0.0f : (pct > 1.0f) ? 1.0f : pct; + v2x_display_rect(x, y, w, h, DISP_COLOR_GRAY, true); + + uint16_t fg = DISP_COLOR_GREEN; + if (pct < 0.3f) fg = DISP_COLOR_RED; + else if (pct < 0.6f) fg = DISP_COLOR_YELLOW; + + int16_t fw = (int16_t)(w * pct); + v2x_display_rect(x, y, fw, h, fg, true); + return ESP_OK; +} diff --git a/v2x_fw/components/v2x_driver/v2x_display_ui.c b/v2x_fw/components/v2x_driver/v2x_display_ui.c new file mode 100644 index 0000000..4965421 --- /dev/null +++ b/v2x_fw/components/v2x_driver/v2x_display_ui.c @@ -0,0 +1,90 @@ +/** + * Display UI Task — aktualisiert SD-Speicher und Frame-Counter + * + * Wird als FreeRTOS-Task gestartet und alle 2 Sekunden: + * - SD-Speicher % berechnen + Balken zeichnen + * - Frame-Counter groß anzeigen + * - Scroller aktualisieren + */ + +#include "esp_log.h" +#include "v2x_display.h" +#include "esp_vfs_fat.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "stdio.h" + +static const char *TAG = "v2x-ui"; + +/* SD-Speicher % berechnen */ +static float get_sd_storage_percent(void) +{ + FATFS *fs; + DWORD free_clusters; + + esp_err_t ret = f_getfree(SD_CARD_MOUNT_POINT, &free_clusters, &fs); + if (ret != FR_OK) return 0.0f; + + /* Gesamt-Cluster zählen */ + uint32_t total = fs->n_fatent - 12; /* FAT32: Cluster ab 12 */ + if (total == 0) return 0.0f; + + return (float)(total - free_clusters) / total; +} + +/* Frame-Counter als extern (wird vom Writer Task gesetzt) */ +extern uint32_t g_v2x_frame_count; + +/* Display UI Task */ +void v2x_display_ui_task(void *pvParameters) +{ + uint32_t last_count = 0; + + ESP_LOGI(TAG, "Display UI Task gestartet"); + + while (1) { + /* --- Display löschen (schwarzer Hintergrund) --- */ + v2x_display_clear(DISP_COLOR_BLACK); + + /* --- SD-Speicher-Balken (Y=0, Höhe=8px) --- */ + float sd_pct = get_sd_storage_percent(); + v2x_display_storage_bar(0, 0, DISP_W, 8, sd_pct); + + /* Text: "SD: ##%" */ + char sd_label[16]; + snprintf(sd_label, sizeof(sd_label), "SD: %3.0f%%", sd_pct * 100); + v2x_display_text(2, 9, sd_label, DISP_COLOR_WHITE, FONT_6x8); + + /* --- Frame-Counter (groß, rot, Y=20) --- */ + uint32_t current_count = g_v2x_frame_count; + if (current_count != last_count) { + v2x_display_frame_count(current_count, 28, DISP_W, 40); + last_count = current_count; + } + + /* --- RSSI-Anzeige (Y=72) --- */ + v2x_display_text(2, 72, "RSSI:", DISP_COLOR_YELLOW, FONT_6x8); + char rssi_label[20]; + snprintf(rssi_label, sizeof(rssi_label), "Max: %3d dBm", g_v2x_max_rssi); + v2x_display_text(40, 72, rssi_label, DISP_COLOR_WHITE, FONT_6x8); + + /* --- Scroller (Y=84) --- */ + scroller_redraw(); + + /* --- Statistik-Balken (Y=132) --- */ + v2x_display_text(2, 132, "CH:", DISP_COLOR_GREEN, FONT_6x8); + + /* Kanal + Frequenz */ + extern uint32_t g_v2x_current_freq; + extern int g_v2x_channel_idx; + char ch_info[20]; + snprintf(ch_info, sizeof(ch_info), "CH:%d %.0fMHz", + g_v2x_channel_idx, g_v2x_current_freq / 1000.0f); + v2x_display_text(20, 132, ch_info, DISP_COLOR_CYAN, FONT_6x8); + + /* BSSID-Farbcodes */ + v2x_display_text(2, 148, "B=BSM C=CAM D=DENM", DISP_COLOR_GRAY, FONT_6x8); + + vTaskDelay(pdMS_TO_TICKS(2000)); + } +} diff --git a/v2x_fw/include/v2x_display.h b/v2x_fw/include/v2x_display.h new file mode 100644 index 0000000..b17fc68 --- /dev/null +++ b/v2x_fw/include/v2x_display.h @@ -0,0 +1,93 @@ +#ifndef V2X_DISPLAY_H +#define V2X_DISPLAY_H + +#include + +/* ST7735 Display: 80x160 Pixel, SPI */ +#define DISP_W 80 +#define DISP_H 160 + +/* Display-Pins (T-Dongle-C5 ST7735) */ +#define DISP_GPIO_CS 10 +#define DISP_GPIO_RST 1 +#define DISP_GPIO_BL 0 +#define DISP_GPIO_SDA 2 /* MOSI */ +#define DISP_GPIO_SCK 6 + +/* Farben */ +#define DISP_COLOR_BLACK 0x0000 +#define DISP_COLOR_WHITE 0xFFFF +#define DISP_COLOR_RED 0xF800 +#define DISP_COLOR_GREEN 0x04E0 +#define DISP_COLOR_BLUE 0x001F +#define DISP_COLOR_CYAN 0x07FF +#define DISP_COLOR_YELLOW 0xFFE0 +#define DISP_COLOR_MAGENTA 0xF81F +#define DISP_COLOR_GRAY 0x6B4D + +/* Zeichenmodus */ +typedef enum { + DISP_MODE_OVERWRITE, + DISP_MODE_XOR, +} disp_draw_mode_t; + +/* Schriftgrößen */ +typedef enum { + FONT_6x8 = 0, /* 6x8 Pixel pro Zeichen */ + FONT_8x16 = 1, /* 8x16 Pixel pro Zeichen */ +} disp_font_t; + +/* Initialisierung */ +esp_err_t v2x_display_init(void); + +/* Display ausschalten (Energiesparen) */ +esp_err_t v2x_display_off(void); + +/* Screen löschen */ +esp_err_t v2x_display_clear(uint16_t color); + +/* Rechteck zeichnen */ +esp_err_t v2x_display_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, bool filled); + +/* Text zeichnen */ +esp_err_t v2x_display_text(int16_t x, int16_t y, const char *text, + uint16_t color, disp_font_t font); + +/* Linie zeichnen */ +esp_err_t v2x_display_line(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color); + +/* Fortschrittsbalken */ +esp_err_t v2x_display_progress_bar(int16_t x, int16_t y, int16_t w, int16_t h, float progress, + uint16_t fg_color, uint16_t bg_color); + +/* Frame-Counter (großes rotes Nummernfeld) */ +esp_err_t v2x_display_frame_count(uint32_t count); + +/* RSSI-Grafik (Balken) */ +esp_err_t v2x_display_rssi_gauge(int8_t rssi); + +/* Frame-Info als Zeile (wird im Scroller verwendet) */ +typedef struct { + uint8_t msg_type; + int8_t rssi; + uint8_t channel; + uint8_t bssid_last3[3]; /* nur letztes Drittel der BSSID für Anzeige */ +} disp_frame_info_t; + +/* Ein Frame als Zeile rendern (wird an Scroller übergeben) */ +esp_err_t v2x_display_render_frame_line(int16_t x, int16_t y, const disp_frame_info_t *info, + uint16_t bg_color); + +/* === Frame-Scroller === */ +void scroller_add_entry(uint8_t msg_type, int8_t rssi, uint8_t ch, const uint8_t *bssid); +void scroller_redraw(void); +void scroller_redraw_line(uint32_t entry_idx); + +/* === Display UI Task === */ +void v2x_display_ui_task(void *pvParameters); + +/* === Globale Display-Variablen (extern für andere Module) === */ +extern esp_lcd_panel_handle_t g_disp_panel; +extern bool g_disp_initialized; + +#endif /* V2X_DISPLAY_H */ diff --git a/v2x_fw/main/main.c b/v2x_fw/main/main.c index 951b8b1..8485bef 100644 --- a/v2x_fw/main/main.c +++ b/v2x_fw/main/main.c @@ -51,8 +51,10 @@ #include "nvs_flash.h" #include "esp_timer.h" #include "driver/gpio.h" -#include "esp_codec_dev.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_vendor.h" #include "v2x_db.h" +#include "v2x_display.h" static const char *TAG = "V2X-Sniffer"; @@ -74,10 +76,23 @@ static const uint32_t v2x_freqs[] = { #define STAT_DUMP_INTERVAL_MS (60 * 1000) /* CSV-Flush alle 200 Frames */ #define CSV_FLUSH_INTERVAL 200 +/* Display Refresh */ +#define DISPLAY_REFRESH_MS 2000 /* Frame Queue */ static QueueHandle_t s_frame_queue; +/* Scroller Queue */ +#define SCROLLER_QUEUE_SIZE 16 +static QueueHandle_t s_scroller_queue; + +/* Globale Variablen für Display */ +extern bool g_disp_initialized; +extern esp_lcd_panel_handle_t g_disp_panel; +uint32_t g_v2x_frame_count = 0; +int8_t g_v2x_max_rssi = -120; +int g_v2x_channel_idx = 0; + /* ====== Message-Type Erkennung ====== */ static v2x_msg_type_t detect_msg_type(const uint8_t *data, uint16_t len) { @@ -158,6 +173,11 @@ static void v2x_promiscuous_cb(void *arg, wifi_promiscuous_pkt_type_t pkt_type) /* Message-Type */ frame.msg_type = detect_msg_type(frame.raw_data, frame.raw_len); + + /* RSSI-Update */ + if (frame.rssi_dbm > g_v2x_max_rssi) { + g_v2x_max_rssi = frame.rssi_dbm; + } } /* Auf Queue pushen (non-blocking) */ @@ -185,6 +205,21 @@ static void v2x_frame_writer_task(void *pvParameters) ESP_LOGW(TAG, "SD-Schreibfehler"); } + /* Frame-Counter hochzählen */ + g_v2x_frame_count++; + + /* An Scroller senden (wenn Display an) */ + if (g_disp_initialized && s_scroller_queue) { + disp_frame_info_t fi; + memset(&fi, 0, sizeof(fi)); + fi.msg_type = frame.msg_type; + fi.rssi = frame.rssi_dbm; + extern uint32_t g_v2x_current_freq; + fi.channel = (uint8_t)(g_v2x_current_freq / 1000 - 5); + memcpy(fi.bssid_last3, frame.bssid + 3, 3); + xQueueSend(s_scroller_queue, &fi, 0); + } + /* Periodisch flushen */ flush_counter++; if (flush_counter % CSV_FLUSH_INTERVAL == 0) { @@ -214,6 +249,7 @@ static void v2x_channel_hopper_task(void *pvParameters) /* Frequenz/Kanal wechseln */ v2x_phy_init(freq); g_v2x_current_freq = freq; + g_v2x_channel_idx = hop_idx; ESP_LOGI(TAG, "CH %d: %lu MHz", hop_idx, (unsigned long)freq); @@ -241,6 +277,24 @@ static void v2x_stats_task(void *pvParameters) } } +/* ====== Scroller Task ====== */ +static void scroller_task(void *pvParameters) +{ + disp_frame_info_t fi; + + while (1) { + if (xQueueReceive(s_scroller_queue, &fi, pdMS_TO_TICKS(500)) == pdTRUE) { + /* Frame als Zeile hinzufügen */ + scroller_add_entry(fi.msg_type, fi.rssi, fi.channel, fi.bssid_last3); + + /* Scroller aktualisieren */ + if (g_disp_initialized) { + scroller_redraw(); + } + } + } +} + /* ====== Application Entry Point ====== */ void app_main(void) { @@ -285,6 +339,16 @@ void app_main(void) } } + /* === Display initialisieren === */ + ESP_LOGI(TAG, "Initialisiere ST7735 Display (GPIO10/1/0/2/6)..."); + ret = v2x_display_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Display fehlgeschlagen: %s", esp_err_to_name(ret)); + ESP_LOGW(TAG, "Fahre ohne Display fort"); + } else { + ESP_LOGI(TAG, "ST7735 Display bereit (80x160)"); + } + /* === Frame Queue erstellen === */ s_frame_queue = xQueueCreate(FRAME_QUEUE_SIZE, sizeof(v2x_frame_t)); if (!s_frame_queue) { @@ -295,6 +359,24 @@ void app_main(void) /* === Stats initialisieren === */ v2x_sd_reset_stats(); + /* === Scroller Queue erstellen === */ + s_scroller_queue = xQueueCreate(SCROLLER_QUEUE_SIZE, sizeof(disp_frame_info_t)); + if (!s_scroller_queue) { + ESP_LOGW(TAG, "Scroller Queue fehlgeschlagen"); + } + + /* === Display UI Task starten === */ + if (g_disp_initialized) { + xTaskCreate(v2x_display_ui_task, "display_ui", 4096, NULL, 2, NULL); + ESP_LOGI(TAG, "Display UI Task gestartet"); + } + + /* === Scroller Task starten === */ + if (s_scroller_queue && g_disp_initialized) { + xTaskCreate(scroller_task, "scroller", 2048, NULL, 2, NULL); + ESP_LOGI(TAG, "Scroller Task gestartet"); + } + /* === Tasks starten === */ /* Channel Hopper */ xTaskCreate(v2x_channel_hopper_task, "channel_hopper", 4096, NULL, 3, NULL);