Files
rfcp/RFCP-Phase-2.1-Desktop-Installer.md

731 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RFCP Phase 2.1: Desktop Application & Installer
**Date:** January 31, 2025
**Type:** Packaging & Distribution
**Estimated:** 20-30 hours
**Priority:** After frontend-backend integration (1.5)
---
## 🎯 Goal
Package RFCP as standalone desktop application with installer for Windows and Linux. Fully offline capable after initial setup.
---
## 📊 Target Specs
| Metric | Target |
|--------|--------|
| Installer size | 200-300 MB |
| Installed size | 500MB - 1GB |
| Platforms | Windows 10/11, Ubuntu 22.04+ |
| Offline | Full offline after region download |
| GPU | Optional, configurable in settings |
---
## 🏗️ Architecture
```
┌─────────────────────────────────────────────┐
│ RFCP Desktop App │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ Electron Shell │ │
│ │ (Chromium + Node.js runtime) │ │
│ └──────────────┬──────────────────────┘ │
│ │ │
│ ┌──────────────▼──────────────────────┐ │
│ │ React Frontend (UI) │ │
│ │ localhost:5173 (dev) / bundled │ │
│ └──────────────┬──────────────────────┘ │
│ │ HTTP API │
│ ┌──────────────▼──────────────────────┐ │
│ │ FastAPI Backend (Python) │ │
│ │ localhost:8888 │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ Propagation Engine │ │ │
│ │ │ ├── CPU (default) │ │ │
│ │ │ └── GPU (optional/CUDA) │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────┬──────────────────────┘ │
│ │ │
│ ┌──────────────▼──────────────────────┐ │
│ │ Local Data Store │ │
│ │ ├── SQLite (projects, settings) │ │
│ │ ├── SRTM tiles (elevation) │ │
│ │ └── OSM cache (buildings, roads) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
---
## 📁 Directory Structure
### Development
```
rfcp/
├── electron/
│ ├── main.js # Electron main process
│ ├── preload.js # Context bridge
│ ├── package.json # Electron deps
│ └── build/ # electron-builder config
│ ├── icon.ico
│ ├── icon.png
│ └── installer.nsh # NSIS customization
├── frontend/ # React (existing)
├── backend/ # FastAPI (existing)
└── scripts/
├── build-windows.sh
├── build-linux.sh
└── package-python.sh
```
### Installed (Windows)
```
C:\Program Files\RFCP\
├── RFCP.exe # Electron app
├── resources/
│ ├── app.asar # Frontend bundle
│ └── backend/
│ ├── rfcp-server.exe # PyInstaller bundle
│ └── app/ # Python code
├── data/
│ ├── rfcp.db # SQLite database
│ ├── srtm/ # Elevation tiles
│ ├── osm/ # OSM cache
│ └── projects/ # User projects
├── python/ # Embedded Python (if not PyInstaller)
└── Uninstall RFCP.exe
```
### Installed (Linux)
```
/opt/rfcp/
├── rfcp # AppImage or binary
├── resources/
│ └── ...
└── data/
└── ...
~/.local/share/rfcp/ # User data
├── rfcp.db
├── srtm/
├── osm/
└── projects/
```
---
## ✅ Tasks
### Task 2.1.1: Electron Shell (4-6 hours)
**electron/main.js:**
```javascript
const { app, BrowserWindow, ipcMain } = require('electron');
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
let mainWindow;
let backendProcess;
// Paths
const isDev = process.env.NODE_ENV === 'development';
const backendPath = isDev
? path.join(__dirname, '../backend')
: path.join(process.resourcesPath, 'backend');
const dataPath = isDev
? path.join(__dirname, '../data')
: path.join(app.getPath('userData'), 'data');
// Ensure data directories exist
function ensureDataDirs() {
const dirs = ['srtm', 'osm', 'projects'];
dirs.forEach(dir => {
const fullPath = path.join(dataPath, dir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
}
});
}
// Start Python backend
function startBackend() {
const pythonExe = isDev
? 'python'
: path.join(process.resourcesPath, 'backend', 'rfcp-server.exe');
const env = {
...process.env,
RFCP_DATA_PATH: dataPath,
RFCP_PORT: '8888'
};
if (isDev) {
backendProcess = spawn(pythonExe, ['-m', 'uvicorn', 'app.main:app', '--port', '8888'], {
cwd: backendPath,
env
});
} else {
backendProcess = spawn(pythonExe, [], { env });
}
backendProcess.stdout.on('data', (data) => {
console.log(`Backend: ${data}`);
});
backendProcess.stderr.on('data', (data) => {
console.error(`Backend Error: ${data}`);
});
return new Promise((resolve) => {
// Wait for backend to be ready
const checkBackend = setInterval(async () => {
try {
const response = await fetch('http://localhost:8888/api/health/');
if (response.ok) {
clearInterval(checkBackend);
resolve();
}
} catch (e) {
// Not ready yet
}
}, 500);
// Timeout after 30s
setTimeout(() => {
clearInterval(checkBackend);
resolve(); // Continue anyway
}, 30000);
});
}
// Create window
async function createWindow() {
ensureDataDirs();
// Show splash while loading
const splash = new BrowserWindow({
width: 400,
height: 300,
frame: false,
alwaysOnTop: true,
transparent: true
});
splash.loadFile('splash.html');
// Start backend
await startBackend();
// Create main window
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
minHeight: 768,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, 'build/icon.png'),
title: 'RFCP - RF Coverage Planner'
});
// Load frontend
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(process.resourcesPath, 'frontend', 'index.html'));
}
// Close splash
splash.close();
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// App lifecycle
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (backendProcess) {
backendProcess.kill();
}
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
// IPC handlers
ipcMain.handle('get-data-path', () => dataPath);
ipcMain.handle('get-gpu-info', () => {
// Detect GPU availability
// Could use node-gpu or check CUDA
return {
available: false, // TODO: implement detection
name: null
};
});
```
**electron/preload.js:**
```javascript
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('rfcp', {
getDataPath: () => ipcRenderer.invoke('get-data-path'),
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),
platform: process.platform,
version: require('./package.json').version
});
```
---
### Task 2.1.2: Python Packaging (4-6 hours)
**PyInstaller spec file (rfcp-server.spec):**
```python
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['app/main.py'],
pathex=[],
binaries=[],
datas=[
('app', 'app'),
],
hiddenimports=[
'uvicorn.logging',
'uvicorn.protocols.http',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto',
'uvicorn.lifespan.on',
'uvicorn.lifespan.off',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='rfcp-server',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True, # False for production
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='../electron/build/icon.ico'
)
```
**Build script (scripts/package-python.sh):**
```bash
#!/bin/bash
set -e
cd backend
# Create venv for packaging
python -m venv build_env
source build_env/bin/activate # or build_env\Scripts\activate on Windows
# Install deps
pip install -r requirements.txt
pip install pyinstaller
# Build
pyinstaller rfcp-server.spec --clean
# Output in dist/rfcp-server.exe
echo "Built: dist/rfcp-server.exe"
```
---
### Task 2.1.3: Electron Builder Config (3-4 hours)
**electron/package.json:**
```json
{
"name": "rfcp",
"version": "1.0.0",
"description": "RF Coverage Planner for Tactical Communications",
"main": "main.js",
"author": "UMTC Project",
"license": "MIT",
"scripts": {
"start": "electron .",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
"build:all": "electron-builder --win --linux"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.0"
},
"build": {
"appId": "one.eliah.rfcp",
"productName": "RFCP",
"copyright": "Copyright © 2025 UMTC Project",
"directories": {
"output": "dist",
"buildResources": "build"
},
"files": [
"main.js",
"preload.js",
"splash.html"
],
"extraResources": [
{
"from": "../frontend/dist",
"to": "frontend"
},
{
"from": "../backend/dist/rfcp-server",
"to": "backend"
}
],
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"icon": "build/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico",
"installerHeaderIcon": "build/icon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "RFCP"
},
"linux": {
"target": [
"AppImage",
"deb"
],
"icon": "build/icon.png",
"category": "Science"
}
}
}
```
---
### Task 2.1.4: First Run & Region Selection (4-6 hours)
**First run wizard:**
```
┌─────────────────────────────────────────┐
│ Welcome to RFCP │
│ RF Coverage Planner │
├─────────────────────────────────────────┤
│ │
│ Select your region for offline maps: │
│ │
│ ┌─────────────────────────────────┐ │
│ │ [x] Ukraine (~2.5 GB) │ │
│ │ [ ] Poland (~1.8 GB) │ │
│ │ [ ] Germany (~2.1 GB) │ │
│ │ [ ] Custom bounding box... │ │
│ └─────────────────────────────────┘ │
│ │
│ Data includes: │
│ • Terrain elevation (SRTM 30m) │
│ • Building footprints (OSM) │
│ • Road network (OSM) │
│ • Base map tiles │
│ │
│ [ ] Download now │
│ [ ] Download later (online mode) │
│ │
│ [Continue →] │
└─────────────────────────────────────────┘
```
**Region download API endpoint:**
```python
@router.post("/regions/download")
async def download_region(region: str, background_tasks: BackgroundTasks):
"""Start region download in background"""
REGIONS = {
"ukraine": {
"bbox": [44.0, 22.0, 52.5, 40.5], # S, W, N, E
"srtm_tiles": ["N44E022", "N44E023", ...], # ~120 tiles
"estimated_size": 2.5 * 1024 * 1024 * 1024 # 2.5 GB
},
# ...
}
if region not in REGIONS:
raise HTTPException(400, "Unknown region")
task_id = str(uuid4())
background_tasks.add_task(download_region_data, task_id, REGIONS[region])
return {"task_id": task_id, "status": "started"}
@router.get("/regions/progress/{task_id}")
async def get_download_progress(task_id: str):
"""Get download progress"""
return {
"task_id": task_id,
"status": "downloading", # started, downloading, extracting, done, error
"progress": 45.5, # percentage
"current_file": "N48E035.hgt",
"downloaded_mb": 1250,
"total_mb": 2500
}
```
---
### Task 2.1.5: Settings - GPU Configuration (2-3 hours)
**Settings panel addition:**
```typescript
// frontend/src/components/settings/PerformanceSettings.tsx
interface PerformanceSettingsProps {
gpuInfo: {
available: boolean;
name: string | null;
memory: number | null;
};
}
export function PerformanceSettings({ gpuInfo }: PerformanceSettingsProps) {
const [useGpu, setUseGpu] = useState(false);
const [maxWorkers, setMaxWorkers] = useState(4);
return (
<div className="settings-section">
<h3>Performance</h3>
<div className="setting-item">
<label>
<input
type="checkbox"
checked={useGpu}
onChange={(e) => setUseGpu(e.target.checked)}
disabled={!gpuInfo.available}
/>
Use GPU acceleration (CUDA)
</label>
{gpuInfo.available ? (
<span className="hint">Detected: {gpuInfo.name}</span>
) : (
<span className="hint warning">No compatible GPU detected</span>
)}
</div>
<div className="setting-item">
<label>CPU Workers</label>
<input
type="range"
min="1"
max="16"
value={maxWorkers}
onChange={(e) => setMaxWorkers(Number(e.target.value))}
/>
<span>{maxWorkers} threads</span>
</div>
<div className="setting-item">
<label>Default Propagation Model</label>
<select>
<option value="fast">Fast (terrain only)</option>
<option value="standard">Standard (+ buildings)</option>
<option value="detailed">Detailed (+ dominant path)</option>
<option value="full">Full (all models)</option>
</select>
</div>
</div>
);
}
```
**Backend GPU detection:**
```python
# app/services/gpu_service.py
def detect_gpu() -> dict:
"""Detect available GPU for CUDA"""
result = {
"available": False,
"name": None,
"memory": None,
"cuda_version": None
}
try:
import cupy as cp
device = cp.cuda.Device(0)
props = cp.cuda.runtime.getDeviceProperties(0)
result["available"] = True
result["name"] = props["name"].decode()
result["memory"] = props["totalGlobalMem"] // (1024**3) # GB
result["cuda_version"] = cp.cuda.runtime.runtimeGetVersion()
except ImportError:
pass # CuPy not installed
except Exception as e:
print(f"GPU detection error: {e}")
return result
```
---
### Task 2.1.6: Build & Test (3-4 hours)
**Build script (scripts/build-windows.sh):**
```bash
#!/bin/bash
set -e
echo "=== RFCP Windows Build ==="
# 1. Build frontend
echo "Building frontend..."
cd frontend
npm run build
cd ..
# 2. Build backend
echo "Building backend..."
cd backend
python -m venv build_env
source build_env/Scripts/activate
pip install -r requirements.txt
pip install pyinstaller
pyinstaller rfcp-server.spec --clean
cd ..
# 3. Build Electron
echo "Building Electron app..."
cd electron
npm install
npm run build:win
cd ..
echo "=== Build complete ==="
echo "Installer: electron/dist/RFCP Setup*.exe"
```
**Test checklist:**
```markdown
## Install Test
- [ ] Installer runs without admin (user install)
- [ ] Installer runs with admin (program files)
- [ ] Desktop shortcut created
- [ ] Start menu entry created
- [ ] Uninstaller works
## First Run
- [ ] Splash screen appears
- [ ] Backend starts successfully
- [ ] Main window loads
- [ ] Region selection dialog shows
- [ ] Can skip region download
## Functionality
- [ ] Map loads (online tiles)
- [ ] Can create sites
- [ ] Coverage calculation works
- [ ] All presets work
- [ ] Settings persist
## Offline
- [ ] Works without internet (after region download)
- [ ] Offline tiles load
- [ ] Terrain data works
- [ ] Buildings/roads cached
## Performance
- [ ] GPU toggle appears (if GPU present)
- [ ] GPU acceleration works
- [ ] Memory usage reasonable (<1GB idle)
```
---
## 📦 Deliverables
1. **RFCP-Setup-1.0.0.exe** — Windows installer (~200MB)
2. **RFCP-1.0.0.AppImage** — Linux portable (~180MB)
3. **RFCP-1.0.0.deb** — Debian package (~180MB)
4. **SHA256SUMS** — Checksums file
5. **README.md** — Installation instructions
---
## 🔜 Future Enhancements (2.2+)
- [ ] Auto-updater (electron-updater)
- [ ] macOS support (.dmg)
- [ ] Portable mode (no install, run from USB)
- [ ] Silent install for deployment
- [ ] MSI installer for enterprise
- [ ] Code signing (Windows/macOS)
---
## 📝 Notes
- SQLite замість MongoDB для локального зберігання (простіше, не потребує сервера)
- SRTM tiles ~25MB кожен, Україна потребує ~120 tiles = ~3GB
- OSM дані можна завантажити з Geofabrik (ukraine-latest.osm.pbf ~1.5GB)
- Map tiles можна кешувати з OpenStreetMap або використати MBTiles
---
**Ready for implementation after 1.5** 🚀