Files
OpsLog/udptrigger.go
T
rouggy 423cf1f998 feat(relays): follow the entry Band selector when there is no CAT link
Relay automatic control and the band-change outbound rows both hang off the rig
state, which is right when there IS a rig: changing Band in the entry strip
pushes a QSY, the new state comes back through the CAT callback, and the relays
follow from there — which is why this works for anyone with rig control.

Without a CAT connection nothing is pushed and nothing comes back, so a station
whose rig OpsLog does not control changed band and the antenna switch sat
exactly where it was. Reported as automatic control not working; it was never
told the band had changed.

The entry selector now says so itself, but only when the CAT push did not
happen, and never when the band or frequency lock is on: a lock means the entry
is deliberately decoupled from the rig, and moving an antenna to match a contact
logged from last year is worse than doing nothing.

The frequency is passed as unknown on that path — a band selector gives a band
and nothing else, and rules written on a frequency RANGE are left alone rather
than evaluated against a made-up dial reading.

The de-duplication of "is this a new band" is now shared by both sources, so a
station that has both does not command its switch twice for one QSY.

Also: the same credit line as the QSL e-mail now closes the default recording
e-mail body, on the same terms — a default in the template, so a stored one is
untouched.
2026-08-16 11:15:56 +02:00

176 lines
4.9 KiB
Go

package main
// Wiring the custom outbound UDP/URL rows to the moments that fire them.
//
// One function per trigger, each building the fields its template can use. The
// field SET is per trigger and that is the point: a rotation has no frequency, a
// lookup has no report. The Settings panel lists what each one offers, because a
// template that quietly renders an empty string is the worst kind of wrong.
import (
"fmt"
"strconv"
"strings"
"sync"
"hamlog/internal/cat"
"hamlog/internal/integrations/udp"
"hamlog/internal/lookup"
"hamlog/internal/qso"
)
// lastTriggerBand remembers the rig's band so a change can be told from a
// frequency moving inside one. Guarded because the CAT callback and a settings
// reload can both reach it.
var (
lastTriggerBandMu sync.Mutex
lastTriggerBand string
)
// udpTriggerQSOLogged fires after a contact is written to the log.
func (a *App) udpTriggerQSOLogged(q qso.QSO) {
if a.udp == nil {
return
}
f := map[string]string{
"call": q.Callsign,
"band": q.Band,
"band_m": bandMetres(q.Band),
"mode": q.Mode,
"grid": q.Grid,
"name": q.Name,
"country": q.Country,
"rst_s": q.RSTSent,
"rst_r": q.RSTRcvd,
"comment": q.Comment,
"date": q.QSODate.UTC().Format("20060102"),
"time": q.QSODate.UTC().Format("150405"),
}
if q.FreqHz != nil && *q.FreqHz > 0 {
f["freq_hz"] = strconv.FormatInt(*q.FreqHz, 10)
f["freq_mhz"] = fmt.Sprintf("%.6f", float64(*q.FreqHz)/1e6)
}
if q.DXCC != nil {
f["dxcc"] = strconv.Itoa(*q.DXCC)
}
a.udp.EmitTrigger(udp.TriggerQSOLogged, f)
}
// udpTriggerRotator fires whenever the antenna is told to turn — the header's
// SP and LP buttons, the compass, a click on a spot.
//
// Hooked to the COMMAND rather than to a button: one place, and it stays true
// when the interface changes. path is "SP", "LP" or "" when the caller did not
// say which.
func (a *App) udpTriggerRotator(az, el int, path string) {
if a.udp == nil {
return
}
f := map[string]string{
"az": strconv.Itoa(az),
"path": path,
}
if el >= 0 {
f["el"] = strconv.Itoa(el)
}
a.udp.EmitTrigger(udp.TriggerRotatorGo, f)
}
// udpTriggerLookup fires after a callbook lookup returns.
func (a *App) udpTriggerLookup(r *lookup.Result) {
if a.udp == nil || r == nil {
return
}
a.udp.EmitTrigger(udp.TriggerLookupDone, map[string]string{
"call": r.Callsign,
"name": r.Name,
"grid": r.Grid,
"country": r.Country,
"qth": r.QTH,
"state": r.State,
"dxcc": strconv.Itoa(r.DXCC),
})
}
// udpTriggerBandChange fires when the RIG changes band — not the entry form: an
// antenna switch follows the radio, not what is being typed.
//
// Called from the CAT state callback, which runs on every user-relevant change,
// so the comparison against the last band is what makes this rare rather than
// continuous. No damping needed beyond that.
func (a *App) udpTriggerBandChange(s cat.RigState) {
if a.udp == nil {
return
}
if !s.Connected {
return
}
a.emitBandChangeTrigger(s.Band, s.Mode, s.FreqHz)
}
// noteBandChange normalises a band and reports whether it is a NEW one.
//
// The state is shared by both sources on purpose. The rig reports a band change
// and so does the entry strip, and on a station that has both, one QSY produces
// both — an antenna switch must be commanded once, not twice.
func noteBandChange(raw string) (string, bool) {
band := strings.ToLower(strings.TrimSpace(raw))
if band == "" {
return "", false
}
lastTriggerBandMu.Lock()
defer lastTriggerBandMu.Unlock()
if band == lastTriggerBand {
return band, false
}
lastTriggerBand = band
return band, true
}
// emitBandChangeTrigger fires the band-change rows, once per NEW band.
//
// Split out of the rig-state path because the rig is not the only thing that
// changes band: a station with no CAT link changes it in the QSO entry strip,
// and that is just as much a band change to the antenna switch on the other end
// of the message. The de-duplication is shared, so the two sources cannot
// double-fire between them.
func (a *App) emitBandChangeTrigger(rawBand, mode string, freqHz int64) {
if a.udp == nil {
return
}
band, changed := noteBandChange(rawBand)
if !changed {
return
}
f := map[string]string{
"band": band,
"band_m": bandMetres(band),
"mode": mode,
}
if freqHz > 0 {
f["freq_hz"] = strconv.FormatInt(freqHz, 10)
f["freq_mhz"] = fmt.Sprintf("%.6f", float64(freqHz)/1e6)
}
a.udp.EmitTrigger(udp.TriggerBandChange, f)
}
// bandMetres turns "20m" into "20" — an antenna switch usually wants the number
// and nothing else, and asking an operator to strip it in a template would be
// one more thing to get wrong.
func bandMetres(band string) string {
b := strings.ToLower(strings.TrimSpace(band))
b = strings.TrimSuffix(b, "m")
b = strings.TrimSuffix(b, "c") // "70cm" → "70c" → "70"
if b == "" {
return ""
}
for _, r := range b {
if r < '0' || r > '9' {
if r != '.' {
return ""
}
}
}
return b
}