From 278e822600011967d28445f8e2fbd9acd419eee5 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sat, 25 Oct 2025 13:52:50 +0200 Subject: [PATCH] faster timeout for games --- .../backend-esp/src/esp_backend.cpp | 11 +- Firmware/sdk/apps/clock/src/clock_app.cpp | 16 +- Firmware/sdk/apps/gameboy/src/gameboy_app.cpp | 52 ++----- .../apps/lockscreen/src/lockscreen_app.cpp | 143 +++++++++--------- Firmware/sdk/apps/menu/src/menu_app.cpp | 20 +-- .../sdk/apps/settings/src/settings_app.cpp | 62 ++++---- Firmware/sdk/apps/snake/src/snake_app.cpp | 40 ++--- Firmware/sdk/apps/tetris/src/tetris_app.cpp | 15 +- .../include/cardboy/sdk/app_events.hpp | 8 +- .../include/cardboy/sdk/services.hpp | 5 +- .../include/cardboy/sdk/app_framework.hpp | 7 +- .../core/include/cardboy/sdk/app_system.hpp | 1 + Firmware/sdk/core/src/app_system.cpp | 13 +- 13 files changed, 197 insertions(+), 196 deletions(-) diff --git a/Firmware/components/backend-esp/src/esp_backend.cpp b/Firmware/components/backend-esp/src/esp_backend.cpp index d7ceede..29eb031 100644 --- a/Firmware/components/backend-esp/src/esp_backend.cpp +++ b/Firmware/components/backend-esp/src/esp_backend.cpp @@ -144,11 +144,14 @@ public: } ~EventBus() override { vQueueDelete(_queueHandle); } - void post(const sdk::AppEvent& event) override { xQueueSendToBack(_queueHandle, &event, portMAX_DELAY); } - sdk::AppEvent pop() override { + void post(const sdk::AppEvent& event) override { xQueueSendToBack(_queueHandle, &event, portMAX_DELAY); } + std::optional pop(std::optional timeout_ms = std::nullopt) override { sdk::AppEvent out; - xQueueReceive(_queueHandle, &out, portMAX_DELAY); - return out; + TickType_t ticks = timeout_ms ? pdMS_TO_TICKS(*timeout_ms) : portMAX_DELAY; + if (xQueueReceive(_queueHandle, &out, ticks) == pdTRUE) { + return out; + } + return std::nullopt; } private: diff --git a/Firmware/sdk/apps/clock/src/clock_app.cpp b/Firmware/sdk/apps/clock/src/clock_app.cpp index ed05876..88ab0a7 100644 --- a/Firmware/sdk/apps/clock/src/clock_app.cpp +++ b/Firmware/sdk/apps/clock/src/clock_app.cpp @@ -18,6 +18,7 @@ namespace apps { namespace { using cardboy::sdk::AppButtonEvent; +using cardboy::sdk::AppTimeoutEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppTimerEvent; @@ -55,13 +56,14 @@ public: void onStop() override { cancelRefreshTimer(); } - void handleEvent(const cardboy::sdk::AppEvent& event) override { - event.visit(cardboy::sdk::overload( - [this](const AppButtonEvent& button) { handleButtonEvent(button); }, - [this](const AppTimerEvent& timer) { - if (timer.handle == refreshTimer) - updateDisplay(); - })); + std::optional handleEvent(const cardboy::sdk::AppEvent& event) override { + event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); }, + [this](const AppTimerEvent& timer) { + if (timer.handle == refreshTimer) + updateDisplay(); + }, + [](const AppTimeoutEvent&) { /* ignore */ })); + return std::nullopt; } private: diff --git a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp index ff93de9..ca3feb1 100644 --- a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp @@ -154,6 +154,7 @@ class GameboyApp; using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; +using cardboy::sdk::AppTimeoutEvent; using cardboy::sdk::AppTimerEvent; using cardboy::sdk::AppTimerHandle; using cardboy::sdk::InputState; @@ -224,7 +225,6 @@ public: ::gAudioWriteThunk = &GameboyApp::audioWriteThunk; apu.attach(this); apu.reset(); - cancelTick(); frameDelayCarryUs = 0; GB_PERF_ONLY(perf.resetAll();) prevInput = context.input.readState(); @@ -233,13 +233,12 @@ public: scaleMode = ScaleMode::FullHeightWide; ensureFilesystemReady(); refreshRomList(); - mode = Mode::Browse; - browserDirty = true; - scheduleNextTick(0); + mode = Mode::Browse; + browserDirty = true; + nextTimeoutMs = 0; } void onStop() override { - cancelTick(); frameDelayCarryUs = 0; GB_PERF_ONLY(perf.maybePrintAggregate(true);) unloadRom(); @@ -252,14 +251,9 @@ public: } } - void handleEvent(const AppEvent& event) override { - bool handled = false; + std::optional handleEvent(const AppEvent& event) override { event.visit(cardboy::sdk::overload( - [this, &handled](const AppTimerEvent& timer) { - if (timer.handle != tickTimer) - return; - handled = true; - tickTimer = kInvalidAppTimer; + [this](const AppTimeoutEvent&) { const uint64_t frameStartUs = nowMicros(); performStep(); const uint64_t frameEndUs = nowMicros(); @@ -269,10 +263,10 @@ public: }, [this](const AppButtonEvent&) { frameDelayCarryUs = 0; - scheduleNextTick(0); - })); - if (handled) - return; + nextTimeoutMs = 0; + }, + [](const AppTimerEvent&) { /* ignore */ })); + return nextTimeoutMs; } void performStep() { @@ -1119,9 +1113,9 @@ public: cardboy::sdk::IFilesystem* filesystem = nullptr; cardboy::sdk::IHighResClock* highResClock = nullptr; PerfTracker perf{}; - AppTimerHandle tickTimer = kInvalidAppTimer; int64_t frameDelayCarryUs = 0; - static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms + std::optional nextTimeoutMs; + static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms Mode mode = Mode::Browse; ScaleMode scaleMode = ScaleMode::FullHeightWide; @@ -1155,20 +1149,6 @@ public: uint8_t lastLoud = 0; uint32_t stableFrames = 0; - void cancelTick() { - if (tickTimer == kInvalidAppTimer) - return; - if (auto* timer = context.timer()) - timer->cancelTimer(tickTimer); - tickTimer = kInvalidAppTimer; - } - - void scheduleNextTick(uint32_t delayMs) { - cancelTick(); - if (auto* timer = context.timer()) - tickTimer = timer->scheduleTimer(delayMs, false); - } - uint32_t idleDelayMs() const { return browserDirty ? 50 : 140; } void scheduleAfterFrame(uint64_t elapsedUs) { @@ -1177,17 +1157,17 @@ public: desiredUs += frameDelayCarryUs; if (desiredUs <= 0) { frameDelayCarryUs = desiredUs; - scheduleNextTick(0); + nextTimeoutMs = 0; return; } frameDelayCarryUs = desiredUs % 1000; desiredUs -= frameDelayCarryUs; uint32_t delayMs = static_cast(desiredUs / 1000); - scheduleNextTick(delayMs); + nextTimeoutMs = delayMs; return; } frameDelayCarryUs = 0; - scheduleNextTick(idleDelayMs()); + nextTimeoutMs = idleDelayMs(); } bool ensureFilesystemReady() { @@ -1838,7 +1818,7 @@ public: promptDirty = true; mode = Mode::Prompt; gb.direct.joypad = 0xFF; - scheduleNextTick(0); + // scheduleNextTick(0); } void exitPrompt(Mode nextMode) { diff --git a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp index e844e43..8acfc3c 100644 --- a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp +++ b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp @@ -5,8 +5,8 @@ #include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_system.hpp" -#include #include +#include #include #include #include @@ -18,6 +18,7 @@ namespace apps { namespace { using cardboy::sdk::AppButtonEvent; +using cardboy::sdk::AppTimeoutEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppTimerEvent; @@ -27,19 +28,13 @@ constexpr std::uint32_t kUnlockHoldMs = 1500; using Framebuffer = typename AppContext::Framebuffer; using Clock = typename AppContext::Clock; -constexpr std::array kArrowUpGlyph{0b00011000, 0b00111100, 0b01111110, - 0b11111111, 0b00011000, 0b00011000, - 0b00011000, 0b00011000, 0b00011000, - 0b00011000, 0b00011000, 0b00011000, - 0b00011000, 0b00011000, 0b00000000, - 0b00000000}; +constexpr std::array kArrowUpGlyph{ + 0b00011000, 0b00111100, 0b01111110, 0b11111111, 0b00011000, 0b00011000, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000, 0b00000000}; -constexpr std::array kArrowDownGlyph{0b00000000, 0b00000000, 0b00011000, - 0b00011000, 0b00011000, 0b00011000, - 0b00011000, 0b00011000, 0b00011000, - 0b00011000, 0b00011000, 0b11111111, - 0b01111110, 0b00111100, 0b00011000, - 0b00000000}; +constexpr std::array kArrowDownGlyph{ + 0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b00011000, 0b11111111, 0b01111110, 0b00111100, 0b00011000, 0b00000000}; struct TimeSnapshot { bool hasWallTime = false; @@ -60,10 +55,10 @@ public: void onStart() override { cancelRefreshTimer(); - lastSnapshot = {}; - holdActive = false; - holdProgressMs = 0; - dirty = true; + lastSnapshot = {}; + holdActive = false; + holdProgressMs = 0; + dirty = true; lastNotificationInteractionMs = clock.millis(); refreshNotifications(); const auto snap = captureTime(); @@ -75,33 +70,34 @@ public: void onStop() override { cancelRefreshTimer(); } - void handleEvent(const cardboy::sdk::AppEvent& event) override { - event.visit(cardboy::sdk::overload( - [this](const AppButtonEvent& button) { handleButtonEvent(button); }, - [this](const AppTimerEvent& timer) { - if (timer.handle == refreshTimer) { - advanceHoldProgress(); - updateDisplay(); - } - })); + std::optional handleEvent(const cardboy::sdk::AppEvent& event) override { + event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); }, + [this](const AppTimerEvent& timer) { + if (timer.handle == refreshTimer) { + advanceHoldProgress(); + updateDisplay(); + } + }, + [](const AppTimeoutEvent&) { /* ignore */ })); + return std::nullopt; } private: - static constexpr std::size_t kMaxDisplayedNotifications = 5; - static constexpr std::uint32_t kNotificationHideMs = 8000; - AppContext& context; - Framebuffer& framebuffer; - Clock& clock; - cardboy::sdk::INotificationCenter* notificationCenter = nullptr; - std::uint32_t lastNotificationRevision = 0; + static constexpr std::size_t kMaxDisplayedNotifications = 5; + static constexpr std::uint32_t kNotificationHideMs = 8000; + AppContext& context; + Framebuffer& framebuffer; + Clock& clock; + cardboy::sdk::INotificationCenter* notificationCenter = nullptr; + std::uint32_t lastNotificationRevision = 0; std::vector notifications; - std::size_t selectedNotification = 0; + std::size_t selectedNotification = 0; bool dirty = false; cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer; TimeSnapshot lastSnapshot{}; - bool holdActive = false; - std::uint32_t holdProgressMs = 0; + bool holdActive = false; + std::uint32_t holdProgressMs = 0; std::uint32_t lastNotificationInteractionMs = 0; void cancelRefreshTimer() { @@ -120,7 +116,7 @@ private: bool navPressed = false; if (!notifications.empty() && (upPressed || downPressed)) { - const std::size_t count = notifications.size(); + const std::size_t count = notifications.size(); lastNotificationInteractionMs = clock.millis(); navPressed = true; if (count > 1) { @@ -133,8 +129,8 @@ private: const bool deletePressed = button.current.b && !button.previous.b; if (deletePressed && notificationCenter && !notifications.empty()) { - const std::size_t index = std::min(selectedNotification, notifications.size() - 1); - const auto& note = notifications[index]; + const std::size_t index = std::min(selectedNotification, notifications.size() - 1); + const auto& note = notifications[index]; std::size_t preferredIndex = index; if (index + 1 < notifications.size()) preferredIndex = index + 1; @@ -144,9 +140,9 @@ private: notificationCenter->removeByExternalId(note.externalId); else notificationCenter->removeById(note.id); - selectedNotification = preferredIndex; + selectedNotification = preferredIndex; lastNotificationInteractionMs = clock.millis(); - dirty = true; + dirty = true; refreshNotifications(); } @@ -161,9 +157,9 @@ private: if (!notificationCenter) { if (!notifications.empty() || lastNotificationRevision != 0) { notifications.clear(); - selectedNotification = 0; - lastNotificationRevision = 0; - dirty = true; + selectedNotification = 0; + lastNotificationRevision = 0; + dirty = true; } return; } @@ -175,7 +171,7 @@ private: const std::uint64_t previousId = (selectedNotification < notifications.size()) ? notifications[selectedNotification].id : 0; - auto latest = notificationCenter->recent(kMaxDisplayedNotifications); + auto latest = notificationCenter->recent(kMaxDisplayedNotifications); notifications = std::move(latest); if (notifications.empty()) { @@ -193,7 +189,7 @@ private: } lastNotificationInteractionMs = clock.millis(); - dirty = true; + dirty = true; } void updateHoldState(bool comboNow) { @@ -213,8 +209,7 @@ private: void advanceHoldProgress() { if (holdActive) { - const std::uint32_t next = - std::min(holdProgressMs + kRefreshIntervalMs, kUnlockHoldMs); + const std::uint32_t next = std::min(holdProgressMs + kRefreshIntervalMs, kUnlockHoldMs); if (next != holdProgressMs) { holdProgressMs = next; dirty = true; @@ -239,8 +234,8 @@ private: } 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; + 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 { @@ -327,7 +322,7 @@ private: if (font16x8::measureText(text, scale, letterSpacing) <= maxWidth) return std::string(text); - std::string result(text.begin(), text.end()); + std::string result(text.begin(), text.end()); const std::string ellipsis = "..."; while (!result.empty()) { result.pop_back(); @@ -369,8 +364,7 @@ private: if (!word.empty()) { std::string candidate = current.empty() ? word : current + " " + word; - if (!current.empty() && - font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) { + if (!current.empty() && font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) { flushCurrent(); if (lines.size() >= static_cast(maxLines)) { truncated = true; @@ -432,20 +426,20 @@ private: framebuffer.frameReady(); - const int scaleTime = 4; - const int scaleSeconds = 2; - const int scaleSmall = 1; + const int scaleTime = 4; + const int scaleSeconds = 2; + const int scaleSmall = 1; const int textLineHeight = font16x8::kGlyphHeight * scaleSmall; - const int cardMarginTop = 4; - const int cardMarginSide = 8; - const int cardPadding = 6; + const int cardMarginTop = 4; + const int cardMarginSide = 8; + const int cardPadding = 6; const int cardLineSpacing = 4; - int cardHeight = 0; - const int cardWidth = framebuffer.width() - cardMarginSide * 2; + int cardHeight = 0; + const int cardWidth = framebuffer.width() - cardMarginSide * 2; - const std::uint32_t nowMs = clock.millis(); - const bool hasNotifications = !notifications.empty(); + const std::uint32_t nowMs = clock.millis(); + const bool hasNotifications = !notifications.empty(); const bool showNotificationDetails = hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs); @@ -481,7 +475,8 @@ private: std::snprintf(counter, sizeof(counter), "%zu/%zu", selectedNotification + 1, notifications.size()); const int counterWidth = font16x8::measureText(counter, scaleSmall, 1); const int counterX = cardMarginSide + cardWidth - cardPadding - counterWidth; - font16x8::drawText(framebuffer, counterX, cardMarginTop + cardPadding, counter, scaleSmall, true, 1); + font16x8::drawText(framebuffer, counterX, cardMarginTop + cardPadding, counter, scaleSmall, true, + 1); const int arrowWidth = font16x8::kGlyphWidth * scaleSmall; const int arrowSpacing = std::max(1, scaleSmall); const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing; @@ -495,7 +490,7 @@ private: if (!bodyLines.empty()) { int bodyY = cardMarginTop + cardPadding + textLineHeight + cardLineSpacing; - for (const auto& line : bodyLines) { + for (const auto& line: bodyLines) { font16x8::drawText(framebuffer, cardMarginSide + cardPadding, bodyY, line, scaleSmall, true, 1); bodyY += textLineHeight + cardLineSpacing; } @@ -531,9 +526,8 @@ private: if (cardHeight > 0) timeY = cardMarginTop + cardHeight + 16; const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16; - const int maxTimeY = - std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48); - timeY = std::clamp(timeY, minTimeY, maxTimeY); + const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48); + timeY = std::clamp(timeY, minTimeY, maxTimeY); char hoursMinutes[6]; std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute); @@ -541,7 +535,7 @@ private: 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]; + char secs[3]; std::snprintf(secs, sizeof(secs), "%02d", snap.second); font16x8::drawText(framebuffer, timeX, timeY, hoursMinutes, scaleTime, true, 0); @@ -550,7 +544,7 @@ private: const std::string dateLine = formatDate(snap); drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 16, dateLine, scaleSmall, 1); - const std::string instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT"; + const std::string instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT"; const int instructionWidth = font16x8::measureText(instruction, scaleSmall, 1); const int barHeight = 14; const int barY = framebuffer.height() - 24; @@ -568,11 +562,10 @@ private: drawRectOutline(framebuffer, barX, barY, barWidth, barHeight); if (holdActive || holdProgressMs > 0) { - const int innerWidth = barWidth - 2; - const int innerHeight = barHeight - 2; - const float ratio = - std::clamp(holdProgressMs / static_cast(kUnlockHoldMs), 0.0f, 1.0f); - const int fillWidth = static_cast(ratio * innerWidth + 0.5f); + const int innerWidth = barWidth - 2; + const int innerHeight = barHeight - 2; + const float ratio = std::clamp(holdProgressMs / static_cast(kUnlockHoldMs), 0.0f, 1.0f); + const int fillWidth = static_cast(ratio * innerWidth + 0.5f); if (fillWidth > 0) fillRect(framebuffer, barX + 1, barY + 1, fillWidth, innerHeight); } diff --git a/Firmware/sdk/apps/menu/src/menu_app.cpp b/Firmware/sdk/apps/menu/src/menu_app.cpp index f7e3f5f..67b8a67 100644 --- a/Firmware/sdk/apps/menu/src/menu_app.cpp +++ b/Firmware/sdk/apps/menu/src/menu_app.cpp @@ -18,6 +18,7 @@ namespace apps { namespace { using cardboy::sdk::AppButtonEvent; +using cardboy::sdk::AppTimeoutEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; using cardboy::sdk::AppTimerEvent; @@ -44,15 +45,16 @@ public: void onStop() override { cancelInactivityTimer(); } - void handleEvent(const cardboy::sdk::AppEvent& event) override { - event.visit(cardboy::sdk::overload( - [this](const AppButtonEvent& button) { handleButtonEvent(button); }, - [this](const AppTimerEvent& timer) { - if (timer.handle == inactivityTimer) { - cancelInactivityTimer(); - context.requestAppSwitchByName(kLockscreenAppName); - } - })); + std::optional handleEvent(const cardboy::sdk::AppEvent& event) override { + event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); }, + [this](const AppTimerEvent& timer) { + if (timer.handle == inactivityTimer) { + cancelInactivityTimer(); + context.requestAppSwitchByName(kLockscreenAppName); + } + }, + [](const AppTimeoutEvent&) { /* ignore */ })); + return std::nullopt; } private: diff --git a/Firmware/sdk/apps/settings/src/settings_app.cpp b/Firmware/sdk/apps/settings/src/settings_app.cpp index 204bf6d..72cc66e 100644 --- a/Firmware/sdk/apps/settings/src/settings_app.cpp +++ b/Firmware/sdk/apps/settings/src/settings_app.cpp @@ -38,42 +38,44 @@ public: renderIfNeeded(); } - void handleEvent(const cardboy::sdk::AppEvent& event) override { - const auto* buttonEvent = event.button(); - if (!buttonEvent) - return; + std::optional handleEvent(const cardboy::sdk::AppEvent& event) override { + event.visit(cardboy::sdk::overload( + [this](const cardboy::sdk::AppButtonEvent& button) { + const auto& current = button.current; + const auto& previous = button.previous; - const auto& current = buttonEvent->current; - const auto& previous = buttonEvent->previous; + const bool previousAvailable = buzzerAvailable; + syncBuzzerState(); + if (previousAvailable != buzzerAvailable) + dirty = true; - const bool previousAvailable = buzzerAvailable; - syncBuzzerState(); - if (previousAvailable != buzzerAvailable) - dirty = true; + if (current.b && !previous.b) { + context.requestAppSwitchByName(kMenuAppName); + return; + } - if (current.b && !previous.b) { - context.requestAppSwitchByName(kMenuAppName); - return; - } + bool moved = false; + if (current.down && !previous.down) { + moveSelection(+1); + moved = true; + } else if (current.up && !previous.up) { + moveSelection(-1); + moved = true; + } - bool moved = false; - if (current.down && !previous.down) { - moveSelection(+1); - moved = true; - } else if (current.up && !previous.up) { - moveSelection(-1); - moved = true; - } + const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) || + (current.select && !previous.select); + if (togglePressed) + handleToggle(); - const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) || - (current.select && !previous.select); - if (togglePressed) - handleToggle(); + if (moved) + dirty = true; - if (moved) - dirty = true; - - renderIfNeeded(); + renderIfNeeded(); + }, + [](const cardboy::sdk::AppTimerEvent&) { /* ignore */ }, + [](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ })); + return std::nullopt; } private: diff --git a/Firmware/sdk/apps/snake/src/snake_app.cpp b/Firmware/sdk/apps/snake/src/snake_app.cpp index 12f3e8b..a4d91ed 100644 --- a/Firmware/sdk/apps/snake/src/snake_app.cpp +++ b/Firmware/sdk/apps/snake/src/snake_app.cpp @@ -20,6 +20,7 @@ namespace apps { namespace { using cardboy::sdk::AppButtonEvent; +using cardboy::sdk::AppTimeoutEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; using cardboy::sdk::AppTimerEvent; @@ -28,13 +29,13 @@ using cardboy::sdk::InputState; constexpr char kSnakeAppName[] = "Snake"; -constexpr int kBoardWidth = 32; -constexpr int kBoardHeight = 20; -constexpr int kCellSize = 10; -constexpr int kInitialSnakeLength = 5; -constexpr int kScorePerFood = 10; -constexpr int kMinMoveIntervalMs = 80; -constexpr int kBaseMoveIntervalMs = 220; +constexpr int kBoardWidth = 32; +constexpr int kBoardHeight = 20; +constexpr int kCellSize = 10; +constexpr int kInitialSnakeLength = 5; +constexpr int kScorePerFood = 10; +constexpr int kMinMoveIntervalMs = 80; +constexpr int kBaseMoveIntervalMs = 220; constexpr int kIntervalSpeedupPerSegment = 4; struct Point { @@ -69,11 +70,12 @@ public: void onStop() { cancelMoveTimer(); } - void handleEvent(const AppEvent& event) { - event.visit(cardboy::sdk::overload( - [this](const AppButtonEvent& button) { handleButtons(button); }, - [this](const AppTimerEvent& timer) { handleTimer(timer.handle); })); + std::optional handleEvent(const AppEvent& event) { + event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtons(button); }, + [this](const AppTimerEvent& timer) { handleTimer(timer.handle); }, + [](const AppTimeoutEvent&) { /* ignore */ })); renderIfNeeded(); + return std::nullopt; } private: @@ -89,8 +91,8 @@ private: bool dirty = false; int score = 0; int highScore = 0; - AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer; - std::mt19937 rng; + AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer; + std::mt19937 rng; void handleButtons(const AppButtonEvent& evt) { const auto& cur = evt.current; @@ -158,7 +160,7 @@ private: } void advance() { - direction = queuedDirection; + direction = queuedDirection; Point nextHead = snake.front(); switch (direction) { case Direction::Up: @@ -299,9 +301,7 @@ private: framebuffer.sendFrame(); } - [[nodiscard]] int boardOriginX() const { - return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2; - } + [[nodiscard]] int boardOriginX() const { return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2; } [[nodiscard]] int boardOriginY() const { const int centered = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2; @@ -406,9 +406,9 @@ class SnakeApp final : public cardboy::sdk::IApp { public: explicit SnakeApp(AppContext& ctx) : game(ctx) {} - void onStart() override { game.onStart(); } - void onStop() override { game.onStop(); } - void handleEvent(const AppEvent& event) override { game.handleEvent(event); } + void onStart() override { game.onStart(); } + void onStop() override { game.onStop(); } + std::optional handleEvent(const AppEvent& event) override { return game.handleEvent(event); } private: SnakeGame game; diff --git a/Firmware/sdk/apps/tetris/src/tetris_app.cpp b/Firmware/sdk/apps/tetris/src/tetris_app.cpp index 1db5b2f..96904c5 100644 --- a/Firmware/sdk/apps/tetris/src/tetris_app.cpp +++ b/Firmware/sdk/apps/tetris/src/tetris_app.cpp @@ -148,11 +148,12 @@ public: void onStop() { cancelTimers(); } - void handleEvent(const AppEvent& event) { - event.visit(cardboy::sdk::overload( - [this](const AppButtonEvent& button) { handleButtons(button); }, - [this](const AppTimerEvent& timer) { handleTimer(timer.handle); })); + std::optional handleEvent(const AppEvent& event) { + event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtons(button); }, + [this](const AppTimerEvent& timer) { handleTimer(timer.handle); }, + [](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ })); renderIfNeeded(); + return std::nullopt; } private: @@ -634,9 +635,9 @@ class TetrisApp final : public cardboy::sdk::IApp { public: explicit TetrisApp(AppContext& ctx) : game(ctx) {} - void onStart() override { game.onStart(); } - void onStop() override { game.onStop(); } - void handleEvent(const AppEvent& event) override { game.handleEvent(event); } + void onStart() override { game.onStart(); } + void onStop() override { game.onStop(); } + std::optional handleEvent(const AppEvent& event) override { return game.handleEvent(event); } private: TetrisGame game; diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp index 776ad71..c147f01 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp @@ -21,14 +21,17 @@ struct AppTimerEvent { AppTimerHandle handle = kInvalidAppTimer; }; +struct AppTimeoutEvent {}; + struct AppEvent { - using Data = std::variant; + using Data = std::variant; std::uint32_t timestamp_ms = 0; Data data{AppButtonEvent{}}; [[nodiscard]] bool isButton() const { return std::holds_alternative(data); } [[nodiscard]] bool isTimer() const { return std::holds_alternative(data); } + [[nodiscard]] bool isTimeout() const { return std::holds_alternative(data); } [[nodiscard]] const AppButtonEvent* button() const { return std::get_if(&data); } [[nodiscard]] AppButtonEvent* button() { return std::get_if(&data); } @@ -36,6 +39,9 @@ struct AppEvent { [[nodiscard]] const AppTimerEvent* timer() const { return std::get_if(&data); } [[nodiscard]] AppTimerEvent* timer() { return std::get_if(&data); } + [[nodiscard]] const AppTimeoutEvent* timeout() const { return std::get_if(&data); } + [[nodiscard]] AppTimeoutEvent* timeout() { return std::get_if(&data); } + template decltype(auto) visit(Visitor&& visitor) { return std::visit(std::forward(visitor), data); diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp index 7ced296..1f38d96 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -97,8 +98,8 @@ class IEventBus { public: virtual ~IEventBus() = default; - virtual void post(const AppEvent& event) = 0; - virtual AppEvent pop() = 0; + virtual void post(const AppEvent& event) = 0; + virtual std::optional pop(std::optional timeout_ms = std::nullopt) = 0; }; struct AppScopedServices { diff --git a/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp b/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp index 6c02885..01a3600 100644 --- a/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp +++ b/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -76,9 +77,9 @@ private: class IApp { public: virtual ~IApp() = default; - virtual void onStart() {} - virtual void onStop() {} - virtual void handleEvent(const AppEvent& event) = 0; + virtual void onStart() {} + virtual void onStop() {} + virtual std::optional handleEvent(const AppEvent& event) = 0; }; class IAppFactory { diff --git a/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp b/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp index 7c85e9b..23bbeb9 100644 --- a/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp +++ b/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp @@ -39,6 +39,7 @@ private: std::size_t _activeIndex = static_cast(-1); std::unique_ptr _scopedServices; std::uint64_t _nextScopedGeneration = 1; + std::optional _currentTimeout; }; } // namespace cardboy::sdk diff --git a/Firmware/sdk/core/src/app_system.cpp b/Firmware/sdk/core/src/app_system.cpp index 30bd238..06775e7 100644 --- a/Firmware/sdk/core/src/app_system.cpp +++ b/Firmware/sdk/core/src/app_system.cpp @@ -3,6 +3,7 @@ #include "cardboy/sdk/status_bar.hpp" #include +#include #include namespace cardboy::sdk { @@ -66,6 +67,7 @@ void AppSystem::startAppByIndex(std::size_t index) { _context._pendingSwitchByName = false; _context._pendingAppName.clear(); _current = std::move(app); + _currentTimeout = std::nullopt; StatusBar::instance().setServices(_context.services); StatusBar::instance().setCurrentAppName(_activeFactory ? _activeFactory->name() : ""); _current->onStart(); @@ -81,14 +83,21 @@ void AppSystem::run() { if (auto* hooks = _context.loopHooks()) hooks->onLoopIteration(); - auto event = _context.eventBus()->pop(); + AppEvent event; + auto event_opt = _context.eventBus()->pop(_currentTimeout); + if (!event_opt) { + event = AppEvent{_context.clock.millis(), AppTimeoutEvent{}}; + } else { + event = *event_opt; + } + if (const auto* btn = event.button()) { const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(btn->current, btn->previous); if (consumedByStatusToggle) { continue; } } - _current->handleEvent(event); + _currentTimeout = _current->handleEvent(event); if (_context._pendingSwitch) { handlePendingSwitchRequest(); continue;