#ifdef ESP_PLATFORM #include #include "Screensaver.h" #include "BouncingBallsScreensaver.h" #include "MatrixRainScreensaver.h" #include "MystifyScreensaver.h" #include "StackChanScreensaver.h" #include "McpScreensaver.h" #include #include #include #include #include #include #include #include #include #include #include namespace tt::service::displayidle { static const auto LOGGER = Logger("DisplayIdle"); constexpr uint32_t kWakeActivityThresholdMs = 100; static std::shared_ptr getDisplay() { return hal::findFirstDevice(hal::Device::Type::Display); } static bool isDeviceCharging() { bool charging = false; hal::findDevices(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(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(); break; case settings::display::ScreensaverType::BouncingBalls: screensaver = std::make_unique(); break; case settings::display::ScreensaverType::MatrixRain: screensaver = std::make_unique(); break; case settings::display::ScreensaverType::StackChan: screensaver = std::make_unique(); break; case settings::display::ScreensaverType::McpScreen: screensaver = std::make_unique(); 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 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(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::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(); screensaver->start(screensaverOverlay, screenW, screenH); lvgl::unlock(); displayDimmed = true; LOGGER.info("MCP screensaver activated"); } std::shared_ptr findService() { return std::static_pointer_cast( findServiceById("DisplayIdle") ); } extern const ServiceManifest manifest = { .id = "DisplayIdle", .createService = create }; } #endif // ESP_PLATFORM