@mytec: initial commit before dt

This commit is contained in:
2026-01-31 13:54:20 +02:00
parent 375a78f5b9
commit 04fe8fb814
18 changed files with 1087 additions and 730 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,730 +0,0 @@
# 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** 🚀

View File

@@ -9,3 +9,5 @@ numpy==1.26.4
scipy==1.12.0 scipy==1.12.0
requests==2.31.0 requests==2.31.0
httpx==0.27.0 httpx==0.27.0
aiosqlite>=0.19.0
sqlalchemy>=2.0.0

BIN
desktop/assets/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

BIN
desktop/assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
desktop/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB