host.nfc_tool.serial_panel

Serial port connection panel — shared across all tabs.

  1"""Serial port connection panel — shared across all tabs."""
  2
  3import tkinter as tk
  4from tkinter import ttk
  5
  6import serial.tools.list_ports
  7
  8
  9class SerialPanel(ttk.Frame):
 10    """COM port selector with connect/disconnect toggle."""
 11
 12    def __init__(self, parent, on_connect=None, on_disconnect=None):
 13        super().__init__(parent)
 14        self._on_connect = on_connect
 15        self._on_disconnect = on_disconnect
 16        self._connected = False
 17
 18        self._build_ui()
 19        self._refresh_ports()
 20
 21    def _build_ui(self):
 22        ttk.Label(self, text="Port:").pack(side=tk.LEFT, padx=(0, 4))
 23
 24        self._port_var = tk.StringVar()
 25        self._port_combo = ttk.Combobox(
 26            self, textvariable=self._port_var,
 27            state='readonly', width=30,
 28        )
 29        self._port_combo.pack(side=tk.LEFT, padx=(0, 4))
 30
 31        self._refresh_btn = ttk.Button(
 32            self, text="Refresh", command=self._refresh_ports, width=8,
 33        )
 34        self._refresh_btn.pack(side=tk.LEFT, padx=(0, 8))
 35
 36        self._connect_btn = ttk.Button(
 37            self, text="Connect", command=self._toggle_connect, width=12,
 38        )
 39        self._connect_btn.pack(side=tk.LEFT, padx=(0, 8))
 40
 41        self._status_var = tk.StringVar(value="Disconnected")
 42        self._status_label = ttk.Label(
 43            self, textvariable=self._status_var,
 44            foreground='gray',
 45        )
 46        self._status_label.pack(side=tk.LEFT)
 47
 48    def _refresh_ports(self):
 49        """Rescan available serial ports."""
 50        ports = serial.tools.list_ports.comports()
 51        descriptions = []
 52        self._port_map = {}
 53        for p in sorted(ports, key=lambda x: x.device):
 54            desc = f"{p.device} - {p.description}"
 55            descriptions.append(desc)
 56            self._port_map[desc] = p.device
 57
 58        self._port_combo['values'] = descriptions
 59        if descriptions and not self._port_var.get():
 60            self._port_combo.current(0)
 61
 62    def _toggle_connect(self):
 63        if self._connected:
 64            self._disconnect()
 65        else:
 66            self._connect()
 67
 68    def _connect(self):
 69        desc = self._port_var.get()
 70        port = self._port_map.get(desc, desc)
 71        if not port:
 72            self.set_status("No port selected", 'red')
 73            return
 74
 75        self.set_status("Connecting...", 'orange')
 76        self._set_controls_enabled(False)
 77
 78        if self._on_connect:
 79            self._on_connect(port)
 80
 81    def _disconnect(self):
 82        if self._on_disconnect:
 83            self._on_disconnect()
 84
 85    def set_connected(self, success, error_msg=''):
 86        """Called by app after worker reports connect result."""
 87        if success:
 88            self._connected = True
 89            self._connect_btn.configure(text="Disconnect")
 90            self.set_status("Connected", 'green')
 91            self._port_combo.configure(state='disabled')
 92            self._refresh_btn.configure(state='disabled')
 93        else:
 94            self._connected = False
 95            self._set_controls_enabled(True)
 96            msg = f"Error: {error_msg}" if error_msg else "Connection failed"
 97            self.set_status(msg, 'red')
 98
 99    def set_disconnected(self):
100        """Called by app after disconnect."""
101        self._connected = False
102        self._connect_btn.configure(text="Connect")
103        self._set_controls_enabled(True)
104        self.set_status("Disconnected", 'gray')
105
106    def set_status(self, text, color='gray'):
107        self._status_var.set(text)
108        self._status_label.configure(foreground=color)
109
110    def _set_controls_enabled(self, enabled):
111        state = 'readonly' if enabled else 'disabled'
112        self._port_combo.configure(state=state)
113        self._refresh_btn.configure(
114            state='normal' if enabled else 'disabled')
115        self._connect_btn.configure(text="Connect")
116
117    @property
118    def is_connected(self):
119        return self._connected
class SerialPanel(tkinter.ttk.Frame):
 10class SerialPanel(ttk.Frame):
 11    """COM port selector with connect/disconnect toggle."""
 12
 13    def __init__(self, parent, on_connect=None, on_disconnect=None):
 14        super().__init__(parent)
 15        self._on_connect = on_connect
 16        self._on_disconnect = on_disconnect
 17        self._connected = False
 18
 19        self._build_ui()
 20        self._refresh_ports()
 21
 22    def _build_ui(self):
 23        ttk.Label(self, text="Port:").pack(side=tk.LEFT, padx=(0, 4))
 24
 25        self._port_var = tk.StringVar()
 26        self._port_combo = ttk.Combobox(
 27            self, textvariable=self._port_var,
 28            state='readonly', width=30,
 29        )
 30        self._port_combo.pack(side=tk.LEFT, padx=(0, 4))
 31
 32        self._refresh_btn = ttk.Button(
 33            self, text="Refresh", command=self._refresh_ports, width=8,
 34        )
 35        self._refresh_btn.pack(side=tk.LEFT, padx=(0, 8))
 36
 37        self._connect_btn = ttk.Button(
 38            self, text="Connect", command=self._toggle_connect, width=12,
 39        )
 40        self._connect_btn.pack(side=tk.LEFT, padx=(0, 8))
 41
 42        self._status_var = tk.StringVar(value="Disconnected")
 43        self._status_label = ttk.Label(
 44            self, textvariable=self._status_var,
 45            foreground='gray',
 46        )
 47        self._status_label.pack(side=tk.LEFT)
 48
 49    def _refresh_ports(self):
 50        """Rescan available serial ports."""
 51        ports = serial.tools.list_ports.comports()
 52        descriptions = []
 53        self._port_map = {}
 54        for p in sorted(ports, key=lambda x: x.device):
 55            desc = f"{p.device} - {p.description}"
 56            descriptions.append(desc)
 57            self._port_map[desc] = p.device
 58
 59        self._port_combo['values'] = descriptions
 60        if descriptions and not self._port_var.get():
 61            self._port_combo.current(0)
 62
 63    def _toggle_connect(self):
 64        if self._connected:
 65            self._disconnect()
 66        else:
 67            self._connect()
 68
 69    def _connect(self):
 70        desc = self._port_var.get()
 71        port = self._port_map.get(desc, desc)
 72        if not port:
 73            self.set_status("No port selected", 'red')
 74            return
 75
 76        self.set_status("Connecting...", 'orange')
 77        self._set_controls_enabled(False)
 78
 79        if self._on_connect:
 80            self._on_connect(port)
 81
 82    def _disconnect(self):
 83        if self._on_disconnect:
 84            self._on_disconnect()
 85
 86    def set_connected(self, success, error_msg=''):
 87        """Called by app after worker reports connect result."""
 88        if success:
 89            self._connected = True
 90            self._connect_btn.configure(text="Disconnect")
 91            self.set_status("Connected", 'green')
 92            self._port_combo.configure(state='disabled')
 93            self._refresh_btn.configure(state='disabled')
 94        else:
 95            self._connected = False
 96            self._set_controls_enabled(True)
 97            msg = f"Error: {error_msg}" if error_msg else "Connection failed"
 98            self.set_status(msg, 'red')
 99
100    def set_disconnected(self):
101        """Called by app after disconnect."""
102        self._connected = False
103        self._connect_btn.configure(text="Connect")
104        self._set_controls_enabled(True)
105        self.set_status("Disconnected", 'gray')
106
107    def set_status(self, text, color='gray'):
108        self._status_var.set(text)
109        self._status_label.configure(foreground=color)
110
111    def _set_controls_enabled(self, enabled):
112        state = 'readonly' if enabled else 'disabled'
113        self._port_combo.configure(state=state)
114        self._refresh_btn.configure(
115            state='normal' if enabled else 'disabled')
116        self._connect_btn.configure(text="Connect")
117
118    @property
119    def is_connected(self):
120        return self._connected

COM port selector with connect/disconnect toggle.

SerialPanel(parent, on_connect=None, on_disconnect=None)
13    def __init__(self, parent, on_connect=None, on_disconnect=None):
14        super().__init__(parent)
15        self._on_connect = on_connect
16        self._on_disconnect = on_disconnect
17        self._connected = False
18
19        self._build_ui()
20        self._refresh_ports()

Construct a Ttk Frame with parent master.

STANDARD OPTIONS

class, cursor, style, takefocus

WIDGET-SPECIFIC OPTIONS

borderwidth, relief, padding, width, height
def set_connected(self, success, error_msg=''):
86    def set_connected(self, success, error_msg=''):
87        """Called by app after worker reports connect result."""
88        if success:
89            self._connected = True
90            self._connect_btn.configure(text="Disconnect")
91            self.set_status("Connected", 'green')
92            self._port_combo.configure(state='disabled')
93            self._refresh_btn.configure(state='disabled')
94        else:
95            self._connected = False
96            self._set_controls_enabled(True)
97            msg = f"Error: {error_msg}" if error_msg else "Connection failed"
98            self.set_status(msg, 'red')

Called by app after worker reports connect result.

def set_disconnected(self):
100    def set_disconnected(self):
101        """Called by app after disconnect."""
102        self._connected = False
103        self._connect_btn.configure(text="Connect")
104        self._set_controls_enabled(True)
105        self.set_status("Disconnected", 'gray')

Called by app after disconnect.

def set_status(self, text, color='gray'):
107    def set_status(self, text, color='gray'):
108        self._status_var.set(text)
109        self._status_label.configure(foreground=color)
is_connected
118    @property
119    def is_connected(self):
120        return self._connected