Python-based time management application with UDP discovery, TCP protocol communication, time sync, and drift monitoring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
"""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
|