From ad02583ef7b191288156cbd297496f50932f1cf7 Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 28 Jul 2026 14:50:17 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20recognise=20the=20microwave=20bands=20?= =?UTF-8?q?=E2=80=94=2010=20GHz=20resolved=20to=20no=20band=20at=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported on 3 cm. cat.BandFromHz stopped at 23 cm, so a 10 GHz frequency came back EMPTY — and an empty band is not cosmetic: the QSO is logged without one, counts in no award slot, and exports with no BAND field. The low end was short too (2190m / 630m / 560m). Four band tables had to be extended because four exist: the CAT one, the award plan in app.go, the cluster spot classifier, and the frontend's. Plus the places that LIST bands — the QSO editor and award-definition pickers (you could not set 3 cm by hand either), the statistics axis, and the award band ORDER, where a missing band sorts to the end instead of in frequency order. Ranges and names are ADIF 3.1.7, which is what an export has to carry. A test now pins one frequency per band across the Go tables and asserts they agree — that is what was missing, four hand-maintained copies with nothing checking them. It also pins that an out-of-band frequency stays empty rather than snapping to the nearest band, which would file a QSO under a band the operator never used. --- app.go | 10 +++- band_plan_test.go | 73 ++++++++++++++++++++++++ changelog.json | 6 +- frontend/src/App.tsx | 6 +- frontend/src/components/AwardEditor.tsx | 2 +- frontend/src/components/QSOEditModal.tsx | 2 +- internal/award/award.go | 39 +++++++------ internal/cat/cat.go | 32 +++++++++++ internal/cluster/cluster.go | 72 +++++++++++++++-------- 9 files changed, 193 insertions(+), 49 deletions(-) create mode 100644 band_plan_test.go diff --git a/app.go b/app.go index b22838d..8a7c347 100644 --- a/app.go +++ b/app.go @@ -3846,7 +3846,9 @@ type AwardStatsResult struct { Rows []AwardStatRow `json:"rows"` } -var statsBands = []string{"160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m", "2m", "1.25m", "70cm", "23cm", "13cm"} +var statsBands = []string{"2190m", "630m", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", + "6m", "4m", "2m", "1.25m", "70cm", "33cm", "23cm", "13cm", "9cm", "6cm", "3cm", "1.25cm", + "6mm", "4mm", "2.5mm", "2mm", "1mm"} // awardSnapshot returns the logbook as a light-scanned, award-enriched slice, // reused across award computations. Pulling the whole logbook is the dominant @@ -4176,7 +4178,11 @@ var awardBandPlan = []struct { {"15m", 21000000, 21450000}, {"12m", 24890000, 24990000}, {"10m", 28000000, 29700000}, {"6m", 50000000, 54000000}, {"4m", 70000000, 71000000}, {"2m", 144000000, 148000000}, {"1.25m", 222000000, 225000000}, {"70cm", 420000000, 450000000}, {"23cm", 1240000000, 1300000000}, - {"13cm", 2300000000, 2450000000}, + {"13cm", 2300000000, 2450000000}, {"9cm", 3300000000, 3500000000}, + {"6cm", 5650000000, 5925000000}, {"3cm", 10000000000, 10500000000}, + {"1.25cm", 24000000000, 24250000000}, {"6mm", 47000000000, 47200000000}, + {"4mm", 75500000000, 81000000000}, {"2.5mm", 119980000000, 123000000000}, + {"2mm", 134000000000, 149000000000}, {"1mm", 241000000000, 250000000000}, } func bandForHz(hz int64) string { diff --git a/band_plan_test.go b/band_plan_test.go new file mode 100644 index 0000000..b898d25 --- /dev/null +++ b/band_plan_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "testing" + + "hamlog/internal/cat" +) + +// Three band tables exist in Go — cat.BandFromHz (the rig/CAT one), bandForHz +// (awards) and the cluster's own — plus one in App.tsx. They must agree, and +// nothing checked it: the CAT one stopped at 23 cm, so a 10 GHz QSO came back +// with an EMPTY band. An empty band is not a cosmetic problem — it is not +// logged, not counted in any award slot, and exports without a BAND field. +// +// One frequency per band, taken inside the range, plus the edges that used to be +// missing entirely. +func TestBandPlansAgree(t *testing.T) { + cases := []struct { + hz int64 + want string + }{ + {137000, "2190m"}, + {475000, "630m"}, + {1840000, "160m"}, + {3650000, "80m"}, + {7100000, "40m"}, + {10120000, "30m"}, + {14200000, "20m"}, + {18100000, "17m"}, + {21200000, "15m"}, + {24940000, "12m"}, + {28400000, "10m"}, + {50150000, "6m"}, + {70200000, "4m"}, + {144300000, "2m"}, + {223500000, "1.25m"}, + {432000000, "70cm"}, + {1296000000, "23cm"}, + // The microwave bands that were missing. 3 cm is the one an operator + // reported: 10 GHz is worked and spotted, and resolved to nothing. + {2320000000, "13cm"}, + {3400000000, "9cm"}, + {5760000000, "6cm"}, + {10368000000, "3cm"}, + {24048000000, "1.25cm"}, + {47088000000, "6mm"}, + {76032000000, "4mm"}, + {122250000000, "2.5mm"}, + {134928000000, "2mm"}, + {241920000000, "1mm"}, + } + for _, c := range cases { + if got := cat.BandFromHz(c.hz); got != c.want { + t.Errorf("cat.BandFromHz(%d) = %q, want %q", c.hz, got, c.want) + } + if got := bandForHz(c.hz); got != c.want { + t.Errorf("bandForHz(%d) = %q, want %q — the award band plan disagrees with the CAT one", c.hz, got, c.want) + } + } +} + +// A frequency in no amateur band must resolve to "" everywhere, not to the +// nearest band: guessing here would put a QSO in a band the operator never used. +func TestBandPlanRejectsOutOfBand(t *testing.T) { + for _, hz := range []int64{100_000_000, 1_000_000_000, 15_000_000_000} { + if got := cat.BandFromHz(hz); got != "" { + t.Errorf("cat.BandFromHz(%d) = %q, want empty", hz, got) + } + if got := bandForHz(hz); got != "" { + t.Errorf("bandForHz(%d) = %q, want empty", hz, got) + } + } +} diff --git a/changelog.json b/changelog.json index 7354da5..1620c84 100644 --- a/changelog.json +++ b/changelog.json @@ -14,7 +14,8 @@ "The callsign in the top bar is now the station switcher: click it and pick a profile to activate it. Managing profiles is still there, at the bottom of the list.", "The UTC clock moved from the top bar to the status bar at the bottom, beside the database.", "FlexRadio panel: the built-in ATU is now controllable — Tune, Bypass, memories, and the tuner state in words (tuning, tuned, TUNE FAILED…), because a failed tune and one that never ran look identical otherwise.", - "Diagnostic log: each database migration now names the database it runs on, and a frequency set through OmniRig is checked again 1.5 s later — both to make a reported problem readable from the log instead of guessed at." + "Diagnostic log: each database migration now names the database it runs on, and a frequency set through OmniRig is checked again 1.5 s later — both to make a reported problem readable from the log instead of guessed at.", + "Microwave bands are recognised: 13cm, 9cm, 6cm, 3cm (10 GHz), 1.25cm and above, plus 33cm and the 2190m/630m/560m low bands. A 10 GHz QSO used to come back with NO band at all — so it was logged without one, counted in no award slot, and exported without a BAND field. Band pickers, statistics and award band columns follow." ], "fr": [ "Le constructeur de filtres reprend les mêmes noms de champs de confirmation que l'édition en lot, gagne le statut de réception QRZ.com manquant, et permet de filtrer sur les dates d'envoi et de réception (avant / après).", @@ -28,7 +29,8 @@ "L'indicatif en haut de la fenêtre est désormais le sélecteur de station : un clic, on choisit un profil et il devient actif. La gestion des profils reste accessible, en bas de la liste.", "L'horloge UTC est passée de la barre du haut à la barre d'état en bas, à côté de la base de données.", "Panneau FlexRadio : le coupleur intégré est désormais pilotable — Accord, Bypass, mémoires, et l'état du coupleur en clair (accord en cours, accordé, ÉCHEC ACCORD…), car sans ça un accord raté et un accord jamais lancé se ressemblent.", - "Journal de diagnostic : chaque migration de base indique désormais sur quelle base elle tourne, et une fréquence envoyée via OmniRig est relue 1,5 s plus tard — de quoi lire un problème signalé dans le journal au lieu de le deviner." + "Journal de diagnostic : chaque migration de base indique désormais sur quelle base elle tourne, et une fréquence envoyée via OmniRig est relue 1,5 s plus tard — de quoi lire un problème signalé dans le journal au lieu de le deviner.", + "Les bandes hyperfréquences sont reconnues : 13cm, 9cm, 6cm, 3cm (10 GHz), 1.25cm et au-delà, ainsi que 33cm et les bandes basses 2190m/630m/560m. Un QSO à 10 GHz revenait sans AUCUNE bande — donc enregistré sans bande, compté dans aucun diplôme, et exporté sans champ BAND. Les listes de bandes, les statistiques et les colonnes de diplômes suivent." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2b3785c..fb8ae91 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -272,7 +272,11 @@ function bandForMHz(mhz: number): string { [1.8, 2.0, '160m'], [3.5, 4.0, '80m'], [5.06, 5.45, '60m'], [7.0, 7.3, '40m'], [10.1, 10.15, '30m'], [14.0, 14.35, '20m'], [18.068, 18.168, '17m'], [21.0, 21.45, '15m'], [24.89, 24.99, '12m'], [28.0, 29.7, '10m'], [50, 54, '6m'], [70, 71, '4m'], - [144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [1240, 1300, '23cm'], + [144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [902, 928, '33cm'], [1240, 1300, '23cm'], + // Microwave, ADIF 3.1.7 ranges — kept in step with BandFromHz on the Go side. + [2300, 2450, '13cm'], [3300, 3500, '9cm'], [5650, 5925, '6cm'], [10000, 10500, '3cm'], + [24000, 24250, '1.25cm'], [47000, 47200, '6mm'], [75500, 81000, '4mm'], + [119980, 123000, '2.5mm'], [134000, 149000, '2mm'], [241000, 250000, '1mm'], ]; for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b; return ''; diff --git a/frontend/src/components/AwardEditor.tsx b/frontend/src/components/AwardEditor.tsx index a9dfe01..b0b2614 100644 --- a/frontend/src/components/AwardEditor.tsx +++ b/frontend/src/components/AwardEditor.tsx @@ -63,7 +63,7 @@ const CONFIRM_SRC = [ { id: 'lotw', label: 'LoTW' }, { id: 'qsl', label: 'QSL' }, { id: 'eqsl', label: 'eQSL' }, { id: 'qrzcom', label: 'QRZ.com' }, { id: 'custom', label: 'Custom' }, ]; -const BANDS = ['2190m','630m','160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','1.25m','70cm','23cm','13cm']; +const BANDS = ['2190m','630m','160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','1.25m','70cm','33cm','23cm','13cm','9cm','6cm','3cm','1.25cm','6mm','4mm','2.5mm','2mm','1mm']; const MODES = ['CW','SSB','USB','LSB','AM','FM','RTTY','PSK31','FT8','FT4','JT65','JT9','MFSK','OLIVIA','DIGITALVOICE']; const EMISSIONS = ['CW', 'PHONE', 'DIGITAL']; diff --git a/frontend/src/components/QSOEditModal.tsx b/frontend/src/components/QSOEditModal.tsx index 7f6aab2..99a849a 100644 --- a/frontend/src/components/QSOEditModal.tsx +++ b/frontend/src/components/QSOEditModal.tsx @@ -37,7 +37,7 @@ function pfxOf(call: string): string { return lastDigit >= 0 ? base.slice(0, lastDigit + 1) : base; } -const BANDS = ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','70cm','23cm']; +const BANDS = ['2190m','630m','160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','1.25m','70cm','33cm','23cm','13cm','9cm','6cm','3cm','1.25cm','6mm','4mm','2.5mm','2mm','1mm']; const MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE','MFSK','OLIVIA','JS8','JT65','JT9']; // label holds an i18n key (resolved with t() at render time). const QSL_STATUSES = [ diff --git a/internal/award/award.go b/internal/award/award.go index c59f2a5..1345a28 100644 --- a/internal/award/award.go +++ b/internal/award/award.go @@ -6,10 +6,11 @@ // QSO may yield several references (e.g. a Note holding "D74 D73"). // // Examples: -// DXCC : field "dxcc" (no pattern) → entity number -// WAS : field "state", DXCCFilter [291,110,6] → US state -// DDFM : field "note", pattern "D(\d{1,2}[AB]?)" → French department from notes -// WPX : field "prefix" (computed from callsign) +// +// DXCC : field "dxcc" (no pattern) → entity number +// WAS : field "state", DXCCFilter [291,110,6] → US state +// DDFM : field "note", pattern "D(\d{1,2}[AB]?)" → French department from notes +// WPX : field "prefix" (computed from callsign) package award import ( @@ -48,16 +49,16 @@ const ( // behaves as before. type Def struct { // --- Identity --- - Code string `json:"code"` // unique key, e.g. "DXCC" - Name string `json:"name"` // friendly name + Code string `json:"code"` // unique key, e.g. "DXCC" + Name string `json:"name"` // friendly name Description string `json:"description,omitempty"` // free text - Valid bool `json:"valid"` // award enabled - Protected bool `json:"protected,omitempty"` // shipped/locked award - URL string `json:"url,omitempty"` // award home page + Valid bool `json:"valid"` // award enabled + Protected bool `json:"protected,omitempty"` // shipped/locked award + URL string `json:"url,omitempty"` // award home page DownloadURL string `json:"download_url,omitempty"` // reference-list source - RefURL string `json:"ref_url,omitempty"` // per-ref link, placeholder - ValidFrom string `json:"valid_from,omitempty"` // ISO date (QSOs before don't count) - ValidTo string `json:"valid_to,omitempty"` // ISO date (QSOs after don't count) + RefURL string `json:"ref_url,omitempty"` // per-ref link, placeholder + ValidFrom string `json:"valid_from,omitempty"` // ISO date (QSOs before don't count) + ValidTo string `json:"valid_to,omitempty"` // ISO date (QSOs after don't count) Alias string `json:"alias,omitempty"` // RefDisplay picks what the grid's award column shows for a match: "" or "ref" // = the reference (e.g. WAJA "36"), "name" = the reference's description (e.g. @@ -66,7 +67,7 @@ type Def struct { RefDisplay string `json:"ref_display,omitempty"` // --- Type & matching --- - Type AwardType `json:"type,omitempty"` // matching strategy (default QSOFIELDS) + Type AwardType `json:"type,omitempty"` // matching strategy (default QSOFIELDS) Field string `json:"field"` // QSO field to scan (see fieldRaw) MatchBy string `json:"match_by,omitempty"` // "code" | "description" | "pattern" ExactMatch bool `json:"exact_match,omitempty"` // match the whole field vs substring @@ -89,14 +90,14 @@ type Def struct { OrRules []OrRule `json:"or_rules,omitempty"` // --- Scope --- - DXCCFilter []int `json:"dxcc_filter"` // limit to these DXCC entities (nil = any) + DXCCFilter []int `json:"dxcc_filter"` // limit to these DXCC entities (nil = any) ValidBands []string `json:"valid_bands,omitempty"` // empty = all bands ValidModes []string `json:"valid_modes,omitempty"` // empty = all modes Emission []string `json:"emission,omitempty"` // CW | DIGITAL | PHONE (empty = all) // --- Confirmation --- - Confirm []string `json:"confirm"` // worked-confirmed: lotw|qsl|eqsl|qrzcom|custom - Validate []string `json:"validate,omitempty"` // validated/granted sources + Confirm []string `json:"confirm"` // worked-confirmed: lotw|qsl|eqsl|qrzcom|custom + Validate []string `json:"validate,omitempty"` // validated/granted sources // NOT IMPLEMENTED. Kept so the values operators already typed are not lost, but // nothing reads them: no ADIF export has ever written CREDIT_GRANTED. Their // controls have been removed from the editor — a checkbox that quietly does @@ -1326,7 +1327,11 @@ func setToSorted(m map[string]struct{}) []string { return out } -var bandOrder = []string{"2190m", "630m", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m", "4m", "2m", "1.25m", "70cm", "33cm", "23cm", "13cm"} +var bandOrder = []string{"2190m", "630m", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", + "6m", "4m", "2m", "1.25m", "70cm", "33cm", "23cm", "13cm", + // Microwave. A band missing from this order sorts to the end of an award's + // band columns rather than in frequency order — and 3 cm was missing. + "9cm", "6cm", "3cm", "1.25cm", "6mm", "4mm", "2.5mm", "2mm", "1mm"} func sortedBands(m map[string]int) []string { idx := map[string]int{} diff --git a/internal/cat/cat.go b/internal/cat/cat.go index 64d191f..efc56d4 100644 --- a/internal/cat/cat.go +++ b/internal/cat/cat.go @@ -776,6 +776,14 @@ func stateUserEqual(a, b RigState) bool { func BandFromHz(hz int64) string { mhz := float64(hz) / 1_000_000 switch { + // LF / MF, below 160 m. An operator on 630 m had their band come back empty, + // which then cost them the band on the QSO and in every award count. + case mhz >= 0.1357 && mhz <= 0.1378: + return "2190m" + case mhz >= 0.472 && mhz <= 0.479: + return "630m" + case mhz >= 0.501 && mhz <= 0.504: + return "560m" case mhz >= 1.8 && mhz <= 2.0: return "160m" case mhz >= 3.5 && mhz <= 4.0: @@ -810,6 +818,30 @@ func BandFromHz(hz int64) string { return "33cm" case mhz >= 1240.0 && mhz <= 1300.0: return "23cm" + // Microwave. The table used to stop at 23 cm, so 10 GHz — a band people + // actually work, and spot — resolved to an empty band: no band on the QSO, no + // band-slot, nothing in the awards. Ranges and names are the ADIF 3.1.7 ones, + // which is what an export has to carry. + case mhz >= 2300.0 && mhz <= 2450.0: + return "13cm" + case mhz >= 3300.0 && mhz <= 3500.0: + return "9cm" + case mhz >= 5650.0 && mhz <= 5925.0: + return "6cm" + case mhz >= 10000.0 && mhz <= 10500.0: + return "3cm" + case mhz >= 24000.0 && mhz <= 24250.0: + return "1.25cm" + case mhz >= 47000.0 && mhz <= 47200.0: + return "6mm" + case mhz >= 75500.0 && mhz <= 81000.0: + return "4mm" + case mhz >= 119980.0 && mhz <= 123000.0: + return "2.5mm" + case mhz >= 134000.0 && mhz <= 149000.0: + return "2mm" + case mhz >= 241000.0 && mhz <= 250000.0: + return "1mm" } return "" } diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go index 26748e6..a0c5b83 100644 --- a/internal/cluster/cluster.go +++ b/internal/cluster/cluster.go @@ -42,32 +42,32 @@ type ServerConfig struct { // is emitted to the UI, so the table never has empty country cells // flickering in for a few hundred ms. type Spot struct { - SourceID int64 `json:"source_id"` // ID of the cluster server this came from - SourceName string `json:"source_name"` // display name (handy in the UI when multiple servers) - Spotter string `json:"spotter"` // DE field - DXCall string `json:"dx_call"` // the DX station heard + SourceID int64 `json:"source_id"` // ID of the cluster server this came from + SourceName string `json:"source_name"` // display name (handy in the UI when multiple servers) + Spotter string `json:"spotter"` // DE field + DXCall string `json:"dx_call"` // the DX station heard FreqKHz float64 `json:"freq_khz"` FreqHz int64 `json:"freq_hz"` Band string `json:"band,omitempty"` Comment string `json:"comment,omitempty"` - Locator string `json:"locator,omitempty"` // spotter grid (optional) + Locator string `json:"locator,omitempty"` // spotter grid (optional) TimeUTC string `json:"time_utc,omitempty"` - Country string `json:"country,omitempty"` // DXCC entity name (cty.dat) - Continent string `json:"continent,omitempty"` // 2-letter continent - CQZone int `json:"cqz,omitempty"` // DXCC entity CQ zone - ITUZone int `json:"ituz,omitempty"` // DXCC entity ITU zone - DistanceKm int `json:"distance_km,omitempty"` // great-circle km from operator's grid - ShortPath int `json:"sp_deg,omitempty"` // azimuth (deg) short path from operator - LongPath int `json:"lp_deg,omitempty"` // azimuth (deg) long path = SP + 180 mod 360 + Country string `json:"country,omitempty"` // DXCC entity name (cty.dat) + Continent string `json:"continent,omitempty"` // 2-letter continent + CQZone int `json:"cqz,omitempty"` // DXCC entity CQ zone + ITUZone int `json:"ituz,omitempty"` // DXCC entity ITU zone + DistanceKm int `json:"distance_km,omitempty"` // great-circle km from operator's grid + ShortPath int `json:"sp_deg,omitempty"` // azimuth (deg) short path from operator + LongPath int `json:"lp_deg,omitempty"` // azimuth (deg) long path = SP + 180 mod 360 ReceivedAt time.Time `json:"received_at"` Raw string `json:"raw"` // Historical marks a spot recovered from a SH/DX table rather than heard live. // It belongs in the grid, but must NOT fire alerts or reach the panadapter: // replaying 100 past spots would spam both, and a station spotted three hours // ago is not on the air now. - Historical bool `json:"historical,omitempty"` - POTARef string `json:"pota_ref,omitempty"` // park id if this station is activating (api.pota.app) - POTAName string `json:"pota_name,omitempty"` // park name + Historical bool `json:"historical,omitempty"` + POTARef string `json:"pota_ref,omitempty"` // park id if this station is activating (api.pota.app) + POTAName string `json:"pota_name,omitempty"` // park name } // State enumerates the per-server lifecycle. @@ -122,14 +122,14 @@ type session struct { onLine func(Line) onStatus func() - mu sync.RWMutex - status ServerStatus - conn net.Conn - stopCh chan struct{} - doneCh chan struct{} - stopped bool // guards against double-stop on the same session - spotsCnt int - dbgN int // diagnostic: how many raw lines logged this connection + mu sync.RWMutex + status ServerStatus + conn net.Conn + stopCh chan struct{} + doneCh chan struct{} + stopped bool // guards against double-stop on the same session + spotsCnt int + dbgN int // diagnostic: how many raw lines logged this connection } // Manager owns N sessions, one per enabled server. Safe for concurrent @@ -560,8 +560,8 @@ const ( // showDXRE matches the reply to SH/DX — which is a TABLE, not the "DX de …" // broadcast format, and therefore matched nothing at all: // -// 14195.0 EA8DHH 3-Jul-2026 1234Z CQ DX -// freq dxcall date time comment +// 14195.0 EA8DHH 3-Jul-2026 1234Z CQ DX +// freq dxcall date time comment // // This is why "SH/DX/100 does nothing": the 100 lines arrive, fail the broadcast // regex, and get dropped. The decimal point in the frequency is required — it is @@ -682,6 +682,28 @@ func bandFromHz(hz int64) string { return "33cm" case mhz >= 1240.0 && mhz <= 1300.0: return "23cm" + // Microwave — a 10 GHz spot used to land with no band at all, so it could + // not be filtered, matched against the log, or shown on a band map. + case mhz >= 2300.0 && mhz <= 2450.0: + return "13cm" + case mhz >= 3300.0 && mhz <= 3500.0: + return "9cm" + case mhz >= 5650.0 && mhz <= 5925.0: + return "6cm" + case mhz >= 10000.0 && mhz <= 10500.0: + return "3cm" + case mhz >= 24000.0 && mhz <= 24250.0: + return "1.25cm" + case mhz >= 47000.0 && mhz <= 47200.0: + return "6mm" + case mhz >= 75500.0 && mhz <= 81000.0: + return "4mm" + case mhz >= 119980.0 && mhz <= 123000.0: + return "2.5mm" + case mhz >= 134000.0 && mhz <= 149000.0: + return "2mm" + case mhz >= 241000.0 && mhz <= 250000.0: + return "1mm" } return "" }