Sample your laptop battery and visualize its charge / discharge curves, dQ/dV differential-capacity curve, and health / wear — the curve families from battery literature (Battery University, BioLogic) — built from your own machine's data.
A single Rust binary with a shared core (sampler + storage + analysis) and three faces: a background logger, an htop-style TUI, and a local web UI with interactive charts.
just build # cargo build
just run-log 30s # background logger -> SQLite (Ctrl-C to stop)
just run-tui # live htop-style monitor (press q / Esc to quit)
just serve 8787 # open http://127.0.0.1:8787 for the analysis charts
just capture # one-shot: capture a session, then print a summary
just paths # show where data is storedThe logger, TUI, and web UI all share the same store, so you can log in one terminal and watch live in another. Run the logger across at least one full charge or discharge cycle before expecting clean V-vs-SoC and dQ/dV curves.
The just recipes are thin wrappers around a single binary, battcurve. You can run it
directly without just.
cargo build --release # build → target/release/battcurve
just install # or: cargo install --path . (→ ~/.cargo/bin/battcurve)
battcurve --help # top-level help; --version for the versionOnce installed (or via ./target/release/battcurve), the subcommands are:
# Background logger — sample forever at an interval; Ctrl-C to stop.
battcurve log [--interval 10s] [--store sqlite|csv] [--battery BAT0]
# One-shot capture — sample until a stop condition, then print a summary.
battcurve capture [--interval 10s] [--store sqlite|csv] [--battery BAT0] \
[--until ctrl-c|full|empty]
# htop-style live terminal monitor (press q / Esc to quit).
battcurve tui [--store sqlite|csv] [--battery BAT0]
# Local web UI with the interactive analysis charts.
battcurve serve [--port 8787] [--store sqlite|csv]
# Print the resolved data-file paths.
battcurve pathsFlags (all optional):
| Flag | Applies to | Default | Meaning |
|---|---|---|---|
--interval |
log, capture |
10s |
Sample period; accepts 500ms, 5s, 1m, 1h, or a bare number (seconds). |
--store |
all but paths |
sqlite |
Storage backend: sqlite (WAL, concurrent) or csv (human-readable). |
--battery |
log, capture, tui |
first BAT* |
Which /sys/class/power_supply device to read. |
--until |
capture |
ctrl-c |
Stop on ctrl-c, when full, or when empty. |
--port |
serve |
8787 |
Port to bind on 127.0.0.1. |
- Charge / discharge over time — SoC %, voltage, and power on a shared time axis.
- Voltage vs State of Charge — the classic discharge plateau / CC–CV charge climb.
- dQ/dV (differential capacity) — peaks mark electrochemical phase transitions; used to track aging. Smoothed with a moving average since it is intrinsically noisy.
- Health summary — full-charge capacity vs design capacity, wear %, and cycle count.
Reads /sys/class/power_supply/BAT* (override with --battery BATx), normalizing both
reporting styles found in the wild:
- energy-based (
energy_now/energy_fullin µWh,voltage_nowin µV) — what this HP laptop uses. - charge-based (
charge_now/charge_fullin µAh,current_nowin µA) — energy and power are derived via voltage.
Some batteries (including the HP this was developed on) expose power_now only
intermittently — or report No such device — and have no current_now at all. But
energy_now updates reliably. Since power is just the signed energy slope, battcurve
derives it from the energy delta over a short trailing window
(analysis::fill_derived_power) wherever a real power reading is missing. Native
power_now readings, when present, are used as-is. The derived power also feeds the
current used for dQ/dV integration (I = power / voltage).
Default backend is SQLite (rusqlite, bundled, WAL mode) at
$XDG_DATA_HOME/battcurve/battcurve.db, so the logger can write while the TUI and web
server read concurrently. Pass --store csv to any command to use a human-readable,
append-only CSV at samples.csv instead. Stored samples are the raw sysfs readings;
power derivation happens at read time, keeping the log a faithful record.
just install-service # installs target/release binary + enables a systemd --user unit
systemctl --user status battcurve-loggerThe unit (systemd/battcurve-logger.service) runs battcurve log --interval 30s and
restarts on failure.
justfile command runner (build / install / test / lint / run-* / serve / install-service)
src/core/ sample, reader (sysfs), storage (csv|sqlite), analysis (sessions, dQ/dV, CC/CV, health)
src/cmd/ log, capture, tui, serve
src/web/ index.html + vendored uplot.min.js / uplot.min.css (inlined when served)
systemd/ user logger unit
just test # unit tests: analysis curves, power derivation, storage round-trips,
# WAL + concurrent reader, reader normalization (energy- and charge-based)
just lint # clippy with -D warnings- The web UI refreshes by polling the JSON API every 5 s (no WebSocket).
- The page is fully self-contained and offline: uPlot's JS/CSS are vendored in
src/web/(uplot.min.js,uplot.min.css) and inlined into the served HTML at compile time, so no CDN or external requests are made. To upgrade uPlot, replace those two files. - Charts follow the OS light/dark theme (canvas axis colors are read from CSS tokens).
- A clean dQ/dV or full discharge curve only appears after a real charge/discharge cycle has been logged.
MIT © 2026 Ye Wang