feat: panadapter spots that read, a CI-V link that survives, no auto-call
Panadapter spots are now worth reading. The comment carries the spotter, the
entity and the status in DXHunter's own shape — "CQ up 2 [F4BPO] [Franz Josef
Land] [New Slot]" — which needed two things nothing documents: SmartSDR splits
its command line on SPACES, so the words ran together until every space became
non-breaking; and it truncates past ~60 characters, so the cluster's own words
are trimmed first and the three brackets always survive. RBN column padding is
collapsed on the way in, or a preserved run of spaces opened a gap wide enough
to push the rest off screen.
"Already worked" means the CALLSIGN is in the log, not the entity: saying it of
a station never contacted was simply wrong. Each status can also be kept off the
panadapter entirely, and the WSJT-X decode spots obey the same switches — the
palette governs the panadapter, not one of the two things that feed it.
And the radio is no longer hammered: a spot whose frequency, colour and comment
are unchanged is not removed and redrawn. A busy skimmer feed re-spots the same
station every few seconds; one two-minute session sent 2128 adds, 88 of them for
a single callsign, and the display did not move a pixel for any of them.
CI-V, from an IC-7850 that kept killing JTDX: a reply the rig sent to another
controller on the same bus is no longer taken for ours, and a set_ptt, set_freq
or set_mode whose acknowledgement goes missing is verified by reading the rig
back instead of being reported as a failure. WSJT-X and JTDX answer a failed
command with a Rig Control Error and drop the link mid-over — 98 keyings, 6 lost
acknowledgements, 2 dropped connections in one session. The check waits 700 ms,
not the poll's 150: the rig has just failed to answer twice because it was
retuning, and a short probe would fail for the same reason.
Auto-call is withdrawn — it duplicated DXHunter, which already answers decodes,
and two programs deciding that from one shack key over each other. The library
is kept whole and dormant; a guard in App.tsx makes sure a stored preference
cannot key a transmitter whose switch no longer exists.
Also:
- the log rotates while running, not only at startup: the CI-V trace left on
wrote 416 MB and nothing would have stopped it before the disk did. Closing
it now releases the crash file too — the runtime keeps its own duplicate.
- the interface zoom announces itself, with a badge, a click back to 100% and
a View menu; Ctrl+wheel and Ctrl+0 always worked and nothing said so.
- no more elastic bounce, and no swipe-to-navigate out of the app.
- Edit QSO: your own TX power and the contacted station's extended locator
were saved and written back with no box to set them.
- FT decodes: continents are a multiple choice; a compound MSHV message that
answers two stations in one line is recognised as addressed to you.
This commit is contained in:
+109
-18
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A colour the Flex refuses is a spot that never appears, and the command error
|
||||
// goes to a log nobody is reading. Anything that is not #AARRGGBB must be
|
||||
@@ -47,30 +50,118 @@ func TestNormSpotColors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSpotComment(t *testing.T) {
|
||||
// The cluster's own text is kept: it carries the signal report and the
|
||||
// operator's note, which is why the spot is readable in the first place.
|
||||
if got := spotComment("CQ up 2", "new"); got != "CQ up 2 [NEW DXCC]" {
|
||||
t.Fatalf("got %q", got)
|
||||
// What the panadapter shows: the cluster's words, who spotted it, the entity,
|
||||
// then what it is worth — DXHunter's order, so an operator reads one
|
||||
// convention and not two.
|
||||
got := spotComment("CQ up 2", "F4BPO", "Franz Josef Land", "new-slot")
|
||||
want := "CQ up 2 [F4BPO] [Franz Josef Land] [New Slot]"
|
||||
if plain(got) != want {
|
||||
t.Fatalf("got %q, want %q", plain(got), want)
|
||||
}
|
||||
// An empty comment must not produce a leading space on the panadapter.
|
||||
if got := spotComment("", "new-band"); got != "[NEW BAND]" {
|
||||
t.Fatalf("got %q", got)
|
||||
// EVERY space is non-breaking, the ones inside the cluster comment included:
|
||||
// SmartSDR splits its command line on spaces, so a real one truncates the
|
||||
// comment at the first word.
|
||||
if strings.Contains(got, " ") {
|
||||
t.Fatalf("an ordinary space survived: %q", got)
|
||||
}
|
||||
// Nothing to say about a station already in the log.
|
||||
if got := spotComment("loud", "worked"); got != "loud" {
|
||||
t.Fatalf("got %q", got)
|
||||
if !strings.Contains(got, " ") {
|
||||
t.Fatal("no non-breaking space in the comment")
|
||||
}
|
||||
if got := spotComment("loud", "none"); got != "loud" {
|
||||
t.Fatalf("got %q", got)
|
||||
// Missing parts leave no empty brackets and no double separators.
|
||||
if p := plain(spotComment("", "", "", "new")); p != "[New DXCC]" {
|
||||
t.Fatalf("got %q", p)
|
||||
}
|
||||
// Every colourable status except the two quiet ones is labelled, or the
|
||||
if p := plain(spotComment("CQ", "", "Spain", "")); p != "CQ [Spain]" {
|
||||
t.Fatalf("got %q", p)
|
||||
}
|
||||
// A status with nothing to announce contributes no tag.
|
||||
if p := plain(spotComment("loud", "DL1ABC", "Germany", "none")); p != "loud [DL1ABC] [Germany]" {
|
||||
t.Fatalf("got %q", p)
|
||||
}
|
||||
// Every colourable status except the unresolved one is labelled, or the
|
||||
// palette would say something the text does not.
|
||||
for _, s := range spotColorOrder {
|
||||
if s == "worked" || s == "none" {
|
||||
for _, st := range spotColorOrder {
|
||||
if st == "none" {
|
||||
continue
|
||||
}
|
||||
if spotStatusTag(s) == "" {
|
||||
t.Fatalf("status %q has a colour but no tag", s)
|
||||
if spotStatusTag(st) == "" {
|
||||
t.Fatalf("status %q has a colour but no tag", st)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// plain puts the ordinary spaces back, for readable assertions.
|
||||
func plain(s string) string { return strings.ReplaceAll(s, " ", " ") }
|
||||
|
||||
// The per-status switch must survive a save with no colours on that row: it is
|
||||
// an instruction in its own right, and dropping it would quietly turn the status
|
||||
// back on. And a palette written before the switch existed must keep sending
|
||||
// everything — "no field" is not "the operator turned this off".
|
||||
func TestSpotColorHideSurvivesNormalisation(t *testing.T) {
|
||||
in := SpotColors{Enabled: true, Colors: map[string]SpotColor{
|
||||
"worked": {Hide: true}, // no colours, just the switch
|
||||
"new": {Text: "#FFFF3B30", Hide: true}, // both
|
||||
"none": {Text: "not a colour"}, // neither survives
|
||||
"new-band": {Text: "#FFFFCC00"}, // colour only
|
||||
}}
|
||||
out := normSpotColors(in)
|
||||
if c, ok := out.Colors["worked"]; !ok || !c.Hide {
|
||||
t.Error("a status hidden with no colour was dropped")
|
||||
}
|
||||
if c := out.Colors["new"]; !c.Hide || c.Text != "#FFFF3B30" {
|
||||
t.Errorf("hide and colour did not both survive: %+v", c)
|
||||
}
|
||||
if _, ok := out.Colors["none"]; ok {
|
||||
t.Error("an entry with nothing valid in it was kept")
|
||||
}
|
||||
if c := out.Colors["new-band"]; c.Hide {
|
||||
t.Error("a colour-only entry came back hidden")
|
||||
}
|
||||
// An older palette says nothing about hiding, and must send everything.
|
||||
old := normSpotColors(SpotColors{Enabled: true, Colors: map[string]SpotColor{"new": {Text: "#FFFF3B30"}}})
|
||||
if old.Colors["new"].Hide {
|
||||
t.Error("a palette from before the switch existed came back hidden")
|
||||
}
|
||||
}
|
||||
|
||||
// SmartSDR cuts a long comment mid-word — one came back from the radio as
|
||||
// "…[European Russia", losing its closing bracket and its status. What the
|
||||
// operator would keep must therefore survive: the spotter, the entity and the
|
||||
// status, with the cluster's own words trimmed to fit.
|
||||
func TestSpotCommentFitsTheRadio(t *testing.T) {
|
||||
long := "FT8 5 dB LN14 CQ DX FROM A VERY TALKATIVE SKIMMER NODE INDEED"
|
||||
got := plain(spotComment(long, "OH6BG-#", "European Russia", "new-band"))
|
||||
if len(got) > panCommentMax {
|
||||
t.Fatalf("comment is %d chars, over the %d the radio keeps: %q", len(got), panCommentMax, got)
|
||||
}
|
||||
for _, must := range []string{"[OH6BG-#]", "[European Russia]", "[New Band]"} {
|
||||
if !strings.Contains(got, must) {
|
||||
t.Errorf("%s was trimmed away — the cluster text should go first: %q", must, got)
|
||||
}
|
||||
}
|
||||
// A short comment is left exactly as it is.
|
||||
short := plain(spotComment("CQ", "K1ABC", "Spain", "new"))
|
||||
if short != "CQ [K1ABC] [Spain] [New DXCC]" {
|
||||
t.Fatalf("got %q", short)
|
||||
}
|
||||
// No room at all for the cluster's words: they go, the brackets stay.
|
||||
tight := plain(spotComment("CQ DX UP 2 LISTENING", "VERYLONGSPOTTER-#", "Democratic Republic of the Congo", "new"))
|
||||
if strings.Contains(tight, "LISTENING") {
|
||||
t.Errorf("kept the cluster text at the expense of the tags: %q", tight)
|
||||
}
|
||||
}
|
||||
|
||||
// RBN lines are column-aligned, so a comment arrives as "FT8 5 dB LN14
|
||||
// CQ". Preserved as non-breaking spaces, those runs rendered on the panadapter
|
||||
// as a gap wide enough to push the rest of the comment off the screen.
|
||||
func TestSpotCommentCollapsesClusterPadding(t *testing.T) {
|
||||
got := plain(spotComment("FT8 -13 dB CQ", "WC2L-#", "United States", "new"))
|
||||
want := "FT8 -13 dB CQ [WC2L-#] [United States] [New DXCC]"
|
||||
if got != want {
|
||||
t.Fatalf("got %q\nwant %q", got, want)
|
||||
}
|
||||
// Tabs and newlines are padding too.
|
||||
if p := plain(spotComment("CQ\t\tDX\n", "K1ABC", "", "")); p != "CQ DX [K1ABC]" {
|
||||
t.Fatalf("got %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user