diff --git a/firmware/esp32-csi-node/main/CMakeLists.txt b/firmware/esp32-csi-node/main/CMakeLists.txt index 0cabf5df2a..a214529475 100644 --- a/firmware/esp32-csi-node/main/CMakeLists.txt +++ b/firmware/esp32-csi-node/main/CMakeLists.txt @@ -52,9 +52,17 @@ if(CONFIG_CSI_MOCK_ENABLED) list(APPEND SRCS "mock_csi.c" "rv_radio_ops_mock.c") endif() -# ADR-045: AMOLED display support (compile-time optional) +# ADR-045: on-device display support (compile-time optional). +# Exactly one panel HAL is compiled, selected by the DISPLAY_PANEL choice. if(CONFIG_DISPLAY_ENABLE) - list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c") + list(APPEND SRCS "display_task.c") + if(CONFIG_DISPLAY_PANEL_ST7789) + # 240x280 ST7789: compact UI + ST7789 HAL. + list(APPEND SRCS "display_hal_st7789.c" "display_ui_st7789.c") + else() + # 368x448 SH8601 AMOLED: 4-view UI + QSPI HAL. + list(APPEND SRCS "display_hal.c" "display_ui.c") + endif() list(APPEND REQUIRES esp_lcd esp_lcd_touch lvgl) endif() diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 18c32ebf94..891684c510 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -170,15 +170,104 @@ menu "Adaptive Controller (ADR-081)" endmenu -menu "AMOLED Display (ADR-045)" +menu "Display (ADR-045)" config DISPLAY_ENABLE - bool "Enable AMOLED display support" + bool "Enable on-device display support" default y help - Enable RM67162 QSPI AMOLED display and LVGL UI. - Auto-detects at boot; gracefully skips if no display hardware. - Requires SPIRAM for frame buffers. + Enable the LVGL UI on an attached panel. Auto-detects at boot; + gracefully skips if no display hardware. Requires SPIRAM for + frame buffers. Choose the panel type below — the CSI pipeline + runs unchanged regardless of panel. + + choice DISPLAY_PANEL + prompt "Display panel" + depends on DISPLAY_ENABLE + default DISPLAY_PANEL_SH8601 + help + Which physical panel is attached. Exactly one HAL is compiled. + + config DISPLAY_PANEL_SH8601 + bool "SH8601 QSPI AMOLED (Waveshare ESP32-S3-Touch-AMOLED-1.8, 368x448)" + + config DISPLAY_PANEL_ST7789 + bool "ST7789V2 SPI LCD (Waveshare ESP32-S3-Touch-LCD-1.69, 240x280)" + endchoice + + # ---- ST7789 SPI LCD pins / geometry (Waveshare 1.69 defaults) ---- + config DISPLAY_ST7789_SCLK + int "ST7789 SPI SCLK GPIO" + default 6 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_MOSI + int "ST7789 SPI MOSI GPIO" + default 7 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_CS + int "ST7789 SPI CS GPIO" + default 5 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_DC + int "ST7789 SPI DC GPIO" + default 4 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_RST + int "ST7789 RST GPIO" + default 8 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_BL + int "ST7789 backlight PWM GPIO" + default 15 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_H_RES + int "ST7789 horizontal resolution" + default 240 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_V_RES + int "ST7789 vertical resolution" + default 280 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_GAP_X + int "ST7789 column offset (gap X)" + default 0 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_GAP_Y + int "ST7789 row offset (gap Y)" + default 20 + depends on DISPLAY_PANEL_ST7789 + help + The 240x280 visible window sits at row 20 of the ST7789's + 240x320 controller RAM. Adjust if the image is shifted. + + config DISPLAY_ST7789_TOUCH_SDA + int "CST816 touch I2C SDA GPIO" + default 11 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_TOUCH_SCL + int "CST816 touch I2C SCL GPIO" + default 10 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_TOUCH_RST + int "CST816 touch RST GPIO" + default 13 + depends on DISPLAY_PANEL_ST7789 + + config DISPLAY_ST7789_TOUCH_INT + int "CST816 touch INT GPIO" + default 14 + depends on DISPLAY_PANEL_ST7789 config DISPLAY_FPS_LIMIT int "Display refresh rate limit (FPS)" diff --git a/firmware/esp32-csi-node/main/display_hal.c b/firmware/esp32-csi-node/main/display_hal.c index dbbf63e81b..5451a7a08a 100644 --- a/firmware/esp32-csi-node/main/display_hal.c +++ b/firmware/esp32-csi-node/main/display_hal.c @@ -379,4 +379,13 @@ void display_hal_set_brightness(uint8_t percent) panel_write_cmd(0x51, &val, 1); } +void display_hal_refresh(void) +{ + /* Re-assert display-on (0x29) + brightness so a transient brownout that + * dimmed the panel self-recovers without a full re-init. */ + if (!s_io_handle) return; + panel_write_cmd(0x29, NULL, 0); + display_hal_set_brightness(CONFIG_DISPLAY_BRIGHTNESS); +} + #endif /* CONFIG_DISPLAY_ENABLE */ diff --git a/firmware/esp32-csi-node/main/display_hal.h b/firmware/esp32-csi-node/main/display_hal.h index de48f50ed4..a9ee41756d 100644 --- a/firmware/esp32-csi-node/main/display_hal.h +++ b/firmware/esp32-csi-node/main/display_hal.h @@ -64,6 +64,16 @@ bool display_hal_touch_read(uint16_t *x, uint16_t *y); */ void display_hal_set_brightness(uint8_t percent); +/** + * Self-heal: re-assert backlight + display-on state. + * + * Called periodically by the display task so a transient power sag + * (e.g. shared-USB brownout dimming the backlight) auto-recovers + * instead of leaving the panel dark while the MCU keeps running. + * Cheap and flicker-free — does NOT re-init the panel. + */ +void display_hal_refresh(void); + #ifdef __cplusplus } #endif diff --git a/firmware/esp32-csi-node/main/display_hal_st7789.c b/firmware/esp32-csi-node/main/display_hal_st7789.c new file mode 100644 index 0000000000..a28735d344 --- /dev/null +++ b/firmware/esp32-csi-node/main/display_hal_st7789.c @@ -0,0 +1,309 @@ +/** + * @file display_hal_st7789.c + * @brief ADR-045: ST7789V2 SPI LCD + CST816 touch HAL. + * + * Hardware abstraction for the Waveshare ESP32-S3-Touch-LCD-1.69 + * (240x280 IPS, ST7789V2 over 4-wire SPI, CST816 capacitive touch). + * + * Implements the same display_hal.h contract as the SH8601 QSPI AMOLED HAL, + * but on ESP-IDF's built-in esp_lcd ST7789 panel driver. Selected at build + * time via CONFIG_DISPLAY_PANEL_ST7789 (see Kconfig.projbuild). Exactly one + * display_hal_*.c is compiled into the image (CMakeLists picks by panel). + * + * Pin assignments (Waveshare ESP32-S3-Touch-LCD-1.69, Kconfig-overridable): + * LCD SPI: SCLK=6, MOSI=7, CS=5, DC=4, RST=8, BL=15 + * Touch: I2C SDA=11, SCL=10, RST=13, INT=14 (CST816 @ 0x15) + * + * Runs concurrently with the full CSI pipeline — the panel sits on SPI2_HOST + * and a private I2C bus, neither of which the radio/UDP data plane touches. + */ + +#include "display_hal.h" +#include "sdkconfig.h" + +#if CONFIG_DISPLAY_ENABLE && CONFIG_DISPLAY_PANEL_ST7789 + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_vendor.h" +#include "driver/spi_master.h" +#include "driver/gpio.h" +#include "driver/ledc.h" +#include "driver/i2c.h" +#include "esp_heap_caps.h" + +static const char *TAG = "disp_hal_st7789"; + +/* ---- LCD SPI pins (Kconfig-overridable) ---- */ +#define LCD_PIN_SCLK CONFIG_DISPLAY_ST7789_SCLK +#define LCD_PIN_MOSI CONFIG_DISPLAY_ST7789_MOSI +#define LCD_PIN_CS CONFIG_DISPLAY_ST7789_CS +#define LCD_PIN_DC CONFIG_DISPLAY_ST7789_DC +#define LCD_PIN_RST CONFIG_DISPLAY_ST7789_RST +#define LCD_PIN_BL CONFIG_DISPLAY_ST7789_BL + +#define LCD_H_RES CONFIG_DISPLAY_ST7789_H_RES +#define LCD_V_RES CONFIG_DISPLAY_ST7789_V_RES +#define LCD_GAP_X CONFIG_DISPLAY_ST7789_GAP_X +#define LCD_GAP_Y CONFIG_DISPLAY_ST7789_GAP_Y + +#define LCD_HOST SPI2_HOST +#define LCD_PCLK_HZ (40 * 1000 * 1000) + +/* ---- Backlight PWM ---- */ +#define BL_LEDC_TIMER LEDC_TIMER_0 +#define BL_LEDC_MODE LEDC_LOW_SPEED_MODE +#define BL_LEDC_CHANNEL LEDC_CHANNEL_0 +#define BL_LEDC_DUTY_RES LEDC_TIMER_8_BIT +#define BL_LEDC_FREQ_HZ 5000 + +/* ---- CST816 touch (I2C) ---- */ +#define TOUCH_I2C_NUM I2C_NUM_0 +#define TOUCH_I2C_FREQ_HZ 400000 +#define TOUCH_PIN_SDA CONFIG_DISPLAY_ST7789_TOUCH_SDA +#define TOUCH_PIN_SCL CONFIG_DISPLAY_ST7789_TOUCH_SCL +#define TOUCH_PIN_RST CONFIG_DISPLAY_ST7789_TOUCH_RST +#define TOUCH_PIN_INT CONFIG_DISPLAY_ST7789_TOUCH_INT +#define CST816_ADDR 0x15 +#define CST816_REG_CHIPID 0xA7 + +/* ---- State ---- */ +static esp_lcd_panel_io_handle_t s_io_handle = NULL; +static esp_lcd_panel_handle_t s_panel = NULL; +static bool s_bl_initialized = false; +static bool s_touch_initialized = false; + +/* ===================== Backlight ===================== */ + +static void init_backlight(void) +{ + ledc_timer_config_t timer = { + .speed_mode = BL_LEDC_MODE, + .timer_num = BL_LEDC_TIMER, + .duty_resolution = BL_LEDC_DUTY_RES, + .freq_hz = BL_LEDC_FREQ_HZ, + .clk_cfg = LEDC_AUTO_CLK, + }; + if (ledc_timer_config(&timer) != ESP_OK) { + ESP_LOGW(TAG, "LEDC timer config failed — backlight will be GPIO-on"); + gpio_set_direction(LCD_PIN_BL, GPIO_MODE_OUTPUT); + gpio_set_level(LCD_PIN_BL, 1); + return; + } + ledc_channel_config_t ch = { + .gpio_num = LCD_PIN_BL, + .speed_mode = BL_LEDC_MODE, + .channel = BL_LEDC_CHANNEL, + .timer_sel = BL_LEDC_TIMER, + .duty = 0, + .hpoint = 0, + }; + ledc_channel_config(&ch); + s_bl_initialized = true; +} + +/* ===================== Panel ===================== */ + +static esp_err_t draw_test_pattern(void) +{ + /* Clear to background once (low power — no full-white frame, which spiked + * current and worsened the startup brownout). LVGL draws over this. */ + uint16_t *line = heap_caps_malloc(LCD_H_RES * sizeof(uint16_t), MALLOC_CAP_DMA); + if (!line) return ESP_ERR_NO_MEM; + for (int x = 0; x < LCD_H_RES; x++) line[x] = 0x0841; /* near-black */ + for (int y = 0; y < LCD_V_RES; y++) + esp_lcd_panel_draw_bitmap(s_panel, 0, y, LCD_H_RES, y + 1, line); + free(line); + return ESP_OK; +} + +esp_err_t display_hal_init_panel(void) +{ + ESP_LOGI(TAG, "Initializing Waveshare 1.69\" LCD (ST7789V2 %dx%d)...", + LCD_H_RES, LCD_V_RES); + + spi_bus_config_t bus_cfg = { + .sclk_io_num = LCD_PIN_SCLK, + .mosi_io_num = LCD_PIN_MOSI, + .miso_io_num = -1, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = LCD_H_RES * 80 * sizeof(uint16_t), + }; + esp_err_t ret = spi_bus_initialize(LCD_HOST, &bus_cfg, SPI_DMA_CH_AUTO); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "SPI bus init failed: %s", esp_err_to_name(ret)); + return ESP_ERR_NOT_FOUND; + } + + esp_lcd_panel_io_spi_config_t io_config = { + .dc_gpio_num = LCD_PIN_DC, + .cs_gpio_num = LCD_PIN_CS, + .pclk_hz = LCD_PCLK_HZ, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .spi_mode = 0, + .trans_queue_depth = 10, + }; + ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &s_io_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Panel IO init failed: %s", esp_err_to_name(ret)); + spi_bus_free(LCD_HOST); + return ESP_ERR_NOT_FOUND; + } + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = LCD_PIN_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + }; + ret = esp_lcd_new_panel_st7789(s_io_handle, &panel_config, &s_panel); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "ST7789 panel create failed: %s", esp_err_to_name(ret)); + esp_lcd_panel_io_del(s_io_handle); + spi_bus_free(LCD_HOST); + s_io_handle = NULL; + return ESP_ERR_NOT_FOUND; + } + + esp_lcd_panel_reset(s_panel); + esp_lcd_panel_init(s_panel); + /* IPS ST7789 panels show inverted colour without this. */ + esp_lcd_panel_invert_color(s_panel, true); + /* 240x280 visible window sits at row LCD_GAP_Y of the 240x320 controller RAM. */ + esp_lcd_panel_set_gap(s_panel, LCD_GAP_X, LCD_GAP_Y); + esp_lcd_panel_disp_on_off(s_panel, true); + + init_backlight(); + display_hal_set_brightness(CONFIG_DISPLAY_BRIGHTNESS); + + draw_test_pattern(); + + ESP_LOGI(TAG, "ST7789 panel init OK (%dx%d, gap %d,%d)", + LCD_H_RES, LCD_V_RES, LCD_GAP_X, LCD_GAP_Y); + return ESP_OK; +} + +void display_hal_draw(int x_start, int y_start, int x_end, int y_end, + const void *color_data) +{ + if (!s_panel) return; + /* esp_lcd takes an exclusive end coord, which is exactly what + * display_task's flush callback already passes (area->x2 + 1). */ + esp_lcd_panel_draw_bitmap(s_panel, x_start, y_start, x_end, y_end, color_data); +} + +/* ===================== Touch (CST816) ===================== */ + +static esp_err_t touch_i2c_init(void) +{ + i2c_config_t cfg = { + .mode = I2C_MODE_MASTER, + .sda_io_num = TOUCH_PIN_SDA, + .scl_io_num = TOUCH_PIN_SCL, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master.clk_speed = TOUCH_I2C_FREQ_HZ, + }; + esp_err_t ret = i2c_param_config(TOUCH_I2C_NUM, &cfg); + if (ret != ESP_OK) return ret; + return i2c_driver_install(TOUCH_I2C_NUM, I2C_MODE_MASTER, 0, 0, 0); +} + +static esp_err_t touch_read_reg(uint8_t reg, uint8_t *data, size_t len) +{ + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (CST816_ADDR << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(cmd, reg, true); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (CST816_ADDR << 1) | I2C_MASTER_READ, true); + i2c_master_read(cmd, data, len, I2C_MASTER_LAST_NACK); + i2c_master_stop(cmd); + esp_err_t ret = i2c_master_cmd_begin(TOUCH_I2C_NUM, cmd, pdMS_TO_TICKS(100)); + i2c_cmd_link_delete(cmd); + return ret; +} + +esp_err_t display_hal_init_touch(void) +{ + ESP_LOGI(TAG, "Probing CST816 touch controller..."); + + /* Hardware reset pulse (CST816 needs RST toggled to boot). */ + gpio_set_direction(TOUCH_PIN_RST, GPIO_MODE_OUTPUT); + gpio_set_level(TOUCH_PIN_RST, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(TOUCH_PIN_RST, 1); + vTaskDelay(pdMS_TO_TICKS(60)); + + gpio_config_t int_cfg = { + .pin_bit_mask = (1ULL << TOUCH_PIN_INT), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&int_cfg); + + esp_err_t ret = touch_i2c_init(); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Touch I2C init failed: %s", esp_err_to_name(ret)); + return ESP_ERR_NOT_FOUND; + } + + uint8_t chip_id = 0; + ret = touch_read_reg(CST816_REG_CHIPID, &chip_id, 1); + if (ret != ESP_OK || chip_id == 0x00 || chip_id == 0xFF) { + ESP_LOGW(TAG, "CST816 not found (ret=%s, id=0x%02X)", esp_err_to_name(ret), chip_id); + return ESP_ERR_NOT_FOUND; + } + + s_touch_initialized = true; + ESP_LOGI(TAG, "CST816 touch init OK (chip_id=0x%02X)", chip_id); + return ESP_OK; +} + +bool display_hal_touch_read(uint16_t *x, uint16_t *y) +{ + if (!s_touch_initialized) return false; + + /* Read gesture(0x01), finger_num(0x02), X hi/lo(0x03/04), Y hi/lo(0x05/06). */ + uint8_t buf[6] = {0}; + if (touch_read_reg(0x01, buf, sizeof(buf)) != ESP_OK) return false; + + uint8_t fingers = buf[1] & 0x0F; + if (fingers == 0) return false; + + *x = ((uint16_t)(buf[2] & 0x0F) << 8) | buf[3]; + *y = ((uint16_t)(buf[4] & 0x0F) << 8) | buf[5]; + return true; +} + +/* ===================== Brightness ===================== */ + +void display_hal_set_brightness(uint8_t percent) +{ + if (percent > 100) percent = 100; + if (!s_bl_initialized) { + /* LEDC unavailable — drive backlight GPIO directly. */ + gpio_set_level(LCD_PIN_BL, percent > 0 ? 1 : 0); + return; + } + uint32_t duty = (uint32_t)percent * 255 / 100; /* 8-bit resolution */ + ledc_set_duty(BL_LEDC_MODE, BL_LEDC_CHANNEL, duty); + ledc_update_duty(BL_LEDC_MODE, BL_LEDC_CHANNEL); +} + +void display_hal_refresh(void) +{ + /* Backlight-only self-heal: re-assert the LEDC duty (no SPI). The earlier + * version also poked esp_lcd_panel_disp_on_off over SPI every 2s, which + * could contend with the in-flight LVGL flush and hang the display task. + * On a stable supply the panel never powers off, so backlight is enough. */ + display_hal_set_brightness(CONFIG_DISPLAY_BRIGHTNESS); +} + +#endif /* CONFIG_DISPLAY_ENABLE && CONFIG_DISPLAY_PANEL_ST7789 */ diff --git a/firmware/esp32-csi-node/main/display_task.c b/firmware/esp32-csi-node/main/display_task.c index 9d834edc5a..7c19818a43 100644 --- a/firmware/esp32-csi-node/main/display_task.c +++ b/firmware/esp32-csi-node/main/display_task.c @@ -16,13 +16,20 @@ #include "freertos/task.h" #include "esp_log.h" #include "esp_heap_caps.h" +#include "esp_timer.h" #include "lvgl.h" #include "display_hal.h" #include "display_ui.h" +/* Panel geometry: ST7789 (240x280) vs SH8601 AMOLED (368x448). */ +#if CONFIG_DISPLAY_PANEL_ST7789 +#define DISP_H_RES CONFIG_DISPLAY_ST7789_H_RES +#define DISP_V_RES CONFIG_DISPLAY_ST7789_V_RES +#else #define DISP_H_RES 368 #define DISP_V_RES 448 +#endif static const char *TAG = "disp_task"; @@ -59,6 +66,16 @@ static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) } } +/* ---- LVGL tick source ---- + * Kconfig has CONFIG_LV_TICK_CUSTOM unset and nothing calls lv_tick_inc(), + * so LVGL's tick never advances and its refresh timer never fires — the panel + * draws once and never repaints. This esp_timer drives the tick so LVGL + * actually refreshes (works headless; esp_timer runs with no USB host). */ +static void lvgl_tick_cb(void *arg) +{ + lv_tick_inc(2); +} + /* ---- Display task ---- */ static void display_task(void *arg) { @@ -70,9 +87,15 @@ static void display_task(void *arg) display_ui_create(lv_scr_act()); TickType_t last_wake = xTaskGetTickCount(); + TickType_t last_heal = last_wake; while (1) { display_ui_update(); lv_timer_handler(); + /* Backlight-only self-heal every ~2s (LEDC, no SPI). */ + if ((xTaskGetTickCount() - last_heal) >= pdMS_TO_TICKS(2000)) { + last_heal = xTaskGetTickCount(); + display_hal_refresh(); + } vTaskDelayUntil(&last_wake, frame_period); } } @@ -109,6 +132,19 @@ esp_err_t display_task_start(void) /* Initialize LVGL */ lv_init(); + /* Start the LVGL tick (2 ms) — WITHOUT this the display never refreshes. */ + const esp_timer_create_args_t tick_args = { + .callback = &lvgl_tick_cb, + .name = "lvgl_tick", + }; + esp_timer_handle_t tick_timer = NULL; + if (esp_timer_create(&tick_args, &tick_timer) == ESP_OK && + esp_timer_start_periodic(tick_timer, 2000) == ESP_OK) { + ESP_LOGI(TAG, "LVGL tick timer started (2 ms)"); + } else { + ESP_LOGE(TAG, "LVGL tick timer failed — display will not refresh"); + } + /* Double-buffered draw buffers — prefer PSRAM, fall back to internal DMA */ size_t buf_lines = use_psram ? DISP_BUF_LINES : 10; /* Smaller buffers without PSRAM */ size_t buf_size = DISP_H_RES * buf_lines * sizeof(lv_color_t); diff --git a/firmware/esp32-csi-node/main/display_ui_st7789.c b/firmware/esp32-csi-node/main/display_ui_st7789.c new file mode 100644 index 0000000000..0fed59c9a2 --- /dev/null +++ b/firmware/esp32-csi-node/main/display_ui_st7789.c @@ -0,0 +1,134 @@ +/** + * @file display_ui_st7789.c + * @brief ADR-045: compact LVGL UI for the 240x280 ST7789 panel. + * + * The 4-view AMOLED UI (display_ui.c) is laid out for 368x448 and overflows a + * 1.69" LCD. This variant is a single legible screen sized for 240x280: node + * identity, a big ACTIVITY bar driven by the CSI motion/presence metrics (so + * movement is visible across a room), and a live CSI packet-rate footer. + * Selected at build time via CONFIG_DISPLAY_PANEL_ST7789 (CMakeLists compiles + * this instead of display_ui.c). Same display_ui.h contract. + */ + +#include "display_ui.h" +#include "csi_collector.h" /* node id + live CSI packet rate */ +#include "sdkconfig.h" + +#if CONFIG_DISPLAY_ENABLE && CONFIG_DISPLAY_PANEL_ST7789 + +#include +#include "esp_log.h" +#include "esp_timer.h" +#include "esp_system.h" +#include "edge_processing.h" + +static const char *TAG = "disp_ui_st7789"; + +#define COLOR_BG lv_color_make(0x0A, 0x0A, 0x0F) +#define COLOR_CYAN lv_color_make(0x00, 0xD4, 0xFF) +#define COLOR_TEXT lv_color_make(0xCC, 0xCC, 0xDD) +#define COLOR_DIM lv_color_make(0x66, 0x66, 0x77) +#define COLOR_GREEN lv_color_make(0x00, 0xFF, 0x80) +#define COLOR_TRACK lv_color_make(0x1C, 0x1C, 0x26) + +/* Activity = max(motion_energy, presence_score); ~0 idle, climbs past 6 on + * movement near the link. Scale x10 to a 0-100 bar (clamped). */ +#define ACT_SCALE 10.0f + +static lv_obj_t *s_node = NULL; +static lv_obj_t *s_act_val = NULL; /* big activity number */ +static lv_obj_t *s_bar = NULL; /* activity bar */ +static lv_obj_t *s_foot = NULL; /* CSI rate + RSSI */ +static lv_obj_t *s_foot2 = NULL; /* uptime + heap */ +static lv_obj_t *s_hb = NULL; /* heartbeat dot */ + +static lv_obj_t *label(lv_obj_t *p, const lv_font_t *font, lv_color_t color, + lv_align_t align, int x, int y, const char *text) +{ + lv_obj_t *l = lv_label_create(p); + lv_label_set_text(l, text); + lv_obj_set_style_text_font(l, font, 0); + lv_obj_set_style_text_color(l, color, 0); + lv_obj_align(l, align, x, y); + return l; +} + +void display_ui_create(lv_obj_t *parent) +{ + lv_obj_set_style_bg_color(parent, COLOR_BG, 0); + lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0); + + label(parent, &lv_font_montserrat_20, COLOR_DIM, LV_ALIGN_TOP_MID, 0, 6, "RuView CSI"); + s_node = label(parent, &lv_font_montserrat_36, COLOR_CYAN, LV_ALIGN_TOP_MID, 0, 30, "NODE -"); + s_hb = label(parent, &lv_font_montserrat_20, COLOR_CYAN, LV_ALIGN_TOP_RIGHT, -10, 8, "*"); + + label(parent, &lv_font_montserrat_20, COLOR_TEXT, LV_ALIGN_TOP_MID, 0, 92, "ACTIVITY"); + s_act_val = label(parent, &lv_font_montserrat_36, COLOR_GREEN, LV_ALIGN_TOP_MID, 0, 116, "0"); + + s_bar = lv_bar_create(parent); + lv_obj_set_size(s_bar, 200, 24); + lv_obj_align(s_bar, LV_ALIGN_TOP_MID, 0, 172); + lv_bar_set_range(s_bar, 0, 100); + lv_bar_set_value(s_bar, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(s_bar, COLOR_TRACK, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_bar, COLOR_GREEN, LV_PART_INDICATOR); + lv_obj_set_style_radius(s_bar, 4, LV_PART_MAIN); + + s_foot = label(parent, &lv_font_montserrat_14, COLOR_TEXT, LV_ALIGN_BOTTOM_MID, 0, -26, "CSI --/s RSSI --"); + s_foot2 = label(parent, &lv_font_montserrat_14, COLOR_DIM, LV_ALIGN_BOTTOM_MID, 0, -6, "up 0h00m heap -- KB"); + + ESP_LOGI(TAG, "Compact ST7789 UI created (240x280, activity bar)"); +} + +void display_ui_update(void) +{ + char buf[48]; + + /* Heartbeat — blink ~2 Hz so the UI is visibly alive regardless of data. */ + static uint32_t s_hb_last = 0; + static bool s_hb_on = false; + uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000); + if (now_ms - s_hb_last >= 500) { + s_hb_last = now_ms; + s_hb_on = !s_hb_on; + lv_label_set_text(s_hb, s_hb_on ? "*" : " "); + } + + snprintf(buf, sizeof(buf), "NODE %u", (unsigned)csi_collector_get_node_id()); + lv_label_set_text(s_node, buf); + + uint16_t pps = csi_collector_get_pkt_yield_per_sec(); + + edge_vitals_pkt_t v; + int rssi = 0; + float activity = 0.0f; + if (edge_get_vitals(&v)) { + rssi = v.rssi; + activity = v.motion_energy > v.presence_score ? v.motion_energy : v.presence_score; + } + + int act100 = (int)(activity * ACT_SCALE); + if (act100 > 100) act100 = 100; + if (act100 < 0) act100 = 0; + lv_bar_set_value(s_bar, act100, LV_ANIM_OFF); + + snprintf(buf, sizeof(buf), "%d", act100); + lv_label_set_text(s_act_val, buf); + lv_obj_set_style_text_color(s_act_val, act100 > 10 ? COLOR_GREEN : COLOR_DIM, 0); + + snprintf(buf, sizeof(buf), "CSI %u/s RSSI %d", (unsigned)pps, rssi); + lv_label_set_text(s_foot, buf); + + uint32_t up = (uint32_t)(esp_timer_get_time() / 1000000); + snprintf(buf, sizeof(buf), "up %luh%02lum heap %luK", + (unsigned long)(up / 3600), (unsigned long)((up % 3600) / 60), + (unsigned long)(esp_get_free_heap_size() / 1024)); + lv_label_set_text(s_foot2, buf); + + /* NOTE: no per-loop ESP_LOG here. On the USB-Serial-JTAG console, logging + * with no host attached (e.g. running off a wall charger) blocks once the + * TX buffer fills — which would hang the display task. Keep this loop + * log-free so the panel keeps refreshing headless. */ +} + +#endif /* CONFIG_DISPLAY_ENABLE && CONFIG_DISPLAY_PANEL_ST7789 */ diff --git a/firmware/esp32-csi-node/sdkconfig.defaults.st7789 b/firmware/esp32-csi-node/sdkconfig.defaults.st7789 new file mode 100644 index 0000000000..ebc194e779 --- /dev/null +++ b/firmware/esp32-csi-node/sdkconfig.defaults.st7789 @@ -0,0 +1,15 @@ +# ST7789 display build overlay — Waveshare ESP32-S3-Touch-LCD-1.69. +# Merge AFTER sdkconfig.defaults (later file wins): +# SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.st7789" +# Keeps the proven CSI-node config; only swaps the display panel HAL. +CONFIG_DISPLAY_ENABLE=y +CONFIG_DISPLAY_PANEL_ST7789=y + +# LVGL fonts for the compact 240x280 UI (LVGL is Kconfig-configured, not lv_conf.h). +CONFIG_LV_FONT_MONTSERRAT_20=y +CONFIG_LV_FONT_MONTSERRAT_36=y +CONFIG_LV_USE_BAR=y + +# Dimmer backlight = less current draw on the marginal single-USB power path +# (helps avoid the startup brownout boot-loop on the LCD board). +CONFIG_DISPLAY_BRIGHTNESS=45