From 6861d9ed970055d0d68d66f60bacc940d7f585e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 23:01:31 +0000 Subject: [PATCH] Merge TCP mode into Bridge tab (Serial/TCP radio toggle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- seismo_lab.py | 381 +++++++++++++++++++++----------------------------- 1 file changed, 163 insertions(+), 218 deletions(-) diff --git a/seismo_lab.py b/seismo_lab.py index 11e7f11..0f757f2 100644 --- a/seismo_lab.py +++ b/seismo_lab.py @@ -97,19 +97,33 @@ class AnalyzerState: class BridgePanel(tk.Frame): """ - All 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. + Bridge controls and live log output. + + 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): 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 + # serial state 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._poll_stdout() + self._poll_log_q() # ── build ───────────────────────────────────────────────────────────── @@ -119,45 +133,80 @@ class BridgePanel(tk.Frame): cfg = tk.Frame(self, bg=BG2) cfg.pack(side=tk.TOP, fill=tk.X, padx=4, pady=4) - # Row 0: ports - tk.Label(cfg, text="BW COM:", bg=BG2, fg=FG, font=MONO).grid(row=0, column=0, sticky="e", **pad) + # Row 0: mode selector + 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") - 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", 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") - 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", 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") - 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", 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) + # Row 1b: TCP connection fields (hidden until TCP mode selected) + 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")) 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) + 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", - 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_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=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, 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.pack(side=tk.TOP, fill=tk.X, padx=4, pady=2) @@ -190,6 +239,14 @@ class BridgePanel(tk.Frame): # ── 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: path = filedialog.askdirectory(initialdir=self.logdir_var.get()) if path: @@ -201,9 +258,50 @@ class BridgePanel(tk.Frame): self.log_view.see(tk.END) 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 == "<>": + self._bridge_ended() + self._on_stopped() + elif msg == "<>": + 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: + 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: messagebox.showinfo("Bridge", "Bridge is already running.") return @@ -231,7 +329,6 @@ class BridgePanel(tk.Frame): raw_s3_path = os.path.join(logdir, f"raw_s3_{ts}.bin") 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") try: @@ -253,11 +350,9 @@ class BridgePanel(tk.Frame): self.stop_btn.configure(state="normal", bg=RED) self.mark_btn.configure(state="normal") 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) - def stop_bridge(self) -> None: + def _stop_serial(self) -> None: if self.process and self.process.poll() is None: self.process.terminate() try: @@ -267,177 +362,20 @@ class BridgePanel(tk.Frame): self._bridge_ended() self._on_stopped() - def _bridge_ended(self) -> None: - self.status_var.set("Stopped") - self.start_btn.configure(state="normal") - self.stop_btn.configure(state="disabled", bg=BG3) - self.mark_btn.configure(state="disabled") - self._append_log("== Bridge stopped ==\n") - def _reader_thread(self) -> None: if not self.process or not self.process.stdout: return for line in self.process.stdout: - self._stdout_q.put(line) - self._stdout_q.put("<>") + self._log_q.put(line) + self._log_q.put("<>") - def _poll_stdout(self) -> None: - try: - while True: - line = self._stdout_q.get_nowait() - if line == "<>": - self._bridge_ended() - self._on_stopped() - break - self._append_log(line) - except queue.Empty: - pass - finally: - self.after(100, self._poll_stdout) + # ── TCP mode ────────────────────────────────────────────────────────── - def add_mark(self) -> None: - if not self.process or not self.process.stdin or self.process.poll() is not None: + def _start_tcp(self) -> None: + if self._server is not None: + messagebox.showinfo("Bridge", "TCP bridge is already listening.") 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 == "<>": - 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) - - # ── server control ──────────────────────────────────────────────────── - - def start_server(self) -> None: try: listen_port = int(self.listen_port_var.get().strip()) remote_host = self.remote_host_var.get().strip() @@ -460,20 +398,20 @@ class TcpBridgePanel(tk.Frame): return self._server = srv - self._stop_event.clear() + self._tcp_stop_event.clear() self.start_btn.configure(state="disabled") self.stop_btn.configure(state="normal", bg=RED) + self.mark_btn.configure(state="normal") self.status_var.set(f"Listening on :{listen_port}") ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self._append_log( f"== TCP Bridge started [{ts}]\n" f" Listening on 0.0.0.0:{listen_port}\n" - f" Forwarding to {remote_host}:{remote_port}\n" - f" Point Blastware call-home to this machine on port {listen_port}\n==\n" + f" Forwarding to {remote_host}:{remote_port}\n==\n" ) - 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() @@ -483,25 +421,20 @@ class TcpBridgePanel(tk.Frame): daemon=True, ).start() - def stop_server(self) -> None: - self._stop_event.set() + def _stop_tcp(self) -> None: + self._tcp_stop_event.set() if self._server: try: self._server.close() except OSError: pass self._server = None - self.start_btn.configure(state="normal") - self.stop_btn.configure(state="disabled", bg=BG3) - self.status_var.set("Idle") - self._append_log("== TCP Bridge stopped ==\n") + self._bridge_ended() self._on_stopped() - # ── accept / session threads ────────────────────────────────────────── - def _accept_loop(self, srv: socket.socket, remote_host: str, remote_port: int, 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: client_sock, addr = srv.accept() except socket.timeout: @@ -527,19 +460,19 @@ class TcpBridgePanel(tk.Frame): 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 - self.after(0, self._notify_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.after(0, self._notify_tcp_session_start, + raw_bw_path, raw_s3_path, peer, remote_host, remote_port) + self._run_tcp_session(client_sock, dev_sock, raw_bw_path, raw_s3_path, ts) self._log_q.put("<>") - 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._on_started(raw_bw_path, raw_s3_path, None) - def _run_session(self, bw_sock: socket.socket, dev_sock: socket.socket, - raw_bw_path: Optional[str], raw_s3_path: Optional[str], - ts: str) -> None: + def _run_tcp_session(self, bw_sock: socket.socket, dev_sock: socket.socket, + raw_bw_path: Optional[str], raw_s3_path: Optional[str], + 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] @@ -587,6 +520,26 @@ class TcpBridgePanel(tk.Frame): if raw_s3_path: 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) @@ -2166,13 +2119,6 @@ class SeismoLab(tk.Tk): ) 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) nb.add(self._analyzer_panel, text=" Analyzer ") @@ -2213,7 +2159,6 @@ class SeismoLab(tk.Tk): def _on_close(self) -> None: self._bridge_panel.stop_bridge() - self._tcp_bridge_panel.stop_server() self._serial_watch_panel._stop() self.destroy()