Display UI: ST7735 Treiber + Frame-Scroller + Speicheranzeige

- 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
This commit is contained in:
Clawdia
2026-05-09 11:47:55 +02:00
parent 874dd969b6
commit 240a374148
7 changed files with 896 additions and 3 deletions
+5 -2
View File
@@ -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
)
+258
View File
@@ -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 <stdio.h>
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;
}
@@ -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);
}
@@ -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;
}
@@ -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));
}
}
+93
View File
@@ -0,0 +1,93 @@
#ifndef V2X_DISPLAY_H
#define V2X_DISPLAY_H
#include <stdint.h>
/* 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 */
+83 -1
View File
@@ -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);