mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-11 11:57:57 +00:00
Refs #1268. #2209 is merged as `cab4f71c`, and master is merged into this branch (`47ce8264`), so the diff against master is this PR's own change: `ea4d1130` plus the fix in `5cbbe5d5`. ## What it does Two nodes hear each other only when SF and bandwidth match and their frequencies are close enough. `/api/rf-params` now also returns `deviations`: nodes whose setting differs from a majority of the nodes around them on the same band. - **Peers:** nodes with GPS and RF params within `rfParams.compareRadiusKm` (default 50) whose frequency is within 30 MHz. - **Same channel:** frequencies within a quarter of the bandwidth count as one channel (15.625 kHz at BW 62.5). The SX1262 tolerates a transmitter/receiver offset of up to 25% of BW without sensitivity loss, for SF5 to SF12 and all bandwidths. Source: the `FERR_L` row in the [S62F module datasheet](https://lora-alliance.org/wp-content/uploads/2020/07/S62F_SPEC_Ver.B.pdf), an SX1262 module; I did not find the figure in a Semtech document itself. That table also lists -50/-100/-200 ppm for SF12/SF11/SF10, which at 869 MHz is wider than BW/4 at 62.5 and 125 kHz, so BW/4 is the tighter bound there. - **Flagged when:** a setting the node cannot hear is shared by more than half of the group, the node itself counted, with at least 3 peers and no other incompatible setting at the same support. So 5 against 5 flags nobody, and 2 of 4 does not count as a majority. Each deviation carries the majority, how many peers are compatible with it, the peer count, and which of freq/sf/bw differ. - **Coding rate is not compared.** RadioLib starts SX126x radios with an explicit LoRa header (`SX126x::begin` sets `RADIOLIB_SX126X_LORA_HEADER_EXPLICIT` at `6d89348`, the commit the firmware pins in `platformio.ini`), and nothing in `firmware/src` switches to implicit. The header carries the CR, so a CR mismatch still decodes. I checked SX126x only, not SX127x or LR11x0. UI: - Nodes list: a badge such as `LoRa SF7` next to the name of a deviating node (the majority in its tooltip), and a `LoRa: differs from nearby (N)` filter kept in the URL as `?lora=differs`. - Node detail: one line, for example "Differs from 12 of 14 nearby nodes on this band, which use 869.618 MHz · SF8 · BW 62.5 kHz". - Badge and filter only exist while the server has map data. ## Why same-band only On our instance (1,110 nodes with params and GPS, measured with the first version of the rule), a plain neighbourhood majority flagged exactly 15 nodes, all of them a deliberate 433 MHz network in Limburg next to the 868 MHz one (names like `NL-ZAY-RP01 | 433 X 868`). The same-band comparison flags none there. On our network this PR shows nothing today. The flags are for instances where a node really is set to a different SF, bandwidth or channel than the nodes around it. ## Cost - Points are bucketed on a radius-sized grid (rows of radius/110.57 degrees, longitude columns widened by 1/cos(lat)) and compared with a squared equirectangular distance. - `BenchmarkRfDeviations1k` (1,100 nodes over Belgium and the southern Netherlands): 4.5 ms. `BenchmarkRfDeviations10k` (10,000 nodes in the same box): 388 ms, 1.3 MB. Both on the first version. - After `5cbbe5d5`, on a slower machine, 3 runs each: 1k 4.4 to 9.5 ms, 10k 821 to 852 ms, 1.8 MB. On that machine the first version measured 7.6 to 9.3 ms and 819 to 861 ms, so the majority step adds no measurable time. It is one pass over the distinct settings around a node, and those are few (one per preset in use). - Inside the radius the work is close to O(n²): 10,000 nodes in one dense box means about 2,000 neighbours per node, and a request after the 5 minute TTL waits for that computation. - The result is cached per map snapshot for 5 minutes; concurrent requests wait for one computation. - On staging `/api/perf` reports `/api/rf-params` at 9.9 ms average, 26 ms max (8 requests). ## Tests - Go: deviation cases (SF, freq within band, BW, CR ignored, other band ignored, no GPS, no params, far away), split vote and too few peers, radius, a peer in another longitude column at 60°N, the cache (same snapshot within TTL, new snapshot, after TTL), the route with four inserted nodes, plus the two benchmarks. - Go, added in `5cbbe5d5`: a 5/5 split and 2 of 4 flag nobody; a 1 and a 15 kHz offset are not deviations while 32 kHz is, and offset peers add up into one majority; the BW/4 boundary; peers two grid columns away at 60°N; a peer at the north edge of the radius. Each fails on the first version. The two-column test also fails with a ±1 column search, which the existing longitude test did not catch. - Full `cmd/server` suite and `test-all.sh` pass locally on `5cbbe5d5`. - Frontend unit: 6 new assertions for `deviationBadge` and the detail line (13 in the file). - E2E: 4 new checks: badge only on the deviating node, filter count and result, `?lora=differs` written and restored, detail line. The disabled check now also asserts no filter and no badges. 8/8 locally. ## Validation On staging with live map data, with the first version: 1,115 nodes served, `deviations: {}`, and the filter shows `LoRa: differs from nearby (0)` with no badges. Because no real deviation exists there, the badge, filter and detail line were checked by serving one simulated deviation through Playwright route interception against the staging page. ## Not done - No map marker for deviating nodes. - `compareRadiusKm` is config-only; the customizer is a later step. The other values are hardcoded and belong in that step too: at least 3 peers, the 30 MHz band, the more-than-half rule, BW/4 and the 5 minute TTL. - Not re-run on staging after `5cbbe5d5`, and the E2E suite was not re-run locally. The fix changes only the server's choice of nodes, not the UI. - The detail line says the peers "use" the majority setting. With the BW/4 tolerance, some of them may be a few kHz off it. - Companions and room servers are compared too, not only repeaters. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
343 lines
12 KiB
Go
343 lines
12 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"math/rand"
|
||
"net/http/httptest"
|
||
"sort"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
type rfTestNode struct {
|
||
pk string
|
||
lat, lon float64
|
||
p *RfParams
|
||
}
|
||
|
||
func rfDevInput(ns []rfTestNode) (map[string]RfParams, []nodeInfo) {
|
||
params := map[string]RfParams{}
|
||
nodes := make([]nodeInfo, 0, len(ns))
|
||
for _, n := range ns {
|
||
nodes = append(nodes, nodeInfo{PublicKey: strings.ToUpper(n.pk), Lat: n.lat, Lon: n.lon, HasGPS: n.lat != 0 || n.lon != 0})
|
||
if n.p != nil {
|
||
params[n.pk] = *n.p
|
||
}
|
||
}
|
||
return params, nodes
|
||
}
|
||
|
||
func rfp(freq float64, sf int, bw float64, cr int) *RfParams {
|
||
return &RfParams{Freq: freq, SF: sf, BW: bw, CR: cr}
|
||
}
|
||
|
||
func devKeys(d map[string]RfDeviation) []string {
|
||
out := make([]string, 0, len(d))
|
||
for k := range d {
|
||
out = append(out, k)
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func TestRfDeviations(t *testing.T) {
|
||
eu := rfp(869.618, 8, 62.5, 8)
|
||
// A cluster around Brussels, all within a few km.
|
||
ns := []rfTestNode{
|
||
{"a1", 50.85, 4.35, eu},
|
||
{"a2", 50.86, 4.36, eu},
|
||
{"a3", 50.87, 4.34, eu},
|
||
{"a4", 50.84, 4.37, rfp(869.618, 8, 62.5, 5)}, // CR only: never a deviation
|
||
{"sf7", 50.85, 4.36, rfp(869.618, 7, 62.5, 8)}, // SF differs
|
||
{"frq", 50.86, 4.35, rfp(869.432, 8, 62.5, 8)}, // freq differs within the band
|
||
{"bw", 50.85, 4.34, rfp(869.618, 8, 125, 8)}, // BW differs
|
||
// A 433 MHz network in the same area: a separate band, compared only
|
||
// with itself, so neither side flags the other.
|
||
{"u1", 50.85, 4.35, rfp(433.65, 8, 62.5, 8)},
|
||
{"u2", 50.86, 4.35, rfp(433.65, 8, 62.5, 8)},
|
||
{"u3", 50.87, 4.35, rfp(433.65, 8, 62.5, 8)},
|
||
{"u4", 50.84, 4.35, rfp(433.65, 8, 62.5, 8)},
|
||
// Far away (Lisbon): no peers within the radius.
|
||
{"far", 38.72, -9.14, rfp(869.618, 7, 62.5, 5)},
|
||
// No GPS and no params: ignored.
|
||
{"nogps", 0, 0, rfp(869.618, 7, 62.5, 5)},
|
||
{"noparams", 50.85, 4.35, nil},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
got := computeRfDeviations(params, nodes, 50, 3)
|
||
|
||
if k := devKeys(got); fmt.Sprint(k) != "[bw frq sf7]" {
|
||
t.Fatalf("deviating nodes = %v, want [bw frq sf7]", k)
|
||
}
|
||
d := got["sf7"]
|
||
if d.Majority.Freq != 869.618 || d.Majority.SF != 8 || d.Majority.BW != 62.5 {
|
||
t.Errorf("sf7 majority = %+v", d.Majority)
|
||
}
|
||
// Peers of sf7 in the 868 band: a1 a2 a3 a4 frq bw = 6; a1-a4 share 869.618/SF8/BW62.5.
|
||
if d.Peers != 6 || d.Agree != 4 {
|
||
t.Errorf("sf7 agree/peers = %d/%d, want 4/6", d.Agree, d.Peers)
|
||
}
|
||
if fmt.Sprint(d.Fields) != "[sf]" {
|
||
t.Errorf("sf7 fields = %v", d.Fields)
|
||
}
|
||
if fmt.Sprint(got["frq"].Fields) != "[freq]" || fmt.Sprint(got["bw"].Fields) != "[bw]" {
|
||
t.Errorf("frq fields %v, bw fields %v", got["frq"].Fields, got["bw"].Fields)
|
||
}
|
||
}
|
||
|
||
func TestRfDeviationsNeedsAClearMajority(t *testing.T) {
|
||
// Two settings split 2-2 around the node: no majority, no flag.
|
||
ns := []rfTestNode{
|
||
{"x", 50.85, 4.35, rfp(869.618, 9, 62.5, 8)},
|
||
{"p1", 50.86, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 50.84, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", 50.85, 4.36, rfp(869.618, 7, 62.5, 5)},
|
||
{"p4", 50.85, 4.34, rfp(869.618, 7, 62.5, 5)},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); got["x"].Peers != 0 {
|
||
t.Errorf("x flagged without a clear majority: %+v", got["x"])
|
||
}
|
||
|
||
// Fewer than minPeers same-band nodes nearby: no flag.
|
||
ns = []rfTestNode{
|
||
{"x", 50.85, 4.35, rfp(869.618, 7, 62.5, 8)},
|
||
{"p1", 50.86, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 50.84, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
}
|
||
params, nodes = rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); len(got) != 0 {
|
||
t.Errorf("flagged with 2 peers: %v", devKeys(got))
|
||
}
|
||
}
|
||
|
||
func TestRfDeviationsRadius(t *testing.T) {
|
||
// Peers about 70 km away count at 100 km, not at 50 km.
|
||
ns := []rfTestNode{
|
||
{"x", 50.85, 4.35, rfp(869.618, 7, 62.5, 8)},
|
||
{"p1", 51.48, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 51.48, 4.36, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", 51.49, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); len(got) != 0 {
|
||
t.Errorf("50 km: flagged %v", devKeys(got))
|
||
}
|
||
if got := computeRfDeviations(params, nodes, 100, 3); len(got) != 1 {
|
||
t.Errorf("100 km: flagged %v, want [x]", devKeys(got))
|
||
}
|
||
}
|
||
|
||
func TestRfDeviationsAcrossLongitudeCells(t *testing.T) {
|
||
// At 60°N a degree of longitude is ~55 km: peers 40 km east sit in a
|
||
// different grid column and must still be found.
|
||
ns := []rfTestNode{
|
||
{"x", 60.0, 10.0, rfp(869.618, 7, 62.5, 8)},
|
||
{"p1", 60.0, 10.72, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 60.01, 10.72, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", 60.0, 10.71, rfp(869.618, 8, 62.5, 8)},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); len(got) != 1 {
|
||
t.Errorf("flagged %v, want [x]", devKeys(got))
|
||
}
|
||
}
|
||
|
||
func TestRfCompareRadiusDefault(t *testing.T) {
|
||
if r := (&RfParamsConfig{NodesURL: "x"}).compareRadiusKm(); r != 50 {
|
||
t.Errorf("default radius = %v", r)
|
||
}
|
||
if r := (&RfParamsConfig{NodesURL: "x", CompareRadiusKm: 20}).compareRadiusKm(); r != 20 {
|
||
t.Errorf("configured radius = %v", r)
|
||
}
|
||
}
|
||
|
||
// BenchmarkRfDeviations1k: 1,100 nodes over Belgium and the southern
|
||
// Netherlands, about the size of our own instance.
|
||
func BenchmarkRfDeviations1k(b *testing.B) {
|
||
r := rand.New(rand.NewSource(1))
|
||
ns := make([]rfTestNode, 1100)
|
||
for i := range ns {
|
||
ns[i] = rfTestNode{fmt.Sprintf("%064x", i), 49.5 + r.Float64()*2.5, 2.5 + r.Float64()*4, rfp(869.618, 8, 62.5, 8)}
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
b.ResetTimer()
|
||
for i := 0; i < b.N; i++ {
|
||
computeRfDeviations(params, nodes, 50, 3)
|
||
}
|
||
}
|
||
|
||
// BenchmarkRfDeviations10k: 10,000 nodes with params spread over a
|
||
// Belgium-sized box, the worst case /api/rf-params computes per request.
|
||
func BenchmarkRfDeviations10k(b *testing.B) {
|
||
r := rand.New(rand.NewSource(1))
|
||
ns := make([]rfTestNode, 10000)
|
||
for i := range ns {
|
||
sf := 8
|
||
if r.Intn(50) == 0 {
|
||
sf = 7
|
||
}
|
||
ns[i] = rfTestNode{fmt.Sprintf("%064x", i), 49.5 + r.Float64()*2, 2.5 + r.Float64()*4, rfp(869.618, sf, 62.5, 8)}
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
b.ResetTimer()
|
||
for i := 0; i < b.N; i++ {
|
||
computeRfDeviations(params, nodes, 50, 3)
|
||
}
|
||
}
|
||
|
||
func TestRfParamsRouteDeviations(t *testing.T) {
|
||
srv, router := setupTestServer(t)
|
||
// Four nodes close together; the first runs SF7, the others SF8.
|
||
pks := []string{rfPK1, rfPK2, rfPK3, rfPK3[:63] + "9"}
|
||
for i, pk := range pks {
|
||
mustExecDB(t, srv.db, fmt.Sprintf(`INSERT INTO nodes (public_key, name, role, lat, lon) VALUES ('%s', 'rfdev%d', 'repeater', %f, 4.35)`, pk, i, 50.85+float64(i)*0.01))
|
||
}
|
||
srv.store.InvalidateNodeCache()
|
||
now := time.Now().UTC().Format(time.RFC3339)
|
||
snapNodes := map[string]RfParams{}
|
||
for i, pk := range pks {
|
||
sf := 8
|
||
if i == 0 {
|
||
sf = 7
|
||
}
|
||
snapNodes[pk] = RfParams{Freq: 869.618, SF: sf, BW: 62.5, CR: 8, LastAdvert: now}
|
||
}
|
||
c := newRfParamsCache(&RfParamsConfig{NodesURL: "http://unused"})
|
||
c.ptr.Store(&rfParamsSnapshot{FetchedAt: time.Now().UTC(), Nodes: snapNodes, Presets: []RfPreset{}})
|
||
srv.rfParams = c
|
||
|
||
w := httptest.NewRecorder()
|
||
router.ServeHTTP(w, httptest.NewRequest("GET", "/api/rf-params", nil))
|
||
var resp RfParamsResponse
|
||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(resp.Nodes) != 4 {
|
||
t.Fatalf("nodes served = %d, want 4", len(resp.Nodes))
|
||
}
|
||
d, ok := resp.Deviations[rfPK1]
|
||
if len(resp.Deviations) != 1 || !ok || d.Majority.SF != 8 || d.Agree != 3 || d.Peers != 3 {
|
||
t.Errorf("deviations = %+v", resp.Deviations)
|
||
}
|
||
}
|
||
|
||
func TestRfDeviationCache(t *testing.T) {
|
||
var c rfDeviationCache
|
||
calls := 0
|
||
compute := func() map[string]RfDeviation { calls++; return map[string]RfDeviation{} }
|
||
s1, s2 := &rfParamsSnapshot{}, &rfParamsSnapshot{}
|
||
t0 := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
|
||
c.get(s1, t0, compute)
|
||
c.get(s1, t0.Add(time.Minute), compute)
|
||
if calls != 1 {
|
||
t.Errorf("same snapshot within TTL computed %d times, want 1", calls)
|
||
}
|
||
c.get(s2, t0.Add(2*time.Minute), compute)
|
||
if calls != 2 {
|
||
t.Errorf("new snapshot: %d computations, want 2", calls)
|
||
}
|
||
c.get(s2, t0.Add(2*time.Minute+rfDeviationTTL), compute)
|
||
if calls != 3 {
|
||
t.Errorf("after TTL: %d computations, want 3", calls)
|
||
}
|
||
}
|
||
|
||
// An even split flags nobody. Before the node's own setting was counted, each
|
||
// side saw the other as "at least half" of its peers and all ten were flagged.
|
||
func TestRfDeviationsEvenSplitFlagsNobody(t *testing.T) {
|
||
var ns []rfTestNode
|
||
for i := 0; i < 5; i++ {
|
||
ns = append(ns,
|
||
rfTestNode{fmt.Sprintf("a%d", i), 50.85 + float64(i)*0.01, 4.35, rfp(869.618, 7, 62.5, 8)},
|
||
rfTestNode{fmt.Sprintf("b%d", i), 50.85 + float64(i)*0.01, 4.36, rfp(869.618, 8, 62.5, 8)})
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); len(got) != 0 {
|
||
t.Errorf("5/5 split flagged %v, want none", devKeys(got))
|
||
}
|
||
|
||
// 2 of 4 is half, not more: x (SF9) with peers SF8, SF8, SF7 is not flagged.
|
||
ns = []rfTestNode{
|
||
{"x", 50.85, 4.35, rfp(869.618, 9, 62.5, 8)},
|
||
{"p1", 50.86, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 50.84, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", 50.85, 4.36, rfp(869.618, 7, 62.5, 8)},
|
||
}
|
||
params, nodes = rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); len(got) != 0 {
|
||
t.Errorf("2 of 4 flagged %v, want none", devKeys(got))
|
||
}
|
||
}
|
||
|
||
// Frequencies within BW/4 are one channel: a 1 kHz offset is not a deviation,
|
||
// and offset peers add up into one majority. 32 kHz at BW 62.5 (BW/4 =
|
||
// 15.625 kHz) is a deviation.
|
||
func TestRfDeviationsFrequencyTolerance(t *testing.T) {
|
||
ns := []rfTestNode{
|
||
{"near", 50.85, 4.35, rfp(869.617, 8, 62.5, 8)},
|
||
{"edge", 50.85, 4.355, rfp(869.633, 8, 62.5, 8)}, // 15 kHz off: still compatible
|
||
{"off", 50.85, 4.36, rfp(869.650, 8, 62.5, 8)}, // 32 kHz off
|
||
{"p1", 50.86, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 50.84, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", 50.85, 4.34, rfp(869.619, 8, 62.5, 8)},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
got := computeRfDeviations(params, nodes, 50, 3)
|
||
if k := devKeys(got); fmt.Sprint(k) != "[off]" {
|
||
t.Fatalf("deviating nodes = %v, want [off]", k)
|
||
}
|
||
d := got["off"]
|
||
if fmt.Sprint(d.Fields) != "[freq]" || d.Agree != 5 || d.Peers != 5 || d.Majority.Freq != 869.618 {
|
||
t.Errorf("off = %+v, want freq differing from 869.618 with 5 of 5 compatible", d)
|
||
}
|
||
if !rfFreqCompatible(869.618, 869.618+0.015625, 62.5) || rfFreqCompatible(869.618, 869.618+0.0157, 62.5) {
|
||
t.Errorf("BW/4 boundary at 62.5 kHz is not 15.625 kHz")
|
||
}
|
||
}
|
||
|
||
// At 60°N a longitude degree is about 55.7 km, so the 50 km radius spans two
|
||
// grid columns. A node near the east edge of its cell has peers two columns
|
||
// over; a column search of ±1 would miss them.
|
||
func TestRfDeviationsTwoColumnsAway(t *testing.T) {
|
||
cellDeg := 50 / 110.57
|
||
lon := (math.Floor(10/cellDeg) + 0.95) * cellDeg
|
||
peerLon := lon + 45/(111.32*math.Cos(60*math.Pi/180)) // 45 km east
|
||
if c0, c1 := math.Floor(lon/cellDeg), math.Floor(peerLon/cellDeg); c1-c0 != 2 {
|
||
t.Fatalf("setup: peers %v columns away, want 2", c1-c0)
|
||
}
|
||
ns := []rfTestNode{
|
||
{"x", 60.0, lon, rfp(869.618, 7, 62.5, 8)},
|
||
{"p1", 60.0, peerLon, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", 60.001, peerLon, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", 59.999, peerLon, rfp(869.618, 8, 62.5, 8)},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); fmt.Sprint(devKeys(got)) != "[x]" {
|
||
t.Errorf("flagged %v, want [x]", devKeys(got))
|
||
}
|
||
}
|
||
|
||
// A peer just inside the radius due north must be found by the ±1 row search.
|
||
// With rows of radius/111 degrees (0.996 × radius), a node at the top of its
|
||
// row had such a peer two rows up; rows are now radius/110.57 degrees, never
|
||
// narrower than the radius. The node sits where the old grid failed.
|
||
func TestRfDeviationsNorthEdgeOfRadius(t *testing.T) {
|
||
oldCellDeg := 50 / 111.0
|
||
lat := (math.Floor(50.5/oldCellDeg) + 0.999) * oldCellDeg
|
||
peerLat := lat + 49.9/110.57
|
||
ns := []rfTestNode{
|
||
{"x", lat, 4.35, rfp(869.618, 7, 62.5, 8)},
|
||
{"p1", peerLat, 4.35, rfp(869.618, 8, 62.5, 8)},
|
||
{"p2", peerLat, 4.3501, rfp(869.618, 8, 62.5, 8)},
|
||
{"p3", peerLat, 4.3499, rfp(869.618, 8, 62.5, 8)},
|
||
}
|
||
params, nodes := rfDevInput(ns)
|
||
if got := computeRfDeviations(params, nodes, 50, 3); fmt.Sprint(devKeys(got)) != "[x]" {
|
||
t.Errorf("flagged %v, want [x]", devKeys(got))
|
||
}
|
||
}
|