""" AIS-catcher process manager. Detects RTL-SDR hardware and manages AIS-catcher.exe. """ from __future__ import annotations import json import os import subprocess import sys from pathlib import Path _HERE = Path(__file__).parent TOOLS_DIR = _HERE.parent.parent / "tools" / "ais-catcher" EXE_PATH = TOOLS_DIR / "AIS-catcher.exe" _process: subprocess.Popen | None = None def exe_exists() -> bool: return EXE_PATH.exists() def detect_sdr() -> list[dict]: """Return list of RTL-SDR devices detected via PowerShell PnP (VID_0BDA).""" found = [] try: ps = ( "Get-PnpDevice -Status OK | " "Where-Object { ($_.HardwareID | Out-String) -match 'VID_0BDA' } | " "Select-Object " "@{N='desc';E={$_.FriendlyName}}," "@{N='hw';E={($_.HardwareID | Select-Object -First 1)}} | " "ConvertTo-Json -Compress" ) out = subprocess.check_output( ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps], text=True, timeout=6, stderr=subprocess.DEVNULL, ).strip() if not out: return found data = json.loads(out) if isinstance(data, dict): data = [data] for d in (data or []): found.append({ "description": d.get("desc") or "RTL-SDR dongle", "hardware_id": d.get("hw") or "", "type": "rtl_sdr", }) except Exception as e: print(f"[ais-catcher] SDR detect error: {e}") return found def _kill_existing(): """Kill any stray AIS-catcher processes already running.""" try: subprocess.run( ["taskkill", "/F", "/IM", "AIS-catcher.exe"], capture_output=True, timeout=5 ) except Exception: pass def is_running() -> bool: global _process if _process is not None and _process.poll() is None: return True # Also check system-wide (could have been launched externally) try: out = subprocess.check_output( ["tasklist", "/FI", "IMAGENAME eq AIS-catcher.exe", "/NH"], text=True, timeout=3, stderr=subprocess.DEVNULL, ) return "AIS-catcher.exe" in out except Exception: return False def launch(udp_port: int = 10110, web_port: int = 8100) -> dict: """ Launch AIS-catcher.exe if not already running. Returns {"ok": bool, "already_running": bool, "pid": int|None, "error": str|None} """ global _process if not exe_exists(): return {"ok": False, "error": f"AIS-catcher.exe not found at {EXE_PATH}"} if is_running(): return {"ok": True, "already_running": True, "pid": _process.pid if _process else None} sdrs = detect_sdr() if not sdrs: return {"ok": False, "error": "No RTL-SDR device detected"} try: _kill_existing() cmd = [ str(EXE_PATH), "-v", "10", "-X", "-u", f"127.0.0.1", str(udp_port), "-N", str(web_port), "PLUGIN_DIR", str(TOOLS_DIR / "plugins"), ] _process = subprocess.Popen( cmd, cwd=str(TOOLS_DIR), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0, ) return {"ok": True, "already_running": False, "pid": _process.pid} except Exception as e: return {"ok": False, "error": str(e)} def stop() -> dict: """Stop the managed AIS-catcher process.""" global _process if _process and _process.poll() is None: _process.terminate() try: _process.wait(timeout=4) except subprocess.TimeoutExpired: _process.kill() _process = None _kill_existing() return {"ok": True} def status() -> dict: sdrs = detect_sdr() running = is_running() pid = _process.pid if (_process and _process.poll() is None) else None return { "exe_found": exe_exists(), "sdr_devices": sdrs, "running": running, "pid": pid, }