next iteration on display, still some artifacting issues, and ux with buttons states and updating

This commit is contained in:
matt committed 2026-08-04 19:38:58 -07:00
1 parent 45998b6176
commit ae5d12b3a2
14 files changed
+860 -398

No files matched your search

+174 -14
View File
@@ -109,6 +109,7 @@ static lv_obj_t *cond_icon;
static lv_obj_t *temp_label;
static lv_obj_t *temp_unit_label;
static lv_obj_t *indoor_detail_label;
static lv_obj_t *trend_indoor_icon;
/* 3x2 reading grid: value and caption per cell. */
enum {
@@ -122,6 +123,8 @@ enum {
};
static lv_obj_t *cell_value[CELL_COUNT];
static lv_obj_t *cell_sub[CELL_COUNT];
/* Trend arrows; only the outdoor and pressure slots are created. */
static lv_obj_t *cell_trend[CELL_COUNT];
static lv_obj_t *light_tile[HA_LIGHT_COUNT];
static lv_obj_t *light_icon[HA_LIGHT_COUNT];
@@ -170,6 +173,41 @@ typedef struct {
} light_hold_t;
static light_hold_t light_hold[HA_LIGHT_COUNT];
/* Slider press bookkeeping: whether the press-down command has been sent for
* the current touch, and at what percentage, so the release command can be
* skipped when the finger never moved. */
static bool slider_touched[HA_LIGHT_COUNT];
static int32_t slider_sent_pct[HA_LIGHT_COUNT];
/*
* One-shot "repair" repaint, rescheduled by every interaction to fire 600ms
* after the last one.
*
* Redrawing is itself what glitches the panel: the flush's PSRAM writes
* contend with the LCD's scan-out, and a burst of interactive redraws is the
* most likely moment for the bounce ISR to lose (README, PSRAM bandwidth).
* The glitched lines then persist because LVGL believes those pixels are
* fine. One full repaint after the burst settles cleans them up within
* ~0.6s instead of leaving them until the next poll's repaint, up to 60s
* away. It runs once per burst, not periodically -- a constant repaint loop
* would be a constant supply of fresh glitch opportunities.
*/
static lv_timer_t *repair_timer;
static void repair_timer_cb(lv_timer_t *t)
{
lv_obj_invalidate(lv_screen_active());
lv_timer_pause(t);
}
static void schedule_repair(void)
{
if (repair_timer != NULL) {
lv_timer_reset(repair_timer);
lv_timer_resume(repair_timer);
}
}
/* --------------------------------------------------------- condition icons */
typedef struct {
@@ -284,6 +322,32 @@ static lv_obj_t *icon_create(lv_obj_t *parent, const char *glyph,
return l;
}
/*
* Trend arrow: up warm, down blue, nothing for flat or unknown. "No arrow"
* covers both "steady" and "not enough history yet" -- a reading that is
* going nowhere and a reading we cannot judge should both look calm.
*/
static void trend_render(lv_obj_t *icon, ha_trend_t t)
{
if (icon == NULL || icon_font_16 == NULL) {
return;
}
switch (t) {
case HA_TREND_RISING:
label_set_if_changed(icon, ICON_TREND_UP);
lv_obj_set_style_text_color(icon, lv_color_hex(COL_ACCENT_WARM), LV_PART_MAIN);
break;
case HA_TREND_FALLING:
label_set_if_changed(icon, ICON_TREND_DOWN);
lv_obj_set_style_text_color(icon, lv_color_hex(COL_ACCENT), LV_PART_MAIN);
break;
default:
label_set_if_changed(icon, "");
break;
}
}
/* ------------------------------------------------------------ light tiles */
static void light_tile_update(int idx, const ha_light_status_t *st);
@@ -341,6 +405,7 @@ static void light_tile_clicked(lv_event_t *e)
: -1,
};
light_apply_optimistic(idx, expect);
schedule_repair();
/* -1: a tap is on/off only. Brightness is the slider's job, and sending
* the current brightness here would fight a user who dimmed the light
@@ -348,6 +413,68 @@ static void light_tile_clicked(lv_event_t *e)
light_cb(idx, turn_on, -1);
}
/* One command for a slider position. Dragging to zero means off, not "on at
* zero brightness" -- HA accepts brightness 0 but leaves the light on, which
* looks like a bug. */
static void light_send_pct(int idx, int32_t pct)
{
if (pct <= 0) {
light_apply_optimistic(idx, (ha_light_status_t){ HA_LIGHT_OFF, -1 });
light_cb(idx, false, -1);
} else {
const int16_t brightness = (int16_t)((pct * 255) / 100);
light_apply_optimistic(idx, (ha_light_status_t){ HA_LIGHT_ON, brightness });
light_cb(idx, true, brightness);
}
slider_sent_pct[idx] = pct;
}
/*
* Fires as the knob moves under a finger (programmatic set_value does not
* raise VALUE_CHANGED, so this is user input only). Two jobs:
*
* - keep the tile's readout live under the finger ("On 43%" tracks the
* drag), because the screen must follow the finger, never the light;
* - send ONE command at the first touch position, so the light comes on
* at the level the finger landed on without waiting for the lift.
*
* Everything between first touch and lift is display-only.
*/
static void light_slider_changed(lv_event_t *e)
{
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
if (idx < 0 || idx >= HA_LIGHT_COUNT) {
return;
}
lv_obj_t *slider = lv_event_get_target_obj(e);
const int32_t pct = lv_slider_get_value(slider);
char buf[24];
if (pct <= 0) {
snprintf(buf, sizeof(buf), "Off");
} else {
snprintf(buf, sizeof(buf), "On %d%%", (int)pct);
}
label_set_if_changed(light_state[idx], buf);
lv_obj_set_style_text_color(light_state[idx],
lv_color_hex(pct > 0 ? COL_ON : COL_TEXT_DIM),
LV_PART_MAIN);
schedule_repair();
if (light_cb == NULL || !lv_obj_has_state(slider, LV_STATE_PRESSED)) {
return;
}
if (!slider_touched[idx]) {
slider_touched[idx] = true;
light_send_pct(idx, pct);
}
}
/* Finger lifted: send the final position -- unless the finger never moved
* from where the press-down command already put it. */
static void light_slider_released(lv_event_t *e)
{
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
@@ -358,17 +485,16 @@ static void light_slider_released(lv_event_t *e)
lv_obj_t *slider = lv_event_get_target_obj(e);
const int32_t pct = lv_slider_get_value(slider);
/* Dragging to zero means off, not "on at zero brightness" -- HA accepts
* brightness 0 but leaves the light on, which looks like a bug. */
if (pct <= 0) {
light_apply_optimistic(idx, (ha_light_status_t){ HA_LIGHT_OFF, -1 });
light_cb(idx, false, -1);
return;
const bool touched = slider_touched[idx];
slider_touched[idx] = false;
schedule_repair();
if (touched && pct == slider_sent_pct[idx]) {
return; /* tap without drag: the press command already said this */
}
const int16_t brightness = (int16_t)((pct * 255) / 100);
light_apply_optimistic(idx, (ha_light_status_t){ HA_LIGHT_ON, brightness });
light_cb(idx, true, brightness);
light_send_pct(idx, pct);
}
static void light_tile_build(lv_obj_t *parent, int idx, int32_t w, int32_t h)
@@ -430,11 +556,15 @@ static void light_tile_build(lv_obj_t *parent, int idx, int32_t w, int32_t h)
lv_obj_set_style_pad_all(s, 2, LV_PART_KNOB);
/*
* RELEASED only: the command is sent when the finger lifts, once.
* VALUE_CHANGED fires per pixel of drag and would queue a command
* storm; it is also why ui_update never writes to a pressed slider
* -- nothing may move the knob out from under the finger.
* Two commands per touch, not per pixel: VALUE_CHANGED sends one at
* the first touch position (light responds immediately where the
* finger lands), RELEASED sends the final position. In between, the
* drag only updates the display -- and ui_update never writes to a
* pressed slider, so nothing moves the knob out from under the
* finger.
*/
lv_obj_add_event_cb(s, light_slider_changed, LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)idx);
lv_obj_add_event_cb(s, light_slider_released, LV_EVENT_RELEASED,
(void *)(intptr_t)idx);
light_slider[idx] = s;
@@ -492,7 +622,11 @@ static void light_tile_update(int idx, const ha_light_status_t *st)
const int32_t pct = (on && st->brightness >= 0)
? (st->brightness * 100 + 127) / 255
: 0;
lv_slider_set_value(light_slider[idx], pct, LV_ANIM_OFF);
/* Never move a knob a finger is holding -- the optimistic apply on
* press-down lands here while the drag is still in progress. */
if (!lv_obj_has_state(light_slider[idx], LV_STATE_PRESSED)) {
lv_slider_set_value(light_slider[idx], pct, LV_ANIM_OFF);
}
/* Bright knob only while the light is on, so the lit tiles are what
* draw the eye along the row. */
@@ -601,6 +735,11 @@ static void build_body(lv_obj_t *scr)
temp_unit_label = label_create(card, "°F", &lv_font_montserrat_20, COL_TEXT_DIM);
lv_obj_align_to(temp_unit_label, temp_label, LV_ALIGN_OUT_RIGHT_TOP, 4, 8);
/* Unit sits high beside the number; the trend arrow sits low, so the
* pair reads as one annotated figure. */
trend_indoor_icon = icon_create(card, "", icon_font_26, COL_TEXT_DIM);
lv_obj_align_to(trend_indoor_icon, temp_label, LV_ALIGN_OUT_RIGHT_BOTTOM, 8, -4);
indoor_detail_label = label_create(card, "--", &lv_font_montserrat_16, COL_TEXT_DIM);
lv_obj_align(indoor_detail_label, LV_ALIGN_BOTTOM_LEFT, 0, 0);
@@ -641,6 +780,12 @@ static void build_body(lv_obj_t *scr)
cell_value[i] = label_create(cell, NO_VALUE, &lv_font_montserrat_24, COL_TEXT);
lv_obj_align(cell_value[i], LV_ALIGN_LEFT_MID, 0, 4);
/* Only readings with a meaningful hourly drift get an arrow slot. */
if (i == CELL_OUTDOOR || i == CELL_PRESSURE) {
cell_trend[i] = icon_create(cell, "", icon_font_16, COL_TEXT_DIM);
lv_obj_align_to(cell_trend[i], cell_value[i], LV_ALIGN_OUT_RIGHT_MID, 6, 0);
}
cell_sub[i] = label_create(cell, "", &lv_font_montserrat_12, COL_TEXT_DIM);
lv_obj_align(cell_sub[i], LV_ALIGN_BOTTOM_LEFT, 0, 0);
}
@@ -709,7 +854,10 @@ static void update_body(const ha_data_t *d)
snprintf(buf, sizeof(buf), NO_VALUE);
}
label_set_if_changed(temp_label, buf);
/* Both hang off the number, whose width just changed with its digits. */
lv_obj_align_to(temp_unit_label, temp_label, LV_ALIGN_OUT_RIGHT_TOP, 4, 8);
trend_render(trend_indoor_icon, d->trend_indoor);
lv_obj_align_to(trend_indoor_icon, temp_label, LV_ALIGN_OUT_RIGHT_BOTTOM, 8, -4);
if (d->indoor_humidity.valid) {
snprintf(buf, sizeof(buf), "%.0f%% humidity", d->indoor_humidity.value);
@@ -725,6 +873,9 @@ static void update_body(const ha_data_t *d)
snprintf(buf, sizeof(buf), NO_VALUE);
}
label_set_if_changed(cell_value[CELL_OUTDOOR], buf);
trend_render(cell_trend[CELL_OUTDOOR], d->trend_outdoor);
lv_obj_align_to(cell_trend[CELL_OUTDOOR], cell_value[CELL_OUTDOOR],
LV_ALIGN_OUT_RIGHT_MID, 6, 0);
if (d->outdoor_humidity.valid && d->dew_point_f.valid) {
snprintf(sub, sizeof(sub), "%.0f%% dew %.0f°",
@@ -783,6 +934,9 @@ static void update_body(const ha_data_t *d)
/* Pressure */
format_value(&d->pressure_inhg, "%.2f", buf, sizeof(buf));
label_set_if_changed(cell_value[CELL_PRESSURE], buf);
trend_render(cell_trend[CELL_PRESSURE], d->trend_pressure);
lv_obj_align_to(cell_trend[CELL_PRESSURE], cell_value[CELL_PRESSURE],
LV_ALIGN_OUT_RIGHT_MID, 6, 0);
label_set_if_changed(cell_sub[CELL_PRESSURE], "inHg");
}
@@ -936,6 +1090,12 @@ void ui_init(void)
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
/* Created paused; interactions reset-and-resume it. See schedule_repair. */
repair_timer = lv_timer_create(repair_timer_cb, 600, NULL);
if (repair_timer != NULL) {
lv_timer_pause(repair_timer);
}
build_header(scr);
build_body(scr);
build_lights(scr);