mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
Compare commits
40 Commits
v2-board
...
5b75ff28e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b75ff28e0 | |||
| e9e371739b | |||
| 28411535bb | |||
| 54d5f85538 | |||
| c3295b9b01 | |||
| 7fc48e5e93 | |||
| afff3d0e02 | |||
| 0660c40ec4 | |||
| 8520ef556b | |||
| 4e78618556 | |||
| 49455d1b36 | |||
| ddf5a47c33 | |||
| 13cdcb01dd | |||
| 4c0fd5243f | |||
| 429d704c8c | |||
| 9a9e25e124 | |||
| ecf6d09651 | |||
| 413e021e49 | |||
| c9f0f59630 | |||
| 9420887392 | |||
| 7df84f1e81 | |||
| 4861d26d8a | |||
| e389a776be | |||
| 8b8d9d3a55 | |||
| 126d377836 | |||
| 3f8d90c18a | |||
| c439aecd03 | |||
| cd72c2d7df | |||
| 589c598b01 | |||
| 95a946e47f | |||
| 48d2089b69 | |||
| 3e9b7b4326 | |||
| e1004ff196 | |||
| 24df0fc825 | |||
| ab32731f4d | |||
| 474a0b2a43 | |||
| 35219c353c | |||
| 8180abed4c | |||
| 12d634ecc9 | |||
| 6a8f74384e |
2
Firmware/.gitignore
vendored
2
Firmware/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
build
|
||||
cmake-build*
|
||||
.idea
|
||||
.cache
|
||||
managed_components
|
||||
23
Firmware/.vscode/c_cpp_properties.json
vendored
Normal file
23
Firmware/.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ESP-IDF",
|
||||
"compilerPath": "${config:idf.toolsPath}/tools/riscv32-esp-elf/esp-14.2.0_20241119/riscv32-esp-elf/bin/riscv32-esp-elf-gcc",
|
||||
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
|
||||
"includePath": [
|
||||
"${config:idf.espIdfPath}/components/**",
|
||||
"${config:idf.espIdfPathWin}/components/**",
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"browse": {
|
||||
"path": [
|
||||
"${config:idf.espIdfPath}/components",
|
||||
"${config:idf.espIdfPathWin}/components",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"limitSymbolsToIncludedHeaders": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
15
Firmware/.vscode/launch.json
vendored
Normal file
15
Firmware/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "gdbtarget",
|
||||
"request": "attach",
|
||||
"name": "Eclipse CDT GDB Adapter"
|
||||
},
|
||||
{
|
||||
"type": "espidf",
|
||||
"name": "Launch",
|
||||
"request": "launch"
|
||||
}
|
||||
]
|
||||
}
|
||||
85
Firmware/.vscode/settings.json
vendored
Normal file
85
Firmware/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"idf.flashType": "JTAG",
|
||||
"idf.port": "/dev/tty.usbmodem12401",
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
"files.associations": {
|
||||
"bitset": "cpp",
|
||||
"chrono": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"random": "cpp",
|
||||
"fstream": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"regex": "cpp",
|
||||
"*.inc": "cpp",
|
||||
"vector": "cpp",
|
||||
"esp_partition.h": "c",
|
||||
"cstring": "cpp",
|
||||
"array": "cpp",
|
||||
"string_view": "cpp",
|
||||
"any": "cpp",
|
||||
"atomic": "cpp",
|
||||
"barrier": "cpp",
|
||||
"bit": "cpp",
|
||||
"cctype": "cpp",
|
||||
"charconv": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"codecvt": "cpp",
|
||||
"compare": "cpp",
|
||||
"complex": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"forward_list": "cpp",
|
||||
"list": "cpp",
|
||||
"map": "cpp",
|
||||
"set": "cpp",
|
||||
"string": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"exception": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"netfwd": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"ratio": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"format": "cpp",
|
||||
"future": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"latch": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numbers": "cpp",
|
||||
"ostream": "cpp",
|
||||
"semaphore": "cpp",
|
||||
"span": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"text_encoding": "cpp",
|
||||
"thread": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp"
|
||||
}
|
||||
}
|
||||
5
Firmware/components/sdk-esp/CMakeLists.txt
Normal file
5
Firmware/components/sdk-esp/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register()
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE cardboy_sdk cardboy_sdk)
|
||||
21
Firmware/dependencies.lock
Normal file
21
Firmware/dependencies.lock
Normal file
@@ -0,0 +1,21 @@
|
||||
dependencies:
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.5.1
|
||||
joltwallet/littlefs:
|
||||
component_hash: 8e12955f47e27e6070b76715a96d6c75fc2b44f069e8c33679332d9bdd3120c4
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.20.1
|
||||
direct_dependencies:
|
||||
- idf
|
||||
- joltwallet/littlefs
|
||||
manifest_hash: 261ed140a57f28f061ce29bcf3ae4833c35c16fa5e15670490bf2aacedefa622
|
||||
target: esp32h2
|
||||
version: 2.0.0
|
||||
@@ -1,5 +1,9 @@
|
||||
idf_component_register(SRCS
|
||||
src/hello_world_main.cpp
|
||||
src/app_main.cpp
|
||||
../sdk/apps/menu_app.cpp
|
||||
../sdk/apps/clock_app.cpp
|
||||
../sdk/apps/tetris_app.cpp
|
||||
../sdk/apps/gameboy_app.cpp
|
||||
src/display.cpp
|
||||
src/bat_mon.cpp
|
||||
src/spi_global.cpp
|
||||
@@ -9,5 +13,11 @@ idf_component_register(SRCS
|
||||
src/shutdowner.cpp
|
||||
src/buttons.cpp
|
||||
src/power_helper.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver
|
||||
INCLUDE_DIRS "include")
|
||||
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" "../sdk/include"
|
||||
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
|
||||
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
|
||||
17
Firmware/main/idf_component.yml
Normal file
17
Firmware/main/idf_component.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: '>=4.1.0'
|
||||
# # Put list of dependencies here
|
||||
# # For components maintained by Espressif:
|
||||
# component: "~1.0.0"
|
||||
# # For 3rd party components:
|
||||
# username/component: ">=1.0.0,<2.0.0"
|
||||
# username2/component2:
|
||||
# version: "~1.0.0"
|
||||
# # For transient dependencies `public` flag can be set.
|
||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
||||
joltwallet/littlefs: ^1.20
|
||||
28
Firmware/main/include/app_framework.hpp
Normal file
28
Firmware/main/include/app_framework.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
|
||||
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
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>
|
||||
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
|
||||
|
||||
using AppContext = cardboy::sdk::AppContext;
|
||||
|
||||
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;
|
||||
using IFilesystem = cardboy::sdk::IFilesystem;
|
||||
73
Firmware/main/include/app_platform.hpp
Normal file
73
Firmware/main/include/app_platform.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <buttons.hpp>
|
||||
#include <disp_tools.hpp>
|
||||
#include <power_helper.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
int width() const override { return cardboy::sdk::kDisplayWidth; }
|
||||
int height() const override { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
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) 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 final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
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)
|
||||
state.left = true;
|
||||
if (pressed & BTN_RIGHT)
|
||||
state.right = true;
|
||||
if (pressed & BTN_DOWN)
|
||||
state.down = true;
|
||||
if (pressed & BTN_A)
|
||||
state.a = true;
|
||||
if (pressed & BTN_B)
|
||||
state.b = true;
|
||||
if (pressed & BTN_SELECT)
|
||||
state.select = true;
|
||||
if (pressed & BTN_START)
|
||||
state.start = true;
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
class PlatformClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
std::uint32_t millis() override {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void sleep_ms(std::uint32_t ms) override {
|
||||
if (ms == 0)
|
||||
return;
|
||||
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
|
||||
}
|
||||
};
|
||||
5
Firmware/main/include/app_system.hpp
Normal file
5
Firmware/main/include/app_system.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
using AppSystem = cardboy::sdk::AppSystem;
|
||||
4
Firmware/main/include/apps/clock_app.hpp
Normal file
4
Firmware/main/include/apps/clock_app.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
3
Firmware/main/include/apps/gameboy_app.hpp
Normal file
3
Firmware/main/include/apps/gameboy_app.hpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
3
Firmware/main/include/apps/menu_app.hpp
Normal file
3
Firmware/main/include/apps/menu_app.hpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
3
Firmware/main/include/apps/tetris_app.hpp
Normal file
3
Firmware/main/include/apps/tetris_app.hpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
@@ -20,9 +20,10 @@ public:
|
||||
void pooler(); // FIXME:
|
||||
private:
|
||||
static inline i2c_device_config_t _dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x70,
|
||||
.scl_speed_hz = 100000,
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x36,
|
||||
.scl_speed_hz = 100000,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
BatMon();
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
#include "freertos/task.h"
|
||||
|
||||
typedef enum {
|
||||
L1 = 1 << 1,
|
||||
L2 = 1 << 6,
|
||||
L3 = 1 << 0,
|
||||
L4 = 1 << 7,
|
||||
R1 = 1 << 5,
|
||||
R2 = 1 << 2,
|
||||
R3 = 1 << 4,
|
||||
R4 = 1 << 3,
|
||||
BTN_START = 1 << 1,
|
||||
BTN_DOWN = 1 << 6,
|
||||
BTN_SELECT = 1 << 0,
|
||||
BTN_LEFT = 1 << 7,
|
||||
BTN_UP = 1 << 5,
|
||||
BTN_B = 1 << 2,
|
||||
BTN_RIGHT = 1 << 4,
|
||||
BTN_A = 1 << 3,
|
||||
} btn_num;
|
||||
|
||||
class Buttons {
|
||||
@@ -24,12 +24,16 @@ public:
|
||||
static Buttons& get();
|
||||
void pooler(); // FIXME:
|
||||
uint8_t get_pressed();
|
||||
void install_isr();
|
||||
void register_listener(TaskHandle_t task);
|
||||
|
||||
TaskHandle_t _pooler_task;
|
||||
|
||||
private:
|
||||
Buttons();
|
||||
|
||||
volatile uint8_t _current;
|
||||
TaskHandle_t _pooler_task;
|
||||
volatile TaskHandle_t _listener = nullptr;
|
||||
};
|
||||
|
||||
|
||||
|
||||
54
Firmware/main/include/buzzer.hpp
Normal file
54
Firmware/main/include/buzzer.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
// Simple piezo buzzer helper using LEDC (PWM) for square wave tones.
|
||||
// Provides a tiny queued pattern player for short game SFX without blocking.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class Buzzer {
|
||||
public:
|
||||
static Buzzer &get();
|
||||
|
||||
void init(); // call once from app_main
|
||||
|
||||
// Queue a tone. freq=0 => silence. gap_ms is silence after tone before next.
|
||||
void tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms = 0);
|
||||
|
||||
// Convenience SFX
|
||||
void beepRotate();
|
||||
void beepMove();
|
||||
void beepLock();
|
||||
void beepLines(int lines); // 1..4 lines
|
||||
void beepLevelUp(int level); // after increment
|
||||
void beepGameOver();
|
||||
|
||||
// Mute controls
|
||||
void setMuted(bool m);
|
||||
void toggleMuted();
|
||||
bool isMuted() const { return _muted; }
|
||||
|
||||
// Persistence
|
||||
void loadState();
|
||||
void saveState();
|
||||
|
||||
private:
|
||||
struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; };
|
||||
static constexpr int MAX_QUEUE = 16;
|
||||
Step _queue[MAX_QUEUE]{};
|
||||
int _q_head = 0; // inclusive
|
||||
int _q_tail = 0; // exclusive
|
||||
bool _running = false;
|
||||
bool _in_gap = false;
|
||||
void *_timer = nullptr; // esp_timer_handle_t (opaque here)
|
||||
bool _muted = false;
|
||||
|
||||
Buzzer() = default;
|
||||
void enqueue(const Step &s);
|
||||
bool empty() const { return _q_head == _q_tail; }
|
||||
Step &front() { return _queue[_q_head]; }
|
||||
void popFront();
|
||||
void startNext();
|
||||
void schedule(uint32_t ms, bool gapPhase);
|
||||
void applyFreq(uint32_t freq);
|
||||
static void timerCb(void *arg);
|
||||
void clearQueue() { _q_head = _q_tail = 0; }
|
||||
};
|
||||
@@ -7,23 +7,24 @@
|
||||
#define I2C_SCL GPIO_NUM_8
|
||||
#define I2C_SDA GPIO_NUM_9
|
||||
|
||||
#define SPI_MOSI GPIO_NUM_5
|
||||
#define SPI_MISO GPIO_NUM_0
|
||||
#define SPI_SCK GPIO_NUM_4
|
||||
#define SPI_DISP_CS GPIO_NUM_11
|
||||
#define SPI_MOSI GPIO_NUM_5
|
||||
#define SPI_MISO GPIO_NUM_0
|
||||
#define SPI_SCK GPIO_NUM_4
|
||||
#define SPI_DISP_CS GPIO_NUM_24
|
||||
#define SPI_DISP_DISP GPIO_NUM_11
|
||||
|
||||
#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
|
||||
|
||||
#define PWR_INT GPIO_NUM_10
|
||||
#define PWR_KILL GPIO_NUM_12
|
||||
|
||||
#define SHR_OUT GPIO_NUM_23
|
||||
#define SHR_CLK GPIO_NUM_3
|
||||
#define SHR_SH GPIO_NUM_2
|
||||
|
||||
#define DIRECT_BTN GPIO_NUM_1
|
||||
#define EXP_INT GPIO_NUM_1
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,24 +4,49 @@
|
||||
|
||||
#ifndef CB_DISP_TOOLS_HPP
|
||||
#define CB_DISP_TOOLS_HPP
|
||||
|
||||
#include <display.hpp>
|
||||
|
||||
namespace DispTools {
|
||||
static void clear() {
|
||||
for (int y = 0; y < DISP_HEIGHT; y++) {
|
||||
for (int x = 0; x < DISP_WIDTH; x++) {
|
||||
SMD::set_pixel(x, y, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
static bool get_pixel(int x, int y) {
|
||||
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
assert(false);
|
||||
assert(false); // Not implemented
|
||||
return true;
|
||||
// return disp_frame[y][x];
|
||||
}
|
||||
static void reset_pixel(int x, int y) {
|
||||
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
assert(false);
|
||||
SMD::set_pixel(x, y, false);
|
||||
}
|
||||
static void set_pixel(int x, int y) {
|
||||
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
assert(false);
|
||||
|
||||
class DispTools {
|
||||
public:
|
||||
static DispTools& get();
|
||||
|
||||
void clear();
|
||||
bool get_pixel(int x, int y);
|
||||
void set_pixel(int x, int y);
|
||||
void reset_pixel(int x, int y);
|
||||
void draw_rectangle(int x1, int y1, int x2, int y2);
|
||||
void draw_line(int x1, int y1, int x2, int y2);
|
||||
void draw_circle(int x, int y, int r);
|
||||
void draw_to_display();
|
||||
|
||||
private:
|
||||
SMD::disp_frame_t disp_frame;
|
||||
SMD::set_pixel(x, y, true);
|
||||
}
|
||||
static void set_pixel(int x, int y, bool on) {
|
||||
if (on) {
|
||||
set_pixel(x, y);
|
||||
} else {
|
||||
reset_pixel(x, y);
|
||||
}
|
||||
}
|
||||
// New simplified async pipeline wrappers
|
||||
static void async_frame_start() { SMD::async_draw_wait(); } // call at frame start
|
||||
static void async_frame_end() { SMD::async_draw_start(); } // call after rendering
|
||||
// Legacy names (temporary) mapped to new API in case of straggling calls
|
||||
static void draw_to_display_async_start() { SMD::async_draw_start(); }
|
||||
static void draw_to_display_async_wait() { SMD::async_draw_wait(); }
|
||||
static bool draw_to_display_async_busy() { return SMD::async_draw_busy(); }
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -8,37 +8,55 @@
|
||||
#include "config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
// (Async memcpy removed for debugging simplification)
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
|
||||
class SMD {
|
||||
public:
|
||||
using disp_line_t = std::bitset<400>;
|
||||
using disp_frame_t = std::array<disp_line_t, 240>;
|
||||
|
||||
static SMD& get();
|
||||
void clear();
|
||||
void draw(const disp_frame_t& frame);
|
||||
namespace SMD {
|
||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
static constexpr size_t kLineMultiSingle = (kLineBytes + 2);
|
||||
static constexpr size_t kLineDataBytes = kLineMultiSingle * DISP_HEIGHT + 2;
|
||||
|
||||
private:
|
||||
SMD();
|
||||
static inline spi_device_interface_config_t _devcfg = {
|
||||
.mode = 0, // SPI mode 0
|
||||
.clock_speed_hz = 2 * 1000 * 1000, // Clock out at 10 MHz
|
||||
.spics_io_num = SPI_DISP_CS, // CS pin
|
||||
.flags = SPI_DEVICE_POSITIVE_CS,
|
||||
.queue_size = 3,
|
||||
// .pre_cb = lcd_spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
|
||||
};
|
||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
spi_device_handle_t _spi;
|
||||
bool _vcom = false;
|
||||
extern uint8_t* dma_buf;
|
||||
|
||||
static constexpr size_t kLineData = (kLineBytes + 4);
|
||||
std::array<uint8_t, kLineData> buf{};
|
||||
void init();
|
||||
// Double-buffered asynchronous frame pipeline:
|
||||
// Usage pattern each frame:
|
||||
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
|
||||
// ... write pixels into dma_buf via set_pixel / surface ...
|
||||
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
|
||||
// // is asynchronously cleared so the alternate buffer is ready for the next frame
|
||||
void async_draw_start();
|
||||
void async_draw_wait();
|
||||
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
|
||||
|
||||
std::array<uint8_t, kLineBytes> prep_line(const SMD::disp_line_t& line);
|
||||
static void set_pixel(int x, int y, bool value) {
|
||||
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
|
||||
|
||||
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
|
||||
unsigned bitIdx = 1 << (7 - (x % 8)) % 8;
|
||||
|
||||
if (value) {
|
||||
dma_buf[lineIdx] &= ~bitIdx;
|
||||
} else {
|
||||
dma_buf[lineIdx] |= bitIdx;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
|
||||
|
||||
static inline spi_device_interface_config_t _devcfg = {
|
||||
.mode = 0, // SPI mode 0
|
||||
.clock_speed_hz = 10 * 1000 * 1000, // Clock out at 10 MHz
|
||||
.spics_io_num = SPI_DISP_CS, // CS pin
|
||||
.flags = SPI_DEVICE_POSITIVE_CS,
|
||||
.queue_size = 1,
|
||||
.pre_cb = nullptr,
|
||||
.post_cb = s_spi_post_cb,
|
||||
};
|
||||
extern spi_device_handle_t _spi;
|
||||
}; // namespace SMD
|
||||
|
||||
#endif // DISPLAY_HPP
|
||||
|
||||
26
Firmware/main/include/fs_helper.hpp
Normal file
26
Firmware/main/include/fs_helper.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
class FsHelper {
|
||||
public:
|
||||
static FsHelper& get();
|
||||
|
||||
esp_err_t mount();
|
||||
void unmount();
|
||||
|
||||
bool isMounted() const { return mounted; }
|
||||
const char* basePath() const { return kBasePath; }
|
||||
const char* partitionLabel() const { return kPartitionLabel; }
|
||||
|
||||
private:
|
||||
FsHelper() = default;
|
||||
|
||||
static constexpr const char* kBasePath = "/lfs";
|
||||
static constexpr const char* kPartitionLabel = "littlefs";
|
||||
static constexpr const bool kFormatOnFailure = true;
|
||||
|
||||
bool mounted = false;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ private:
|
||||
.scl_io_num = I2C_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags = {.enable_internal_pullup = false, .allow_pd = true},
|
||||
.flags = {.enable_internal_pullup = true, .allow_pd = true},
|
||||
};
|
||||
i2c_master_bus_handle_t _bus_handle;
|
||||
}; // namespace i2c_global
|
||||
|
||||
5
Firmware/main/include/input_state.hpp
Normal file
5
Firmware/main/include/input_state.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
using InputState = cardboy::sdk::InputState;
|
||||
@@ -11,10 +11,10 @@ class PowerHelper {
|
||||
public:
|
||||
static PowerHelper& get();
|
||||
|
||||
bool is_slow() const;
|
||||
void set_slow(bool slow);
|
||||
void reset_slow_isr(); // FIXME:
|
||||
void delay(int slow_ms, int normal_ms);
|
||||
bool is_slow() const;
|
||||
void set_slow(bool slow);
|
||||
BaseType_t reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken);
|
||||
void delay(int slow_ms, int normal_ms);
|
||||
|
||||
void install_isr();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class Shutdowner {
|
||||
public:
|
||||
static Shutdowner& get();
|
||||
void install_isr();
|
||||
void shutdown();
|
||||
private:
|
||||
Shutdowner();
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ private:
|
||||
.sclk_io_num = SPI_SCK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 400 * 240 * 2};
|
||||
.max_transfer_sz = 12482U};
|
||||
|
||||
}; // namespace spi_global
|
||||
|
||||
|
||||
7
Firmware/main/roms/README.md
Normal file
7
Firmware/main/roms/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Built-in ROM placeholders
|
||||
|
||||
This directory holds Game Boy ROM images that get embedded into the firmware via `EMBED_FILES`.
|
||||
The repository includes two small placeholder files (`builtin_demo1.gb` and `builtin_demo2.gb`) so
|
||||
that the build system always has something to embed, but they are not valid games. Replace them
|
||||
with legally distributable ROMs to ship useful built-in titles. Filenames are used to derive the
|
||||
save-game slot name.
|
||||
342
Firmware/main/src/app_main.cpp
Normal file
342
Firmware/main/src/app_main.cpp
Normal file
@@ -0,0 +1,342 @@
|
||||
// Cardboy firmware entry point: boot platform services and run the modular app system.
|
||||
|
||||
#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>
|
||||
#include <buzzer.hpp>
|
||||
#include <disp_tools.hpp>
|
||||
#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>
|
||||
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#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"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#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(); }
|
||||
};
|
||||
|
||||
class EspFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
|
||||
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
|
||||
[[nodiscard]] std::string basePath() const override {
|
||||
const char* path = FsHelper::get().basePath();
|
||||
return path ? std::string(path) : std::string{};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||
namespace {
|
||||
|
||||
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
|
||||
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
|
||||
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
|
||||
constexpr uint32_t kStatsTaskStack = 4096;
|
||||
constexpr char kStatsTaskName[] = "TaskStats";
|
||||
|
||||
struct TaskRuntimeSample {
|
||||
TaskHandle_t handle;
|
||||
uint32_t runtime;
|
||||
};
|
||||
|
||||
struct TaskUsageRow {
|
||||
std::string name;
|
||||
uint64_t delta;
|
||||
UBaseType_t priority;
|
||||
uint32_t stackHighWaterBytes;
|
||||
bool isIdle;
|
||||
};
|
||||
|
||||
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
|
||||
if (current >= previous)
|
||||
return static_cast<uint64_t>(current - previous);
|
||||
return static_cast<uint64_t>(current) + (static_cast<uint64_t>(UINT32_MAX) - previous) + 1ULL;
|
||||
}
|
||||
|
||||
void task_usage_monitor(void*) {
|
||||
static constexpr char tag[] = "TaskUsage";
|
||||
std::vector<TaskRuntimeSample> lastSamples;
|
||||
uint32_t lastTotal = 0;
|
||||
|
||||
vTaskDelay(kStatsWarmupDelay);
|
||||
|
||||
while (true) {
|
||||
vTaskDelay(kStatsTaskDelayTicks);
|
||||
|
||||
const UBaseType_t taskCount = uxTaskGetNumberOfTasks();
|
||||
if (taskCount == 0)
|
||||
continue;
|
||||
|
||||
std::vector<TaskStatus_t> statusBuffer(taskCount);
|
||||
uint32_t totalRuntime = 0;
|
||||
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
|
||||
if (captured == 0)
|
||||
continue;
|
||||
statusBuffer.resize(captured);
|
||||
|
||||
std::vector<TaskRuntimeSample> currentSamples;
|
||||
currentSamples.reserve(statusBuffer.size());
|
||||
|
||||
if (lastTotal == 0) {
|
||||
for (const auto& status: statusBuffer) {
|
||||
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
|
||||
}
|
||||
lastSamples = std::move(currentSamples);
|
||||
lastTotal = totalRuntime;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t totalDelta = deltaWithWrap(totalRuntime, lastTotal);
|
||||
if (totalDelta == 0)
|
||||
continue;
|
||||
|
||||
std::vector<TaskUsageRow> rows;
|
||||
rows.reserve(statusBuffer.size());
|
||||
|
||||
uint64_t idleDelta = 0;
|
||||
uint64_t activeDelta = 0;
|
||||
uint64_t accountedDelta = 0;
|
||||
|
||||
for (const auto& status: statusBuffer) {
|
||||
const auto it = std::find_if(lastSamples.begin(), lastSamples.end(), [&](const TaskRuntimeSample& entry) {
|
||||
return entry.handle == status.xHandle;
|
||||
});
|
||||
|
||||
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
|
||||
const uint64_t taskDelta = (it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
|
||||
|
||||
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
|
||||
|
||||
TaskUsageRow row{
|
||||
.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
|
||||
.delta = taskDelta,
|
||||
.priority = status.uxCurrentPriority,
|
||||
.stackHighWaterBytes = static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
|
||||
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
|
||||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)
|
||||
};
|
||||
|
||||
rows.push_back(std::move(row));
|
||||
|
||||
accountedDelta += taskDelta;
|
||||
if (rows.back().isIdle)
|
||||
idleDelta += taskDelta;
|
||||
else
|
||||
activeDelta += taskDelta;
|
||||
}
|
||||
|
||||
lastSamples = std::move(currentSamples);
|
||||
lastTotal = totalRuntime;
|
||||
|
||||
if (rows.empty())
|
||||
continue;
|
||||
|
||||
std::sort(rows.begin(), rows.end(), [](const TaskUsageRow& a, const TaskUsageRow& b) {
|
||||
return a.delta > b.delta;
|
||||
});
|
||||
|
||||
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
|
||||
|
||||
std::printf("\n[%s] CPU usage over %.1f ms window\n", tag, windowMs);
|
||||
|
||||
for (const auto& row: rows) {
|
||||
if (row.delta == 0 || row.isIdle)
|
||||
continue;
|
||||
const double pct = (static_cast<double>(row.delta) * 100.0) / static_cast<double>(totalDelta);
|
||||
std::printf(" %-16s %6.2f%% (prio=%u stack_free=%lu B)\n", row.name.c_str(), pct, row.priority,
|
||||
static_cast<unsigned long>(row.stackHighWaterBytes));
|
||||
}
|
||||
|
||||
const double idlePct = (idleDelta * 100.0) / static_cast<double>(totalDelta);
|
||||
std::printf(" %-16s %6.2f%% (aggregated idle)\n", "<idle>", idlePct);
|
||||
|
||||
const uint64_t residual = (accountedDelta >= totalDelta) ? 0ULL : (totalDelta - accountedDelta);
|
||||
if (residual > 0) {
|
||||
const double residualPct = (static_cast<double>(residual) * 100.0) / static_cast<double>(totalDelta);
|
||||
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
|
||||
}
|
||||
|
||||
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag,
|
||||
(activeDelta * 100.0) / static_cast<double>(totalDelta), idlePct);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
void start_task_usage_monitor() {
|
||||
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#else
|
||||
inline void start_task_usage_monitor() {}
|
||||
#endif
|
||||
|
||||
extern "C" void app_main() {
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
// const esp_pm_config_t pm_config = {
|
||||
// .max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
|
||||
// .min_freq_mhz = 16,
|
||||
// .light_sleep_enable = true};
|
||||
// ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
|
||||
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||
#endif
|
||||
|
||||
PowerHelper::get();
|
||||
Shutdowner::get();
|
||||
Buttons::get();
|
||||
|
||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
Shutdowner::get().install_isr();
|
||||
PowerHelper::get().install_isr();
|
||||
Buttons::get().install_isr();
|
||||
|
||||
I2cGlobal::get();
|
||||
BatMon::get();
|
||||
SpiGlobal::get();
|
||||
SMD::init();
|
||||
|
||||
DispTools::clear();
|
||||
Buzzer::get().init();
|
||||
|
||||
FsHelper::get().mount();
|
||||
|
||||
static PlatformFramebuffer framebuffer;
|
||||
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 EspFilesystem filesystemService;
|
||||
|
||||
static cardboy::sdk::Services services{};
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResClockService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
|
||||
AppContext context(framebuffer, input, clock);
|
||||
context.services = &services;
|
||||
AppSystem system(context);
|
||||
context.system = &system;
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createClockAppFactory());
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
|
||||
start_task_usage_monitor();
|
||||
|
||||
system.run();
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
#include "shutdowner.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
|
||||
@@ -20,25 +21,78 @@ BatMon& BatMon::get() {
|
||||
|
||||
static void start_pooler(void* arg) { static_cast<BatMon*>(arg)->pooler(); }
|
||||
|
||||
void WriteRegister(uint8_t reg, uint16_t value) {
|
||||
uint8_t buf2[3];
|
||||
buf2[0] = reg;
|
||||
buf2[1] = value & 0xFF;
|
||||
buf2[2] = value >> 8;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
}
|
||||
|
||||
uint16_t ReadRegister(uint8_t reg) {
|
||||
uint16_t buffer;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
|
||||
return buffer;
|
||||
}
|
||||
void WriteAndVerifyRegister(char RegisterAddress, int RegisterValueToWrite) {
|
||||
int attempt = 0;
|
||||
uint16_t RegisterValueRead;
|
||||
do {
|
||||
WriteRegister(RegisterAddress, RegisterValueToWrite);
|
||||
vTaskDelay(1 / portTICK_PERIOD_MS);
|
||||
RegisterValueRead = ReadRegister(RegisterAddress);
|
||||
} while (RegisterValueToWrite != RegisterValueRead && attempt++ < 3);
|
||||
}
|
||||
|
||||
static constexpr float RSense = 0.1; // 100mOhm
|
||||
static constexpr uint16_t DesignCapMah = 180; // 100mOhm
|
||||
|
||||
constexpr float mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
|
||||
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.0) / RSense; }
|
||||
constexpr float regToCurrent(uint16_t reg) {
|
||||
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
|
||||
}
|
||||
constexpr uint16_t currentToReg(float current) { return static_cast<uint16_t>(current * RSense / 0.0015625f); }
|
||||
constexpr float regToVoltage(uint16_t reg) {
|
||||
return reg * 0.078125f * 0.001f; // Convert to volts
|
||||
}
|
||||
constexpr uint16_t voltageToReg(float voltage) {
|
||||
return static_cast<uint16_t>(voltage / (0.078125f * 0.001f)); // Convert to register value
|
||||
}
|
||||
static constexpr uint16_t DesignCap = mahToCap(DesignCapMah);
|
||||
static constexpr uint16_t IchgTerm = currentToReg(10);
|
||||
static constexpr uint16_t VEmpty = 0b1001011001100001; // (3V/3.88V)
|
||||
static constexpr uint16_t dQAcc = (DesignCap / 32);
|
||||
|
||||
BatMon::BatMon() {
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &_dev_cfg, &dev_handle));
|
||||
|
||||
uint8_t reg = 1;
|
||||
uint8_t buffer;
|
||||
uint8_t buf2[2];
|
||||
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
||||
if (buffer & (1 << 4)) // POR reset
|
||||
bool StatusPOR = ReadRegister(0x00) & 0x0002;
|
||||
if (StatusPOR) // POR reset
|
||||
{
|
||||
printf("Gas gauge reset!\n");
|
||||
buf2[0] = 1;
|
||||
buf2[1] = 0 << 4;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
while (ReadRegister(0x3D) & 1)
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
buf2[0] = 0;
|
||||
buf2[1] = 1 << 4 | 1 << 2; // 10 bit adc
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
uint16_t HibCFG = ReadRegister(0xBA); // Store original HibCFG value
|
||||
WriteRegister(0x60, 0x90); // Exit Hibernate Mode step 1
|
||||
WriteRegister(0xBA, 0x0); // Exit Hibernate Mode step 2
|
||||
WriteRegister(0x60, 0x0); // Exit Hibernate Mode step 3
|
||||
WriteRegister(0x18, DesignCap); // Write DesignCap
|
||||
WriteRegister(0x45, DesignCap / 32); // Write dQAcc
|
||||
WriteRegister(0x1E, IchgTerm); // Write IchgTerm
|
||||
WriteRegister(0x3A, VEmpty); // Write VEmpty
|
||||
WriteRegister(0x46, dQAcc * 44138 / DesignCap); // Write dPAcc
|
||||
WriteRegister(0xDB, 0x8000); // Write ModelCFG
|
||||
|
||||
// Poll ModelCFG.Refresh(highest bit), proceed to Step 4 when ModelCFG.Refresh = 0.
|
||||
while (ReadRegister(0xDB) & 0x8000)
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS); // 10ms wait loop. Do not continue until ModelCFG.Refresh == 0.
|
||||
WriteRegister(0xBA, HibCFG); // Restore Original HibCFG value
|
||||
|
||||
uint16_t Status = ReadRegister(0x00); // Read Status
|
||||
WriteAndVerifyRegister(0x00, Status & 0xFFFD); // Write and Verify Status with POR bit cleared
|
||||
}
|
||||
|
||||
xTaskCreate(&start_pooler, "BatMon", 2048, this, tskIDLE_PRIORITY, &_pooler_task);
|
||||
@@ -48,28 +102,13 @@ void BatMon::pooler() {
|
||||
while (true) {
|
||||
uint8_t reg = 8;
|
||||
uint16_t buffer;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
|
||||
float voltage = buffer;
|
||||
voltage *= 2.44f;
|
||||
voltage /= 1000;
|
||||
_voltage = voltage;
|
||||
reg = 2;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
|
||||
float charge = *reinterpret_cast<int16_t*>(&buffer);
|
||||
charge *= 6.70f;
|
||||
charge /= 50;
|
||||
_charge = charge;
|
||||
reg = 6;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
|
||||
float current = static_cast<int16_t>(buffer << 2);
|
||||
current *= 11.77f;
|
||||
current /= 50;
|
||||
current /= 4;
|
||||
_current = current;
|
||||
_charge = capToMah(ReadRegister(0x05));
|
||||
_current = regToCurrent(ReadRegister(0x0B));
|
||||
_voltage = regToVoltage(ReadRegister(0x09));
|
||||
PowerHelper::get().delay(10000, 1000);
|
||||
if (_voltage < 3.0f) {
|
||||
Shutdowner::get().shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "i2c_global.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
static inline i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x20,
|
||||
.scl_speed_hz = 50000,
|
||||
};
|
||||
|
||||
Buttons& Buttons::get() {
|
||||
static Buttons buttons;
|
||||
@@ -21,18 +29,48 @@ Buttons& Buttons::get() {
|
||||
|
||||
static void start_pooler(void* arg) { static_cast<Buttons*>(arg)->pooler(); }
|
||||
|
||||
Buttons::Buttons() {
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(SHR_OUT));
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(SHR_CLK));
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(SHR_SH));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(SHR_OUT, GPIO_MODE_INPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_pull_mode(SHR_OUT, GPIO_FLOATING));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(SHR_SH, GPIO_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(SHR_CLK, GPIO_MODE_OUTPUT));
|
||||
static bool is_on_low;
|
||||
|
||||
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 1, &_pooler_task);
|
||||
static void wakeup(void* arg) {
|
||||
if (is_on_low) {
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_HIGH_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_HIGH_LEVEL));
|
||||
is_on_low = false;
|
||||
|
||||
BaseType_t xResult = pdFAIL;
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
|
||||
xTaskNotifyFromISR(Buttons::get()._pooler_task, 0, eNoAction, &xHigherPriorityTaskWoken);
|
||||
|
||||
PowerHelper::get().reset_slow_isr(&xHigherPriorityTaskWoken);
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
} else {
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
is_on_low = true;
|
||||
}
|
||||
}
|
||||
|
||||
Buttons::Buttons() {
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &dev_cfg, &dev_handle));
|
||||
uint8_t buf2[2];
|
||||
|
||||
// Config
|
||||
buf2[0] = 6;
|
||||
buf2[1] = 0xFF;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
buf2[0] = 7;
|
||||
buf2[1] = 0x80;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 2, &_pooler_task);
|
||||
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(EXP_INT));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(EXP_INT, GPIO_MODE_INPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_pull_mode(EXP_INT, GPIO_FLOATING));
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
is_on_low = true;
|
||||
}
|
||||
|
||||
static void delay(unsigned long long loop) {
|
||||
for (unsigned long long i = 0; i < loop; i++) {
|
||||
@@ -42,18 +80,21 @@ static void delay(unsigned long long loop) {
|
||||
|
||||
void Buttons::pooler() {
|
||||
while (true) {
|
||||
ESP_ERROR_CHECK(gpio_set_level(SHR_SH, 0));
|
||||
ESP_ERROR_CHECK(gpio_set_level(SHR_SH, 1));
|
||||
|
||||
uint8_t new_val = 0;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
ESP_ERROR_CHECK(gpio_set_level(SHR_CLK, 0));
|
||||
new_val |= gpio_get_level(SHR_OUT) << i;
|
||||
ESP_ERROR_CHECK(gpio_set_level(SHR_CLK, 1));
|
||||
}
|
||||
_current = new_val;
|
||||
PowerHelper::get().delay(10000, 100);
|
||||
BaseType_t xResult = xTaskNotifyWait(pdFALSE, ULONG_MAX, nullptr, portMAX_DELAY);
|
||||
uint8_t reg = 0;
|
||||
uint8_t buffer;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
||||
_current = buffer;
|
||||
// read second port too to clear the interrupt
|
||||
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; }
|
||||
|
||||
191
Firmware/main/src/buzzer.cpp
Normal file
191
Firmware/main/src/buzzer.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
// Buzzer implementation
|
||||
#include "buzzer.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <driver/ledc.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_timer.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <nvs.h>
|
||||
|
||||
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
|
||||
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
|
||||
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
|
||||
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
|
||||
|
||||
Buzzer &Buzzer::get() {
|
||||
static Buzzer b;
|
||||
return b;
|
||||
}
|
||||
|
||||
void Buzzer::init() {
|
||||
// Initialize NVS once (safe if already done)
|
||||
static bool nvsInited = false;
|
||||
if (!nvsInited) {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
}
|
||||
nvsInited = true;
|
||||
}
|
||||
ledc_timer_config_t tcfg{};
|
||||
tcfg.speed_mode = LEDC_MODE;
|
||||
tcfg.timer_num = LEDC_TIMER;
|
||||
tcfg.duty_resolution = LEDC_BITS;
|
||||
tcfg.freq_hz = 1000; // placeholder, changed per tone
|
||||
tcfg.clk_cfg = LEDC_AUTO_CLK;
|
||||
ESP_ERROR_CHECK(ledc_timer_config(&tcfg));
|
||||
|
||||
ledc_channel_config_t ccfg{};
|
||||
ccfg.speed_mode = LEDC_MODE;
|
||||
ccfg.channel = LEDC_CH;
|
||||
ccfg.timer_sel = LEDC_TIMER;
|
||||
ccfg.gpio_num = static_cast<int>(BUZZER_PIN);
|
||||
ccfg.duty = 0; // start silent
|
||||
ccfg.hpoint = 0;
|
||||
ccfg.intr_type = LEDC_INTR_DISABLE;
|
||||
ESP_ERROR_CHECK(ledc_channel_config(&ccfg));
|
||||
|
||||
esp_timer_create_args_t args{};
|
||||
args.callback = &Buzzer::timerCb;
|
||||
args.arg = this;
|
||||
args.name = "buzz";
|
||||
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
|
||||
loadState();
|
||||
}
|
||||
|
||||
void Buzzer::applyFreq(uint32_t freq) {
|
||||
if (freq == 0) {
|
||||
ledc_stop(LEDC_MODE, LEDC_CH, 0);
|
||||
return;
|
||||
}
|
||||
ledc_set_freq(LEDC_MODE, LEDC_TIMER, freq);
|
||||
ledc_set_duty(LEDC_MODE, LEDC_CH, (1 << LEDC_BITS) / 2);
|
||||
ledc_update_duty(LEDC_MODE, LEDC_CH);
|
||||
}
|
||||
|
||||
void Buzzer::enqueue(const Step &s) {
|
||||
int nextTail = (_q_tail + 1) % MAX_QUEUE;
|
||||
if (nextTail == _q_head) { // full, drop oldest
|
||||
_q_head = (_q_head + 1) % MAX_QUEUE;
|
||||
}
|
||||
_queue[_q_tail] = s;
|
||||
_q_tail = nextTail;
|
||||
}
|
||||
|
||||
void Buzzer::popFront() {
|
||||
if (!empty())
|
||||
_q_head = (_q_head + 1) % MAX_QUEUE;
|
||||
}
|
||||
|
||||
void Buzzer::startNext() {
|
||||
if (empty()) {
|
||||
_running = false;
|
||||
applyFreq(0);
|
||||
return;
|
||||
}
|
||||
_running = true;
|
||||
_in_gap = false;
|
||||
Step &s = front();
|
||||
applyFreq(s.freq);
|
||||
schedule(s.dur_ms, false);
|
||||
}
|
||||
|
||||
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
|
||||
if (!_timer) return;
|
||||
_in_gap = gapPhase;
|
||||
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
|
||||
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
|
||||
}
|
||||
|
||||
void Buzzer::timerCb(void *arg) {
|
||||
auto *self = static_cast<Buzzer*>(arg);
|
||||
if (!self) return;
|
||||
if (self->_in_gap) {
|
||||
self->popFront();
|
||||
self->startNext();
|
||||
return;
|
||||
}
|
||||
// Tone finished
|
||||
if (!self->empty()) {
|
||||
auto &s = self->front();
|
||||
if (s.gap_ms) {
|
||||
self->applyFreq(0);
|
||||
self->schedule(s.gap_ms, true);
|
||||
return;
|
||||
}
|
||||
self->popFront();
|
||||
self->startNext();
|
||||
}
|
||||
}
|
||||
|
||||
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
|
||||
if (_muted) return; // ignore while muted
|
||||
Step s{freq, duration_ms, gap_ms};
|
||||
enqueue(s);
|
||||
if (!_running)
|
||||
startNext();
|
||||
}
|
||||
|
||||
// ---- Game SFX ----
|
||||
void Buzzer::beepRotate() { tone(1800, 25); }
|
||||
void Buzzer::beepMove() { tone(1200, 12); }
|
||||
void Buzzer::beepLock() { tone(900, 25); }
|
||||
void Buzzer::beepLines(int lines) {
|
||||
static const uint32_t base = 1100;
|
||||
for (int i = 0; i < lines; ++i) {
|
||||
tone(base + i * 190, 40, 12);
|
||||
}
|
||||
}
|
||||
void Buzzer::beepLevelUp(int) {
|
||||
tone(1600, 70, 25);
|
||||
tone(2000, 90, 0);
|
||||
}
|
||||
void Buzzer::beepGameOver() {
|
||||
tone(1000, 140, 40);
|
||||
tone(700, 140, 40);
|
||||
tone(400, 260, 0);
|
||||
}
|
||||
|
||||
void Buzzer::setMuted(bool m) {
|
||||
if (m == _muted) return;
|
||||
_muted = m;
|
||||
if (_muted) {
|
||||
clearQueue();
|
||||
applyFreq(0);
|
||||
if (_timer) {
|
||||
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
|
||||
}
|
||||
_running = false;
|
||||
_in_gap = false;
|
||||
} else {
|
||||
// confirmation chirp
|
||||
tone(1500, 40, 10);
|
||||
tone(1900, 60, 0);
|
||||
}
|
||||
saveState();
|
||||
}
|
||||
|
||||
void Buzzer::toggleMuted() { setMuted(!_muted); }
|
||||
|
||||
void Buzzer::loadState() {
|
||||
nvs_handle_t h;
|
||||
if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) {
|
||||
uint8_t v = 0;
|
||||
if (nvs_get_u8(h, "mute", &v) == ESP_OK) {
|
||||
_muted = (v != 0);
|
||||
}
|
||||
nvs_close(h);
|
||||
}
|
||||
if (_muted) applyFreq(0);
|
||||
}
|
||||
|
||||
void Buzzer::saveState() {
|
||||
nvs_handle_t h;
|
||||
if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) {
|
||||
nvs_set_u8(h, "mute", _muted ? 1 : 0);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
}
|
||||
}
|
||||
@@ -8,68 +8,3 @@
|
||||
|
||||
#include <display.hpp>
|
||||
|
||||
DispTools& DispTools::get() {
|
||||
static DispTools disp_tools;
|
||||
return disp_tools;
|
||||
}
|
||||
|
||||
void DispTools::clear() {
|
||||
for (int y = 0; y < DISP_HEIGHT; y++) {
|
||||
for (int x = 0; x < DISP_WIDTH; x++) {
|
||||
disp_frame[y][x] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool DispTools::get_pixel(int x, int y) { return disp_frame[y][x]; }
|
||||
void DispTools::reset_pixel(int x, int y) { disp_frame[y][x] = true; }
|
||||
void DispTools::set_pixel(int x, int y) { disp_frame[y][x] = false; }
|
||||
void DispTools::draw_rectangle(int x1, int y1, int x2, int y2) {
|
||||
int dy = y2 - y1;
|
||||
while (std::abs(dy) > 0) {
|
||||
draw_line(x1, y1 + dy, x2, y1 + dy);
|
||||
dy += (dy > 0) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
void DispTools::draw_line(int x1, int y1, int x2, int y2) {
|
||||
int dx = x2 - x1;
|
||||
int dy = y2 - y1;
|
||||
int a = 0, b = 0, diff = 0;
|
||||
|
||||
if (dx == 0) {
|
||||
while (dy != 0) {
|
||||
set_pixel(x1, y1 + dy);
|
||||
dy += (dy > 0) ? -1 : 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (dy == 0) {
|
||||
while (dx != 0) {
|
||||
set_pixel(x1 + dx, y1);
|
||||
dx += (dx > 0) ? -1 : 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (std::abs(a) <= std::abs(dx) && std::abs(b) <= std::abs(dy)) {
|
||||
set_pixel(x1 + a, y1 + b);
|
||||
if (diff < 0) {
|
||||
a += (dx > 0) ? 1 : -1;
|
||||
diff += std::abs(dy);
|
||||
} else {
|
||||
b += (dy > 0) ? 1 : -1;
|
||||
diff -= std::abs(dx);
|
||||
}
|
||||
}
|
||||
}
|
||||
void DispTools::draw_circle(int x, int y, int r) {
|
||||
if (r > 181)
|
||||
return;
|
||||
int dy = -r;
|
||||
while (dy <= r) {
|
||||
int dx = static_cast<int>(std::sqrt(r * r - dy * dy));
|
||||
draw_line(x - dx, y + dy, x + dx, y + dy);
|
||||
dy++;
|
||||
}
|
||||
}
|
||||
void DispTools::draw_to_display() { SMD::get().draw(disp_frame); }
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
|
||||
#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++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
bool color = fonts_Terminess_Powerline[_buf[col][row]][y] & (1 << (8 - x));
|
||||
if (color)
|
||||
DispTools::get().set_pixel(col * 8 + x, row * 16 + y);
|
||||
DispTools::set_pixel(col * 8 + x, row * 16 + y);
|
||||
else
|
||||
DispTools::get().reset_pixel(col * 8 + x, row * 16 + y);
|
||||
DispTools::reset_pixel(col * 8 + x, row * 16 + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,145 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
// Double-buffered display implementation with async memcpy ---------------------------------
|
||||
|
||||
#include "display.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include "disp_tools.hpp"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_async_memcpy.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
// This solution is attributed to Rich Schroeppel in the Programming Hacks section
|
||||
// TODO: Why does the device flag not work?
|
||||
unsigned char reverse_bits3(unsigned char b) { return (b * 0x0202020202ULL & 0x010884422010ULL) % 0x3ff; }
|
||||
DMA_ATTR static uint8_t s_dma_buffer0[SMD::kLineDataBytes]{};
|
||||
DMA_ATTR static uint8_t s_dma_buffer1[SMD::kLineDataBytes]{};
|
||||
static uint8_t* s_dma_buffers[2] = {s_dma_buffer0, s_dma_buffer1};
|
||||
DMA_ATTR static uint8_t dma_buf_template[SMD::kLineDataBytes]{};
|
||||
|
||||
std::array<uint8_t, SMD::kLineBytes> SMD::prep_line(const SMD::disp_line_t& line) {
|
||||
std::array<uint8_t, kLineBytes> data{};
|
||||
for (int i = 0; i < DISP_WIDTH; i++) {
|
||||
data[i / 8] = data[i / 8] | (line[i] << (i % 8));
|
||||
}
|
||||
for (int i = 0; i < kLineBytes; i++) {
|
||||
data[i] = reverse_bits3(data[i]);
|
||||
}
|
||||
return data;
|
||||
uint8_t* SMD::dma_buf = s_dma_buffers[0];
|
||||
|
||||
spi_device_handle_t SMD::_spi;
|
||||
|
||||
static spi_transaction_t _tx{};
|
||||
static SemaphoreHandle_t _txSem = nullptr;
|
||||
static bool _vcom = false;
|
||||
static TaskHandle_t s_clearTaskHandle = nullptr;
|
||||
static SemaphoreHandle_t s_clearReqSem = nullptr;
|
||||
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
|
||||
|
||||
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
|
||||
// update the maximum data stream supported by underlying DMA engine
|
||||
static async_memcpy_handle_t driver = nullptr;
|
||||
|
||||
static volatile int s_drawBufIdx = 0;
|
||||
|
||||
static unsigned char reverse_bits3(unsigned char b) { return (b * 0x0202020202ULL & 0x010884422010ULL) % 0x3ff; }
|
||||
|
||||
static bool IRAM_ATTR my_async_memcpy_cb(async_memcpy_handle_t /*mcp_hdl*/, async_memcpy_event_t* /*event*/,
|
||||
void* cb_args) {
|
||||
BaseType_t high_task_wakeup = pdFALSE;
|
||||
auto sem = static_cast<SemaphoreHandle_t>(cb_args);
|
||||
xSemaphoreGiveFromISR(sem, &high_task_wakeup);
|
||||
return high_task_wakeup == pdTRUE;
|
||||
}
|
||||
|
||||
SMD& SMD::get() {
|
||||
static SMD smd;
|
||||
return smd;
|
||||
extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
|
||||
BaseType_t hpw = pdFALSE;
|
||||
xSemaphoreGiveFromISR(s_clearReqSem, &hpw);
|
||||
if (hpw)
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
SMD::SMD() { spi_bus_add_device(SPI_BUS, &_devcfg, &_spi); }
|
||||
|
||||
void SMD::clear() {
|
||||
std::array<uint8_t, 2> buf{};
|
||||
buf[0] = 0b00100000;
|
||||
spi_transaction_t t{};
|
||||
|
||||
t.tx_buffer = buf.data();
|
||||
t.length = buf.size() * 8;
|
||||
ESP_ERROR_CHECK(spi_device_transmit(_spi, &t));
|
||||
}
|
||||
|
||||
void SMD::draw(const disp_frame_t& frame) {
|
||||
_vcom = !_vcom;
|
||||
for (uint8_t i = 0; i < DISP_HEIGHT; i++) {
|
||||
spi_transaction_t t{};
|
||||
|
||||
t.tx_buffer = buf.data();
|
||||
t.length = buf.size() * 8;
|
||||
|
||||
buf[0] = 0b10000000 | (_vcom << 6);
|
||||
buf[1] = reverse_bits3(i + 1);
|
||||
|
||||
auto prepared = prep_line(frame.at(i));
|
||||
memcpy(buf.data() + 2, prepared.data(), kLineBytes);
|
||||
|
||||
buf[2 + kLineBytes] = 0;
|
||||
buf[2 + kLineBytes + 1] = 0;
|
||||
|
||||
ESP_ERROR_CHECK(spi_device_transmit(_spi, &t));
|
||||
static void clear_task(void*) {
|
||||
for (;;) {
|
||||
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
|
||||
spi_transaction_t* r = nullptr;
|
||||
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
|
||||
int bufIdx = (int) r->user;
|
||||
xSemaphoreGive(_txSem);
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, 12480U,
|
||||
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SMD::init() {
|
||||
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(SPI_DISP_DISP));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(SPI_DISP_DISP, GPIO_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_level(SPI_DISP_DISP, 1));
|
||||
ESP_ERROR_CHECK(gpio_hold_en(SPI_DISP_DISP));
|
||||
for (int buf = 0; buf < 2; ++buf) {
|
||||
auto* fb = s_dma_buffers[buf];
|
||||
for (uint8_t i = 0; i < DISP_HEIGHT; ++i) {
|
||||
fb[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
||||
fb[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
||||
}
|
||||
fb[kLineDataBytes - 1] = 0;
|
||||
}
|
||||
|
||||
s_drawBufIdx = 0;
|
||||
dma_buf = s_dma_buffers[s_drawBufIdx];
|
||||
|
||||
for (int y = 0; y < DISP_HEIGHT; ++y)
|
||||
for (int x = 0; x < DISP_WIDTH; ++x)
|
||||
set_pixel(x, y, false);
|
||||
|
||||
std::memcpy(dma_buf_template, dma_buf, sizeof(dma_buf_template));
|
||||
std::memcpy(s_dma_buffers[1], dma_buf_template, sizeof(dma_buf_template));
|
||||
ESP_ERROR_CHECK(esp_async_memcpy_install(&config, &driver)); // install driver with default DMA engine
|
||||
|
||||
s_clearReqSem = xSemaphoreCreateBinary();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
s_bufferSem[i] = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(s_bufferSem[i]);
|
||||
}
|
||||
|
||||
_txSem = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(_txSem);
|
||||
xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle);
|
||||
}
|
||||
|
||||
bool SMD::async_draw_busy() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
|
||||
|
||||
void SMD::async_draw_start() {
|
||||
assert(driver != nullptr);
|
||||
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
|
||||
assert(false);
|
||||
|
||||
const int sendIdx = s_drawBufIdx;
|
||||
assert(sendIdx >= 0 && sendIdx < 2);
|
||||
|
||||
SemaphoreHandle_t sem = s_bufferSem[sendIdx];
|
||||
if (!xSemaphoreTake(sem, 0))
|
||||
assert(false);
|
||||
|
||||
const int nextDrawIdx = sendIdx ^ 1;
|
||||
|
||||
_vcom = !_vcom;
|
||||
_tx = {};
|
||||
_tx.tx_buffer = s_dma_buffers[sendIdx];
|
||||
_tx.length = SMD::kLineDataBytes * 8;
|
||||
_tx.user = (void*) (sendIdx);
|
||||
s_dma_buffers[sendIdx][0] = 0b10000000 | (_vcom << 6);
|
||||
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
||||
|
||||
s_drawBufIdx = nextDrawIdx;
|
||||
dma_buf = s_dma_buffers[nextDrawIdx];
|
||||
}
|
||||
|
||||
void SMD::async_draw_wait() {
|
||||
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
|
||||
// uint64_t waitedUs = 0;
|
||||
if (!uxSemaphoreGetCount(sem)) {
|
||||
// uint64_t start = esp_timer_get_time();
|
||||
if (!xSemaphoreTake(sem, portMAX_DELAY))
|
||||
assert(false);
|
||||
if (!xSemaphoreGive(sem))
|
||||
assert(false);
|
||||
// waitedUs = esp_timer_get_time() - start;
|
||||
}
|
||||
// if (waitedUs)
|
||||
// printf("Waited %" PRIu64 " us\n", waitedUs);
|
||||
}
|
||||
|
||||
68
Firmware/main/src/fs_helper.cpp
Normal file
68
Firmware/main/src/fs_helper.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "fs_helper.hpp"
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_littlefs.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
constexpr const char* kTag = "FsHelper";
|
||||
} // namespace
|
||||
|
||||
FsHelper& FsHelper::get() {
|
||||
static FsHelper instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
esp_err_t FsHelper::mount() {
|
||||
if (mounted)
|
||||
return ESP_OK;
|
||||
|
||||
esp_vfs_littlefs_conf_t conf{};
|
||||
conf.base_path = kBasePath;
|
||||
conf.partition_label = kPartitionLabel;
|
||||
conf.format_if_mount_failed = kFormatOnFailure;
|
||||
conf.dont_mount = false;
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
conf.read_only = false;
|
||||
#endif
|
||||
|
||||
const esp_err_t err = esp_vfs_littlefs_register(&conf);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_NOT_FOUND) {
|
||||
ESP_LOGE(kTag, "Failed to find LittleFS partition '%s'", kPartitionLabel);
|
||||
} else if (err == ESP_FAIL) {
|
||||
ESP_LOGE(kTag, "Failed to mount LittleFS at %s (consider enabling format)",
|
||||
kBasePath);
|
||||
} else {
|
||||
ESP_LOGE(kTag, "esp_vfs_littlefs_register failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
mounted = true;
|
||||
|
||||
size_t total = 0;
|
||||
size_t used = 0;
|
||||
const esp_err_t infoErr = esp_littlefs_info(kPartitionLabel, &total, &used);
|
||||
if (infoErr == ESP_OK) {
|
||||
ESP_LOGI(kTag, "LittleFS mounted at %s (%zu / %zu bytes used)", kBasePath, used, total);
|
||||
} else {
|
||||
ESP_LOGW(kTag, "LittleFS mounted but failed to query usage: %s", esp_err_to_name(infoErr));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void FsHelper::unmount() {
|
||||
if (!mounted)
|
||||
return;
|
||||
const esp_err_t err = esp_vfs_littlefs_unregister(kPartitionLabel);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "Failed to unmount LittleFS (%s)", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
mounted = false;
|
||||
ESP_LOGI(kTag, "LittleFS unmounted from %s", kBasePath);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
#include <buttons.hpp>
|
||||
#include <cstdint>
|
||||
#include <disp_tools.hpp>
|
||||
#include <disp_tty.hpp>
|
||||
#include <esp_pm.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "display.hpp"
|
||||
|
||||
#include "bat_mon.hpp"
|
||||
#include "driver/i2c_master.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "i2c_global.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <memory>
|
||||
#include <power_helper.hpp>
|
||||
#include <shutdowner.hpp>
|
||||
#include <spi_global.hpp>
|
||||
#include <string>
|
||||
|
||||
FbTty tty;
|
||||
|
||||
|
||||
extern "C" void app_main() {
|
||||
esp_pm_config_t pm_config = {
|
||||
.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ, .min_freq_mhz = 16, .light_sleep_enable = true};
|
||||
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
|
||||
printf("Hello world!\n");
|
||||
// TODO: Where to put that?
|
||||
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||
// For some reason, calling it here hangs on startup, sometimes
|
||||
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
PowerHelper::get();
|
||||
Shutdowner::get();
|
||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
Shutdowner::get().install_isr();
|
||||
PowerHelper::get().install_isr();
|
||||
Buttons::get();
|
||||
I2cGlobal::get();
|
||||
BatMon::get();
|
||||
SpiGlobal::get();
|
||||
SMD::get();
|
||||
SMD::get().clear();
|
||||
DispTools::get().clear();
|
||||
DispTools::get().draw_line(0, 0, 400, 240);
|
||||
DispTools::get().draw_circle(100, 100, 20);
|
||||
DispTools::get().draw_to_display();
|
||||
tty.putstr("Hello\nworld!");
|
||||
DispTools::get().draw_to_display();
|
||||
|
||||
int rx = 30, ry = 30;
|
||||
|
||||
int lastmove = 0;
|
||||
|
||||
while (true) {
|
||||
// SMD::clear();
|
||||
// printf("Voltage: %f\n", BatMon::get_voltage());
|
||||
DispTools::get().clear();
|
||||
tty.reset();
|
||||
|
||||
uint8_t pressed = Buttons::get().get_pressed();
|
||||
if (pressed & L3)
|
||||
rx -= 5;
|
||||
if (pressed & L4)
|
||||
ry += 5;
|
||||
if (pressed & R3)
|
||||
ry -= 5;
|
||||
if (pressed & R4)
|
||||
rx += 5;
|
||||
|
||||
if (pressed == 0 && !PowerHelper::get().is_slow())
|
||||
lastmove++;
|
||||
else if (pressed != 0) {
|
||||
lastmove = 0;
|
||||
PowerHelper::get().set_slow(false);
|
||||
}
|
||||
|
||||
if (lastmove > 20) {
|
||||
lastmove = 0;
|
||||
PowerHelper::get().set_slow(true);
|
||||
}
|
||||
|
||||
bool slow = PowerHelper::get().is_slow();
|
||||
tty.fmt("{:.1f}mA {:.1f}V {:.1f}mAh {}", BatMon::get().get_current(), BatMon::get().get_voltage(),
|
||||
BatMon::get().get_charge(), slow ? "S" : "");
|
||||
|
||||
if (rx < 30)
|
||||
rx = 30;
|
||||
if (rx > 370)
|
||||
rx = 370;
|
||||
if (ry < 30)
|
||||
ry = 30;
|
||||
if (ry > 210)
|
||||
ry = 210;
|
||||
// tty.fmt("Button: {}", pressed);
|
||||
DispTools::get().draw_circle(rx, ry, 20);
|
||||
// printf("Restarting in %d seconds...\n", i);
|
||||
DispTools::get().draw_to_display();
|
||||
PowerHelper::get().delay(10000, 30);
|
||||
}
|
||||
// printf("Restarting now.\n");
|
||||
// fflush(stdout);
|
||||
// esp_restart();
|
||||
}
|
||||
@@ -25,32 +25,22 @@ void PowerHelper::set_slow(bool slow) {
|
||||
}
|
||||
}
|
||||
|
||||
void PowerHelper::reset_slow_isr() {
|
||||
BaseType_t PowerHelper::reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken) {
|
||||
_slow = false;
|
||||
return xEventGroupSetBitsFromISR(_event_group, 1, xHigherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
static void wakeup(void* arg) {
|
||||
BaseType_t xHigherPriorityTaskWoken, xResult;
|
||||
|
||||
xHigherPriorityTaskWoken = pdFALSE;
|
||||
|
||||
_slow = false;
|
||||
xResult = xEventGroupSetBitsFromISR(_event_group, 1, &xHigherPriorityTaskWoken);
|
||||
|
||||
xResult = static_cast<PowerHelper*>(arg)->reset_slow_isr(&xHigherPriorityTaskWoken);
|
||||
if (xResult != pdFAIL) {
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void wakeup(void* arg) { static_cast<PowerHelper*>(arg)->reset_slow_isr(); }
|
||||
|
||||
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) {
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(DIRECT_BTN));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(DIRECT_BTN, GPIO_MODE_INPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_pull_mode(DIRECT_BTN, GPIO_FLOATING));
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(DIRECT_BTN, GPIO_INTR_HIGH_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(DIRECT_BTN, GPIO_INTR_HIGH_LEVEL));
|
||||
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
// gpio_isr_handler_add(DIRECT_BTN, wakeup, this);
|
||||
|
||||
set_slow(false);
|
||||
}
|
||||
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) { set_slow(false); }
|
||||
|
||||
void PowerHelper::delay(int slow_ms, int normal_ms) {
|
||||
if (is_slow()) {
|
||||
@@ -67,4 +57,6 @@ void PowerHelper::delay(int slow_ms, int normal_ms) {
|
||||
vTaskDelay(normal_ms / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
void PowerHelper::install_isr() { gpio_isr_handler_add(DIRECT_BTN, wakeup, this); }
|
||||
void PowerHelper::install_isr() {
|
||||
// gpio_isr_handler_add(EXP_INT, wakeup, this);
|
||||
}
|
||||
|
||||
@@ -14,26 +14,31 @@ Shutdowner& Shutdowner::get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR shutdown(void* arg) {
|
||||
static void IRAM_ATTR int_shutdown(void* arg) {
|
||||
// printf("Shutting down...\n");
|
||||
ESP_ERROR_CHECK(gpio_hold_dis(PWR_KILL));
|
||||
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 0));
|
||||
}
|
||||
|
||||
Shutdowner::Shutdowner() {
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(PWR_INT));
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(PWR_KILL));
|
||||
void Shutdowner::shutdown() {
|
||||
ESP_ERROR_CHECK(gpio_hold_dis(PWR_KILL));
|
||||
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 0));
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(gpio_set_direction(PWR_INT, GPIO_MODE_INPUT));
|
||||
Shutdowner::Shutdowner() {
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(PWR_KILL));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(PWR_KILL, GPIO_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 1));
|
||||
ESP_ERROR_CHECK(gpio_hold_en(PWR_KILL));
|
||||
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(PWR_INT));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(PWR_INT, GPIO_MODE_INPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_pull_mode(PWR_INT, GPIO_FLOATING));
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(PWR_INT, GPIO_INTR_LOW_LEVEL));
|
||||
// ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(PWR_INT, GPIO_INTR_LOW_LEVEL));
|
||||
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
ESP_ERROR_CHECK(gpio_hold_en(PWR_KILL));
|
||||
// gpio_isr_handler_add(PWR_INT, shutdown, nullptr);
|
||||
}
|
||||
|
||||
void Shutdowner::install_isr() { gpio_isr_handler_add(PWR_INT, shutdown, nullptr); }
|
||||
void Shutdowner::install_isr() { gpio_isr_handler_add(PWR_INT, int_shutdown, nullptr); }
|
||||
|
||||
6
Firmware/partitions.csv
Normal file
6
Firmware/partitions.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
phy_init, data, phy, 0x10000, 0x1000,
|
||||
factory, app, factory, 0x20000, 0x250000,
|
||||
littlefs, data, littlefs,, 0x190000,
|
||||
|
76
Firmware/sdk/.clang-format
Normal file
76
Firmware/sdk/.clang-format
Normal file
@@ -0,0 +1,76 @@
|
||||
# Generated from CLion C/C++ Code Style settings
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignTrailingComments:
|
||||
Kind: Always
|
||||
OverEmptyLines: 2
|
||||
SpacesBeforeTrailingComments: 1
|
||||
AlignOperands: Align
|
||||
AlignEscapedNewlines: Right
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBraces: Custom
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 120
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ContinuationIndentWidth: 8
|
||||
IncludeCategories:
|
||||
- Regex: '^<.*'
|
||||
Priority: 1
|
||||
- Regex: '^".*'
|
||||
Priority: 2
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||
IndentCaseLabels: true
|
||||
IndentWidth: 4
|
||||
InsertNewlineAtEOF: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 2
|
||||
PointerAlignment: Left
|
||||
SpaceAfterCStyleCast: true
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeRangeBasedForLoopColon: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesInAngles: false
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
...
|
||||
70
Firmware/sdk/CMakeLists.txt
Normal file
70
Firmware/sdk/CMakeLists.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(cardboy_sdk LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
||||
set(CMAKE_CXX_EXTENSIONS NO)
|
||||
|
||||
add_library(cardboy_sdk STATIC
|
||||
src/app_system.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_sdk
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||
|
||||
add_library(cardboy_apps STATIC
|
||||
apps/menu_app.cpp
|
||||
apps/clock_app.cpp
|
||||
apps/tetris_app.cpp
|
||||
apps/gameboy_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_apps
|
||||
PUBLIC
|
||||
cardboy_sdk
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
||||
|
||||
option(CARDBOY_BUILD_SFML "Build SFML harness" OFF)
|
||||
|
||||
if (CARDBOY_BUILD_SFML)
|
||||
include(FetchContent)
|
||||
|
||||
set(SFML_BUILD_AUDIO OFF CACHE BOOL "Disable SFML audio module" FORCE)
|
||||
set(SFML_BUILD_NETWORK OFF CACHE BOOL "Disable SFML network module" FORCE)
|
||||
set(SFML_BUILD_EXAMPLES OFF CACHE BOOL "Disable SFML examples" FORCE)
|
||||
set(SFML_BUILD_TESTS OFF CACHE BOOL "Disable SFML tests" FORCE)
|
||||
set(SFML_USE_SYSTEM_DEPS OFF CACHE BOOL "Use bundled SFML dependencies" FORCE)
|
||||
|
||||
FetchContent_Declare(
|
||||
SFML
|
||||
GIT_REPOSITORY https://github.com/SFML/SFML.git
|
||||
GIT_TAG 3.0.2
|
||||
GIT_SHALLOW ON
|
||||
)
|
||||
FetchContent_MakeAvailable(SFML)
|
||||
|
||||
add_executable(cardboy_desktop
|
||||
hosts/sfml_main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_desktop
|
||||
PRIVATE
|
||||
cardboy_apps
|
||||
SFML::Graphics
|
||||
SFML::Window
|
||||
SFML::System
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
|
||||
endif ()
|
||||
239
Firmware/sdk/apps/clock_app.cpp
Normal file
239
Firmware/sdk/apps/clock_app.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
|
||||
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;
|
||||
std::uint64_t uptimeSeconds = 0;
|
||||
};
|
||||
|
||||
class ClockApp final : public cardboy::sdk::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 cardboy::sdk::AppEvent& event) override {
|
||||
switch (event.type) {
|
||||
case cardboy::sdk::AppEventType::Button:
|
||||
handleButtonEvent(event.button);
|
||||
break;
|
||||
case cardboy::sdk::AppEventType::Timer:
|
||||
if (event.timer.handle == refreshTimer)
|
||||
updateDisplay();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
Clock& clock;
|
||||
|
||||
bool use24Hour = true;
|
||||
bool dirty = false;
|
||||
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
TimeSnapshot lastSnapshot{};
|
||||
|
||||
void cancelRefreshTimer() {
|
||||
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(refreshTimer);
|
||||
refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void handleButtonEvent(const cardboy::sdk::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;
|
||||
|
||||
std::time_t raw = 0;
|
||||
if (std::time(&raw) != static_cast<std::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;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
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 std::uint64_t days = snap.uptimeSeconds / 86400ULL;
|
||||
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
||||
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
||||
const std::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);
|
||||
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
};
|
||||
|
||||
class ClockAppFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kClockAppName; }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||
return std::make_unique<ClockApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
1413
Firmware/sdk/apps/gameboy_app.cpp
Normal file
1413
Firmware/sdk/apps/gameboy_app.cpp
Normal file
File diff suppressed because it is too large
Load Diff
177
Firmware/sdk/apps/menu_app.cpp
Normal file
177
Firmware/sdk/apps/menu_app.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
|
||||
struct MenuEntry {
|
||||
std::string name;
|
||||
std::size_t index = 0;
|
||||
};
|
||||
|
||||
class MenuApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
|
||||
|
||||
void onStart() override {
|
||||
refreshEntries();
|
||||
dirty = true;
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
||||
if (event.type != cardboy::sdk::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 cardboy::sdk::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;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
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);
|
||||
}
|
||||
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
};
|
||||
|
||||
class MenuAppFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kMenuAppName; }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||
return std::make_unique<MenuApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
624
Firmware/sdk/apps/tetris_app.cpp
Normal file
624
Firmware/sdk/apps/tetris_app.cpp
Normal file
@@ -0,0 +1,624 @@
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace apps {
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppButtonEvent;
|
||||
using cardboy::sdk::AppContext;
|
||||
using cardboy::sdk::AppEvent;
|
||||
using cardboy::sdk::AppEventType;
|
||||
using cardboy::sdk::AppTimerHandle;
|
||||
using cardboy::sdk::InputState;
|
||||
|
||||
constexpr char kTetrisAppName[] = "Tetris";
|
||||
|
||||
constexpr int kBoardWidth = 10;
|
||||
constexpr int kBoardHeight = 20;
|
||||
constexpr int kCellSize = 10;
|
||||
|
||||
constexpr std::array<int, 5> kLineScores = {0, 40, 100, 300, 1200};
|
||||
|
||||
struct BlockOffset {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
|
||||
struct Tetromino {
|
||||
std::array<std::array<BlockOffset, 4>, 4> rotations{};
|
||||
};
|
||||
|
||||
constexpr std::array<BlockOffset, 4> makeOffsets(std::initializer_list<BlockOffset> blocks) {
|
||||
std::array<BlockOffset, 4> out{};
|
||||
std::size_t idx = 0;
|
||||
for (const auto& b: blocks) {
|
||||
out[idx++] = b;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
constexpr std::array<BlockOffset, 4> rotate(const std::array<BlockOffset, 4>& src) {
|
||||
std::array<BlockOffset, 4> out{};
|
||||
for (std::size_t i = 0; i < src.size(); ++i) {
|
||||
out[i].x = -src[i].y;
|
||||
out[i].y = src[i].x;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
constexpr Tetromino makeTetromino(std::initializer_list<BlockOffset> baseBlocks) {
|
||||
Tetromino tet{};
|
||||
tet.rotations[0] = makeOffsets(baseBlocks);
|
||||
for (int r = 1; r < 4; ++r)
|
||||
tet.rotations[r] = rotate(tet.rotations[r - 1]);
|
||||
return tet;
|
||||
}
|
||||
|
||||
constexpr std::array<Tetromino, 7> kPieces = {{
|
||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
|
||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
|
||||
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
|
||||
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
|
||||
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
|
||||
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
|
||||
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
|
||||
}};
|
||||
|
||||
class RandomBag {
|
||||
public:
|
||||
RandomBag() { refill(); }
|
||||
|
||||
void seed(std::uint32_t value) { rng.seed(value); }
|
||||
|
||||
int next() {
|
||||
if (bag.empty())
|
||||
refill();
|
||||
int val = bag.back();
|
||||
bag.pop_back();
|
||||
return val;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<int> bag;
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
|
||||
void refill() {
|
||||
bag.clear();
|
||||
bag.reserve(7);
|
||||
for (int i = 0; i < 7; ++i)
|
||||
bag.push_back(i);
|
||||
std::shuffle(bag.begin(), bag.end(), rng);
|
||||
}
|
||||
};
|
||||
|
||||
struct ActivePiece {
|
||||
int type = 0;
|
||||
int rotation = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
|
||||
struct GameState {
|
||||
std::array<int, kBoardWidth * kBoardHeight> board{};
|
||||
ActivePiece current{};
|
||||
int nextPiece = 0;
|
||||
int level = 1;
|
||||
int linesCleared = 0;
|
||||
int score = 0;
|
||||
int highScore = 0;
|
||||
bool paused = false;
|
||||
bool gameOver = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
|
||||
if (auto* rnd = ctx.random())
|
||||
return rnd->nextUint32();
|
||||
static std::random_device rd;
|
||||
return rd();
|
||||
}
|
||||
|
||||
class TetrisGame {
|
||||
public:
|
||||
explicit TetrisGame(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {
|
||||
bag.seed(randomSeed(context));
|
||||
loadHighScore();
|
||||
reset();
|
||||
}
|
||||
|
||||
void onStart() {
|
||||
scheduleDropTimer();
|
||||
dirty = true;
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
void onStop() { cancelTimers(); }
|
||||
|
||||
void handleEvent(const AppEvent& event) {
|
||||
switch (event.type) {
|
||||
case AppEventType::Button:
|
||||
handleButtons(event.button);
|
||||
break;
|
||||
case AppEventType::Timer:
|
||||
handleTimer(event.timer.handle);
|
||||
break;
|
||||
}
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
private:
|
||||
AppContext& context;
|
||||
typename AppContext::Framebuffer& framebuffer;
|
||||
|
||||
GameState state;
|
||||
RandomBag bag;
|
||||
InputState lastInput{};
|
||||
bool dirty = false;
|
||||
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
void reset() {
|
||||
cancelTimers();
|
||||
int oldHigh = state.highScore;
|
||||
state = {};
|
||||
state.highScore = oldHigh;
|
||||
state.current.type = bag.next();
|
||||
state.nextPiece = bag.next();
|
||||
state.current.x = kBoardWidth / 2;
|
||||
state.current.y = 0;
|
||||
state.level = 1;
|
||||
state.gameOver = false;
|
||||
state.paused = false;
|
||||
dirty = true;
|
||||
scheduleDropTimer();
|
||||
if (auto* power = context.powerManager())
|
||||
power->setSlowMode(false);
|
||||
}
|
||||
|
||||
void handleButtons(const AppButtonEvent& evt) {
|
||||
const auto& cur = evt.current;
|
||||
const auto& prev = evt.previous;
|
||||
lastInput = cur;
|
||||
|
||||
if (cur.b && !prev.b) {
|
||||
context.requestAppSwitchByName(kMenuAppName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cur.start && !prev.start) {
|
||||
if (state.gameOver) {
|
||||
reset();
|
||||
} else {
|
||||
state.paused = !state.paused;
|
||||
if (auto* power = context.powerManager())
|
||||
power->setSlowMode(state.paused);
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (state.paused || state.gameOver)
|
||||
return;
|
||||
|
||||
if (cur.left && !prev.left)
|
||||
tryMove(-1, 0);
|
||||
if (cur.right && !prev.right)
|
||||
tryMove(1, 0);
|
||||
if (cur.a && !prev.a)
|
||||
rotate(1);
|
||||
if (cur.select && !prev.select)
|
||||
hardDrop();
|
||||
|
||||
if (cur.down && !prev.down) {
|
||||
softDropStep();
|
||||
scheduleSoftDropTimer();
|
||||
} else if (!cur.down && prev.down) {
|
||||
cancelSoftDropTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void handleTimer(AppTimerHandle handle) {
|
||||
if (handle == dropTimer) {
|
||||
if (!state.paused && !state.gameOver)
|
||||
gravityStep();
|
||||
} else if (handle == softTimer) {
|
||||
if (lastInput.down && !state.paused && !state.gameOver)
|
||||
softDropStep();
|
||||
else
|
||||
cancelSoftDropTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelTimers() {
|
||||
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(dropTimer);
|
||||
dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
cancelSoftDropTimer();
|
||||
}
|
||||
|
||||
void cancelSoftDropTimer() {
|
||||
if (softTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(softTimer);
|
||||
softTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void scheduleDropTimer() {
|
||||
cancelDropTimer();
|
||||
const std::uint32_t interval = dropIntervalMs();
|
||||
dropTimer = context.scheduleRepeatingTimer(interval);
|
||||
}
|
||||
|
||||
void cancelDropTimer() {
|
||||
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||
context.cancelTimer(dropTimer);
|
||||
dropTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void scheduleSoftDropTimer() {
|
||||
cancelSoftDropTimer();
|
||||
softTimer = context.scheduleRepeatingTimer(60);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t dropIntervalMs() const {
|
||||
const int base = 700;
|
||||
const int step = 50;
|
||||
int interval = base - (state.level - 1) * step;
|
||||
if (interval < 120)
|
||||
interval = 120;
|
||||
return static_cast<std::uint32_t>(interval);
|
||||
}
|
||||
|
||||
[[nodiscard]] const Tetromino& currentPiece() const { return kPieces[state.current.type]; }
|
||||
|
||||
bool canPlace(int nx, int ny, int rot) const {
|
||||
const auto& piece = kPieces[state.current.type];
|
||||
rot = ((rot % 4) + 4) % 4;
|
||||
for (const auto& block: piece.rotations[rot]) {
|
||||
int gx = nx + block.x;
|
||||
int gy = ny + block.y;
|
||||
if (gx < 0 || gx >= kBoardWidth)
|
||||
return false;
|
||||
if (gy >= kBoardHeight)
|
||||
return false;
|
||||
if (gy >= 0 && cellAt(gx, gy) != 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] int cellAt(int x, int y) const { return state.board[y * kBoardWidth + x]; }
|
||||
|
||||
void setCell(int x, int y, int value) { state.board[y * kBoardWidth + x] = value; }
|
||||
|
||||
void tryMove(int dx, int dy) {
|
||||
int nx = state.current.x + dx;
|
||||
int ny = state.current.y + dy;
|
||||
if (canPlace(nx, ny, state.current.rotation)) {
|
||||
state.current.x = nx;
|
||||
state.current.y = ny;
|
||||
dirty = true;
|
||||
if (dx != 0) {
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rotate(int direction) {
|
||||
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
|
||||
nextRot = ((nextRot % 4) + 4) % 4;
|
||||
if (canPlace(state.current.x, state.current.y, nextRot)) {
|
||||
state.current.rotation = nextRot;
|
||||
dirty = true;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepRotate();
|
||||
}
|
||||
}
|
||||
|
||||
void gravityStep() {
|
||||
if (!canPlace(state.current.x, state.current.y + 1, state.current.rotation)) {
|
||||
lockPiece();
|
||||
} else {
|
||||
state.current.y++;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void softDropStep() {
|
||||
if (canPlace(state.current.x, state.current.y + 1, state.current.rotation)) {
|
||||
state.current.y++;
|
||||
state.score += 1;
|
||||
updateHighScore();
|
||||
dirty = true;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
} else {
|
||||
lockPiece();
|
||||
}
|
||||
}
|
||||
|
||||
void hardDrop() {
|
||||
int distance = 0;
|
||||
while (canPlace(state.current.x, state.current.y + distance + 1, state.current.rotation))
|
||||
++distance;
|
||||
if (distance > 0) {
|
||||
state.current.y += distance;
|
||||
state.score += distance * 2;
|
||||
updateHighScore();
|
||||
dirty = true;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
}
|
||||
lockPiece();
|
||||
}
|
||||
|
||||
void lockPiece() {
|
||||
for (const auto& block: currentPiece().rotations[state.current.rotation]) {
|
||||
int gx = state.current.x + block.x;
|
||||
int gy = state.current.y + block.y;
|
||||
if (gy >= 0 && gy < kBoardHeight && gx >= 0 && gx < kBoardWidth)
|
||||
setCell(gx, gy, state.current.type + 1);
|
||||
if (gy < 0)
|
||||
state.gameOver = true;
|
||||
}
|
||||
|
||||
handleLineClear();
|
||||
spawnNext();
|
||||
dirty = true;
|
||||
|
||||
if (state.gameOver) {
|
||||
cancelSoftDropTimer();
|
||||
cancelDropTimer();
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepGameOver();
|
||||
if (auto* power = context.powerManager())
|
||||
power->setSlowMode(true);
|
||||
} else {
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepLock();
|
||||
}
|
||||
}
|
||||
|
||||
void handleLineClear() {
|
||||
int cleared = 0;
|
||||
for (int y = kBoardHeight - 1; y >= 0; --y) {
|
||||
bool full = true;
|
||||
for (int x = 0; x < kBoardWidth; ++x) {
|
||||
if (cellAt(x, y) == 0) {
|
||||
full = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (full) {
|
||||
++cleared;
|
||||
for (int pull = y; pull > 0; --pull)
|
||||
for (int x = 0; x < kBoardWidth; ++x)
|
||||
setCell(x, pull, cellAt(x, pull - 1));
|
||||
for (int x = 0; x < kBoardWidth; ++x)
|
||||
setCell(x, 0, 0);
|
||||
++y; // re-check same row after collapse
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared > 0) {
|
||||
state.linesCleared += cleared;
|
||||
if (cleared < static_cast<int>(kLineScores.size()))
|
||||
state.score += kLineScores[cleared] * state.level;
|
||||
else
|
||||
state.score += kLineScores.back() * state.level;
|
||||
|
||||
int newLevel = 1 + state.linesCleared / 10;
|
||||
if (newLevel != state.level) {
|
||||
state.level = newLevel;
|
||||
scheduleDropTimer();
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepLevelUp(state.level);
|
||||
}
|
||||
updateHighScore();
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepLines(cleared);
|
||||
}
|
||||
}
|
||||
|
||||
void spawnNext() {
|
||||
state.current.type = state.nextPiece;
|
||||
state.current.rotation = 0;
|
||||
state.current.x = kBoardWidth / 2;
|
||||
state.current.y = 0;
|
||||
state.nextPiece = bag.next();
|
||||
if (!canPlace(state.current.x, state.current.y, state.current.rotation))
|
||||
state.gameOver = true;
|
||||
}
|
||||
|
||||
void updateHighScore() {
|
||||
if (state.score > state.highScore) {
|
||||
state.highScore = state.score;
|
||||
if (auto* storage = context.storage())
|
||||
storage->writeUint32("tetris", "best", static_cast<std::uint32_t>(state.highScore));
|
||||
}
|
||||
}
|
||||
|
||||
void loadHighScore() {
|
||||
if (auto* storage = context.storage()) {
|
||||
std::uint32_t stored = 0;
|
||||
if (storage->readUint32("tetris", "best", stored))
|
||||
state.highScore = static_cast<int>(stored);
|
||||
}
|
||||
}
|
||||
|
||||
void renderIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
drawBoard();
|
||||
drawActivePiece();
|
||||
drawNextPreview();
|
||||
drawHUD();
|
||||
|
||||
framebuffer.endFrame();
|
||||
}
|
||||
|
||||
void drawBoard() {
|
||||
const int originX = (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
|
||||
const int originY = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
|
||||
|
||||
for (int y = 0; y < kBoardHeight; ++y) {
|
||||
for (int x = 0; x < kBoardWidth; ++x) {
|
||||
if (int value = cellAt(x, y); value != 0)
|
||||
drawCell(originX, originY, x, y, value, true);
|
||||
}
|
||||
}
|
||||
|
||||
drawGuides(originX, originY);
|
||||
}
|
||||
|
||||
void drawActivePiece() {
|
||||
if (state.gameOver)
|
||||
return;
|
||||
const int originX = (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
|
||||
const int originY = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
|
||||
|
||||
for (const auto& block: currentPiece().rotations[state.current.rotation]) {
|
||||
int gx = state.current.x + block.x;
|
||||
int gy = state.current.y + block.y;
|
||||
if (gy < 0)
|
||||
continue;
|
||||
drawCell(originX, originY, gx, gy, state.current.type + 1, false);
|
||||
}
|
||||
}
|
||||
|
||||
void drawCell(int originX, int originY, int cx, int cy, int value, bool solid) {
|
||||
const int x0 = originX + cx * kCellSize;
|
||||
const int y0 = originY + cy * kCellSize;
|
||||
for (int dy = 0; dy < kCellSize; ++dy) {
|
||||
for (int dx = 0; dx < kCellSize; ++dx) {
|
||||
bool on = solid ? true : (dx == 0 || dx == kCellSize - 1 || dy == 0 || dy == kCellSize - 1);
|
||||
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
|
||||
}
|
||||
}
|
||||
(void) value; // value currently unused (monochrome display)
|
||||
}
|
||||
|
||||
void drawGuides(int originX, int originY) {
|
||||
for (int y = 0; y <= kBoardHeight; ++y) {
|
||||
const int py = originY + y * kCellSize;
|
||||
for (int x = 0; x < kBoardWidth * kCellSize; ++x)
|
||||
framebuffer.drawPixel(originX + x, py, (y % 5) == 0);
|
||||
}
|
||||
for (int x = 0; x <= kBoardWidth; ++x) {
|
||||
const int px = originX + x * kCellSize;
|
||||
for (int y = 0; y < kBoardHeight * kCellSize; ++y)
|
||||
framebuffer.drawPixel(px, originY + y, (x % 5) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void drawNextPreview() {
|
||||
const int blockSize = kCellSize;
|
||||
const int boxSize = blockSize * 4;
|
||||
const int originX = (cardboy::sdk::kDisplayWidth + kBoardWidth * kCellSize) / 2 + 24;
|
||||
const int originY = (cardboy::sdk::kDisplayHeight - boxSize) / 2;
|
||||
|
||||
for (int dy = 0; dy < boxSize; ++dy)
|
||||
for (int dx = 0; dx < boxSize; ++dx)
|
||||
framebuffer.drawPixel(originX + dx, originY + dy, (dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
|
||||
|
||||
const auto& piece = kPieces[state.nextPiece];
|
||||
for (const auto& block: piece.rotations[0]) {
|
||||
const int px = originX + (block.x + 1) * blockSize;
|
||||
const int py = originY + (block.y + 1) * blockSize;
|
||||
for (int dy = 1; dy < blockSize - 1; ++dy)
|
||||
for (int dx = 1; dx < blockSize - 1; ++dx)
|
||||
framebuffer.drawPixel(px + dx, py + dy, true);
|
||||
}
|
||||
}
|
||||
|
||||
void drawLabel(int x, int y, std::string_view text, int scale = 1) {
|
||||
font16x8::drawText(framebuffer, x, y, text, scale, true, 1);
|
||||
}
|
||||
|
||||
void drawHUD() {
|
||||
const int margin = 16;
|
||||
drawLabel(margin, margin, "SCORE", 1);
|
||||
drawLabel(margin, margin + 16, std::to_string(state.score), 1);
|
||||
|
||||
drawLabel(margin, margin + 40, "BEST", 1);
|
||||
drawLabel(margin, margin + 56, std::to_string(state.highScore), 1);
|
||||
|
||||
drawLabel(margin, margin + 80, "LEVEL", 1);
|
||||
drawLabel(margin, margin + 96, std::to_string(state.level), 1);
|
||||
|
||||
if (auto* battery = context.battery(); battery && battery->hasData()) {
|
||||
char line[32];
|
||||
std::snprintf(line, sizeof(line), "BAT %.2fV", battery->voltage());
|
||||
drawLabel(margin, margin + 120, line, 1);
|
||||
}
|
||||
|
||||
drawLabel(margin, cardboy::sdk::kDisplayHeight - 48, "A ROTATE", 1);
|
||||
drawLabel(margin, cardboy::sdk::kDisplayHeight - 32, "DOWN DROP", 1);
|
||||
drawLabel(margin, cardboy::sdk::kDisplayHeight - 16, "B MENU", 1);
|
||||
|
||||
if (state.paused)
|
||||
drawCenteredBanner("PAUSED");
|
||||
else if (state.gameOver)
|
||||
drawCenteredBanner("GAME OVER");
|
||||
}
|
||||
|
||||
void drawCenteredBanner(std::string_view text) {
|
||||
const int w = font16x8::measureText(text, 2, 1);
|
||||
const int h = font16x8::kGlyphHeight * 2;
|
||||
const int x = (cardboy::sdk::kDisplayWidth - w) / 2;
|
||||
const int y = (cardboy::sdk::kDisplayHeight - h) / 2;
|
||||
for (int yy = -4; yy < h + 4; ++yy)
|
||||
for (int xx = -6; xx < w + 6; ++xx)
|
||||
framebuffer.drawPixel(x + xx, y + yy, yy == -4 || yy == h + 3 || xx == -6 || xx == w + 5);
|
||||
font16x8::drawText(framebuffer, x, y, text, 2, true, 1);
|
||||
}
|
||||
};
|
||||
|
||||
class TetrisApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
|
||||
|
||||
void onStart() override { game.onStart(); }
|
||||
void onStop() override { game.onStop(); }
|
||||
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
|
||||
|
||||
private:
|
||||
TetrisGame game;
|
||||
};
|
||||
|
||||
class TetrisFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kTetrisAppName; }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
|
||||
return std::make_unique<TetrisApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() {
|
||||
return std::make_unique<TetrisFactory>();
|
||||
}
|
||||
|
||||
} // namespace apps
|
||||
397
Firmware/sdk/hosts/sfml_main.cpp
Normal file
397
Firmware/sdk/hosts/sfml_main.cpp
Normal file
@@ -0,0 +1,397 @@
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kPixelScale = 2;
|
||||
|
||||
class DesktopBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {}
|
||||
void beepRotate() override {}
|
||||
void beepMove() override {}
|
||||
void beepLock() override {}
|
||||
void beepLines(int) override {}
|
||||
void beepLevelUp(int) override {}
|
||||
void beepGameOver() override {}
|
||||
};
|
||||
|
||||
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return false; }
|
||||
};
|
||||
|
||||
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
auto it = data.find(composeKey(ns, key));
|
||||
if (it == data.end())
|
||||
return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
data[composeKey(ns, key)] = value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::uint32_t> data;
|
||||
|
||||
static std::string composeKey(std::string_view ns, std::string_view key) {
|
||||
std::string result;
|
||||
result.reserve(ns.size() + key.size() + 1);
|
||||
result.append(ns.begin(), ns.end());
|
||||
result.push_back(':');
|
||||
result.append(key.begin(), key.end());
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits<std::uint32_t>::max()) {}
|
||||
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return dist(rng); }
|
||||
|
||||
private:
|
||||
std::mt19937 rng;
|
||||
std::uniform_int_distribution<std::uint32_t> dist;
|
||||
};
|
||||
|
||||
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
DesktopHighResClock() : start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
[[nodiscard]] std::uint64_t micros() override {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now - start).count());
|
||||
}
|
||||
|
||||
private:
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopPowerManager final : public cardboy::sdk::IPowerManager {
|
||||
public:
|
||||
void setSlowMode(bool enable) override { slowMode = enable; }
|
||||
[[nodiscard]] bool isSlowMode() const override { return slowMode; }
|
||||
|
||||
private:
|
||||
bool slowMode = false;
|
||||
};
|
||||
|
||||
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
DesktopFilesystem() {
|
||||
if (const char* env = std::getenv("CARDBOY_ROM_DIR"); env && *env) {
|
||||
basePathPath = std::filesystem::path(env);
|
||||
} else {
|
||||
basePathPath = std::filesystem::current_path() / "roms";
|
||||
}
|
||||
}
|
||||
|
||||
bool mount() override {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(basePathPath, ec)) {
|
||||
mounted = std::filesystem::is_directory(basePathPath, ec);
|
||||
} else {
|
||||
mounted = std::filesystem::create_directories(basePathPath, ec);
|
||||
}
|
||||
return mounted;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isMounted() const override { return mounted; }
|
||||
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
|
||||
|
||||
private:
|
||||
std::filesystem::path basePathPath;
|
||||
bool mounted = false;
|
||||
};
|
||||
|
||||
class DesktopRuntime;
|
||||
|
||||
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
explicit DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
int width() const override;
|
||||
int height() const override;
|
||||
void drawPixel(int x, int y, bool on) override;
|
||||
void clear(bool on) override;
|
||||
void beginFrame() override {}
|
||||
void endFrame() override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
};
|
||||
|
||||
class DesktopInput final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
explicit DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
cardboy::sdk::InputState readState() override { return state; }
|
||||
|
||||
void handleKey(sf::Keyboard::Key key, bool pressed);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
cardboy::sdk::InputState state{};
|
||||
};
|
||||
|
||||
class DesktopClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
explicit DesktopClock(DesktopRuntime& runtime)
|
||||
: runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint32_t millis() override {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
||||
}
|
||||
|
||||
void sleep_ms(std::uint32_t ms) override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopRuntime {
|
||||
private:
|
||||
friend class DesktopFramebuffer;
|
||||
friend class DesktopInput;
|
||||
friend class DesktopClock;
|
||||
|
||||
sf::RenderWindow window;
|
||||
sf::Texture texture;
|
||||
sf::Sprite sprite;
|
||||
std::vector<std::uint8_t> pixels; // RGBA buffer
|
||||
bool dirty = true;
|
||||
bool running = true;
|
||||
|
||||
DesktopBuzzer buzzerService;
|
||||
DesktopBattery batteryService;
|
||||
DesktopStorage storageService;
|
||||
DesktopRandom randomService;
|
||||
DesktopHighResClock highResService;
|
||||
DesktopPowerManager powerService;
|
||||
DesktopFilesystem filesystemService;
|
||||
cardboy::sdk::Services services{};
|
||||
|
||||
public:
|
||||
DesktopRuntime();
|
||||
|
||||
DesktopFramebuffer framebuffer;
|
||||
DesktopInput input;
|
||||
DesktopClock clock;
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry() { return services; }
|
||||
|
||||
void processEvents();
|
||||
void presentIfNeeded();
|
||||
void sleepFor(std::uint32_t ms);
|
||||
|
||||
[[nodiscard]] bool isRunning() const { return running; }
|
||||
|
||||
private:
|
||||
void setPixel(int x, int y, bool on);
|
||||
void clearPixels(bool on);
|
||||
};
|
||||
|
||||
DesktopRuntime::DesktopRuntime()
|
||||
: window(sf::VideoMode(sf::Vector2u{cardboy::sdk::kDisplayWidth * kPixelScale,
|
||||
cardboy::sdk::kDisplayHeight * kPixelScale}),
|
||||
"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) {
|
||||
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");
|
||||
sprite.setTexture(texture, true);
|
||||
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
|
||||
clearPixels(false);
|
||||
presentIfNeeded();
|
||||
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
}
|
||||
|
||||
void DesktopRuntime::setPixel(int x, int y, bool on) {
|
||||
if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight)
|
||||
return;
|
||||
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
|
||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||
pixels[idx + 0] = value;
|
||||
pixels[idx + 1] = value;
|
||||
pixels[idx + 2] = value;
|
||||
pixels[idx + 3] = 255;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::clearPixels(bool on) {
|
||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
||||
pixels[i + 0] = value;
|
||||
pixels[i + 1] = value;
|
||||
pixels[i + 2] = value;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::processEvents() {
|
||||
while (auto eventOpt = window.pollEvent()) {
|
||||
const sf::Event& event = *eventOpt;
|
||||
|
||||
if (event.is<sf::Event::Closed>()) {
|
||||
running = false;
|
||||
window.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyPressed = event.getIf<sf::Event::KeyPressed>()) {
|
||||
input.handleKey(keyPressed->code, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyReleased = event.getIf<sf::Event::KeyReleased>()) {
|
||||
input.handleKey(keyReleased->code, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopRuntime::presentIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
texture.update(pixels.data());
|
||||
window.clear(sf::Color::Black);
|
||||
window.draw(sprite);
|
||||
window.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void DesktopRuntime::sleepFor(std::uint32_t ms) {
|
||||
const auto target = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
|
||||
do {
|
||||
processEvents();
|
||||
presentIfNeeded();
|
||||
if (!running)
|
||||
std::exit(0);
|
||||
if (ms == 0)
|
||||
return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= target)
|
||||
return;
|
||||
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(target - now);
|
||||
if (remaining.count() > 2)
|
||||
std::this_thread::sleep_for(std::min<std::chrono::milliseconds>(remaining, std::chrono::milliseconds(8)));
|
||||
else
|
||||
std::this_thread::yield();
|
||||
} while (true);
|
||||
}
|
||||
|
||||
int DesktopFramebuffer::width() const { return cardboy::sdk::kDisplayWidth; }
|
||||
|
||||
int DesktopFramebuffer::height() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void DesktopFramebuffer::drawPixel(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
||||
|
||||
void DesktopFramebuffer::clear(bool on) { runtime.clearPixels(on); }
|
||||
|
||||
void DesktopFramebuffer::endFrame() { runtime.dirty = true; }
|
||||
|
||||
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
||||
switch (key) {
|
||||
case sf::Keyboard::Key::Up:
|
||||
state.up = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Down:
|
||||
state.down = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Left:
|
||||
state.left = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Right:
|
||||
state.right = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Z:
|
||||
case sf::Keyboard::Key::A:
|
||||
state.a = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::X:
|
||||
case sf::Keyboard::Key::S:
|
||||
state.b = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Space:
|
||||
state.select = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Enter:
|
||||
state.start = pressed;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopClock::sleep_ms(std::uint32_t ms) { runtime.sleepFor(ms); }
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DesktopRuntime runtime;
|
||||
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createClockAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
|
||||
system.run();
|
||||
} catch (const std::exception& ex) {
|
||||
std::fprintf(stderr, "Cardboy desktop runtime failed: %s\n", ex.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
12
Firmware/sdk/include/cardboy/apps/clock_app.hpp
Normal file
12
Firmware/sdk/include/cardboy/apps/clock_app.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kGameboyAppName[] = "Game Boy";
|
||||
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
16
Firmware/sdk/include/cardboy/apps/menu_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/menu_app.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/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<cardboy::sdk::IAppFactory> createMenuAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
4042
Firmware/sdk/include/cardboy/apps/peanut_gb.h
Normal file
4042
Firmware/sdk/include/cardboy/apps/peanut_gb.h
Normal file
File diff suppressed because it is too large
Load Diff
12
Firmware/sdk/include/cardboy/apps/tetris_app.hpp
Normal file
12
Firmware/sdk/include/cardboy/apps/tetris_app.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
62
Firmware/sdk/include/cardboy/gfx/font16x8.hpp
Normal file
62
Firmware/sdk/include/cardboy/gfx/font16x8.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/gfx/Fonts.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
|
||||
133
Firmware/sdk/include/cardboy/sdk/app_framework.hpp
Normal file
133
Firmware/sdk/include/cardboy/sdk/app_framework.hpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include "platform.hpp"
|
||||
#include "services.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class AppSystem;
|
||||
|
||||
using AppTimerHandle = std::uint32_t;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = 0;
|
||||
|
||||
enum class AppEventType {
|
||||
Button,
|
||||
Timer,
|
||||
};
|
||||
|
||||
struct AppButtonEvent {
|
||||
InputState current{};
|
||||
InputState previous{};
|
||||
};
|
||||
|
||||
struct AppTimerEvent {
|
||||
AppTimerHandle handle = kInvalidAppTimer;
|
||||
};
|
||||
|
||||
struct AppEvent {
|
||||
AppEventType type;
|
||||
std::uint32_t timestamp_ms = 0;
|
||||
AppButtonEvent button{};
|
||||
AppTimerEvent timer{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
struct BasicAppContext {
|
||||
using Framebuffer = FramebufferT;
|
||||
using Input = InputT;
|
||||
using Clock = ClockT;
|
||||
|
||||
BasicAppContext() = delete;
|
||||
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
|
||||
FramebufferT& framebuffer;
|
||||
InputT& input;
|
||||
ClockT& clock;
|
||||
AppSystem* system = nullptr;
|
||||
Services* services = nullptr;
|
||||
|
||||
[[nodiscard]] Services* getServices() const { return services; }
|
||||
|
||||
[[nodiscard]] IBuzzer* buzzer() const { return services ? services->buzzer : nullptr; }
|
||||
[[nodiscard]] IBatteryMonitor* battery() const { return services ? services->battery : nullptr; }
|
||||
[[nodiscard]] IStorage* storage() const { return services ? services->storage : nullptr; }
|
||||
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
|
||||
[[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; }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasPendingAppSwitch() const { return pendingSwitch; }
|
||||
|
||||
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat = false) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(delay_ms, repeat);
|
||||
}
|
||||
|
||||
AppTimerHandle scheduleRepeatingTimer(std::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(std::uint32_t delay_ms, bool repeat);
|
||||
void cancelTimerInternal(AppTimerHandle handle);
|
||||
void cancelAllTimersInternal();
|
||||
};
|
||||
|
||||
using AppContext = BasicAppContext<IFramebuffer, IInput, IClock>;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
85
Firmware/sdk/include/cardboy/sdk/app_system.hpp
Normal file
85
Firmware/sdk/include/cardboy/sdk/app_system.hpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
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(std::uint32_t delay_ms, bool repeat);
|
||||
void cancelTimer(AppTimerHandle handle);
|
||||
void cancelAllTimers();
|
||||
|
||||
void dispatchEvent(const AppEvent& event);
|
||||
|
||||
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
|
||||
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
|
||||
void clearTimersForCurrentApp();
|
||||
TimerRecord* findTimer(AppTimerHandle handle);
|
||||
bool handlePendingSwitchRequest();
|
||||
|
||||
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(std::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();
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
9
Firmware/sdk/include/cardboy/sdk/display_spec.hpp
Normal file
9
Firmware/sdk/include/cardboy/sdk/display_spec.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
inline constexpr int kDisplayWidth = 400;
|
||||
inline constexpr int kDisplayHeight = 240;
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
17
Firmware/sdk/include/cardboy/sdk/input_state.hpp
Normal file
17
Firmware/sdk/include/cardboy/sdk/input_state.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
40
Firmware/sdk/include/cardboy/sdk/platform.hpp
Normal file
40
Firmware/sdk/include/cardboy/sdk/platform.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class IFramebuffer {
|
||||
public:
|
||||
virtual ~IFramebuffer() = default;
|
||||
|
||||
virtual int width() const = 0;
|
||||
virtual int height() const = 0;
|
||||
virtual void drawPixel(int x, int y, bool on) = 0;
|
||||
virtual void clear(bool on) = 0;
|
||||
|
||||
// Optional hooks for async display pipelines; default to no-ops.
|
||||
virtual void beginFrame() {}
|
||||
virtual void endFrame() {}
|
||||
virtual bool isFrameInFlight() const { return false; }
|
||||
};
|
||||
|
||||
class IInput {
|
||||
public:
|
||||
virtual ~IInput() = default;
|
||||
|
||||
virtual InputState readState() = 0;
|
||||
};
|
||||
|
||||
class IClock {
|
||||
public:
|
||||
virtual ~IClock() = default;
|
||||
|
||||
virtual std::uint32_t millis() = 0;
|
||||
virtual void sleep_ms(std::uint32_t ms) = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
87
Firmware/sdk/include/cardboy/sdk/services.hpp
Normal file
87
Firmware/sdk/include/cardboy/sdk/services.hpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class IBuzzer {
|
||||
public:
|
||||
virtual ~IBuzzer() = default;
|
||||
|
||||
virtual void init() {}
|
||||
virtual void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) = 0;
|
||||
|
||||
virtual void beepRotate() {}
|
||||
virtual void beepMove() {}
|
||||
virtual void beepLock() {}
|
||||
virtual void beepLines(int /*lines*/) {}
|
||||
virtual void beepLevelUp(int /*level*/) {}
|
||||
virtual void beepGameOver() {}
|
||||
|
||||
virtual void setMuted(bool /*muted*/) {}
|
||||
virtual void toggleMuted() {}
|
||||
[[nodiscard]] virtual bool isMuted() const { return false; }
|
||||
};
|
||||
|
||||
class IBatteryMonitor {
|
||||
public:
|
||||
virtual ~IBatteryMonitor() = default;
|
||||
|
||||
[[nodiscard]] virtual bool hasData() const { return false; }
|
||||
[[nodiscard]] virtual float voltage() const { return 0.0f; }
|
||||
[[nodiscard]] virtual float charge() const { return 0.0f; }
|
||||
[[nodiscard]] virtual float current() const { return 0.0f; }
|
||||
};
|
||||
|
||||
class IStorage {
|
||||
public:
|
||||
virtual ~IStorage() = default;
|
||||
|
||||
[[nodiscard]] virtual bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) = 0;
|
||||
virtual void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) = 0;
|
||||
};
|
||||
|
||||
class IRandom {
|
||||
public:
|
||||
virtual ~IRandom() = default;
|
||||
|
||||
[[nodiscard]] virtual std::uint32_t nextUint32() = 0;
|
||||
};
|
||||
|
||||
class IHighResClock {
|
||||
public:
|
||||
virtual ~IHighResClock() = default;
|
||||
|
||||
[[nodiscard]] virtual std::uint64_t micros() = 0;
|
||||
};
|
||||
|
||||
class IPowerManager {
|
||||
public:
|
||||
virtual ~IPowerManager() = default;
|
||||
|
||||
virtual void setSlowMode(bool enable) = 0;
|
||||
[[nodiscard]] virtual bool isSlowMode() const = 0;
|
||||
};
|
||||
|
||||
class IFilesystem {
|
||||
public:
|
||||
virtual ~IFilesystem() = default;
|
||||
|
||||
virtual bool mount() = 0;
|
||||
[[nodiscard]] virtual bool isMounted() const = 0;
|
||||
[[nodiscard]] virtual std::string basePath() const = 0;
|
||||
};
|
||||
|
||||
struct Services {
|
||||
IBuzzer* buzzer = nullptr;
|
||||
IBatteryMonitor* battery = nullptr;
|
||||
IStorage* storage = nullptr;
|
||||
IRandom* random = nullptr;
|
||||
IHighResClock* highResClock = nullptr;
|
||||
IPowerManager* powerManager = nullptr;
|
||||
IFilesystem* filesystem = nullptr;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
231
Firmware/sdk/src/app_system.cpp
Normal file
231
Firmware/sdk/src/app_system.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
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;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t kIdlePollMs = 16;
|
||||
} // 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;
|
||||
}
|
||||
|
||||
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();
|
||||
std::uint32_t waitMs = nextTimerDueMs(waitBase);
|
||||
|
||||
if (waitMs == 0)
|
||||
continue;
|
||||
|
||||
if (waitMs == std::numeric_limits<std::uint32_t>::max())
|
||||
waitMs = kIdlePollMs;
|
||||
else
|
||||
waitMs = std::min(waitMs, kIdlePollMs);
|
||||
|
||||
if (waitMs > 0)
|
||||
context.clock.sleep_ms(waitMs);
|
||||
}
|
||||
}
|
||||
|
||||
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(std::uint32_t delay_ms, bool repeat) {
|
||||
if (!current)
|
||||
return kInvalidAppTimer;
|
||||
TimerRecord record;
|
||||
record.id = nextTimerId++;
|
||||
if (record.id == kInvalidAppTimer)
|
||||
record.id = nextTimerId++;
|
||||
record.generation = currentGeneration;
|
||||
const auto now = context.clock.millis();
|
||||
record.due_ms = now + delay_ms;
|
||||
record.interval_ms = repeat ? std::max<std::uint32_t>(1, delay_ms) : 0;
|
||||
record.repeat = repeat;
|
||||
record.active = true;
|
||||
timers.push_back(record);
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user