Reported from a station running MSHV, WSJT-X and JTDX together: click a call in MSHV and the entry field filled, emptied, refilled — once a second — with the map zooming in and out to match. Two causes, both about reading one program's statement as another's. The clear was tracked per LISTENER. Several decoders commonly share one multicast group, so an idle WSJT-X reporting no DX Call — which is simply true, and which it repeats every second — was read as MSHV abandoning the station it was calling. "The operator cleared the DX Call" is a statement about one program, never about a socket, so it is now tracked per program, and a clear carries the id of whoever made it. And nothing arbitrated between them. The program that announces a station now holds the entry field, and the others cannot touch it until it lets go: it clears its own call, it stops sending (closed), or the QSO is logged. That is the operator's own suggestion, and it is the right one — between overs there is no way to tell "I have nothing" from "I am not the one you are working" except by remembering who was. Refusing another program's callsign is logged once per focus, not once a second: an operator whose second decoder "stopped filling the call" needs something to read.
109 lines
3.6 KiB
Go
109 lines
3.6 KiB
Go
package main
|
|
|
|
// Which decoder the entry field belongs to, when several are running.
|
|
//
|
|
// A station running WSJT-X, JTDX and MSHV at once has three programs sending
|
|
// Status once a second each. Click a call in one of them and only that one has
|
|
// a DX Call; the other two are idle and say so. Both statements are true, and
|
|
// both arrive — so the entry field is filled by the program the operator is
|
|
// working and emptied by the two that are not, once a second, and the map
|
|
// zooms in and out with it.
|
|
//
|
|
// So the first program to announce a station is FOCUSED, and until it lets go
|
|
// the others cannot touch the entry field. That is the operator's own answer:
|
|
// "if I call on one program, keep that one's UDP for the duration of the QSO".
|
|
//
|
|
// Focus is released when the focused program clears its own DX Call, when it
|
|
// stops sending altogether (it was closed), or when a QSO is logged — never on
|
|
// a timer that could hand the field to another program mid-over.
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"hamlog/internal/applog"
|
|
)
|
|
|
|
// udpFocusIdle is how long a focused program may go silent before the focus is
|
|
// given up.
|
|
//
|
|
// Generous on purpose: a decoder sends Status every second, so anything above a
|
|
// few seconds means it has been closed or has lost its network. Thirty is long
|
|
// enough to survive a machine that stutters and short enough that a program
|
|
// closed mid-QSO does not lock the entry field for the rest of the evening.
|
|
const udpFocusIdle = 30 * time.Second
|
|
|
|
type udpFocus struct {
|
|
mu sync.Mutex
|
|
inst string
|
|
at time.Time
|
|
// told marks that the log already carries the line explaining why another
|
|
// program's callsign is being ignored. Once per focus, not once a second.
|
|
told map[string]bool
|
|
}
|
|
|
|
// claim records that inst is announcing a station, and reports whether inst is
|
|
// the program the entry field currently belongs to.
|
|
func (f *udpFocus) claim(inst string) bool {
|
|
if inst == "" {
|
|
return true // a sender with no id: nothing to arbitrate between
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.inst == "" || f.inst == inst || time.Since(f.at) > udpFocusIdle {
|
|
if f.inst != inst {
|
|
applog.Printf("udp: the entry field follows %s while it is calling", inst)
|
|
f.told = nil
|
|
}
|
|
f.inst, f.at = inst, time.Now()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// holds reports whether inst may act on the entry field, without claiming it.
|
|
// Used for the clear: a program that is not focused clearing its own DX Call
|
|
// says nothing about the QSO in progress somewhere else.
|
|
func (f *udpFocus) holds(inst string) bool {
|
|
if inst == "" {
|
|
return true
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.inst == "" || time.Since(f.at) > udpFocusIdle {
|
|
return true
|
|
}
|
|
return f.inst == inst
|
|
}
|
|
|
|
// release gives the field up — the focused program cleared its call, or a QSO
|
|
// was logged and the next station may come from anywhere.
|
|
func (f *udpFocus) release(why string) {
|
|
f.mu.Lock()
|
|
had := f.inst
|
|
f.inst, f.at, f.told = "", time.Time{}, nil
|
|
f.mu.Unlock()
|
|
if had != "" {
|
|
applog.Printf("udp: the entry field is free again (%s let go: %s)", had, why)
|
|
}
|
|
}
|
|
|
|
// noteIgnored logs, once per focused program, that another one's callsign was
|
|
// not applied. Without it the behaviour is invisible: an operator whose second
|
|
// decoder "stopped filling the call" has nothing to read.
|
|
func (f *udpFocus) noteIgnored(inst, call string) {
|
|
f.mu.Lock()
|
|
if f.told == nil {
|
|
f.told = map[string]bool{}
|
|
}
|
|
first := !f.told[inst]
|
|
f.told[inst] = true
|
|
holder := f.inst
|
|
f.mu.Unlock()
|
|
if first {
|
|
applog.Printf("udp: [%s] %q not applied — %s has the entry field while it is calling",
|
|
inst, strings.ToUpper(call), holder)
|
|
}
|
|
}
|