battery percentage

This commit is contained in:
2025-10-25 23:31:11 +02:00
parent 96f5b1f0ee
commit 961da2ba33
7 changed files with 227 additions and 211 deletions

View File

@@ -18,6 +18,7 @@ public:
float get_voltage() const; float get_voltage() const;
float get_charge() const; float get_charge() const;
float get_current() const; float get_current() const;
float get_percentage() const;
void pooler(); // FIXME: void pooler(); // FIXME:
private: private:
@@ -33,6 +34,7 @@ private:
volatile float _voltage; volatile float _voltage;
volatile float _current; volatile float _current;
volatile float _charge; volatile float _charge;
volatile float _percentage;
TaskHandle_t _pooler_task; TaskHandle_t _pooler_task;
}; };

View File

@@ -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 mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.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<float>(reg) / 256.0f; }
constexpr float regToCurrent(uint16_t reg) { constexpr float regToCurrent(uint16_t reg) {
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
} }
@@ -103,6 +105,7 @@ void BatMon::pooler() {
_charge = capToMah(ReadRegister(0x05)); _charge = capToMah(ReadRegister(0x05));
_current = regToCurrent(ReadRegister(0x0B)); _current = regToCurrent(ReadRegister(0x0B));
_voltage = regToVoltage(ReadRegister(0x09)); _voltage = regToVoltage(ReadRegister(0x09));
_percentage = regToPercent(ReadRegister(0x06));
vTaskDelay(pdMS_TO_TICKS(10000)); vTaskDelay(pdMS_TO_TICKS(10000));
if (_voltage < 3.0f) { if (_voltage < 3.0f) {
Shutdowner::get().shutdown(); Shutdowner::get().shutdown();
@@ -113,3 +116,4 @@ void BatMon::pooler() {
float BatMon::get_voltage() const { return _voltage; } float BatMon::get_voltage() const { return _voltage; }
float BatMon::get_charge() const { return _charge; } float BatMon::get_charge() const { return _charge; }
float BatMon::get_current() const { return _current; } float BatMon::get_current() const { return _current; }
float BatMon::get_percentage() const { return _percentage; }

View File

@@ -78,6 +78,7 @@ public:
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); } [[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); } [[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
[[nodiscard]] float current() const override { return BatMon::get().get_current(); } [[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 { class EspRuntime::StorageService final : public cardboy::sdk::IStorage {

View File

@@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cmath>
#include <cstdio> #include <cstdio>
#include <ctime> #include <ctime>
#include <string> #include <string>
@@ -446,6 +447,18 @@ private:
framebuffer.frameReady(); 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<double>(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 scaleTime = 4;
const int scaleSeconds = 2; const int scaleSeconds = 2;
const int scaleSmall = 1; const int scaleSmall = 1;
@@ -531,10 +544,10 @@ private:
} }
} }
const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8; const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8;
const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16; const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16;
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48); const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY); const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY);
char hoursMinutes[6]; char hoursMinutes[6];
std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute); std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute);

View File

@@ -39,6 +39,7 @@ public:
[[nodiscard]] virtual float voltage() const { return 0.0f; } [[nodiscard]] virtual float voltage() const { return 0.0f; }
[[nodiscard]] virtual float charge() const { return 0.0f; } [[nodiscard]] virtual float charge() const { return 0.0f; }
[[nodiscard]] virtual float current() const { return 0.0f; } [[nodiscard]] virtual float current() const { return 0.0f; }
[[nodiscard]] virtual float percentage() const { return 0.0f; }
}; };
class IStorage { class IStorage {

View File

@@ -40,233 +40,232 @@ public:
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor { class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
public: public:
[[nodiscard]] bool hasData() const override { return false; } [[nodiscard]] bool hasData() const override { return false; }
};
class DesktopStorage final : public cardboy::sdk::IStorage { class DesktopStorage final : public cardboy::sdk::IStorage {
public: public:
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override; [[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; void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
private: private:
std::unordered_map<std::string, std::uint32_t> data; std::unordered_map<std::string, std::uint32_t> data;
static std::string composeKey(std::string_view ns, std::string_view key); 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<std::uint32_t> 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<Notification> 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<Notification> 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<cardboy::sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override;
private:
std::mutex mutex;
std::condition_variable cv;
std::deque<cardboy::sdk::AppEvent> 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;
}; };
void workerLoop(); class DesktopRandom final : public cardboy::sdk::IRandom {
void wakeWorker(); public:
void cleanupInactive(); DesktopRandom();
DesktopRuntime& runtime; [[nodiscard]] std::uint32_t nextUint32() override;
cardboy::sdk::IEventBus& eventBus;
std::mutex mutex;
std::condition_variable cv;
std::vector<TimerRecord> timers;
bool stopWorker = false;
std::thread worker;
cardboy::sdk::AppTimerHandle nextHandle = 1;
};
class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider { private:
public: std::mt19937 rng;
DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus); std::uniform_int_distribution<std::uint32_t> dist;
[[nodiscard]] std::unique_ptr<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override;
private:
struct ScopedServices final : cardboy::sdk::AppScopedServices {
std::unique_ptr<DesktopTimerService> ownedTimer;
}; };
DesktopRuntime& runtime; class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
cardboy::sdk::IEventBus& eventBus; public:
}; DesktopHighResClock();
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> { [[nodiscard]] std::uint64_t micros() override;
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
[[nodiscard]] int width_impl() const; private:
[[nodiscard]] int height_impl() const; const std::chrono::steady_clock::time_point start;
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: class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
DesktopRuntime& runtime; public:
}; DesktopFilesystem();
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> { bool mount() override;
public: [[nodiscard]] bool isMounted() const override { return mounted; }
explicit DesktopInput(DesktopRuntime& runtime); [[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
cardboy::sdk::InputState readState_impl(); private:
void handleKey(sf::Keyboard::Key key, bool pressed); std::filesystem::path basePathPath;
bool mounted = false;
};
private: class DesktopNotificationCenter final : public cardboy::sdk::INotificationCenter {
DesktopRuntime& runtime; public:
cardboy::sdk::InputState state{}; void pushNotification(Notification notification) override;
}; [[nodiscard]] std::uint32_t revision() const override;
[[nodiscard]] std::vector<Notification> 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<DesktopClock> { private:
public: static constexpr std::size_t kMaxEntries = 8;
explicit DesktopClock(DesktopRuntime& runtime);
std::uint32_t millis_impl(); mutable std::mutex mutex;
void sleep_ms_impl(std::uint32_t ms); std::vector<Notification> entries;
std::uint64_t nextId = 1;
std::uint32_t revisionCounter = 0;
};
private: class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks {
DesktopRuntime& runtime; public:
const std::chrono::steady_clock::time_point start; explicit DesktopLoopHooks(DesktopRuntime& owner);
};
class DesktopRuntime { void onLoopIteration() override;
public:
DesktopRuntime();
cardboy::sdk::Services& serviceRegistry(); private:
void processEvents(); DesktopRuntime& runtime;
void presentIfNeeded(); };
void sleepFor(std::uint32_t ms);
[[nodiscard]] bool isRunning() const { return running; } class DesktopEventBus final : public cardboy::sdk::IEventBus {
public:
void post(const cardboy::sdk::AppEvent& event) override;
std::optional<cardboy::sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override;
DesktopFramebuffer framebuffer; private:
DesktopInput input; std::mutex mutex;
DesktopClock clock; std::condition_variable cv;
std::deque<cardboy::sdk::AppEvent> queue;
};
private: class DesktopTimerService final : public cardboy::sdk::ITimerService {
friend class DesktopFramebuffer; public:
friend class DesktopInput; DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus);
friend class DesktopClock; ~DesktopTimerService() override;
void setPixel(int x, int y, bool on); cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override;
void clearPixels(bool on); void cancelTimer(cardboy::sdk::AppTimerHandle handle) override;
void cancelAllTimers() override;
sf::RenderWindow window; private:
sf::Texture texture; struct TimerRecord {
sf::Sprite sprite; cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer;
std::vector<std::uint8_t> pixels; std::chrono::steady_clock::time_point due;
bool dirty = true; std::chrono::milliseconds interval{0};
bool running = true; bool repeat = false;
bool clearNextFrame = true; bool active = true;
};
DesktopBuzzer buzzerService; void workerLoop();
DesktopBattery batteryService; void wakeWorker();
DesktopStorage storageService; void cleanupInactive();
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
DesktopAppServiceProvider appServiceProvider;
DesktopNotificationCenter notificationService;
DesktopLoopHooks loopHooksService;
cardboy::sdk::Services services{};
};
struct Backend { DesktopRuntime& runtime;
using Framebuffer = DesktopFramebuffer; cardboy::sdk::IEventBus& eventBus;
using Input = DesktopInput; std::mutex mutex;
using Clock = DesktopClock; std::condition_variable cv;
}; std::vector<TimerRecord> 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<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override;
private:
struct ScopedServices final : cardboy::sdk::AppScopedServices {
std::unique_ptr<DesktopTimerService> ownedTimer;
};
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
};
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
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<DesktopInput> {
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<DesktopClock> {
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<std::uint8_t> 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::desktop
namespace cardboy::backend { namespace cardboy::backend {
using DesktopBackend = desktop::Backend; using DesktopBackend = desktop::Backend;
} // namespace cardboy::backend } // namespace cardboy::backend

View File

@@ -50,16 +50,12 @@ std::string StatusBar::prepareRightText() const {
std::string right; std::string right;
if (_services->battery && _services->battery->hasData()) { if (_services->battery && _services->battery->hasData()) {
const float current = _services->battery->current(); const float current = _services->battery->current();
const float chargeMah = _services->battery->charge(); const float chargeMah = _services->battery->charge();
const float fallbackV = _services->battery->voltage(); const float percentage = _services->battery->percentage();
char buf[64]; char buf[64];
if (std::isfinite(current) && std::isfinite(chargeMah)) { std::snprintf(buf, sizeof(buf), "%.2fmA %.2fmAh %.0f%%", static_cast<double>(current),
std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast<double>(current), static_cast<double>(chargeMah), static_cast<double>(percentage));
static_cast<double>(chargeMah));
} else {
std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast<double>(fallbackV));
}
right.assign(buf); right.assign(buf);
} }