feat: enhance session completeness tracking in s3_analyzer and seismo_lab
This commit is contained in:
@@ -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? | 1–9 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? | 1–9 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 |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user