mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-09-11 18:29:22 -07:00
pm5_battery_test: merge nieldk version and add test scenarios
This commit is contained in:
@@ -1,76 +1,169 @@
|
||||
#!/usr/bin/env -S uv run pm3 -y
|
||||
|
||||
import re
|
||||
import time
|
||||
import csv
|
||||
"""
|
||||
PM5 / BWM battery discharge (or charge) logger.
|
||||
|
||||
Polls `hw status` at a fixed interval and logs the fuel-gauge readings to CSV
|
||||
(plot with pm5_battery_test_graph.py).
|
||||
|
||||
If not yet done, you should set up the gauge first for meaningful numbers:
|
||||
hw bwmsetcap --cap 500 # your cell's mAh
|
||||
then fully charge
|
||||
|
||||
Usage:
|
||||
pm5 --> script run pm5_battery_test.py
|
||||
pm5 --> script run pm5_battery_test.py --cutoff 3200 --interval 10 --scenario idle
|
||||
|
||||
To trigger firmware auto-shutdown: keep the load on and let it run past the
|
||||
firmware floor for the full debounce window, e.g.
|
||||
pm5 --> script run pm5_battery_test.py --cutoff 0 --scenario hf_field_on
|
||||
(do this on battery, not USB - the shutdown bails while USB power is good.)
|
||||
|
||||
NOTE: this deliberately runs a Li-ion cell down under load. Don't leave it
|
||||
unattended, and don't push --cutoff below ~3000 mV.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import time
|
||||
import argparse
|
||||
import pm3
|
||||
|
||||
|
||||
# Limit the number of loops
|
||||
DEBUG_LIMIT = 0
|
||||
# DEBUG_LIMIT = 5
|
||||
scenarios = {
|
||||
'idle': {
|
||||
"on_start": lambda _: None,
|
||||
"in_loop": lambda _, s: time.sleep(s),
|
||||
"on_stop": lambda _: None
|
||||
},
|
||||
'hf_field_on': {
|
||||
"on_start": lambda p: p.console("hf 14a raw -ak 00"),
|
||||
"in_loop": lambda _, s: time.sleep(s),
|
||||
"on_stop": lambda p: p.console("hf 14a read --drop"),
|
||||
},
|
||||
'hf_14a_polling': {
|
||||
"on_start": lambda _: None,
|
||||
# one poll takes about 0.5s when a card is present
|
||||
"in_loop": lambda p, s: p.console(f"hf 14a read -n {2*s}"),
|
||||
"on_stop": lambda _: None,
|
||||
}
|
||||
}
|
||||
|
||||
# To test fast discharge, you can activate the HF field before running this script
|
||||
# hf 14a raw -ak 00
|
||||
|
||||
def parse_args():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="PM5/BWM battery discharge/charge logger.")
|
||||
ap.add_argument("--cutoff", type=float, default=3200.0,
|
||||
help="stop below this battery voltage in mV "
|
||||
"(0 disables the voltage stop). default: 3200")
|
||||
ap.add_argument("--interval", type=float, default=10.0,
|
||||
help="seconds between polls. default: 10")
|
||||
ap.add_argument("--scenario", default="idle", choices=scenarios.keys(),
|
||||
help="scenario to run. default: idle")
|
||||
ap.add_argument("--limit", type=int, default=0,
|
||||
help="stop after N samples (0 = unlimited). default: 0")
|
||||
ap.add_argument("--csv", default=None,
|
||||
help="output CSV path "
|
||||
"(default: battery_log_<timestamp>.csv)")
|
||||
# ignore anything the pm3 wrapper may inject into argv
|
||||
args, _ = ap.parse_known_args()
|
||||
return args
|
||||
|
||||
|
||||
# `hw status` prints one "Label..... <num> <unit>" line per field. Pull the
|
||||
# first number out, tolerating a missing/garbled line (comms hiccup) by
|
||||
# returning None instead of crashing the run.
|
||||
def grab(output, label, unit):
|
||||
for line in output.split("\n"):
|
||||
if label in line:
|
||||
m = re.search(r"([-\d.]+)\s*" + re.escape(unit), line)
|
||||
if m:
|
||||
return float(m.group(1)), line
|
||||
return None, None
|
||||
|
||||
|
||||
def fmt(v):
|
||||
return f"{v:g}" if v is not None else "?"
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
args = parse_args()
|
||||
scenario = scenarios['idle']
|
||||
|
||||
csv_path = args.csv or f"battery_log_{time.strftime('%Y-%m-%d_%H-%M-%S')}.csv"
|
||||
|
||||
p = pm3.pm3()
|
||||
|
||||
voltage = current = temp = capacity = soc = 0
|
||||
stop = False
|
||||
print(f"Logging to {csv_path}")
|
||||
print(f"cutoff={args.cutoff:g} mV interval={args.interval:g}s "
|
||||
f"scenario={args.scenario} limit={args.limit or 'none'}")
|
||||
|
||||
csv_path = f"battery_log_{time.strftime('%Y-%m-%d_%H-%M-%S')}.csv"
|
||||
csv_file = open(csv_path, "w", newline="")
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(["timestamp", "voltage_mV", "current_mA", "temp_C", "capacity_mAh", "soc_percent"])
|
||||
try:
|
||||
# `with` + per-row flush() keeps the CSV valid even if the run is
|
||||
# interrupted or the device drops off mid-log.
|
||||
with open(csv_path, "w", newline="") as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(["timestamp", "elapsed_s", "voltage_mV", "current_mA", "temp_C", "capacity_mAh", "soc_percent"])
|
||||
|
||||
n = 0
|
||||
while DEBUG_LIMIT == 0 or n < DEBUG_LIMIT:
|
||||
p.console("hw status")
|
||||
for line in p.grabbed_output.split('\n'):
|
||||
if "Battery voltage" in line:
|
||||
# print(line)
|
||||
voltage = float(re.search(r"([\d.]+)\s*mV", line).group(1))
|
||||
if voltage < 3200:
|
||||
print(f"Voltage {voltage} mV is below 3200 mV, stopping test.")
|
||||
stop = True
|
||||
if "Battery current" in line:
|
||||
# print(line)
|
||||
current = float(re.search(r"([\d.]+)\s*mA", line).group(1))
|
||||
if "idle" in line:
|
||||
stop = True
|
||||
if "Battery SoC" in line:
|
||||
# print(line)
|
||||
soc = float(re.search(r"([\d.]+)\s*%", line).group(1))
|
||||
if "Temp (gauge)" in line:
|
||||
# print(line)
|
||||
temp = float(re.search(r"([\d.]+)\s*C", line).group(1))
|
||||
if "Remaining capacity" in line:
|
||||
# print(line)
|
||||
capacity = float(re.search(r"([\d.]+)\s*mAh", line).group(1))
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{timestamp}] Voltage: {voltage} mV, Current: {current} mA, "
|
||||
f"Temp: {temp} C, Capacity: {capacity} mAh, SoC: {soc} %")
|
||||
writer.writerow([timestamp, voltage, current, temp, capacity, soc])
|
||||
csv_file.flush()
|
||||
os.fsync(csv_file.fileno())
|
||||
if stop or (DEBUG_LIMIT != 0 and n >= DEBUG_LIMIT):
|
||||
break
|
||||
scenario["on_start"](p)
|
||||
t0 = time.monotonic()
|
||||
n = 0
|
||||
while args.limit == 0 or n < args.limit:
|
||||
p.console("hw status --ms 0")
|
||||
out = p.grabbed_output
|
||||
|
||||
time.sleep(10)
|
||||
# if current > 0:
|
||||
# # Charging
|
||||
# time.sleep(10)
|
||||
# else:
|
||||
# # Discharging
|
||||
# p.console("hf 14a read -n 20")
|
||||
# p.console("hf 14a raw -ak 00")
|
||||
n += 1
|
||||
voltage, _ = grab(out, "Battery voltage", "mV")
|
||||
current, cline = grab(out, "Battery current", "mA")
|
||||
soc, _ = grab(out, "Battery SoC", "%")
|
||||
temp, _ = grab(out, "Temp (gauge)", "C")
|
||||
capacity, _ = grab(out, "Remaining capacity", "mAh")
|
||||
|
||||
csv_file.close()
|
||||
p.console("hf 14a read --drop")
|
||||
elapsed = time.monotonic() - t0
|
||||
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
print(f"[{ts}] +{elapsed:8.1f}s "
|
||||
f"V={fmt(voltage)} mV I={fmt(current)} mA "
|
||||
f"T={fmt(temp)} C Cap={fmt(capacity)} mAh "
|
||||
f"SoC={fmt(soc)} %")
|
||||
|
||||
writer.writerow([
|
||||
ts, f"{elapsed:.1f}",
|
||||
voltage if voltage is not None else "",
|
||||
current if current is not None else "",
|
||||
temp if temp is not None else "",
|
||||
capacity if capacity is not None else "",
|
||||
soc if soc is not None else "",
|
||||
])
|
||||
csv_file.flush()
|
||||
|
||||
os.fsync(csv_file.fileno())
|
||||
|
||||
# --- stop conditions ---
|
||||
if args.cutoff > 0 and voltage is not None and voltage < args.cutoff:
|
||||
print(f"Voltage {voltage:g} mV below cutoff "
|
||||
f"{args.cutoff:g} mV, stopping.")
|
||||
break
|
||||
# gauge marks the current line "idle" when neither charging nor
|
||||
# discharging - nothing more to log
|
||||
if cline is not None and "idle" in cline:
|
||||
print("Gauge current idle, stopping.")
|
||||
break
|
||||
|
||||
scenario["in_loop"](p)
|
||||
n += 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted, stopping.")
|
||||
except Exception as e: # e.g. device powered off / comms dropped mid-run
|
||||
print(f"\nStopped: {e}")
|
||||
finally:
|
||||
try:
|
||||
scenario["on_stop"](p)
|
||||
except Exception as e:
|
||||
print(f"(Error: {e})")
|
||||
print(f"CSV saved: {csv_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -82,7 +82,7 @@ def plot(rows, output_path):
|
||||
|
||||
axes[-1].set_xlabel("Elapsed time (minutes)")
|
||||
fig.suptitle("Battery status over time")
|
||||
fig.tight_layout(rect=[0, 0, 1, 0.97])
|
||||
fig.tight_layout(rect=(0.0, 0.0, 1.0, 0.97))
|
||||
fig.savefig(output_path, dpi=150)
|
||||
print(f"Saved plot to {output_path}")
|
||||
return output_path
|
||||
|
||||
Reference in New Issue
Block a user