819  Mobile Labs: Coverage Planning

819.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Calculate electromagnetic wave properties: Use Python to compute wavelength, energy, and path loss for different frequencies
  • Analyze spectrum usage: Build tools to identify interference and recommend optimal channels
  • Perform link budget analysis: Calculate transmitter-to-receiver power budgets for IoT deployments
  • Plan RF coverage: Create visualization tools to identify dead zones and optimize AP placement
  • Build spectrum monitoring systems: Implement ESP32-based continuous monitoring with web interfaces

819.2 Prerequisites

Before diving into this chapter, you should be familiar with:

  • Mobile Wireless: Fundamentals: Understanding path loss, wavelength, and frequency relationships
  • Mobile Labs: Wi-Fi Spectrum Analysis: Practical Wi-Fi scanning and RSSI measurement
  • Python programming: Familiarity with numpy and matplotlib for data analysis and visualization
  • Basic RF concepts: Link budget, receiver sensitivity, and transmit power

Deep Dives: - Mobile Labs: Cellular Modem Integration - AT commands and modem setup - Mobile Labs: Wi-Fi Spectrum Analysis - ESP32 scanning tools

Comparisons: - Mobile Wireless Comprehensive Review - Technology comparison matrix - LPWAN Comparison - Range vs power trade-offs

Hands-On: - Simulations Hub - RF propagation simulators

819.3 Python Implementation 1: Wave Property Calculator

This implementation provides comprehensive calculations for electromagnetic wave properties, including frequency/wavelength relationships, energy calculations, and path loss analysis.

import math

# Physical constants
SPEED_OF_LIGHT = 299792458  # m/s
PLANCK_CONSTANT = 6.62607015e-34  # JΒ·s


def frequency_to_wavelength(frequency_hz: float) -> float:
    """Convert frequency to wavelength."""
    return SPEED_OF_LIGHT / frequency_hz


def wavelength_to_frequency(wavelength_m: float) -> float:
    """Convert wavelength to frequency."""
    return SPEED_OF_LIGHT / wavelength_m


def photon_energy(frequency_hz: float) -> float:
    """Calculate photon energy in Joules."""
    return PLANCK_CONSTANT * frequency_hz


def free_space_path_loss_db(distance_m: float, frequency_hz: float) -> float:
    """
    Calculate free-space path loss in dB.
    FSPL = 20*log10(4*pi*d/lambda)
    """
    wavelength = frequency_to_wavelength(frequency_hz)
    if distance_m <= 0:
        return 0
    return 20 * math.log10(4 * math.pi * distance_m / wavelength)


def max_range_m(
    tx_power_dbm: float,
    rx_sensitivity_dbm: float,
    frequency_hz: float,
    fade_margin_db: float = 10.0,
    extra_losses_db: float = 0.0
) -> float:
    """Calculate maximum theoretical range given link budget."""
    available_loss = tx_power_dbm - rx_sensitivity_dbm - fade_margin_db - extra_losses_db
    wavelength = frequency_to_wavelength(frequency_hz)
    # Rearrange FSPL: d = lambda * 10^(FSPL/20) / (4*pi)
    distance = wavelength * (10 ** (available_loss / 20)) / (4 * math.pi)
    return distance


def fresnel_zone_radius(distance_m: float, frequency_hz: float, zone: int = 1) -> float:
    """Calculate Fresnel zone radius at midpoint."""
    wavelength = frequency_to_wavelength(frequency_hz)
    # r_n = sqrt(n * lambda * d1 * d2 / (d1 + d2))
    # At midpoint: d1 = d2 = d/2
    d_half = distance_m / 2
    return math.sqrt(zone * wavelength * d_half * d_half / distance_m)


def main():
    print("=== Electromagnetic Wave Properties ===\n")

    frequencies = [
        ("433 MHz ISM", 433e6),
        ("868 MHz (Europe)", 868e6),
        ("2.4 GHz ISM", 2.4e9),
    ]

    for name, freq in frequencies:
        wavelength = frequency_to_wavelength(freq)
        energy = photon_energy(freq)
        print(f"{name}:")
        print(f"Frequency: {freq/1e6:.2f} MHz ({freq:.2e} Hz)")
        print(f"Wavelength: {wavelength:.4f} m ({wavelength*100:.2f} cm)")
        print(f"Energy: {energy:.2e} Joules\n")

    print("=== Path Loss Comparison at 100m ===\n")
    distance = 100
    test_freqs = [
        ("868 MHz (Europe)", 868e6),
        ("2.4 GHz ISM", 2.4e9),
        ("5 GHz", 5e9),
    ]

    for name, freq in test_freqs:
        pl = free_space_path_loss_db(distance, freq)
        print(f"{name}: {pl:.2f} dB")

    # Compare 868 MHz vs 2.4 GHz advantage
    pl_868 = free_space_path_loss_db(distance, 868e6)
    pl_2400 = free_space_path_loss_db(distance, 2.4e9)
    print(f"\nPath loss advantage of 868 MHz over 2.4 GHz: {pl_2400 - pl_868:.2f} dB")

    print("\n=== Range Calculation ===\n")
    for name, freq in [("868 MHz (Europe)", 868e6), ("2.4 GHz ISM", 2.4e9)]:
        range_m = max_range_m(
            tx_power_dbm=14,
            rx_sensitivity_dbm=-137,
            frequency_hz=freq,
            fade_margin_db=10,
            extra_losses_db=0
        )
        print(f"{name}: {range_m/1000:.2f} km range")

    print("\n=== Fresnel Zone Analysis ===\n")
    link_distance = 1000  # 1 km
    freq = 2.4e9
    fz_radius = fresnel_zone_radius(link_distance, freq)
    print(f"First Fresnel zone radius at midpoint of 1 km link (2.4 GHz): {fz_radius:.2f} m")
    print(f"Required clearance (60% of first Fresnel zone): {fz_radius * 0.6:.2f} m")


if __name__ == "__main__":
    main()

Expected Output:

=== Electromagnetic Wave Properties ===

433 MHz ISM:
Frequency: 433.00 MHz (4.33e+08 Hz)
Wavelength: 0.6924 m (69.24 cm)
Energy: 2.87e-25 Joules

868 MHz (Europe):
Frequency: 868.00 MHz (8.68e+08 Hz)
Wavelength: 0.3454 m (34.54 cm)
Energy: 5.75e-25 Joules

2.4 GHz ISM:
Frequency: 2400.00 MHz (2.40e+09 Hz)
Wavelength: 0.1249 m (12.49 cm)
Energy: 1.59e-24 Joules

=== Path Loss Comparison at 100m ===

868 MHz (Europe): 71.21 dB
2.4 GHz ISM: 80.05 dB
5 GHz: 86.44 dB

Path loss advantage of 868 MHz over 2.4 GHz: 8.84 dB

=== Range Calculation ===

868 MHz (Europe): 15.77 km range
2.4 GHz ISM: 5.70 km range

=== Fresnel Zone Analysis ===

First Fresnel zone radius at midpoint of 1 km link (2.4 GHz): 3.97 m
Required clearance (60% of first Fresnel zone): 2.38 m

819.5 Lab: ESP32 Real-Time Spectrum Monitor

This advanced lab creates a continuous spectrum monitoring system with data logging and web interface.

819.5.1 Hardware Required

  • ESP32 development board
  • MicroSD card module
  • SD card (formatted FAT32)
  • Breadboard and jumper wires
  • Optional: OLED display (128x64, I2C)

819.5.2 Wiring Diagram

ESP32          SD Card Module
-----          --------------
GPIO 5    -->  CS
GPIO 18   -->  SCK
GPIO 19   -->  MISO
GPIO 23   -->  MOSI
3.3V      -->  VCC
GND       -->  GND

Optional OLED:
GPIO 21   -->  SDA
GPIO 22   -->  SCL

819.5.3 Complete Spectrum Monitor Code

#include <WiFi.h>
#include <WebServer.h>
#include <SD.h>
#include <SPI.h>
#include <time.h>

// SD Card pins
#define SD_CS 5

// Web server
WebServer server(80);

// Scan configuration
#define SCAN_INTERVAL_MS 30000  // 30 seconds
#define MAX_NETWORKS 50

struct NetworkScan {
    char ssid[33];
    int channel;
    int rssi;
    char encryption[20];
    unsigned long timestamp;
};

NetworkScan scanHistory[MAX_NETWORKS];
int scanCount = 0;
unsigned long lastScanTime = 0;

// Statistics
int channelCounts[14] = {0};
float channelAvgRSSI[14] = {0};
int totalScans = 0;

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n\n================================");
    Serial.println("ESP32 Spectrum Monitor");
    Serial.println("================================\n");

    // Initialize SD card
    if (!SD.begin(SD_CS)) {
        Serial.println("SD Card initialization failed!");
    } else {
        Serial.println("SD Card initialized successfully");

        // Create header in log file if new
        if (!SD.exists("/spectrum_log.csv")) {
            File logFile = SD.open("/spectrum_log.csv", FILE_WRITE);
            if (logFile) {
                logFile.println("Timestamp,SSID,Channel,RSSI,Encryption");
                logFile.close();
                Serial.println("Created new log file");
            }
        }
    }

    // Set Wi-Fi mode
    WiFi.mode(WIFI_STA);
    WiFi.disconnect();

    // Start access point for web interface
    WiFi.softAP("ESP32-SpectrumMonitor", "spectrum123");
    IPAddress IP = WiFi.softAPIP();
    Serial.print("AP IP address: ");
    Serial.println(IP);

    // Setup web server routes
    server.on("/", handleRoot);
    server.on("/scan", handleScan);
    server.on("/data", handleData);
    server.on("/download", handleDownload);
    server.begin();

    Serial.println("Web server started");
    Serial.println("Connect to 'ESP32-SpectrumMonitor' and navigate to " + IP.toString());
    Serial.println("\nStarting continuous monitoring...\n");
}

void loop() {
    server.handleClient();

    // Periodic scan
    if (millis() - lastScanTime >= SCAN_INTERVAL_MS) {
        performScan();
        lastScanTime = millis();
    }
}

void performScan() {
    Serial.println("Scanning networks...");

    int n = WiFi.scanNetworks();
    totalScans++;

    // Reset channel statistics
    for (int i = 0; i < 14; i++) {
        channelCounts[i] = 0;
        channelAvgRSSI[i] = 0;
    }

    Serial.printf("Found %d networks\n\n", n);

    if (n > 0) {
        // Clear previous scan
        scanCount = 0;

        // Open log file for appending
        File logFile = SD.open("/spectrum_log.csv", FILE_APPEND);

        for (int i = 0; i < n && i < MAX_NETWORKS; i++) {
            // Store in memory
            strncpy(scanHistory[scanCount].ssid, WiFi.SSID(i).c_str(), 32);
            scanHistory[scanCount].channel = WiFi.channel(i);
            scanHistory[scanCount].rssi = WiFi.RSSI(i);
            strncpy(scanHistory[scanCount].encryption,
                   getEncryptionType(WiFi.encryptionType(i)), 19);
            scanHistory[scanCount].timestamp = millis();

            // Update channel statistics
            int ch = WiFi.channel(i);
            if (ch >= 1 && ch <= 13) {
                channelCounts[ch]++;
                channelAvgRSSI[ch] += WiFi.RSSI(i);
            }

            // Log to SD card
            if (logFile) {
                logFile.printf("%lu,%s,%d,%d,%s\n",
                              millis(),
                              WiFi.SSID(i).c_str(),
                              WiFi.channel(i),
                              WiFi.RSSI(i),
                              getEncryptionType(WiFi.encryptionType(i)));
            }

            scanCount++;
        }

        if (logFile) {
            logFile.close();
        }

        // Calculate average RSSI per channel
        for (int ch = 1; ch <= 13; ch++) {
            if (channelCounts[ch] > 0) {
                channelAvgRSSI[ch] /= channelCounts[ch];
            }
        }

        // Print summary
        printChannelSummary();
    }
}

void printChannelSummary() {
    Serial.println("\nChannel Summary:");
    Serial.println("Ch | Networks | Avg RSSI | Bar Chart");
    Serial.println("---|----------|----------|------------------------------");

    for (int ch = 1; ch <= 13; ch++) {
        Serial.printf("%2d | %8d | %8.1f | ",
                     ch, channelCounts[ch], channelAvgRSSI[ch]);

        for (int i = 0; i < channelCounts[ch]; i++) {
            Serial.print("β–ˆ");
        }
        Serial.println();
    }

    // Find best channel
    int bestChannel = 1;
    int minCount = 999;
    for (int ch : {1, 6, 11}) {
        if (channelCounts[ch] < minCount) {
            minCount = channelCounts[ch];
            bestChannel = ch;
        }
    }

    Serial.printf("\nRecommended channel: %d (%d networks)\n",
                 bestChannel, minCount);
}

void handleRoot() {
    String html = R"(
<!DOCTYPE html>
<html>
<head>
    <title>ESP32 Spectrum Monitor</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body { font-family: Arial; margin: 20px; background: #f0f0f0; }
        .container { max-width: 1000px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; }
        h1 { color: #333; }
        button { background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
        button:hover { background: #45a049; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background: #4CAF50; color: white; }
        .channel-viz { margin-top: 20px; }
        .bar { background: #2196F3; color: white; padding: 5px; margin: 2px 0; }
    </style>
    <script>
        function refreshData() {
            fetch('/data')
                .then(response => response.json())
                .then(data => updateDisplay(data));
        }

        function updateDisplay(data) {
            document.getElementById('scanCount').innerText = data.totalScans;
            document.getElementById('networkCount').innerText = data.currentNetworks;

            let table = '<table><tr><th>SSID</th><th>Channel</th><th>RSSI</th><th>Security</th></tr>';
            data.networks.forEach(net => {
                table += '<tr><td>' + net.ssid + '</td><td>' + net.channel + '</td><td>' + net.rssi + ' dBm</td><td>' + net.encryption + '</td></tr>';
            });
            table += '</table>';
            document.getElementById('networkTable').innerHTML = table;

            let viz = '';
            for (let ch = 1; ch <= 13; ch++) {
                let count = data.channelCounts[ch] || 0;
                let width = (count * 30) + 'px';
                viz += '<div class="bar" style="width: ' + width + '">Ch ' + ch + ': ' + count + ' networks</div>';
            }
            document.getElementById('channelViz').innerHTML = viz;
        }

        setInterval(refreshData, 5000);
        window.onload = refreshData;
    </script>
</head>
<body>
    <div class="container">
        <h1>ESP32 Spectrum Monitor</h1>
        <p>Total Scans: <strong id="scanCount">0</strong> |
           Current Networks: <strong id="networkCount">0</strong></p>

        <button onclick="fetch('/scan').then(() => setTimeout(refreshData, 2000))">Scan Now</button>
        <button onclick="refreshData()">Refresh</button>
        <button onclick="window.location='/download'">Download Log</button>

        <div class="channel-viz">
            <h2>Channel Distribution</h2>
            <div id="channelViz"></div>
        </div>

        <div id="networkTable"></div>
    </div>
</body>
</html>
    )";

    server.send(200, "text/html", html);
}

void handleScan() {
    performScan();
    server.send(200, "text/plain", "Scan initiated");
}

void handleData() {
    String json = "{";
    json += "\"totalScans\":" + String(totalScans) + ",";
    json += "\"currentNetworks\":" + String(scanCount) + ",";
    json += "\"networks\":[";

    for (int i = 0; i < scanCount; i++) {
        if (i > 0) json += ",";
        json += "{";
        json += "\"ssid\":\"" + String(scanHistory[i].ssid) + "\",";
        json += "\"channel\":" + String(scanHistory[i].channel) + ",";
        json += "\"rssi\":" + String(scanHistory[i].rssi) + ",";
        json += "\"encryption\":\"" + String(scanHistory[i].encryption) + "\"";
        json += "}";
    }
    json += "],";

    json += "\"channelCounts\":{";
    for (int ch = 1; ch <= 13; ch++) {
        if (ch > 1) json += ",";
        json += "\"" + String(ch) + "\":" + String(channelCounts[ch]);
    }
    json += "}}";

    server.send(200, "application/json", json);
}

void handleDownload() {
    File logFile = SD.open("/spectrum_log.csv");
    if (!logFile) {
        server.send(404, "text/plain", "Log file not found");
        return;
    }

    server.sendHeader("Content-Disposition", "attachment; filename=spectrum_log.csv");
    server.streamFile(logFile, "text/csv");
    logFile.close();
}

const char* getEncryptionType(wifi_auth_mode_t encryptionType) {
    switch (encryptionType) {
        case WIFI_AUTH_OPEN: return "Open";
        case WIFI_AUTH_WEP: return "WEP";
        case WIFI_AUTH_WPA_PSK: return "WPA-PSK";
        case WIFI_AUTH_WPA2_PSK: return "WPA2-PSK";
        case WIFI_AUTH_WPA_WPA2_PSK: return "WPA/WPA2";
        case WIFI_AUTH_WPA2_ENTERPRISE: return "WPA2-Enterprise";
#ifdef WIFI_AUTH_WPA3_PSK
        case WIFI_AUTH_WPA3_PSK: return "WPA3-PSK";
#endif
#ifdef WIFI_AUTH_WPA2_WPA3_PSK
        case WIFI_AUTH_WPA2_WPA3_PSK: return "WPA2/WPA3";
#endif
        default: return "Unknown";
    }
}

819.5.4 Lab Tasks

  1. Deploy the monitor in your environment and let it run for 1 hour
  2. Access the web interface at 192.168.4.1 from your phone or laptop
  3. Download the CSV log and analyze temporal patterns in Excel/Python
  4. Identify the busiest times for network activity
  5. Test channel switching: Change your Wi-Fi router to the recommended channel and measure performance improvement

819.6 Lab: Python RF Coverage Planner

This lab creates a tool to plan IoT network deployments with coverage analysis.

819.6.1 Installation

python3 -m pip install numpy matplotlib

819.6.2 Complete Coverage Planner Code

Save the following as rf_coverage_planner.py and run python3 rf_coverage_planner.py:

import math

import matplotlib.pyplot as plt
import numpy as np


def free_space_path_loss_db(distance_m: np.ndarray, frequency_hz: float) -> np.ndarray:
    distance_m = np.maximum(distance_m, 1e-3)
    wavelength_m = 3e8 / frequency_hz
    return 20 * np.log10(4 * math.pi * distance_m / wavelength_m)


def received_power_dbm(
    tx_power_dbm: float,
    distance_m: np.ndarray,
    frequency_hz: float,
    path_loss_exponent: float = 3.0,
    reference_distance_m: float = 1.0,
    extra_losses_db: float = 0.0,
) -> np.ndarray:
    pl_ref = free_space_path_loss_db(reference_distance_m, frequency_hz)
    pl = pl_ref + 10 * path_loss_exponent * np.log10(np.maximum(distance_m, reference_distance_m) / reference_distance_m)
    return tx_power_dbm - pl - extra_losses_db


def main() -> None:
    frequency_hz = 2.4e9
    width_m, height_m = 50, 30
    sensitivity_dbm = -85

    access_points = [
        {"name": "AP1", "x_m": 10, "y_m": 15, "tx_dbm": 20},
        {"name": "AP2", "x_m": 40, "y_m": 15, "tx_dbm": 20},
    ]

    grid_resolution_m = 0.5
    x = np.arange(0, width_m + grid_resolution_m, grid_resolution_m)
    y = np.arange(0, height_m + grid_resolution_m, grid_resolution_m)
    xx, yy = np.meshgrid(x, y)

    rssi_stack = []
    for ap in access_points:
        distance = np.sqrt((xx - ap["x_m"]) ** 2 + (yy - ap["y_m"]) ** 2)
        rssi = received_power_dbm(ap["tx_dbm"], distance, frequency_hz, path_loss_exponent=3.0, extra_losses_db=8.0)
        rssi_stack.append(rssi)

    best_rssi = np.max(np.stack(rssi_stack, axis=0), axis=0)

    coverage_mask = best_rssi >= sensitivity_dbm
    coverage_percent = 100 * float(np.mean(coverage_mask))
    mean_rssi = float(np.mean(best_rssi))
    min_rssi = float(np.min(best_rssi))
    dead_points = int(np.sum(~coverage_mask))

    def pct(mask: np.ndarray) -> float:
        return 100 * float(np.mean(mask))

    excellent = best_rssi >= -50
    good = (best_rssi < -50) & (best_rssi >= -65)
    fair = (best_rssi < -65) & (best_rssi >= -75)
    poor = (best_rssi < -75) & (best_rssi >= -85)
    dead = best_rssi < -85

    print("=" * 60)
    print("RF NETWORK COVERAGE PLANNER")
    print("=" * 60)
    print(f"\nArea: {width_m}m x {height_m}m")
    print(f"Access Points: {len(access_points)}")
    for ap in access_points:
        print(f"  {ap['name']}: ({ap['x_m']}m, {ap['y_m']}m), {ap['tx_dbm']} dBm")

    print("\nCalculating coverage map...")

    print("\n" + "=" * 60)
    print("COVERAGE ANALYSIS")
    print("=" * 60)
    print(f"Sensitivity threshold: {sensitivity_dbm:.0f} dBm")
    print(f"Overall coverage: {coverage_percent:.1f}%")
    print(f"Mean RSSI: {mean_rssi:.1f} dBm")
    print(f"Minimum RSSI: {min_rssi:.1f} dBm")
    print("\nSignal Quality Distribution:")
    print(f"  Excellent (β‰₯-50 dBm): {pct(excellent):.1f}%")
    print(f"  Good (-50 to -65 dBm): {pct(good):.1f}%")
    print(f"  Fair (-65 to -75 dBm): {pct(fair):.1f}%")
    print(f"  Poor (-75 to -85 dBm): {pct(poor):.1f}%")
    print(f"  Dead Zone (<-85 dBm): {pct(dead):.1f}%")
    print(f"\nDead zone locations: {dead_points} points")

    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5), constrained_layout=True)

    im = ax1.imshow(best_rssi, origin="lower", extent=[0, width_m, 0, height_m], cmap="viridis")
    ax1.set_title("Best RSSI (dBm)")
    ax1.set_xlabel("x (m)")
    ax1.set_ylabel("y (m)")
    for ap in access_points:
        ax1.scatter(ap["x_m"], ap["y_m"], c="red", s=60, marker="x")
        ax1.text(ap["x_m"] + 0.5, ap["y_m"] + 0.5, ap["name"], color="white", fontsize=9)
    fig.colorbar(im, ax=ax1, shrink=0.85)

    categories = np.zeros_like(best_rssi, dtype=int)
    categories[good] = 1
    categories[fair] = 2
    categories[poor] = 3
    categories[dead] = 4
    cmap = plt.cm.get_cmap("Set1", 5)
    ax2.imshow(categories, origin="lower", extent=[0, width_m, 0, height_m], cmap=cmap, vmin=0, vmax=4)
    ax2.set_title("Coverage Zones")
    ax2.set_xlabel("x (m)")
    ax2.set_ylabel("y (m)")

    fig.suptitle("RF Coverage Planner (Log-Distance Model)", fontsize=12)
    out_path = "coverage_map.png"
    print("\nGenerating visualization...")
    fig.savefig(out_path, dpi=150)
    print(f"\nVisualization saved as '{out_path}'")

    print("\nRecommendations:")
    if coverage_percent < 95:
        print("  - Coverage is below 95%. Consider adding more access points.")
    else:
        print("  - Coverage meets or exceeds 95%. Validate with a site survey.")

    if dead_points > 0:
        print("  - Focus on dead zone areas identified in the visualization.")


if __name__ == "__main__":
    main()

819.6.3 Expected Output

============================================================
RF NETWORK COVERAGE PLANNER
============================================================

Area: 50m x 30m
Access Points: 2
  AP1: (10m, 15m), 20 dBm
  AP2: (40m, 15m), 20 dBm

Calculating coverage map...

============================================================
COVERAGE ANALYSIS
============================================================
Sensitivity threshold: -85 dBm
Overall coverage: 94.3%
Mean RSSI: -64.2 dBm
Minimum RSSI: -97.5 dBm

Signal Quality Distribution:
  Excellent (β‰₯-50 dBm): 15.2%
  Good (-50 to -65 dBm): 42.8%
  Fair (-65 to -75 dBm): 28.1%
  Poor (-75 to -85 dBm): 8.2%
  Dead Zone (<-85 dBm): 5.7%

Dead zone locations: 342 points

Generating visualization...

Visualization saved as 'coverage_map.png'

Recommendations:
  - Coverage is below 95%. Consider adding more access points.
  - Focus on dead zone areas identified in the visualization.

The visualization shows:

  1. Left plot: Continuous RSSI heatmap with AP locations marked
  2. Right plot: Discrete coverage quality zones (color-coded from dead zone to excellent)

819.7 Knowledge Check: Coverage Planning

Question: In the provided RF coverage planner, which parameter is intended to represent additional losses (e.g., walls, clutter) beyond the log-distance model?

Explanation: extra_losses_db is subtracted from the received power estimate to model additional attenuation that isn’t captured by the distance/path-loss exponent alone.

Question: A link budget calculation shows +43 dB margin for a LoRa link and +24.6 dB for a Wi-Fi link. Both require 10 dB fade margin. Which statement is correct?

Explanation: Both link margins (43 dB and 24.6 dB) exceed the 10 dB fade margin requirement. LoRa has 33 dB extra margin; Wi-Fi has 14.6 dB extra margin. Both are viable.

Question: The coverage planner shows 94.3% coverage with 5.7% dead zones. What is the most effective improvement?

Explanation: Dead zones indicate areas where no AP provides adequate signal. Adding a third AP strategically placed near these zones directly addresses the coverage gap. Increasing TX power helps but may not eliminate corner dead zones. 5 GHz has higher path loss. Grid resolution affects simulation accuracy, not actual coverage.

819.9 Summary

This chapter provided comprehensive tools for RF coverage planning and analysis:

  • Wave property calculations enable you to quickly compare frequencies for wavelength, path loss, and range
  • Link budget analysis determines if a deployment is viable before you install hardware
  • Spectrum monitoring captures temporal patterns to identify time-varying interference
  • Coverage planning visualizes RSSI heatmaps and dead zones to guide AP placement
  • Python tools provide reusable code for deployment planning and validation

819.10 What’s Next

With coverage planning tools mastered, continue your wireless IoT journey with: