From d758825c672703c9e51e3e3dfcfe906604b871e1 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 1 May 2026 20:28:55 -0400 Subject: [PATCH] fix(protocol): correct continuous-mode record header classification for accurate timestamp extraction --- CHANGELOG.md | 21 ++++++++++++++++ CLAUDE.md | 2 +- minimateplus/client.py | 55 +++++++++++++++++++++++++++--------------- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3737924..a72537f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to seismo-relay are documented here. --- +## v0.13.1 — 2026-05-01 + +### Fixed + +- **`_extract_record_type` — Continuous-mode record headers misclassified as Unknown.** + In single-shot mode the 0C waveform record's 9-byte header puts the sub_code + marker `0x10` at byte 1, with the day at byte 0. In Continuous mode the + header is 10 bytes with the marker at byte 0 *and* byte 2, and the day at + byte 1. Previous logic only inspected byte 1 and treated any value other + than `0x10` / `0x03` as `"Unknown"`, which prevented `event.timestamp` from + being populated for any continuous-mode event whose day-of-month wasn't + exactly 3 or 16. As a downstream effect, `blastware_filename()` saw + `event.timestamp == None`, fell back to `stem="0000"` / `ab="00"`, and + produced filenames like `M5290000.000`. Discovered from a live SFM run on + BE11529 in continuous mode (day-of-month = 5). + Now disambiguates by checking BOTH byte 0 and byte 2: if both are `0x10`, + it's the 10-byte continuous header; else if byte 1 is `0x10`, it's the + 9-byte single-shot header. Day-of-month no longer matters. + +--- + ## v0.13.0 — 2026-05-01 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 240e96b..f908461 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem -(Sierra Wireless RV50 / RV55). Current version: **v0.13.0**. +(Sierra Wireless RV50 / RV55). Current version: **v0.13.1**. When new information about the protocol is discovered, please update the instantel_protocol_reference.md with the findings in addition to this document diff --git a/minimateplus/client.py b/minimateplus/client.py index 8f01f3d..12664c0 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -1638,31 +1638,48 @@ def _decode_a5_waveform( def _extract_record_type(data: bytes) -> Optional[str]: """ - Decode the recording mode from byte[1] of the 210-byte waveform record. + Detect the waveform record format by inspecting the first 3 bytes of the + 210-byte record returned by SUB 0C. - Byte[1] is the sub-record code that immediately follows the day byte in the - 9-byte timestamp header at the start of each waveform record: - [day:1] [sub_code:1] [month:1] [year:2 BE] ... + Two formats exist (confirmed from BE11529 captures and CLAUDE.md docs): - Confirmed codes (✅ 2026-04-01): - 0x10 → "Waveform" (continuous / single-shot mode) + Single-shot mode — 9-byte header: + data[0] = day + data[1] = 0x10 ← sub_code marker + data[2] = month + data[3:5] = year (BE) + ... - Histogram mode code is not yet confirmed — a histogram event must be - captured with debug=true to identify it. Returns None for unknown codes. + Continuous mode — 10-byte header: + data[0] = 0x10 ← marker A + data[1] = day ← variable (NOT 0x10) + data[2] = 0x10 ← marker B + data[3] = month + data[4:6] = year (BE) + ... + + Disambiguate by checking BOTH data[0] and data[2]: + - data[0]==0x10 AND data[2]==0x10 → Continuous (10-byte header) + - data[1]==0x10 → Single-shot (9-byte header) + - otherwise → Unknown + + Previous logic only checked data[1] and so mis-classified continuous-mode + records as "Unknown(0xXX)" wherever day != 0x10 — see filename + M5290000.000 regression report (2026-05-01 SFM log). """ - if len(data) < 2: + if len(data) < 3: return None - code = data[1] - if code == 0x10: - return "Waveform" - if code == 0x03: - # Continuous mode waveform record (confirmed by user — NOT a monitor log). - # The byte layout differs from 0x10 single-shot records: the timestamp - # fields decode as garbage under the 0x10 waveform layout. - # TODO: confirm correct timestamp layout for 0x03 records from a known-time event. + # 10-byte continuous format: 0x10 markers at byte 0 AND byte 2 + if data[0] == 0x10 and data[2] == 0x10: return "Waveform (Continuous)" - log.warning("_extract_record_type: unknown sub_code=0x%02X", code) - return f"Unknown(0x{code:02X})" + # 9-byte single-shot format: 0x10 sub_code marker at byte 1 + if data[1] == 0x10: + return "Waveform" + log.warning( + "_extract_record_type: unrecognized header: data[0:3]=%02X %02X %02X", + data[0], data[1], data[2], + ) + return f"Unknown({data[0]:02X}.{data[1]:02X}.{data[2]:02X})" def _extract_peak_floats(data: bytes) -> Optional[PeakValues]: