From 016629eb8223434ee2b0f80179502ff7c6250788 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sun, 19 Oct 2025 23:27:16 +0200 Subject: [PATCH] lockscreen app --- Firmware/main/src/app_main.cpp | 2 + Firmware/sdk/apps/CMakeLists.txt | 1 + Firmware/sdk/apps/lockscreen/CMakeLists.txt | 10 + .../include/cardboy/apps/lockscreen_app.hpp | 16 ++ .../apps/lockscreen/src/lockscreen_app.cpp | 192 ++++++++++++++++++ Firmware/sdk/apps/menu/src/menu_app.cpp | 68 +++++-- Firmware/sdk/launchers/desktop/src/main.cpp | 2 + 7 files changed, 277 insertions(+), 14 deletions(-) create mode 100644 Firmware/sdk/apps/lockscreen/CMakeLists.txt create mode 100644 Firmware/sdk/apps/lockscreen/include/cardboy/apps/lockscreen_app.hpp create mode 100644 Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index 633f75f..5859895 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -3,6 +3,7 @@ #include "cardboy/apps/clock_app.hpp" #include "cardboy/apps/gameboy_app.hpp" #include "cardboy/apps/menu_app.hpp" +#include "cardboy/apps/lockscreen_app.hpp" #include "cardboy/apps/settings_app.hpp" #include "cardboy/apps/snake_app.hpp" #include "cardboy/apps/tetris_app.hpp" @@ -233,6 +234,7 @@ extern "C" void app_main() { #endif system.registerApp(apps::createMenuAppFactory()); + system.registerApp(apps::createLockscreenAppFactory()); system.registerApp(apps::createSettingsAppFactory()); system.registerApp(apps::createClockAppFactory()); system.registerApp(apps::createSnakeAppFactory()); diff --git a/Firmware/sdk/apps/CMakeLists.txt b/Firmware/sdk/apps/CMakeLists.txt index 3beac57..5b1ce9a 100644 --- a/Firmware/sdk/apps/CMakeLists.txt +++ b/Firmware/sdk/apps/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries(cardboy_apps target_compile_features(cardboy_apps PUBLIC cxx_std_20) add_subdirectory(menu) +add_subdirectory(lockscreen) add_subdirectory(clock) add_subdirectory(settings) add_subdirectory(gameboy) diff --git a/Firmware/sdk/apps/lockscreen/CMakeLists.txt b/Firmware/sdk/apps/lockscreen/CMakeLists.txt new file mode 100644 index 0000000..94e6bc5 --- /dev/null +++ b/Firmware/sdk/apps/lockscreen/CMakeLists.txt @@ -0,0 +1,10 @@ +target_sources(cardboy_apps + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/lockscreen_app.cpp +) + +target_include_directories(cardboy_apps + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + diff --git a/Firmware/sdk/apps/lockscreen/include/cardboy/apps/lockscreen_app.hpp b/Firmware/sdk/apps/lockscreen/include/cardboy/apps/lockscreen_app.hpp new file mode 100644 index 0000000..9cbe8d0 --- /dev/null +++ b/Firmware/sdk/apps/lockscreen/include/cardboy/apps/lockscreen_app.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "cardboy/sdk/app_framework.hpp" + +#include +#include + +namespace apps { + +inline constexpr char kLockscreenAppName[] = "Lockscreen"; +inline constexpr std::string_view kLockscreenAppNameView = kLockscreenAppName; + +std::unique_ptr createLockscreenAppFactory(); + +} // namespace apps + diff --git a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp new file mode 100644 index 0000000..e7bda6d --- /dev/null +++ b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp @@ -0,0 +1,192 @@ +#include "cardboy/apps/lockscreen_app.hpp" + +#include "cardboy/apps/menu_app.hpp" +#include "cardboy/gfx/font16x8.hpp" +#include "cardboy/sdk/app_framework.hpp" +#include "cardboy/sdk/app_system.hpp" + +#include +#include +#include +#include +#include + +namespace apps { + +namespace { + +using cardboy::sdk::AppContext; + +constexpr std::uint32_t kRefreshIntervalMs = 500; + +using Framebuffer = typename AppContext::Framebuffer; +using Clock = typename AppContext::Clock; + +struct TimeSnapshot { + bool hasWallTime = false; + int hour24 = 0; + int minute = 0; + int second = 0; + int year = 0; + int month = 0; + int day = 0; + int weekday = 0; + std::uint64_t uptimeSeconds = 0; +}; + +class LockscreenApp final : public cardboy::sdk::IApp { +public: + explicit LockscreenApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {} + + void onStart() override { + cancelRefreshTimer(); + lastSnapshot = {}; + dirty = true; + const auto snap = captureTime(); + renderIfNeeded(snap); + lastSnapshot = snap; + refreshTimer = context.scheduleRepeatingTimer(kRefreshIntervalMs); + } + + void onStop() override { cancelRefreshTimer(); } + + void handleEvent(const cardboy::sdk::AppEvent& event) override { + switch (event.type) { + case cardboy::sdk::AppEventType::Button: + if (anyNewPress(event.button)) + context.requestAppSwitchByName(kMenuAppName); + break; + case cardboy::sdk::AppEventType::Timer: + if (event.timer.handle == refreshTimer) + updateDisplay(); + break; + } + } + +private: + AppContext& context; + Framebuffer& framebuffer; + Clock& clock; + + bool dirty = false; + cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer; + TimeSnapshot lastSnapshot{}; + + void cancelRefreshTimer() { + if (refreshTimer != cardboy::sdk::kInvalidAppTimer) { + context.cancelTimer(refreshTimer); + refreshTimer = cardboy::sdk::kInvalidAppTimer; + } + } + + static bool anyNewPress(const cardboy::sdk::AppButtonEvent& button) { + const auto& current = button.current; + const auto& previous = button.previous; + return (current.a && !previous.a) || (current.b && !previous.b) || (current.start && !previous.start) || + (current.select && !previous.select) || (current.up && !previous.up) || (current.down && !previous.down) || + (current.left && !previous.left) || (current.right && !previous.right); + } + + void updateDisplay() { + const auto snap = captureTime(); + if (!sameSnapshot(snap, lastSnapshot)) + dirty = true; + renderIfNeeded(snap); + lastSnapshot = snap; + } + + static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) { + return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && + a.second == b.second && a.day == b.day && a.month == b.month && a.year == b.year; + } + + TimeSnapshot captureTime() const { + TimeSnapshot snap{}; + snap.uptimeSeconds = clock.millis() / 1000ULL; + + std::time_t raw = 0; + if (std::time(&raw) != static_cast(-1) && raw > 0) { + std::tm tm{}; + if (localtime_r(&raw, &tm) != nullptr) { + snap.hasWallTime = true; + snap.hour24 = tm.tm_hour; + snap.minute = tm.tm_min; + snap.second = tm.tm_sec; + snap.year = tm.tm_year + 1900; + snap.month = tm.tm_mon + 1; + snap.day = tm.tm_mday; + snap.weekday = tm.tm_wday; + return snap; + } + } + + snap.hasWallTime = false; + snap.hour24 = static_cast((snap.uptimeSeconds / 3600ULL) % 24ULL); + snap.minute = static_cast((snap.uptimeSeconds / 60ULL) % 60ULL); + snap.second = static_cast(snap.uptimeSeconds % 60ULL); + return snap; + } + + static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) { + const int width = font16x8::measureText(text, scale, letterSpacing); + const int x = (fb.width() - width) / 2; + font16x8::drawText(fb, x, y, text, scale, true, letterSpacing); + } + + static std::string formatDate(const TimeSnapshot& snap) { + static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + if (!snap.hasWallTime) + return "UPTIME MODE"; + const char* weekday = (snap.weekday >= 0 && snap.weekday < 7) ? kWeekdays[snap.weekday] : ""; + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%s %04d-%02d-%02d", weekday, snap.year, snap.month, snap.day); + return buffer; + } + + void renderIfNeeded(const TimeSnapshot& snap) { + if (!dirty) + return; + dirty = false; + + framebuffer.frameReady(); + + const int scaleTime = 4; + const int scaleSeconds = 2; + const int scaleSmall = 1; + + char hoursMinutes[6]; + std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute); + const int mainW = font16x8::measureText(hoursMinutes, scaleTime, 0); + const int timeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8; + const int timeX = (framebuffer.width() - mainW) / 2; + const int secX = timeX + mainW + 12; + const int secY = timeY + font16x8::kGlyphHeight * scaleTime - font16x8::kGlyphHeight * scaleSeconds; + char secs[3]; + std::snprintf(secs, sizeof(secs), "%02d", snap.second); + + font16x8::drawText(framebuffer, timeX, timeY, hoursMinutes, scaleTime, true, 0); + font16x8::drawText(framebuffer, secX, secY, secs, scaleSeconds, true, 0); + + const std::string dateLine = formatDate(snap); + drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 24, dateLine, scaleSmall, 1); + drawCenteredText(framebuffer, framebuffer.height() - 40, "PRESS ANY BUTTON", scaleSmall, 1); + + framebuffer.sendFrame(); + } +}; + +class LockscreenAppFactory final : public cardboy::sdk::IAppFactory { +public: + const char* name() const override { return kLockscreenAppName; } + std::unique_ptr create(cardboy::sdk::AppContext& context) override { + return std::make_unique(context); + } +}; + +} // namespace + +std::unique_ptr createLockscreenAppFactory() { + return std::make_unique(); +} + +} // namespace apps diff --git a/Firmware/sdk/apps/menu/src/menu_app.cpp b/Firmware/sdk/apps/menu/src/menu_app.cpp index b68151e..aa06f7b 100644 --- a/Firmware/sdk/apps/menu/src/menu_app.cpp +++ b/Firmware/sdk/apps/menu/src/menu_app.cpp @@ -1,4 +1,5 @@ #include "cardboy/apps/menu_app.hpp" +#include "cardboy/apps/lockscreen_app.hpp" #include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_system.hpp" @@ -6,6 +7,7 @@ #include "cardboy/gfx/font16x8.hpp" #include +#include #include #include #include @@ -19,6 +21,8 @@ using cardboy::sdk::AppContext; using Framebuffer = typename AppContext::Framebuffer; +constexpr std::uint32_t kIdleTimeoutMs = 15000; + struct MenuEntry { std::string name; std::size_t index = 0; @@ -31,15 +35,46 @@ public: void onStart() override { refreshEntries(); dirty = true; + resetInactivityTimer(); renderIfNeeded(); } - void handleEvent(const cardboy::sdk::AppEvent& event) override { - if (event.type != cardboy::sdk::AppEventType::Button) - return; + void onStop() override { cancelInactivityTimer(); } - const auto& current = event.button.current; - const auto& previous = event.button.previous; + void handleEvent(const cardboy::sdk::AppEvent& event) override { + switch (event.type) { + case cardboy::sdk::AppEventType::Button: + handleButtonEvent(event.button); + break; + case cardboy::sdk::AppEventType::Timer: + if (event.timer.handle == inactivityTimer) { + cancelInactivityTimer(); + context.requestAppSwitchByName(kLockscreenAppName); + } + break; + } + } + +private: + AppContext& context; + Framebuffer& framebuffer; + std::vector entries; + std::size_t selected = 0; + + bool dirty = false; + + cardboy::sdk::AppTimerHandle inactivityTimer = cardboy::sdk::kInvalidAppTimer; + + void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) { + resetInactivityTimer(); + + const auto& current = button.current; + const auto& previous = button.previous; + + if (current.b && !previous.b) { + context.requestAppSwitchByName(kLockscreenAppName); + return; + } if (current.left && !previous.left) { moveSelection(-1); @@ -54,14 +89,6 @@ public: renderIfNeeded(); } -private: - AppContext& context; - Framebuffer& framebuffer; - std::vector entries; - std::size_t selected = 0; - - bool dirty = false; - void moveSelection(int step) { if (entries.empty()) return; @@ -93,7 +120,8 @@ private: const char* name = factory->name(); if (!name) continue; - if (std::string_view(name) == kMenuAppNameView) + const std::string_view appName(name); + if (appName == kMenuAppNameView || appName == kLockscreenAppNameView) continue; entries.push_back(MenuEntry{std::string(name), i}); } @@ -159,6 +187,18 @@ private: framebuffer.sendFrame(); } + + void cancelInactivityTimer() { + if (inactivityTimer != cardboy::sdk::kInvalidAppTimer) { + context.cancelTimer(inactivityTimer); + inactivityTimer = cardboy::sdk::kInvalidAppTimer; + } + } + + void resetInactivityTimer() { + cancelInactivityTimer(); + inactivityTimer = context.scheduleTimer(kIdleTimeoutMs); + } }; class MenuAppFactory final : public cardboy::sdk::IAppFactory { diff --git a/Firmware/sdk/launchers/desktop/src/main.cpp b/Firmware/sdk/launchers/desktop/src/main.cpp index cf02b94..ba4f48f 100644 --- a/Firmware/sdk/launchers/desktop/src/main.cpp +++ b/Firmware/sdk/launchers/desktop/src/main.cpp @@ -1,6 +1,7 @@ #include "cardboy/apps/clock_app.hpp" #include "cardboy/apps/gameboy_app.hpp" #include "cardboy/apps/menu_app.hpp" +#include "cardboy/apps/lockscreen_app.hpp" #include "cardboy/apps/settings_app.hpp" #include "cardboy/apps/snake_app.hpp" #include "cardboy/apps/tetris_app.hpp" @@ -27,6 +28,7 @@ int main() { buzzer->setMuted(persistentSettings.mute); system.registerApp(apps::createMenuAppFactory()); + system.registerApp(apps::createLockscreenAppFactory()); system.registerApp(apps::createSettingsAppFactory()); system.registerApp(apps::createClockAppFactory()); system.registerApp(apps::createGameboyAppFactory());