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.
This commit is contained in:
2026-09-05 01:31:47 +01:00
parent 20e1a9e921
commit 5551666865
4 changed files with 144 additions and 1 deletions
+14
View File
@@ -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",
)
+129
View File
@@ -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 <furi.h>
#include <furi_hal_power.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <input/input.h>
#include <stdio.h>
#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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

+1 -1
View File
@@ -21,7 +21,7 @@
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APPS=(bt_inspector ble_hid_host ble_sensor ble_tracker ble_gattfuzz ble_beacon ble_gattsrv subghz_scan subghz_spectrum) # app dirs under $HERE; BLE apps staged with libble/ APPS=(bt_inspector ble_hid_host ble_sensor ble_tracker ble_gattfuzz ble_beacon ble_gattsrv subghz_scan subghz_spectrum battery_health) # app dirs under $HERE; BLE apps staged with libble/
APP_SRC="$HERE/bt_inspector" # used only for the BLE_API=min symbol scan APP_SRC="$HERE/bt_inspector" # used only for the BLE_API=min symbol scan
FW_DIR="${FW_DIR:-$HERE/flipperzero-firmware}" FW_DIR="${FW_DIR:-$HERE/flipperzero-firmware}"
FW_TAG="${FW_TAG:-1.4.3}" FW_TAG="${FW_TAG:-1.4.3}"