Skip to content

Estimated Accuracy ​

When MeshMonitor draws a node at an estimated position, it also draws a dashed accuracy circle around it. This page explains exactly how that circle β€” the node's uncertaintyKm β€” is computed, so you can read it with confidence and know when to trust an estimate.

In one sentence

The estimate is an SNR- and time-weighted centroid of every anchor that heard the node; the circle radius is the weighted spread of those anchors, shrunk by how many effective observations you really have. One anchor β†’ a big "somewhere within radio range" circle; many converging anchors β†’ a small, confident circle.

For what position estimation is, how to turn it on, and the settings that control it, see Position Estimation. This page is the deep-dive on the accuracy math.

The pipeline at a glance ​

 traceroutes ┐                 β”Œβ”€ weight each observation ─┐
             β”‚   observations  β”‚   w = time-decay Γ— SNR     β”‚   weighted
 neighbor ───┼──▢ (anchor pos, β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”œβ”€β–Ά centroid  ─▢ (lat, lon)
   info     β”˜     SNR, time)   β”‚                            β”‚      β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β–Ό
                                                            uncertainty radius
                                                          (spread ÷ √effective-N,
                                                           blended w/ radio range)

Every estimate is a pure function of a node's observations. An observation is "an anchor (a node whose position we already know) heard this node, at this SNR, at this time." Two data sources produce them:

SourceObservation it createsSNR used
Tracerouteeach A β†’ X β†’ B hop anchors the middle node X toward its positioned path-neighborsper-hop SNR (raw Γ· 4 β†’ dB)
NeighborInfoa direct-RF pair anchors the unpositioned side to the positioned sidelink SNR (already dB)

Step 1 β€” Weight each observation ​

Every observation gets a single scalar weight, the product of two independent factors:

weight = time_decay Γ— snr_weight

SNR weight β€” stronger signal pulls harder ​

SNR is converted to linear signal power:

snr_weight = 10 ^ (SNR_dB / 10)

This is the physically correct mapping: a higher (less-negative) SNR means the anchor is more likely to be near the node, so it should pull the estimate toward itself harder. Weak signals barely move the estimate.

 SNR (dB)   snr_weight = 10^(SNR/10)     relative pull
 ───────    ───────────────────────     ─────────────────────────
  +15            31.6                    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  very strong
  +10            10.0                    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ              strong
   +5             3.16                   β–ˆβ–ˆβ–ˆβ–ˆ                      good
    0             1.0                    β–ˆ                         reference
   βˆ’6             0.251                  ▏                         weak
  βˆ’12             0.063                  Β·                         very weak (poor link)
  βˆ’20             0.01                   Β·                         near noise floor

A βˆ’12 dB anchor barely counts

At βˆ’12 dB an anchor carries ~0.06 of the weight of a 0 dB anchor and ~1/160th of a +10 dB anchor. That asymmetry is the heart of the accuracy math below β€” and the reason a single weak anchor must never produce a confident estimate.

Time-decay weight β€” newer observations count more ​

Observations lose half their weight every 24 hours:

time_decay = 0.5 ^ (age_hours / 24)     (exponential, half-life = 24h)
 age        time_decay
 ────       ──────────
 now        1.00
 12 h       0.71
 24 h       0.50
 48 h       0.25
 72 h       0.125
 7 days     0.008

So a fresh βˆ’6 dB report can outweigh a three-day-old +0 dB report. The lookback window sets how far back observations are even considered.

Step 2 β€” Weighted centroid (the position) ​

The estimated location is the weight-weighted average of all anchor positions:

        Ξ£ (wα΅’ Β· latα΅’)              Ξ£ (wα΅’ Β· lonα΅’)
 lat =  ─────────────       lon =  ─────────────
            Ξ£ wα΅’                       Ξ£ wα΅’

For the simplest case β€” a single traceroute segment with two anchors β€” this reduces exactly to the classic SNR-weighted midpoint. With many anchors heard from many directions, the centroid converges on the node's true location:

   Anchor B (SNR +8, wβ‰ˆ6)
        ●
         \                      β˜… = weighted centroid (the estimate)
          \                     ●  pulls toward the strong/near anchors,
     β˜…     \                       barely toward the weak/far ones
   ●────────●  Anchor A (SNR 0, w=1)
   β”‚
   ● Anchor C (SNR βˆ’10, wβ‰ˆ0.1)   ← far + weak β‡’ almost no pull

Step 3 β€” The accuracy radius (uncertaintyKm) ​

This is the circle you see. It is built from three ingredients.

3a. Weighted RMS spread ​

How far the anchors sit from the centroid, weighted:

            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 rms_km  =  β”‚  Ξ£ (wα΅’ Β· distα΅’Β²)  /  Ξ£ wα΅’
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   (square root of the weighted mean
                                            squared distance to the centroid)

3b. Effective sample size (Kish) ​

Counting anchors naively is misleading when one anchor dominates the weights, so we use the Kish effective sample size:

          (Ξ£ wα΅’)Β²
 n_eff =  ────────
           Ξ£ wα΅’Β²

n_eff answers "how many balanced observations is this really worth?"

  • Two equally-weighted anchors β†’ n_eff = 2.
  • One anchor at weight 10 plus one at weight 0.06 β†’ n_eff β‰ˆ 1.01 β€” i.e. effectively a single observation, even though two anchors exist.

3c. Confidence blend ​

A raw "spread ÷ √n_eff" statistic looks dangerously confident when n_eff is barely above 1 (the dominated case above). So the final radius blends a conservative radio-range default toward the statistical estimate, using a confidence factor derived from n_eff:

 statistical_km = rms_km / √n_eff          (clamped to β‰₯ 0.05 km)
 confidence     = clamp(n_eff βˆ’ 1, 0, 1)   (0 at n_eff=1 … 1 at n_effβ‰₯2)

 uncertainty_km = 5 km Γ— (1 βˆ’ confidence)  +  statistical_km Γ— confidence
 n_eff   confidence   what the circle reflects
 ─────   ──────────   ────────────────────────────────────────────
  1.0       0.00      pure 5 km radio-range default (single / dominated)
  1.25      0.25      mostly default, a little statistics
  1.5       0.50      half-and-half
  1.75      0.75      mostly statistics
 β‰₯2.0       1.00      pure statistical radius (balanced multi-anchor)
  • A lone anchor can only say "within radio range," so it gets the full ~5 km default.
  • A balanced multi-anchor solve (n_eff β‰₯ 2) trusts the geometry fully and can report a sub-kilometre radius.
  • Anything in between is blended, so a near-single solve can never sneak out a falsely tight circle.
Why the blend exists (issue #3616)

Before this refinement, the radius was just rms_km / √n_eff. With one strong anchor and one weak/far anchor, the centroid collapses onto the strong anchor, the weak anchor contributes almost nothing to rms_km, and n_eff lands at ~1.01 β€” just above the single-anchor cut-off. The result was a tiny, falsely confident circle for what was effectively one observation (a node heard once at βˆ’12 dB appearing pinned, with confidence, inside the one house that heard it). Blending on n_eff closes that gap without changing the correct weight model or any genuinely balanced estimate.

Worked examples ​

All three assume fresh observations (time_decay β‰ˆ 1) so we can focus on SNR and geometry.

Example A β€” single anchor (one house heard it once) ​

 Observations: 1   Β·   anchor at the listener, SNR βˆ’12 dB
 wSum = 0.063      wΒ²Sum = 0.063Β²        n_eff = 0.063Β² / 0.063Β² = 1.00
 centroid = the anchor itself  β‡’  rms_km = 0
 confidence = clamp(1 βˆ’ 1) = 0
 uncertainty = 5 km Γ— 1  +  0.05 km Γ— 0  =  5 km

➑ Estimate sits at the anchor, circle = 5 km. Correctly screams "I only know it's within radio range of this one node."

Example B β€” one strong + one weak/far anchor (the #3616 case) ​

 Anchor 1: SNR +10 (w = 10)   at the reporter's house
 Anchor 2: SNR βˆ’12 (w = 0.063) ~4 km away

 wSum = 10.063   wΒ²Sum = 100.004   n_eff = 10.063Β² / 100.004 β‰ˆ 1.013
 centroid β‰ˆ 0.025 km from anchor 1 (the weak anchor barely tugs it)
 rms_km β‰ˆ 0.32 km        statistical_km β‰ˆ 0.31 km
 confidence = clamp(1.013 βˆ’ 1) = 0.013

 uncertainty = 5 km Γ— 0.987  +  0.31 km Γ— 0.013  β‰ˆ  4.94 km

➑ Estimate near the strong anchor, but circle β‰ˆ 4.94 km β€” honestly unreliable. (Before the blend fix this reported β‰ˆ 0.31 km β€” confident and wrong.)

Example C β€” balanced four-anchor solve ​

 Four anchors, comparable SNR (w β‰ˆ 1 each), ~1 km from the node on four sides
 wSum = 4   wΒ²Sum = 4   n_eff = 16 / 4 = 4   β†’  confidence = 1
 rms_km β‰ˆ 1 km          statistical_km = 1 / √4 = 0.5 km
 uncertainty = 5 km Γ— 0  +  0.5 km Γ— 1  =  0.5 km

➑ Tight 0.5 km circle β€” the geometry is trusted because four independent directions genuinely constrain the node.

Reading the circle on the map ​

CircleMeaningTypical cause
Large (~5 km)low confidence β€” "within radio range of one node"single anchor, or one dominant anchor
Medium (1–3 km)partial triangulationa few anchors, uneven SNR
Small (< 1 km)well-triangulated, trustworthyseveral balanced anchors from different directions

Show or hide circles with the Show Accuracy toggle in the Map Features panel; the marker itself follows Show Estimated Positions. A circle is only ever drawn together with its marker.

Hiding the loose ones automatically ​

Because the biggest circles are the honest ~5 km single-anchor estimates, you can keep the map clean with Global Settings β†’ Position Estimation β†’ Maximum acceptable accuracy: any estimate whose uncertaintyKm exceeds your cutoff is discarded instead of stored. A value of 2–3 km keeps well-triangulated nodes and drops the "somewhere out there" guesses. 0 means no limit. See Choosing a maximum accuracy.

Properties worth knowing ​

  • Deterministic. Same observations in the lookback window β†’ same estimate and same circle, every run. There is no randomness.
  • Meshtastic-only, global. Observations are pooled across all Meshtastic sources (including the embedded MQTT broker and bridges) into one estimate per physical node. MeshCore sources are excluded.
  • Self-correcting. As soon as a node reports a real GPS position it leaves the estimate set and instead becomes an anchor for everyone else.
  • Floored, not zeroed. The radius can never drop below 0.05 km, so a multi-anchor estimate never claims absurd precision.

Reference β€” the formulas ​

 weight          wα΅’      = 0.5^(age_h / 24) Β· 10^(SNR_dB / 10)
 position        lat,lon = Ξ£(wα΅’Β·posα΅’) / Ξ£wα΅’            (weighted centroid)
 spread          rms     = √( Σ(wᡒ·distᡒ²) / Σwᡒ )
 effective N     n_eff   = (Ξ£wα΅’)Β² / Ξ£wα΅’Β²               (Kish)
 statistical     stat    = max(0.05, rms / √n_eff)
 confidence      c       = clamp(n_eff βˆ’ 1, 0, 1)
 accuracy circle unc_km  = max(0.05, 5Β·(1βˆ’c) + statΒ·c)

Implementation: observationWeight() and solveNodePosition() in src/server/services/positionEstimationService.ts.