fix: recognise the microwave bands — 10 GHz resolved to no band at all
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.
This commit is contained in:
@@ -3846,7 +3846,9 @@ type AwardStatsResult struct {
|
|||||||
Rows []AwardStatRow `json:"rows"`
|
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,
|
// awardSnapshot returns the logbook as a light-scanned, award-enriched slice,
|
||||||
// reused across award computations. Pulling the whole logbook is the dominant
|
// 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},
|
{"15m", 21000000, 21450000}, {"12m", 24890000, 24990000}, {"10m", 28000000, 29700000},
|
||||||
{"6m", 50000000, 54000000}, {"4m", 70000000, 71000000}, {"2m", 144000000, 148000000},
|
{"6m", 50000000, 54000000}, {"4m", 70000000, 71000000}, {"2m", 144000000, 148000000},
|
||||||
{"1.25m", 222000000, 225000000}, {"70cm", 420000000, 450000000}, {"23cm", 1240000000, 1300000000},
|
{"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 {
|
func bandForHz(hz int64) string {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -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 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.",
|
"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.",
|
"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": [
|
"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).",
|
"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'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.",
|
"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.",
|
"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."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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'],
|
[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'],
|
[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'],
|
[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;
|
for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b;
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const CONFIRM_SRC = [
|
|||||||
{ id: 'lotw', label: 'LoTW' }, { id: 'qsl', label: 'QSL' }, { id: 'eqsl', label: 'eQSL' },
|
{ id: 'lotw', label: 'LoTW' }, { id: 'qsl', label: 'QSL' }, { id: 'eqsl', label: 'eQSL' },
|
||||||
{ id: 'qrzcom', label: 'QRZ.com' }, { id: 'custom', label: 'Custom' },
|
{ 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 MODES = ['CW','SSB','USB','LSB','AM','FM','RTTY','PSK31','FT8','FT4','JT65','JT9','MFSK','OLIVIA','DIGITALVOICE'];
|
||||||
const EMISSIONS = ['CW', 'PHONE', 'DIGITAL'];
|
const EMISSIONS = ['CW', 'PHONE', 'DIGITAL'];
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function pfxOf(call: string): string {
|
|||||||
return lastDigit >= 0 ? base.slice(0, lastDigit + 1) : base;
|
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'];
|
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).
|
// label holds an i18n key (resolved with t() at render time).
|
||||||
const QSL_STATUSES = [
|
const QSL_STATUSES = [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// QSO may yield several references (e.g. a Note holding "D74 D73").
|
// QSO may yield several references (e.g. a Note holding "D74 D73").
|
||||||
//
|
//
|
||||||
// Examples:
|
// Examples:
|
||||||
|
//
|
||||||
// DXCC : field "dxcc" (no pattern) → entity number
|
// DXCC : field "dxcc" (no pattern) → entity number
|
||||||
// WAS : field "state", DXCCFilter [291,110,6] → US state
|
// WAS : field "state", DXCCFilter [291,110,6] → US state
|
||||||
// DDFM : field "note", pattern "D(\d{1,2}[AB]?)" → French department from notes
|
// DDFM : field "note", pattern "D(\d{1,2}[AB]?)" → French department from notes
|
||||||
@@ -1326,7 +1327,11 @@ func setToSorted(m map[string]struct{}) []string {
|
|||||||
return out
|
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 {
|
func sortedBands(m map[string]int) []string {
|
||||||
idx := map[string]int{}
|
idx := map[string]int{}
|
||||||
|
|||||||
@@ -776,6 +776,14 @@ func stateUserEqual(a, b RigState) bool {
|
|||||||
func BandFromHz(hz int64) string {
|
func BandFromHz(hz int64) string {
|
||||||
mhz := float64(hz) / 1_000_000
|
mhz := float64(hz) / 1_000_000
|
||||||
switch {
|
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:
|
case mhz >= 1.8 && mhz <= 2.0:
|
||||||
return "160m"
|
return "160m"
|
||||||
case mhz >= 3.5 && mhz <= 4.0:
|
case mhz >= 3.5 && mhz <= 4.0:
|
||||||
@@ -810,6 +818,30 @@ func BandFromHz(hz int64) string {
|
|||||||
return "33cm"
|
return "33cm"
|
||||||
case mhz >= 1240.0 && mhz <= 1300.0:
|
case mhz >= 1240.0 && mhz <= 1300.0:
|
||||||
return "23cm"
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -682,6 +682,28 @@ func bandFromHz(hz int64) string {
|
|||||||
return "33cm"
|
return "33cm"
|
||||||
case mhz >= 1240.0 && mhz <= 1300.0:
|
case mhz >= 1240.0 && mhz <= 1300.0:
|
||||||
return "23cm"
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user