fix: release TX when the elements stop; WSJT-X colours, and worked beats the watch list
The motorised antenna gagged the transmitter for a second or two after it had finished moving. Three delays were stacked: the antenna polled every two seconds, the gag held for three after the command whatever the antenna said, and the widget refreshed every three. The antenna is now asked four times a second WHILE IT MOVES — an idle one has nothing to say and stays at two seconds — the gag only bridges the command itself (900 ms), and the widget follows at half a second while moving. WSJT-X highlighting: - A watch-list station already worked on this band and mode is no longer painted as one to call. The list is a statement of intent, not of what is left to do, and its pink outranked every other verdict including the log's, so a worked station stayed pink for the session with nothing to tell it from one still needed. - The four colours are the operator's to choose (Settings → UDP). Only the background: the text colour is derived by luma, so a dark blue cannot come back as black-on-black in somebody else's window. Changing one clears the installed highlights, or the de-duplication would keep showing yesterday's colour until a callsign changed verdict.
This commit is contained in:
+115
-23
@@ -7,6 +7,8 @@ package main
|
||||
// the spot grid, so the two windows can never disagree.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hamlog/internal/applog"
|
||||
@@ -18,6 +20,13 @@ const (
|
||||
keyWsjtHighlight = "udp.wsjt.highlight"
|
||||
keyWsjtFollowMode = "udp.wsjt.followmode" // spot clicks switch the decoder's mode
|
||||
keyWsjtHLWorked = "udp.wsjt.highlight_worked"
|
||||
// One key per verdict, holding "#RRGGBB". Only the BACKGROUND is stored: the
|
||||
// text colour is computed from it, so a chosen colour can never come out
|
||||
// unreadable in somebody else's window.
|
||||
keyWsjtColWatchlist = "udp.wsjt.colour.watchlist"
|
||||
keyWsjtColNewDXCC = "udp.wsjt.colour.new_dxcc"
|
||||
keyWsjtColNewBand = "udp.wsjt.colour.new_band"
|
||||
keyWsjtColWorked = "udp.wsjt.colour.worked"
|
||||
)
|
||||
|
||||
// wsjtModes are the modes a Configure message can meaningfully ask for — the
|
||||
@@ -53,8 +62,9 @@ func (a *App) ConfigureDecoderMode(mode string) {
|
||||
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.
|
||||
// The DEFAULT palette. Fixed colours, not theme tokens — they are painted into
|
||||
// another application's window, which has no idea what theme OpsLog wears, and
|
||||
// the operator can change each of them (see WsjtHighlightColours).
|
||||
var (
|
||||
hlWatchlist = udp.RGB{R: 244, G: 114, B: 182} // the watchlist pink
|
||||
hlNewDXCC = udp.RGB{R: 22, G: 130, B: 60} // green
|
||||
@@ -64,10 +74,82 @@ var (
|
||||
// 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}
|
||||
hlWorked = udp.RGB{R: 75, G: 85, B: 99}
|
||||
)
|
||||
|
||||
// WsjtHighlightColours is the operator's palette, one background per verdict.
|
||||
type WsjtHighlightColours struct {
|
||||
Watchlist string `json:"watchlist"`
|
||||
NewDXCC string `json:"new_dxcc"`
|
||||
NewBand string `json:"new_band"`
|
||||
Worked string `json:"worked"`
|
||||
}
|
||||
|
||||
// GetWsjtHighlightColours returns the palette in "#RRGGBB", defaults included.
|
||||
func (a *App) GetWsjtHighlightColours() WsjtHighlightColours {
|
||||
return WsjtHighlightColours{
|
||||
Watchlist: a.settingOr(keyWsjtColWatchlist, hexOfRGB(hlWatchlist)),
|
||||
NewDXCC: a.settingOr(keyWsjtColNewDXCC, hexOfRGB(hlNewDXCC)),
|
||||
NewBand: a.settingOr(keyWsjtColNewBand, hexOfRGB(hlNewBand)),
|
||||
Worked: a.settingOr(keyWsjtColWorked, hexOfRGB(hlWorked)),
|
||||
}
|
||||
}
|
||||
|
||||
// SetWsjtHighlightColours stores the palette and repaints.
|
||||
//
|
||||
// The repaint is the whole point of clearing: the de-duplication remembers what
|
||||
// it has already told each decoder, so without this a callsign keeps yesterday's
|
||||
// colour until it changes verdict — and the operator, having just picked a new
|
||||
// one, sees nothing happen.
|
||||
func (a *App) SetWsjtHighlightColours(c WsjtHighlightColours) {
|
||||
set := func(key, v, def string) {
|
||||
if _, ok := parseHexRGB(v); !ok {
|
||||
v = def
|
||||
}
|
||||
a.setSetting(key, strings.ToUpper(strings.TrimSpace(v)))
|
||||
}
|
||||
set(keyWsjtColWatchlist, c.Watchlist, hexOfRGB(hlWatchlist))
|
||||
set(keyWsjtColNewDXCC, c.NewDXCC, hexOfRGB(hlNewDXCC))
|
||||
set(keyWsjtColNewBand, c.NewBand, hexOfRGB(hlNewBand))
|
||||
set(keyWsjtColWorked, c.Worked, hexOfRGB(hlWorked))
|
||||
a.clearWsjtHighlights()
|
||||
applog.Printf("wsjt highlight: palette changed — repainting")
|
||||
}
|
||||
|
||||
// colourFor reads one verdict's background and picks a legible foreground.
|
||||
//
|
||||
// The text colour is DERIVED, never stored: an operator choosing a dark blue
|
||||
// would otherwise get black text on it in somebody else's window and conclude
|
||||
// the feature is broken. Rec. 601 luma, the same rule a browser's contrast
|
||||
// checker uses, with the threshold where black stops being readable.
|
||||
func (a *App) colourFor(key, def string) (udp.RGB, udp.RGB) {
|
||||
bg, ok := parseHexRGB(a.settingOr(key, def))
|
||||
if !ok {
|
||||
bg, _ = parseHexRGB(def)
|
||||
}
|
||||
luma := (299*int(bg.R) + 587*int(bg.G) + 114*int(bg.B)) / 1000
|
||||
if luma < 140 {
|
||||
return bg, hlWhite
|
||||
}
|
||||
return bg, hlBlack
|
||||
}
|
||||
|
||||
func hexOfRGB(c udp.RGB) string { return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B) }
|
||||
|
||||
// parseHexRGB reads "#RRGGBB" (or "RRGGBB"). Anything else is refused rather
|
||||
// than half-read: a colour that silently becomes black is worse than a default.
|
||||
func parseHexRGB(s string) (udp.RGB, bool) {
|
||||
s = strings.TrimPrefix(strings.TrimSpace(s), "#")
|
||||
if len(s) != 6 {
|
||||
return udp.RGB{}, false
|
||||
}
|
||||
v, err := strconv.ParseUint(s, 16, 32)
|
||||
if err != nil {
|
||||
return udp.RGB{}, false
|
||||
}
|
||||
return udp.RGB{R: byte(v >> 16), G: byte(v >> 8), B: byte(v)}, true
|
||||
}
|
||||
|
||||
// GetWsjtHighlightWorked reports whether stations already worked on this band
|
||||
// and mode are greyed out as well.
|
||||
//
|
||||
@@ -174,42 +256,52 @@ func (a *App) maybeHighlightDecode(instance, call, band, mode string) {
|
||||
// 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 {
|
||||
c := a.clusterStatusMaps()
|
||||
// ALREADY WORKED HERE, whatever else the station is.
|
||||
//
|
||||
// Settled first because it is the one fact that cancels the others. A watch
|
||||
// list entry worked on this band and mode stayed pink for the rest of the
|
||||
// session — the list is a statement of intent, not of what is left to do, and
|
||||
// the colour that means "call this one" was being shown for a station already
|
||||
// in the log. From the operator's side there was no way to tell the two
|
||||
// apart, which is the only thing the colours are for.
|
||||
workedHere := false
|
||||
if band != "" && mode != "" && c.workedCallSlots != nil {
|
||||
m := strings.ToUpper(strings.TrimSpace(mode))
|
||||
if c.normMode != nil {
|
||||
m = c.normMode(m)
|
||||
}
|
||||
_, workedHere = c.workedCallSlots[strings.ToUpper(call)+"|"+strings.ToLower(band)+"|"+m]
|
||||
}
|
||||
if a.watchlist != nil && !workedHere {
|
||||
if _, ok := a.watchlist.Match(call); ok {
|
||||
c := hlWatchlist
|
||||
f := hlBlack
|
||||
return &c, &f, "watchlist"
|
||||
bgc, fgc := a.colourFor(keyWsjtColWatchlist, hexOfRGB(hlWatchlist))
|
||||
return &bgc, &fgc, "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
|
||||
bgc, fgc := a.colourFor(keyWsjtColNewDXCC, hexOfRGB(hlNewDXCC))
|
||||
return &bgc, &fgc, "new-dxcc"
|
||||
}
|
||||
if band != "" {
|
||||
if _, workedBand := ent.Bands[strings.ToLower(band)]; !workedBand {
|
||||
bgc, fgc := hlNewBand, hlBlack
|
||||
bgc, fgc := a.colourFor(keyWsjtColNewBand, hexOfRGB(hlNewBand))
|
||||
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"
|
||||
}
|
||||
// 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.
|
||||
// Its own option: on a well-filled log this matches most of a period, and a
|
||||
// screen where nearly every line is coloured has stopped saying anything.
|
||||
if workedHere && a.wsjtHLWorkedOn.Load() {
|
||||
bgc, fgc := a.colourFor(keyWsjtColWorked, hexOfRGB(hlWorked))
|
||||
return &bgc, &fgc, "worked"
|
||||
}
|
||||
return nil, nil, ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user