From 55516668652fb325f9734fe0a3bb06e8163266ad Mon Sep 17 00:00:00 2001 From: andy Date: Sat, 5 Sep 2026 01:31:47 +0100 Subject: [PATCH] Add Battery Health monitor app Live fuel-gauge readings (voltage, current, temperature, remaining/full/design capacity, health, wear) with a rolling voltage graph. Read-only; the only app that also runs on stock firmware. --- battery_health/application.fam | 14 ++++ battery_health/battery_health.c | 129 ++++++++++++++++++++++++++++++++ battery_health/icon.png | Bin 0 -> 95 bytes build.sh | 2 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 battery_health/application.fam create mode 100644 battery_health/battery_health.c create mode 100644 battery_health/icon.png diff --git a/battery_health/application.fam b/battery_health/application.fam new file mode 100644 index 0000000..e4989c3 --- /dev/null +++ b/battery_health/application.fam @@ -0,0 +1,14 @@ +App( + appid="battery_health", + name="Battery Health", + apptype=FlipperAppType.EXTERNAL, + entry_point="battery_health_app", + requires=["gui"], + stack_size=2 * 1024, + fap_category="Tools", + fap_icon="icon.png", + fap_version="1.0", + fap_description="Live battery gauge readings, health and wear, with a voltage graph", + fap_author="andydixon", + fap_weburl="https://github.com/andydixon/flipper-bt-inspector", +) diff --git a/battery_health/battery_health.c b/battery_health/battery_health.c new file mode 100644 index 0000000..80de31c --- /dev/null +++ b/battery_health/battery_health.c @@ -0,0 +1,129 @@ +// Battery Health: live fuel-gauge readings (voltage, current, temperature, +// capacity, health, charge state) with a rolling voltage graph. Read-only. +#include +#include +#include +#include +#include +#include + +#define GRAPH_N 96 // one sample column per ~2 s across the screen width + +typedef struct { + float v, i, temp, vbus; + uint32_t rem_mah, full_mah, design_mah; + uint8_t pct, health; + bool charging, charged; + float graph[GRAPH_N]; + int graph_count; +} Model; + +typedef struct { + Gui* gui; + ViewPort* vp; + FuriMutex* mutex; + FuriMessageQueue* input; + Model m; + bool running; +} App; + +static void sample(App* app) { + Model* m = &app->m; + furi_mutex_acquire(app->mutex, FuriWaitForever); + m->v = furi_hal_power_get_battery_voltage(FuriHalPowerICFuelGauge); + m->i = furi_hal_power_get_battery_current(FuriHalPowerICFuelGauge); + m->temp = furi_hal_power_get_battery_temperature(FuriHalPowerICFuelGauge); + m->vbus = furi_hal_power_get_usb_voltage(); + m->rem_mah = furi_hal_power_get_battery_remaining_capacity(); + m->full_mah = furi_hal_power_get_battery_full_capacity(); + m->design_mah = furi_hal_power_get_battery_design_capacity(); + m->pct = furi_hal_power_get_pct(); + m->health = furi_hal_power_get_bat_health_pct(); + m->charging = furi_hal_power_is_charging(); + m->charged = furi_hal_power_is_charging_done(); + if(m->graph_count < GRAPH_N) { + m->graph[m->graph_count++] = m->v; + } else { + memmove(m->graph, m->graph + 1, sizeof(float) * (GRAPH_N - 1)); + m->graph[GRAPH_N - 1] = m->v; + } + furi_mutex_release(app->mutex); +} + +static void draw_cb(Canvas* c, void* ctx) { + App* app = ctx; + furi_mutex_acquire(app->mutex, FuriWaitForever); + Model* m = &app->m; + char buf[64]; + canvas_clear(c); + canvas_set_font(c, FontPrimary); + snprintf(buf, sizeof(buf), "Battery %u%%", m->pct); + canvas_draw_str(c, 2, 9, buf); + const char* st = m->charged ? "full" : m->charging ? "charging" : "discharging"; + canvas_set_font(c, FontSecondary); + canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, st); + canvas_draw_line(c, 0, 11, 127, 11); + + snprintf(buf, sizeof(buf), "%.3f V %+.0f mA %.1f C", (double)m->v, (double)(m->i * 1000), (double)m->temp); + canvas_draw_str(c, 2, 21, buf); + snprintf( + buf, sizeof(buf), "%lu/%lu mAh (design %lu)", (unsigned long)m->rem_mah, + (unsigned long)m->full_mah, (unsigned long)m->design_mah); + canvas_draw_str(c, 2, 31, buf); + int wear = m->design_mah ? 100 - (int)(m->full_mah * 100 / m->design_mah) : 0; + snprintf(buf, sizeof(buf), "Health %u%% Wear %d%% VBUS %.2fV", m->health, wear, (double)m->vbus); + canvas_draw_str(c, 2, 41, buf); + + // Voltage graph, auto-scaled to 3.3-4.2 V window. + int gx = 2, gy = 45, gw = GRAPH_N, gh = 17; + canvas_draw_frame(c, gx, gy, gw + 2, gh + 2); + for(int k = 0; k < m->graph_count; k++) { + float v = m->graph[k]; + if(v < 3.3f) v = 3.3f; + if(v > 4.2f) v = 4.2f; + int h = (int)((v - 3.3f) / 0.9f * gh); + canvas_draw_line(c, gx + 1 + k, gy + 1 + gh - h, gx + 1 + k, gy + 1 + gh); + } + furi_mutex_release(app->mutex); +} + +static void input_cb(InputEvent* e, void* ctx) { + App* app = ctx; + furi_message_queue_put(app->input, e, FuriWaitForever); +} + +int32_t battery_health_app(void* p) { + UNUSED(p); + App* app = malloc(sizeof(App)); + memset(app, 0, sizeof(App)); + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + app->input = furi_message_queue_alloc(8, sizeof(InputEvent)); + app->gui = furi_record_open(RECORD_GUI); + app->vp = view_port_alloc(); + view_port_draw_callback_set(app->vp, draw_cb, app); + view_port_input_callback_set(app->vp, input_cb, app); + gui_add_view_port(app->gui, app->vp, GuiLayerFullscreen); + + app->running = true; + sample(app); + uint32_t last = furi_get_tick(); + while(app->running) { + InputEvent e; + if(furi_message_queue_get(app->input, &e, 200) == FuriStatusOk) { + if(e.type == InputTypeShort && e.key == InputKeyBack) app->running = false; + } + if(furi_get_tick() - last >= furi_ms_to_ticks(2000)) { + sample(app); + last = furi_get_tick(); + view_port_update(app->vp); + } + } + + gui_remove_view_port(app->gui, app->vp); + view_port_free(app->vp); + furi_record_close(RECORD_GUI); + furi_message_queue_free(app->input); + furi_mutex_free(app->mutex); + free(app); + return 0; +} diff --git a/battery_health/icon.png b/battery_health/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6f2fbc8f428fe0b6c579297e80bee719714064 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2VkYHF5IUx~9sd>6MhE&{2{=v_}^Z%g=GjkK? rvI8nYO`T0s4qY)y2>mVZse2`cq5H7+*7zpl8jwy;S3j3^P6