first commit
This commit is contained in:
commit
0ef2017d18
28 files changed
+1882
No files matched your search
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Minimal LVGL 9 port layer: tick source, service task, and a recursive lock.
|
||||
*
|
||||
* LVGL needs three things the application must supply: a millisecond tick, a
|
||||
* periodic call to lv_timer_handler(), and mutual exclusion around its API.
|
||||
*/
|
||||
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#include "lvgl_port.h"
|
||||
|
||||
static const char *TAG = "lvgl_port";
|
||||
|
||||
#define LVGL_TICK_PERIOD_MS 2
|
||||
#define LVGL_TASK_STACK_SIZE 8192
|
||||
#define LVGL_TASK_PRIORITY 4
|
||||
#define LVGL_TASK_MAX_DELAY_MS 500
|
||||
#define LVGL_TASK_MIN_DELAY_MS 1
|
||||
|
||||
static SemaphoreHandle_t lvgl_mutex = NULL;
|
||||
static esp_timer_handle_t tick_timer = NULL;
|
||||
static volatile bool paused = false;
|
||||
|
||||
static void lvgl_tick_cb(void *arg)
|
||||
{
|
||||
lv_tick_inc(LVGL_TICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void lvgl_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "LVGL task started");
|
||||
|
||||
while (1) {
|
||||
uint32_t delay_ms = LVGL_TASK_MAX_DELAY_MS;
|
||||
|
||||
if (paused) {
|
||||
/* Leave the framebuffer untouched -- the panel keeps scanning it
|
||||
* out, so the last frame stays on screen. */
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lvgl_port_lock(-1)) {
|
||||
delay_ms = lv_timer_handler();
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
if (delay_ms > LVGL_TASK_MAX_DELAY_MS) {
|
||||
delay_ms = LVGL_TASK_MAX_DELAY_MS;
|
||||
} else if (delay_ms < LVGL_TASK_MIN_DELAY_MS) {
|
||||
delay_ms = LVGL_TASK_MIN_DELAY_MS;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t lvgl_port_init(void)
|
||||
{
|
||||
/* Recursive: lv_timer_handler() runs callbacks that may re-enter code
|
||||
* which also takes the lock. */
|
||||
lvgl_mutex = xSemaphoreCreateRecursiveMutex();
|
||||
ESP_RETURN_ON_FALSE(lvgl_mutex != NULL, ESP_ERR_NO_MEM, TAG, "mutex alloc failed");
|
||||
|
||||
lv_init();
|
||||
|
||||
ESP_LOGI(TAG, "LVGL %d.%d.%d initialized",
|
||||
lv_version_major(), lv_version_minor(), lv_version_patch());
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t lvgl_port_start(void)
|
||||
{
|
||||
const esp_timer_create_args_t timer_args = {
|
||||
.callback = lvgl_tick_cb,
|
||||
.name = "lvgl_tick",
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(esp_timer_create(&timer_args, &tick_timer), TAG,
|
||||
"tick timer create failed");
|
||||
ESP_RETURN_ON_ERROR(esp_timer_start_periodic(tick_timer, LVGL_TICK_PERIOD_MS * 1000),
|
||||
TAG, "tick timer start failed");
|
||||
|
||||
const BaseType_t ok = xTaskCreate(lvgl_task, "lvgl", LVGL_TASK_STACK_SIZE,
|
||||
NULL, LVGL_TASK_PRIORITY, NULL);
|
||||
ESP_RETURN_ON_FALSE(ok == pdPASS, ESP_ERR_NO_MEM, TAG, "LVGL task create failed");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void lvgl_port_pause(void)
|
||||
{
|
||||
if (paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Take the lock before setting the flag so we cannot stop the task
|
||||
* mid-render, then release it -- the flag alone keeps the task idle and
|
||||
* callers stay free to mutate widgets while paused.
|
||||
*/
|
||||
if (lvgl_port_lock(1000)) {
|
||||
paused = true;
|
||||
lvgl_port_unlock();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "pause: lock timed out, pausing anyway");
|
||||
paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
void lvgl_port_resume(void)
|
||||
{
|
||||
paused = false;
|
||||
}
|
||||
|
||||
bool lvgl_port_is_paused(void)
|
||||
{
|
||||
return paused;
|
||||
}
|
||||
|
||||
bool lvgl_port_lock(int timeout_ms)
|
||||
{
|
||||
const TickType_t ticks = (timeout_ms < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
||||
return xSemaphoreTakeRecursive(lvgl_mutex, ticks) == pdTRUE;
|
||||
}
|
||||
|
||||
void lvgl_port_unlock(void)
|
||||
{
|
||||
xSemaphoreGiveRecursive(lvgl_mutex);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef LVGL_PORT_H
|
||||
#define LVGL_PORT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
/* Calls lv_init() and creates the LVGL mutex. Must run before display_init()
|
||||
* and touch_init(), which register themselves with LVGL. */
|
||||
esp_err_t lvgl_port_init(void);
|
||||
|
||||
/* Starts the tick source and the task that services lv_timer_handler().
|
||||
* Call once the display and input devices are registered. */
|
||||
esp_err_t lvgl_port_start(void);
|
||||
|
||||
/*
|
||||
* LVGL is not thread safe. Any code touching an lv_* object from outside the
|
||||
* LVGL task must hold this lock. Returns false on timeout.
|
||||
*/
|
||||
bool lvgl_port_lock(int timeout_ms);
|
||||
void lvgl_port_unlock(void);
|
||||
|
||||
/*
|
||||
* Suspend/resume LVGL rendering.
|
||||
*
|
||||
* The panel keeps scanning out whatever frame is already in the framebuffer,
|
||||
* so the image stays on screen -- it simply stops being redrawn. Used to keep
|
||||
* LVGL off the PSRAM bus while a network fetch is in flight, since the redraw,
|
||||
* the flush and the TLS session otherwise contend with the LCD's own DMA.
|
||||
*
|
||||
* Calls do not nest.
|
||||
*/
|
||||
void lvgl_port_pause(void);
|
||||
void lvgl_port_resume(void);
|
||||
bool lvgl_port_is_paused(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,162 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#include "lvgl_port.h"
|
||||
#include "ui_manager.h"
|
||||
|
||||
static const char *TAG = "ui_manager";
|
||||
|
||||
#define UI_LOCK_TIMEOUT_MS 1000
|
||||
|
||||
static lv_obj_t *temp_label = NULL;
|
||||
static lv_obj_t *cond_label = NULL;
|
||||
static lv_obj_t *status_label = NULL;
|
||||
static lv_obj_t *touch_label = NULL;
|
||||
|
||||
/*
|
||||
* lv_label_set_text() invalidates the label unconditionally, even when the
|
||||
* new text is identical -- which would repaint the temperature every poll
|
||||
* whether or not it moved. Comparing first means an unchanged value costs
|
||||
* nothing: no invalidation, so no redraw and no flush.
|
||||
*/
|
||||
static void label_set_if_changed(lv_obj_t *label, const char *text)
|
||||
{
|
||||
if (label == NULL || text == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *current = lv_label_get_text(label);
|
||||
if (current != NULL && strcmp(current, text) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_label_set_text(label, text);
|
||||
}
|
||||
|
||||
/*
|
||||
* Live readout of the last contact.
|
||||
*
|
||||
* Polled from an LVGL timer rather than driven by a widget event. An event
|
||||
* handler on the screen object only fires for taps that actually reach the
|
||||
* screen: LVGL does not bubble events to parents by default, so anything
|
||||
* landing on the header or the temperature card is consumed by that child and
|
||||
* never seen here. That made whole regions of the panel look like dead touch
|
||||
* zones when the hardware was reporting them perfectly well.
|
||||
*/
|
||||
static void touch_readout_timer_cb(lv_timer_t *timer)
|
||||
{
|
||||
lv_indev_t *indev = lv_indev_get_next(NULL);
|
||||
if (indev == NULL || touch_label == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_point_t p;
|
||||
lv_indev_get_point(indev, &p);
|
||||
|
||||
char buf[48];
|
||||
if (lv_indev_get_state(indev) == LV_INDEV_STATE_PRESSED) {
|
||||
snprintf(buf, sizeof(buf), "touch %d, %d", (int)p.x, (int)p.y);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "touch %d, %d (released)", (int)p.x, (int)p.y);
|
||||
}
|
||||
label_set_if_changed(touch_label, buf);
|
||||
}
|
||||
|
||||
void ui_update_weather(int16_t temp_f, const char *condition)
|
||||
{
|
||||
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
|
||||
ESP_LOGW(TAG, "Dropped weather update: LVGL lock timed out");
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%d°F", temp_f);
|
||||
label_set_if_changed(temp_label, buf);
|
||||
label_set_if_changed(cond_label, condition);
|
||||
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
void ui_set_status(const char *status)
|
||||
{
|
||||
if (status == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
|
||||
ESP_LOGW(TAG, "Dropped status update: LVGL lock timed out");
|
||||
return;
|
||||
}
|
||||
|
||||
label_set_if_changed(status_label, status);
|
||||
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
void ui_init(void)
|
||||
{
|
||||
/* ui_init() runs from app_main, not the LVGL task, so it needs the lock
|
||||
* like any other outside caller. */
|
||||
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
|
||||
ESP_LOGE(TAG, "Could not acquire LVGL lock for UI init");
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_t *scr = lv_screen_active();
|
||||
lv_obj_set_style_bg_color(scr, lv_color_hex(0x1A2332), LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_all(scr, 0, LV_PART_MAIN);
|
||||
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(scr, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
lv_obj_t *header = lv_obj_create(scr);
|
||||
lv_obj_set_size(header, LV_PCT(100), 64);
|
||||
lv_obj_set_style_radius(header, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(header, lv_color_hex(0x0D1620), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(header, 0, LV_PART_MAIN);
|
||||
lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t *title = lv_label_create(header);
|
||||
lv_label_set_text(title, "Thermostat");
|
||||
lv_obj_set_style_text_color(title, lv_color_white(), LV_PART_MAIN);
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_28, LV_PART_MAIN);
|
||||
lv_obj_center(title);
|
||||
|
||||
lv_obj_t *card = lv_obj_create(scr);
|
||||
lv_obj_set_size(card, LV_PCT(80), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_color(card, lv_color_hex(0x2C3E50), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(card, 2, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_color(card, lv_color_hex(0x3498DB), LV_PART_MAIN);
|
||||
lv_obj_set_style_radius(card, 12, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_all(card, 24, LV_PART_MAIN);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
|
||||
LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
temp_label = lv_label_create(card);
|
||||
lv_label_set_text(temp_label, "--°F");
|
||||
lv_obj_set_style_text_color(temp_label, lv_color_white(), LV_PART_MAIN);
|
||||
lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_48, LV_PART_MAIN);
|
||||
|
||||
cond_label = lv_label_create(card);
|
||||
lv_label_set_text(cond_label, "Waiting for data...");
|
||||
lv_obj_set_style_text_color(cond_label, lv_color_hex(0xBDC3C7), LV_PART_MAIN);
|
||||
|
||||
status_label = lv_label_create(scr);
|
||||
lv_label_set_text(status_label, "Starting...");
|
||||
lv_obj_set_style_text_color(status_label, lv_color_hex(0x7F8C8D), LV_PART_MAIN);
|
||||
|
||||
touch_label = lv_label_create(scr);
|
||||
lv_label_set_text(touch_label, "touch --, --");
|
||||
lv_obj_set_style_text_color(touch_label, lv_color_hex(0x3498DB), LV_PART_MAIN);
|
||||
|
||||
lv_timer_create(touch_readout_timer_cb, 100, NULL);
|
||||
|
||||
lvgl_port_unlock();
|
||||
|
||||
ESP_LOGI(TAG, "UI initialized");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef UI_MANAGER_H
|
||||
#define UI_MANAGER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "lvgl.h"
|
||||
|
||||
void ui_init(void);
|
||||
|
||||
/* Safe to call from any task -- these take the LVGL lock internally. */
|
||||
void ui_update_weather(int16_t temp_f, const char *condition);
|
||||
void ui_set_status(const char *status);
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user