From b2d10fd6890bd051a057f627ac9f7f2d9edc0f59 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Thu, 2 Apr 2026 01:59:56 -0400 Subject: [PATCH] client: wire trigger_level_geo, alarm_level_geo, max_range_geo from channel block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The channel block is only present in the full ~2126-byte cfg (when frame D delivers correctly rather than duplicating frame B's page). Layout per §7.6: [00 00][max_range f32][00 00][trigger f32]["in.\0"][alarm f32]["/s\0\0"][00 01][label] Relative offsets from the "Tran" label position (label-24/label-18/label-10) are validated by checking the unit strings "in.\0" at label-14 and "/s\0\0" at label-6 before reading the floats. Guard against "Tran2" false-match. When frame D duplicates, cfg is ~1071 bytes and tran_pos search returns a hit without the unit string sentinels — we log the miss and leave fields None. Co-Authored-By: Claude Sonnet 4.6 --- minimateplus/client.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/minimateplus/client.py b/minimateplus/client.py index 8033697..9941bee 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -682,6 +682,42 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: except Exception as exc: log.warning("compliance_config: project string extraction failed: %s", exc) + # ── Channel block: trigger_level_geo, alarm_level_geo, max_range_geo ───── + # The channel block is only present in the full cfg (frame D delivered, + # ~2126 bytes). Per §7.6, each channel record ends with the label string: + # [00 00][max_range f32][00 00][trigger f32]["in.\0"][alarm f32]["/s\0\0"][00 01][label] + # Relative offsets from the "Tran" label position: + # trigger = float32_BE at label - 18 + # alarm = float32_BE at label - 10 + # max_range = float32_BE at label - 24 + # Validated by checking unit strings "in.\0" at label-14 and "/s\0\0" at label-6. + # "Tran2" at a later position won't match because its surrounding bytes differ. + try: + tran_pos = data.find(b"Tran", 1000) + if ( + tran_pos >= 24 + and data[tran_pos + 4 : tran_pos + 5] != b"2" # not "Tran2" + and data[tran_pos - 14 : tran_pos - 10] == b"in.\x00" + and data[tran_pos - 6 : tran_pos - 2 ] == b"/s\x00\x00" + ): + config.trigger_level_geo = struct.unpack_from(">f", data, tran_pos - 18)[0] + config.alarm_level_geo = struct.unpack_from(">f", data, tran_pos - 10)[0] + config.max_range_geo = struct.unpack_from(">f", data, tran_pos - 24)[0] + log.debug( + "compliance_config: trigger=%.4f alarm=%.4f max_range=%.4f in/s", + config.trigger_level_geo, config.alarm_level_geo, config.max_range_geo, + ) + elif tran_pos >= 0: + log.debug( + "compliance_config: 'Tran' at %d but unit strings absent " + "— channel block not yet in cfg (frame D duplicate?)", + tran_pos, + ) + else: + log.debug("compliance_config: channel block not present in cfg (len=%d)", len(data)) + except Exception as exc: + log.warning("compliance_config: channel block extraction failed: %s", exc) + info.compliance_config = config