Configure (message 15), mode field only, every other field sent as no-change: an FT4 spot clicked while WSJT-X sits in FT8 lands the operator ready to decode instead of staring at gibberish. Sent to every instance heard this session — one already in the right mode treats it as a no-op — and only for modes the decoder actually speaks (FT8/FT4/JT65/JT9/MSK144/ Q65/FST4); CW and SSB are none of its business. Toggle in the Connections panel, on by default.
156 lines
5.0 KiB
Go
156 lines
5.0 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
|
|
)
|
|
|
|
// 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}
|
|
)
|
|
|
|
// 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.udp != nil {
|
|
for _, inst := range a.udp.Instances() {
|
|
_ = a.udp.SendClearHighlights(inst)
|
|
}
|
|
a.wsjtHLMu.Lock()
|
|
a.wsjtHLSent = map[string]string{}
|
|
a.wsjtHLMu.Unlock()
|
|
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 string) {
|
|
if !a.wsjtHighlightOn.Load() || a.udp == nil || call == "" || instance == "" {
|
|
return
|
|
}
|
|
bg, fg, verdict := a.decodeHighlightVerdict(call, band)
|
|
key := instance + "|" + strings.ToUpper(call) + "|" + band
|
|
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; anything else is "no colour". The empty verdict doubles as the
|
|
// clear signal in maybeHighlightDecode.
|
|
func (a *App) decodeHighlightVerdict(call, band 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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, ""
|
|
}
|