mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
event loop
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -90,7 +90,11 @@ void Buttons::pooler() {
|
||||
reg = 1;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, 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; }
|
||||
|
||||
Reference in New Issue
Block a user