fix: PGXL meters/auth/fan, TCI spots+click, PTT-over-CAT, CW <LOGQSO>
- PGXL: remote AUTH password; direct-link meter fallback (VITA-first) with 250 ms poll + peak-hold; fan mode uses bare "fanmode=" (was ignored). - TCI: spot colour sent as decimal ARGB (ExpertSDR dropped the hex string); spot click handled via CLICKED_ON_SPOT / RX_CLICKED_ON_SPOT to fill the entry. - FlexRadio: clicking an OpsLog spot on the panadapter now applies the mode too. - Filter presets: the trash icon deletes again (Radix pointer-down intercept). - PTT hotkey: keys over CAT when a backend is active (was hijacked by a stale Audio-tab RTS/DTR serial setting); AltGr keys allowed; presses logged. - CW macro <LOGQSO>: logs after the preceding CW is sent, not on the first letter.
This commit is contained in:
+21
-6
@@ -61,10 +61,12 @@ type Flex struct {
|
||||
|
||||
spotsEnabled bool // push cluster spots + manage the panadapter overlay
|
||||
spotIdx map[int]bool // panadapter spot indices currently known to the radio
|
||||
pendingSpot map[int]string // seq → callsign, awaiting the spot index in the R response
|
||||
pendingSplit map[int]bool // seq → awaiting the new TX slice's index (split create)
|
||||
spotCall map[int]string // spot index → callsign (to fill the call on a panadapter click)
|
||||
spotByCall map[string]int // callsign → live spot index, so re-spotting a call replaces its old spot (WSJT decodes re-fire every cycle)
|
||||
pendingSpot map[int]string // seq → callsign, awaiting the spot index in the R response
|
||||
pendingSpotMode map[int]string // seq → ADIF mode, paired with pendingSpot
|
||||
pendingSplit map[int]bool // seq → awaiting the new TX slice's index (split create)
|
||||
spotCall map[int]string // spot index → callsign (to fill the call on a panadapter click)
|
||||
spotMode map[int]string // spot index → ADIF mode, so a click can also set the slice mode (SmartSDR tunes the spot's freq but not its mode)
|
||||
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
|
||||
|
||||
// OnSpotClick is called (off the reader goroutine's hot path) when the user
|
||||
@@ -185,7 +187,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{}, spotCall: map[int]string{}, 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{}, 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,
|
||||
@@ -338,11 +340,17 @@ func (f *Flex) reader(conn net.Conn) {
|
||||
if idx, err := strconv.Atoi(mm[1]); err == nil {
|
||||
f.mu.Lock()
|
||||
call := f.spotCall[idx]
|
||||
mode := f.spotMode[idx]
|
||||
handler := f.OnSpotClick
|
||||
f.mu.Unlock()
|
||||
if call != "" && handler != nil {
|
||||
debugLog.Printf("Flex: spot %d triggered → %s", idx, call)
|
||||
debugLog.Printf("Flex: spot %d triggered → %s (mode %s)", idx, call, mode)
|
||||
go handler(call, 0)
|
||||
// SmartSDR tunes the spot's frequency on click but does NOT apply
|
||||
// its mode=, so set the slice mode ourselves from what we spotted.
|
||||
if mode != "" {
|
||||
go func(m string) { _ = f.SetMode(m) }(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,12 +387,15 @@ func (f *Flex) reader(conn net.Conn) {
|
||||
// pair it with the callsign we stashed under this seq.
|
||||
f.mu.Lock()
|
||||
call, pending := f.pendingSpot[seq]
|
||||
spotMode := f.pendingSpotMode[seq]
|
||||
if pending {
|
||||
delete(f.pendingSpot, seq)
|
||||
delete(f.pendingSpotMode, seq)
|
||||
}
|
||||
if pending && ok && len(parts) >= 3 {
|
||||
if idx, e := strconv.Atoi(strings.TrimSpace(parts[2])); e == nil {
|
||||
f.spotCall[idx] = call
|
||||
f.spotMode[idx] = spotMode
|
||||
f.spotIdx[idx] = true
|
||||
f.spotByCall[strings.ToUpper(call)] = idx
|
||||
}
|
||||
@@ -801,6 +812,7 @@ func (f *Flex) handleStatus(payload string) {
|
||||
if removed {
|
||||
delete(f.spotIdx, idx)
|
||||
delete(f.spotCall, idx)
|
||||
delete(f.spotMode, idx)
|
||||
} else {
|
||||
f.spotIdx[idx] = true
|
||||
}
|
||||
@@ -1263,6 +1275,7 @@ func (f *Flex) SendSpot(s SpotInfo) error {
|
||||
if hadOld {
|
||||
delete(f.spotByCall, upperCall)
|
||||
delete(f.spotCall, old)
|
||||
delete(f.spotMode, old)
|
||||
delete(f.spotIdx, old)
|
||||
}
|
||||
f.mu.Unlock()
|
||||
@@ -1288,6 +1301,7 @@ func (f *Flex) SendSpot(s SpotInfo) error {
|
||||
// (trigger) can be resolved back to the callsign.
|
||||
f.mu.Lock()
|
||||
f.pendingSpot[seq] = s.Callsign
|
||||
f.pendingSpotMode[seq] = s.Mode
|
||||
f.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
@@ -1320,6 +1334,7 @@ func (f *Flex) ClearSpots() error {
|
||||
f.mu.Lock()
|
||||
f.spotIdx = map[int]bool{}
|
||||
f.spotCall = map[int]string{}
|
||||
f.spotMode = map[int]string{}
|
||||
f.spotByCall = map[string]int{}
|
||||
connected := f.conn != nil
|
||||
f.mu.Unlock()
|
||||
|
||||
+47
-14
@@ -116,15 +116,32 @@ func (t *TCI) SendSpot(s SpotInfo) error {
|
||||
if call == "" || s.FreqHz <= 0 {
|
||||
return nil
|
||||
}
|
||||
color := strings.TrimSpace(s.Color)
|
||||
if color == "" {
|
||||
color = "#FFFFA500" // opaque orange default
|
||||
// TCI's SPOT command wants the colour as a signed 32-bit DECIMAL integer in
|
||||
// 0xAARRGGBB order — NOT a "0x…" hex string (e.g. "spot:UN7GK,cw,14025000,
|
||||
// -16776961,test;"). ExpertSDR silently drops a spot whose colour field it
|
||||
// can't parse as a number, which is why spots never showed on the panorama
|
||||
// while tuning (a separate command) still worked.
|
||||
hex := strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(s.Color), "#"), "0x")
|
||||
if hex == "" {
|
||||
hex = "FFFFA500" // opaque orange default
|
||||
}
|
||||
if len(hex) == 6 {
|
||||
hex = "FF" + hex // add full-opacity alpha when only RGB was supplied
|
||||
}
|
||||
argb, err := strconv.ParseUint(hex, 16, 32)
|
||||
if err != nil {
|
||||
argb = 0xFFFFA500
|
||||
}
|
||||
// Use a valid TCI modulation (usb/lsb/cw/digl…) so ExpertSDR accepts the spot;
|
||||
// fall back to the raw label if we can't map it. The click-to-tune path already
|
||||
// maps the mode separately, so this only affects the spot's displayed mode.
|
||||
mode := adifToTCIMode(s.Mode, s.FreqHz)
|
||||
if mode == "" {
|
||||
mode = strings.ToLower(strings.TrimSpace(s.Mode))
|
||||
}
|
||||
color = "0x" + strings.TrimPrefix(color, "#")
|
||||
mode := strings.ToLower(strings.TrimSpace(s.Mode))
|
||||
// Commas/semicolons would break TCI's comma-separated argument parsing.
|
||||
text := strings.NewReplacer(",", " ", ";", " ").Replace(s.Comment)
|
||||
return t.send(fmt.Sprintf("spot:%s,%s,%d,%s,%s;", call, mode, s.FreqHz, color, text))
|
||||
return t.send(fmt.Sprintf("spot:%s,%s,%d,%d,%s;", call, mode, s.FreqHz, int32(argb), text))
|
||||
}
|
||||
|
||||
// Disconnect closes the WebSocket; the reader goroutine then exits.
|
||||
@@ -302,14 +319,30 @@ func (t *TCI) handle(msg string) {
|
||||
}
|
||||
default:
|
||||
lname := strings.ToLower(name)
|
||||
// A click on one of our panorama spots comes back as a "spot…" message.
|
||||
// The exact shape isn't well documented, so read a callsign (+ freq) from
|
||||
// any spot-related message and fire the callback. The log line lets us pin
|
||||
// the real format against a radio if it differs.
|
||||
if strings.HasPrefix(lname, "spot") {
|
||||
debugLog.Printf("TCI: spot event: %s", msg)
|
||||
call := get(0)
|
||||
hz, _ := strconv.ParseInt(get(2), 10, 64)
|
||||
// A click on one of our panorama spots comes back as
|
||||
// CLICKED_ON_SPOT:<call>,<hz> (legacy)
|
||||
// RX_CLICKED_ON_SPOT:<rx>,<ch>,<call>,<hz>
|
||||
// Neither name starts with "spot", which is why the click was silently
|
||||
// ignored before. Read the callsign (the one non-numeric field) and the
|
||||
// frequency (the large numeric field) positionally-independently, so both
|
||||
// shapes work without depending on the exact arg order.
|
||||
if strings.Contains(lname, "spot") {
|
||||
var call string
|
||||
var hz int64
|
||||
for _, raw := range f {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
if n >= 10000 { // a real frequency, not an rx/channel index
|
||||
hz = n
|
||||
}
|
||||
} else if call == "" {
|
||||
call = strings.ToUpper(v) // callsigns always carry letters
|
||||
}
|
||||
}
|
||||
debugLog.Printf("TCI: spot click %q → call=%s freq=%d", msg, call, hz)
|
||||
if call != "" && t.OnSpotClick != nil {
|
||||
cb := t.OnSpotClick
|
||||
go cb(call, hz)
|
||||
|
||||
Reference in New Issue
Block a user