mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
239 lines
8.4 KiB
C++
239 lines
8.4 KiB
C++
#include "cardboy/apps/clock_app.hpp"
|
|
|
|
#include "cardboy/apps/menu_app.hpp"
|
|
#include "cardboy/sdk/app_framework.hpp"
|
|
#include "cardboy/sdk/app_system.hpp"
|
|
|
|
#include "cardboy/gfx/font16x8.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <string_view>
|
|
|
|
namespace apps {
|
|
|
|
namespace {
|
|
|
|
using cardboy::sdk::AppContext;
|
|
|
|
constexpr const char* kClockAppName = "Clock";
|
|
|
|
using Framebuffer = typename AppContext::Framebuffer;
|
|
using Clock = typename AppContext::Clock;
|
|
|
|
struct TimeSnapshot {
|
|
bool hasWallTime = false;
|
|
int hour24 = 0;
|
|
int minute = 0;
|
|
int second = 0;
|
|
int year = 0;
|
|
int month = 0;
|
|
int day = 0;
|
|
int weekday = 0;
|
|
std::uint64_t uptimeSeconds = 0;
|
|
};
|
|
|
|
class ClockApp final : public cardboy::sdk::IApp {
|
|
public:
|
|
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
|
|
|
void onStart() override {
|
|
cancelRefreshTimer();
|
|
lastSnapshot = {};
|
|
dirty = true;
|
|
const auto snap = captureTime();
|
|
renderIfNeeded(snap);
|
|
lastSnapshot = snap;
|
|
refreshTimer = context.scheduleRepeatingTimer(200);
|
|
}
|
|
|
|
void onStop() override { cancelRefreshTimer(); }
|
|
|
|
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
|
switch (event.type) {
|
|
case cardboy::sdk::AppEventType::Button:
|
|
handleButtonEvent(event.button);
|
|
break;
|
|
case cardboy::sdk::AppEventType::Timer:
|
|
if (event.timer.handle == refreshTimer)
|
|
updateDisplay();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private:
|
|
AppContext& context;
|
|
Framebuffer& framebuffer;
|
|
Clock& clock;
|
|
|
|
bool use24Hour = true;
|
|
bool dirty = false;
|
|
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
|
|
|
TimeSnapshot lastSnapshot{};
|
|
|
|
void cancelRefreshTimer() {
|
|
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
|
|
context.cancelTimer(refreshTimer);
|
|
refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
|
}
|
|
}
|
|
|
|
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {
|
|
const auto& current = button.current;
|
|
const auto& previous = button.previous;
|
|
|
|
if (current.b && !previous.b) {
|
|
context.requestAppSwitchByName(kMenuAppName);
|
|
return;
|
|
}
|
|
|
|
if (current.select && !previous.select) {
|
|
use24Hour = !use24Hour;
|
|
dirty = true;
|
|
}
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
void updateDisplay() {
|
|
const auto snap = captureTime();
|
|
if (!sameSnapshot(snap, lastSnapshot))
|
|
dirty = true;
|
|
renderIfNeeded(snap);
|
|
lastSnapshot = snap;
|
|
}
|
|
|
|
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
|
|
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second;
|
|
}
|
|
|
|
TimeSnapshot captureTime() const {
|
|
TimeSnapshot snap{};
|
|
snap.uptimeSeconds = clock.millis() / 1000ULL;
|
|
|
|
std::time_t raw = 0;
|
|
if (std::time(&raw) != static_cast<std::time_t>(-1) && raw > 0) {
|
|
std::tm tm{};
|
|
if (localtime_r(&raw, &tm) != nullptr) {
|
|
snap.hasWallTime = true;
|
|
snap.hour24 = tm.tm_hour;
|
|
snap.minute = tm.tm_min;
|
|
snap.second = tm.tm_sec;
|
|
snap.year = tm.tm_year + 1900;
|
|
snap.month = tm.tm_mon + 1;
|
|
snap.day = tm.tm_mday;
|
|
snap.weekday = tm.tm_wday;
|
|
return snap;
|
|
}
|
|
}
|
|
|
|
// Fallback to uptime-derived clock
|
|
snap.hasWallTime = false;
|
|
snap.hour24 = static_cast<int>((snap.uptimeSeconds / 3600ULL) % 24ULL);
|
|
snap.minute = static_cast<int>((snap.uptimeSeconds / 60ULL) % 60ULL);
|
|
snap.second = static_cast<int>(snap.uptimeSeconds % 60ULL);
|
|
return snap;
|
|
}
|
|
|
|
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
|
|
const int width = font16x8::measureText(text, scale, letterSpacing);
|
|
const int x = (fb.width() - width) / 2;
|
|
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
|
|
}
|
|
|
|
static std::string formatDate(const TimeSnapshot& snap) {
|
|
static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
|
|
if (!snap.hasWallTime)
|
|
return "UPTIME MODE";
|
|
const char* weekday = (snap.weekday >= 0 && snap.weekday < 7) ? kWeekdays[snap.weekday] : "";
|
|
char buffer[32];
|
|
std::snprintf(buffer, sizeof(buffer), "%s %04d-%02d-%02d", weekday, snap.year, snap.month, snap.day);
|
|
return buffer;
|
|
}
|
|
|
|
void renderIfNeeded(const TimeSnapshot& snap) {
|
|
if (!dirty)
|
|
return;
|
|
dirty = false;
|
|
|
|
framebuffer.frameReady();
|
|
|
|
const int scaleLarge = 3;
|
|
const int scaleSeconds = 2;
|
|
const int scaleSmall = 1;
|
|
|
|
int hourDisplay = snap.hour24;
|
|
bool isPm = false;
|
|
if (!use24Hour) {
|
|
isPm = hourDisplay >= 12;
|
|
int h12 = hourDisplay % 12;
|
|
if (h12 == 0)
|
|
h12 = 12;
|
|
hourDisplay = h12;
|
|
}
|
|
|
|
char mainLine[6];
|
|
std::snprintf(mainLine, sizeof(mainLine), "%02d:%02d", hourDisplay, snap.minute);
|
|
const int mainW = font16x8::measureText(mainLine, scaleLarge, 0);
|
|
const int timeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleLarge) / 2 - 12;
|
|
const int timeX = (framebuffer.width() - mainW) / 2;
|
|
font16x8::drawText(framebuffer, timeX, timeY, mainLine, scaleLarge, true, 0);
|
|
|
|
char secondsLine[3];
|
|
std::snprintf(secondsLine, sizeof(secondsLine), "%02d", snap.second);
|
|
const int secondsX = timeX + mainW + 12;
|
|
const int secondsY = timeY + font16x8::kGlyphHeight * scaleLarge - font16x8::kGlyphHeight * scaleSeconds;
|
|
font16x8::drawText(framebuffer, secondsX, secondsY, secondsLine, scaleSeconds, true, 0);
|
|
|
|
if (!use24Hour) {
|
|
font16x8::drawText(framebuffer, timeX + mainW + 12, timeY, isPm ? "PM" : "AM", scaleSmall, true, 0);
|
|
} else {
|
|
font16x8::drawText(framebuffer, timeX + mainW + 12, timeY, "24H", scaleSmall, true, 0);
|
|
}
|
|
|
|
const std::string dateLine = formatDate(snap);
|
|
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleLarge + 28, dateLine, scaleSmall, 1);
|
|
|
|
if (!snap.hasWallTime) {
|
|
char uptimeLine[32];
|
|
const std::uint64_t days = snap.uptimeSeconds / 86400ULL;
|
|
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
|
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
|
const std::uint64_t secs = snap.uptimeSeconds % 60ULL;
|
|
if (days > 0) {
|
|
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
|
|
static_cast<unsigned long long>(days), static_cast<unsigned long long>(hrs),
|
|
static_cast<unsigned long long>(mins), static_cast<unsigned long long>(secs));
|
|
} else {
|
|
std::snprintf(uptimeLine, sizeof(uptimeLine), "%02llu:%02llu:%02llu UP",
|
|
static_cast<unsigned long long>(hrs), static_cast<unsigned long long>(mins),
|
|
static_cast<unsigned long long>(secs));
|
|
}
|
|
drawCenteredText(framebuffer, framebuffer.height() - 68, uptimeLine, scaleSmall, 1);
|
|
}
|
|
|
|
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
|
|
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
|
|
|
|
framebuffer.sendFrame();
|
|
}
|
|
};
|
|
|
|
class ClockAppFactory final : public cardboy::sdk::IAppFactory {
|
|
public:
|
|
const char* name() const override { return kClockAppName; }
|
|
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
|
return std::make_unique<ClockApp>(context);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
|
|
|
|
} // namespace apps
|