mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
Compare commits
2 Commits
4112efd60b
...
1ee132898b
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ee132898b | |||
| 5ddd38e5d7 |
@@ -1,4 +1,4 @@
|
||||
To build:
|
||||
(in zsh)
|
||||
(in zsh, bash doesn't work)
|
||||
. "$HOME/esp/esp-idf/export.sh"
|
||||
idf.py build
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
@@ -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 = ¬ificationService;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user