event loop

This commit is contained in:
2025-10-09 17:12:17 +02:00
parent 49455d1b36
commit 4e78618556
9 changed files with 420 additions and 104 deletions

View File

@@ -11,6 +11,30 @@
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{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
struct BasicAppContext {
using Framebuffer = FramebufferT;
@@ -40,28 +64,50 @@ struct BasicAppContext {
bool hasPendingAppSwitch() const { return pendingSwitch; }
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat = false) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(delay_ms, repeat);
}
AppTimerHandle scheduleRepeatingTimer(uint32_t interval_ms) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(interval_ms, true);
}
void cancelTimer(AppTimerHandle handle) {
if (!system)
return;
cancelTimerInternal(handle);
}
void cancelAllTimers() {
if (!system)
return;
cancelAllTimersInternal();
}
private:
friend class AppSystem;
bool pendingSwitch = false;
bool pendingSwitchByName = false;
std::size_t pendingAppIndex = 0;
std::string pendingAppName;
AppTimerHandle scheduleTimerInternal(uint32_t delay_ms, bool repeat);
void cancelTimerInternal(AppTimerHandle handle);
void cancelAllTimersInternal();
};
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
struct AppSleepPlan {
uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh
uint32_t normal_ms = 0; // short sleep for responsiveness on input wake
};
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void step() = 0;
virtual AppSleepPlan sleepPlan(uint32_t now) const { return {}; }
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
};
class IAppFactory {

View File

@@ -2,6 +2,7 @@
#include "app_framework.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
@@ -25,9 +26,54 @@ public:
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private:
template<typename FramebufferT, typename InputT, typename ClockT>
friend struct BasicAppContext;
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(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();
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;
InputState lastInputState{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(uint32_t delay_ms, bool repeat) {
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
}
template<typename FramebufferT, typename InputT, typename ClockT>
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
if (system)
system->cancelTimer(handle);
}
template<typename FramebufferT, typename InputT, typename ClockT>
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
if (system)
system->cancelAllTimers();
}

View File

@@ -25,6 +25,7 @@ public:
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
void register_listener(TaskHandle_t task);
TaskHandle_t _pooler_task;
@@ -32,6 +33,7 @@ private:
Buttons();
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
};

View File

@@ -1,9 +1,21 @@
#include "app_system.hpp"
#include <power_helper.hpp>
#include <buttons.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <algorithm>
#include <limits>
#include <utility>
namespace {
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
return a.up != b.up || a.down != b.down || a.left != b.left || a.right != b.right || a.a != b.a ||
a.b != b.b || a.select != b.select || a.start != b.start;
}
} // namespace
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
context.system = this;
}
@@ -43,7 +55,9 @@ bool AppSystem::startAppByIndex(std::size_t index) {
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
current = std::move(app);
clearTimersForCurrentApp();
current = std::move(app);
lastInputState = context.input.readState();
current->onStart();
return true;
}
@@ -54,29 +68,58 @@ void AppSystem::run() {
return;
}
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
std::vector<AppEvent> events;
events.reserve(4);
while (true) {
current->step();
if (context.pendingSwitch) {
const bool byName = context.pendingSwitchByName;
const std::size_t reqIndex = context.pendingAppIndex;
const std::string reqName = context.pendingAppName;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
bool switched = false;
if (byName) {
switched = startApp(reqName);
} else {
switched = startAppByIndex(reqIndex);
events.clear();
const std::uint32_t now = context.clock.millis();
processDueTimers(now, events);
const InputState inputNow = context.input.readState();
if (inputsDiffer(inputNow, lastInputState)) {
AppEvent evt{};
evt.type = AppEventType::Button;
evt.timestamp_ms = now;
evt.button.current = inputNow;
evt.button.previous = lastInputState;
events.push_back(evt);
lastInputState = inputNow;
}
if (!events.empty()) {
for (const auto& evt: events) {
dispatchEvent(evt);
if (handlePendingSwitchRequest()) {
lastInputState = context.input.readState();
break;
}
}
if (switched)
if (handlePendingSwitchRequest())
continue;
// Process newly generated events without blocking
continue;
}
const auto now = context.clock.millis();
auto plan = current->sleepPlan(now);
if (plan.slow_ms || plan.normal_ms) {
PowerHelper::get().delay(static_cast<int>(plan.slow_ms), static_cast<int>(plan.normal_ms));
if (handlePendingSwitchRequest())
continue;
const std::uint32_t waitBase = context.clock.millis();
const std::uint32_t waitMs = nextTimerDueMs(waitBase);
TickType_t waitTicks;
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
waitTicks = portMAX_DELAY;
} else if (waitMs == 0) {
waitTicks = 0;
} else {
waitTicks = pdMS_TO_TICKS(waitMs);
if (waitTicks == 0)
waitTicks = 1;
}
ulTaskNotifyTake(pdTRUE, waitTicks);
if (waitTicks == 0)
taskYIELD();
}
}
@@ -95,3 +138,114 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
}
return static_cast<std::size_t>(-1);
}
AppTimerHandle AppSystem::scheduleTimer(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);
return record.id;
}
void AppSystem::cancelTimer(AppTimerHandle handle) {
auto* timer = findTimer(handle);
if (timer)
timer->active = false;
timers.erase(std::remove_if(timers.begin(), timers.end(),
[](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
void AppSystem::cancelAllTimers() {
for (auto& timer: timers) {
if (timer.generation == currentGeneration)
timer.active = false;
}
timers.erase(std::remove_if(timers.begin(), timers.end(),
[](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
void AppSystem::dispatchEvent(const AppEvent& event) {
if (current)
current->handleEvent(event);
}
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;
}
}
}
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() {
++currentGeneration;
timers.clear();
}
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;
}
bool AppSystem::handlePendingSwitchRequest() {
if (!context.pendingSwitch)
return false;
const bool byName = context.pendingSwitchByName;
const std::size_t reqIndex = context.pendingAppIndex;
const std::string reqName = context.pendingAppName;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
bool switched = false;
if (byName) {
switched = startApp(reqName);
} else {
switched = startAppByIndex(reqIndex);
}
return switched;
}

View File

@@ -39,43 +39,27 @@ public:
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
void onStart() override {
cancelRefreshTimer();
lastSnapshot = {};
dirty = true;
renderIfNeeded(captureTime());
}
void step() override {
const auto snap = captureTime();
InputState st = context.input.readState();
if (st.b && !backPrev) {
context.requestAppSwitchByName(kMenuAppName);
backPrev = st.b;
selectPrev = st.select;
return;
}
if (st.select && !selectPrev) {
use24Hour = !use24Hour;
dirty = true;
}
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
renderIfNeeded(snap);
backPrev = st.b;
selectPrev = st.select;
lastSnapshot = snap;
refreshTimer = context.scheduleRepeatingTimer(200);
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
AppSleepPlan plan;
plan.slow_ms = 200;
plan.normal_ms = 40;
return plan;
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const AppEvent& event) override {
switch (event.type) {
case AppEventType::Button:
handleButtonEvent(event.button);
break;
case AppEventType::Timer:
if (event.timer.handle == refreshTimer)
updateDisplay();
break;
}
}
private:
@@ -83,13 +67,44 @@ private:
Framebuffer& framebuffer;
Clock& clock;
bool use24Hour = true;
bool dirty = false;
bool backPrev = false;
bool selectPrev = false;
bool use24Hour = true;
bool dirty = false;
AppTimerHandle refreshTimer = kInvalidAppTimer;
TimeSnapshot lastSnapshot{};
void cancelRefreshTimer() {
if (refreshTimer != kInvalidAppTimer) {
context.cancelTimer(refreshTimer);
refreshTimer = kInvalidAppTimer;
}
}
void handleButtonEvent(const AppButtonEvent& button) {
const auto& current = button.current;
const auto& previous = button.previous;
if (current.b && !previous.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (current.select && !previous.select) {
use24Hour = !use24Hour;
dirty = true;
}
updateDisplay();
}
void updateDisplay() {
const auto snap = captureTime();
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
renderIfNeeded(snap);
lastSnapshot = snap;
}
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second;
}

View File

@@ -3,6 +3,7 @@
#include "apps/peanut_gb.h"
#include "app_framework.hpp"
#include "app_system.hpp"
#include "font16x8.hpp"
#include <disp_tools.hpp>
@@ -159,6 +160,7 @@ public:
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
void onStart() override {
cancelTick();
perf.resetAll();
prevInput = {};
statusMessage.clear();
@@ -169,14 +171,28 @@ public:
refreshRomList();
mode = Mode::Browse;
browserDirty = true;
scheduleNextTick(0);
}
void onStop() override {
cancelTick();
perf.maybePrintAggregate(true);
unloadRom();
}
void step() override {
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
performStep();
scheduleNextTick(nextDelayMs());
return;
}
if (event.type == AppEventType::Button) {
scheduleNextTick(0);
}
}
void performStep() {
perf.resetForStep();
const uint64_t inputStartUs = esp_timer_get_time();
@@ -235,15 +251,6 @@ public:
perf.maybePrintAggregate();
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
if (mode == Mode::Running)
return {};
AppSleepPlan plan;
plan.slow_ms = 140;
plan.normal_ms = 50;
return plan;
}
private:
enum class Mode { Browse, Running };
enum class ScaleMode { Original, FullHeight };
@@ -484,9 +491,10 @@ private:
std::array<int, LCD_WIDTH> colXEnd{};
};
AppContext& context;
Framebuffer& framebuffer;
PerfTracker perf{};
AppContext& context;
Framebuffer& framebuffer;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::Original;
@@ -512,6 +520,24 @@ private:
std::string activeRomName;
std::string activeRomSavePath;
void cancelTick() {
if (tickTimer != kInvalidAppTimer) {
context.cancelTimer(tickTimer);
tickTimer = kInvalidAppTimer;
}
}
void scheduleNextTick(uint32_t delayMs) {
cancelTick();
tickTimer = context.scheduleTimer(delayMs, false);
}
uint32_t nextDelayMs() const {
if (mode == Mode::Running)
return 0;
return browserDirty ? 50 : 140;
}
bool ensureFilesystemReady() {
esp_err_t err = FsHelper::get().mount();
if (err != ESP_OK) {

View File

@@ -32,45 +32,33 @@ public:
renderIfNeeded();
}
void step() override {
InputState st = context.input.readState();
void handleEvent(const AppEvent& event) override {
if (event.type != AppEventType::Button)
return;
if (st.left && !leftPrev) {
const auto& current = event.button.current;
const auto& previous = event.button.previous;
if (current.left && !previous.left) {
moveSelection(-1);
} else if (st.right && !rightPrev) {
} else if (current.right && !previous.right) {
moveSelection(+1);
}
const bool launch = (st.a && !rotatePrev) || (st.select && !selectPrev);
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
if (launch)
launchSelected();
leftPrev = st.left;
rightPrev = st.right;
rotatePrev = st.a;
selectPrev = st.select;
renderIfNeeded();
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
AppSleepPlan plan;
plan.slow_ms = 120;
plan.normal_ms = 40;
return plan;
}
private:
AppContext& context;
Framebuffer& framebuffer;
std::vector<MenuEntry> entries;
std::size_t selected = 0;
bool dirty = false;
bool leftPrev = false;
bool rightPrev = false;
bool rotatePrev = false;
bool selectPrev = false;
bool dirty = false;
void moveSelection(int step) {
if (entries.empty())

View File

@@ -4,6 +4,7 @@
#include "apps/tetris_app.hpp"
#include "app_framework.hpp"
#include "app_system.hpp"
#include "apps/menu_app.hpp"
#include "font16x8.hpp"
@@ -1149,17 +1150,51 @@ namespace {
class TetrisApp final : public IApp {
public:
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
explicit TetrisApp(AppContext& ctx) : context(ctx), game(ctx) {}
void step() override { game.step(); }
void onStart() override {
cancelTick();
scheduleNextTick(0);
}
AppSleepPlan sleepPlan(uint32_t now) const override {
auto plan = game.recommendedSleepMs(now);
return {plan.slow_ms, plan.normal_ms};
void onStop() override { cancelTick(); }
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
runStep();
return;
}
if (event.type == AppEventType::Button) {
scheduleNextTick(0);
}
}
private:
tetris::Game game;
AppContext& context;
tetris::Game game;
AppTimerHandle tickTimer = kInvalidAppTimer;
void runStep() {
game.step();
const auto now = context.clock.millis();
const auto plan = game.recommendedSleepMs(now);
uint32_t delay = plan.normal_ms != 0 ? plan.normal_ms : plan.slow_ms;
scheduleNextTick(delay);
}
void scheduleNextTick(uint32_t delayMs) {
if (tickTimer != kInvalidAppTimer)
context.cancelTimer(tickTimer);
tickTimer = context.scheduleTimer(delayMs, false);
}
void cancelTick() {
if (tickTimer != kInvalidAppTimer) {
context.cancelTimer(tickTimer);
tickTimer = kInvalidAppTimer;
}
}
};
class TetrisAppFactory final : public IAppFactory {

View File

@@ -90,7 +90,11 @@ void Buttons::pooler() {
reg = 1;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
if (_listener)
xTaskNotifyGive(_listener);
}
}
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; }