This commit is contained in:
2025-10-08 21:21:33 +02:00
parent ecf6d09651
commit 9a9e25e124
14 changed files with 1996 additions and 1365 deletions

View File

@@ -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
} }

View File

@@ -1,4 +1,5 @@
{ {
"idf.flashType": "JTAG", "idf.flashType": "JTAG",
"idf.port": "/dev/tty.usbmodem12401" "idf.port": "/dev/tty.usbmodem12401",
"C_Cpp.intelliSenseEngine": "default"
} }

View File

@@ -1,5 +1,9 @@
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/display.cpp src/display.cpp
src/bat_mon.cpp src/bat_mon.cpp
src/spi_global.cpp src/spi_global.cpp

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

@@ -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

File diff suppressed because it is too large Load Diff

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

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