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>
188 lines
4.9 KiB
C
188 lines
4.9 KiB
C
#include "lvgl_port.h"
|
|
|
|
#include "cst826.h"
|
|
#include "display.h"
|
|
#include "esp_check.h"
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "freertos/task.h"
|
|
|
|
static const char *TAG = "lvgl";
|
|
|
|
#define TICK_PERIOD_MS 2
|
|
#define TASK_STACK 8192
|
|
#define TASK_PRIO 3
|
|
#define TASK_MAX_DELAY_MS 20
|
|
#define TASK_MIN_DELAY_MS 2
|
|
|
|
static SemaphoreHandle_t s_lock;
|
|
static lv_display_t *s_disp;
|
|
static lv_indev_t *s_touch_indev;
|
|
|
|
/* ---------------------------------------------------------------- tick -- */
|
|
|
|
static void tick_cb(void *arg)
|
|
{
|
|
(void)arg;
|
|
lv_tick_inc(TICK_PERIOD_MS);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- flush -- */
|
|
|
|
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 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.
|
|
*/
|
|
display_present_frame(px_map);
|
|
|
|
lv_display_flush_ready(disp);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- input -- */
|
|
|
|
static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
|
|
{
|
|
(void)indev;
|
|
|
|
cst826_touch_t t;
|
|
cst826_read(&t);
|
|
|
|
if (t.pressed) {
|
|
data->point.x = t.x;
|
|
data->point.y = t.y;
|
|
data->state = LV_INDEV_STATE_PRESSED;
|
|
} else {
|
|
data->state = LV_INDEV_STATE_RELEASED;
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- task -- */
|
|
|
|
static void lvgl_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
ESP_LOGI(TAG, "service task running");
|
|
|
|
while (1) {
|
|
uint32_t next = TASK_MAX_DELAY_MS;
|
|
|
|
if (lvgl_port_lock(-1)) {
|
|
next = lv_timer_handler();
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
if (next > TASK_MAX_DELAY_MS) {
|
|
next = TASK_MAX_DELAY_MS;
|
|
} else if (next < TASK_MIN_DELAY_MS) {
|
|
next = TASK_MIN_DELAY_MS;
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(next));
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- init -- */
|
|
|
|
esp_err_t lvgl_port_init(void)
|
|
{
|
|
if (s_disp) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
void *fb0 = NULL, *fb1 = NULL;
|
|
esp_err_t err = display_get_framebuffers(&fb0, &fb1);
|
|
if (err != ESP_OK || !fb0 || !fb1) {
|
|
ESP_LOGE(TAG, "no framebuffers — display_init() must run first");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
s_lock = xSemaphoreCreateRecursiveMutex();
|
|
if (!s_lock) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
/*
|
|
* PRD 8.2: lv_init() before creating any LVGL object. Calling
|
|
* lv_display_create() first faults with LoadProhibited inside the TLSF
|
|
* allocator, which is a confusing way to discover an ordering bug.
|
|
*/
|
|
if (!lv_is_initialized()) {
|
|
lv_init();
|
|
}
|
|
|
|
s_disp = lv_display_create(DISPLAY_H_RES, DISPLAY_V_RES);
|
|
if (!s_disp) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
lv_display_set_color_format(s_disp, LV_COLOR_FORMAT_RGB565);
|
|
lv_display_set_flush_cb(s_disp, flush_cb);
|
|
|
|
size_t fb_bytes = (size_t)DISPLAY_H_RES * DISPLAY_V_RES * 2;
|
|
lv_display_set_buffers(s_disp, fb0, fb1, fb_bytes,
|
|
LV_DISPLAY_RENDER_MODE_FULL);
|
|
|
|
/* Touch, if the controller answered. The thermostat is fully usable from
|
|
* the encoder alone, so a missing touch panel is not fatal. */
|
|
if (cst826_is_ready()) {
|
|
s_touch_indev = lv_indev_create();
|
|
lv_indev_set_type(s_touch_indev, LV_INDEV_TYPE_POINTER);
|
|
lv_indev_set_read_cb(s_touch_indev, touch_read_cb);
|
|
lv_indev_set_display(s_touch_indev, s_disp);
|
|
} else {
|
|
ESP_LOGW(TAG, "touch unavailable — encoder only");
|
|
}
|
|
|
|
const esp_timer_create_args_t tick_args = {
|
|
.callback = tick_cb,
|
|
.name = "lv_tick",
|
|
};
|
|
esp_timer_handle_t tick_timer;
|
|
ESP_RETURN_ON_ERROR(esp_timer_create(&tick_args, &tick_timer),
|
|
TAG, "tick timer");
|
|
ESP_RETURN_ON_ERROR(
|
|
esp_timer_start_periodic(tick_timer, TICK_PERIOD_MS * 1000),
|
|
TAG, "tick start");
|
|
|
|
if (xTaskCreate(lvgl_task, "lvgl", TASK_STACK, NULL, TASK_PRIO, NULL)
|
|
!= pdPASS) {
|
|
ESP_LOGE(TAG, "task create failed");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "up: %dx%d, full-refresh double buffered",
|
|
DISPLAY_H_RES, DISPLAY_V_RES);
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool lvgl_port_lock(int timeout_ms)
|
|
{
|
|
if (!s_lock) {
|
|
return false;
|
|
}
|
|
TickType_t wait = (timeout_ms < 0) ? portMAX_DELAY
|
|
: pdMS_TO_TICKS(timeout_ms);
|
|
return xSemaphoreTakeRecursive(s_lock, wait) == pdTRUE;
|
|
}
|
|
|
|
void lvgl_port_unlock(void)
|
|
{
|
|
if (s_lock) {
|
|
xSemaphoreGiveRecursive(s_lock);
|
|
}
|
|
}
|
|
|
|
lv_display_t *lvgl_port_display(void)
|
|
{
|
|
return s_disp;
|
|
}
|