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, "" }