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 } band := strings.ToLower(strings.TrimSpace(s.Band)) if band == "" || !s.Connected { return } lastTriggerBandMu.Lock() changed := band != lastTriggerBand if changed { lastTriggerBand = band } lastTriggerBandMu.Unlock() if !changed { return } f := map[string]string{ "band": band, "band_m": bandMetres(band), "mode": s.Mode, } if s.FreqHz > 0 { f["freq_hz"] = strconv.FormatInt(s.FreqHz, 10) f["freq_mhz"] = fmt.Sprintf("%.6f", float64(s.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 }