mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
kinda sdk
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
idf_component_register(SRCS
|
||||
src/app_main.cpp
|
||||
src/app_system.cpp
|
||||
src/apps/menu_app.cpp
|
||||
src/apps/clock_app.cpp
|
||||
src/apps/tetris_app.cpp
|
||||
../sdk/apps/menu_app.cpp
|
||||
../sdk/apps/clock_app.cpp
|
||||
../sdk/apps/tetris_app.cpp
|
||||
src/apps/gameboy_app.cpp
|
||||
src/display.cpp
|
||||
src/bat_mon.cpp
|
||||
@@ -16,8 +15,9 @@ idf_component_register(SRCS
|
||||
src/power_helper.cpp
|
||||
src/buzzer.cpp
|
||||
src/fs_helper.cpp
|
||||
../sdk/src/app_system.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
|
||||
INCLUDE_DIRS "include"
|
||||
INCLUDE_DIRS "include" "../sdk/include"
|
||||
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
|
||||
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,118 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_platform.hpp"
|
||||
#include "input_state.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
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 AppEventType = cardboy::sdk::AppEventType;
|
||||
using AppButtonEvent = cardboy::sdk::AppButtonEvent;
|
||||
using AppTimerEvent = cardboy::sdk::AppTimerEvent;
|
||||
using AppEvent = cardboy::sdk::AppEvent;
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
struct BasicAppContext {
|
||||
using Framebuffer = FramebufferT;
|
||||
using Input = InputT;
|
||||
using Clock = ClockT;
|
||||
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
|
||||
|
||||
BasicAppContext() = delete;
|
||||
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
using AppContext = cardboy::sdk::AppContext;
|
||||
|
||||
FramebufferT& framebuffer;
|
||||
InputT& input;
|
||||
ClockT& clock;
|
||||
AppSystem* system = nullptr;
|
||||
|
||||
void requestAppSwitchByIndex(std::size_t index) {
|
||||
pendingAppIndex = index;
|
||||
pendingAppName.clear();
|
||||
pendingSwitchByName = false;
|
||||
pendingSwitch = true;
|
||||
}
|
||||
|
||||
void requestAppSwitchByName(std::string_view name) {
|
||||
pendingAppName.assign(name.begin(), name.end());
|
||||
pendingSwitchByName = true;
|
||||
pendingSwitch = true;
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
class IApp {
|
||||
public:
|
||||
virtual ~IApp() = default;
|
||||
virtual void onStart() {}
|
||||
virtual void onStop() {}
|
||||
virtual void handleEvent(const AppEvent& event) = 0;
|
||||
};
|
||||
|
||||
class IAppFactory {
|
||||
public:
|
||||
virtual ~IAppFactory() = default;
|
||||
virtual const char* name() const = 0;
|
||||
virtual std::unique_ptr<IApp> create(AppContext& context) = 0;
|
||||
};
|
||||
using IApp = cardboy::sdk::IApp;
|
||||
using IAppFactory = cardboy::sdk::IAppFactory;
|
||||
using Services = cardboy::sdk::Services;
|
||||
using IBuzzer = cardboy::sdk::IBuzzer;
|
||||
using IBatteryMonitor = cardboy::sdk::IBatteryMonitor;
|
||||
using IStorage = cardboy::sdk::IStorage;
|
||||
using IRandom = cardboy::sdk::IRandom;
|
||||
using IHighResClock = cardboy::sdk::IHighResClock;
|
||||
using IPowerManager = cardboy::sdk::IPowerManager;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "config.hpp"
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <buttons.hpp>
|
||||
#include <disp_tools.hpp>
|
||||
@@ -10,29 +11,33 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class PlatformFramebuffer {
|
||||
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
int width() const { return DISP_WIDTH; }
|
||||
int height() const { return DISP_HEIGHT; }
|
||||
int width() const override { return cardboy::sdk::kDisplayWidth; }
|
||||
int height() const override { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void drawPixel(int x, int y, bool on) {
|
||||
void drawPixel(int x, int y, bool on) override {
|
||||
if (x < 0 || y < 0 || x >= width() || y >= height())
|
||||
return;
|
||||
DispTools::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void clear(bool on) {
|
||||
void clear(bool on) override {
|
||||
for (int y = 0; y < height(); ++y)
|
||||
for (int x = 0; x < width(); ++x)
|
||||
DispTools::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void beginFrame() override { DispTools::draw_to_display_async_wait(); }
|
||||
void endFrame() override { DispTools::draw_to_display_async_start(); }
|
||||
bool isFrameInFlight() const override { return DispTools::draw_to_display_async_busy(); }
|
||||
};
|
||||
|
||||
class PlatformInput {
|
||||
class PlatformInput final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
InputState readState() {
|
||||
InputState state{};
|
||||
const uint8_t pressed = Buttons::get().get_pressed();
|
||||
cardboy::sdk::InputState readState() override {
|
||||
cardboy::sdk::InputState state{};
|
||||
const uint8_t pressed = Buttons::get().get_pressed();
|
||||
if (pressed & BTN_UP)
|
||||
state.up = true;
|
||||
if (pressed & BTN_LEFT)
|
||||
@@ -53,12 +58,16 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class PlatformClock {
|
||||
class PlatformClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
uint32_t millis() {
|
||||
std::uint32_t millis() override {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<uint32_t>((static_cast<uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void sleep_ms(uint32_t ms) { PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); }
|
||||
void sleep_ms(std::uint32_t ms) override {
|
||||
if (ms == 0)
|
||||
return;
|
||||
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,79 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class AppSystem {
|
||||
public:
|
||||
explicit AppSystem(AppContext context);
|
||||
|
||||
void registerApp(std::unique_ptr<IAppFactory> factory);
|
||||
bool startApp(const std::string& name);
|
||||
bool startAppByIndex(std::size_t index);
|
||||
|
||||
void run();
|
||||
|
||||
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
|
||||
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
|
||||
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
|
||||
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
|
||||
|
||||
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
|
||||
[[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();
|
||||
}
|
||||
using AppSystem = cardboy::sdk::AppSystem;
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<IAppFactory> createClockAppFactory();
|
||||
|
||||
}
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kMenuAppName[] = "Menu";
|
||||
inline constexpr std::string_view kMenuAppNameView = kMenuAppName;
|
||||
|
||||
std::unique_ptr<IAppFactory> createMenuAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<IAppFactory> createTetrisAppFactory();
|
||||
|
||||
}
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
|
||||
#define SPI_BUS SPI2_HOST
|
||||
|
||||
#define DISP_WIDTH 400
|
||||
#define DISP_HEIGHT 240
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
|
||||
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
|
||||
|
||||
#define BUZZER_PIN GPIO_NUM_25
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
|
||||
#include "Surface.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
namespace SMD {
|
||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Fonts.hpp"
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <string_view>
|
||||
|
||||
namespace font16x8 {
|
||||
|
||||
constexpr int kGlyphWidth = 8;
|
||||
constexpr int kGlyphHeight = 16;
|
||||
constexpr unsigned char kFallbackChar = '?';
|
||||
|
||||
inline unsigned char normalizeChar(char ch) {
|
||||
unsigned char uc = static_cast<unsigned char>(ch);
|
||||
if (uc >= 'a' && uc <= 'z')
|
||||
uc = static_cast<unsigned char>(std::toupper(static_cast<unsigned char>(uc)));
|
||||
if (!std::isprint(static_cast<unsigned char>(uc)))
|
||||
return kFallbackChar;
|
||||
return uc;
|
||||
}
|
||||
|
||||
inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
|
||||
unsigned char uc = normalizeChar(ch);
|
||||
return fonts_Terminess_Powerline[uc];
|
||||
}
|
||||
|
||||
template<typename Framebuffer>
|
||||
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) {
|
||||
const auto& rows = glyphBitmap(ch);
|
||||
for (int row = 0; row < kGlyphHeight; ++row) {
|
||||
const uint8_t rowBits = rows[row];
|
||||
for (int col = 0; col < kGlyphWidth; ++col) {
|
||||
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
|
||||
if (rowBits & mask) {
|
||||
for (int sx = 0; sx < scale; ++sx)
|
||||
for (int sy = 0; sy < scale; ++sy)
|
||||
fb.drawPixel(x + col * scale + sx, y + row * scale + sy, on);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
|
||||
if (text.empty())
|
||||
return 0;
|
||||
const int advance = (kGlyphWidth + letterSpacing) * scale;
|
||||
return static_cast<int>(text.size()) * advance - letterSpacing * scale;
|
||||
}
|
||||
|
||||
template<typename Framebuffer>
|
||||
inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true,
|
||||
int letterSpacing = 1) {
|
||||
int cursor = x;
|
||||
for (char ch: text) {
|
||||
drawGlyph(fb, cursor, y, ch, scale, on);
|
||||
cursor += (kGlyphWidth + letterSpacing) * scale;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace font16x8
|
||||
@@ -1,12 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
struct InputState {
|
||||
bool up = false;
|
||||
bool left = false;
|
||||
bool right = false;
|
||||
bool down = false;
|
||||
bool a = false;
|
||||
bool b = false;
|
||||
bool select = false;
|
||||
bool start = false;
|
||||
};
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
using InputState = cardboy::sdk::InputState;
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include "app_system.hpp"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_platform.hpp"
|
||||
#include "apps/clock_app.hpp"
|
||||
#include "apps/gameboy_app.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "apps/tetris_app.hpp"
|
||||
#include "config.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <bat_mon.hpp>
|
||||
#include <buttons.hpp>
|
||||
@@ -16,6 +18,8 @@
|
||||
#include <display.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
#include <i2c_global.hpp>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <power_helper.hpp>
|
||||
#include <shutdowner.hpp>
|
||||
#include <spi_global.hpp>
|
||||
@@ -26,6 +30,8 @@
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "sdkconfig.h"
|
||||
@@ -36,8 +42,84 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
class EspBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) override {
|
||||
Buzzer::get().tone(freq, duration_ms, gap_ms);
|
||||
}
|
||||
|
||||
void beepRotate() override { Buzzer::get().beepRotate(); }
|
||||
void beepMove() override { Buzzer::get().beepMove(); }
|
||||
void beepLock() override { Buzzer::get().beepLock(); }
|
||||
void beepLines(int lines) override { Buzzer::get().beepLines(lines); }
|
||||
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
|
||||
void beepGameOver() override { Buzzer::get().beepGameOver(); }
|
||||
|
||||
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
|
||||
void toggleMuted() override { Buzzer::get().toggleMuted(); }
|
||||
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
|
||||
};
|
||||
|
||||
class EspBatteryMonitor final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return true; }
|
||||
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
||||
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
||||
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
||||
};
|
||||
|
||||
class EspStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
std::uint32_t value = 0;
|
||||
esp_err_t err = nvs_get_u32(handle, keyStr.c_str(), &value);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK)
|
||||
return false;
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READWRITE, &handle) != ESP_OK)
|
||||
return;
|
||||
nvs_set_u32(handle, keyStr.c_str(), value);
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
};
|
||||
|
||||
class EspRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
||||
};
|
||||
|
||||
class EspHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
||||
};
|
||||
|
||||
class EspPowerManager final : public cardboy::sdk::IPowerManager {
|
||||
public:
|
||||
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
|
||||
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||
namespace {
|
||||
|
||||
@@ -217,8 +299,24 @@ extern "C" void app_main() {
|
||||
static PlatformInput input;
|
||||
static PlatformClock clock;
|
||||
|
||||
static EspBuzzer buzzerService;
|
||||
static EspBatteryMonitor batteryService;
|
||||
static EspStorage storageService;
|
||||
static EspRandom randomService;
|
||||
static EspHighResClock highResClockService;
|
||||
static EspPowerManager powerService;
|
||||
|
||||
static cardboy::sdk::Services services{};
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResClockService;
|
||||
services.powerManager = &powerService;
|
||||
|
||||
AppContext context(framebuffer, input, clock);
|
||||
AppSystem system(context);
|
||||
context.services = &services;
|
||||
AppSystem system(context);
|
||||
context.system = &system;
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
#include "app_system.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; }
|
||||
|
||||
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
||||
if (!factory)
|
||||
return;
|
||||
factories.emplace_back(std::move(factory));
|
||||
}
|
||||
|
||||
bool AppSystem::startApp(const std::string& name) {
|
||||
for (std::size_t i = 0; i < factories.size(); ++i) {
|
||||
if (factories[i]->name() == name) {
|
||||
return startAppByIndex(i);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AppSystem::startAppByIndex(std::size_t index) {
|
||||
if (index >= factories.size())
|
||||
return false;
|
||||
|
||||
context.system = this;
|
||||
auto& factory = factories[index];
|
||||
auto app = factory->create(context);
|
||||
if (!app)
|
||||
return false;
|
||||
|
||||
if (current) {
|
||||
current->onStop();
|
||||
current.reset();
|
||||
}
|
||||
|
||||
activeFactory = factory.get();
|
||||
activeIndex = index;
|
||||
context.pendingSwitch = false;
|
||||
context.pendingSwitchByName = false;
|
||||
context.pendingAppName.clear();
|
||||
clearTimersForCurrentApp();
|
||||
current = std::move(app);
|
||||
lastInputState = context.input.readState();
|
||||
current->onStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppSystem::run() {
|
||||
if (!current) {
|
||||
if (factories.empty() || !startAppByIndex(0))
|
||||
return;
|
||||
}
|
||||
|
||||
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
|
||||
std::vector<AppEvent> events;
|
||||
events.reserve(4);
|
||||
|
||||
while (true) {
|
||||
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;
|
||||
}
|
||||
|
||||
for (const auto& evt: events) {
|
||||
dispatchEvent(evt);
|
||||
if (handlePendingSwitchRequest()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
waitTicks = pdMS_TO_TICKS(waitMs);
|
||||
if (waitTicks == 0)
|
||||
waitTicks = 1;
|
||||
}
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, waitTicks);
|
||||
}
|
||||
}
|
||||
|
||||
const IAppFactory* AppSystem::factoryAt(std::size_t index) const {
|
||||
if (index >= factories.size())
|
||||
return nullptr;
|
||||
return factories[index].get();
|
||||
}
|
||||
|
||||
std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
|
||||
if (!factory)
|
||||
return static_cast<std::size_t>(-1);
|
||||
for (std::size_t i = 0; i < factories.size(); ++i) {
|
||||
if (factories[i].get() == factory)
|
||||
return i;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
#include "apps/clock_app.hpp"
|
||||
|
||||
#include "app_system.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "font16x8.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kClockAppName = "Clock";
|
||||
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
using Clock = typename AppContext::Clock;
|
||||
|
||||
struct TimeSnapshot {
|
||||
bool hasWallTime = false;
|
||||
int hour24 = 0;
|
||||
int minute = 0;
|
||||
int second = 0;
|
||||
int year = 0;
|
||||
int month = 0;
|
||||
int day = 0;
|
||||
int weekday = 0;
|
||||
uint64_t uptimeSeconds = 0;
|
||||
};
|
||||
|
||||
class ClockApp final : public IApp {
|
||||
public:
|
||||
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
||||
|
||||
void onStart() override {
|
||||
cancelRefreshTimer();
|
||||
lastSnapshot = {};
|
||||
dirty = true;
|
||||
const auto snap = captureTime();
|
||||
renderIfNeeded(snap);
|
||||
lastSnapshot = snap;
|
||||
refreshTimer = context.scheduleRepeatingTimer(200);
|
||||
}
|
||||
|
||||
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:
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
Clock& clock;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
TimeSnapshot captureTime() const {
|
||||
TimeSnapshot snap{};
|
||||
snap.uptimeSeconds = clock.millis() / 1000ULL;
|
||||
|
||||
time_t raw = 0;
|
||||
if (time(&raw) != static_cast<time_t>(-1) && raw > 0) {
|
||||
std::tm tm{};
|
||||
if (localtime_r(&raw, &tm) != nullptr) {
|
||||
snap.hasWallTime = true;
|
||||
snap.hour24 = tm.tm_hour;
|
||||
snap.minute = tm.tm_min;
|
||||
snap.second = tm.tm_sec;
|
||||
snap.year = tm.tm_year + 1900;
|
||||
snap.month = tm.tm_mon + 1;
|
||||
snap.day = tm.tm_mday;
|
||||
snap.weekday = tm.tm_wday;
|
||||
return snap;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to uptime-derived clock
|
||||
snap.hasWallTime = false;
|
||||
snap.hour24 = static_cast<int>((snap.uptimeSeconds / 3600ULL) % 24ULL);
|
||||
snap.minute = static_cast<int>((snap.uptimeSeconds / 60ULL) % 60ULL);
|
||||
snap.second = static_cast<int>(snap.uptimeSeconds % 60ULL);
|
||||
return snap;
|
||||
}
|
||||
|
||||
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
|
||||
const int width = font16x8::measureText(text, scale, letterSpacing);
|
||||
const int x = (fb.width() - width) / 2;
|
||||
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
|
||||
}
|
||||
|
||||
static std::string formatDate(const TimeSnapshot& snap) {
|
||||
static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
|
||||
if (!snap.hasWallTime)
|
||||
return "UPTIME MODE";
|
||||
const char* weekday = (snap.weekday >= 0 && snap.weekday < 7) ? kWeekdays[snap.weekday] : "";
|
||||
char buffer[32];
|
||||
std::snprintf(buffer, sizeof(buffer), "%s %04d-%02d-%02d", weekday, snap.year, snap.month, snap.day);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void renderIfNeeded(const TimeSnapshot& snap) {
|
||||
if (!dirty)
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
DispTools::draw_to_display_async_wait();
|
||||
framebuffer.clear(false);
|
||||
|
||||
const int scaleLarge = 3;
|
||||
const int scaleSeconds = 2;
|
||||
const int scaleSmall = 1;
|
||||
|
||||
int hourDisplay = snap.hour24;
|
||||
bool isPm = false;
|
||||
if (!use24Hour) {
|
||||
isPm = hourDisplay >= 12;
|
||||
int h12 = hourDisplay % 12;
|
||||
if (h12 == 0)
|
||||
h12 = 12;
|
||||
hourDisplay = h12;
|
||||
}
|
||||
|
||||
char mainLine[6];
|
||||
std::snprintf(mainLine, sizeof(mainLine), "%02d:%02d", hourDisplay, snap.minute);
|
||||
const int mainW = font16x8::measureText(mainLine, scaleLarge, 0);
|
||||
const int timeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleLarge) / 2 - 12;
|
||||
const int timeX = (framebuffer.width() - mainW) / 2;
|
||||
font16x8::drawText(framebuffer, timeX, timeY, mainLine, scaleLarge, true, 0);
|
||||
|
||||
char secondsLine[3];
|
||||
std::snprintf(secondsLine, sizeof(secondsLine), "%02d", snap.second);
|
||||
const int secondsX = timeX + mainW + 12;
|
||||
const int secondsY = timeY + font16x8::kGlyphHeight * scaleLarge - font16x8::kGlyphHeight * scaleSeconds;
|
||||
font16x8::drawText(framebuffer, secondsX, secondsY, secondsLine, scaleSeconds, true, 0);
|
||||
|
||||
if (!use24Hour) {
|
||||
font16x8::drawText(framebuffer, timeX + mainW + 12, timeY, isPm ? "PM" : "AM", scaleSmall, true, 0);
|
||||
} else {
|
||||
font16x8::drawText(framebuffer, timeX + mainW + 12, timeY, "24H", scaleSmall, true, 0);
|
||||
}
|
||||
|
||||
const std::string dateLine = formatDate(snap);
|
||||
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleLarge + 28, dateLine, scaleSmall, 1);
|
||||
|
||||
if (!snap.hasWallTime) {
|
||||
char uptimeLine[32];
|
||||
const uint64_t days = snap.uptimeSeconds / 86400ULL;
|
||||
const uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
||||
const uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
||||
const uint64_t secs = snap.uptimeSeconds % 60ULL;
|
||||
if (days > 0) {
|
||||
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
|
||||
static_cast<unsigned long long>(days), static_cast<unsigned long long>(hrs),
|
||||
static_cast<unsigned long long>(mins), static_cast<unsigned long long>(secs));
|
||||
} else {
|
||||
std::snprintf(uptimeLine, sizeof(uptimeLine), "%02llu:%02llu:%02llu UP",
|
||||
static_cast<unsigned long long>(hrs), static_cast<unsigned long long>(mins),
|
||||
static_cast<unsigned long long>(secs));
|
||||
}
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 68, uptimeLine, scaleSmall, 1);
|
||||
}
|
||||
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
|
||||
|
||||
DispTools::draw_to_display_async_start();
|
||||
}
|
||||
};
|
||||
|
||||
class ClockAppFactory final : public IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kClockAppName; }
|
||||
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<ClockApp>(context); }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_system.hpp"
|
||||
#include "font16x8.hpp"
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
#include "apps/menu_app.hpp"
|
||||
|
||||
#include "app_system.hpp"
|
||||
#include "font16x8.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
|
||||
struct MenuEntry {
|
||||
std::string name;
|
||||
std::size_t index = 0;
|
||||
};
|
||||
|
||||
class MenuApp final : public IApp {
|
||||
public:
|
||||
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
|
||||
|
||||
void onStart() override {
|
||||
refreshEntries();
|
||||
dirty = true;
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
if (event.type != AppEventType::Button)
|
||||
return;
|
||||
|
||||
const auto& current = event.button.current;
|
||||
const auto& previous = event.button.previous;
|
||||
|
||||
if (current.left && !previous.left) {
|
||||
moveSelection(-1);
|
||||
} else if (current.right && !previous.right) {
|
||||
moveSelection(+1);
|
||||
}
|
||||
|
||||
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
|
||||
if (launch)
|
||||
launchSelected();
|
||||
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
private:
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
std::vector<MenuEntry> entries;
|
||||
std::size_t selected = 0;
|
||||
|
||||
bool dirty = false;
|
||||
|
||||
void moveSelection(int step) {
|
||||
if (entries.empty())
|
||||
return;
|
||||
const int count = static_cast<int>(entries.size());
|
||||
int next = static_cast<int>(selected) + step;
|
||||
next = (next % count + count) % count;
|
||||
selected = static_cast<std::size_t>(next);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void launchSelected() {
|
||||
if (entries.empty())
|
||||
return;
|
||||
const auto target = entries[selected].index;
|
||||
if (context.system && context.system->currentFactoryIndex() == target)
|
||||
return;
|
||||
context.requestAppSwitchByIndex(target);
|
||||
}
|
||||
|
||||
void refreshEntries() {
|
||||
entries.clear();
|
||||
if (!context.system)
|
||||
return;
|
||||
const std::size_t total = context.system->appCount();
|
||||
for (std::size_t i = 0; i < total; ++i) {
|
||||
const IAppFactory* factory = context.system->factoryAt(i);
|
||||
if (!factory)
|
||||
continue;
|
||||
const char* name = factory->name();
|
||||
if (!name)
|
||||
continue;
|
||||
if (std::string_view(name) == kMenuAppNameView)
|
||||
continue;
|
||||
entries.push_back(MenuEntry{std::string(name), i});
|
||||
}
|
||||
if (selected >= entries.size())
|
||||
selected = entries.empty() ? 0 : entries.size() - 1;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
|
||||
const int width = font16x8::measureText(text, scale, letterSpacing);
|
||||
const int x = (fb.width() - width) / 2;
|
||||
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
|
||||
}
|
||||
|
||||
void drawPagerDots() {
|
||||
if (entries.size() <= 1)
|
||||
return;
|
||||
const int count = static_cast<int>(entries.size());
|
||||
const int spacing = 20;
|
||||
const int dotSize = 7;
|
||||
const int totalW = spacing * (count - 1);
|
||||
const int startX = (framebuffer.width() - totalW) / 2;
|
||||
const int baseline = framebuffer.height() - (font16x8::kGlyphHeight + 48);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const int cx = startX + i * spacing;
|
||||
for (int dx = -dotSize / 2; dx <= dotSize / 2; ++dx) {
|
||||
for (int dy = -dotSize / 2; dy <= dotSize / 2; ++dy) {
|
||||
const bool isSelected = (static_cast<std::size_t>(i) == selected);
|
||||
const bool on = isSelected || std::abs(dx) == dotSize / 2 || std::abs(dy) == dotSize / 2;
|
||||
if (on)
|
||||
framebuffer.drawPixel(cx + dx, baseline + dy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void renderIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
DispTools::draw_to_display_async_wait();
|
||||
framebuffer.clear(false);
|
||||
|
||||
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
|
||||
|
||||
if (entries.empty()) {
|
||||
drawCenteredText(framebuffer, framebuffer.height() / 2 - 18, "NO OTHER APPS", 2, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 72, "ADD MORE IN FIRMWARE", 1, 1);
|
||||
} else {
|
||||
const std::string& name = entries[selected].name;
|
||||
const int titleScale = 2;
|
||||
const int centerY = framebuffer.height() / 2 - (font16x8::kGlyphHeight * titleScale) / 2;
|
||||
drawCenteredText(framebuffer, centerY, name, titleScale, 0);
|
||||
|
||||
const std::string indexLabel = std::to_string(selected + 1) + "/" + std::to_string(entries.size());
|
||||
const int topRightX = framebuffer.width() - font16x8::measureText(indexLabel, 1, 0) - 16;
|
||||
font16x8::drawText(framebuffer, topRightX, 20, indexLabel, 1, true, 0);
|
||||
|
||||
drawPagerDots();
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 48, "A/SELECT START", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
|
||||
}
|
||||
|
||||
DispTools::draw_to_display_async_start();
|
||||
}
|
||||
};
|
||||
|
||||
class MenuAppFactory final : public IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kMenuAppName; }
|
||||
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<MenuApp>(context); }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
|
||||
#include "Fonts.hpp"
|
||||
#include "cardboy/gfx/Fonts.hpp"
|
||||
|
||||
void FbTty::draw_char(int col, int row) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
|
||||
Reference in New Issue
Block a user