#!/home/noise/.local/share/uv/tools/powermon/bin/python """eg4-snapshot — single-shot register dump for correlation against the EG4 BMS Tool UI. Prints a 47-row table with: reg | raw u16 | hex | our decode | BMS Tool UI field ----+---------+-----+------------------------------+------------------- Run while the BMS Tool is open on the same pack; fill in the BMS Tool column by reading the matching values off the UI. Send the result back and we refine `decode_eg4_modbus_regs` in `bin/eg4-battery`. Usage: eg4-snapshot # auto-detect FTDI, query 0x40 eg4-snapshot [addr] # explicit port / address """ import glob, sys, time, serial PORT = sys.argv[1] if len(sys.argv) > 1 else None ADDR = int(sys.argv[2], 0) if len(sys.argv) > 2 else 0x40 if PORT is None: hits = sorted(glob.glob("/dev/serial/by-id/usb-FTDI_*")) if not hits: sys.exit("no FTDI adapter visible") PORT = hits[0] def crc16(data): c = 0xFFFF for b in data: c ^= b for _ in range(8): c = (c >> 1) ^ 0xA001 if c & 1 else c >> 1 return c def modbus_read_47(addr): body = bytes([addr, 0x03, 0, 0, 0, 47]) cr = crc16(body) return body + bytes([cr & 0xFF, cr >> 8]) def _s16(v): return v - 0x10000 if v & 0x8000 else v # What we THINK each register means right now. Keep in sync with # bin/eg4-battery::decode_eg4_modbus_regs. MAP = [ # (name, decode fn) ("Total_Voltage", lambda v, regs: f"{v/100:.2f} V"), ("Current_I (signed)", lambda v, regs: f"{_s16(v)/100:+.2f} A"), ("Vol_Cell01 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell02 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell03 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell04 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell05 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell06 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell07 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell08 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell09 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell10 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell11 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell12 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell13 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell14 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell15 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Vol_Cell16 (mV)", lambda v, regs: f"{v/1000:.3f} V"), ("Temp_01", lambda v, regs: f"{v} °C"), ("Temp_02", lambda v, regs: f"{v} °C"), ("Temp_03", lambda v, regs: f"{v} °C"), ("Temp_04 (MOS?)", lambda v, regs: f"{v} °C"), ("SOC", lambda v, regs: f"{v} %"), ("SOH", lambda v, regs: f"{v} %"), ("Temp_PCB", lambda v, regs: f"{v} °C"), ("? reserved-25", lambda v, regs: f"{v}"), ("? reserved-26", lambda v, regs: f"{v}"), ("? reserved-27", lambda v, regs: f"{v}"), ("? reserved-28", lambda v, regs: f"{v}"), ("? reserved-29", lambda v, regs: f"{v}"), ("Heater/Status", lambda v, regs: f"{v}"), ("MAX_Curren", lambda v, regs: f"{v/100:.2f} A"), ("? reg-32", lambda v, regs: f"{v} (0x{v:04x})"), ("Warning bits", lambda v, regs: f"0x{v:04x} ({bin(v)[2:].zfill(16)})"), ("Protection bits", lambda v, regs: f"0x{v:04x} ({bin(v)[2:].zfill(16)})"), ("Error_Code", lambda v, regs: f"{v}"), ("Cell_Num", lambda v, regs: f"{v}"), ("Capacity", lambda v, regs: f"{v/10:.1f} Ah"), ("Remaining", lambda v, regs: f"{v/100:.2f} Ah"), ("CycleNum", lambda v, regs: f"{v}"), ("Battery_Mode", lambda v, regs: f"{v}"), ("BMS_Version (hi)", lambda v, regs: f"0x{v:04x}"), ("BMS_Version (lo)", lambda v, regs: f"0x{v:04x}"), ("? reg-43", lambda v, regs: f"{v}"), ("? reg-44", lambda v, regs: f"{v}"), ("? reg-45", lambda v, regs: f"{v}"), ("uptime counter", lambda v, regs: f"{v}"), ] assert len(MAP) == 47, f"MAP has {len(MAP)} entries, expected 47" with serial.Serial(PORT, 9600, bytesize=8, parity="N", stopbits=1, timeout=0.3) as p: p.reset_input_buffer() p.write(modbus_read_47(ADDR)) time.sleep(0.5) buf = p.read(512) if len(buf) < 5 + 94 or buf[0] != ADDR or buf[1] != 0x03: sys.exit(f"bad response ({len(buf)} B): {buf.hex(' ')[:80]}") bc = buf[2] data = buf[3:3 + bc] regs = [(data[i] << 8) | data[i + 1] for i in range(0, bc, 2)] print(f"# eg4-snapshot port={PORT} addr=0x{ADDR:02x} time={time.strftime('%Y-%m-%d %H:%M:%S')}") print() print(f"{'reg':>4} {'raw':>6} {'hex':>7} {'our decode':<34} BMS Tool UI value (fill me in)") print(f"{'---':>4} {'-----':>6} {'-----':>7} {'-'*34} {'-'*40}") for i, (label, fn) in enumerate(MAP): raw = regs[i] try: dec = fn(raw, regs) except Exception as e: dec = f"(decode error: {e})" print(f"{i:>4} {raw:>6} 0x{raw:04x} {label:<34} {dec}")