From 5ab8662332639b9c925b286eafebba04ac7e9cb2 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sun, 12 Oct 2025 15:03:34 +0200 Subject: [PATCH] statusbar --- .../include/cardboy/sdk/framebuffer_hooks.hpp | 18 ++++ .../include/cardboy/sdk/platform.hpp | 5 +- Firmware/sdk/core/CMakeLists.txt | 2 + .../core/include/cardboy/sdk/app_system.hpp | 1 + .../include/cardboy/sdk/framebuffer_hooks.hpp | 20 +++++ .../core/include/cardboy/sdk/status_bar.hpp | 83 +++++++++++++++++++ Firmware/sdk/core/src/app_system.cpp | 30 ++++++- Firmware/sdk/core/src/framebuffer_hooks.cpp | 23 +++++ Firmware/sdk/core/src/status_bar.cpp | 79 ++++++++++++++++++ 9 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 Firmware/sdk/backend_interface/include/cardboy/sdk/framebuffer_hooks.hpp create mode 100644 Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp create mode 100644 Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp create mode 100644 Firmware/sdk/core/src/framebuffer_hooks.cpp create mode 100644 Firmware/sdk/core/src/status_bar.cpp diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/framebuffer_hooks.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/framebuffer_hooks.hpp new file mode 100644 index 0000000..6bbec6d --- /dev/null +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/framebuffer_hooks.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace cardboy::sdk { + +class FramebufferHooks { +public: + using PreSendHook = void (*)(void* framebuffer, void* userData); + + static void setPreSendHook(PreSendHook hook, void* userData); + static void clearPreSendHook(); + static void invokePreSend(void* framebuffer); + +private: + static PreSendHook hook_; + static void* userData_; +}; + +} // namespace cardboy::sdk diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp index 6cb9fd9..9002800 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp @@ -1,6 +1,7 @@ #pragma once #include "input_state.hpp" +#include "cardboy/sdk/framebuffer_hooks.hpp" #include #include @@ -67,8 +68,10 @@ public: } __attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) { - if constexpr (detail::HasSendFrameImpl) + if constexpr (detail::HasSendFrameImpl) { + FramebufferHooks::invokePreSend(&impl()); impl().sendFrame_impl(clearDrawBuffer); + } } [[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const { diff --git a/Firmware/sdk/core/CMakeLists.txt b/Firmware/sdk/core/CMakeLists.txt index 68aed36..4b30d08 100644 --- a/Firmware/sdk/core/CMakeLists.txt +++ b/Firmware/sdk/core/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16) add_library(cardboy_sdk STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp ) set_target_properties(cardboy_sdk PROPERTIES diff --git a/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp b/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp index 8dc5e62..796786b 100644 --- a/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp +++ b/Firmware/sdk/core/include/cardboy/sdk/app_system.hpp @@ -12,6 +12,7 @@ namespace cardboy::sdk { class AppSystem { public: explicit AppSystem(AppContext context); + ~AppSystem(); void registerApp(std::unique_ptr factory); bool startApp(const std::string& name); diff --git a/Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp b/Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp new file mode 100644 index 0000000..23301ea --- /dev/null +++ b/Firmware/sdk/core/include/cardboy/sdk/framebuffer_hooks.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace cardboy::sdk { + +class FramebufferHooks { +public: + using PreSendHook = void (*)(void* framebuffer, void* userData); + + static void setPreSendHook(PreSendHook hook, void* userData); + static void clearPreSendHook(); + static void invokePreSend(void* framebuffer); + +private: + static PreSendHook hook_; + static void* userData_; +}; + +} // namespace cardboy::sdk diff --git a/Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp b/Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp new file mode 100644 index 0000000..bd8ff12 --- /dev/null +++ b/Firmware/sdk/core/include/cardboy/sdk/status_bar.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "cardboy/sdk/input_state.hpp" +#include "cardboy/sdk/services.hpp" + +#include "cardboy/gfx/font16x8.hpp" + +#include +#include +#include +#include +#include + +namespace cardboy::sdk { + +class StatusBar { +public: + static StatusBar& instance(); + + void setServices(Services* services) { services_ = services; } + + void setEnabled(bool value); + void toggle(); + [[nodiscard]] bool isEnabled() const { return enabled_; } + + void setCurrentAppName(std::string_view name); + + [[nodiscard]] bool handleToggleInput(const InputState& current, const InputState& previous); + + template + void renderIfEnabled(Framebuffer& fb) { + if (!enabled_) + return; + renderBar(fb); + } + +private: + StatusBar() = default; + + template + void renderBar(Framebuffer& fb) { + const int width = fb.width(); + if (width <= 0) + return; + + const std::string leftText = prepareLeftText(width); + const std::string rightText = prepareRightText(); + + for (int x = 0; x < width; ++x) + fb.drawPixel(x, 0, true); + + const int textY = 1; + const int barHeight = font16x8::kGlyphHeight + 2; + const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1; + if (bottomSeparatorY < barHeight && bottomSeparatorY < fb.height()) { + for (int x = 0; x < width; ++x) + fb.drawPixel(x, bottomSeparatorY, (x % 2) == 0); + } + + const int leftX = 2; + if (!leftText.empty()) + font16x8::drawText(fb, leftX, textY, leftText, 1, true, 1); + + if (!rightText.empty()) { + int rightWidth = font16x8::measureText(rightText, 1, 1); + int rightX = width - rightWidth - 2; + const int minRightX = leftX + font16x8::measureText(leftText, 1, 1) + 6; + if (rightX < minRightX) + rightX = std::max(minRightX, width / 2); + if (rightX < width) + font16x8::drawText(fb, rightX, textY, rightText, 1, true, 1); + } + } + + [[nodiscard]] std::string prepareLeftText(int displayWidth) const; + [[nodiscard]] std::string prepareRightText() const; + + bool enabled_ = false; + Services* services_ = nullptr; + std::string appName_{}; +}; + +} // namespace cardboy::sdk diff --git a/Firmware/sdk/core/src/app_system.cpp b/Firmware/sdk/core/src/app_system.cpp index c0f8a50..e422ca3 100644 --- a/Firmware/sdk/core/src/app_system.cpp +++ b/Firmware/sdk/core/src/app_system.cpp @@ -1,4 +1,6 @@ #include "cardboy/sdk/app_system.hpp" +#include "cardboy/sdk/framebuffer_hooks.hpp" +#include "cardboy/sdk/status_bar.hpp" #include #include @@ -12,9 +14,25 @@ namespace { } constexpr std::uint32_t kIdlePollMs = 16; + +template +void statusBarPreSendHook(void* framebuffer, void* userData) { + auto* fb = static_cast(framebuffer); + auto* status = static_cast(userData); + if (fb && status) + status->renderIfEnabled(*fb); +} } // namespace -AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; } +AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { + context.system = this; + auto& statusBar = StatusBar::instance(); + statusBar.setServices(context.services); + using FBType = typename AppContext::Framebuffer; + FramebufferHooks::setPreSendHook(&statusBarPreSendHook, &statusBar); +} + +AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); } void AppSystem::registerApp(std::unique_ptr factory) { if (!factory) @@ -53,6 +71,8 @@ bool AppSystem::startAppByIndex(std::size_t index) { clearTimersForCurrentApp(); current = std::move(app); lastInputState = context.input.readState(); + StatusBar::instance().setServices(context.services); + StatusBar::instance().setCurrentAppName(activeFactory ? activeFactory->name() : ""); current->onStart(); return true; } @@ -71,8 +91,10 @@ void AppSystem::run() { const std::uint32_t now = context.clock.millis(); processDueTimers(now, events); - const InputState inputNow = context.input.readState(); - if (inputsDiffer(inputNow, lastInputState)) { + const InputState inputNow = context.input.readState(); + const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState); + + if (!consumedByStatusToggle && inputsDiffer(inputNow, lastInputState)) { AppEvent evt{}; evt.type = AppEventType::Button; evt.timestamp_ms = now; @@ -80,6 +102,8 @@ void AppSystem::run() { evt.button.previous = lastInputState; events.push_back(evt); lastInputState = inputNow; + } else if (consumedByStatusToggle) { + lastInputState = inputNow; } for (const auto& evt: events) { diff --git a/Firmware/sdk/core/src/framebuffer_hooks.cpp b/Firmware/sdk/core/src/framebuffer_hooks.cpp new file mode 100644 index 0000000..13d3142 --- /dev/null +++ b/Firmware/sdk/core/src/framebuffer_hooks.cpp @@ -0,0 +1,23 @@ +#include "cardboy/sdk/framebuffer_hooks.hpp" + +namespace cardboy::sdk { + +FramebufferHooks::PreSendHook FramebufferHooks::hook_ = nullptr; +void* FramebufferHooks::userData_ = nullptr; + +void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) { + hook_ = hook; + userData_ = userData; +} + +void FramebufferHooks::clearPreSendHook() { + hook_ = nullptr; + userData_ = nullptr; +} + +void FramebufferHooks::invokePreSend(void* framebuffer) { + if (hook_) + hook_(framebuffer, userData_); +} + +} // namespace cardboy::sdk diff --git a/Firmware/sdk/core/src/status_bar.cpp b/Firmware/sdk/core/src/status_bar.cpp new file mode 100644 index 0000000..e29728a --- /dev/null +++ b/Firmware/sdk/core/src/status_bar.cpp @@ -0,0 +1,79 @@ +#include "cardboy/sdk/status_bar.hpp" + +#include +#include +#include + +namespace cardboy::sdk { + +StatusBar& StatusBar::instance() { + static StatusBar bar; + return bar; +} + +void StatusBar::setEnabled(bool value) { enabled_ = value; } + +void StatusBar::toggle() { + enabled_ = !enabled_; + if (services_ && services_->buzzer) + services_->buzzer->beepMove(); +} + +void StatusBar::setCurrentAppName(std::string_view name) { + appName_.assign(name.begin(), name.end()); + std::transform(appName_.begin(), appName_.end(), appName_.begin(), [](unsigned char ch) { + return static_cast(std::toupper(ch)); + }); +} + +bool StatusBar::handleToggleInput(const InputState& current, const InputState& previous) { + const bool comboNow = current.start && current.select && current.up; + const bool comboPrev = previous.start && previous.select && previous.up; + if (comboNow && !comboPrev) { + toggle(); + return true; + } + return false; +} + +std::string StatusBar::prepareLeftText(int displayWidth) const { + std::string text = appName_.empty() ? std::string("CARDBOY") : appName_; + int maxWidth = std::max(0, displayWidth - 32); + while (!text.empty() && font16x8::measureText(text, 1, 1) > maxWidth) + text.pop_back(); + return text; +} + +std::string StatusBar::prepareRightText() const { + if (!services_) + return {}; + + std::string right; + if (services_->battery && services_->battery->hasData()) { + const float charge = services_->battery->charge(); + char buf[32]; + if (charge > 0.0f && charge <= 1.5f) { + const int pct = std::clamp(static_cast(charge * 100.0f + 0.5f), 0, 100); + std::snprintf(buf, sizeof(buf), "BAT %d%%", pct); + } else { + std::snprintf(buf, sizeof(buf), "BAT %.2fV", static_cast(services_->battery->voltage())); + } + right.assign(buf); + } + + if (services_->powerManager && services_->powerManager->isSlowMode()) { + if (!right.empty()) + right.append(" "); + right.append("SLOW"); + } + + if (services_->buzzer && services_->buzzer->isMuted()) { + if (!right.empty()) + right.append(" "); + right.append("MUTE"); + } + + return right; +} + +} // namespace cardboy::sdk