diff --git a/eg4battery/bin/eg4-battery b/eg4battery/bin/eg4-battery index 292b631..dfbbd67 100755 --- a/eg4battery/bin/eg4-battery +++ b/eg4battery/bin/eg4-battery @@ -433,6 +433,12 @@ class ModbusActivePoller: bytesize=8, parity="N", stopbits=1, ) + # Some pack/FTDI combos (observed on 2026-05-11 batch) emit one or two + # line-idle bytes before the real Modbus frame, e.g. "00 f0 40 03 4e …". + # Tolerate up to this many leading non-frame bytes by sliding the + # window and CRC-checking each candidate offset. + _MAX_LEADING_IDLE = 4 + def _read_block(self, start: int, count: int) -> list[int]: body = bytes([self._address, 0x03, start >> 8, start & 0xFF, count >> 8, count & 0xFF]) @@ -443,7 +449,8 @@ class ModbusActivePoller: self._ser.reset_input_buffer() self._ser.write(frame) - expected = 3 + count * 2 + 2 + # Read enough bytes to also cover any leading idle bytes. + expected = 3 + count * 2 + 2 + self._MAX_LEADING_IDLE buf = bytearray() deadline = time.monotonic() + self._timeout_s while time.monotonic() < deadline and len(buf) < expected: @@ -454,15 +461,21 @@ class ModbusActivePoller: log.debug("pack 0x%02x [%d@%d] tx=%s rx=%s", self._address, count, start, frame.hex(" "), raw.hex(" ")) - if len(raw) < 5 or raw[0] != self._address or raw[1] != 0x03: - raise RuntimeError(f"no/bad response ({len(raw)} B) for read({count}@{start})") - bc = raw[2] - if len(raw) < 3 + bc + 2: - raise RuntimeError(f"truncated response for read({count}@{start})") - if not _crc_ok(raw, 0, 3 + bc + 2): - raise RuntimeError(f"CRC mismatch for read({count}@{start})") - data = raw[3:3 + bc] - return [(data[i] << 8) | data[i + 1] for i in range(0, len(data), 2)] + # Locate the real frame: address byte + fn=0x03 + valid CRC, at + # offset 0..MAX_LEADING_IDLE. Reject any candidate whose CRC fails. + for off in range(min(self._MAX_LEADING_IDLE + 1, max(len(raw) - 4, 0))): + if raw[off] != self._address or raw[off + 1] != 0x03: + continue + bc = raw[off + 2] + frame_end = off + 3 + bc + 2 + if frame_end > len(raw): + continue + if not _crc_ok(raw, off, 3 + bc + 2): + continue + data = raw[off + 3:off + 3 + bc] + return [(data[i] << 8) | data[i + 1] for i in range(0, len(data), 2)] + + raise RuntimeError(f"no valid frame in {len(raw)} bytes for read({count}@{start})") def poll(self) -> list[int]: self._open()