fix(pskr): a ten-minute window, both directions, and always an answer

Measured against DXHunter on the same station at the same second: 18
decodes here against 27 there.

- The window was five minutes. PSK Reporter's uploaders batch their
  reports, most of them every five, so a five-minute window catches
  about one upload cycle per station. Ten, as DXHunter has always had
  behind a label that says four.

- The history query ran once per target and only on the narrow feed.
  It now runs in both scopes and again every five minutes while a
  station is watched, which is the cadence the uploaders keep.

- It asked only what the target RECEIVED. Both directions now, so
  "who is hearing him" starts full too.

- The suggested call offset looked for a run of empty slots and said
  nothing when there was none — exactly the case it exists for: a
  hundred decodes across a 2800 Hz passband leave no gap. Failing a
  gap it names the quietest slot, ties to the higher offset, never
  above the ceiling.

Chase new: the receiver squares now follow the radius that was asked
for (it was one fixed ring whatever the setting said, so raising it
bought nothing), and the panel says what the feed is doing rather than
leaving an empty list to speak for itself. A change of hunt empties it:
its rows are verdicts reached under the old rule and nothing re-judged
them. Its own band selection, because what a station CAN work and what
is worth watching tonight are different questions.
This commit is contained in:
2026-09-05 21:45:39 +02:00
parent a41626955e
commit 470eaf5d80
4 changed files with 206 additions and 31 deletions
+28 -4
View File
@@ -99,10 +99,12 @@ func TestSuggestOffsetAvoidsTheOccupiedBinsAndTheCeiling(t *testing.T) {
if got < 1620 || got > 2340 {
t.Errorf("suggested %d Hz, want somewhere in the empty 1560-2400 run", got)
}
// A passband that stops low must not produce advice above it: transmitting
// past the DX's filter is the one outcome worse than picking a busy slot.
if got := suggestOffset(bins, 1500); got != 0 {
t.Errorf("suggested %d Hz with a 1500 Hz ceiling and no room, want none", got)
// A passband with no gap at all still gets an answer — the quietest slot,
// which is what an operator would look for by eye. What it must never do is
// advise ABOVE the ceiling: transmitting past the DX's filter is the one
// outcome worse than picking a busy slot.
if got := suggestOffset(bins, 1500); got <= 1000 || got > 1500 {
t.Errorf("suggested %d Hz with a full 1500 Hz passband, want a slot inside it", got)
}
if got := suggestOffset(nil, 0); got != 0 {
t.Errorf("suggested %d Hz with no data at all, want none", got)
@@ -132,3 +134,25 @@ func TestTopicsFollowTheScope(t *testing.T) {
t.Errorf("band scope = %v, want the one band-wide filter", got)
}
}
// The case reported from the air: a busy DX, 103 decodes across a 2805 Hz
// passband, and the panel drew the whole histogram with no advice under it.
func TestACrowdedPassbandStillGetsAnAnswer(t *testing.T) {
// Every bin from 1020 to 2800 occupied — no clear run anywhere, and the
// quietest slot is the answer.
bins := []Bin{}
for hz := 1020; hz <= 2760; hz += binHz {
n := 5
if hz == 2400 { // one slot noticeably quieter than the rest
n = 1
}
bins = append(bins, Bin{OffsetHz: hz, Count: n})
}
got := suggestOffset(bins, 2805)
if got == 0 {
t.Fatal("no advice on a full passband — this is exactly when it is wanted")
}
if got < 2400 || got > 2460 {
t.Errorf("suggested %d Hz, want the quietest slot around 2400", got)
}
}