mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-18 10:53:17 +00:00
Screensavers (#462)
Adds screensavers in addition to the backlight idle off.
More info in screensavers.md
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **New Features**
* Added a screensaver system with multiple styles (Bouncing Balls, Mystify, Matrix Rain) and a common screensaver interface; auto-starts after inactivity, dismisses on interaction, and supports auto-off/backlight behavior.
* New Display setting and UI dropdown to choose and persist the screensaver.
* **Documentation**
* Added comprehensive screensaver docs covering usage, extension, and configuration.
* **Chores**
* Registered the display-idle service.
* **Bug Fixes**
* Updated LVGL API calls to match renamed functions.
<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
e6abd496f9
commit
c05d46a28c
@ -107,7 +107,7 @@ static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
data->enc_diff = static_cast<int16_t>(clamp(diff, INT16_MIN, INT16_MAX));
|
||||
|
||||
if (diff != 0) {
|
||||
lv_disp_trig_activity(nullptr);
|
||||
lv_display_trigger_activity(nullptr);
|
||||
}
|
||||
} else {
|
||||
// Pointer mode: read and clamp cursor position
|
||||
@ -127,7 +127,7 @@ static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
|
||||
if (pressed) {
|
||||
lv_disp_trig_activity(nullptr);
|
||||
lv_display_trigger_activity(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
// TODO: Avoid performance hit by calling loadOrGetDefault() on each key press
|
||||
// Ensure LVGL activity is triggered so idle services can wake the display
|
||||
lv_disp_trig_activity(nullptr);
|
||||
lv_display_trigger_activity(nullptr);
|
||||
|
||||
// Actively wake display/backlights immediately on key press (independent of idle tick)
|
||||
// Restore display backlight if off (we assume duty 0 means dimmed)
|
||||
|
||||
110
Documentation/screensavers.md
Normal file
110
Documentation/screensavers.md
Normal file
@ -0,0 +1,110 @@
|
||||
# DisplayIdle Service
|
||||
|
||||
The DisplayIdle service manages screen timeout, screensavers, and backlight control for Tactility devices.
|
||||
|
||||
## Features
|
||||
|
||||
### Screen Timeout
|
||||
When enabled, the display will automatically dim after a configurable period of inactivity. Timeout options:
|
||||
- 15 seconds
|
||||
- 30 seconds
|
||||
- 1 minute
|
||||
- 2 minutes
|
||||
- 5 minutes
|
||||
- Never
|
||||
|
||||
### Screensavers
|
||||
Four screensaver options are available:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| **None** | Black screen only, backlight turns off immediately |
|
||||
| **Bouncing Balls** | Colored balls bouncing around the screen |
|
||||
| **Mystify** | Classic Windows-style polygon trails with color-changing effects |
|
||||
| **Matrix Rain** | Digital rain effect with terminal-style grid movement, 6-color gradient trails, glow effects, and random character flicker |
|
||||
|
||||
### Auto-Off Feature
|
||||
After 5 minutes of screensaver activity, the screensaver animation stops and the backlight turns off completely to save power. Touching the screen restores normal operation.
|
||||
|
||||
## Public API
|
||||
|
||||
The service exposes a public header for external control:
|
||||
|
||||
```cpp
|
||||
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
||||
|
||||
// Get service instance
|
||||
auto displayIdle = tt::service::displayidle::findService();
|
||||
|
||||
// Force start screensaver immediately
|
||||
displayIdle->startScreensaver();
|
||||
|
||||
// Force stop screensaver and restore backlight
|
||||
displayIdle->stopScreensaver();
|
||||
|
||||
// Check if screensaver is currently active
|
||||
bool active = displayIdle->isScreensaverActive();
|
||||
|
||||
// Reload settings (call after external settings changes)
|
||||
displayIdle->reloadSettings();
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `DisplayIdleService.h` | Public header with service interface |
|
||||
| `DisplayIdle.cpp` | Service implementation |
|
||||
| `Screensaver.h` | Base class for screensaver implementations |
|
||||
| `BouncingBallsScreensaver.h/cpp` | Bouncing balls screensaver |
|
||||
| `MystifyScreensaver.h/cpp` | Mystify polygon screensaver |
|
||||
| `MatrixRainScreensaver.h/cpp` | Matrix digital rain screensaver |
|
||||
|
||||
### Screensaver Base Class
|
||||
|
||||
All screensavers inherit from the `Screensaver` base class:
|
||||
|
||||
```cpp
|
||||
class Screensaver {
|
||||
public:
|
||||
virtual void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual void update(lv_coord_t screenW, lv_coord_t screenH) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### Adding a New Screensaver
|
||||
|
||||
1. Create header and implementation files inheriting from `Screensaver`
|
||||
2. Add enum value to `ScreensaverType` in `DisplaySettings.h` (before `Count` sentinel)
|
||||
3. Add string conversion in `DisplaySettings.cpp` (`toString` and `fromString`)
|
||||
4. Add dropdown option in `Display.cpp` (order must match enum order)
|
||||
5. Add case in `DisplayIdle.cpp` `activateScreensaver()` switch
|
||||
6. Include the new header in `DisplayIdle.cpp`
|
||||
|
||||
**Note:** The `ScreensaverType::Count` sentinel must always be the last enum value - it's used for bounds checking in the UI.
|
||||
|
||||
## Settings Integration
|
||||
|
||||
Settings are stored in `/data/settings/display.properties` and managed through `DisplaySettings.h`:
|
||||
|
||||
```cpp
|
||||
struct DisplaySettings {
|
||||
Orientation orientation;
|
||||
uint8_t gammaCurve;
|
||||
uint8_t backlightDuty;
|
||||
bool backlightTimeoutEnabled;
|
||||
uint32_t backlightTimeoutMs;
|
||||
ScreensaverType screensaverType;
|
||||
};
|
||||
```
|
||||
|
||||
The Display app (`Display.cpp`) provides the UI for configuring these settings and notifies the DisplayIdle service when settings change via `reloadSettings()`.
|
||||
|
||||
## Timing
|
||||
|
||||
- Service tick interval: 50ms
|
||||
- Wake activity threshold: 100ms
|
||||
- Screensaver auto-off: 5 minutes (6000 ticks)
|
||||
@ -12,12 +12,21 @@ enum class Orientation {
|
||||
PortraitFlipped,
|
||||
};
|
||||
|
||||
enum class ScreensaverType {
|
||||
None, // Just black screen
|
||||
BouncingBalls,
|
||||
Mystify,
|
||||
MatrixRain,
|
||||
Count // Sentinel for bounds checking - must be last
|
||||
};
|
||||
|
||||
struct DisplaySettings {
|
||||
Orientation orientation;
|
||||
uint8_t gammaCurve;
|
||||
uint8_t backlightDuty;
|
||||
bool backlightTimeoutEnabled;
|
||||
uint32_t backlightTimeoutMs; // 0 = Never
|
||||
ScreensaverType screensaverType = ScreensaverType::BouncingBalls;
|
||||
};
|
||||
|
||||
/** Compares default settings with the function parameter to return the difference */
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/service/Service.h>
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
#include <Tactility/Timer.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
// Forward declarations
|
||||
typedef struct _lv_obj_t lv_obj_t;
|
||||
typedef struct _lv_event_t lv_event_t;
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
class Screensaver;
|
||||
|
||||
class DisplayIdleService final : public Service {
|
||||
|
||||
std::unique_ptr<Timer> timer;
|
||||
bool displayDimmed = false;
|
||||
settings::display::DisplaySettings cachedDisplaySettings;
|
||||
|
||||
lv_obj_t* screensaverOverlay = nullptr;
|
||||
std::atomic<bool> stopScreensaverRequested{false};
|
||||
std::atomic<bool> settingsReloadRequested{false};
|
||||
|
||||
// Active screensaver instance
|
||||
std::unique_ptr<Screensaver> screensaver;
|
||||
|
||||
// Screensaver auto-off: turn off backlight after 5 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 int SCREENSAVER_AUTO_OFF_TICKS = SCREENSAVER_AUTO_OFF_MS / TICK_INTERVAL_MS;
|
||||
int screensaverActiveCounter = 0;
|
||||
bool backlightOff = false;
|
||||
|
||||
static void stopScreensaverCb(lv_event_t* e);
|
||||
|
||||
/** @pre Caller must hold LVGL lock */
|
||||
void activateScreensaver();
|
||||
|
||||
/** @pre Caller must hold LVGL lock */
|
||||
void updateScreensaver();
|
||||
|
||||
void tick();
|
||||
|
||||
public:
|
||||
bool onStart(ServiceContext& service) override;
|
||||
void onStop(ServiceContext& service) override;
|
||||
|
||||
/**
|
||||
* Force the screensaver to start immediately, regardless of idle timeout.
|
||||
* @note Not thread-safe. Call from LVGL/main context only, not from
|
||||
* arbitrary threads while the timer is running.
|
||||
*/
|
||||
void startScreensaver();
|
||||
|
||||
/**
|
||||
* Force the screensaver to stop immediately and restore backlight.
|
||||
* @note Not thread-safe. Call from LVGL/main context only, not from
|
||||
* arbitrary threads while the timer is running.
|
||||
*/
|
||||
void stopScreensaver();
|
||||
|
||||
/**
|
||||
* Check if the screensaver is currently active.
|
||||
* @return true if the screensaver overlay is visible
|
||||
* @note Not thread-safe. Call from timer thread or LVGL context only.
|
||||
*/
|
||||
bool isScreensaverActive() const;
|
||||
|
||||
/**
|
||||
* Request reload of display settings from storage.
|
||||
* Thread-safe: can be called from any thread. Actual reload
|
||||
* happens on the next timer tick.
|
||||
*/
|
||||
void reloadSettings();
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the DisplayIdle service instance.
|
||||
* @return shared pointer to the service, or nullptr if not found
|
||||
*/
|
||||
std::shared_ptr<DisplayIdleService> findService();
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -53,7 +53,9 @@ namespace service {
|
||||
namespace loader { extern const ServiceManifest manifest; }
|
||||
namespace memorychecker { extern const ServiceManifest manifest; }
|
||||
namespace statusbar { extern const ServiceManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace displayidle { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
|
||||
namespace keyboardidle { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
@ -251,7 +253,9 @@ static void registerAndStartSecondaryServices() {
|
||||
addService(service::loader::manifest);
|
||||
addService(service::gui::manifest);
|
||||
addService(service::statusbar::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::displayidle::manifest);
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
|
||||
addService(service::keyboardidle::manifest);
|
||||
#endif
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
||||
#endif
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
#include <Tactility/Assets.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
@ -22,6 +25,7 @@ class DisplayApp final : public App {
|
||||
bool displaySettingsUpdated = false;
|
||||
lv_obj_t* timeoutSwitch = nullptr;
|
||||
lv_obj_t* timeoutDropdown = nullptr;
|
||||
lv_obj_t* screensaverDropdown = nullptr;
|
||||
|
||||
static void onBacklightSliderEvent(lv_event_t* event) {
|
||||
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
@ -73,8 +77,14 @@ class DisplayApp final : public App {
|
||||
if (app->timeoutDropdown) {
|
||||
if (enabled) {
|
||||
lv_obj_clear_state(app->timeoutDropdown, LV_STATE_DISABLED);
|
||||
if (app->screensaverDropdown) {
|
||||
lv_obj_clear_state(app->screensaverDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
} else {
|
||||
lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED);
|
||||
if (app->screensaverDropdown) {
|
||||
lv_obj_add_state(app->screensaverDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,6 +101,21 @@ class DisplayApp final : public App {
|
||||
}
|
||||
}
|
||||
|
||||
static void onScreensaverChanged(lv_event_t* event) {
|
||||
auto* app = static_cast<DisplayApp*>(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);
|
||||
// Validate index bounds before casting to enum
|
||||
if (idx >= static_cast<uint32_t>(settings::display::ScreensaverType::Count)) {
|
||||
return;
|
||||
}
|
||||
auto selected_type = static_cast<settings::display::ScreensaverType>(idx);
|
||||
if (selected_type != app->displaySettings.screensaverType) {
|
||||
app->displaySettings.screensaverType = selected_type;
|
||||
app->displaySettingsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
@ -233,6 +258,28 @@ public:
|
||||
if (!displaySettings.backlightTimeoutEnabled) {
|
||||
lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
// Screensaver type
|
||||
auto* screensaver_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(screensaver_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(screensaver_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(screensaver_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* screensaver_label = lv_label_create(screensaver_wrapper);
|
||||
lv_label_set_text(screensaver_label, "Screensaver");
|
||||
lv_obj_align(screensaver_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
|
||||
// Note: order correlates with settings::display::ScreensaverType enum order
|
||||
lv_dropdown_set_options(screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain");
|
||||
lv_obj_align(screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_style_border_color(screensaverDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(screensaverDropdown, 1, LV_PART_MAIN);
|
||||
lv_obj_add_event_cb(screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_dropdown_set_selected(screensaverDropdown, static_cast<uint16_t>(displaySettings.screensaverType));
|
||||
if (!displaySettings.backlightTimeoutEnabled) {
|
||||
lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,6 +289,13 @@ public:
|
||||
const settings::display::DisplaySettings settings_to_save = displaySettings;
|
||||
getMainDispatcher().dispatch([settings_to_save] {
|
||||
settings::display::save(settings_to_save);
|
||||
#ifdef ESP_PLATFORM
|
||||
// Notify DisplayIdle service to reload settings
|
||||
auto displayIdle = service::displayidle::findService();
|
||||
if (displayIdle) {
|
||||
displayIdle->reloadSettings();
|
||||
}
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,125 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "BouncingBallsScreensaver.h"
|
||||
#include <cstdlib>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
void BouncingBallsScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) {
|
||||
for (int i = 0; i < NUM_BALLS; i++) {
|
||||
initBall(balls_[i], overlay, screenW, screenH, i);
|
||||
}
|
||||
}
|
||||
|
||||
void BouncingBallsScreensaver::stop() {
|
||||
for (auto& ball : balls_) {
|
||||
ball.obj = nullptr; // Objects deleted by parent overlay
|
||||
}
|
||||
}
|
||||
|
||||
void BouncingBallsScreensaver::update(lv_coord_t screenW, lv_coord_t screenH) {
|
||||
for (auto& ball : balls_) {
|
||||
if (!ball.obj) continue;
|
||||
|
||||
ball.x += ball.dx;
|
||||
ball.y += ball.dy;
|
||||
|
||||
bool bounced = false;
|
||||
|
||||
if (ball.x <= 0) {
|
||||
ball.x = 0;
|
||||
ball.dx = -ball.dx;
|
||||
bounced = true;
|
||||
} else if (ball.x >= screenW - BALL_SIZE) {
|
||||
ball.x = screenW - BALL_SIZE;
|
||||
ball.dx = -ball.dx;
|
||||
bounced = true;
|
||||
}
|
||||
|
||||
if (ball.y <= 0) {
|
||||
ball.y = 0;
|
||||
ball.dy = -ball.dy;
|
||||
bounced = true;
|
||||
} else if (ball.y >= screenH - BALL_SIZE) {
|
||||
ball.y = screenH - BALL_SIZE;
|
||||
ball.dy = -ball.dy;
|
||||
bounced = true;
|
||||
}
|
||||
|
||||
if (bounced) {
|
||||
setRandomTargetColor(ball);
|
||||
}
|
||||
|
||||
updateBallColor(ball);
|
||||
lv_obj_set_pos(ball.obj, ball.x, ball.y);
|
||||
}
|
||||
}
|
||||
|
||||
void BouncingBallsScreensaver::initBall(Ball& ball, lv_obj_t* parent, lv_coord_t screenW, lv_coord_t screenH, int index) {
|
||||
ball.obj = lv_obj_create(parent);
|
||||
if (ball.obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_remove_style_all(ball.obj);
|
||||
lv_obj_set_size(ball.obj, BALL_SIZE, BALL_SIZE);
|
||||
lv_obj_set_style_bg_opa(ball.obj, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_radius(ball.obj, BALL_SIZE / 2, 0);
|
||||
|
||||
ball.x = (screenW > BALL_SIZE) ? rand() % (screenW - BALL_SIZE) : 0;
|
||||
ball.y = (screenH > BALL_SIZE) ? rand() % (screenH - BALL_SIZE) : 0;
|
||||
|
||||
// Random speeds between 1-4 with random direction
|
||||
// Note: adjustment below may extend range to 1-5 to ensure varied paths
|
||||
ball.dx = (rand() % 4) + 1;
|
||||
ball.dy = (rand() % 4) + 1;
|
||||
if (rand() % 2) ball.dx = -ball.dx;
|
||||
if (rand() % 2) ball.dy = -ball.dy;
|
||||
// Ensure dx != dy for more varied paths (may adjust dy to 5 or -5)
|
||||
if (abs(ball.dx) == abs(ball.dy)) {
|
||||
ball.dy = ball.dy > 0 ? ball.dy + 1 : ball.dy - 1;
|
||||
}
|
||||
|
||||
uint32_t color = COLORS[index % COLORS.size()];
|
||||
ball.currentR = ball.targetR = (color >> 16) & 0xFF;
|
||||
ball.currentG = ball.targetG = (color >> 8) & 0xFF;
|
||||
ball.currentB = ball.targetB = color & 0xFF;
|
||||
ball.colorStep = 0;
|
||||
|
||||
lv_obj_set_style_bg_color(ball.obj, lv_color_make(ball.currentR, ball.currentG, ball.currentB), 0);
|
||||
lv_obj_set_pos(ball.obj, ball.x, ball.y);
|
||||
}
|
||||
|
||||
void BouncingBallsScreensaver::setRandomTargetColor(Ball& ball) {
|
||||
uint32_t color = COLORS[rand() % COLORS.size()];
|
||||
ball.targetR = (color >> 16) & 0xFF;
|
||||
ball.targetG = (color >> 8) & 0xFF;
|
||||
ball.targetB = color & 0xFF;
|
||||
ball.colorStep = COLOR_FADE_STEPS;
|
||||
}
|
||||
|
||||
void BouncingBallsScreensaver::updateBallColor(Ball& ball) {
|
||||
if (ball.colorStep > 0) {
|
||||
int stepR = (static_cast<int>(ball.targetR) - ball.currentR) / ball.colorStep;
|
||||
int stepG = (static_cast<int>(ball.targetG) - ball.currentG) / ball.colorStep;
|
||||
int stepB = (static_cast<int>(ball.targetB) - ball.currentB) / ball.colorStep;
|
||||
|
||||
ball.currentR = static_cast<uint8_t>(ball.currentR + stepR);
|
||||
ball.currentG = static_cast<uint8_t>(ball.currentG + stepG);
|
||||
ball.currentB = static_cast<uint8_t>(ball.currentB + stepB);
|
||||
ball.colorStep--;
|
||||
|
||||
if (ball.colorStep == 0) {
|
||||
ball.currentR = ball.targetR;
|
||||
ball.currentG = ball.targetG;
|
||||
ball.currentB = ball.targetB;
|
||||
}
|
||||
|
||||
if (ball.obj) {
|
||||
lv_obj_set_style_bg_color(ball.obj, lv_color_make(ball.currentR, ball.currentG, ball.currentB), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Screensaver.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
class BouncingBallsScreensaver final : public Screensaver {
|
||||
public:
|
||||
BouncingBallsScreensaver() = default;
|
||||
~BouncingBallsScreensaver() override = default;
|
||||
BouncingBallsScreensaver(const BouncingBallsScreensaver&) = delete;
|
||||
BouncingBallsScreensaver& operator=(const BouncingBallsScreensaver&) = delete;
|
||||
BouncingBallsScreensaver(BouncingBallsScreensaver&&) = delete;
|
||||
BouncingBallsScreensaver& operator=(BouncingBallsScreensaver&&) = delete;
|
||||
|
||||
void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) override;
|
||||
void stop() override;
|
||||
void update(lv_coord_t screenW, lv_coord_t screenH) override;
|
||||
|
||||
private:
|
||||
static constexpr int BALL_SIZE = 20;
|
||||
static constexpr int NUM_BALLS = 5;
|
||||
static constexpr int COLOR_FADE_STEPS = 15;
|
||||
|
||||
struct Ball {
|
||||
lv_obj_t* obj = nullptr;
|
||||
lv_coord_t x = 0;
|
||||
lv_coord_t y = 0;
|
||||
lv_coord_t dx = 0;
|
||||
lv_coord_t dy = 0;
|
||||
uint8_t currentR = 0, currentG = 0, currentB = 0;
|
||||
uint8_t targetR = 0, targetG = 0, targetB = 0;
|
||||
int colorStep = 0;
|
||||
};
|
||||
|
||||
static constexpr std::array<uint32_t, 8> COLORS = {
|
||||
0xFF0000, // Red
|
||||
0x00FF00, // Green
|
||||
0x0000FF, // Blue
|
||||
0xFFFF00, // Yellow
|
||||
0xFF00FF, // Magenta
|
||||
0x00FFFF, // Cyan
|
||||
0xFF8000, // Orange
|
||||
0x80FF00, // Lime
|
||||
};
|
||||
|
||||
std::array<Ball, NUM_BALLS> balls_;
|
||||
|
||||
void initBall(Ball& ball, lv_obj_t* parent, lv_coord_t screenW, lv_coord_t screenH, int index);
|
||||
static void setRandomTargetColor(Ball& ball);
|
||||
static void updateBallColor(Ball& ball);
|
||||
};
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -1,91 +1,265 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
||||
|
||||
#include "Screensaver.h"
|
||||
#include "BouncingBallsScreensaver.h"
|
||||
#include "MatrixRainScreensaver.h"
|
||||
#include "MystifyScreensaver.h"
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/CoreDefines.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
class DisplayIdleService final : public Service {
|
||||
static const auto LOGGER = Logger("DisplayIdle");
|
||||
|
||||
std::unique_ptr<Timer> timer;
|
||||
bool displayDimmed = false;
|
||||
settings::display::DisplaySettings cachedDisplaySettings;
|
||||
constexpr uint32_t kWakeActivityThresholdMs = 100;
|
||||
|
||||
static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
|
||||
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
||||
static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
|
||||
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void DisplayIdleService::stopScreensaver() {
|
||||
if (!lvgl::lock(100)) {
|
||||
// Lock failed - keep flag set to retry on next tick
|
||||
return;
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Settings are now cached and event-driven (no file I/O in timer callback!)
|
||||
// This prevents watchdog timeout from blocking the Timer Service task
|
||||
const auto restoreDuty = cachedDisplaySettings.backlightDuty;
|
||||
const bool wasDimmed = displayDimmed;
|
||||
|
||||
if (lv_disp_get_default() == nullptr) {
|
||||
return;
|
||||
if (screensaverOverlay) {
|
||||
if (screensaver) {
|
||||
screensaver->stop();
|
||||
screensaver.reset();
|
||||
}
|
||||
lv_obj_delete(screensaverOverlay);
|
||||
screensaverOverlay = nullptr;
|
||||
}
|
||||
lvgl::unlock();
|
||||
stopScreensaverRequested.store(false, std::memory_order_relaxed);
|
||||
|
||||
// Query LVGL inactivity once for both checks
|
||||
uint32_t inactive_ms = 0;
|
||||
if (lvgl::lock(100)) {
|
||||
inactive_ms = lv_disp_get_inactive_time(nullptr);
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Reset auto-off state
|
||||
screensaverActiveCounter = 0;
|
||||
backlightOff = false;
|
||||
|
||||
// TODO: The following logic only works with the first display. There might be multiple displays.
|
||||
// Handle display backlight
|
||||
auto display = getDisplay();
|
||||
if (display != nullptr && display->supportsBacklightDuty()) {
|
||||
// If timeout disabled, ensure backlight restored if we had dimmed it
|
||||
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
|
||||
if (displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
} else {
|
||||
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
|
||||
display->setBacklightDuty(0);
|
||||
displayDimmed = true;
|
||||
} else if (displayDimmed && inactive_ms < 100) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
// Restore backlight if display was dimmed
|
||||
auto display = getDisplay();
|
||||
if (display && wasDimmed) {
|
||||
display->setBacklightDuty(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::None:
|
||||
default:
|
||||
// Just black screen, no animated screensaver
|
||||
screensaver = nullptr;
|
||||
break;
|
||||
}
|
||||
|
||||
if (screensaver) {
|
||||
screensaver->start(screensaverOverlay, screenW, screenH);
|
||||
}
|
||||
}
|
||||
|
||||
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::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();
|
||||
}
|
||||
auto display = getDisplay();
|
||||
if (display) {
|
||||
display->setBacklightDuty(0);
|
||||
}
|
||||
backlightOff = true;
|
||||
} else {
|
||||
updateScreensaver();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lvgl::unlock();
|
||||
|
||||
// Check stop request early for faster response
|
||||
if (stopScreensaverRequested.load(std::memory_order_acquire)) {
|
||||
stopScreensaver();
|
||||
return;
|
||||
}
|
||||
|
||||
public:
|
||||
bool onStart(ServiceContext& service) override {
|
||||
// Load settings once at startup and cache them
|
||||
// This eliminates file I/O from timer callback (prevents watchdog timeout)
|
||||
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
||||
|
||||
// Note: Settings changes require service restart to take effect
|
||||
// TODO: Add DisplaySettingsChanged events for dynamic updates
|
||||
|
||||
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(250), [this]{ this->tick(); });
|
||||
timer->setCallbackPriority(Thread::Priority::Lower);
|
||||
timer->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(ServiceContext& service) override {
|
||||
if (timer) {
|
||||
timer->stop();
|
||||
timer = nullptr;
|
||||
}
|
||||
// Ensure display restored on stop
|
||||
auto display = getDisplay();
|
||||
if (display && displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
auto display = getDisplay();
|
||||
if (display != nullptr && display->supportsBacklightDuty()) {
|
||||
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
|
||||
if (displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
} else {
|
||||
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
|
||||
if (!lvgl::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) {
|
||||
display->setBacklightDuty(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, kernel::millisToTicks(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) {
|
||||
kernel::delayMillis(50); // Brief delay before retry
|
||||
}
|
||||
}
|
||||
if (screensaverOverlay) {
|
||||
LOGGER.warn("Failed to stop screensaver during shutdown - potential resource leak");
|
||||
}
|
||||
}
|
||||
screensaver.reset();
|
||||
}
|
||||
|
||||
void DisplayIdleService::startScreensaver() {
|
||||
if (!lvgl::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
|
||||
auto display = getDisplay();
|
||||
if (display && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
|
||||
display->setBacklightDuty(0);
|
||||
}
|
||||
displayDimmed = true;
|
||||
}
|
||||
|
||||
bool DisplayIdleService::isScreensaverActive() const {
|
||||
return screensaverOverlay != nullptr;
|
||||
}
|
||||
|
||||
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("DisplayIdle")
|
||||
);
|
||||
}
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
.id = "DisplayIdle",
|
||||
@ -93,3 +267,5 @@ extern const ServiceManifest manifest = {
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
320
Tactility/Source/service/displayidle/MatrixRainScreensaver.cpp
Normal file
320
Tactility/Source/service/displayidle/MatrixRainScreensaver.cpp
Normal file
@ -0,0 +1,320 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "MatrixRainScreensaver.h"
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
static_assert(MatrixRainScreensaver::MAX_TRAIL_LENGTH > MatrixRainScreensaver::MIN_TRAIL_LENGTH,
|
||||
"MAX_TRAIL_LENGTH must be greater than MIN_TRAIL_LENGTH");
|
||||
static_assert(MatrixRainScreensaver::MAX_TICK_DELAY >= MatrixRainScreensaver::MIN_TICK_DELAY,
|
||||
"MAX_TICK_DELAY must be >= MIN_TICK_DELAY");
|
||||
|
||||
static constexpr int TRAIL_LENGTH_RANGE = MatrixRainScreensaver::MAX_TRAIL_LENGTH - MatrixRainScreensaver::MIN_TRAIL_LENGTH;
|
||||
static constexpr int TICK_DELAY_RANGE = MatrixRainScreensaver::MAX_TICK_DELAY - MatrixRainScreensaver::MIN_TICK_DELAY + 1;
|
||||
|
||||
char MatrixRainScreensaver::randomChar() {
|
||||
static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*<>?";
|
||||
return chars[rand() % (sizeof(chars) - 1)];
|
||||
}
|
||||
|
||||
lv_color_t MatrixRainScreensaver::getTrailColor(int index, int trailLength) const {
|
||||
if (trailLength <= 1) {
|
||||
return lv_color_hex(COLOR_GRADIENT[0]);
|
||||
}
|
||||
|
||||
float intensity = static_cast<float>(index) / static_cast<float>(trailLength - 1);
|
||||
int colorIndex = static_cast<int>(intensity * (COLOR_GRADIENT.size() - 1) + 0.5f);
|
||||
|
||||
if (colorIndex < 0) colorIndex = 0;
|
||||
if (colorIndex >= static_cast<int>(COLOR_GRADIENT.size())) {
|
||||
colorIndex = COLOR_GRADIENT.size() - 1;
|
||||
}
|
||||
|
||||
return lv_color_hex(COLOR_GRADIENT[colorIndex]);
|
||||
}
|
||||
|
||||
int MatrixRainScreensaver::getRandomAvailableColumn() {
|
||||
if (availableColumns_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
int idx = rand() % availableColumns_.size();
|
||||
int column = availableColumns_[idx];
|
||||
// O(1) removal: swap with last element and pop
|
||||
availableColumns_[idx] = availableColumns_.back();
|
||||
availableColumns_.pop_back();
|
||||
return column;
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::releaseColumn(int column) {
|
||||
// Add column back to available pool if not already present (duplicate protection)
|
||||
bool alreadyPresent = std::find(availableColumns_.begin(), availableColumns_.end(), column) != availableColumns_.end();
|
||||
assert(!alreadyPresent && "Column released twice - indicates bug in column management");
|
||||
if (!alreadyPresent) {
|
||||
availableColumns_.push_back(column);
|
||||
}
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::createRainCharLabels(RainChar& rc, int trailIndex, int trailLength) {
|
||||
if (overlay_ == nullptr) {
|
||||
rc.label = nullptr;
|
||||
rc.glowLabel = nullptr;
|
||||
return;
|
||||
}
|
||||
rc.ch = randomChar();
|
||||
|
||||
// Create glow layer first (behind main text)
|
||||
rc.glowLabel = lv_label_create(overlay_);
|
||||
if (rc.glowLabel == nullptr) {
|
||||
rc.label = nullptr;
|
||||
return;
|
||||
}
|
||||
lv_obj_set_style_text_font(rc.glowLabel, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(rc.glowLabel, lv_color_hex(COLOR_GLOW), 0);
|
||||
lv_obj_set_style_opa(rc.glowLabel, LV_OPA_70, 0);
|
||||
|
||||
// Create main character label
|
||||
rc.label = lv_label_create(overlay_);
|
||||
if (rc.label == nullptr) {
|
||||
lv_obj_delete(rc.glowLabel);
|
||||
rc.glowLabel = nullptr;
|
||||
return;
|
||||
}
|
||||
lv_obj_set_style_text_font(rc.label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(rc.label, getTrailColor(trailIndex, trailLength), 0);
|
||||
|
||||
char text[2] = {rc.ch, '\0'};
|
||||
lv_label_set_text(rc.label, text);
|
||||
lv_label_set_text(rc.glowLabel, text);
|
||||
|
||||
// Hide initially
|
||||
lv_obj_set_pos(rc.label, -100, -100);
|
||||
lv_obj_set_pos(rc.glowLabel, -100, -100);
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::initRaindrop(Raindrop& drop, int column) {
|
||||
drop.column = column;
|
||||
drop.trailLength = MIN_TRAIL_LENGTH + rand() % TRAIL_LENGTH_RANGE;
|
||||
drop.headRow = -(drop.trailLength); // Start above screen
|
||||
drop.tickDelay = MIN_TICK_DELAY + rand() % TICK_DELAY_RANGE;
|
||||
drop.tickCounter = 0;
|
||||
drop.active = true;
|
||||
|
||||
// Create character labels for trail
|
||||
drop.chars.resize(drop.trailLength);
|
||||
for (int i = 0; i < drop.trailLength; i++) {
|
||||
createRainCharLabels(drop.chars[i], i, drop.trailLength);
|
||||
}
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::resetRaindrop(Raindrop& drop) {
|
||||
releaseColumn(drop.column);
|
||||
|
||||
int newColumn = getRandomAvailableColumn();
|
||||
if (newColumn < 0) {
|
||||
drop.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
drop.column = newColumn;
|
||||
drop.tickDelay = MIN_TICK_DELAY + rand() % TICK_DELAY_RANGE;
|
||||
drop.tickCounter = 0;
|
||||
|
||||
int newTrailLength = MIN_TRAIL_LENGTH + rand() % TRAIL_LENGTH_RANGE;
|
||||
|
||||
// Resize if needed
|
||||
if (newTrailLength != drop.trailLength) {
|
||||
// Delete excess labels
|
||||
for (size_t i = newTrailLength; i < drop.chars.size(); i++) {
|
||||
if (drop.chars[i].label) {
|
||||
lv_obj_delete(drop.chars[i].label);
|
||||
}
|
||||
if (drop.chars[i].glowLabel) {
|
||||
lv_obj_delete(drop.chars[i].glowLabel);
|
||||
}
|
||||
}
|
||||
|
||||
size_t oldSize = drop.chars.size();
|
||||
drop.chars.resize(newTrailLength);
|
||||
|
||||
// Create new labels if needed (will be refreshed below)
|
||||
for (size_t i = oldSize; i < drop.chars.size(); i++) {
|
||||
createRainCharLabels(drop.chars[i], static_cast<int>(i), newTrailLength);
|
||||
}
|
||||
|
||||
drop.trailLength = newTrailLength;
|
||||
}
|
||||
|
||||
// Reset position above screen
|
||||
drop.headRow = -(drop.trailLength);
|
||||
|
||||
// Refresh characters and colors for all labels
|
||||
for (int i = 0; i < drop.trailLength; i++) {
|
||||
auto& rc = drop.chars[i];
|
||||
if (rc.label == nullptr || rc.glowLabel == nullptr) {
|
||||
continue;
|
||||
}
|
||||
rc.ch = randomChar();
|
||||
|
||||
char text[2] = {rc.ch, '\0'};
|
||||
lv_label_set_text(rc.label, text);
|
||||
lv_label_set_text(rc.glowLabel, text);
|
||||
lv_obj_set_style_text_color(rc.label, getTrailColor(i, drop.trailLength), 0);
|
||||
|
||||
lv_obj_set_pos(rc.label, -100, -100);
|
||||
lv_obj_set_pos(rc.glowLabel, -100, -100);
|
||||
}
|
||||
|
||||
drop.active = true;
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::updateRaindropDisplay(Raindrop& drop) {
|
||||
int xPos = drop.column * CHAR_WIDTH;
|
||||
|
||||
for (int i = 0; i < drop.trailLength; i++) {
|
||||
auto& rc = drop.chars[i];
|
||||
if (rc.label == nullptr || rc.glowLabel == nullptr) {
|
||||
continue;
|
||||
}
|
||||
int row = drop.headRow - i;
|
||||
int yPos = row * CHAR_HEIGHT;
|
||||
|
||||
// Only show characters that are on screen (grid-snapped positions)
|
||||
if (row >= 0 && row < numRows_) {
|
||||
lv_obj_set_pos(rc.label, xPos, yPos);
|
||||
lv_obj_set_pos(rc.glowLabel, xPos + GLOW_OFFSET, yPos + GLOW_OFFSET);
|
||||
} else {
|
||||
lv_obj_set_pos(rc.label, -100, -100);
|
||||
lv_obj_set_pos(rc.glowLabel, -100, -100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::flickerRandomChars() {
|
||||
constexpr int ERROR_PERCENT = 3;
|
||||
|
||||
for (auto& drop : drops_) {
|
||||
if (!drop.active) continue;
|
||||
|
||||
for (int i = 0; i < drop.trailLength; i++) {
|
||||
int row = drop.headRow - i;
|
||||
|
||||
// Only process visible characters
|
||||
if (row >= 0 && row < numRows_) {
|
||||
if (rand() % 100 < ERROR_PERCENT) {
|
||||
auto& rc = drop.chars[i];
|
||||
if (rc.label == nullptr || rc.glowLabel == nullptr) {
|
||||
continue;
|
||||
}
|
||||
rc.ch = randomChar();
|
||||
char text[2] = {rc.ch, '\0'};
|
||||
lv_label_set_text(rc.label, text);
|
||||
lv_label_set_text(rc.glowLabel, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) {
|
||||
if (!drops_.empty()) {
|
||||
stop();
|
||||
}
|
||||
|
||||
overlay_ = overlay;
|
||||
screenW_ = screenW;
|
||||
screenH_ = screenH;
|
||||
globalFlickerCounter_ = 0;
|
||||
|
||||
numColumns_ = screenW / CHAR_WIDTH;
|
||||
numRows_ = screenH / CHAR_HEIGHT;
|
||||
|
||||
availableColumns_.clear();
|
||||
for (int i = 0; i < numColumns_; i++) {
|
||||
availableColumns_.push_back(i);
|
||||
}
|
||||
|
||||
int numDrops = std::min(MAX_ACTIVE_DROPS, numColumns_);
|
||||
drops_.resize(numDrops);
|
||||
|
||||
for (int i = 0; i < numDrops; i++) {
|
||||
int col = getRandomAvailableColumn();
|
||||
if (col >= 0) {
|
||||
initRaindrop(drops_[i], col);
|
||||
// Stagger start positions (guard against tiny screens)
|
||||
int staggerRange = numRows_ / 2;
|
||||
if (staggerRange > 0) {
|
||||
drops_[i].headRow = -(rand() % staggerRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::stop() {
|
||||
// Explicitly delete labels to prevent resource leaks
|
||||
for (auto& drop : drops_) {
|
||||
for (auto& rc : drop.chars) {
|
||||
if (rc.label) {
|
||||
lv_obj_delete(rc.label);
|
||||
rc.label = nullptr;
|
||||
}
|
||||
if (rc.glowLabel) {
|
||||
lv_obj_delete(rc.glowLabel);
|
||||
rc.glowLabel = nullptr;
|
||||
}
|
||||
}
|
||||
drop.chars.clear();
|
||||
}
|
||||
drops_.clear();
|
||||
availableColumns_.clear();
|
||||
overlay_ = nullptr;
|
||||
}
|
||||
|
||||
void MatrixRainScreensaver::update(lv_coord_t screenW, lv_coord_t screenH) {
|
||||
// Screen dimensions captured at start() - no dynamic resize support during screensaver
|
||||
LV_UNUSED(screenW);
|
||||
LV_UNUSED(screenH);
|
||||
|
||||
// Global glitch effect
|
||||
globalFlickerCounter_++;
|
||||
if (globalFlickerCounter_ >= 5) {
|
||||
globalFlickerCounter_ = 0;
|
||||
flickerRandomChars();
|
||||
}
|
||||
|
||||
for (auto& drop : drops_) {
|
||||
if (!drop.active) continue;
|
||||
|
||||
// Tick-based movement (terminal style - advance by row)
|
||||
drop.tickCounter++;
|
||||
if (drop.tickCounter >= drop.tickDelay) {
|
||||
drop.tickCounter = 0;
|
||||
|
||||
// Advance head down by one row
|
||||
drop.headRow++;
|
||||
|
||||
// Randomly change the head character when it advances
|
||||
if (!drop.chars.empty() && drop.chars[0].label != nullptr && drop.chars[0].glowLabel != nullptr) {
|
||||
drop.chars[0].ch = randomChar();
|
||||
char text[2] = {drop.chars[0].ch, '\0'};
|
||||
lv_label_set_text(drop.chars[0].label, text);
|
||||
lv_label_set_text(drop.chars[0].glowLabel, text);
|
||||
}
|
||||
|
||||
// Check if tail has completely left the screen
|
||||
int tailRow = drop.headRow - (drop.trailLength - 1);
|
||||
if (tailRow >= numRows_) {
|
||||
resetRaindrop(drop);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
updateRaindropDisplay(drop);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
86
Tactility/Source/service/displayidle/MatrixRainScreensaver.h
Normal file
86
Tactility/Source/service/displayidle/MatrixRainScreensaver.h
Normal file
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Screensaver.h"
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
class MatrixRainScreensaver final : public Screensaver {
|
||||
public:
|
||||
MatrixRainScreensaver() = default;
|
||||
MatrixRainScreensaver(const MatrixRainScreensaver&) = delete;
|
||||
MatrixRainScreensaver& operator=(const MatrixRainScreensaver&) = delete;
|
||||
MatrixRainScreensaver(MatrixRainScreensaver&&) = delete;
|
||||
MatrixRainScreensaver& operator=(MatrixRainScreensaver&&) = delete;
|
||||
|
||||
static constexpr int CHAR_WIDTH = 12;
|
||||
static constexpr int CHAR_HEIGHT = 16;
|
||||
static constexpr int MIN_TRAIL_LENGTH = 8;
|
||||
static constexpr int MAX_TRAIL_LENGTH = 20;
|
||||
static constexpr int MAX_ACTIVE_DROPS = 16;
|
||||
static constexpr int MIN_TICK_DELAY = 2; // Fastest: advance every 2 frames
|
||||
static constexpr int MAX_TICK_DELAY = 5; // Slowest: advance every 5 frames
|
||||
static constexpr int GLOW_OFFSET = 2;
|
||||
|
||||
~MatrixRainScreensaver() override = default;
|
||||
|
||||
void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) override;
|
||||
void stop() override;
|
||||
void update(lv_coord_t screenW, lv_coord_t screenH) override;
|
||||
|
||||
private:
|
||||
// A single character cell in the rain
|
||||
struct RainChar {
|
||||
lv_obj_t* label = nullptr;
|
||||
lv_obj_t* glowLabel = nullptr;
|
||||
char ch = ' ';
|
||||
};
|
||||
|
||||
// A raindrop - now uses integer row positions (terminal-style grid)
|
||||
struct Raindrop {
|
||||
int headRow = 0; // Current head row position (grid-based)
|
||||
int column = 0; // Column index
|
||||
int trailLength = 10;
|
||||
int tickDelay = 3; // Frames between row advances
|
||||
int tickCounter = 0; // Current tick count
|
||||
std::vector<RainChar> chars;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
std::vector<Raindrop> drops_;
|
||||
std::vector<int> availableColumns_;
|
||||
lv_obj_t* overlay_ = nullptr;
|
||||
lv_coord_t screenW_ = 0;
|
||||
lv_coord_t screenH_ = 0;
|
||||
int numColumns_ = 0;
|
||||
int numRows_ = 0;
|
||||
int globalFlickerCounter_ = 0;
|
||||
|
||||
// Six-color gradient palette
|
||||
static constexpr std::array<uint32_t, 6> COLOR_GRADIENT = {
|
||||
0xAAFFDD, // 0: Bright cyan-white (head)
|
||||
0x00FF66, // 1: Bright green
|
||||
0x00DD44, // 2: Medium-bright green
|
||||
0x00AA33, // 3: Medium green
|
||||
0x006622, // 4: Dim green
|
||||
0x003311, // 5: Dark green (tail end)
|
||||
};
|
||||
static constexpr uint32_t COLOR_GLOW = 0x002208;
|
||||
|
||||
void initRaindrop(Raindrop& drop, int column);
|
||||
void resetRaindrop(Raindrop& drop);
|
||||
void updateRaindropDisplay(Raindrop& drop);
|
||||
void flickerRandomChars();
|
||||
void createRainCharLabels(RainChar& rc, int trailIndex, int trailLength);
|
||||
lv_color_t getTrailColor(int index, int trailLength) const;
|
||||
static char randomChar();
|
||||
int getRandomAvailableColumn();
|
||||
void releaseColumn(int column);
|
||||
};
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
209
Tactility/Source/service/displayidle/MystifyScreensaver.cpp
Normal file
209
Tactility/Source/service/displayidle/MystifyScreensaver.cpp
Normal file
@ -0,0 +1,209 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "MystifyScreensaver.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
// Helper to generate random float in range [min, max]
|
||||
static float randomFloat(float min, float max) {
|
||||
if (max <= min) return min;
|
||||
return min + (max - min) * static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||
}
|
||||
|
||||
void MystifyScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) {
|
||||
for (auto& polygon : polygons_) {
|
||||
initPolygon(polygon, overlay, screenW, screenH);
|
||||
}
|
||||
}
|
||||
|
||||
void MystifyScreensaver::stop() {
|
||||
for (auto& polygon : polygons_) {
|
||||
for (auto& trailLines : polygon.lines) {
|
||||
for (auto& line : trailLines) {
|
||||
line = nullptr; // Objects deleted by parent overlay
|
||||
}
|
||||
}
|
||||
polygon.historyHead = 0;
|
||||
polygon.historyFull = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MystifyScreensaver::update(lv_coord_t screenW, lv_coord_t screenH) {
|
||||
constexpr float minSpeed = 0.5f;
|
||||
constexpr float maxSpeed = 2.5f;
|
||||
|
||||
for (auto& polygon : polygons_) {
|
||||
// Periodic color change
|
||||
polygon.colorChangeCounter++;
|
||||
if (polygon.colorChangeCounter >= COLOR_CHANGE_INTERVAL) {
|
||||
polygon.colorChangeCounter = 0;
|
||||
uint32_t colorHex = COLOR_POOL[rand() % COLOR_POOL.size()];
|
||||
polygon.color = lv_color_hex(colorHex);
|
||||
updateLineColors(polygon);
|
||||
}
|
||||
|
||||
// Move vertices
|
||||
for (auto& vertex : polygon.vertices) {
|
||||
vertex.x += vertex.dx;
|
||||
vertex.y += vertex.dy;
|
||||
|
||||
// Bounce off edges with slight angle variation for more organic movement
|
||||
if (vertex.x <= 0) {
|
||||
vertex.x = 0;
|
||||
vertex.dx = std::fabs(vertex.dx) + randomFloat(-0.2f, 0.2f);
|
||||
} else if (vertex.x >= screenW - 1) {
|
||||
vertex.x = static_cast<float>(screenW - 1);
|
||||
vertex.dx = -std::fabs(vertex.dx) + randomFloat(-0.2f, 0.2f);
|
||||
}
|
||||
|
||||
if (vertex.y <= 0) {
|
||||
vertex.y = 0;
|
||||
vertex.dy = std::fabs(vertex.dy) + randomFloat(-0.2f, 0.2f);
|
||||
} else if (vertex.y >= screenH - 1) {
|
||||
vertex.y = static_cast<float>(screenH - 1);
|
||||
vertex.dy = -std::fabs(vertex.dy) + randomFloat(-0.2f, 0.2f);
|
||||
}
|
||||
|
||||
// Clamp speeds to prevent runaway acceleration or stalling
|
||||
if (std::fabs(vertex.dx) < minSpeed) vertex.dx = (vertex.dx >= 0 ? minSpeed : -minSpeed);
|
||||
if (std::fabs(vertex.dy) < minSpeed) vertex.dy = (vertex.dy >= 0 ? minSpeed : -minSpeed);
|
||||
if (std::fabs(vertex.dx) > maxSpeed) vertex.dx = (vertex.dx >= 0 ? maxSpeed : -maxSpeed);
|
||||
if (std::fabs(vertex.dy) > maxSpeed) vertex.dy = (vertex.dy >= 0 ? maxSpeed : -maxSpeed);
|
||||
}
|
||||
|
||||
// Shift history
|
||||
int newHead = polygon.historyHead + 1;
|
||||
if (newHead >= TRAIL_LENGTH) {
|
||||
newHead = 0;
|
||||
polygon.historyFull = true;
|
||||
}
|
||||
polygon.historyHead = newHead;
|
||||
|
||||
// Store current positions at head
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
polygon.history[newHead][v].x = polygon.vertices[v].x;
|
||||
polygon.history[newHead][v].y = polygon.vertices[v].y;
|
||||
}
|
||||
|
||||
// Pre-calculate history indices
|
||||
int histIndices[TRAIL_LENGTH];
|
||||
for (int t = 0; t < TRAIL_LENGTH; t++) {
|
||||
int idx = newHead - t;
|
||||
histIndices[t] = (idx >= 0) ? idx : idx + TRAIL_LENGTH;
|
||||
}
|
||||
|
||||
// Calculate valid trail count
|
||||
int validTrailCount = polygon.historyFull ? TRAIL_LENGTH : (newHead + 1);
|
||||
|
||||
// Update line positions
|
||||
for (int t = 0; t < TRAIL_LENGTH; t++) {
|
||||
auto& trailLines = polygon.lines[t];
|
||||
|
||||
// Hide lines beyond valid history
|
||||
if (t >= validTrailCount) {
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
if (trailLines[e]) {
|
||||
lv_obj_add_flag(trailLines[e], LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int histIndex = histIndices[t];
|
||||
auto& hist = polygon.history[histIndex];
|
||||
auto& linePointsTrail = polygon.linePoints[t];
|
||||
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
if (!trailLines[e]) continue;
|
||||
|
||||
lv_obj_remove_flag(trailLines[e], LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
int v2 = (e + 1) % NUM_VERTICES;
|
||||
|
||||
linePointsTrail[e][0].x = hist[e].x;
|
||||
linePointsTrail[e][0].y = hist[e].y;
|
||||
linePointsTrail[e][1].x = hist[v2].x;
|
||||
linePointsTrail[e][1].y = hist[v2].y;
|
||||
|
||||
lv_line_set_points(trailLines[e], linePointsTrail[e].data(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MystifyScreensaver::initPolygon(Polygon& polygon, lv_obj_t* parent, lv_coord_t screenW, lv_coord_t screenH) {
|
||||
// Sanity check - avoid undefined behavior from modulo by zero
|
||||
if (screenW <= 0 || screenH <= 0) return;
|
||||
|
||||
// Pick a random color from the pool
|
||||
uint32_t colorHex = COLOR_POOL[rand() % COLOR_POOL.size()];
|
||||
polygon.color = lv_color_hex(colorHex);
|
||||
polygon.historyHead = 0;
|
||||
polygon.historyFull = false;
|
||||
// Stagger color changes so polygons don't all change at once
|
||||
polygon.colorChangeCounter = rand() % COLOR_CHANGE_INTERVAL;
|
||||
|
||||
// Initialize vertices with random positions and velocities
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
auto& vertex = polygon.vertices[v];
|
||||
vertex.x = static_cast<float>(rand() % screenW);
|
||||
vertex.y = static_cast<float>(rand() % screenH);
|
||||
|
||||
// Slower speeds (0.8-2.0) for smooth movement
|
||||
vertex.dx = randomFloat(0.8f, 2.0f);
|
||||
vertex.dy = randomFloat(0.8f, 2.0f);
|
||||
if (rand() % 2) vertex.dx = -vertex.dx;
|
||||
if (rand() % 2) vertex.dy = -vertex.dy;
|
||||
|
||||
// Ensure dx != dy for more interesting movement patterns
|
||||
if (std::fabs(std::fabs(vertex.dx) - std::fabs(vertex.dy)) < 0.3f) {
|
||||
vertex.dy += (vertex.dy > 0 ? 0.5f : -0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize history with current positions
|
||||
for (int t = 0; t < TRAIL_LENGTH; t++) {
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
polygon.history[t][v].x = polygon.vertices[v].x;
|
||||
polygon.history[t][v].y = polygon.vertices[v].y;
|
||||
}
|
||||
}
|
||||
|
||||
// Create line objects with pre-computed opacity values
|
||||
for (int t = 0; t < TRAIL_LENGTH; t++) {
|
||||
int opacity = LV_OPA_COVER - (t * LV_OPA_COVER / TRAIL_LENGTH);
|
||||
if (opacity < LV_OPA_10) opacity = LV_OPA_10;
|
||||
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
lv_obj_t* line = lv_line_create(parent);
|
||||
if (line == nullptr) {
|
||||
polygon.lines[t][e] = nullptr;
|
||||
continue;
|
||||
}
|
||||
lv_obj_remove_style_all(line);
|
||||
|
||||
lv_obj_set_style_line_color(line, polygon.color, 0);
|
||||
lv_obj_set_style_line_opa(line, opacity, 0);
|
||||
lv_obj_set_style_line_width(line, 2, 0);
|
||||
|
||||
polygon.lines[t][e] = line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MystifyScreensaver::updateLineColors(Polygon& polygon) {
|
||||
// Update all line colors when color changes
|
||||
for (int t = 0; t < TRAIL_LENGTH; t++) {
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
if (polygon.lines[t][e]) {
|
||||
lv_obj_set_style_line_color(polygon.lines[t][e], polygon.color, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
72
Tactility/Source/service/displayidle/MystifyScreensaver.h
Normal file
72
Tactility/Source/service/displayidle/MystifyScreensaver.h
Normal file
@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Screensaver.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
class MystifyScreensaver final : public Screensaver {
|
||||
public:
|
||||
static constexpr int NUM_POLYGONS = 2;
|
||||
static constexpr int NUM_VERTICES = 4;
|
||||
static constexpr int TRAIL_LENGTH = 8;
|
||||
static constexpr int COLOR_CHANGE_INTERVAL = 200; // Frames between color changes (~10s at 20fps)
|
||||
|
||||
~MystifyScreensaver() override = default;
|
||||
MystifyScreensaver() = default;
|
||||
MystifyScreensaver(const MystifyScreensaver&) = delete;
|
||||
MystifyScreensaver& operator=(const MystifyScreensaver&) = delete;
|
||||
MystifyScreensaver(MystifyScreensaver&&) = delete;
|
||||
MystifyScreensaver& operator=(MystifyScreensaver&&) = delete;
|
||||
|
||||
void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) override;
|
||||
void stop() override;
|
||||
void update(lv_coord_t screenW, lv_coord_t screenH) override;
|
||||
|
||||
private:
|
||||
// Use floats for smooth sub-pixel movement
|
||||
struct Vertex {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
float dx = 0;
|
||||
float dy = 0;
|
||||
};
|
||||
|
||||
struct Polygon {
|
||||
std::array<Vertex, NUM_VERTICES> vertices;
|
||||
// History: [trail_index][vertex_index] = {x, y}
|
||||
std::array<std::array<lv_point_precise_t, NUM_VERTICES>, TRAIL_LENGTH> history;
|
||||
// Line objects: [trail_index][edge_index]
|
||||
std::array<std::array<lv_obj_t*, NUM_VERTICES>, TRAIL_LENGTH> lines{};
|
||||
// Persistent point arrays for each line (required by lv_line_set_points)
|
||||
std::array<std::array<std::array<lv_point_precise_t, 2>, NUM_VERTICES>, TRAIL_LENGTH> linePoints;
|
||||
lv_color_t color = {};
|
||||
int historyHead = 0;
|
||||
bool historyFull = false;
|
||||
int colorChangeCounter = 0;
|
||||
};
|
||||
|
||||
// Vibrant color pool for random selection
|
||||
static constexpr std::array<uint32_t, 8> COLOR_POOL = {
|
||||
0xFF00FF, // Magenta
|
||||
0x00FFFF, // Cyan
|
||||
0xFFFF00, // Yellow
|
||||
0xFF6600, // Orange
|
||||
0x00FF66, // Spring green
|
||||
0x6600FF, // Purple
|
||||
0xFF0066, // Hot pink
|
||||
0x66FF00, // Lime
|
||||
};
|
||||
|
||||
std::array<Polygon, NUM_POLYGONS> polygons_;
|
||||
|
||||
void initPolygon(Polygon& polygon, lv_obj_t* parent, lv_coord_t screenW, lv_coord_t screenH);
|
||||
void updateLineColors(Polygon& polygon);
|
||||
};
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
48
Tactility/Source/service/displayidle/Screensaver.h
Normal file
48
Tactility/Source/service/displayidle/Screensaver.h
Normal file
@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
/**
|
||||
* Base class for screensaver implementations.
|
||||
* Screensavers are responsible for creating their visual elements
|
||||
* on the provided overlay and updating them each tick.
|
||||
*/
|
||||
class Screensaver {
|
||||
public:
|
||||
Screensaver() = default;
|
||||
virtual ~Screensaver() = default;
|
||||
Screensaver(const Screensaver&) = delete;
|
||||
Screensaver& operator=(const Screensaver&) = delete;
|
||||
Screensaver(Screensaver&&) = delete;
|
||||
Screensaver& operator=(Screensaver&&) = delete;
|
||||
|
||||
/**
|
||||
* Start the screensaver, creating visual elements on the overlay.
|
||||
* @param overlay The parent LVGL object to create elements on
|
||||
* @param screenW Screen width in pixels
|
||||
* @param screenH Screen height in pixels
|
||||
*/
|
||||
virtual void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) = 0;
|
||||
|
||||
/**
|
||||
* Stop the screensaver, cleaning up any internal state.
|
||||
* Note: LVGL objects are deleted by the parent overlay, but any
|
||||
* internal references should be cleared here.
|
||||
*/
|
||||
virtual void stop() = 0;
|
||||
|
||||
/**
|
||||
* Update the screensaver animation for one frame.
|
||||
* Called periodically while the screensaver is active.
|
||||
* @param screenW Screen width in pixels
|
||||
* @param screenH Screen height in pixels
|
||||
*/
|
||||
virtual void update(lv_coord_t screenW, lv_coord_t screenH) = 0;
|
||||
};
|
||||
|
||||
} // namespace tt::service::displayidle
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -32,7 +32,7 @@ class KeyboardIdleService final : public Service {
|
||||
// Query LVGL inactivity once for both checks
|
||||
uint32_t inactive_ms = 0;
|
||||
if (lvgl::lock(100)) {
|
||||
inactive_ms = lv_disp_get_inactive_time(nullptr);
|
||||
inactive_ms = lv_display_get_inactive_time(nullptr);
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve";
|
||||
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";
|
||||
|
||||
static Orientation getDefaultOrientation() {
|
||||
auto* display = lv_display_get_default();
|
||||
@ -64,6 +65,40 @@ static bool fromString(const std::string& str, Orientation& orientation) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string toString(ScreensaverType type) {
|
||||
switch (type) {
|
||||
using enum ScreensaverType;
|
||||
case None:
|
||||
return "None";
|
||||
case BouncingBalls:
|
||||
return "BouncingBalls";
|
||||
case Mystify:
|
||||
return "Mystify";
|
||||
case MatrixRain:
|
||||
return "MatrixRain";
|
||||
default:
|
||||
std::unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
static bool fromString(const std::string& str, ScreensaverType& type) {
|
||||
if (str == "None") {
|
||||
type = ScreensaverType::None;
|
||||
return true;
|
||||
} else if (str == "BouncingBalls") {
|
||||
type = ScreensaverType::BouncingBalls;
|
||||
return true;
|
||||
} else if (str == "Mystify") {
|
||||
type = ScreensaverType::Mystify;
|
||||
return true;
|
||||
} else if (str == "MatrixRain") {
|
||||
type = ScreensaverType::MatrixRain;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool load(DisplaySettings& settings) {
|
||||
std::map<std::string, std::string> map;
|
||||
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
|
||||
@ -103,11 +138,18 @@ bool load(DisplaySettings& settings) {
|
||||
timeout_ms = static_cast<uint32_t>(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10));
|
||||
}
|
||||
|
||||
auto screensaver_entry = map.find(SETTINGS_KEY_SCREENSAVER_TYPE);
|
||||
ScreensaverType screensaver_type = ScreensaverType::BouncingBalls;
|
||||
if (screensaver_entry != map.end()) {
|
||||
fromString(screensaver_entry->second, screensaver_type);
|
||||
}
|
||||
|
||||
settings.orientation = orientation;
|
||||
settings.gammaCurve = gamma_curve;
|
||||
settings.backlightDuty = backlight_duty;
|
||||
settings.backlightTimeoutEnabled = timeout_enabled;
|
||||
settings.backlightTimeoutMs = timeout_ms;
|
||||
settings.screensaverType = screensaver_type;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -118,7 +160,8 @@ DisplaySettings getDefault() {
|
||||
.gammaCurve = 1,
|
||||
.backlightDuty = 200,
|
||||
.backlightTimeoutEnabled = false,
|
||||
.backlightTimeoutMs = 60000
|
||||
.backlightTimeoutMs = 60000,
|
||||
.screensaverType = ScreensaverType::BouncingBalls
|
||||
};
|
||||
}
|
||||
|
||||
@ -137,6 +180,7 @@ bool save(const DisplaySettings& settings) {
|
||||
map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation);
|
||||
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);
|
||||
return file::savePropertiesFile(SETTINGS_FILE, map);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user