Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -35,16 +35,26 @@ 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 working: polling mode, mirror_x + mirror_y transforms confirmed correct
|
- Touchscreen working: polling mode, portrait-orientation transform confirmed correct
|
||||||
- LVGL touch input and button state working end-to-end
|
- LVGL touch input and button state working end-to-end
|
||||||
|
- Panel now mounted vertically (portrait); 4-row layout for light/fan controls
|
||||||
- **Active: LVGL layout work**
|
- **Active: LVGL layout work**
|
||||||
|
|
||||||
## Display Refresh Pattern
|
## Display Refresh Pattern
|
||||||
Two-tier refresh wired to `lvgl.on_draw_end` via two `mode: restart` scripts:
|
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
|
- **Partial** (`refresh_display`, 80ms then a 350ms retry): 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
|
- **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`.
|
The retry exists because `display_partial()` silently drops calls when the
|
||||||
|
e-paper is still busy from a previous refresh (`waveshare_epaper_2bit.cpp`
|
||||||
|
~line 2003). Without it, HA-triggered widget updates (e.g. fan speed sync
|
||||||
|
arriving 200-500ms after a tap) could land during the busy window of the
|
||||||
|
tap's partial refresh and be lost until the 10s full refresh. The retry
|
||||||
|
fires ~430ms after the last draw, by which point the e-paper's ~300ms
|
||||||
|
partial cycle has cleared. `mode: restart` cancels the retry if a new
|
||||||
|
draw arrives, so it only fires in the quiet period.
|
||||||
|
|
||||||
|
`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`
|
||||||
@@ -75,7 +85,11 @@ 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 confirmed correct; swap_xy not needed
|
- Coordinate transform (portrait, display rotation 0°): `swap_xy: true, mirror_x: true`,
|
||||||
|
calibration `x_max: 127, y_max: 295`. Gotcha: ESPHome's base class applies `swap_xy`
|
||||||
|
*before* calibration normalization (see `touchscreen.cpp::add_raw_touch_position_`),
|
||||||
|
so calibration `x_max`/`y_max` must describe the **post-swap** range — i.e., match
|
||||||
|
the display width/height, not the raw sensor's native axes.
|
||||||
|
|
||||||
## Relevant Files
|
## Relevant Files
|
||||||
- Custom component: `custom_components/waveshare_epaper_2bit/`
|
- Custom component: `custom_components/waveshare_epaper_2bit/`
|
||||||
@@ -85,4 +99,5 @@ Amplitude chosen to keep 0xAAAAAA and 0x555555 as solid fills.
|
|||||||
- `waveshare_epaper_2bit.h` — header
|
- `waveshare_epaper_2bit.h` — header
|
||||||
- Main config: `waveshare-test.yaml` (pin table here is source of truth)
|
- Main config: `waveshare-test.yaml` (pin table here is source of truth)
|
||||||
- Build cache: `.esphome/build/waveshare-epaper-test/src/esphome/components/`
|
- Build cache: `.esphome/build/waveshare-epaper-test/src/esphome/components/`
|
||||||
- ESPHome source (venv): ~/Code/esphome-2026.1.0/
|
- ESPHome venv (Windows): `C:\Projects\python_virtual\esphome-2026.1.0` (activate before `esphome` commands)
|
||||||
|
- ESPHome source (WSL venv): ~/Code/esphome-2026.1.0/
|
||||||
|
|||||||
+136
-34
@@ -81,7 +81,7 @@ display:
|
|||||||
model: 2.90inv2-r2-2bpp
|
model: 2.90inv2-r2-2bpp
|
||||||
# model: 2.90inv2-r2
|
# model: 2.90inv2-r2
|
||||||
full_update_every: 30
|
full_update_every: 30
|
||||||
rotation: 270°
|
rotation: 0°
|
||||||
auto_clear_enabled: false
|
auto_clear_enabled: false
|
||||||
update_interval: never
|
update_interval: never
|
||||||
|
|
||||||
@@ -101,12 +101,12 @@ touchscreen:
|
|||||||
update_interval: 50ms
|
update_interval: 50ms
|
||||||
calibration:
|
calibration:
|
||||||
x_min: 0
|
x_min: 0
|
||||||
x_max: 295
|
x_max: 127
|
||||||
y_min: 0
|
y_min: 0
|
||||||
y_max: 127
|
y_max: 295
|
||||||
transform:
|
transform:
|
||||||
|
swap_xy: true
|
||||||
mirror_x: true
|
mirror_x: true
|
||||||
mirror_y: true
|
|
||||||
|
|
||||||
### Okay, now the Good Stuff ###
|
### Okay, now the Good Stuff ###
|
||||||
|
|
||||||
@@ -151,6 +151,8 @@ image:
|
|||||||
id: floor_lamp
|
id: floor_lamp
|
||||||
- file: "mdi:home-off"
|
- file: "mdi:home-off"
|
||||||
id: away_button
|
id: away_button
|
||||||
|
- file: "mdi:thermometer"
|
||||||
|
id: light_temp
|
||||||
|
|
||||||
# When the user taps the screen, introduce a small delay for the LVGL framebuffer
|
# 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
|
# to stabilize, then do a quick, 1-bit B/W selective update to visually indicate
|
||||||
@@ -162,7 +164,12 @@ script:
|
|||||||
- id: refresh_display
|
- id: refresh_display
|
||||||
mode: restart
|
mode: restart
|
||||||
then:
|
then:
|
||||||
- delay: 60ms
|
- delay: 80ms
|
||||||
|
- lambda: 'id(display0).display_partial();'
|
||||||
|
# Retry once the e-paper's busy window has cleared, to catch HA-synced
|
||||||
|
# widget updates that landed while the first partial refresh was in flight
|
||||||
|
# (display_partial drops calls when busy).
|
||||||
|
- delay: 350ms
|
||||||
- lambda: 'id(display0).display_partial();'
|
- lambda: 'id(display0).display_partial();'
|
||||||
- id: full_refresh_display
|
- id: full_refresh_display
|
||||||
mode: restart
|
mode: restart
|
||||||
@@ -188,8 +195,8 @@ binary_sensor:
|
|||||||
- component.update: display0
|
- component.update: display0
|
||||||
# List the actual home entitites that we want this panel to control here
|
# List the actual home entitites that we want this panel to control here
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
id: goat_summit_light
|
id: alpha_bedroom_light
|
||||||
entity_id: light.goat_summit_light
|
entity_id: light.alpha_bedroom_light
|
||||||
publish_initial_state: true
|
publish_initial_state: true
|
||||||
on_state:
|
on_state:
|
||||||
then:
|
then:
|
||||||
@@ -197,6 +204,38 @@ binary_sensor:
|
|||||||
id: button_ceiling_fan_light
|
id: button_ceiling_fan_light
|
||||||
state:
|
state:
|
||||||
checked: !lambda return x;
|
checked: !lambda return x;
|
||||||
|
- platform: homeassistant
|
||||||
|
id: alpha_bedroom_ceiling_fan
|
||||||
|
entity_id: fan.alpha_bedroom_ceiling_fan
|
||||||
|
publish_initial_state: true
|
||||||
|
on_state:
|
||||||
|
then:
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_ceiling_fan
|
||||||
|
state:
|
||||||
|
checked: !lambda return x;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
# Track the fan's current percentage so the speed buttons reflect the
|
||||||
|
# actual HA state (including changes made via other controls).
|
||||||
|
- platform: homeassistant
|
||||||
|
id: alpha_bedroom_ceiling_fan_pct
|
||||||
|
entity_id: fan.alpha_bedroom_ceiling_fan
|
||||||
|
attribute: percentage
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_1
|
||||||
|
state:
|
||||||
|
checked: !lambda 'return x >= 1 && x < 50;'
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_2
|
||||||
|
state:
|
||||||
|
checked: !lambda 'return x >= 50 && x < 84;'
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_3
|
||||||
|
state:
|
||||||
|
checked: !lambda 'return x >= 84;'
|
||||||
|
|
||||||
# Here's where we put the actual GUI layout
|
# Here's where we put the actual GUI layout
|
||||||
lvgl:
|
lvgl:
|
||||||
@@ -239,50 +278,74 @@ lvgl:
|
|||||||
bg_color: 0xFFFFFF
|
bg_color: 0xFFFFFF
|
||||||
layout:
|
layout:
|
||||||
type: GRID
|
type: GRID
|
||||||
grid_rows: [fr(1), fr(1)]
|
grid_rows: [fr(1), fr(1), fr(1), fr(1)]
|
||||||
grid_columns: [fr(1), fr(1), fr(1), fr(1)]
|
grid_columns: [fr(1), fr(1)]
|
||||||
widgets:
|
widgets:
|
||||||
- button:
|
# Row 1: Light on/off (full width)
|
||||||
id: button_ceiling_fan
|
|
||||||
grid_cell_row_pos: 0
|
|
||||||
grid_cell_column_pos: 0
|
|
||||||
on_click:
|
|
||||||
- logger.log: "ceiling_fan"
|
|
||||||
widgets:
|
|
||||||
- image:
|
|
||||||
src: ceiling_fan
|
|
||||||
align: CENTER
|
|
||||||
- button:
|
- button:
|
||||||
id: button_ceiling_fan_light
|
id: button_ceiling_fan_light
|
||||||
grid_cell_row_pos: 0
|
grid_cell_row_pos: 0
|
||||||
grid_cell_column_pos: 1
|
grid_cell_column_pos: 0
|
||||||
|
grid_cell_column_span: 2
|
||||||
|
grid_cell_x_align: STRETCH
|
||||||
|
width: 124
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "ceiling_fan_light"
|
- logger.log: "ceiling_fan_light"
|
||||||
- homeassistant.action:
|
- homeassistant.action:
|
||||||
action: light.toggle
|
action: light.toggle
|
||||||
data:
|
data:
|
||||||
entity_id: light.goat_summit_light
|
entity_id: light.alpha_bedroom_light
|
||||||
checkable: true
|
checkable: true
|
||||||
widgets:
|
widgets:
|
||||||
- image:
|
- image:
|
||||||
src: ceiling_fan_light
|
src: ceiling_fan_light
|
||||||
|
align: CENTER
|
||||||
|
# Row 2: Fan on/off (full width)
|
||||||
- button:
|
- button:
|
||||||
id: button_fan_off
|
id: button_ceiling_fan
|
||||||
grid_cell_row_pos: 0
|
grid_cell_row_pos: 1
|
||||||
grid_cell_column_pos: 2
|
grid_cell_column_pos: 0
|
||||||
|
grid_cell_column_span: 2
|
||||||
|
grid_cell_x_align: STRETCH
|
||||||
|
width: 124
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "fan_off"
|
- logger.log: "ceiling_fan"
|
||||||
|
- homeassistant.action:
|
||||||
|
action: fan.toggle
|
||||||
|
data:
|
||||||
|
entity_id: fan.alpha_bedroom_ceiling_fan
|
||||||
checkable: true
|
checkable: true
|
||||||
widgets:
|
widgets:
|
||||||
- image:
|
- image:
|
||||||
src: fan_off
|
src: ceiling_fan
|
||||||
|
align: CENTER
|
||||||
|
# Row 3: Light temperature | Fan low
|
||||||
|
- button:
|
||||||
|
id: button_light_temp
|
||||||
|
grid_cell_row_pos: 2
|
||||||
|
grid_cell_column_pos: 0
|
||||||
|
on_click:
|
||||||
|
- logger.log: "light_temp"
|
||||||
|
- homeassistant.action:
|
||||||
|
action: button.press
|
||||||
|
data:
|
||||||
|
entity_id: button.alpha_bedroom_color_temp
|
||||||
|
widgets:
|
||||||
|
- image:
|
||||||
|
src: light_temp
|
||||||
|
align: CENTER
|
||||||
- button:
|
- button:
|
||||||
id: button_fan_1
|
id: button_fan_1
|
||||||
grid_cell_row_pos: 0
|
grid_cell_row_pos: 2
|
||||||
grid_cell_column_pos: 3
|
grid_cell_column_pos: 1
|
||||||
checkable: true
|
checkable: true
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "fan_speed_1"
|
- logger.log: "fan_speed_1"
|
||||||
|
- homeassistant.action:
|
||||||
|
action: fan.set_percentage
|
||||||
|
data:
|
||||||
|
entity_id: fan.alpha_bedroom_ceiling_fan
|
||||||
|
percentage: '33'
|
||||||
- lvgl.widget.update:
|
- lvgl.widget.update:
|
||||||
id: button_fan_2
|
id: button_fan_2
|
||||||
state:
|
state:
|
||||||
@@ -294,30 +357,71 @@ lvgl:
|
|||||||
widgets:
|
widgets:
|
||||||
- image:
|
- image:
|
||||||
src: fan_speed_1
|
src: fan_speed_1
|
||||||
|
# Row 4: Fan medium | Fan high
|
||||||
- button:
|
- button:
|
||||||
id: button_fan_2
|
id: button_fan_2
|
||||||
grid_cell_row_pos: 1
|
grid_cell_row_pos: 3
|
||||||
grid_cell_column_pos: 0
|
grid_cell_column_pos: 0
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "fan_speed_2"
|
- logger.log: "fan_speed_2"
|
||||||
|
- homeassistant.action:
|
||||||
|
action: fan.set_percentage
|
||||||
|
data:
|
||||||
|
entity_id: fan.alpha_bedroom_ceiling_fan
|
||||||
|
percentage: '66'
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_1
|
||||||
|
state:
|
||||||
|
checked: false
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_3
|
||||||
|
state:
|
||||||
|
checked: false
|
||||||
checkable: true
|
checkable: true
|
||||||
widgets:
|
widgets:
|
||||||
- image:
|
- image:
|
||||||
src: fan_speed_2
|
src: fan_speed_2
|
||||||
- button:
|
- button:
|
||||||
id: button_fan_3
|
id: button_fan_3
|
||||||
grid_cell_row_pos: 1
|
grid_cell_row_pos: 3
|
||||||
grid_cell_column_pos: 1
|
grid_cell_column_pos: 1
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "fan_speed_3"
|
- logger.log: "fan_speed_3"
|
||||||
|
- homeassistant.action:
|
||||||
|
action: fan.set_percentage
|
||||||
|
data:
|
||||||
|
entity_id: fan.alpha_bedroom_ceiling_fan
|
||||||
|
percentage: '100'
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_1
|
||||||
|
state:
|
||||||
|
checked: false
|
||||||
|
- lvgl.widget.update:
|
||||||
|
id: button_fan_2
|
||||||
|
state:
|
||||||
|
checked: false
|
||||||
checkable: true
|
checkable: true
|
||||||
widgets:
|
widgets:
|
||||||
- image:
|
- image:
|
||||||
src: fan_speed_3
|
src: fan_speed_3
|
||||||
|
# Unused buttons — kept defined but hidden for future use
|
||||||
|
- obj:
|
||||||
|
hidden: true
|
||||||
|
width: 1
|
||||||
|
height: 1
|
||||||
|
border_width: 0
|
||||||
|
pad_all: 0
|
||||||
|
widgets:
|
||||||
|
- button:
|
||||||
|
id: button_fan_off
|
||||||
|
on_click:
|
||||||
|
- logger.log: "fan_off"
|
||||||
|
checkable: true
|
||||||
|
widgets:
|
||||||
|
- image:
|
||||||
|
src: fan_off
|
||||||
- button:
|
- button:
|
||||||
id: button_floor_lamp
|
id: button_floor_lamp
|
||||||
grid_cell_row_pos: 1
|
|
||||||
grid_cell_column_pos: 2
|
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "floor_lamp"
|
- logger.log: "floor_lamp"
|
||||||
checkable: true
|
checkable: true
|
||||||
@@ -326,8 +430,6 @@ lvgl:
|
|||||||
src: floor_lamp
|
src: floor_lamp
|
||||||
- button:
|
- button:
|
||||||
id: button_away
|
id: button_away
|
||||||
grid_cell_row_pos: 1
|
|
||||||
grid_cell_column_pos: 3
|
|
||||||
on_click:
|
on_click:
|
||||||
- logger.log: "away_button"
|
- logger.log: "away_button"
|
||||||
checkable: true
|
checkable: true
|
||||||
|
|||||||
Reference in New Issue
Block a user