Compare commits

...

2 Commits

Author SHA1 Message Date
1ee132898b some refactoring 2025-10-22 14:46:20 +02:00
5ddd38e5d7 remove notifications 2025-10-21 23:35:11 +02:00
22 changed files with 781 additions and 348 deletions

View File

@@ -1,4 +1,4 @@
To build:
(in zsh)
(in zsh, bash doesn't work)
. "$HOME/esp/esp-idf/export.sh"
idf.py build

View File

@@ -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

View File

@@ -62,6 +62,7 @@ private:
class FilesystemService;
class LoopHooksService;
class NotificationService;
class TimerService;
std::unique_ptr<BuzzerService> buzzerService;
std::unique_ptr<BatteryService> batteryService;
@@ -70,6 +71,7 @@ private:
std::unique_ptr<HighResClockService> highResClockService;
std::unique_ptr<FilesystemService> filesystemService;
std::unique_ptr<EventBus> eventBus;
std::unique_ptr<TimerService> timerService;
std::unique_ptr<LoopHooksService> loopHooksService;
std::unique_ptr<NotificationService> notificationService;

View File

@@ -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 <algorithm>
#include <cstdint>
#include <ctime>
#include <list>
#include <mutex>
#include <string>
#include <string_view>
@@ -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<TimerRecord> 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 {
@@ -195,6 +255,23 @@ public:
++revisionCounter;
}
void removeById(std::uint64_t id) override {
if (id == 0)
return;
std::lock_guard<std::mutex> lock(mutex);
bool removed = false;
for (auto it = entries.begin(); it != entries.end();) {
if (it->id == id) {
it = entries.erase(it);
removed = true;
} else {
++it;
}
}
if (removed)
++revisionCounter;
}
void removeByExternalId(std::uint64_t externalId) override {
if (externalId == 0)
return;
@@ -230,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<std::uint32_t>(1, delay_ms);
const TickType_t ticks = std::max<TickType_t>(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<TimerHandle_t> 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<TimerRecord*>(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<std::uint32_t>(esp_timer_get_time() / 1000ULL);
event.data = timerEvent;
eventBus.postEvent(event);
}
void EspRuntime::TimerService::shutdown() {
if (!mutex)
return;
std::vector<TimerHandle_t> 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();
@@ -240,6 +527,7 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
highResClockService = std::make_unique<HighResClockService>();
filesystemService = std::make_unique<FilesystemService>();
eventBus = std::make_unique<EventBus>();
timerService = std::make_unique<TimerService>(*eventBus);
loopHooksService = std::make_unique<LoopHooksService>();
notificationService = std::make_unique<NotificationService>();
@@ -250,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();

View File

@@ -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<EventBus*>(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<std::uint32_t>(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<TickType_t>(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

View File

@@ -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:

View File

@@ -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() {

View File

@@ -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:
@@ -130,6 +129,25 @@ private:
}
}
const bool deletePressed = button.current.b && !button.previous.b;
if (deletePressed && notificationCenter && !notifications.empty()) {
const std::size_t index = std::min<std::size_t>(selectedNotification, notifications.size() - 1);
const auto& note = notifications[index];
std::size_t preferredIndex = index;
if (index + 1 < notifications.size())
preferredIndex = index + 1;
else if (index > 0)
preferredIndex = index - 1;
if (note.externalId != 0)
notificationCenter->removeByExternalId(note.externalId);
else
notificationCenter->removeById(note.id);
selectedNotification = preferredIndex;
lastNotificationInteractionMs = clock.millis();
dirty = true;
refreshNotifications();
}
const bool comboNow = comboPressed(button.current);
updateHoldState(comboNow);
if (navPressed)

View File

@@ -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:

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include "cardboy/sdk/input_state.hpp"
#include <cstdint>
#include <type_traits>
#include <utility>
#include <variant>
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<AppButtonEvent, AppTimerEvent>;
std::uint32_t timestamp_ms = 0;
Data data{AppButtonEvent{}};
[[nodiscard]] bool isButton() const { return std::holds_alternative<AppButtonEvent>(data); }
[[nodiscard]] bool isTimer() const { return std::holds_alternative<AppTimerEvent>(data); }
[[nodiscard]] const AppButtonEvent* button() const { return std::get_if<AppButtonEvent>(&data); }
[[nodiscard]] AppButtonEvent* button() { return std::get_if<AppButtonEvent>(&data); }
[[nodiscard]] const AppTimerEvent* timer() const { return std::get_if<AppTimerEvent>(&data); }
[[nodiscard]] AppTimerEvent* timer() { return std::get_if<AppTimerEvent>(&data); }
template<typename Visitor>
decltype(auto) visit(Visitor&& visitor) {
return std::visit(std::forward<Visitor>(visitor), data);
}
template<typename Visitor>
decltype(auto) visit(Visitor&& visitor) const {
return std::visit(std::forward<Visitor>(visitor), data);
}
};
template<typename... Ts>
struct Overload : Ts... {
using Ts::operator()...;
};
template<typename... Ts>
Overload(Ts...) -> Overload<Ts...>;
template<typename... Ts>
constexpr auto overload(Ts&&... ts) {
return Overload<std::decay_t<Ts>...>{std::forward<Ts>(ts)...};
}
} // namespace cardboy::sdk

View File

@@ -1,5 +1,7 @@
#pragma once
#include "cardboy/sdk/app_events.hpp"
#include <cstdint>
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

View File

@@ -2,6 +2,7 @@
#include "cardboy/sdk/event_bus.hpp"
#include "cardboy/sdk/loop_hooks.hpp"
#include "cardboy/sdk/timer_service.hpp"
#include <cstdint>
#include <string>
@@ -88,6 +89,7 @@ public:
[[nodiscard]] virtual std::vector<Notification> recent(std::size_t limit) const = 0;
virtual void markAllRead() = 0;
virtual void clear() = 0;
virtual void removeById(std::uint64_t id) = 0;
virtual void removeByExternalId(std::uint64_t externalId) = 0;
};
@@ -99,6 +101,7 @@ struct Services {
IHighResClock* highResClock = nullptr;
IFilesystem* filesystem = nullptr;
IEventBus* eventBus = nullptr;
ITimerService* timer = nullptr;
ILoopHooks* loopHooks = nullptr;
INotificationCenter* notifications = nullptr;
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "cardboy/sdk/app_events.hpp"
#include <cstdint>
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

View File

@@ -8,6 +8,7 @@
#include <SFML/Window/Keyboard.hpp>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <cstdint>
#include <filesystem>
#include <limits>
@@ -93,6 +94,7 @@ public:
[[nodiscard]] std::vector<Notification> recent(std::size_t limit) const override;
void markAllRead() override;
void clear() override;
void removeById(std::uint64_t id) override;
void removeByExternalId(std::uint64_t externalId) override;
private:
@@ -107,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<cardboy::sdk::AppEvent> 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<TimerRecord> timers;
bool stopWorker = false;
std::thread worker;
cardboy::sdk::AppTimerHandle nextHandle = 1;
cardboy::sdk::TimerClientId nextClient = 1;
};
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
@@ -205,6 +240,7 @@ private:
DesktopHighResClock highResService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
DesktopTimerService timerService;
DesktopNotificationCenter notificationService;
cardboy::sdk::Services services{};
};

View File

@@ -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<std::mutex> lock(timerMutex);
timerCancel = false;
std::lock_guard<std::mutex> lock(eventMutex);
if (eventQueue.size() >= kDesktopEventQueueLimit)
eventQueue.pop_front();
eventQueue.push_back(event);
}
timerThread = std::thread([this, delay_ms]() {
std::unique_lock<std::mutex> 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<std::mutex> 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<std::mutex> lock(timerMutex);
timerCancel = true;
std::lock_guard<std::mutex> 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<std::mutex> 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<std::uint32_t>(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<std::mutex> lock(timerMutex);
timerCancel = false;
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(mutex);
for (auto& record: timers) {
if (record.clientId == clientId)
record.active = false;
}
cleanupInactive();
wakeWorker();
}
void DesktopTimerService::workerLoop() {
std::unique_lock<std::mutex> 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())
@@ -212,6 +329,23 @@ void DesktopNotificationCenter::clear() {
++revisionCounter;
}
void DesktopNotificationCenter::removeById(std::uint64_t id) {
if (id == 0)
return;
std::lock_guard<std::mutex> lock(mutex);
bool removed = false;
for (auto it = entries.begin(); it != entries.end();) {
if (it->id == id) {
it = entries.erase(it);
removed = true;
} else {
++it;
}
}
if (removed)
++revisionCounter;
}
void DesktopNotificationCenter::removeByExternalId(std::uint64_t externalId) {
if (externalId == 0)
return;
@@ -307,7 +441,7 @@ DesktopRuntime::DesktopRuntime() :
"Cardboy Desktop"),
texture(), sprite(texture),
pixels(static_cast<std::size_t>(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");
@@ -323,6 +457,7 @@ DesktopRuntime::DesktopRuntime() :
services.highResClock = &highResService;
services.filesystem = &filesystemService;
services.eventBus = &eventBusService;
services.timer = &timerService;
services.loopHooks = nullptr;
services.notifications = &notificationService;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cardboy/sdk/app_events.hpp>
#include <cardboy/sdk/backend.hpp>
#include <cardboy/sdk/platform.hpp>
#include <cardboy/sdk/services.hpp>
@@ -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 {

View File

@@ -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<AppEvent>& 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<std::unique_ptr<IAppFactory>> factories;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::size_t activeIndex = static_cast<std::size_t>(-1);
std::vector<TimerRecord> 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

View File

@@ -3,7 +3,6 @@
#include "cardboy/sdk/status_bar.hpp"
#include <algorithm>
#include <limits>
#include <utility>
namespace cardboy::sdk {
@@ -34,7 +33,10 @@ AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<FBType>, &statusBar);
}
AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); }
AppSystem::~AppSystem() {
detachTimerClient();
FramebufferHooks::clearPreSendHook();
}
void AppSystem::registerApp(std::unique_ptr<IAppFactory> 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<AppEvent> 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<std::uint32_t>::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<std::size_t>(-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<std::uint32_t>(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<AppEvent>& outEvents) {
for (auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (static_cast<std::int32_t>(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<std::uint32_t>::max();
for (const auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (static_cast<std::int32_t>(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

View File

@@ -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