From 037a13132aabd0befa86732c4c865d9248438322 Mon Sep 17 00:00:00 2001 From: e1z0 <7213361+e1z0@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:54:13 +0300 Subject: [PATCH] Add MAX17048 battery voltage measurement script This script interfaces with the MAX17048 battery fuel gauge to read voltage, state of charge (SOC), and charge rate. It also calculates an estimated time of battery depletion (ETA) based on the current SOC and rate. --- Battery_Voltage_Measure/get_battery.py | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Battery_Voltage_Measure/get_battery.py diff --git a/Battery_Voltage_Measure/get_battery.py b/Battery_Voltage_Measure/get_battery.py new file mode 100644 index 0000000..6f69e41 --- /dev/null +++ b/Battery_Voltage_Measure/get_battery.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import smbus2, time, collections +from datetime import timedelta + +class MAX17048: + def __init__(self, i2c_bus=13, i2c_address=0x36): + self.bus = smbus2.SMBus(i2c_bus) + self.address = i2c_address + + def _read_u16(self, reg): + b = self.bus.read_i2c_block_data(self.address, reg, 2) + return (b[0] << 8) | b[1] + + def read_voltage(self): + # VCELL 0x02–0x03: 78.125 µV/LSB (1.25 mV / 16) + raw = self._read_u16(0x02) + uV = raw * 78.125 + return uV / 1_000_000.0 + + def read_soc(self): + # SOC 0x04–0x05: MSB integer %, LSB in 1/256 % + raw = self._read_u16(0x04) + return ((raw >> 8) & 0xFF) + ((raw & 0xFF) / 256.0) + + def read_rate(self): + # CRATE 0x16–0x17: signed, LSB ≈ 0.208 %/h + raw = self._read_u16(0x16) + if raw & 0x8000: + raw = -((~raw & 0xFFFF) + 1) + return raw * 0.208 + + def close(self): + self.bus.close() + +def fmt_eta(hours): + if hours is None or hours <= 0 or hours > 1e4: + return "ETA: n/a" + t = int(hours * 3600) + h, m = divmod(t // 60, 60) + return f"ETA: {h}h{m:02d}m" + +if __name__ == "__main__": + g = MAX17048() + try: + # Keep last ~120 seconds of (time,soc) for slope-based ETA + hist = collections.deque(maxlen=120) + + while True: + try: + v = g.read_voltage() + soc = g.read_soc() + rate = g.read_rate() # % per hour, signed + + # --- ETA from CRATE --- + eta_h = None + if abs(rate) > 0.05: # ignore tiny noise + if rate < 0: + eta_h = soc / (-rate) # to empty + else: + eta_h = max(0.0, (100.0 - soc) / rate) # to full + + # --- Fallback: slope-based ETA over recent history --- + now = time.time() + hist.append((now, soc)) + if eta_h is None and len(hist) >= 30: # need ~30s of data + t0, s0 = hist[0] + t1, s1 = hist[-1] + dt_h = max((t1 - t0) / 3600.0, 1e-6) + slope = (s1 - s0) / dt_h # % per hour (signed) + if slope < -0.1: + eta_h = soc / (-slope) + elif slope > 0.1: + eta_h = max(0.0, (100.0 - soc) / slope) + + parts = [] + if v is not None: parts.append(f"{v:4.2f} V") + if soc is not None: parts.append(f"{soc:6.2f} %") + if rate is not None:parts.append(f"{rate:+6.2f} %/h") + parts.append(fmt_eta(eta_h)) + print(" | ".join(parts)) + + except OSError as e: + print(f"I2C error: {e}") + + time.sleep(1) + + except KeyboardInterrupt: + pass + finally: + g.close()