Compare commits
2 Commits
8074bf0fee
...
6a0422a6fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a0422a6fc | |||
| 1078576023 |
@@ -0,0 +1,84 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to seismo-relay are documented here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.5.0 — 2026-03-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Console tab in `seismo_lab.py`** — direct device connection without the bridge subprocess.
|
||||||
|
- Serial and TCP transport selectable via radio buttons.
|
||||||
|
- Four one-click commands: POLL, Serial #, Full Config, Event Index.
|
||||||
|
- Colour-coded scrolling output: TX (blue), RX raw hex (teal), parsed/decoded (green), errors (red).
|
||||||
|
- Save Log and Send to Analyzer buttons; logs auto-saved to `bridges/captures/console_<ts>.log`.
|
||||||
|
- Queue/`after(100)` pattern — no UI blocking or performance impact.
|
||||||
|
- **`minimateplus` package** — clean Python client library for the MiniMate Plus S3 protocol.
|
||||||
|
- `SerialTransport` and `TcpTransport` (for Sierra Wireless RV50/RV55 cellular modems).
|
||||||
|
- `MiniMateProtocol` — DLE frame parser/builder, two-step paged reads, checksum validation.
|
||||||
|
- `MiniMateClient` — high-level client: `connect()`, `get_serial()`, `get_config()`, `get_events()`.
|
||||||
|
- **TCP/cellular transport** (`TcpTransport`) — connect to field units via Sierra Wireless RV50/RV55 modems over cellular.
|
||||||
|
- `read_until_idle(idle_gap=1.5s)` to handle modem data-forwarding buffer delay.
|
||||||
|
- Confirmed working end-to-end: TCP → RV50/RV55 → RS-232 → MiniMate Plus.
|
||||||
|
- **`bridges/tcp_serial_bridge.py`** — local TCP-to-serial bridge for bench testing `TcpTransport` without a cellular modem.
|
||||||
|
- **SFM REST server** (`sfm/server.py`) — FastAPI server with device info, event list, and event record endpoints over both serial and TCP.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `protocol.py` `startup()` was using a hardcoded `POLL_RECV_TIMEOUT = 10.0` constant, ignoring the configurable `self._recv_timeout`. Fixed to use `self._recv_timeout` throughout.
|
||||||
|
- `sfm/server.py` now retries once on `ProtocolError` for TCP connections to handle cold-boot timing on first connect.
|
||||||
|
|
||||||
|
### Protocol / Documentation
|
||||||
|
- **Sierra Wireless RV50/RV55 modem config** — confirmed required ACEmanager settings: Quiet Mode = Enable, Data Forwarding Timeout = 1, TCP Connect Response Delay = 0. Quiet Mode disabled causes modem to inject `RING\r\nCONNECT\r\n` onto the serial line, breaking the S3 handshake.
|
||||||
|
- **Calibration year** confirmed at SUB FE (Full Config) destuffed payload offset 0x56–0x57 (uint16 BE). `0x07E7` = 2023, `0x07E9` = 2025.
|
||||||
|
- **`"Operating System"` boot string** — 16-byte UART boot message captured on cold-start before unit enters DLE-framed mode. Parser handles correctly by scanning for DLE+STX.
|
||||||
|
- RV50/RV55 sends `RING`/`CONNECT` over TCP to the calling client even with Quiet Mode enabled — this is normal behaviour, parser discards it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.4.0 — 2026-03-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`seismo_lab.py`** — combined Bridge + Analyzer GUI. Single window with two tabs; bridge start auto-wires live mode in the Analyzer.
|
||||||
|
- **`frame_db.py`** — SQLite frame database. Captures accumulate over time; Query DB tab searches across all sessions.
|
||||||
|
- **`bridges/s3-bridge/proxy.py`** — bridge proxy module.
|
||||||
|
- Large BW→S3 write frame checksum algorithm confirmed and implemented (`SUM8` of payload `[2:-1]` skipping `0x10` bytes, plus constant `0x10`, mod 256).
|
||||||
|
- SUB `A4` identified as composite container frame with embedded inner frames; `_extract_a4_inner_frames()` and `_diff_a4_payloads()` reduce diff noise from 2300 → 17 meaningful entries.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- BAD CHK false positives on BW POLL frames — BW frame terminator `03 41` was being included in the de-stuffed payload. Fixed to strip correctly.
|
||||||
|
- Aux Trigger read location confirmed at SUB FE offset `0x0109`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.3.0 — 2026-03-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Record time confirmed at SUB E5 page2 offset `+0x28` as float32 BE.
|
||||||
|
- Trigger Sample Width confirmed at BW→S3 write frame SUB `0x82`, destuffed payload offset `[22]`.
|
||||||
|
- Mode-gating documented: several settings only appear on the wire when the appropriate mode is active.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `0x082A` mystery resolved — fixed-size E5 payload length (2090 bytes), not a record-time field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.2.0 — 2026-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Channel config float layout fully confirmed: trigger level, alarm level, and unit string per channel (IEEE 754 BE floats).
|
||||||
|
- Blastware `.set` file format decoded — little-endian binary struct mirroring the wire payload.
|
||||||
|
- Operator manual (716U0101 Rev 15) added as cross-reference source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.1.0 — 2026-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial `s3_bridge.py` serial bridge — transparent RS-232 tap between Blastware and MiniMate Plus.
|
||||||
|
- `s3_parser.py` — deterministic DLE state machine frame extractor.
|
||||||
|
- `s3_analyzer.py` — session parser, frame differ, Claude export.
|
||||||
|
- `gui_bridge.py` and `gui_analyzer.py` — Tkinter GUIs.
|
||||||
|
- DLE framing confirmed: `DLE+STX` / `DLE+ETX`, `0x41` = ACK (not STX), DLE stuffing rule.
|
||||||
|
- Response SUB rule confirmed: `response_SUB = 0xFF - request_SUB`.
|
||||||
|
- Year `0x07CB` = 1995 confirmed as MiniMate factory RTC default.
|
||||||
|
- Full write command family documented (SUBs `68`–`83`).
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
# seismo-relay
|
# seismo-relay `v0.5.0`
|
||||||
|
|
||||||
Tools for capturing and reverse-engineering the RS-232 serial protocol between
|
A ground-up replacement for **Blastware** — Instantel's aging Windows-only
|
||||||
**Blastware** software and **Instantel MiniMate Plus** seismographs.
|
software for managing MiniMate Plus seismographs.
|
||||||
|
|
||||||
Built for Windows, stdlib-only (plus `pyserial` for the bridge).
|
Built in Python. Runs on Windows. Connects to instruments over direct RS-232
|
||||||
|
or cellular modem (Sierra Wireless RV50 / RV55).
|
||||||
|
|
||||||
|
> **Status:** Active development. Core read pipeline working (device info,
|
||||||
|
> config, event index). Event download and write commands in progress.
|
||||||
|
> See [CHANGELOG.md](CHANGELOG.md) for version history.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,217 +16,177 @@ Built for Windows, stdlib-only (plus `pyserial` for the bridge).
|
|||||||
|
|
||||||
```
|
```
|
||||||
seismo-relay/
|
seismo-relay/
|
||||||
|
├── seismo_lab.py ← Main GUI (Bridge + Analyzer + Console tabs)
|
||||||
|
│
|
||||||
|
├── minimateplus/ ← MiniMate Plus client library
|
||||||
|
│ ├── transport.py ← SerialTransport and TcpTransport
|
||||||
|
│ ├── protocol.py ← DLE frame layer (read/write/parse)
|
||||||
|
│ ├── client.py ← High-level client (connect, get_config, etc.)
|
||||||
|
│ ├── framing.py ← Frame builder/parser primitives
|
||||||
|
│ └── models.py ← DeviceInfo, EventRecord, etc.
|
||||||
|
│
|
||||||
|
├── sfm/ ← SFM REST API server (FastAPI)
|
||||||
|
│ └── server.py ← /device/info, /device/events, /device/event
|
||||||
|
│
|
||||||
├── bridges/
|
├── bridges/
|
||||||
│ ├── s3-bridge/
|
│ ├── s3-bridge/
|
||||||
│ │ └── s3_bridge.py ← The serial bridge (core capture tool)
|
│ │ └── s3_bridge.py ← RS-232 serial bridge (capture tool)
|
||||||
│ ├── gui_bridge.py ← Tkinter GUI wrapper for s3_bridge
|
│ ├── tcp_serial_bridge.py ← Local TCP↔serial bridge (bench testing)
|
||||||
│ └── raw_capture.py ← Simpler raw-only capture tool
|
│ ├── gui_bridge.py ← Standalone bridge GUI (legacy)
|
||||||
└── parsers/
|
│ └── raw_capture.py ← Simple raw capture tool
|
||||||
├── s3_parser.py ← Low-level DLE frame extractor
|
│
|
||||||
├── s3_analyzer.py ← Protocol analyzer (sessions, diffs, exports)
|
├── parsers/
|
||||||
├── gui_analyzer.py ← Tkinter GUI for the analyzer
|
│ ├── s3_parser.py ← DLE frame extractor
|
||||||
└── frame_db.py ← SQLite frame database
|
│ ├── s3_analyzer.py ← Session parser, differ, Claude export
|
||||||
|
│ ├── gui_analyzer.py ← Standalone analyzer GUI (legacy)
|
||||||
|
│ └── frame_db.py ← SQLite frame database
|
||||||
|
│
|
||||||
|
└── docs/
|
||||||
|
└── instantel_protocol_reference.md ← Reverse-engineered protocol spec
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How it all fits together
|
## Quick start
|
||||||
|
|
||||||
The workflow has two phases: **capture**, then **analyze**.
|
### Seismo Lab (main GUI)
|
||||||
|
|
||||||
|
The all-in-one tool. Three tabs: **Bridge**, **Analyzer**, **Console**.
|
||||||
|
|
||||||
```
|
```
|
||||||
Blastware PC
|
python seismo_lab.py
|
||||||
│
|
|
||||||
Virtual COM (e.g. COM4)
|
|
||||||
│
|
|
||||||
s3_bridge.py ←─── sits in the middle, forwards all bytes both ways
|
|
||||||
│ writes raw_bw.bin and raw_s3.bin
|
|
||||||
Physical COM (e.g. COM5)
|
|
||||||
│
|
|
||||||
MiniMate Plus seismograph
|
|
||||||
```
|
```
|
||||||
|
|
||||||
After capturing, you point the analyzer at the two `.bin` files to inspect
|
### SFM REST server
|
||||||
what happened.
|
|
||||||
|
Exposes MiniMate Plus commands as a REST API for integration with other systems.
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sfm
|
||||||
|
uvicorn server:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
|
||||||
|
| Method | URL | Description |
|
||||||
|
|--------|-----|-------------|
|
||||||
|
| `GET` | `/device/info?port=COM5` | Device info via serial |
|
||||||
|
| `GET` | `/device/info?host=1.2.3.4&tcp_port=9034` | Device info via cellular modem |
|
||||||
|
| `GET` | `/device/events?port=COM5` | Event index |
|
||||||
|
| `GET` | `/device/event?port=COM5&index=0` | Single event record |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Part 1 — The Bridge
|
## Seismo Lab tabs
|
||||||
|
|
||||||
### `s3_bridge.py` — Serial bridge
|
### Bridge tab
|
||||||
|
|
||||||
Transparently forwards bytes between Blastware and the seismograph while
|
Captures live RS-232 traffic between Blastware and the seismograph. Sits in
|
||||||
logging everything to disk. Blastware operates normally and has no idea the
|
the middle as a transparent pass-through while logging everything to disk.
|
||||||
bridge is there.
|
|
||||||
|
|
||||||
**Run it:**
|
|
||||||
```
|
```
|
||||||
python bridges/s3-bridge/s3_bridge.py --bw COM4 --s3 COM5 --logdir captures/
|
Blastware → COM4 (virtual) ↔ s3_bridge ↔ COM5 (physical) → MiniMate Plus
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key flags:**
|
Set your COM ports and log directory, then hit **Start Bridge**. Use
|
||||||
| Flag | Default | Description |
|
**Add Mark** to annotate the capture at specific moments (e.g. "changed
|
||||||
|------|---------|-------------|
|
trigger level"). When the bridge starts, the Analyzer tab automatically wires
|
||||||
| `--bw` | required | COM port connected to Blastware |
|
up to the live files and starts updating in real time.
|
||||||
| `--s3` | required | COM port connected to the seismograph |
|
|
||||||
| `--baud` | 38400 | Baud rate (match your device) |
|
|
||||||
| `--logdir` | `.` | Where to write log/bin files |
|
|
||||||
| `--raw-bw` | off | Also write a flat raw file for BW→S3 traffic |
|
|
||||||
| `--raw-s3` | off | Also write a flat raw file for S3→BW traffic |
|
|
||||||
|
|
||||||
**Output files (in `--logdir`):**
|
### Analyzer tab
|
||||||
- `s3_session_<timestamp>.bin` — structured binary log with timestamps
|
|
||||||
and direction tags (record format: `[type:1][ts_us:8][len:4][payload]`)
|
|
||||||
- `s3_session_<timestamp>.log` — human-readable hex dump (text)
|
|
||||||
- `raw_bw.bin` — flat BW→S3 byte stream (if `--raw-bw` used)
|
|
||||||
- `raw_s3.bin` — flat S3→BW byte stream (if `--raw-s3` used)
|
|
||||||
|
|
||||||
> The analyzer needs `raw_bw.bin` + `raw_s3.bin`. Always use `--raw-bw` and
|
Parses raw captures into DLE-framed protocol sessions, diffs consecutive
|
||||||
> `--raw-s3` when capturing.
|
sessions to show exactly which bytes changed, and lets you query across all
|
||||||
|
historical captures via the built-in SQLite database.
|
||||||
|
|
||||||
**Interactive commands** (type while bridge is running):
|
- **Inventory** — all frames in a session, click to drill in
|
||||||
- `m` + Enter → prompts for a label and inserts a MARK record into the log
|
- **Hex Dump** — full payload hex dump with changed-byte annotations
|
||||||
- `q` + Enter → quit
|
- **Diff** — byte-level before/after diff between sessions
|
||||||
|
- **Full Report** — plain text session report
|
||||||
|
- **Query DB** — search across all captures by SUB, direction, or byte value
|
||||||
|
|
||||||
|
Use **Export for Claude** to generate a self-contained `.md` report for
|
||||||
|
AI-assisted field mapping.
|
||||||
|
|
||||||
|
### Console tab
|
||||||
|
|
||||||
|
Direct connection to a MiniMate Plus — no bridge, no Blastware. Useful for
|
||||||
|
diagnosing field units over cellular without a full capture session.
|
||||||
|
|
||||||
|
**Connection:** choose Serial (COM port + baud) or TCP (IP + port for
|
||||||
|
cellular modem).
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
| Button | What it does |
|
||||||
|
|--------|-------------|
|
||||||
|
| POLL | Startup handshake — confirms unit is alive and identifies model |
|
||||||
|
| Serial # | Reads unit serial number |
|
||||||
|
| Full Config | Reads full 166-byte config block (firmware version, channel scales, etc.) |
|
||||||
|
| Event Index | Reads stored event list |
|
||||||
|
|
||||||
|
Output is colour-coded: TX in blue, raw RX bytes in teal, decoded fields in
|
||||||
|
green, errors in red. **Save Log** writes a timestamped `.log` file to
|
||||||
|
`bridges/captures/`. **Send to Analyzer** injects the captured bytes into the
|
||||||
|
Analyzer tab for deeper inspection.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `gui_bridge.py` — Bridge GUI
|
## Connecting over cellular (RV50 / RV55 modems)
|
||||||
|
|
||||||
A simple point-and-click wrapper around `s3_bridge.py`. Easier than the
|
Field units connect via Sierra Wireless RV50 or RV55 cellular modems. Use
|
||||||
command line if you don't want to type flags every time.
|
TCP mode in the Console or SFM:
|
||||||
|
|
||||||
```
|
```
|
||||||
python bridges/gui_bridge.py
|
# Console tab
|
||||||
|
Transport: TCP
|
||||||
|
Host: <modem public IP>
|
||||||
|
Port: 9034 ← Device Port in ACEmanager (call-up mode)
|
||||||
```
|
```
|
||||||
|
|
||||||
Set your COM ports, log directory, and tick the raw tap checkboxes before
|
```python
|
||||||
hitting **Start**. The **Add Mark** button lets you annotate the capture
|
# In code
|
||||||
at any point (e.g. "changed record time to 13s").
|
from minimateplus.transport import TcpTransport
|
||||||
|
from minimateplus.client import MiniMateClient
|
||||||
|
|
||||||
|
client = MiniMateClient(transport=TcpTransport("1.2.3.4", 9034))
|
||||||
|
info = client.connect()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required ACEmanager settings (Serial tab)
|
||||||
|
|
||||||
|
These must match exactly — a single wrong setting causes the unit to beep
|
||||||
|
on connect but never respond:
|
||||||
|
|
||||||
|
| Setting | Value | Why |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| Configure Serial Port | `38400,8N1` | Must match MiniMate baud rate |
|
||||||
|
| Flow Control | `None` | Hardware flow control blocks unit TX if pins unconnected |
|
||||||
|
| **Quiet Mode** | **Enable** | **Critical.** Disabled → modem injects `RING`/`CONNECT` onto serial line, corrupting the S3 handshake |
|
||||||
|
| Data Forwarding Timeout | `1` (= 0.1 s) | Lower latency; `5` works but is sluggish |
|
||||||
|
| TCP Connect Response Delay | `0` | Non-zero silently drops the first POLL frame |
|
||||||
|
| TCP Idle Timeout | `2` (minutes) | Prevents premature disconnect |
|
||||||
|
| DB9 Serial Echo | `Disable` | Echo corrupts the data stream |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Part 2 — The Analyzer
|
## minimateplus library
|
||||||
|
|
||||||
After capturing, you have `raw_bw.bin` (bytes Blastware sent) and `raw_s3.bin`
|
```python
|
||||||
(bytes the seismograph replied with). The analyzer parses these into protocol
|
from minimateplus import MiniMateClient
|
||||||
frames, groups them into sessions, and helps you figure out what each byte means.
|
from minimateplus.transport import SerialTransport, TcpTransport
|
||||||
|
|
||||||
### What's a "session"?
|
# Serial
|
||||||
|
client = MiniMateClient(port="COM5")
|
||||||
|
|
||||||
Each time you open the settings dialog in Blastware and click Apply/OK, that's
|
# TCP (cellular modem)
|
||||||
one session — a complete read/modify/write cycle. The bridge detects session
|
client = MiniMateClient(transport=TcpTransport("1.2.3.4", 9034), timeout=30.0)
|
||||||
boundaries by watching for the final write-confirm packet (SUB `0x74`).
|
|
||||||
|
|
||||||
Each session contains a sequence of request/response frame pairs:
|
|
||||||
- Blastware sends a **request** (BW→S3): "give me your config block"
|
|
||||||
- The seismograph sends a **response** (S3→BW): here it is
|
|
||||||
- At the end, Blastware sends the modified settings back in a series of write packets
|
|
||||||
|
|
||||||
The analyzer lines these up and diffs consecutive sessions to show you exactly
|
|
||||||
which bytes changed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `gui_analyzer.py` — Analyzer GUI
|
|
||||||
|
|
||||||
|
with client:
|
||||||
|
info = client.connect() # DeviceInfo — model, serial, firmware
|
||||||
|
serial = client.get_serial() # Serial number string
|
||||||
|
config = client.get_config() # Full config block (bytes)
|
||||||
|
events = client.get_events() # Event index
|
||||||
```
|
```
|
||||||
python parsers/gui_analyzer.py
|
|
||||||
```
|
|
||||||
|
|
||||||
This is the main tool. It has five tabs:
|
|
||||||
|
|
||||||
#### Toolbar
|
|
||||||
- **S3 raw / BW raw** — browse to your `raw_s3.bin` and `raw_bw.bin` files
|
|
||||||
- **Analyze** — parse and load the captures
|
|
||||||
- **Live: OFF/ON** — watch the files grow in real time while the bridge is running
|
|
||||||
- **Export for Claude** — generate a self-contained `.md` report for AI-assisted analysis
|
|
||||||
|
|
||||||
#### Inventory tab
|
|
||||||
Shows all frames in the selected session — direction, SUB command, page,
|
|
||||||
length, and checksum status. Click any frame in the left tree to drill in.
|
|
||||||
|
|
||||||
#### Hex Dump tab
|
|
||||||
Full hex dump of the selected frame's payload. If the frame had changed bytes
|
|
||||||
vs the previous session, those are listed below the dump with before/after values
|
|
||||||
and field names where known.
|
|
||||||
|
|
||||||
#### Diff tab
|
|
||||||
Side-by-side byte-level diff between the current session and the previous one.
|
|
||||||
Only SUBs (command types) that actually changed are shown.
|
|
||||||
|
|
||||||
#### Full Report tab
|
|
||||||
Raw text version of the session report — useful for copying into notes.
|
|
||||||
|
|
||||||
#### Query DB tab
|
|
||||||
Search across all your captured sessions using the built-in database.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `s3_analyzer.py` — Analyzer (command line)
|
|
||||||
|
|
||||||
If you prefer the terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
python parsers/s3_analyzer.py --s3 raw_s3.bin --bw raw_bw.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
**Flags:**
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `--s3` | Path to raw_s3.bin |
|
|
||||||
| `--bw` | Path to raw_bw.bin |
|
|
||||||
| `--live` | Tail files in real time (poll mode) |
|
|
||||||
| `--export` | Also write a `claude_export_<ts>.md` file |
|
|
||||||
| `--outdir` | Where to write `.report` files (default: same folder as input) |
|
|
||||||
| `--poll` | Live mode poll interval in seconds (default: 0.05) |
|
|
||||||
|
|
||||||
Writes one `.report` file per session and prints a summary to the console.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Frame Database
|
|
||||||
|
|
||||||
Every time you click **Analyze**, the frames are automatically saved to a
|
|
||||||
SQLite database at:
|
|
||||||
|
|
||||||
```
|
|
||||||
C:\Users\<you>\.seismo_lab\frames.db
|
|
||||||
```
|
|
||||||
|
|
||||||
This accumulates captures over time so you can query across sessions and dates.
|
|
||||||
|
|
||||||
### Query DB tab
|
|
||||||
|
|
||||||
Use the filter bar to search:
|
|
||||||
- **Capture** — narrow to a specific capture (timestamp shown)
|
|
||||||
- **Dir** — BW (requests) or S3 (responses) only
|
|
||||||
- **SUB** — filter by command type (e.g. `0xF7` = EVENT_INDEX_RESPONSE)
|
|
||||||
- **Offset** — filter to frames that have a specific byte offset
|
|
||||||
- **Value** — combined with Offset: "show frames where byte 85 = 0x0A"
|
|
||||||
|
|
||||||
Click any result row, then use the **Byte interpretation** panel at the bottom
|
|
||||||
to see what that offset's bytes look like as uint8, int8, uint16 BE/LE,
|
|
||||||
uint32 BE/LE, and float32 BE/LE simultaneously.
|
|
||||||
|
|
||||||
This is the main tool for mapping unknown fields — if you change one setting in
|
|
||||||
Blastware, capture before and after, then query for frames where that offset
|
|
||||||
moved, you can pin down exactly which byte controls what.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Export for Claude
|
|
||||||
|
|
||||||
The **Export for Claude** button (orange, in the toolbar) generates a single
|
|
||||||
`.md` file containing:
|
|
||||||
|
|
||||||
1. Protocol background and known field map
|
|
||||||
2. Capture summary (session count, frame counts, what changed)
|
|
||||||
3. Per-diff tables — before/after bytes for every changed offset, with field
|
|
||||||
names where known
|
|
||||||
4. Full hex dumps of all frames in the baseline session
|
|
||||||
|
|
||||||
Paste this file into a Claude conversation to get help mapping unknown fields,
|
|
||||||
interpreting data structures, or understanding sequences.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -232,47 +197,55 @@ interpreting data structures, or understanding sequences.
|
|||||||
| DLE | `0x10` | Data Link Escape |
|
| DLE | `0x10` | Data Link Escape |
|
||||||
| STX | `0x02` | Start of frame |
|
| STX | `0x02` | Start of frame |
|
||||||
| ETX | `0x03` | End of frame |
|
| ETX | `0x03` | End of frame |
|
||||||
| ACK | `0x41` | Frame start marker (BW side) |
|
| ACK | `0x41` (`'A'`) | Frame-start marker sent before every frame |
|
||||||
| DLE stuffing | `10 10` on wire | Literal `0x10` in payload |
|
| DLE stuffing | `10 10` on wire | Literal `0x10` in payload |
|
||||||
|
|
||||||
**S3-side frame** (seismograph → Blastware): `DLE STX [payload] DLE ETX`
|
**S3-side frame** (seismograph → Blastware): `ACK DLE+STX [payload] CHK DLE+ETX`
|
||||||
**BW-side frame** (Blastware → seismograph): `ACK STX [payload] ETX`
|
|
||||||
|
|
||||||
**De-stuffed payload header** (first 5 bytes after de-stuffing):
|
**De-stuffed payload header:**
|
||||||
```
|
```
|
||||||
[0] CMD 0x10 = BW request, 0x00 = S3 response
|
[0] CMD 0x10 = BW request, 0x00 = S3 response
|
||||||
[1] ? 0x00 (BW) or 0x10 (S3)
|
[1] ? unknown (0x00 BW / 0x10 S3)
|
||||||
[2] SUB Command/response identifier ← the key field
|
[2] SUB Command/response identifier ← the key field
|
||||||
[3] OFFSET_HI Page address high byte
|
[3] PAGE_HI Page address high byte
|
||||||
[4] OFFSET_LO Page address low byte
|
[4] PAGE_LO Page address low byte
|
||||||
[5+] DATA Payload content
|
[5+] DATA Payload content
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response SUB rule:** `response_SUB = 0xFF - request_SUB`
|
**Response SUB rule:** `response_SUB = 0xFF - request_SUB`
|
||||||
Example: request SUB `0x08` → response SUB `0xF7`
|
Example: request SUB `0x08` (Event Index) → response SUB `0xF7`
|
||||||
|
|
||||||
|
Full protocol documentation: [`docs/instantel_protocol_reference.md`](docs/instantel_protocol_reference.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install pyserial
|
pip install pyserial fastapi uvicorn
|
||||||
```
|
```
|
||||||
|
|
||||||
Python 3.10+. Everything else is stdlib (Tkinter, sqlite3, struct, hashlib).
|
Python 3.10+. Tkinter is included with the standard Python installer on
|
||||||
|
Windows (make sure "tcl/tk and IDLE" is checked during install).
|
||||||
Tkinter is included with the standard Python installer on Windows. If it's
|
|
||||||
missing, reinstall Python and make sure "tcl/tk and IDLE" is checked.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Virtual COM ports
|
## Virtual COM ports (bridge capture)
|
||||||
|
|
||||||
The bridge needs two COM ports on the same PC — one that Blastware connects to,
|
The bridge needs two COM ports on the same PC — one that Blastware connects
|
||||||
and one wired to the actual seismograph. On Windows, use a virtual COM port pair
|
to, and one wired to the seismograph. Use a virtual COM port pair
|
||||||
(e.g. **com0com** or **VSPD**) to give Blastware a port to talk to while the
|
(**com0com** or **VSPD**) to give Blastware a port to talk to.
|
||||||
bridge sits in the middle.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Blastware → COM4 (virtual) ↔ s3_bridge ↔ COM5 (physical) → MiniMate
|
Blastware → COM4 (virtual) ↔ s3_bridge.py ↔ COM5 (physical) → MiniMate Plus
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Event download — pull waveform records from the unit (SUBs `1E` → `0A` → `0C` → `5A`)
|
||||||
|
- [ ] Write commands — push config changes to the unit (compliance setup, channel config, trigger settings)
|
||||||
|
- [ ] ACH inbound server — accept call-home connections from field units
|
||||||
|
- [ ] Modem manager — push standard configs to RV50/RV55 fleet via Sierra Wireless API
|
||||||
|
- [ ] Full Blastware parity — complete read/write/download cycle without Blastware
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
| 2026-03-30 | §3, §5.1 | **CONFIRMED — BW→S3 two-step read offset is at payload[5], NOT payload[3:4].** All BW read-command frames have `payload[3] = 0x00` and `payload[4] = 0x00` unconditionally. The two-step offset byte lives at `payload[5]`: `0x00` for the length-probe step, `DATA_LEN` for the data-fetch step. Validated against all captured frames in `bridges/captures/3-11-26/raw_bw_*.bin` — every frame is an exact bit-for-bit match when built with offset at `[5]`. The `page_hi`/`page_lo` framing in the docstring was a misattribution from the S3-side response layout (where `[3]`/`[4]` ARE page bytes). |
|
| 2026-03-30 | §3, §5.1 | **CONFIRMED — BW→S3 two-step read offset is at payload[5], NOT payload[3:4].** All BW read-command frames have `payload[3] = 0x00` and `payload[4] = 0x00` unconditionally. The two-step offset byte lives at `payload[5]`: `0x00` for the length-probe step, `DATA_LEN` for the data-fetch step. Validated against all captured frames in `bridges/captures/3-11-26/raw_bw_*.bin` — every frame is an exact bit-for-bit match when built with offset at `[5]`. The `page_hi`/`page_lo` framing in the docstring was a misattribution from the S3-side response layout (where `[3]`/`[4]` ARE page bytes). |
|
||||||
| 2026-03-30 | §4, §5.2 | **CONFIRMED — S3 probe response page_key is always 0x0000.** The S3 response to a length-probe step does NOT carry the data length back in `page_hi`/`page_lo`. Both bytes are `0x00` in every observed probe response. Data lengths for each SUB are fixed constants (see §5.1 table). The `minimateplus` library now uses a hardcoded `DATA_LENGTHS` dict rather than trying to read the length from the probe response. |
|
| 2026-03-30 | §4, §5.2 | **CONFIRMED — S3 probe response page_key is always 0x0000.** The S3 response to a length-probe step does NOT carry the data length back in `page_hi`/`page_lo`. Both bytes are `0x00` in every observed probe response. Data lengths for each SUB are fixed constants (see §5.1 table). The `minimateplus` library now uses a hardcoded `DATA_LENGTHS` dict rather than trying to read the length from the probe response. |
|
||||||
| 2026-03-31 | §12 TCP Transport | **NEW SECTION — TCP/modem transport confirmed transparent from Blastware Operator Manual (714U0301 Rev 22).** Key facts confirmed: (1) Protocol bytes over TCP are bit-for-bit identical to RS-232 — no handshake framing. (2) No ENQ byte on TCP connect (`Enable ENQ on TCP Connect: 0-Disable` in Raven ACEmanager). (3) Raven modem `Data Forwarding Timeout = 1 second` — modem buffers serial bytes up to 1s before forwarding over TCP; `TcpTransport.read_until_idle` uses `idle_gap=1.5s` to compensate. (4) TCP port is user-configurable (12335 in manual example; user's install uses 12345). (5) Baud rate over serial link to modem is 38400,8N1 regardless of TCP path. (6) ACH (Auto Call Home) = INBOUND to server (unit calls home); "call up" = OUTBOUND from client (Blastware/SFM connects to modem IP). `TcpTransport` implements outbound (call-up) mode. |
|
| 2026-03-31 | §12 TCP Transport | **NEW SECTION — TCP/modem transport confirmed transparent from Blastware Operator Manual (714U0301 Rev 22).** Key facts confirmed: (1) Protocol bytes over TCP are bit-for-bit identical to RS-232 — no handshake framing. (2) No ENQ byte on TCP connect (`Enable ENQ on TCP Connect: 0-Disable` in Raven ACEmanager). (3) Raven modem `Data Forwarding Timeout = 1 second` — modem buffers serial bytes up to 1s before forwarding over TCP; `TcpTransport.read_until_idle` uses `idle_gap=1.5s` to compensate. (4) TCP port is user-configurable (12335 in manual example; user's install uses 12345). (5) Baud rate over serial link to modem is 38400,8N1 regardless of TCP path. (6) ACH (Auto Call Home) = INBOUND to server (unit calls home); "call up" = OUTBOUND from client (Blastware/SFM connects to modem IP). `TcpTransport` implements outbound (call-up) mode. |
|
||||||
|
| 2026-03-31 | §14.3 | **NEW — Sierra Wireless RV50/RV55 Quiet Mode requirement confirmed.** Quiet Mode (ATQ) must be **enabled** on the serial port. When disabled (+ Verbose mode on), the modem injects `RING\r\nCONNECT\r\n` onto the RS-232 serial line at connection time — MiniMate receives unexpected bytes, loses protocol sync, and never responds to POLL (unit beeps but returns no S3 frame). Working RV50 field config: Quiet Mode enabled, Data Forwarding Timeout=1, TCP Connect Response Delay=0. Misconfigured RV55 had all three wrong. |
|
||||||
|
| 2026-03-31 | §14.2 | **CORRECTED — Sierra Wireless RV50/RV55 sends `RING`/`CONNECT` over TCP to caller even with Quiet Mode enabled.** Quiet Mode suppresses these only on the serial port (protecting the MiniMate). TCP client still receives `\r\nRING\r\n\r\nCONNECT\r\n` prefixed before the first S3 frame bytes. Parser handles correctly by scanning for DLE+STX (`0x10 0x02`) and discarding prefix bytes. Previous note "no CONNECT string" described Raven X ENQ-disable behaviour; RV50/RV55 differ. |
|
||||||
|
| 2026-03-31 | §7.3 | **NEW — Calibration date field confirmed** at Full Config (SUB FE) destuffed payload offsets 0x53–0x57. Two-unit comparison: BE18189 (calibrated 2023) has `07 E7` at 0x56–0x57; BE11529 (calibrated 2025) has `07 E9`. Bytes 0x56–0x57 = uint16 BE calibration year ✅ CONFIRMED. Adjacent bytes at 0x53–0x55 likely encode month/day (both units show `0x10` at offset 0x54 = BCD October; 0x53 and 0x55 differ between units). Full date layout 🔶 INFERRED — pending third-unit capture or recalibration diff. Resolves open question. |
|
||||||
|
| 2026-03-31 | §9 | **CONFIRMED via Console cold-start capture** — `"Operating System"` (16 B: `4f 70 65 72 61 74 69 6e 67 20 53 79 73 74 65 6d`) arrives as first TCP bytes on cold-connect before unit enters DLE-framed mode. `TcpTransport` + retry logic handles gracefully: first attempt times out waiting for SUB A4; second connect (after unit fully booted) succeeds. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -357,6 +361,10 @@ Unit 2: serial="BE11529" trail=70 11 firmware=S337.17
|
|||||||
| 0x1C | `3F 80 00 00` ×6 | IEEE 754 float = **1.0** ×6 (remaining channel scales) | 🔶 INFERRED |
|
| 0x1C | `3F 80 00 00` ×6 | IEEE 754 float = **1.0** ×6 (remaining channel scales) | 🔶 INFERRED |
|
||||||
| 0x34 | `53 33 33 37 2E 31 37 00` | `"S337.17\x00"` — Firmware version | ✅ CONFIRMED |
|
| 0x34 | `53 33 33 37 2E 31 37 00` | `"S337.17\x00"` — Firmware version | ✅ CONFIRMED |
|
||||||
| 0x3C | `31 30 2E 37 32 00` | `"10.72\x00"` — DSP / secondary firmware version | ✅ CONFIRMED |
|
| 0x3C | `31 30 2E 37 32 00` | `"10.72\x00"` — DSP / secondary firmware version | ✅ CONFIRMED |
|
||||||
|
| 0x53 | varies | Likely calibration day or time field — 0x15 (BE18189), 0x1D (BE11529) | 🔶 INFERRED |
|
||||||
|
| 0x54 | `10` | Calibration month — BCD `0x10` = October (both units) | 🔶 INFERRED |
|
||||||
|
| 0x55 | varies | Calibration day — `0x02` (BE18189), `0x04` (BE11529) | 🔶 INFERRED |
|
||||||
|
| 0x56–0x57 | `07 E7` / `07 E9` | Calibration year — uint16 BE. `0x07E7`=2023, `0x07E9`=2025 | ✅ CONFIRMED — 2026-03-31 |
|
||||||
| 0x44 | `49 6E 73 74 61 6E 74 65 6C...` | `"Instantel"` — Manufacturer (repeated) | ✅ CONFIRMED |
|
| 0x44 | `49 6E 73 74 61 6E 74 65 6C...` | `"Instantel"` — Manufacturer (repeated) | ✅ CONFIRMED |
|
||||||
| 0x6D | `4D 69 6E 69 4D 61 74 65 20 50 6C 75 73` | `"MiniMate Plus"` — Model name | ✅ CONFIRMED |
|
| 0x6D | `4D 69 6E 69 4D 61 74 65 20 50 6C 75 73` | `"MiniMate Plus"` — Model name | ✅ CONFIRMED |
|
||||||
|
|
||||||
@@ -937,7 +945,11 @@ Enable ENQ on TCP Connect: 0-Disable
|
|||||||
|
|
||||||
When a TCP connection is established (in either direction), **no ENQ byte or other handshake marker is sent** by the modem before the protocol stream starts. The first byte from either side is a raw protocol byte — for SFM-initiated call-up, SFM sends POLL_PROBE immediately after `connect()`.
|
When a TCP connection is established (in either direction), **no ENQ byte or other handshake marker is sent** by the modem before the protocol stream starts. The first byte from either side is a raw protocol byte — for SFM-initiated call-up, SFM sends POLL_PROBE immediately after `connect()`.
|
||||||
|
|
||||||
No banner, no "CONNECT" string, no Telnet negotiation preamble. The Raven modem's TCP dialog is configured with:
|
**Sierra Wireless RV50/RV55 note:** Even with Quiet Mode enabled, these modems send `\r\nRING\r\n\r\nCONNECT\r\n` over the TCP connection to the calling client at connect time. Quiet Mode only suppresses these strings on the *serial* port (protecting the MiniMate Plus). The TCP client must tolerate these prefix bytes — scan for DLE+STX (`0x10 0x02`) and discard everything before it. This is the same approach used for the `"Operating System"` boot string (§9).
|
||||||
|
|
||||||
|
The Raven X (deprecated) did not exhibit this behaviour. The note below about "no CONNECT string" describes Raven X with ENQ-disable; it does **not** apply to RV50/RV55.
|
||||||
|
|
||||||
|
No ENQ byte or other application-layer handshake is added. The Raven modem's TCP dialog is configured with:
|
||||||
|
|
||||||
| ACEmanager Setting | Value | Meaning |
|
| ACEmanager Setting | Value | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -971,6 +983,21 @@ The **Data Forwarding Timeout** is the most protocol-critical setting. The mode
|
|||||||
|
|
||||||
If connecting to a unit via a direct Ethernet connection (no serial modem in the path), the 1.5 s idle gap will still work but will feel slower. In that case you can pass `idle_gap=0.1` explicitly.
|
If connecting to a unit via a direct Ethernet connection (no serial modem in the path), the 1.5 s idle gap will still work but will feel slower. In that case you can pass `idle_gap=0.1` explicitly.
|
||||||
|
|
||||||
|
#### Sierra Wireless RV50 / RV55 Required Settings
|
||||||
|
> ✅ **CONFIRMED — 2026-03-31** from working RV50 field config vs misconfigured RV55.
|
||||||
|
|
||||||
|
The following ACEmanager Serial settings are required for correct transparent operation. A single wrong setting is enough to break the protocol exchange (unit beeps on connect but never returns an S3 frame):
|
||||||
|
|
||||||
|
| ACEmanager Setting | Required Value | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **Quiet Mode** | **Enable** | Disabling it causes the modem to inject `RING\r\nCONNECT\r\n` onto the RS-232 serial line at connection time, corrupting the S3 handshake. |
|
||||||
|
| **Configure Serial Port** | `38400,8N1` | Must match MiniMate baud rate. |
|
||||||
|
| **Flow Control** | `None` | Hardware flow control (CTS/RTS) will block unit's serial TX if pins are not wired. |
|
||||||
|
| **Data Forwarding Timeout** | `1` (= 0.1 s) | Controls RS-232→TCP forwarding latency. `5` (0.5 s) works but is sluggish; `1` matches working field units. |
|
||||||
|
| **TCP Connect Response Delay** | `0` | Any non-zero value causes the modem to silently discard our POLL frame during the delay window. |
|
||||||
|
| **TCP Idle Timeout** | `2` (minutes) | Prevents premature disconnect. Too low and the modem drops the session before the unit responds. |
|
||||||
|
| **DB9 Serial Echo** | `Disable` | Echo would corrupt the S3 stream. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 14.4 Connection Timeouts on the Unit Side
|
### 14.4 Connection Timeouts on the Unit Side
|
||||||
@@ -1052,7 +1079,7 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger
|
|||||||
| Unknown uint16 fields at channel block +0A (=80), +0C (=15), +0E (=40), +10 (=21) — manual describes "Sensitive (Gain=8) / Normal (Gain=1)" per-channel range; 80/15/40/21 might encode gain, sensitivity, or ADC config. | LOW | 2026-03-01 | |
|
| Unknown uint16 fields at channel block +0A (=80), +0C (=15), +0E (=40), +10 (=21) — manual describes "Sensitive (Gain=8) / Normal (Gain=1)" per-channel range; 80/15/40/21 might encode gain, sensitivity, or ADC config. | LOW | 2026-03-01 | |
|
||||||
| Full trigger configuration field mapping (SUB `1C` / write `82`) | LOW | 2026-02-26 | |
|
| Full trigger configuration field mapping (SUB `1C` / write `82`) | LOW | 2026-02-26 | |
|
||||||
| Whether SUB `24`/`25` are distinct from SUB `5A` or redundant | LOW | 2026-02-26 | |
|
| Whether SUB `24`/`25` are distinct from SUB `5A` or redundant | LOW | 2026-02-26 | |
|
||||||
| Meaning of `0x07 E7` field in config block | LOW | 2026-02-26 | |
|
| **Meaning of `0x07 E7` field in config block — RESOLVED:** Calibration year. uint16 BE at destuffed payload offset 0x56–0x57. Confirmed via two-unit comparison: BE18189 (calibrated 2023) = `07 E7`; BE11529 (calibrated 2025) = `07 E9`. Adjacent bytes at 0x53–0x55 encode remaining calibration date (month confirmed as BCD October for both units; full layout 🔶 INFERRED). | RESOLVED | 2026-02-26 | Resolved 2026-03-31 |
|
||||||
| **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 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 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 |
|
||||||
|
|||||||
+410
-2
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
seismo_lab.py — Combined S3 Bridge + Protocol Analyzer GUI.
|
seismo_lab.py — Combined S3 Bridge + Protocol Analyzer + Device Console GUI.
|
||||||
|
|
||||||
Single window with two top-level tabs:
|
Single window with three top-level tabs:
|
||||||
Bridge — capture live serial traffic (wraps s3_bridge.py as subprocess)
|
Bridge — capture live serial traffic (wraps s3_bridge.py as subprocess)
|
||||||
Analyzer — parse, diff, and query captured frames
|
Analyzer — parse, diff, and query captured frames
|
||||||
|
Console — direct device connection; runs commands and shows raw bytes +
|
||||||
|
decoded output; colour-coded TX/RX console with log save and
|
||||||
|
Send-to-Analyzer support
|
||||||
|
|
||||||
When the bridge starts:
|
When the bridge starts:
|
||||||
- raw tap paths are auto-filled in the Analyzer tab
|
- raw tap paths are auto-filled in the Analyzer tab
|
||||||
@@ -32,6 +35,7 @@ SCRIPT_DIR = Path(__file__).parent
|
|||||||
BRIDGE_PATH = SCRIPT_DIR / "bridges" / "s3-bridge" / "s3_bridge.py"
|
BRIDGE_PATH = SCRIPT_DIR / "bridges" / "s3-bridge" / "s3_bridge.py"
|
||||||
PARSERS_DIR = SCRIPT_DIR / "parsers"
|
PARSERS_DIR = SCRIPT_DIR / "parsers"
|
||||||
sys.path.insert(0, str(PARSERS_DIR))
|
sys.path.insert(0, str(PARSERS_DIR))
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR)) # for minimateplus package
|
||||||
|
|
||||||
from s3_analyzer import ( # noqa: E402
|
from s3_analyzer import ( # noqa: E402
|
||||||
AnnotatedFrame,
|
AnnotatedFrame,
|
||||||
@@ -1066,6 +1070,399 @@ class AnalyzerPanel(tk.Frame):
|
|||||||
w.configure(state="normal"); w.insert(tk.END, "\n"); w.configure(state="disabled")
|
w.configure(state="normal"); w.insert(tk.END, "\n"); w.configure(state="disabled")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Console panel (tk.Frame — lives inside a notebook tab)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ConsolePanel(tk.Frame):
|
||||||
|
"""
|
||||||
|
Direct device connection and diagnostic console.
|
||||||
|
|
||||||
|
Lets you run individual protocol commands against a MiniMate Plus via
|
||||||
|
serial or TCP, showing colour-coded TX/RX bytes and decoded output in a
|
||||||
|
scrolling console.
|
||||||
|
|
||||||
|
Colour scheme:
|
||||||
|
TX frames — ACCENT blue (#569cd6)
|
||||||
|
RX raw hex — teal (#4ec9b0)
|
||||||
|
Parsed/decoded — green (#4caf50)
|
||||||
|
Errors — red (#f44747)
|
||||||
|
Status/info — dim grey (#6a6a6a)
|
||||||
|
Section heads — yellow (#dcdcaa)
|
||||||
|
|
||||||
|
Log is auto-saved on "Save Log"; "Send to Analyzer" writes the captured
|
||||||
|
RX bytes as a raw .bin file and injects the path into the Analyzer tab.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TAG_TX = "tx"
|
||||||
|
TAG_RX_RAW = "rx_raw"
|
||||||
|
TAG_PARSED = "parsed"
|
||||||
|
TAG_ERROR = "error"
|
||||||
|
TAG_STATUS = "status"
|
||||||
|
TAG_HEAD = "head"
|
||||||
|
|
||||||
|
MAX_LINES = 5000
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Widget, on_send_to_analyzer=None, **kw):
|
||||||
|
super().__init__(parent, bg=BG2, **kw)
|
||||||
|
self._on_send_to_analyzer = on_send_to_analyzer
|
||||||
|
self._q: queue.Queue = queue.Queue()
|
||||||
|
self._running = False
|
||||||
|
self._log_lines: list[str] = []
|
||||||
|
self._last_raw_rx: Optional[bytes] = None
|
||||||
|
self._cmd_btns: list[tk.Button] = []
|
||||||
|
self._build()
|
||||||
|
self._poll_q()
|
||||||
|
|
||||||
|
# ── build ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build(self) -> None:
|
||||||
|
pad = {"padx": 5, "pady": 3}
|
||||||
|
|
||||||
|
# ── top config row ────────────────────────────────────────────────
|
||||||
|
cfg = tk.Frame(self, bg=BG2)
|
||||||
|
cfg.pack(side=tk.TOP, fill=tk.X, padx=6, pady=4)
|
||||||
|
|
||||||
|
# Transport radio buttons
|
||||||
|
self._transport_var = tk.StringVar(value="tcp")
|
||||||
|
tk.Radiobutton(
|
||||||
|
cfg, text="TCP", variable=self._transport_var, value="tcp",
|
||||||
|
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
||||||
|
font=MONO, command=self._on_transport_change,
|
||||||
|
).grid(row=0, column=0, padx=(0, 4))
|
||||||
|
tk.Radiobutton(
|
||||||
|
cfg, text="Serial", variable=self._transport_var, value="serial",
|
||||||
|
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
||||||
|
font=MONO, command=self._on_transport_change,
|
||||||
|
).grid(row=0, column=1, padx=(0, 12))
|
||||||
|
|
||||||
|
# TCP fields
|
||||||
|
self._tcp_frame = tk.Frame(cfg, bg=BG2)
|
||||||
|
self._tcp_frame.grid(row=0, column=2, sticky="w")
|
||||||
|
tk.Label(self._tcp_frame, text="Host:", bg=BG2, fg=FG, font=MONO).pack(side=tk.LEFT, **pad)
|
||||||
|
self._host_var = tk.StringVar(value="127.0.0.1")
|
||||||
|
tk.Entry(
|
||||||
|
self._tcp_frame, textvariable=self._host_var, width=18,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat", font=MONO,
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
tk.Label(self._tcp_frame, text="Port:", bg=BG2, fg=FG, font=MONO).pack(side=tk.LEFT, padx=(10, 4))
|
||||||
|
self._tcp_port_var = tk.StringVar(value="9034")
|
||||||
|
tk.Entry(
|
||||||
|
self._tcp_frame, textvariable=self._tcp_port_var, width=6,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat", font=MONO,
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Serial fields (hidden by default)
|
||||||
|
self._serial_frame = tk.Frame(cfg, bg=BG2)
|
||||||
|
tk.Label(self._serial_frame, text="Port:", bg=BG2, fg=FG, font=MONO).pack(side=tk.LEFT, **pad)
|
||||||
|
self._port_var = tk.StringVar(value="COM5")
|
||||||
|
tk.Entry(
|
||||||
|
self._serial_frame, textvariable=self._port_var, width=10,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat", font=MONO,
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
tk.Label(self._serial_frame, text="Baud:", bg=BG2, fg=FG, font=MONO).pack(side=tk.LEFT, padx=(10, 4))
|
||||||
|
self._baud_var = tk.StringVar(value="38400")
|
||||||
|
tk.Entry(
|
||||||
|
self._serial_frame, textvariable=self._baud_var, width=8,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat", font=MONO,
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Timeout
|
||||||
|
tk.Label(cfg, text="Timeout:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=3, padx=(18, 4))
|
||||||
|
self._timeout_var = tk.StringVar(value="30")
|
||||||
|
tk.Entry(
|
||||||
|
cfg, textvariable=self._timeout_var, width=5,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat", font=MONO,
|
||||||
|
).grid(row=0, column=4, padx=2)
|
||||||
|
tk.Label(cfg, text="s", bg=BG2, fg=FG_DIM, font=MONO).grid(row=0, column=5)
|
||||||
|
|
||||||
|
# ── command buttons row ───────────────────────────────────────────
|
||||||
|
cmd_row = tk.Frame(self, bg=BG2)
|
||||||
|
cmd_row.pack(side=tk.TOP, fill=tk.X, padx=6, pady=(0, 4))
|
||||||
|
|
||||||
|
tk.Label(cmd_row, text="Commands:", bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
|
||||||
|
for label, cmd in [
|
||||||
|
("POLL", "poll"),
|
||||||
|
("Serial #", "serial_number"),
|
||||||
|
("Full Config", "full_config"),
|
||||||
|
("Event Index", "event_index"),
|
||||||
|
]:
|
||||||
|
btn = tk.Button(
|
||||||
|
cmd_row, text=label, bg=ACCENT, fg="#ffffff",
|
||||||
|
relief="flat", padx=10, cursor="hand2", font=MONO,
|
||||||
|
command=lambda c=cmd: self._run_command(c),
|
||||||
|
)
|
||||||
|
btn.pack(side=tk.LEFT, padx=4)
|
||||||
|
self._cmd_btns.append(btn)
|
||||||
|
|
||||||
|
self._status_var = tk.StringVar(value="Ready")
|
||||||
|
tk.Label(cmd_row, textvariable=self._status_var,
|
||||||
|
bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=14)
|
||||||
|
|
||||||
|
# ── console output ────────────────────────────────────────────────
|
||||||
|
self._console = scrolledtext.ScrolledText(
|
||||||
|
self, height=20, font=MONO_SM,
|
||||||
|
bg=BG, fg=FG, insertbackground=FG,
|
||||||
|
relief="flat", state="disabled",
|
||||||
|
)
|
||||||
|
self._console.pack(fill=tk.BOTH, expand=True, padx=6, pady=4)
|
||||||
|
|
||||||
|
self._console.tag_configure(self.TAG_TX, foreground=ACCENT)
|
||||||
|
self._console.tag_configure(self.TAG_RX_RAW, foreground=COL_S3)
|
||||||
|
self._console.tag_configure(self.TAG_PARSED, foreground=GREEN)
|
||||||
|
self._console.tag_configure(self.TAG_ERROR, foreground=RED)
|
||||||
|
self._console.tag_configure(self.TAG_STATUS, foreground=FG_DIM)
|
||||||
|
self._console.tag_configure(self.TAG_HEAD, foreground=YELLOW, font=MONO_B)
|
||||||
|
|
||||||
|
# ── bottom bar ────────────────────────────────────────────────────
|
||||||
|
bot = tk.Frame(self, bg=BG2)
|
||||||
|
bot.pack(side=tk.BOTTOM, fill=tk.X, padx=6, pady=4)
|
||||||
|
|
||||||
|
tk.Button(
|
||||||
|
bot, text="Clear", bg=BG3, fg=FG, relief="flat",
|
||||||
|
padx=10, cursor="hand2", font=MONO,
|
||||||
|
command=self._clear_console,
|
||||||
|
).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
|
tk.Button(
|
||||||
|
bot, text="Save Log", bg=BG3, fg=FG, relief="flat",
|
||||||
|
padx=10, cursor="hand2", font=MONO,
|
||||||
|
command=self._save_log,
|
||||||
|
).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
|
self._send_btn = tk.Button(
|
||||||
|
bot, text="Send to Analyzer", bg=BG3, fg=FG_DIM, relief="flat",
|
||||||
|
padx=10, cursor="hand2", font=MONO,
|
||||||
|
command=self._send_to_analyzer, state="disabled",
|
||||||
|
)
|
||||||
|
self._send_btn.pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
|
# ── transport toggle ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _on_transport_change(self) -> None:
|
||||||
|
if self._transport_var.get() == "tcp":
|
||||||
|
self._serial_frame.grid_remove()
|
||||||
|
self._tcp_frame.grid(row=0, column=2, sticky="w")
|
||||||
|
else:
|
||||||
|
self._tcp_frame.grid_remove()
|
||||||
|
self._serial_frame.grid(row=0, column=2, sticky="w")
|
||||||
|
|
||||||
|
# ── console helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _append(self, text: str, tag: str = "status") -> None:
|
||||||
|
"""Append coloured text (main thread only — called via _poll_q)."""
|
||||||
|
self._log_lines.append(text)
|
||||||
|
self._console.configure(state="normal")
|
||||||
|
self._console.insert(tk.END, text, tag)
|
||||||
|
line_count = int(self._console.index("end-1c").split(".")[0])
|
||||||
|
if line_count > self.MAX_LINES:
|
||||||
|
self._console.delete("1.0", f"{line_count - self.MAX_LINES}.0")
|
||||||
|
self._console.see(tk.END)
|
||||||
|
self._console.configure(state="disabled")
|
||||||
|
|
||||||
|
def _clear_console(self) -> None:
|
||||||
|
self._console.configure(state="normal")
|
||||||
|
self._console.delete("1.0", tk.END)
|
||||||
|
self._console.configure(state="disabled")
|
||||||
|
self._log_lines.clear()
|
||||||
|
|
||||||
|
def _save_log(self) -> None:
|
||||||
|
cap_dir = SCRIPT_DIR / "bridges" / "captures"
|
||||||
|
cap_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = cap_dir / f"console_{ts}.log"
|
||||||
|
try:
|
||||||
|
path.write_text("".join(self._log_lines), encoding="utf-8")
|
||||||
|
self._q.put(("status", f"Log saved → {path.name}"))
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("Save Error", str(exc))
|
||||||
|
|
||||||
|
def _send_to_analyzer(self) -> None:
|
||||||
|
if not self._last_raw_rx or not self._on_send_to_analyzer:
|
||||||
|
return
|
||||||
|
cap_dir = SCRIPT_DIR / "bridges" / "captures"
|
||||||
|
cap_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
raw_path = cap_dir / f"console_s3_{ts}.bin"
|
||||||
|
try:
|
||||||
|
raw_path.write_bytes(self._last_raw_rx)
|
||||||
|
self._on_send_to_analyzer(str(raw_path))
|
||||||
|
self._q.put(("status", f"Sent to Analyzer → {raw_path.name}"))
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("Error", str(exc))
|
||||||
|
|
||||||
|
# ── command dispatch ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _set_buttons_state(self, state: str) -> None:
|
||||||
|
for btn in self._cmd_btns:
|
||||||
|
btn.configure(state=state)
|
||||||
|
|
||||||
|
def _run_command(self, cmd: str) -> None:
|
||||||
|
if self._running:
|
||||||
|
return
|
||||||
|
# Snapshot config in main thread before handing off to worker
|
||||||
|
config = {
|
||||||
|
"transport": self._transport_var.get(),
|
||||||
|
"host": self._host_var.get().strip(),
|
||||||
|
"tcp_port": int(self._tcp_port_var.get().strip() or "9034"),
|
||||||
|
"port": self._port_var.get().strip(),
|
||||||
|
"baud": int(self._baud_var.get().strip() or "38400"),
|
||||||
|
"timeout": float(self._timeout_var.get().strip() or "30"),
|
||||||
|
"cmd": cmd,
|
||||||
|
}
|
||||||
|
self._running = True
|
||||||
|
self._set_buttons_state("disabled")
|
||||||
|
self._status_var.set("Running…")
|
||||||
|
threading.Thread(target=self._worker, args=(config,), daemon=True).start()
|
||||||
|
|
||||||
|
# ── worker thread ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _worker(self, cfg: dict) -> None:
|
||||||
|
"""Background thread — open transport, run command, post results to queue."""
|
||||||
|
q = self._q
|
||||||
|
|
||||||
|
def post(kind: str, text: str) -> None:
|
||||||
|
q.put((kind, text))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from minimateplus.transport import SerialTransport, TcpTransport
|
||||||
|
from minimateplus.protocol import (
|
||||||
|
MiniMateProtocol,
|
||||||
|
SUB_SERIAL_NUMBER,
|
||||||
|
SUB_FULL_CONFIG,
|
||||||
|
SUB_EVENT_INDEX,
|
||||||
|
)
|
||||||
|
except ImportError as exc:
|
||||||
|
post("error", f"Import error: {exc}\nIs minimateplus installed?\n")
|
||||||
|
q.put(("done", None))
|
||||||
|
return
|
||||||
|
|
||||||
|
timeout = cfg["timeout"]
|
||||||
|
cmd = cfg["cmd"]
|
||||||
|
|
||||||
|
# Build transport
|
||||||
|
if cfg["transport"] == "tcp":
|
||||||
|
host = cfg["host"]
|
||||||
|
tcp_port = cfg["tcp_port"]
|
||||||
|
post("status", f"Connecting {host}:{tcp_port}…")
|
||||||
|
transport = TcpTransport(host, tcp_port, connect_timeout=timeout)
|
||||||
|
else:
|
||||||
|
port = cfg["port"]
|
||||||
|
baud = cfg["baud"]
|
||||||
|
post("status", f"Opening {port} @ {baud} baud…")
|
||||||
|
transport = SerialTransport(port, baud)
|
||||||
|
|
||||||
|
# Wrap transport to capture every TX/RX byte
|
||||||
|
raw_rx = bytearray()
|
||||||
|
orig_write = transport.write
|
||||||
|
orig_read = transport.read
|
||||||
|
|
||||||
|
def logged_write(data: bytes) -> None:
|
||||||
|
post("tx", f"TX [{len(data):3d}B]: {data.hex()}\n")
|
||||||
|
orig_write(data)
|
||||||
|
|
||||||
|
def logged_read(n: int) -> bytes:
|
||||||
|
result = orig_read(n)
|
||||||
|
if result:
|
||||||
|
raw_rx.extend(result)
|
||||||
|
post("rx_raw", f"RX [{len(result):3d}B]: {result.hex()}\n")
|
||||||
|
return result
|
||||||
|
|
||||||
|
transport.write = logged_write # type: ignore[method-assign]
|
||||||
|
transport.read = logged_read # type: ignore[method-assign]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transport:
|
||||||
|
post("status", "Connected.")
|
||||||
|
proto = MiniMateProtocol(transport, recv_timeout=timeout)
|
||||||
|
|
||||||
|
if cmd == "poll":
|
||||||
|
post("head", "\n── POLL startup ─────────────────────────────\n")
|
||||||
|
frame = proto.startup()
|
||||||
|
post("parsed", f" payload ({len(frame.data)} B): {frame.data.hex()}\n")
|
||||||
|
try:
|
||||||
|
text = frame.data.decode("ascii", errors="replace")
|
||||||
|
post("parsed", f" text: {text!r}\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif cmd == "serial_number":
|
||||||
|
post("head", "\n── POLL startup ─────────────────────────────\n")
|
||||||
|
proto.startup()
|
||||||
|
post("head", "\n── Serial Number (SUB 0x15) ─────────────────\n")
|
||||||
|
data = proto.read(SUB_SERIAL_NUMBER)
|
||||||
|
post("parsed", f" raw ({len(data)} B): {data.hex()}\n")
|
||||||
|
sn = data.rstrip(b"\x00").decode("ascii", errors="replace").strip()
|
||||||
|
post("parsed", f" serial: {sn!r}\n")
|
||||||
|
|
||||||
|
elif cmd == "full_config":
|
||||||
|
post("head", "\n── POLL startup ─────────────────────────────\n")
|
||||||
|
proto.startup()
|
||||||
|
post("head", "\n── Full Config (SUB 0x01) ───────────────────\n")
|
||||||
|
data = proto.read(SUB_FULL_CONFIG)
|
||||||
|
post("parsed", f" raw ({len(data)} B):\n")
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
chunk = data[i:i + 16]
|
||||||
|
hex_part = " ".join(f"{b:02X}" for b in chunk)
|
||||||
|
asc_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
post("parsed", f" {i:04X}: {hex_part:<48} {asc_part}\n")
|
||||||
|
|
||||||
|
elif cmd == "event_index":
|
||||||
|
post("head", "\n── POLL startup ─────────────────────────────\n")
|
||||||
|
proto.startup()
|
||||||
|
post("head", "\n── Event Index (SUB 0x08) ───────────────────\n")
|
||||||
|
data = proto.read(SUB_EVENT_INDEX)
|
||||||
|
post("parsed", f" raw ({len(data)} B):\n")
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
chunk = data[i:i + 16]
|
||||||
|
hex_part = " ".join(f"{b:02X}" for b in chunk)
|
||||||
|
asc_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
post("parsed", f" {i:04X}: {hex_part:<48} {asc_part}\n")
|
||||||
|
|
||||||
|
post("status", "Done.")
|
||||||
|
q.put(("save_raw", bytes(raw_rx)))
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
post("error", f"\nError: {exc}\n")
|
||||||
|
finally:
|
||||||
|
q.put(("done", None))
|
||||||
|
|
||||||
|
# ── queue poll ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _poll_q(self) -> None:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
kind, payload = self._q.get_nowait()
|
||||||
|
|
||||||
|
if kind == "tx":
|
||||||
|
self._append(payload, self.TAG_TX)
|
||||||
|
elif kind == "rx_raw":
|
||||||
|
self._append(payload, self.TAG_RX_RAW)
|
||||||
|
elif kind == "parsed":
|
||||||
|
self._append(payload, self.TAG_PARSED)
|
||||||
|
elif kind == "error":
|
||||||
|
self._append(payload, self.TAG_ERROR)
|
||||||
|
elif kind == "head":
|
||||||
|
self._append(payload, self.TAG_HEAD)
|
||||||
|
elif kind == "status":
|
||||||
|
self._status_var.set(str(payload))
|
||||||
|
self._append(f" [{payload}]\n", self.TAG_STATUS)
|
||||||
|
elif kind == "save_raw":
|
||||||
|
self._last_raw_rx = payload
|
||||||
|
if payload:
|
||||||
|
self._send_btn.configure(state="normal", fg=FG)
|
||||||
|
elif kind == "done":
|
||||||
|
self._running = False
|
||||||
|
self._set_buttons_state("normal")
|
||||||
|
self._status_var.set("Ready")
|
||||||
|
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.after(100, self._poll_q)
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Main application window
|
# Main application window
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1101,6 +1498,12 @@ class SeismoLab(tk.Tk):
|
|||||||
self._analyzer_panel = AnalyzerPanel(nb, db=self._db)
|
self._analyzer_panel = AnalyzerPanel(nb, db=self._db)
|
||||||
nb.add(self._analyzer_panel, text=" Analyzer ")
|
nb.add(self._analyzer_panel, text=" Analyzer ")
|
||||||
|
|
||||||
|
self._console_panel = ConsolePanel(
|
||||||
|
nb,
|
||||||
|
on_send_to_analyzer=self._on_console_send_to_analyzer,
|
||||||
|
)
|
||||||
|
nb.add(self._console_panel, text=" Console ")
|
||||||
|
|
||||||
self._nb = nb
|
self._nb = nb
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
|
||||||
@@ -1114,6 +1517,11 @@ class SeismoLab(tk.Tk):
|
|||||||
def _on_bridge_stopped(self) -> None:
|
def _on_bridge_stopped(self) -> None:
|
||||||
self._analyzer_panel.stop_live()
|
self._analyzer_panel.stop_live()
|
||||||
|
|
||||||
|
def _on_console_send_to_analyzer(self, raw_s3_path: str) -> None:
|
||||||
|
"""Console captured bytes → inject into Analyzer S3 field and switch tab."""
|
||||||
|
self._analyzer_panel.s3_var.set(raw_s3_path)
|
||||||
|
self._nb.select(1)
|
||||||
|
|
||||||
def _on_close(self) -> None:
|
def _on_close(self) -> None:
|
||||||
self._bridge_panel.stop_bridge()
|
self._bridge_panel.stop_bridge()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user