Fix TWDT starvation, vsync race, and UI responsiveness from review

Three bugs and two UX defects found on review, plus hardening:

- The control loop fed the task watchdog then slept exactly the watchdog
  period (both 10s), tripping the TWDT fractionally late on every single
  iteration. Sleep is now sliced 1s with a reset per slice. Also set
  CONFIG_ESP_TASK_WDT_PANIC=y - the sdkconfig comment claimed a hung task
  panics the chip, but nothing actually made that true.

- The LVGL flush called draw_bitmap (which only queues the framebuffer
  switch) and released LVGL immediately, letting it render the next frame
  into the buffer still being scanned out - the exact PRD 8.2 race the
  double-buffer design exists to prevent. display_present_frame() now
  blocks on a vsync handshake until the swap has latched.

- WiFi reconnect slept up to 30s inside the system event handler, stalling
  all queued WiFi/IP events. Moved to an esp_timer.

- The setpoint label, arc, and mode/room lines rendered the control loop's
  copy of state, which refreshes every 10s - every knob detent looked dead
  for up to 10 seconds. The UI now reads settings live; when the fallback
  ladder clamps the setpoint it shows both values. Room-select focus now
  shows which room is being selected.

- z2m ISO_8601_local timestamps carry a UTC offset the parser ignored,
  which would shift apparent reading age by the local offset (5-6h in the
  US) and permanently trip the fallback ladder. Offset is now honoured.

- Suppressed the cosmetic "control loop stopped" fault in the window
  between relay_init and the control task's first call-in at boot.

- Explicit pullups on the encoder A/B lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mattandClaude Opus 5 committed 2026-08-03 20:05:51 -07:00
1 parent d2e4c974dd
commit 4c7b120473
10 files changed
+206 -27

No files matched your search

+5 -5
View File
@@ -36,12 +36,12 @@ static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
(void)area;
/*
* px_map is one of the panel's own framebuffers, so this does not copy —
* the RGB driver just switches which buffer is being scanned out and waits
* for the switch to take effect.
* px_map is one of the panel's own framebuffers, so no copy happens — the
* RGB driver switches which buffer is scanned out. present_frame blocks
* until that switch has latched at vsync; releasing LVGL any earlier would
* let it render the next frame into the buffer still on screen.
*/
esp_lcd_panel_handle_t panel = display_panel();
esp_lcd_panel_draw_bitmap(panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, px_map);
display_present_frame(px_map);
lv_display_flush_ready(disp);
}
+29 -9
View File
@@ -179,7 +179,7 @@ static void build_screen(void)
static void refresh_temperatures(const control_state_t *st)
{
char buf[32];
char buf[40];
if (isnan(st->governing_temp_c)) {
set_text_if_changed(s_lbl_temp, "--" DEG);
@@ -189,15 +189,27 @@ static void refresh_temperatures(const control_state_t *st)
}
/*
* Show the effective setpoint, not the requested one. When the fallback
* ladder has clamped it, the screen must say what the thermostat is
* actually doing — the fault text below explains why.
* The setpoint shown must come from settings, live — not from control
* state, which only refreshes every CONTROL_INTERVAL_S. Rendering the
* control loop's copy makes the knob feel dead for up to 10 seconds per
* detent. When the fallback ladder has clamped the working setpoint, show
* both: what the user asked for and what the thermostat is actually
* holding — the fault text below explains why they differ.
*/
snprintf(buf, sizeof(buf), "Set %.0f%s",
c_to_display(st->effective_setpoint_c), unit_suffix());
float live_sp = control_get_setpoint();
bool capped = st->effective_setpoint_c < live_sp - 0.05f;
if (capped) {
snprintf(buf, sizeof(buf), "Set %.0f%s (cap %.0f)",
c_to_display(live_sp), unit_suffix(),
c_to_display(st->effective_setpoint_c));
} else {
snprintf(buf, sizeof(buf), "Set %.0f%s",
c_to_display(live_sp), unit_suffix());
}
set_text_if_changed(s_lbl_setpoint, buf);
int pct = (int)((st->effective_setpoint_c - CONTROL_SETPOINT_MIN_C) * 100.0f /
int pct = (int)((live_sp - CONTROL_SETPOINT_MIN_C) * 100.0f /
(CONTROL_SETPOINT_MAX_C - CONTROL_SETPOINT_MIN_C));
if (pct < 0) {
pct = 0;
@@ -216,13 +228,21 @@ static void refresh_room_and_mode(const control_state_t *st)
: "No sensor";
set_text_if_changed(s_lbl_room, room);
/*
* While a selection is in progress, read the *settings* value, not control
* state: the control loop only re-evaluates every CONTROL_INTERVAL_S, and
* showing its lagging copy would make the knob appear to do nothing.
* control_get_mode()/control_get_selected_room() go straight to settings.
*/
char buf[48];
switch (s_focus) {
case FOCUS_ROOM:
snprintf(buf, sizeof(buf), "> SELECT ROOM <");
snprintf(buf, sizeof(buf), "> ROOM: %s <",
zigbee_client_room_name(control_get_selected_room()));
break;
case FOCUS_MODE:
snprintf(buf, sizeof(buf), "> MODE: %s <", control_mode_str(st->mode));
snprintf(buf, sizeof(buf), "> MODE: %s <",
control_mode_str(control_get_mode()));
break;
default:
snprintf(buf, sizeof(buf), "%s", control_mode_str(st->mode));