Merge TCP mode into Bridge tab (Serial/TCP radio toggle)
Removes the separate 'TCP Capture' tab and folds TCP MITM capture directly into the existing Bridge tab. A Serial/TCP radio selector at the top swaps the connection fields (COM ports vs. listen port + device host:port) while keeping the same Start Bridge / Stop Bridge / Add Mark buttons, capture checkboxes, log dir, and live log — identical UX for both modes. https://claude.ai/code/session_014NczSHUz9uTzCAf4cVASTJ
This commit is contained in:
+160
-215
@@ -97,19 +97,33 @@ class AnalyzerState:
|
|||||||
|
|
||||||
class BridgePanel(tk.Frame):
|
class BridgePanel(tk.Frame):
|
||||||
"""
|
"""
|
||||||
All bridge controls and live log output.
|
Bridge controls and live log output.
|
||||||
Calls on_bridge_started(raw_bw_path, raw_s3_path) when the bridge starts
|
|
||||||
so the parent can wire up the Analyzer.
|
Two modes selectable at the top:
|
||||||
|
- Serial: wraps s3_bridge.py as a subprocess (two COM ports)
|
||||||
|
- TCP: MITM proxy — listens for an incoming Blastware connection,
|
||||||
|
forwards all bytes to the real device over IP, captures both
|
||||||
|
directions to raw .bin files
|
||||||
|
|
||||||
|
Calls on_bridge_started(raw_bw_path, raw_s3_path, struct_bin_path) when
|
||||||
|
traffic begins so the parent can wire up the Analyzer in live mode.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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, **kw):
|
||||||
super().__init__(parent, bg=BG2, **kw)
|
super().__init__(parent, bg=BG2, **kw)
|
||||||
self._on_started = on_bridge_started # signature: (raw_bw, raw_s3, struct_bin)
|
self._on_started = on_bridge_started
|
||||||
self._on_stopped = on_bridge_stopped
|
self._on_stopped = on_bridge_stopped
|
||||||
|
# serial state
|
||||||
self.process: Optional[subprocess.Popen] = None
|
self.process: Optional[subprocess.Popen] = None
|
||||||
self._stdout_q: queue.Queue[str] = queue.Queue()
|
# tcp state
|
||||||
|
self._server: Optional[socket.socket] = None
|
||||||
|
self._tcp_stop_event = threading.Event()
|
||||||
|
# unified log queue (serial reader thread + TCP pipe threads both push here)
|
||||||
|
self._log_q: queue.Queue[str] = queue.Queue()
|
||||||
|
# mode
|
||||||
|
self._mode = tk.StringVar(value="serial")
|
||||||
self._build()
|
self._build()
|
||||||
self._poll_stdout()
|
self._poll_log_q()
|
||||||
|
|
||||||
# ── build ─────────────────────────────────────────────────────────────
|
# ── build ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -119,45 +133,80 @@ class BridgePanel(tk.Frame):
|
|||||||
cfg = tk.Frame(self, bg=BG2)
|
cfg = tk.Frame(self, bg=BG2)
|
||||||
cfg.pack(side=tk.TOP, fill=tk.X, padx=4, pady=4)
|
cfg.pack(side=tk.TOP, fill=tk.X, padx=4, pady=4)
|
||||||
|
|
||||||
# Row 0: ports
|
# Row 0: mode selector
|
||||||
tk.Label(cfg, text="BW COM:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=0, sticky="e", **pad)
|
mode_row = tk.Frame(cfg, bg=BG2)
|
||||||
|
mode_row.grid(row=0, column=0, columnspan=6, sticky="w", padx=6, pady=(4, 0))
|
||||||
|
tk.Label(mode_row, text="Mode:", bg=BG2, fg=FG, font=MONO).pack(side=tk.LEFT, padx=(0, 8))
|
||||||
|
tk.Radiobutton(mode_row, text="Serial", variable=self._mode, value="serial",
|
||||||
|
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
||||||
|
font=MONO, command=self._on_mode_change).pack(side=tk.LEFT, padx=4)
|
||||||
|
tk.Radiobutton(mode_row, text="TCP", variable=self._mode, value="tcp",
|
||||||
|
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
||||||
|
font=MONO, command=self._on_mode_change).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
|
# Row 1a: serial connection fields (shown by default)
|
||||||
|
self._serial_frame = tk.Frame(cfg, bg=BG2)
|
||||||
|
self._serial_frame.grid(row=1, column=0, columnspan=6, sticky="w")
|
||||||
|
|
||||||
|
tk.Label(self._serial_frame, text="BW COM:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=0, sticky="e", **pad)
|
||||||
self.bw_var = tk.StringVar(value="COM4")
|
self.bw_var = tk.StringVar(value="COM4")
|
||||||
tk.Entry(cfg, textvariable=self.bw_var, width=10,
|
tk.Entry(self._serial_frame, textvariable=self.bw_var, width=10,
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
font=MONO).grid(row=0, column=1, sticky="w", **pad)
|
font=MONO).grid(row=0, column=1, sticky="w", **pad)
|
||||||
|
|
||||||
tk.Label(cfg, text="S3 COM:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=2, sticky="e", **pad)
|
tk.Label(self._serial_frame, text="S3 COM:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=2, sticky="e", **pad)
|
||||||
self.s3_var = tk.StringVar(value="COM5")
|
self.s3_var = tk.StringVar(value="COM5")
|
||||||
tk.Entry(cfg, textvariable=self.s3_var, width=10,
|
tk.Entry(self._serial_frame, textvariable=self.s3_var, width=10,
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
font=MONO).grid(row=0, column=3, sticky="w", **pad)
|
font=MONO).grid(row=0, column=3, sticky="w", **pad)
|
||||||
|
|
||||||
tk.Label(cfg, text="Baud:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=4, sticky="e", **pad)
|
tk.Label(self._serial_frame, text="Baud:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=4, sticky="e", **pad)
|
||||||
self.baud_var = tk.StringVar(value="38400")
|
self.baud_var = tk.StringVar(value="38400")
|
||||||
tk.Entry(cfg, textvariable=self.baud_var, width=8,
|
tk.Entry(self._serial_frame, textvariable=self.baud_var, width=8,
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
font=MONO).grid(row=0, column=5, sticky="w", **pad)
|
font=MONO).grid(row=0, column=5, sticky="w", **pad)
|
||||||
|
|
||||||
# Row 1: log dir
|
# Row 1b: TCP connection fields (hidden until TCP mode selected)
|
||||||
tk.Label(cfg, text="Log dir:", bg=BG2, fg=FG, font=MONO).grid(row=1, column=0, sticky="e", **pad)
|
self._tcp_frame = tk.Frame(cfg, bg=BG2)
|
||||||
|
|
||||||
|
tk.Label(self._tcp_frame, text="Listen port:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=0, sticky="e", **pad)
|
||||||
|
self.listen_port_var = tk.StringVar(value="9034")
|
||||||
|
tk.Entry(self._tcp_frame, textvariable=self.listen_port_var, width=8,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
|
font=MONO).grid(row=0, column=1, sticky="w", **pad)
|
||||||
|
|
||||||
|
tk.Label(self._tcp_frame, text="Device host:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=2, sticky="e", **pad)
|
||||||
|
self.remote_host_var = tk.StringVar(value="63.43.212.232")
|
||||||
|
tk.Entry(self._tcp_frame, textvariable=self.remote_host_var, width=18,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
|
font=MONO).grid(row=0, column=3, sticky="w", **pad)
|
||||||
|
|
||||||
|
tk.Label(self._tcp_frame, text="Port:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=4, sticky="e", **pad)
|
||||||
|
self.remote_port_var = tk.StringVar(value="9034")
|
||||||
|
tk.Entry(self._tcp_frame, textvariable=self.remote_port_var, width=8,
|
||||||
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
|
font=MONO).grid(row=0, column=5, sticky="w", **pad)
|
||||||
|
|
||||||
|
# Row 2: log dir
|
||||||
|
tk.Label(cfg, text="Log dir:", bg=BG2, fg=FG, font=MONO).grid(row=2, column=0, sticky="e", **pad)
|
||||||
self.logdir_var = tk.StringVar(value=str(SCRIPT_DIR / "bridges" / "captures"))
|
self.logdir_var = tk.StringVar(value=str(SCRIPT_DIR / "bridges" / "captures"))
|
||||||
tk.Entry(cfg, textvariable=self.logdir_var, width=40,
|
tk.Entry(cfg, textvariable=self.logdir_var, width=40,
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
||||||
font=MONO).grid(row=1, column=1, columnspan=4, sticky="we", **pad)
|
font=MONO).grid(row=2, column=1, columnspan=4, sticky="we", **pad)
|
||||||
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=1, column=5, **pad)
|
font=MONO, command=self._choose_dir).grid(row=2, column=5, **pad)
|
||||||
|
|
||||||
# Row 2: raw taps (always enabled — timestamped names generated at start)
|
# Row 3: raw capture checkboxes
|
||||||
self._raw_bw_on = tk.BooleanVar(value=True)
|
self._raw_bw_on = tk.BooleanVar(value=True)
|
||||||
self._raw_s3_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,
|
tk.Checkbutton(cfg, text="Capture BW->S3 raw", variable=self._raw_bw_on,
|
||||||
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
||||||
font=MONO).grid(row=2, column=0, columnspan=2, sticky="w", **pad)
|
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,
|
tk.Checkbutton(cfg, text="Capture S3->BW raw", variable=self._raw_s3_on,
|
||||||
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
||||||
font=MONO).grid(row=2, column=2, columnspan=2, sticky="w", **pad)
|
font=MONO).grid(row=3, column=2, columnspan=2, sticky="w", **pad)
|
||||||
|
|
||||||
# Row 3: 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)
|
||||||
|
|
||||||
@@ -190,6 +239,14 @@ class BridgePanel(tk.Frame):
|
|||||||
|
|
||||||
# ── helpers ───────────────────────────────────────────────────────────
|
# ── helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _on_mode_change(self) -> None:
|
||||||
|
if self._mode.get() == "serial":
|
||||||
|
self._tcp_frame.grid_remove()
|
||||||
|
self._serial_frame.grid(row=1, column=0, columnspan=6, sticky="w")
|
||||||
|
else:
|
||||||
|
self._serial_frame.grid_remove()
|
||||||
|
self._tcp_frame.grid(row=1, column=0, columnspan=6, sticky="w")
|
||||||
|
|
||||||
def _choose_dir(self) -> None:
|
def _choose_dir(self) -> None:
|
||||||
path = filedialog.askdirectory(initialdir=self.logdir_var.get())
|
path = filedialog.askdirectory(initialdir=self.logdir_var.get())
|
||||||
if path:
|
if path:
|
||||||
@@ -201,9 +258,50 @@ 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")
|
||||||
|
|
||||||
# ── bridge control ────────────────────────────────────────────────────
|
# ── unified log-queue polling (serial subprocess + TCP threads both push here)
|
||||||
|
|
||||||
|
def _poll_log_q(self) -> None:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = self._log_q.get_nowait()
|
||||||
|
if msg == "<<exit>>":
|
||||||
|
self._bridge_ended()
|
||||||
|
self._on_stopped()
|
||||||
|
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) ───────────────────────
|
||||||
|
|
||||||
def start_bridge(self) -> None:
|
def start_bridge(self) -> None:
|
||||||
|
if self._mode.get() == "tcp":
|
||||||
|
self._start_tcp()
|
||||||
|
else:
|
||||||
|
self._start_serial()
|
||||||
|
|
||||||
|
def stop_bridge(self) -> None:
|
||||||
|
if self._mode.get() == "tcp":
|
||||||
|
self._stop_tcp()
|
||||||
|
else:
|
||||||
|
self._stop_serial()
|
||||||
|
|
||||||
|
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.mark_btn.configure(state="disabled")
|
||||||
|
self._append_log("== Bridge stopped ==\n")
|
||||||
|
|
||||||
|
# ── serial mode ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _start_serial(self) -> None:
|
||||||
if self.process and self.process.poll() is None:
|
if self.process and self.process.poll() is None:
|
||||||
messagebox.showinfo("Bridge", "Bridge is already running.")
|
messagebox.showinfo("Bridge", "Bridge is already running.")
|
||||||
return
|
return
|
||||||
@@ -231,7 +329,6 @@ class BridgePanel(tk.Frame):
|
|||||||
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin")
|
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin")
|
||||||
args += ["--raw-s3", raw_s3_path]
|
args += ["--raw-s3", raw_s3_path]
|
||||||
|
|
||||||
# Structured bin path — written by bridge automatically, named by ts
|
|
||||||
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:
|
||||||
@@ -253,11 +350,9 @@ class BridgePanel(tk.Frame):
|
|||||||
self.stop_btn.configure(state="normal", bg=RED)
|
self.stop_btn.configure(state="normal", bg=RED)
|
||||||
self.mark_btn.configure(state="normal")
|
self.mark_btn.configure(state="normal")
|
||||||
self._append_log(f"== Bridge started [{ts}] ==\n")
|
self._append_log(f"== Bridge started [{ts}] ==\n")
|
||||||
|
|
||||||
# Notify parent so Analyzer can wire up live mode
|
|
||||||
self._on_started(raw_bw_path, raw_s3_path, struct_bin_path)
|
self._on_started(raw_bw_path, raw_s3_path, struct_bin_path)
|
||||||
|
|
||||||
def stop_bridge(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:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
try:
|
try:
|
||||||
@@ -267,177 +362,20 @@ class BridgePanel(tk.Frame):
|
|||||||
self._bridge_ended()
|
self._bridge_ended()
|
||||||
self._on_stopped()
|
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.mark_btn.configure(state="disabled")
|
|
||||||
self._append_log("== Bridge stopped ==\n")
|
|
||||||
|
|
||||||
def _reader_thread(self) -> None:
|
def _reader_thread(self) -> None:
|
||||||
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._stdout_q.put(line)
|
self._log_q.put(line)
|
||||||
self._stdout_q.put("<<exit>>")
|
self._log_q.put("<<exit>>")
|
||||||
|
|
||||||
def _poll_stdout(self) -> None:
|
# ── TCP mode ──────────────────────────────────────────────────────────
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
line = self._stdout_q.get_nowait()
|
|
||||||
if line == "<<exit>>":
|
|
||||||
self._bridge_ended()
|
|
||||||
self._on_stopped()
|
|
||||||
break
|
|
||||||
self._append_log(line)
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.after(100, self._poll_stdout)
|
|
||||||
|
|
||||||
def add_mark(self) -> None:
|
def _start_tcp(self) -> None:
|
||||||
if not self.process or not self.process.stdin or self.process.poll() is not None:
|
|
||||||
return
|
|
||||||
label = simpledialog.askstring("Mark", "Enter label for this mark:", parent=self)
|
|
||||||
if not label or not label.strip():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.process.stdin.write("m\n")
|
|
||||||
self.process.stdin.write(label.strip() + "\n")
|
|
||||||
self.process.stdin.flush()
|
|
||||||
self._append_log(f"[MARK] {label.strip()}\n")
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("Error", f"Failed to send mark:\n{e}")
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
# TCP Bridge panel — MITM capture over IP
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class TcpBridgePanel(tk.Frame):
|
|
||||||
"""
|
|
||||||
TCP man-in-the-middle capture panel.
|
|
||||||
|
|
||||||
Listens on a local TCP port for an incoming Blastware connection, forwards
|
|
||||||
all traffic to the real device, and saves both directions to raw .bin files.
|
|
||||||
|
|
||||||
Calls on_bridge_started(raw_bw_path, raw_s3_path, None) when a session
|
|
||||||
begins so the Analyzer can wire up live mode — same signature as BridgePanel.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped, **kw):
|
|
||||||
super().__init__(parent, bg=BG2, **kw)
|
|
||||||
self._on_started = on_bridge_started
|
|
||||||
self._on_stopped = on_bridge_stopped
|
|
||||||
self._server: Optional[socket.socket] = None
|
|
||||||
self._stop_event = threading.Event()
|
|
||||||
self._log_q: queue.Queue[str] = queue.Queue()
|
|
||||||
self._build()
|
|
||||||
self._poll_log_q()
|
|
||||||
|
|
||||||
# ── build ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _build(self) -> None:
|
|
||||||
pad = {"padx": 6, "pady": 4}
|
|
||||||
|
|
||||||
cfg = tk.Frame(self, bg=BG2)
|
|
||||||
cfg.pack(side=tk.TOP, fill=tk.X, padx=4, pady=4)
|
|
||||||
|
|
||||||
# Row 0: listen port + remote host + remote port
|
|
||||||
tk.Label(cfg, text="Listen port:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=0, sticky="e", **pad)
|
|
||||||
self.listen_port_var = tk.StringVar(value="9034")
|
|
||||||
tk.Entry(cfg, textvariable=self.listen_port_var, width=8,
|
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
|
||||||
font=MONO).grid(row=0, column=1, sticky="w", **pad)
|
|
||||||
|
|
||||||
tk.Label(cfg, text="Device host:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=2, sticky="e", **pad)
|
|
||||||
self.remote_host_var = tk.StringVar(value="63.43.212.232")
|
|
||||||
tk.Entry(cfg, textvariable=self.remote_host_var, width=18,
|
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
|
||||||
font=MONO).grid(row=0, column=3, sticky="w", **pad)
|
|
||||||
|
|
||||||
tk.Label(cfg, text="Port:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=4, sticky="e", **pad)
|
|
||||||
self.remote_port_var = tk.StringVar(value="9034")
|
|
||||||
tk.Entry(cfg, textvariable=self.remote_port_var, width=8,
|
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
|
||||||
font=MONO).grid(row=0, column=5, sticky="w", **pad)
|
|
||||||
|
|
||||||
# Row 1: log dir
|
|
||||||
tk.Label(cfg, text="Log dir:", bg=BG2, fg=FG, font=MONO).grid(row=1, column=0, sticky="e", **pad)
|
|
||||||
self.logdir_var = tk.StringVar(value=str(SCRIPT_DIR / "bridges" / "captures" / "mitm"))
|
|
||||||
tk.Entry(cfg, textvariable=self.logdir_var, width=40,
|
|
||||||
bg=BG3, fg=FG, insertbackground=FG, relief="flat",
|
|
||||||
font=MONO).grid(row=1, column=1, columnspan=4, sticky="we", **pad)
|
|
||||||
tk.Button(cfg, text="Browse", bg=BG3, fg=FG, relief="flat", cursor="hand2",
|
|
||||||
font=MONO, command=self._choose_dir).grid(row=1, column=5, **pad)
|
|
||||||
|
|
||||||
# Row 2: capture checkboxes
|
|
||||||
self._raw_bw_on = tk.BooleanVar(value=True)
|
|
||||||
self._raw_s3_on = tk.BooleanVar(value=True)
|
|
||||||
tk.Checkbutton(cfg, text="Capture BW→device raw", variable=self._raw_bw_on,
|
|
||||||
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
|
||||||
font=MONO).grid(row=2, column=0, columnspan=2, sticky="w", **pad)
|
|
||||||
tk.Checkbutton(cfg, text="Capture device→BW raw", variable=self._raw_s3_on,
|
|
||||||
bg=BG2, fg=FG, selectcolor=BG3, activebackground=BG2,
|
|
||||||
font=MONO).grid(row=2, column=2, columnspan=2, sticky="w", **pad)
|
|
||||||
|
|
||||||
# Row 3: buttons + status
|
|
||||||
btn_row = tk.Frame(self, bg=BG2)
|
|
||||||
btn_row.pack(side=tk.TOP, fill=tk.X, padx=4, pady=2)
|
|
||||||
|
|
||||||
self.start_btn = tk.Button(btn_row, text="Start Listening", bg=GREEN, fg="#000000",
|
|
||||||
relief="flat", padx=12, cursor="hand2", font=MONO_B,
|
|
||||||
command=self.start_server)
|
|
||||||
self.start_btn.pack(side=tk.LEFT, padx=6)
|
|
||||||
|
|
||||||
self.stop_btn = tk.Button(btn_row, text="Stop", bg=BG3, fg=FG,
|
|
||||||
relief="flat", padx=12, cursor="hand2", font=MONO,
|
|
||||||
command=self.stop_server, state="disabled")
|
|
||||||
self.stop_btn.pack(side=tk.LEFT, padx=4)
|
|
||||||
|
|
||||||
self.status_var = tk.StringVar(value="Idle")
|
|
||||||
tk.Label(btn_row, textvariable=self.status_var,
|
|
||||||
bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=10)
|
|
||||||
|
|
||||||
# Log output
|
|
||||||
self.log_view = scrolledtext.ScrolledText(
|
|
||||||
self, height=18, font=MONO_SM,
|
|
||||||
bg=BG, fg=FG, insertbackground=FG,
|
|
||||||
relief="flat", state="disabled",
|
|
||||||
)
|
|
||||||
self.log_view.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
|
||||||
|
|
||||||
# ── helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _choose_dir(self) -> None:
|
|
||||||
path = filedialog.askdirectory(initialdir=self.logdir_var.get())
|
|
||||||
if path:
|
|
||||||
self.logdir_var.set(path)
|
|
||||||
|
|
||||||
def _append_log(self, text: str) -> None:
|
|
||||||
self.log_view.configure(state="normal")
|
|
||||||
self.log_view.insert(tk.END, text)
|
|
||||||
self.log_view.see(tk.END)
|
|
||||||
self.log_view.configure(state="disabled")
|
|
||||||
|
|
||||||
def _poll_log_q(self) -> None:
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
msg = self._log_q.get_nowait()
|
|
||||||
if msg == "<<session_ended>>":
|
|
||||||
if self._server is not None:
|
if self._server is not None:
|
||||||
self.status_var.set(f"Listening on :{self.listen_port_var.get()}")
|
messagebox.showinfo("Bridge", "TCP bridge is already listening.")
|
||||||
self._on_stopped()
|
return
|
||||||
else:
|
|
||||||
self._append_log(msg)
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.after(100, self._poll_log_q)
|
|
||||||
|
|
||||||
# ── server control ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def start_server(self) -> None:
|
|
||||||
try:
|
try:
|
||||||
listen_port = int(self.listen_port_var.get().strip())
|
listen_port = int(self.listen_port_var.get().strip())
|
||||||
remote_host = self.remote_host_var.get().strip()
|
remote_host = self.remote_host_var.get().strip()
|
||||||
@@ -460,17 +398,17 @@ class TcpBridgePanel(tk.Frame):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._server = srv
|
self._server = srv
|
||||||
self._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"
|
f" Forwarding to {remote_host}:{remote_port}\n==\n"
|
||||||
f" Point Blastware call-home to this machine on port {listen_port}\n==\n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logdir = self.logdir_var.get().strip() or "."
|
logdir = self.logdir_var.get().strip() or "."
|
||||||
@@ -483,25 +421,20 @@ class TcpBridgePanel(tk.Frame):
|
|||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
def stop_server(self) -> None:
|
def _stop_tcp(self) -> None:
|
||||||
self._stop_event.set()
|
self._tcp_stop_event.set()
|
||||||
if self._server:
|
if self._server:
|
||||||
try:
|
try:
|
||||||
self._server.close()
|
self._server.close()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
self._server = None
|
self._server = None
|
||||||
self.start_btn.configure(state="normal")
|
self._bridge_ended()
|
||||||
self.stop_btn.configure(state="disabled", bg=BG3)
|
|
||||||
self.status_var.set("Idle")
|
|
||||||
self._append_log("== TCP Bridge stopped ==\n")
|
|
||||||
self._on_stopped()
|
self._on_stopped()
|
||||||
|
|
||||||
# ── accept / session threads ──────────────────────────────────────────
|
|
||||||
|
|
||||||
def _accept_loop(self, srv: socket.socket, remote_host: str, remote_port: int,
|
def _accept_loop(self, srv: socket.socket, remote_host: str, remote_port: int,
|
||||||
logdir: str, raw_bw_on: bool, raw_s3_on: bool) -> None:
|
logdir: str, raw_bw_on: bool, raw_s3_on: bool) -> None:
|
||||||
while not self._stop_event.is_set():
|
while not self._tcp_stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
client_sock, addr = srv.accept()
|
client_sock, addr = srv.accept()
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
@@ -527,17 +460,17 @@ class TcpBridgePanel(tk.Frame):
|
|||||||
raw_bw_path = os.path.join(logdir, f"raw_bw_{ts}.bin") if raw_bw_on else None
|
raw_bw_path = os.path.join(logdir, f"raw_bw_{ts}.bin") if raw_bw_on else None
|
||||||
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin") if raw_s3_on else None
|
raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin") if raw_s3_on else None
|
||||||
|
|
||||||
self.after(0, self._notify_session_start, raw_bw_path, raw_s3_path, peer, remote_host, remote_port)
|
self.after(0, self._notify_tcp_session_start,
|
||||||
|
raw_bw_path, raw_s3_path, peer, remote_host, remote_port)
|
||||||
self._run_session(client_sock, dev_sock, raw_bw_path, raw_s3_path, ts)
|
self._run_tcp_session(client_sock, dev_sock, raw_bw_path, raw_s3_path, ts)
|
||||||
|
|
||||||
self._log_q.put("<<session_ended>>")
|
self._log_q.put("<<session_ended>>")
|
||||||
|
|
||||||
def _notify_session_start(self, raw_bw_path, raw_s3_path, peer, remote_host, remote_port) -> None:
|
def _notify_tcp_session_start(self, raw_bw_path, raw_s3_path,
|
||||||
|
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._on_started(raw_bw_path, raw_s3_path, None)
|
||||||
|
|
||||||
def _run_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],
|
raw_bw_path: Optional[str], raw_s3_path: Optional[str],
|
||||||
ts: str) -> None:
|
ts: str) -> None:
|
||||||
bw_fh = open(raw_bw_path, "wb") if raw_bw_path else None
|
bw_fh = open(raw_bw_path, "wb") if raw_bw_path else None
|
||||||
@@ -587,6 +520,26 @@ class TcpBridgePanel(tk.Frame):
|
|||||||
if raw_s3_path:
|
if raw_s3_path:
|
||||||
self._log_q.put(f"[TCP] S3 capture: {raw_s3_path}\n")
|
self._log_q.put(f"[TCP] S3 capture: {raw_s3_path}\n")
|
||||||
|
|
||||||
|
# ── marks ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def add_mark(self) -> None:
|
||||||
|
label = simpledialog.askstring("Mark", "Enter label for this mark:", parent=self)
|
||||||
|
if not label or not label.strip():
|
||||||
|
return
|
||||||
|
if self._mode.get() == "tcp":
|
||||||
|
ts = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
|
self._append_log(f"[MARK {ts}] {label.strip()}\n")
|
||||||
|
else:
|
||||||
|
if not self.process or not self.process.stdin or self.process.poll() is not None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.process.stdin.write("m\n")
|
||||||
|
self.process.stdin.write(label.strip() + "\n")
|
||||||
|
self.process.stdin.flush()
|
||||||
|
self._append_log(f"[MARK] {label.strip()}\n")
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Error", f"Failed to send mark:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Analyzer panel (tk.Frame — lives inside a notebook tab)
|
# Analyzer panel (tk.Frame — lives inside a notebook tab)
|
||||||
@@ -2166,13 +2119,6 @@ class SeismoLab(tk.Tk):
|
|||||||
)
|
)
|
||||||
nb.add(self._bridge_panel, text=" Bridge ")
|
nb.add(self._bridge_panel, text=" Bridge ")
|
||||||
|
|
||||||
self._tcp_bridge_panel = TcpBridgePanel(
|
|
||||||
nb,
|
|
||||||
on_bridge_started=self._on_bridge_started,
|
|
||||||
on_bridge_stopped=self._on_bridge_stopped,
|
|
||||||
)
|
|
||||||
nb.add(self._tcp_bridge_panel, text=" TCP Capture ")
|
|
||||||
|
|
||||||
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 ")
|
||||||
|
|
||||||
@@ -2213,7 +2159,6 @@ class SeismoLab(tk.Tk):
|
|||||||
|
|
||||||
def _on_close(self) -> None:
|
def _on_close(self) -> None:
|
||||||
self._bridge_panel.stop_bridge()
|
self._bridge_panel.stop_bridge()
|
||||||
self._tcp_bridge_panel.stop_server()
|
|
||||||
self._serial_watch_panel._stop()
|
self._serial_watch_panel._stop()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user