diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8425587..3e6a7a4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -44,7 +44,8 @@ "Bash(sort:*)", "Bash(journalctl:*)", "Bash(pkill:*)", - "Bash(pip3 list:*)" + "Bash(pip3 list:*)", + "Bash(chmod:*)" ] } } diff --git a/RFCP-Iteration-3.5.2-Native-GPU-Polish.md b/RFCP-Iteration-3.5.2-Native-GPU-Polish.md new file mode 100644 index 0000000..b6e28e9 --- /dev/null +++ b/RFCP-Iteration-3.5.2-Native-GPU-Polish.md @@ -0,0 +1,504 @@ +# RFCP — Iteration 3.5.2: Native Backend + GPU Fix + UI Polish + +## Overview + +Fix critical architecture issues: GPU indicator dropdown broken, GPU acceleration not working +(CuPy in wrong Python environment), and prepare path to remove WSL2 dependency for end users. +Plus UI polish items carried over from 3.5.1. + +**Priority:** GPU fixes first, then UI polish, then native Windows exploration. + +--- + +## CRITICAL CONTEXT + +### Current Architecture Problem + +``` +RFCP.exe (Electron, Windows) + └── launches backend via WSL2: + python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8090 + └── /usr/bin/python3 (WSL2 system Python 3.12) + └── NO venv, NO CuPy installed + +User installed CuPy in Windows Python → backend doesn't see it. +User installed CuPy in WSL system Python → needs --break-system-packages +``` + +### GPU Hardware (Confirmed Working) + +``` +nvidia-smi output (from WSL2): + NVIDIA GeForce RTX 4060 Laptop GPU + Driver: 581.42 (Windows) / 580.95.02 (WSL2) + CUDA: 13.0 + VRAM: 8188 MiB + GPU passthrough: WORKING ✅ +``` + +### Files to Reference + +``` +backend/app/services/gpu_backend.py — GPUManager class +backend/app/api/routes/gpu.py — GPU API endpoints +frontend/src/components/ui/GPUIndicator.tsx — GPU badge/dropdown +desktop/ — Electron app source +installer/ — Build scripts +``` + +--- + +## Task 1: Fix GPU Indicator Dropdown Z-Index (Priority 1 — 10 min) + +### Problem +GPU dropdown WORKS (opens on click, shows diagnostics, install hints) but renders +BEHIND the right sidebar panel. The sidebar (Sites, Coverage Settings) has higher +z-index than the GPU dropdown, so the dropdown is invisible/hidden underneath. + +See screenshots: dropdown is partially visible only when sidebar is made very narrow. +It shows: "COMPUTE DEVICES", "CPU (NumPy)", install hints, "Run Diagnostics", +and even diagnostics JSON — all working but hidden behind sidebar. + +### Root Cause +GPUIndicator dropdown z-index is lower than the right sidebar panel z-index. + +### Solution + +In `GPUIndicator.tsx` — find the dropdown container div and set z-index +higher than the sidebar: + +```tsx +{isOpen && ( +
+ ... +
+)} +``` + +**Key requirements:** +1. `z-index: 9999` (or at minimum higher than sidebar) +2. Position: dropdown should open to the LEFT (toward center of screen) + to avoid being cut off by right edge +3. `right-0` on the absolute positioning (anchored to right edge of badge) + +**Alternative approach** — use Tailwind z-index: +```tsx +className="absolute top-full right-0 mt-1 z-[9999] ..." +``` + +**Also check:** The parent container of GPUIndicator might need `position: relative` +for absolute positioning to work correctly against the right sidebar. + +### Testing +- [ ] Click "CPU" badge → dropdown appears ABOVE the sidebar +- [ ] Full dropdown visible: devices, install hints, diagnostics +- [ ] Dropdown doesn't get cut off on right side +- [ ] Click outside → dropdown closes +- [ ] Dropdown works at any window width + +--- + +## Task 2: Install CuPy in WSL Backend (Priority 1 — 10 min) + +### Problem +CuPy installed in Windows Python, but backend runs in WSL2 system Python. + +### Solution + +Add a startup check in the backend that detects missing GPU packages +and provides clear instructions. Also, the Electron app should try to +install dependencies on first launch. + +**Step 1: Backend startup GPU check** + +In `backend/app/main.py`, add on startup: + +```python +@app.on_event("startup") +async def check_gpu_availability(): + """Log GPU status on startup for debugging.""" + import logging + logger = logging.getLogger("rfcp.gpu") + + # Check CuPy + try: + import cupy as cp + device_count = cp.cuda.runtime.getDeviceCount() + if device_count > 0: + name = cp.cuda.Device(0).name + mem = cp.cuda.Device(0).mem_info[1] // 1024 // 1024 + logger.info(f"✅ GPU detected: {name} ({mem} MB VRAM)") + logger.info(f" CuPy {cp.__version__}, CUDA devices: {device_count}") + else: + logger.warning("⚠️ CuPy installed but no CUDA devices found") + except ImportError: + logger.warning("⚠️ CuPy not installed — GPU acceleration disabled") + logger.warning(" Install: pip install cupy-cuda12x --break-system-packages") + except Exception as e: + logger.warning(f"⚠️ CuPy error: {e}") + + # Check PyOpenCL + try: + import pyopencl as cl + platforms = cl.get_platforms() + for p in platforms: + for d in p.get_devices(): + logger.info(f"✅ OpenCL device: {d.name.strip()}") + except ImportError: + logger.info("ℹ️ PyOpenCL not installed (optional)") + except Exception: + pass +``` + +**Step 2: GPU diagnostics endpoint enhancement** + +Enhance `/api/gpu/diagnostics` to return install commands: + +```python +@router.get("/diagnostics") +async def gpu_diagnostics(): + import platform, sys + + diagnostics = { + "python": sys.version, + "platform": platform.platform(), + "executable": sys.executable, + "is_wsl": "microsoft" in platform.release().lower(), + "cuda_available": False, + "opencl_available": False, + "install_hint": "", + "devices": [] + } + + # Check nvidia-smi + try: + import subprocess + result = subprocess.run( + ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + diagnostics["nvidia_smi"] = result.stdout.strip() + except: + diagnostics["nvidia_smi"] = "not found" + + # Check CuPy + try: + import cupy + diagnostics["cupy_version"] = cupy.__version__ + diagnostics["cuda_available"] = True + count = cupy.cuda.runtime.getDeviceCount() + for i in range(count): + d = cupy.cuda.Device(i) + diagnostics["devices"].append({ + "id": i, + "name": d.name, + "memory_mb": d.mem_info[1] // 1024 // 1024, + "backend": "CUDA" + }) + except ImportError: + if diagnostics.get("is_wsl"): + diagnostics["install_hint"] = "pip3 install cupy-cuda12x --break-system-packages" + else: + diagnostics["install_hint"] = "pip install cupy-cuda12x" + + return diagnostics +``` + +**Step 3: Frontend shows diagnostics clearly** + +In GPUIndicator dropdown, show: +``` +⚠ No GPU detected + +Your system: WSL2 + NVIDIA RTX 4060 + +To enable GPU acceleration: +┌─────────────────────────────────────────────┐ +│ pip3 install cupy-cuda12x │ +│ --break-system-packages │ +└─────────────────────────────────────────────┘ +Then restart RFCP. + +[Copy Command] [Run Diagnostics] +``` + +### Testing +- [ ] Backend startup logs GPU status +- [ ] /api/gpu/diagnostics returns WSL detection + install hint +- [ ] Frontend shows clear install instructions +- [ ] After installing CuPy in WSL + restart → GPU appears in list + +--- + +## Task 3: Terrain Profile Click Fix (Priority 2 — 5 min) + +### Problem +Clicking "Terrain Profile" button in ruler measurement also adds a point on the map. + +### Solution +In the Terrain Profile button handler: + +```tsx +const handleTerrainProfile = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + // ... open terrain profile +}; +``` + +Also check if the button is rendered inside a map click handler area — +may need `L.DomEvent.disableClickPropagation(container)` on the parent. + +### Testing +- [ ] Click "Terrain Profile" → opens profile, NO new ruler point added +- [ ] Map click still works normally when not clicking the button + +--- + +## Task 4: Coverage Boundary — Real Contour Shape (Priority 2 — 45 min) + +### Problem +Current boundary is a rough circle/ellipse. Should follow actual coverage contour. + +### Approaches + +**Option A: Shapely Alpha Shape (recommended)** + +```python +# backend/app/services/boundary_service.py + +from shapely.geometry import MultiPoint +from shapely.ops import unary_union +import numpy as np + +def calculate_coverage_boundary(points: list, threshold_dbm: float = -100) -> list: + """Calculate concave hull of coverage area above threshold.""" + + # Filter points above threshold + valid = [(p['lon'], p['lat']) for p in points if p['rsrp'] >= threshold_dbm] + + if len(valid) < 3: + return [] + + mp = MultiPoint(valid) + + # Use convex hull first, then try concave + try: + # Shapely 2.0+ has concave_hull + from shapely import concave_hull + hull = concave_hull(mp, ratio=0.3) + except ImportError: + # Fallback to convex hull + hull = mp.convex_hull + + # Simplify to reduce points (0.001 deg ≈ 100m) + simplified = hull.simplify(0.001, preserve_topology=True) + + # Extract coordinates + if simplified.geom_type == 'Polygon': + coords = list(simplified.exterior.coords) + return [{'lat': c[1], 'lon': c[0]} for c in coords] + + return [] +``` + +**Option B: Grid-based contour (simpler)** + +```python +def grid_contour_boundary(points: list, threshold_dbm: float, resolution: float): + """Find boundary by detecting edge cells in grid.""" + + # Create binary grid: 1 = above threshold, 0 = below + # Find cells where 1 is adjacent to 0 = boundary + # Convert cell coords back to lat/lon + # Return ordered boundary points +``` + +### API Endpoint + +```python +# Add to coverage calculation response +@router.post("/coverage/calculate") +async def calculate_coverage(...): + result = coverage_service.calculate(...) + + # Calculate boundary + if result.points: + boundary = calculate_coverage_boundary( + result.points, + threshold_dbm=settings.min_signal + ) + result.boundary = boundary + + return result +``` + +### Frontend + +```tsx +// CoverageBoundary.tsx — use returned boundary coords +// Instead of calculating alpha shape on frontend + +const CoverageBoundary = ({ points, boundary }) => { + // If server returned boundary, use it + if (boundary && boundary.length > 0) { + return [p.lat, p.lon])} />; + } + + // Fallback to current convex hull implementation + return ; +}; +``` + +### Dependencies +Need `shapely` installed: +``` +pip install shapely # or pip3 install shapely --break-system-packages +``` + +Check if already in requirements.txt. + +### Testing +- [ ] 5km calculation → boundary follows actual coverage shape +- [ ] 10km calculation → boundary is irregular (terrain-dependent) +- [ ] Toggle boundary on/off works +- [ ] Boundary doesn't crash with < 3 points + +--- + +## Task 5: Results Popup Enhancement (Priority 3 — 15 min) + +### Problem +Calculation complete toast/popup doesn't show which models were used. + +### Solution +Enhance the toast message after calculation: + +```tsx +// Current: +toast.success(`Calculated ${points} points in ${time}s`); + +// Enhanced: +const modelCount = result.modelsUsed?.length ?? 0; +const freq = sites[0]?.frequency ?? 0; +const presetName = settings.preset ?? 'custom'; + +toast.success( + `${points} pts • ${time}s • ${presetName} • ${freq} MHz • ${modelCount} models`, + { duration: 5000 } +); +``` + +### Testing +- [ ] After calculation, toast shows: points, time, preset, frequency, model count + +--- + +## Task 6: Native Windows Backend (Priority 3 — Research/Plan) + +### Problem +Current setup REQUIRES WSL2. Users without WSL2 can't use RFCP at all. + +### Current Flow +``` +RFCP.exe (Electron) + → detects WSL2 + → launches: wsl python3 -m uvicorn ... + → backend runs in WSL2 Linux +``` + +### Target Flow +``` +RFCP.exe (Electron) + → Option A: embedded Python (Windows native) + → Option B: detect system Python (Windows) + → Option C: keep WSL2 but with fallback +``` + +### Research Tasks (don't implement yet, just investigate) + +1. **Check how Electron currently launches backend:** +```bash +# Look at desktop/ directory +cat desktop/src/main.ts # or main.js +# Find where it spawns python/uvicorn +``` + +2. **Check if Windows Python works for backend:** +```powershell +# In Windows PowerShell: +cd D:\root\rfcp\backend +python -m uvicorn app.main:app --host 0.0.0.0 --port 8090 +# Does it start? What errors? +``` + +3. **Evaluate embedded Python options:** + - python-embedded (official, ~30 MB) + - PyInstaller (bundle backend as .exe) + - cx_Freeze + - Nuitka (compile Python to C) + +4. **Document findings** — create a brief report: +``` +RFCP-Native-Backend-Research.md + - Current architecture (WSL2 dependency) + - Windows Python compatibility test results + - Recommended approach + - Migration steps + - Timeline estimate +``` + +### Goal +User downloads RFCP.exe → installs → clicks icon → everything works. +No WSL2. No manual pip install. GPU auto-detected. + +--- + +## Implementation Order + +### Priority 1 (30 min total) +1. **Task 1:** Fix GPU dropdown — make it clickable again +2. **Task 2:** GPU diagnostics + install instructions in UI +3. **Task 3:** Terrain Profile click propagation fix + +### Priority 2 (1 hour) +4. **Task 4:** Coverage boundary real contour (shapely) +5. **Task 5:** Results popup enhancement + +### Priority 3 (Research only) +6. **Task 6:** Investigate native Windows backend — report only, no implementation + +--- + +## Build & Deploy + +```bash +# After implementation: +cd /mnt/d/root/rfcp/frontend +npx tsc --noEmit # TypeScript check +npm run build # Production build + +# Rebuild Electron: +cd /mnt/d/root/rfcp/installer +bash build-win.sh + +# Test: +# Install new .exe and verify GPU indicator works +``` + +--- + +## Success Criteria + +- [ ] GPU dropdown opens when clicking badge +- [ ] Dropdown shows device list or install instructions +- [ ] After `pip3 install cupy-cuda12x --break-system-packages` in WSL + restart → GPU visible +- [ ] Terrain Profile click doesn't add ruler points +- [ ] Coverage boundary follows actual signal contour +- [ ] Results toast shows model count and frequency +- [ ] Native Windows backend research document created diff --git a/RFCP.bat b/RFCP.bat new file mode 100644 index 0000000..be7145a --- /dev/null +++ b/RFCP.bat @@ -0,0 +1,23 @@ +@echo off +title RFCP - RF Coverage Planner +cd /d "%~dp0" + +REM Check if backend exists +if not exist "backend\app\main.py" ( + echo ERROR: RFCP backend not found. + echo Run install.bat first or check your installation. + pause + exit /b 1 +) + +echo ============================================ +echo RFCP - RF Coverage Planner +echo ============================================ +echo. +echo Starting backend server... +echo Open http://localhost:8090 in your browser +echo Press Ctrl+C to stop +echo. + +cd backend +python -m uvicorn app.main:app --host 0.0.0.0 --port 8090 diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..af1472a --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,8 @@ +# Development and testing dependencies +# Install with: pip install -r requirements-dev.txt + +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +httpx>=0.27.0 +ruff>=0.1.0 +mypy>=1.7.0 diff --git a/backend/requirements-gpu-nvidia.txt b/backend/requirements-gpu-nvidia.txt new file mode 100644 index 0000000..049cb89 --- /dev/null +++ b/backend/requirements-gpu-nvidia.txt @@ -0,0 +1,10 @@ +# NVIDIA GPU acceleration via CuPy +# Install with: pip install -r requirements-gpu-nvidia.txt +# +# Choose ONE based on your CUDA version: +# - cupy-cuda12x for CUDA 12.x (RTX 30xx, 40xx, newer) +# - cupy-cuda11x for CUDA 11.x (older cards) +# +# CuPy bundles CUDA runtime (~700 MB) - no separate CUDA install needed + +cupy-cuda12x>=13.0.0 diff --git a/backend/requirements-gpu-opencl.txt b/backend/requirements-gpu-opencl.txt new file mode 100644 index 0000000..7b0179a --- /dev/null +++ b/backend/requirements-gpu-opencl.txt @@ -0,0 +1,14 @@ +# Intel/AMD GPU acceleration via PyOpenCL +# Install with: pip install -r requirements-gpu-opencl.txt +# +# Works with: +# - Intel UHD/Iris Graphics (integrated) +# - AMD Radeon (discrete) +# - NVIDIA GPUs (alternative to CUDA) +# +# Requires OpenCL runtime: +# - Intel: Intel GPU Computing Runtime +# - AMD: AMD Adrenalin driver (includes OpenCL) +# - NVIDIA: NVIDIA driver (includes OpenCL) + +pyopencl>=2023.1 diff --git a/frontend/src/components/panels/BatchFrequencyChange.tsx b/frontend/src/components/panels/BatchFrequencyChange.tsx index 14e2529..022d78b 100644 --- a/frontend/src/components/panels/BatchFrequencyChange.tsx +++ b/frontend/src/components/panels/BatchFrequencyChange.tsx @@ -4,7 +4,7 @@ */ import { useSitesStore } from '@/store/sites.ts'; -import { COMMON_FREQUENCIES, FREQUENCY_GROUPS } from '@/constants/frequencies.ts'; +import { COMMON_FREQUENCIES } from '@/constants/frequencies.ts'; const QUICK_BANDS = [ { freq: 70, label: '70', color: 'text-indigo-400' }, diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..5d77106 --- /dev/null +++ b/install.bat @@ -0,0 +1,41 @@ +@echo off +title RFCP - First Time Setup +echo ============================================ +echo RFCP - RF Coverage Planner - Setup +echo ============================================ +echo. + +REM Check if Python exists +python --version >nul 2>&1 +if errorlevel 1 ( + echo ERROR: Python not found! + echo. + echo Please install Python 3.10+ from: + echo https://www.python.org/downloads/ + echo. + echo Make sure to check "Add Python to PATH" during installation. + echo. + pause + exit /b 1 +) + +echo Python found: +python --version +echo. + +REM Change to script directory +cd /d "%~dp0" + +REM Run installer +echo Running RFCP installer... +echo. +python install_rfcp.py + +echo. +echo ============================================ +echo Setup complete! +echo. +echo To start RFCP, run: RFCP.bat +echo Then open: http://localhost:8090 +echo ============================================ +pause diff --git a/install_rfcp.py b/install_rfcp.py new file mode 100644 index 0000000..77d68da --- /dev/null +++ b/install_rfcp.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +""" +RFCP Installer — Detects hardware, installs dependencies, sets up GPU acceleration. + +Usage: + python install_rfcp.py + +The installer handles: + - Python dependency installation + - GPU detection (NVIDIA/Intel/AMD) + - GPU acceleration setup (CuPy for CUDA, PyOpenCL for Intel/AMD) + - Frontend build (if Node.js available) + - Verification of installation +""" + +import subprocess +import sys +import platform +import os +import shutil + + +def print_header(text: str): + """Print section header.""" + print(f"\n{'=' * 60}") + print(f" {text}") + print('=' * 60) + + +def print_step(text: str): + """Print step indicator.""" + print(f"\n>>> {text}") + + +def check_python() -> bool: + """Verify Python 3.10+ is available.""" + version = sys.version_info + if version.major < 3 or version.minor < 10: + print(f"[X] Python 3.10+ required, found {version.major}.{version.minor}") + return False + print(f"[OK] Python {version.major}.{version.minor}.{version.micro}") + return True + + +def check_node() -> bool: + """Verify Node.js 18+ is available.""" + try: + result = subprocess.run( + ["node", "--version"], + capture_output=True, + text=True, + timeout=10 + ) + version = result.stdout.strip().lstrip('v') + major = int(version.split('.')[0]) + if major < 18: + print(f"[!] Node.js 18+ recommended, found {version}") + return False + print(f"[OK] Node.js {version}") + return True + except FileNotFoundError: + print("[!] Node.js not found (frontend build will be skipped)") + return False + except Exception as e: + print(f"[!] Node.js check failed: {e}") + return False + + +def detect_gpu() -> dict: + """Detect available GPU hardware.""" + gpus = { + "nvidia": False, + "nvidia_name": "", + "nvidia_memory_mb": 0, + "intel": False, + "intel_name": "", + "amd": False, + "amd_name": "" + } + + # Check NVIDIA via nvidia-smi + try: + result = subprocess.run( + ["nvidia-smi", "--query-gpu=name,driver_version,memory.total", + "--format=csv,noheader"], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0 and result.stdout.strip(): + info = result.stdout.strip() + parts = info.split(",") + gpus["nvidia"] = True + gpus["nvidia_name"] = parts[0].strip() + if len(parts) >= 3: + mem_str = parts[2].strip().replace(" MiB", "").replace(" MB", "") + try: + gpus["nvidia_memory_mb"] = int(mem_str) + except ValueError: + pass + print(f"[OK] NVIDIA GPU: {gpus['nvidia_name']}") + except FileNotFoundError: + pass + except subprocess.TimeoutExpired: + print("[!] nvidia-smi timed out") + except Exception as e: + print(f"[!] NVIDIA detection error: {e}") + + # Check Intel/AMD via WMI (Windows) or lspci (Linux) + if platform.system() == "Windows": + try: + result = subprocess.run( + ["wmic", "path", "win32_videocontroller", "get", + "name", "/format:csv"], + capture_output=True, + text=True, + timeout=10 + ) + for line in result.stdout.strip().split('\n'): + line_lower = line.lower() + if 'intel' in line_lower and ('uhd' in line_lower or 'iris' in line_lower or 'hd graphics' in line_lower): + gpus["intel"] = True + # Extract name from CSV + parts = line.split(',') + for part in parts: + if 'Intel' in part: + gpus["intel_name"] = part.strip() + break + if gpus["intel_name"]: + print(f"[OK] Intel GPU: {gpus['intel_name']}") + elif 'amd' in line_lower or 'radeon' in line_lower: + gpus["amd"] = True + parts = line.split(',') + for part in parts: + if 'AMD' in part or 'Radeon' in part: + gpus["amd_name"] = part.strip() + break + if gpus["amd_name"]: + print(f"[OK] AMD GPU: {gpus['amd_name']}") + except Exception: + pass + else: + # Linux: use lspci + try: + result = subprocess.run( + ["lspci"], + capture_output=True, + text=True, + timeout=10 + ) + for line in result.stdout.split('\n'): + if 'VGA' in line or 'Display' in line or '3D' in line: + if 'Intel' in line: + gpus["intel"] = True + gpus["intel_name"] = line.split(':')[-1].strip() if ':' in line else "Intel GPU" + print(f"[OK] Intel GPU: {gpus['intel_name']}") + elif 'AMD' in line or 'Radeon' in line: + gpus["amd"] = True + gpus["amd_name"] = line.split(':')[-1].strip() if ':' in line else "AMD GPU" + print(f"[OK] AMD GPU: {gpus['amd_name']}") + except Exception: + pass + + if not gpus["nvidia"] and not gpus["intel"] and not gpus["amd"]: + print("[i] No GPU detected - will use CPU (NumPy)") + + return gpus + + +def install_core_dependencies() -> bool: + """Install core Python dependencies.""" + print_step("Installing core dependencies...") + + req_file = os.path.join(os.path.dirname(__file__), "backend", "requirements.txt") + if not os.path.exists(req_file): + print(f"[X] requirements.txt not found at {req_file}") + return False + + try: + subprocess.run( + [sys.executable, "-m", "pip", "install", "-r", req_file, + "--quiet", "--no-warn-script-location"], + check=True, + timeout=600 + ) + print("[OK] Core dependencies installed") + return True + except subprocess.CalledProcessError as e: + print(f"[X] pip install failed: {e}") + return False + except subprocess.TimeoutExpired: + print("[X] pip install timed out (10 min)") + return False + + +def install_gpu_dependencies(gpus: dict) -> bool: + """Install GPU-specific dependencies based on detected hardware.""" + print_step("Setting up GPU acceleration...") + + gpu_installed = False + + # NVIDIA - install CuPy (includes CUDA runtime) + if gpus["nvidia"]: + print(f" Installing CuPy for {gpus['nvidia_name']}...") + try: + # Try CUDA 12 first (newer cards, RTX 30xx/40xx) + subprocess.run( + [sys.executable, "-m", "pip", "install", "cupy-cuda12x", + "--quiet", "--no-warn-script-location"], + check=True, + timeout=600 + ) + print(f" [OK] CuPy (CUDA 12) installed") + gpu_installed = True + except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + try: + # Fallback to CUDA 11 (older cards) + print(" [!] CUDA 12 failed, trying CUDA 11...") + subprocess.run( + [sys.executable, "-m", "pip", "install", "cupy-cuda11x", + "--quiet", "--no-warn-script-location"], + check=True, + timeout=600 + ) + print(f" [OK] CuPy (CUDA 11) installed") + gpu_installed = True + except Exception as e: + print(f" [X] CuPy installation failed: {e}") + print(f" Manual install: pip install cupy-cuda12x") + + # Intel/AMD - install PyOpenCL + if gpus["intel"] or gpus["amd"]: + gpu_name = gpus["intel_name"] or gpus["amd_name"] + print(f" Installing PyOpenCL for {gpu_name}...") + try: + subprocess.run( + [sys.executable, "-m", "pip", "install", "pyopencl", + "--quiet", "--no-warn-script-location"], + check=True, + timeout=300 + ) + print(f" [OK] PyOpenCL installed") + gpu_installed = True + except Exception as e: + print(f" [X] PyOpenCL installation failed: {e}") + print(f" Manual install: pip install pyopencl") + + if not gpu_installed and not gpus["nvidia"] and not gpus["intel"] and not gpus["amd"]: + print(" [i] No GPU acceleration - using CPU (NumPy)") + print(" This is fine! GPU just makes large calculations faster.") + + return gpu_installed + + +def install_frontend(has_node: bool) -> bool: + """Install frontend dependencies and build.""" + if not has_node: + print_step("Skipping frontend build (Node.js not available)") + return False + + print_step("Setting up frontend...") + frontend_dir = os.path.join(os.path.dirname(__file__), "frontend") + + if not os.path.exists(os.path.join(frontend_dir, "package.json")): + print("[!] Frontend directory not found") + return False + + try: + print(" Installing npm packages...") + subprocess.run( + ["npm", "install"], + cwd=frontend_dir, + check=True, + timeout=300, + capture_output=True + ) + print(" Building frontend...") + subprocess.run( + ["npm", "run", "build"], + cwd=frontend_dir, + check=True, + timeout=300, + capture_output=True + ) + print("[OK] Frontend built") + return True + except subprocess.CalledProcessError as e: + print(f"[X] Frontend build failed: {e}") + return False + except subprocess.TimeoutExpired: + print("[X] Frontend build timed out") + return False + + +def create_launcher() -> bool: + """Create launcher scripts.""" + print_step("Creating launcher scripts...") + + base_dir = os.path.dirname(os.path.abspath(__file__)) + + if platform.system() == "Windows": + # Create RFCP.bat + launcher_path = os.path.join(base_dir, "RFCP.bat") + with open(launcher_path, 'w') as f: + f.write('@echo off\n') + f.write('title RFCP - RF Coverage Planner\n') + f.write(f'cd /d "{base_dir}"\n') + f.write('echo Starting RFCP...\n') + f.write('echo Open http://localhost:8090 in your browser\n') + f.write('echo Press Ctrl+C to stop\n') + f.write('echo.\n') + f.write(f'cd backend\n') + f.write(f'"{sys.executable}" -m uvicorn app.main:app --host 0.0.0.0 --port 8090\n') + print(f" [OK] Created: RFCP.bat") + + # Create install.bat for first-time setup + install_bat_path = os.path.join(base_dir, "install.bat") + with open(install_bat_path, 'w') as f: + f.write('@echo off\n') + f.write('title RFCP - First Time Setup\n') + f.write('echo ============================================\n') + f.write('echo RFCP - RF Coverage Planner - Setup\n') + f.write('echo ============================================\n') + f.write('echo.\n') + f.write('python --version >nul 2>&1\n') + f.write('if errorlevel 1 (\n') + f.write(' echo ERROR: Python not found!\n') + f.write(' echo Please install Python 3.10+ from python.org\n') + f.write(' pause\n') + f.write(' exit /b 1\n') + f.write(')\n') + f.write(f'cd /d "{base_dir}"\n') + f.write('python install_rfcp.py\n') + f.write('echo.\n') + f.write('echo Setup complete! Run RFCP.bat to start.\n') + f.write('pause\n') + print(f" [OK] Created: install.bat") + else: + # Linux/macOS + launcher_path = os.path.join(base_dir, "rfcp.sh") + with open(launcher_path, 'w') as f: + f.write('#!/bin/bash\n') + f.write(f'cd "{base_dir}"\n') + f.write('echo "Starting RFCP..."\n') + f.write('echo "Open http://localhost:8090 in your browser"\n') + f.write('echo "Press Ctrl+C to stop"\n') + f.write('cd backend\n') + f.write(f'{sys.executable} -m uvicorn app.main:app --host 0.0.0.0 --port 8090\n') + os.chmod(launcher_path, 0o755) + print(f" [OK] Created: rfcp.sh") + + return True + + +def verify_installation() -> bool: + """Run quick verification tests.""" + print_step("Verifying installation...") + + checks = [] + critical_fail = False + + # Check core imports + try: + import numpy as np + checks.append(f"[OK] NumPy {np.__version__}") + except ImportError: + checks.append("[X] NumPy missing") + critical_fail = True + + try: + import scipy + checks.append(f"[OK] SciPy {scipy.__version__}") + except ImportError: + checks.append("[X] SciPy missing") + critical_fail = True + + try: + import fastapi + checks.append(f"[OK] FastAPI {fastapi.__version__}") + except ImportError: + checks.append("[X] FastAPI missing") + critical_fail = True + + try: + import uvicorn + checks.append(f"[OK] Uvicorn {uvicorn.__version__}") + except ImportError: + checks.append("[X] Uvicorn missing") + critical_fail = True + + # Check GPU acceleration + try: + import cupy as cp + device_count = cp.cuda.runtime.getDeviceCount() + if device_count > 0: + props = cp.cuda.runtime.getDeviceProperties(0) + name = props["name"] + if isinstance(name, bytes): + name = name.decode() + mem_mb = props["totalGlobalMem"] // (1024 * 1024) + checks.append(f"[OK] CuPy (CUDA) -> {name} ({mem_mb} MB)") + else: + checks.append("[i] CuPy installed but no CUDA devices found") + except ImportError: + checks.append("[i] CuPy not available (NVIDIA GPU acceleration disabled)") + except Exception as e: + checks.append(f"[!] CuPy error: {e}") + + try: + import pyopencl as cl + devices = [] + for p in cl.get_platforms(): + for d in p.get_devices(): + devices.append(d.name.strip()) + if devices: + checks.append(f"[OK] PyOpenCL -> {', '.join(devices[:2])}") + else: + checks.append("[i] PyOpenCL installed but no devices found") + except ImportError: + checks.append("[i] PyOpenCL not available (Intel/AMD GPU acceleration disabled)") + except Exception as e: + checks.append(f"[!] PyOpenCL error: {e}") + + for check in checks: + print(f" {check}") + + return not critical_fail + + +def main(): + """Main installer entry point.""" + print_header("RFCP - RF Coverage Planner - Installer") + + # Step 1: Check prerequisites + print_step("Checking prerequisites...") + if not check_python(): + print("\n[X] Python 3.10+ is required. Please install from python.org") + sys.exit(1) + + has_node = check_node() + + # Step 2: Detect GPU + print_step("Detecting GPU hardware...") + gpus = detect_gpu() + + # Step 3: Install core dependencies + if not install_core_dependencies(): + print("\n[X] Core dependency installation failed") + sys.exit(1) + + # Step 4: Install GPU dependencies + install_gpu_dependencies(gpus) + + # Step 5: Frontend (optional) + install_frontend(has_node) + + # Step 6: Create launcher + create_launcher() + + # Step 7: Verify + success = verify_installation() + + # Summary + print_header("Installation Summary") + + if success: + print(" [OK] RFCP installed successfully!") + print() + print(" To start RFCP:") + if platform.system() == "Windows": + print(" Double-click RFCP.bat") + print(" Or run: python -m uvicorn app.main:app --port 8090") + else: + print(" Run: ./rfcp.sh") + print(" Or: python -m uvicorn app.main:app --port 8090") + print() + print(" Then open: http://localhost:8090") + print() + + # GPU summary + if gpus["nvidia"]: + print(f" GPU: {gpus['nvidia_name']} (CUDA)") + elif gpus["intel"]: + print(f" GPU: {gpus['intel_name']} (OpenCL)") + elif gpus["amd"]: + print(f" GPU: {gpus['amd_name']} (OpenCL)") + else: + print(" Mode: CPU only (NumPy)") + else: + print(" [!] Installation completed with errors") + print(" Some features may not work correctly") + + print() + print('=' * 60) + + +if __name__ == "__main__": + main() diff --git a/rfcp.sh b/rfcp.sh new file mode 100644 index 0000000..1d49403 --- /dev/null +++ b/rfcp.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# RFCP - RF Coverage Planner - Launcher + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Check if backend exists +if [ ! -f "backend/app/main.py" ]; then + echo "ERROR: RFCP backend not found." + echo "Run: python install_rfcp.py" + exit 1 +fi + +echo "============================================" +echo " RFCP - RF Coverage Planner" +echo "============================================" +echo "" +echo "Starting backend server..." +echo "Open http://localhost:8090 in your browser" +echo "Press Ctrl+C to stop" +echo "" + +cd backend +python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8090