diff --git a/Firmware/AGENTS.md b/Firmware/AGENTS.md index a010379..84ab561 100644 --- a/Firmware/AGENTS.md +++ b/Firmware/AGENTS.md @@ -1,4 +1,4 @@ To build: -(in zsh) +(in zsh, bash doesn't work) . "$HOME/esp/esp-idf/export.sh" idf.py build \ No newline at end of file diff --git a/Firmware/components/backend-esp/include/cardboy/backend/esp/event_bus.hpp b/Firmware/components/backend-esp/include/cardboy/backend/esp/event_bus.hpp index b7b1430..abb5551 100644 --- a/Firmware/components/backend-esp/include/cardboy/backend/esp/event_bus.hpp +++ b/Firmware/components/backend-esp/include/cardboy/backend/esp/event_bus.hpp @@ -4,7 +4,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" -#include "freertos/timers.h" +#include "freertos/queue.h" namespace cardboy::backend::esp { @@ -16,12 +16,12 @@ public: void signal(std::uint32_t bits) override; void signalFromISR(std::uint32_t bits) override; std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override; - void scheduleTimerSignal(std::uint32_t delay_ms) override; - void cancelTimerSignal() override; + void postEvent(const cardboy::sdk::AppEvent& event) override; + bool popEvent(cardboy::sdk::AppEvent& outEvent) override; private: EventGroupHandle_t group; - TimerHandle_t timer; + QueueHandle_t eventQueue; }; } // namespace cardboy::backend::esp diff --git a/Firmware/components/backend-esp/include/cardboy/backend/esp_backend.hpp b/Firmware/components/backend-esp/include/cardboy/backend/esp_backend.hpp index 230b1ae..b726194 100644 --- a/Firmware/components/backend-esp/include/cardboy/backend/esp_backend.hpp +++ b/Firmware/components/backend-esp/include/cardboy/backend/esp_backend.hpp @@ -62,6 +62,7 @@ private: class FilesystemService; class LoopHooksService; class NotificationService; + class TimerService; std::unique_ptr buzzerService; std::unique_ptr batteryService; @@ -70,6 +71,7 @@ private: std::unique_ptr highResClockService; std::unique_ptr filesystemService; std::unique_ptr eventBus; + std::unique_ptr timerService; std::unique_ptr loopHooksService; std::unique_ptr notificationService; diff --git a/Firmware/components/backend-esp/src/esp_backend.cpp b/Firmware/components/backend-esp/src/esp_backend.cpp index f2c6000..4d0e547 100644 --- a/Firmware/components/backend-esp/src/esp_backend.cpp +++ b/Firmware/components/backend-esp/src/esp_backend.cpp @@ -19,12 +19,15 @@ #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/timers.h" #include "nvs.h" #include "nvs_flash.h" #include #include #include +#include #include #include #include @@ -131,6 +134,63 @@ public: void onLoopIteration() override { vTaskDelay(1); } }; +class EspRuntime::TimerService final : public cardboy::sdk::ITimerService { +public: + explicit TimerService(cardboy::sdk::IEventBus& bus); + ~TimerService() override; + + cardboy::sdk::TimerClientId acquireClient() override; + void releaseClient(cardboy::sdk::TimerClientId clientId) override; + cardboy::sdk::AppTimerHandle scheduleTimer(cardboy::sdk::TimerClientId clientId, std::uint32_t delay_ms, + bool repeat) override; + void cancelTimer(cardboy::sdk::TimerClientId clientId, cardboy::sdk::AppTimerHandle handle) override; + void cancelAllTimers(cardboy::sdk::TimerClientId clientId) override; + +private: + struct TimerRecord { + TimerService* owner = nullptr; + TimerHandle_t timer = nullptr; + cardboy::sdk::TimerClientId clientId = cardboy::sdk::kInvalidTimerClient; + cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer; + bool repeat = false; + bool active = true; + }; + + class RecursiveLock { + public: + explicit RecursiveLock(SemaphoreHandle_t m) : mutex(m) { lock(); } + ~RecursiveLock() { unlock(); } + + void lock() { + if (mutex && !locked) { + if (xSemaphoreTakeRecursive(mutex, portMAX_DELAY) == pdTRUE) + locked = true; + } + } + + void unlock() { + if (mutex && locked) { + xSemaphoreGiveRecursive(mutex); + locked = false; + } + } + + private: + SemaphoreHandle_t mutex; + bool locked = false; + }; + + static void timerCallback(TimerHandle_t timer); + void handleTimer(TimerRecord* record); + void shutdown(); + + cardboy::sdk::IEventBus& eventBus; + SemaphoreHandle_t mutex = nullptr; + std::list timers; + cardboy::sdk::AppTimerHandle nextTimerHandle = 1; + cardboy::sdk::TimerClientId nextClientId = 1; +}; + class EspRuntime::NotificationService final : public cardboy::sdk::INotificationCenter { public: void pushNotification(Notification notification) override { @@ -247,6 +307,216 @@ private: std::uint32_t revisionCounter = 0; }; +EspRuntime::TimerService::TimerService(cardboy::sdk::IEventBus& bus) : eventBus(bus) { + mutex = xSemaphoreCreateRecursiveMutex(); +} + +EspRuntime::TimerService::~TimerService() { + shutdown(); + if (mutex) + vSemaphoreDelete(mutex); +} + +cardboy::sdk::TimerClientId EspRuntime::TimerService::acquireClient() { + if (!mutex) + return cardboy::sdk::kInvalidTimerClient; + RecursiveLock lock(mutex); + cardboy::sdk::TimerClientId id = cardboy::sdk::kInvalidTimerClient; + do { + id = nextClientId++; + } while (id == cardboy::sdk::kInvalidTimerClient); + if (nextClientId == cardboy::sdk::kInvalidTimerClient) + ++nextClientId; + return id; +} + +void EspRuntime::TimerService::releaseClient(cardboy::sdk::TimerClientId clientId) { cancelAllTimers(clientId); } + +cardboy::sdk::AppTimerHandle EspRuntime::TimerService::scheduleTimer(cardboy::sdk::TimerClientId clientId, + std::uint32_t delay_ms, bool repeat) { + if (!mutex || clientId == cardboy::sdk::kInvalidTimerClient) + return cardboy::sdk::kInvalidAppTimer; + + TimerRecord* storedRecord = nullptr; + { + RecursiveLock lock(mutex); + TimerRecord record{}; + record.owner = this; + record.clientId = clientId; + record.repeat = repeat; + record.active = true; + cardboy::sdk::AppTimerHandle newHandle = cardboy::sdk::kInvalidAppTimer; + do { + newHandle = nextTimerHandle++; + } while (newHandle == cardboy::sdk::kInvalidAppTimer); + if (nextTimerHandle == cardboy::sdk::kInvalidAppTimer) + ++nextTimerHandle; + record.handle = newHandle; + timers.emplace_back(record); + storedRecord = &timers.back(); + } + + const std::uint32_t effectiveDelay = std::max(1, delay_ms); + const TickType_t ticks = std::max(pdMS_TO_TICKS(effectiveDelay), 1); + + TimerHandle_t timerHandle = + xTimerCreate("AppSvcTimer", ticks, repeat ? pdTRUE : pdFALSE, storedRecord, &TimerService::timerCallback); + if (!timerHandle) { + RecursiveLock lock(mutex); + for (auto it = timers.begin(); it != timers.end(); ++it) { + if (&(*it) == storedRecord) { + timers.erase(it); + break; + } + } + return cardboy::sdk::kInvalidAppTimer; + } + + { + RecursiveLock lock(mutex); + storedRecord->timer = timerHandle; + } + + if (xTimerStart(timerHandle, 0) != pdPASS) { + cancelTimer(clientId, storedRecord->handle); + return cardboy::sdk::kInvalidAppTimer; + } + + return storedRecord->handle; +} + +void EspRuntime::TimerService::cancelTimer(cardboy::sdk::TimerClientId clientId, + cardboy::sdk::AppTimerHandle handle) { + if (!mutex || clientId == cardboy::sdk::kInvalidTimerClient || handle == cardboy::sdk::kInvalidAppTimer) + return; + + TimerHandle_t timerHandle = nullptr; + { + RecursiveLock lock(mutex); + for (auto& record: timers) { + if (record.clientId == clientId && record.handle == handle) { + record.active = false; + timerHandle = record.timer; + break; + } + } + } + + if (!timerHandle) + return; + + xTimerStop(timerHandle, portMAX_DELAY); + xTimerDelete(timerHandle, portMAX_DELAY); + + { + RecursiveLock lock(mutex); + for (auto it = timers.begin(); it != timers.end();) { + if (it->clientId == clientId && it->handle == handle) + it = timers.erase(it); + else + ++it; + } + } +} + +void EspRuntime::TimerService::cancelAllTimers(cardboy::sdk::TimerClientId clientId) { + if (!mutex || clientId == cardboy::sdk::kInvalidTimerClient) + return; + + std::vector handles; + { + RecursiveLock lock(mutex); + for (auto& record: timers) { + if (record.clientId == clientId) { + record.active = false; + if (record.timer) + handles.push_back(record.timer); + } + } + } + + for (auto timerHandle: handles) { + xTimerStop(timerHandle, portMAX_DELAY); + xTimerDelete(timerHandle, portMAX_DELAY); + } + + { + RecursiveLock lock(mutex); + for (auto it = timers.begin(); it != timers.end();) { + if (it->clientId == clientId) + it = timers.erase(it); + else + ++it; + } + } +} + +void EspRuntime::TimerService::timerCallback(TimerHandle_t timer) { + auto* record = static_cast(pvTimerGetTimerID(timer)); + if (!record || !record->owner) + return; + record->owner->handleTimer(record); +} + +void EspRuntime::TimerService::handleTimer(TimerRecord* record) { + if (!record || !record->active) + return; + + const cardboy::sdk::TimerClientId clientId = record->clientId; + const cardboy::sdk::AppTimerHandle handle = record->handle; + const bool isRepeating = record->repeat; + TimerHandle_t timerHandle = record->timer; + + if (!isRepeating) { + record->active = false; + { + RecursiveLock lock(mutex); + for (auto it = timers.begin(); it != timers.end();) { + if (&(*it) == record) + it = timers.erase(it); + else + ++it; + } + } + if (timerHandle) + xTimerDelete(timerHandle, 0); + } + + cardboy::sdk::AppTimerEvent timerEvent{}; + timerEvent.clientId = clientId; + timerEvent.handle = handle; + + cardboy::sdk::AppEvent event{}; + event.timestamp_ms = static_cast(esp_timer_get_time() / 1000ULL); + event.data = timerEvent; + eventBus.postEvent(event); +} + +void EspRuntime::TimerService::shutdown() { + if (!mutex) + return; + + std::vector handles; + { + RecursiveLock lock(mutex); + for (auto& record: timers) { + record.active = false; + if (record.timer) + handles.push_back(record.timer); + } + } + + for (auto timerHandle: handles) { + xTimerStop(timerHandle, portMAX_DELAY); + xTimerDelete(timerHandle, portMAX_DELAY); + } + + { + RecursiveLock lock(mutex); + timers.clear(); + } +} + EspRuntime::EspRuntime() : framebuffer(), input(), clock() { initializeHardware(); @@ -257,6 +527,7 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() { highResClockService = std::make_unique(); filesystemService = std::make_unique(); eventBus = std::make_unique(); + timerService = std::make_unique(*eventBus); loopHooksService = std::make_unique(); notificationService = std::make_unique(); @@ -267,6 +538,7 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() { services.highResClock = highResClockService.get(); services.filesystem = filesystemService.get(); services.eventBus = eventBus.get(); + services.timer = timerService.get(); services.loopHooks = loopHooksService.get(); services.notifications = notificationService.get(); diff --git a/Firmware/components/backend-esp/src/event_bus.cpp b/Firmware/components/backend-esp/src/event_bus.cpp index 54171cd..b434be3 100644 --- a/Firmware/components/backend-esp/src/event_bus.cpp +++ b/Firmware/components/backend-esp/src/event_bus.cpp @@ -14,22 +14,19 @@ namespace { return portMAX_DELAY; return pdMS_TO_TICKS(timeout_ms); } + +constexpr UBaseType_t kEventQueueLength = 16; } // namespace -static void timerCallback(TimerHandle_t handle) { - auto* bus = static_cast(pvTimerGetTimerID(handle)); - if (bus) - bus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer)); -} - EventBus::EventBus() : - group(xEventGroupCreate()), timer(xTimerCreate("EventBusTimer", pdMS_TO_TICKS(1), pdFALSE, this, timerCallback)) {} + group(xEventGroupCreate()), + eventQueue(xQueueCreate(kEventQueueLength, sizeof(cardboy::sdk::AppEvent))) {} EventBus::~EventBus() { - if (timer) - xTimerDelete(timer, portMAX_DELAY); if (group) vEventGroupDelete(group); + if (eventQueue) + vQueueDelete(eventQueue); } void EventBus::signal(std::uint32_t bits) { @@ -54,28 +51,23 @@ std::uint32_t EventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) { return static_cast(bits & mask); } -void EventBus::scheduleTimerSignal(std::uint32_t delay_ms) { - if (!timer) - return; - xTimerStop(timer, 0); - - if (delay_ms == cardboy::sdk::IEventBus::kWaitForever) +void EventBus::postEvent(const cardboy::sdk::AppEvent& event) { + if (!eventQueue) return; - if (delay_ms == 0) { - signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer)); - return; + if (xQueueSend(eventQueue, &event, 0) == errQUEUE_FULL) { + cardboy::sdk::AppEvent discarded{}; + if (xQueueReceive(eventQueue, &discarded, 0) == pdTRUE) + xQueueSend(eventQueue, &event, 0); } - const TickType_t ticks = std::max(pdMS_TO_TICKS(delay_ms), 1); - if (xTimerChangePeriod(timer, ticks, 0) == pdPASS) - xTimerStart(timer, 0); + signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer)); } -void EventBus::cancelTimerSignal() { - if (!timer) - return; - xTimerStop(timer, 0); +bool EventBus::popEvent(cardboy::sdk::AppEvent& outEvent) { + if (!eventQueue) + return false; + return xQueueReceive(eventQueue, &outEvent, 0) == pdTRUE; } } // namespace cardboy::backend::esp diff --git a/Firmware/sdk/apps/clock/src/clock_app.cpp b/Firmware/sdk/apps/clock/src/clock_app.cpp index 0b2d74f..c7b6530 100644 --- a/Firmware/sdk/apps/clock/src/clock_app.cpp +++ b/Firmware/sdk/apps/clock/src/clock_app.cpp @@ -17,7 +17,9 @@ namespace apps { namespace { +using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; +using cardboy::sdk::AppTimerEvent; constexpr const char* kClockAppName = "Clock"; @@ -53,15 +55,12 @@ public: void onStop() override { cancelRefreshTimer(); } 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 == refreshTimer) - updateDisplay(); - break; - } + event.visit(cardboy::sdk::overload( + [this](const AppButtonEvent& button) { handleButtonEvent(button); }, + [this](const AppTimerEvent& timer) { + if (timer.handle == refreshTimer) + updateDisplay(); + })); } private: diff --git a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp index bbb09d8..8965fde 100644 --- a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp @@ -151,9 +151,10 @@ constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6; class GameboyApp; +using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; -using cardboy::sdk::AppEventType; +using cardboy::sdk::AppTimerEvent; using cardboy::sdk::AppTimerHandle; using cardboy::sdk::InputState; using cardboy::sdk::kInvalidAppTimer; @@ -252,20 +253,26 @@ public: } void handleEvent(const AppEvent& event) override { - if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) { - tickTimer = kInvalidAppTimer; - const uint64_t frameStartUs = nowMicros(); - performStep(); - const uint64_t frameEndUs = nowMicros(); - const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0; - GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs)); - scheduleAfterFrame(elapsedUs); + bool handled = false; + event.visit(cardboy::sdk::overload( + [this, &handled](const AppTimerEvent& timer) { + if (timer.handle != tickTimer) + return; + handled = true; + tickTimer = kInvalidAppTimer; + const uint64_t frameStartUs = nowMicros(); + performStep(); + const uint64_t frameEndUs = nowMicros(); + const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0; + GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs)); + scheduleAfterFrame(elapsedUs); + }, + [this](const AppButtonEvent&) { + frameDelayCarryUs = 0; + scheduleNextTick(0); + })); + if (handled) return; - } - if (event.type == AppEventType::Button) { - frameDelayCarryUs = 0; - scheduleNextTick(0); - } } void performStep() { diff --git a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp index 78b7adf..18a039a 100644 --- a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp +++ b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp @@ -17,7 +17,9 @@ namespace apps { namespace { +using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; +using cardboy::sdk::AppTimerEvent; constexpr std::uint32_t kRefreshIntervalMs = 100; constexpr std::uint32_t kUnlockHoldMs = 1500; @@ -73,17 +75,14 @@ public: void onStop() override { cancelRefreshTimer(); } 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 == refreshTimer) { - advanceHoldProgress(); - updateDisplay(); - } - break; - } + event.visit(cardboy::sdk::overload( + [this](const AppButtonEvent& button) { handleButtonEvent(button); }, + [this](const AppTimerEvent& timer) { + if (timer.handle == refreshTimer) { + advanceHoldProgress(); + updateDisplay(); + } + })); } private: diff --git a/Firmware/sdk/apps/menu/src/menu_app.cpp b/Firmware/sdk/apps/menu/src/menu_app.cpp index aa06f7b..0cba807 100644 --- a/Firmware/sdk/apps/menu/src/menu_app.cpp +++ b/Firmware/sdk/apps/menu/src/menu_app.cpp @@ -17,7 +17,10 @@ namespace apps { namespace { +using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; +using cardboy::sdk::AppEvent; +using cardboy::sdk::AppTimerEvent; using Framebuffer = typename AppContext::Framebuffer; @@ -42,17 +45,14 @@ public: void onStop() override { cancelInactivityTimer(); } 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; - } + event.visit(cardboy::sdk::overload( + [this](const AppButtonEvent& button) { handleButtonEvent(button); }, + [this](const AppTimerEvent& timer) { + if (timer.handle == inactivityTimer) { + cancelInactivityTimer(); + context.requestAppSwitchByName(kLockscreenAppName); + } + })); } private: diff --git a/Firmware/sdk/apps/settings/src/settings_app.cpp b/Firmware/sdk/apps/settings/src/settings_app.cpp index badcce2..204bf6d 100644 --- a/Firmware/sdk/apps/settings/src/settings_app.cpp +++ b/Firmware/sdk/apps/settings/src/settings_app.cpp @@ -39,11 +39,12 @@ public: } void handleEvent(const cardboy::sdk::AppEvent& event) override { - if (event.type != cardboy::sdk::AppEventType::Button) + const auto* buttonEvent = event.button(); + if (!buttonEvent) return; - const auto& current = event.button.current; - const auto& previous = event.button.previous; + const auto& current = buttonEvent->current; + const auto& previous = buttonEvent->previous; const bool previousAvailable = buzzerAvailable; syncBuzzerState(); diff --git a/Firmware/sdk/apps/snake/src/snake_app.cpp b/Firmware/sdk/apps/snake/src/snake_app.cpp index aefe5c0..7131063 100644 --- a/Firmware/sdk/apps/snake/src/snake_app.cpp +++ b/Firmware/sdk/apps/snake/src/snake_app.cpp @@ -22,7 +22,7 @@ namespace { using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; -using cardboy::sdk::AppEventType; +using cardboy::sdk::AppTimerEvent; using cardboy::sdk::AppTimerHandle; using cardboy::sdk::InputState; @@ -70,14 +70,9 @@ public: void onStop() { cancelMoveTimer(); } void handleEvent(const AppEvent& event) { - switch (event.type) { - case AppEventType::Button: - handleButtons(event.button); - break; - case AppEventType::Timer: - handleTimer(event.timer.handle); - break; - } + event.visit(cardboy::sdk::overload( + [this](const AppButtonEvent& button) { handleButtons(button); }, + [this](const AppTimerEvent& timer) { handleTimer(timer.handle); })); renderIfNeeded(); } diff --git a/Firmware/sdk/apps/tetris/src/tetris_app.cpp b/Firmware/sdk/apps/tetris/src/tetris_app.cpp index 4522a5b..15352f8 100644 --- a/Firmware/sdk/apps/tetris/src/tetris_app.cpp +++ b/Firmware/sdk/apps/tetris/src/tetris_app.cpp @@ -22,7 +22,7 @@ namespace { using cardboy::sdk::AppButtonEvent; using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; -using cardboy::sdk::AppEventType; +using cardboy::sdk::AppTimerEvent; using cardboy::sdk::AppTimerHandle; using cardboy::sdk::InputState; @@ -149,14 +149,9 @@ public: void onStop() { cancelTimers(); } void handleEvent(const AppEvent& event) { - switch (event.type) { - case AppEventType::Button: - handleButtons(event.button); - break; - case AppEventType::Timer: - handleTimer(event.timer.handle); - break; - } + event.visit(cardboy::sdk::overload( + [this](const AppButtonEvent& button) { handleButtons(button); }, + [this](const AppTimerEvent& timer) { handleTimer(timer.handle); })); renderIfNeeded(); } diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp new file mode 100644 index 0000000..42f7351 --- /dev/null +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/app_events.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "cardboy/sdk/input_state.hpp" + +#include +#include +#include +#include + +namespace cardboy::sdk { + +using AppTimerHandle = std::uint32_t; +constexpr AppTimerHandle kInvalidAppTimer = 0; + +using TimerClientId = std::uint32_t; +constexpr TimerClientId kInvalidTimerClient = 0; + +struct AppButtonEvent { + InputState current{}; + InputState previous{}; +}; + +struct AppTimerEvent { + TimerClientId clientId = kInvalidTimerClient; + AppTimerHandle handle = kInvalidAppTimer; +}; + +struct AppEvent { + 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]] const AppButtonEvent* button() const { return std::get_if(&data); } + [[nodiscard]] AppButtonEvent* button() { return std::get_if(&data); } + + [[nodiscard]] const AppTimerEvent* timer() const { return std::get_if(&data); } + [[nodiscard]] AppTimerEvent* timer() { return std::get_if(&data); } + + template + decltype(auto) visit(Visitor&& visitor) { + return std::visit(std::forward(visitor), data); + } + + template + decltype(auto) visit(Visitor&& visitor) const { + return std::visit(std::forward(visitor), data); + } +}; + +template +struct Overload : Ts... { + using Ts::operator()...; +}; + +template +Overload(Ts...) -> Overload; + +template +constexpr auto overload(Ts&&... ts) { + return Overload...>{std::forward(ts)...}; +} + +} // namespace cardboy::sdk diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/event_bus.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/event_bus.hpp index 4149e38..97b5302 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/event_bus.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/event_bus.hpp @@ -1,5 +1,7 @@ #pragma once +#include "cardboy/sdk/app_events.hpp" + #include namespace cardboy::sdk { @@ -35,8 +37,8 @@ public: virtual void signal(std::uint32_t bits) = 0; virtual void signalFromISR(std::uint32_t bits) = 0; virtual std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) = 0; - virtual void scheduleTimerSignal(std::uint32_t delay_ms) = 0; - virtual void cancelTimerSignal() = 0; + virtual void postEvent(const AppEvent& event) = 0; + virtual bool popEvent(AppEvent& outEvent) = 0; }; } // namespace cardboy::sdk diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp index a435eec..5315012 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp @@ -2,6 +2,7 @@ #include "cardboy/sdk/event_bus.hpp" #include "cardboy/sdk/loop_hooks.hpp" +#include "cardboy/sdk/timer_service.hpp" #include #include @@ -100,6 +101,7 @@ struct Services { IHighResClock* highResClock = nullptr; IFilesystem* filesystem = nullptr; IEventBus* eventBus = nullptr; + ITimerService* timer = nullptr; ILoopHooks* loopHooks = nullptr; INotificationCenter* notifications = nullptr; }; diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/timer_service.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/timer_service.hpp new file mode 100644 index 0000000..90cb6b6 --- /dev/null +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/timer_service.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "cardboy/sdk/app_events.hpp" + +#include + +namespace cardboy::sdk { + +class ITimerService { +public: + virtual ~ITimerService() = default; + + virtual TimerClientId acquireClient() = 0; + virtual void releaseClient(TimerClientId clientId) = 0; + virtual AppTimerHandle scheduleTimer(TimerClientId clientId, std::uint32_t delay_ms, bool repeat) = 0; + virtual void cancelTimer(TimerClientId clientId, AppTimerHandle handle) = 0; + virtual void cancelAllTimers(TimerClientId clientId) = 0; +}; + +} // namespace cardboy::sdk + diff --git a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp index 73d8d4c..5ec2f2e 100644 --- a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp +++ b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -108,24 +109,57 @@ private: class DesktopEventBus final : public cardboy::sdk::IEventBus { public: explicit DesktopEventBus(DesktopRuntime& owner); - ~DesktopEventBus() override; void signal(std::uint32_t bits) override; void signalFromISR(std::uint32_t bits) override; std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override; - void scheduleTimerSignal(std::uint32_t delay_ms) override; - void cancelTimerSignal() override; + void postEvent(const cardboy::sdk::AppEvent& event) override; + bool popEvent(cardboy::sdk::AppEvent& outEvent) override; private: DesktopRuntime& runtime; std::mutex mutex; std::condition_variable cv; std::uint32_t pendingBits = 0; + std::mutex eventMutex; + std::deque eventQueue; +}; - std::mutex timerMutex; - std::condition_variable timerCv; - std::thread timerThread; - bool timerCancel = false; +class DesktopTimerService final : public cardboy::sdk::ITimerService { +public: + DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus); + ~DesktopTimerService() override; + + cardboy::sdk::TimerClientId acquireClient() override; + void releaseClient(cardboy::sdk::TimerClientId clientId) override; + cardboy::sdk::AppTimerHandle scheduleTimer(cardboy::sdk::TimerClientId clientId, std::uint32_t delay_ms, + bool repeat) override; + void cancelTimer(cardboy::sdk::TimerClientId clientId, cardboy::sdk::AppTimerHandle handle) override; + void cancelAllTimers(cardboy::sdk::TimerClientId clientId) override; + +private: + struct TimerRecord { + cardboy::sdk::TimerClientId clientId = cardboy::sdk::kInvalidTimerClient; + cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer; + std::chrono::steady_clock::time_point due; + std::chrono::milliseconds interval{0}; + bool repeat = false; + bool active = true; + }; + + void workerLoop(); + void wakeWorker(); + void cleanupInactive(); + + DesktopRuntime& runtime; + cardboy::sdk::IEventBus& eventBus; + std::mutex mutex; + std::condition_variable cv; + std::vector timers; + bool stopWorker = false; + std::thread worker; + cardboy::sdk::AppTimerHandle nextHandle = 1; + cardboy::sdk::TimerClientId nextClient = 1; }; class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade { @@ -206,6 +240,7 @@ private: DesktopHighResClock highResService; DesktopFilesystem filesystemService; DesktopEventBus eventBusService; + DesktopTimerService timerService; DesktopNotificationCenter notificationService; cardboy::sdk::Services services{}; }; diff --git a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp index cffa411..cc3a35e 100644 --- a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp +++ b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp @@ -15,9 +15,11 @@ namespace cardboy::backend::desktop { -DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {} +namespace { +constexpr std::size_t kDesktopEventQueueLimit = 64; +} // namespace -DesktopEventBus::~DesktopEventBus() { cancelTimerSignal(); } +DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {} void DesktopEventBus::signal(std::uint32_t bits) { if (bits == 0) @@ -61,46 +63,161 @@ std::uint32_t DesktopEventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms } } -void DesktopEventBus::scheduleTimerSignal(std::uint32_t delay_ms) { - cancelTimerSignal(); - - if (delay_ms == cardboy::sdk::IEventBus::kWaitForever) - return; - - if (delay_ms == 0) { - signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer)); - return; - } - +void DesktopEventBus::postEvent(const cardboy::sdk::AppEvent& event) { { - std::lock_guard lock(timerMutex); - timerCancel = false; + std::lock_guard lock(eventMutex); + if (eventQueue.size() >= kDesktopEventQueueLimit) + eventQueue.pop_front(); + eventQueue.push_back(event); } - - timerThread = std::thread([this, delay_ms]() { - std::unique_lock lock(timerMutex); - const bool cancelled = - timerCv.wait_for(lock, std::chrono::milliseconds(delay_ms), [this] { return timerCancel; }); - lock.unlock(); - if (!cancelled) - signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer)); - }); + signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer)); } -void DesktopEventBus::cancelTimerSignal() { +bool DesktopEventBus::popEvent(cardboy::sdk::AppEvent& outEvent) { + std::lock_guard lock(eventMutex); + if (eventQueue.empty()) + return false; + outEvent = eventQueue.front(); + eventQueue.pop_front(); + return true; +} + +DesktopTimerService::DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus) : runtime(owner), eventBus(bus) { + worker = std::thread(&DesktopTimerService::workerLoop, this); +} + +DesktopTimerService::~DesktopTimerService() { { - std::lock_guard lock(timerMutex); - timerCancel = true; + std::lock_guard lock(mutex); + stopWorker = true; } - timerCv.notify_all(); - if (timerThread.joinable()) - timerThread.join(); + cv.notify_all(); + if (worker.joinable()) + worker.join(); +} + +cardboy::sdk::TimerClientId DesktopTimerService::acquireClient() { + std::lock_guard lock(mutex); + cardboy::sdk::TimerClientId id = cardboy::sdk::kInvalidTimerClient; + do { + id = nextClient++; + } while (id == cardboy::sdk::kInvalidTimerClient); + if (nextClient == cardboy::sdk::kInvalidTimerClient) + ++nextClient; + return id; +} + +void DesktopTimerService::releaseClient(cardboy::sdk::TimerClientId clientId) { cancelAllTimers(clientId); } + +cardboy::sdk::AppTimerHandle DesktopTimerService::scheduleTimer(cardboy::sdk::TimerClientId clientId, + std::uint32_t delay_ms, bool repeat) { + if (clientId == cardboy::sdk::kInvalidTimerClient) + return cardboy::sdk::kInvalidAppTimer; + + const auto now = std::chrono::steady_clock::now(); + const auto effectiveDelayMs = std::chrono::milliseconds(delay_ms); + const auto dueTime = delay_ms == 0 ? now : now + effectiveDelayMs; + const auto interval = std::chrono::milliseconds(std::max(1, repeat ? delay_ms : std::max(delay_ms, 1u))); + + TimerRecord record{}; + record.clientId = clientId; + record.repeat = repeat; + record.interval = interval; + record.due = dueTime; + record.active = true; + { - std::lock_guard lock(timerMutex); - timerCancel = false; + std::lock_guard lock(mutex); + cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer; + do { + handle = nextHandle++; + } while (handle == cardboy::sdk::kInvalidAppTimer); + if (nextHandle == cardboy::sdk::kInvalidAppTimer) + ++nextHandle; + record.handle = handle; + timers.push_back(record); + wakeWorker(); + return handle; } } +void DesktopTimerService::cancelTimer(cardboy::sdk::TimerClientId clientId, cardboy::sdk::AppTimerHandle handle) { + if (clientId == cardboy::sdk::kInvalidTimerClient || handle == cardboy::sdk::kInvalidAppTimer) + return; + std::lock_guard lock(mutex); + for (auto& record: timers) { + if (record.clientId == clientId && record.handle == handle) + record.active = false; + } + cleanupInactive(); + wakeWorker(); +} + +void DesktopTimerService::cancelAllTimers(cardboy::sdk::TimerClientId clientId) { + if (clientId == cardboy::sdk::kInvalidTimerClient) + return; + std::lock_guard lock(mutex); + for (auto& record: timers) { + if (record.clientId == clientId) + record.active = false; + } + cleanupInactive(); + wakeWorker(); +} + +void DesktopTimerService::workerLoop() { + std::unique_lock lock(mutex); + while (!stopWorker) { + cleanupInactive(); + if (timers.empty()) { + cv.wait(lock, [this] { return stopWorker || !timers.empty(); }); + continue; + } + + auto nextIt = std::min_element(timers.begin(), timers.end(), [](const TimerRecord& a, const TimerRecord& b) { + return a.due < b.due; + }); + + if (nextIt == timers.end()) + continue; + + if (!nextIt->active) + continue; + + const auto now = std::chrono::steady_clock::now(); + if (now >= nextIt->due) { + TimerRecord record = *nextIt; + if (record.repeat) { + nextIt->due = now + record.interval; + } else { + nextIt->active = false; + } + lock.unlock(); + + cardboy::sdk::AppTimerEvent timerEvent{}; + timerEvent.clientId = record.clientId; + timerEvent.handle = record.handle; + + cardboy::sdk::AppEvent event{}; + event.timestamp_ms = runtime.clock.millis(); + event.data = timerEvent; + eventBus.postEvent(event); + + lock.lock(); + continue; + } + + cv.wait_until(lock, nextIt->due, [this] { return stopWorker; }); + } +} + +void DesktopTimerService::wakeWorker() { cv.notify_all(); } + +void DesktopTimerService::cleanupInactive() { + timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& record) { return !record.active; }), + timers.end()); +} + bool DesktopStorage::readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) { auto it = data.find(composeKey(ns, key)); if (it == data.end()) @@ -324,7 +441,7 @@ DesktopRuntime::DesktopRuntime() : "Cardboy Desktop"), texture(), sprite(texture), pixels(static_cast(cardboy::sdk::kDisplayWidth * cardboy::sdk::kDisplayHeight) * 4, 0), - framebuffer(*this), input(*this), clock(*this), eventBusService(*this) { + framebuffer(*this), input(*this), clock(*this), eventBusService(*this), timerService(*this, eventBusService) { window.setFramerateLimit(60); if (!texture.resize(sf::Vector2u{cardboy::sdk::kDisplayWidth, cardboy::sdk::kDisplayHeight})) throw std::runtime_error("Failed to allocate texture for desktop framebuffer"); @@ -340,6 +457,7 @@ DesktopRuntime::DesktopRuntime() : services.highResClock = &highResService; services.filesystem = &filesystemService; services.eventBus = &eventBusService; + services.timer = &timerService; services.loopHooks = nullptr; services.notifications = ¬ificationService; } diff --git a/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp b/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp index ec0c2cf..f311b9f 100644 --- a/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp +++ b/Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -14,30 +15,6 @@ namespace cardboy::sdk { class AppSystem; -using AppTimerHandle = std::uint32_t; -constexpr AppTimerHandle kInvalidAppTimer = 0; - -enum class AppEventType { - Button, - Timer, -}; - -struct AppButtonEvent { - InputState current{}; - InputState previous{}; -}; - -struct AppTimerEvent { - AppTimerHandle handle = kInvalidAppTimer; -}; - -struct AppEvent { - AppEventType type; - std::uint32_t timestamp_ms = 0; - AppButtonEvent button{}; - AppTimerEvent timer{}; -}; - using ActiveBackend = cardboy::backend::ActiveBackend; struct AppContext { @@ -63,6 +40,7 @@ struct AppContext { [[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; } [[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; } [[nodiscard]] IEventBus* eventBus() const { return services ? services->eventBus : nullptr; } + [[nodiscard]] ITimerService* timerService() const { return services ? services->timer : nullptr; } [[nodiscard]] ILoopHooks* loopHooks() const { return services ? services->loopHooks : nullptr; } [[nodiscard]] INotificationCenter* notificationCenter() const { return services ? services->notifications : nullptr; @@ -84,27 +62,28 @@ struct AppContext { [[nodiscard]] bool hasPendingAppSwitch() const { return pendingSwitch; } AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat = false) { - if (!system) + auto* timer = timerService(); + if (!timer || timerClientId == kInvalidTimerClient) return kInvalidAppTimer; - return scheduleTimerInternal(delay_ms, repeat); + return timer->scheduleTimer(timerClientId, delay_ms, repeat); } AppTimerHandle scheduleRepeatingTimer(std::uint32_t interval_ms) { - if (!system) - return kInvalidAppTimer; - return scheduleTimerInternal(interval_ms, true); + return scheduleTimer(interval_ms, true); } void cancelTimer(AppTimerHandle handle) { - if (!system) + auto* timer = timerService(); + if (!timer || timerClientId == kInvalidTimerClient) return; - cancelTimerInternal(handle); + timer->cancelTimer(timerClientId, handle); } void cancelAllTimers() { - if (!system) + auto* timer = timerService(); + if (!timer || timerClientId == kInvalidTimerClient) return; - cancelAllTimersInternal(); + timer->cancelAllTimers(timerClientId); } private: @@ -113,10 +92,7 @@ private: bool pendingSwitchByName = false; std::size_t pendingAppIndex = 0; std::string pendingAppName; - - AppTimerHandle scheduleTimerInternal(std::uint32_t delay_ms, bool repeat); - void cancelTimerInternal(AppTimerHandle handle); - void cancelAllTimersInternal(); + TimerClientId timerClientId = kInvalidTimerClient; }; class IApp { diff --git a/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp b/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp index f363ee0..791337c 100644 --- a/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp +++ b/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp @@ -32,52 +32,20 @@ public: private: friend struct AppContext; - struct TimerRecord { - AppTimerHandle id = kInvalidAppTimer; - std::uint32_t generation = 0; - std::uint32_t due_ms = 0; - std::uint32_t interval_ms = 0; - bool repeat = false; - bool active = false; - }; - - AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat); - void cancelTimer(AppTimerHandle handle); - void cancelAllTimers(); - void dispatchEvent(const AppEvent& event); - - void processDueTimers(std::uint32_t now, std::vector& outEvents); - std::uint32_t nextTimerDueMs(std::uint32_t now) const; - void clearTimersForCurrentApp(); - TimerRecord* findTimer(AppTimerHandle handle); - bool handlePendingSwitchRequest(); - void notifyEventBus(EventBusSignal signal); + bool handlePendingSwitchRequest(); + void attachTimerClient(); + void detachTimerClient(); + void processEventBusEvents(); AppContext context; std::vector> factories; std::unique_ptr current; IAppFactory* activeFactory = nullptr; std::size_t activeIndex = static_cast(-1); - std::vector timers; - AppTimerHandle nextTimerId = 1; - std::uint32_t currentGeneration = 0; + TimerClientId timerClientId = kInvalidTimerClient; InputState lastInputState{}; bool suppressInputs = false; }; -inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) { - return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer; -} - -inline void AppContext::cancelTimerInternal(AppTimerHandle handle) { - if (system) - system->cancelTimer(handle); -} - -inline void AppContext::cancelAllTimersInternal() { - if (system) - system->cancelAllTimers(); -} - } // namespace cardboy::sdk diff --git a/Firmware/sdk/core/src/app_system.cpp b/Firmware/sdk/core/src/app_system.cpp index a3470a3..9fe0bd2 100644 --- a/Firmware/sdk/core/src/app_system.cpp +++ b/Firmware/sdk/core/src/app_system.cpp @@ -3,7 +3,6 @@ #include "cardboy/sdk/status_bar.hpp" #include -#include #include namespace cardboy::sdk { @@ -34,7 +33,10 @@ AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { FramebufferHooks::setPreSendHook(&statusBarPreSendHook, &statusBar); } -AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); } +AppSystem::~AppSystem() { + detachTimerClient(); + FramebufferHooks::clearPreSendHook(); +} void AppSystem::registerApp(std::unique_ptr factory) { if (!factory) @@ -63,6 +65,7 @@ bool AppSystem::startAppByIndex(std::size_t index) { if (current) { current->onStop(); current.reset(); + detachTimerClient(); } activeFactory = factory.get(); @@ -70,8 +73,8 @@ bool AppSystem::startAppByIndex(std::size_t index) { context.pendingSwitch = false; context.pendingSwitchByName = false; context.pendingAppName.clear(); - clearTimersForCurrentApp(); current = std::move(app); + attachTimerClient(); lastInputState = context.input.readState(); suppressInputs = true; StatusBar::instance().setServices(context.services); @@ -86,17 +89,19 @@ void AppSystem::run() { return; } - std::vector events; - events.reserve(4); - while (true) { + auto* eventBus = context.eventBus(); + if (!eventBus) + return; + if (auto* hooks = context.loopHooks()) hooks->onLoopIteration(); - events.clear(); - const std::uint32_t now = context.clock.millis(); - processDueTimers(now, events); + processEventBusEvents(); + if (handlePendingSwitchRequest()) + continue; + const std::uint32_t now = context.clock.millis(); const InputState inputNow = context.input.readState(); const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState); @@ -105,42 +110,25 @@ void AppSystem::run() { if (!anyButtonPressed(inputNow)) suppressInputs = false; } else if (!consumedByStatusToggle && inputsDiffer(inputNow, lastInputState)) { + AppButtonEvent button{}; + button.current = inputNow; + button.previous = lastInputState; + AppEvent evt{}; - evt.type = AppEventType::Button; - evt.timestamp_ms = now; - evt.button.current = inputNow; - evt.button.previous = lastInputState; - events.push_back(evt); + evt.timestamp_ms = now; + evt.data = button; + lastInputState = inputNow; + dispatchEvent(evt); } else if (consumedByStatusToggle) { lastInputState = inputNow; } - for (const auto& evt: events) { - dispatchEvent(evt); - if (handlePendingSwitchRequest()) - break; - } - - const std::uint32_t waitBase = context.clock.millis(); - std::uint32_t waitMs = nextTimerDueMs(waitBase); - - if (waitMs == 0) + if (handlePendingSwitchRequest()) continue; - auto* eventBus = context.eventBus(); - if (!eventBus) - return; - const std::uint32_t mask = to_event_bits(EventBusSignal::Input) | to_event_bits(EventBusSignal::Timer); - - if (waitMs == std::numeric_limits::max()) { - eventBus->cancelTimerSignal(); - eventBus->wait(mask, IEventBus::kWaitForever); - } else { - eventBus->scheduleTimerSignal(waitMs); - eventBus->wait(mask, IEventBus::kWaitForever); - } + eventBus->wait(mask, IEventBus::kWaitForever); } } @@ -160,105 +148,16 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const { return static_cast(-1); } -AppTimerHandle AppSystem::scheduleTimer(std::uint32_t delay_ms, bool repeat) { - if (!current) - return kInvalidAppTimer; - TimerRecord record; - record.id = nextTimerId++; - if (record.id == kInvalidAppTimer) - record.id = nextTimerId++; - record.generation = currentGeneration; - const auto now = context.clock.millis(); - record.due_ms = now + delay_ms; - record.interval_ms = repeat ? std::max(1, delay_ms) : 0; - record.repeat = repeat; - record.active = true; - timers.push_back(record); - notifyEventBus(EventBusSignal::Timer); - return record.id; -} - -void AppSystem::cancelTimer(AppTimerHandle handle) { - auto* timer = findTimer(handle); - if (!timer) - return; - timer->active = false; - timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }), - timers.end()); - notifyEventBus(EventBusSignal::Timer); -} - -void AppSystem::cancelAllTimers() { - bool changed = false; - for (auto& timer: timers) { - if (timer.generation == currentGeneration && timer.active) { - timer.active = false; - changed = true; - } - } - timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }), - timers.end()); - if (changed) - notifyEventBus(EventBusSignal::Timer); -} - void AppSystem::dispatchEvent(const AppEvent& event) { - if (current) - current->handleEvent(event); -} + if (!current) + return; -void AppSystem::processDueTimers(std::uint32_t now, std::vector& outEvents) { - for (auto& timer: timers) { - if (!timer.active || timer.generation != currentGeneration) - continue; - if (static_cast(now - timer.due_ms) >= 0) { - AppEvent ev{}; - ev.type = AppEventType::Timer; - ev.timestamp_ms = now; - ev.timer.handle = timer.id; - outEvents.push_back(ev); - if (timer.repeat) { - const std::uint32_t interval = timer.interval_ms ? timer.interval_ms : 1; - timer.due_ms = now + interval; - } else { - timer.active = false; - } - } + if (const auto* timer = event.timer()) { + if (timer->clientId != timerClientId) + return; } - timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }), - timers.end()); -} -std::uint32_t AppSystem::nextTimerDueMs(std::uint32_t now) const { - std::uint32_t minWait = std::numeric_limits::max(); - for (const auto& timer: timers) { - if (!timer.active || timer.generation != currentGeneration) - continue; - if (static_cast(now - timer.due_ms) >= 0) - return 0; - const std::uint32_t delta = timer.due_ms - now; - if (delta < minWait) - minWait = delta; - } - return minWait; -} - -void AppSystem::clearTimersForCurrentApp() { - const bool hadTimers = !timers.empty(); - ++currentGeneration; - timers.clear(); - if (hadTimers) - notifyEventBus(EventBusSignal::Timer); -} - -AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) { - for (auto& timer: timers) { - if (!timer.active || timer.generation != currentGeneration) - continue; - if (timer.id == handle) - return &timer; - } - return nullptr; + current->handleEvent(event); } bool AppSystem::handlePendingSwitchRequest() { @@ -278,11 +177,39 @@ bool AppSystem::handlePendingSwitchRequest() { return switched; } -void AppSystem::notifyEventBus(EventBusSignal signal) { - if (signal == EventBusSignal::None) +void AppSystem::attachTimerClient() { + auto* timer = context.timerService(); + if (!timer) { + timerClientId = kInvalidTimerClient; + context.timerClientId = kInvalidTimerClient; return; - if (auto* bus = context.eventBus()) - bus->signal(to_event_bits(signal)); + } + + timerClientId = timer->acquireClient(); + context.timerClientId = timerClientId; +} + +void AppSystem::detachTimerClient() { + auto* timer = context.timerService(); + if (!timer || timerClientId == kInvalidTimerClient) + return; + + timer->releaseClient(timerClientId); + timerClientId = kInvalidTimerClient; + context.timerClientId = kInvalidTimerClient; +} + +void AppSystem::processEventBusEvents() { + auto* eventBus = context.eventBus(); + if (!eventBus) + return; + + AppEvent event{}; + while (eventBus->popEvent(event)) { + dispatchEvent(event); + if (context.pendingSwitch) + break; + } } } // namespace cardboy::sdk diff --git a/Firmware/sdkconfig b/Firmware/sdkconfig index 4e9c05e..8f7159a 100644 --- a/Firmware/sdkconfig +++ b/Firmware/sdkconfig @@ -2497,7 +2497,7 @@ CONFIG_NIMBLE_ROLE_CENTRAL=y CONFIG_NIMBLE_ROLE_PERIPHERAL=y CONFIG_NIMBLE_ROLE_BROADCASTER=y CONFIG_NIMBLE_ROLE_OBSERVER=y -# CONFIG_NIMBLE_NVS_PERSIST is not set +CONFIG_NIMBLE_NVS_PERSIST=y CONFIG_NIMBLE_SM_LEGACY=y CONFIG_NIMBLE_SM_SC=y # CONFIG_NIMBLE_SM_SC_DEBUG_KEYS is not set