diff --git a/cmd/server/config.go b/cmd/server/config.go index dbfee265..0f6428f6 100644 --- a/cmd/server/config.go +++ b/cmd/server/config.go @@ -1023,8 +1023,8 @@ type AnalyticsConfig struct { RecomputeIntervalSeconds map[string]int `json:"recomputeIntervalSeconds,omitempty"` // LoRaPreset is the assumed PHY preset used by the relay-airtime-share // metric to compute true Time-on-Air (issue #1768). Defaults to the - // EU MeshCore deployment: 869.6 MHz / BW 62.5 kHz / SF 8 / CR 4/5. - // freq is informational only and surfaces in the analytics caption. + // MeshCore deployment: BW 62.5 kHz / SF 7 / CR 4/5. + // freq is informational only and is not displayed or used in ToA. LoRaPreset *LoRaPresetConfig `json:"loraPreset,omitempty"` } diff --git a/cmd/server/relay_airtime_share.go b/cmd/server/relay_airtime_share.go index 3b634566..35f0ee41 100644 --- a/cmd/server/relay_airtime_share.go +++ b/cmd/server/relay_airtime_share.go @@ -22,7 +22,7 @@ import ( // // The PHY preset is config-driven (analytics.loraPreset in // config.example.json); defaults match the actual deployment -// preset 869.6 MHz / BW 62.5 kHz / SF 8 / CR 4/5, with the +// preset BW 62.5 kHz / SF 7 / CR 4/5, with the // SF-dependent preamble pulled from internal/lora.PreambleForSF. // // Aggregated by payload_type. Originator TX is deliberately excluded — a @@ -30,14 +30,13 @@ import ( // "relay amplification" metric. In-memory only; no SQL, no new index. // defaultLoRaPreset is the canonical fallback when config is absent. -// Matches the reporter's `get radio` output `869.6179809, 62.5, 8, 5`. +// Frequency does not affect ToA; it is kept only as an optional config field. func defaultLoRaPreset() lora.Preset { return lora.Preset{ - FreqHz: 869.6e6, BWkHz: 62.5, - SF: 8, + SF: 7, CR: 5, - Preamble: lora.PreambleForSF(8), + Preamble: lora.PreambleForSF(7), } } diff --git a/cmd/server/relay_airtime_share_test.go b/cmd/server/relay_airtime_share_test.go index b08732ec..78f19a17 100644 --- a/cmd/server/relay_airtime_share_test.go +++ b/cmd/server/relay_airtime_share_test.go @@ -97,7 +97,7 @@ func TestRelayAirtimeShare_ADVERTvsACKDivergence(t *testing.T) { if !ok { t.Fatalf("result['preset'] missing or wrong type: %T", result["preset"]) } - if preset.SF == 0 || preset.BWkHz == 0 || preset.CR == 0 || preset.Preamble == 0 || preset.FreqHz == 0 { + if preset.SF == 0 || preset.BWkHz == 0 || preset.CR == 0 || preset.Preamble == 0 { t.Errorf("result['preset'] has zero fields: %+v", preset) } @@ -160,18 +160,18 @@ func TestRelayAirtimeShare_ADVERTvsACKDivergence(t *testing.T) { // gate: airtime_pct must follow true LoRa Time-on-Air, NOT bytes. // // Setup: 1 ADVERT (200 B, 1 relay) and 1 ACK (10 B, 1 relay) with the -// default EU preset (869.6 MHz / BW 62.5 kHz / SF 8 / CR 4/5, +// default US preset (BW 62.5 kHz / SF 7 / CR 4/5, // preamble 32 per firmware preambleLengthForSF). // // Old byte-proxy would give ADVERT 200/(200+10) = 95.24 %. // True ToA per #1768 closed form: // -// T_sym = 256 / 62500 = 4.096 ms -// ADVERT (PL=200): symbols = 36.25 + (8 + ceil((1600-32+44)/32)*5) -// = 36.25 + (8 + 51*5) = 299.25 → 1225.728 ms -// ACK (PL=10): symbols = 36.25 + (8 + ceil((80-32+44)/32)*5) -// = 36.25 + (8 + 3*5) = 59.25 → 242.688 ms -// ADVERT share = 1225.728 / (1225.728 + 242.688) = 0.83476 → 83.48 % +// T_sym = 128 / 62500 = 2.048 ms +// ADVERT (PL=200): symbols = 36.25 + (8 + ceil((1600-28+44)/28)*5) +// = 36.25 + (8 + 58*5) = 334.25 +// ACK (PL=10): symbols = 36.25 + (8 + ceil((80-28+44)/28)*5) +// = 36.25 + (8 + 4*5) = 64.25 +// ADVERT share = 334.25 / (334.25 + 64.25) = 0.83877 → 83.88 % // // 83 % vs 95 % is the whole point: small frames are no longer crushed // against large ones because the additive preamble + fixed-overhead @@ -214,11 +214,11 @@ func TestRelayAirtimeShare_ToAReplacesByteProxy(t *testing.T) { // Sanity guards on the independent calculation. Tolerances ±0.05 pp // around the AN1200.13 hand-computed values keep this honest if the // upstream lora package ever drifts. - if math.Abs(wantAdvert-83.4754) > 0.05 { - t.Fatalf("independent ADVERT ToA share drifted: got %.4f want ~83.4754", wantAdvert) + if math.Abs(wantAdvert-83.8770) > 0.05 { + t.Fatalf("independent ADVERT ToA share drifted: got %.4f want ~83.8770", wantAdvert) } - if math.Abs(wantAck-16.5246) > 0.05 { - t.Fatalf("independent ACK ToA share drifted: got %.4f want ~16.5246", wantAck) + if math.Abs(wantAck-16.1230) > 0.05 { + t.Fatalf("independent ACK ToA share drifted: got %.4f want ~16.1230", wantAck) } if math.Abs(advertPct-wantAdvert) > 0.2 { t.Errorf("ADVERT airtime_pct = %.4f, want %.4f (true ToA)", advertPct, wantAdvert) diff --git a/config.example.json b/config.example.json index 7977e2fd..d65263b8 100644 --- a/config.example.json +++ b/config.example.json @@ -425,11 +425,10 @@ "nodesClockSkew": 300 }, "loraPreset": { - "freq": 869600000, "bw": 62.5, - "sf": 8, + "sf": 7, "cr": 5, - "_comment_": "Issue #1768. LoRa PHY preset assumed by the Relay Airtime Share metric to compute true Time-on-Air. Share numbers are only meaningful relative to one preset, so operators MUST set this to match their mesh — freq is informational (surfaces in the chart caption) and does not affect ToA; bw is bandwidth in kHz (e.g. 62.5, 125, 250); sf is spreading factor (6..12); cr is the denominator of the 4/N coding rate (5 ⇒ 4/5 … 8 ⇒ 4/8). Defaults reproduce the typical EU MeshCore deployment 869.6 MHz / BW 62.5 kHz / SF 8 / CR 4/5. CRC=1, IH=0, DE (T_sym ≥ 16 ms), and preamble (32 for SF≤8, 16 otherwise, per firmware preambleLengthForSF) are firmware-fixed and intentionally not exposed as config." + "_comment_": "Issue #1768. LoRa PHY preset assumed by the Relay Airtime Share metric to compute true Time-on-Air. Share numbers are only meaningful relative to one preset, so operators MUST set this to match their mesh — freq is optional, informational only, and neither displayed nor used in ToA; bw is bandwidth in kHz (e.g. 62.5, 125, 250); sf is spreading factor (6..12); cr is the denominator of the 4/N coding rate (5 ⇒ 4/5 … 8 ⇒ 4/8). Defaults: BW 62.5 kHz / SF 7 / CR 4/5. CRC=1, IH=0, DE (T_sym ≥ 16 ms), and preamble (32 for SF≤8, 16 otherwise, per firmware preambleLengthForSF) are firmware-fixed and intentionally not exposed as config." } }, "_comment_analytics": "Issue #1240 + #1256 + #1265. Each analytics endpoint (topology, rf, distance, channels, hashCollisions, hashSizes, roles, observersClockSkew, nodesClockSkew) is recomputed in the background on the configured interval and served from an atomic-pointer cache. Reads never block on compute. Default 300s (5 min) per endpoint reflects the operator principle: serving slightly stale data quickly beats real-time data slowly. Lower values = fresher data at higher CPU cost. Only the default query (no region/window) is precomputed; region- and window-filtered requests fall back to the legacy on-request compute + 60s TTL cache.", diff --git a/public/analytics.js b/public/analytics.js index 62c13565..d2a47e4c 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -521,13 +521,11 @@ var preset = data && data.preset; var presetCaption = ''; if (preset && typeof preset === 'object') { - var freqMHz = Number(preset.freq_hz || 0) / 1e6; var bwKhz = Number(preset.bw_khz || 0); var bwStr = bwKhz ? bwKhz.toFixed(1).replace(/\.0$/, '') : '0'; presetCaption = '