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:
Adolfo Reyna
2026-09-12 09:03:37 -04:00
parent 85da419a98
commit 054d1286b2
5 changed files with 93 additions and 15 deletions
@@ -19,6 +19,7 @@ enum class ScreensaverType {
MatrixRain,
StackChan,
McpScreen,
BibleVerse, // Launch the Bible Verse app (if installed)
Count // Sentinel for bounds checking - must be last
};
@@ -5,6 +5,8 @@
#include <Tactility/settings/DisplaySettings.h>
#include <Tactility/Timer.h>
#include <app/instance.h>
#include <atomic>
#include <memory>
@@ -29,9 +31,15 @@ class DisplayIdleService final : public Service {
// Active screensaver instance
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 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;
int screensaverActiveCounter = 0;
bool backlightOff = false;
+9 -5
View File
@@ -135,8 +135,8 @@ void onTimeoutChanged(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);
// Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never
static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0};
// 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, 900000, 1800000, 0};
if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) {
ctx->displaySettings.backlightTimeoutMs = values_ms[idx];
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);
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_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx);
// Initialize dropdown selection from settings
@@ -368,8 +368,12 @@ void createWidgets(lv_obj_t* parent, void* userData) {
idx = 3;
else if (ms == 300000)
idx = 4;
else if (ms == 0)
else if (ms == 900000)
idx = 5;
else if (ms == 1800000)
idx = 6;
else if (ms == 0)
idx = 7;
lv_dropdown_set_selected(ctx->timeoutDropdown, idx);
if (!ctx->displaySettings.backlightTimeoutEnabled) {
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);
// 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_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx);
lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
@@ -5,6 +5,9 @@
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/mcp/McpSystem.h>
#include <app/manager.h>
#include <app/start.h>
#include "BouncingBallsScreensaver.h"
#include "MatrixRainScreensaver.h"
#include "MystifyScreensaver.h"
@@ -107,6 +110,17 @@ void DisplayIdleService::stopScreensaver() {
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
@@ -122,14 +136,31 @@ void DisplayIdleService::stopScreensaver() {
}
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;
// 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);
@@ -201,8 +232,8 @@ void DisplayIdleService::tick() {
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 (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) {
@@ -211,6 +242,13 @@ void DisplayIdleService::tick() {
screensaver->stop();
screensaver.reset();
}
if (screensaverAppActive) {
screensaverAppActive = false;
if (screensaverAppInstanceId != 0) {
app_manager_stop(screensaverAppInstanceId);
screensaverAppInstanceId = 0;
}
}
setBacklightBrightness(0);
backlightOff = true;
} else {
@@ -246,6 +284,10 @@ void DisplayIdleService::tick() {
}
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();
}
}
@@ -282,6 +324,13 @@ void DisplayIdleService::onStop(ServiceContext& service) {
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();
}
@@ -294,8 +343,19 @@ void DisplayIdleService::startScreensaver() {
// 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();
lvgl_unlock();
if (!launches_app) {
lvgl_unlock();
}
// Turn off backlight for "None" screensaver
if (hasDisplayWithBacklight() && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
@@ -305,7 +365,7 @@ void DisplayIdleService::startScreensaver() {
}
bool DisplayIdleService::isScreensaverActive() const {
return screensaverOverlay != nullptr;
return screensaverOverlay != nullptr || screensaverAppActive;
}
void DisplayIdleService::startMcpScreensaver() {
@@ -121,6 +121,8 @@ static std::string toString(ScreensaverType type) {
return "StackChan";
case McpScreen:
return "McpScreen";
case BibleVerse:
return "BibleVerse";
default:
std::unreachable();
}
@@ -145,6 +147,9 @@ static bool fromString(const std::string& str, ScreensaverType& type) {
} else if (str == "McpScreen") {
type = ScreensaverType::McpScreen;
return true;
} else if (str == "BibleVerse") {
type = ScreensaverType::BibleVerse;
return true;
} else {
return false;
}