diff --git a/CLAUDE.md b/CLAUDE.md index eee2bc2..90e4248 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -360,6 +360,7 @@ Do NOT use fixed absolute offsets for sample_rate or record_time. | Field | How to find it | |---|---| +| **recording_mode** | **uint8 at anchor − 3 (write payload) / anchor − 4 (read response)** ✅ confirmed 2026-04-20 | | sample_rate | uint16 BE at anchor − 2 | | record_time | float32 BE at anchor + 10 | | trigger_level_geo | float32 BE, located in channel block | @@ -371,6 +372,18 @@ Do NOT use fixed absolute offsets for sample_rate or record_time. Anchor: `b'\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00'`, search `cfg[0:150]` +**recording_mode enum** (confirmed 2026-04-20 from 4-20-26 captures): + +| Value | Mode | +|---|---| +| `0x00` | Single Shot | +| `0x01` | Continuous | +| `0x02` | ❓ not observed | +| `0x03` | Histogram | +| `0x04` | Histogram + Continuous | + +**Offset note:** The write payload (SUB 71 cfg) has recording_mode 3 bytes before the anchor (`anchor_pos − 3`). The E5 read response has it 4 bytes before (`anchor_pos − 4`), with an extra `0x10` byte sitting between recording_mode and sample_rate in the read format. Use `anchor_pos − 3` when encoding writes; use `anchor_pos − 4` when decoding reads. + ### SUB 0C — Waveform Record (210 bytes = data[11:11+0xD2]) **sub_code=0x10 (Waveform single-shot) — 9-byte timestamp header:** @@ -704,8 +717,8 @@ Fields visible in the Blastware Compliance Setup dialog — most are NOT YET dec offsets in the raw 1A/E5 payload. Only fields with `✅` have confirmed offsets in the code. **Recording Setup tab:** -- Recording Mode: Continuous / Single Shot / Histogram (enum) -- Record Stop Mode: Fixed Record Time / Auto / Manual Stop (enum) +- Recording Mode: Continuous / Single Shot / Histogram / Histogram+Continuous ✅ (uint8 at anchor−3 in write, anchor−4 in read; 0x00=Single Shot, 0x01=Continuous, 0x03=Histogram, 0x04=Histogram+Continuous) — confirmed 2026-04-20 +- Record Stop Mode: Fixed Record Time / Auto / Manual Stop (enum) ❓ (byte near recording_mode; data[40] in E5 sf1 changed 0x01→0x00 alongside Continuous→Single Shot — may be this field) - Sample Rate: Standard 1024 / Fast 2048 / Faster 4096 sps ✅ (anchor−2) - Record Time: float, seconds ✅ (anchor+10) - Histogram Interval: 5 / 15 / 30 / 60 minutes (enum, mode-gated) diff --git a/docs/instantel_protocol_reference.md b/docs/instantel_protocol_reference.md index 5cd3801..73ff02b 100644 --- a/docs/instantel_protocol_reference.md +++ b/docs/instantel_protocol_reference.md @@ -104,6 +104,7 @@ | 2026-04-11 | §7.11 (NEW) | **NEW — §7.11 Erase-All Protocol added.** Full wire sequence, SUB 0x06 storage range payload layout, post-erase key counter reset (resets to `0x01110000`). Confirmed from 4-11-26 MITM capture of live Blastware ACH session. | | 2026-04-11 | §14.6 | **RESOLVED — ACH Session Lifecycle is no longer "Future".** `bridges/ach_server.py` fully implements inbound ACH: POLL handshake, device info, event download. State tracked via `ach_state.json` (key-based, with `max_downloaded_key` for post-erase detection). `--clear-after-download` flag added for the standard delete-after-upload workflow. | | 2026-04-17 | §7.6.2, §14 | **RESOLVED — Float 6.206053 at channel_label+28 is the ADC-to-velocity scale factor.** Confirmed from Series III Interface Handbook §4.5 formula: `Range (×1) = 1.61133 V / Sensitivity (V/unit)`. For the standard Instantel geophone at Normal range (10.000 in/s): Sensitivity = 1.61133 / 10 = 0.161133 V/(in/s). The stored value is the **inverse sensitivity** = 1/0.161133 = **6.206053 (in/s)/V**. Cross-check: 1.61133 V × 6.206053 = 10.000 in/s ✅. The firmware uses it as: `PPV (in/s) = ADC_voltage (V) × 6.206053`. Value is identical on all Instantel standard geophones — it is a hardware/firmware constant, NOT a user-configurable setting. Do NOT write this field. Open question §14 item "Max Geo Range float 6.2061" is now **RESOLVED**. | +| 2026-04-20 | §7.6.4 (NEW), §7.9, Appendix B | **CONFIRMED — Recording Mode byte location.** Three targeted captures (4-20-26) confirmed `recording_mode` at **`cfg[5]`** in the SUB 71 write payload (3-chunk compliance write). Method: single Blastware session, one initial E5 config pull, then three sequential "Send to unit" writes changing Recording Mode only. Diff of SUB 71 chunk-1 payloads: only `cfg[5]` and `cfg[1024]` changed; `cfg[1024]` delta exactly equals `cfg[5]` delta (chunk running checksum). In the E5 read response (sub-frame 1, page=0x0010), the field is at **`data[17]`** (= **anchor − 4** from the 10-byte anchor), one position earlier than in the write payload due to an extra `0x10` byte at `data[18]` present only in the read format. Enum: `0x00`=Single Shot, `0x01`=Continuous, `0x03`=Histogram, `0x04`=Histogram+Continuous. `0x02` value not yet observed. See §7.6.4 for full details. | --- @@ -258,7 +259,7 @@ Step 4 — Device sends actual data payload: | `24` | **WAVEFORM PAGE A?** | Paged waveform read, possibly channel group A. | 🔶 INFERRED | | `25` | **WAVEFORM PAGE B?** | Paged waveform read, possibly channel group B. | 🔶 INFERRED | | `09` | **UNKNOWN READ A** | Read command, response (`F6`) returns 0xCA (202) bytes. Purpose unknown. | 🔶 INFERRED | -| `1A` | **COMPLIANCE CONFIG READ** | Multi-step sequence (A+B+C+D frames). Response (E5) carries sample_rate (uint16 BE at anchor−2), record_time (float32 BE at anchor+10), trigger/alarm/max_range floats, and project strings. Anchor: `\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00`, search cfg[0:150]. Total ~2126 cfg bytes. | ✅ CONFIRMED 2026-04-02 | +| `1A` | **COMPLIANCE CONFIG READ** | Multi-step sequence (A+B+C+D frames). Response (E5) carries recording_mode (uint8 at anchor−4 in E5 sf1), sample_rate (uint16 BE at anchor−2), record_time (float32 BE at anchor+10), trigger/alarm/max_range floats, and project strings. Anchor: `\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00`, search cfg[0:150]. Total ~2126 cfg bytes. See §7.6.4 for recording_mode enum. | ✅ CONFIRMED 2026-04-02; recording_mode added 2026-04-20 | | `2E` | **UNKNOWN READ B** | Read command, response (`D1`) returns 0x1A (26) bytes. Purpose unknown. | 🔶 INFERRED | | `0E` | **CHANNEL SENSOR DATA** | Real-time sensor reading for one channel. Two-step read, data length 0x0A (10 bytes). Channel selector in params[6:8] (0x0000–0x0007 for 8 channels). Response (F1) carries amplitude, frequency, overswing data for that channel. Used by Blastware "Unit Channel Test" comms check. | ✅ CONFIRMED 2026-04-08 | | `98` | **TRIGGER TEST** | Trigger-test command. Single probe frame; `params[0] = 0xFF`. Response (0x67) is all-zero data. Sent twice per Blastware comms-check cycle. Not a full POLL, no monitor state change. | ✅ CONFIRMED 2026-04-08 | @@ -621,6 +622,53 @@ The sample rate bytes sit immediately before a `0x10` (DLE) prefix byte in the r --- +### 7.6.4 Recording Mode + +> ✅ **CONFIRMED — 2026-04-20** (BE11529 / firmware S338.17). Three targeted captures in a single Blastware session (4-20-26 directory), changing Recording Mode only between each write. + +Recording mode is stored as a **uint8** with different anchor-relative positions depending on whether you are reading from a device response or constructing a write payload. + +**In the SUB 71 write payload (3-chunk compliance write, `cfg[5]`):** + +| Enum | Mode | +|---|---| +| `0x00` | Single Shot | +| `0x01` | Continuous | +| `0x02` | Unknown (not yet observed) | +| `0x03` | Histogram | +| `0x04` | Histogram + Continuous (combined mode) | + +Anchor-relative position: **anchor − 3** (3 bytes before the 10-byte anchor in the write payload). The write payload layout in the region around the anchor: + +``` +cfg[anchor - 3] = recording_mode (uint8) +cfg[anchor - 2] = sample_rate_hi (uint8, MSB of uint16 BE) +cfg[anchor - 1] = sample_rate_lo (uint8, LSB of uint16 BE) +cfg[anchor:anchor+10] = \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00 ← anchor +cfg[anchor + 10:anchor + 14] = record_time (float32 BE) +``` + +**In the E5 read response (sub-frame 1, page=`0x0010`, `data[17]`):** + +The anchor appears at `data[21]` in this sub-frame. Recording mode is at `data[17]` = **anchor − 4** (one position earlier than in the write payload). This is because an extra `0x10` byte is present at `data[18]` in the read format (between recording_mode and sample_rate), which is NOT present in the write payload. The read-format layout: + +``` +data[17] = recording_mode (uint8) +data[18] = 0x10 ← extra byte present in E5 read only; absent in SUB 71 write +data[19] = sample_rate_hi (uint8, MSB of uint16 BE) +data[20] = sample_rate_lo (uint8, LSB of uint16 BE) +data[21:31] = anchor (\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00) +data[31:35] = record_time (float32 BE) +``` + +**Chunk checksum at `cfg[1024]`:** The first of the three SUB 71 write chunks (1027 bytes) contains a running checksum byte at `cfg[1024]` whose delta exactly equals the delta of `cfg[5]` (recording_mode). This byte reflects the cumulative change from `recording_mode` through to its position and should not be mistaken for a second copy of the recording_mode field. + +**Decode path (`_decode_compliance_config_into`):** use `data[anchor_pos - 4]` where `anchor_pos` is the index of the first byte of the anchor in the assembled E5 cfg bytes. + +**Encode path (`_encode_compliance_config`):** use `cfg[anchor_pos - 3]` = recording_mode value (write-payload offset; no extra `0x10` byte). + +--- + ### 7.7 Blastware `.set` File Format > 🔶 **INFERRED — 2026-03-01** from `Standard_Recording_Setup.set` cross-referenced against known wire payloads. @@ -1264,8 +1312,8 @@ Fields visible in the Blastware "Compliance Setup" dialog. ✅ = byte offset co | Field | Values / Type | Status | |---|---|---| -| Recording Mode | Continuous / Single Shot / Histogram | ❓ | -| Record Stop Mode | Fixed Record Time / Auto / Manual Stop | ❓ | +| Recording Mode | Single Shot (`0x00`) / Continuous (`0x01`) / Histogram (`0x03`) / Histogram+Continuous (`0x04`) | ✅ `recording_mode` — write: `cfg[anchor−3]`; read E5 sf1: `data[anchor−4]` — confirmed 2026-04-20 | +| Record Stop Mode | Fixed Record Time / Auto / Manual Stop | ❓ Hint: `data[40]` in E5 sf1 changed `01 7F` → `00 00` alongside Continuous → Single Shot; may be related but unconfirmed independently | | Sample Rate | Standard 1024 / Fast 2048 / Faster 4096 sps | ✅ `sample_rate` (anchor−2) | | Record Time | float, seconds (3, 5, 8, 10, 13…) | ✅ `record_time` (anchor+10) | | Histogram Interval | 5 / 15 / 30 / 60 min (mode-gated behind Histogram mode) | ❓ | @@ -1974,7 +2022,7 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger | Max Geo Range Enum (max_range_geo_enum) | §3.8.4 | Channel block, Tran+20, uint8 | uint8 | ❓ UNCONFIRMED — reads `0x01` on both tested units (Normal range). Hypothesis: `0x01`=Normal (Gain=1, 10 in/s), `0x00`=Sensitive (Gain=8, 1.25 in/s). Need 1.25 in/s capture to verify. | | Microphone Units | §3.9.7 | Inline unit string | char[4] | `"psi\0"`, `"pa.\0"`, `"dB\0\0"` | | Sample Rate | §3.8.2 | cfg anchor−2, uint16 BE — anchor=`\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00` in cfg[40:100] | uint16 BE | Normal=1024, Fast=2048, Faster=4096 ✅ CONFIRMED 2026-04-01 (BE11529 S338.17). Anchor required — see §7.6.3 DLE jitter. | -| Record Mode | §3.8.1 | Unknown | — | Single Shot, Continuous, Manual, Histogram, Histogram Combo | +| Record Mode | §3.8.1 | Write: `cfg[anchor−3]`, uint8. Read (E5 sf1): `data[anchor−4]`, uint8. Note: extra `0x10` byte at read `data[anchor−3]` shifts offset by 1 vs write. | uint8 | `0x00`=Single Shot, `0x01`=Continuous, `0x02`=unknown, `0x03`=Histogram, `0x04`=Histogram+Continuous. ✅ CONFIRMED 2026-04-20 | | Trigger Sample Width | §3.13.1h | BW→S3 SUB `0x82` write frame, destuffed `[22]`, uint8 | uint8 | Default=2; confirmed 4=`0x04`, 3=`0x03`. **BW-side write only** — not visible in S3 compliance reads. Mode-gated: only sent in Compliance/Single-Shot/Fixed mode. | | Auto Window | §3.13.1b | **Mode-gated — NOT YET MAPPED** | uint8? | 1–9 seconds; only active when Record Stop Mode = Auto. Capture in Fixed mode produced no wire change. | | Auxiliary Trigger | §3.13.1d | SUB `FE` (FULL_CONFIG_RESPONSE) offset `0x0109` (read); write path not yet isolated | uint8 (bool) | `0x00`=disabled, `0x01`=enabled; confirmed 2026-03-11 | diff --git a/minimateplus/client.py b/minimateplus/client.py index 6cc5edd..62fb9c3 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -847,6 +847,7 @@ class MiniMateClient: self, *, # Recording parameters + recording_mode: Optional[int] = None, sample_rate: Optional[int] = None, record_time: Optional[float] = None, # Threshold parameters (geo channels, in/s) @@ -869,6 +870,7 @@ class MiniMateClient: Configurable fields ------------------- Recording parameters: + recording_mode : int — 0x00=Single Shot, 0x01=Continuous, 0x03=Histogram, 0x04=Histogram+Continuous sample_rate : int — samples/sec; valid values: 1024, 2048, 4096 record_time : float — record duration in seconds (e.g. 2.0, 3.0) @@ -914,6 +916,7 @@ class MiniMateClient: # 2. Patch the compliance buffer and build the 2128-byte write payload compliance_data = _encode_compliance_config( compliance_raw, + recording_mode=recording_mode, sample_rate=sample_rate, record_time=record_time, trigger_level_geo=trigger_level_geo, @@ -1650,6 +1653,7 @@ def _extract_project_strings(data: bytes) -> Optional[ProjectInfo]: def _encode_compliance_config( raw: bytes, *, + recording_mode: Optional[int] = None, sample_rate: Optional[int] = None, record_time: Optional[float] = None, trigger_level_geo: Optional[float] = None, @@ -1670,6 +1674,11 @@ def _encode_compliance_config( DLE-jitter shifts): Anchor: b'\\xbe\\x80\\x00\\x00\\x00\\x00' (confirmed stable, both BE11529 and BE18189) + recording_mode → uint8 at anchor_pos - 7 (write payload; one byte earlier than read format) + Values: 0x00=Single Shot, 0x01=Continuous, 0x03=Histogram, 0x04=Histogram+Continuous + NOTE: In the E5 read response the field is at anchor_pos - 8 due to an extra + 0x10 byte at anchor_pos - 7 present only in the read format. The write + payload does NOT have this extra byte — use anchor_pos - 7 here. sample_rate → uint16 BE at anchor_pos - 6 record_time → float32 BE at anchor_pos + 6 @@ -1702,10 +1711,18 @@ def _encode_compliance_config( buf = bytearray(raw) - # ── Numeric: sample_rate + record_time (anchor-relative) ───────────────── + # ── Numeric: recording_mode + sample_rate + record_time (anchor-relative) ── _ANC = b'\xbe\x80\x00\x00\x00\x00' _anc = buf.find(_ANC, 0, 150) + if recording_mode is not None: + if _anc < 7: + log.warning("_encode_compliance_config: anchor not found — cannot write recording_mode") + else: + buf[_anc - 7] = recording_mode & 0xFF + log.debug("_encode_compliance_config: recording_mode=0x%02X -> offset %d", + recording_mode, _anc - 7) + if sample_rate is not None: if _anc < 6: log.warning("_encode_compliance_config: anchor not found — cannot write sample_rate") @@ -1838,7 +1855,22 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: # record_time : float32 BE at anchor_pos + 6 _ANCHOR = b'\xbe\x80\x00\x00\x00\x00' _anchor = data.find(_ANCHOR, 0, 150) - if _anchor >= 6 and _anchor + 10 <= len(data): + if _anchor >= 8 and _anchor + 10 <= len(data): + # Layout (E5 read format, relative to 6-byte anchor suffix): + # _anchor - 8 : recording_mode (uint8) + # _anchor - 7 : 0x10 (extra byte present in E5 read only; absent in SUB 71 write) + # _anchor - 6 : sample_rate_hi (uint8, MSB of uint16 BE) + # _anchor - 5 : sample_rate_lo (uint8, LSB of uint16 BE) + # _anchor - 4 : \x01\x2c\x00\x00 (10-byte anchor prefix) + # _anchor : \xbe\x80\x00\x00\x00\x00 (6-byte anchor suffix) + # _anchor + 6 : record_time (float32 BE) + try: + config.recording_mode = data[_anchor - 8] + log.debug( + "compliance_config: recording_mode = 0x%02X (anchor@%d)", config.recording_mode, _anchor + ) + except Exception as exc: + log.warning("compliance_config: recording_mode extraction failed: %s", exc) try: config.sample_rate = struct.unpack_from(">H", data, _anchor - 6)[0] log.debug( @@ -1853,10 +1885,21 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None: ) except Exception as exc: log.warning("compliance_config: record_time extraction failed: %s", exc) + elif _anchor >= 6 and _anchor + 10 <= len(data): + # Fallback: anchor found but not enough bytes before it for recording_mode + log.warning("compliance_config: anchor too close to start (anchor@%d) — skipping recording_mode", _anchor) + try: + config.sample_rate = struct.unpack_from(">H", data, _anchor - 6)[0] + except Exception: + pass + try: + config.record_time = struct.unpack_from(">f", data, _anchor + 6)[0] + except Exception: + pass else: log.warning( "compliance_config: anchor %s not found in cfg[0:150] (len=%d) " - "— sample_rate and record_time will be None", + "— sample_rate, record_time and recording_mode will be None", _ANCHOR.hex(), len(data), ) diff --git a/minimateplus/models.py b/minimateplus/models.py index 67c8b7b..9f7f9a4 100644 --- a/minimateplus/models.py +++ b/minimateplus/models.py @@ -338,8 +338,12 @@ class ComplianceConfig: raw: Optional[bytes] = None # full 2090-byte payload (for debugging) # Recording parameters (✅ CONFIRMED from §7.6) - record_time: Optional[float] = None # seconds (7.0, 10.0, 13.0, etc.) - sample_rate: Optional[int] = None # sps (1024, 2048, 4096, etc.) — NOT YET FOUND ❓ + recording_mode: Optional[int] = None # uint8: 0x00=Single Shot, 0x01=Continuous, + # 0x03=Histogram, 0x04=Histogram+Continuous ✅ confirmed 2026-04-20 + # Read (E5 sf1): data[anchor_pos - 4] + # Write (SUB 71 payload): cfg[anchor_pos - 3] + record_time: Optional[float] = None # seconds (7.0, 10.0, 13.0, etc.) + sample_rate: Optional[int] = None # sps (1024, 2048, 4096, etc.) # Trigger/alarm levels (✅ CONFIRMED per-channel at §7.6) # For now we store the first geo channel (Transverse) as representatives;