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:
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user