Big event bugfix. see details:
## v0.13.0 — 2026-05-01 ### Fixed - **SUB 5A bulk waveform stream — over-read bug for events ≥ 2 sec.** `read_bulk_waveform_stream` was walking the chunk counter past the actual end of the event, picking up post-event circular-buffer garbage that corrupted reconstructed Blastware files for any waveform > ~1 sec. The loop now extracts the event's `end_offset` from the STRT record at `data[23:27]` of the probe response and stops the chunk walk when the next counter would step past it. Verified against three BW MITM captures (4-27-26 + 5-1-26): 2-sec event drops from 37 over-read chunks to 7 bounded chunks; 3-sec drops to 9; non-zero-start "event 2" drops to 9. ### Added - `framing.bulk_waveform_term_v2(key4, end_offset, last_chunk_counter)` — computes the corrected SUB 5A TERM frame's `(offset_word, params)` per the formula confirmed across all 3 BW captures. Not yet wired into `read_bulk_waveform_stream` (the legacy TERM is still used to preserve the existing `blastware_file.write_blastware_file` frame-structure expectations); available for the next iteration that switches to BW's 0x0200 chunk step. - `framing.parse_strt_end_offset(a5_data)` — extracts the event-end pointer from the STRT record in an A5 response payload.
This commit is contained in:
@@ -4,6 +4,60 @@ All notable changes to seismo-relay are documented here.
|
||||
|
||||
---
|
||||
|
||||
## v0.13.0 — 2026-05-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **SUB 5A bulk waveform stream — over-read bug for events ≥ 2 sec.**
|
||||
`read_bulk_waveform_stream` was walking the chunk counter past the actual
|
||||
end of the event, picking up post-event circular-buffer garbage that
|
||||
corrupted reconstructed Blastware files for any waveform > ~1 sec. The
|
||||
loop now extracts the event's `end_offset` from the STRT record at
|
||||
`data[23:27]` of the probe response and stops the chunk walk when the next
|
||||
counter would step past it. Verified against three BW MITM captures
|
||||
(4-27-26 + 5-1-26): 2-sec event drops from 37 over-read chunks to 7
|
||||
bounded chunks; 3-sec drops to 9; non-zero-start "event 2" drops to 9.
|
||||
|
||||
### Added
|
||||
|
||||
- `framing.bulk_waveform_term_v2(key4, end_offset, last_chunk_counter)` —
|
||||
computes the corrected SUB 5A TERM frame's `(offset_word, params)` per the
|
||||
formula confirmed across all 3 BW captures. Not yet wired into
|
||||
`read_bulk_waveform_stream` (the legacy TERM is still used to preserve the
|
||||
existing `blastware_file.write_blastware_file` frame-structure expectations);
|
||||
available for the next iteration that switches to BW's 0x0200 chunk step.
|
||||
- `framing.parse_strt_end_offset(a5_data)` — extracts the event-end pointer
|
||||
from the STRT record in an A5 response payload.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **CLAUDE.md and `docs/instantel_protocol_reference.md` extensively
|
||||
rewritten** to reflect the corrected SUB 5A protocol. See:
|
||||
- CLAUDE.md "SUB 5A — chunk counter formula (REWRITTEN 2026-05-01)"
|
||||
- CLAUDE.md "SUB 5A — STRT record encodes end_offset"
|
||||
- CLAUDE.md "SUB 5A — TERM frame formula"
|
||||
- CLAUDE.md "SUB 5A — fixed metadata pages 0x1002 and 0x1004"
|
||||
- CLAUDE.md "SUB 0A — WAVEHDR response length distinguishes events from
|
||||
boundaries" (0x46 = real event, 0x2C = boundary marker)
|
||||
- protocol reference §7.8.5 / §7.8.6 / §7.8.7 / §7.8.8
|
||||
- The previous chunk-counter formula (`max(key4[2:4], 0x0400) + (chunk-1) *
|
||||
0x0400`) is now marked DEPRECATED and explicitly tagged WRONG with
|
||||
pointers to the new sections, so future work doesn't re-derive it.
|
||||
|
||||
### Known minor diffs vs Blastware (deferred to a follow-up)
|
||||
|
||||
- We still use the OLD 0x0400 chunk step rather than BW's 0x0200; switching
|
||||
also requires updating `blastware_file.write_blastware_file`'s skip values
|
||||
and "extra chunk after metadata" logic, which depends on a fresh capture
|
||||
to verify.
|
||||
- We still use the legacy fixed `offset_word=0x005A` TERM frame rather than
|
||||
BW's `end_offset - next_boundary` formula, for the same reason.
|
||||
- Two fixed metadata pages at counter `0x1002` and `0x1004` are not yet
|
||||
read explicitly; under the current 0x0400 walk their content is reachable
|
||||
via the sample chunk that covers buffer addresses `[0x1000, 0x1400)`.
|
||||
|
||||
---
|
||||
|
||||
## v0.12.5 — 2026-04-21
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -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.12.3**.
|
||||
(Sierra Wireless RV50 / RV55). Current version: **v0.13.0**.
|
||||
|
||||
When new information about the protocol is discovered, please update the instantel_protocol_reference.md with the findings in addition to this document
|
||||
|
||||
@@ -41,7 +41,7 @@ Full read pipeline + write pipeline + erase pipeline + monitor log + call home c
|
||||
| Event header / first key | 1E | ✅ |
|
||||
| Waveform header | 0A | ✅ |
|
||||
| Waveform record (peaks, timestamp, project) | 0C | ✅ |
|
||||
| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ new v0.6.0 |
|
||||
| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ over-read bug fixed v0.13.0 (chunk loop bounded by STRT end_offset); minor wire diffs vs BW deferred — see "SUB 5A — chunk counter formula" |
|
||||
| Event advance / next key | 1F | ✅ |
|
||||
| **Write commands (push config to device)** | **68–83** | ✅ new v0.8.0 |
|
||||
| **Erase all events** | **0xA3 → 0x1C → 0x06 → 0xA2** | ✅ new v0.9.0 |
|
||||
|
||||
+130
-18
@@ -123,8 +123,11 @@ def build_5a_frame(offset_word: int, raw_params: bytes) -> bytes:
|
||||
Returns:
|
||||
Complete frame bytes: [ACK][STX][stuffed_section][chk][ETX]
|
||||
"""
|
||||
if len(raw_params) not in (10, 11):
|
||||
raise ValueError(f"raw_params must be 10 or 11 bytes, got {len(raw_params)}")
|
||||
if len(raw_params) not in (10, 11, 12):
|
||||
# 10 = termination params; 11 = regular probe / chunk params;
|
||||
# 12 = metadata-page params (extra trailing 0x00 — BW byte-perfect quirk
|
||||
# for the two fixed metadata reads at counter=0x1002 and 0x1004).
|
||||
raise ValueError(f"raw_params must be 10/11/12 bytes, got {len(raw_params)}")
|
||||
|
||||
# Build stuffed section between STX and checksum
|
||||
s = bytearray()
|
||||
@@ -398,28 +401,21 @@ def bulk_waveform_params(key4: bytes, counter: int, *, is_probe: bool = False) -
|
||||
|
||||
def bulk_waveform_term_params(key4: bytes, counter: int) -> bytes:
|
||||
"""
|
||||
Build the 10-byte params block for the SUB 5A termination request.
|
||||
DEPRECATED 2026-05-01 — see bulk_waveform_term_v2().
|
||||
|
||||
The termination request uses offset=0x005A and a DIFFERENT params layout —
|
||||
the leading 0x00 byte is dropped, key4[0:2] shifts to params[0:2], and the
|
||||
counter high byte is at params[2]:
|
||||
Build the 10-byte params block for the SUB 5A termination request, OLD layout
|
||||
(used in conjunction with the fixed offset_word=0x005A). Kept for backward
|
||||
compatibility — produces a tiny ~100-byte device-side terminator response
|
||||
rather than the proper partial-last-chunk + footer payload that BW gets.
|
||||
|
||||
params[0] = key4[0]
|
||||
params[1] = key4[1]
|
||||
params[2] = (counter >> 8) & 0xFF
|
||||
params[3:] = zeros
|
||||
|
||||
Counter for the termination request = last_regular_counter + 0x0400.
|
||||
|
||||
Confirmed from 1-2-26 BW TX capture: final request (frame 83) uses
|
||||
offset=0x005A, params[0:3] = key4[0:2] + term_counter_hi.
|
||||
|
||||
Args:
|
||||
key4: 4-byte waveform key.
|
||||
counter: Termination counter (= last regular counter + 0x0400).
|
||||
|
||||
Returns:
|
||||
10-byte params block.
|
||||
Use bulk_waveform_term_v2() for new code — it computes the verified
|
||||
offset_word + params from end_offset (extracted from STRT) and the last
|
||||
chunk counter.
|
||||
"""
|
||||
if len(key4) != 4:
|
||||
raise ValueError(f"waveform key must be 4 bytes, got {len(key4)}")
|
||||
@@ -430,6 +426,123 @@ def bulk_waveform_term_params(key4: bytes, counter: int) -> bytes:
|
||||
return bytes(p)
|
||||
|
||||
|
||||
def bulk_waveform_term_v2(
|
||||
key4: bytes,
|
||||
end_offset: int,
|
||||
last_chunk_counter: int,
|
||||
) -> tuple[int, bytes]:
|
||||
"""
|
||||
Compute the SUB 5A TERM frame's offset_word and 10-byte params block.
|
||||
|
||||
Confirmed across 3 events (4-27-26 + 5-1-26 captures):
|
||||
|
||||
next_boundary = last_chunk_counter + 0x0200
|
||||
offset_word = end_offset - next_boundary (residual byte count)
|
||||
params[0] = key4[0] (= 0x01 on every observed device)
|
||||
params[1] = key4[1] (= 0x11)
|
||||
params[2] = (next_boundary >> 8) & 0xFF
|
||||
params[3] = next_boundary & 0xFF
|
||||
params[4:10] = zeros
|
||||
|
||||
Verification:
|
||||
| end_offset | last_chunk | next_boundary | offset_word | params[2:4] |
|
||||
| 0x1ABE | 0x1800 | 0x1A00 | 0x00BE | 1A 00 |
|
||||
| 0x21F2 | 0x1E00 | 0x2000 | 0x01F2 | 20 00 |
|
||||
| 0x417E | 0x3E38 | 0x4038 | 0x0146 | 40 38 |
|
||||
|
||||
The device receives `requested_address = (params[2] << 8) | offset_word`
|
||||
and replies with `(end_offset - next_boundary)` bytes of waveform tail
|
||||
starting at `next_boundary` — including the 26-byte file footer.
|
||||
|
||||
Args:
|
||||
key4: 4-byte waveform key for this event.
|
||||
end_offset: Event-end pointer (= `(end_key[2] << 8) | end_key[3]`
|
||||
from the STRT record at data[23:27] of A5[0]).
|
||||
last_chunk_counter: Counter of the last full 0x0200-byte chunk fetched
|
||||
(the chunk that covers [last_chunk_counter,
|
||||
last_chunk_counter + 0x0200)).
|
||||
|
||||
Returns:
|
||||
(offset_word, params10) tuple. Pass as
|
||||
`build_5a_frame(offset_word, params)`.
|
||||
|
||||
Raises:
|
||||
ValueError: on inconsistent inputs.
|
||||
"""
|
||||
if len(key4) != 4:
|
||||
raise ValueError(f"waveform key must be 4 bytes, got {len(key4)}")
|
||||
next_boundary = last_chunk_counter + 0x0200
|
||||
if next_boundary > 0xFFFF:
|
||||
raise ValueError(
|
||||
f"next_boundary 0x{next_boundary:04X} exceeds uint16; check inputs"
|
||||
)
|
||||
if end_offset <= last_chunk_counter:
|
||||
raise ValueError(
|
||||
f"end_offset 0x{end_offset:04X} must be > "
|
||||
f"last_chunk_counter 0x{last_chunk_counter:04X}"
|
||||
)
|
||||
offset_word = end_offset - next_boundary
|
||||
if offset_word < 0:
|
||||
# Last chunk overshot end_offset; caller should have stopped one chunk
|
||||
# earlier. Treat as zero residual.
|
||||
offset_word = 0
|
||||
if offset_word > 0xFFFF:
|
||||
raise ValueError(
|
||||
f"offset_word 0x{offset_word:04X} exceeds uint16"
|
||||
)
|
||||
p = bytearray(10)
|
||||
p[0] = key4[0]
|
||||
p[1] = key4[1]
|
||||
p[2] = (next_boundary >> 8) & 0xFF
|
||||
p[3] = next_boundary & 0xFF
|
||||
return offset_word, bytes(p)
|
||||
|
||||
|
||||
# ── End-offset extraction from STRT record ────────────────────────────────────
|
||||
|
||||
STRT_MARKER = b"STRT"
|
||||
|
||||
|
||||
def parse_strt_end_offset(a5_data: bytes) -> Optional[int]:
|
||||
"""
|
||||
Extract the event-end offset from the STRT record in an A5 response payload.
|
||||
|
||||
The first A5 response (the probe response, or the first chunk for events
|
||||
with non-zero start_key[2:4]) contains a STRT record at byte offset 17 of
|
||||
`data`. Layout:
|
||||
|
||||
data[17:21] "STRT"
|
||||
data[21:23] ff fe sentinel
|
||||
data[23:27] end_key ← 4-byte key of where this event ENDS
|
||||
data[27:31] start_key
|
||||
...
|
||||
|
||||
Returns `(end_key[2] << 8) | end_key[3]` — the absolute device-buffer
|
||||
address where the event ends. Use this to bound the chunk loop and to
|
||||
compute the TERM frame.
|
||||
|
||||
Verified end_offset values:
|
||||
| event start_key | end_key | end_offset |
|
||||
| 01110000 | 01111ABE | 0x1ABE |
|
||||
| 01110000 | 011121F2 | 0x21F2 |
|
||||
| 011121F2 | 0111417E | 0x417E |
|
||||
|
||||
Args:
|
||||
a5_data: The `data` field of an A5 response frame (frame.data).
|
||||
|
||||
Returns:
|
||||
The end_offset (uint16) if STRT is found, else None.
|
||||
"""
|
||||
pos = a5_data.find(STRT_MARKER)
|
||||
if pos < 0 or pos + 10 > len(a5_data):
|
||||
return None
|
||||
# data[pos+4:pos+6] is "ff fe"; data[pos+6:pos+10] is end_key.
|
||||
end_key = a5_data[pos + 6 : pos + 10]
|
||||
if len(end_key) < 4:
|
||||
return None
|
||||
return (end_key[2] << 8) | end_key[3]
|
||||
|
||||
|
||||
# ── Pre-built POLL frames ─────────────────────────────────────────────────────
|
||||
#
|
||||
# POLL (SUB 0x5B) uses the same two-step pattern as all other reads — the
|
||||
@@ -470,7 +583,6 @@ class S3Frame:
|
||||
|
||||
|
||||
# ── Streaming S3 frame parser ─────────────────────────────────────────────────
|
||||
|
||||
class S3FrameParser:
|
||||
"""
|
||||
Incremental byte-stream parser for S3→BW response frames.
|
||||
|
||||
+45
-10
@@ -35,6 +35,8 @@ from .framing import (
|
||||
token_params,
|
||||
bulk_waveform_params,
|
||||
bulk_waveform_term_params,
|
||||
bulk_waveform_term_v2,
|
||||
parse_strt_end_offset,
|
||||
POLL_PROBE,
|
||||
POLL_DATA,
|
||||
SESSION_RESET,
|
||||
@@ -122,16 +124,21 @@ DATA_LENGTHS: dict[int, int] = {
|
||||
}
|
||||
|
||||
# SUB 5A (BULK_WAVEFORM_STREAM) protocol constants.
|
||||
# Confirmed from 1-2-26 BW TX capture analysis (2026-04-02).
|
||||
_BULK_CHUNK_OFFSET = 0x1004 # offset field for probe + all regular chunk requests ✅
|
||||
_BULK_TERM_OFFSET = 0x005A # offset field for termination request ✅
|
||||
_BULK_COUNTER_STEP = 0x0400 # chunk counter increment per chunk ✅
|
||||
# Chunk counter formula: key4[2:4] + (chunk_num - 1) * 0x0400
|
||||
# where key4[2:4] is the event's circular-buffer base offset ((key4[2]<<8)|key4[3]).
|
||||
# Earlier captures showed 0x1004 for chunk 1 of key 01110000 — that was a Blastware
|
||||
# artifact. For keys where key4[2:4] != 0x0000 (e.g. key 01111884) the old
|
||||
# "n * 0x0400" formula sends counters from the wrong buffer region and the device
|
||||
# returns data from a different event. Confirmed correct 2026-04-24.
|
||||
#
|
||||
# 2026-05-01 minimal-fix: the chunk-counter walk is now bounded by the event's
|
||||
# `end_offset` extracted from the STRT record at data[23:27] of the probe
|
||||
# response. Without this bound the loop kept asking for chunks past the event
|
||||
# end and the device responded with post-event circular-buffer garbage,
|
||||
# corrupting reconstructed Blastware files for events ≥ 2 sec.
|
||||
#
|
||||
# We keep the OLD 0x0400 chunk step here (BW actually uses 0x0200 — see §7.8.5
|
||||
# of the protocol reference for the corrected understanding) because the
|
||||
# existing blastware_file.py builder relies on the 0x0400-step frame structure
|
||||
# to produce valid files. Switching to BW's 0x0200 step is a separate task
|
||||
# that also requires updating the file builder.
|
||||
_BULK_CHUNK_OFFSET = 0x1004 # offset_word for probe + all chunk requests
|
||||
_BULK_TERM_OFFSET = 0x005A # offset_word for the legacy terminator
|
||||
_BULK_COUNTER_STEP = 0x0400 # chunk counter increment
|
||||
|
||||
# Default timeout values (seconds).
|
||||
# MiniMate Plus is a slow device — keep these generous.
|
||||
@@ -610,6 +617,24 @@ class MiniMateProtocol:
|
||||
frames_data.append(rsp)
|
||||
log.debug("5A A5[0] page_key=0x%04X %d bytes", rsp.page_key, len(rsp.data))
|
||||
|
||||
# ── Parse STRT end_offset from probe response (NEW 2026-05-01) ────────
|
||||
# The first A5 response contains a STRT record at data[17:]. The
|
||||
# bytes at data[23:27] are the event's end-key, whose low 16 bits
|
||||
# are the absolute device-buffer address where the event ends. Use
|
||||
# this to bound the chunk loop and stop the over-read past event end.
|
||||
# See docs/instantel_protocol_reference.md §7.8.5 and CLAUDE.md
|
||||
# "SUB 5A — STRT record encodes end_offset".
|
||||
_end_offset = parse_strt_end_offset(rsp.data)
|
||||
if _end_offset is None:
|
||||
# Defensive fallback — let max_chunks cap the walk.
|
||||
log.warning("5A: STRT not found in probe; cannot bound chunk loop")
|
||||
_end_offset = 0xFFFF
|
||||
else:
|
||||
log.debug(
|
||||
"5A STRT start_offset=0x%04X end_offset=0x%04X size=0x%04X",
|
||||
_key4_offset, _end_offset, _end_offset - _key4_offset,
|
||||
)
|
||||
|
||||
# ── Step 2: chunk loop ───────────────────────────────────────────────
|
||||
# Counter formula: _chunk_base + (chunk_num - 1) * 0x0400
|
||||
# where _chunk_base = max(key4[2:4], 0x0400).
|
||||
@@ -629,6 +654,16 @@ class MiniMateProtocol:
|
||||
_chunk_base = max(_key4_offset, _BULK_COUNTER_STEP)
|
||||
for chunk_num in range(1, max_chunks + 1):
|
||||
counter = _chunk_base + (chunk_num - 1) * _BULK_COUNTER_STEP
|
||||
# Stop when we'd step past the event end (NEW 2026-05-01). Without
|
||||
# this, the device returns post-event circular-buffer data which
|
||||
# corrupts the reconstructed file for events ≥ 2 sec.
|
||||
if counter >= _end_offset:
|
||||
log.debug(
|
||||
"5A chunk loop done at counter=0x%04X (end=0x%04X); "
|
||||
"%d chunks fetched",
|
||||
counter, _end_offset, len(frames_data),
|
||||
)
|
||||
break
|
||||
params = bulk_waveform_params(key4, counter)
|
||||
log.debug("5A chunk %d counter=0x%04X", chunk_num, counter)
|
||||
self._send(build_5a_frame(_BULK_CHUNK_OFFSET, params))
|
||||
|
||||
+4
-128
@@ -114,8 +114,6 @@ class BridgePanel(tk.Frame):
|
||||
on_capture_complete(bw_path, s3_path, label)— a capture segment finished
|
||||
"""
|
||||
|
||||
def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped,
|
||||
on_capture_started=None, on_capture_complete=None, **kw):
|
||||
def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped,
|
||||
on_capture_started=None, on_capture_complete=None, **kw):
|
||||
super().__init__(parent, bg=BG2, **kw)
|
||||
@@ -141,10 +139,6 @@ class BridgePanel(tk.Frame):
|
||||
self._cap_history: list[dict] = [] # {label, status, bw, s3}
|
||||
# mode
|
||||
self._mode = tk.StringVar(value="serial")
|
||||
# Capture state
|
||||
self._capturing = False
|
||||
self._cap_label: Optional[str] = None
|
||||
self._cap_history: list[dict] = [] # {label, status, bw, s3}
|
||||
self._build()
|
||||
self._poll_stdout()
|
||||
self._poll_tcp_log()
|
||||
@@ -246,18 +240,6 @@ class BridgePanel(tk.Frame):
|
||||
command=self._stop_capture, state="disabled")
|
||||
self.stop_cap_btn.pack(side=tk.LEFT, padx=4)
|
||||
|
||||
tk.Frame(btn_row, bg=BG2, width=16).pack(side=tk.LEFT) # spacer
|
||||
|
||||
self.cap_btn = tk.Button(btn_row, text="⬤ New Capture", bg=ORANGE, fg="#000000",
|
||||
relief="flat", padx=10, cursor="hand2", font=MONO_B,
|
||||
command=self._start_capture, state="disabled")
|
||||
self.cap_btn.pack(side=tk.LEFT, padx=4)
|
||||
|
||||
self.stop_cap_btn = tk.Button(btn_row, text="■ Stop Capture", bg=BG3, fg=RED,
|
||||
relief="flat", padx=10, cursor="hand2", font=MONO_B,
|
||||
command=self._stop_capture, state="disabled")
|
||||
self.stop_cap_btn.pack(side=tk.LEFT, padx=4)
|
||||
|
||||
self.mark_btn = tk.Button(btn_row, text="Add Mark", bg=BG3, fg=FG,
|
||||
relief="flat", padx=10, cursor="hand2", font=MONO,
|
||||
command=self.add_mark, state="disabled")
|
||||
@@ -319,7 +301,6 @@ class BridgePanel(tk.Frame):
|
||||
|
||||
# Log output
|
||||
self.log_view = scrolledtext.ScrolledText(
|
||||
self, height=14, font=MONO_SM,
|
||||
self, height=14, font=MONO_SM,
|
||||
bg=BG, fg=FG, insertbackground=FG,
|
||||
relief="flat", state="disabled",
|
||||
@@ -462,15 +443,12 @@ class BridgePanel(tk.Frame):
|
||||
self.start_btn.configure(state="disabled")
|
||||
self.stop_btn.configure(state="normal", bg=RED)
|
||||
self.cap_btn.configure(state="normal")
|
||||
self.cap_btn.configure(state="normal")
|
||||
self._append_log(f"== Bridge started [{ts}] ==\n")
|
||||
self._append_log(" Click 'New Capture' when ready to record.\n")
|
||||
self._on_started(struct_bin_path)
|
||||
|
||||
# Notify parent — no raw files yet, just the structured bin path
|
||||
self._on_started(struct_bin_path)
|
||||
|
||||
def stop_bridge(self) -> None:
|
||||
def _stop_serial(self) -> None:
|
||||
if self.process and self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
@@ -480,17 +458,6 @@ class BridgePanel(tk.Frame):
|
||||
self._bridge_ended()
|
||||
self._on_stopped()
|
||||
|
||||
def _bridge_ended(self) -> None:
|
||||
self.status_var.set("Stopped")
|
||||
self.start_btn.configure(state="normal")
|
||||
self.stop_btn.configure(state="disabled", bg=BG3)
|
||||
self.cap_btn.configure(state="disabled")
|
||||
self.stop_cap_btn.configure(state="disabled", bg=BG3)
|
||||
self.mark_btn.configure(state="disabled")
|
||||
self._capturing = False
|
||||
self._cap_label = None
|
||||
self._append_log("== Bridge stopped ==\n")
|
||||
|
||||
def _reader_thread(self) -> None:
|
||||
if not self.process or not self.process.stdout:
|
||||
return
|
||||
@@ -531,9 +498,7 @@ class BridgePanel(tk.Frame):
|
||||
# ── capture control ───────────────────────────────────────────────────
|
||||
|
||||
def _start_capture(self) -> None:
|
||||
"""Ask for a label and tell the bridge to start writing raw tap files."""
|
||||
if not self.process or self.process.poll() is not None:
|
||||
return
|
||||
"""Ask for a label and start writing raw tap files (serial subprocess or TCP files)."""
|
||||
label = simpledialog.askstring(
|
||||
"New Capture",
|
||||
"Label for this capture\n(e.g. 'recording_mode_continuous').\nLeave blank for timestamp only:",
|
||||
@@ -542,88 +507,6 @@ class BridgePanel(tk.Frame):
|
||||
if label is None:
|
||||
return # user hit Cancel
|
||||
label = label.strip()
|
||||
try:
|
||||
self.process.stdin.write(f"CAP_START:{label}\n")
|
||||
self.process.stdin.flush()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to start capture:\n{e}")
|
||||
return
|
||||
self._capturing = True
|
||||
self._cap_label = label or datetime.datetime.now().strftime("%H%M%S")
|
||||
self.cap_btn.configure(state="disabled")
|
||||
self.stop_cap_btn.configure(state="normal", bg=RED)
|
||||
self.mark_btn.configure(state="normal")
|
||||
self._append_log(f"[CAPTURE] Starting: {self._cap_label!r}...\n")
|
||||
# Add to history as recording (paths filled in when [CAP_START] arrives)
|
||||
self._cap_history.append({"label": self._cap_label, "status": "recording",
|
||||
"bw": None, "s3": None})
|
||||
self._refresh_hist()
|
||||
|
||||
def _stop_capture(self) -> None:
|
||||
"""Tell the bridge to flush and close the current raw tap files."""
|
||||
if not self.process or self.process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
self.process.stdin.write("CAP_STOP\n")
|
||||
self.process.stdin.flush()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to stop capture:\n{e}")
|
||||
# UI is updated when [CAP_STOP] arrives in stdout
|
||||
|
||||
def _on_cap_started_msg(self, bw_path: str, s3_path: str) -> None:
|
||||
"""Called when bridge confirms capture has started (files are open)."""
|
||||
# Fill in paths for the last 'recording' history entry
|
||||
for entry in reversed(self._cap_history):
|
||||
if entry["status"] == "recording" and entry["bw"] is None:
|
||||
entry["bw"] = bw_path
|
||||
entry["s3"] = s3_path
|
||||
break
|
||||
if self._on_cap_started:
|
||||
self._on_cap_started(bw_path, s3_path, self._cap_label or "")
|
||||
|
||||
def _on_cap_stopped_msg(self, bw_path: str, s3_path: str) -> None:
|
||||
"""Called when bridge confirms capture has stopped (files are closed)."""
|
||||
label = self._cap_label or "capture"
|
||||
# Mark history entry as done
|
||||
for entry in reversed(self._cap_history):
|
||||
if entry["status"] == "recording":
|
||||
entry["status"] = "done"
|
||||
entry["bw"] = bw_path
|
||||
entry["s3"] = s3_path
|
||||
break
|
||||
self._refresh_hist()
|
||||
self._capturing = False
|
||||
self._cap_label = None
|
||||
self.cap_btn.configure(state="normal")
|
||||
self.stop_cap_btn.configure(state="disabled", bg=BG3)
|
||||
self._append_log(f"[CAPTURE] Done: {label!r} — ready in Analyzer\n")
|
||||
if self._on_cap_complete:
|
||||
self._on_cap_complete(bw_path, s3_path, label)
|
||||
|
||||
def _refresh_hist(self) -> None:
|
||||
self._hist_lb.delete(0, tk.END)
|
||||
for entry in self._cap_history:
|
||||
icon = "🔴" if entry["status"] == "recording" else "✅"
|
||||
label = entry["label"] or "(unlabeled)"
|
||||
self._hist_lb.insert(tk.END, f" {icon} {label}")
|
||||
if self._cap_history:
|
||||
self._hist_lb.see(tk.END)
|
||||
|
||||
def _on_hist_dblclick(self, _e=None) -> None:
|
||||
sel = self._hist_lb.curselection()
|
||||
if not sel:
|
||||
return
|
||||
entry = self._cap_history[sel[0]]
|
||||
if entry["status"] == "done" and entry["bw"] and entry["s3"]:
|
||||
if self._on_cap_complete:
|
||||
self._on_cap_complete(entry["bw"], entry["s3"], entry["label"])
|
||||
|
||||
# ── mark ──────────────────────────────────────────────────────────────
|
||||
|
||||
def add_mark(self) -> None:
|
||||
if not self.process or not self.process.stdin or self.process.poll() is not None:
|
||||
return
|
||||
label = label.strip()
|
||||
self._capturing = True
|
||||
self._cap_label = label or datetime.datetime.now().strftime("%H%M%S")
|
||||
|
||||
@@ -664,6 +547,7 @@ class BridgePanel(tk.Frame):
|
||||
self._append_log(f"[CAPTURE] Starting: {self._cap_label!r}...\n")
|
||||
|
||||
def _stop_capture(self) -> None:
|
||||
"""Flush and close the current raw tap files (TCP) or signal the bridge subprocess (serial)."""
|
||||
if self._mode.get() == "tcp":
|
||||
with self._tcp_cap_lock:
|
||||
bw_path = self._tcp_cap_bw_path
|
||||
@@ -686,6 +570,7 @@ class BridgePanel(tk.Frame):
|
||||
self.process.stdin.flush()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to stop capture:\n{e}")
|
||||
# UI is updated when [CAP_STOP] arrives in stdout
|
||||
|
||||
# ── TCP mode ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1173,14 +1058,6 @@ class AnalyzerPanel(tk.Frame):
|
||||
self.state.bw_path = bwp
|
||||
self._do_analyze(s3p, bwp)
|
||||
|
||||
def _browse_bin(self) -> None:
|
||||
path = filedialog.askopenfilename(
|
||||
title="Select session .bin file",
|
||||
filetypes=[("Binary", "*.bin"), ("All files", "*.*")],
|
||||
)
|
||||
if path:
|
||||
self.bin_var.set(path)
|
||||
|
||||
def _do_analyze(self, s3_path: Path, bw_path: Path) -> None:
|
||||
self.status_var.set("Parsing...")
|
||||
self.update_idletasks()
|
||||
@@ -1611,7 +1488,6 @@ class AnalyzerPanel(tk.Frame):
|
||||
w.configure(state="normal"); w.insert(tk.END, "\n"); w.configure(state="disabled")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Serial Watch panel — tap the RS-232 line between device and modem
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user