diff --git a/CLAUDE.md b/CLAUDE.md index f1e65ab..44d8978 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,16 @@ output (LOW silences it). No impact on UART download mode or OTA flashing. - 2-bit grayscale fully working via custom component `waveshare_epaper_2bit` - LVGL rendering working, 4-shade colorspace confirmed - Bayer ordered dithering implemented for gradient smoothing -- Touchscreen wiring complete, driver integration in progress +- Touchscreen working: polling mode, mirror_x + mirror_y transforms confirmed correct +- LVGL touch input and button state working end-to-end +- **Active: LVGL layout work** + +## Display Refresh Pattern +Two-tier refresh wired to `lvgl.on_draw_end` via two `mode: restart` scripts: +- **Partial** (`refresh_display`, 60ms): calls `display_partial()` — fast 1-bit, non-blocking, immediate interaction feedback +- **Full** (`full_refresh_display`, 10s): calls `component.update: display0` → `display()` — full 2-bit grayscale quality restore after idle + +60ms was empirically tuned: 80ms felt sluggish, 40ms intermittently raced. `component.update: display0` only pushes the frame buffer to hardware and does not re-trigger `on_draw_end`. ## Grayscale Implementation Custom component model name: `2.90inv2-r2-2bpp` @@ -43,7 +52,7 @@ Custom component model name: `2.90inv2-r2-2bpp` Key implementation details: - Gray4 waveform LUT loaded via register 0x32 on every display() call - Activation uses 0xC7 (custom LUT), not 0xF7 (which reloads OTP LUT) -- Always does full refresh — partial update removed from 2bpp class +- `display()` = full 2-bit grayscale refresh; `display_partial()` = fast 1-bit partial refresh - Second bitplane buffer (`buffer2_`) allocated in `initialize()` Confirmed bitplane table for this panel with Gray4 LUT: @@ -66,7 +75,7 @@ Amplitude chosen to keep 0xAAAAAA and 0x555555 as solid fills. - Touch count at register 0x1001, touch data at 0x1002 (7 bytes/point), clear by writing 0x00 to 0x1001 - Waveshare's own driver inverts both axes: X = 295 - raw_x, Y = 127 - raw_y - INT pulse is very brief (sub-ms); component works in polling mode regardless -- Coordinate transform (mirror_x, mirror_y, swap_xy) to be confirmed after first flash +- Coordinate transform: mirror_x + mirror_y confirmed correct; swap_xy not needed ## Relevant Files - Custom component: `custom_components/waveshare_epaper_2bit/` diff --git a/waveshare-test.yaml b/waveshare-test.yaml index 7a3315d..584fe7a 100644 --- a/waveshare-test.yaml +++ b/waveshare-test.yaml @@ -59,12 +59,28 @@ captive_portal: ### Setup Interfaces ### -# For the display +# Display spi: clk_pin: GPIO18 mosi_pin: GPIO23 -# For the touchscreen +display: + # This platform is a custom_component forked from upstream waveshare_epaper + # but modified by Claude to support 2bpp greyscale + - platform: waveshare_epaper_2bit + id: display0 + cs_pin: GPIO19 + dc_pin: GPIO17 + busy_pin: GPIO4 + reset_pin: GPIO5 + model: 2.90inv2-r2-2bpp + # model: 2.90inv2-r2 + full_update_every: 30 + rotation: 270° + auto_clear_enabled: false + update_interval: never + +# Touchscreen i2c: id: i2c_bus sda: GPIO21 @@ -72,6 +88,7 @@ i2c: scan: false touchscreen: + # This platform is a custom_component written from scratch by Claude platform: icnt86x id: touchscreen0 address: 0x48 @@ -88,7 +105,7 @@ touchscreen: ### Okay, now the Good Stuff ### -# Load some fonts for the display renderer (we don't neeed these with LVGL) +# Load some fonts for the display renderer font: - file: "FreeSans.ttf" id: font1 @@ -102,6 +119,7 @@ font: size: 30 # Pull in some icons from the material design library +# https://pictogrammers.com/library/mdi/ image: defaults: transparency: chroma_key @@ -129,8 +147,26 @@ image: - file: "mdi:home-off" id: away_button -# Force a full display refresh when we press the "boot" button on the devboard +# When the user taps the screen, introduce a small delay for the LVGL framebuffer +# to stabilize, then do a quick, 1-bit B/W selective update to visually indicate +# that a button was pushed. +# +# When the user is done interacting with the screen and several seconds have +# passed, do a slow, full-screen redraw at 2-bit grayscale. +script: + - id: refresh_display + mode: restart + then: + - delay: 60ms + - lambda: 'id(display0).display_partial();' + - id: full_refresh_display + mode: restart + then: + - delay: 10s + - component.update: display0 + binary_sensor: + # Force a full display refresh when we press the "boot" button on the devboard - platform: gpio pin: number: GPIO0 @@ -145,56 +181,52 @@ binary_sensor: - component.update: lvgl0 - delay: 1s - component.update: display0 + # List the actual home entitites that we want this panel to control here + - platform: homeassistant + id: goat_summit_light + entity_id: light.goat_summit_light + publish_initial_state: true + on_state: + then: + - lvgl.widget.update: + id: button_ceiling_fan_light + state: + checked: !lambda return x; -display: - - platform: waveshare_epaper_2bit - id: display0 - cs_pin: GPIO19 - dc_pin: GPIO17 - busy_pin: GPIO4 - reset_pin: GPIO5 - model: 2.90inv2-r2-2bpp - full_update_every: 30 - rotation: 270° - auto_clear_enabled: false - update_interval: never - # show_test_card: true - # lambda: |- - # it.print(0, 0, id(headerfont), "Bleat!"); - +# Here's where we put the actual GUI layout lvgl: - id: lvgl0 displays: - display0 touchscreens: - touchscreen0 - # on_draw_end: - # - component.update: display0 - bg_color: 0xFFFFFF + on_draw_end: + - script.execute: refresh_display + - script.execute: full_refresh_display # Black: 0x000000 # Dark Grey: 0x555555 # Light Grey: 0xAAAAAA # White: 0xFFFFFF + bg_color: 0x000000 theme: - label: - text_color: 0x000000 button: - # bg_color: 0xAAAAAA bg_color: 0xFFFFFF text_color: 0x000000 - height: 64 + height: 60 width: 64 pad_all: 0 border_width: 2 border_color: 0x000000 checked: - bg_color: 0x555555 + bg_color: 0x888888 + align: CENTER obj: bg_color: 0xFFFFFF widgets: - obj: width: 100% height: 100% + scrollable: false pad_all: 0 # pad_row: 0 # pad_column: 0 @@ -206,6 +238,7 @@ lvgl: grid_columns: [fr(1), fr(1), fr(1), fr(1)] widgets: - button: + id: button_ceiling_fan grid_cell_row_pos: 0 grid_cell_column_pos: 0 on_click: @@ -213,75 +246,81 @@ lvgl: widgets: - image: src: ceiling_fan + align: CENTER - button: + id: button_ceiling_fan_light grid_cell_row_pos: 0 grid_cell_column_pos: 1 on_click: - logger.log: "ceiling_fan_light" - - lambda: 'id(display0).display_partial();' + - homeassistant.action: + action: light.toggle + data: + entity_id: light.goat_summit_light checkable: true widgets: - image: src: ceiling_fan_light - button: + id: button_fan_off grid_cell_row_pos: 0 grid_cell_column_pos: 2 on_click: - logger.log: "fan_off" - - component.update: display0 checkable: true widgets: - image: src: fan_off - button: + id: button_fan_1 grid_cell_row_pos: 0 grid_cell_column_pos: 3 checkable: true on_click: - logger.log: "fan_speed_1" - lvgl.widget.update: - id: fan_speed_2 + id: button_fan_2 state: checked: false - lvgl.widget.update: - id: fan_speed_3 + id: button_fan_3 state: checked: false - - component.update: display0 widgets: - image: src: fan_speed_1 - button: + id: button_fan_2 grid_cell_row_pos: 1 grid_cell_column_pos: 0 on_click: - logger.log: "fan_speed_2" - - component.update: display0 checkable: true widgets: - image: src: fan_speed_2 - button: + id: button_fan_3 grid_cell_row_pos: 1 grid_cell_column_pos: 1 on_click: - logger.log: "fan_speed_3" - - component.update: display0 checkable: true widgets: - image: src: fan_speed_3 - button: + id: button_floor_lamp grid_cell_row_pos: 1 grid_cell_column_pos: 2 on_click: - logger.log: "floor_lamp" - - component.update: display0 checkable: true widgets: - image: src: floor_lamp - button: + id: button_away grid_cell_row_pos: 1 grid_cell_column_pos: 3 on_click: @@ -290,86 +329,3 @@ lvgl: widgets: - image: src: away_button - # - label: - # text: 'Test 2bpp 1636' - # align: TOP_LEFT - # text_font: font1 - # - button: - # id: button0 - # align: TOP_RIGHT - # on_click: - # - logger.log: "button0 clicked (unlock)" - # widgets: - # - image: - # src: home_lock_open - # outline_width: 1 - # - button: - # id: button1 - # align: BOTTOM_RIGHT - # on_click: - # - logger.log: "button1 clicked (lock)" - # widgets: - # - image: - # src: home_lock - # outline_width: 1 - # - slider: - # id: slider0 - # align: LEFT_MID - # width: 180 - # height: 16 - # min_value: 0 - # max_value: 100 - # value: 50 - # on_value: - # - logger.log: - # format: "Slider: %.0f" - # args: ['x'] - # - lambda: 'id(display0).display_partial();' - # - label: - # text: "0xFFFFFF" - # text_color: 0xFFFFFF - # x: 0 - # y: 15 - # - label: - # text: "0xAAAAAA" - # text_color: 0xAAAAAA - # x: 0 - # y: 30 - # - label: - # text: "0x555555" - # text_color: 0x555555 - # x: 0 - # y: 45 - # - label: - # text: "0x000000" - # text_color: 0x000000 - # x: 0 - # y: 60 - # - obj: - # align: BOTTOM_LEFT - # width: 33% - # height: 30% - # bg_color: 0x000000 #Black - # - obj: - # align: BOTTOM_MID - # width: 33% - # height: 30% - # bg_color: 0x555555 #Dark Grey - # - obj: - # align: BOTTOM_RIGHT - # width: 33% - # height: 30% - # bg_color: 0xAAAAAA #Light Grey - # - obj: - # align: TOP_RIGHT - # width: 33% - # height: 30% - # bg_color: 0xFFFFFF #White - # - obj: - # align: LEFT_MID - # width: 66% - # height: 30% - # bg_color: 0x000000 - # bg_grad_color: 0xFFFFFF - # bg_grad_dir: HOR -