790 RF Path Loss Calculator
Interactive Propagation Model Calculator with Link Budget Analysis
790.1 RF Path Loss Calculator
This interactive tool calculates path loss using various propagation models and helps you design reliable wireless links for IoT deployments. Compare models, calculate link budgets, and visualize coverage.
This comprehensive path loss calculator provides:
- Multiple Propagation Models: FSPL, Log-distance, Okumura-Hata, COST 231, ITU Indoor
- Link Budget Analysis: Complete TX to RX power calculations
- Frequency Band Presets: Common IoT frequencies (433 MHz, 868/915 MHz, 2.4 GHz, 5 GHz)
- Coverage Visualization: Path loss vs distance graphs
- Model Comparison: Overlay multiple models on one chart
- Fade Margin: Account for environmental variations
- Select a propagation model appropriate for your environment
- Set frequency using presets or custom value
- Adjust distance to your deployment scenario
- Enter transmit power and antenna gains
- View calculated path loss and received power
- Enable comparison mode to overlay multiple models
- Use the link budget table for complete analysis
Show code
// ============================================
// RF Path Loss Calculator
// Self-contained OJS implementation
// ============================================
{
// IEEE Color palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
blue: "#3498DB",
darkGray: "#34495E"
};
// Propagation model definitions
const models = {
fspl: {
name: "Free Space Path Loss (FSPL)",
description: "Ideal line-of-sight propagation in free space",
color: colors.teal,
environments: ["Line-of-sight", "Satellite links", "Open areas"],
calculate: (f, d) => {
// FSPL = 20*log10(d) + 20*log10(f) + 20*log10(4*pi/c)
// Simplified: FSPL(dB) = 20*log10(d_m) + 20*log10(f_MHz) - 27.55
if (d <= 0) return 0;
return 20 * Math.log10(d) + 20 * Math.log10(f) - 27.55;
}
},
logDistance: {
name: "Log-Distance Path Loss",
description: "Generic model with configurable path loss exponent",
color: colors.blue,
environments: ["General indoor/outdoor", "Adjustable for any environment"],
calculate: (f, d, params = {}) => {
const n = params.pathLossExponent || 3.0;
const d0 = params.referenceDistance || 1;
// PL(d) = PL(d0) + 10*n*log10(d/d0)
// PL(d0) uses FSPL at reference distance
if (d <= 0) return 0;
const pl_d0 = 20 * Math.log10(d0) + 20 * Math.log10(f) - 27.55;
return pl_d0 + 10 * n * Math.log10(d / d0);
}
},
okumuraHataUrban: {
name: "Okumura-Hata (Urban)",
description: "Empirical model for urban macro cells (150-1500 MHz)",
color: colors.orange,
environments: ["Urban areas", "Cities with tall buildings"],
calculate: (f, d, params = {}) => {
// Valid: 150-1500 MHz, 1-20 km, hb: 30-200m, hm: 1-10m
const hb = params.baseHeight || 30; // Base station height (m)
const hm = params.mobileHeight || 1.5; // Mobile height (m)
if (d < 0.001) return 0; // Minimum 1m
const d_km = d / 1000;
if (d_km < 0.001) return 0;
// Correction factor for mobile antenna height (urban)
const aHm = (1.1 * Math.log10(f) - 0.7) * hm - (1.56 * Math.log10(f) - 0.8);
// Path loss formula
const pl = 69.55 + 26.16 * Math.log10(f) - 13.82 * Math.log10(hb) - aHm +
(44.9 - 6.55 * Math.log10(hb)) * Math.log10(d_km);
return Math.max(0, pl);
}
},
okumuraHataSuburban: {
name: "Okumura-Hata (Suburban)",
description: "Empirical model for suburban areas",
color: colors.purple,
environments: ["Suburban areas", "Residential neighborhoods"],
calculate: (f, d, params = {}) => {
const hb = params.baseHeight || 30;
const hm = params.mobileHeight || 1.5;
if (d < 0.001) return 0;
const d_km = d / 1000;
if (d_km < 0.001) return 0;
const aHm = (1.1 * Math.log10(f) - 0.7) * hm - (1.56 * Math.log10(f) - 0.8);
const plUrban = 69.55 + 26.16 * Math.log10(f) - 13.82 * Math.log10(hb) - aHm +
(44.9 - 6.55 * Math.log10(hb)) * Math.log10(d_km);
// Suburban correction
const correction = 2 * Math.pow(Math.log10(f / 28), 2) + 5.4;
return Math.max(0, plUrban - correction);
}
},
okumuraHataRural: {
name: "Okumura-Hata (Rural)",
description: "Empirical model for open rural areas",
color: colors.green,
environments: ["Rural areas", "Farmland", "Open terrain"],
calculate: (f, d, params = {}) => {
const hb = params.baseHeight || 30;
const hm = params.mobileHeight || 1.5;
if (d < 0.001) return 0;
const d_km = d / 1000;
if (d_km < 0.001) return 0;
const aHm = (1.1 * Math.log10(f) - 0.7) * hm - (1.56 * Math.log10(f) - 0.8);
const plUrban = 69.55 + 26.16 * Math.log10(f) - 13.82 * Math.log10(hb) - aHm +
(44.9 - 6.55 * Math.log10(hb)) * Math.log10(d_km);
// Rural correction (open area)
const correction = 4.78 * Math.pow(Math.log10(f), 2) - 18.33 * Math.log10(f) + 40.94;
return Math.max(0, plUrban - correction);
}
},
cost231: {
name: "COST 231 Hata",
description: "Extended Hata model for 1500-2000 MHz",
color: colors.red,
environments: ["Urban/suburban at higher frequencies", "1800 MHz cellular"],
calculate: (f, d, params = {}) => {
const hb = params.baseHeight || 30;
const hm = params.mobileHeight || 1.5;
const isUrban = params.isUrban !== false;
if (d < 0.001) return 0;
const d_km = d / 1000;
if (d_km < 0.001) return 0;
const aHm = (1.1 * Math.log10(f) - 0.7) * hm - (1.56 * Math.log10(f) - 0.8);
const Cm = isUrban ? 3 : 0; // Urban correction
const pl = 46.3 + 33.9 * Math.log10(f) - 13.82 * Math.log10(hb) - aHm +
(44.9 - 6.55 * Math.log10(hb)) * Math.log10(d_km) + Cm;
return Math.max(0, pl);
}
},
ituIndoor: {
name: "ITU Indoor",
description: "ITU-R P.1238 model for indoor propagation",
color: colors.yellow,
environments: ["Office buildings", "Residential", "Commercial indoor"],
calculate: (f, d, params = {}) => {
// ITU-R P.1238: L = 20*log10(f) + N*log10(d) + Lf(n) - 28
// N: distance power loss coefficient
// Lf(n): floor penetration loss
const N = params.distanceCoeff || 30; // Office: 30, Residential: 28
const numFloors = params.numFloors || 0;
const floorLoss = params.floorLoss || 15; // ~15 dB per floor
if (d <= 0) return 0;
const pl = 20 * Math.log10(f) + N * Math.log10(d) + numFloors * floorLoss - 28;
return Math.max(0, pl);
}
}
};
// Frequency presets
const frequencyPresets = {
"433 MHz (ISM)": { freq: 433, desc: "LoRa, Sigfox (some regions)" },
"868 MHz (EU ISM)": { freq: 868, desc: "LoRaWAN EU, Sigfox EU" },
"915 MHz (US ISM)": { freq: 915, desc: "LoRaWAN US, Sigfox US" },
"2400 MHz (2.4 GHz)": { freq: 2400, desc: "Wi-Fi, BLE, Zigbee, Thread" },
"5000 MHz (5 GHz)": { freq: 5000, desc: "Wi-Fi 5/6, 802.11ac/ax" },
"5800 MHz (5.8 GHz)": { freq: 5800, desc: "Wi-Fi, ISM" }
};
// Layout
const width = 1000, height = 1500;
// State
let selectedModel = "fspl";
let frequency = 868; // MHz
let distance = 1000; // meters
let txPower = 14; // dBm
let txGain = 2; // dBi
let rxGain = 0; // dBi
let fadeMargin = 10; // dB
let rxSensitivity = -120; // dBm
let pathLossExponent = 3.0;
let baseHeight = 30;
let mobileHeight = 1.5;
let comparisonMode = false;
let selectedModelsForComparison = ["fspl", "logDistance", "okumuraHataUrban"];
// Create container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1000px")
.style("margin", "0 auto");
// Header
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, #1a252f 100%)`)
.style("border-radius", "12px 12px 0 0")
.style("padding", "20px")
.style("color", colors.white);
header.append("h2")
.style("margin", "0 0 10px 0")
.style("font-size", "24px")
.text("RF Path Loss Calculator");
header.append("p")
.style("margin", "0")
.style("opacity", "0.9")
.style("font-size", "14px")
.text("Calculate wireless signal attenuation using industry-standard propagation models");
// Controls panel
const controlsPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "20px")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "20px");
// Model selector
const modelControl = controlsPanel.append("div");
modelControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Propagation Model:");
const modelSelect = modelControl.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `2px solid ${colors.navy}`)
.style("font-size", "14px")
.style("cursor", "pointer");
Object.entries(models).forEach(([key, model]) => {
modelSelect.append("option")
.attr("value", key)
.attr("selected", key === selectedModel ? true : null)
.text(model.name);
});
// Frequency control
const freqControl = controlsPanel.append("div");
freqControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Frequency (MHz):");
const freqPresetSelect = freqControl.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "13px")
.style("margin-bottom", "8px")
.style("cursor", "pointer");
freqPresetSelect.append("option").attr("value", "custom").text("Custom frequency...");
Object.entries(frequencyPresets).forEach(([name, data]) => {
freqPresetSelect.append("option")
.attr("value", data.freq)
.attr("selected", data.freq === frequency ? true : null)
.text(name);
});
const freqInput = freqControl.append("input")
.attr("type", "number")
.attr("min", 100)
.attr("max", 10000)
.attr("value", frequency)
.style("width", "calc(100% - 22px)")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px");
// Distance control
const distControl = controlsPanel.append("div");
distControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Distance (m):");
const distSlider = distControl.append("input")
.attr("type", "range")
.attr("min", 1)
.attr("max", 10000)
.attr("value", distance)
.style("width", "100%")
.style("cursor", "pointer");
const distDisplay = distControl.append("div")
.style("text-align", "center")
.style("font-size", "18px")
.style("font-weight", "bold")
.style("color", colors.teal)
.text(`${distance} m`);
// TX Power control
const txControl = controlsPanel.append("div");
txControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("TX Power (dBm):");
const txSlider = txControl.append("input")
.attr("type", "range")
.attr("min", -10)
.attr("max", 30)
.attr("value", txPower)
.style("width", "100%")
.style("cursor", "pointer");
const txDisplay = txControl.append("div")
.style("text-align", "center")
.style("font-size", "18px")
.style("font-weight", "bold")
.style("color", colors.orange)
.text(`${txPower} dBm`);
// Antenna gains
const gainControl = controlsPanel.append("div");
gainControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Antenna Gains (dBi):");
const gainRow = gainControl.append("div")
.style("display", "flex")
.style("gap", "10px");
gainRow.append("div")
.style("flex", "1")
.html(`<label style="font-size:12px;color:${colors.gray}">TX Gain:</label>`)
.append("input")
.attr("type", "number")
.attr("min", -5)
.attr("max", 20)
.attr("value", txGain)
.attr("id", "txGainInput")
.style("width", "calc(100% - 20px)")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`);
gainRow.append("div")
.style("flex", "1")
.html(`<label style="font-size:12px;color:${colors.gray}">RX Gain:</label>`)
.append("input")
.attr("type", "number")
.attr("min", -5)
.attr("max", 20)
.attr("value", rxGain)
.attr("id", "rxGainInput")
.style("width", "calc(100% - 20px)")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`);
// Fade margin
const fadeControl = controlsPanel.append("div");
fadeControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("Fade Margin (dB):");
const fadeSlider = fadeControl.append("input")
.attr("type", "range")
.attr("min", 0)
.attr("max", 30)
.attr("value", fadeMargin)
.style("width", "100%")
.style("cursor", "pointer");
const fadeDisplay = fadeControl.append("div")
.style("text-align", "center")
.style("font-size", "18px")
.style("font-weight", "bold")
.style("color", colors.purple)
.text(`${fadeMargin} dB`);
// RX Sensitivity
const sensControl = controlsPanel.append("div");
sensControl.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.text("RX Sensitivity (dBm):");
const sensInput = sensControl.append("input")
.attr("type", "number")
.attr("min", -150)
.attr("max", -50)
.attr("value", rxSensitivity)
.style("width", "calc(100% - 22px)")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px");
// Model-specific parameters
const modelParamsPanel = container.append("div")
.style("background", colors.white)
.style("padding", "15px 20px")
.style("border-bottom", `1px solid ${colors.lightGray}`)
.attr("class", "model-params");
// Main content
const mainContent = container.append("div")
.style("background", colors.white)
.style("padding", "20px");
// Create SVG
const svg = mainContent.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("width", "100%")
.style("display", "block");
// Results panel
const resultsGroup = svg.append("g")
.attr("transform", "translate(50, 30)");
resultsGroup.append("text")
.attr("x", 430)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("font-size", "20px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Calculation Results");
// Result boxes
const resultBoxes = [
{ id: "pathLoss", label: "Path Loss", unit: "dB", color: colors.red, x: 0 },
{ id: "rxPower", label: "Received Power", unit: "dBm", color: colors.teal, x: 220 },
{ id: "linkMargin", label: "Link Margin", unit: "dB", color: colors.green, x: 440 },
{ id: "maxRange", label: "Max Range", unit: "m", color: colors.orange, x: 660 }
];
resultBoxes.forEach(box => {
const g = resultsGroup.append("g")
.attr("transform", `translate(${box.x}, 30)`);
g.append("rect")
.attr("width", 200)
.attr("height", 90)
.attr("fill", colors.white)
.attr("stroke", box.color)
.attr("stroke-width", 3)
.attr("rx", 10);
g.append("text")
.attr("x", 100)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", colors.gray)
.text(box.label);
g.append("text")
.attr("class", `result-${box.id}`)
.attr("x", 100)
.attr("y", 60)
.attr("text-anchor", "middle")
.attr("font-size", "28px")
.attr("font-weight", "bold")
.attr("fill", box.color);
g.append("text")
.attr("x", 100)
.attr("y", 80)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", colors.gray)
.text(box.unit);
});
// Link status indicator
const statusGroup = resultsGroup.append("g")
.attr("transform", "translate(350, 140)");
statusGroup.append("rect")
.attr("class", "status-bg")
.attr("x", -80)
.attr("y", -20)
.attr("width", 160)
.attr("height", 40)
.attr("rx", 20);
statusGroup.append("text")
.attr("class", "status-text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.attr("fill", colors.white);
// Path Loss vs Distance Chart
const chartGroup = svg.append("g")
.attr("transform", "translate(80, 220)");
chartGroup.append("text")
.attr("x", 400)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Path Loss vs Distance");
const chartWidth = 800;
const chartHeight = 350;
const chartMargin = { top: 30, right: 30, bottom: 50, left: 60 };
// Chart background
chartGroup.append("rect")
.attr("x", chartMargin.left)
.attr("y", chartMargin.top)
.attr("width", chartWidth - chartMargin.left - chartMargin.right)
.attr("height", chartHeight - chartMargin.top - chartMargin.bottom)
.attr("fill", "#f8f9fa")
.attr("stroke", colors.lightGray);
// Chart axes groups
const xAxisGroup = chartGroup.append("g")
.attr("transform", `translate(0, ${chartHeight - chartMargin.bottom})`);
const yAxisGroup = chartGroup.append("g")
.attr("transform", `translate(${chartMargin.left}, 0)`);
// Axis labels
chartGroup.append("text")
.attr("x", chartWidth / 2)
.attr("y", chartHeight - 5)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", colors.navy)
.text("Distance (m)");
chartGroup.append("text")
.attr("x", -chartHeight / 2 + chartMargin.top)
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", colors.navy)
.attr("transform", "rotate(-90)")
.text("Path Loss (dB)");
// Chart path lines container
const chartLines = chartGroup.append("g").attr("class", "chart-lines");
// Current distance marker
const distMarker = chartGroup.append("g").attr("class", "dist-marker");
// Comparison mode toggle
const comparisonToggle = svg.append("g")
.attr("transform", "translate(750, 600)");
const toggleBg = comparisonToggle.append("rect")
.attr("width", 180)
.attr("height", 35)
.attr("fill", colors.lightGray)
.attr("stroke", colors.navy)
.attr("stroke-width", 2)
.attr("rx", 17)
.style("cursor", "pointer");
const toggleText = comparisonToggle.append("text")
.attr("x", 90)
.attr("y", 23)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.style("cursor", "pointer")
.text("Enable Comparison");
comparisonToggle.on("click", () => {
comparisonMode = !comparisonMode;
toggleBg.attr("fill", comparisonMode ? colors.teal : colors.lightGray);
toggleText.text(comparisonMode ? "Comparison ON" : "Enable Comparison")
.attr("fill", comparisonMode ? colors.white : colors.navy);
updateCalculations();
});
// Link Budget Table
const budgetGroup = svg.append("g")
.attr("transform", "translate(50, 650)");
budgetGroup.append("text")
.attr("x", 430)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Link Budget Analysis");
const budgetTable = budgetGroup.append("g")
.attr("transform", "translate(100, 30)")
.attr("class", "budget-table");
// Model information panel
const modelInfoGroup = svg.append("g")
.attr("transform", "translate(50, 950)");
modelInfoGroup.append("text")
.attr("x", 430)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Propagation Model Reference");
const modelInfoContainer = modelInfoGroup.append("g")
.attr("transform", "translate(0, 30)")
.attr("class", "model-info");
// Coverage radius calculator
const coverageGroup = svg.append("g")
.attr("transform", "translate(50, 1200)");
coverageGroup.append("text")
.attr("x", 430)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Coverage Area Visualization");
const coverageCanvas = coverageGroup.append("g")
.attr("transform", "translate(200, 50)")
.attr("class", "coverage-viz");
// Update model-specific parameters UI
function updateModelParams() {
modelParamsPanel.selectAll("*").remove();
const model = models[selectedModel];
modelParamsPanel.append("span")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-right", "20px")
.text(`${model.name} Parameters:`);
if (selectedModel === "logDistance") {
const paramDiv = modelParamsPanel.append("div")
.style("display", "inline-flex")
.style("gap", "20px")
.style("align-items", "center");
paramDiv.append("label")
.style("font-size", "13px")
.style("color", colors.gray)
.text("Path Loss Exponent (n):");
paramDiv.append("input")
.attr("type", "number")
.attr("min", 1.5)
.attr("max", 6)
.attr("step", 0.1)
.attr("value", pathLossExponent)
.style("width", "70px")
.style("padding", "5px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.on("input", function() {
pathLossExponent = parseFloat(this.value) || 3.0;
updateCalculations();
});
paramDiv.append("span")
.style("font-size", "11px")
.style("color", colors.gray)
.text("(Free space: 2, Urban: 3-4, Indoor: 4-6)");
}
if (selectedModel.includes("okumura") || selectedModel === "cost231") {
const paramDiv = modelParamsPanel.append("div")
.style("display", "inline-flex")
.style("gap", "20px")
.style("align-items", "center");
paramDiv.append("label")
.style("font-size", "13px")
.style("color", colors.gray)
.text("Base Station Height (m):");
paramDiv.append("input")
.attr("type", "number")
.attr("min", 10)
.attr("max", 200)
.attr("value", baseHeight)
.style("width", "60px")
.style("padding", "5px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.on("input", function() {
baseHeight = parseFloat(this.value) || 30;
updateCalculations();
});
paramDiv.append("label")
.style("font-size", "13px")
.style("color", colors.gray)
.text("Mobile Height (m):");
paramDiv.append("input")
.attr("type", "number")
.attr("min", 0.5)
.attr("max", 10)
.attr("step", 0.5)
.attr("value", mobileHeight)
.style("width", "60px")
.style("padding", "5px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.on("input", function() {
mobileHeight = parseFloat(this.value) || 1.5;
updateCalculations();
});
}
if (selectedModel === "fspl") {
modelParamsPanel.append("span")
.style("font-size", "13px")
.style("color", colors.gray)
.style("font-style", "italic")
.text("FSPL has no adjustable parameters - assumes ideal free space propagation");
}
}
// Calculate path loss for given model
function calculatePathLoss(modelKey, freq, dist) {
const model = models[modelKey];
const params = {
pathLossExponent: pathLossExponent,
baseHeight: baseHeight,
mobileHeight: mobileHeight
};
return model.calculate(freq, dist, params);
}
// Find maximum range for given constraints
function findMaxRange(modelKey, freq, txPwr, txG, rxG, rxSens, fadeMarg) {
const eirp = txPwr + txG;
const maxPathLoss = eirp + rxG - rxSens - fadeMarg;
// Binary search for max range
let low = 1, high = 100000, mid;
for (let i = 0; i < 50; i++) {
mid = (low + high) / 2;
const pl = calculatePathLoss(modelKey, freq, mid);
if (pl < maxPathLoss) {
low = mid;
} else {
high = mid;
}
}
return Math.round(mid);
}
// Update all calculations
function updateCalculations() {
// Calculate current values
const pathLoss = calculatePathLoss(selectedModel, frequency, distance);
const eirp = txPower + txGain;
const rxPower = eirp - pathLoss + rxGain;
const linkMargin = rxPower - rxSensitivity - fadeMargin;
const maxRange = findMaxRange(selectedModel, frequency, txPower, txGain, rxGain, rxSensitivity, fadeMargin);
// Update result displays
resultsGroup.select(".result-pathLoss").text(pathLoss.toFixed(1));
resultsGroup.select(".result-rxPower").text(rxPower.toFixed(1));
resultsGroup.select(".result-linkMargin").text(linkMargin.toFixed(1));
resultsGroup.select(".result-maxRange").text(maxRange >= 1000 ? `${(maxRange/1000).toFixed(1)}k` : maxRange);
// Update link status
const linkOK = linkMargin >= 0;
statusGroup.select(".status-bg")
.attr("fill", linkOK ? colors.green : colors.red);
statusGroup.select(".status-text")
.text(linkOK ? "LINK OK" : "LINK FAIL");
// Update chart
updateChart();
// Update link budget table
updateBudgetTable(pathLoss, eirp, rxPower, linkMargin);
// Update model info
updateModelInfo();
// Update coverage visualization
updateCoverage(maxRange);
}
// Update the path loss chart
function updateChart() {
chartLines.selectAll("*").remove();
distMarker.selectAll("*").remove();
const maxDist = Math.max(distance * 2, 5000);
const xScale = d3.scaleLog()
.domain([1, maxDist])
.range([chartMargin.left, chartWidth - chartMargin.right]);
// Calculate y domain based on data
const pathLosses = [];
for (let d = 1; d <= maxDist; d *= 1.2) {
if (comparisonMode) {
selectedModelsForComparison.forEach(m => {
pathLosses.push(calculatePathLoss(m, frequency, d));
});
} else {
pathLosses.push(calculatePathLoss(selectedModel, frequency, d));
}
}
const maxPL = Math.max(...pathLosses.filter(p => isFinite(p) && p > 0)) * 1.1;
const minPL = Math.min(...pathLosses.filter(p => isFinite(p) && p > 0)) * 0.9;
const yScale = d3.scaleLinear()
.domain([Math.max(0, minPL - 10), maxPL + 10])
.range([chartHeight - chartMargin.bottom, chartMargin.top]);
// Update axes
const xAxis = d3.axisBottom(xScale)
.tickValues([1, 10, 100, 1000, 10000].filter(v => v <= maxDist))
.tickFormat(d => d >= 1000 ? `${d/1000}k` : d);
const yAxis = d3.axisLeft(yScale).ticks(8);
xAxisGroup.call(xAxis);
yAxisGroup.call(yAxis);
// Draw sensitivity line
const sensWithMargin = Math.abs(rxSensitivity) + fadeMargin - txPower - txGain - rxGain;
if (sensWithMargin >= yScale.domain()[0] && sensWithMargin <= yScale.domain()[1]) {
chartLines.append("line")
.attr("x1", chartMargin.left)
.attr("y1", yScale(sensWithMargin))
.attr("x2", chartWidth - chartMargin.right)
.attr("y2", yScale(sensWithMargin))
.attr("stroke", colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "8,4");
chartLines.append("text")
.attr("x", chartWidth - chartMargin.right - 5)
.attr("y", yScale(sensWithMargin) - 8)
.attr("text-anchor", "end")
.attr("font-size", "11px")
.attr("fill", colors.red)
.text("Link Limit");
}
// Generate line data
const lineGenerator = d3.line()
.x(d => xScale(d.distance))
.y(d => yScale(d.pathLoss))
.defined(d => isFinite(d.pathLoss) && d.pathLoss > 0);
const modelsToPlot = comparisonMode ? selectedModelsForComparison : [selectedModel];
modelsToPlot.forEach(modelKey => {
const model = models[modelKey];
const data = [];
for (let d = 1; d <= maxDist; d *= 1.1) {
const pl = calculatePathLoss(modelKey, frequency, d);
if (isFinite(pl) && pl > 0) {
data.push({ distance: d, pathLoss: pl });
}
}
chartLines.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", model.color)
.attr("stroke-width", 3)
.attr("d", lineGenerator);
// Add label
if (data.length > 0) {
const labelPoint = data[Math.floor(data.length * 0.7)];
chartLines.append("text")
.attr("x", xScale(labelPoint.distance) + 5)
.attr("y", yScale(labelPoint.pathLoss) - 10)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", model.color)
.text(modelKey === selectedModel ? model.name.split(" ")[0] : modelKey);
}
});
// Draw current distance marker
const currentPL = calculatePathLoss(selectedModel, frequency, distance);
if (isFinite(currentPL) && currentPL > 0) {
distMarker.append("line")
.attr("x1", xScale(distance))
.attr("y1", chartMargin.top)
.attr("x2", xScale(distance))
.attr("y2", chartHeight - chartMargin.bottom)
.attr("stroke", colors.navy)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "4,4");
distMarker.append("circle")
.attr("cx", xScale(distance))
.attr("cy", yScale(currentPL))
.attr("r", 8)
.attr("fill", colors.navy)
.attr("stroke", colors.white)
.attr("stroke-width", 2);
distMarker.append("text")
.attr("x", xScale(distance))
.attr("y", chartMargin.top - 5)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(`${distance}m: ${currentPL.toFixed(1)} dB`);
}
}
// Update link budget table
function updateBudgetTable(pathLoss, eirp, rxPower, linkMargin) {
budgetTable.selectAll("*").remove();
const rows = [
{ label: "Transmit Power", value: `${txPower} dBm`, sign: "", cumulative: txPower },
{ label: "TX Antenna Gain", value: `${txGain} dBi`, sign: "+", cumulative: txPower + txGain },
{ label: "EIRP", value: `${eirp} dBm`, sign: "=", cumulative: eirp, highlight: true },
{ label: "Path Loss", value: `${pathLoss.toFixed(1)} dB`, sign: "-", cumulative: eirp - pathLoss },
{ label: "RX Antenna Gain", value: `${rxGain} dBi`, sign: "+", cumulative: eirp - pathLoss + rxGain },
{ label: "Received Power", value: `${rxPower.toFixed(1)} dBm`, sign: "=", cumulative: rxPower, highlight: true },
{ label: "RX Sensitivity", value: `${rxSensitivity} dBm`, sign: "-", cumulative: rxPower - rxSensitivity },
{ label: "Fade Margin", value: `${fadeMargin} dB`, sign: "-", cumulative: linkMargin },
{ label: "Link Margin", value: `${linkMargin.toFixed(1)} dB`, sign: "=", cumulative: linkMargin, highlight: true, final: true }
];
const colWidth = 180;
const rowHeight = 32;
// Header
["Parameter", "Value", "Sign", "Running Total"].forEach((header, i) => {
budgetTable.append("rect")
.attr("x", i * colWidth)
.attr("y", 0)
.attr("width", colWidth)
.attr("height", rowHeight)
.attr("fill", colors.navy);
budgetTable.append("text")
.attr("x", i * colWidth + colWidth / 2)
.attr("y", rowHeight / 2 + 5)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(header);
});
// Data rows
rows.forEach((row, ri) => {
const y = rowHeight + ri * rowHeight;
const bgColor = row.highlight ? (row.final ? (linkMargin >= 0 ? colors.green : colors.red) : colors.lightGray) : (ri % 2 === 0 ? colors.white : "#f8f9fa");
[row.label, row.value, row.sign, `${row.cumulative.toFixed(1)} dBm`].forEach((cell, ci) => {
budgetTable.append("rect")
.attr("x", ci * colWidth)
.attr("y", y)
.attr("width", colWidth)
.attr("height", rowHeight)
.attr("fill", bgColor)
.attr("stroke", colors.lightGray);
budgetTable.append("text")
.attr("x", ci * colWidth + colWidth / 2)
.attr("y", y + rowHeight / 2 + 5)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", row.highlight ? "bold" : "normal")
.attr("fill", row.final && row.highlight ? colors.white : colors.navy)
.text(cell);
});
});
}
// Update model information
function updateModelInfo() {
modelInfoContainer.selectAll("*").remove();
const modelsToShow = comparisonMode ? selectedModelsForComparison : [selectedModel];
const cardWidth = 280;
const cardHeight = 120;
modelsToShow.forEach((modelKey, i) => {
const model = models[modelKey];
const x = (i % 3) * (cardWidth + 20);
const y = Math.floor(i / 3) * (cardHeight + 15);
const card = modelInfoContainer.append("g")
.attr("transform", `translate(${x}, ${y})`);
card.append("rect")
.attr("width", cardWidth)
.attr("height", cardHeight)
.attr("fill", colors.white)
.attr("stroke", model.color)
.attr("stroke-width", 2)
.attr("rx", 8);
card.append("rect")
.attr("width", cardWidth)
.attr("height", 30)
.attr("fill", model.color)
.attr("rx", "8 8 0 0");
card.append("text")
.attr("x", cardWidth / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(model.name);
card.append("text")
.attr("x", 10)
.attr("y", 50)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(model.description);
card.append("text")
.attr("x", 10)
.attr("y", 70)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Best for:");
model.environments.slice(0, 2).forEach((env, ei) => {
card.append("text")
.attr("x", 15)
.attr("y", 85 + ei * 14)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(`- ${env}`);
});
});
}
// Update coverage visualization
function updateCoverage(maxRange) {
coverageCanvas.selectAll("*").remove();
const maxRadius = 200;
const scale = maxRadius / Math.max(maxRange, 100);
const displayRadius = Math.min(maxRange * scale, maxRadius);
// Coverage circle
coverageCanvas.append("circle")
.attr("r", displayRadius)
.attr("fill", colors.teal)
.attr("fill-opacity", 0.2)
.attr("stroke", colors.teal)
.attr("stroke-width", 3);
// Range rings
[0.25, 0.5, 0.75, 1].forEach(ratio => {
const r = displayRadius * ratio;
coverageCanvas.append("circle")
.attr("r", r)
.attr("fill", "none")
.attr("stroke", colors.lightGray)
.attr("stroke-dasharray", "4,4");
if (ratio < 1) {
coverageCanvas.append("text")
.attr("x", r + 5)
.attr("y", -5)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(`${Math.round(maxRange * ratio)}m`);
}
});
// Center point (transmitter)
coverageCanvas.append("circle")
.attr("r", 10)
.attr("fill", colors.navy);
coverageCanvas.append("text")
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("fill", colors.navy)
.text("TX");
// Coverage area info
const areaKm2 = Math.PI * Math.pow(maxRange / 1000, 2);
coverageCanvas.append("text")
.attr("y", displayRadius + 40)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(`Max Range: ${maxRange >= 1000 ? (maxRange/1000).toFixed(1) + " km" : maxRange + " m"}`);
coverageCanvas.append("text")
.attr("y", displayRadius + 60)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", colors.gray)
.text(`Coverage Area: ${areaKm2.toFixed(2)} km²`);
}
// Event listeners
modelSelect.on("change", function() {
selectedModel = this.value;
updateModelParams();
updateCalculations();
});
freqPresetSelect.on("change", function() {
if (this.value !== "custom") {
frequency = parseInt(this.value);
freqInput.property("value", frequency);
updateCalculations();
}
});
freqInput.on("input", function() {
frequency = parseFloat(this.value) || 868;
freqPresetSelect.property("value", "custom");
updateCalculations();
});
distSlider.on("input", function() {
distance = parseFloat(this.value);
distDisplay.text(distance >= 1000 ? `${(distance/1000).toFixed(1)} km` : `${distance} m`);
updateCalculations();
});
txSlider.on("input", function() {
txPower = parseFloat(this.value);
txDisplay.text(`${txPower} dBm`);
updateCalculations();
});
fadeSlider.on("input", function() {
fadeMargin = parseFloat(this.value);
fadeDisplay.text(`${fadeMargin} dB`);
updateCalculations();
});
sensInput.on("input", function() {
rxSensitivity = parseFloat(this.value) || -120;
updateCalculations();
});
// Handle antenna gain inputs
container.selectAll("#txGainInput").on("input", function() {
txGain = parseFloat(this.value) || 0;
updateCalculations();
});
container.selectAll("#rxGainInput").on("input", function() {
rxGain = parseFloat(this.value) || 0;
updateCalculations();
});
// Initial render
updateModelParams();
updateCalculations();
return container.node();
}Core Concept: In ideal conditions, signal strength decreases by 6 dB every time you double the distance, and by 6 dB every time you double the frequency - this is the absolute minimum loss any wireless link will experience. Why It Matters: FSPL sets the baseline for all wireless system design. At 2.4 GHz and 100 meters, expect at least 80 dB of path loss before accounting for obstacles, walls, or environmental factors. Real-world losses are always higher. Key Takeaway: FSPL formula: 20log(d) + 20log(f) - 27.55 dB (d in meters, f in MHz). Memorize that 868 MHz at 1 km gives roughly 92 dB loss, and 2.4 GHz at 100 m gives roughly 80 dB loss as quick reference points.
790.2 Propagation Model Reference
790.2.1 Free Space Path Loss (FSPL)
The most basic model, assuming ideal line-of-sight:
\[FSPL(dB) = 20 \log_{10}(d) + 20 \log_{10}(f) - 27.55\]
Where: - d = distance in meters - f = frequency in MHz
- Satellite communications
- Open outdoor areas with clear line-of-sight
- As a baseline reference for other models
790.2.2 Log-Distance Model
Generic model with adjustable path loss exponent:
\[PL(d) = PL(d_0) + 10n \log_{10}\left(\frac{d}{d_0}\right)\]
| Environment | Path Loss Exponent (n) |
|---|---|
| Free space | 2.0 |
| Urban cellular | 2.7-3.5 |
| Shadowed urban | 3.0-5.0 |
| Indoor (LOS) | 1.6-1.8 |
| Indoor (obstructed) | 4.0-6.0 |
790.2.3 Okumura-Hata Model
Empirical model for cellular frequencies (150-1500 MHz):
- Urban: Higher path loss due to buildings
- Suburban: Reduced loss with correction factor
- Rural: Lowest loss in open areas
The Okumura-Hata model is only valid for: - Frequency: 150-1500 MHz - Distance: 1-20 km - Base station height: 30-200 m - Mobile height: 1-10 m
790.2.4 COST 231 Hata
Extended Hata model for 1500-2000 MHz:
\[PL = 46.3 + 33.9\log_{10}(f) - 13.82\log_{10}(h_b) + (44.9 - 6.55\log_{10}(h_b))\log_{10}(d) + C_m\]
Where C_m = 0 (suburban) or 3 dB (urban)
790.2.5 ITU Indoor Model
For indoor propagation:
\[PL = 20\log_{10}(f) + N\log_{10}(d) + L_f(n) - 28\]
| Building Type | N (Distance Coefficient) |
|---|---|
| Residential | 28 |
| Office | 30 |
| Commercial | 22 |
790.3 Link Budget Fundamentals
A complete link budget accounts for all gains and losses:
Received Power = TX Power + TX Gain - Path Loss + RX Gain
Link Margin = Received Power - Sensitivity - Fade Margin
- Minimum: 6 dB (95% reliability)
- Recommended: 10-15 dB (99% reliability)
- High reliability: 20+ dB (99.9% reliability)
790.4 Frequency Band Considerations
| Band | Propagation | Range | Penetration | Common Uses |
|---|---|---|---|---|
| 433 MHz | Excellent | Long | Good | LoRa, ISM devices |
| 868/915 MHz | Very good | Long | Good | LoRaWAN, Sigfox |
| 2.4 GHz | Good | Medium | Moderate | Wi-Fi, BLE, Zigbee |
| 5 GHz | Moderate | Short | Poor | Wi-Fi 5/6 |
790.5 What’s Next
- Networking Basics - RF fundamentals
- IoT Protocols Overview - Protocol selection guide
- LoRaWAN Overview - LPWAN deep dive
- Wireless Range Estimator - LoRa-specific tool
- Simulations Hub - More interactive tools
This interactive calculator is implemented in approximately 900 lines of Observable JavaScript. Key features:
- Six propagation models: FSPL, Log-distance, Okumura-Hata (3 variants), COST 231, ITU Indoor
- Real-time calculations: Path loss, received power, link margin, max range
- Interactive chart: Path loss vs distance with model comparison
- Link budget table: Complete TX to RX analysis
- Coverage visualization: Area coverage display
- Frequency presets: Common IoT bands
The calculator uses the IEEE color palette for consistency: - Navy (#2C3E50): Primary UI elements - Teal (#16A085): FSPL, good indicators - Orange (#E67E22): Okumura-Hata Urban - Red (#E74C3C): COST 231, warnings - Green (#27AE60): Okumura-Hata Rural, success