Skip to content

Latest commit

 

History

History
215 lines (143 loc) · 14 KB

File metadata and controls

215 lines (143 loc) · 14 KB

MeshInfo Coverage / Scan RF Model

This is the propagation model behind the Coverage and Best Neighbors (Scan) tools on the map. It predicts how far a given Meshtastic node can reach in a specific terrain — the output is the colored coverage bubble around an origin pin and the per-target reach classification in the scan tool.

Executive summary

For each receiver pixel (or scan target), MeshInfo computes:

RSSI = TX_power + TX_gain + RX_gain
       − path_loss_ITM        ← terrain diffraction (Longley-Rice)
       − clutter_loss         ← buildings + vegetation (ITU-R)
       − cable_loss
margin = RSSI − sensitivity − fade_margin

Pixels with positive margin are reachable; pixels with margin ≥ 15 dB are reliable. The model is terrain-aware (real elevation everywhere) and per-pixel land-cover-aware (real building / forest classification at every sample). It's calibrated to ITU-R recommendations, not invented numbers.

The three components

1. Path loss — ITM / Longley-Rice (ITU-R P.526 family)

The Irregular Terrain Model is the industry standard for predicting how a radio wave bends, diffracts, and scatters over real terrain. MeshInfo runs the canonical ITM v1.4 implementation as WebAssembly per pixel, sampling real elevations from USGS 3DEP / Tilezen along each propagation path.

ITM models terrain diffraction over bare earth and produces a basic transmission loss in dB. It does not model anything above the ground — no buildings, no trees, no rooftops. That's where the next two components come in.

2. Endpoint clutter loss — ITU-R P.452-17 §4.5.4

Each end of the path (TX and RX) has a height-gain correction based on:

  • The antenna's height above local terrain (h)
  • The nominal clutter height at that location (hₐ — e.g. 20 m for evergreen forest, 25 m for dense urban)
  • The nominal distance from antenna to the clutter (dₖ — typically 20–100 m)
  • Frequency (915 MHz for US Meshtastic)

The formula:

A_h = 10.25 · F_fc · exp(−d_k) · {1 − tanh[6 · (h/h_a − 0.625)]} − 0.33   [dB]

At 915 MHz, F_fc ≈ 1. The shape: antenna inside clutter ≈ 19 dB loss; antenna above clutter ≈ 0 dB, smooth transition through h ≈ 0.625 · hₐ.

Worked examples:

Class hₐ dₖ h Aₕ
Dense urban 25 m 0.02 km 2 m (handheld) 19.7 dB
Dense urban 25 m 0.02 km 30 m (tower) ≈ 0 dB
Suburban 9 m 0.025 km 2 m 19.5 dB
Suburban 9 m 0.025 km 10 m ≈ 0 dB
Evergreen forest 20 m 0.05 km 2 m 19.1 dB
Evergreen forest 20 m 0.05 km 25 m ≈ 0 dB
Open water any 0 dB

This explains why a handheld inside trees barely gets a kilometer, but the same node 5 m above the canopy reaches 30+ km — the model captures both regimes correctly.

3. Path-traversed vegetation — ITU-R P.833-9 §4.1 (modified exponential decay)

For path segments that pass through foliage (between the endpoint zones), the model accumulates additional attenuation:

L = A · {1 − exp[−γ · d / A]}   [dB]

Where γ is the per-metre specific attenuation (dB/m) and A is the saturation ceiling (dB) for the class. Properties:

  • Linear in distance for short grazes (10 m of evergreen → ~6 dB)
  • Saturates for long penetration (1000 m of evergreen → ~27 dB, doesn't grow further)

Per-class accumulation: a path crossing 200 m of deciduous forest then 200 m of evergreen forest computes both losses separately and sums them.

Avoiding double-counting

P.452 endpoint clutter handles "antenna immersed in nearby clutter" (within ~50 m). P.833 path-integration handles "ray traveling through canopy beyond that zone." MeshInfo skips the first/last dₖ km of profile from MED accumulation so the same metre of clutter isn't counted twice.

Where the land cover comes from

The per-pixel classification uses USGS NLCD (National Land Cover Database) — public domain, 30 m native resolution, 16 standard classes for CONUS, well-validated by EPA / USGS / MRLC.

Classes the model uses:

ID Class Penetrable?
11 Open Water
12 Perennial Ice/Snow
21 Developed, Open Space
22 Developed, Low Intensity
23 Developed, Medium Intensity
24 Developed, High Intensity
31 Barren Land
41 Deciduous Forest yes
42 Evergreen Forest yes
43 Mixed Forest yes
52 Shrub/Scrub yes
71 Grassland/Herbaceous
81 Pasture/Hay
82 Cultivated Crops yes
90 Woody Wetlands yes
95 Emergent Herbaceous Wetlands yes

The full per-class P.452 (hₐ, dₖ) and P.833 (γ, A) parameters are visible in the class legend popover inside the Coverage and Scan settings panels — click "Show class legend" to see the dB at 2 m AGL for every class.

Outside the United States (or anywhere the NLCD bake doesn't cover), the model falls back to Mixed Forest as a conservative default for every pixel. Future work tracks adding ESA WorldCover as a global fallback.

Per-pixel building heights — JRC GHS-BUILT-H 100 m (optional)

NLCD's developed-intensity classes (21/22/23/24) ship with class-nominal heights of 4 / 9 / 15 / 25 m from P.452 Table 4. These are continental averages — wrong by ±15 m for any specific neighbourhood. They also can't put real diffractors (apartment blocks, industrial silos, isolated office towers) into the ITM propagation profile.

When building tiles are baked from JRC GHS-BUILT-H R2023A ANBH (Average Net Building Height, 100 m global, CC BY 4.0), two integration sites in the model use the measurements:

  1. DSM into the ITM profile. Mid-path samples become terrain + measured_building_height so ITM's diffraction algorithm sees rooftops as actual obstacles. Endpoints (TX and RX) stay bare-earth — your txHeightM / rxHeightM are AGL above local ground, not on top of a presumed building. Operators with rooftop-mounted antennas should add the building's height to their antenna AGL field manually.
  2. Measured h_a in P.452 endpoint clutter. For developed-class pixels (21/22/23/24) at TX and RX, the measured ANBH replaces cls.nominalHeightM in the height-gain formula. A 2 m handheld in a 100 m cell that ANBH says is 4 m gets the same ~19 dB endpoint loss; in a cell that's actually 14 m, the cell-specific value drives the calculation. Non-developed classes (vegetation, water, etc.) keep their class-nominal heights so trees don't get erased when a forest pixel happens to read ANBH = 0.

100 m averaging caveats. ANBH averages over the built portion of each 100 m cell, so a neighbourhood of 30% 9 m houses + 5% 25 m apartment block reads as ~11 m — the apartment block is captured as elevated DSM but its sharp edges get smeared. ITM's above-rooftop diffraction (the dominant path at typical link distances) is well-served by this; ground-level street-canyon waveguide effects need ray-tracing, not raster, regardless of resolution.

The "Building heights" toggle in the Coverage and Scan settings panels lets operators turn the refinement off (compare measured-vs-nominal, or use bare-earth ITM for a baseline). Default is on.

Operator runbook for the building bake: scripts/README-buildings.md.

Per-pixel canopy heights — ETH 10 m (optional)

NLCD tells the model what kind of canopy is at a pixel; class-nominal heights from P.452 Table 4 (15 m deciduous, 20 m evergreen, 15 m mixed, 10 m woody-wetland, 2 m emergent-wetland) tell it how tall that canopy is. Real canopy varies — a 50 m redwood next to a 5 m madrone in the same NLCD "Evergreen Forest" pixel both got coded as 20 m without measurement.

When canopy tiles are baked from ETH Global Canopy Height 2020 (Lang et al. 2023, 10 m global, CC BY 4.0), the P.833 MED loop uses the measured height per sample instead of the class-nominal:

  • The path-integral gate zPath < terrain + canopyHeight evaluates against measured canopy at every profile sample.
  • Where the measured value is 0 (clearings, fire scars, recent logging), MED contributes nothing for that sample — the path is above any canopy.
  • Where the bake doesn't cover a pixel (out-of-bbox / ocean adjacency), the loop falls back to the class-nominal value silently.
  • Endpoint clutter (P.452) still uses class-nominal heights — Tier 3 only refines the path-integrated MED. Endpoint heights are a Tier-2 (per-pixel building heights) refinement.

If the bake includes the optional standard-deviation file (scripts/canopy_tiles.py --include-stddev), pixels with σ ≥ height are blended 50/50 with the class-nominal so a single noisy ETH pixel can't dominate the integral.

The "Canopy heights" toggle in the Coverage and Scan settings panels lets operators turn the refinement off (e.g. to compare measured-vs-nominal predictions, or to validate the older behavior). Default is on.

Operator runbook for the canopy bake: scripts/README-canopy.md.

Setup — running the bake

NLCD data is not bundled with MeshInfo (it's ~2 GB). Operators run a one-shot bake script after deploying:

pip install -r scripts/requirements-landcover.txt
# Download NLCD source from MRLC and extract, then:
python scripts/landcover_tiles.py --source /path/to/nlcd_*.tif --out output/landcover

CONUS at full resolution takes 15–90 min depending on CPU. Tiles are bind-mounted into the meshinfo container automatically, no extra config needed.

Full operator runbook: scripts/README-landcover.md

Once tiles exist, the API mounts them at /tiles/landcover/{z}/{x}/{y}.png and the frontend fetches them on every coverage compute. The "Land cover: USGS NLCD" status chip in the Coverage panel shows whether tiles are healthy or the model is using the fallback class.

For the optional canopy-height refinement, run the canopy bake separately:

docker compose --profile bake run --rm canopy-bake     # CONUS default; --scope global also supported

CONUS canopy bake is ~30 GB of source COGs + ~1–2 GB of output tiles. Time: 1–4 hours depending on the ETH file server and CPU. Without this bake, the model uses class-nominal canopy heights — the coverage tool still works.

For the optional building-height refinement, run the building bake:

docker compose --profile bake run --rm building-bake   # global default; --scope conus also supported

Global building bake is ~1.8 GB of source GeoTIFF + ~1–2 GB of output tiles. Time: 1–3 hours. Without this bake, the model uses class-nominal heights for developed classes and bare-earth ITM — the coverage tool still works.

The aggression scaler

Inside the Coverage and Scan panels, the Clutter control is a 3-stop slider:

Stop Multiplier When to use
Conservative 0.7× Predictions are pessimistic — measured links beat what the model says
Calibrated 1.0× Default. ITU baseline. Use this unless you have measured-link data
Aggressive 1.3× Predictions are optimistic — measured links fall short. Heavier obstruction than published averages

The scaler multiplies the final (A_h_TX + A_h_RX + L_v) clutter-loss sum. ITM path loss and free-space loss are unaffected.

The default (1.0×) reflects ITU-R P.452 / P.833 published values. Don't sit at 0.7× or 1.3× without a reason — the calibrated baseline is the most accurate prediction. The slider exists because:

  • Real-world obstruction varies: a Sierra-foothills canopy is denser than the published "evergreen forest" averages; a Mojave Desert "shrub/scrub" is thinner than the averages
  • Operators with measured links can tune to match observations
  • The same calibration won't be perfect everywhere on Earth

What the model does not do

These are scope limits that affect prediction accuracy in specific scenarios:

  • Indoor receivers (ITU-R P.2109) — RX inside a building gets +10–20 dB additional loss not modeled. Outdoor-to-outdoor only.
  • Per-building height — when the building bake is in place, JRC GHS-BUILT-H 100 m measurements replace class-nominal heights for developed classes and feed buildings into the ITM DSM. The 100 m averaging captures first-order diffraction physics but smooths individual rooftops; per-building precision (a single tall office tower across the street from your antenna) needs vector pipelines beyond this raster model.
  • Seasonal foliage — deciduous classes assume leaf-on (summer) at 0.5 dB/m. Out-of-leaf is ~0.15 dB/m; not modeled. Future work.
  • Frequencies other than 915 MHz — the P.833 specific-attenuation values are calibrated at 915 MHz. Adding 868 MHz (EU) or 433 MHz would need a per-band lookup.
  • Polarization-dependent vegetation loss — uses unpolarized averages (the slight per-polarization difference is in the noise floor here).
  • Atmospheric refractivity — fixed at N=301 (Continental Temperate, NA Meshtastic default). Maritime / arid regions are slightly different.

References

How to validate

The right way to know if the model is accurate for your mesh: pick known links with measured RSSI/SNR and predicted distance, run them through the Best Neighbors / Coverage tools, compare predicted vs. measured. The aggression slider exists for exactly this tuning step.

A formal validation harness (feed N known links → publish RMSE) is on the roadmap but depends on a corpus of validated link data appearing.