fix(tci): pick the sideband from the frequency just commanded
Clicking a spot sets the frequency, then the mode 150 ms later, and SetMode chose USB or LSB from t.freqA — the frequency the radio had ECHOED. From 7 MHz onto a 14 MHz spot that echo has often not landed, so the sideband was computed for the band being left: LSB on 20 m. Clicking the same spot again, the echo having arrived, gave USB. Reported from a SunSDR as 'the frequency is right, the mode is wrong until I click twice'. SetFrequency now records what was asked for and SetMode prefers it until the radio's own echo confirms the move. The server's 'protocol:' announcement is also kept instead of being filed under unhandled: it names the ExpertSDR and its TCI version, and panorama spots need TCI 1.5 — older servers accept the spot commands and silently drop them, which is indistinguishable from a broken logger. Now it says so.
This commit is contained in:
+4
-2
@@ -4,11 +4,13 @@
|
|||||||
"date": "",
|
"date": "",
|
||||||
"en": [
|
"en": [
|
||||||
"LoTW download: the QSL details (QSL date, grid, state, county) are now optional and off by default — LoTW takes about ten times longer to build that report, twenty minutes against two on the same account, and marking a confirmation needs none of it. Still asked for automatically when adding the QSOs not found in the log.",
|
"LoTW download: the QSL details (QSL date, grid, state, county) are now optional and off by default — LoTW takes about ten times longer to build that report, twenty minutes against two on the same account, and marking a confirmation needs none of it. Still asked for automatically when adding the QSOs not found in the log.",
|
||||||
"Band map: the tooltip now also names a new prefix or a new grid square. They stay off the 22-pixel colour strip, but leaving them out of the text made the two panels look as though they disagreed — the cluster said NEW PFX about a spot the map called Worked, and both were right. The map’s “Worked” also says whose: it is the ENTITY that was worked on that band and mode, not the callsign, which is what made the two readings look contradictory."
|
"Band map: the tooltip now also names a new prefix or a new grid square. They stay off the 22-pixel colour strip, but leaving them out of the text made the two panels look as though they disagreed — the cluster said NEW PFX about a spot the map called Worked, and both were right. The map’s “Worked” also says whose: it is the ENTITY that was worked on that band and mode, not the callsign, which is what made the two readings look contradictory.",
|
||||||
|
"TCI (SunSDR): clicking a cluster spot no longer needs a second click to get the mode right — the sideband was chosen from the frequency the radio had last reported instead of the one just asked for. The log also names the ExpertSDR version now, and says so when it is older than the 1.5 that panorama spots need."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Téléchargement LoTW : les détails QSL (date du QSL, locator, état, comté) deviennent optionnels et désactivés par défaut — LoTW met environ dix fois plus longtemps à construire ce rapport, vingt minutes contre deux sur le même compte, et marquer une confirmation n'en a pas besoin. Toujours demandés automatiquement quand on ajoute les QSO absents du log.",
|
"Téléchargement LoTW : les détails QSL (date du QSL, locator, état, comté) deviennent optionnels et désactivés par défaut — LoTW met environ dix fois plus longtemps à construire ce rapport, vingt minutes contre deux sur le même compte, et marquer une confirmation n'en a pas besoin. Toujours demandés automatiquement quand on ajoute les QSO absents du log.",
|
||||||
"Carte des bandes : l'infobulle indique aussi un nouveau préfixe ou un nouveau locator. Ils restent hors de la bande de couleur de 22 pixels, mais les omettre du texte donnait l'impression que les deux panneaux se contredisaient — le cluster annonçait NOUVEAU PFX pour un spot que la carte disait contacté, et les deux avaient raison. Le « Contacté » de la carte dit aussi de qui il parle : c'est l'ENTITÉ qui a été contactée sur cette bande et ce mode, pas l'indicatif — d'où l'impression de contradiction."
|
"Carte des bandes : l'infobulle indique aussi un nouveau préfixe ou un nouveau locator. Ils restent hors de la bande de couleur de 22 pixels, mais les omettre du texte donnait l'impression que les deux panneaux se contredisaient — le cluster annonçait NOUVEAU PFX pour un spot que la carte disait contacté, et les deux avaient raison. Le « Contacté » de la carte dit aussi de qui il parle : c'est l'ENTITÉ qui a été contactée sur cette bande et ce mode, pas l'indicatif — d'où l'impression de contradiction.",
|
||||||
|
"TCI (SunSDR) : cliquer un spot du cluster ne demande plus un second clic pour obtenir le bon mode — la bande latérale était choisie d'après la fréquence encore annoncée par la radio au lieu de celle qu'on venait de demander. Le journal indique aussi la version d'ExpertSDR, et signale si elle est antérieure à la 1.5 qu'exigent les spots sur le panorama."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ type TCI struct {
|
|||||||
|
|
||||||
digitalDefault string // surfaced when the rig reports a digital mode (FT8/…)
|
digitalDefault string // surfaced when the rig reports a digital mode (FT8/…)
|
||||||
spotsEnabled bool // mirror cluster spots onto the TCI panorama
|
spotsEnabled bool // mirror cluster spots onto the TCI panorama
|
||||||
|
// wantFreq is the frequency last COMMANDED and not yet echoed back, used to
|
||||||
|
// pick the sideband before the radio has confirmed the move.
|
||||||
|
wantFreq int64
|
||||||
|
// What the server said it is, from its "protocol:" announcement.
|
||||||
|
serverName string
|
||||||
|
serverVersion string
|
||||||
|
|
||||||
// OnSpotClick is called when the user clicks one of our spots on the TCI
|
// OnSpotClick is called when the user clicks one of our spots on the TCI
|
||||||
// panorama (callsign + freq), so the host can fill the entry form. Set before
|
// panorama (callsign + freq), so the host can fill the entry form. Set before
|
||||||
@@ -334,6 +340,11 @@ func (t *TCI) ReadState() (RigState, error) {
|
|||||||
|
|
||||||
// SetFrequency tunes VFO A (the main/RX VFO).
|
// SetFrequency tunes VFO A (the main/RX VFO).
|
||||||
func (t *TCI) SetFrequency(hz int64) error {
|
func (t *TCI) SetFrequency(hz int64) error {
|
||||||
|
// Remember what we ASKED for. SetMode reads it to choose the sideband, and
|
||||||
|
// the radio's own echo can be a moment behind — see SetMode.
|
||||||
|
t.mu.Lock()
|
||||||
|
t.wantFreq = hz
|
||||||
|
t.mu.Unlock()
|
||||||
return t.send(fmt.Sprintf("vfo:0,0,%d;", hz))
|
return t.send(fmt.Sprintf("vfo:0,0,%d;", hz))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +353,17 @@ func (t *TCI) SetFrequency(hz int64) error {
|
|||||||
func (t *TCI) SetMode(mode string) error {
|
func (t *TCI) SetMode(mode string) error {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
freq := t.freqA
|
freq := t.freqA
|
||||||
|
// Prefer the frequency we just COMMANDED over the one the radio has echoed.
|
||||||
|
//
|
||||||
|
// Clicking a spot sets the frequency and then the mode, and the sideband is
|
||||||
|
// chosen from the frequency (below 10 MHz → LSB). Read from the echo, that
|
||||||
|
// is the frequency we were on BEFORE the click whenever the echo has not
|
||||||
|
// landed yet: a 14 MHz spot clicked from 7 MHz got LSB, and clicking the same
|
||||||
|
// spot again — now that the echo has arrived — got USB. Reported from a
|
||||||
|
// SunSDR as "the frequency is right, the mode is wrong until I click twice".
|
||||||
|
if t.wantFreq > 0 {
|
||||||
|
freq = t.wantFreq
|
||||||
|
}
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
m := adifToTCIMode(mode, freq)
|
m := adifToTCIMode(mode, freq)
|
||||||
if m == "" {
|
if m == "" {
|
||||||
@@ -493,6 +515,18 @@ func (t *TCI) handle(msg string) {
|
|||||||
switch lower {
|
switch lower {
|
||||||
case "device":
|
case "device":
|
||||||
t.device = strings.TrimSpace(args)
|
t.device = strings.TrimSpace(args)
|
||||||
|
// The server's own announcement: "protocol:ExpertSDR3,1.9;" — its name and
|
||||||
|
// the TCI version it speaks. Worth keeping rather than filing under
|
||||||
|
// "unhandled": panorama spots need a version that HAS the spot command, and
|
||||||
|
// without this an operator on an older ExpertSDR sees nothing on the
|
||||||
|
// waterfall and nothing anywhere saying why.
|
||||||
|
case "protocol":
|
||||||
|
t.serverName, t.serverVersion = get(0), get(1)
|
||||||
|
debugLog.Printf("TCI: server is %s, TCI %s", t.serverName, t.serverVersion)
|
||||||
|
if t.spotsEnabled && tciSpotsUnsupported(t.serverVersion) {
|
||||||
|
debugLog.Printf("TCI: this server speaks TCI %s — panorama spots need 1.5 or later, so they will not appear",
|
||||||
|
t.serverVersion)
|
||||||
|
}
|
||||||
// The radio ANNOUNCES its audio format at connect —
|
// The radio ANNOUNCES its audio format at connect —
|
||||||
// "audio_stream_sample_type:float32" and "audio_stream_channels:2" — which
|
// "audio_stream_sample_type:float32" and "audio_stream_channels:2" — which
|
||||||
// is better evidence than anything derived from a frame, and it arrives
|
// is better evidence than anything derived from a frame, and it arrives
|
||||||
@@ -516,6 +550,10 @@ func (t *TCI) handle(msg string) {
|
|||||||
switch get(1) {
|
switch get(1) {
|
||||||
case "0":
|
case "0":
|
||||||
t.freqA = hz
|
t.freqA = hz
|
||||||
|
// The radio has caught up: from here the echo IS the truth.
|
||||||
|
if t.wantFreq != 0 && absInt64(hz-t.wantFreq) < 100 {
|
||||||
|
t.wantFreq = 0
|
||||||
|
}
|
||||||
case "1":
|
case "1":
|
||||||
t.freqB = hz
|
t.freqB = hz
|
||||||
}
|
}
|
||||||
@@ -651,3 +689,18 @@ func adifToTCIMode(mode string, freqHz int64) string {
|
|||||||
return "digu"
|
return "digu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tciSpotsUnsupported reports whether a TCI version predates the spot commands.
|
||||||
|
//
|
||||||
|
// SPOT / SPOT_DELETE / SPOT_CLEAR arrived in TCI 1.5. An older ExpertSDR accepts
|
||||||
|
// the connection, answers frequency and mode perfectly, and silently ignores
|
||||||
|
// every spot — which is indistinguishable from a bug in the logger unless
|
||||||
|
// somebody says so. Anything unparseable is treated as supported: refusing to
|
||||||
|
// draw on a doubt would be the worse mistake.
|
||||||
|
func tciSpotsUnsupported(version string) bool {
|
||||||
|
var maj, min int
|
||||||
|
if n, err := fmt.Sscanf(strings.TrimSpace(version), "%d.%d", &maj, &min); n < 2 || err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return maj < 1 || (maj == 1 && min < 5)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package cat
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTCISpotsUnsupported(t *testing.T) {
|
||||||
|
// SPOT arrived in TCI 1.5. The SunSDR2 PRO report that prompted this was an
|
||||||
|
// ExpertSDR announcing 1.3 — spots accepted and silently dropped.
|
||||||
|
for _, c := range []struct {
|
||||||
|
version string
|
||||||
|
old bool
|
||||||
|
}{
|
||||||
|
{"1.3", true},
|
||||||
|
{"1.4", true},
|
||||||
|
{"1.5", false},
|
||||||
|
{"1.9", false},
|
||||||
|
{"2.0", false},
|
||||||
|
{"", false}, // unparseable → assume it works
|
||||||
|
{"weird", false}, // refusing to draw on a doubt is the worse mistake
|
||||||
|
} {
|
||||||
|
if got := tciSpotsUnsupported(c.version); got != c.old {
|
||||||
|
t.Errorf("tciSpotsUnsupported(%q) = %v, want %v", c.version, got, c.old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user