"""탭1: 컨트롤러 검색/IP변경 패널""" import tkinter as tk from tkinter import ttk, messagebox import threading from models.controller import Controller from network.udp_discovery import get_network_interfaces, search_controllers, send_sett, send_reset class DiscoveryPanel(ttk.Frame): """컨트롤러 검색 및 설정 변경 패널.""" def __init__(self, parent, shared_controllers: list[Controller]): super().__init__(parent) self._controllers = shared_controllers self._searching = False self._build_ui() def _build_ui(self): # --- 상단: NIC 선택 + 검색 --- top = ttk.LabelFrame(self, text="컨트롤러 검색", padding=5) top.pack(fill=tk.X, padx=5, pady=5) ttk.Label(top, text="네트워크:").grid(row=0, column=0, padx=(0, 5)) self._nic_var = tk.StringVar() self._nic_combo = ttk.Combobox(top, textvariable=self._nic_var, state="readonly", width=20) self._nic_combo.grid(row=0, column=1, padx=(0, 10)) self._btn_search = ttk.Button(top, text="검색", command=self._on_search) self._btn_search.grid(row=0, column=2, padx=(0, 5)) self._search_status = ttk.Label(top, text="") self._search_status.grid(row=0, column=3, padx=5) self._refresh_nics() # --- 중간: 컨트롤러 목록 --- mid = ttk.LabelFrame(self, text="검색된 컨트롤러", padding=5) mid.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) cols = ("mac", "ip", "port", "name", "subnet", "gateway") self._tree = ttk.Treeview(mid, columns=cols, show="headings", height=8) self._tree.heading("mac", text="MAC") self._tree.heading("ip", text="IP") self._tree.heading("port", text="포트") self._tree.heading("name", text="이름") self._tree.heading("subnet", text="서브넷") self._tree.heading("gateway", text="게이트웨이") self._tree.column("mac", width=140) self._tree.column("ip", width=120) self._tree.column("port", width=60) self._tree.column("name", width=120) self._tree.column("subnet", width=120) self._tree.column("gateway", width=120) scroll = ttk.Scrollbar(mid, orient=tk.VERTICAL, command=self._tree.yview) self._tree.configure(yscrollcommand=scroll.set) self._tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll.pack(side=tk.RIGHT, fill=tk.Y) self._tree.bind("<>", self._on_select) # --- 로그 영역 --- log_frame = ttk.LabelFrame(self, text="검색 로그", padding=5) log_frame.pack(fill=tk.X, padx=5, pady=(0, 5)) self._log = tk.Text(log_frame, height=5, state=tk.DISABLED, wrap=tk.WORD, font=("Consolas", 9)) log_scroll = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self._log.yview) self._log.configure(yscrollcommand=log_scroll.set) self._log.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) log_scroll.pack(side=tk.RIGHT, fill=tk.Y) # --- 하단: 설정 변경 --- bot = ttk.LabelFrame(self, text="설정 변경", padding=5) bot.pack(fill=tk.X, padx=5, pady=5) fields = [ ("IP:", "ip_var", 15), ("포트:", "port_var", 6), ("이름:", "name_var", 15), ("서브넷:", "subnet_var", 15), ("게이트웨이:", "gw_var", 15), ] self._edit_vars = {} for i, (label, var_name, width) in enumerate(fields): ttk.Label(bot, text=label).grid(row=i // 3, column=(i % 3) * 2, padx=2, sticky=tk.E) var = tk.StringVar() self._edit_vars[var_name] = var ttk.Entry(bot, textvariable=var, width=width).grid( row=i // 3, column=(i % 3) * 2 + 1, padx=(0, 10), pady=2 ) btn_frame = ttk.Frame(bot) btn_frame.grid(row=2, column=0, columnspan=6, pady=5) ttk.Button(btn_frame, text="적용", command=self._on_apply).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="리셋", command=self._on_reset).pack(side=tk.LEFT, padx=5) def _log_message(self, msg: str): self._log.config(state=tk.NORMAL) self._log.insert(tk.END, msg + "\n") self._log.see(tk.END) self._log.config(state=tk.DISABLED) def _refresh_nics(self): nics = get_network_interfaces() self._nic_list = nics self._nic_combo["values"] = [f"{name} ({ip})" for ip, name in nics] if nics: self._nic_combo.current(0) def _get_bind_ip(self) -> str: idx = self._nic_combo.current() if idx >= 0 and idx < len(self._nic_list): return self._nic_list[idx][0] return "0.0.0.0" def _on_search(self): if self._searching: return self._searching = True self._btn_search.config(state=tk.DISABLED) self._search_status.config(text="검색중...") self._tree.delete(*self._tree.get_children()) self._controllers.clear() # 로그 초기화 self._log.config(state=tk.NORMAL) self._log.delete("1.0", tk.END) self._log.config(state=tk.DISABLED) bind_ip = self._get_bind_ip() self._log_message(f"검색 시작 (NIC: {bind_ip})") # 검색 로그를 GUI로 전달하는 큐 self._log_queue: list[str] = [] threading.Thread(target=self._search_thread, args=(bind_ip,), daemon=True).start() self._poll_logs() def _search_thread(self, bind_ip: str): def on_log(msg: str): self._log_queue.append(msg) results = search_controllers(bind_ip=bind_ip, timeout=3.0, on_log=on_log) self._search_results = results self._search_finished = True def _poll_logs(self): """백그라운드 스레드의 로그를 GUI에 표시.""" while self._log_queue: msg = self._log_queue.pop(0) self._log_message(msg) if hasattr(self, "_search_finished") and self._search_finished: self._search_finished = False self._search_done(self._search_results) return self.after(100, self._poll_logs) def _search_done(self, results: list[Controller]): self._searching = False self._btn_search.config(state=tk.NORMAL) self._controllers.clear() self._controllers.extend(results) for ctrl in results: self._tree.insert("", tk.END, values=( ctrl.mac, ctrl.ip, ctrl.port, ctrl.name, ctrl.subnet, ctrl.gateway )) self._search_status.config(text=f"{len(results)}개 발견") def _on_select(self, event): sel = self._tree.selection() if not sel: return values = self._tree.item(sel[0], "values") # mac, ip, port, name, subnet, gateway self._edit_vars["ip_var"].set(values[1]) self._edit_vars["port_var"].set(values[2]) self._edit_vars["name_var"].set(values[3]) self._edit_vars["subnet_var"].set(values[4]) self._edit_vars["gw_var"].set(values[5]) def _get_selected_controller(self) -> Controller | None: sel = self._tree.selection() if not sel: messagebox.showwarning("선택 필요", "컨트롤러를 선택하세요.") return None values = self._tree.item(sel[0], "values") mac = values[0] for c in self._controllers: if c.mac == mac: return c return None def _on_apply(self): ctrl = self._get_selected_controller() if not ctrl: return ctrl.ip = self._edit_vars["ip_var"].get().strip() try: ctrl.port = int(self._edit_vars["port_var"].get().strip()) except ValueError: messagebox.showerror("오류", "포트는 숫자여야 합니다.") return ctrl.name = self._edit_vars["name_var"].get().strip() ctrl.subnet = self._edit_vars["subnet_var"].get().strip() ctrl.gateway = self._edit_vars["gw_var"].get().strip() bind_ip = self._get_bind_ip() ok = send_sett(ctrl, bind_ip) if ok: messagebox.showinfo("성공", "설정이 전송되었습니다.\n적용을 위해 리셋하세요.") sel = self._tree.selection() if sel: self._tree.item(sel[0], values=( ctrl.mac, ctrl.ip, ctrl.port, ctrl.name, ctrl.subnet, ctrl.gateway )) else: messagebox.showerror("실패", "설정 전송에 실패했습니다.") def _on_reset(self): ctrl = self._get_selected_controller() if not ctrl: return if not messagebox.askyesno("확인", f"{ctrl.mac} 컨트롤러를 리셋하시겠습니까?"): return bind_ip = self._get_bind_ip() ok = send_reset(ctrl.mac, bind_ip) if ok: messagebox.showinfo("성공", "리셋 명령이 전송되었습니다.") else: messagebox.showerror("실패", "리셋 전송에 실패했습니다.")