From f765e417357d48cd513c1622eb7e69ed3c5a6af2 Mon Sep 17 00:00:00 2001 From: Jonathon Leight Date: Mon, 22 Jun 2026 14:10:39 -0400 Subject: [PATCH] Update radio presets --- README.md | 1 - internal/config/config.go | 23 +---------- internal/core/confirm_integration_test.go | 2 +- internal/core/presets.go | 49 ++++++++++++++++++----- internal/core/repeaters.go | 6 +-- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index af43617..a2e139b 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ must serve the confirm/control pages over HTTPS. | `MESHTENDER_MASTER_KEY` | 64 hex chars (32 bytes); AES-GCM key encrypting the identity seed at rest (required) | | `MESHTENDER_ADDR` | listen address (default `:8080`) | | `MESHTENDER_RP_ID` / `_RP_ORIGIN` / `_RP_NAME` | WebAuthn relying-party settings | -| `MESHTENDER_RADIO_FREQ_HZ` / `_BW_HZ` / `_SF` / `_CR` | default LoRa params suggested when adding a repeater | > **Note:** `MESHTENDER_MASTER_KEY` is coupled to the stored identity — changing it makes the > existing `server_identity` row undecryptable. Keep it stable. diff --git a/internal/config/config.go b/internal/config/config.go index aabd552..227993d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" "os" - "strconv" "strings" ) @@ -62,13 +61,10 @@ type Config struct { // production TLS usually terminates at a proxy and these stay empty. TLSCert string TLSKey string - - // DefaultRadio holds fallback LoRa parameters offered when adding a - // repeater. Per-repeater values in the DB take precedence. - DefaultRadio RadioDefaults } -// RadioDefaults are the suggested LoRa parameters for a new repeater. +// RadioDefaults is a set of LoRa parameters, used to match a repeater's stored +// radio config against the region presets offered in the UI. type RadioDefaults struct { FreqHz uint32 BwHz uint32 @@ -91,12 +87,6 @@ func Load() (*Config, error) { WWWHost: os.Getenv("MESHTENDER_WWW_HOST"), TLSCert: os.Getenv("MESHTENDER_TLS_CERT"), TLSKey: os.Getenv("MESHTENDER_TLS_KEY"), - DefaultRadio: RadioDefaults{ - FreqHz: uint32(envUintOr("MESHTENDER_RADIO_FREQ_HZ", 869525000)), - BwHz: uint32(envUintOr("MESHTENDER_RADIO_BW_HZ", 250000)), - SF: uint8(envUintOr("MESHTENDER_RADIO_SF", 11)), - CR: uint8(envUintOr("MESHTENDER_RADIO_CR", 5)), - }, } if c.RootHost != "" && c.WWWHost == "" { @@ -149,12 +139,3 @@ func splitOrigins(s string) []string { } return out } - -func envUintOr(key string, def uint64) uint64 { - if v := os.Getenv(key); v != "" { - if n, err := strconv.ParseUint(v, 10, 64); err == nil { - return n - } - } - return def -} diff --git a/internal/core/confirm_integration_test.go b/internal/core/confirm_integration_test.go index 6216e30..136502d 100644 --- a/internal/core/confirm_integration_test.go +++ b/internal/core/confirm_integration_test.go @@ -44,7 +44,7 @@ func TestConfirmRoundTrip(t *testing.T) { if err != nil { t.Fatalf("auth: %v", err) } - cfg := &config.Config{DefaultRadio: config.RadioDefaults{FreqHz: 869525000, BwHz: 250000, SF: 11, CR: 5}} + cfg := &config.Config{} srv, err := NewServer(st, authSvc, idSvc, cfg) if err != nil { t.Fatalf("server: %v", err) diff --git a/internal/core/presets.go b/internal/core/presets.go index 3b2623f..f834b89 100644 --- a/internal/core/presets.go +++ b/internal/core/presets.go @@ -14,18 +14,49 @@ type RadioPreset struct { CR int } -// radioPresets are the region presets shown in the dropdown. +// radioPresets are the community-suggested region presets shown in the +// dropdown. The list mirrors the "Select Radio Settings" presets in the +// MeshCore companion app. var radioPresets = []RadioPreset{ - {"eu868", "Europe 868 — 869.525 MHz · BW250 · SF11 · CR5", 869_525_000, 250_000, 11, 5}, - {"eu433", "Europe 433 — 433.525 MHz · BW250 · SF11 · CR5", 433_525_000, 250_000, 11, 5}, - {"us915", "US / Canada 915 — 910.525 MHz · BW250 · SF11 · CR5", 910_525_000, 250_000, 11, 5}, - {"us915n", "US / Canada 915 narrow — 910.525 MHz · BW62.5 · SF7 · CR5", 910_525_000, 62_500, 7, 5}, - {"anz915", "Australia / NZ — 915.800 MHz · BW250 · SF10 · CR5", 915_800_000, 250_000, 10, 5}, + {"au", "Australia — 915.800 MHz · BW250 · SF10 · CR5", 915_800_000, 250_000, 10, 5}, + {"au_narrow", "Australia (Narrow) — 916.575 MHz · BW62.5 · SF7 · CR8", 916_575_000, 62_500, 7, 8}, + {"au_mid", "Australia (Mid) — 915.075 MHz · BW125 · SF9 · CR5", 915_075_000, 125_000, 9, 5}, + {"au_sawa", "Australia: SA, WA — 923.125 MHz · BW62.5 · SF8 · CR8", 923_125_000, 62_500, 8, 8}, + {"au_qld", "Australia: QLD — 923.125 MHz · BW62.5 · SF8 · CR5", 923_125_000, 62_500, 8, 5}, + {"br", "Brazil — 923.125 MHz · BW62.5 · SF8 · CR8", 923_125_000, 62_500, 8, 8}, + {"euuk_narrow", "EU/UK (Narrow) — 869.618 MHz · BW62.5 · SF8 · CR8", 869_618_000, 62_500, 8, 8}, + {"euuk_dep", "EU/UK (Deprecated) — 869.525 MHz · BW250 · SF11 · CR5", 869_525_000, 250_000, 11, 5}, + {"cz_narrow", "Czech Republic (Narrow) — 869.432 MHz · BW62.5 · SF7 · CR5", 869_432_000, 62_500, 7, 5}, + {"eu433_lr", "EU 433 (Long Range) — 433.650 MHz · BW250 · SF11 · CR5", 433_650_000, 250_000, 11, 5}, + {"eu433_narrow", "EU 433 (Narrow) — 433.650 MHz · BW62.5 · SF8 · CR8", 433_650_000, 62_500, 8, 8}, + {"nl", "Netherlands — 869.618 MHz · BW62.5 · SF7 · CR5", 869_618_000, 62_500, 7, 5}, + {"nz", "New Zealand — 917.375 MHz · BW250 · SF11 · CR5", 917_375_000, 250_000, 11, 5}, + {"nz_narrow", "New Zealand (Narrow) — 917.375 MHz · BW62.5 · SF7 · CR5", 917_375_000, 62_500, 7, 5}, + {"pt433", "Portugal 433 — 433.375 MHz · BW62.5 · SF9 · CR6", 433_375_000, 62_500, 9, 6}, + {"pt868", "Portugal 868 — 869.618 MHz · BW62.5 · SF7 · CR6", 869_618_000, 62_500, 7, 6}, + {"ch", "Switzerland — 869.618 MHz · BW62.5 · SF8 · CR8", 869_618_000, 62_500, 8, 8}, + {"usca", "USA/Canada (Recommended) — 910.525 MHz · BW62.5 · SF7 · CR5", 910_525_000, 62_500, 7, 5}, + {"vn_narrow", "Vietnam (Narrow) — 920.250 MHz · BW62.5 · SF8 · CR5", 920_250_000, 62_500, 8, 5}, + {"vn_dep", "Vietnam (Deprecated) — 920.250 MHz · BW250 · SF11 · CR5", 920_250_000, 250_000, 11, 5}, } -// defaultPresetID returns the preset id matching the configured defaults, or -// "custom" when no preset matches. -func defaultPresetID(d config.RadioDefaults) string { +// defaultPresetID is the preset selected by default when adding a repeater: +// MeshCore's recommended USA/Canada profile. +const defaultPresetID = "usca" + +// defaultPreset returns the preset offered first when adding a repeater. +func defaultPreset() RadioPreset { + for _, p := range radioPresets { + if p.ID == defaultPresetID { + return p + } + } + return radioPresets[0] +} + +// presetIDFor returns the preset id whose parameters match d, or "custom" when +// none match. Used to pre-select the dropdown when editing an existing repeater. +func presetIDFor(d config.RadioDefaults) string { for _, p := range radioPresets { if p.FreqHz == int64(d.FreqHz) && p.BwHz == int64(d.BwHz) && p.SF == int(d.SF) && p.CR == int(d.CR) { return p.ID diff --git a/internal/core/repeaters.go b/internal/core/repeaters.go index 1a7da5f..3b8cdef 100644 --- a/internal/core/repeaters.go +++ b/internal/core/repeaters.go @@ -33,9 +33,9 @@ func (s *Handlers) pageAddRepeater(w http.ResponseWriter, r *http.Request) { "ServerPubKey": s.Identity.PublicKeyHex(), "SetPermCommand": s.Identity.SetPermCommand(), "RevokeCommand": s.Identity.RevokePermCommand(), - "Defaults": s.Cfg.DefaultRadio, + "Defaults": defaultPreset(), "Presets": radioPresets, - "DefaultPresetID": defaultPresetID(s.Cfg.DefaultRadio), + "DefaultPresetID": defaultPresetID, "Error": r.URL.Query().Get("error"), }) } @@ -190,7 +190,7 @@ func (s *Handlers) pageEditRepeater(w http.ResponseWriter, r *http.Request) { s.Render(w, r, "edit_repeater.html", map[string]any{ "Repeater": rep, "Presets": radioPresets, - "SelectedPreset": defaultPresetID(config.RadioDefaults{FreqHz: uint32(rep.RadioFreqHz), BwHz: uint32(rep.RadioBwHz), SF: uint8(rep.RadioSF), CR: uint8(rep.RadioCR)}), + "SelectedPreset": presetIDFor(config.RadioDefaults{FreqHz: uint32(rep.RadioFreqHz), BwHz: uint32(rep.RadioBwHz), SF: uint8(rep.RadioSF), CR: uint8(rep.RadioCR)}), "RevokeCommand": s.Identity.RevokePermCommand(), "Error": r.URL.Query().Get("error"), })