better timers

This commit is contained in:
2025-10-12 20:34:05 +02:00
parent d91b7540fc
commit 088c6e47bd
15 changed files with 332 additions and 19 deletions

View File

@@ -4,6 +4,7 @@ idf_component_register(
"src/buttons.cpp"
"src/buzzer.cpp"
"src/esp_backend.cpp"
"src/event_bus.cpp"
"src/display.cpp"
"src/fs_helper.cpp"
"src/i2c_global.cpp"

View File

@@ -5,9 +5,11 @@
#ifndef BUTTONS_HPP
#define BUTTONS_HPP
#include "cardboy/sdk/event_bus.hpp"
#include <cstdint>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <cstdint>
typedef enum {
BTN_START = 1 << 1,
@@ -25,16 +27,18 @@ public:
static Buttons& get();
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
void register_listener(TaskHandle_t task);
void install_isr();
void register_listener(TaskHandle_t task);
void setEventBus(cardboy::sdk::IEventBus* bus);
TaskHandle_t _pooler_task;
TaskHandle_t _pooler_task;
private:
Buttons();
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
cardboy::sdk::IEventBus* _eventBus = nullptr;
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cardboy/sdk/event_bus.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/timers.h"
namespace cardboy::backend::esp {
class EventBus final : public cardboy::sdk::IEventBus {
public:
EventBus();
~EventBus() 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;
private:
EventGroupHandle_t group;
TimerHandle_t timer;
};
} // namespace cardboy::backend::esp

View File

@@ -2,6 +2,7 @@
#include <cardboy/sdk/display_spec.hpp>
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/backend/esp/event_bus.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
@@ -68,6 +69,7 @@ private:
std::unique_ptr<HighResClockService> highResClockService;
std::unique_ptr<PowerService> powerService;
std::unique_ptr<FilesystemService> filesystemService;
std::unique_ptr<EventBus> eventBus;
cardboy::sdk::Services services{};
};

View File

@@ -6,14 +6,15 @@
#include <driver/gpio.h>
#include <esp_err.h>
#include "cardboy/backend/esp/power_helper.hpp"
#include <rom/ets_sys.h>
#include "cardboy/backend/esp/power_helper.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "cardboy/backend/esp/config.hpp"
#include "cardboy/backend/esp/i2c_global.hpp"
#include "cardboy/sdk/event_bus.hpp"
static i2c_master_dev_handle_t dev_handle;
static inline i2c_device_config_t dev_cfg = {
@@ -92,9 +93,13 @@ void Buttons::pooler() {
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
if (_listener)
xTaskNotifyGive(_listener);
if (_eventBus)
_eventBus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
}
}
uint8_t Buttons::get_pressed() { return _current; }
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
void Buttons::setEventBus(cardboy::sdk::IEventBus* bus) { _eventBus = bus; }

View File

@@ -138,6 +138,7 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
highResClockService = std::make_unique<HighResClockService>();
powerService = std::make_unique<PowerService>();
filesystemService = std::make_unique<FilesystemService>();
eventBus = std::make_unique<EventBus>();
services.buzzer = buzzerService.get();
services.battery = batteryService.get();
@@ -146,6 +147,9 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
services.highResClock = highResClockService.get();
services.powerManager = powerService.get();
services.filesystem = filesystemService.get();
services.eventBus = eventBus.get();
Buttons::get().setEventBus(eventBus.get());
}
EspRuntime::~EspRuntime() = default;

View File

@@ -0,0 +1,81 @@
#include "cardboy/backend/esp/event_bus.hpp"
#include "cardboy/sdk/event_bus.hpp"
#include "freertos/portmacro.h"
#include <algorithm>
namespace cardboy::backend::esp {
namespace {
[[nodiscard]] TickType_t toTicks(std::uint32_t timeout_ms) {
if (timeout_ms == cardboy::sdk::IEventBus::kWaitForever)
return portMAX_DELAY;
return pdMS_TO_TICKS(timeout_ms);
}
} // 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)) {}
EventBus::~EventBus() {
if (timer)
xTimerDelete(timer, portMAX_DELAY);
if (group)
vEventGroupDelete(group);
}
void EventBus::signal(std::uint32_t bits) {
if (!group || bits == 0)
return;
xEventGroupSetBits(group, bits);
}
void EventBus::signalFromISR(std::uint32_t bits) {
if (!group || bits == 0)
return;
BaseType_t higherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(group, bits, &higherPriorityTaskWoken);
if (higherPriorityTaskWoken == pdTRUE)
portYIELD_FROM_ISR(higherPriorityTaskWoken);
}
std::uint32_t EventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (!group || mask == 0)
return 0;
const EventBits_t bits = xEventGroupWaitBits(group, mask, pdTRUE, pdFALSE, toTicks(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)
return;
if (delay_ms == 0) {
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
return;
}
const TickType_t ticks = std::max<TickType_t>(pdMS_TO_TICKS(delay_ms), 1);
if (xTimerChangePeriod(timer, ticks, 0) == pdPASS)
xTimerStart(timer, 0);
}
void EventBus::cancelTimerSignal() {
if (!timer)
return;
xTimerStop(timer, 0);
}
} // namespace cardboy::backend::esp

View File

@@ -16,6 +16,7 @@ target_sources(cardboy_backend_interface
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/backend/backend_interface.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/backend.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/display_spec.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/event_bus.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/input_state.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/platform.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/services.hpp

View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
namespace cardboy::sdk {
enum class EventBusSignal : std::uint32_t {
None = 0,
Input = 1u << 0,
Timer = 1u << 1,
External = 1u << 2,
};
inline EventBusSignal operator|(EventBusSignal lhs, EventBusSignal rhs) {
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) | static_cast<std::uint32_t>(rhs));
}
inline EventBusSignal& operator|=(EventBusSignal& lhs, EventBusSignal rhs) {
lhs = lhs | rhs;
return lhs;
}
inline EventBusSignal operator&(EventBusSignal lhs, EventBusSignal rhs) {
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) & static_cast<std::uint32_t>(rhs));
}
inline std::uint32_t to_event_bits(EventBusSignal signal) { return static_cast<std::uint32_t>(signal); }
class IEventBus {
public:
static constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
virtual ~IEventBus() = default;
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;
};
} // namespace cardboy::sdk

View File

@@ -1,5 +1,7 @@
#pragma once
#include "cardboy/sdk/event_bus.hpp"
#include <cstdint>
#include <string>
#include <string_view>
@@ -82,6 +84,7 @@ struct Services {
IHighResClock* highResClock = nullptr;
IPowerManager* powerManager = nullptr;
IFilesystem* filesystem = nullptr;
IEventBus* eventBus = nullptr;
};
} // namespace cardboy::sdk

View File

@@ -1,14 +1,17 @@
#pragma once
#include "cardboy/sdk/event_bus.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
#include <SFML/Graphics.hpp>
#include <SFML/Window/Keyboard.hpp>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <mutex>
#include <random>
#include <string>
#include <string_view>
@@ -92,6 +95,29 @@ private:
bool mounted = false;
};
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;
private:
DesktopRuntime& runtime;
std::mutex mutex;
std::condition_variable cv;
std::uint32_t pendingBits = 0;
std::mutex timerMutex;
std::condition_variable timerCv;
std::thread timerThread;
bool timerCancel = false;
};
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
@@ -170,6 +196,7 @@ private:
DesktopHighResClock highResService;
DesktopPowerManager powerService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
cardboy::sdk::Services services{};
};

View File

@@ -13,6 +13,92 @@
namespace cardboy::backend::desktop {
DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {}
DesktopEventBus::~DesktopEventBus() { cancelTimerSignal(); }
void DesktopEventBus::signal(std::uint32_t bits) {
if (bits == 0)
return;
{
std::lock_guard<std::mutex> lock(mutex);
pendingBits |= bits;
}
cv.notify_all();
}
void DesktopEventBus::signalFromISR(std::uint32_t bits) { signal(bits); }
std::uint32_t DesktopEventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (mask == 0)
return 0;
const auto start = std::chrono::steady_clock::now();
const bool infinite = timeout_ms == cardboy::sdk::IEventBus::kWaitForever;
while (true) {
{
std::lock_guard<std::mutex> lock(mutex);
const std::uint32_t ready = pendingBits & mask;
if (ready != 0) {
pendingBits &= ~mask;
return ready;
}
}
if (!infinite) {
const auto now = std::chrono::steady_clock::now();
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
if (elapsedMs >= static_cast<std::int64_t>(timeout_ms))
return 0;
const auto remaining = timeout_ms - static_cast<std::uint32_t>(elapsedMs);
runtime.sleepFor(std::min<std::uint32_t>(remaining, 8));
} else {
runtime.sleepFor(8);
}
}
}
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;
}
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
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));
});
}
void DesktopEventBus::cancelTimerSignal() {
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = true;
}
timerCv.notify_all();
if (timerThread.joinable())
timerThread.join();
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
}
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())
@@ -90,6 +176,7 @@ DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
bool handled = true;
switch (key) {
case sf::Keyboard::Key::Up:
state.up = pressed;
@@ -118,8 +205,11 @@ void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
state.start = pressed;
break;
default:
handled = false;
break;
}
if (handled)
runtime.eventBusService.signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
}
DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {}
@@ -137,7 +227,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) {
framebuffer(*this), input(*this), clock(*this), eventBusService(*this) {
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");
@@ -153,6 +243,7 @@ DesktopRuntime::DesktopRuntime() :
services.highResClock = &highResService;
services.powerManager = &powerService;
services.filesystem = &filesystemService;
services.eventBus = &eventBusService;
}
cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; }

View File

@@ -63,6 +63,7 @@ struct AppContext {
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
[[nodiscard]] IPowerManager* powerManager() const { return services ? services->powerManager : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
[[nodiscard]] IEventBus* eventBus() const { return services ? services->eventBus : nullptr; }
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cardboy/sdk/event_bus.hpp>
#include "app_framework.hpp"
#include <cstdint>
@@ -51,6 +52,7 @@ private:
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
void notifyEventBus(EventBusSignal signal);
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;

View File

@@ -17,8 +17,6 @@ namespace {
return state.up || state.down || state.left || state.right || state.a || state.b || state.select || state.start;
}
constexpr std::uint32_t kIdlePollMs = 16;
template<typename Framebuffer>
void statusBarPreSendHook(void* framebuffer, void* userData) {
auto* fb = static_cast<Framebuffer*>(framebuffer);
@@ -127,13 +125,19 @@ void AppSystem::run() {
if (waitMs == 0)
continue;
if (waitMs == std::numeric_limits<std::uint32_t>::max())
waitMs = kIdlePollMs;
else
waitMs = std::min(waitMs, kIdlePollMs);
auto* eventBus = context.eventBus();
if (!eventBus)
return;
if (waitMs > 0)
context.clock.sleep_ms(waitMs);
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);
}
}
}
@@ -167,24 +171,32 @@ AppTimerHandle AppSystem::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
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)
timer->active = false;
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)
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) {
@@ -229,8 +241,11 @@ std::uint32_t AppSystem::nextTimerDueMs(std::uint32_t now) const {
}
void AppSystem::clearTimersForCurrentApp() {
const bool hadTimers = !timers.empty();
++currentGeneration;
timers.clear();
if (hadTimers)
notifyEventBus(EventBusSignal::Timer);
}
AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) {
@@ -260,4 +275,11 @@ bool AppSystem::handlePendingSwitchRequest() {
return switched;
}
void AppSystem::notifyEventBus(EventBusSignal signal) {
if (signal == EventBusSignal::None)
return;
if (auto* bus = context.eventBus())
bus->signal(to_event_bits(signal));
}
} // namespace cardboy::sdk