feat(flex): send SmartSDR's spot priority; fix(chase): the same station on the same slot has nothing left to give
The panadapter has finite room: spots close in frequency are stacked behind a '+' and only one is drawn, chosen by PRIORITY — a parameter 'spot add' accepts and OpsLog never sent. So a new entity sat invisible behind three stations already in the log. DXHunter has sent it for years; the tiers here are the same idea in the operator's own words: the entity never worked (with my own callsign, for a multi-op), then band/mode/slot, then the reference hunts, then everything else. And a callsign already worked on this exact band and mode stops advertising a need. Working it again cannot turn a missing QSL into a confirmation — the QSO is already there — so shouting NEW DXCC over a station worked an hour ago only teaches an operator to distrust the colour. The need is real and stays on every OTHER station of the entity, which is where it can be answered. Applied on both paths out of the verdict, including the early one that leaves the loop first.
This commit is contained in:
@@ -9153,6 +9153,7 @@ func (a *App) clusterEventWorker() {
|
|||||||
FreqHz: s.FreqHz,
|
FreqHz: s.FreqHz,
|
||||||
Callsign: s.DXCall,
|
Callsign: s.DXCall,
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
|
Priority: spotPriority(status),
|
||||||
Comment: spotComment(s.Comment, s.Spotter, s.Country, status),
|
Comment: spotComment(s.Comment, s.Spotter, s.Country, status),
|
||||||
Color: col.Text,
|
Color: col.Text,
|
||||||
BackgroundColor: col.Bg,
|
BackgroundColor: col.Bg,
|
||||||
@@ -14398,6 +14399,7 @@ func (a *App) consumeUDPEvents() {
|
|||||||
BackgroundColor: bgCol,
|
BackgroundColor: bgCol,
|
||||||
Comment: spotComment(fmt.Sprintf("%s %+ddB", ev.Mode, ev.DecodeSNR), "", a.countryFor(ev.DecodeCall), status),
|
Comment: spotComment(fmt.Sprintf("%s %+ddB", ev.Mode, ev.DecodeSNR), "", a.countryFor(ev.DecodeCall), status),
|
||||||
LifetimeSec: secs,
|
LifetimeSec: secs,
|
||||||
|
Priority: spotPriority(status),
|
||||||
}:
|
}:
|
||||||
default:
|
default:
|
||||||
// Radio not keeping up: skip the spot rather than stall the
|
// Radio not keeping up: skip the spot rather than stall the
|
||||||
@@ -21043,6 +21045,12 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
|
|||||||
if !worked {
|
if !worked {
|
||||||
out[i].Status = "new"
|
out[i].Status = "new"
|
||||||
if _, ever := entities[dxccNum]; ever {
|
if _, ever := entities[dxccNum]; ever {
|
||||||
|
// Worked, never confirmed — and if it was THIS station on THIS
|
||||||
|
// slot, the need is not its to answer (see the rule below).
|
||||||
|
if out[i].WorkedSlot {
|
||||||
|
out[i].Status = "worked"
|
||||||
|
continue
|
||||||
|
}
|
||||||
out[i].UnconfStatus = true
|
out[i].UnconfStatus = true
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -21079,6 +21087,21 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// THIS station, already in the log on THIS slot, has nothing left to
|
||||||
|
// give. Working it a second time on the same band and mode cannot make
|
||||||
|
// the entity count: the QSO is already there and only the QSL is
|
||||||
|
// missing, and a duplicate does not produce one. So the need is real —
|
||||||
|
// it is simply not this station's to answer, and shouting NEW DXCC over
|
||||||
|
// a callsign worked an hour ago is how an operator learns to distrust
|
||||||
|
// the colour. Another station in the entity still carries the badge.
|
||||||
|
//
|
||||||
|
// Only for a need that IS a missing confirmation: a genuinely new
|
||||||
|
// entity cannot co-exist with a worked callsign, so nothing real is
|
||||||
|
// suppressed here.
|
||||||
|
if out[i].UnconfStatus && out[i].WorkedSlot {
|
||||||
|
out[i].Status = "worked"
|
||||||
|
out[i].UnconfStatus = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -15,7 +15,9 @@
|
|||||||
"New DXHunter page in Preferences: every Chase setting moves there from the DX Cluster page — the hunt, its confirmation sources, and the POTA / SOTA / US counties / prefixes / grids switches. They govern three screens now, so they no longer live under the name of one of them; more of DXHunter’s ideas will land beside them.",
|
"New DXHunter page in Preferences: every Chase setting moves there from the DX Cluster page — the hunt, its confirmation sources, and the POTA / SOTA / US counties / prefixes / grids switches. They govern three screens now, so they no longer live under the name of one of them; more of DXHunter’s ideas will land beside them.",
|
||||||
"Chase US states joins the other switches: unchecked, the NEW STATE badge and its filter chip go quiet everywhere.",
|
"Chase US states joins the other switches: unchecked, the NEW STATE badge and its filter chip go quiet everywhere.",
|
||||||
"FT Map: zooming no longer leaves a dead strip along the bottom. Leaflet measures its container once, when the map is created — which here is the instant the tab is selected, before the layout has settled — and it is now told whenever that box changes size.",
|
"FT Map: zooming no longer leaves a dead strip along the bottom. Leaflet measures its container once, when the map is created — which here is the instant the tab is selected, before the layout has settled — and it is now told whenever that box changes size.",
|
||||||
"FT decodes: the continent filter shows all seven, always, instead of only those currently on the feed — the row no longer reshuffles under the pointer when the first Asian station decodes, and it says what the filter can do before anything has been heard."
|
"FT decodes: the continent filter shows all seven, always, instead of only those currently on the feed — the row no longer reshuffles under the pointer when the first Asian station decodes, and it says what the filter can do before anything has been heard.",
|
||||||
|
"Panadapter spots now carry SmartSDR’s priority: an entity never worked comes first, then the band / mode / slot needs, then POTA, SOTA, county and prefix, and everything else last. The radio stacks spots that sit close in frequency behind a “+” and draws only one — it picks by priority, so a new DXCC was disappearing behind stations already in the log simply because OpsLog never said which was worth the space.",
|
||||||
|
"A station already worked on the SAME band and mode no longer advertises a need. Working it a second time cannot turn a missing QSL into a confirmation, so the badge goes quiet on that callsign — and stays on every other station of the entity, which is where the need can actually be answered."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"DX Cluster : un serveur déconnecté garde sa pastille et peut donc être reconnecté — le déconnecter le faisait disparaître avec le seul moyen d’y revenir.",
|
"DX Cluster : un serveur déconnecté garde sa pastille et peut donc être reconnecté — le déconnecter le faisait disparaître avec le seul moyen d’y revenir.",
|
||||||
@@ -30,7 +32,9 @@
|
|||||||
"Nouvelle page DXHunter dans les Préférences : tous les réglages de chasse y déménagent depuis la page DX Cluster — le mode de chasse, ses sources de confirmation et les cases POTA / SOTA / comtés US / préfixes / locators. Ils commandent trois écrans, ils ne vivent donc plus sous le nom d’un seul ; d’autres idées de DXHunter viendront s’y ajouter.",
|
"Nouvelle page DXHunter dans les Préférences : tous les réglages de chasse y déménagent depuis la page DX Cluster — le mode de chasse, ses sources de confirmation et les cases POTA / SOTA / comtés US / préfixes / locators. Ils commandent trois écrans, ils ne vivent donc plus sous le nom d’un seul ; d’autres idées de DXHunter viendront s’y ajouter.",
|
||||||
"Chasser les états US rejoint les autres cases : décochée, le badge NOUVEL ÉTAT et sa puce de filtre se taisent partout.",
|
"Chasser les états US rejoint les autres cases : décochée, le badge NOUVEL ÉTAT et sa puce de filtre se taisent partout.",
|
||||||
"FT Map : le zoom ne laisse plus une bande morte en bas. Leaflet mesure son conteneur une seule fois, à la création de la carte — ici l’instant où l’onglet est sélectionné, avant que la mise en page ne se soit stabilisée — et il est désormais prévenu à chaque changement de taille.",
|
"FT Map : le zoom ne laisse plus une bande morte en bas. Leaflet mesure son conteneur une seule fois, à la création de la carte — ici l’instant où l’onglet est sélectionné, avant que la mise en page ne se soit stabilisée — et il est désormais prévenu à chaque changement de taille.",
|
||||||
"FT decodes : le filtre continent affiche les sept, toujours, au lieu des seuls présents dans le flux — la rangée ne se réorganise plus sous le pointeur quand la première station asiatique décode, et elle annonce ce que le filtre sait faire avant même d’avoir entendu quoi que ce soit."
|
"FT decodes : le filtre continent affiche les sept, toujours, au lieu des seuls présents dans le flux — la rangée ne se réorganise plus sous le pointeur quand la première station asiatique décode, et elle annonce ce que le filtre sait faire avant même d’avoir entendu quoi que ce soit.",
|
||||||
|
"Les spots du panadapter portent désormais la priorité SmartSDR : une entité jamais contactée d’abord, puis les besoins bande / mode / slot, puis POTA, SOTA, comté et préfixe, et le reste en dernier. La radio empile les spots proches en fréquence derrière un « + » et n’en dessine qu’un — elle choisit par priorité, si bien qu’un nouveau DXCC disparaissait derrière des stations déjà au log, faute pour OpsLog d’avoir dit laquelle méritait la place.",
|
||||||
|
"Une station déjà contactée sur la MÊME bande et le même mode n’annonce plus de besoin. La recontacter ne transformera pas une QSL manquante en confirmation : le badge se tait sur cet indicatif — et reste sur toutes les autres stations de l’entité, là où le besoin peut réellement être comblé."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -304,6 +304,14 @@ type SpotInfo struct {
|
|||||||
BackgroundColor string
|
BackgroundColor string
|
||||||
Comment string
|
Comment string
|
||||||
LifetimeSec int // panadapter display seconds before auto-removal (0 = backend default)
|
LifetimeSec int // panadapter display seconds before auto-removal (0 = backend default)
|
||||||
|
// Priority is SmartSDR's own tie-breaker, 1 (highest) to 5.
|
||||||
|
//
|
||||||
|
// It matters because the panadapter has finite room: spots close in
|
||||||
|
// frequency are stacked behind a "+" and only one of them is drawn. The
|
||||||
|
// radio picks that one by priority — so an entity never worked can sit
|
||||||
|
// invisible behind three stations already in the log unless we say which
|
||||||
|
// is worth the space. 0 leaves the field off the command entirely.
|
||||||
|
Priority int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spotter is an OPTIONAL backend capability: show cluster spots on the radio
|
// Spotter is an OPTIONAL backend capability: show cluster spots on the radio
|
||||||
|
|||||||
@@ -1557,6 +1557,10 @@ func (f *Flex) SendSpot(s SpotInfo) error {
|
|||||||
if hadOld {
|
if hadOld {
|
||||||
f.send(fmt.Sprintf("spot remove %d", old))
|
f.send(fmt.Sprintf("spot remove %d", old))
|
||||||
}
|
}
|
||||||
|
prio := ""
|
||||||
|
if s.Priority >= 1 && s.Priority <= 5 {
|
||||||
|
prio = fmt.Sprintf(" priority=%d", s.Priority)
|
||||||
|
}
|
||||||
cmd := fmt.Sprintf("spot add rx_freq=%.6f callsign=%s color=%s source=OpsLog lifetime_seconds=%d trigger_action=Tune timestamp=%d",
|
cmd := fmt.Sprintf("spot add rx_freq=%.6f callsign=%s color=%s source=OpsLog lifetime_seconds=%d trigger_action=Tune timestamp=%d",
|
||||||
float64(s.FreqHz)/1e6, call, color, life, time.Now().Unix())
|
float64(s.FreqHz)/1e6, call, color, life, time.Now().Unix())
|
||||||
// Convert to a real Flex mode (USB/LSB/CW/DIGU/…): SmartSDR only switches the
|
// Convert to a real Flex mode (USB/LSB/CW/DIGU/…): SmartSDR only switches the
|
||||||
@@ -1575,6 +1579,7 @@ func (f *Flex) SendSpot(s SpotInfo) error {
|
|||||||
if c := flexEncode(s.Comment); c != "" {
|
if c := flexEncode(s.Comment); c != "" {
|
||||||
cmd += " comment=" + c
|
cmd += " comment=" + c
|
||||||
}
|
}
|
||||||
|
cmd += prio
|
||||||
seq := f.send(cmd)
|
seq := f.send(cmd)
|
||||||
if seq > 0 {
|
if seq > 0 {
|
||||||
// Remember which call this add was for; the R<seq> response carries the
|
// Remember which call this add was for; the R<seq> response carries the
|
||||||
|
|||||||
@@ -314,6 +314,30 @@ func bracket(s string) string {
|
|||||||
//
|
//
|
||||||
// Read from the same cached palette as the colours — see spotColorFor — because
|
// Read from the same cached palette as the colours — see spotColorFor — because
|
||||||
// it is consulted on every spot from every cluster.
|
// it is consulted on every spot from every cluster.
|
||||||
|
// spotPriority ranks a spot for SmartSDR's own tie-breaker, 1 (highest) to 5.
|
||||||
|
//
|
||||||
|
// The panadapter has finite room: spots close in frequency are stacked behind a
|
||||||
|
// "+" and only one of them is drawn. The radio chooses by priority — so an
|
||||||
|
// entity never worked was disappearing behind three stations already in the
|
||||||
|
// log, simply because OpsLog never said which was worth the space.
|
||||||
|
//
|
||||||
|
// The tiers are the operator's own: the entity never worked first, then the
|
||||||
|
// pieces of one already worked (band, mode, slot), then the reference hunts
|
||||||
|
// (POTA, SOTA, county, prefix), and everything else last. My own callsign rides
|
||||||
|
// at the top with the first — it is how a multi-op sees where it already is.
|
||||||
|
func spotPriority(status string) int {
|
||||||
|
switch status {
|
||||||
|
case "new", "my-call":
|
||||||
|
return 1
|
||||||
|
case "new-band-mode", "new-band", "new-mode", "new-slot":
|
||||||
|
return 2
|
||||||
|
case "new-pota", "new-sota", "new-county", "new-pfx":
|
||||||
|
return 3
|
||||||
|
default:
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) spotHidden(status string) bool {
|
func (a *App) spotHidden(status string) bool {
|
||||||
a.spotColorsMu.Lock()
|
a.spotColorsMu.Lock()
|
||||||
if a.spotColorsCache == nil {
|
if a.spotColorsCache == nil {
|
||||||
|
|||||||
+45
-1
@@ -57,7 +57,9 @@ func TestClusterStatusFlagsUnconfirmedBand(t *testing.T) {
|
|||||||
add("ZS6GAV", "15m", "FT8", "N") // 15m worked, never confirmed
|
add("ZS6GAV", "15m", "FT8", "N") // 15m worked, never confirmed
|
||||||
add("ZS4AW", "15m", "FT8", "N")
|
add("ZS4AW", "15m", "FT8", "N")
|
||||||
|
|
||||||
got := a.ClusterSpotStatuses([]SpotQuery{{Call: "ZS4AW", Band: "15m", Mode: "FT8"}})
|
// A station of the entity that is NOT the one already worked on this slot:
|
||||||
|
// that case has a rule of its own, tested below.
|
||||||
|
got := a.ClusterSpotStatuses([]SpotQuery{{Call: "ZS9XX", Band: "15m", Mode: "FT8"}})
|
||||||
if len(got) != 1 {
|
if len(got) != 1 {
|
||||||
t.Fatalf("got %d results", len(got))
|
t.Fatalf("got %d results", len(got))
|
||||||
}
|
}
|
||||||
@@ -68,3 +70,45 @@ func TestClusterStatusFlagsUnconfirmedBand(t *testing.T) {
|
|||||||
t.Error("UnconfStatus is false — the badge draws solid, as if 15m had never been worked")
|
t.Error("UnconfStatus is false — the badge draws solid, as if 15m had never been worked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same station, already worked on the same band and mode, has nothing left
|
||||||
|
// to give: working it again cannot turn a missing QSL into a confirmation. The
|
||||||
|
// need belongs to the entity, not to that callsign, so the badge goes quiet on
|
||||||
|
// it — and stays on every other station of the entity.
|
||||||
|
func TestWorkedSameSlotDropsTheUnconfirmedBadge(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
conn, err := db.Open(filepath.Join(dir, "log.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { conn.Close() })
|
||||||
|
|
||||||
|
a := &App{ctx: context.Background(), qso: qso.NewRepo(conn), settings: settings.NewStore(conn)}
|
||||||
|
a.settingsScoped.Store(true)
|
||||||
|
_ = a.settings.Set(a.ctx, keyChaseMode, "new_unconfirmed")
|
||||||
|
_ = a.settings.Set(a.ctx, keyChaseConfirm, "lotw,card")
|
||||||
|
a.dxcc = dxcc.NewManager(filepath.Join("build", "bin", "data"))
|
||||||
|
if err := a.dxcc.LoadFromDisk(); err != nil {
|
||||||
|
t.Skipf("cty.dat not available here: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hz := int64(10136000)
|
||||||
|
if _, err := a.qso.Add(a.ctx, qso.QSO{
|
||||||
|
Callsign: "TN8GD", Band: "30m", Mode: "FT8", FreqHz: &hz,
|
||||||
|
QSODate: time.Now().UTC().Add(-24 * time.Hour),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := a.ClusterSpotStatuses([]SpotQuery{
|
||||||
|
{Call: "TN8GD", Band: "30m", Mode: "FT8"}, // the very station worked
|
||||||
|
{Call: "TN4XY", Band: "30m", Mode: "FT8"}, // another in the same entity
|
||||||
|
})
|
||||||
|
if got[0].Status == "new" || got[0].UnconfStatus {
|
||||||
|
t.Errorf("the worked station still advertises a need: status=%q unconf=%v",
|
||||||
|
got[0].Status, got[0].UnconfStatus)
|
||||||
|
}
|
||||||
|
if got[1].Status != "new" {
|
||||||
|
t.Errorf("another station in the entity lost its badge: status=%q", got[1].Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user