diff --git a/clients/nfc_autoconnect.py b/clients/nfc_autoconnect.py index 657b773..9906e67 100644 --- a/clients/nfc_autoconnect.py +++ b/clients/nfc_autoconnect.py @@ -1 +1,290 @@ -$(cat /home/claude/nfc_autoconnect.py) \ No newline at end of file +#!/usr/bin/env python3 +""" +Smart Auto-Connect NFC Network Client +Moonlight Drive-In Theater System + +Enhanced client with smart DNS resolution and automatic connection. +Compatible with NFCNetworkClient v1.1.0 (global input capture). +""" + +import tkinter as tk +from tkinter import ttk +import sys +import os +import socket +import subprocess +import time +from typing import Optional, List, Tuple + +# Import the base client +try: + from nfc_network_client import NFCNetworkClient, CLIENT_VERSION as BASE_VERSION +except ImportError: + print("ERROR: Cannot find nfc_network_client.py") + print("Make sure nfc_network_client.py is in the same directory") + sys.exit(1) + +SMART_CLIENT_VERSION = "2.0.0" + +class DNSResolver: + """Smart DNS resolution with multiple fallback methods""" + + def __init__(self, device_name: str, fallback_ip: str, port: int = 8547): + self.device_name = device_name + self.fallback_ip = fallback_ip + self.port = port + self.last_known_ip: Optional[str] = None + + def resolve(self) -> Tuple[Optional[str], str]: + """ + Try multiple DNS resolution methods + Returns: (ip_address, method_name) + """ + print(f"\nAttempting to locate device: {self.device_name}") + + # Method 1: Standard DNS + result = self._try_standard_dns() + if result: + return result, "STANDARD_DNS" + + # Method 2: mDNS (.local) + result = self._try_mdns() + if result: + return result, "MDNS" + + # Method 3: Tailscale DNS + result = self._try_tailscale_dns() + if result: + return result, "TAILSCALE_DNS" + + # Method 4: Fallback to known IP + if self.fallback_ip: + print(f"\nMethod: Fallback to known IP") + if self._check_host_reachable(self.fallback_ip): + print(f"SUCCESS: Fallback IP is reachable: {self.fallback_ip}") + return self.fallback_ip, "FALLBACK_IP" + + print("\nERROR: Could not locate device using any method") + return None, "FAILED" + + def _try_standard_dns(self) -> Optional[str]: + """Try standard DNS resolution""" + print(f"\nMethod: Standard DNS") + try: + ip = socket.gethostbyname(self.device_name) + print(f" [DNS] ✓ Resolved: {self.device_name} -> {ip}") + + if self._check_host_reachable(ip): + print(f"SUCCESS: Found {self.device_name} at {ip} via Standard DNS") + return ip + except socket.gaierror: + print(f" [DNS] ✗ Failed to resolve: {self.device_name}") + + return None + + def _try_mdns(self) -> Optional[str]: + """Try mDNS resolution (.local)""" + mdns_name = f"{self.device_name}.local" + print(f"\nMethod: mDNS (Bonjour)") + + try: + ip = socket.gethostbyname(mdns_name) + print(f" [mDNS] ✓ Resolved: {mdns_name} -> {ip}") + + if self._check_host_reachable(ip): + print(f"SUCCESS: Found {self.device_name} at {ip} via mDNS") + return ip + except socket.gaierror: + print(f" [mDNS] ✗ Failed to resolve: {mdns_name}") + + return None + + def _try_tailscale_dns(self) -> Optional[str]: + """Try Tailscale DNS resolution""" + ts_name = f"{self.device_name}.tail-scale.ts.net" + print(f"\nMethod: Tailscale VPN DNS") + + try: + ip = socket.gethostbyname(ts_name) + print(f" [Tailscale] ✓ Resolved: {ts_name} -> {ip}") + + if self._check_host_reachable(ip): + print(f"SUCCESS: Found {self.device_name} at {ip} via Tailscale") + return ip + except socket.gaierror: + print(f" [Tailscale] ✗ Failed to resolve: {ts_name}") + + return None + + def _check_host_reachable(self, ip: str) -> bool: + """Check if host is reachable via ping""" + try: + # Try quick ping (1 packet, 1 second timeout) + if sys.platform == "win32": + result = subprocess.run( + ["ping", "-n", "1", "-w", "1000", ip], + capture_output=True, + timeout=2 + ) + else: + result = subprocess.run( + ["ping", "-c", "1", "-W", "1", ip], + capture_output=True, + timeout=2 + ) + + if result.returncode == 0: + print(f" [Ping] ✓ Host reachable: {ip}") + return True + else: + print(f" [Ping] ✗ Host unreachable: {ip}") + return False + + except (subprocess.TimeoutExpired, Exception) as e: + print(f" [Ping] ✗ Ping failed: {e}") + return False + + +class SmartAutoConnectClient(NFCNetworkClient): + """Enhanced NFC client with smart auto-connect""" + + def __init__(self, root: tk.Tk, device_name: str, fallback_ip: str, port: int = 8547): + # Store smart connect settings + self.device_name = device_name + self.fallback_ip = fallback_ip + self.target_port = port + self.resolver = DNSResolver(device_name, fallback_ip, port) + + # Initialize parent with root + super().__init__(root) + + # Update title to show smart connect + self.root.title(f"NFC Network Client - Smart Connect v{SMART_CLIENT_VERSION}") + + # Customize UI for smart connect + self._customize_ui() + + def _customize_ui(self): + """Add smart connect specific UI elements""" + # Update server entry to show device name + self.server_entry.delete(0, tk.END) + self.server_entry.insert(0, f"{self.device_name} (auto-resolved)") + self.server_entry.config(state="disabled") + + def connect_to_server(self): + """Override connect to use smart DNS resolution""" + self.log(f"Smart connecting to: {self.device_name}") + self.update_status("Resolving...", "orange") + self.connect_btn.config(state="disabled") + + # Resolve in background thread + def resolve_and_connect(): + ip, method = self.resolver.resolve() + + if ip: + print(f"\n{'='*70}") + print(f"WILL CONNECT TO: {ip}:{self.target_port}") + print(f"Resolution method: {method}") + print(f"{'='*70}\n") + + # Try to connect + url = f"http://{ip}:{self.target_port}" + + # Test the connection + import requests + try: + response = requests.get( + f"{url}/api/status", + timeout=2 + ) + + if response.status_code == 200: + data = response.json() + if data.get("server_type") == "moonlight_drivein": + # Success! + self.root.after(0, lambda: self.connection_success(url)) + return + except: + pass + + # Failed + self.root.after(0, self.connection_failed) + + import threading + threading.Thread(target=resolve_and_connect, daemon=True).start() + + def connection_failed(self): + """Handle connection failure with smart reconnect""" + super().connection_failed() + self.log("Will retry in 5 seconds...", "INFO") + + # Auto-retry after 5 seconds + self.root.after(5000, self.connect_to_server) + + +def print_banner(): + """Print startup banner""" + print("\n" + "="*70) + print(" " * 20 + "NFC Network Client - Smart Auto-Connect") + print("="*70) + print(f"\nTarget device: {CONFIG['device_name']}") + print(f"Fallback IP: {CONFIG['fallback_ip']}") + print(f"Port: {CONFIG['port']}") + print("\n" + "="*70) + + +def load_config() -> dict: + """Load configuration from environment or defaults""" + return { + 'device_name': os.environ.get('DRIVEIN_DEVICE', 'drive-in'), + 'fallback_ip': os.environ.get('DRIVEIN_IP', '100.94.163.117'), + 'port': int(os.environ.get('DRIVEIN_PORT', '8547')) + } + + +# Global config +CONFIG = load_config() + + +def main(): + """Main entry point""" + print(f"\nNFC Network Client - Smart Auto-Connect v{SMART_CLIENT_VERSION}") + print(f"Based on NFC Network Client v{BASE_VERSION}") + print("="*70) + + print_banner() + + # Create Tkinter root + root = tk.Tk() + + try: + # Create and run smart client + app = SmartAutoConnectClient( + root, + device_name=CONFIG['device_name'], + fallback_ip=CONFIG['fallback_ip'], + port=CONFIG['port'] + ) + + print("Client started successfully!") + print("Window should appear shortly...") + print("="*70 + "\n") + + root.mainloop() + + except Exception as e: + print(f"\nERROR: Client failed to start") + print(f"Error: {e}") + import traceback + traceback.print_exc() + return 1 + + finally: + if hasattr(app, 'keyboard_listener') and app.keyboard_listener: + app.keyboard_listener.stop() + + return 0 + + +if __name__ == "__main__": + sys.exit(main())