next iteration on display, still some artifacting issues, and ux with buttons states and updating
This commit is contained in:
1 parent
45998b6176
commit
ae5d12b3a2
14 files changed
+860
-398
No files matched your search
+174
-14
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user