v0.12.6 #10
+274
-114
@@ -100,30 +100,43 @@ class BridgePanel(tk.Frame):
|
|||||||
Bridge controls and live log output.
|
Bridge controls and live log output.
|
||||||
|
|
||||||
Two modes selectable at the top:
|
Two modes selectable at the top:
|
||||||
- Serial: wraps s3_bridge.py as a subprocess (two COM ports)
|
- Serial: wraps s3_bridge.py as a subprocess (two COM ports).
|
||||||
- TCP: MITM proxy — listens for an incoming Blastware connection,
|
Single bridge session; use New Capture / Stop Capture to create
|
||||||
forwards all bytes to the real device over IP, captures both
|
labelled raw-file segments on demand.
|
||||||
directions to raw .bin files
|
- TCP: MITM proxy — listens for Blastware on a local port, forwards to
|
||||||
|
the real device. Each incoming connection is a capture; segments
|
||||||
|
appear in the history list automatically.
|
||||||
|
|
||||||
Calls on_bridge_started(raw_bw_path, raw_s3_path, struct_bin_path) when
|
Callbacks (all optional except on_bridge_started / on_bridge_stopped):
|
||||||
traffic begins so the parent can wire up the Analyzer in live mode.
|
on_bridge_started(struct_bin_path) — bridge is up
|
||||||
|
on_bridge_stopped() — bridge stopped
|
||||||
|
on_capture_started(bw_path, s3_path, label) — a capture segment began
|
||||||
|
on_capture_complete(bw_path, s3_path, label)— a capture segment finished
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped, **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)
|
super().__init__(parent, bg=BG2, **kw)
|
||||||
self._on_started = on_bridge_started
|
self._on_started = on_bridge_started
|
||||||
self._on_stopped = on_bridge_stopped
|
self._on_stopped = on_bridge_stopped
|
||||||
|
self._on_cap_started = on_capture_started
|
||||||
|
self._on_cap_complete = on_capture_complete
|
||||||
# serial state
|
# serial state
|
||||||
self.process: Optional[subprocess.Popen] = None
|
self.process: Optional[subprocess.Popen] = None
|
||||||
|
self._stdout_q: queue.Queue[str] = queue.Queue()
|
||||||
# tcp state
|
# tcp state
|
||||||
self._server: Optional[socket.socket] = None
|
self._server: Optional[socket.socket] = None
|
||||||
self._tcp_stop_event = threading.Event()
|
self._tcp_stop_event = threading.Event()
|
||||||
# unified log queue (serial reader thread + TCP pipe threads both push here)
|
self._tcp_log_q: queue.Queue[str] = queue.Queue()
|
||||||
self._log_q: queue.Queue[str] = queue.Queue()
|
# shared capture state
|
||||||
|
self._capturing = False
|
||||||
|
self._cap_label: Optional[str] = None
|
||||||
|
self._cap_history: list[dict] = [] # {label, status, bw, s3}
|
||||||
# mode
|
# mode
|
||||||
self._mode = tk.StringVar(value="serial")
|
self._mode = tk.StringVar(value="serial")
|
||||||
self._build()
|
self._build()
|
||||||
self._poll_log_q()
|
self._poll_stdout()
|
||||||
|
self._poll_tcp_log()
|
||||||
|
|
||||||
# ── build ─────────────────────────────────────────────────────────────
|
# ── build ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -196,16 +209,6 @@ class BridgePanel(tk.Frame):
|
|||||||
tk.Button(cfg, text="Browse", bg=BG3, fg=FG, relief="flat", cursor="hand2",
|
tk.Button(cfg, text="Browse", bg=BG3, fg=FG, relief="flat", cursor="hand2",
|
||||||
font=MONO, command=self._choose_dir).grid(row=2, column=5, **pad)
|
font=MONO, command=self._choose_dir).grid(row=2, column=5, **pad)
|
||||||
|
|
||||||
# Row 3: raw capture checkboxes
|
|
||||||
self._raw_bw_on = tk.BooleanVar(value=True)
|
|
||||||
self._raw_s3_on = tk.BooleanVar(value=True)
|
|
||||||
tk.Checkbutton(cfg, text="Capture BW->S3 raw", variable=self._raw_bw_on,
|
|
||||||
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
|
||||||
font=MONO).grid(row=3, column=0, columnspan=2, sticky="w", **pad)
|
|
||||||
tk.Checkbutton(cfg, text="Capture S3->BW raw", variable=self._raw_s3_on,
|
|
||||||
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
|
||||||
font=MONO).grid(row=3, column=2, columnspan=2, sticky="w", **pad)
|
|
||||||
|
|
||||||
# Buttons + status
|
# Buttons + status
|
||||||
btn_row = tk.Frame(self, bg=BG2)
|
btn_row = tk.Frame(self, bg=BG2)
|
||||||
btn_row.pack(side=tk.TOP, fill=tk.X, padx=4, pady=2)
|
btn_row.pack(side=tk.TOP, fill=tk.X, padx=4, pady=2)
|
||||||
@@ -220,6 +223,18 @@ class BridgePanel(tk.Frame):
|
|||||||
command=self.stop_bridge, state="disabled")
|
command=self.stop_bridge, state="disabled")
|
||||||
self.stop_btn.pack(side=tk.LEFT, padx=4)
|
self.stop_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,
|
self.mark_btn = tk.Button(btn_row, text="Add Mark", bg=BG3, fg=FG,
|
||||||
relief="flat", padx=10, cursor="hand2", font=MONO,
|
relief="flat", padx=10, cursor="hand2", font=MONO,
|
||||||
command=self.add_mark, state="disabled")
|
command=self.add_mark, state="disabled")
|
||||||
@@ -229,9 +244,34 @@ class BridgePanel(tk.Frame):
|
|||||||
tk.Label(btn_row, textvariable=self.status_var,
|
tk.Label(btn_row, textvariable=self.status_var,
|
||||||
bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=10)
|
bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
# Capture history list
|
||||||
|
hist_outer = tk.Frame(self, bg=BG2)
|
||||||
|
hist_outer.pack(side=tk.TOP, fill=tk.X, padx=4, pady=(2, 0))
|
||||||
|
|
||||||
|
tk.Label(hist_outer, text="Captures:", bg=BG2, fg=FG_DIM,
|
||||||
|
font=MONO_SM, anchor="w").pack(side=tk.LEFT, padx=(4, 6))
|
||||||
|
|
||||||
|
hist_inner = tk.Frame(hist_outer, bg=BG2)
|
||||||
|
hist_inner.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
self._hist_lb = tk.Listbox(
|
||||||
|
hist_inner, bg=BG3, fg=FG, font=MONO_SM,
|
||||||
|
height=3, relief="flat", selectbackground=BG,
|
||||||
|
selectforeground=ACCENT, activestyle="none",
|
||||||
|
highlightthickness=0,
|
||||||
|
)
|
||||||
|
hist_vsb = ttk.Scrollbar(hist_inner, orient="vertical", command=self._hist_lb.yview)
|
||||||
|
self._hist_lb.configure(yscrollcommand=hist_vsb.set)
|
||||||
|
hist_vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self._hist_lb.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
self._hist_lb.bind("<Double-Button-1>", self._on_hist_dblclick)
|
||||||
|
|
||||||
|
tk.Label(hist_outer, text="dbl-click to reload", bg=BG2, fg=FG_DIM,
|
||||||
|
font=MONO_SM, anchor="e").pack(side=tk.RIGHT, padx=6)
|
||||||
|
|
||||||
# Log output
|
# Log output
|
||||||
self.log_view = scrolledtext.ScrolledText(
|
self.log_view = scrolledtext.ScrolledText(
|
||||||
self, height=18, font=MONO_SM,
|
self, height=14, font=MONO_SM,
|
||||||
bg=BG, fg=FG, insertbackground=FG,
|
bg=BG, fg=FG, insertbackground=FG,
|
||||||
relief="flat", state="disabled",
|
relief="flat", state="disabled",
|
||||||
)
|
)
|
||||||
@@ -258,25 +298,21 @@ class BridgePanel(tk.Frame):
|
|||||||
self.log_view.see(tk.END)
|
self.log_view.see(tk.END)
|
||||||
self.log_view.configure(state="disabled")
|
self.log_view.configure(state="disabled")
|
||||||
|
|
||||||
# ── unified log-queue polling (serial subprocess + TCP threads both push here)
|
def _refresh_hist(self) -> None:
|
||||||
|
self._hist_lb.delete(0, tk.END)
|
||||||
|
for entry in self._cap_history:
|
||||||
|
icon = "\U0001f534" if entry["status"] == "recording" else "✅"
|
||||||
|
self._hist_lb.insert(tk.END, f" {icon} {entry['label'] or '(unlabeled)'}")
|
||||||
|
if self._cap_history:
|
||||||
|
self._hist_lb.see(tk.END)
|
||||||
|
|
||||||
def _poll_log_q(self) -> None:
|
def _on_hist_dblclick(self, _e=None) -> None:
|
||||||
try:
|
sel = self._hist_lb.curselection()
|
||||||
while True:
|
if not sel:
|
||||||
msg = self._log_q.get_nowait()
|
return
|
||||||
if msg == "<<exit>>":
|
entry = self._cap_history[sel[0]]
|
||||||
self._bridge_ended()
|
if entry["status"] == "done" and entry["bw"] and entry["s3"] and self._on_cap_complete:
|
||||||
self._on_stopped()
|
self._on_cap_complete(entry["bw"], entry["s3"], entry["label"])
|
||||||
elif msg == "<<session_ended>>":
|
|
||||||
if self._server is not None:
|
|
||||||
self.status_var.set(f"Listening on :{self.listen_port_var.get()}")
|
|
||||||
self._on_stopped()
|
|
||||||
else:
|
|
||||||
self._append_log(msg)
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.after(100, self._poll_log_q)
|
|
||||||
|
|
||||||
# ── bridge control (delegates to serial or TCP) ───────────────────────
|
# ── bridge control (delegates to serial or TCP) ───────────────────────
|
||||||
|
|
||||||
@@ -296,9 +332,42 @@ class BridgePanel(tk.Frame):
|
|||||||
self.status_var.set("Stopped")
|
self.status_var.set("Stopped")
|
||||||
self.start_btn.configure(state="normal")
|
self.start_btn.configure(state="normal")
|
||||||
self.stop_btn.configure(state="disabled", bg=BG3)
|
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.mark_btn.configure(state="disabled")
|
||||||
|
self._capturing = False
|
||||||
|
self._cap_label = None
|
||||||
self._append_log("== Bridge stopped ==\n")
|
self._append_log("== Bridge stopped ==\n")
|
||||||
|
|
||||||
|
# ── capture lifecycle (shared by serial and TCP) ──────────────────────
|
||||||
|
|
||||||
|
def _on_cap_started_msg(self, bw_path: str, s3_path: str) -> None:
|
||||||
|
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
|
||||||
|
self._refresh_hist()
|
||||||
|
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:
|
||||||
|
label = self._cap_label or "capture"
|
||||||
|
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)
|
||||||
|
|
||||||
# ── serial mode ───────────────────────────────────────────────────────
|
# ── serial mode ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _start_serial(self) -> None:
|
def _start_serial(self) -> None:
|
||||||
@@ -321,14 +390,6 @@ class BridgePanel(tk.Frame):
|
|||||||
args = [sys.executable, str(BRIDGE_PATH),
|
args = [sys.executable, str(BRIDGE_PATH),
|
||||||
"--bw", bw, "--s3", s3, "--baud", baud, "--logdir", logdir]
|
"--bw", bw, "--s3", s3, "--baud", baud, "--logdir", logdir]
|
||||||
|
|
||||||
raw_bw_path = raw_s3_path = None
|
|
||||||
if self._raw_bw_on.get():
|
|
||||||
raw_bw_path = os.path.join(logdir, f"raw_bw_{ts}.bin")
|
|
||||||
args += ["--raw-bw", raw_bw_path]
|
|
||||||
if self._raw_s3_on.get():
|
|
||||||
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin")
|
|
||||||
args += ["--raw-s3", raw_s3_path]
|
|
||||||
|
|
||||||
struct_bin_path = os.path.join(logdir, f"s3_session_{ts}.bin")
|
struct_bin_path = os.path.join(logdir, f"s3_session_{ts}.bin")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -348,9 +409,10 @@ class BridgePanel(tk.Frame):
|
|||||||
self.status_var.set(f"Running — {bw} <-> {s3}")
|
self.status_var.set(f"Running — {bw} <-> {s3}")
|
||||||
self.start_btn.configure(state="disabled")
|
self.start_btn.configure(state="disabled")
|
||||||
self.stop_btn.configure(state="normal", bg=RED)
|
self.stop_btn.configure(state="normal", bg=RED)
|
||||||
self.mark_btn.configure(state="normal")
|
self.cap_btn.configure(state="normal")
|
||||||
self._append_log(f"== Bridge started [{ts}] ==\n")
|
self._append_log(f"== Bridge started [{ts}] ==\n")
|
||||||
self._on_started(raw_bw_path, raw_s3_path, struct_bin_path)
|
self._append_log(" Click 'New Capture' when ready to record.\n")
|
||||||
|
self._on_started(struct_bin_path)
|
||||||
|
|
||||||
def _stop_serial(self) -> None:
|
def _stop_serial(self) -> None:
|
||||||
if self.process and self.process.poll() is None:
|
if self.process and self.process.poll() is None:
|
||||||
@@ -366,8 +428,79 @@ class BridgePanel(tk.Frame):
|
|||||||
if not self.process or not self.process.stdout:
|
if not self.process or not self.process.stdout:
|
||||||
return
|
return
|
||||||
for line in self.process.stdout:
|
for line in self.process.stdout:
|
||||||
self._log_q.put(line)
|
self._stdout_q.put(line)
|
||||||
self._log_q.put("<<exit>>")
|
self._stdout_q.put("<<exit>>")
|
||||||
|
|
||||||
|
def _poll_stdout(self) -> None:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = self._stdout_q.get_nowait()
|
||||||
|
if line == "<<exit>>":
|
||||||
|
self._bridge_ended()
|
||||||
|
self._on_stopped()
|
||||||
|
break
|
||||||
|
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("[CAP_START] ") and "\t" in stripped:
|
||||||
|
parts = stripped[12:].split("\t", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
self._on_cap_started_msg(parts[0].strip(), parts[1].strip())
|
||||||
|
elif stripped.startswith("[CAP_STOP] ") and "\t" in stripped:
|
||||||
|
parts = stripped[11:].split("\t", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
self._on_cap_stopped_msg(parts[0].strip(), parts[1].strip())
|
||||||
|
|
||||||
|
self._append_log(line)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.after(100, self._poll_stdout)
|
||||||
|
|
||||||
|
def _start_capture(self) -> None:
|
||||||
|
if not self.process or self.process.poll() is not None:
|
||||||
|
return
|
||||||
|
label = simpledialog.askstring(
|
||||||
|
"New Capture",
|
||||||
|
"Label for this capture\n(e.g. 'copy_event_download').\nLeave blank for timestamp only:",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
if label is None:
|
||||||
|
return
|
||||||
|
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")
|
||||||
|
self._cap_history.append({"label": self._cap_label, "status": "recording",
|
||||||
|
"bw": None, "s3": None})
|
||||||
|
self._refresh_hist()
|
||||||
|
|
||||||
|
def _stop_capture(self) -> None:
|
||||||
|
if self._mode.get() == "tcp":
|
||||||
|
# TCP: close the server so the current session ends naturally
|
||||||
|
self._tcp_stop_event.set()
|
||||||
|
if self._server:
|
||||||
|
try:
|
||||||
|
self._server.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self._server = None
|
||||||
|
return
|
||||||
|
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}")
|
||||||
|
|
||||||
# ── TCP mode ──────────────────────────────────────────────────────────
|
# ── TCP mode ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -401,23 +534,21 @@ class BridgePanel(tk.Frame):
|
|||||||
self._tcp_stop_event.clear()
|
self._tcp_stop_event.clear()
|
||||||
self.start_btn.configure(state="disabled")
|
self.start_btn.configure(state="disabled")
|
||||||
self.stop_btn.configure(state="normal", bg=RED)
|
self.stop_btn.configure(state="normal", bg=RED)
|
||||||
self.mark_btn.configure(state="normal")
|
|
||||||
self.status_var.set(f"Listening on :{listen_port}")
|
self.status_var.set(f"Listening on :{listen_port}")
|
||||||
|
|
||||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
self._append_log(
|
self._append_log(
|
||||||
f"== TCP Bridge started [{ts}]\n"
|
f"== TCP Bridge started [{ts}]\n"
|
||||||
f" Listening on 0.0.0.0:{listen_port}\n"
|
f" Listening on 0.0.0.0:{listen_port}\n"
|
||||||
f" Forwarding to {remote_host}:{remote_port}\n==\n"
|
f" Forwarding to {remote_host}:{remote_port}\n"
|
||||||
|
f" Each Blastware connection is automatically captured.\n==\n"
|
||||||
)
|
)
|
||||||
|
self._on_started(None)
|
||||||
|
|
||||||
logdir = self.logdir_var.get().strip() or "."
|
logdir = self.logdir_var.get().strip() or "."
|
||||||
raw_bw_on = self._raw_bw_on.get()
|
|
||||||
raw_s3_on = self._raw_s3_on.get()
|
|
||||||
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._accept_loop,
|
target=self._accept_loop,
|
||||||
args=(srv, remote_host, remote_port, logdir, raw_bw_on, raw_s3_on),
|
args=(srv, remote_host, remote_port, logdir),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
@@ -432,8 +563,8 @@ class BridgePanel(tk.Frame):
|
|||||||
self._bridge_ended()
|
self._bridge_ended()
|
||||||
self._on_stopped()
|
self._on_stopped()
|
||||||
|
|
||||||
def _accept_loop(self, srv: socket.socket, remote_host: str, remote_port: int,
|
def _accept_loop(self, srv: socket.socket, remote_host: str,
|
||||||
logdir: str, raw_bw_on: bool, raw_s3_on: bool) -> None:
|
remote_port: int, logdir: str) -> None:
|
||||||
while not self._tcp_stop_event.is_set():
|
while not self._tcp_stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
client_sock, addr = srv.accept()
|
client_sock, addr = srv.accept()
|
||||||
@@ -443,82 +574,98 @@ class BridgePanel(tk.Frame):
|
|||||||
break
|
break
|
||||||
|
|
||||||
peer = f"{addr[0]}:{addr[1]}"
|
peer = f"{addr[0]}:{addr[1]}"
|
||||||
self._log_q.put(f"[TCP] Blastware connected from {peer}\n")
|
self._tcp_log_q.put(f"[TCP] Blastware connected from {peer}\n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dev_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
dev_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
dev_sock.connect((remote_host, remote_port))
|
dev_sock.connect((remote_host, remote_port))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self._log_q.put(f"[TCP] Cannot reach device {remote_host}:{remote_port}: {e}\n")
|
self._tcp_log_q.put(f"[TCP] Cannot reach device {remote_host}:{remote_port}: {e}\n")
|
||||||
client_sock.close()
|
client_sock.close()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._log_q.put(f"[TCP] Connected to device at {remote_host}:{remote_port}\n")
|
self._tcp_log_q.put(f"[TCP] Connected to device at {remote_host}:{remote_port}\n")
|
||||||
|
|
||||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
os.makedirs(logdir, exist_ok=True)
|
os.makedirs(logdir, exist_ok=True)
|
||||||
raw_bw_path = os.path.join(logdir, f"raw_bw_{ts}.bin") if raw_bw_on else None
|
bw_path = os.path.join(logdir, f"raw_bw_{ts}.bin")
|
||||||
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin") if raw_s3_on else None
|
s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin")
|
||||||
|
|
||||||
self.after(0, self._notify_tcp_session_start,
|
# Auto-register in history as recording
|
||||||
raw_bw_path, raw_s3_path, peer, remote_host, remote_port)
|
label = f"tcp_{ts}"
|
||||||
self._run_tcp_session(client_sock, dev_sock, raw_bw_path, raw_s3_path, ts)
|
self.after(0, self._tcp_session_started, bw_path, s3_path, label, peer, remote_host, remote_port)
|
||||||
self._log_q.put("<<session_ended>>")
|
|
||||||
|
|
||||||
def _notify_tcp_session_start(self, raw_bw_path, raw_s3_path,
|
self._run_tcp_session(client_sock, dev_sock, bw_path, s3_path, ts)
|
||||||
peer, remote_host, remote_port) -> None:
|
|
||||||
|
self._tcp_log_q.put(f"<<tcp_session_ended>>\t{bw_path}\t{s3_path}\t{label}")
|
||||||
|
|
||||||
|
def _tcp_session_started(self, bw_path, s3_path, label, peer, remote_host, remote_port) -> None:
|
||||||
self.status_var.set(f"Active: {peer} → {remote_host}:{remote_port}")
|
self.status_var.set(f"Active: {peer} → {remote_host}:{remote_port}")
|
||||||
self._on_started(raw_bw_path, raw_s3_path, None)
|
self._cap_label = label
|
||||||
|
self._cap_history.append({"label": label, "status": "recording",
|
||||||
|
"bw": bw_path, "s3": s3_path})
|
||||||
|
self._refresh_hist()
|
||||||
|
self.mark_btn.configure(state="normal")
|
||||||
|
self.stop_cap_btn.configure(state="normal", bg=RED)
|
||||||
|
if self._on_cap_started:
|
||||||
|
self._on_cap_started(bw_path, s3_path, label)
|
||||||
|
|
||||||
def _run_tcp_session(self, bw_sock: socket.socket, dev_sock: socket.socket,
|
def _run_tcp_session(self, bw_sock: socket.socket, dev_sock: socket.socket,
|
||||||
raw_bw_path: Optional[str], raw_s3_path: Optional[str],
|
bw_path: str, s3_path: str, ts: str) -> None:
|
||||||
ts: str) -> None:
|
|
||||||
bw_fh = open(raw_bw_path, "wb") if raw_bw_path else None
|
|
||||||
s3_fh = open(raw_s3_path, "wb") if raw_s3_path else None
|
|
||||||
bw_bytes = [0]
|
bw_bytes = [0]
|
||||||
s3_bytes = [0]
|
s3_bytes = [0]
|
||||||
|
|
||||||
def _pipe(src, dst, fh, counter):
|
with open(bw_path, "wb") as bw_fh, open(s3_path, "wb") as s3_fh:
|
||||||
try:
|
def _pipe(src, dst, fh, counter):
|
||||||
while True:
|
try:
|
||||||
data = src.recv(4096)
|
while True:
|
||||||
if not data:
|
data = src.recv(4096)
|
||||||
break
|
if not data:
|
||||||
dst.sendall(data)
|
break
|
||||||
if fh:
|
dst.sendall(data)
|
||||||
fh.write(data)
|
fh.write(data)
|
||||||
fh.flush()
|
fh.flush()
|
||||||
counter[0] += len(data)
|
counter[0] += len(data)
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
dst.shutdown(socket.SHUT_WR)
|
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
dst.shutdown(socket.SHUT_WR)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
t_bw = threading.Thread(target=_pipe, args=(bw_sock, dev_sock, bw_fh, bw_bytes), daemon=True)
|
t_bw = threading.Thread(target=_pipe, args=(bw_sock, dev_sock, bw_fh, bw_bytes), daemon=True)
|
||||||
t_s3 = threading.Thread(target=_pipe, args=(dev_sock, bw_sock, s3_fh, s3_bytes), daemon=True)
|
t_s3 = threading.Thread(target=_pipe, args=(dev_sock, bw_sock, s3_fh, s3_bytes), daemon=True)
|
||||||
t_bw.start()
|
t_bw.start()
|
||||||
t_s3.start()
|
t_s3.start()
|
||||||
t_bw.join()
|
t_bw.join()
|
||||||
t_s3.join()
|
t_s3.join()
|
||||||
|
|
||||||
bw_sock.close()
|
bw_sock.close()
|
||||||
dev_sock.close()
|
dev_sock.close()
|
||||||
if bw_fh:
|
self._tcp_log_q.put(
|
||||||
bw_fh.close()
|
|
||||||
if s3_fh:
|
|
||||||
s3_fh.close()
|
|
||||||
|
|
||||||
self._log_q.put(
|
|
||||||
f"[TCP] Session {ts} done "
|
f"[TCP] Session {ts} done "
|
||||||
f"BW→dev: {bw_bytes[0]} bytes dev→BW: {s3_bytes[0]} bytes\n"
|
f"BW→dev: {bw_bytes[0]} bytes dev→BW: {s3_bytes[0]} bytes\n"
|
||||||
)
|
)
|
||||||
if raw_bw_path:
|
|
||||||
self._log_q.put(f"[TCP] BW capture: {raw_bw_path}\n")
|
def _poll_tcp_log(self) -> None:
|
||||||
if raw_s3_path:
|
try:
|
||||||
self._log_q.put(f"[TCP] S3 capture: {raw_s3_path}\n")
|
while True:
|
||||||
|
msg = self._tcp_log_q.get_nowait()
|
||||||
|
if msg.startswith("<<tcp_session_ended>>"):
|
||||||
|
parts = msg.split("\t")
|
||||||
|
if len(parts) == 4:
|
||||||
|
_, bw_path, s3_path, label = parts
|
||||||
|
self._on_cap_stopped_msg(bw_path, s3_path)
|
||||||
|
if self._server is not None:
|
||||||
|
self.status_var.set(f"Listening on :{self.listen_port_var.get()}")
|
||||||
|
self.stop_cap_btn.configure(state="disabled", bg=BG3)
|
||||||
|
else:
|
||||||
|
self._append_log(msg)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.after(100, self._poll_tcp_log)
|
||||||
|
|
||||||
# ── marks ─────────────────────────────────────────────────────────────
|
# ── marks ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2116,6 +2263,8 @@ class SeismoLab(tk.Tk):
|
|||||||
nb,
|
nb,
|
||||||
on_bridge_started=self._on_bridge_started,
|
on_bridge_started=self._on_bridge_started,
|
||||||
on_bridge_stopped=self._on_bridge_stopped,
|
on_bridge_stopped=self._on_bridge_stopped,
|
||||||
|
on_capture_started=self._on_capture_started,
|
||||||
|
on_capture_complete=self._on_capture_complete,
|
||||||
)
|
)
|
||||||
nb.add(self._bridge_panel, text=" Bridge ")
|
nb.add(self._bridge_panel, text=" Bridge ")
|
||||||
|
|
||||||
@@ -2137,16 +2286,27 @@ class SeismoLab(tk.Tk):
|
|||||||
self._nb = nb
|
self._nb = nb
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
|
||||||
def _on_bridge_started(self, raw_bw: Optional[str], raw_s3: Optional[str],
|
def _on_bridge_started(self, struct_bin: Optional[str] = None) -> None:
|
||||||
struct_bin: Optional[str] = None) -> None:
|
"""Bridge is up — store struct bin path; stay on Bridge tab."""
|
||||||
"""Bridge started — inject paths into analyzer and start live mode."""
|
if struct_bin:
|
||||||
self._analyzer_panel.set_live_files(raw_bw, raw_s3, struct_bin)
|
self._analyzer_panel.bin_var.set(struct_bin)
|
||||||
# Switch to Analyzer tab so the user can watch it update
|
|
||||||
self._nb.select(1)
|
|
||||||
|
|
||||||
def _on_bridge_stopped(self) -> None:
|
def _on_bridge_stopped(self) -> None:
|
||||||
self._analyzer_panel.stop_live()
|
self._analyzer_panel.stop_live()
|
||||||
|
|
||||||
|
def _on_capture_started(self, bw_path: str, s3_path: str, label: str) -> None:
|
||||||
|
"""A capture segment began — wire live mode and switch to Analyzer."""
|
||||||
|
self._analyzer_panel.set_live_files(bw_path, s3_path)
|
||||||
|
self._nb.select(1)
|
||||||
|
|
||||||
|
def _on_capture_complete(self, bw_path: str, s3_path: str, label: str) -> None:
|
||||||
|
"""A capture segment finished — run full analysis and switch to Analyzer."""
|
||||||
|
self._analyzer_panel.stop_live()
|
||||||
|
self._analyzer_panel.s3_var.set(s3_path)
|
||||||
|
self._analyzer_panel.bw_var.set(bw_path)
|
||||||
|
self._analyzer_panel._run_analyze()
|
||||||
|
self._nb.select(1)
|
||||||
|
|
||||||
def _on_console_send_to_analyzer(self, raw_s3_path: str) -> None:
|
def _on_console_send_to_analyzer(self, raw_s3_path: str) -> None:
|
||||||
"""Console captured bytes → inject into Analyzer S3 field and switch tab."""
|
"""Console captured bytes → inject into Analyzer S3 field and switch tab."""
|
||||||
self._analyzer_panel.s3_var.set(raw_s3_path)
|
self._analyzer_panel.s3_var.set(raw_s3_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user