Compare commits

..

28 Commits

Author SHA1 Message Date
13cdcb01dd gameboy stats 2025-10-09 00:16:25 +02:00
4c0fd5243f fps counter 2025-10-08 22:41:48 +02:00
429d704c8c vibecoded gb emulator 2025-10-08 22:19:21 +02:00
9a9e25e124 app menu 2025-10-08 21:21:33 +02:00
ecf6d09651 buzzer 2025-10-07 22:50:45 +02:00
413e021e49 async display 2025-10-07 14:57:15 +02:00
c9f0f59630 dump 2025-10-07 10:11:39 +02:00
9420887392 more cleanup 2025-10-07 01:26:39 +02:00
7df84f1e81 better gpt tetris 4 2025-10-07 01:14:38 +02:00
4861d26d8a better 2025-10-07 00:29:04 +02:00
e389a776be broken game over 2025-10-06 09:47:50 +02:00
8b8d9d3a55 pause 2025-10-06 09:34:33 +02:00
126d377836 re-enable auto sleep 2025-10-06 09:16:19 +02:00
3f8d90c18a bat stats 2025-10-06 09:14:25 +02:00
c439aecd03 craptrix 3 2025-10-06 08:50:02 +02:00
cd72c2d7df craptrix 2 2025-10-05 23:23:24 +02:00
589c598b01 craptrix 2025-10-05 22:25:19 +02:00
95a946e47f less crap spi 2025-07-31 17:00:47 +02:00
48d2089b69 get rid of window refresh 2025-07-31 16:13:54 +02:00
3e9b7b4326 less template pain 2025-07-31 16:12:35 +02:00
e1004ff196 x11 thread workaround 2025-07-31 14:36:21 +02:00
24df0fc825 add some missed includes 2025-07-31 14:35:58 +02:00
ab32731f4d dump 2025-07-28 09:39:13 +02:00
474a0b2a43 buttons interrupt 2025-07-26 15:34:20 +02:00
35219c353c set correct charge termination current 2025-07-26 12:43:20 +02:00
8180abed4c shutdown on 3v 2025-07-26 12:18:52 +02:00
12d634ecc9 set VEmpty to 3v 2025-07-26 12:07:53 +02:00
6a8f74384e some firmware updates (fuel gauge and port extender) 2025-07-26 11:45:47 +02:00
67 changed files with 10942 additions and 694 deletions

2
Firmware/.gitignore vendored
View File

@@ -1,3 +1,5 @@
build
cmake-build*
.idea
.cache
managed_components

23
Firmware/.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"configurations": [
{
"name": "ESP-IDF",
"compilerPath": "${config:idf.toolsPath}/tools/riscv32-esp-elf/esp-14.2.0_20241119/riscv32-esp-elf/bin/riscv32-esp-elf-gcc",
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
"includePath": [
"${config:idf.espIdfPath}/components/**",
"${config:idf.espIdfPathWin}/components/**",
"${workspaceFolder}/**"
],
"browse": {
"path": [
"${config:idf.espIdfPath}/components",
"${config:idf.espIdfPathWin}/components",
"${workspaceFolder}"
],
"limitSymbolsToIncludedHeaders": true
}
}
],
"version": 4
}

15
Firmware/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "gdbtarget",
"request": "attach",
"name": "Eclipse CDT GDB Adapter"
},
{
"type": "espidf",
"name": "Launch",
"request": "launch"
}
]
}

85
Firmware/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,85 @@
{
"idf.flashType": "JTAG",
"idf.port": "/dev/tty.usbmodem12401",
"C_Cpp.intelliSenseEngine": "default",
"files.associations": {
"bitset": "cpp",
"chrono": "cpp",
"algorithm": "cpp",
"random": "cpp",
"fstream": "cpp",
"streambuf": "cpp",
"regex": "cpp",
"*.inc": "cpp",
"vector": "cpp",
"esp_partition.h": "c",
"cstring": "cpp",
"array": "cpp",
"string_view": "cpp",
"any": "cpp",
"atomic": "cpp",
"barrier": "cpp",
"bit": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"complex": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"forward_list": "cpp",
"list": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"exception": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"netfwd": "cpp",
"numeric": "cpp",
"optional": "cpp",
"ratio": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"format": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"latch": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"semaphore": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"text_encoding": "cpp",
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"variant": "cpp"
}
}

View File

@@ -0,0 +1,5 @@
idf_component_register()
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
target_link_libraries(${COMPONENT_LIB} INTERFACE cbsdk)

View 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

View File

@@ -1,5 +1,10 @@
idf_component_register(SRCS
src/hello_world_main.cpp
src/app_main.cpp
src/app_system.cpp
src/apps/menu_app.cpp
src/apps/clock_app.cpp
src/apps/tetris_app.cpp
src/apps/gameboy_app.cpp
src/display.cpp
src/bat_mon.cpp
src/spi_global.cpp
@@ -9,5 +14,10 @@ idf_component_register(SRCS
src/shutdowner.cpp
src/buttons.cpp
src/power_helper.cpp
PRIV_REQUIRES spi_flash esp_driver_i2c driver
INCLUDE_DIRS "include")
src/buzzer.cpp
src/fs_helper.cpp
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
INCLUDE_DIRS "include" "Peanut-GB"
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)

View 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

View File

@@ -0,0 +1,95 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
class AppSystem;
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;
};
class IFramebuffer {
public:
virtual ~IFramebuffer() = default;
virtual void drawPixel(int x, int y, bool on) = 0; // on=true => black
virtual void clear(bool on) = 0; // fill full screen to on/off
virtual int width() const = 0;
virtual int height() const = 0;
};
class IInput {
public:
virtual ~IInput() = default;
virtual InputState readState() = 0;
};
class IClock {
public:
virtual ~IClock() = default;
virtual uint32_t millis() = 0;
virtual void sleep_ms(uint32_t ms) = 0;
};
struct AppContext {
AppContext() = delete;
AppContext(IFramebuffer& fb, IInput& in, IClock& clk) : framebuffer(fb), input(in), clock(clk) {}
IFramebuffer& framebuffer;
IInput& input;
IClock& clock;
AppSystem* system = nullptr;
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;
pendingAppName.clear();
pendingSwitchByName = false;
pendingSwitch = true;
}
void requestAppSwitchByName(std::string_view name) {
pendingAppName.assign(name.begin(), name.end());
pendingSwitchByName = true;
pendingSwitch = true;
}
bool hasPendingAppSwitch() const { return pendingSwitch; }
private:
friend class AppSystem;
bool pendingSwitch = false;
bool pendingSwitchByName = false;
std::size_t pendingAppIndex = 0;
std::string pendingAppName;
};
struct AppSleepPlan {
uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh
uint32_t normal_ms = 0; // short sleep for responsiveness on input wake
};
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void step() = 0;
virtual AppSleepPlan sleepPlan(uint32_t now) const { return {}; }
};
class IAppFactory {
public:
virtual ~IAppFactory() = default;
virtual const char* name() const = 0;
virtual std::unique_ptr<IApp> create(AppContext& context) = 0;
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
#include <string>
#include <vector>
class AppSystem {
public:
explicit AppSystem(AppContext context);
void registerApp(std::unique_ptr<IAppFactory> factory);
bool startApp(const std::string& name);
bool startAppByIndex(std::size_t index);
void run();
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private:
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);
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<IAppFactory> createClockAppFactory();
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kGameboyAppName[] = "Game Boy";
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
std::unique_ptr<IAppFactory> createGameboyAppFactory();
} // namespace apps

View File

@@ -0,0 +1,15 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kMenuAppName[] = "Menu";
inline constexpr std::string_view kMenuAppNameView = kMenuAppName;
std::unique_ptr<IAppFactory> createMenuAppFactory();
} // namespace apps

View File

@@ -0,0 +1,11 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<IAppFactory> createTetrisAppFactory();
}

View File

@@ -20,9 +20,10 @@ public:
void pooler(); // FIXME:
private:
static inline i2c_device_config_t _dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x70,
.scl_speed_hz = 100000,
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x36,
.scl_speed_hz = 100000,
.flags = 0,
};
BatMon();

View File

@@ -9,14 +9,14 @@
#include "freertos/task.h"
typedef enum {
L1 = 1 << 1,
L2 = 1 << 6,
L3 = 1 << 0,
L4 = 1 << 7,
R1 = 1 << 5,
R2 = 1 << 2,
R3 = 1 << 4,
R4 = 1 << 3,
BTN_START = 1 << 1,
BTN_DOWN = 1 << 6,
BTN_SELECT = 1 << 0,
BTN_LEFT = 1 << 7,
BTN_UP = 1 << 5,
BTN_B = 1 << 2,
BTN_RIGHT = 1 << 4,
BTN_A = 1 << 3,
} btn_num;
class Buttons {
@@ -24,12 +24,14 @@ public:
static Buttons& get();
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
TaskHandle_t _pooler_task;
private:
Buttons();
volatile uint8_t _current;
TaskHandle_t _pooler_task;
};

View File

@@ -0,0 +1,54 @@
// Simple piezo buzzer helper using LEDC (PWM) for square wave tones.
// Provides a tiny queued pattern player for short game SFX without blocking.
#pragma once
#include <cstdint>
class Buzzer {
public:
static Buzzer &get();
void init(); // call once from app_main
// Queue a tone. freq=0 => silence. gap_ms is silence after tone before next.
void tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms = 0);
// Convenience SFX
void beepRotate();
void beepMove();
void beepLock();
void beepLines(int lines); // 1..4 lines
void beepLevelUp(int level); // after increment
void beepGameOver();
// Mute controls
void setMuted(bool m);
void toggleMuted();
bool isMuted() const { return _muted; }
// Persistence
void loadState();
void saveState();
private:
struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; };
static constexpr int MAX_QUEUE = 16;
Step _queue[MAX_QUEUE]{};
int _q_head = 0; // inclusive
int _q_tail = 0; // exclusive
bool _running = false;
bool _in_gap = false;
void *_timer = nullptr; // esp_timer_handle_t (opaque here)
bool _muted = false;
Buzzer() = default;
void enqueue(const Step &s);
bool empty() const { return _q_head == _q_tail; }
Step &front() { return _queue[_q_head]; }
void popFront();
void startNext();
void schedule(uint32_t ms, bool gapPhase);
void applyFreq(uint32_t freq);
static void timerCb(void *arg);
void clearQueue() { _q_head = _q_tail = 0; }
};

View File

@@ -7,23 +7,22 @@
#define I2C_SCL GPIO_NUM_8
#define I2C_SDA GPIO_NUM_9
#define SPI_MOSI GPIO_NUM_5
#define SPI_MISO GPIO_NUM_0
#define SPI_SCK GPIO_NUM_4
#define SPI_DISP_CS GPIO_NUM_11
#define SPI_MOSI GPIO_NUM_5
#define SPI_MISO GPIO_NUM_0
#define SPI_SCK GPIO_NUM_4
#define SPI_DISP_CS GPIO_NUM_24
#define SPI_DISP_DISP GPIO_NUM_11
#define SPI_BUS SPI2_HOST
#define DISP_WIDTH 400
#define DISP_HEIGHT 240
#define BUZZER_PIN GPIO_NUM_25
#define PWR_INT GPIO_NUM_10
#define PWR_KILL GPIO_NUM_12
#define SHR_OUT GPIO_NUM_23
#define SHR_CLK GPIO_NUM_3
#define SHR_SH GPIO_NUM_2
#define DIRECT_BTN GPIO_NUM_1
#define EXP_INT GPIO_NUM_1
#endif

View File

@@ -4,24 +4,49 @@
#ifndef CB_DISP_TOOLS_HPP
#define CB_DISP_TOOLS_HPP
#include <display.hpp>
namespace DispTools {
static void clear() {
for (int y = 0; y < DISP_HEIGHT; y++) {
for (int x = 0; x < DISP_WIDTH; x++) {
SMD::set_pixel(x, y, true);
}
}
}
static bool get_pixel(int x, int y) {
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
assert(false);
assert(false); // Not implemented
return true;
// return disp_frame[y][x];
}
static void reset_pixel(int x, int y) {
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
assert(false);
SMD::set_pixel(x, y, false);
}
static void set_pixel(int x, int y) {
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
assert(false);
class DispTools {
public:
static DispTools& get();
void clear();
bool get_pixel(int x, int y);
void set_pixel(int x, int y);
void reset_pixel(int x, int y);
void draw_rectangle(int x1, int y1, int x2, int y2);
void draw_line(int x1, int y1, int x2, int y2);
void draw_circle(int x, int y, int r);
void draw_to_display();
private:
SMD::disp_frame_t disp_frame;
SMD::set_pixel(x, y, true);
}
static void set_pixel(int x, int y, bool on) {
if (on) {
set_pixel(x, y);
} else {
reset_pixel(x, y);
}
}
// New simplified async pipeline wrappers
static void async_frame_start() { SMD::async_draw_wait(); } // call at frame start
static void async_frame_end() { SMD::async_draw_start(); } // call after rendering
// Legacy names (temporary) mapped to new API in case of straggling calls
static void draw_to_display_async_start() { SMD::async_draw_start(); }
static void draw_to_display_async_wait() { SMD::async_draw_wait(); }
static bool draw_to_display_async_busy() { return SMD::async_draw_busy(); }
};

View File

@@ -8,37 +8,83 @@
#include "config.hpp"
#include "driver/spi_master.h"
// (Async memcpy removed for debugging simplification)
#include <array>
#include <bitset>
class SMD {
#include "Surface.hpp"
#include "Window.hpp"
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[SMD::kLineDataBytes];
void init();
// Simplified asynchronous frame pipeline:
// Usage pattern each frame:
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer+clear & guarantees pixel area is zeroed
// ... write pixels into dma_buf via set_pixel / surface ...
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; when DMA completes it triggers
// // a background clear of pixel bytes for next frame
void async_draw_start();
void async_draw_wait();
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
static void set_pixel(int x, int y, bool value) {
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
unsigned bitIdx = 1 << (7 - (x % 8)) % 8;
if (value) {
dma_buf[lineIdx] &= ~bitIdx;
} else {
dma_buf[lineIdx] |= bitIdx;
}
}
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
static inline spi_device_interface_config_t _devcfg = {
.mode = 0, // SPI mode 0
.clock_speed_hz = 6 * 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 = nullptr,
.post_cb = s_spi_post_cb,
};
extern spi_device_handle_t _spi;
void ensure_clear_task(); // idempotent; called from init
}; // namespace SMD
class SMDSurface : public Surface<SMDSurface, BwPixel>, public StandardEventQueue<SMDSurface> {
public:
using disp_line_t = std::bitset<400>;
using disp_frame_t = std::array<disp_line_t, 240>;
using PixelType = BwPixel;
static SMD& get();
void clear();
void draw(const disp_frame_t& frame);
SMDSurface(EventLoop* loop);
private:
SMD();
static inline spi_device_interface_config_t _devcfg = {
.mode = 0, // SPI mode 0
.clock_speed_hz = 2 * 1000 * 1000, // Clock out at 10 MHz
.spics_io_num = SPI_DISP_CS, // CS pin
.flags = SPI_DEVICE_POSITIVE_CS,
.queue_size = 3,
// .pre_cb = lcd_spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
};
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
spi_device_handle_t _spi;
bool _vcom = false;
~SMDSurface() override;
static constexpr size_t kLineData = (kLineBytes + 4);
std::array<uint8_t, kLineData> buf{};
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
std::array<uint8_t, kLineBytes> prep_line(const SMD::disp_line_t& line);
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

View File

@@ -0,0 +1,61 @@
#pragma once
#include "Fonts.hpp"
#include "app_framework.hpp"
#include <array>
#include <cctype>
#include <string_view>
namespace font16x8 {
constexpr int kGlyphWidth = 8;
constexpr int kGlyphHeight = 16;
constexpr unsigned char kFallbackChar = '?';
inline unsigned char normalizeChar(char ch) {
unsigned char uc = static_cast<unsigned char>(ch);
if (uc >= 'a' && uc <= 'z')
uc = static_cast<unsigned char>(std::toupper(static_cast<unsigned char>(uc)));
if (!std::isprint(static_cast<unsigned char>(uc)))
return kFallbackChar;
return uc;
}
inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
unsigned char uc = normalizeChar(ch);
return fonts_Terminess_Powerline[uc];
}
inline void drawGlyph(IFramebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) {
const auto& rows = glyphBitmap(ch);
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
for (int col = 0; col < kGlyphWidth; ++col) {
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
if (rowBits & mask) {
for (int sx = 0; sx < scale; ++sx)
for (int sy = 0; sy < scale; ++sy)
fb.drawPixel(x + col * scale + sx, y + row * scale + sy, on);
}
}
}
}
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
if (text.empty())
return 0;
const int advance = (kGlyphWidth + letterSpacing) * scale;
return static_cast<int>(text.size()) * advance - letterSpacing * scale;
}
inline void drawText(IFramebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true,
int letterSpacing = 1) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on);
cursor += (kGlyphWidth + letterSpacing) * scale;
}
}
} // namespace font16x8

View File

@@ -0,0 +1,26 @@
#pragma once
#include <esp_err.h>
#include <string_view>
class FsHelper {
public:
static FsHelper& get();
esp_err_t mount();
void unmount();
bool isMounted() const { return mounted; }
const char* basePath() const { return kBasePath; }
const char* partitionLabel() const { return kPartitionLabel; }
private:
FsHelper() = default;
static constexpr const char* kBasePath = "/lfs";
static constexpr const char* kPartitionLabel = "littlefs";
static constexpr const bool kFormatOnFailure = true;
bool mounted = false;
};

View File

@@ -11,10 +11,10 @@ class PowerHelper {
public:
static PowerHelper& get();
bool is_slow() const;
void set_slow(bool slow);
void reset_slow_isr(); // FIXME:
void delay(int slow_ms, int normal_ms);
bool is_slow() const;
void set_slow(bool slow);
BaseType_t reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken);
void delay(int slow_ms, int normal_ms);
void install_isr();

View File

@@ -10,6 +10,7 @@ class Shutdowner {
public:
static Shutdowner& get();
void install_isr();
void shutdown();
private:
Shutdowner();
};

View 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.

View File

@@ -0,0 +1,132 @@
// Cardboy firmware entry point: boot platform services and run the modular app system.
#include "app_system.hpp"
#include "app_framework.hpp"
#include "apps/clock_app.hpp"
#include "apps/gameboy_app.hpp"
#include "apps/menu_app.hpp"
#include "apps/tetris_app.hpp"
#include "config.hpp"
#include <bat_mon.hpp>
#include <buttons.hpp>
#include <buzzer.hpp>
#include <disp_tools.hpp>
#include <display.hpp>
#include <i2c_global.hpp>
#include <fs_helper.hpp>
#include <power_helper.hpp>
#include <shutdowner.hpp>
#include <spi_global.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_pm.h"
#include "esp_sleep.h"
#include "sdkconfig.h"
namespace {
class PlatformFramebuffer final : public IFramebuffer {
public:
int width() const override { return DISP_WIDTH; }
int height() const override { return DISP_HEIGHT; }
void drawPixel(int x, int y, bool on) override {
if (x < 0 || y < 0 || x >= width() || y >= height())
return;
DispTools::set_pixel(x, y, on);
}
void clear(bool on) override {
for (int y = 0; y < height(); ++y)
for (int x = 0; x < width(); ++x)
DispTools::set_pixel(x, y, on);
}
};
class PlatformInput final : public IInput {
public:
InputState readState() override {
InputState state{};
const uint8_t pressed = Buttons::get().get_pressed();
if (pressed & BTN_UP)
state.up = true;
if (pressed & BTN_LEFT)
state.left = true;
if (pressed & BTN_RIGHT)
state.right = true;
if (pressed & BTN_DOWN)
state.down = true;
if (pressed & BTN_A)
state.a = true;
if (pressed & BTN_B)
state.b = true;
if (pressed & BTN_SELECT)
state.select = true;
if (pressed & BTN_START)
state.start = true;
return state;
}
};
class PlatformClock final : public IClock {
public:
uint32_t millis() override {
TickType_t ticks = xTaskGetTickCount();
return static_cast<uint32_t>((static_cast<uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
}
void sleep_ms(uint32_t ms) override { PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); }
};
} // namespace
extern "C" void app_main() {
#ifdef CONFIG_PM_ENABLE
// const esp_pm_config_t pm_config = {
// .max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
// .min_freq_mhz = 16,
// .light_sleep_enable = true};
// ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
#endif
PowerHelper::get();
Shutdowner::get();
Buttons::get();
ESP_ERROR_CHECK(gpio_install_isr_service(0));
Shutdowner::get().install_isr();
PowerHelper::get().install_isr();
Buttons::get().install_isr();
I2cGlobal::get();
BatMon::get();
SpiGlobal::get();
SMD::init();
DispTools::clear();
Buzzer::get().init();
FsHelper::get().mount();
static PlatformFramebuffer framebuffer;
static PlatformInput input;
static PlatformClock clock;
AppContext context(framebuffer, input, clock);
AppSystem system(context);
context.system = &system;
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.registerApp(apps::createGameboyAppFactory());
system.run();
}

View File

@@ -0,0 +1,97 @@
#include "app_system.hpp"
#include <power_helper.hpp>
#include <utility>
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
context.system = this;
}
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
if (!factory)
return;
factories.emplace_back(std::move(factory));
}
bool AppSystem::startApp(const std::string& name) {
for (std::size_t i = 0; i < factories.size(); ++i) {
if (factories[i]->name() == name) {
return startAppByIndex(i);
}
}
return false;
}
bool AppSystem::startAppByIndex(std::size_t index) {
if (index >= factories.size())
return false;
context.system = this;
auto& factory = factories[index];
auto app = factory->create(context);
if (!app)
return false;
if (current) {
current->onStop();
current.reset();
}
activeFactory = factory.get();
activeIndex = index;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
current = std::move(app);
current->onStart();
return true;
}
void AppSystem::run() {
if (!current) {
if (factories.empty() || !startAppByIndex(0))
return;
}
while (true) {
current->step();
if (context.pendingSwitch) {
const bool byName = context.pendingSwitchByName;
const std::size_t reqIndex = context.pendingAppIndex;
const std::string reqName = context.pendingAppName;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
bool switched = false;
if (byName) {
switched = startApp(reqName);
} else {
switched = startAppByIndex(reqIndex);
}
if (switched)
continue;
}
const auto now = context.clock.millis();
auto plan = current->sleepPlan(now);
if (plan.slow_ms || plan.normal_ms) {
PowerHelper::get().delay(static_cast<int>(plan.slow_ms), static_cast<int>(plan.normal_ms));
}
}
}
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);
}

View File

@@ -0,0 +1,217 @@
#include "apps/clock_app.hpp"
#include "app_system.hpp"
#include "apps/menu_app.hpp"
#include "font16x8.hpp"
#include <disp_tools.hpp>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <ctime>
#include <string>
#include <string_view>
namespace apps {
namespace {
constexpr const char* kClockAppName = "Clock";
struct TimeSnapshot {
bool hasWallTime = false;
int hour24 = 0;
int minute = 0;
int second = 0;
int year = 0;
int month = 0;
int day = 0;
int weekday = 0;
uint64_t uptimeSeconds = 0;
};
class ClockApp final : public IApp {
public:
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
void onStart() override {
lastSnapshot = {};
dirty = true;
renderIfNeeded(captureTime());
}
void step() override {
const auto snap = captureTime();
InputState st = context.input.readState();
if (st.b && !backPrev) {
context.requestAppSwitchByName(kMenuAppName);
backPrev = st.b;
selectPrev = st.select;
return;
}
if (st.select && !selectPrev) {
use24Hour = !use24Hour;
dirty = true;
}
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
renderIfNeeded(snap);
backPrev = st.b;
selectPrev = st.select;
lastSnapshot = snap;
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
AppSleepPlan plan;
plan.slow_ms = 200;
plan.normal_ms = 40;
return plan;
}
private:
AppContext& context;
IFramebuffer& framebuffer;
IClock& clock;
bool use24Hour = true;
bool dirty = false;
bool backPrev = false;
bool selectPrev = false;
TimeSnapshot lastSnapshot{};
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second;
}
TimeSnapshot captureTime() const {
TimeSnapshot snap{};
snap.uptimeSeconds = clock.millis() / 1000ULL;
time_t raw = 0;
if (time(&raw) != static_cast<time_t>(-1) && raw > 0) {
std::tm tm{};
if (localtime_r(&raw, &tm) != nullptr) {
snap.hasWallTime = true;
snap.hour24 = tm.tm_hour;
snap.minute = tm.tm_min;
snap.second = tm.tm_sec;
snap.year = tm.tm_year + 1900;
snap.month = tm.tm_mon + 1;
snap.day = tm.tm_mday;
snap.weekday = tm.tm_wday;
return snap;
}
}
// Fallback to uptime-derived clock
snap.hasWallTime = false;
snap.hour24 = static_cast<int>((snap.uptimeSeconds / 3600ULL) % 24ULL);
snap.minute = static_cast<int>((snap.uptimeSeconds / 60ULL) % 60ULL);
snap.second = static_cast<int>(snap.uptimeSeconds % 60ULL);
return snap;
}
static void drawCenteredText(IFramebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
const int width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
}
static std::string formatDate(const TimeSnapshot& snap) {
static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
if (!snap.hasWallTime)
return "UPTIME MODE";
const char* weekday = (snap.weekday >= 0 && snap.weekday < 7) ? kWeekdays[snap.weekday] : "";
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%s %04d-%02d-%02d", weekday, snap.year, snap.month, snap.day);
return buffer;
}
void renderIfNeeded(const TimeSnapshot& snap) {
if (!dirty)
return;
dirty = false;
DispTools::draw_to_display_async_wait();
framebuffer.clear(false);
const int scaleLarge = 3;
const int scaleSeconds = 2;
const int scaleSmall = 1;
int hourDisplay = snap.hour24;
bool isPm = false;
if (!use24Hour) {
isPm = hourDisplay >= 12;
int h12 = hourDisplay % 12;
if (h12 == 0)
h12 = 12;
hourDisplay = h12;
}
char mainLine[6];
std::snprintf(mainLine, sizeof(mainLine), "%02d:%02d", hourDisplay, snap.minute);
const int mainW = font16x8::measureText(mainLine, scaleLarge, 0);
const int timeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleLarge) / 2 - 12;
const int timeX = (framebuffer.width() - mainW) / 2;
font16x8::drawText(framebuffer, timeX, timeY, mainLine, scaleLarge, true, 0);
char secondsLine[3];
std::snprintf(secondsLine, sizeof(secondsLine), "%02d", snap.second);
const int secondsX = timeX + mainW + 12;
const int secondsY = timeY + font16x8::kGlyphHeight * scaleLarge - font16x8::kGlyphHeight * scaleSeconds;
font16x8::drawText(framebuffer, secondsX, secondsY, secondsLine, scaleSeconds, true, 0);
if (!use24Hour) {
font16x8::drawText(framebuffer, timeX + mainW + 12, timeY, isPm ? "PM" : "AM", scaleSmall, true, 0);
} else {
font16x8::drawText(framebuffer, timeX + mainW + 12, timeY, "24H", scaleSmall, true, 0);
}
const std::string dateLine = formatDate(snap);
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleLarge + 28, dateLine, scaleSmall, 1);
if (!snap.hasWallTime) {
char uptimeLine[32];
const uint64_t days = snap.uptimeSeconds / 86400ULL;
const uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
const uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
const uint64_t secs = snap.uptimeSeconds % 60ULL;
if (days > 0) {
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
static_cast<unsigned long long>(days), static_cast<unsigned long long>(hrs),
static_cast<unsigned long long>(mins), static_cast<unsigned long long>(secs));
} else {
std::snprintf(uptimeLine, sizeof(uptimeLine), "%02llu:%02llu:%02llu UP",
static_cast<unsigned long long>(hrs), static_cast<unsigned long long>(mins),
static_cast<unsigned long long>(secs));
}
drawCenteredText(framebuffer, framebuffer.height() - 68, uptimeLine, scaleSmall, 1);
}
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
DispTools::draw_to_display_async_start();
}
};
class ClockAppFactory final : public IAppFactory {
public:
const char* name() const override { return kClockAppName; }
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<ClockApp>(context); }
};
} // namespace
std::unique_ptr<IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
} // namespace apps

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,183 @@
#include "apps/menu_app.hpp"
#include "app_system.hpp"
#include "font16x8.hpp"
#include <disp_tools.hpp>
#include <algorithm>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
namespace apps {
namespace {
struct MenuEntry {
std::string name;
std::size_t index = 0;
};
class MenuApp final : public IApp {
public:
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
void onStart() override {
refreshEntries();
dirty = true;
renderIfNeeded();
}
void step() override {
InputState st = context.input.readState();
if (st.left && !leftPrev) {
moveSelection(-1);
} else if (st.right && !rightPrev) {
moveSelection(+1);
}
const bool launch = (st.a && !rotatePrev) || (st.select && !selectPrev);
if (launch)
launchSelected();
leftPrev = st.left;
rightPrev = st.right;
rotatePrev = st.a;
selectPrev = st.select;
renderIfNeeded();
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
AppSleepPlan plan;
plan.slow_ms = 120;
plan.normal_ms = 40;
return plan;
}
private:
AppContext& context;
IFramebuffer& framebuffer;
std::vector<MenuEntry> entries;
std::size_t selected = 0;
bool dirty = false;
bool leftPrev = false;
bool rightPrev = false;
bool rotatePrev = false;
bool selectPrev = false;
void moveSelection(int step) {
if (entries.empty())
return;
const int count = static_cast<int>(entries.size());
int next = static_cast<int>(selected) + step;
next = (next % count + count) % count;
selected = static_cast<std::size_t>(next);
dirty = true;
}
void launchSelected() {
if (entries.empty())
return;
const auto target = entries[selected].index;
if (context.system && context.system->currentFactoryIndex() == target)
return;
context.requestAppSwitchByIndex(target);
}
void refreshEntries() {
entries.clear();
if (!context.system)
return;
const std::size_t total = context.system->appCount();
for (std::size_t i = 0; i < total; ++i) {
const IAppFactory* factory = context.system->factoryAt(i);
if (!factory)
continue;
const char* name = factory->name();
if (!name)
continue;
if (std::string_view(name) == kMenuAppNameView)
continue;
entries.push_back(MenuEntry{std::string(name), i});
}
if (selected >= entries.size())
selected = entries.empty() ? 0 : entries.size() - 1;
dirty = true;
}
static void drawCenteredText(IFramebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
const int width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
}
void drawPagerDots() {
if (entries.size() <= 1)
return;
const int count = static_cast<int>(entries.size());
const int spacing = 20;
const int dotSize = 7;
const int totalW = spacing * (count - 1);
const int startX = (framebuffer.width() - totalW) / 2;
const int baseline = framebuffer.height() - (font16x8::kGlyphHeight + 48);
for (int i = 0; i < count; ++i) {
const int cx = startX + i * spacing;
for (int dx = -dotSize / 2; dx <= dotSize / 2; ++dx) {
for (int dy = -dotSize / 2; dy <= dotSize / 2; ++dy) {
const bool isSelected = (static_cast<std::size_t>(i) == selected);
const bool on = isSelected || std::abs(dx) == dotSize / 2 || std::abs(dy) == dotSize / 2;
if (on)
framebuffer.drawPixel(cx + dx, baseline + dy, true);
}
}
}
}
void renderIfNeeded() {
if (!dirty)
return;
dirty = false;
DispTools::draw_to_display_async_wait();
framebuffer.clear(false);
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
if (entries.empty()) {
drawCenteredText(framebuffer, framebuffer.height() / 2 - 18, "NO OTHER APPS", 2, 1);
drawCenteredText(framebuffer, framebuffer.height() - 72, "ADD MORE IN FIRMWARE", 1, 1);
} else {
const std::string& name = entries[selected].name;
const int titleScale = 2;
const int centerY = framebuffer.height() / 2 - (font16x8::kGlyphHeight * titleScale) / 2;
drawCenteredText(framebuffer, centerY, name, titleScale, 0);
const std::string indexLabel = std::to_string(selected + 1) + "/" + std::to_string(entries.size());
const int topRightX = framebuffer.width() - font16x8::measureText(indexLabel, 1, 0) - 16;
font16x8::drawText(framebuffer, topRightX, 20, indexLabel, 1, true, 0);
drawPagerDots();
drawCenteredText(framebuffer, framebuffer.height() - 48, "A/SELECT START", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
}
DispTools::draw_to_display_async_start();
}
};
class MenuAppFactory final : public IAppFactory {
public:
const char* name() const override { return kMenuAppName; }
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<MenuApp>(context); }
};
} // namespace
std::unique_ptr<IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
} // namespace apps

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
#include "freertos/task.h"
#include "i2c_global.hpp"
#include "shutdowner.hpp"
static i2c_master_dev_handle_t dev_handle;
@@ -20,25 +21,78 @@ BatMon& BatMon::get() {
static void start_pooler(void* arg) { static_cast<BatMon*>(arg)->pooler(); }
void WriteRegister(uint8_t reg, uint16_t value) {
uint8_t buf2[3];
buf2[0] = reg;
buf2[1] = value & 0xFF;
buf2[2] = value >> 8;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
}
uint16_t ReadRegister(uint8_t reg) {
uint16_t buffer;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
return buffer;
}
void WriteAndVerifyRegister(char RegisterAddress, int RegisterValueToWrite) {
int attempt = 0;
uint16_t RegisterValueRead;
do {
WriteRegister(RegisterAddress, RegisterValueToWrite);
vTaskDelay(1 / portTICK_PERIOD_MS);
RegisterValueRead = ReadRegister(RegisterAddress);
} while (RegisterValueToWrite != RegisterValueRead && attempt++ < 3);
}
static constexpr float RSense = 0.1; // 100mOhm
static constexpr uint16_t DesignCapMah = 180; // 100mOhm
constexpr float mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.0) / RSense; }
constexpr float regToCurrent(uint16_t reg) {
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
}
constexpr uint16_t currentToReg(float current) { return static_cast<uint16_t>(current * RSense / 0.0015625f); }
constexpr float regToVoltage(uint16_t reg) {
return reg * 0.078125f * 0.001f; // Convert to volts
}
constexpr uint16_t voltageToReg(float voltage) {
return static_cast<uint16_t>(voltage / (0.078125f * 0.001f)); // Convert to register value
}
static constexpr uint16_t DesignCap = mahToCap(DesignCapMah);
static constexpr uint16_t IchgTerm = currentToReg(10);
static constexpr uint16_t VEmpty = 0b1001011001100001; // (3V/3.88V)
static constexpr uint16_t dQAcc = (DesignCap / 32);
BatMon::BatMon() {
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &_dev_cfg, &dev_handle));
uint8_t reg = 1;
uint8_t buffer;
uint8_t buf2[2];
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
if (buffer & (1 << 4)) // POR reset
bool StatusPOR = ReadRegister(0x00) & 0x0002;
if (StatusPOR) // POR reset
{
printf("Gas gauge reset!\n");
buf2[0] = 1;
buf2[1] = 0 << 4;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
while (ReadRegister(0x3D) & 1)
vTaskDelay(10 / portTICK_PERIOD_MS);
buf2[0] = 0;
buf2[1] = 1 << 4 | 1 << 2; // 10 bit adc
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
uint16_t HibCFG = ReadRegister(0xBA); // Store original HibCFG value
WriteRegister(0x60, 0x90); // Exit Hibernate Mode step 1
WriteRegister(0xBA, 0x0); // Exit Hibernate Mode step 2
WriteRegister(0x60, 0x0); // Exit Hibernate Mode step 3
WriteRegister(0x18, DesignCap); // Write DesignCap
WriteRegister(0x45, DesignCap / 32); // Write dQAcc
WriteRegister(0x1E, IchgTerm); // Write IchgTerm
WriteRegister(0x3A, VEmpty); // Write VEmpty
WriteRegister(0x46, dQAcc * 44138 / DesignCap); // Write dPAcc
WriteRegister(0xDB, 0x8000); // Write ModelCFG
// Poll ModelCFG.Refresh(highest bit), proceed to Step 4 when ModelCFG.Refresh = 0.
while (ReadRegister(0xDB) & 0x8000)
vTaskDelay(10 / portTICK_PERIOD_MS); // 10ms wait loop. Do not continue until ModelCFG.Refresh == 0.
WriteRegister(0xBA, HibCFG); // Restore Original HibCFG value
uint16_t Status = ReadRegister(0x00); // Read Status
WriteAndVerifyRegister(0x00, Status & 0xFFFD); // Write and Verify Status with POR bit cleared
}
xTaskCreate(&start_pooler, "BatMon", 2048, this, tskIDLE_PRIORITY, &_pooler_task);
@@ -48,28 +102,13 @@ void BatMon::pooler() {
while (true) {
uint8_t reg = 8;
uint16_t buffer;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
float voltage = buffer;
voltage *= 2.44f;
voltage /= 1000;
_voltage = voltage;
reg = 2;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
float charge = *reinterpret_cast<int16_t*>(&buffer);
charge *= 6.70f;
charge /= 50;
_charge = charge;
reg = 6;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
float current = static_cast<int16_t>(buffer << 2);
current *= 11.77f;
current /= 50;
current /= 4;
_current = current;
_charge = capToMah(ReadRegister(0x05));
_current = regToCurrent(ReadRegister(0x0B));
_voltage = regToVoltage(ReadRegister(0x09));
PowerHelper::get().delay(10000, 1000);
if (_voltage < 3.0f) {
Shutdowner::get().shutdown();
}
}
}

View File

@@ -13,6 +13,14 @@
#include "freertos/task.h"
#include "config.hpp"
#include "i2c_global.hpp"
static i2c_master_dev_handle_t dev_handle;
static inline i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x20,
.scl_speed_hz = 100000,
};
Buttons& Buttons::get() {
static Buttons buttons;
@@ -21,18 +29,48 @@ Buttons& Buttons::get() {
static void start_pooler(void* arg) { static_cast<Buttons*>(arg)->pooler(); }
Buttons::Buttons() {
ESP_ERROR_CHECK(gpio_reset_pin(SHR_OUT));
ESP_ERROR_CHECK(gpio_reset_pin(SHR_CLK));
ESP_ERROR_CHECK(gpio_reset_pin(SHR_SH));
ESP_ERROR_CHECK(gpio_set_direction(SHR_OUT, GPIO_MODE_INPUT));
ESP_ERROR_CHECK(gpio_set_pull_mode(SHR_OUT, GPIO_FLOATING));
ESP_ERROR_CHECK(gpio_set_direction(SHR_SH, GPIO_MODE_OUTPUT));
ESP_ERROR_CHECK(gpio_set_direction(SHR_CLK, GPIO_MODE_OUTPUT));
static bool is_on_low;
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 1, &_pooler_task);
static void wakeup(void* arg) {
if (is_on_low) {
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_HIGH_LEVEL));
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_HIGH_LEVEL));
is_on_low = false;
BaseType_t xResult = pdFAIL;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR(Buttons::get()._pooler_task, 0, eNoAction, &xHigherPriorityTaskWoken);
PowerHelper::get().reset_slow_isr(&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
} else {
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_LOW_LEVEL));
is_on_low = true;
}
}
Buttons::Buttons() {
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &dev_cfg, &dev_handle));
uint8_t buf2[2];
// Config
buf2[0] = 6;
buf2[1] = 0xFF;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
buf2[0] = 7;
buf2[1] = 0x80;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 1, &_pooler_task);
ESP_ERROR_CHECK(gpio_reset_pin(EXP_INT));
ESP_ERROR_CHECK(gpio_set_direction(EXP_INT, GPIO_MODE_INPUT));
ESP_ERROR_CHECK(gpio_set_pull_mode(EXP_INT, GPIO_FLOATING));
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_LOW_LEVEL));
is_on_low = true;
}
static void delay(unsigned long long loop) {
for (unsigned long long i = 0; i < loop; i++) {
@@ -42,18 +80,17 @@ static void delay(unsigned long long loop) {
void Buttons::pooler() {
while (true) {
ESP_ERROR_CHECK(gpio_set_level(SHR_SH, 0));
ESP_ERROR_CHECK(gpio_set_level(SHR_SH, 1));
uint8_t new_val = 0;
for (int i = 0; i < 8; i++) {
ESP_ERROR_CHECK(gpio_set_level(SHR_CLK, 0));
new_val |= gpio_get_level(SHR_OUT) << i;
ESP_ERROR_CHECK(gpio_set_level(SHR_CLK, 1));
}
_current = new_val;
PowerHelper::get().delay(10000, 100);
BaseType_t xResult = xTaskNotifyWait(pdFALSE, ULONG_MAX, nullptr, portMAX_DELAY);
uint8_t reg = 0;
uint8_t buffer;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
_current = buffer;
// read second port too to clear the interrupt
reg = 1;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
}
}
uint8_t Buttons::get_pressed() { return _current; }
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }

View File

@@ -0,0 +1,191 @@
// Buzzer implementation
#include "buzzer.hpp"
#include "config.hpp"
#include <driver/ledc.h>
#include <esp_err.h>
#include <esp_timer.h>
#include <nvs_flash.h>
#include <nvs.h>
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
Buzzer &Buzzer::get() {
static Buzzer b;
return b;
}
void Buzzer::init() {
// Initialize NVS once (safe if already done)
static bool nvsInited = false;
if (!nvsInited) {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
nvsInited = true;
}
ledc_timer_config_t tcfg{};
tcfg.speed_mode = LEDC_MODE;
tcfg.timer_num = LEDC_TIMER;
tcfg.duty_resolution = LEDC_BITS;
tcfg.freq_hz = 1000; // placeholder, changed per tone
tcfg.clk_cfg = LEDC_AUTO_CLK;
ESP_ERROR_CHECK(ledc_timer_config(&tcfg));
ledc_channel_config_t ccfg{};
ccfg.speed_mode = LEDC_MODE;
ccfg.channel = LEDC_CH;
ccfg.timer_sel = LEDC_TIMER;
ccfg.gpio_num = static_cast<int>(BUZZER_PIN);
ccfg.duty = 0; // start silent
ccfg.hpoint = 0;
ccfg.intr_type = LEDC_INTR_DISABLE;
ESP_ERROR_CHECK(ledc_channel_config(&ccfg));
esp_timer_create_args_t args{};
args.callback = &Buzzer::timerCb;
args.arg = this;
args.name = "buzz";
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
loadState();
}
void Buzzer::applyFreq(uint32_t freq) {
if (freq == 0) {
ledc_stop(LEDC_MODE, LEDC_CH, 0);
return;
}
ledc_set_freq(LEDC_MODE, LEDC_TIMER, freq);
ledc_set_duty(LEDC_MODE, LEDC_CH, (1 << LEDC_BITS) / 2);
ledc_update_duty(LEDC_MODE, LEDC_CH);
}
void Buzzer::enqueue(const Step &s) {
int nextTail = (_q_tail + 1) % MAX_QUEUE;
if (nextTail == _q_head) { // full, drop oldest
_q_head = (_q_head + 1) % MAX_QUEUE;
}
_queue[_q_tail] = s;
_q_tail = nextTail;
}
void Buzzer::popFront() {
if (!empty())
_q_head = (_q_head + 1) % MAX_QUEUE;
}
void Buzzer::startNext() {
if (empty()) {
_running = false;
applyFreq(0);
return;
}
_running = true;
_in_gap = false;
Step &s = front();
applyFreq(s.freq);
schedule(s.dur_ms, false);
}
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
if (!_timer) return;
_in_gap = gapPhase;
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
}
void Buzzer::timerCb(void *arg) {
auto *self = static_cast<Buzzer*>(arg);
if (!self) return;
if (self->_in_gap) {
self->popFront();
self->startNext();
return;
}
// Tone finished
if (!self->empty()) {
auto &s = self->front();
if (s.gap_ms) {
self->applyFreq(0);
self->schedule(s.gap_ms, true);
return;
}
self->popFront();
self->startNext();
}
}
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
if (_muted) return; // ignore while muted
Step s{freq, duration_ms, gap_ms};
enqueue(s);
if (!_running)
startNext();
}
// ---- Game SFX ----
void Buzzer::beepRotate() { tone(1800, 25); }
void Buzzer::beepMove() { tone(1200, 12); }
void Buzzer::beepLock() { tone(900, 25); }
void Buzzer::beepLines(int lines) {
static const uint32_t base = 1100;
for (int i = 0; i < lines; ++i) {
tone(base + i * 190, 40, 12);
}
}
void Buzzer::beepLevelUp(int) {
tone(1600, 70, 25);
tone(2000, 90, 0);
}
void Buzzer::beepGameOver() {
tone(1000, 140, 40);
tone(700, 140, 40);
tone(400, 260, 0);
}
void Buzzer::setMuted(bool m) {
if (m == _muted) return;
_muted = m;
if (_muted) {
clearQueue();
applyFreq(0);
if (_timer) {
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
}
_running = false;
_in_gap = false;
} else {
// confirmation chirp
tone(1500, 40, 10);
tone(1900, 60, 0);
}
saveState();
}
void Buzzer::toggleMuted() { setMuted(!_muted); }
void Buzzer::loadState() {
nvs_handle_t h;
if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) {
uint8_t v = 0;
if (nvs_get_u8(h, "mute", &v) == ESP_OK) {
_muted = (v != 0);
}
nvs_close(h);
}
if (_muted) applyFreq(0);
}
void Buzzer::saveState() {
nvs_handle_t h;
if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, "mute", _muted ? 1 : 0);
nvs_commit(h);
nvs_close(h);
}
}

View File

@@ -8,68 +8,3 @@
#include <display.hpp>
DispTools& DispTools::get() {
static DispTools disp_tools;
return disp_tools;
}
void DispTools::clear() {
for (int y = 0; y < DISP_HEIGHT; y++) {
for (int x = 0; x < DISP_WIDTH; x++) {
disp_frame[y][x] = 1;
}
}
}
bool DispTools::get_pixel(int x, int y) { return disp_frame[y][x]; }
void DispTools::reset_pixel(int x, int y) { disp_frame[y][x] = true; }
void DispTools::set_pixel(int x, int y) { disp_frame[y][x] = false; }
void DispTools::draw_rectangle(int x1, int y1, int x2, int y2) {
int dy = y2 - y1;
while (std::abs(dy) > 0) {
draw_line(x1, y1 + dy, x2, y1 + dy);
dy += (dy > 0) ? -1 : 1;
}
}
void DispTools::draw_line(int x1, int y1, int x2, int y2) {
int dx = x2 - x1;
int dy = y2 - y1;
int a = 0, b = 0, diff = 0;
if (dx == 0) {
while (dy != 0) {
set_pixel(x1, y1 + dy);
dy += (dy > 0) ? -1 : 1;
}
return;
}
if (dy == 0) {
while (dx != 0) {
set_pixel(x1 + dx, y1);
dx += (dx > 0) ? -1 : 1;
}
return;
}
while (std::abs(a) <= std::abs(dx) && std::abs(b) <= std::abs(dy)) {
set_pixel(x1 + a, y1 + b);
if (diff < 0) {
a += (dx > 0) ? 1 : -1;
diff += std::abs(dy);
} else {
b += (dy > 0) ? 1 : -1;
diff -= std::abs(dx);
}
}
}
void DispTools::draw_circle(int x, int y, int r) {
if (r > 181)
return;
int dy = -r;
while (dy <= r) {
int dx = static_cast<int>(std::sqrt(r * r - dy * dy));
draw_line(x - dx, y + dy, x + dx, y + dy);
dy++;
}
}
void DispTools::draw_to_display() { SMD::get().draw(disp_frame); }

View File

@@ -13,9 +13,9 @@ void FbTty::draw_char(int col, int row) {
for (int y = 0; y < 16; y++) {
bool color = fonts_Terminess_Powerline[_buf[col][row]][y] & (1 << (8 - x));
if (color)
DispTools::get().set_pixel(col * 8 + x, row * 16 + y);
DispTools::set_pixel(col * 8 + x, row * 16 + y);
else
DispTools::get().reset_pixel(col * 8 + x, row * 16 + y);
DispTools::reset_pixel(col * 8 + x, row * 16 + y);
}
}
}

View File

@@ -1,62 +1,137 @@
//
// Created by Stepan Usatiuk on 02.03.2025.
//
// Simplified display implementation (no async memcpy) ---------------------------------
#include "display.hpp"
#include <cstring>
#include <driver/gpio.h>
#include "disp_tools.hpp"
#include "driver/spi_master.h"
#include "esp_async_memcpy.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
// This solution is attributed to Rich Schroeppel in the Programming Hacks section
// TODO: Why does the device flag not work?
unsigned char reverse_bits3(unsigned char b) { return (b * 0x0202020202ULL & 0x010884422010ULL) % 0x3ff; }
DMA_ATTR uint8_t SMD::dma_buf[SMD::kLineDataBytes]{};
DMA_ATTR uint8_t dma_buf_template[SMD::kLineDataBytes]{};
std::array<uint8_t, SMD::kLineBytes> SMD::prep_line(const SMD::disp_line_t& line) {
std::array<uint8_t, kLineBytes> data{};
for (int i = 0; i < DISP_WIDTH; i++) {
data[i / 8] = data[i / 8] | (line[i] << (i % 8));
spi_device_handle_t SMD::_spi;
static spi_transaction_t _tx{};
static bool _vcom = false;
volatile bool _inFlight = false;
static TaskHandle_t s_clearTaskHandle = nullptr;
static SemaphoreHandle_t s_clearReqSem = nullptr;
static SemaphoreHandle_t s_clearSem = nullptr;
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
// update the maximum data stream supported by underlying DMA engine
static async_memcpy_handle_t driver = NULL;
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;
_inFlight = false;
xSemaphoreGiveFromISR(s_clearSem,
&high_task_wakeup); // high_task_wakeup set to pdTRUE if some high priority task unblocked
return high_task_wakeup == pdTRUE;
}
static void zero_framebuffer_payload() {
ESP_ERROR_CHECK(esp_async_memcpy(driver, SMD::dma_buf, dma_buf_template, 12480, my_async_memcpy_cb, nullptr));
}
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) {
printf("Started zeroing\n");
spi_transaction_t* r = nullptr;
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
zero_framebuffer_payload();
// printf("Zeroing done\n");
}
}
for (int i = 0; i < kLineBytes; i++) {
data[i] = reverse_bits3(data[i]);
}
return data;
}
SMD& SMD::get() {
static SMD smd;
return smd;
void SMD::ensure_clear_task() {
if (!s_clearReqSem)
s_clearReqSem = xSemaphoreCreateBinary();
if (!s_clearSem)
s_clearSem = xSemaphoreCreateBinary();
xSemaphoreGive(s_clearSem);
if (!s_clearTaskHandle)
xTaskCreatePinnedToCore(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle, 0);
}
SMD::SMD() { spi_bus_add_device(SPI_BUS, &_devcfg, &_spi); }
void SMD::clear() {
std::array<uint8_t, 2> buf{};
buf[0] = 0b00100000;
spi_transaction_t t{};
t.tx_buffer = buf.data();
t.length = buf.size() * 8;
ESP_ERROR_CHECK(spi_device_transmit(_spi, &t));
}
void SMD::draw(const disp_frame_t& frame) {
_vcom = !_vcom;
void SMD::init() {
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
ensure_clear_task();
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++) {
spi_transaction_t t{};
t.tx_buffer = buf.data();
t.length = buf.size() * 8;
buf[0] = 0b10000000 | (_vcom << 6);
buf[1] = reverse_bits3(i + 1);
auto prepared = prep_line(frame.at(i));
memcpy(buf.data() + 2, prepared.data(), kLineBytes);
buf[2 + kLineBytes] = 0;
buf[2 + kLineBytes + 1] = 0;
ESP_ERROR_CHECK(spi_device_transmit(_spi, &t));
dma_buf[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
dma_buf[2 + kLineMultiSingle * i + kLineBytes] = 0;
}
dma_buf[kLineDataBytes - 1] = 0;
for (int y = 0; y < DISP_HEIGHT; ++y)
for (int x = 0; x < DISP_WIDTH; ++x)
DispTools::set_pixel(x, y, false);
std::memcpy(dma_buf_template, dma_buf, sizeof(dma_buf_template));
ESP_ERROR_CHECK(esp_async_memcpy_install(&config, &driver)); // install driver with default DMA engine
}
bool SMD::async_draw_busy() { return _inFlight; }
void SMD::async_draw_start() {
assert(!_inFlight);
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
assert(false);
_vcom = !_vcom;
_tx = {};
_tx.tx_buffer = dma_buf;
_tx.length = SMD::kLineDataBytes * 8;
dma_buf[0] = 0b10000000 | (_vcom << 6);
_inFlight = true;
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
}
void SMD::async_draw_wait() {
if (!_inFlight || uxSemaphoreGetCount(s_clearSem)) {
// assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight);
return;
}
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
assert(false);
if (!xSemaphoreGive(s_clearSem))
assert(false);
assert(!_inFlight);
}
// (clear_in_progress / wait_clear / request_clear removed from public API)
// Surface implementation ------------------------------------------------------
void SMDSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
if (pixel.on)
DispTools::set_pixel(x, y);
else
DispTools::reset_pixel(x, y);
}
void SMDSurface::clear_impl() { DispTools::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() {}

View File

@@ -0,0 +1,68 @@
#include "fs_helper.hpp"
#include <esp_idf_version.h>
#include <esp_littlefs.h>
#include <esp_log.h>
#include <cstring>
namespace {
constexpr const char* kTag = "FsHelper";
} // namespace
FsHelper& FsHelper::get() {
static FsHelper instance;
return instance;
}
esp_err_t FsHelper::mount() {
if (mounted)
return ESP_OK;
esp_vfs_littlefs_conf_t conf{};
conf.base_path = kBasePath;
conf.partition_label = kPartitionLabel;
conf.format_if_mount_failed = kFormatOnFailure;
conf.dont_mount = false;
#if ESP_IDF_VERSION_MAJOR >= 5
conf.read_only = false;
#endif
const esp_err_t err = esp_vfs_littlefs_register(&conf);
if (err != ESP_OK) {
if (err == ESP_ERR_NOT_FOUND) {
ESP_LOGE(kTag, "Failed to find LittleFS partition '%s'", kPartitionLabel);
} else if (err == ESP_FAIL) {
ESP_LOGE(kTag, "Failed to mount LittleFS at %s (consider enabling format)",
kBasePath);
} else {
ESP_LOGE(kTag, "esp_vfs_littlefs_register failed: %s", esp_err_to_name(err));
}
return err;
}
mounted = true;
size_t total = 0;
size_t used = 0;
const esp_err_t infoErr = esp_littlefs_info(kPartitionLabel, &total, &used);
if (infoErr == ESP_OK) {
ESP_LOGI(kTag, "LittleFS mounted at %s (%zu / %zu bytes used)", kBasePath, used, total);
} else {
ESP_LOGW(kTag, "LittleFS mounted but failed to query usage: %s", esp_err_to_name(infoErr));
}
return ESP_OK;
}
void FsHelper::unmount() {
if (!mounted)
return;
const esp_err_t err = esp_vfs_littlefs_unregister(kPartitionLabel);
if (err != ESP_OK) {
ESP_LOGW(kTag, "Failed to unmount LittleFS (%s)", esp_err_to_name(err));
return;
}
mounted = false;
ESP_LOGI(kTag, "LittleFS unmounted from %s", kBasePath);
}

View File

@@ -1,119 +0,0 @@
/*
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <buttons.hpp>
#include <cstdint>
#include <disp_tools.hpp>
#include <disp_tty.hpp>
#include <esp_pm.h>
#include <inttypes.h>
#include <stdio.h>
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "display.hpp"
#include "bat_mon.hpp"
#include "driver/i2c_master.h"
#include "driver/spi_master.h"
#include "i2c_global.hpp"
#include <driver/gpio.h>
#include <esp_sleep.h>
#include <memory>
#include <power_helper.hpp>
#include <shutdowner.hpp>
#include <spi_global.hpp>
#include <string>
FbTty tty;
extern "C" void app_main() {
esp_pm_config_t pm_config = {
.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ, .min_freq_mhz = 16, .light_sleep_enable = true};
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
printf("Hello world!\n");
// TODO: Where to put that?
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
// For some reason, calling it here hangs on startup, sometimes
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
PowerHelper::get();
Shutdowner::get();
ESP_ERROR_CHECK(gpio_install_isr_service(0));
Shutdowner::get().install_isr();
PowerHelper::get().install_isr();
Buttons::get();
I2cGlobal::get();
BatMon::get();
SpiGlobal::get();
SMD::get();
SMD::get().clear();
DispTools::get().clear();
DispTools::get().draw_line(0, 0, 400, 240);
DispTools::get().draw_circle(100, 100, 20);
DispTools::get().draw_to_display();
tty.putstr("Hello\nworld!");
DispTools::get().draw_to_display();
int rx = 30, ry = 30;
int lastmove = 0;
while (true) {
// SMD::clear();
// printf("Voltage: %f\n", BatMon::get_voltage());
DispTools::get().clear();
tty.reset();
uint8_t pressed = Buttons::get().get_pressed();
if (pressed & L3)
rx -= 5;
if (pressed & L4)
ry += 5;
if (pressed & R3)
ry -= 5;
if (pressed & R4)
rx += 5;
if (pressed == 0 && !PowerHelper::get().is_slow())
lastmove++;
else if (pressed != 0) {
lastmove = 0;
PowerHelper::get().set_slow(false);
}
if (lastmove > 20) {
lastmove = 0;
PowerHelper::get().set_slow(true);
}
bool slow = PowerHelper::get().is_slow();
tty.fmt("{:.1f}mA {:.1f}V {:.1f}mAh {}", BatMon::get().get_current(), BatMon::get().get_voltage(),
BatMon::get().get_charge(), slow ? "S" : "");
if (rx < 30)
rx = 30;
if (rx > 370)
rx = 370;
if (ry < 30)
ry = 30;
if (ry > 210)
ry = 210;
// tty.fmt("Button: {}", pressed);
DispTools::get().draw_circle(rx, ry, 20);
// printf("Restarting in %d seconds...\n", i);
DispTools::get().draw_to_display();
PowerHelper::get().delay(10000, 30);
}
// printf("Restarting now.\n");
// fflush(stdout);
// esp_restart();
}

View File

@@ -25,32 +25,22 @@ void PowerHelper::set_slow(bool slow) {
}
}
void PowerHelper::reset_slow_isr() {
BaseType_t PowerHelper::reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken) {
_slow = false;
return xEventGroupSetBitsFromISR(_event_group, 1, xHigherPriorityTaskWoken);
}
static void wakeup(void* arg) {
BaseType_t xHigherPriorityTaskWoken, xResult;
xHigherPriorityTaskWoken = pdFALSE;
_slow = false;
xResult = xEventGroupSetBitsFromISR(_event_group, 1, &xHigherPriorityTaskWoken);
xResult = static_cast<PowerHelper*>(arg)->reset_slow_isr(&xHigherPriorityTaskWoken);
if (xResult != pdFAIL) {
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
};
}
static void wakeup(void* arg) { static_cast<PowerHelper*>(arg)->reset_slow_isr(); }
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) {
ESP_ERROR_CHECK(gpio_reset_pin(DIRECT_BTN));
ESP_ERROR_CHECK(gpio_set_direction(DIRECT_BTN, GPIO_MODE_INPUT));
ESP_ERROR_CHECK(gpio_set_pull_mode(DIRECT_BTN, GPIO_FLOATING));
ESP_ERROR_CHECK(gpio_set_intr_type(DIRECT_BTN, GPIO_INTR_HIGH_LEVEL));
ESP_ERROR_CHECK(gpio_wakeup_enable(DIRECT_BTN, GPIO_INTR_HIGH_LEVEL));
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
// gpio_isr_handler_add(DIRECT_BTN, wakeup, this);
set_slow(false);
}
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) { set_slow(false); }
void PowerHelper::delay(int slow_ms, int normal_ms) {
if (is_slow()) {
@@ -67,4 +57,6 @@ void PowerHelper::delay(int slow_ms, int normal_ms) {
vTaskDelay(normal_ms / portTICK_PERIOD_MS);
}
}
void PowerHelper::install_isr() { gpio_isr_handler_add(DIRECT_BTN, wakeup, this); }
void PowerHelper::install_isr() {
// gpio_isr_handler_add(EXP_INT, wakeup, this);
}

View File

@@ -14,26 +14,31 @@ Shutdowner& Shutdowner::get() {
return instance;
}
static void IRAM_ATTR shutdown(void* arg) {
static void IRAM_ATTR int_shutdown(void* arg) {
// printf("Shutting down...\n");
ESP_ERROR_CHECK(gpio_hold_dis(PWR_KILL));
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 0));
}
Shutdowner::Shutdowner() {
ESP_ERROR_CHECK(gpio_reset_pin(PWR_INT));
ESP_ERROR_CHECK(gpio_reset_pin(PWR_KILL));
void Shutdowner::shutdown() {
ESP_ERROR_CHECK(gpio_hold_dis(PWR_KILL));
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 0));
}
ESP_ERROR_CHECK(gpio_set_direction(PWR_INT, GPIO_MODE_INPUT));
Shutdowner::Shutdowner() {
ESP_ERROR_CHECK(gpio_reset_pin(PWR_KILL));
ESP_ERROR_CHECK(gpio_set_direction(PWR_KILL, GPIO_MODE_OUTPUT));
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 1));
ESP_ERROR_CHECK(gpio_hold_en(PWR_KILL));
ESP_ERROR_CHECK(gpio_reset_pin(PWR_INT));
ESP_ERROR_CHECK(gpio_set_direction(PWR_INT, GPIO_MODE_INPUT));
ESP_ERROR_CHECK(gpio_set_pull_mode(PWR_INT, GPIO_FLOATING));
ESP_ERROR_CHECK(gpio_set_intr_type(PWR_INT, GPIO_INTR_LOW_LEVEL));
// ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
ESP_ERROR_CHECK(gpio_wakeup_enable(PWR_INT, GPIO_INTR_LOW_LEVEL));
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_hold_en(PWR_KILL));
// gpio_isr_handler_add(PWR_INT, shutdown, nullptr);
}
void Shutdowner::install_isr() { gpio_isr_handler_add(PWR_INT, shutdown, nullptr); }
void Shutdowner::install_isr() { gpio_isr_handler_add(PWR_INT, int_shutdown, nullptr); }

6
Firmware/partitions.csv Normal file
View 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 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 phy_init data phy 0x10000 0x1000
5 factory app factory 0x20000 0x250000
6 littlefs data littlefs 0x190000

View File

@@ -0,0 +1,76 @@
# Generated from CLion C/C++ Code Style settings
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignTrailingComments:
Kind: Always
OverEmptyLines: 2
SpacesBeforeTrailingComments: 1
AlignOperands: Align
AlignEscapedNewlines: Right
AlwaysBreakTemplateDeclarations: Yes
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBraces: Custom
BreakConstructorInitializers: AfterColon
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 120
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ContinuationIndentWidth: 8
IncludeCategories:
- Regex: '^<.*'
Priority: 1
- Regex: '^".*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseLabels: true
IndentWidth: 4
InsertNewlineAtEOF: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 2
PointerAlignment: Left
SpaceAfterCStyleCast: true
SpaceAfterTemplateKeyword: false
SpaceBeforeRangeBasedForLoopColon: false
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
...

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.10)
project(sdk-top)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
add_subdirectory(library)
if (NOT CMAKE_CROSSCOMPILING)
add_subdirectory(sfml-port)
add_subdirectory(examples)
endif ()

View File

View File

@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.10)
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)
target_include_directories(cbsdk PUBLIC include_public)
target_include_directories(cbsdk PRIVATE include)
if (NOT CMAKE_CROSSCOMPILING)
add_subdirectory(test)
endif ()

View File

@@ -0,0 +1,139 @@
//
// 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>
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...>); }
};
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

View File

@@ -0,0 +1,128 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef GRIDWINDOW_HPP
#define GRIDWINDOW_HPP
#include <string>
#include "Fonts.hpp"
#include "SubSurface.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename SurfaceType, unsigned nWidth, unsigned nHeight>
class GridWindow : public Window<SurfaceType> {
public:
using PixelType = typename SurfaceType::PixelType;
explicit GridWindow(SurfaceType* owner) : Window<SurfaceType>(owner) {
for (int i = 0; i < nWidth; ++i) {
for (int j = 0; j < nHeight; ++j) {
_grid[i][j].emplace(owner);
}
}
}
EventHandlingResult handle_v(KeyboardEvent keyboardEvent) override {
if (keyboardEvent.key_code == Key::Escape) {
if (!_has_focus) {
return EventHandlingResult::CONTINUE;
} else {
auto res = _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
if (res == EventHandlingResult::DONE) {
return EventHandlingResult::DONE;
} else {
_has_focus = false;
}
}
} else if (keyboardEvent.key_code == Key::Enter) {
if (!_has_focus) {
_has_focus = true;
} else {
return _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
}
} else {
if (_has_focus) {
return _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
}
if (keyboardEvent.key_code == Key::Left) {
if (_current_focus_x > 0) {
_current_focus_x--;
}
} else if (keyboardEvent.key_code == Key::Right) {
if (_current_focus_x < nWidth - 1) {
_current_focus_x++;
}
} else if (keyboardEvent.key_code == Key::Up) {
if (_current_focus_y > 0) {
_current_focus_y--;
}
} else if (keyboardEvent.key_code == Key::Down) {
if (_current_focus_y < nHeight - 1) {
_current_focus_y++;
}
}
}
refresh();
return EventHandlingResult::DONE;
}
EventHandlingResult handle_v(SurfaceResizeEvent resize) override {
_cell_width = this->_owner->get_width() / nWidth;
_cell_height = this->_owner->get_height() / nHeight;
for (int i = 0; i < nWidth; ++i) {
for (int j = 0; j < nHeight; ++j) {
if constexpr (is_specialization_of<SubSurface, SurfaceType>::value) {
_grid[i][j]->set_pos(this->_owner->get_x_offset() + i * _cell_width + 1,
this->_owner->get_y_offset() + j * _cell_height + 1, _cell_width - 2,
_cell_height - 2);
} else {
_grid[i][j]->set_pos(i * _cell_width + 1, j * _cell_height + 1, _cell_width - 2, _cell_height - 2);
}
}
}
refresh();
return EventHandlingResult::DONE;
}
template<typename WindowType, typename... Args>
void set_window(unsigned x, unsigned y, Args&&... args) {
_grid[x][y]->template set_window<WindowType>(std::forward<Args>(args)...);
}
SubSurface<SurfaceType>& get_subsurface(unsigned x, unsigned y) {
// assert(x >= nWidth && y >= nHeight);
return *_grid[x][y];
}
void refresh() {
for (int i = 0; i < nWidth; ++i) {
for (int j = 0; j < nHeight; ++j) {
if (i == _current_focus_x && j == _current_focus_y) {
this->_owner->draw_rect(i * _cell_width, j * _cell_height, _cell_width, _cell_height,
PixelType(true));
} else {
this->_owner->draw_rect(i * _cell_width, j * _cell_height, _cell_width, _cell_height,
PixelType(false));
}
}
}
}
private:
using SubType = std::conditional_t<is_specialization_of<SubSurface, SurfaceType>::value, SurfaceType,
SubSurface<SurfaceType>>;
std::array<std::array<std::optional<SubType>, nWidth>, nHeight> _grid;
unsigned _cell_width = 0;
unsigned _cell_height = 0;
unsigned _current_focus_x = 0;
unsigned _current_focus_y = 0;
bool _has_focus = false;
};
#endif // TEXTWINDOW_HPP

View File

@@ -0,0 +1,21 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef PIXEL_HPP
#define PIXEL_HPP
class Pixel {
};
struct BwPixel : public Pixel {
bool on = false;
BwPixel() = default;
BwPixel(bool on) : on(on) {}
bool operator==(const BwPixel& other) const { return on == other.on; }
bool operator!=(const BwPixel& other) const { return !(*this == other); }
};
#endif //PIXEL_HPP

View File

@@ -0,0 +1,163 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef STANDARDEVENTS_HPP
#define STANDARDEVENTS_HPP
#include "Event.hpp"
// TODO: rewrite this
////////////////////////////////////////////////////////////
//
// SFML - Simple and Fast Multimedia Library
// Copyright (C) 2007-2025 Laurent Gomila (laurent@sfml-dev.org)
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented;
// you must not claim that you wrote the original software.
// If you use this software in a product, an acknowledgment
// in the product documentation would be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such,
// and must not be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
////////////////////////////////////////////////////////////
enum class Key {
Unknown = -1, //!< Unhandled key
A = 0, //!< The A key
B, //!< The B key
C, //!< The C key
D, //!< The D key
E, //!< The E key
F, //!< The F key
G, //!< The G key
H, //!< The H key
I, //!< The I key
J, //!< The J key
K, //!< The K key
L, //!< The L key
M, //!< The M key
N, //!< The N key
O, //!< The O key
P, //!< The P key
Q, //!< The Q key
R, //!< The R key
S, //!< The S key
T, //!< The T key
U, //!< The U key
V, //!< The V key
W, //!< The W key
X, //!< The X key
Y, //!< The Y key
Z, //!< The Z key
Num0, //!< The 0 key
Num1, //!< The 1 key
Num2, //!< The 2 key
Num3, //!< The 3 key
Num4, //!< The 4 key
Num5, //!< The 5 key
Num6, //!< The 6 key
Num7, //!< The 7 key
Num8, //!< The 8 key
Num9, //!< The 9 key
Escape, //!< The Escape key
LControl, //!< The left Control key
LShift, //!< The left Shift key
LAlt, //!< The left Alt key
LSystem, //!< The left OS specific key: window (Windows and Linux), apple (macOS), ...
RControl, //!< The right Control key
RShift, //!< The right Shift key
RAlt, //!< The right Alt key
RSystem, //!< The right OS specific key: window (Windows and Linux), apple (macOS), ...
Menu, //!< The Menu key
LBracket, //!< The [ key
RBracket, //!< The ] key
Semicolon, //!< The ; key
Comma, //!< The , key
Period, //!< The . key
Apostrophe, //!< The ' key
Slash, //!< The / key
Backslash, //!< The \ key
Grave, //!< The ` key
Equal, //!< The = key
Hyphen, //!< The - key (hyphen)
Space, //!< The Space key
Enter, //!< The Enter/Return keys
Backspace, //!< The Backspace key
Tab, //!< The Tabulation key
PageUp, //!< The Page up key
PageDown, //!< The Page down key
End, //!< The End key
Home, //!< The Home key
Insert, //!< The Insert key
Delete, //!< The Delete key
Add, //!< The + key
Subtract, //!< The - key (minus, usually from numpad)
Multiply, //!< The * key
Divide, //!< The / key
Left, //!< Left arrow
Right, //!< Right arrow
Up, //!< Up arrow
Down, //!< Down arrow
Numpad0, //!< The numpad 0 key
Numpad1, //!< The numpad 1 key
Numpad2, //!< The numpad 2 key
Numpad3, //!< The numpad 3 key
Numpad4, //!< The numpad 4 key
Numpad5, //!< The numpad 5 key
Numpad6, //!< The numpad 6 key
Numpad7, //!< The numpad 7 key
Numpad8, //!< The numpad 8 key
Numpad9, //!< The numpad 9 key
F1, //!< The F1 key
F2, //!< The F2 key
F3, //!< The F3 key
F4, //!< The F4 key
F5, //!< The F5 key
F6, //!< The F6 key
F7, //!< The F7 key
F8, //!< The F8 key
F9, //!< The F9 key
F10, //!< The F10 key
F11, //!< The F11 key
F12, //!< The F12 key
F13, //!< The F13 key
F14, //!< The F14 key
F15, //!< The F15 key
Pause, //!< The Pause key
};
struct KeyboardEvent : public Event {
KeyboardEvent(Key key_code) : key_code(key_code) {}
Key key_code;
};
struct SurfaceEvent : public Event {
enum class EventType { CLOSED, OPENED };
EventType type;
};
struct SurfaceResizeEvent : public Event {
SurfaceResizeEvent(unsigned int width, unsigned int height) : width(width), height(height) {}
unsigned width;
unsigned height;
};
template<typename Derived>
using StandardEventHandler = EventHandler<Derived, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>;
template<typename Derived>
using StandardEventQueue = EventQueue<Derived, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>;
#endif // STANDARDEVENTS_HPP

View File

@@ -0,0 +1,58 @@
//
// Created by Stepan Usatiuk on 27.07.2025.
//
#ifndef SUBSURFACE_HPP
#define SUBSURFACE_HPP
#include <memory>
#include <type_traits>
#include "Pixel.hpp"
#include "StandardEvents.hpp"
#include "Surface.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename SurfaceParent>
class SubSurface : public Surface<SubSurface<SurfaceParent>, typename SurfaceParent::PixelType> {
public:
using PixelType = typename SurfaceParent::PixelType;
SubSurface(SurfaceParent* parent) : _parent(parent) {}
SubSurface(SubSurface<SurfaceParent>* parent) : _parent(parent->_parent) {}
void draw_pixel_impl(unsigned x, unsigned y, const PixelType& pixel) {
if (x >= _x_size || y >= _y_size) {
assert(false);
}
_parent->draw_pixel(x + _x_offset, y + _y_offset, pixel);
}
unsigned get_x_offset() const { return _x_offset; }
unsigned get_y_offset() const { return _y_offset; }
unsigned get_width_impl() const { return _x_size; }
unsigned get_height_impl() const { return _y_size; }
void set_pos(unsigned x_offset, unsigned y_offset, unsigned x_size, unsigned y_size) {
_x_offset = x_offset;
_y_offset = y_offset;
_x_size = x_size;
_y_size = y_size;
this->handle(SurfaceResizeEvent(x_size, y_size));
}
private:
unsigned _x_offset = 0;
unsigned _y_offset = 0;
unsigned _x_size = 0;
unsigned _y_size = 0;
SurfaceParent* _parent;
};
#endif // SUBSURFACE_HPP

View File

@@ -0,0 +1,81 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef SURFACE_HPP
#define SURFACE_HPP
#include <cassert>
#include <memory>
#include <type_traits>
#include "Pixel.hpp"
#include "StandardEvents.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename Derived, typename PixelType>
requires std::is_base_of_v<Pixel, PixelType>
class Surface : public StandardEventHandler<Derived> {
public:
Surface() { static_assert(std::is_same_v<PixelType, typename Derived::PixelType>); }
void draw_pixel(unsigned x, unsigned y, const BwPixel& pixel) {
static_cast<Derived*>(this)->draw_pixel_impl(x, y, pixel);
}
void draw_rect(unsigned x, unsigned y, unsigned width, unsigned height, const BwPixel& pixel) {
for (unsigned i = 0; i < width; ++i) {
draw_pixel(x + i, y, pixel);
draw_pixel(x + i, y + height - 1, pixel);
}
for (unsigned i = 0; i < height; ++i) {
draw_pixel(x, y + i, pixel);
draw_pixel(x + width - 1, y + i, pixel);
}
}
void clear() {
for (unsigned x = 0; x < get_width(); x++) {
for (unsigned y = 0; y < get_height(); y++) {
draw_pixel(x, y, PixelType());
}
}
}
int get_width() const { return static_cast<const Derived*>(this)->get_width_impl(); }
int get_height() const { return static_cast<const Derived*>(this)->get_height_impl(); }
template<typename T>
EventHandlingResult handle(const T& event) {
if (_window.get())
return _window->handle(event);
return EventHandlingResult::CONTINUE;
}
template<typename WindowType, typename... Args>
void set_window(Args&&... args) {
_window = std::make_unique<WindowType>(static_cast<Derived*>(this), std::forward<Args>(args)...);
}
Surface(const Surface& other) = delete;
Surface(Surface&& other) noexcept = delete;
Surface& operator=(const Surface& other) = delete;
Surface& operator=(Surface&& other) noexcept = delete;
bool has_window() const { return _window != nullptr; }
Window<Derived>* get_window() {
assert(has_window());
return _window.get();
}
protected:
std::unique_ptr<Window<Derived>> _window = nullptr;
};
#endif // SURFACE_HPP

View File

@@ -0,0 +1,72 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef TEXTWINDOW_HPP
#define TEXTWINDOW_HPP
#include <string>
#include "Fonts.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename StringType>
struct TextUpdateEvent : public Event {
TextUpdateEvent(StringType text) : new_text(std::move(text)) {}
StringType new_text;
};
template<typename SurfaceType, typename StringType>
class TextWindow : public Window<SurfaceType>,
public EventHandler<TextUpdateEvent<StringType>>,
public EventQueue<TextWindow<SurfaceType, StringType>, TextUpdateEvent<StringType>> {
public:
using PixelType = typename SurfaceType::PixelType;
explicit TextWindow(SurfaceType* owner, EventLoop* loop, StringType text = "") :
Window<SurfaceType>(owner), EventQueue<TextWindow, TextUpdateEvent<StringType>>(loop, this),
_text(std::move(text)) {}
EventHandlingResult handle_v(SurfaceResizeEvent resize) override {
refresh();
return EventHandlingResult::DONE;
}
EventHandlingResult handle(TextUpdateEvent<StringType> event) {
_text = std::move(event.new_text);
refresh();
return EventHandlingResult::DONE;
}
void refresh() {
this->_owner->clear();
size_t _max_col = this->_owner->get_width() / 8;
size_t _max_row = this->_owner->get_height() / 16;
int col = 0, row = 0;
for (char c: _text) {
if (c == '\n' || col >= _max_col) {
row++;
col = 0;
if (c == '\n')
continue;
}
if (row >= _max_row) {
break;
}
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 16; y++) {
bool color = fonts_Terminess_Powerline[c][y] & (1 << (8 - x));
this->_owner->draw_pixel(col * 8 + x, row * 16 + y, PixelType(color));
}
}
col++;
}
}
private:
StringType _text;
};
#endif // TEXTWINDOW_HPP

View File

@@ -0,0 +1,40 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef WINDOW_HPP
#define WINDOW_HPP
#include <type_traits>
#include "Event.hpp"
#include "Pixel.hpp"
#include "StandardEvents.hpp"
#include "utils.hpp"
template<typename Derived, typename PixelType>
requires std::is_base_of_v<Pixel, PixelType>
class Surface;
template<typename SurfaceType>
class Window : StandardEventHandler<Window<SurfaceType>> {
public:
using PixelType = typename SurfaceType::PixelType;
explicit Window(SurfaceType* owner) : _owner(owner) {
// static_assert(is_specialization_of<Surface, SurfaceType>::value);
}
virtual ~Window() = default;
EventHandlingResult handle(auto Event) { return handle_v(Event); }
virtual EventHandlingResult handle_v(KeyboardEvent) { return EventHandlingResult::CONTINUE; }
virtual EventHandlingResult handle_v(SurfaceEvent) { return EventHandlingResult::CONTINUE; }
virtual EventHandlingResult handle_v(SurfaceResizeEvent) { return EventHandlingResult::CONTINUE; }
protected:
SurfaceType* _owner = nullptr;
};
#endif // SURFACE_HPP

View File

@@ -0,0 +1,14 @@
//
// Created by Stepan Usatiuk on 27.07.2025.
//
#ifndef UTILS_HPP
#define UTILS_HPP
template <template <typename...> class T, typename U>
struct is_specialization_of: std::false_type {};
template <template <typename...> class T, typename... Us>
struct is_specialization_of<T, T<Us...>>: std::true_type {};
#endif //UTILS_HPP

View File

@@ -0,0 +1,5 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "Event.hpp"

View File

@@ -0,0 +1,8 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "TextWindow.hpp"
#include "Fonts.hpp"
#include "Surface.hpp"

View File

@@ -0,0 +1,5 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "Window.hpp"

View File

@@ -0,0 +1,23 @@
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
include(GoogleTest)
add_executable(
EventTests
src/EventTests.cpp
)
target_link_libraries(
EventTests PRIVATE
GTest::gtest_main cbsdk
)
gtest_discover_tests(EventTests DISCOVERY_TIMEOUT 600)

View File

@@ -0,0 +1,44 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include <gtest/gtest.h>
#include "Event.hpp"
struct EventOne : public Event {
std::string name;
};
struct EventTwo : public Event {
int value;
};
template<typename Derived>
using TestEventHandler = EventHandler<Derived, EventOne, EventTwo>;
class EventHandlerTest : public TestEventHandler<EventHandlerTest> {
public:
template<typename T>
void handle(const T& event) {
seen_unknown = true;
}
void handle(const EventOne& event) {
seen_event_one = true;
}
bool seen_event_one = false;
bool seen_unknown = false;
};
TEST(Event, Basic) {
EventHandlerTest handler;
EventOne event_one;
EventTwo event_two;
handler.handle(event_one);
ASSERT_TRUE(handler.seen_event_one);
handler.handle(event_two);
ASSERT_TRUE(handler.seen_unknown);
}

View File

@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.10)
include(FetchContent)
FetchContent_Declare(SFML
GIT_REPOSITORY https://github.com/SFML/SFML.git
GIT_TAG 3.0.1
GIT_SHALLOW ON
EXCLUDE_FROM_ALL
SYSTEM)
FetchContent_MakeAvailable(SFML)
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
src/SfmlWindow.cpp
include_public/SfmlWindow.hpp)
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)

View File

@@ -0,0 +1,41 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef SFMLWINDOW_HPP
#define SFMLWINDOW_HPP
#include "Surface.hpp"
#include "Window.hpp"
#include <SFML/Graphics.hpp>
class SfmlSurface : public Surface<SfmlSurface, BwPixel>, public StandardEventQueue<SfmlSurface> {
public:
using PixelType = BwPixel;
SfmlSurface(EventLoop* loop);
~SfmlSurface(); // override;
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
unsigned get_width_impl() const;
unsigned get_height_impl() const;
template<typename T>
EventHandlingResult handle(const T& event) {
return _window->handle(event);
}
EventHandlingResult handle(SurfaceResizeEvent event);
sf::RenderWindow _sf_window;
sf::Image _image;
sf::Texture _texture;
sf::Sprite _sprite;
};
#endif // SFMLWINDOW_HPP

View File

@@ -0,0 +1,37 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "SfmlWindow.hpp"
void SfmlSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
_image.setPixel({x, y}, pixel.on ? sf::Color::Black : sf::Color::White);
}
unsigned SfmlSurface::get_width_impl() const { return _image.getSize().x; }
unsigned SfmlSurface::get_height_impl() const { return _image.getSize().y; }
EventHandlingResult SfmlSurface::handle(SurfaceResizeEvent event) {
_sf_window.clear();
_image.resize({event.width, event.height});
_texture.resize({event.width, event.height});
_texture.update(_image);
_sprite = sf::Sprite(_texture);
sf::FloatRect view({0, 0}, {static_cast<float>(event.width), static_cast<float>(event.height)});
_sf_window.setView(sf::View(view));
return _window->handle(event);
}
SfmlSurface::SfmlSurface(EventLoop* loop) :
Surface<SfmlSurface, BwPixel>(),
EventQueue<SfmlSurface, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>(loop, this),
_sf_window(sf::VideoMode({640, 480}), "Test"), _image({640, 480}, sf::Color::White), _texture(_image),
_sprite(_texture) {
_sf_window.setFramerateLimit(60);
_sf_window.clear();
_sf_window.draw(_sprite);
_sf_window.display();
}
SfmlSurface::~SfmlSurface() {}

View File

@@ -0,0 +1,75 @@
#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};
SfmlSurface* surface_ptr;
int i = 0;
std::thread loop_thread{[&] {
SfmlSurface surface(&loop);
surface_ptr = &surface;
barrier.count_down();
surface.set_window<GridWindow<SfmlSurface, 2, 2>>();
GridWindow<SfmlSurface, 2, 2>* window =
static_cast<GridWindow<SfmlSurface, 2, 2>*>(surface.get_window());
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(0, 0, &loop, "hello");
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(0, 1, &loop, "hello1");
window->set_window<GridWindow<SubSurface<SfmlSurface>, 2, 2>>(1, 0);
GridWindow<SubSurface<SfmlSurface>, 2, 2>* window2 =
static_cast<GridWindow<SubSurface<SfmlSurface>, 2, 2>*>(
window->get_subsurface(1, 0).get_window());
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(1, 1, &loop, "hello3");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
0, 0, &loop, "hello2");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
0, 1, &loop, "hello4");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
1, 0, &loop, "hello5");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, 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<SfmlSurface>, 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();
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff