/* * Host implementation of the firmware's lvgl_port.h contract. * * The firmware version (main/ui/lvgl_port.c) is built on FreeRTOS tasks and * esp_timer. This file provides the same six entry points on pthreads so that * main/ui/ui_manager.c -- the file whose layout we actually iterate on -- * compiles and runs unchanged on the desktop. * * The one deliberate behavioural difference is lvgl_port_start(): on the * device it spawns the task that calls lv_timer_handler(), whereas here the * simulator's own main loop drives LVGL. See the comment on that function. */ #include #include #include #include "esp_log.h" #include "lvgl.h" #include "lvgl_port.h" static const char *TAG = "lvgl_port_sim"; static pthread_mutex_t lvgl_mutex; static bool mutex_ready = false; static volatile bool paused = false; /* LVGL's millisecond time base. The firmware drives this from a 2ms esp_timer * calling lv_tick_inc(); on the host a monotonic clock read is both simpler * and more accurate. */ static uint32_t tick_get_cb(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint32_t)(ts.tv_sec * 1000U + ts.tv_nsec / 1000000U); } esp_err_t lvgl_port_init(void) { /* * Recursive, matching the device: lv_timer_handler() runs callbacks that * may re-enter code which also takes the lock. Keeping the same semantics * here means a deadlock introduced in shared UI code reproduces in the * simulator instead of only showing up on hardware. */ pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); if (pthread_mutex_init(&lvgl_mutex, &attr) != 0) { pthread_mutexattr_destroy(&attr); return ESP_ERR_NO_MEM; } pthread_mutexattr_destroy(&attr); mutex_ready = true; lv_init(); lv_tick_set_cb(tick_get_cb); ESP_LOGI(TAG, "LVGL %d.%d.%d initialized (simulator)", lv_version_major(), lv_version_minor(), lv_version_patch()); return ESP_OK; } /* * No-op by design. * * On the device this starts the tick timer and the LVGL service task. Here the * tick is a clock callback installed in lvgl_port_init(), and lv_timer_handler() * is called from the simulator's main loop -- running it from a second thread * as well would mean two threads rendering into one framebuffer. * * It still exists so that shared start-up code can call it unconditionally. */ esp_err_t lvgl_port_start(void) { return ESP_OK; } void lvgl_port_pause(void) { paused = true; } void lvgl_port_resume(void) { paused = false; } bool lvgl_port_is_paused(void) { return paused; } bool lvgl_port_lock(int timeout_ms) { if (!mutex_ready) { return false; } if (timeout_ms < 0) { return pthread_mutex_lock(&lvgl_mutex) == 0; } struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += timeout_ms / 1000; deadline.tv_nsec += (long)(timeout_ms % 1000) * 1000000L; if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; } return pthread_mutex_timedlock(&lvgl_mutex, &deadline) == 0; } void lvgl_port_unlock(void) { if (mutex_ready) { pthread_mutex_unlock(&lvgl_mutex); } }