Language: EN | RU
ToolkitRC battery charger log analyzer and PDF report generator (report example).
Parses the log files written by ToolkitRC chargers and turns them into readable PDF reports: charger settings, per-cycle and whole-test statistics, and plots of various charge/discharge parameters.
Tested on files recorded by a ToolkitRC M8D charger:
- firmware 3.01: old format, one file for the whole test;
- firmware 3.06: new format, one file per charge/discharge cycle.
Requires Python 3.8+.
pip install -e .To install the package itself (e.g. as an entry point in another
project's setup.py):
entry_points={
'console_scripts': [
'toolkitrc-report = toolkitrc_report.cli:cli',
],
},The tool has two input modes, mutually exclusive:
# Old firmware: every cycle of one test is in a single log file.
python -m toolkitrc_report -f path/to/log.xls
# New firmware: one file per cycle, scanned from a directory
# (including its subdirectories — see "Directory layout" below).
python -m toolkitrc_report -d path/to/logs/Or, once installed, via the console script:
toolkitrc-report -d path/to/logs/ -o path/to/reports/Full option list:
-f, --file FILE process a single log file
-d, --dir DIR process a directory with per-cycle log files
-o, --output OUTPUT output directory for PDF reports
(default: next to the input)
-v, --verbose log analysis decisions (-v) and low-level
details (-vv)
--log LOG also write log messages to this file,
in addition to stderr
--strict treat any charger setting difference between
files (CC/CV/DC/DV and others), not just the
test-defining ones, as a test break condition
(only affects -d mode)
-v/-vv are useful whenever a report's grouping, cycle
classification, or "full cycle" flags look surprising — every
non-obvious decision described below is logged at INFO or DEBUG.
matplotlib's own logger is always silenced so -v output stays
readable.
# Single old-format log file, report next to it.
python -m toolkitrc_report -f 2024-09-16_21-23-32.xls
# A directory of new-format per-cycle files, with decision logging.
python -m toolkitrc_report -d ./M8D-log -o ./reports -v
# Same, also saving the full log to a file for later inspection.
python -m toolkitrc_report -d ./M8D-log -o ./reports -vv --log run.log
# Strict mode: any CC/CV/DC/DV difference between files breaks the
# test, instead of only Mode/DMode/Cyc.
python -m toolkitrc_report -d ./M8D-log -o ./reports --strictEach .xls file is a plain TSV-like text file (despite the
extension) with four sections:
==Items==
Type:LiPo Cells:4S Mode:Cycle DMode:Inter CC:500mA CV:4200mV ...
==Data==
time Vin Iin PowerIn Vout Iout PowerCh Capa InTmp ...
0:0:1 19750 145 3 16700 -500 8 1 25 ...
...
==End==
Capacity:2265mAh
IntRes1-8S(mou): 0 0 0 0 ...
==Error==
:Exceed safe capacity!
- Items — charger settings for this file: battery type, cell count, mode, charge/discharge current and cut-off voltage, cycle count, etc.
- Data — one row per logged second (the logging interval can be changed in the charger's settings — use the smallest available interval for the most accurate data): voltages, currents, power, accumulated capacity, temperatures, and per-cell voltages (up to 8S).
- End — written once the file's recording stops; holds the final capacity and per-cell internal resistance.
- Error — written together with
End; either a real error (e.g.Exceed safe capacity!) or a benign completion note (NO ERROR,No need).
Some firmware versions duplicate the End/Error sections; the
parser detects and collapses the duplicate.
| Old (firmware 3.01) | New (firmware 3.03+) | |
|---|---|---|
| Cycles per file | all cycles of a test in one file | each cycle in its own file |
| File naming | free-form (e.g. a timestamp) | Ch{N}_{Type}_{Cells}S_{Pass}.xls |
| Selected via | -f |
-d |
The scanner treats the given directory and every subdirectory that
contains .xls files as an independent scan candidate — logs for
different batteries kept in separate subfolders are picked up
automatically, one level deep. Within each candidate, files sharing
a channel, battery type and cell count are grouped and split into
individual test runs (see Test boundaries below).
Report naming depends on what a candidate directory turns out to contain:
- one test → the report is named after the directory itself;
- several tests with identical settings (a battery re-tested
several times) → named as a test sequence,
<dirname>-01,<dirname>-02, … in pass-number order, starting at 1 regardless of the underlying charger pass numbers; - several tests with different settings → the regular
Ch{N}_{Type}_{Cells}S_{passes}naming is used, prefixed with the directory name for subdirectories (to avoid same-named reports from different subfolders overwriting each other).
Files that don't match the per-cycle naming pattern are treated as
standalone single-file tests, same as -f mode.
The charger has no internal real-time clock (RTC), so every log file is created with the same fixed 1980 timestamp — the file name is the only reliable way to tell what is where. So plan test runs with that in mind. On top of that, whenever the charger reboots, its internal file counter resets to 1 regardless of how many files with the same settings are already on the SD card, and existing files can be silently overwritten.
Warning
Clearing the log folder before you start testing is the single most important step for avoiding confusing or incorrect reports.
A workflow that produces reliable results:
- clear the log folder on the charger's SD card before starting;
- run a test, or a sequence of tests, without power-cycling the charger in between (different channels can run in parallel, since the file name includes the channel number);
- copy the collected log files to your PC into a dedicated folder per battery, using a meaningful directory name — it becomes the report name (see "Directory layout" above);
- run the report generator on that folder and enjoy the detailed report.
One PDF per detected test, named as described above. Structure:
Page 1 — summary
- Title, generation timestamp, and the list of source log files
(compacted, e.g.
CH0_LiPo_4S_{1-3}.xls). - Charger parameters — a table of the
Itemssettings with human-readable names (CC→ Charge current,PeakV→ ΔPeak voltage, etc.). - Test results — a table of capacity, energy, cycle time, and cycle counts, separately for charge and discharge, with median ± spread across cycles; the overall battery efficiency — median discharge/charge energy ratio across adjacent full cycle pairs, shown only when at least one such pair exists; plus the overall test status (see Test status) and the total test time.
- Test summary — a table with one row per cycle: mode, full/not-full flag, duration, start/end voltage, capacity, energy, and per-file status, color-coded green for charge and red for discharge; a final Eff, % column shows each pair's efficiency (see Battery efficiency), with the cell spanning the exact two rows that pair's ratio was computed from.
Page 2 — test summary plots (only when the test has 3 or more working cycles, regardless of full/incomplete status) — charge/ discharge capacity and charge/discharge energy, one point per round trip on a shared "pass number" x-axis, starting from the first round trip; capacity and energy include every round trip regardless of full/not-full status, since those are raw measurements either way. A third plot adds per-pair efficiency (same pairing as the Eff, % column, so it only has a point where both cycles of a round trip are full — other passes are left as a gap on the shared axis); this third plot is included only when at least two such points exist.
One page per working cycle — duration/voltage/capacity/energy table plus voltage & current, power, capacity & energy plots; battery temperature and per-cell voltage plots when the data is present.
Two closing pages — whole-test overview — the same plots over the entire test's normalized timeline (all charger-clock resets and gaps between files collapsed into one continuous scale), with charge/discharge periods shaded, plus charger input voltage/current/ power and temperature.
The report is built from raw samples, so a number of non-obvious
calls have to be made to turn that into "cycle 3 was a full
discharge" or "this directory is one battery tested six times".
Every one of them is logged (-v/-vv). All the "value ± spread"
statistics in the report use the median, not the mean, so a single
anomalous cycle (e.g. from a misdetected full cycle) doesn't skew
the reported value more than it should.
Files sharing a channel/battery/cell-count key are ordered by their
pass number and split into tests. A test is expected to span
2 * Cyc sequentially-numbered files in Cycle mode (one
charge-file and one discharge-file per cycle), or a single file for
any other mode. A new test starts when:
- the previous file's cycle was aborted by a real error (see below),
- the previous file contains an
Endsection (explicit test termination), - the expected file count (
2 * Cyc) has been reached — the next file starts a new run of the same test (a "test sequence"), - the pass numbering breaks (gap, or reset after a charger reboot),
- or the test-defining settings change.
If a test ends with fewer files than expected (without an error or
End), its data is marked incomplete rather than silently treated
as a full test — this catches partially overwritten pass sequences.
"Same settings" only compares the settings that define what the
test is (Mode, DMode, Cyc), value-wise rather than by raw
text — CC/CV/DC/DV are legitimately retuned by the user
between cycles of one test and don't break the grouping by default.
With --strict, any difference in any charger setting — including
CC/CV/DC/DV — breaks the test, and the same stricter
comparison is used when deciding whether a directory's tests share
"identical settings" for test-sequence naming (see "Directory
layout" above).
The raw data stream is first split wherever the charger's internal
clock jumps — a reset to 0:0:0, a negative jump, or a gap far
larger than the normal 1-second logging interval (the charger's
clock is not fully reliable, especially mid-cycle on some channels).
Each resulting segment is then classified:
- segments with a negligible capacity change are idle (rest/waste-timeout periods);
- the rest are working segments, but only if their mean current is
at least 25% of the strongest segment in the same file — during
rest the charger still logs a small residual current and a
creeping capacity counter, so a capacity change alone isn't
enough; a segment that starts exactly at
0:0:0is always counted as working regardless of current, since low-current cycles (long CV taper, deliberately interrupted cycles) are legitimate; - for per-cycle files, consecutive working fragments of the same direction are merged back into one cycle, since a clock glitch can chop a single cycle into several pieces.
A cycle is a candidate "full" charge/discharge if it ends within 2%
of the configured cut-off voltage (DV/CV × cell count) and
starts close enough to the opposite limit — within 10% for charge,
5% for discharge. Discharge uses a tighter start bound because it's
usually the first cycle of a test (with no matching same-test full
charge to have just come from), so a start voltage several percent
off the charge cut-off is a stronger signal of a genuinely partial
cycle than the same relative gap on a charge start.
That condition alone isn't fully reliable — a cycle that happens to touch both voltage limits can still be spurious. For example if it's logged right after an external state reset, or if the battery sat unused for a long time after a partial discharge and its voltage relaxed back toward its nominal value. So when at least three candidates of the same kind exist, any one deviating by more than 25% (duration and/or energy) from the median of the group is demoted back to "not full".
Median/spread statistics on the summary page use only full cycles when at least one exists for that kind, falling back to all cycles otherwise; the spread is omitted when only a single value is available.
The efficiency row pairs up full cycles of opposite kind that are truly adjacent — consecutive cycle numbers, with nothing (full or not) between them — and takes the discharge/charge energy ratio of each pair. This is the round-trip energy efficiency of that specific pair, not a per-cycle value. Cycles are only paired in the test's own direction: the kind of the very first working cycle (full or not) sets that direction for the whole test, so if the test runs discharge-then-charge, a full charge is only paired with the full discharge immediately before it, never with a following discharge. A full cycle whose expected neighbor isn't full, or that's separated from its neighbor by a gap, is left unpaired rather than matched with a more distant one.
The row is shown only when at least one such pair exists, as the median of all pair ratios; the spread is shown only when two or more pairs exist — same convention as every other median/spread statistic in the report.
Derived from whether the last file has an End section, whether a
real error aborted a cycle, and the expected-vs-actual file count:
-
completed— normal end, optionally with; warning: <text>if the firmware logged a non-fatal warning at completion; -
error: <text>— the test was aborted by a real error; -
incomplete (N of M files)— fewer files than the expected2 * Cycwere found, with no error orEndexplaining why; -
incomplete (log truncated)— the last file has noEndsection at all.
An error text only counts as aborting the cycle if the file also ends at a substantial current — the firmware writes warnings (e.g. "Exceed safe capacity!") into files whose cycle still completed its normal taper, so text alone isn't a reliable abort signal.
Power is computed as Vout × Iout rather than taken from the
file's own power column, whose resolution is too coarse for
meaningful integration. Energy is the trapezoidal integral of power
over time. For "whole test" plots, every segment's relative time is
offset onto one continuous scale with each clock reset collapsed to
a single log interval, so multi-file tests plot as one uninterrupted
timeline.
The toolkitrc_report package can be imported directly:
from pathlib import Path
from toolkitrc_report import DirectoryScanner, ReportGenerator
for test in DirectoryScanner(Path('./M8D-log')).scan():
ReportGenerator(test, Path('./reports') / '{}.pdf'.format(
test.title)).build()Public API:
LogFile,LogParseError,ItemParam(single log file parsing);Segment(one charge/discharge/idle interval);BatteryTest(one detected test program);DirectoryScanner(directory scanner);ReportGenerator(report generator);- the
format_durationhelper function.