216 lines
7.5 KiB
Go
216 lines
7.5 KiB
Go
package main
|
|
|
|
// Log-aware colours in WSJT-X / JTDX's own Band Activity window (message 13),
|
|
// the way JTAlert paints them: a decode of a watchlist member, a new DXCC or a
|
|
// new band for its entity is highlighted where the operator is actually
|
|
// looking. The verdicts come from the same cluster status cache that colours
|
|
// the spot grid, so the two windows can never disagree.
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"hamlog/internal/applog"
|
|
"hamlog/internal/dxcc"
|
|
udp "hamlog/internal/integrations/udp"
|
|
)
|
|
|
|
const (
|
|
keyWsjtHighlight = "udp.wsjt.highlight"
|
|
keyWsjtFollowMode = "udp.wsjt.followmode" // spot clicks switch the decoder's mode
|
|
keyWsjtHLWorked = "udp.wsjt.highlight_worked"
|
|
)
|
|
|
|
// wsjtModes are the modes a Configure message can meaningfully ask for — the
|
|
// decoder's own vocabulary. Anything else (CW, SSB, RTTY) is none of its
|
|
// business and is not sent.
|
|
var wsjtModes = map[string]bool{
|
|
"FT8": true, "FT4": true, "JT65": true, "JT9": true,
|
|
"MSK144": true, "Q65": true, "FST4": true, "JS8": false, // JS8Call speaks another protocol
|
|
}
|
|
|
|
// GetWsjtFollowMode reports whether spot clicks retune the decoder's mode.
|
|
func (a *App) GetWsjtFollowMode() bool {
|
|
return a.settingOr(keyWsjtFollowMode, "1") == "1"
|
|
}
|
|
|
|
// SetWsjtFollowMode flips it.
|
|
func (a *App) SetWsjtFollowMode(on bool) {
|
|
v := "0"
|
|
if on {
|
|
v = "1"
|
|
}
|
|
a.setSetting(keyWsjtFollowMode, v)
|
|
}
|
|
|
|
// ConfigureDecoderMode asks the connected decoders to switch mode — called by
|
|
// the frontend after a spot click has tuned the radio. A no-op for modes the
|
|
// decoder does not speak, and when the option is off or nothing is connected.
|
|
func (a *App) ConfigureDecoderMode(mode string) {
|
|
mode = strings.ToUpper(strings.TrimSpace(mode))
|
|
if a.udp == nil || !wsjtModes[mode] || !a.GetWsjtFollowMode() {
|
|
return
|
|
}
|
|
a.udp.SendConfigureMode(mode)
|
|
}
|
|
|
|
// The palette. Fixed colours, not theme tokens — they are painted into another
|
|
// application's window, which has no idea what theme OpsLog wears.
|
|
var (
|
|
hlWatchlist = udp.RGB{R: 244, G: 114, B: 182} // the watchlist pink
|
|
hlNewDXCC = udp.RGB{R: 22, G: 130, B: 60} // green
|
|
hlNewBand = udp.RGB{R: 226, G: 122, B: 24} // orange
|
|
hlWhite = udp.RGB{R: 255, G: 255, B: 255}
|
|
hlBlack = udp.RGB{R: 20, G: 20, B: 20}
|
|
// Worked already, on this band and in this mode. Grey on purpose, and the
|
|
// only DIM colour of the four: the others say "look at this", and this one
|
|
// says the opposite — it has to recede, not compete with them.
|
|
hlWorked = udp.RGB{R: 75, G: 85, B: 99}
|
|
hlWorkedFg = udp.RGB{R: 203, G: 213, B: 225}
|
|
)
|
|
|
|
// GetWsjtHighlightWorked reports whether stations already worked on this band
|
|
// and mode are greyed out as well.
|
|
//
|
|
// Its own switch, and off by default. The other three verdicts pick out a
|
|
// handful of decodes in a period; this one can match most of them on a
|
|
// well-filled log, and a screen where nearly every line is coloured has stopped
|
|
// saying anything. It is worth having only for an operator who wants the dupes
|
|
// struck out rather than the catches picked out.
|
|
func (a *App) GetWsjtHighlightWorked() bool {
|
|
return a.settingOr(keyWsjtHLWorked, "0") == "1"
|
|
}
|
|
|
|
// SetWsjtHighlightWorked flips it, and repaints.
|
|
//
|
|
// Turning it OFF has to clear what it painted: those callsigns keep their grey
|
|
// in the decoder's window otherwise, and nothing would ever say another word
|
|
// about them — the de-duplication remembers that they were already told.
|
|
func (a *App) SetWsjtHighlightWorked(on bool) {
|
|
v := "0"
|
|
if on {
|
|
v = "1"
|
|
}
|
|
a.setSetting(keyWsjtHLWorked, v)
|
|
a.wsjtHLWorkedOn.Store(on)
|
|
a.clearWsjtHighlights()
|
|
applog.Printf("wsjt highlight: worked stations %v", on)
|
|
}
|
|
|
|
// clearWsjtHighlights wipes every instruction OpsLog installed, in every
|
|
// running decoder, and forgets what it had said.
|
|
func (a *App) clearWsjtHighlights() {
|
|
if a.udp == nil {
|
|
return
|
|
}
|
|
for _, inst := range a.udp.Instances() {
|
|
_ = a.udp.SendClearHighlights(inst)
|
|
}
|
|
a.wsjtHLMu.Lock()
|
|
a.wsjtHLSent = map[string]string{}
|
|
a.wsjtHLMu.Unlock()
|
|
}
|
|
|
|
// GetWsjtHighlight reports whether decode highlighting is on.
|
|
func (a *App) GetWsjtHighlight() bool {
|
|
return a.settingOr(keyWsjtHighlight, "0") == "1"
|
|
}
|
|
|
|
// SetWsjtHighlight turns decode highlighting on or off. Turning it OFF also
|
|
// clears every instruction OpsLog installed in the running applications — a
|
|
// disabled option that leaves stale colours behind looks broken, not disabled.
|
|
func (a *App) SetWsjtHighlight(on bool) {
|
|
v := "0"
|
|
if on {
|
|
v = "1"
|
|
}
|
|
a.setSetting(keyWsjtHighlight, v)
|
|
a.wsjtHighlightOn.Store(on)
|
|
if !on {
|
|
a.clearWsjtHighlights()
|
|
applog.Printf("wsjt highlight: off — cleared in every instance")
|
|
}
|
|
}
|
|
|
|
// maybeHighlightDecode paints one decoded callsign in the instance that heard
|
|
// it, when the option is on and the verdict is worth a colour. De-duplicated
|
|
// per instance+call+verdict: a station CQing all evening is decoded four times
|
|
// a minute, and the instruction only needs to be said once.
|
|
func (a *App) maybeHighlightDecode(instance, call, band, mode string) {
|
|
if !a.wsjtHighlightOn.Load() || a.udp == nil || call == "" || instance == "" {
|
|
return
|
|
}
|
|
bg, fg, verdict := a.decodeHighlightVerdict(call, band, mode)
|
|
// The MODE is part of the key: one receiver can be handed FT8 and FT4 in
|
|
// the same session, and the verdict on a callsign is not the same in both.
|
|
key := instance + "|" + strings.ToUpper(call) + "|" + band + "|" + strings.ToUpper(mode)
|
|
a.wsjtHLMu.Lock()
|
|
if a.wsjtHLSent == nil {
|
|
a.wsjtHLSent = map[string]string{}
|
|
}
|
|
if len(a.wsjtHLSent) > 4000 { // bounded; a long session just re-says a few
|
|
a.wsjtHLSent = map[string]string{}
|
|
}
|
|
prev, had := a.wsjtHLSent[key]
|
|
if had && prev == verdict {
|
|
a.wsjtHLMu.Unlock()
|
|
return
|
|
}
|
|
a.wsjtHLSent[key] = verdict
|
|
a.wsjtHLMu.Unlock()
|
|
if verdict == "" {
|
|
// Was highlighted under an earlier verdict and no longer deserves it
|
|
// (the operator just worked them): clear that one callsign.
|
|
if had && prev != "" {
|
|
_ = a.udp.SendHighlight(instance, call, nil, nil, false)
|
|
}
|
|
return
|
|
}
|
|
_ = a.udp.SendHighlight(instance, call, bg, fg, false)
|
|
}
|
|
|
|
// decodeHighlightVerdict ranks a callsign: watchlist beats new-DXCC beats
|
|
// new-band, and "already worked here" comes last of all — it is the only
|
|
// verdict that says do NOT call, so anything worth calling for outranks it.
|
|
// Anything else is "no colour", and the empty verdict doubles as the clear
|
|
// signal in maybeHighlightDecode.
|
|
func (a *App) decodeHighlightVerdict(call, band, mode string) (bg, fg *udp.RGB, verdict string) {
|
|
if a.watchlist != nil {
|
|
if _, ok := a.watchlist.Match(call); ok {
|
|
c := hlWatchlist
|
|
f := hlBlack
|
|
return &c, &f, "watchlist"
|
|
}
|
|
}
|
|
c := a.clusterStatusMaps()
|
|
if a.dxcc != nil {
|
|
if m, ok := a.dxcc.Lookup(call); ok && m.Entity != nil {
|
|
num := dxcc.EntityDXCC(m.Entity.Name)
|
|
ent := c.entities[num]
|
|
if ent == nil {
|
|
bgc, fgc := hlNewDXCC, hlWhite
|
|
return &bgc, &fgc, "new-dxcc"
|
|
}
|
|
if band != "" {
|
|
if _, workedBand := ent.Bands[strings.ToLower(band)]; !workedBand {
|
|
bgc, fgc := hlNewBand, hlBlack
|
|
return &bgc, &fgc, "new-band"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Worked already, this exact callsign on this band in this mode — a dupe,
|
|
// judged by the same ledger and the same digital-mode grouping the cluster
|
|
// uses, so the two windows cannot disagree about what "worked" means.
|
|
if a.wsjtHLWorkedOn.Load() && band != "" && mode != "" {
|
|
m := strings.ToUpper(strings.TrimSpace(mode))
|
|
if c.normMode != nil {
|
|
m = c.normMode(m)
|
|
}
|
|
if _, ok := c.workedCallSlots[strings.ToUpper(call)+"|"+strings.ToLower(band)+"|"+m]; ok {
|
|
bgc, fgc := hlWorked, hlWorkedFg
|
|
return &bgc, &fgc, "worked"
|
|
}
|
|
}
|
|
return nil, nil, ""
|
|
}
|