#ifdef ESP_PLATFORM #include #include #include #include #include #include #include "BouncingBallsScreensaver.h" #include "MatrixRainScreensaver.h" #include "MystifyScreensaver.h" #include "McpScreensaver.h" #include "Screensaver.h" #include "StackChanScreensaver.h" #include #include #include #include #include #include #include namespace tt::service::displayidle { constexpr auto* TAG = "DisplayIdle"; constexpr uint32_t kWakeActivityThresholdMs = 100; void DisplayIdleService::stopScreensaverCb(lv_event_t* e) { auto* self = static_cast(lv_event_get_user_data(e)); lv_event_stop_bubbling(e); self->stopScreensaverRequested.store(true, std::memory_order_release); lv_display_trigger_activity(nullptr); } static void setBacklightBrightness(uint8_t brightness) { ::Device* display; if (device_get_first_active_by_type(&DISPLAY_TYPE, &display) == ERROR_NONE) { ::Device* backlight; if (display_get_backlight(display, &backlight) == ERROR_NONE) { backlight_set_brightness(backlight, brightness); device_put(backlight); } device_put(display); } } static bool hasDisplayWithBacklight() { ::Device* display; bool result = false; if (device_get_first_active_by_type(&DISPLAY_TYPE, &display) == ERROR_NONE) { ::Device* backlight; if (display_get_backlight(display, &backlight) == ERROR_NONE) { result = true; device_put(backlight); } device_put(display); } return result; } /** * Effective backlight duty for the screensaver: the dimmed night level when * night dim is enabled and the local hour is inside the configured window, * otherwise the regular duty. A start == end window covers the whole day. */ static uint8_t screensaverBacklightDuty(const settings::display::DisplaySettings& displaySettings) { if (!displaySettings.nightDimEnabled) { return displaySettings.backlightDuty; } const std::time_t now = std::time(nullptr); std::tm local_time {}; if (localtime_r(&now, &local_time) == nullptr || local_time.tm_year < 125) { return displaySettings.backlightDuty; } const uint8_t hour = static_cast(local_time.tm_hour); const uint8_t start = displaySettings.nightDimStartHour; const uint8_t end = displaySettings.nightDimEndHour; bool in_window; if (start == end) { in_window = true; } else if (start < end) { in_window = hour >= start && hour < end; } else { in_window = hour >= start || hour < end; } return in_window ? displaySettings.nightDimDuty : displaySettings.backlightDuty; } void DisplayIdleService::stopScreensaver() { if (!lvgl_try_lock(100)) { // Lock failed - keep flag set to retry on next tick return; } const auto restoreDuty = cachedDisplaySettings.backlightDuty; const bool wasDimmed = displayDimmed; if (screensaverOverlay) { if (screensaver) { screensaver->stop(); screensaver.reset(); } lv_obj_delete(screensaverOverlay); screensaverOverlay = nullptr; } lvgl_unlock(); // Stop an app launched as screensaver (e.g. Bible Verse): on wake the // device returns to the launcher. Stopping is safe when the user already // closed it (unknown ids report STOPPED / stop is a no-op for them). if (screensaverAppActive) { screensaverAppActive = false; if (screensaverAppInstanceId != 0) { app_manager_stop(screensaverAppInstanceId); screensaverAppInstanceId = 0; } } stopScreensaverRequested.store(false, std::memory_order_relaxed); // Reset auto-off state screensaverActiveCounter = 0; backlightOff = false; // Restore backlight if display was dimmed if (wasDimmed) { setBacklightBrightness(restoreDuty); } displayDimmed = wasDimmed ? false : displayDimmed; } void DisplayIdleService::activateScreensaver() { // Reset auto-off counter when starting screensaver screensaverActiveCounter = 0; backlightOff = false; // Bible Verse screensaver: launch the app instead of drawing an overlay. // Falls back to a plain black overlay when the app is not installed. if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::BibleVerse) { constexpr auto* BIBLE_VERSE_APP_ID = "one.tactility.bibleverse"; AppManifest manifest {}; AppInstanceId instance_id = 0; if (app_manager_find_manifest(BIBLE_VERSE_APP_ID, &manifest) == ERROR_NONE && app_start(BIBLE_VERSE_APP_ID, 0, nullptr, &instance_id) == ERROR_NONE) { LOG_I(TAG, "Bible Verse screensaver: launched instance %u", (unsigned)instance_id); screensaverAppInstanceId = instance_id; screensaverAppActive = true; setBacklightBrightness(screensaverBacklightDuty(cachedDisplaySettings)); return; } LOG_W(TAG, "Bible Verse app not installed, falling back to black screen"); } lv_obj_t* top = lv_layer_top(); if (screensaverOverlay != nullptr) return; 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); } setBacklightBrightness(screensaverBacklightDuty(cachedDisplaySettings)); } 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() { if (!lvgl_try_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) if (displayDimmed && (screensaverOverlay || screensaverAppActive) && !stopScreensaverRequested.load(std::memory_order_acquire)) { // Check if screensaver should auto-off after 30 minutes if (!backlightOff) { screensaverActiveCounter++; if (screensaverActiveCounter >= SCREENSAVER_AUTO_OFF_TICKS) { // Stop screensaver animation and turn off backlight if (screensaver) { screensaver->stop(); screensaver.reset(); } if (screensaverAppActive) { screensaverAppActive = false; if (screensaverAppInstanceId != 0) { app_manager_stop(screensaverAppInstanceId); screensaverAppInstanceId = 0; } } setBacklightBrightness(0); backlightOff = true; } else { updateScreensaver(); } } } lvgl_unlock(); // Check stop request early for faster response if (stopScreensaverRequested.load(std::memory_order_acquire)) { stopScreensaver(); return; } if (hasDisplayWithBacklight()) { if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) { if (displayDimmed) { setBacklightBrightness(cachedDisplaySettings.backlightDuty); displayDimmed = false; } } else { if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) { if (!lvgl_try_lock(100)) { return; // Retry on next tick } activateScreensaver(); lvgl_unlock(); // Turn off backlight for "None" screensaver (just black screen) if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) { setBacklightBrightness(0); } displayDimmed = true; } else if (displayDimmed && (inactive_ms < kWakeActivityThresholdMs)) { // The Bible Verse app keeps LVGL timers running (minute-advance), // so plain inactivity never drops while it is up. Taps still // register as activity: stop the verse app to return to launcher. // If the user already closed it, stopScreensaver() is a safe no-op. stopScreensaver(); } } } } bool DisplayIdleService::onStart(ServiceContext& service) { // Seed random number generator for varied screensaver patterns srand(static_cast(time(nullptr))); cachedDisplaySettings = settings::display::loadOrGetDefault(); timer = std::make_unique(Timer::Type::Periodic, millis_to_ticks(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) { delay_millis(50); // Brief delay before retry } } if (screensaverOverlay) { LOG_W(TAG, "Failed to stop screensaver during shutdown - potential resource leak"); } } if (screensaverAppActive) { screensaverAppActive = false; if (screensaverAppInstanceId != 0) { app_manager_stop(screensaverAppInstanceId); screensaverAppInstanceId = 0; } } screensaver.reset(); } void DisplayIdleService::startScreensaver() { if (!lvgl_try_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(); // The Bible Verse path launches an app task (not an overlay), which must // not run under the LVGL lock: drop it before activating. const bool launches_app = cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::BibleVerse; if (launches_app) { lvgl_unlock(); } activateScreensaver(); if (!launches_app) { lvgl_unlock(); } // Turn off backlight for "None" screensaver if (hasDisplayWithBacklight() && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) { setBacklightBrightness(0); } displayDimmed = true; } bool DisplayIdleService::isScreensaverActive() const { return screensaverOverlay != nullptr || screensaverAppActive; } void DisplayIdleService::startMcpScreensaver() { if (!lvgl_try_lock(200)) { LOG_W(TAG, "startMcpScreensaver: failed to acquire LVGL lock"); return; } if (screensaverOverlay != nullptr) { const auto& mcpState = mcp::getState(); if (mcpState.drawArea != nullptr) { lvgl_unlock(); return; } if (screensaver) { screensaver->stop(); screensaver.reset(); } lv_obj_delete(screensaverOverlay); screensaverOverlay = nullptr; } screensaverActiveCounter = 0; backlightOff = false; setBacklightBrightness(cachedDisplaySettings.backlightDuty == 0 ? 255 : screensaverBacklightDuty(cachedDisplaySettings)); lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr); lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr); screensaverOverlay = lv_obj_create(lv_layer_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; LOG_I(TAG, "MCP screensaver activated"); } void DisplayIdleService::reloadSettings() { // Set flag for thread-safe reload - actual reload happens in tick() settingsReloadRequested.store(true, std::memory_order_release); } std::shared_ptr findService() { return std::static_pointer_cast( findServiceById("tactility.displayidle") ); } extern const ServiceManifest manifest = { .id = "tactility.displayidle", .createService = create }; } #endif // ESP_PLATFORM