From ea4475c9ad51eb1876a9e832bf535ca77f35a8c8 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 1 Apr 2026 16:45:40 -0400 Subject: [PATCH] client: use anchor-relative offsets for record_time and sample_rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed a 1-byte offset jitter that produced garbage values when the device was set to "faster" (4096 Sa/s) mode. Root cause: 4096 = 0x1000, so the sample_rate bytes in the raw S3 frame are `10 10 00` (DLE-escaped). After DLE unstuffing → `10 00` (2 bytes vs 3 for 1024/2048), making frame C 1 byte shorter and shifting all subsequent field offsets by -1. Fix: locate the stable 10-byte anchor `01 2c 00 00 be 80 00 00 00 00` (max-record-limit constant + first two alarm-level floats) and read: sample_rate = uint16_BE at anchor - 2 record_time = float32_BE at anchor + 10 Offline-validated against all five logged hex dumps (1071 and 1070 byte cfg, record times 3/5/8 s, sample rates 1024 and 4096): all five: correct values with anchor approach. --- minimateplus/client.py | 65 +++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/minimateplus/client.py b/minimateplus/client.py index 3986f41..52614d1 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -584,18 +584,21 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: (frames B+C+D concatenated, echo headers stripped). Confirmed field locations (BE11529 with 3-step read, duplicate detection): - - cfg[89] = setup_name: first long ASCII string in cfg[40:250] - - cfg[64] = record_time: float32 BE (BE11529 shows 3.0 s) ✅ candidate - - cfg[52] = sample_rate: uint16 BE (BE11529 shows 1024 Sa/s) ✅ candidate + - cfg[89] = setup_name: first long ASCII string in cfg[40:250] ✅ + - ANCHOR = b'\\x01\\x2c\\x00\\x00\\xbe\\x80\\x00\\x00\\x00\\x00' in cfg[40:100] ✅ + - anchor - 2 = sample_rate uint16_BE (1024 normal / 2048 fast / 4096 faster) + - anchor + 10 = record_time float32_BE - "Project:" needle → project string - "Client:" needle → client string - "User Name:" needle → operator string - "Seis Loc:" needle → sensor_location string - "Extended Notes" needle → notes string - ⚠️ record_time and sample_rate offsets are confirmed candidates from - diagnostic scan but have NOT yet been validated by changing device settings - and re-reading. Mark as ✅ once validated. + Anchor approach is required because a DLE byte in the sample_rate field + (4096 = 0x1000 → stored as 10 10 00 in raw S3 frame → unstuffed to 10 00, + 1 byte shorter than 04 00 or 08 00) causes frame C to be 1 byte shorter + for "faster" mode, shifting all subsequent offsets by 1. The 10-byte + anchor is stable across all modes. Modifies info.compliance_config in-place. """ @@ -623,16 +626,36 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: except Exception as exc: log.warning("compliance_config: setup_name extraction failed: %s", exc) - # ── Record Time — float32 BE at cfg[64] ────────────────────────────────── - # Diagnostic confirmed: cfg[64] float32_BE = 3.0 on BE11529 (set to 3 s). - # Previous guess of 0x28 was wrong (that offset holds the "(L)" label string). - # ⚠️ Validate by changing device record time and re-reading — mark ✅ once confirmed. - try: - if len(data) > 68: - config.record_time = struct.unpack_from(">f", data, 64)[0] - log.debug("compliance_config: record_time = %.3f s", config.record_time) - except Exception as exc: - log.warning("compliance_config: record_time extraction failed: %s", exc) + # ── Record time + sample rate — anchor-relative ─────────────────────────── + # The 10-byte anchor sits between sample_rate and record_time in the cfg. + # Absolute offsets are NOT reliable because sample_rate = 4096 (0x1000) is + # DLE-escaped in the raw S3 frame (10 10 00 → 10 00 after unstuffing), + # making frame C 1 byte shorter than for 1024/2048 and shifting everything. + # sample_rate: uint16_BE at anchor - 2 + # record_time: float32_BE at anchor + 10 + _ANCHOR = b'\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00' + _anchor = data.find(_ANCHOR, 40, 100) + if _anchor >= 2 and _anchor + 14 <= len(data): + try: + config.sample_rate = struct.unpack_from(">H", data, _anchor - 2)[0] + log.debug( + "compliance_config: sample_rate = %d Sa/s (anchor@%d)", config.sample_rate, _anchor + ) + except Exception as exc: + log.warning("compliance_config: sample_rate extraction failed: %s", exc) + try: + config.record_time = struct.unpack_from(">f", data, _anchor + 10)[0] + log.debug( + "compliance_config: record_time = %.3f s (anchor@%d)", config.record_time, _anchor + ) + except Exception as exc: + log.warning("compliance_config: record_time extraction failed: %s", exc) + else: + log.warning( + "compliance_config: anchor %s not found in cfg[40:100] (len=%d) " + "— sample_rate and record_time will be None", + _ANCHOR.hex(), len(data), + ) # ── Project strings ─────────────────────────────────────────────────────── try: @@ -664,16 +687,6 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: except Exception as exc: log.warning("compliance_config: project string extraction failed: %s", exc) - # ── Sample Rate — uint16 BE at cfg[52] ─────────────────────────────────── - # Diagnostic confirmed: cfg[52] uint16_BE = 1024 on BE11529 (standard mode). - # ⚠️ Validate by changing device sample rate and re-reading — mark ✅ once confirmed. - try: - if len(data) > 54: - config.sample_rate = struct.unpack_from(">H", data, 52)[0] - log.debug("compliance_config: sample_rate = %d Sa/s", config.sample_rate) - except Exception as exc: - log.warning("compliance_config: sample_rate extraction failed: %s", exc) - info.compliance_config = config