"""UDP 검색, SETT 설정, RESET""" import socket import struct import time import threading from typing import Callable from config import ( UDP_PORT_ETHERNET, UDP_PORT_WIFI, UDP_BROADCAST_IP, SEARCH_CMD, SETT_PREFIX, RESET_PREFIX, UDP_RECV_BUFFER, UDP_RECV_TIMEOUT, PROTOCOL_ENCODING, ) from models.controller import Controller from network.packet_parser import parse_dibd_packet from utils.ip_format import pad_ip, sized_string def get_network_interfaces() -> list[tuple[str, str]]: """사용 가능한 네트워크 인터페이스 목록 반환. Returns: [(ip, name), ...] """ interfaces = [] try: hostname = socket.gethostname() addrs = socket.getaddrinfo(hostname, None, socket.AF_INET) seen = set() for addr in addrs: ip = addr[4][0] if ip not in seen and not ip.startswith("127."): seen.add(ip) interfaces.append((ip, ip)) except Exception: pass if not interfaces: interfaces.append(("0.0.0.0", "기본")) return interfaces def search_controllers( bind_ip: str = "0.0.0.0", timeout: float = UDP_RECV_TIMEOUT, on_found: Callable[[Controller], None] | None = None, on_log: Callable[[str], None] | None = None, ) -> list[Controller]: """UDP 브로드캐스트로 컨트롤러를 검색. Java 참조: - 소켓을 특정 NIC IP + 고정 포트(5109)에 바인딩 - 검색 패킷을 5108, 5107 순서로 100ms 간격 전송 - 별도 리스닝 스레드에서 응답 수신 (블로킹) """ def log(msg: str): if on_log: on_log(msg) results: list[Controller] = [] mac_set: set[str] = set() sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Java: 특정 NIC IP + 포트 5109에 바인딩 bind_port = 5109 try: sock.bind((bind_ip, bind_port)) log(f"소켓 바인딩: {bind_ip}:{bind_port}") except OSError: try: sock.bind((bind_ip, 0)) local_addr = sock.getsockname() log(f"포트 5109 사용 불가, 대체 바인딩: {local_addr[0]}:{local_addr[1]}") except OSError as e: log(f"소켓 바인딩 실패: {e}") sock.close() return results cmd = SEARCH_CMD.encode(PROTOCOL_ENCODING) log(f"검색 패킷: {SEARCH_CMD.strip()!r} ({len(cmd)}바이트)") # 이더넷 포트(5108)로 전송 try: sock.sendto(cmd, (UDP_BROADCAST_IP, UDP_PORT_ETHERNET)) log(f"전송 → {UDP_BROADCAST_IP}:{UDP_PORT_ETHERNET}") except Exception as e: log(f"전송 실패 (5108): {e}") time.sleep(0.1) # WiFi 포트(5107)로 전송 try: sock.sendto(cmd, (UDP_BROADCAST_IP, UDP_PORT_WIFI)) log(f"전송 → {UDP_BROADCAST_IP}:{UDP_PORT_WIFI}") except Exception as e: log(f"전송 실패 (5107): {e}") # 응답 수신: 짧은 per-recv 타임아웃(0.5초) + 긴 전체 대기시간 sock.settimeout(0.5) deadline = time.time() + timeout recv_count = 0 while time.time() < deadline: try: data, addr = sock.recvfrom(UDP_RECV_BUFFER) recv_count += 1 log(f"수신 #{recv_count}: {addr[0]}:{addr[1]}, {len(data)}바이트") except socket.timeout: continue # 타임아웃이면 deadline까지 계속 시도 except OSError as e: log(f"수신 오류: {e}") break ctrl = parse_dibd_packet(data) if ctrl and ctrl.mac: if ctrl.mac not in mac_set: mac_set.add(ctrl.mac) results.append(ctrl) log(f"발견: {ctrl.mac} ({ctrl.ip}:{ctrl.port} {ctrl.name})") if on_found: on_found(ctrl) elif data: header = data[:4].decode(PROTOCOL_ENCODING, errors="replace") log(f"무시됨: 헤더={header!r}, 길이={len(data)}") sock.close() log(f"검색 완료: {len(results)}개 발견 (총 {recv_count}개 패킷 수신)") return results def send_sett(ctrl: Controller, bind_ip: str = "0.0.0.0") -> bool: """SETT 명령으로 컨트롤러 설정 변경.""" data = SETT_PREFIX data += ctrl.mac + " " data += pad_ip(ctrl.ip) + " " data += pad_ip(ctrl.subnet) + " " data += pad_ip(ctrl.gateway) + " " data += f"{ctrl.port:05d}" + " " data += f"3{ctrl.dhcp_mode}" + " " data += pad_ip(ctrl.server_ip) + " " data += f"{ctrl.server_port:05d}" + " " data += f"3{ctrl.server_mode}" + " " data += sized_string(ctrl.name, 20) + " " data += f"{ctrl.keep_alive:03d}" + " " data += sized_string(ctrl.dns_name, 30) + " " data += sized_string(ctrl.ap_ssid_name, 20) + " " data += sized_string(ctrl.ap_ssid_pw, 20) + " " data += f"3{ctrl.ap_mode}" data += "\r\n" return _send_udp(data, bind_ip) def send_reset(mac: str, bind_ip: str = "0.0.0.0") -> bool: """RESET 명령 전송.""" data = RESET_PREFIX + mac + "\r\n" return _send_udp(data, bind_ip) def _send_udp(data: str, bind_ip: str) -> bool: """UDP 패킷을 브로드캐스트로 전송.""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) try: sock.bind((bind_ip, 0)) except OSError: sock.bind(("0.0.0.0", 0)) buf = data.encode(PROTOCOL_ENCODING) sock.sendto(buf, (UDP_BROADCAST_IP, UDP_PORT_ETHERNET)) time.sleep(0.1) sock.sendto(buf, (UDP_BROADCAST_IP, UDP_PORT_WIFI)) sock.close() return True except Exception: return False