feat(display): Bible Verse screensaver + longer idle timeouts
- ScreensaverType::BibleVerse launches one.tactility.bibleverse on idle (falls back to black when not installed); tap stops the app and returns to launcher; 30-minute backlight auto-off stops it too - Timeout options extended to 15/30 minutes; auto-off 5m -> 30m - Screensaver dropdown lists Bible Verse
This commit is contained in:
@@ -19,6 +19,7 @@ enum class ScreensaverType {
|
|||||||
MatrixRain,
|
MatrixRain,
|
||||||
StackChan,
|
StackChan,
|
||||||
McpScreen,
|
McpScreen,
|
||||||
|
BibleVerse, // Launch the Bible Verse app (if installed)
|
||||||
Count // Sentinel for bounds checking - must be last
|
Count // Sentinel for bounds checking - must be last
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <Tactility/settings/DisplaySettings.h>
|
#include <Tactility/settings/DisplaySettings.h>
|
||||||
#include <Tactility/Timer.h>
|
#include <Tactility/Timer.h>
|
||||||
|
|
||||||
|
#include <app/instance.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -29,9 +31,15 @@ class DisplayIdleService final : public Service {
|
|||||||
// Active screensaver instance
|
// Active screensaver instance
|
||||||
std::unique_ptr<Screensaver> screensaver;
|
std::unique_ptr<Screensaver> screensaver;
|
||||||
|
|
||||||
// Screensaver auto-off: turn off backlight after 5 minutes of screensaver
|
// App launched as screensaver (Bible Verse): instance id, 0 when none.
|
||||||
|
// A separate flag (not just the id) tracks intent, so a user-closed app
|
||||||
|
// doesn't look like an active screensaver and vice versa.
|
||||||
|
AppInstanceId screensaverAppInstanceId = 0;
|
||||||
|
bool screensaverAppActive = false;
|
||||||
|
|
||||||
|
// Screensaver auto-off: turn off backlight after 30 minutes of screensaver
|
||||||
static constexpr uint32_t TICK_INTERVAL_MS = 50;
|
static constexpr uint32_t TICK_INTERVAL_MS = 50;
|
||||||
static constexpr uint32_t SCREENSAVER_AUTO_OFF_MS = 5 * 60 * 1000; // 5 minutes
|
static constexpr uint32_t SCREENSAVER_AUTO_OFF_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
static constexpr int SCREENSAVER_AUTO_OFF_TICKS = SCREENSAVER_AUTO_OFF_MS / TICK_INTERVAL_MS;
|
static constexpr int SCREENSAVER_AUTO_OFF_TICKS = SCREENSAVER_AUTO_OFF_MS / TICK_INTERVAL_MS;
|
||||||
int screensaverActiveCounter = 0;
|
int screensaverActiveCounter = 0;
|
||||||
bool backlightOff = false;
|
bool backlightOff = false;
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ void onTimeoutChanged(lv_event_t* event) {
|
|||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||||
uint32_t idx = lv_dropdown_get_selected(dropdown);
|
uint32_t idx = lv_dropdown_get_selected(dropdown);
|
||||||
// Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never
|
// Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=15m,6=30m,7=Never
|
||||||
static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0};
|
static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 900000, 1800000, 0};
|
||||||
if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) {
|
if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) {
|
||||||
ctx->displaySettings.backlightTimeoutMs = values_ms[idx];
|
ctx->displaySettings.backlightTimeoutMs = values_ms[idx];
|
||||||
ctx->displaySettingsUpdated = true;
|
ctx->displaySettingsUpdated = true;
|
||||||
@@ -353,7 +353,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0);
|
lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||||
|
|
||||||
ctx->timeoutDropdown = lv_dropdown_create(timeout_select_wrapper);
|
ctx->timeoutDropdown = lv_dropdown_create(timeout_select_wrapper);
|
||||||
lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever");
|
lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\n15 minutes\n30 minutes\nNever");
|
||||||
lv_obj_align(ctx->timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
lv_obj_align(ctx->timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
lv_obj_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
lv_obj_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||||
// Initialize dropdown selection from settings
|
// Initialize dropdown selection from settings
|
||||||
@@ -368,8 +368,12 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
idx = 3;
|
idx = 3;
|
||||||
else if (ms == 300000)
|
else if (ms == 300000)
|
||||||
idx = 4;
|
idx = 4;
|
||||||
else if (ms == 0)
|
else if (ms == 900000)
|
||||||
idx = 5;
|
idx = 5;
|
||||||
|
else if (ms == 1800000)
|
||||||
|
idx = 6;
|
||||||
|
else if (ms == 0)
|
||||||
|
idx = 7;
|
||||||
lv_dropdown_set_selected(ctx->timeoutDropdown, idx);
|
lv_dropdown_set_selected(ctx->timeoutDropdown, idx);
|
||||||
if (!ctx->displaySettings.backlightTimeoutEnabled) {
|
if (!ctx->displaySettings.backlightTimeoutEnabled) {
|
||||||
lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED);
|
lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED);
|
||||||
@@ -387,7 +391,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
|
|
||||||
ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
|
ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
|
||||||
// Note: order correlates with settings::display::ScreensaverType enum order
|
// Note: order correlates with settings::display::ScreensaverType enum order
|
||||||
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen");
|
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen\nBible Verse");
|
||||||
lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||||
lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
|
lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
#include <Tactility/service/ServiceRegistration.h>
|
#include <Tactility/service/ServiceRegistration.h>
|
||||||
#include <Tactility/mcp/McpSystem.h>
|
#include <Tactility/mcp/McpSystem.h>
|
||||||
|
|
||||||
|
#include <app/manager.h>
|
||||||
|
#include <app/start.h>
|
||||||
|
|
||||||
#include "BouncingBallsScreensaver.h"
|
#include "BouncingBallsScreensaver.h"
|
||||||
#include "MatrixRainScreensaver.h"
|
#include "MatrixRainScreensaver.h"
|
||||||
#include "MystifyScreensaver.h"
|
#include "MystifyScreensaver.h"
|
||||||
@@ -107,6 +110,17 @@ void DisplayIdleService::stopScreensaver() {
|
|||||||
screensaverOverlay = nullptr;
|
screensaverOverlay = nullptr;
|
||||||
}
|
}
|
||||||
lvgl_unlock();
|
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);
|
stopScreensaverRequested.store(false, std::memory_order_relaxed);
|
||||||
|
|
||||||
// Reset auto-off state
|
// Reset auto-off state
|
||||||
@@ -122,14 +136,31 @@ void DisplayIdleService::stopScreensaver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DisplayIdleService::activateScreensaver() {
|
void DisplayIdleService::activateScreensaver() {
|
||||||
lv_obj_t* top = lv_layer_top();
|
|
||||||
|
|
||||||
if (screensaverOverlay != nullptr) return;
|
|
||||||
|
|
||||||
// Reset auto-off counter when starting screensaver
|
// Reset auto-off counter when starting screensaver
|
||||||
screensaverActiveCounter = 0;
|
screensaverActiveCounter = 0;
|
||||||
backlightOff = false;
|
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 screenW = lv_display_get_horizontal_resolution(nullptr);
|
||||||
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
|
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
|
||||||
|
|
||||||
@@ -201,8 +232,8 @@ void DisplayIdleService::tick() {
|
|||||||
inactive_ms = lv_display_get_inactive_time(nullptr);
|
inactive_ms = lv_display_get_inactive_time(nullptr);
|
||||||
|
|
||||||
// Only update if not stopping (prevents lag on touch)
|
// Only update if not stopping (prevents lag on touch)
|
||||||
if (displayDimmed && screensaverOverlay && !stopScreensaverRequested.load(std::memory_order_acquire)) {
|
if (displayDimmed && (screensaverOverlay || screensaverAppActive) && !stopScreensaverRequested.load(std::memory_order_acquire)) {
|
||||||
// Check if screensaver should auto-off after 5 minutes
|
// Check if screensaver should auto-off after 30 minutes
|
||||||
if (!backlightOff) {
|
if (!backlightOff) {
|
||||||
screensaverActiveCounter++;
|
screensaverActiveCounter++;
|
||||||
if (screensaverActiveCounter >= SCREENSAVER_AUTO_OFF_TICKS) {
|
if (screensaverActiveCounter >= SCREENSAVER_AUTO_OFF_TICKS) {
|
||||||
@@ -211,6 +242,13 @@ void DisplayIdleService::tick() {
|
|||||||
screensaver->stop();
|
screensaver->stop();
|
||||||
screensaver.reset();
|
screensaver.reset();
|
||||||
}
|
}
|
||||||
|
if (screensaverAppActive) {
|
||||||
|
screensaverAppActive = false;
|
||||||
|
if (screensaverAppInstanceId != 0) {
|
||||||
|
app_manager_stop(screensaverAppInstanceId);
|
||||||
|
screensaverAppInstanceId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
setBacklightBrightness(0);
|
setBacklightBrightness(0);
|
||||||
backlightOff = true;
|
backlightOff = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -246,6 +284,10 @@ void DisplayIdleService::tick() {
|
|||||||
}
|
}
|
||||||
displayDimmed = true;
|
displayDimmed = true;
|
||||||
} else if (displayDimmed && (inactive_ms < kWakeActivityThresholdMs)) {
|
} 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();
|
stopScreensaver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,6 +324,13 @@ void DisplayIdleService::onStop(ServiceContext& service) {
|
|||||||
LOG_W(TAG, "Failed to stop screensaver during shutdown - potential resource leak");
|
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();
|
screensaver.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,8 +343,19 @@ void DisplayIdleService::startScreensaver() {
|
|||||||
// Note: This is safe because we hold the LVGL lock which serializes with tick()
|
// Note: This is safe because we hold the LVGL lock which serializes with tick()
|
||||||
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
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();
|
activateScreensaver();
|
||||||
lvgl_unlock();
|
|
||||||
|
if (!launches_app) {
|
||||||
|
lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
// Turn off backlight for "None" screensaver
|
// Turn off backlight for "None" screensaver
|
||||||
if (hasDisplayWithBacklight() && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
|
if (hasDisplayWithBacklight() && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
|
||||||
@@ -305,7 +365,7 @@ void DisplayIdleService::startScreensaver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool DisplayIdleService::isScreensaverActive() const {
|
bool DisplayIdleService::isScreensaverActive() const {
|
||||||
return screensaverOverlay != nullptr;
|
return screensaverOverlay != nullptr || screensaverAppActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayIdleService::startMcpScreensaver() {
|
void DisplayIdleService::startMcpScreensaver() {
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ static std::string toString(ScreensaverType type) {
|
|||||||
return "StackChan";
|
return "StackChan";
|
||||||
case McpScreen:
|
case McpScreen:
|
||||||
return "McpScreen";
|
return "McpScreen";
|
||||||
|
case BibleVerse:
|
||||||
|
return "BibleVerse";
|
||||||
default:
|
default:
|
||||||
std::unreachable();
|
std::unreachable();
|
||||||
}
|
}
|
||||||
@@ -145,6 +147,9 @@ static bool fromString(const std::string& str, ScreensaverType& type) {
|
|||||||
} else if (str == "McpScreen") {
|
} else if (str == "McpScreen") {
|
||||||
type = ScreensaverType::McpScreen;
|
type = ScreensaverType::McpScreen;
|
||||||
return true;
|
return true;
|
||||||
|
} else if (str == "BibleVerse") {
|
||||||
|
type = ScreensaverType::BibleVerse;
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user