feat: docked watch-list panel, and auto-call learns the orthogonal markers
The watch list was a tab, and an operator working FT8 lives on the decodes one: a station they had asked to be told about turned up on a screen they were not looking at. The same answer is now docked in the widget strip, above the tabs, reduced to what is worth acting on — on the air and still needed, one row per band and mode, with the cluster's own NEW DXCC / NEW BAND / NEW SLOT badge and a click that tunes. Off by default. The "active and needed" answer costs a debounced query per visible slot, so it is written once (lib/watchlistSpots) and the tab uses it too. Auto-call: - It answers a new prefix, county, state, square or park. Those markers are orthogonal to the entity, they ranked as nothing-needed, and the engine sat through a never-worked WPX prefix calling CQ. New rung at the foot of the ladder, gated by the chase switches the badges use — which meant making those switches portable, since the backend cannot read localStorage. - It calls THROUGH a pileup. Giving up the moment the DX answered somebody else is precisely how a queue is not worked; the call and miss counters already bound the effort, and a station in mid-exchange is still never chosen as a new target. The PSK Reporter panel now follows the station auto-call is waiting for: the analysis takes a history query and a period or two to fill, so starting it when the DX comes free is starting it too late. Callbook lookup: a compound callsign with a page of its OWN keeps that page's location. QRZ files HP/WE9G under exactly that form, with the Panama square the station is operating from, and the rule that drops a home address from a portable call was throwing it away. The record's own country tells an operation's page from a home page. Changelog: entries may open with [NEW], drawn as a pill in the What's new dialog — a release is mostly fixes and the two or three genuinely new things should not have to be found by reading all of it.
This commit is contained in:
@@ -65,6 +65,9 @@ func TestLadderOrder(t *testing.T) {
|
||||
cq("I", NeedNone, 0, watched),
|
||||
cq("B", NeedDXCC, 0), cq("D", NeedBand, 0),
|
||||
cq("F", NeedMode, 0), cq("H", NeedSlot, 0),
|
||||
// The orthogonal markers sit at the foot of the ladder: worth a call
|
||||
// when nothing better is on the air, never worth leaving a band for.
|
||||
cq("J", NeedExtra, 0),
|
||||
}
|
||||
for i := 1; i < len(order); i++ {
|
||||
if rank(order[i-1]) <= rank(order[i]) {
|
||||
@@ -640,7 +643,7 @@ func TestTraceSaysWhyNobodyWasCalled(t *testing.T) {
|
||||
// And when it DOES call, the line names the candidates it ranked.
|
||||
lines = nil
|
||||
e.OnPeriod(period(2, cq("DX", NeedBand, -9, watched)))
|
||||
if len(lines) == 0 || !strings.Contains(lines[0], "DX(watched band,-9 dB,r114)") {
|
||||
if len(lines) == 0 || !strings.Contains(lines[0], "DX(watched band,-9 dB,r118)") {
|
||||
t.Errorf("trace %v does not describe the station it called", lines)
|
||||
}
|
||||
}
|
||||
@@ -678,34 +681,26 @@ func TestAStationCallingUsIsAnsweredEvenWithNothingToGain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestATargetThatStartsWorkingSomebodyElseIsDropped(t *testing.T) {
|
||||
// TestAStationWorkingSomebodyElseIsNotCHOSEN — the test above is about a target
|
||||
// already being called; this is about picking one. A reply to a decode in
|
||||
// mid-exchange is a reply WSJT-X and JTDX may refuse outright, so it never
|
||||
// starts a call there.
|
||||
func TestAStationWorkingSomebodyElseIsNotCHOSEN(t *testing.T) {
|
||||
e := on()
|
||||
// Picked on its CQ.
|
||||
if a := e.OnPeriod(period(0, cq("ON7GB", NeedSlot, +5))); a.Decode.Call != "ON7GB" {
|
||||
t.Fatalf("not called: %+v", a)
|
||||
}
|
||||
// Next period it is answering somebody else. Calling it is pointless: it is
|
||||
// committed, and the seven attempts would be spent transmitting at a
|
||||
// station that cannot hear them.
|
||||
busyNow := busy("ON7GB", NeedSlot, +5)
|
||||
busyNow.Msg = "PY2SAD ON7GB JO21"
|
||||
a := e.OnPeriod(period(2, busyNow))
|
||||
if a.Kind != DoHalt {
|
||||
t.Fatalf("kept calling a station in a QSO with someone else: %+v", a)
|
||||
}
|
||||
if !strings.Contains(a.Reason, "PY2SAD") {
|
||||
t.Errorf("reason %q does not name the station it is working", a.Reason)
|
||||
if a := e.OnPeriod(period(0, busyNow)); a.Kind == DoReply {
|
||||
t.Fatalf("called %+v — it is in a QSO with PY2SAD", a)
|
||||
}
|
||||
if e.Target() != "" {
|
||||
t.Error("the target was not released")
|
||||
t.Errorf("target is %q", e.Target())
|
||||
}
|
||||
// No verdict attached: the moment it CQs again it is fair game, with a full
|
||||
// allowance — it was never given up on.
|
||||
if a := e.OnPeriod(period(4, cq("ON7GB", NeedSlot, +5))); a.Kind != DoReply {
|
||||
t.Errorf("not called again once free: %+v", a)
|
||||
}
|
||||
if e.Status().Attempts != 0 {
|
||||
t.Errorf("attempts = %d on a fresh series, want 0", e.Status().Attempts)
|
||||
// Its final frame is different: one period from free is the best moment
|
||||
// there is to be calling it.
|
||||
last := busy("ON7GB", NeedSlot, +5)
|
||||
last.Msg = "PY2SAD ON7GB RR73"
|
||||
if a := e.OnPeriod(period(2, last)); a.Kind != DoReply {
|
||||
t.Errorf("%+v — a station on its last frame was not called", a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,29 +859,39 @@ func TestAnExchangeInProgressIsNeverAbandoned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheSlotIsNotWastedWhenTheTargetTurnsOutToBeBusy(t *testing.T) {
|
||||
// TestAPileupIsWorkedByCallingThroughIt is the shack report: D44TWO finishing a
|
||||
// contact, the call started, the DX coming back to somebody else — and the
|
||||
// engine giving up on a station that was one period from being free.
|
||||
func TestAPileupIsWorkedByCallingThroughIt(t *testing.T) {
|
||||
e := on()
|
||||
if a := e.OnPeriod(period(0, cq("DX", NeedBand, -7))); a.Kind != DoReply {
|
||||
if a := e.OnPeriod(period(0, cq("D44TWO", NeedDXCC, -7))); a.Kind != DoReply {
|
||||
t.Fatalf("%+v", a)
|
||||
}
|
||||
// It answers somebody else, and a station of the same value is calling CQ
|
||||
// in the very same period. Waiting for the next one throws away a slot.
|
||||
a := e.OnPeriod(period(2, busy("DX", NeedBand, -7), cq("ER1CW", NeedBand, -8)))
|
||||
if a.Kind != DoReply || a.Decode.Call != "ER1CW" {
|
||||
t.Fatalf("%+v — the freed slot was not used", a)
|
||||
// It answers three other callers in a row — which is what a DX with a queue
|
||||
// does, and calling through it is how the queue is joined.
|
||||
does := func(n int) Action { return e.OnPeriod(period(n, busy("D44TWO", NeedDXCC, -7))) }
|
||||
for _, n := range []int{2, 4, 6} {
|
||||
if a := does(n); a.Kind != DoNothing {
|
||||
t.Fatalf("%+v — stopped calling a station working the pileup", a)
|
||||
}
|
||||
if e.Target() != "D44TWO" {
|
||||
t.Fatalf("target is %q — the station was released while it worked others", e.Target())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(a.Reason, "cannot answer") || !strings.Contains(a.Reason, "calling ER1CW") {
|
||||
t.Errorf("reason %q says neither what was left nor what was taken", a.Reason)
|
||||
// And when it comes to us, the exchange starts as usual.
|
||||
if a := e.OnPeriod(period(8, callsMe("D44TWO", NeedDXCC, -7))); a.Kind != DoNothing {
|
||||
t.Errorf("%+v", a)
|
||||
}
|
||||
if !e.answered {
|
||||
t.Error("the reply was not taken as the start of the exchange")
|
||||
}
|
||||
|
||||
// Mid-over, it still waits: cutting our own transmission in half is worse
|
||||
// than losing the slot.
|
||||
// A BETTER station may still take the slot while it is busy: nothing has
|
||||
// been answered, so nothing is lost by moving.
|
||||
e = on()
|
||||
e.OnPeriod(period(4, cq("DX", NeedBand, -7)))
|
||||
pp := period(6, busy("DX", NeedBand, -7), cq("ER1CW", NeedBand, -8))
|
||||
pp.TX = TXState{Transmitting: true}
|
||||
if a := e.OnPeriod(pp); a.Kind != DoHalt || !a.Soft {
|
||||
t.Errorf("%+v — replied over our own transmission", a)
|
||||
e.OnPeriod(period(10, cq("D44TWO", NeedSlot, -7)))
|
||||
if a := e.OnPeriod(period(12, busy("D44TWO", NeedSlot, -7), cq("RARE", NeedDXCC, -20, watched))); a.Kind != DoReply || a.Decode.Call != "RARE" {
|
||||
t.Errorf("%+v — a watched new one did not take the slot from a busy station", a)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user