Files
tactility/Tactility/Source/service/displayidle/DisplayIdle.cpp
T
Adolfo Reyna 3629ffef2c fix(displayidle): avoid LVGL->MCP lock inversion that could freeze RLCD
Read MCP override state outside LVGL lock in tick().
Previously: LVGL lock -> MCP mutex (tick) vs MCP mutex -> LVGL lock (video task startMcpScreensaver) = deadlock risk observed on RLCD when text tool called.
Now: MCP state read first, then LVGL lock. Preserves <1s flash guard (isMcpActive).

Board: waveshare-esp32-s3-rlcd 0x2b78b0 32%% free, flashed to /dev/cu.usbmodem101

Co-Authored-By: internal-model
2026-07-11 21:32:11 -04:00

380 lines
13 KiB
C++

#ifdef ESP_PLATFORM
#include <Tactility/service/displayidle/DisplayIdleService.h>
#include "Screensaver.h"
#include "BouncingBallsScreensaver.h"
#include "MatrixRainScreensaver.h"
#include "MystifyScreensaver.h"
#include "StackChanScreensaver.h"
#include "McpScreensaver.h"
#include <Tactility/Logger.h>
#include <Tactility/CoreDefines.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/mcp/McpSystem.h>
#include <cstdlib>
#include <ctime>
namespace tt::service::displayidle {
static const auto LOGGER = Logger("DisplayIdle");
constexpr uint32_t kWakeActivityThresholdMs = 100;
static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
}
static bool isDeviceCharging() {
bool charging = false;
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&charging](const auto& power) {
if (!power->supportsMetric(hal::power::PowerDevice::MetricType::IsCharging)) {
return true; // continue
}
hal::power::PowerDevice::MetricData data;
if (power->getMetric(hal::power::PowerDevice::MetricType::IsCharging, data) && data.valueAsBool) {
charging = true;
return false; // stop iter
}
return true;
});
return charging;
}
void DisplayIdleService::stopScreensaverCb(lv_event_t* e) {
auto* self = static_cast<DisplayIdleService*>(lv_event_get_user_data(e));
self->stopScreensaverLocked();
}
void DisplayIdleService::stopScreensaverLocked() {
const auto restoreDuty = cachedDisplaySettings.backlightDuty;
const bool wasDimmed = displayDimmed;
if (screensaverOverlay) {
if (screensaver) {
screensaver->stop();
screensaver.reset();
}
lv_obj_delete(screensaverOverlay);
screensaverOverlay = nullptr;
}
stopScreensaverRequested.store(false, std::memory_order_relaxed);
// Reset auto-off state
screensaverActiveCounter = 0;
backlightOff = false;
// Restore backlight if display was dimmed
auto display = getDisplay();
if (display && wasDimmed) {
display->setBacklightDuty(restoreDuty);
}
displayDimmed = wasDimmed ? false : displayDimmed;
}
void DisplayIdleService::stopScreensaver() {
if (!lvgl::lock(100)) {
// Lock failed - keep flag set to retry on next tick
return;
}
stopScreensaverLocked();
lvgl::unlock();
}
void DisplayIdleService::activateScreensaver() {
lv_obj_t* top = lv_layer_top();
if (screensaverOverlay != nullptr) return;
// Reset auto-off counter when starting screensaver
screensaverActiveCounter = 0;
backlightOff = false;
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
// Black background overlay
screensaverOverlay = lv_obj_create(top);
lv_obj_remove_style_all(screensaverOverlay);
lv_obj_set_size(screensaverOverlay, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(screensaverOverlay, 0, 0);
lv_obj_set_style_bg_color(screensaverOverlay, lv_color_black(), 0);
lv_obj_set_style_bg_opa(screensaverOverlay, LV_OPA_COVER, 0);
lv_obj_add_flag(screensaverOverlay, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(screensaverOverlay, stopScreensaverCb, LV_EVENT_CLICKED, this);
// Create and start the screensaver based on settings
switch (cachedDisplaySettings.screensaverType) {
case settings::display::ScreensaverType::Mystify:
screensaver = std::make_unique<MystifyScreensaver>();
break;
case settings::display::ScreensaverType::BouncingBalls:
screensaver = std::make_unique<BouncingBallsScreensaver>();
break;
case settings::display::ScreensaverType::MatrixRain:
screensaver = std::make_unique<MatrixRainScreensaver>();
break;
case settings::display::ScreensaverType::StackChan:
screensaver = std::make_unique<StackChanScreensaver>();
break;
case settings::display::ScreensaverType::McpScreen:
screensaver = std::make_unique<McpScreensaver>();
break;
case settings::display::ScreensaverType::None:
default:
// Just black screen, no animated screensaver
screensaver = nullptr;
break;
}
if (screensaver) {
screensaver->start(screensaverOverlay, screenW, screenH);
}
}
void DisplayIdleService::updateScreensaver() {
if (screensaver) {
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
screensaver->update(screenW, screenH);
}
}
void DisplayIdleService::tick() {
// Check if MCP override is active — must not be auto-stopped by idle logic
// READ OUTSIDE LVGL lock to avoid lock inversion:
// MCP video task locks mutex -> LVGL, we must NOT do LVGL -> mutex
bool isMcpActive = false;
{
auto& st = mcp::getState();
std::lock_guard<std::mutex> lk(st.mutex);
isMcpActive = (st.drawArea != nullptr) || st.overrideActive;
}
if (!lvgl::lock(100)) {
return;
}
if (lv_display_get_default() == nullptr) {
lvgl::unlock();
return;
}
// Check for settings reload request (thread-safe)
if (settingsReloadRequested.exchange(false, std::memory_order_acquire)) {
cachedDisplaySettings = settings::display::loadOrGetDefault();
}
uint32_t inactive_ms = 0;
inactive_ms = lv_display_get_inactive_time(nullptr);
// Only update if not stopping (prevents lag on touch) — skip for MCP (no animation)
if (displayDimmed && screensaverOverlay && !stopScreensaverRequested.load(std::memory_order_acquire) && !isMcpActive) {
// Check if screensaver should auto-off after 5 minutes
if (!backlightOff) {
screensaverActiveCounter++;
if (screensaverActiveCounter >= SCREENSAVER_AUTO_OFF_TICKS) {
// Stop screensaver animation and turn off backlight
if (screensaver) {
screensaver->stop();
screensaver.reset();
}
auto display = getDisplay();
if (display) {
display->setBacklightDuty(0);
}
backlightOff = true;
} else {
updateScreensaver();
}
}
}
lvgl::unlock();
// Check stop request early for faster response
if (stopScreensaverRequested.load(std::memory_order_acquire)) {
stopScreensaver();
return;
}
auto display = getDisplay();
bool supportsBacklight = display != nullptr && display->supportsBacklightDuty();
if (supportsBacklight) {
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
if (displayDimmed && !isMcpActive) {
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
displayDimmed = false;
}
} else {
bool charging_blocks = cachedDisplaySettings.disableScreensaverWhenCharging && isDeviceCharging();
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
if (charging_blocks) {
// Skip screensaver while charging
} else {
if (!lvgl::lock(100)) {
return; // Retry on next tick
}
activateScreensaver();
lvgl::unlock();
if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
display->setBacklightDuty(0);
}
displayDimmed = true;
}
} else if (displayDimmed && !isMcpActive) {
if (inactive_ms < kWakeActivityThresholdMs) {
stopScreensaver();
} else if (charging_blocks) {
stopScreensaver();
}
}
}
}
}
bool DisplayIdleService::onStart(ServiceContext& service) {
// Seed random number generator for varied screensaver patterns
srand(static_cast<unsigned int>(time(nullptr)));
cachedDisplaySettings = settings::display::loadOrGetDefault();
auto display = getDisplay();
if (display != nullptr && !display->supportsBacklightDuty()) {
LOGGER.info("Monochrome/RLCD display detected (no backlight control): idle timer will run but auto-backlight off is disabled");
}
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(TICK_INTERVAL_MS), [this]{ this->tick(); });
timer->setCallbackPriority(Thread::Priority::Lower);
timer->start();
return true;
}
void DisplayIdleService::onStop(ServiceContext& service) {
if (timer) {
timer->stop();
timer = nullptr;
}
if (screensaverOverlay) {
// Retry screensaver cleanup during shutdown
constexpr int maxRetries = 5;
for (int i = 0; i < maxRetries && screensaverOverlay; ++i) {
stopScreensaver();
if (screensaverOverlay && i < maxRetries - 1) {
kernel::delayMillis(50); // Brief delay before retry
}
}
if (screensaverOverlay) {
LOGGER.warn("Failed to stop screensaver during shutdown - potential resource leak");
}
}
screensaver.reset();
}
void DisplayIdleService::startScreensaver() {
if (!lvgl::lock(100)) {
return;
}
// Reload settings to get current screensaver type
// Note: This is safe because we hold the LVGL lock which serializes with tick()
cachedDisplaySettings = settings::display::loadOrGetDefault();
activateScreensaver();
lvgl::unlock();
// Turn off backlight for "None" screensaver
auto display = getDisplay();
if (display && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
display->setBacklightDuty(0);
}
displayDimmed = true;
}
bool DisplayIdleService::isScreensaverActive() const {
return screensaverOverlay != nullptr;
}
void DisplayIdleService::reloadSettings() {
// Set flag for thread-safe reload - actual reload happens in tick()
settingsReloadRequested.store(true, std::memory_order_release);
}
void DisplayIdleService::startMcpScreensaver() {
if (!lvgl::lock(200)) {
LOGGER.warn("startMcpScreensaver: failed to acquire LVGL lock");
return;
}
if (screensaverOverlay != nullptr) {
// Screensaver already active — if drawArea is registered we're done,
// otherwise stop the current one so we can replace it with McpScreensaver.
const auto& mcpState = mcp::getState();
if (mcpState.drawArea != nullptr) {
lvgl::unlock();
return; // McpScreensaver already running
}
// Wrong screensaver type active — tear it down first
if (screensaver) {
screensaver->stop();
screensaver.reset();
}
lv_obj_delete(screensaverOverlay);
screensaverOverlay = nullptr;
}
screensaverActiveCounter = 0;
backlightOff = false;
// Ensure backlight is active if the display supports it
auto display = getDisplay();
if (display != nullptr && display->supportsBacklightDuty()) {
uint8_t duty = cachedDisplaySettings.backlightDuty;
if (duty == 0) duty = 255; // ensure visible if settings not loaded / default
display->setBacklightDuty(duty);
}
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
lv_obj_t* top = lv_layer_top();
screensaverOverlay = lv_obj_create(top);
lv_obj_remove_style_all(screensaverOverlay);
lv_obj_set_size(screensaverOverlay, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(screensaverOverlay, 0, 0);
lv_obj_set_style_bg_color(screensaverOverlay, lv_color_black(), 0);
lv_obj_set_style_bg_opa(screensaverOverlay, LV_OPA_COVER, 0);
lv_obj_add_flag(screensaverOverlay, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(screensaverOverlay, stopScreensaverCb, LV_EVENT_CLICKED, this);
screensaver = std::make_unique<McpScreensaver>();
screensaver->start(screensaverOverlay, screenW, screenH);
lvgl::unlock();
displayDimmed = true;
LOGGER.info("MCP screensaver activated");
}
std::shared_ptr<DisplayIdleService> findService() {
return std::static_pointer_cast<DisplayIdleService>(
findServiceById("DisplayIdle")
);
}
extern const ServiceManifest manifest = {
.id = "DisplayIdle",
.createService = create<DisplayIdleService>
};
}
#endif // ESP_PLATFORM