Your first feed#
In this tutorial you will collect battery data with battfeed, look at exactly what lands on disk, and validate it against the BDF reference implementation. No hardware needed: the built-in simulator behaves like a small coin cell under constant-current discharge.
You need Python 3.10 or newer and about ten minutes.
1. Install battfeed#
pip install battfeed
$ battfeed --version
battfeed 0.5.0
2. See what you can collect from#
$ battfeed sources
android Read an Android device battery through ``adb shell`` (dumpsys + sysfs). [unavailable: requires the Android platform-tools "adb" executable on PATH]
options: serial=None, adb_path='adb', use_sysfs=True, backend=None
csvtail Follow a CSV file that another process is appending to.
options: path, column_map, unit_scale=None, name='csvtail', encoding='utf-8'
dji Import DJI Fly app flight records as per-(pack, flight) BDF feeds.
options: path, ledger_path=None, dji_log_bin=None, api_key=None, include_gps=False
mc3000 Read one bay of a SkyRC MC3000 charger/analyzer. [unavailable: BLE needs pip install "battfeed[mc3000-ble]"; USB needs pip install "battfeed[mc3000-usb]" (transport="mock" needs neither)]
options: slot=0, transport='ble', address=None, time_base='collection'
simulator Simulate a CR2032-ish coin cell under constant-current discharge.
options: name='simulator', steps_to_empty=3600, discharge_current_a=0.002, full_voltage_v=3.0, empty_voltage_v=2.0, ambient_c=25.0
wmi Read the laptop/tablet battery through the Windows ``root\wmi`` classes.
options: name='wmi'
Read this listing carefully, because it teaches battfeed’s manners: sources that cannot run here say so and say why (adb not on PATH; a Bluetooth extra not installed) instead of failing later, and every source advertises its constructor options right where you need them.
3. Collect ten seconds of data#
$ battfeed collect --source simulator --duration 10 --interval 1 --institution LOCAL --cell DemoCell
Collected 10 sample(s) from 'simulator' in 10.0 s -> LOCAL__DemoCell__20260811_001.bdf.csv
Omit --duration and collection runs until Ctrl-C, which stops gracefully and finalises the files — that is the mode real collectors run in.
4. Look at what landed on disk#
Two files appeared, and their names are part of the contract (Institution__Cell__YYYYMMDD_NNN):
$ ls
LOCAL__DemoCell__20260811_001.bdf.csv
LOCAL__DemoCell__20260811_001.meta.json
The data file is a conforming BDF CSV. The required trio test_time_second, voltage_volt, current_ampere leads the header, extra columns follow alphabetically, and the current is negative because the cell is discharging (BDF sign convention: positive current charges):
$ head -4 LOCAL__DemoCell__20260811_001.bdf.csv
test_time_second,voltage_volt,current_ampere,surface_temperature_celsius
0.0,3.0,-0.002,25.0
1.0,3.0,-0.002,25.016664
2.0,3.0,-0.002,25.033309
The sidecar records everything about how the data was collected — the source and its self-description, your run parameters, timestamps, the columns, the row count:
{
"file": "LOCAL__DemoCell__20260811_001.bdf.csv",
"metadata": {
"institution": "LOCAL",
"cell_name": "DemoCell",
"source": {
"source": "simulator",
"kind": "simulated",
"cell": "CR2032-like coin cell (synthetic)",
"chemistry": "Li-MnO2 (synthetic)",
"sign_convention": "positive current charges the cell (BDF)"
},
"requested_duration_second": 10.0,
"requested_interval_second": 1.0
},
"started_at": "2026-08-11T13:08:50+00:00",
"finished_at": "2026-08-11T13:09:00+00:00",
"battfeed_version": "0.5.0",
"columns": ["test_time_second", "voltage_volt", "current_ampere", "surface_temperature_celsius"],
"rows": 10,
"finalized": true
}
That last field matters operationally: the sidecar is written early with "finalized": false and flipped to true only on a clean exit, so downstream tooling can tell a finished capture from a crashed one just by reading it. The unattended-operation guide builds on exactly this.
5. The same thing from Python#
The CLI is a thin wrapper over three objects you can use directly:
from battfeed import BdfCsvSink, Harvester, create_source
harvester = Harvester()
harvester.register(create_source("simulator"))
sink = BdfCsvSink("LOCAL__DemoCell__20260811_002.bdf.csv", metadata={"operator": "me"})
harvester.collect("simulator", duration_s=10, interval_s=1.0, sink=sink)
sink.close() # finalises the CSV and writes the .meta.json sidecar
6. Validate the output#
Don’t take battfeed’s word for it that the file conforms — check it against the BDF reference implementation:
pip install "battfeed[bdf]"
>>> from battfeed.sinks.bdf_csv import validate_file
>>> validate_file("LOCAL__DemoCell__20260811_001.bdf.csv")
{'ok': True, 'missing': [], 'extras': ['surface_temperature_celsius'], ...}
ok: True, nothing missing; the temperature column is a legitimate extra beyond the required trio.
7. Where you are now#
You have produced, understood, and independently validated a BDF capture. From here:
Tail a live instrument log replaces the simulator with a real, growing file.
Write your own source is the tutorial for connecting your own hardware.
If your machine is a Windows laptop, collect its own battery right now — it is one command.