feat: enhance session completeness tracking in s3_analyzer and seismo_lab

This commit is contained in:
serversdwn
2026-03-11 18:48:42 -04:00
parent 41606d2f31
commit 99d66453fe
3 changed files with 36 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
# Instantel MiniMate Plus — Blastware RS-232 Protocol Reference # Instantel MiniMate Plus — Blastware RS-232 Protocol Reference v0.22
### "The Rosetta Stone" ### "The Rosetta Stone"
> Reverse-engineered via RS-232 serial bridge sniffing between Blastware software and an Instantel MiniMate Plus seismograph (S/N: BE18189). > Reverse-engineered via RS-232 serial bridge sniffing between Blastware software and an Instantel MiniMate Plus seismograph (S/N: BE18189).
> Cross-referenced against Instantel MiniMate Plus Operator Manual (716U0101 Rev 15) from v0.18 onward. > Cross-referenced against Instantel MiniMate Plus Operator Manual (716U0101 Rev 15) from v0.18 onward.
@@ -50,6 +50,10 @@
| 2026-03-09 | §7.8, §14, Appendix B | **NEW — Trigger Sample Width confirmed:** Located in BW→S3 write frame SUB `0x82`, destuffed payload offset `[22]`, uint8. Confirmed via BW-side capture (`raw_bw.bin`) diffing two sessions: Width=4 → `0x04`, Width=3 → `0x03`. Setting is **transmitted only on BW→S3 write** (SUB `0x82`), invisible in S3-side compliance dumps. | | 2026-03-09 | §7.8, §14, Appendix B | **NEW — Trigger Sample Width confirmed:** Located in BW→S3 write frame SUB `0x82`, destuffed payload offset `[22]`, uint8. Confirmed via BW-side capture (`raw_bw.bin`) diffing two sessions: Width=4 → `0x04`, Width=3 → `0x03`. Setting is **transmitted only on BW→S3 write** (SUB `0x82`), invisible in S3-side compliance dumps. |
| 2026-03-09 | §14, Appendix B | **CONFIRMED — Mode gating is a real protocol behavior:** Several settings are only transmitted (and possibly only interpreted by the device) when the required mode is active. Trigger Sample Width is only sent when in Compliance/Single-Shot/Fixed Record Time mode. Auto Window is only relevant when Record Stop Mode = Auto — attempting to capture it in Fixed mode produced no change on the wire (F7 and D1 blocks identical before/after). This is an architectural property, not a gap in the capture methodology. Future capture attempts for mode-gated settings must first activate the appropriate mode. | | 2026-03-09 | §14, Appendix B | **CONFIRMED — Mode gating is a real protocol behavior:** Several settings are only transmitted (and possibly only interpreted by the device) when the required mode is active. Trigger Sample Width is only sent when in Compliance/Single-Shot/Fixed Record Time mode. Auto Window is only relevant when Record Stop Mode = Auto — attempting to capture it in Fixed mode produced no change on the wire (F7 and D1 blocks identical before/after). This is an architectural property, not a gap in the capture methodology. Future capture attempts for mode-gated settings must first activate the appropriate mode. |
| 2026-03-09 | §14 | **UPDATED — Auto Window:** Capture attempted (Auto Window 3→9) in Fixed record time mode. No change observed in any S3-side frame (F7, D1, E5 all identical). Confirmed mode-gated behind Record Stop Mode = Auto. Not capturable without switching modes — deferred. | | 2026-03-09 | §14 | **UPDATED — Auto Window:** Capture attempted (Auto Window 3→9) in Fixed record time mode. No change observed in any S3-side frame (F7, D1, E5 all identical). Confirmed mode-gated behind Record Stop Mode = Auto. Not capturable without switching modes — deferred. |
| 2026-03-11 | §14, Appendix B | **CONFIRMED — Aux Trigger read location:** SUB `FE` (FULL_CONFIG_RESPONSE), destuffed payload offset `0x0109`, uint8. `0x00` = disabled, `0x01` = enabled. Confirmed via controlled capture: changed Aux Trigger in Blastware, sent to unit, re-read config. FE diff showed clean isolated flip at `0x0109` with only 3 other bytes changing (likely counters/checksums at `0x0033`, `0x00C0`, `0x04ED`). |
| 2026-03-11 | §14, Appendix B | **PARTIAL — Aux Trigger write path:** Write command not yet isolated. The BW→S3 write appears to occur inside the A4 (POLL_RESPONSE) stream via inner frame handshaking — multiple WRITE_CONFIRM_RESPONSE inner frames (SUBs `7C`, `7D`, `8B`, `8C`, `8D`, `8E`, `96`, `97`) appeared in A4 after the write, and the TRIGGER_CONFIG_RESPONSE (SUB `E3`) inner frames were removed. Write command itself not yet captured in a clean session — likely SUB `15` or embedded in the partial session 0. Write path deferred for a future clean capture. |
| 2026-03-11 | §4, §14 | **NEW — SUB A4 is a composite container frame:** A4 (POLL_RESPONSE) payload contains multiple embedded inner frames using the same DLE framing (10 02 start, 10 03 end, 10 10 stuffing). Phase-shift diffing issue resolved in s3_analyzer.py by adding `_extract_a4_inner_frames()` and `_diff_a4_payloads()` — diff count reduced from 2300 → 17 meaningful entries. |
| 2026-03-11 | §14 | **NEW — SUB `6E` response anomaly:** BW sends SUB `1C` (TRIGGER_CONFIG_READ) and S3 responds with SUB `6E` — does NOT follow the `0xFF - SUB` rule (`0xFF - 0x1C = 0xE3`). Only known exception to the response pairing rule observed to date. SUB `6E` payload starts with ASCII string `"Long2"`. |
--- ---
@@ -293,7 +297,9 @@ Write commands are initiated by Blastware (`BW->S3`) and use SUB bytes in the `0
## 7. Known Data Payloads ## 7. Known Data Payloads
### 7.1 Poll Response (SUB A4) — Device Identity Block ### 7.1 Poll Response (SUB A4) — Device Identity Block / Composite Container
> ⚠️ **SUB A4 is a composite container frame.** The large A4 payload (~3600+ bytes) contains multiple embedded inner sub-frames using the same DLE framing as the outer protocol (`10 02` start, `10 03` end, `10 10` stuffing). Inner frames carry WRITE_CONFIRM_RESPONSE and TRIGGER_CONFIG_RESPONSE sub-frames among others. Flat byte-by-byte diffing of A4 is unreliable due to phase shifting — use inner-frame-aware diffing (`_diff_a4_payloads()` in s3_analyzer.py). Confirmed 2026-03-11.
Two-step read. Data payload = 0x30 bytes. Two-step read. Data payload = 0x30 bytes.
@@ -883,7 +889,9 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger
| Meaning of `0x07 E7` field in config block | LOW | 2026-02-26 | | | Meaning of `0x07 E7` field in config block | LOW | 2026-02-26 | |
| **Trigger Sample Width****RESOLVED:** BW→S3 write frame SUB `0x82`, destuffed payload offset `[22]`, uint8. Width=4 → `0x04`, Width=3 → `0x03`. Confirmed via BW-side capture diff. Only visible in `raw_bw.bin` write traffic, not in S3-side compliance reads. | RESOLVED | 2026-03-02 | Confirmed 2026-03-09 | | **Trigger Sample Width****RESOLVED:** BW→S3 write frame SUB `0x82`, destuffed payload offset `[22]`, uint8. Width=4 → `0x04`, Width=3 → `0x03`. Confirmed via BW-side capture diff. Only visible in `raw_bw.bin` write traffic, not in S3-side compliance reads. | RESOLVED | 2026-03-02 | Confirmed 2026-03-09 |
| **Auto Window** — "1 to 9 seconds" per manual (§3.13.1b). **Mode-gated:** only transmitted/active when Record Stop Mode = Auto. Capture attempted in Fixed mode (3→9 change) — no wire change observed in any frame. Deferred pending mode switch. | LOW | 2026-03-02 | Updated 2026-03-09 | | **Auto Window** — "1 to 9 seconds" per manual (§3.13.1b). **Mode-gated:** only transmitted/active when Record Stop Mode = Auto. Capture attempted in Fixed mode (3→9 change) — no wire change observed in any frame. Deferred pending mode switch. | LOW | 2026-03-02 | Updated 2026-03-09 |
| **Auxiliary Trigger** — Enabled/Disabled per manual (§3.13.1d). Location in protocol not yet mapped. | LOW | 2026-03-02 | NEW | | **Auxiliary Trigger read location****RESOLVED:** SUB `FE` offset `0x0109`, uint8, `0x00`=disabled, `0x01`=enabled. Confirmed 2026-03-11 via controlled toggle capture. | RESOLVED | 2026-03-02 | Resolved 2026-03-11 |
| **Auxiliary Trigger write path** — Write command not yet captured in a clean session. Inner frame handshake visible in A4 (multiple WRITE_CONFIRM_RESPONSE SUBs appear, TRIGGER_CONFIG_RESPONSE removed), but the BW→S3 write command itself was in a partial session. Likely SUB `15` or similar. Deferred for clean capture. | LOW | 2026-03-11 | NEW |
| **SUB `6E` response to SUB `1C`** — S3 responds to TRIGGER_CONFIG_READ (SUB `1C`) with SUB `6E`, NOT `0xE3` as the `0xFF - SUB` rule would predict. Only known exception to the response pairing rule observed to date. Payload starts with ASCII `"Long2"`. Purpose unknown. | LOW | 2026-03-11 | NEW |
| **Max Geo Range float 6.2061 in/s** — NOT a user-selectable range (manual only shows 1.25 and 10.0 in/s). Likely internal ADC full-scale constant or hardware range ceiling. Not worth capturing. | LOW | 2026-02-26 | Downgraded 2026-03-02 | | **Max Geo Range float 6.2061 in/s** — NOT a user-selectable range (manual only shows 1.25 and 10.0 in/s). Likely internal ADC full-scale constant or hardware range ceiling. Not worth capturing. | LOW | 2026-02-26 | Downgraded 2026-03-02 |
| MicL channel units — **RESOLVED: psi**, confirmed from `.set` file unit string `"psi\0"` | RESOLVED | 2026-03-01 | | | MicL channel units — **RESOLVED: psi**, confirmed from `.set` file unit string `"psi\0"` | RESOLVED | 2026-03-01 | |
| Backlight offset — **RESOLVED: +4B in event index data**, uint8, seconds | RESOLVED | 2026-03-02 | | | Backlight offset — **RESOLVED: +4B in event index data**, uint8, seconds | RESOLVED | 2026-03-02 | |
@@ -914,7 +922,7 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger
| Record Mode | §3.8.1 | Unknown | — | Single Shot, Continuous, Manual, Histogram, Histogram Combo | | Record Mode | §3.8.1 | Unknown | — | Single Shot, Continuous, Manual, Histogram, Histogram Combo |
| 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. | | 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? | 19 seconds; only active when Record Stop Mode = Auto. Capture in Fixed mode produced no wire change. | | Auto Window | §3.13.1b | **Mode-gated — NOT YET MAPPED** | uint8? | 19 seconds; only active when Record Stop Mode = Auto. Capture in Fixed mode produced no wire change. |
| Auxiliary Trigger | §3.13.1d | **NOT YET MAPPED** | bool | Enabled/Disabled | | 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 |
| Password | §3.13.1c | Unknown | — | 4-key sequence | | Password | §3.13.1c | Unknown | — | 4-key sequence |
| Serial Connection | §3.9.11 | Unknown | — | Direct / Via Modem | | Serial Connection | §3.9.11 | Unknown | — | Direct / Via Modem |
| Baud Rate | §3.9.12 | Unknown | — | 38400 for direct | | Baud Rate | §3.9.12 | Unknown | — | 38400 for direct |

View File

@@ -140,6 +140,15 @@ class Session:
index: int index: int
bw_frames: list[AnnotatedFrame] bw_frames: list[AnnotatedFrame]
s3_frames: list[AnnotatedFrame] s3_frames: list[AnnotatedFrame]
# None = infer from SUB 0x74 presence; True/False = explicitly set by splitter
complete: Optional[bool] = None
def is_complete(self) -> bool:
"""A session is complete if explicitly marked, or if it contains SUB 0x74."""
if self.complete is not None:
return self.complete
return any(af.header is not None and af.header.sub == SESSION_CLOSE_SUB
for af in self.bw_frames)
@property @property
def all_frames(self) -> list[AnnotatedFrame]: def all_frames(self) -> list[AnnotatedFrame]:
@@ -384,6 +393,7 @@ def split_sessions_at_marks(
session_offset = 0 session_offset = 0
bw_prev = s3_prev = 0 bw_prev = s3_prev = 0
n_segments = len(bw_cuts)
for seg_i, (bw_end, s3_end) in enumerate(zip(bw_cuts, s3_cuts)): for seg_i, (bw_end, s3_end) in enumerate(zip(bw_cuts, s3_cuts)):
bw_chunk = bw_blob[bw_prev:bw_end] bw_chunk = bw_blob[bw_prev:bw_end]
s3_chunk = s3_blob[s3_prev:s3_end] s3_chunk = s3_blob[s3_prev:s3_end]
@@ -394,11 +404,20 @@ def split_sessions_at_marks(
seg_sessions = split_into_sessions(bw_frames, s3_frames) seg_sessions = split_into_sessions(bw_frames, s3_frames)
# A mark-bounded segment is complete by definition — the user placed the
# mark after the read finished. Only the last segment (trailing, unbounded)
# may be genuinely in-progress.
is_last_segment = (seg_i == n_segments - 1)
# Re-index sessions so they are globally unique # Re-index sessions so they are globally unique
for sess in seg_sessions: for sess in seg_sessions:
sess.index = session_offset sess.index = session_offset
for f in sess.all_frames: for f in sess.all_frames:
f.session_idx = session_offset f.session_idx = session_offset
# Explicitly mark completeness: mark-bounded segments are complete;
# the trailing segment falls back to 0x74 inference.
if not is_last_segment:
sess.complete = True
session_offset += 1 session_offset += 1
all_sessions.append(sess) all_sessions.append(sess)
@@ -683,11 +702,7 @@ def render_session_report(
n_bw = len(session.bw_frames) n_bw = len(session.bw_frames)
n_s3 = len(session.s3_frames) n_s3 = len(session.s3_frames)
total = n_bw + n_s3 total = n_bw + n_s3
is_complete = any( status = "" if session.is_complete() else " [IN PROGRESS]"
af.header is not None and af.header.sub == SESSION_CLOSE_SUB
for af in session.bw_frames
)
status = "" if is_complete else " [IN PROGRESS]"
lines.append(f"{'='*72}") lines.append(f"{'='*72}")
lines.append(f"SESSION {session.index}{status}") lines.append(f"SESSION {session.index}{status}")
@@ -847,11 +862,7 @@ def render_claude_export(
lines += ["## Capture Summary", ""] lines += ["## Capture Summary", ""]
lines.append(f"Sessions found: {len(sessions)}") lines.append(f"Sessions found: {len(sessions)}")
for sess in sessions: for sess in sessions:
is_complete = any( status = "complete" if sess.is_complete() else "partial/in-progress"
af.header is not None and af.header.sub == SESSION_CLOSE_SUB
for af in sess.bw_frames
)
status = "complete" if is_complete else "partial/in-progress"
n_bw, n_s3 = len(sess.bw_frames), len(sess.s3_frames) n_bw, n_s3 = len(sess.bw_frames), len(sess.s3_frames)
changed = len(diffs[sess.index] or []) if sess.index < len(diffs) else 0 changed = len(diffs[sess.index] or []) if sess.index < len(diffs) else 0
changed_str = f" ({changed} SUBs changed vs prev)" if sess.index > 0 else " (baseline)" changed_str = f" ({changed} SUBs changed vs prev)" if sess.index > 0 else " (baseline)"
@@ -1119,14 +1130,7 @@ def live_loop(
# Check for session close # Check for session close
all_sessions = split_into_sessions(bw_annotated, s3_annotated) all_sessions = split_into_sessions(bw_annotated, s3_annotated)
# A complete session has the closing 0x74 complete_sessions = [s for s in all_sessions if s.is_complete()]
complete_sessions = [
s for s in all_sessions
if any(
af.header is not None and af.header.sub == SESSION_CLOSE_SUB
for af in s.bw_frames
)
]
# Emit reports for newly completed sessions # Emit reports for newly completed sessions
for sess in complete_sessions[len(sessions):]: for sess in complete_sessions[len(sessions):]:
@@ -1157,13 +1161,7 @@ def live_loop(
s3_annotated = annotate_frames(s3_frames_raw, "S3") s3_annotated = annotate_frames(s3_frames_raw, "S3")
bw_annotated = annotate_frames(bw_frames_raw, "BW") bw_annotated = annotate_frames(bw_frames_raw, "BW")
all_sessions = split_into_sessions(bw_annotated, s3_annotated) all_sessions = split_into_sessions(bw_annotated, s3_annotated)
incomplete = [ incomplete = [s for s in all_sessions if not s.is_complete()]
s for s in all_sessions
if not any(
af.header is not None and af.header.sub == SESSION_CLOSE_SUB
for af in s.bw_frames
)
]
for sess in incomplete: for sess in incomplete:
report = render_session_report(sess, diffs=None, prev_session_index=None) report = render_session_report(sess, diffs=None, prev_session_index=None)
out_path = write_report(sess, report, outdir) out_path = write_report(sess, report, outdir)

View File

@@ -706,9 +706,7 @@ class AnalyzerPanel(tk.Frame):
def _rebuild_tree(self) -> None: def _rebuild_tree(self) -> None:
self.tree.delete(*self.tree.get_children()) self.tree.delete(*self.tree.get_children())
for sess in self.state.sessions: for sess in self.state.sessions:
is_complete = any(af.header and af.header.sub == 0x74 label = f"Session {sess.index}" + ("" if sess.is_complete() else " [partial]")
for af in sess.bw_frames)
label = f"Session {sess.index}" + ("" if is_complete else " [partial]")
n_diff = len(self.state.diffs[sess.index] or []) n_diff = len(self.state.diffs[sess.index] or [])
diff_str = f"{n_diff} changes" if n_diff else "" diff_str = f"{n_diff} changes" if n_diff else ""
sid = self.tree.insert("", tk.END, text=label, sid = self.tree.insert("", tk.END, text=label,