mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
Compare commits
4 Commits
ecf6d09651
...
13cdcb01dd
| Author | SHA1 | Date | |
|---|---|---|---|
| 13cdcb01dd | |||
| 4c0fd5243f | |||
| 429d704c8c | |||
| 9a9e25e124 |
1
Firmware/.gitignore
vendored
1
Firmware/.gitignore
vendored
@@ -2,3 +2,4 @@ build
|
|||||||
cmake-build*
|
cmake-build*
|
||||||
.idea
|
.idea
|
||||||
.cache
|
.cache
|
||||||
|
managed_components
|
||||||
42
Firmware/.vscode/c_cpp_properties.json
vendored
42
Firmware/.vscode/c_cpp_properties.json
vendored
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "ESP-IDF",
|
"name": "ESP-IDF",
|
||||||
"compilerPath": "${config:idf.toolsPath}/tools/riscv32-esp-elf/esp-14.2.0_20241119/riscv32-esp-elf/bin/riscv32-esp-elf-gcc",
|
"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",
|
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
|
||||||
"includePath": [
|
"includePath": [
|
||||||
"${config:idf.espIdfPath}/components/**",
|
"${config:idf.espIdfPath}/components/**",
|
||||||
"${config:idf.espIdfPathWin}/components/**",
|
"${config:idf.espIdfPathWin}/components/**",
|
||||||
"${workspaceFolder}/**"
|
"${workspaceFolder}/**"
|
||||||
],
|
],
|
||||||
"browse": {
|
"browse": {
|
||||||
"path": [
|
"path": [
|
||||||
"${config:idf.espIdfPath}/components",
|
"${config:idf.espIdfPath}/components",
|
||||||
"${config:idf.espIdfPathWin}/components",
|
"${config:idf.espIdfPathWin}/components",
|
||||||
"${workspaceFolder}"
|
"${workspaceFolder}"
|
||||||
],
|
],
|
||||||
"limitSymbolsToIncludedHeaders": true
|
"limitSymbolsToIncludedHeaders": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": 4
|
"version": 4
|
||||||
}
|
}
|
||||||
|
|||||||
83
Firmware/.vscode/settings.json
vendored
83
Firmware/.vscode/settings.json
vendored
@@ -1,4 +1,85 @@
|
|||||||
{
|
{
|
||||||
"idf.flashType": "JTAG",
|
"idf.flashType": "JTAG",
|
||||||
"idf.port": "/dev/tty.usbmodem12401"
|
"idf.port": "/dev/tty.usbmodem12401",
|
||||||
|
"C_Cpp.intelliSenseEngine": "default",
|
||||||
|
"files.associations": {
|
||||||
|
"bitset": "cpp",
|
||||||
|
"chrono": "cpp",
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"random": "cpp",
|
||||||
|
"fstream": "cpp",
|
||||||
|
"streambuf": "cpp",
|
||||||
|
"regex": "cpp",
|
||||||
|
"*.inc": "cpp",
|
||||||
|
"vector": "cpp",
|
||||||
|
"esp_partition.h": "c",
|
||||||
|
"cstring": "cpp",
|
||||||
|
"array": "cpp",
|
||||||
|
"string_view": "cpp",
|
||||||
|
"any": "cpp",
|
||||||
|
"atomic": "cpp",
|
||||||
|
"barrier": "cpp",
|
||||||
|
"bit": "cpp",
|
||||||
|
"cctype": "cpp",
|
||||||
|
"charconv": "cpp",
|
||||||
|
"clocale": "cpp",
|
||||||
|
"cmath": "cpp",
|
||||||
|
"codecvt": "cpp",
|
||||||
|
"compare": "cpp",
|
||||||
|
"complex": "cpp",
|
||||||
|
"concepts": "cpp",
|
||||||
|
"condition_variable": "cpp",
|
||||||
|
"cstdarg": "cpp",
|
||||||
|
"cstddef": "cpp",
|
||||||
|
"cstdint": "cpp",
|
||||||
|
"cstdio": "cpp",
|
||||||
|
"cstdlib": "cpp",
|
||||||
|
"ctime": "cpp",
|
||||||
|
"cwchar": "cpp",
|
||||||
|
"cwctype": "cpp",
|
||||||
|
"deque": "cpp",
|
||||||
|
"forward_list": "cpp",
|
||||||
|
"list": "cpp",
|
||||||
|
"map": "cpp",
|
||||||
|
"set": "cpp",
|
||||||
|
"string": "cpp",
|
||||||
|
"unordered_map": "cpp",
|
||||||
|
"unordered_set": "cpp",
|
||||||
|
"exception": "cpp",
|
||||||
|
"functional": "cpp",
|
||||||
|
"iterator": "cpp",
|
||||||
|
"memory": "cpp",
|
||||||
|
"memory_resource": "cpp",
|
||||||
|
"netfwd": "cpp",
|
||||||
|
"numeric": "cpp",
|
||||||
|
"optional": "cpp",
|
||||||
|
"ratio": "cpp",
|
||||||
|
"system_error": "cpp",
|
||||||
|
"tuple": "cpp",
|
||||||
|
"type_traits": "cpp",
|
||||||
|
"utility": "cpp",
|
||||||
|
"format": "cpp",
|
||||||
|
"future": "cpp",
|
||||||
|
"initializer_list": "cpp",
|
||||||
|
"iomanip": "cpp",
|
||||||
|
"iosfwd": "cpp",
|
||||||
|
"iostream": "cpp",
|
||||||
|
"istream": "cpp",
|
||||||
|
"latch": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"mutex": "cpp",
|
||||||
|
"new": "cpp",
|
||||||
|
"numbers": "cpp",
|
||||||
|
"ostream": "cpp",
|
||||||
|
"semaphore": "cpp",
|
||||||
|
"span": "cpp",
|
||||||
|
"sstream": "cpp",
|
||||||
|
"stdexcept": "cpp",
|
||||||
|
"stop_token": "cpp",
|
||||||
|
"text_encoding": "cpp",
|
||||||
|
"thread": "cpp",
|
||||||
|
"cinttypes": "cpp",
|
||||||
|
"typeinfo": "cpp",
|
||||||
|
"variant": "cpp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
Firmware/dependencies.lock
Normal file
21
Firmware/dependencies.lock
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
dependencies:
|
||||||
|
idf:
|
||||||
|
source:
|
||||||
|
type: idf
|
||||||
|
version: 5.5.1
|
||||||
|
joltwallet/littlefs:
|
||||||
|
component_hash: 8e12955f47e27e6070b76715a96d6c75fc2b44f069e8c33679332d9bdd3120c4
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.20.1
|
||||||
|
direct_dependencies:
|
||||||
|
- idf
|
||||||
|
- joltwallet/littlefs
|
||||||
|
manifest_hash: 261ed140a57f28f061ce29bcf3ae4833c35c16fa5e15670490bf2aacedefa622
|
||||||
|
target: esp32h2
|
||||||
|
version: 2.0.0
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
idf_component_register(SRCS
|
idf_component_register(SRCS
|
||||||
src/app_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/display.cpp
|
||||||
src/bat_mon.cpp
|
src/bat_mon.cpp
|
||||||
src/spi_global.cpp
|
src/spi_global.cpp
|
||||||
@@ -10,5 +15,9 @@ idf_component_register(SRCS
|
|||||||
src/buttons.cpp
|
src/buttons.cpp
|
||||||
src/power_helper.cpp
|
src/power_helper.cpp
|
||||||
src/buzzer.cpp
|
src/buzzer.cpp
|
||||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash
|
src/fs_helper.cpp
|
||||||
INCLUDE_DIRS "include")
|
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)
|
||||||
17
Firmware/main/idf_component.yml
Normal file
17
Firmware/main/idf_component.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
## IDF Component Manager Manifest File
|
||||||
|
dependencies:
|
||||||
|
## Required IDF version
|
||||||
|
idf:
|
||||||
|
version: '>=4.1.0'
|
||||||
|
# # Put list of dependencies here
|
||||||
|
# # For components maintained by Espressif:
|
||||||
|
# component: "~1.0.0"
|
||||||
|
# # For 3rd party components:
|
||||||
|
# username/component: ">=1.0.0,<2.0.0"
|
||||||
|
# username2/component2:
|
||||||
|
# version: "~1.0.0"
|
||||||
|
# # For transient dependencies `public` flag can be set.
|
||||||
|
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||||
|
# # All dependencies of `main` are public by default.
|
||||||
|
# public: true
|
||||||
|
joltwallet/littlefs: ^1.20
|
||||||
95
Firmware/main/include/app_framework.hpp
Normal file
95
Firmware/main/include/app_framework.hpp
Normal 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;
|
||||||
|
};
|
||||||
33
Firmware/main/include/app_system.hpp
Normal file
33
Firmware/main/include/app_system.hpp
Normal 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);
|
||||||
|
};
|
||||||
12
Firmware/main/include/apps/clock_app.hpp
Normal file
12
Firmware/main/include/apps/clock_app.hpp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_framework.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace apps {
|
||||||
|
|
||||||
|
std::unique_ptr<IAppFactory> createClockAppFactory();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
15
Firmware/main/include/apps/gameboy_app.hpp
Normal file
15
Firmware/main/include/apps/gameboy_app.hpp
Normal 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
|
||||||
15
Firmware/main/include/apps/menu_app.hpp
Normal file
15
Firmware/main/include/apps/menu_app.hpp
Normal 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
|
||||||
11
Firmware/main/include/apps/tetris_app.hpp
Normal file
11
Firmware/main/include/apps/tetris_app.hpp
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_framework.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace apps {
|
||||||
|
|
||||||
|
std::unique_ptr<IAppFactory> createTetrisAppFactory();
|
||||||
|
|
||||||
|
}
|
||||||
61
Firmware/main/include/font16x8.hpp
Normal file
61
Firmware/main/include/font16x8.hpp
Normal 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
|
||||||
26
Firmware/main/include/fs_helper.hpp
Normal file
26
Firmware/main/include/fs_helper.hpp
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <esp_err.h>
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
class FsHelper {
|
||||||
|
public:
|
||||||
|
static FsHelper& get();
|
||||||
|
|
||||||
|
esp_err_t mount();
|
||||||
|
void unmount();
|
||||||
|
|
||||||
|
bool isMounted() const { return mounted; }
|
||||||
|
const char* basePath() const { return kBasePath; }
|
||||||
|
const char* partitionLabel() const { return kPartitionLabel; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
FsHelper() = default;
|
||||||
|
|
||||||
|
static constexpr const char* kBasePath = "/lfs";
|
||||||
|
static constexpr const char* kPartitionLabel = "littlefs";
|
||||||
|
static constexpr const bool kFormatOnFailure = true;
|
||||||
|
|
||||||
|
bool mounted = false;
|
||||||
|
};
|
||||||
7
Firmware/main/roms/README.md
Normal file
7
Firmware/main/roms/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Built-in ROM placeholders
|
||||||
|
|
||||||
|
This directory holds Game Boy ROM images that get embedded into the firmware via `EMBED_FILES`.
|
||||||
|
The repository includes two small placeholder files (`builtin_demo1.gb` and `builtin_demo2.gb`) so
|
||||||
|
that the build system always has something to embed, but they are not valid games. Replace them
|
||||||
|
with legally distributable ROMs to ship useful built-in titles. Filenames are used to derive the
|
||||||
|
save-game slot name.
|
||||||
File diff suppressed because it is too large
Load Diff
97
Firmware/main/src/app_system.cpp
Normal file
97
Firmware/main/src/app_system.cpp
Normal 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);
|
||||||
|
}
|
||||||
217
Firmware/main/src/apps/clock_app.cpp
Normal file
217
Firmware/main/src/apps/clock_app.cpp
Normal 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
|
||||||
1256
Firmware/main/src/apps/gameboy_app.cpp
Normal file
1256
Firmware/main/src/apps/gameboy_app.cpp
Normal file
File diff suppressed because it is too large
Load Diff
183
Firmware/main/src/apps/menu_app.cpp
Normal file
183
Firmware/main/src/apps/menu_app.cpp
Normal 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
|
||||||
1172
Firmware/main/src/apps/tetris_app.cpp
Normal file
1172
Firmware/main/src/apps/tetris_app.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -102,13 +102,13 @@ void SMD::async_draw_start() {
|
|||||||
_tx.tx_buffer = dma_buf;
|
_tx.tx_buffer = dma_buf;
|
||||||
_tx.length = SMD::kLineDataBytes * 8;
|
_tx.length = SMD::kLineDataBytes * 8;
|
||||||
dma_buf[0] = 0b10000000 | (_vcom << 6);
|
dma_buf[0] = 0b10000000 | (_vcom << 6);
|
||||||
_inFlight = true;
|
_inFlight = true;
|
||||||
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SMD::async_draw_wait() {
|
void SMD::async_draw_wait() {
|
||||||
if (uxSemaphoreGetCount(s_clearSem) || !_inFlight) {
|
if (!_inFlight || uxSemaphoreGetCount(s_clearSem)) {
|
||||||
assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight);
|
// assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
|
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
|
||||||
|
|||||||
68
Firmware/main/src/fs_helper.cpp
Normal file
68
Firmware/main/src/fs_helper.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#include "fs_helper.hpp"
|
||||||
|
|
||||||
|
#include <esp_idf_version.h>
|
||||||
|
#include <esp_littlefs.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char* kTag = "FsHelper";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
FsHelper& FsHelper::get() {
|
||||||
|
static FsHelper instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t FsHelper::mount() {
|
||||||
|
if (mounted)
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
esp_vfs_littlefs_conf_t conf{};
|
||||||
|
conf.base_path = kBasePath;
|
||||||
|
conf.partition_label = kPartitionLabel;
|
||||||
|
conf.format_if_mount_failed = kFormatOnFailure;
|
||||||
|
conf.dont_mount = false;
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
conf.read_only = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const esp_err_t err = esp_vfs_littlefs_register(&conf);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
if (err == ESP_ERR_NOT_FOUND) {
|
||||||
|
ESP_LOGE(kTag, "Failed to find LittleFS partition '%s'", kPartitionLabel);
|
||||||
|
} else if (err == ESP_FAIL) {
|
||||||
|
ESP_LOGE(kTag, "Failed to mount LittleFS at %s (consider enabling format)",
|
||||||
|
kBasePath);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(kTag, "esp_vfs_littlefs_register failed: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted = true;
|
||||||
|
|
||||||
|
size_t total = 0;
|
||||||
|
size_t used = 0;
|
||||||
|
const esp_err_t infoErr = esp_littlefs_info(kPartitionLabel, &total, &used);
|
||||||
|
if (infoErr == ESP_OK) {
|
||||||
|
ESP_LOGI(kTag, "LittleFS mounted at %s (%zu / %zu bytes used)", kBasePath, used, total);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(kTag, "LittleFS mounted but failed to query usage: %s", esp_err_to_name(infoErr));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FsHelper::unmount() {
|
||||||
|
if (!mounted)
|
||||||
|
return;
|
||||||
|
const esp_err_t err = esp_vfs_littlefs_unregister(kPartitionLabel);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "Failed to unmount LittleFS (%s)", esp_err_to_name(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mounted = false;
|
||||||
|
ESP_LOGI(kTag, "LittleFS unmounted from %s", kBasePath);
|
||||||
|
}
|
||||||
6
Firmware/partitions.csv
Normal file
6
Firmware/partitions.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xe000, 0x2000,
|
||||||
|
phy_init, data, phy, 0x10000, 0x1000,
|
||||||
|
factory, app, factory, 0x20000, 0x250000,
|
||||||
|
littlefs, data, littlefs,, 0x190000,
|
||||||
|
@@ -599,13 +599,13 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
|
|||||||
#
|
#
|
||||||
# Partition Table
|
# Partition Table
|
||||||
#
|
#
|
||||||
CONFIG_PARTITION_TABLE_SINGLE_APP=y
|
# CONFIG_PARTITION_TABLE_SINGLE_APP is not set
|
||||||
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set
|
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set
|
||||||
# CONFIG_PARTITION_TABLE_TWO_OTA is not set
|
# CONFIG_PARTITION_TABLE_TWO_OTA is not set
|
||||||
# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
|
# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
|
||||||
# CONFIG_PARTITION_TABLE_CUSTOM is not set
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv"
|
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||||
CONFIG_PARTITION_TABLE_OFFSET=0x8000
|
CONFIG_PARTITION_TABLE_OFFSET=0x8000
|
||||||
CONFIG_PARTITION_TABLE_MD5=y
|
CONFIG_PARTITION_TABLE_MD5=y
|
||||||
# end of Partition Table
|
# end of Partition Table
|
||||||
@@ -2409,6 +2409,34 @@ CONFIG_WL_SECTOR_SIZE=4096
|
|||||||
CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16
|
CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16
|
||||||
CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
|
CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
|
||||||
CONFIG_WIFI_PROV_BLE_SEC_CONN=y
|
CONFIG_WIFI_PROV_BLE_SEC_CONN=y
|
||||||
|
|
||||||
|
#
|
||||||
|
# LittleFS
|
||||||
|
#
|
||||||
|
# CONFIG_LITTLEFS_SDMMC_SUPPORT is not set
|
||||||
|
CONFIG_LITTLEFS_MAX_PARTITIONS=3
|
||||||
|
CONFIG_LITTLEFS_PAGE_SIZE=256
|
||||||
|
CONFIG_LITTLEFS_OBJ_NAME_LEN=64
|
||||||
|
CONFIG_LITTLEFS_READ_SIZE=128
|
||||||
|
CONFIG_LITTLEFS_WRITE_SIZE=128
|
||||||
|
CONFIG_LITTLEFS_LOOKAHEAD_SIZE=128
|
||||||
|
CONFIG_LITTLEFS_CACHE_SIZE=512
|
||||||
|
CONFIG_LITTLEFS_BLOCK_CYCLES=512
|
||||||
|
CONFIG_LITTLEFS_USE_MTIME=y
|
||||||
|
# CONFIG_LITTLEFS_USE_ONLY_HASH is not set
|
||||||
|
# CONFIG_LITTLEFS_HUMAN_READABLE is not set
|
||||||
|
CONFIG_LITTLEFS_MTIME_USE_SECONDS=y
|
||||||
|
# CONFIG_LITTLEFS_MTIME_USE_NONCE is not set
|
||||||
|
# CONFIG_LITTLEFS_SPIFFS_COMPAT is not set
|
||||||
|
# CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not set
|
||||||
|
# CONFIG_LITTLEFS_FCNTL_GET_PATH is not set
|
||||||
|
# CONFIG_LITTLEFS_MULTIVERSION is not set
|
||||||
|
# CONFIG_LITTLEFS_MALLOC_STRATEGY_DISABLE is not set
|
||||||
|
CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y
|
||||||
|
# CONFIG_LITTLEFS_MALLOC_STRATEGY_INTERNAL is not set
|
||||||
|
CONFIG_LITTLEFS_ASSERTS=y
|
||||||
|
# CONFIG_LITTLEFS_MMAP_PARTITION is not set
|
||||||
|
# end of LittleFS
|
||||||
# end of Component config
|
# end of Component config
|
||||||
|
|
||||||
# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set
|
# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set
|
||||||
|
|||||||
Reference in New Issue
Block a user