Comment cleanup, added refresh delays for stability

This commit is contained in:
2026-05-09 10:16:59 -07:00
parent 5d3164847a
commit 53f91f5540
2 changed files with 87 additions and 122 deletions
+12 -3
View File
@@ -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` - 2-bit grayscale fully working via custom component `waveshare_epaper_2bit`
- LVGL rendering working, 4-shade colorspace confirmed - LVGL rendering working, 4-shade colorspace confirmed
- Bayer ordered dithering implemented for gradient smoothing - 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 ## Grayscale Implementation
Custom component model name: `2.90inv2-r2-2bpp` Custom component model name: `2.90inv2-r2-2bpp`
@@ -43,7 +52,7 @@ Custom component model name: `2.90inv2-r2-2bpp`
Key implementation details: Key implementation details:
- Gray4 waveform LUT loaded via register 0x32 on every display() call - Gray4 waveform LUT loaded via register 0x32 on every display() call
- Activation uses 0xC7 (custom LUT), not 0xF7 (which reloads OTP LUT) - 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()` - Second bitplane buffer (`buffer2_`) allocated in `initialize()`
Confirmed bitplane table for this panel with Gray4 LUT: 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 - 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 - 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 - 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 ## Relevant Files
- Custom component: `custom_components/waveshare_epaper_2bit/` - Custom component: `custom_components/waveshare_epaper_2bit/`
+75 -119
View File
@@ -59,12 +59,28 @@ captive_portal:
### Setup Interfaces ### ### Setup Interfaces ###
# For the display # Display
spi: spi:
clk_pin: GPIO18 clk_pin: GPIO18
mosi_pin: GPIO23 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: i2c:
id: i2c_bus id: i2c_bus
sda: GPIO21 sda: GPIO21
@@ -72,6 +88,7 @@ i2c:
scan: false scan: false
touchscreen: touchscreen:
# This platform is a custom_component written from scratch by Claude
platform: icnt86x platform: icnt86x
id: touchscreen0 id: touchscreen0
address: 0x48 address: 0x48
@@ -88,7 +105,7 @@ touchscreen:
### Okay, now the Good Stuff ### ### 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: font:
- file: "FreeSans.ttf" - file: "FreeSans.ttf"
id: font1 id: font1
@@ -102,6 +119,7 @@ font:
size: 30 size: 30
# Pull in some icons from the material design library # Pull in some icons from the material design library
# https://pictogrammers.com/library/mdi/
image: image:
defaults: defaults:
transparency: chroma_key transparency: chroma_key
@@ -129,8 +147,26 @@ image:
- file: "mdi:home-off" - file: "mdi:home-off"
id: away_button 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: binary_sensor:
# Force a full display refresh when we press the "boot" button on the devboard
- platform: gpio - platform: gpio
pin: pin:
number: GPIO0 number: GPIO0
@@ -145,56 +181,52 @@ binary_sensor:
- component.update: lvgl0 - component.update: lvgl0
- delay: 1s - delay: 1s
- component.update: display0 - 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: # Here's where we put the actual GUI layout
- 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!");
lvgl: lvgl:
- id: lvgl0 - id: lvgl0
displays: displays:
- display0 - display0
touchscreens: touchscreens:
- touchscreen0 - touchscreen0
# on_draw_end: on_draw_end:
# - component.update: display0 - script.execute: refresh_display
bg_color: 0xFFFFFF - script.execute: full_refresh_display
# Black: 0x000000 # Black: 0x000000
# Dark Grey: 0x555555 # Dark Grey: 0x555555
# Light Grey: 0xAAAAAA # Light Grey: 0xAAAAAA
# White: 0xFFFFFF # White: 0xFFFFFF
bg_color: 0x000000
theme: theme:
label:
text_color: 0x000000
button: button:
# bg_color: 0xAAAAAA
bg_color: 0xFFFFFF bg_color: 0xFFFFFF
text_color: 0x000000 text_color: 0x000000
height: 64 height: 60
width: 64 width: 64
pad_all: 0 pad_all: 0
border_width: 2 border_width: 2
border_color: 0x000000 border_color: 0x000000
checked: checked:
bg_color: 0x555555 bg_color: 0x888888
align: CENTER
obj: obj:
bg_color: 0xFFFFFF bg_color: 0xFFFFFF
widgets: widgets:
- obj: - obj:
width: 100% width: 100%
height: 100% height: 100%
scrollable: false
pad_all: 0 pad_all: 0
# pad_row: 0 # pad_row: 0
# pad_column: 0 # pad_column: 0
@@ -206,6 +238,7 @@ lvgl:
grid_columns: [fr(1), fr(1), fr(1), fr(1)] grid_columns: [fr(1), fr(1), fr(1), fr(1)]
widgets: widgets:
- button: - button:
id: button_ceiling_fan
grid_cell_row_pos: 0 grid_cell_row_pos: 0
grid_cell_column_pos: 0 grid_cell_column_pos: 0
on_click: on_click:
@@ -213,75 +246,81 @@ lvgl:
widgets: widgets:
- image: - image:
src: ceiling_fan src: ceiling_fan
align: CENTER
- button: - button:
id: button_ceiling_fan_light
grid_cell_row_pos: 0 grid_cell_row_pos: 0
grid_cell_column_pos: 1 grid_cell_column_pos: 1
on_click: on_click:
- logger.log: "ceiling_fan_light" - logger.log: "ceiling_fan_light"
- lambda: 'id(display0).display_partial();' - homeassistant.action:
action: light.toggle
data:
entity_id: light.goat_summit_light
checkable: true checkable: true
widgets: widgets:
- image: - image:
src: ceiling_fan_light src: ceiling_fan_light
- button: - button:
id: button_fan_off
grid_cell_row_pos: 0 grid_cell_row_pos: 0
grid_cell_column_pos: 2 grid_cell_column_pos: 2
on_click: on_click:
- logger.log: "fan_off" - logger.log: "fan_off"
- component.update: display0
checkable: true checkable: true
widgets: widgets:
- image: - image:
src: fan_off src: fan_off
- button: - button:
id: button_fan_1
grid_cell_row_pos: 0 grid_cell_row_pos: 0
grid_cell_column_pos: 3 grid_cell_column_pos: 3
checkable: true checkable: true
on_click: on_click:
- logger.log: "fan_speed_1" - logger.log: "fan_speed_1"
- lvgl.widget.update: - lvgl.widget.update:
id: fan_speed_2 id: button_fan_2
state: state:
checked: false checked: false
- lvgl.widget.update: - lvgl.widget.update:
id: fan_speed_3 id: button_fan_3
state: state:
checked: false checked: false
- component.update: display0
widgets: widgets:
- image: - image:
src: fan_speed_1 src: fan_speed_1
- button: - button:
id: button_fan_2
grid_cell_row_pos: 1 grid_cell_row_pos: 1
grid_cell_column_pos: 0 grid_cell_column_pos: 0
on_click: on_click:
- logger.log: "fan_speed_2" - logger.log: "fan_speed_2"
- component.update: display0
checkable: true checkable: true
widgets: widgets:
- image: - image:
src: fan_speed_2 src: fan_speed_2
- button: - button:
id: button_fan_3
grid_cell_row_pos: 1 grid_cell_row_pos: 1
grid_cell_column_pos: 1 grid_cell_column_pos: 1
on_click: on_click:
- logger.log: "fan_speed_3" - logger.log: "fan_speed_3"
- component.update: display0
checkable: true checkable: true
widgets: widgets:
- image: - image:
src: fan_speed_3 src: fan_speed_3
- button: - button:
id: button_floor_lamp
grid_cell_row_pos: 1 grid_cell_row_pos: 1
grid_cell_column_pos: 2 grid_cell_column_pos: 2
on_click: on_click:
- logger.log: "floor_lamp" - logger.log: "floor_lamp"
- component.update: display0
checkable: true checkable: true
widgets: widgets:
- image: - image:
src: floor_lamp src: floor_lamp
- button: - button:
id: button_away
grid_cell_row_pos: 1 grid_cell_row_pos: 1
grid_cell_column_pos: 3 grid_cell_column_pos: 3
on_click: on_click:
@@ -290,86 +329,3 @@ lvgl:
widgets: widgets:
- image: - image:
src: away_button 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