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:
+34
-1
@@ -74,6 +74,15 @@ type Flex struct {
|
||||
// end up outside the new window — see ZoomPan.
|
||||
panWindow map[string]panView
|
||||
|
||||
// spotSig is what each live spot LOOKS like, and when it was drawn. A
|
||||
// callsign spotted again with the same frequency, colour and comment does not
|
||||
// need to be removed and re-added: the panadapter would not change by one
|
||||
// pixel, and a busy RBN feed re-spots the same station every few seconds. One
|
||||
// two-minute session sent 2 128 adds and 1 634 removes, 88 of them for a
|
||||
// single callsign — see SendSpot.
|
||||
spotSig map[string]string
|
||||
spotSent map[string]time.Time
|
||||
|
||||
spotByCall map[string]int // callsign → live spot index, so re-spotting a call replaces its old spot (WSJT decodes re-fire every cycle)
|
||||
sentCmds map[int]string // seq → command text, so an R<seq> error names the command
|
||||
|
||||
@@ -205,7 +214,7 @@ func NewFlex(host string, port int, spotsEnabled bool) *Flex {
|
||||
return &Flex{
|
||||
host: strings.TrimSpace(host), port: port,
|
||||
slices: map[int]*flexSlice{}, spotsEnabled: spotsEnabled,
|
||||
spotIdx: map[int]bool{}, pendingSpot: map[int]string{}, pendingSpotMode: map[int]string{}, spotCall: map[int]string{}, spotMode: map[int]string{}, spotFreq: map[int]int64{}, pendingSpotFreq: map[int]int64{}, panWindow: map[string]panView{}, spotByCall: map[string]int{}, pendingSplit: map[int]bool{},
|
||||
spotIdx: map[int]bool{}, pendingSpot: map[int]string{}, pendingSpotMode: map[int]string{}, spotCall: map[int]string{}, spotMode: map[int]string{}, spotFreq: map[int]int64{}, pendingSpotFreq: map[int]int64{}, panWindow: map[string]panView{}, spotSig: map[string]string{}, spotSent: map[string]time.Time{}, spotByCall: map[string]int{}, pendingSplit: map[int]bool{},
|
||||
meterMeta: map[int]meterInfo{}, meterVal: map[int]float64{}, meterSub: map[int]bool{},
|
||||
sentCmds: map[int]string{}, txSetAt: map[string]time.Time{},
|
||||
pinnedSlice: -1,
|
||||
@@ -859,6 +868,13 @@ func (f *Flex) handleStatus(payload string) {
|
||||
}
|
||||
f.mu.Lock()
|
||||
if removed {
|
||||
// The radio expired or dropped this spot: forget what it looked
|
||||
// like, or the next identical spot would be skipped as "already
|
||||
// drawn" and the station would never come back.
|
||||
if c, ok := f.spotCall[idx]; ok {
|
||||
delete(f.spotSig, strings.ToUpper(c))
|
||||
delete(f.spotSent, strings.ToUpper(c))
|
||||
}
|
||||
delete(f.spotIdx, idx)
|
||||
delete(f.spotCall, idx)
|
||||
delete(f.spotMode, idx)
|
||||
@@ -1390,7 +1406,22 @@ func (f *Flex) SendSpot(s SpotInfo) error {
|
||||
// and Go mutexes aren't reentrant (calling send while locked deadlocks the
|
||||
// whole Flex goroutine → the radio drops OFFLINE).
|
||||
upperCall := strings.ToUpper(s.Callsign)
|
||||
// Unchanged and still fresh → leave it alone.
|
||||
//
|
||||
// Refreshed once past half its lifetime, so a long-lived spot still gets its
|
||||
// timer wound on and never quietly expires off the panadapter; anything that
|
||||
// actually differs (a new frequency, a status that has changed colour, a
|
||||
// different comment) is redrawn at once.
|
||||
sig := fmt.Sprintf("%d|%s|%s|%s|%s", s.FreqHz, s.Mode, s.Color, s.BackgroundColor, s.Comment)
|
||||
f.mu.Lock()
|
||||
if prev, ok := f.spotSig[upperCall]; ok && prev == sig {
|
||||
if at, ok := f.spotSent[upperCall]; ok && time.Since(at) < time.Duration(life)*time.Second/2 {
|
||||
f.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
f.spotSig[upperCall] = sig
|
||||
f.spotSent[upperCall] = time.Now()
|
||||
old, hadOld := f.spotByCall[upperCall]
|
||||
if hadOld {
|
||||
delete(f.spotByCall, upperCall)
|
||||
@@ -1464,6 +1495,8 @@ func (f *Flex) ClearSpots() error {
|
||||
f.spotCall = map[int]string{}
|
||||
f.spotMode = map[int]string{}
|
||||
f.spotFreq = map[int]int64{}
|
||||
f.spotSig = map[string]string{}
|
||||
f.spotSent = map[string]time.Time{}
|
||||
f.spotByCall = map[string]int{}
|
||||
connected := f.conn != nil
|
||||
f.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user