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) } }