feat(display): night-dim backlight window for screensaver

- 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)
This commit is contained in:
Adolfo Reyna
2026-09-12 08:17:45 -04:00
parent 87c82f5314
commit 85da419a98
4 changed files with 225 additions and 2 deletions
@@ -37,6 +37,14 @@ struct DisplaySettings {
bool backlightTimeoutEnabled;
uint32_t backlightTimeoutMs; // 0 = Never
ScreensaverType screensaverType = ScreensaverType::BouncingBalls;
// Night dim: when enabled and the local hour falls inside
// [nightDimStartHour, nightDimEndHour), the screensaver runs at
// nightDimDuty instead of backlightDuty. A start == end window
// means the whole day.
bool nightDimEnabled = false;
uint8_t nightDimStartHour = 22;
uint8_t nightDimEndHour = 7;
uint8_t nightDimDuty = 20;
};
/** Compares default settings with the function parameter to return the difference */
+145
View File
@@ -42,6 +42,10 @@ struct Context {
lv_obj_t* timeoutSwitch = nullptr;
lv_obj_t* timeoutDropdown = nullptr;
lv_obj_t* screensaverDropdown = nullptr;
lv_obj_t* nightDimSwitch = nullptr;
lv_obj_t* nightDimStartDropdown = nullptr;
lv_obj_t* nightDimEndDropdown = nullptr;
lv_obj_t* nightDimDutySlider = nullptr;
};
@@ -154,6 +158,82 @@ void onScreensaverChanged(lv_event_t* event) {
}
}
static void setNightDimControlsEnabled(Context* ctx, bool enabled) {
if (ctx->nightDimStartDropdown != nullptr) {
if (enabled) {
lv_obj_clear_state(ctx->nightDimStartDropdown, LV_STATE_DISABLED);
} else {
lv_obj_add_state(ctx->nightDimStartDropdown, LV_STATE_DISABLED);
}
}
if (ctx->nightDimEndDropdown != nullptr) {
if (enabled) {
lv_obj_clear_state(ctx->nightDimEndDropdown, LV_STATE_DISABLED);
} else {
lv_obj_add_state(ctx->nightDimEndDropdown, LV_STATE_DISABLED);
}
}
if (ctx->nightDimDutySlider != nullptr) {
if (enabled) {
lv_obj_clear_state(ctx->nightDimDutySlider, LV_STATE_DISABLED);
} else {
lv_obj_add_state(ctx->nightDimDutySlider, LV_STATE_DISABLED);
}
}
}
static const char* hourOptions() {
// 00..23, one per line
static const char* options =
"00\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n11\n"
"12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23";
return options;
}
void onNightDimSwitch(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
ctx->displaySettings.nightDimEnabled = enabled;
ctx->displaySettingsUpdated = true;
setNightDimControlsEnabled(ctx, enabled);
}
void onNightDimStartChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t idx = lv_dropdown_get_selected(dropdown);
if (idx > 23) {
return;
}
ctx->displaySettings.nightDimStartHour = static_cast<uint8_t>(idx);
ctx->displaySettingsUpdated = true;
}
void onNightDimEndChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t idx = lv_dropdown_get_selected(dropdown);
if (idx > 23) {
return;
}
ctx->displaySettings.nightDimEndHour = static_cast<uint8_t>(idx);
ctx->displaySettingsUpdated = true;
}
void onNightDimDutyChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
int32_t value = lv_slider_get_value(slider);
if (value < 0) {
value = 0;
} else if (value > 255) {
value = 255;
}
ctx->displaySettings.nightDimDuty = static_cast<uint8_t>(value);
ctx->displaySettingsUpdated = true;
}
void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
@@ -314,6 +394,71 @@ void createWidgets(lv_obj_t* parent, void* userData) {
if (!ctx->displaySettings.backlightTimeoutEnabled) {
lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED);
}
// Night dim: dim the backlight while the screensaver is up, during a time window
auto* night_dim_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(night_dim_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(night_dim_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(night_dim_wrapper, 0, LV_STATE_DEFAULT);
auto* night_dim_label = lv_label_create(night_dim_wrapper);
lv_label_set_text(night_dim_label, "Night dim");
lv_obj_align(night_dim_label, LV_ALIGN_LEFT_MID, 0, 0);
ctx->nightDimSwitch = lv_switch_create(night_dim_wrapper);
if (ctx->displaySettings.nightDimEnabled) {
lv_obj_add_state(ctx->nightDimSwitch, LV_STATE_CHECKED);
}
lv_obj_align(ctx->nightDimSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(ctx->nightDimSwitch, onNightDimSwitch, LV_EVENT_VALUE_CHANGED, ctx);
auto* night_dim_start_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(night_dim_start_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(night_dim_start_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(night_dim_start_wrapper, 0, LV_STATE_DEFAULT);
auto* night_dim_start_label = lv_label_create(night_dim_start_wrapper);
lv_label_set_text(night_dim_start_label, "Dim from");
lv_obj_align(night_dim_start_label, LV_ALIGN_LEFT_MID, 0, 0);
ctx->nightDimStartDropdown = lv_dropdown_create(night_dim_start_wrapper);
lv_dropdown_set_options(ctx->nightDimStartDropdown, hourOptions());
lv_obj_align(ctx->nightDimStartDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(ctx->nightDimStartDropdown, onNightDimStartChanged, LV_EVENT_VALUE_CHANGED, ctx);
lv_dropdown_set_selected(ctx->nightDimStartDropdown, ctx->displaySettings.nightDimStartHour > 23 ? 0 : ctx->displaySettings.nightDimStartHour);
auto* night_dim_end_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(night_dim_end_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(night_dim_end_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(night_dim_end_wrapper, 0, LV_STATE_DEFAULT);
auto* night_dim_end_label = lv_label_create(night_dim_end_wrapper);
lv_label_set_text(night_dim_end_label, "Dim until");
lv_obj_align(night_dim_end_label, LV_ALIGN_LEFT_MID, 0, 0);
ctx->nightDimEndDropdown = lv_dropdown_create(night_dim_end_wrapper);
lv_dropdown_set_options(ctx->nightDimEndDropdown, hourOptions());
lv_obj_align(ctx->nightDimEndDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(ctx->nightDimEndDropdown, onNightDimEndChanged, LV_EVENT_VALUE_CHANGED, ctx);
lv_dropdown_set_selected(ctx->nightDimEndDropdown, ctx->displaySettings.nightDimEndHour > 23 ? 0 : ctx->displaySettings.nightDimEndHour);
auto* night_dim_duty_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(night_dim_duty_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(night_dim_duty_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(night_dim_duty_wrapper, 0, LV_STATE_DEFAULT);
auto* night_dim_duty_label = lv_label_create(night_dim_duty_wrapper);
lv_label_set_text(night_dim_duty_label, "Dim brightness");
lv_obj_align(night_dim_duty_label, LV_ALIGN_LEFT_MID, 0, 0);
ctx->nightDimDutySlider = lv_slider_create(night_dim_duty_wrapper);
lv_obj_set_width(ctx->nightDimDutySlider, LV_PCT(50));
lv_obj_align(ctx->nightDimDutySlider, LV_ALIGN_RIGHT_MID, 0, 0);
lv_slider_set_range(ctx->nightDimDutySlider, 0, 255);
lv_slider_set_value(ctx->nightDimDutySlider, ctx->displaySettings.nightDimDuty, LV_ANIM_OFF);
lv_obj_add_event_cb(ctx->nightDimDutySlider, onNightDimDutyChanged, LV_EVENT_VALUE_CHANGED, ctx);
setNightDimControlsEnabled(ctx, ctx->displaySettings.nightDimEnabled);
}
}
@@ -61,6 +61,34 @@ static bool hasDisplayWithBacklight() {
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
@@ -142,6 +170,8 @@ void DisplayIdleService::activateScreensaver() {
if (screensaver) {
screensaver->start(screensaverOverlay, screenW, screenH);
}
setBacklightBrightness(screensaverBacklightDuty(cachedDisplaySettings));
}
void DisplayIdleService::updateScreensaver() {
@@ -301,7 +331,7 @@ void DisplayIdleService::startMcpScreensaver() {
screensaverActiveCounter = 0;
backlightOff = false;
setBacklightBrightness(cachedDisplaySettings.backlightDuty == 0
? 255 : cachedDisplaySettings.backlightDuty);
? 255 : screensaverBacklightDuty(cachedDisplaySettings));
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
+41 -1
View File
@@ -26,6 +26,10 @@ constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs";
constexpr auto* SETTINGS_KEY_SCREENSAVER_TYPE = "screensaverType";
constexpr auto* SETTINGS_KEY_NIGHT_DIM_ENABLED = "nightDimEnabled";
constexpr auto* SETTINGS_KEY_NIGHT_DIM_START_HOUR = "nightDimStartHour";
constexpr auto* SETTINGS_KEY_NIGHT_DIM_END_HOUR = "nightDimEndHour";
constexpr auto* SETTINGS_KEY_NIGHT_DIM_DUTY = "nightDimDuty";
static Orientation getDefaultOrientation() {
auto* display = lv_display_get_default();
@@ -202,6 +206,30 @@ bool load(DisplaySettings& settings) {
fromString(screensaver_entry->second, screensaver_type);
}
auto night_dim_enabled_entry = map.find(SETTINGS_KEY_NIGHT_DIM_ENABLED);
bool night_dim_enabled = false;
if (night_dim_enabled_entry != map.end()) {
night_dim_enabled = (night_dim_enabled_entry->second == "1" || night_dim_enabled_entry->second == "true" || night_dim_enabled_entry->second == "True");
}
auto night_dim_start_entry = map.find(SETTINGS_KEY_NIGHT_DIM_START_HOUR);
int night_dim_start_hour = 22;
if (night_dim_start_entry != map.end()) {
night_dim_start_hour = atoi(night_dim_start_entry->second.c_str());
}
auto night_dim_end_entry = map.find(SETTINGS_KEY_NIGHT_DIM_END_HOUR);
int night_dim_end_hour = 7;
if (night_dim_end_entry != map.end()) {
night_dim_end_hour = atoi(night_dim_end_entry->second.c_str());
}
auto night_dim_duty_entry = map.find(SETTINGS_KEY_NIGHT_DIM_DUTY);
int night_dim_duty = 20;
if (night_dim_duty_entry != map.end()) {
night_dim_duty = atoi(night_dim_duty_entry->second.c_str());
}
settings.orientation = orientation;
settings.fontSize = font_size;
settings.gammaCurve = gamma_curve;
@@ -209,6 +237,10 @@ bool load(DisplaySettings& settings) {
settings.backlightTimeoutEnabled = timeout_enabled;
settings.backlightTimeoutMs = timeout_ms;
settings.screensaverType = screensaver_type;
settings.nightDimEnabled = night_dim_enabled;
settings.nightDimStartHour = static_cast<uint8_t>(night_dim_start_hour < 0 ? 0 : (night_dim_start_hour > 23 ? 23 : night_dim_start_hour));
settings.nightDimEndHour = static_cast<uint8_t>(night_dim_end_hour < 0 ? 0 : (night_dim_end_hour > 23 ? 23 : night_dim_end_hour));
settings.nightDimDuty = static_cast<uint8_t>(night_dim_duty < 0 ? 0 : (night_dim_duty > 255 ? 255 : night_dim_duty));
return true;
}
@@ -221,7 +253,11 @@ DisplaySettings getDefault() {
.backlightDuty = 200,
.backlightTimeoutEnabled = false,
.backlightTimeoutMs = 60000,
.screensaverType = ScreensaverType::BouncingBalls
.screensaverType = ScreensaverType::BouncingBalls,
.nightDimEnabled = false,
.nightDimStartHour = 22,
.nightDimEndHour = 7,
.nightDimDuty = 20
};
}
@@ -242,6 +278,10 @@ bool save(const DisplaySettings& settings) {
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType);
map[SETTINGS_KEY_NIGHT_DIM_ENABLED] = settings.nightDimEnabled ? "1" : "0";
map[SETTINGS_KEY_NIGHT_DIM_START_HOUR] = std::to_string(settings.nightDimStartHour);
map[SETTINGS_KEY_NIGHT_DIM_END_HOUR] = std::to_string(settings.nightDimEndHour);
map[SETTINGS_KEY_NIGHT_DIM_DUTY] = std::to_string(settings.nightDimDuty);
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
return false;