From 961da2ba3314339d35d31c7f6d7d81c451a3a1a2 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sat, 25 Oct 2025 23:31:11 +0200 Subject: [PATCH] battery percentage --- .../include/cardboy/backend/esp/bat_mon.hpp | 2 + .../components/backend-esp/src/bat_mon.cpp | 4 + .../backend-esp/src/esp_backend.cpp | 1 + .../apps/lockscreen/src/lockscreen_app.cpp | 21 +- .../include/cardboy/sdk/services.hpp | 1 + .../cardboy/backend/desktop_backend.hpp | 395 +++++++++--------- Firmware/sdk/core/src/status_bar.cpp | 14 +- 7 files changed, 227 insertions(+), 211 deletions(-) diff --git a/Firmware/components/backend-esp/include/cardboy/backend/esp/bat_mon.hpp b/Firmware/components/backend-esp/include/cardboy/backend/esp/bat_mon.hpp index 805aed8..0992d7b 100644 --- a/Firmware/components/backend-esp/include/cardboy/backend/esp/bat_mon.hpp +++ b/Firmware/components/backend-esp/include/cardboy/backend/esp/bat_mon.hpp @@ -18,6 +18,7 @@ public: float get_voltage() const; float get_charge() const; float get_current() const; + float get_percentage() const; void pooler(); // FIXME: private: @@ -33,6 +34,7 @@ private: volatile float _voltage; volatile float _current; volatile float _charge; + volatile float _percentage; TaskHandle_t _pooler_task; }; diff --git a/Firmware/components/backend-esp/src/bat_mon.cpp b/Firmware/components/backend-esp/src/bat_mon.cpp index e1ab57c..4e26cb2 100644 --- a/Firmware/components/backend-esp/src/bat_mon.cpp +++ b/Firmware/components/backend-esp/src/bat_mon.cpp @@ -48,6 +48,8 @@ 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; } +// lsb is 1/256% +constexpr float regToPercent(uint16_t reg) { return static_cast(reg) / 256.0f; } constexpr float regToCurrent(uint16_t reg) { return static_cast(static_cast(reg)) * 0.0015625f / RSense; // Convert to mA } @@ -103,6 +105,7 @@ void BatMon::pooler() { _charge = capToMah(ReadRegister(0x05)); _current = regToCurrent(ReadRegister(0x0B)); _voltage = regToVoltage(ReadRegister(0x09)); + _percentage = regToPercent(ReadRegister(0x06)); vTaskDelay(pdMS_TO_TICKS(10000)); if (_voltage < 3.0f) { Shutdowner::get().shutdown(); @@ -113,3 +116,4 @@ void BatMon::pooler() { float BatMon::get_voltage() const { return _voltage; } float BatMon::get_charge() const { return _charge; } float BatMon::get_current() const { return _current; } +float BatMon::get_percentage() const { return _percentage; } diff --git a/Firmware/components/backend-esp/src/esp_backend.cpp b/Firmware/components/backend-esp/src/esp_backend.cpp index 29eb031..d717e11 100644 --- a/Firmware/components/backend-esp/src/esp_backend.cpp +++ b/Firmware/components/backend-esp/src/esp_backend.cpp @@ -78,6 +78,7 @@ public: [[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); } [[nodiscard]] float charge() const override { return BatMon::get().get_charge(); } [[nodiscard]] float current() const override { return BatMon::get().get_current(); } + [[nodiscard]] float percentage() const override { return BatMon::get().get_percentage(); } }; class EspRuntime::StorageService final : public cardboy::sdk::IStorage { diff --git a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp index 95b3afa..a98dfc9 100644 --- a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp +++ b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -446,6 +447,18 @@ private: framebuffer.frameReady(); + if (auto* battery = context.battery(); battery && battery->hasData()) { + const float percentage = battery->percentage(); + if (std::isfinite(percentage) && percentage >= 0.0f) { + char pct[8]; + std::snprintf(pct, sizeof(pct), "%.0f%%", static_cast(percentage)); + const int pctWidth = font16x8::measureText(pct, 1, 1); + const int pctX = framebuffer.width() - pctWidth - 4; + const int pctY = 4; + font16x8::drawText(framebuffer, pctX, pctY, pct, 1, true, 1); + } + } + const int scaleTime = 4; const int scaleSeconds = 2; const int scaleSmall = 1; @@ -531,10 +544,10 @@ private: } } - const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8; - const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16; - const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48); - const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY); + const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8; + const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16; + const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48); + const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY); char hoursMinutes[6]; std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute); diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp index 1f38d96..72ab8cd 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp @@ -39,6 +39,7 @@ public: [[nodiscard]] virtual float voltage() const { return 0.0f; } [[nodiscard]] virtual float charge() const { return 0.0f; } [[nodiscard]] virtual float current() const { return 0.0f; } + [[nodiscard]] virtual float percentage() const { return 0.0f; } }; class IStorage { diff --git a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp index c656ec1..19f8e67 100644 --- a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp +++ b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp @@ -40,233 +40,232 @@ public: class DesktopBattery final : public cardboy::sdk::IBatteryMonitor { public: [[nodiscard]] bool hasData() const override { return false; } -}; -class DesktopStorage final : public cardboy::sdk::IStorage { -public: - [[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override; - void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override; + class DesktopStorage final : public cardboy::sdk::IStorage { + public: + [[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override; + void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override; -private: - std::unordered_map data; + private: + std::unordered_map data; - static std::string composeKey(std::string_view ns, std::string_view key); -}; - -class DesktopRandom final : public cardboy::sdk::IRandom { -public: - DesktopRandom(); - - [[nodiscard]] std::uint32_t nextUint32() override; - -private: - std::mt19937 rng; - std::uniform_int_distribution dist; -}; - -class DesktopHighResClock final : public cardboy::sdk::IHighResClock { -public: - DesktopHighResClock(); - - [[nodiscard]] std::uint64_t micros() override; - -private: - const std::chrono::steady_clock::time_point start; -}; - -class DesktopFilesystem final : public cardboy::sdk::IFilesystem { -public: - DesktopFilesystem(); - - bool mount() override; - [[nodiscard]] bool isMounted() const override { return mounted; } - [[nodiscard]] std::string basePath() const override { return basePathPath.string(); } - -private: - std::filesystem::path basePathPath; - bool mounted = false; -}; - -class DesktopNotificationCenter final : public cardboy::sdk::INotificationCenter { -public: - void pushNotification(Notification notification) override; - [[nodiscard]] std::uint32_t revision() const override; - [[nodiscard]] std::vector recent(std::size_t limit) const override; - void markAllRead() override; - void clear() override; - void removeById(std::uint64_t id) override; - void removeByExternalId(std::uint64_t externalId) override; - -private: - static constexpr std::size_t kMaxEntries = 8; - - mutable std::mutex mutex; - std::vector entries; - std::uint64_t nextId = 1; - std::uint32_t revisionCounter = 0; -}; - -class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks { -public: - explicit DesktopLoopHooks(DesktopRuntime& owner); - - void onLoopIteration() override; - -private: - DesktopRuntime& runtime; -}; - -class DesktopEventBus final : public cardboy::sdk::IEventBus { -public: - void post(const cardboy::sdk::AppEvent& event) override; - std::optional pop(std::optional timeout_ms = std::nullopt) override; - -private: - std::mutex mutex; - std::condition_variable cv; - std::deque queue; -}; - -class DesktopTimerService final : public cardboy::sdk::ITimerService { -public: - DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus); - ~DesktopTimerService() override; - - cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override; - void cancelTimer(cardboy::sdk::AppTimerHandle handle) override; - void cancelAllTimers() override; - -private: - struct TimerRecord { - cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer; - std::chrono::steady_clock::time_point due; - std::chrono::milliseconds interval{0}; - bool repeat = false; - bool active = true; + static std::string composeKey(std::string_view ns, std::string_view key); }; - void workerLoop(); - void wakeWorker(); - void cleanupInactive(); + class DesktopRandom final : public cardboy::sdk::IRandom { + public: + DesktopRandom(); - DesktopRuntime& runtime; - cardboy::sdk::IEventBus& eventBus; - std::mutex mutex; - std::condition_variable cv; - std::vector timers; - bool stopWorker = false; - std::thread worker; - cardboy::sdk::AppTimerHandle nextHandle = 1; -}; + [[nodiscard]] std::uint32_t nextUint32() override; -class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider { -public: - DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus); - - [[nodiscard]] std::unique_ptr - createScopedServices(std::uint64_t generation) override; - -private: - struct ScopedServices final : cardboy::sdk::AppScopedServices { - std::unique_ptr ownedTimer; + private: + std::mt19937 rng; + std::uniform_int_distribution dist; }; - DesktopRuntime& runtime; - cardboy::sdk::IEventBus& eventBus; -}; + class DesktopHighResClock final : public cardboy::sdk::IHighResClock { + public: + DesktopHighResClock(); -class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade { -public: - explicit DesktopFramebuffer(DesktopRuntime& runtime); + [[nodiscard]] std::uint64_t micros() override; - [[nodiscard]] int width_impl() const; - [[nodiscard]] int height_impl() const; - void drawPixel_impl(int x, int y, bool on); - void clear_impl(bool on); - void frameReady_impl(); - void sendFrame_impl(bool clearAfterSend); - [[nodiscard]] bool frameInFlight_impl() const { return false; } + private: + const std::chrono::steady_clock::time_point start; + }; -private: - DesktopRuntime& runtime; -}; + class DesktopFilesystem final : public cardboy::sdk::IFilesystem { + public: + DesktopFilesystem(); -class DesktopInput final : public cardboy::sdk::InputFacade { -public: - explicit DesktopInput(DesktopRuntime& runtime); + bool mount() override; + [[nodiscard]] bool isMounted() const override { return mounted; } + [[nodiscard]] std::string basePath() const override { return basePathPath.string(); } - cardboy::sdk::InputState readState_impl(); - void handleKey(sf::Keyboard::Key key, bool pressed); + private: + std::filesystem::path basePathPath; + bool mounted = false; + }; -private: - DesktopRuntime& runtime; - cardboy::sdk::InputState state{}; -}; + class DesktopNotificationCenter final : public cardboy::sdk::INotificationCenter { + public: + void pushNotification(Notification notification) override; + [[nodiscard]] std::uint32_t revision() const override; + [[nodiscard]] std::vector recent(std::size_t limit) const override; + void markAllRead() override; + void clear() override; + void removeById(std::uint64_t id) override; + void removeByExternalId(std::uint64_t externalId) override; -class DesktopClock final : public cardboy::sdk::ClockFacade { -public: - explicit DesktopClock(DesktopRuntime& runtime); + private: + static constexpr std::size_t kMaxEntries = 8; - std::uint32_t millis_impl(); - void sleep_ms_impl(std::uint32_t ms); + mutable std::mutex mutex; + std::vector entries; + std::uint64_t nextId = 1; + std::uint32_t revisionCounter = 0; + }; -private: - DesktopRuntime& runtime; - const std::chrono::steady_clock::time_point start; -}; + class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks { + public: + explicit DesktopLoopHooks(DesktopRuntime& owner); -class DesktopRuntime { -public: - DesktopRuntime(); + void onLoopIteration() override; - cardboy::sdk::Services& serviceRegistry(); - void processEvents(); - void presentIfNeeded(); - void sleepFor(std::uint32_t ms); + private: + DesktopRuntime& runtime; + }; - [[nodiscard]] bool isRunning() const { return running; } + class DesktopEventBus final : public cardboy::sdk::IEventBus { + public: + void post(const cardboy::sdk::AppEvent& event) override; + std::optional pop(std::optional timeout_ms = std::nullopt) override; - DesktopFramebuffer framebuffer; - DesktopInput input; - DesktopClock clock; + private: + std::mutex mutex; + std::condition_variable cv; + std::deque queue; + }; -private: - friend class DesktopFramebuffer; - friend class DesktopInput; - friend class DesktopClock; + class DesktopTimerService final : public cardboy::sdk::ITimerService { + public: + DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus); + ~DesktopTimerService() override; - void setPixel(int x, int y, bool on); - void clearPixels(bool on); + cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override; + void cancelTimer(cardboy::sdk::AppTimerHandle handle) override; + void cancelAllTimers() override; - sf::RenderWindow window; - sf::Texture texture; - sf::Sprite sprite; - std::vector pixels; - bool dirty = true; - bool running = true; - bool clearNextFrame = true; + private: + struct TimerRecord { + cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer; + std::chrono::steady_clock::time_point due; + std::chrono::milliseconds interval{0}; + bool repeat = false; + bool active = true; + }; - DesktopBuzzer buzzerService; - DesktopBattery batteryService; - DesktopStorage storageService; - DesktopRandom randomService; - DesktopHighResClock highResService; - DesktopFilesystem filesystemService; - DesktopEventBus eventBusService; - DesktopAppServiceProvider appServiceProvider; - DesktopNotificationCenter notificationService; - DesktopLoopHooks loopHooksService; - cardboy::sdk::Services services{}; -}; + void workerLoop(); + void wakeWorker(); + void cleanupInactive(); -struct Backend { - using Framebuffer = DesktopFramebuffer; - using Input = DesktopInput; - using Clock = DesktopClock; -}; + DesktopRuntime& runtime; + cardboy::sdk::IEventBus& eventBus; + std::mutex mutex; + std::condition_variable cv; + std::vector timers; + bool stopWorker = false; + std::thread worker; + cardboy::sdk::AppTimerHandle nextHandle = 1; + }; + + class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider { + public: + DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus); + + [[nodiscard]] std::unique_ptr + createScopedServices(std::uint64_t generation) override; + + private: + struct ScopedServices final : cardboy::sdk::AppScopedServices { + std::unique_ptr ownedTimer; + }; + + DesktopRuntime& runtime; + cardboy::sdk::IEventBus& eventBus; + }; + + class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade { + public: + explicit DesktopFramebuffer(DesktopRuntime& runtime); + + [[nodiscard]] int width_impl() const; + [[nodiscard]] int height_impl() const; + void drawPixel_impl(int x, int y, bool on); + void clear_impl(bool on); + void frameReady_impl(); + void sendFrame_impl(bool clearAfterSend); + [[nodiscard]] bool frameInFlight_impl() const { return false; } + + private: + DesktopRuntime& runtime; + }; + + class DesktopInput final : public cardboy::sdk::InputFacade { + public: + explicit DesktopInput(DesktopRuntime& runtime); + + cardboy::sdk::InputState readState_impl(); + void handleKey(sf::Keyboard::Key key, bool pressed); + + private: + DesktopRuntime& runtime; + cardboy::sdk::InputState state{}; + }; + + class DesktopClock final : public cardboy::sdk::ClockFacade { + public: + explicit DesktopClock(DesktopRuntime& runtime); + + std::uint32_t millis_impl(); + void sleep_ms_impl(std::uint32_t ms); + + private: + DesktopRuntime& runtime; + const std::chrono::steady_clock::time_point start; + }; + + class DesktopRuntime { + public: + DesktopRuntime(); + + cardboy::sdk::Services& serviceRegistry(); + void processEvents(); + void presentIfNeeded(); + void sleepFor(std::uint32_t ms); + + [[nodiscard]] bool isRunning() const { return running; } + + DesktopFramebuffer framebuffer; + DesktopInput input; + DesktopClock clock; + + private: + friend class DesktopFramebuffer; + friend class DesktopInput; + friend class DesktopClock; + + void setPixel(int x, int y, bool on); + void clearPixels(bool on); + + sf::RenderWindow window; + sf::Texture texture; + sf::Sprite sprite; + std::vector pixels; + bool dirty = true; + bool running = true; + bool clearNextFrame = true; + + DesktopBuzzer buzzerService; + DesktopBattery batteryService; + DesktopStorage storageService; + DesktopRandom randomService; + DesktopHighResClock highResService; + DesktopFilesystem filesystemService; + DesktopEventBus eventBusService; + DesktopAppServiceProvider appServiceProvider; + DesktopNotificationCenter notificationService; + DesktopLoopHooks loopHooksService; + cardboy::sdk::Services services{}; + }; + + struct Backend { + using Framebuffer = DesktopFramebuffer; + using Input = DesktopInput; + using Clock = DesktopClock; + }; } // namespace cardboy::backend::desktop namespace cardboy::backend { -using DesktopBackend = desktop::Backend; + using DesktopBackend = desktop::Backend; } // namespace cardboy::backend diff --git a/Firmware/sdk/core/src/status_bar.cpp b/Firmware/sdk/core/src/status_bar.cpp index 97dcc22..101acf5 100644 --- a/Firmware/sdk/core/src/status_bar.cpp +++ b/Firmware/sdk/core/src/status_bar.cpp @@ -50,16 +50,12 @@ std::string StatusBar::prepareRightText() const { std::string right; if (_services->battery && _services->battery->hasData()) { - const float current = _services->battery->current(); - const float chargeMah = _services->battery->charge(); - const float fallbackV = _services->battery->voltage(); + const float current = _services->battery->current(); + const float chargeMah = _services->battery->charge(); + const float percentage = _services->battery->percentage(); char buf[64]; - if (std::isfinite(current) && std::isfinite(chargeMah)) { - std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast(current), - static_cast(chargeMah)); - } else { - std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast(fallbackV)); - } + std::snprintf(buf, sizeof(buf), "%.2fmA %.2fmAh %.0f%%", static_cast(current), + static_cast(chargeMah), static_cast(percentage)); right.assign(buf); }