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