From 1a3c3e0f11a9ca935e436c2a7d7ad5e443f21c12 Mon Sep 17 00:00:00 2001 From: mytec Date: Fri, 30 Jan 2026 12:05:05 +0200 Subject: [PATCH] @mytec: iter5 start --- RFCP-Iteration5-RF-Accuracy.md | 505 +++++++++++++++++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 RFCP-Iteration5-RF-Accuracy.md diff --git a/RFCP-Iteration5-RF-Accuracy.md b/RFCP-Iteration5-RF-Accuracy.md new file mode 100644 index 0000000..a556410 --- /dev/null +++ b/RFCP-Iteration5-RF-Accuracy.md @@ -0,0 +1,505 @@ +# RFCP - Iteration 5: RF Accuracy & UX Polish + +## Issues Identified + +1. ❌ **Antenna back lobe missing** - Real antennas radiate backwards (F/B ratio ~20-25 dB) +2. ❌ **Heatmap colors change with zoom** - Same RSRP shows different colors at different zoom levels +3. ❌ **Antenna gain ignored** - 25 dBi vs 0 dBi produces identical coverage +4. ❌ **Antenna height ignored** - 10m vs 100m towers show same coverage +5. ❌ **Batch edit UX issues** - Edit panel stays open, values not updated dynamically + +--- + +## CRITICAL FIX 1: Accurate 3GPP Antenna Pattern + +**Problem:** Current implementation has hard cutoff at beamwidth/2. Real sector antennas have: +- Main lobe with gradual attenuation +- Side lobes (~15-20 dB down) +- Back lobe (~20-25 dB down, not -∞) + +### Correct 3GPP Formula + +**File:** `frontend/src/workers/rf-worker.js` + +Replace antenna pattern calculation: + +```javascript +function calculate3GPPPattern(azimuth, bearing, beamwidth = 65, frontBackRatio = 25) { + // Normalize angle difference to -180...+180 + let angleDiff = bearing - azimuth; + while (angleDiff > 180) angleDiff -= 360; + while (angleDiff < -180) angleDiff += 360; + + const theta = Math.abs(angleDiff); + + // 3GPP TR 36.814 Horizontal Pattern + // A(θ) = -min[12(θ/θ_3dB)², A_m] + const theta3dB = beamwidth / 2; // Half-power beamwidth + const Am = frontBackRatio; // Maximum attenuation (front-to-back ratio) + + let attenuation; + + if (theta <= 180) { + // Front hemisphere (includes main lobe and side lobes) + attenuation = -Math.min(12 * Math.pow(theta / theta3dB, 2), Am); + } else { + // This shouldn't happen after normalization, but just in case + attenuation = -Am; + } + + return attenuation; // Returns dB loss (negative value) +} + +// In coverage calculation: +if (site.antennaType === 'sector') { + const bearing = calculateBearing(site.lat, site.lon, lat, lon); + const azimuth = site.azimuth || 0; + const beamwidth = site.beamwidth || 65; + + // Get antenna pattern loss + const patternLoss = calculate3GPPPattern(azimuth, bearing, beamwidth, 25); + + // Add pattern loss to path loss + const effectiveRSRP = site.power + site.antennaGain + patternLoss - fspl; + + // No hard cutoff! Back lobe will be ~25 dB weaker +} +``` + +### Visualization Update + +Show actual pattern with gradient opacity: + +**File:** `frontend/src/components/map/SiteMarker.tsx` + +```typescript +// Generate sector wedge with gradient +function generateSectorWedge(site: Site) { + const mainLobe = generateArc(site, site.azimuth, site.beamwidth, 0.8); // Main beam + const sideLobe = generateArc(site, site.azimuth, 180, 0.3); // Sides + const backLobe = generateArc(site, site.azimuth + 180, 60, 0.1); // Back + + return [mainLobe, sideLobe, backLobe]; // Array of polygons +} +``` + +--- + +## CRITICAL FIX 2: Zoom-Independent Heatmap Colors + +**Problem:** `maxIntensity` changes with zoom, so same RSRP value maps to different colors. + +**Solution:** Keep RSRP thresholds constant, adjust only visual radius/blur. + +**File:** `frontend/src/components/map/Heatmap.tsx` + +```typescript +export function Heatmap({ points, visible, opacity = 0.7 }: HeatmapProps) { + const map = useMap(); + const [mapZoom, setMapZoom] = useState(map.getZoom()); + + useEffect(() => { + const handleZoomEnd = () => setMapZoom(map.getZoom()); + map.on('zoomend', handleZoomEnd); + return () => { map.off('zoomend', handleZoomEnd); }; + }, [map]); + + if (!visible || points.length === 0) return null; + + // FIXED: Normalize RSRP consistently regardless of zoom + const normalizeRSRP = (rsrp: number): number => { + const minRSRP = -120; // Very weak + const maxRSRP = -70; // Excellent + const normalized = (rsrp - minRSRP) / (maxRSRP - minRSRP); + return Math.max(0, Math.min(1, normalized)); + }; + + // Zoom-dependent visual parameters ONLY + const radius = Math.max(8, Math.min(40, 50 - mapZoom * 2.5)); + const blur = Math.max(6, Math.min(20, 30 - mapZoom * 1.5)); + + // CRITICAL FIX: maxIntensity is now CONSTANT + const maxIntensity = 1.0; // Always 1.0, NEVER changes with zoom + + const heatmapPoints = points.map(p => [ + p.lat, + p.lon, + normalizeRSRP(p.rsrp) + ] as [number, number, number]); + + return ( +
+ p[1]} + latitudeExtractor={(p) => p[0]} + intensityExtractor={(p) => p[2]} + gradient={{ + 0.0: '#0d47a1', // -120 dBm (dark blue, no service) + 0.2: '#00bcd4', // -110 dBm (cyan, weak) + 0.4: '#4caf50', // -100 dBm (green, fair) + 0.6: '#ffeb3b', // -85 dBm (yellow, good) + 0.8: '#ff9800', // -75 dBm (orange, strong) + 1.0: '#f44336', // -70 dBm (red, excellent) + }} + radius={radius} + blur={blur} + max={maxIntensity} // ALWAYS 1.0 + minOpacity={0.3} + /> +
+ ); +} +``` + +### Add Legend with Actual dBm Values + +**File:** `frontend/src/components/map/Legend.tsx` + +```typescript +export function Legend() { + return ( +
+

Signal Strength (RSRP)

+
+ + Excellent (> -70 dBm) +
+
+ + Strong (-75 to -70 dBm) +
+
+ + Good (-85 to -75 dBm) +
+
+ + Fair (-100 to -85 dBm) +
+
+ + Weak (-110 to -100 dBm) +
+
+ + No Service (< -120 dBm) +
+
+ ); +} +``` + +--- + +## CRITICAL FIX 3: Antenna Gain Effect + +**Problem:** Antenna gain is stored but not used in RSRP calculation. + +**Formula:** `RSRP = TxPower + TxGain - PathLoss - PatternLoss` + +**File:** `frontend/src/workers/rf-worker.js` + +```javascript +// In calculateCoverage function: + +for (const site of sites) { + for (let latIdx = 0; latIdx < latPoints; latIdx++) { + for (let lonIdx = 0; lonIdx < lonPoints; lonIdx++) { + const lat = minLat + latIdx * latStep; + const lon = minLon + lonIdx * lonStep; + + const distance = calculateDistance(site.lat, site.lon, lat, lon); + if (distance > radius) continue; + + // Path loss (FSPL) + const fspl = calculateFSPL(distance, site.frequency); + + // Antenna pattern loss + let patternLoss = 0; + if (site.antennaType === 'sector') { + const bearing = calculateBearing(site.lat, site.lon, lat, lon); + patternLoss = -calculate3GPPPattern(site.azimuth, bearing, site.beamwidth, 25); + } + + // CRITICAL: Include antenna gain! + const antennaGain = site.antennaGain || 0; // dBi + + // Final RSRP calculation + const rsrp = site.power + antennaGain - fspl - patternLoss; + + // Store point + if (rsrp > rsrpThreshold) { + points.push({ lat, lon, rsrp, siteId: site.id }); + } + } + } +} +``` + +**Expected Behavior:** +- 0 dBi omni (dipole): baseline coverage +- 8 dBi omni (collinear): +8 dB → ~2x distance +- 15 dBi sector (panel): +15 dB → ~5x distance +- 25 dBi sector (parabolic): +25 dB → ~17x distance + +--- + +## CRITICAL FIX 4: Antenna Height Effect + +**Problem:** Height is stored but ignored. In reality, height affects: +1. **Line-of-sight distance** (radio horizon) +2. **Terrain shadowing** (future Phase 4) + +For now, implement **radio horizon** effect: + +**Formula:** `d_horizon = 3.57 * sqrt(h)` km, where h is in meters + +**File:** `frontend/src/workers/rf-worker.js` + +```javascript +function calculateRadioHorizon(heightMeters) { + // Earth curvature formula + // Assumes 4/3 Earth radius (k=4/3 for standard atmosphere) + return 3.57 * Math.sqrt(heightMeters); // km +} + +// In coverage calculation: +for (const site of sites) { + const siteHorizon = calculateRadioHorizon(site.height); + const maxRange = Math.min(radius, siteHorizon); + + for (let latIdx = 0; latIdx < latPoints; latIdx++) { + for (let lonIdx = 0; lonIdx < lonPoints; lonIdx++) { + const lat = minLat + latIdx * latStep; + const lon = minLon + lonIdx * lonStep; + + const distance = calculateDistance(site.lat, site.lon, lat, lon); + + // Check horizon limit + if (distance > maxRange) continue; + + // ... rest of calculation + } + } +} +``` + +**Expected Behavior:** +- 10m tower: horizon = 11.3 km +- 30m tower: horizon = 19.5 km +- 100m tower: horizon = 35.7 km + +**Note:** This is a simplified model. Phase 4 will add terrain-aware line-of-sight. + +--- + +## FIX 5: Batch Edit UX Improvements + +**Problem:** +1. Edit panel stays open during batch operations +2. Selected site values don't update dynamically +3. Unclear what changed + +### Solution A: Close Edit Panel on Batch Operation + +**File:** `frontend/src/components/panels/SiteList.tsx` + +```typescript +const handleBatchUpdate = (delta: number) => { + batchUpdateHeight(delta); + + // Close edit panel if currently editing a selected site + if (editingSiteId && selectedSiteIds.includes(editingSiteId)) { + setEditingSiteId(null); + } + + toast.success(`Updated ${selectedSiteIds.length} sites by ${delta > 0 ? '+' : ''}${delta}m`); +}; + +const handleBatchSet = (height: number) => { + batchSetHeight(height); + + // Close edit panel + if (editingSiteId && selectedSiteIds.includes(editingSiteId)) { + setEditingSiteId(null); + } + + toast.success(`Set ${selectedSiteIds.length} sites to ${height}m`); +}; +``` + +### Solution B: Live Update Edit Panel + +**File:** `frontend/src/components/panels/SiteForm.tsx` + +```typescript +// Watch for external changes to site being edited +useEffect(() => { + if (site) { + // Sync form with latest site data + setFormData({ + name: site.name, + frequency: site.frequency, + power: site.power, + height: site.height, // Updated dynamically! + antennaType: site.antennaType, + azimuth: site.azimuth, + beamwidth: site.beamwidth, + antennaGain: site.antennaGain, + }); + } +}, [site.height, site.power]); // Re-sync when these change +``` + +### Visual Feedback + +Add flash animation when batch updated: + +```typescript +// In SiteList item +
+ {/* ... */} +
+``` + +```css +@keyframes flash-update { + 0%, 100% { background-color: transparent; } + 50% { background-color: rgba(59, 130, 246, 0.3); } +} + +.flash-update { + animation: flash-update 0.6s ease-in-out; +} +``` + +--- + +## TESTING CHECKLIST + +### Antenna Pattern Test: +- [ ] Sector antenna (65° beam, 25 dBi gain): + - [ ] Main lobe strongest (0 dB loss) + - [ ] ±30° from azimuth: ~-12 dB loss (visible but weaker) + - [ ] ±90° from azimuth: ~-25 dB loss (faint coverage) + - [ ] 180° from azimuth: ~-25 dB loss (back lobe visible) +- [ ] Omni antenna: perfect circle (no directivity) + +### Heatmap Color Consistency Test: +- [ ] Place site, calculate coverage at zoom 8 +- [ ] Note color at specific location (e.g., 5 km away) +- [ ] Zoom to level 12, color at same location UNCHANGED +- [ ] Zoom to level 16, color STILL THE SAME +- [ ] Legend shows actual dBm values + +### Antenna Gain Test: +- [ ] Site A: 0 dBi omni, 43 dBm power → note coverage radius +- [ ] Site B: 15 dBi sector, 43 dBm power → coverage should be ~5x larger +- [ ] Site C: 25 dBi sector, 43 dBm power → coverage should be ~17x larger + +### Antenna Height Test: +- [ ] Site A: 10m height → coverage radius ~11 km max +- [ ] Site B: 30m height → coverage radius ~19 km max +- [ ] Site C: 100m height → coverage radius ~35 km max +- [ ] Even with high power, coverage stops at horizon + +### Batch Edit UX Test: +- [ ] Select 3 sites +- [ ] Edit one of them (panel opens) +- [ ] Click +10m batch operation +- [ ] Edit panel closes OR values update live +- [ ] Flash animation on updated sites +- [ ] Toast shows "Updated 3 sites by +10m" + +--- + +## BUILD & DEPLOY + +```bash +cd /opt/rfcp/frontend +npm run build +sudo systemctl reload caddy + +# Test +curl https://rfcp.eliah.one/api/health +``` + +--- + +## COMMIT MESSAGE + +``` +fix(rf): implement accurate 3GPP antenna patterns + +- Added proper 3GPP TR 36.814 horizontal pattern formula +- Sector antennas now show main lobe, side lobes, and back lobe +- Front-to-back ratio 25 dB (realistic for sector panels) +- Removed hard cutoff at beamwidth/2 + +fix(heatmap): ensure zoom-independent color mapping + +- Removed dynamic maxIntensity (now constant 1.0) +- Same RSRP now shows same color regardless of zoom level +- Added Legend component with actual dBm thresholds +- Only radius and blur adjust with zoom (visual quality) + +fix(rf): apply antenna gain to coverage calculations + +- Antenna gain now affects RSRP: TxPower + Gain - PathLoss +- 0 dBi vs 25 dBi now shows massive coverage difference +- Formula: RSRP = Power + AntennaGain - FSPL - PatternLoss + +fix(rf): implement radio horizon based on antenna height + +- Added Earth curvature calculation (d = 3.57 * sqrt(h)) +- 10m tower: 11 km horizon | 100m tower: 35 km horizon +- Coverage now limited by line-of-sight distance +- Prepares for Phase 4 terrain-aware LOS + +fix(ux): improve batch edit workflow + +- Edit panel closes when batch operation affects edited site +- Added flash animation on batch-updated sites +- Toast shows count of affected sites +- Clear visual feedback for all operations +``` + +--- + +## Expected Results + +**After all fixes:** + +1. **Antenna Pattern:** + - Sector shows wedge with weak back lobe + - No sudden cutoff, gradual attenuation + - Realistic F/B ratio ~25 dB + +2. **Heatmap:** + - Colors consistent across ALL zoom levels + - Blue = weak, red = strong, ALWAYS + - Legend shows exact dBm ranges + +3. **Antenna Gain:** + - High gain = much larger coverage + - 25 dBi sector vs 0 dBi omni: huge difference + - Realistic for real equipment + +4. **Antenna Height:** + - Tall towers = wider coverage + - Coverage stops at radio horizon + - 100m tower can't reach 100 km (limited by curvature) + +5. **UX:** + - Batch edit doesn't confuse users + - Clear feedback on what changed + - Smooth, professional workflow + +🚀 Ready to implement!