85da419a98
- DisplaySettings: nightDimEnabled/startHour/endHour/duty (default 22:00-07:00 at duty 20), persisted in display.properties - Display settings page: Night dim switch + Dim from/until hour dropdowns + Dim brightness slider - DisplayIdle: screensaver runs at the dimmed level when the local hour is inside the window (wrap-around aware); falls back to the regular duty when time is unknown (year < 2025)
373 lines
12 KiB
C++
373 lines
12 KiB
C++
#ifdef ESP_PLATFORM
|
|
|
|
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
|
#include <Tactility/service/ServiceManifest.h>
|
|
#include <Tactility/service/ServiceRegistration.h>
|
|
#include <Tactility/mcp/McpSystem.h>
|
|
|
|
#include "BouncingBallsScreensaver.h"
|
|
#include "MatrixRainScreensaver.h"
|
|
#include "MystifyScreensaver.h"
|
|
#include "McpScreensaver.h"
|
|
#include "Screensaver.h"
|
|
#include "StackChanScreensaver.h"
|
|
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
|
|
#include <tactility/delay.h>
|
|
#include <tactility/log.h>
|
|
#include <tactility/drivers/display.h>
|
|
#include <tactility/drivers/backlight.h>
|
|
|
|
#include <lvgl/lvgl.h>
|
|
|
|
namespace tt::service::displayidle {
|
|
|
|
constexpr auto* TAG = "DisplayIdle";
|
|
|
|
constexpr uint32_t kWakeActivityThresholdMs = 100;
|
|
|
|
void DisplayIdleService::stopScreensaverCb(lv_event_t* e) {
|
|
auto* self = static_cast<DisplayIdleService*>(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<uint8_t>(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();
|
|
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() {
|
|
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);
|
|
}
|
|
|
|
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 && !stopScreensaverRequested.load(std::memory_order_acquire)) {
|
|
// 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();
|
|
}
|
|
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)) {
|
|
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();
|
|
|
|
timer = std::make_unique<Timer>(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");
|
|
}
|
|
}
|
|
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();
|
|
|
|
activateScreensaver();
|
|
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;
|
|
}
|
|
|
|
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<McpScreensaver>();
|
|
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<DisplayIdleService> findService() {
|
|
return std::static_pointer_cast<DisplayIdleService>(
|
|
findServiceById("tactility.displayidle")
|
|
);
|
|
}
|
|
|
|
extern const ServiceManifest manifest = {
|
|
.id = "tactility.displayidle",
|
|
.createService = create<DisplayIdleService>
|
|
};
|
|
|
|
}
|
|
|
|
#endif // ESP_PLATFORM
|