505 lines
14 KiB
Markdown
505 lines
14 KiB
Markdown
# 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 && (
|
||
<div
|
||
className="absolute top-full mt-1 bg-dark-surface border border-dark-border
|
||
rounded-lg shadow-2xl p-3 min-w-[300px]"
|
||
style={{ zIndex: 9999 }} // MUST be above sidebar (which is ~z-50 or z-auto)
|
||
>
|
||
...
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**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 <Polygon positions={boundary.map(p => [p.lat, p.lon])} />;
|
||
}
|
||
|
||
// Fallback to current convex hull implementation
|
||
return <CurrentImplementation points={points} />;
|
||
};
|
||
```
|
||
|
||
### 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
|