From b523963944884dd68cf0e9988e7a8c4f48590cc0 Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Mon, 28 Sep 2026 23:13:00 +0200 Subject: [PATCH] TFT: switch to LVGL, add XPT2046 touch --- mcu-tft.yaml | 347 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 222 insertions(+), 125 deletions(-) diff --git a/mcu-tft.yaml b/mcu-tft.yaml index 7ee515f..fd9cf0c 100644 --- a/mcu-tft.yaml +++ b/mcu-tft.yaml @@ -10,9 +10,10 @@ esphome: friendly_name: ${name} area: ${area} comment: ${comment} - platformio_options: - board_build.flash_mode: dio - + on_boot: + - priority: -100 + then: + - lvgl.page.show: status_page esp32: variant: esp32c3 @@ -23,39 +24,118 @@ packages: common: !include common/common.yaml ipv6_only: !include common/ipv6_only.yaml -logger: - logs: - component: ERROR - -# GPIO1 UART TX -# GPIO3 UART RX -# GPIO4 I2C SDA -# GPIO5 I2C SCL -# GPIO12 DISPLAY DIO -# GPIO12 DISPLAY CLOCK -# GPIO14 BUZZER -# GPIO16 RGB +api: + # status page until Home Assistant connects (log viewers don't subscribe to states) + on_client_connected: + - delay: 2s + - if: + condition: + api.connected: + state_subscription_only: true + then: + - lvgl.page.show: main_page + on_client_disconnected: + - if: + condition: + not: + api.connected: + state_subscription_only: true + then: + - lvgl.page.show: status_page spi: clk_pin: 4 mosi_pin: 5 miso_pin: 6 -color: - - id: my_green - hex: 00ff00 - - id: my_magenta - hex: ff00ff - - id: my_red - hex: ff0000 - - id: my_white - hex: ffffff - - id: status_ok - hex: 4dd966 - - id: status_wait - hex: ffb300 - - id: status_dim - hex: 8c8c8c +display: + - platform: mipi_spi + id: main_display + model: ILI9341 + cs_pin: 7 + dc_pin: 8 + reset_pin: 10 + invert_colors: false + data_rate: 40MHz + update_interval: never + auto_clear_enabled: false + +touchscreen: + - platform: xpt2046 + display: main_display + cs_pin: 0 + interrupt_pin: 1 + transform: + mirror_x: true + calibration: + x_min: 330 + x_max: 3620 + y_min: 230 + y_max: 3680 + +script: + - id: update_status + then: + - lambda: |- + // same colors as the other status screens: ok green, waiting orange, details gray + static const uint32_t OK = 0x4DD966, WAIT = 0xFFB300; + auto set = [](lv_obj_t *label, const std::string &text, uint32_t color) { + lv_label_set_text(label, text.c_str()); + lv_obj_set_style_text_color(label, lv_color_hex(color), LV_PART_MAIN); + }; + + const bool wifi_ok = wifi::global_wifi_component->is_connected(); + set(id(status_wifi), + wifi_ok ? str_sprintf("WiFi: connected (%d dBm)", wifi::global_wifi_component->wifi_rssi()) + : std::string("WiFi: connecting..."), + wifi_ok ? OK : WAIT); + + // IPv6 first (skip link-local), then IPv4 + std::string ips; + if (wifi_ok) { + char buf[network::IP_ADDRESS_BUFFER_SIZE]; + for (bool v6 : {true, false}) { + for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) { + if (!ip.is_set() || ip.is_ip6() != v6) + continue; + ip.str_to(buf); + if (v6 && strncmp(buf, "fe80", 4) == 0) + continue; + if (!ips.empty()) + ips += "\n"; + ips += buf; + } + } + } + lv_label_set_text(id(status_ips), ips.c_str()); + if (ips.empty()) + lv_obj_add_flag(id(status_ips), LV_OBJ_FLAG_HIDDEN); + else + lv_obj_remove_flag(id(status_ips), LV_OBJ_FLAG_HIDDEN); + + const bool api_ok = api::global_api_server->is_connected(); + set(id(status_ha), api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...", + api_ok ? OK : WAIT); + + const uint32_t up = millis() / 1000; + lv_label_set_text(id(status_footer), + str_sprintf("ESPHome %s\nup %ud %02u:%02u:%02u", ESPHOME_VERSION, up / 86400, + (up / 3600) % 24, (up / 60) % 60, up % 60).c_str()); +interval: + - interval: 1s + then: + - lvgl.label.update: + id: clock_label + text: !lambda |- + auto now = id(ha_time).now(); + if (!now.is_valid()) + return std::string("--:--"); + return now.strftime("%H:%M"); + - if: + condition: + lvgl.page.is_showing: status_page + then: + - script.execute: update_status image: - platform: file @@ -66,103 +146,120 @@ image: transparency: alpha_channel font: + - file: "gfonts://Roboto@500" + id: font_clock + size: 80 + glyphs: "0123456789:-" - file: "gfonts://Roboto" - id: font_default - size: 20 - - file: "gfonts://B612" - id: b612_24 - size: 24 - - file: "gfonts://B612" - id: b612_16 - size: 16 - - file: "gfonts://B612" - id: b612_12 - size: 12 - - file: "gfonts://B612 Mono" - id: b612_24_mono - size: 24 - - file: "gfonts://B612 Mono" - id: b612_16_mono - size: 16 - - file: "gfonts://B612 Mono" - id: b612_12_mono - size: 12 - - file: "gfonts://Roboto" - id: roboto_32 - size: 32 - - file: "gfonts://Roboto" - id: roboto_24 + id: font_title size: 24 - file: "gfonts://Roboto" - id: roboto_16 - size: 16 - - file: "gfonts://Roboto" - id: roboto_12 - size: 12 - - file: "gfonts://Roboto Mono" - id: roboto_32_mono - size: 32 - - file: "gfonts://Roboto Mono" - id: roboto_24_mono - size: 24 - - file: "gfonts://Roboto Mono" - id: roboto_16_mono - size: 16 - - file: "gfonts://Roboto Mono" - id: roboto_12_mono - size: 12 + id: font_status + size: 14 +lvgl: + log_level: WARN + color_depth: 16 + bg_color: 0x000000 + text_color: 0xFFFFFF + theme: + button: + bg_color: 0x2B2F36 + radius: 12 + checked: + bg_color: 0xE08A00 + buttonmatrix: + bg_opa: TRANSP + border_width: 0 + items: + bg_color: 0x2B2F36 + text_color: 0xFFFFFF + radius: 12 + checked: + bg_color: 0xE08A00 + top_layer: + widgets: + # same navigation bar as on the other LVGL displays + - buttonmatrix: + id: nav_bar + align: bottom_mid + width: 100% + height: 48 + pad_all: 4 + pad_column: 6 + items: + text_font: montserrat_20 + rows: + - buttons: + - id: page_prev + text: "" + on_press: + then: + lvgl.page.previous: + - id: page_home + text: "" + on_press: + then: + lvgl.page.show: main_page + - id: page_next + text: "" + on_press: + then: + lvgl.page.next: + pages: + # 1: clock + - id: main_page + widgets: + - label: + id: clock_label + align: CENTER + y: -24 + text_font: font_clock + text: "--:--" -display: - - platform: mipi_spi - model: ILI9341 - cs_pin: 7 - dc_pin: 8 - reset_pin: 10 - invert_colors: false - lambda: |- - it.fill(Color::BLACK); - - // status screen until Home Assistant connects (240x320) - if (!api::global_api_server->is_connected_with_state_subscription()) { - const int cx = it.get_width() / 2; - it.image(cx, 48, id(esphome_logo), ImageAlign::CENTER); - it.print(cx, 100, id(roboto_24), id(my_white), TextAlign::CENTER, "${name}"); - - int y = 130; - if (wifi::global_wifi_component->is_connected()) { - it.printf(8, y, id(roboto_16), id(status_ok), "WiFi: connected (%d dBm)", - wifi::global_wifi_component->wifi_rssi()); - y += 22; - // IPv6 first (skip link-local), then IPv4 - char buf[network::IP_ADDRESS_BUFFER_SIZE]; - for (bool v6 : {true, false}) { - for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) { - if (!ip.is_set() || ip.is_ip6() != v6 || y > 200) - continue; - ip.str_to(buf); - if (v6 && strncmp(buf, "fe80", 4) == 0) - continue; - it.print(8, y, id(roboto_12), id(status_dim), buf); - y += 16; - } - } - } else { - it.print(8, y, id(roboto_16), id(status_wait), "WiFi: connecting..."); - y += 22; - } - y += 8; - it.print(8, y, id(roboto_16), api::global_api_server->is_connected() ? id(status_ok) : id(status_wait), - api::global_api_server->is_connected() ? "Home Assistant: connected" : "Home Assistant: waiting..."); - - it.printf(8, it.get_height() - 8, id(roboto_12), id(status_dim), TextAlign::BOTTOM_LEFT, "ESPHome %s", - ESPHOME_VERSION); - it.printf(it.get_width() - 8, it.get_height() - 8, id(roboto_12), id(status_dim), TextAlign::BOTTOM_RIGHT, - "up %us", (unsigned) (millis() / 1000)); - return; - } - - it.print(0, 0, id(b612_12), id(my_white), TextAlign::TOP_LEFT, "HELLO WORLD!"); - it.print(0, 14, id(b612_12), id(my_green), TextAlign::TOP_LEFT, "HELLO WORLD!"); - it.print(0, 28, id(b612_12), id(my_magenta), TextAlign::TOP_LEFT, "HELLO WORLD!"); - it.print(0, 42, id(b612_12), id(my_red), TextAlign::TOP_LEFT, "HELLO WORLD!"); + # 2: status (shown until Home Assistant connects) + - id: status_page + on_load: + - script.execute: update_status + widgets: + - image: + src: esphome_logo + align: TOP_MID + y: 8 + - label: + align: TOP_MID + y: 78 + text_font: font_title + text: "${name}" + - obj: + x: 8 + y: 116 + width: 224 + height: SIZE_CONTENT + bg_opa: TRANSP + border_width: 0 + pad_all: 0 + scrollbar_mode: "OFF" + layout: + type: flex + flex_flow: column + pad_row: 6 + widgets: + - label: + id: status_wifi + text_font: font_status + text: "" + - label: + id: status_ips + text_font: font_status + text_color: 0x8C8C8C + text: "" + - label: + id: status_ha + text_font: font_status + text: "" + - label: + id: status_footer + text_font: font_status + text_color: 0x8C8C8C + text: ""