mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
Compare commits
65 Commits
d1a2a65c1b
...
sound
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a5d1c2819 | |||
| 1bc5b75dba | |||
| e37f8e3dc8 | |||
| df7c4ff3b9 | |||
| 07186b4b73 | |||
| 6a1f7d48ce | |||
| 031ff1952b | |||
| df55b8f2e1 | |||
| ed1cee82d2 | |||
| 7474f65aaa | |||
| 088c6e47bd | |||
| d91b7540fc | |||
| d5506b9455 | |||
| db88e16aaa | |||
| a6713859b2 | |||
| aaac0514c0 | |||
| 1b6e9a0f78 | |||
| c64f03a09f | |||
| 5ab8662332 | |||
| 6d8834d9b2 | |||
| 83ba775971 | |||
| df57e55171 | |||
| a3b837f329 | |||
| fc9e85aea0 | |||
| b55feb68f8 | |||
| f04b026d46 | |||
| e18278e130 | |||
| 9a392d6aec | |||
| 961453e28a | |||
| a4c2719077 | |||
| f721ebcb4c | |||
| e9a05259c5 | |||
| 23400d817b | |||
| fa2715a60a | |||
| 899bfeef41 | |||
| 535b0078e5 | |||
| 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 |
1
Firmware/.gitignore
vendored
1
Firmware/.gitignore
vendored
@@ -2,3 +2,4 @@ build
|
||||
cmake-build*
|
||||
.idea
|
||||
.cache
|
||||
managed_components
|
||||
42
Firmware/.vscode/c_cpp_properties.json
vendored
42
Firmware/.vscode/c_cpp_properties.json
vendored
@@ -1,23 +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
|
||||
"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
|
||||
}
|
||||
|
||||
83
Firmware/.vscode/settings.json
vendored
83
Firmware/.vscode/settings.json
vendored
@@ -1,4 +1,85 @@
|
||||
{
|
||||
"idf.flashType": "JTAG",
|
||||
"idf.port": "/dev/tty.usbmodem12401"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
4
Firmware/AGENTS.md
Normal file
4
Firmware/AGENTS.md
Normal file
@@ -0,0 +1,4 @@
|
||||
To build:
|
||||
(in zsh)
|
||||
. "$HOME/esp/esp-idf/export.sh"
|
||||
idf.py build
|
||||
@@ -2,5 +2,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
add_compile_options(-Ofast)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(hello_world)
|
||||
|
||||
36
Firmware/components/backend-esp/CMakeLists.txt
Normal file
36
Firmware/components/backend-esp/CMakeLists.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/bat_mon.cpp"
|
||||
"src/buttons.cpp"
|
||||
"src/buzzer.cpp"
|
||||
"src/esp_backend.cpp"
|
||||
"src/event_bus.cpp"
|
||||
"src/display.cpp"
|
||||
"src/fs_helper.cpp"
|
||||
"src/i2c_global.cpp"
|
||||
"src/shutdowner.cpp"
|
||||
"src/spi_global.cpp"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
PRIV_REQUIRES
|
||||
driver
|
||||
esp_timer
|
||||
esp_driver_i2c
|
||||
esp_driver_spi
|
||||
littlefs
|
||||
nvs_flash
|
||||
)
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/utils" cardboy_utils_esp)
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/backend_interface" backend_interface_from_backend_esp)
|
||||
|
||||
add_library(cardboy_backend_esp INTERFACE)
|
||||
target_link_libraries(cardboy_backend_esp
|
||||
INTERFACE
|
||||
${COMPONENT_LIB}
|
||||
)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB}
|
||||
PUBLIC
|
||||
cardboy_backend_interface
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
|
||||
namespace cardboy::backend {
|
||||
using ActiveBackend = EspBackend;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
#ifndef CB_BAT_MON_HPP
|
||||
#define CB_BAT_MON_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <cstdint>
|
||||
|
||||
class BatMon {
|
||||
public:
|
||||
@@ -20,9 +22,10 @@ public:
|
||||
void pooler(); // FIXME:
|
||||
private:
|
||||
static inline i2c_device_config_t _dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x36,
|
||||
.scl_speed_hz = 100000,
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x36,
|
||||
.scl_speed_hz = 100000,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
BatMon();
|
||||
@@ -5,6 +5,9 @@
|
||||
#ifndef BUTTONS_HPP
|
||||
#define BUTTONS_HPP
|
||||
|
||||
#include "cardboy/sdk/event_bus.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
@@ -24,14 +27,18 @@ public:
|
||||
static Buttons& get();
|
||||
void pooler(); // FIXME:
|
||||
uint8_t get_pressed();
|
||||
void install_isr();
|
||||
void install_isr();
|
||||
void register_listener(TaskHandle_t task);
|
||||
void setEventBus(cardboy::sdk::IEventBus* bus);
|
||||
|
||||
TaskHandle_t _pooler_task;
|
||||
TaskHandle_t _pooler_task;
|
||||
|
||||
private:
|
||||
Buttons();
|
||||
|
||||
volatile uint8_t _current;
|
||||
volatile uint8_t _current;
|
||||
volatile TaskHandle_t _listener = nullptr;
|
||||
cardboy::sdk::IEventBus* _eventBus = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
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; }
|
||||
};
|
||||
@@ -15,8 +15,12 @@
|
||||
|
||||
#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_22
|
||||
|
||||
#define PWR_INT GPIO_NUM_10
|
||||
#define PWR_KILL GPIO_NUM_12
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_DISPLAY_HPP
|
||||
#define CB_DISPLAY_HPP
|
||||
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
#include "cardboy/utils/utils.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
// (Async memcpy removed for debugging simplification)
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
|
||||
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;
|
||||
|
||||
extern uint8_t* dma_buf;
|
||||
|
||||
void init();
|
||||
// Double-buffered asynchronous frame pipeline:
|
||||
// Usage pattern each frame:
|
||||
// SMD::frame_ready(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
|
||||
// ... write pixels into dma_buf via set_pixel / surface ...
|
||||
// SMD::send_frame(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
|
||||
// // is optionally cleared so the alternate buffer is ready for the next frame
|
||||
void send_frame(bool clear_after_send = true);
|
||||
void frame_ready();
|
||||
bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
|
||||
|
||||
__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
|
||||
CARDBOY_CHECK(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;
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) static void set_pixel_8(int x, int y, std::uint8_t value) {
|
||||
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
|
||||
CARDBOY_CHECK((x % 8) == 0);
|
||||
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
|
||||
dma_buf[lineIdx] = ~value;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cardboy/sdk/event_bus.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/timers.h"
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
class EventBus final : public cardboy::sdk::IEventBus {
|
||||
public:
|
||||
EventBus();
|
||||
~EventBus() override;
|
||||
|
||||
void signal(std::uint32_t bits) override;
|
||||
void signalFromISR(std::uint32_t bits) override;
|
||||
std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override;
|
||||
void scheduleTimerSignal(std::uint32_t delay_ms) override;
|
||||
void cancelTimerSignal() override;
|
||||
|
||||
private:
|
||||
EventGroupHandle_t group;
|
||||
TimerHandle_t timer;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
@@ -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;
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifndef CB_I2C_GLOBAL_HPP
|
||||
#define CB_I2C_GLOBAL_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
@@ -22,7 +22,8 @@ private:
|
||||
.scl_io_num = I2C_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags = {.enable_internal_pullup = false, .allow_pd = true},
|
||||
.intr_priority = 2,
|
||||
.flags = {.enable_internal_pullup = true, .allow_pd = true},
|
||||
};
|
||||
i2c_master_bus_handle_t _bus_handle;
|
||||
}; // namespace i2c_global
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifndef CB_SPI_GLOBAL_HPP
|
||||
#define CB_SPI_GLOBAL_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <cardboy/sdk/display_spec.hpp>
|
||||
#include "cardboy/backend/esp/display.hpp"
|
||||
#include "cardboy/backend/esp/event_bus.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
class EspRuntime;
|
||||
|
||||
class EspFramebuffer final : public cardboy::sdk::FramebufferFacade<EspFramebuffer> {
|
||||
public:
|
||||
EspFramebuffer() = default;
|
||||
|
||||
[[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
||||
[[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
||||
void __attribute__((always_inline)) drawPixel_impl(int x, int y, bool on) { SMD::set_pixel(x, y, on); }
|
||||
void __attribute__((always_inline)) drawBits8_impl(int x, int y, std::uint8_t bits) {
|
||||
SMD::set_pixel_8(x, y, bits);
|
||||
}
|
||||
void clear_impl(bool on);
|
||||
void frameReady_impl();
|
||||
void sendFrame_impl(bool clearAfterSend);
|
||||
[[nodiscard]] bool frameInFlight_impl() const;
|
||||
};
|
||||
|
||||
class EspInput final : public cardboy::sdk::InputFacade<EspInput> {
|
||||
public:
|
||||
cardboy::sdk::InputState readState_impl();
|
||||
};
|
||||
|
||||
class EspClock final : public cardboy::sdk::ClockFacade<EspClock> {
|
||||
public:
|
||||
std::uint32_t millis_impl();
|
||||
void sleep_ms_impl(std::uint32_t ms);
|
||||
};
|
||||
|
||||
class EspRuntime {
|
||||
public:
|
||||
EspRuntime();
|
||||
~EspRuntime();
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry();
|
||||
|
||||
EspFramebuffer framebuffer;
|
||||
EspInput input;
|
||||
EspClock clock;
|
||||
|
||||
private:
|
||||
void initializeHardware();
|
||||
|
||||
class BuzzerService;
|
||||
class BatteryService;
|
||||
class StorageService;
|
||||
class RandomService;
|
||||
class HighResClockService;
|
||||
class FilesystemService;
|
||||
class LoopHooksService;
|
||||
|
||||
std::unique_ptr<BuzzerService> buzzerService;
|
||||
std::unique_ptr<BatteryService> batteryService;
|
||||
std::unique_ptr<StorageService> storageService;
|
||||
std::unique_ptr<RandomService> randomService;
|
||||
std::unique_ptr<HighResClockService> highResClockService;
|
||||
std::unique_ptr<FilesystemService> filesystemService;
|
||||
std::unique_ptr<EventBus> eventBus;
|
||||
std::unique_ptr<LoopHooksService> loopHooksService;
|
||||
|
||||
cardboy::sdk::Services services{};
|
||||
};
|
||||
|
||||
struct Backend {
|
||||
using Framebuffer = EspFramebuffer;
|
||||
using Input = EspInput;
|
||||
using Clock = EspClock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
|
||||
namespace cardboy::backend {
|
||||
using EspBackend = esp::Backend;
|
||||
} // namespace cardboy::backend
|
||||
@@ -2,15 +2,13 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "bat_mon.hpp"
|
||||
|
||||
#include <power_helper.hpp>
|
||||
#include "cardboy/backend/esp/bat_mon.hpp"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
#include "shutdowner.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/shutdowner.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
|
||||
@@ -105,7 +103,7 @@ void BatMon::pooler() {
|
||||
_charge = capToMah(ReadRegister(0x05));
|
||||
_current = regToCurrent(ReadRegister(0x0B));
|
||||
_voltage = regToVoltage(ReadRegister(0x09));
|
||||
PowerHelper::get().delay(10000, 1000);
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
if (_voltage < 3.0f) {
|
||||
Shutdowner::get().shutdown();
|
||||
}
|
||||
@@ -2,24 +2,24 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "buttons.hpp"
|
||||
#include "cardboy/backend/esp/buttons.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_err.h>
|
||||
#include <power_helper.hpp>
|
||||
#include <rom/ets_sys.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
#include "cardboy/sdk/event_bus.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
static inline i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x20,
|
||||
.scl_speed_hz = 100000,
|
||||
.scl_speed_hz = 50000,
|
||||
};
|
||||
|
||||
Buttons& Buttons::get() {
|
||||
@@ -37,12 +37,10 @@ static void wakeup(void* arg) {
|
||||
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));
|
||||
@@ -62,7 +60,7 @@ Buttons::Buttons() {
|
||||
buf2[0] = 7;
|
||||
buf2[1] = 0x80;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 1, &_pooler_task);
|
||||
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));
|
||||
@@ -90,7 +88,15 @@ void Buttons::pooler() {
|
||||
reg = 1;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
||||
if (_listener)
|
||||
xTaskNotifyGive(_listener);
|
||||
if (_eventBus)
|
||||
_eventBus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
|
||||
}
|
||||
}
|
||||
uint8_t Buttons::get_pressed() { return _current; }
|
||||
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
|
||||
|
||||
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
|
||||
|
||||
void Buttons::setEventBus(cardboy::sdk::IEventBus* bus) { _eventBus = bus; }
|
||||
160
Firmware/components/backend-esp/src/buzzer.cpp
Normal file
160
Firmware/components/backend-esp/src/buzzer.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
// Buzzer implementation
|
||||
#include "cardboy/backend/esp/buzzer.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include <driver/ledc.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_timer.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() {
|
||||
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)));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void Buzzer::toggleMuted() { setMuted(!_muted); }
|
||||
155
Firmware/components/backend-esp/src/display.cpp
Normal file
155
Firmware/components/backend-esp/src/display.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
// Double-buffered display implementation with async memcpy ---------------------------------
|
||||
|
||||
#include "cardboy/backend/esp/display.hpp"
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <driver/gpio.h>
|
||||
#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"
|
||||
|
||||
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]{};
|
||||
|
||||
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 bool s_clearPending[2] = {true, true};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
const bool shouldClear = s_clearPending[bufIdx];
|
||||
s_clearPending[bufIdx] = true;
|
||||
if (shouldClear) {
|
||||
constexpr unsigned alignedSize = SMD::kLineDataBytes - (SMD::kLineDataBytes % 4);
|
||||
static_assert(SMD::kLineDataBytes - alignedSize < 8); // Last byte is zero anyway
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, alignedSize,
|
||||
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
|
||||
} else {
|
||||
if (!xSemaphoreGive(s_bufferSem[bufIdx]))
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::frame_transfer_in_flight() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
|
||||
|
||||
void SMD::send_frame(bool clear_after_send) {
|
||||
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;
|
||||
s_clearPending[sendIdx] = clear_after_send;
|
||||
|
||||
_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::frame_ready() {
|
||||
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);
|
||||
}
|
||||
230
Firmware/components/backend-esp/src/esp_backend.cpp
Normal file
230
Firmware/components/backend-esp/src/esp_backend.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
|
||||
#include "cardboy/backend/esp/bat_mon.hpp"
|
||||
#include "cardboy/backend/esp/buttons.hpp"
|
||||
#include "cardboy/backend/esp/buzzer.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
#include "cardboy/backend/esp/display.hpp"
|
||||
#include "cardboy/backend/esp/fs_helper.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/shutdowner.hpp"
|
||||
#include "cardboy/backend/esp/spi_global.hpp"
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
namespace {
|
||||
void ensureNvsInit() {
|
||||
static bool nvsReady = false;
|
||||
if (nvsReady)
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
nvsReady = true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class EspRuntime::BuzzerService 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 EspRuntime::BatteryService 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 EspRuntime::StorageService final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
ensureNvsInit();
|
||||
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 {
|
||||
ensureNvsInit();
|
||||
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 EspRuntime::RandomService final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
||||
};
|
||||
|
||||
class EspRuntime::HighResClockService final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
||||
};
|
||||
|
||||
class EspRuntime::FilesystemService 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{};
|
||||
}
|
||||
};
|
||||
|
||||
class EspRuntime::LoopHooksService final : public cardboy::sdk::ILoopHooks {
|
||||
public:
|
||||
void onLoopIteration() override { vTaskDelay(1); }
|
||||
};
|
||||
|
||||
EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
|
||||
initializeHardware();
|
||||
|
||||
buzzerService = std::make_unique<BuzzerService>();
|
||||
batteryService = std::make_unique<BatteryService>();
|
||||
storageService = std::make_unique<StorageService>();
|
||||
randomService = std::make_unique<RandomService>();
|
||||
highResClockService = std::make_unique<HighResClockService>();
|
||||
filesystemService = std::make_unique<FilesystemService>();
|
||||
eventBus = std::make_unique<EventBus>();
|
||||
loopHooksService = std::make_unique<LoopHooksService>();
|
||||
|
||||
services.buzzer = buzzerService.get();
|
||||
services.battery = batteryService.get();
|
||||
services.storage = storageService.get();
|
||||
services.random = randomService.get();
|
||||
services.highResClock = highResClockService.get();
|
||||
services.filesystem = filesystemService.get();
|
||||
services.eventBus = eventBus.get();
|
||||
services.loopHooks = loopHooksService.get();
|
||||
|
||||
Buttons::get().setEventBus(eventBus.get());
|
||||
}
|
||||
|
||||
EspRuntime::~EspRuntime() = default;
|
||||
|
||||
cardboy::sdk::Services& EspRuntime::serviceRegistry() { return services; }
|
||||
|
||||
void EspRuntime::initializeHardware() {
|
||||
static bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
|
||||
ensureNvsInit();
|
||||
|
||||
Shutdowner::get();
|
||||
Buttons::get();
|
||||
|
||||
esp_err_t isrErr = gpio_install_isr_service(0);
|
||||
if (isrErr != ESP_OK && isrErr != ESP_ERR_INVALID_STATE) {
|
||||
ESP_ERROR_CHECK(isrErr);
|
||||
}
|
||||
|
||||
Shutdowner::get().install_isr();
|
||||
Buttons::get().install_isr();
|
||||
|
||||
I2cGlobal::get();
|
||||
BatMon::get();
|
||||
SpiGlobal::get();
|
||||
SMD::init();
|
||||
Buzzer::get().init();
|
||||
|
||||
FsHelper::get().mount();
|
||||
}
|
||||
|
||||
void EspFramebuffer::clear_impl(bool on) {
|
||||
for (int y = 0; y < height_impl(); ++y)
|
||||
for (int x = 0; x < width_impl(); ++x)
|
||||
SMD::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void EspFramebuffer::frameReady_impl() { SMD::frame_ready(); }
|
||||
|
||||
void EspFramebuffer::sendFrame_impl(bool clearAfterSend) { SMD::send_frame(clearAfterSend); }
|
||||
|
||||
bool EspFramebuffer::frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); }
|
||||
|
||||
cardboy::sdk::InputState EspInput::readState_impl() {
|
||||
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;
|
||||
}
|
||||
|
||||
std::uint32_t EspClock::millis_impl() {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void EspClock::sleep_ms_impl(std::uint32_t ms) {
|
||||
if (ms == 0)
|
||||
return;
|
||||
vTaskDelay(pdMS_TO_TICKS(ms));
|
||||
}
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
81
Firmware/components/backend-esp/src/event_bus.cpp
Normal file
81
Firmware/components/backend-esp/src/event_bus.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "cardboy/backend/esp/event_bus.hpp"
|
||||
|
||||
#include "cardboy/sdk/event_bus.hpp"
|
||||
|
||||
#include "freertos/portmacro.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] TickType_t toTicks(std::uint32_t timeout_ms) {
|
||||
if (timeout_ms == cardboy::sdk::IEventBus::kWaitForever)
|
||||
return portMAX_DELAY;
|
||||
return pdMS_TO_TICKS(timeout_ms);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static void timerCallback(TimerHandle_t handle) {
|
||||
auto* bus = static_cast<EventBus*>(pvTimerGetTimerID(handle));
|
||||
if (bus)
|
||||
bus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
|
||||
}
|
||||
|
||||
EventBus::EventBus() :
|
||||
group(xEventGroupCreate()), timer(xTimerCreate("EventBusTimer", pdMS_TO_TICKS(1), pdFALSE, this, timerCallback)) {}
|
||||
|
||||
EventBus::~EventBus() {
|
||||
if (timer)
|
||||
xTimerDelete(timer, portMAX_DELAY);
|
||||
if (group)
|
||||
vEventGroupDelete(group);
|
||||
}
|
||||
|
||||
void EventBus::signal(std::uint32_t bits) {
|
||||
if (!group || bits == 0)
|
||||
return;
|
||||
xEventGroupSetBits(group, bits);
|
||||
}
|
||||
|
||||
void EventBus::signalFromISR(std::uint32_t bits) {
|
||||
if (!group || bits == 0)
|
||||
return;
|
||||
BaseType_t higherPriorityTaskWoken = pdFALSE;
|
||||
xEventGroupSetBitsFromISR(group, bits, &higherPriorityTaskWoken);
|
||||
if (higherPriorityTaskWoken == pdTRUE)
|
||||
portYIELD_FROM_ISR(higherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
std::uint32_t EventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
|
||||
if (!group || mask == 0)
|
||||
return 0;
|
||||
const EventBits_t bits = xEventGroupWaitBits(group, mask, pdTRUE, pdFALSE, toTicks(timeout_ms));
|
||||
return static_cast<std::uint32_t>(bits & mask);
|
||||
}
|
||||
|
||||
void EventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
|
||||
if (!timer)
|
||||
return;
|
||||
xTimerStop(timer, 0);
|
||||
|
||||
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
|
||||
return;
|
||||
|
||||
if (delay_ms == 0) {
|
||||
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
|
||||
return;
|
||||
}
|
||||
|
||||
const TickType_t ticks = std::max<TickType_t>(pdMS_TO_TICKS(delay_ms), 1);
|
||||
if (xTimerChangePeriod(timer, ticks, 0) == pdPASS)
|
||||
xTimerStart(timer, 0);
|
||||
}
|
||||
|
||||
void EventBus::cancelTimerSignal() {
|
||||
if (!timer)
|
||||
return;
|
||||
xTimerStop(timer, 0);
|
||||
}
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
68
Firmware/components/backend-esp/src/fs_helper.cpp
Normal file
68
Firmware/components/backend-esp/src/fs_helper.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "cardboy/backend/esp/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);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
|
||||
|
||||
I2cGlobal& I2cGlobal::get() {
|
||||
@@ -2,12 +2,12 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "shutdowner.hpp"
|
||||
#include "cardboy/backend/esp/shutdowner.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
Shutdowner& Shutdowner::get() {
|
||||
static Shutdowner instance;
|
||||
@@ -2,7 +2,7 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "spi_global.hpp"
|
||||
#include "cardboy/backend/esp/spi_global.hpp"
|
||||
|
||||
SpiGlobal& SpiGlobal::get() {
|
||||
static SpiGlobal SpiGlobal;
|
||||
@@ -1,5 +1,31 @@
|
||||
idf_component_register()
|
||||
idf_component_register(
|
||||
INCLUDE_DIRS ""
|
||||
REQUIRES backend-esp
|
||||
)
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
|
||||
set(CARDBOY_BUILD_SFML OFF CACHE BOOL "Disable desktop backend build" FORCE)
|
||||
set(CARDBOY_SDK_BACKEND_LIBRARY cardboy_backend_esp CACHE STRING "Cardboy backend implementation" FORCE)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE cbsdk)
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/core" cb-sdk-build-core)
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/apps" cb-sdk-build-apps)
|
||||
|
||||
target_compile_options(cardboy_backend_esp
|
||||
INTERFACE
|
||||
-fjump-tables
|
||||
-ftree-switch-conversion
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_backend_esp
|
||||
INTERFACE
|
||||
idf::driver
|
||||
idf::esp_timer
|
||||
idf::esp_driver_spi
|
||||
idf::freertos
|
||||
)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB}
|
||||
INTERFACE
|
||||
cardboy_backend_esp
|
||||
cardboy_sdk
|
||||
cardboy_apps
|
||||
)
|
||||
|
||||
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
|
||||
248
Firmware/ghettoprof.sh
Executable file
248
Firmware/ghettoprof.sh
Executable file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env bash
|
||||
# parallel-pc-profile.sh — parallel symbol resolver + optional annotated disassembly
|
||||
# Supports C++ demangling, LLVM disassembler, and optional no-inlines aggregation (symbol-table based).
|
||||
#
|
||||
# Usage:
|
||||
# ./parallel-pc-profile.sh [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt"
|
||||
exit 1
|
||||
}
|
||||
|
||||
ANNOTATE=0
|
||||
JOBS=""
|
||||
NO_INLINES=0
|
||||
|
||||
# ---- args ----
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-j) JOBS="$2"; shift 2 ;;
|
||||
--annotate) ANNOTATE=1; shift ;;
|
||||
--no-inlines) NO_INLINES=1; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
[[ $# -lt 2 ]] && usage
|
||||
ELF="$1"
|
||||
PCS_IN="$2"
|
||||
|
||||
[[ ! -f "$ELF" ]] && { echo "ELF not found: $ELF" >&2; exit 2; }
|
||||
[[ ! -f "$PCS_IN" ]] && { echo "PC log not found: $PCS_IN" >&2; exit 3; }
|
||||
|
||||
# ---- tools ----
|
||||
ADDR2LINE=""
|
||||
for t in llvm-addr2line eu-addr2line riscv32-esp-elf-addr2line xtensa-esp32-elf-addr2line addr2line; do
|
||||
if command -v "$t" >/dev/null 2>&1; then ADDR2LINE="$t"; break; fi
|
||||
done
|
||||
[[ -z "$ADDR2LINE" ]] && { echo "No addr2line found"; exit 4; }
|
||||
|
||||
if command -v llvm-objdump >/dev/null 2>&1; then
|
||||
OBJDUMP="llvm-objdump"
|
||||
else
|
||||
for t in riscv32-esp-elf-objdump xtensa-esp32-elf-objdump objdump; do
|
||||
if command -v "$t" >/dev/null 2>&1; then OBJDUMP="$t"; break; fi
|
||||
done
|
||||
fi
|
||||
[[ -z "${OBJDUMP:-}" ]] && { echo "No objdump found"; exit 5; }
|
||||
|
||||
if command -v llvm-nm >/dev/null 2>&1; then
|
||||
NM="llvm-nm"
|
||||
elif command -v nm >/dev/null 2>&1; then
|
||||
NM="nm"
|
||||
else
|
||||
NM=""
|
||||
fi
|
||||
|
||||
if command -v c++filt >/dev/null 2>&1; then
|
||||
CPPFILT="c++filt"
|
||||
elif command -v llvm-cxxfilt >/dev/null 2>&1; then
|
||||
CPPFILT="llvm-cxxfilt"
|
||||
else
|
||||
CPPFILT=""
|
||||
fi
|
||||
|
||||
# ---- cores ----
|
||||
if [[ -z "$JOBS" ]]; then
|
||||
if command -v nproc >/dev/null 2>&1; then JOBS=$(nproc)
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||
else JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
|
||||
fi
|
||||
fi
|
||||
(( JOBS = JOBS > 1 ? JOBS - 1 : 1 ))
|
||||
echo ">> Using $JOBS parallel jobs"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# ---- extract PCs ----
|
||||
grep -aoE '0x[0-9a-fA-F]+' "$PCS_IN" | tr 'A-F' 'a-f' | sort | uniq -c >"$TMP/pc_counts.txt" || true
|
||||
awk '{print $2}' "$TMP/pc_counts.txt" >"$TMP/addrs.txt"
|
||||
[[ ! -s "$TMP/addrs.txt" ]] && { echo "No addresses found"; exit 5; }
|
||||
|
||||
# ---- parallel addr2line (live PC -> function to stderr) ----
|
||||
CHUNK=400
|
||||
split -l "$CHUNK" "$TMP/addrs.txt" "$TMP/chunk."
|
||||
|
||||
find "$TMP" -name 'chunk.*' -type f -print0 \
|
||||
| xargs -0 -I{} -n1 -P "$JOBS" bash -c '
|
||||
set -euo pipefail
|
||||
ADDR2LINE="$1"; ELF="$2"; CHUNK="$3"; CPP="$4"
|
||||
OUT="${CHUNK}.sym"
|
||||
"$ADDR2LINE" -a -f -e "$ELF" $(cat "$CHUNK") \
|
||||
| tee "$OUT" \
|
||||
| awk '"'"'NR%3==1{a=$0;next} NR%3==2{f=$0; printf "%s\t%s\n",a,f; next} NR%3==0{next}'"'"' \
|
||||
| { if [[ -n "$CPP" ]]; then "$CPP"; else cat; fi; } 1>&2
|
||||
' _ "$ADDR2LINE" "$ELF" {} "$CPPFILT"
|
||||
|
||||
# Collate triplets
|
||||
cat "$TMP"/chunk.*.sym > "$TMP/symbols.raw"
|
||||
|
||||
# ---- parse 3-line addr/func/file:line ----
|
||||
# Normalize leading zeros in addresses so joins match grep-extracted PCs
|
||||
awk 'NR%3==1{a=$0; sub(/^0x0+/, "0x", a); next} NR%3==2{f=$0; next} NR%3==0{print a "\t" f "\t" $0}' \
|
||||
"$TMP/symbols.raw" >"$TMP/map.tsv"
|
||||
|
||||
# ---- counts: addr -> samplecount ----
|
||||
awk '{printf "%s\t%s\n",$2,$1}' "$TMP/pc_counts.txt" | sort -k1,1 >"$TMP/counts.tsv"
|
||||
|
||||
# ---- choose mapping: default (addr2line; may show inlined names) vs --no-inlines (symbol-table) ----
|
||||
DEFAULT_ADDR_FUNC="$TMP/addr_func.tsv"
|
||||
cut -f1,2 "$TMP/map.tsv" | sort -k1,1 >"$DEFAULT_ADDR_FUNC"
|
||||
|
||||
if [[ "$NO_INLINES" == "1" ]]; then
|
||||
if [[ -z "$NM" ]]; then
|
||||
echo "WARNING: nm/llvm-nm not found; falling back to inline-aware mapping." >&2
|
||||
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
|
||||
else
|
||||
echo ">> Building symbol table for no-inlines mapping..."
|
||||
# Create sorted function symbols: hexaddr\tname (demangled if possible afterwards)
|
||||
# Try llvm-nm format first; fall back to generic nm.
|
||||
if [[ "$NM" == "llvm-nm" ]]; then
|
||||
# llvm-nm -n --defined-only emits: ADDRESS TYPE NAME
|
||||
"$NM" -n --defined-only "$ELF" \
|
||||
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw"
|
||||
else
|
||||
# generic nm -n emits: ADDRESS TYPE NAME (varies a bit across platforms)
|
||||
"$NM" -n --defined-only "$ELF" 2>/dev/null \
|
||||
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
|
||||
# macOS nm might output different columns; handle common alt layout:
|
||||
if [[ ! -s "$TMP/syms.raw" ]]; then
|
||||
"$NM" -n "$ELF" 2>/dev/null | awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$CPPFILT" && -s "$TMP/syms.raw" ]]; then
|
||||
"$CPPFILT" < "$TMP/syms.raw" > "$TMP/syms.dem.raw" || cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
|
||||
else
|
||||
cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
|
||||
fi
|
||||
|
||||
# Normalize addresses and sort ascending
|
||||
awk '{addr=$1; sub(/^0x0+/, "0x", addr); print addr "\t" $2}' "$TMP/syms.dem.raw" \
|
||||
| awk 'NF' \
|
||||
| sort -k1,1 > "$TMP/syms.tsv"
|
||||
|
||||
if [[ ! -s "$TMP/syms.tsv" ]]; then
|
||||
echo "WARNING: no text symbols found; falling back to inline-aware mapping." >&2
|
||||
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
|
||||
else
|
||||
# Map each PC to the *containing* function: last symbol with addr <= PC.
|
||||
# Both syms.tsv and addrs.txt are sorted asc → single pass merge.
|
||||
awk '
|
||||
function hex2num(h, x, n,i,c) {
|
||||
gsub(/^0x/,"",h); n=0
|
||||
for(i=1;i<=length(h);i++){ c=substr(h,i,1)
|
||||
x = index("0123456789abcdef", tolower(c)) - 1
|
||||
if (x<0) x = index("0123456789ABCDEF", c) - 1
|
||||
n = n*16 + x
|
||||
}
|
||||
return n
|
||||
}
|
||||
BEGIN {
|
||||
# preload symbols
|
||||
while ((getline < ARGV[1]) > 0) {
|
||||
saddr[NSYM]=$1; sname[NSYM]=$2; NSYM++
|
||||
}
|
||||
# load PCs
|
||||
while ((getline < ARGV[2]) > 0) {
|
||||
pc[NPC]=$0; NPC++
|
||||
}
|
||||
# pointers
|
||||
si=0
|
||||
for (i=0; i<NPC; i++) {
|
||||
p=pc[i]; pn=hex2num(p)
|
||||
# advance symbol index while next symbol start <= pc
|
||||
while (si+1<NSYM && hex2num(saddr[si+1]) <= pn) si++
|
||||
# output mapping: p -> sname[si] (if any)
|
||||
if (si<NSYM && hex2num(saddr[si]) <= pn)
|
||||
printf "%s\t%s\n", p, sname[si]
|
||||
else
|
||||
printf "%s\t<unknown>\n", p
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
' "$TMP/syms.tsv" "$TMP/addrs.txt" \
|
||||
| sort -k1,1 > "$TMP/addr_func.noinline.tsv"
|
||||
|
||||
ADDR_FUNC_FILE="$TMP/addr_func.noinline.tsv"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
|
||||
fi
|
||||
|
||||
# ---- aggregate to hot functions ----
|
||||
join -t $'\t' -a1 -e "<unknown>" -o 1.2,2.2 "$TMP/counts.tsv" "$ADDR_FUNC_FILE" \
|
||||
| awk -F'\t' '{s[$2]+=$1} END{for(k in s) printf "%8d %s\n",s[k],k}' \
|
||||
| sort -nr > "$TMP/hot.txt"
|
||||
|
||||
# ---- demangle final hot list (if available) ----
|
||||
if [[ -n "$CPPFILT" ]]; then
|
||||
"$CPPFILT" < "$TMP/hot.txt" > hot_functions.txt
|
||||
else
|
||||
cp "$TMP/hot.txt" hot_functions.txt
|
||||
fi
|
||||
|
||||
echo "=== Top 50 hot functions ==="
|
||||
head -50 hot_functions.txt
|
||||
echo "Full list in: hot_functions.txt"
|
||||
|
||||
# ---- annotated source+assembly (optional) ----
|
||||
if (( ANNOTATE )); then
|
||||
echo ">> Generating annotated source+assembly..."
|
||||
awk '{printf "%s %s\n",$2,$1}' "$TMP/pc_counts.txt" >"$TMP/count.map"
|
||||
|
||||
if [[ "$OBJDUMP" == "llvm-objdump" ]]; then
|
||||
# Portable across llvm-objdump versions
|
||||
"$OBJDUMP" --source -l --demangle -d "$ELF" >"$TMP/disasm.txt"
|
||||
else
|
||||
"$OBJDUMP" -S -C -l -d "$ELF" >"$TMP/disasm.txt"
|
||||
fi
|
||||
|
||||
# Overlay hit counts onto the disassembly
|
||||
awk -v counts="$TMP/count.map" '
|
||||
BEGIN {
|
||||
while ((getline < counts) > 0) {
|
||||
addr=$1; cnt=$2
|
||||
gsub(/^0x/,"",addr)
|
||||
map[addr]=cnt
|
||||
}
|
||||
}
|
||||
/^[[:space:]]*[0-9a-f]+:/ {
|
||||
split($1,a,":"); key=a[1]
|
||||
if (key in map)
|
||||
printf("%-12s %6d | %s\n", $1, map[key], substr($0, index($0,$2)))
|
||||
else
|
||||
print $0
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$TMP/disasm.txt" > annotated.S
|
||||
|
||||
echo "Annotated source + assembly written to: annotated.S"
|
||||
echo "Tip: less -R annotated.S"
|
||||
fi
|
||||
@@ -1,13 +1,16 @@
|
||||
idf_component_register(SRCS
|
||||
src/hello_world_main.cpp
|
||||
src/display.cpp
|
||||
src/bat_mon.cpp
|
||||
src/spi_global.cpp
|
||||
src/i2c_global.cpp
|
||||
src/disp_tools.cpp
|
||||
src/disp_tty.cpp
|
||||
src/shutdowner.cpp
|
||||
src/buttons.cpp
|
||||
src/power_helper.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp
|
||||
INCLUDE_DIRS "include")
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/app_main.cpp"
|
||||
PRIV_REQUIRES
|
||||
sdk-esp
|
||||
littlefs
|
||||
REQUIRES
|
||||
backend-esp
|
||||
EMBED_FILES
|
||||
"roms/builtin_demo1.gb"
|
||||
"roms/builtin_demo2.gb"
|
||||
INCLUDE_DIRS
|
||||
""
|
||||
)
|
||||
|
||||
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
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_DISP_TOOLS_HPP
|
||||
#define CB_DISP_TOOLS_HPP
|
||||
|
||||
#include <display.hpp>
|
||||
|
||||
class DispTools {
|
||||
public:
|
||||
static DispTools& get();
|
||||
|
||||
void clear() {
|
||||
for (int y = 0; y < DISP_HEIGHT; y++) {
|
||||
for (int x = 0; x < DISP_WIDTH; x++) {
|
||||
SMD::get().set_pixel(x, y, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool get_pixel(int x, int y) {
|
||||
// if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
// assert(false);
|
||||
return true;
|
||||
// return disp_frame[y][x];
|
||||
}
|
||||
void reset_pixel(int x, int y) {
|
||||
// if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
// assert(false);
|
||||
SMD::get().set_pixel(x, y, false);
|
||||
}
|
||||
void set_pixel(int x, int y) {
|
||||
// if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
// assert(false);
|
||||
//
|
||||
SMD::get().set_pixel(x, y, true);
|
||||
}
|
||||
void draw_to_display();
|
||||
};
|
||||
|
||||
|
||||
#endif // DISP_TOOLS_HPP
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef DISP_TTY_HPP
|
||||
#define DISP_TTY_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <format>
|
||||
|
||||
class FbTty {
|
||||
public:
|
||||
void putchar(char c);
|
||||
void putstr(const char* str);
|
||||
void reset();
|
||||
|
||||
template<typename... Args>
|
||||
auto fmt(std::format_string<Args...> fmt, Args&&... args) {
|
||||
auto str = std::format(fmt, std::forward<Args>(args)...);
|
||||
putstr(str.c_str());
|
||||
}
|
||||
private:
|
||||
void draw_char(int col, int row);
|
||||
|
||||
int _cur_col = 0;
|
||||
int _cur_row = 0;
|
||||
|
||||
static constexpr size_t _max_col = DISP_WIDTH / 8;
|
||||
static constexpr size_t _max_row = DISP_HEIGHT / 16;
|
||||
|
||||
std::array<std::array<char, _max_row>, _max_col> _buf = {};
|
||||
|
||||
void next_col();
|
||||
void next_row();
|
||||
};
|
||||
|
||||
|
||||
#endif // DISP_TTY_HPP
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_DISPLAY_HPP
|
||||
#define CB_DISPLAY_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
|
||||
#include "Surface.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
class SMD {
|
||||
public:
|
||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
static constexpr size_t kLineMultiSingle = (kLineBytes + 2);
|
||||
static constexpr size_t kLineDataBytes = kLineMultiSingle * DISP_HEIGHT + 2;
|
||||
|
||||
static DMA_ATTR uint8_t dma_buf[SMD::kLineDataBytes];
|
||||
|
||||
static SMD& get();
|
||||
void clear();
|
||||
void draw();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
spi_device_handle_t _spi;
|
||||
bool _vcom = false;
|
||||
};
|
||||
|
||||
class SMDSurface : public Surface<SMDSurface, BwPixel>, public StandardEventQueue<SMDSurface> {
|
||||
public:
|
||||
using PixelType = BwPixel;
|
||||
|
||||
SMDSurface(EventLoop* loop);
|
||||
|
||||
~SMDSurface() override;
|
||||
|
||||
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
|
||||
|
||||
void clear_impl();
|
||||
|
||||
int get_width_impl() const;
|
||||
|
||||
int get_height_impl() const;
|
||||
|
||||
template<typename T>
|
||||
EventHandlingResult handle(const T& event) {
|
||||
return _window->handle(event);
|
||||
}
|
||||
|
||||
EventHandlingResult handle(SurfaceResizeEvent event);
|
||||
};
|
||||
|
||||
|
||||
#endif // DISPLAY_HPP
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 03.03.2025.
|
||||
//
|
||||
|
||||
#ifndef POWER_HELPER_HPP
|
||||
#define POWER_HELPER_HPP
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
class PowerHelper {
|
||||
public:
|
||||
static PowerHelper& get();
|
||||
|
||||
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();
|
||||
|
||||
private:
|
||||
PowerHelper();
|
||||
|
||||
bool _slow = false;
|
||||
EventGroupHandle_t _event_group;
|
||||
};
|
||||
|
||||
|
||||
#endif // POWER_HELPER_HPP
|
||||
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.
|
||||
243
Firmware/main/src/app_main.cpp
Normal file
243
Firmware/main/src/app_main.cpp
Normal file
@@ -0,0 +1,243 @@
|
||||
// Cardboy firmware entry point: boot platform services and run the modular app system.
|
||||
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/settings_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/persistent_settings.hpp"
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
extern "C" {
|
||||
extern const uint8_t _binary_builtin_demo1_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo1_gb_end[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_end[];
|
||||
}
|
||||
|
||||
constexpr apps::EmbeddedRomDescriptor kEmbeddedRoms[] = {
|
||||
{
|
||||
.name = "Builtin Demo 1",
|
||||
.saveSlug = "builtin_demo1",
|
||||
.start = _binary_builtin_demo1_gb_start,
|
||||
.end = _binary_builtin_demo1_gb_end,
|
||||
},
|
||||
{
|
||||
.name = "Builtin Demo 2",
|
||||
.saveSlug = "builtin_demo2",
|
||||
.start = _binary_builtin_demo2_gb_start,
|
||||
.end = _binary_builtin_demo2_gb_end,
|
||||
},
|
||||
};
|
||||
|
||||
} // 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);
|
||||
|
||||
const uint32_t heapFree = esp_get_free_heap_size();
|
||||
const uint32_t heapMinimum = esp_get_minimum_free_heap_size();
|
||||
std::printf("[%s] Heap free %lu B | Min free %lu B\n", tag, static_cast<unsigned long>(heapFree),
|
||||
static_cast<unsigned long>(heapMinimum));
|
||||
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() {
|
||||
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
|
||||
|
||||
static cardboy::backend::esp::EspRuntime runtime;
|
||||
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
context.system = &system;
|
||||
|
||||
const cardboy::sdk::PersistentSettings persistentSettings =
|
||||
cardboy::sdk::loadPersistentSettings(context.getServices());
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->setMuted(persistentSettings.mute);
|
||||
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
if (persistentSettings.autoLightSleep) {
|
||||
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
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createSettingsAppFactory());
|
||||
system.registerApp(apps::createClockAppFactory());
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
|
||||
// start_task_usage_monitor();
|
||||
|
||||
system.run();
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "disp_tools.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <display.hpp>
|
||||
|
||||
DispTools& DispTools::get() {
|
||||
static DispTools disp_tools;
|
||||
return disp_tools;
|
||||
}
|
||||
|
||||
void DispTools::draw_to_display() { SMD::get().draw(); }
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.04.2024.
|
||||
//
|
||||
|
||||
#include "disp_tty.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
|
||||
#include "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);
|
||||
else
|
||||
DispTools::get().reset_pixel(col * 8 + x, row * 16 + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
void FbTty::reset() {
|
||||
_cur_col = 0;
|
||||
_cur_row = 0;
|
||||
}
|
||||
void FbTty::putchar(char c) {
|
||||
if (c == '\n') {
|
||||
next_row();
|
||||
return;
|
||||
}
|
||||
|
||||
_buf[_cur_col][_cur_row] = c;
|
||||
|
||||
draw_char(_cur_col, _cur_row);
|
||||
|
||||
next_col();
|
||||
}
|
||||
void FbTty::putstr(const char* str) {
|
||||
while (*str != 0) {
|
||||
putchar(*str);
|
||||
str++;
|
||||
}
|
||||
}
|
||||
void FbTty::next_col() {
|
||||
_cur_col++;
|
||||
_cur_col = _cur_col % _max_col;
|
||||
if (_cur_col == 0) {
|
||||
next_row();
|
||||
} else {
|
||||
_buf[_cur_col][_cur_row] = ' ';
|
||||
draw_char(_cur_col, _cur_row);
|
||||
}
|
||||
}
|
||||
void FbTty::next_row() {
|
||||
_cur_col = 0;
|
||||
_cur_row++;
|
||||
_cur_row = _cur_row % _max_row;
|
||||
for (int i = 0; i < _max_col; i++) {
|
||||
_buf[i][_cur_row] = ' ';
|
||||
draw_char(i, _cur_row);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "display.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
#include "disp_tools.hpp"
|
||||
|
||||
DMA_ATTR uint8_t SMD::dma_buf[SMD::kLineDataBytes]{};
|
||||
|
||||
// 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; }
|
||||
|
||||
SMD& SMD::get() {
|
||||
static SMD smd;
|
||||
return smd;
|
||||
}
|
||||
|
||||
SMD::SMD() {
|
||||
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 (uint8_t i = 0; i < DISP_HEIGHT; i++) {
|
||||
dma_buf[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
||||
dma_buf[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
||||
}
|
||||
dma_buf[kLineDataBytes - 1] = 0;
|
||||
}
|
||||
|
||||
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() {
|
||||
_vcom = !_vcom;
|
||||
spi_transaction_t t{};
|
||||
|
||||
t.tx_buffer = dma_buf;
|
||||
t.length = SMD::kLineDataBytes * 8;
|
||||
dma_buf[0] = 0b10000000 | (_vcom << 6);
|
||||
|
||||
ESP_ERROR_CHECK(spi_device_transmit(_spi, &t));
|
||||
}
|
||||
|
||||
void SMDSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
|
||||
if (pixel.on)
|
||||
DispTools::get().set_pixel(x, y);
|
||||
else
|
||||
DispTools::get().reset_pixel(x, y);
|
||||
}
|
||||
|
||||
void SMDSurface::clear_impl() { DispTools::get().clear(); }
|
||||
|
||||
int SMDSurface::get_width_impl() const { return DISP_WIDTH; }
|
||||
|
||||
int SMDSurface::get_height_impl() const { return DISP_HEIGHT; }
|
||||
|
||||
EventHandlingResult SMDSurface::handle(SurfaceResizeEvent event) { return _window->handle(event); }
|
||||
|
||||
SMDSurface::SMDSurface(EventLoop* loop) :
|
||||
Surface<SMDSurface, BwPixel>(),
|
||||
EventQueue<SMDSurface, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>(loop, this) {}
|
||||
|
||||
SMDSurface::~SMDSurface() {}
|
||||
@@ -1,166 +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>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "GridWindow.hpp"
|
||||
#include "TextWindow.hpp"
|
||||
#include "display.hpp"
|
||||
|
||||
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();
|
||||
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::get();
|
||||
SMD::get().clear();
|
||||
DispTools::get().clear();
|
||||
// DispTools::get().draw_line(0, 0, 399, 239);
|
||||
// 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;
|
||||
|
||||
EventLoop loop;
|
||||
SMDSurface surface(&loop);
|
||||
|
||||
surface.set_window<GridWindow<SMDSurface, 2, 2>>();
|
||||
|
||||
GridWindow<SMDSurface, 2, 2>* window =
|
||||
static_cast<GridWindow<SMDSurface, 2, 2>*>(surface.get_window());
|
||||
window->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(0, 0, &loop, "hello");
|
||||
window->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(0, 1, &loop, "hello1");
|
||||
window->set_window<GridWindow<SubSurface<SMDSurface>, 2, 2>>(1, 0);
|
||||
GridWindow<SubSurface<SMDSurface>, 2, 2>* window2 =
|
||||
static_cast<GridWindow<SubSurface<SMDSurface>, 2, 2>*>(
|
||||
window->get_subsurface(1, 0).get_window());
|
||||
window->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(1, 1, &loop, "hello3");
|
||||
|
||||
window2->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(
|
||||
0, 0, &loop, "hello2");
|
||||
window2->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(
|
||||
0, 1, &loop, "hello4");
|
||||
window2->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(
|
||||
1, 0, &loop, "hello5");
|
||||
window2->set_window<TextWindow<SubSurface<SMDSurface>, std::string>>(
|
||||
1, 1, &loop, "hello6");
|
||||
|
||||
auto* tl_text = static_cast<TextWindow<SubSurface<SMDSurface>, std::string>*>(
|
||||
window->get_subsurface(0, 0).get_window());
|
||||
|
||||
surface.handle(SurfaceResizeEvent{DISP_WIDTH, DISP_HEIGHT});
|
||||
std::thread loop_thread{[&] { loop.run([&] { DispTools::get().draw_to_display(); }); }};
|
||||
|
||||
uint8_t old_pressed = 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 & BTN_LEFT) && !(old_pressed & BTN_LEFT))
|
||||
surface.push(KeyboardEvent{Key::Left});
|
||||
if ((pressed & BTN_DOWN) && !(old_pressed & BTN_DOWN))
|
||||
surface.push(KeyboardEvent{Key::Down});
|
||||
if ((pressed & BTN_UP) && !(old_pressed & BTN_UP))
|
||||
surface.push(KeyboardEvent{Key::Up});
|
||||
if ((pressed & BTN_RIGHT) && !(old_pressed & BTN_RIGHT))
|
||||
surface.push(KeyboardEvent{Key::Right});
|
||||
if ((pressed & BTN_SELECT) && !(old_pressed & BTN_SELECT))
|
||||
surface.push(KeyboardEvent{Key::Escape});
|
||||
if ((pressed & BTN_START) && !(old_pressed & BTN_START))
|
||||
surface.push(KeyboardEvent{Key::Enter});
|
||||
|
||||
old_pressed = pressed;
|
||||
|
||||
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();
|
||||
tl_text->push(TextUpdateEvent<std::string>(std::format("{:.1f}mA {:.1f}V {:.1f}mAh {}\n Buttons: {:08b}",
|
||||
BatMon::get().get_current(), BatMon::get().get_voltage(),
|
||||
BatMon::get().get_charge(), slow ? "S" : "", pressed)));
|
||||
|
||||
// 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();
|
||||
loop_thread.join();
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 03.03.2025.
|
||||
//
|
||||
|
||||
#include "power_helper.hpp"
|
||||
|
||||
#include <config.hpp>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
PowerHelper& PowerHelper::get() {
|
||||
static PowerHelper powerHelper;
|
||||
return powerHelper;
|
||||
}
|
||||
bool PowerHelper::is_slow() const { return _slow; }
|
||||
void PowerHelper::set_slow(bool slow) {
|
||||
_slow = slow;
|
||||
if (_slow) {
|
||||
xEventGroupClearBits(_event_group, 1);
|
||||
} else {
|
||||
xEventGroupSetBits(_event_group, 1);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
xResult = static_cast<PowerHelper*>(arg)->reset_slow_isr(&xHigherPriorityTaskWoken);
|
||||
if (xResult != pdFAIL) {
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
};
|
||||
}
|
||||
|
||||
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) { set_slow(false); }
|
||||
|
||||
void PowerHelper::delay(int slow_ms, int normal_ms) {
|
||||
if (is_slow()) {
|
||||
auto cur_ticks = xTaskGetTickCount();
|
||||
TickType_t to_wait = slow_ms / portTICK_PERIOD_MS;
|
||||
TickType_t to_wait_normal = normal_ms / portTICK_PERIOD_MS;
|
||||
auto expected_ticks = cur_ticks + to_wait_normal;
|
||||
xEventGroupWaitBits(_event_group, 1, pdFALSE, pdTRUE, to_wait);
|
||||
auto realTicks = xTaskGetTickCount();
|
||||
if (realTicks < expected_ticks) {
|
||||
vTaskDelay(expected_ticks - realTicks);
|
||||
}
|
||||
} else {
|
||||
vTaskDelay(normal_ms / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
void PowerHelper::install_isr() {
|
||||
// gpio_isr_handler_add(EXP_INT, wakeup, this);
|
||||
}
|
||||
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,
|
||||
|
@@ -1,16 +1,37 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(sdk-top)
|
||||
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)
|
||||
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
set(CBSDK_BACKEND_LIBRARY cbsdk_sfml)
|
||||
add_subdirectory(utils)
|
||||
|
||||
add_subdirectory(backend_interface)
|
||||
|
||||
set(CARDBOY_SDK_BACKEND_LIBRARY "" CACHE STRING "Backend implementation library for Cardboy SDK")
|
||||
set(_cardboy_backend_default "${CARDBOY_SDK_BACKEND_LIBRARY}")
|
||||
|
||||
option(CARDBOY_BUILD_SFML "Build desktop SFML backend and launcher" ON)
|
||||
|
||||
if (CARDBOY_BUILD_SFML)
|
||||
add_subdirectory(backends/desktop)
|
||||
if (DEFINED CARDBOY_DESKTOP_BACKEND_TARGET AND NOT CARDBOY_DESKTOP_BACKEND_TARGET STREQUAL "")
|
||||
set(_cardboy_backend_default "${CARDBOY_DESKTOP_BACKEND_TARGET}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_subdirectory(library)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
add_subdirectory(sfml-port)
|
||||
add_subdirectory(examples)
|
||||
if (_cardboy_backend_default STREQUAL "")
|
||||
message(FATAL_ERROR "CARDBOY_SDK_BACKEND_LIBRARY is not set. Provide a backend implementation library or enable one of the available backends.")
|
||||
endif ()
|
||||
|
||||
set(CARDBOY_SDK_BACKEND_LIBRARY "${_cardboy_backend_default}" CACHE STRING "Backend implementation library for Cardboy SDK" FORCE)
|
||||
|
||||
|
||||
add_subdirectory(core)
|
||||
|
||||
add_subdirectory(apps)
|
||||
|
||||
if (CARDBOY_BUILD_SFML)
|
||||
add_subdirectory(launchers/desktop)
|
||||
endif ()
|
||||
|
||||
19
Firmware/sdk/apps/CMakeLists.txt
Normal file
19
Firmware/sdk/apps/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
add_library(cardboy_apps STATIC)
|
||||
|
||||
set_target_properties(cardboy_apps PROPERTIES
|
||||
EXPORT_NAME apps
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_apps
|
||||
PUBLIC
|
||||
cardboy_sdk
|
||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
||||
|
||||
add_subdirectory(menu)
|
||||
add_subdirectory(clock)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(gameboy)
|
||||
add_subdirectory(tetris)
|
||||
9
Firmware/sdk/apps/clock/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/clock/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/clock_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
12
Firmware/sdk/apps/clock/include/cardboy/apps/clock_app.hpp
Normal file
12
Firmware/sdk/apps/clock/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
|
||||
|
||||
238
Firmware/sdk/apps/clock/src/clock_app.cpp
Normal file
238
Firmware/sdk/apps/clock/src/clock_app.cpp
Normal file
@@ -0,0 +1,238 @@
|
||||
#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.frameReady();
|
||||
|
||||
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.sendFrame();
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
17
Firmware/sdk/apps/gameboy/CMakeLists.txt
Normal file
17
Firmware/sdk/apps/gameboy/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/minigb_apu/minigb_apu.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_compile_definitions(cardboy_apps
|
||||
PRIVATE
|
||||
MINIGB_APU_AUDIO_FORMAT_S16SYS=1
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
#include <cstdint>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kGameboyAppName[] = "Game Boy";
|
||||
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
|
||||
|
||||
struct EmbeddedRomDescriptor {
|
||||
std::string_view name;
|
||||
std::string_view saveSlug;
|
||||
const std::uint8_t* start = nullptr;
|
||||
const std::uint8_t* end = nullptr;
|
||||
};
|
||||
|
||||
void setGameboyEmbeddedRoms(std::span<const EmbeddedRomDescriptor> descriptors);
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
4036
Firmware/sdk/apps/gameboy/include/cardboy/apps/peanut_gb.h
Normal file
4036
Firmware/sdk/apps/gameboy/include/cardboy/apps/peanut_gb.h
Normal file
File diff suppressed because it is too large
Load Diff
542
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c
Normal file
542
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.c
Normal file
@@ -0,0 +1,542 @@
|
||||
/**
|
||||
* Game Boy APU emulator.
|
||||
* Copyright (c) 2019 Mahyar Koshkouei <mk@deltabeard.com>
|
||||
* Copyright (c) 2017 Alex Baines <alex@abaines.me.uk>
|
||||
* minigb_apu is released under the terms of the MIT license.
|
||||
*
|
||||
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
|
||||
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "minigb_apu.h"
|
||||
|
||||
#define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ)
|
||||
#define AUDIO_NSAMPLES (AUDIO_SAMPLES_TOTAL)
|
||||
|
||||
#define MAX(a, b) ( a > b ? a : b )
|
||||
#define MIN(a, b) ( a <= b ? a : b )
|
||||
|
||||
/* Factor in which values are multiplied to compensate for fixed-point
|
||||
* arithmetic. Some hard-coded values in this project must be recreated. */
|
||||
#ifndef FREQ_INC_MULT
|
||||
# define FREQ_INC_MULT 105
|
||||
#endif
|
||||
/* Handles time keeping for sound generation.
|
||||
* FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order
|
||||
* to avoid a division by zero error.
|
||||
* Using a square of 2 simplifies calculations. */
|
||||
#define FREQ_INC_REF (AUDIO_SAMPLE_RATE * FREQ_INC_MULT)
|
||||
|
||||
#define MAX_CHAN_VOLUME 15
|
||||
|
||||
static void set_note_freq(struct chan *c)
|
||||
{
|
||||
/* Lowest expected value of freq is 64. */
|
||||
uint32_t freq = (DMG_CLOCK_FREQ_U / 4) / (2048 - c->freq);
|
||||
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
static void chan_enable(struct minigb_apu_ctx *ctx,
|
||||
const uint_fast8_t i, const bool enable)
|
||||
{
|
||||
uint8_t val;
|
||||
|
||||
ctx->chans[i].enabled = enable;
|
||||
val = (ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) |
|
||||
(ctx->chans[3].enabled << 3) | (ctx->chans[2].enabled << 2) |
|
||||
(ctx->chans[1].enabled << 1) | (ctx->chans[0].enabled << 0);
|
||||
|
||||
ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val;
|
||||
}
|
||||
|
||||
static void update_env(struct chan *c)
|
||||
{
|
||||
c->env.counter += c->env.inc;
|
||||
|
||||
while (c->env.counter > FREQ_INC_REF) {
|
||||
if (c->env.step) {
|
||||
c->volume += c->env.up ? 1 : -1;
|
||||
if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) {
|
||||
c->env.inc = 0;
|
||||
}
|
||||
c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume));
|
||||
}
|
||||
c->env.counter -= FREQ_INC_REF;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_len(struct minigb_apu_ctx *ctx, struct chan *c)
|
||||
{
|
||||
if (!c->len.enabled)
|
||||
return;
|
||||
|
||||
c->len.counter += c->len.inc;
|
||||
if (c->len.counter > FREQ_INC_REF) {
|
||||
chan_enable(ctx, c - ctx->chans, 0);
|
||||
c->len.counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool update_freq(struct chan *c, uint32_t *pos)
|
||||
{
|
||||
uint32_t inc = c->freq_inc - *pos;
|
||||
c->freq_counter += inc;
|
||||
|
||||
if (c->freq_counter > FREQ_INC_REF) {
|
||||
*pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF);
|
||||
c->freq_counter = 0;
|
||||
return true;
|
||||
} else {
|
||||
*pos = c->freq_inc;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_sweep(struct chan *c)
|
||||
{
|
||||
c->sweep.counter += c->sweep.inc;
|
||||
|
||||
while (c->sweep.counter > FREQ_INC_REF) {
|
||||
if (c->sweep.shift) {
|
||||
uint16_t inc = (c->sweep.freq >> c->sweep.shift);
|
||||
if (c->sweep.down)
|
||||
inc *= -1;
|
||||
|
||||
c->freq = c->sweep.freq + inc;
|
||||
if (c->freq > 2047) {
|
||||
c->enabled = 0;
|
||||
} else {
|
||||
set_note_freq(c);
|
||||
c->sweep.freq = c->freq;
|
||||
}
|
||||
} else if (c->sweep.rate) {
|
||||
c->enabled = 0;
|
||||
}
|
||||
c->sweep.counter -= FREQ_INC_REF;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_square(struct minigb_apu_ctx *ctx, audio_sample_t *samples,
|
||||
const bool ch2)
|
||||
{
|
||||
struct chan *c = &ctx->chans[ch2];
|
||||
|
||||
if (!c->powered || !c->enabled)
|
||||
return;
|
||||
|
||||
set_note_freq(c);
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(ctx, c);
|
||||
if (!c->enabled)
|
||||
return;
|
||||
|
||||
update_env(c);
|
||||
if (!c->volume)
|
||||
continue;
|
||||
|
||||
if (!ch2)
|
||||
update_sweep(c);
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->square.duty_counter = (c->square.duty_counter + 1) & 7;
|
||||
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
|
||||
c->val = (c->square.duty & (1 << c->square.duty_counter)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += c->val;
|
||||
sample *= c->volume;
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * ctx->vol_l;
|
||||
samples[i + 1] += sample * c->on_right * ctx->vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t wave_sample(struct minigb_apu_ctx *ctx,
|
||||
const unsigned int pos, const unsigned int volume)
|
||||
{
|
||||
uint8_t sample;
|
||||
|
||||
sample = ctx->audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION];
|
||||
if (pos & 1) {
|
||||
sample &= 0xF;
|
||||
} else {
|
||||
sample >>= 4;
|
||||
}
|
||||
return volume ? (sample >> (volume - 1)) : 0;
|
||||
}
|
||||
|
||||
static void update_wave(struct minigb_apu_ctx *ctx, audio_sample_t *samples)
|
||||
{
|
||||
struct chan *c = &ctx->chans[2];
|
||||
|
||||
if (!c->powered || !c->enabled || !c->volume)
|
||||
return;
|
||||
|
||||
set_note_freq(c);
|
||||
c->freq_inc *= 2;
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(ctx, c);
|
||||
if (!c->enabled)
|
||||
return;
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
audio_sample_t sample = 0;
|
||||
|
||||
c->wave.sample = wave_sample(ctx, c->val, c->volume);
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->val = (c->val + 1) & 31;
|
||||
sample += ((pos - prev_pos) / c->freq_inc) *
|
||||
((audio_sample_t)c->wave.sample - 8) *
|
||||
(AUDIO_SAMPLE_MAX/64);
|
||||
c->wave.sample = wave_sample(ctx, c->val, c->volume);
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += ((audio_sample_t)c->wave.sample - 8) *
|
||||
(audio_sample_t)(AUDIO_SAMPLE_MAX/64);
|
||||
{
|
||||
/* First element is unused. */
|
||||
audio_sample_t div[] = { AUDIO_SAMPLE_MAX, 1, 2, 4 };
|
||||
sample = sample / (div[c->volume]);
|
||||
}
|
||||
|
||||
sample /= 4;
|
||||
samples[i + 0] += sample * c->on_left * ctx->vol_l;
|
||||
samples[i + 1] += sample * c->on_right * ctx->vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_noise(struct minigb_apu_ctx *ctx, audio_sample_t *samples)
|
||||
{
|
||||
struct chan *c = &ctx->chans[3];
|
||||
|
||||
if (c->freq >= 14)
|
||||
c->enabled = 0;
|
||||
|
||||
if (!c->powered || !c->enabled)
|
||||
return;
|
||||
|
||||
{
|
||||
const uint32_t lfsr_div_lut[] = {
|
||||
8, 16, 32, 48, 64, 80, 96, 112
|
||||
};
|
||||
uint32_t freq;
|
||||
|
||||
freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq);
|
||||
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(ctx, c);
|
||||
if (!c->enabled)
|
||||
return;
|
||||
|
||||
update_env(c);
|
||||
if (!c->volume)
|
||||
continue;
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) |
|
||||
(c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME);
|
||||
|
||||
if (c->noise.lfsr_wide) {
|
||||
c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^
|
||||
((c->noise.lfsr_reg >> 13) & 1)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
} else {
|
||||
c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^
|
||||
((c->noise.lfsr_reg >> 5) & 1)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
}
|
||||
|
||||
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += c->val;
|
||||
sample *= c->volume;
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * ctx->vol_l;
|
||||
samples[i + 1] += sample * c->on_right * ctx->vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SDL2 style audio callback function.
|
||||
*/
|
||||
void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx,
|
||||
audio_sample_t *stream)
|
||||
{
|
||||
memset(stream, 0, AUDIO_SAMPLES_TOTAL * sizeof(audio_sample_t));
|
||||
update_square(ctx, stream, 0);
|
||||
update_square(ctx, stream, 1);
|
||||
update_wave(ctx, stream);
|
||||
update_noise(ctx, stream);
|
||||
}
|
||||
|
||||
static void chan_trigger(struct minigb_apu_ctx *ctx, uint_fast8_t i)
|
||||
{
|
||||
struct chan *c = &ctx->chans[i];
|
||||
|
||||
chan_enable(ctx, i, 1);
|
||||
c->volume = c->volume_init;
|
||||
|
||||
// volume envelope
|
||||
{
|
||||
/* LUT created in Julia with:
|
||||
* `(FREQ_INC_MULT * 64)./vcat(8, 1:7)`
|
||||
* Must be recreated when FREQ_INC_MULT modified.
|
||||
*/
|
||||
const uint32_t inc_lut[8] = {
|
||||
#if FREQ_INC_MULT == 16
|
||||
128, 1024, 512, 341,
|
||||
256, 205, 171, 146
|
||||
#elif FREQ_INC_MULT == 64
|
||||
512, 4096, 2048, 1365,
|
||||
1024, 819, 683, 585
|
||||
#elif FREQ_INC_MULT == 105
|
||||
/* Multiples of 105 provide integer values. */
|
||||
840, 6720, 3360, 2240,
|
||||
1680, 1344, 1120, 960
|
||||
#else
|
||||
#error "LUT not calculated for this value of FREQ_INC_MULT"
|
||||
#endif
|
||||
};
|
||||
uint8_t val;
|
||||
|
||||
val = ctx->audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION];
|
||||
|
||||
c->env.step = val & 0x7;
|
||||
c->env.up = val & 0x8;
|
||||
c->env.inc = inc_lut[c->env.step];
|
||||
c->env.counter = 0;
|
||||
}
|
||||
|
||||
// freq sweep
|
||||
if (i == 0) {
|
||||
uint8_t val = ctx->audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION];
|
||||
|
||||
c->sweep.freq = c->freq;
|
||||
c->sweep.rate = (val >> 4) & 0x07;
|
||||
c->sweep.down = (val & 0x08);
|
||||
c->sweep.shift = (val & 0x07);
|
||||
c->sweep.inc = c->sweep.rate ?
|
||||
((128u * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0;
|
||||
c->sweep.counter = FREQ_INC_REF;
|
||||
}
|
||||
|
||||
int len_max = 64;
|
||||
|
||||
if (i == 2) { // wave
|
||||
len_max = 256;
|
||||
c->val = 0;
|
||||
} else if (i == 3) { // noise
|
||||
c->noise.lfsr_reg = 0xFFFF;
|
||||
c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
}
|
||||
|
||||
c->len.inc = (256u * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load));
|
||||
c->len.counter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read audio register.
|
||||
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
|
||||
* This is not checked in this function.
|
||||
* \return Byte at address.
|
||||
*/
|
||||
uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr)
|
||||
{
|
||||
static const uint8_t ortab[] = {
|
||||
0x80, 0x3f, 0x00, 0xff, 0xbf,
|
||||
0xff, 0x3f, 0x00, 0xff, 0xbf,
|
||||
0x7f, 0xff, 0x9f, 0xff, 0xbf,
|
||||
0xff, 0xff, 0x00, 0x00, 0xbf,
|
||||
0x00, 0x00, 0x70,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
return ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] |
|
||||
ortab[addr - AUDIO_ADDR_COMPENSATION];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write audio register.
|
||||
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
|
||||
* This is not checked in this function.
|
||||
* \param val Byte to write at address.
|
||||
*/
|
||||
void minigb_apu_audio_write(struct minigb_apu_ctx *ctx,
|
||||
const uint16_t addr, const uint8_t val)
|
||||
{
|
||||
/* Find sound channel corresponding to register address. */
|
||||
uint_fast8_t i;
|
||||
|
||||
if(addr == 0xFF26)
|
||||
{
|
||||
ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80;
|
||||
/* On APU power off, clear all registers apart from wave
|
||||
* RAM. */
|
||||
if((val & 0x80) == 0)
|
||||
{
|
||||
memset(ctx->audio_mem,
|
||||
0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION);
|
||||
ctx->chans[0].enabled = false;
|
||||
ctx->chans[1].enabled = false;
|
||||
ctx->chans[2].enabled = false;
|
||||
ctx->chans[3].enabled = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ignore register writes if APU powered off. */
|
||||
if(ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00)
|
||||
return;
|
||||
|
||||
ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val;
|
||||
i = (addr - AUDIO_ADDR_COMPENSATION) / 5;
|
||||
|
||||
switch (addr) {
|
||||
case 0xFF12:
|
||||
case 0xFF17:
|
||||
case 0xFF21: {
|
||||
ctx->chans[i].volume_init = val >> 4;
|
||||
ctx->chans[i].powered = (val >> 3) != 0;
|
||||
|
||||
// "zombie mode" stuff, needed for Prehistorik Man and probably
|
||||
// others
|
||||
if (ctx->chans[i].powered && ctx->chans[i].enabled) {
|
||||
if ((ctx->chans[i].env.step == 0 && ctx->chans[i].env.inc != 0)) {
|
||||
if (val & 0x08) {
|
||||
ctx->chans[i].volume++;
|
||||
} else {
|
||||
ctx->chans[i].volume += 2;
|
||||
}
|
||||
} else {
|
||||
ctx->chans[i].volume = 16 - ctx->chans[i].volume;
|
||||
}
|
||||
|
||||
ctx->chans[i].volume &= 0x0F;
|
||||
ctx->chans[i].env.step = val & 0x07;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0xFF1C:
|
||||
ctx->chans[i].volume = ctx->chans[i].volume_init = (val >> 5) & 0x03;
|
||||
break;
|
||||
|
||||
case 0xFF11:
|
||||
case 0xFF16:
|
||||
case 0xFF20: {
|
||||
const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF };
|
||||
ctx->chans[i].len.load = val & 0x3f;
|
||||
ctx->chans[i].square.duty = duty_lookup[val >> 6];
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xFF1B:
|
||||
ctx->chans[i].len.load = val;
|
||||
break;
|
||||
|
||||
case 0xFF13:
|
||||
case 0xFF18:
|
||||
case 0xFF1D:
|
||||
ctx->chans[i].freq &= 0xFF00;
|
||||
ctx->chans[i].freq |= val;
|
||||
break;
|
||||
|
||||
case 0xFF1A:
|
||||
ctx->chans[i].powered = (val & 0x80) != 0;
|
||||
chan_enable(ctx, i, val & 0x80);
|
||||
break;
|
||||
|
||||
case 0xFF14:
|
||||
case 0xFF19:
|
||||
case 0xFF1E:
|
||||
ctx->chans[i].freq &= 0x00FF;
|
||||
ctx->chans[i].freq |= ((val & 0x07) << 8);
|
||||
/* Intentional fall-through. */
|
||||
case 0xFF23:
|
||||
ctx->chans[i].len.enabled = val & 0x40;
|
||||
if (val & 0x80)
|
||||
chan_trigger(ctx, i);
|
||||
|
||||
break;
|
||||
|
||||
case 0xFF22:
|
||||
ctx->chans[3].freq = val >> 4;
|
||||
ctx->chans[3].noise.lfsr_wide = !(val & 0x08);
|
||||
ctx->chans[3].noise.lfsr_div = val & 0x07;
|
||||
break;
|
||||
|
||||
case 0xFF24:
|
||||
{
|
||||
ctx->vol_l = ((val >> 4) & 0x07);
|
||||
ctx->vol_r = (val & 0x07);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xFF25:
|
||||
for (uint_fast8_t j = 0; j < 4; j++) {
|
||||
ctx->chans[j].on_left = (val >> (4 + j)) & 1;
|
||||
ctx->chans[j].on_right = (val >> j) & 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void minigb_apu_audio_init(struct minigb_apu_ctx *ctx)
|
||||
{
|
||||
/* Initialise channels and samples. */
|
||||
memset(ctx->chans, 0, sizeof(ctx->chans));
|
||||
ctx->chans[0].val = ctx->chans[1].val = -1;
|
||||
|
||||
/* Initialise IO registers. */
|
||||
{
|
||||
const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F,
|
||||
0xFF, 0x3F, 0x00, 0xFF, 0x3F,
|
||||
0x7F, 0xFF, 0x9F, 0xFF, 0x3F,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x3F,
|
||||
0x77, 0xF3, 0xF1 };
|
||||
|
||||
for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i)
|
||||
minigb_apu_audio_write(ctx, 0xFF10 + i, regs_init[i]);
|
||||
}
|
||||
|
||||
/* Initialise Wave Pattern RAM. */
|
||||
{
|
||||
const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48,
|
||||
0x36, 0x02, 0xcf, 0x16,
|
||||
0x2c, 0x04, 0xe5, 0x2c,
|
||||
0xac, 0xdd, 0xda, 0x48 };
|
||||
|
||||
for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i)
|
||||
minigb_apu_audio_write(ctx, 0xFF30 + i, wave_init[i]);
|
||||
}
|
||||
}
|
||||
149
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h
Normal file
149
Firmware/sdk/apps/gameboy/minigb_apu/minigb_apu.h
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* minigb_apu is released under the terms listed within the LICENSE file.
|
||||
*
|
||||
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
|
||||
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef AUDIO_SAMPLE_RATE
|
||||
# define AUDIO_SAMPLE_RATE 20000
|
||||
#endif
|
||||
|
||||
/* The audio output format is in platform native endian. */
|
||||
#if defined(MINIGB_APU_AUDIO_FORMAT_S16SYS)
|
||||
typedef int16_t audio_sample_t;
|
||||
# define AUDIO_SAMPLE_MAX INT16_MAX
|
||||
# define AUDIO_SAMPLE_MIN INT16_MIN
|
||||
# define VOL_INIT_MAX (AUDIO_SAMPLE_MAX/8)
|
||||
# define VOL_INIT_MIN (AUDIO_SAMPLE_MIN/8)
|
||||
#elif defined(MINIGB_APU_AUDIO_FORMAT_S32SYS)
|
||||
typedef int32_t audio_sample_t;
|
||||
# define AUDIO_SAMPLE_MAX INT32_MAX
|
||||
# define AUDIO_SAMPLE_MIN INT32_MIN
|
||||
# define VOL_INIT_MAX (INT32_MAX/8)
|
||||
# define VOL_INIT_MIN (INT32_MIN/8)
|
||||
#else
|
||||
#error MiniGB APU: Invalid or unsupported audio format selected
|
||||
#endif
|
||||
|
||||
#define DMG_CLOCK_FREQ 4194304.0
|
||||
#define SCREEN_REFRESH_CYCLES 70224.0
|
||||
#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES)
|
||||
|
||||
/* Number of audio samples in each channel. */
|
||||
#define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC))
|
||||
/* Number of audio channels. The audio output is in interleaved stereo format.*/
|
||||
#define AUDIO_CHANNELS 2
|
||||
/* Number of audio samples output in each audio_callback call. */
|
||||
#define AUDIO_SAMPLES_TOTAL (AUDIO_SAMPLES * 2)
|
||||
|
||||
#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1)
|
||||
#define AUDIO_ADDR_COMPENSATION 0xFF10
|
||||
|
||||
struct chan_len_ctr {
|
||||
uint8_t load;
|
||||
uint8_t enabled;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan_vol_env {
|
||||
uint8_t step;
|
||||
uint8_t up;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan_freq_sweep {
|
||||
uint8_t rate;
|
||||
uint8_t shift;
|
||||
uint8_t down;
|
||||
uint16_t freq;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan {
|
||||
uint8_t enabled;
|
||||
uint8_t powered;
|
||||
uint8_t on_left;
|
||||
uint8_t on_right;
|
||||
|
||||
uint8_t volume;
|
||||
uint8_t volume_init;
|
||||
|
||||
uint16_t freq;
|
||||
uint32_t freq_counter;
|
||||
uint32_t freq_inc;
|
||||
|
||||
int32_t val;
|
||||
|
||||
struct chan_len_ctr len;
|
||||
struct chan_vol_env env;
|
||||
struct chan_freq_sweep sweep;
|
||||
|
||||
union {
|
||||
struct {
|
||||
uint8_t duty;
|
||||
uint8_t duty_counter;
|
||||
} square;
|
||||
struct {
|
||||
uint16_t lfsr_reg;
|
||||
uint8_t lfsr_wide;
|
||||
uint8_t lfsr_div;
|
||||
} noise;
|
||||
struct {
|
||||
uint8_t sample;
|
||||
} wave;
|
||||
};
|
||||
};
|
||||
|
||||
struct minigb_apu_ctx {
|
||||
struct chan chans[4];
|
||||
int32_t vol_l, vol_r;
|
||||
|
||||
/**
|
||||
* Memory holding audio registers between 0xFF10 and 0xFF3F inclusive.
|
||||
*/
|
||||
uint8_t audio_mem[AUDIO_MEM_SIZE];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fill allocated buffer "stream" with AUDIO_SAMPLES_TOTAL number of 16-bit
|
||||
* signed samples (native endian order) in stereo interleaved format.
|
||||
* Each call corresponds to the time taken for each VSYNC in the Game Boy.
|
||||
*
|
||||
* \param ctx Library context. Must be initialised with audio_init().
|
||||
* \param stream Allocated pointer to store audio samples. Must be at least
|
||||
* AUDIO_SAMPLES_TOTAL in size.
|
||||
*/
|
||||
void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx,
|
||||
audio_sample_t *stream);
|
||||
|
||||
/**
|
||||
* Read audio register at given address "addr".
|
||||
* \param ctx Library context. Must be initialised with audio_init().
|
||||
* \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F,
|
||||
* inclusive.
|
||||
*/
|
||||
uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr);
|
||||
|
||||
/**
|
||||
* Write "val" to audio register at given address "addr".
|
||||
* \param ctx Library context. Must be initialised with audio_init().
|
||||
* \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F,
|
||||
* inclusive.
|
||||
* \param val Value to write to address.
|
||||
*/
|
||||
void minigb_apu_audio_write(struct minigb_apu_ctx *ctx,
|
||||
const uint16_t addr, const uint8_t val);
|
||||
|
||||
/**
|
||||
* Initialise audio driver.
|
||||
* \param ctx Library context.
|
||||
*/
|
||||
void minigb_apu_audio_init(struct minigb_apu_ctx *ctx);
|
||||
1812
Firmware/sdk/apps/gameboy/src/gameboy_app.cpp
Normal file
1812
Firmware/sdk/apps/gameboy/src/gameboy_app.cpp
Normal file
File diff suppressed because it is too large
Load Diff
9
Firmware/sdk/apps/menu/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/menu/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/menu_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
16
Firmware/sdk/apps/menu/include/cardboy/apps/menu_app.hpp
Normal file
16
Firmware/sdk/apps/menu/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
|
||||
|
||||
176
Firmware/sdk/apps/menu/src/menu_app.cpp
Normal file
176
Firmware/sdk/apps/menu/src/menu_app.cpp
Normal file
@@ -0,0 +1,176 @@
|
||||
#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.start && !previous.start);
|
||||
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.frameReady();
|
||||
|
||||
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 START APP", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
|
||||
}
|
||||
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
9
Firmware/sdk/apps/settings/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/settings/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/settings_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kSettingsAppName[] = "Settings";
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
198
Firmware/sdk/apps/settings/src/settings_app.cpp
Normal file
198
Firmware/sdk/apps/settings/src/settings_app.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "cardboy/apps/settings_app.hpp"
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/persistent_settings.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace apps {
|
||||
|
||||
namespace {
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
using Framebuffer = typename AppContext::Framebuffer;
|
||||
|
||||
enum class SettingOption {
|
||||
Sound,
|
||||
AutoLightSleep,
|
||||
};
|
||||
|
||||
constexpr std::array<SettingOption, 2> kOptions = {
|
||||
SettingOption::Sound,
|
||||
SettingOption::AutoLightSleep,
|
||||
};
|
||||
|
||||
class SettingsApp final : public cardboy::sdk::IApp {
|
||||
public:
|
||||
explicit SettingsApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
||||
|
||||
void onStart() override {
|
||||
loadSettings();
|
||||
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;
|
||||
|
||||
const bool previousAvailable = buzzerAvailable;
|
||||
syncBuzzerState();
|
||||
if (previousAvailable != buzzerAvailable)
|
||||
dirty = true;
|
||||
|
||||
if (current.b && !previous.b) {
|
||||
context.requestAppSwitchByName(kMenuAppName);
|
||||
return;
|
||||
}
|
||||
|
||||
bool moved = false;
|
||||
if (current.down && !previous.down) {
|
||||
moveSelection(+1);
|
||||
moved = true;
|
||||
} else if (current.up && !previous.up) {
|
||||
moveSelection(-1);
|
||||
moved = true;
|
||||
}
|
||||
|
||||
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
|
||||
(current.select && !previous.select);
|
||||
if (togglePressed)
|
||||
handleToggle();
|
||||
|
||||
if (moved)
|
||||
dirty = true;
|
||||
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
private:
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
|
||||
bool buzzerAvailable = false;
|
||||
cardboy::sdk::PersistentSettings settings{};
|
||||
std::size_t selectedIndex = 0;
|
||||
bool dirty = false;
|
||||
|
||||
void loadSettings() {
|
||||
settings = cardboy::sdk::loadPersistentSettings(context.getServices());
|
||||
syncBuzzerState();
|
||||
}
|
||||
|
||||
void syncBuzzerState() {
|
||||
auto* buzzer = context.buzzer();
|
||||
buzzerAvailable = (buzzer != nullptr);
|
||||
if (!buzzer)
|
||||
return;
|
||||
if (buzzer->isMuted() != settings.mute)
|
||||
buzzer->setMuted(settings.mute);
|
||||
}
|
||||
|
||||
void moveSelection(int delta) {
|
||||
const int count = static_cast<int>(kOptions.size());
|
||||
if (count == 0)
|
||||
return;
|
||||
const int current = static_cast<int>(selectedIndex);
|
||||
int next = (current + delta) % count;
|
||||
if (next < 0)
|
||||
next += count;
|
||||
selectedIndex = static_cast<std::size_t>(next);
|
||||
}
|
||||
|
||||
void handleToggle() {
|
||||
switch (kOptions[selectedIndex]) {
|
||||
case SettingOption::Sound:
|
||||
toggleSound();
|
||||
break;
|
||||
case SettingOption::AutoLightSleep:
|
||||
toggleAutoLightSleep();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void toggleSound() {
|
||||
if (!buzzerAvailable)
|
||||
return;
|
||||
settings.mute = !settings.mute;
|
||||
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
|
||||
syncBuzzerState();
|
||||
if (!settings.mute) {
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->beepMove();
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void toggleAutoLightSleep() {
|
||||
settings.autoLightSleep = !settings.autoLightSleep;
|
||||
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 1) {
|
||||
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 drawOptionRow(int row, std::string_view label, std::string_view value, bool selected) {
|
||||
std::string prefix = selected ? "> " : " ";
|
||||
std::string line = prefix;
|
||||
line.append(label);
|
||||
line.append(": ");
|
||||
line.append(value);
|
||||
const int x = 24;
|
||||
const int y = 56 + row * 24;
|
||||
font16x8::drawText(framebuffer, x, y, line, 1, true, 1);
|
||||
}
|
||||
|
||||
void renderIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
framebuffer.frameReady();
|
||||
framebuffer.clear(false);
|
||||
|
||||
drawCenteredText(framebuffer, 24, "SETTINGS", 1, 1);
|
||||
|
||||
const std::string soundValue = buzzerAvailable ? (settings.mute ? "OFF" : "ON") : "N/A";
|
||||
drawOptionRow(0, "SOUND", soundValue, selectedIndex == 0);
|
||||
|
||||
const std::string lightSleepValue = settings.autoLightSleep ? "ON" : "OFF";
|
||||
drawOptionRow(1, "AUTO LIGHT SLEEP", lightSleepValue, selectedIndex == 1);
|
||||
|
||||
if (!buzzerAvailable)
|
||||
drawCenteredText(framebuffer, 120, "SOUND CONTROL UNAVAILABLE", 1, 1);
|
||||
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 54, "UP/DOWN MOVE", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 36, "A/START/SELECT TOGGLE", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK | LIGHT SLEEP AFTER RESET", 1, 1);
|
||||
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
};
|
||||
|
||||
class SettingsAppFactory final : public cardboy::sdk::IAppFactory {
|
||||
public:
|
||||
const char* name() const override { return kSettingsAppName; }
|
||||
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
||||
return std::make_unique<SettingsApp>(context);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory() { return std::make_unique<SettingsAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
9
Firmware/sdk/apps/tetris/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/tetris/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/tetris_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
12
Firmware/sdk/apps/tetris/include/cardboy/apps/tetris_app.hpp
Normal file
12
Firmware/sdk/apps/tetris/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
|
||||
|
||||
657
Firmware/sdk/apps/tetris/src/tetris_app.cpp
Normal file
657
Firmware/sdk/apps/tetris/src/tetris_app.cpp
Normal file
@@ -0,0 +1,657 @@
|
||||
#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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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();
|
||||
} 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.frameReady();
|
||||
|
||||
drawBoard();
|
||||
drawActivePiece();
|
||||
drawNextPreview();
|
||||
drawHUD();
|
||||
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
|
||||
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 - 1, true);
|
||||
}
|
||||
}
|
||||
drawBoardFrame(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, false);
|
||||
}
|
||||
}
|
||||
|
||||
static bool patternPixel(int pieceIndex, int dx, int dy) {
|
||||
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
|
||||
const int mx = dx & 0x3;
|
||||
const int my = dy & 0x3;
|
||||
switch (idx) {
|
||||
case 0: // I - vertical stripes
|
||||
return mx < 2;
|
||||
case 1: // J - horizontal stripes
|
||||
return my < 2;
|
||||
case 2: { // L - forward diagonal
|
||||
const int sum = (mx + my) & 0x3;
|
||||
return sum < 2;
|
||||
}
|
||||
case 3: // O - diamond centerpiece
|
||||
return (mx == 1 && my == 1) || (mx == 2 && my == 1) || (mx == 1 && my == 2) || (mx == 2 && my == 2);
|
||||
case 4: // S - checkerboard
|
||||
return ((mx ^ my) & 0x1) == 0;
|
||||
case 5: { // T - cross
|
||||
return (mx == 0) || (my == 0);
|
||||
}
|
||||
case 6: { // Z - backward diagonal
|
||||
int diff = mx - my;
|
||||
if (diff < 0)
|
||||
diff += 4;
|
||||
diff &= 0x3;
|
||||
return diff < 2;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void drawPatternBlock(int x0, int y0, int size, int pieceIndex, bool locked) {
|
||||
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
|
||||
for (int dy = 0; dy < size; ++dy) {
|
||||
for (int dx = 0; dx < size; ++dx) {
|
||||
const bool border = dx == 0 || dx == size - 1 || dy == 0 || dy == size - 1;
|
||||
bool fill = patternPixel(idx, dx, dy);
|
||||
if (!locked && !border)
|
||||
fill = fill && (((dx + dy) & 0x1) == 0);
|
||||
const bool on = border || fill;
|
||||
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawCell(int originX, int originY, int cx, int cy, int pieceIndex, bool locked) {
|
||||
const int x0 = originX + cx * kCellSize;
|
||||
const int y0 = originY + cy * kCellSize;
|
||||
drawPatternBlock(x0, y0, kCellSize, pieceIndex, locked);
|
||||
}
|
||||
|
||||
void drawBoardFrame(int originX, int originY) {
|
||||
const int widthPixels = kBoardWidth * kCellSize;
|
||||
const int heightPixels = kBoardHeight * kCellSize;
|
||||
const int x0 = originX;
|
||||
const int y0 = originY;
|
||||
const int x1 = originX + widthPixels - 1;
|
||||
const int y1 = originY + heightPixels - 1;
|
||||
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
framebuffer.drawPixel(x, y0, true);
|
||||
framebuffer.drawPixel(x, y1, true);
|
||||
}
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
framebuffer.drawPixel(x0, y, true);
|
||||
framebuffer.drawPixel(x1, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
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 + 1;
|
||||
const int py = originY + (block.y + 1) * blockSize + 1;
|
||||
drawPatternBlock(px, py, blockSize - 2, state.nextPiece, 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
|
||||
24
Firmware/sdk/backend_interface/CMakeLists.txt
Normal file
24
Firmware/sdk/backend_interface/CMakeLists.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
add_library(cardboy_backend_interface INTERFACE)
|
||||
|
||||
target_link_libraries(cardboy_backend_interface INTERFACE cardboy_utils)
|
||||
|
||||
set_target_properties(cardboy_backend_interface PROPERTIES
|
||||
EXPORT_NAME backend_interface
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_backend_interface
|
||||
INTERFACE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_sources(cardboy_backend_interface
|
||||
INTERFACE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/backend/backend_interface.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/backend.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/display_spec.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/event_bus.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/loop_hooks.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/input_state.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/platform.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/services.hpp
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
|
||||
namespace cardboy::backend {
|
||||
|
||||
template<typename Backend>
|
||||
concept BackendInterface = requires {
|
||||
typename Backend::Framebuffer;
|
||||
typename Backend::Input;
|
||||
typename Backend::Clock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cardboy/sdk/platform.hpp>
|
||||
|
||||
#include <cardboy/backend/backend_impl.hpp>
|
||||
#include <cardboy/backend/backend_interface.hpp>
|
||||
|
||||
namespace cardboy::backend {
|
||||
static_assert(BackendInterface<ActiveBackend>, "ActiveBackend must provide Framebuffer, Input, Clock types");
|
||||
} // namespace cardboy::backend
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
inline constexpr int kDisplayWidth = 400;
|
||||
inline constexpr int kDisplayHeight = 240;
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
enum class EventBusSignal : std::uint32_t {
|
||||
None = 0,
|
||||
Input = 1u << 0,
|
||||
Timer = 1u << 1,
|
||||
External = 1u << 2,
|
||||
};
|
||||
|
||||
inline EventBusSignal operator|(EventBusSignal lhs, EventBusSignal rhs) {
|
||||
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) | static_cast<std::uint32_t>(rhs));
|
||||
}
|
||||
|
||||
inline EventBusSignal& operator|=(EventBusSignal& lhs, EventBusSignal rhs) {
|
||||
lhs = lhs | rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
inline EventBusSignal operator&(EventBusSignal lhs, EventBusSignal rhs) {
|
||||
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) & static_cast<std::uint32_t>(rhs));
|
||||
}
|
||||
|
||||
inline std::uint32_t to_event_bits(EventBusSignal signal) { return static_cast<std::uint32_t>(signal); }
|
||||
|
||||
class IEventBus {
|
||||
public:
|
||||
static constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
|
||||
|
||||
virtual ~IEventBus() = default;
|
||||
|
||||
virtual void signal(std::uint32_t bits) = 0;
|
||||
virtual void signalFromISR(std::uint32_t bits) = 0;
|
||||
virtual std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) = 0;
|
||||
virtual void scheduleTimerSignal(std::uint32_t delay_ms) = 0;
|
||||
virtual void cancelTimerSignal() = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class FramebufferHooks {
|
||||
public:
|
||||
using PreSendHook = void (*)(void* framebuffer, void* userData);
|
||||
|
||||
static void setPreSendHook(PreSendHook hook, void* userData);
|
||||
static void clearPreSendHook();
|
||||
static void invokePreSend(void* framebuffer);
|
||||
|
||||
private:
|
||||
static PreSendHook hook_;
|
||||
static void* userData_;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -0,0 +1,16 @@
|
||||
#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
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class ILoopHooks {
|
||||
public:
|
||||
virtual ~ILoopHooks() = default;
|
||||
virtual void onLoopIteration() = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
137
Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp
Normal file
137
Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
namespace detail {
|
||||
template<typename Impl>
|
||||
concept HasClearImpl = requires(Impl& impl, bool value) {
|
||||
{ impl.clear_impl(value) };
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasFrameReadyImpl = requires(Impl& impl) {
|
||||
{ impl.frameReady_impl() };
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasSendFrameImpl = requires(Impl& impl, bool flag) {
|
||||
{ impl.sendFrame_impl(flag) };
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasDrawBits8Impl = requires(Impl& impl, int x, int y, std::uint8_t bits) { impl.drawBits8_impl(x, y, bits); };
|
||||
|
||||
template<typename Impl>
|
||||
concept HasFrameInFlightImpl = requires(const Impl& impl) {
|
||||
{ impl.frameInFlight_impl() } -> std::convertible_to<bool>;
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) {
|
||||
{ impl.sleep_ms_impl(value) };
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
template<typename Impl>
|
||||
class FramebufferFacade {
|
||||
public:
|
||||
[[nodiscard]] __attribute__((always_inline)) int width() const { return impl().width_impl(); }
|
||||
[[nodiscard]] __attribute__((always_inline)) int height() const { return impl().height_impl(); }
|
||||
|
||||
__attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); }
|
||||
|
||||
__attribute__((always_inline)) void drawBits8(int x, int y, std::uint8_t bits) {
|
||||
if constexpr (detail::HasDrawBits8Impl<Impl>) {
|
||||
impl().drawBits8_impl(x, y, bits);
|
||||
} else {
|
||||
defaultDrawBits8(x, y, bits);
|
||||
}
|
||||
}
|
||||
|
||||
void clear(bool on) {
|
||||
if constexpr (detail::HasClearImpl<Impl>) {
|
||||
impl().clear_impl(on);
|
||||
} else {
|
||||
defaultClear(on);
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) void frameReady() {
|
||||
if constexpr (detail::HasFrameReadyImpl<Impl>)
|
||||
impl().frameReady_impl();
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
|
||||
if constexpr (detail::HasSendFrameImpl<Impl>) {
|
||||
FramebufferHooks::invokePreSend(&impl());
|
||||
impl().sendFrame_impl(clearDrawBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
|
||||
if constexpr (detail::HasFrameInFlightImpl<Impl>)
|
||||
return impl().frameInFlight_impl();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected:
|
||||
FramebufferFacade() = default;
|
||||
~FramebufferFacade() = default;
|
||||
|
||||
private:
|
||||
[[nodiscard]] __attribute__((always_inline)) Impl& impl() { return static_cast<Impl&>(*this); }
|
||||
[[nodiscard]] __attribute__((always_inline)) const Impl& impl() const { return static_cast<const Impl&>(*this); }
|
||||
|
||||
void defaultClear(bool on) {
|
||||
for (int y = 0; y < height(); ++y)
|
||||
for (int x = 0; x < width(); ++x)
|
||||
drawPixel(x, y, on);
|
||||
}
|
||||
|
||||
void defaultDrawBits8(int x, int y, std::uint8_t bits) {
|
||||
for (int col = 0; col < 8; ++col) {
|
||||
const std::uint8_t mask = static_cast<std::uint8_t>(1u << (7 - col));
|
||||
const bool pixelOn = (bits & mask) != 0;
|
||||
drawPixel(x + col, y, pixelOn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
class InputFacade {
|
||||
public:
|
||||
InputState readState() { return impl().readState_impl(); }
|
||||
|
||||
protected:
|
||||
InputFacade() = default;
|
||||
~InputFacade() = default;
|
||||
|
||||
private:
|
||||
[[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
class ClockFacade {
|
||||
public:
|
||||
std::uint32_t millis() { return impl().millis_impl(); }
|
||||
|
||||
void sleep_ms(std::uint32_t ms) {
|
||||
if constexpr (detail::HasSleepMsImpl<Impl>)
|
||||
impl().sleep_ms_impl(ms);
|
||||
}
|
||||
|
||||
protected:
|
||||
ClockFacade() = default;
|
||||
~ClockFacade() = default;
|
||||
|
||||
private:
|
||||
[[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/event_bus.hpp"
|
||||
#include "cardboy/sdk/loop_hooks.hpp"
|
||||
|
||||
#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 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;
|
||||
IFilesystem* filesystem = nullptr;
|
||||
IEventBus* eventBus = nullptr;
|
||||
ILoopHooks* loopHooks = nullptr;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
38
Firmware/sdk/backends/desktop/CMakeLists.txt
Normal file
38
Firmware/sdk/backends/desktop/CMakeLists.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
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_library(cardboy_backend_desktop STATIC
|
||||
src/desktop_backend.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_backend_desktop PROPERTIES
|
||||
EXPORT_NAME backend_desktop
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_backend_desktop
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_backend_desktop
|
||||
PUBLIC
|
||||
cardboy_backend_interface
|
||||
SFML::Window
|
||||
SFML::Graphics
|
||||
SFML::System
|
||||
)
|
||||
|
||||
set(CARDBOY_DESKTOP_BACKEND_TARGET cardboy_backend_desktop PARENT_SCOPE)
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/backend/desktop_backend.hpp"
|
||||
|
||||
namespace cardboy::backend {
|
||||
using ActiveBackend = DesktopBackend;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/event_bus.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window/Keyboard.hpp>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::backend::desktop {
|
||||
|
||||
constexpr int kPixelScale = 2;
|
||||
|
||||
class DesktopRuntime;
|
||||
|
||||
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;
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::uint32_t> data;
|
||||
|
||||
static std::string composeKey(std::string_view ns, std::string_view key);
|
||||
};
|
||||
|
||||
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
DesktopRandom();
|
||||
|
||||
[[nodiscard]] std::uint32_t nextUint32() override;
|
||||
|
||||
private:
|
||||
std::mt19937 rng;
|
||||
std::uniform_int_distribution<std::uint32_t> dist;
|
||||
};
|
||||
|
||||
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
DesktopHighResClock();
|
||||
|
||||
[[nodiscard]] std::uint64_t micros() override;
|
||||
|
||||
private:
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
DesktopFilesystem();
|
||||
|
||||
bool mount() override;
|
||||
[[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 DesktopEventBus final : public cardboy::sdk::IEventBus {
|
||||
public:
|
||||
explicit DesktopEventBus(DesktopRuntime& owner);
|
||||
~DesktopEventBus() override;
|
||||
|
||||
void signal(std::uint32_t bits) override;
|
||||
void signalFromISR(std::uint32_t bits) override;
|
||||
std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override;
|
||||
void scheduleTimerSignal(std::uint32_t delay_ms) override;
|
||||
void cancelTimerSignal() override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::uint32_t pendingBits = 0;
|
||||
|
||||
std::mutex timerMutex;
|
||||
std::condition_variable timerCv;
|
||||
std::thread timerThread;
|
||||
bool timerCancel = false;
|
||||
};
|
||||
|
||||
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
|
||||
public:
|
||||
explicit DesktopFramebuffer(DesktopRuntime& runtime);
|
||||
|
||||
[[nodiscard]] int width_impl() const;
|
||||
[[nodiscard]] int height_impl() const;
|
||||
void drawPixel_impl(int x, int y, bool on);
|
||||
void clear_impl(bool on);
|
||||
void frameReady_impl();
|
||||
void sendFrame_impl(bool clearAfterSend);
|
||||
[[nodiscard]] bool frameInFlight_impl() const { return false; }
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
};
|
||||
|
||||
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
|
||||
public:
|
||||
explicit DesktopInput(DesktopRuntime& runtime);
|
||||
|
||||
cardboy::sdk::InputState readState_impl();
|
||||
void handleKey(sf::Keyboard::Key key, bool pressed);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
cardboy::sdk::InputState state{};
|
||||
};
|
||||
|
||||
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
|
||||
public:
|
||||
explicit DesktopClock(DesktopRuntime& runtime);
|
||||
|
||||
std::uint32_t millis_impl();
|
||||
void sleep_ms_impl(std::uint32_t ms);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopRuntime {
|
||||
public:
|
||||
DesktopRuntime();
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry();
|
||||
void processEvents();
|
||||
void presentIfNeeded();
|
||||
void sleepFor(std::uint32_t ms);
|
||||
|
||||
[[nodiscard]] bool isRunning() const { return running; }
|
||||
|
||||
DesktopFramebuffer framebuffer;
|
||||
DesktopInput input;
|
||||
DesktopClock clock;
|
||||
|
||||
private:
|
||||
friend class DesktopFramebuffer;
|
||||
friend class DesktopInput;
|
||||
friend class DesktopClock;
|
||||
|
||||
void setPixel(int x, int y, bool on);
|
||||
void clearPixels(bool on);
|
||||
|
||||
sf::RenderWindow window;
|
||||
sf::Texture texture;
|
||||
sf::Sprite sprite;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
bool dirty = true;
|
||||
bool running = true;
|
||||
bool clearNextFrame = true;
|
||||
|
||||
DesktopBuzzer buzzerService;
|
||||
DesktopBattery batteryService;
|
||||
DesktopStorage storageService;
|
||||
DesktopRandom randomService;
|
||||
DesktopHighResClock highResService;
|
||||
DesktopFilesystem filesystemService;
|
||||
DesktopEventBus eventBusService;
|
||||
cardboy::sdk::Services services{};
|
||||
};
|
||||
|
||||
struct Backend {
|
||||
using Framebuffer = DesktopFramebuffer;
|
||||
using Input = DesktopInput;
|
||||
using Clock = DesktopClock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend::desktop
|
||||
|
||||
namespace cardboy::backend {
|
||||
using DesktopBackend = desktop::Backend;
|
||||
} // namespace cardboy::backend
|
||||
326
Firmware/sdk/backends/desktop/src/desktop_backend.cpp
Normal file
326
Firmware/sdk/backends/desktop/src/desktop_backend.cpp
Normal file
@@ -0,0 +1,326 @@
|
||||
#include "cardboy/backend/desktop_backend.hpp"
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
namespace cardboy::backend::desktop {
|
||||
|
||||
DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {}
|
||||
|
||||
DesktopEventBus::~DesktopEventBus() { cancelTimerSignal(); }
|
||||
|
||||
void DesktopEventBus::signal(std::uint32_t bits) {
|
||||
if (bits == 0)
|
||||
return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
pendingBits |= bits;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
void DesktopEventBus::signalFromISR(std::uint32_t bits) { signal(bits); }
|
||||
|
||||
std::uint32_t DesktopEventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
|
||||
if (mask == 0)
|
||||
return 0;
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const bool infinite = timeout_ms == cardboy::sdk::IEventBus::kWaitForever;
|
||||
|
||||
while (true) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
const std::uint32_t ready = pendingBits & mask;
|
||||
if (ready != 0) {
|
||||
pendingBits &= ~mask;
|
||||
return ready;
|
||||
}
|
||||
}
|
||||
|
||||
if (!infinite) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
|
||||
if (elapsedMs >= static_cast<std::int64_t>(timeout_ms))
|
||||
return 0;
|
||||
const auto remaining = timeout_ms - static_cast<std::uint32_t>(elapsedMs);
|
||||
runtime.sleepFor(std::min<std::uint32_t>(remaining, 8));
|
||||
} else {
|
||||
runtime.sleepFor(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopEventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
|
||||
cancelTimerSignal();
|
||||
|
||||
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
|
||||
return;
|
||||
|
||||
if (delay_ms == 0) {
|
||||
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(timerMutex);
|
||||
timerCancel = false;
|
||||
}
|
||||
|
||||
timerThread = std::thread([this, delay_ms]() {
|
||||
std::unique_lock<std::mutex> lock(timerMutex);
|
||||
const bool cancelled =
|
||||
timerCv.wait_for(lock, std::chrono::milliseconds(delay_ms), [this] { return timerCancel; });
|
||||
lock.unlock();
|
||||
if (!cancelled)
|
||||
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
|
||||
});
|
||||
}
|
||||
|
||||
void DesktopEventBus::cancelTimerSignal() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(timerMutex);
|
||||
timerCancel = true;
|
||||
}
|
||||
timerCv.notify_all();
|
||||
if (timerThread.joinable())
|
||||
timerThread.join();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(timerMutex);
|
||||
timerCancel = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DesktopStorage::readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) {
|
||||
auto it = data.find(composeKey(ns, key));
|
||||
if (it == data.end())
|
||||
return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DesktopStorage::writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) {
|
||||
data[composeKey(ns, key)] = value;
|
||||
}
|
||||
|
||||
std::string DesktopStorage::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;
|
||||
}
|
||||
|
||||
DesktopRandom::DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits<std::uint32_t>::max()) {}
|
||||
|
||||
std::uint32_t DesktopRandom::nextUint32() { return dist(rng); }
|
||||
|
||||
DesktopHighResClock::DesktopHighResClock() : start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint64_t DesktopHighResClock::micros() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(now - start).count());
|
||||
}
|
||||
|
||||
DesktopFilesystem::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 DesktopFilesystem::mount() {
|
||||
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;
|
||||
}
|
||||
|
||||
DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
||||
|
||||
int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
||||
|
||||
void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); }
|
||||
|
||||
void DesktopFramebuffer::frameReady_impl() {
|
||||
if (runtime.clearNextFrame) {
|
||||
runtime.clearPixels(false);
|
||||
runtime.clearNextFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) {
|
||||
runtime.clearNextFrame = clearAfterSend;
|
||||
runtime.dirty = true;
|
||||
}
|
||||
|
||||
DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
|
||||
|
||||
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
||||
bool handled = true;
|
||||
switch (key) {
|
||||
case sf::Keyboard::Key::Up:
|
||||
state.up = pressed;
|
||||
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:
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
if (handled)
|
||||
runtime.eventBusService.signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
|
||||
}
|
||||
|
||||
DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint32_t DesktopClock::millis_impl() {
|
||||
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 DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); }
|
||||
|
||||
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), eventBusService(*this) {
|
||||
window.setFramerateLimit(60);
|
||||
if (!texture.resize(sf::Vector2u{cardboy::sdk::kDisplayWidth, cardboy::sdk::kDisplayHeight}))
|
||||
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
|
||||
sprite.setTexture(texture, true);
|
||||
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
|
||||
clearPixels(true);
|
||||
presentIfNeeded();
|
||||
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResService;
|
||||
services.filesystem = &filesystemService;
|
||||
services.eventBus = &eventBusService;
|
||||
services.loopHooks = nullptr;
|
||||
}
|
||||
|
||||
cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; }
|
||||
|
||||
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>(0) : static_cast<std::uint8_t>(255);
|
||||
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>(0) : static_cast<std::uint8_t>(255);
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace cardboy::backend::desktop
|
||||
25
Firmware/sdk/core/CMakeLists.txt
Normal file
25
Firmware/sdk/core/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
add_library(cardboy_sdk STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_sdk PROPERTIES
|
||||
EXPORT_NAME sdk
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_sdk
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||
|
||||
target_link_libraries(cardboy_sdk
|
||||
PUBLIC
|
||||
cardboy_backend_interface
|
||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||
)
|
||||
127
Firmware/sdk/core/include/cardboy/gfx/font16x8.hpp
Normal file
127
Firmware/sdk/core/include/cardboy/gfx/font16x8.hpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#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];
|
||||
}
|
||||
|
||||
enum class Rotation {
|
||||
None,
|
||||
Clockwise90,
|
||||
CounterClockwise90,
|
||||
};
|
||||
|
||||
struct TextBounds {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
template<typename Framebuffer>
|
||||
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
|
||||
Rotation rotation = Rotation::None) {
|
||||
const auto& rows = glyphBitmap(ch);
|
||||
if (rotation == Rotation::None && scale == 1 && on && ((x % 8) == 0)) {
|
||||
for (int row = 0; row < kGlyphHeight; ++row) {
|
||||
const uint8_t rowBits = rows[row];
|
||||
fb.drawBits8(x, y + row, rowBits);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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) == 0)
|
||||
continue;
|
||||
for (int sx = 0; sx < scale; ++sx) {
|
||||
for (int sy = 0; sy < scale; ++sy) {
|
||||
int dstX = x;
|
||||
int dstY = y;
|
||||
switch (rotation) {
|
||||
case Rotation::None:
|
||||
dstX += col * scale + sx;
|
||||
dstY += row * scale + sy;
|
||||
break;
|
||||
case Rotation::Clockwise90:
|
||||
dstX += row * scale + sx;
|
||||
dstY += (kGlyphWidth - 1 - col) * scale + sy;
|
||||
break;
|
||||
case Rotation::CounterClockwise90:
|
||||
dstX += (kGlyphHeight - 1 - row) * scale + sx;
|
||||
dstY += col * scale + sy;
|
||||
break;
|
||||
}
|
||||
fb.drawPixel(dstX, dstY, on);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
|
||||
Rotation rotation = Rotation::None) {
|
||||
if (text.empty())
|
||||
return {};
|
||||
const int advance = (kGlyphWidth + letterSpacing) * scale;
|
||||
const int extent = static_cast<int>(text.size()) * advance - letterSpacing * scale;
|
||||
const int height = kGlyphHeight * scale;
|
||||
switch (rotation) {
|
||||
case Rotation::None:
|
||||
return {extent, height};
|
||||
case Rotation::Clockwise90:
|
||||
case Rotation::CounterClockwise90:
|
||||
return {height, extent};
|
||||
}
|
||||
return {extent, height};
|
||||
}
|
||||
|
||||
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, Rotation rotation = Rotation::None) {
|
||||
if (text.empty())
|
||||
return;
|
||||
const int advance = (kGlyphWidth + letterSpacing) * scale;
|
||||
if (rotation == Rotation::None) {
|
||||
int cursor = x;
|
||||
for (char ch: text) {
|
||||
drawGlyph(fb, cursor, y, ch, scale, on, rotation);
|
||||
cursor += advance;
|
||||
}
|
||||
} else {
|
||||
int cursor = y;
|
||||
for (char ch: text) {
|
||||
drawGlyph(fb, x, cursor, ch, scale, on, rotation);
|
||||
cursor += advance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace font16x8
|
||||
134
Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp
Normal file
134
Firmware/sdk/core/include/cardboy/sdk/app_framework.hpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <cardboy/sdk/backend.hpp>
|
||||
#include <cardboy/sdk/platform.hpp>
|
||||
#include <cardboy/sdk/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{};
|
||||
};
|
||||
|
||||
using ActiveBackend = cardboy::backend::ActiveBackend;
|
||||
|
||||
struct AppContext {
|
||||
using Framebuffer = typename ActiveBackend::Framebuffer;
|
||||
using Input = typename ActiveBackend::Input;
|
||||
using Clock = typename ActiveBackend::Clock;
|
||||
|
||||
AppContext() = delete;
|
||||
AppContext(Framebuffer& fb, Input& in, Clock& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
|
||||
Framebuffer& framebuffer;
|
||||
Input& input;
|
||||
Clock& 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]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
|
||||
[[nodiscard]] IEventBus* eventBus() const { return services ? services->eventBus : nullptr; }
|
||||
[[nodiscard]] ILoopHooks* loopHooks() const { return services ? services->loopHooks : 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();
|
||||
};
|
||||
|
||||
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
|
||||
83
Firmware/sdk/core/include/cardboy/sdk/app_system.hpp
Normal file
83
Firmware/sdk/core/include/cardboy/sdk/app_system.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <cardboy/sdk/event_bus.hpp>
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class AppSystem {
|
||||
public:
|
||||
explicit AppSystem(AppContext context);
|
||||
~AppSystem();
|
||||
|
||||
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:
|
||||
friend struct AppContext;
|
||||
|
||||
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();
|
||||
void notifyEventBus(EventBusSignal signal);
|
||||
|
||||
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{};
|
||||
bool suppressInputs = false;
|
||||
};
|
||||
|
||||
inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {
|
||||
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
|
||||
}
|
||||
|
||||
inline void AppContext::cancelTimerInternal(AppTimerHandle handle) {
|
||||
if (system)
|
||||
system->cancelTimer(handle);
|
||||
}
|
||||
|
||||
inline void AppContext::cancelAllTimersInternal() {
|
||||
if (system)
|
||||
system->cancelAllTimers();
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
20
Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp
Normal file
20
Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class FramebufferHooks {
|
||||
public:
|
||||
using PreSendHook = void (*)(void* framebuffer, void* userData);
|
||||
|
||||
static void setPreSendHook(PreSendHook hook, void* userData);
|
||||
static void clearPreSendHook();
|
||||
static void invokePreSend(void* framebuffer);
|
||||
|
||||
private:
|
||||
static PreSendHook hook_;
|
||||
static void* userData_;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
struct PersistentSettings {
|
||||
bool mute = false;
|
||||
bool autoLightSleep = false;
|
||||
};
|
||||
|
||||
PersistentSettings loadPersistentSettings(Services* services);
|
||||
void savePersistentSettings(Services* services, const PersistentSettings& settings);
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
92
Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp
Normal file
92
Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class StatusBar {
|
||||
public:
|
||||
static StatusBar& instance();
|
||||
|
||||
void setServices(Services* services) { services_ = services; }
|
||||
|
||||
void setEnabled(bool value);
|
||||
void toggle();
|
||||
[[nodiscard]] bool isEnabled() const { return enabled_; }
|
||||
|
||||
void setCurrentAppName(std::string_view name);
|
||||
|
||||
[[nodiscard]] bool handleToggleInput(const InputState& current, const InputState& previous);
|
||||
|
||||
template<typename Framebuffer>
|
||||
void renderIfEnabled(Framebuffer& fb) {
|
||||
if (!enabled_)
|
||||
return;
|
||||
renderBar(fb);
|
||||
}
|
||||
|
||||
private:
|
||||
StatusBar() = default;
|
||||
|
||||
template<typename Framebuffer>
|
||||
void renderBar(Framebuffer& fb) {
|
||||
const int width = fb.width();
|
||||
if (width <= 0)
|
||||
return;
|
||||
|
||||
const int barHeight = font16x8::kGlyphHeight + 2;
|
||||
const int fillHeight = std::min(barHeight, fb.height());
|
||||
if (fillHeight <= 0)
|
||||
return;
|
||||
|
||||
const std::string leftText = prepareLeftText(width);
|
||||
const std::string rightText = prepareRightText();
|
||||
|
||||
for (int y = 0; y < fillHeight; ++y) {
|
||||
for (int x = 0; x < width; ++x)
|
||||
fb.drawPixel(x, y, true);
|
||||
}
|
||||
|
||||
for (int x = 0; x < width; ++x)
|
||||
fb.drawPixel(x, 0, false);
|
||||
|
||||
const int textY = 1;
|
||||
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
|
||||
if (bottomSeparatorY < fillHeight) {
|
||||
for (int x = 0; x < width; ++x)
|
||||
fb.drawPixel(x, bottomSeparatorY, (x % 2) != 0);
|
||||
}
|
||||
|
||||
const int leftX = 2;
|
||||
if (!leftText.empty())
|
||||
font16x8::drawText(fb, leftX, textY, leftText, 1, false, 1);
|
||||
|
||||
if (!rightText.empty()) {
|
||||
int rightWidth = font16x8::measureText(rightText, 1, 1);
|
||||
int rightX = width - rightWidth - 2;
|
||||
const int minRightX = leftX + font16x8::measureText(leftText, 1, 1) + 6;
|
||||
if (rightX < minRightX)
|
||||
rightX = std::max(minRightX, width / 2);
|
||||
if (rightX < width)
|
||||
font16x8::drawText(fb, rightX, textY, rightText, 1, false, 1);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string prepareLeftText(int displayWidth) const;
|
||||
[[nodiscard]] std::string prepareRightText() const;
|
||||
|
||||
bool enabled_ = false;
|
||||
Services* services_ = nullptr;
|
||||
std::string appName_{};
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
288
Firmware/sdk/core/src/app_system.cpp
Normal file
288
Firmware/sdk/core/src/app_system.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||
#include "cardboy/sdk/status_bar.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;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool anyButtonPressed(const InputState& state) {
|
||||
return state.up || state.down || state.left || state.right || state.a || state.b || state.select || state.start;
|
||||
}
|
||||
|
||||
template<typename Framebuffer>
|
||||
void statusBarPreSendHook(void* framebuffer, void* userData) {
|
||||
auto* fb = static_cast<Framebuffer*>(framebuffer);
|
||||
auto* status = static_cast<StatusBar*>(userData);
|
||||
if (fb && status)
|
||||
status->renderIfEnabled(*fb);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
|
||||
context.system = this;
|
||||
auto& statusBar = StatusBar::instance();
|
||||
statusBar.setServices(context.services);
|
||||
using FBType = typename AppContext::Framebuffer;
|
||||
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<FBType>, &statusBar);
|
||||
}
|
||||
|
||||
AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); }
|
||||
|
||||
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();
|
||||
suppressInputs = true;
|
||||
StatusBar::instance().setServices(context.services);
|
||||
StatusBar::instance().setCurrentAppName(activeFactory ? activeFactory->name() : "");
|
||||
current->onStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppSystem::run() {
|
||||
if (!current) {
|
||||
if (factories.empty() || !startAppByIndex(0))
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<AppEvent> events;
|
||||
events.reserve(4);
|
||||
|
||||
while (true) {
|
||||
if (auto* hooks = context.loopHooks())
|
||||
hooks->onLoopIteration();
|
||||
|
||||
events.clear();
|
||||
const std::uint32_t now = context.clock.millis();
|
||||
processDueTimers(now, events);
|
||||
|
||||
const InputState inputNow = context.input.readState();
|
||||
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState);
|
||||
|
||||
if (suppressInputs) {
|
||||
lastInputState = inputNow;
|
||||
if (!anyButtonPressed(inputNow))
|
||||
suppressInputs = false;
|
||||
} else if (!consumedByStatusToggle && 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;
|
||||
} else if (consumedByStatusToggle) {
|
||||
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;
|
||||
|
||||
auto* eventBus = context.eventBus();
|
||||
if (!eventBus)
|
||||
return;
|
||||
|
||||
const std::uint32_t mask = to_event_bits(EventBusSignal::Input) | to_event_bits(EventBusSignal::Timer);
|
||||
|
||||
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
|
||||
eventBus->cancelTimerSignal();
|
||||
eventBus->wait(mask, IEventBus::kWaitForever);
|
||||
} else {
|
||||
eventBus->scheduleTimerSignal(waitMs);
|
||||
eventBus->wait(mask, IEventBus::kWaitForever);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
notifyEventBus(EventBusSignal::Timer);
|
||||
return record.id;
|
||||
}
|
||||
|
||||
void AppSystem::cancelTimer(AppTimerHandle handle) {
|
||||
auto* timer = findTimer(handle);
|
||||
if (!timer)
|
||||
return;
|
||||
timer->active = false;
|
||||
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
|
||||
timers.end());
|
||||
notifyEventBus(EventBusSignal::Timer);
|
||||
}
|
||||
|
||||
void AppSystem::cancelAllTimers() {
|
||||
bool changed = false;
|
||||
for (auto& timer: timers) {
|
||||
if (timer.generation == currentGeneration && timer.active) {
|
||||
timer.active = false;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
|
||||
timers.end());
|
||||
if (changed)
|
||||
notifyEventBus(EventBusSignal::Timer);
|
||||
}
|
||||
|
||||
void AppSystem::dispatchEvent(const AppEvent& event) {
|
||||
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() {
|
||||
const bool hadTimers = !timers.empty();
|
||||
++currentGeneration;
|
||||
timers.clear();
|
||||
if (hadTimers)
|
||||
notifyEventBus(EventBusSignal::Timer);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void AppSystem::notifyEventBus(EventBusSignal signal) {
|
||||
if (signal == EventBusSignal::None)
|
||||
return;
|
||||
if (auto* bus = context.eventBus())
|
||||
bus->signal(to_event_bits(signal));
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
23
Firmware/sdk/core/src/framebuffer_hooks.cpp
Normal file
23
Firmware/sdk/core/src/framebuffer_hooks.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "cardboy/sdk/framebuffer_hooks.hpp"
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
FramebufferHooks::PreSendHook FramebufferHooks::hook_ = nullptr;
|
||||
void* FramebufferHooks::userData_ = nullptr;
|
||||
|
||||
void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) {
|
||||
hook_ = hook;
|
||||
userData_ = userData;
|
||||
}
|
||||
|
||||
void FramebufferHooks::clearPreSendHook() {
|
||||
hook_ = nullptr;
|
||||
userData_ = nullptr;
|
||||
}
|
||||
|
||||
void FramebufferHooks::invokePreSend(void* framebuffer) {
|
||||
if (hook_)
|
||||
hook_(framebuffer, userData_);
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
40
Firmware/sdk/core/src/persistent_settings.cpp
Normal file
40
Firmware/sdk/core/src/persistent_settings.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "cardboy/sdk/persistent_settings.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view kNamespace = "settings";
|
||||
constexpr std::string_view kMuteKey = "mute";
|
||||
constexpr std::string_view kAutoLightSleepKey = "autosleep";
|
||||
|
||||
[[nodiscard]] std::uint32_t boolToStorage(bool value) { return value ? 1U : 0U; }
|
||||
[[nodiscard]] bool storageToBool(std::uint32_t value) { return value != 0U; }
|
||||
} // namespace
|
||||
|
||||
PersistentSettings loadPersistentSettings(Services* services) {
|
||||
PersistentSettings settings{};
|
||||
if (!services || !services->storage)
|
||||
return settings;
|
||||
|
||||
std::uint32_t raw = 0;
|
||||
if (services->storage->readUint32(kNamespace, kMuteKey, raw))
|
||||
settings.mute = storageToBool(raw);
|
||||
|
||||
if (services->storage->readUint32(kNamespace, kAutoLightSleepKey, raw))
|
||||
settings.autoLightSleep = storageToBool(raw);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
void savePersistentSettings(Services* services, const PersistentSettings& settings) {
|
||||
if (!services || !services->storage)
|
||||
return;
|
||||
|
||||
services->storage->writeUint32(kNamespace, kMuteKey, boolToStorage(settings.mute));
|
||||
services->storage->writeUint32(kNamespace, kAutoLightSleepKey, boolToStorage(settings.autoLightSleep));
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
75
Firmware/sdk/core/src/status_bar.cpp
Normal file
75
Firmware/sdk/core/src/status_bar.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "cardboy/sdk/status_bar.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
StatusBar& StatusBar::instance() {
|
||||
static StatusBar bar;
|
||||
return bar;
|
||||
}
|
||||
|
||||
void StatusBar::setEnabled(bool value) { enabled_ = value; }
|
||||
|
||||
void StatusBar::toggle() {
|
||||
enabled_ = !enabled_;
|
||||
if (services_ && services_->buzzer)
|
||||
services_->buzzer->beepMove();
|
||||
}
|
||||
|
||||
void StatusBar::setCurrentAppName(std::string_view name) {
|
||||
appName_.assign(name.begin(), name.end());
|
||||
std::transform(appName_.begin(), appName_.end(), appName_.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
|
||||
}
|
||||
|
||||
bool StatusBar::handleToggleInput(const InputState& current, const InputState& previous) {
|
||||
const bool comboNow = current.start && current.select && current.up;
|
||||
const bool comboPrev = previous.start && previous.select && previous.up;
|
||||
if (comboNow && !comboPrev) {
|
||||
toggle();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string StatusBar::prepareLeftText(int displayWidth) const {
|
||||
std::string text = appName_.empty() ? std::string("CARDBOY") : appName_;
|
||||
int maxWidth = std::max(0, displayWidth - 32);
|
||||
while (!text.empty() && font16x8::measureText(text, 1, 1) > maxWidth)
|
||||
text.pop_back();
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string StatusBar::prepareRightText() const {
|
||||
if (!services_)
|
||||
return {};
|
||||
|
||||
std::string right;
|
||||
if (services_->battery && services_->battery->hasData()) {
|
||||
const float current = services_->battery->current();
|
||||
const float chargeMah = services_->battery->charge();
|
||||
const float fallbackV = services_->battery->voltage();
|
||||
char buf[64];
|
||||
if (std::isfinite(current) && std::isfinite(chargeMah)) {
|
||||
std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast<double>(current),
|
||||
static_cast<double>(chargeMah));
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast<double>(fallbackV));
|
||||
}
|
||||
right.assign(buf);
|
||||
}
|
||||
|
||||
if (services_->buzzer && services_->buzzer->isMuted()) {
|
||||
if (!right.empty())
|
||||
right.append(" ");
|
||||
right.append("MUTE");
|
||||
}
|
||||
|
||||
return right;
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -1,34 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
# if (NOT DEFINED SANITIZE)
|
||||
# set(SANITIZE YES)
|
||||
# endif ()
|
||||
endif ()
|
||||
|
||||
if (SANITIZE STREQUAL "YES")
|
||||
message(STATUS "Enabling sanitizers!")
|
||||
add_compile_options(-Werror -O0 -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-unused-variable
|
||||
-Wno-error=unused-function
|
||||
-Wshadow -Wformat=2 -Wfloat-equal -D_GLIBCXX_DEBUG -Wconversion)
|
||||
add_compile_options(-fsanitize=address -fno-sanitize-recover)
|
||||
add_link_options(-fsanitize=address -fno-sanitize-recover)
|
||||
endif ()
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
|
||||
endif ()
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
add_compile_options(-O3)
|
||||
add_link_options(-O3)
|
||||
endif ()
|
||||
|
||||
add_executable(main src/main.cpp)
|
||||
|
||||
set(CBSDK_BACKEND_INTERFACE cbsdk_sfml)
|
||||
|
||||
#target_include_directories(main PRIVATE include)
|
||||
#target_include_directories(main PUBLIC include_public)
|
||||
target_link_libraries(main PRIVATE SFML::Graphics)
|
||||
target_link_libraries(main PUBLIC cbsdk)
|
||||
@@ -1,75 +0,0 @@
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <barrier>
|
||||
#include <latch>
|
||||
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "GridWindow.hpp"
|
||||
#include "SfmlWindow.hpp"
|
||||
#include "TextWindow.hpp"
|
||||
|
||||
int main() {
|
||||
EventLoop loop;
|
||||
std::latch barrier{1};
|
||||
|
||||
SurfaceBase* surface_ptr;
|
||||
int i = 0;
|
||||
std::thread loop_thread{[&] {
|
||||
SurfaceBase surface(&loop);
|
||||
surface_ptr = &surface;
|
||||
|
||||
barrier.count_down();
|
||||
|
||||
surface.set_window<GridWindow<SurfaceBase, 2, 2>>();
|
||||
|
||||
GridWindow<SurfaceBase, 2, 2>* window =
|
||||
static_cast<GridWindow<SurfaceBase, 2, 2>*>(surface.get_window());
|
||||
window->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(0, 0, &loop, "hello");
|
||||
window->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(0, 1, &loop, "hello1");
|
||||
window->set_window<GridWindow<SubSurface<SurfaceBase>, 2, 2>>(1, 0);
|
||||
GridWindow<SubSurface<SurfaceBase>, 2, 2>* window2 =
|
||||
static_cast<GridWindow<SubSurface<SurfaceBase>, 2, 2>*>(
|
||||
window->get_subsurface(1, 0).get_window());
|
||||
window->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(1, 1, &loop, "hello3");
|
||||
|
||||
window2->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(
|
||||
0, 0, &loop, "hello2");
|
||||
window2->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(
|
||||
0, 1, &loop, "hello4");
|
||||
window2->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(
|
||||
1, 0, &loop, "hello5");
|
||||
window2->set_window<TextWindow<SubSurface<SurfaceBase>, std::string>>(
|
||||
1, 1, &loop, "hello6");
|
||||
loop.run([&] {
|
||||
surface._sf_window.clear();
|
||||
surface._texture.update(surface._image);
|
||||
surface._sf_window.draw(surface._sprite);
|
||||
surface._sf_window.display();
|
||||
static_cast<TextWindow<SubSurface<SurfaceBase>, std::string>*>(
|
||||
window->get_subsurface(0, 0).get_window())
|
||||
->push(TextUpdateEvent<std::string>{std::string("Hello, SFML!") + std::to_string(i++)});
|
||||
});
|
||||
}};
|
||||
|
||||
barrier.wait();
|
||||
|
||||
while (surface_ptr->_sf_window.isOpen()) {
|
||||
while (const std::optional event = surface_ptr->_sf_window.pollEvent()) {
|
||||
if (event->is<sf::Event::Closed>()) {
|
||||
surface_ptr->_sf_window.close();
|
||||
loop.push(LoopQuitEvent{});
|
||||
}
|
||||
if (event->is<sf::Event::Resized>()) {
|
||||
auto newSize = event->getIf<sf::Event::Resized>()->size;
|
||||
surface_ptr->push(SurfaceResizeEvent{newSize.x, newSize.y});
|
||||
}
|
||||
if (event->is<sf::Event::KeyPressed>()) {
|
||||
auto key = event->getIf<sf::Event::KeyPressed>();
|
||||
surface_ptr->push(KeyboardEvent{static_cast<Key>(key->code)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop_thread.join();
|
||||
}
|
||||
16
Firmware/sdk/launchers/desktop/CMakeLists.txt
Normal file
16
Firmware/sdk/launchers/desktop/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
add_executable(cardboy_desktop
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_desktop PROPERTIES
|
||||
EXPORT_NAME desktop_launcher
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_desktop
|
||||
PRIVATE
|
||||
cardboy_apps
|
||||
cardboy_sdk
|
||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
|
||||
41
Firmware/sdk/launchers/desktop/src/main.cpp
Normal file
41
Firmware/sdk/launchers/desktop/src/main.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/settings_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/backend/desktop_backend.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/persistent_settings.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
|
||||
using cardboy::backend::desktop::DesktopRuntime;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DesktopRuntime runtime;
|
||||
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
|
||||
const cardboy::sdk::PersistentSettings persistentSettings =
|
||||
cardboy::sdk::loadPersistentSettings(context.getServices());
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->setMuted(persistentSettings.mute);
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createSettingsAppFactory());
|
||||
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;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
add_subdirectory(backend_interface)
|
||||
|
||||
add_library(cbsdk
|
||||
src/Window.cpp
|
||||
include_public/Window.hpp
|
||||
include_public/Pixel.hpp
|
||||
src/Event.cpp
|
||||
include_public/Event.hpp
|
||||
include_public/StandardEvents.hpp
|
||||
include_public/Surface.hpp
|
||||
include_public/Fonts.hpp
|
||||
src/TextWindow.cpp
|
||||
include_public/TextWindow.hpp
|
||||
include_public/utils.hpp
|
||||
include_public/SubSurface.hpp
|
||||
src/Surface.cpp)
|
||||
|
||||
target_include_directories(cbsdk PUBLIC include_public)
|
||||
target_include_directories(cbsdk PRIVATE include)
|
||||
|
||||
if(NOT CBSDK_BACKEND_LIBRARY)
|
||||
message(FATAL_ERROR "CBSDK_BACKEND_LIBRARY not set!")
|
||||
endif ()
|
||||
|
||||
target_link_libraries(cbsdk PUBLIC cbsdk_backend_interface ${CBSDK_BACKEND_LIBRARY})
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
add_subdirectory(test)
|
||||
endif ()
|
||||
@@ -1,10 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
add_library(cbsdk_backend_interface INTERFACE
|
||||
include_public/backend_interface.hpp)
|
||||
|
||||
target_include_directories(cbsdk_backend_interface INTERFACE include_public)
|
||||
|
||||
#if (NOT CMAKE_CROSSCOMPILING)
|
||||
# add_subdirectory(test)
|
||||
#endif ()
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Created by stepus53 on 14.08.25.
|
||||
//
|
||||
|
||||
#ifndef SDK_TOP_BACKEND_INTERFACE_HPP
|
||||
#define SDK_TOP_BACKEND_INTERFACE_HPP
|
||||
|
||||
namespace SdkPort {
|
||||
|
||||
struct Pixel;
|
||||
class SurfaceBase;
|
||||
|
||||
} // namespace SdkPort
|
||||
|
||||
#endif // SDK_TOP_BACKEND_INTERFACE_HPP
|
||||
@@ -1,143 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.07.2025.
|
||||
//
|
||||
|
||||
#ifndef EVENT_HPP
|
||||
#define EVENT_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <concepts>
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <functional>
|
||||
|
||||
#include "utils.hpp"
|
||||
|
||||
enum class EventHandlingResult { DONE, IGNORE, CONTINUE };
|
||||
|
||||
class Event {};
|
||||
|
||||
struct LoopQuitEvent : public Event {};
|
||||
|
||||
template<typename T>
|
||||
concept IsEvent = std::is_base_of_v<Event, T>;
|
||||
|
||||
template<typename H, typename E>
|
||||
concept HasHandleFor = requires(H h, E e) {
|
||||
{ h.handle(e) } -> std::same_as<EventHandlingResult>;
|
||||
};
|
||||
|
||||
template<typename H, typename... Ts>
|
||||
concept HandlesAll = (HasHandleFor<H, Ts> && ...);
|
||||
|
||||
template<typename Derived, typename... T>
|
||||
requires(IsEvent<T> && ...)
|
||||
class EventHandler {
|
||||
public:
|
||||
EventHandler() { static_assert(HandlesAll<Derived, T...>); }
|
||||
|
||||
using HandledTypes = type_list<T...>;
|
||||
};
|
||||
|
||||
class EventLoop;
|
||||
|
||||
class EventQueueBase {
|
||||
public:
|
||||
virtual void process_events() = 0;
|
||||
|
||||
virtual ~EventQueueBase() = default;
|
||||
};
|
||||
|
||||
template<typename HandlerType, typename... Ts>
|
||||
class EventQueue : public EventQueueBase {
|
||||
public:
|
||||
EventQueue(EventLoop* loop, HandlerType* handler) : _loop(loop), _handler(handler) {};
|
||||
|
||||
std::optional<std::variant<Ts...>> poll();
|
||||
|
||||
void process_events() override {
|
||||
while (auto event = poll()) {
|
||||
std::visit([this](auto&& e) { _handler->handle(e); }, *event);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires std::disjunction_v<std::is_same<T, Ts>...>
|
||||
void push(T&& event);
|
||||
|
||||
private:
|
||||
EventLoop* _loop;
|
||||
HandlerType* _handler;
|
||||
std::list<std::variant<Ts...>> _events;
|
||||
};
|
||||
|
||||
class EventLoop : EventHandler<EventLoop, LoopQuitEvent>, public EventQueue<EventLoop, LoopQuitEvent> {
|
||||
public:
|
||||
EventLoop() : EventQueue<EventLoop, LoopQuitEvent>(this, this) {}
|
||||
|
||||
template<typename... Ts>
|
||||
void notify_pending(EventQueue<Ts...>* queue) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
// TODO:
|
||||
if (std::find(_events.begin(), _events.end(), queue) != _events.end()) {
|
||||
return; // Already registered
|
||||
}
|
||||
_events.push_back(queue);
|
||||
_condition.notify_all();
|
||||
}
|
||||
|
||||
void run(std::function<void()> after_callback) {
|
||||
while (_running) {
|
||||
std::list<EventQueueBase*> new_events;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
_condition.wait(lock, [this] { return !_events.empty() || !_running; });
|
||||
std::swap(new_events, _events);
|
||||
}
|
||||
|
||||
for (auto queue: new_events) {
|
||||
queue->process_events();
|
||||
}
|
||||
|
||||
after_callback();
|
||||
}
|
||||
}
|
||||
|
||||
EventHandlingResult handle(LoopQuitEvent event) {
|
||||
_running = false;
|
||||
_condition.notify_all();
|
||||
return EventHandlingResult::DONE;
|
||||
}
|
||||
|
||||
private:
|
||||
std::list<EventQueueBase*> _events;
|
||||
std::mutex _mutex;
|
||||
std::condition_variable _condition;
|
||||
bool _running = true;
|
||||
};
|
||||
|
||||
template<typename HandlerType, typename... Ts>
|
||||
std::optional<std::variant<Ts...>> EventQueue<HandlerType, Ts...>::poll() {
|
||||
if (_events.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto event = std::move(_events.front());
|
||||
_events.pop_front();
|
||||
return event;
|
||||
}
|
||||
|
||||
template<typename HandlerType, typename... Ts>
|
||||
template<typename T>
|
||||
requires std::disjunction_v<std::is_same<T, Ts>...>
|
||||
void EventQueue<HandlerType, Ts...>::push(T&& event) {
|
||||
_events.emplace_back(std::forward<T>(event));
|
||||
_loop->notify_pending(static_cast<HandlerType*>(this));
|
||||
}
|
||||
|
||||
#endif // EVENT_HPP
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user