feat: Implemented UDP Outbound Adif message, freq to pstrotator

This commit is contained in:
2026-07-05 18:17:30 +02:00
parent a8b3269b1e
commit 4f32012930
16 changed files with 704 additions and 123 deletions
+82 -3
View File
@@ -176,7 +176,7 @@ const (
keyWKUsePTT = "winkeyer.use_ptt"
keyWKSerialEcho = "winkeyer.serial_echo"
keyWKMacros = "winkeyer.macros" // JSON array of {label,text}
keyWKEngine = "winkeyer.engine" // "winkeyer" | "tci"
keyWKEngine = "winkeyer.engine" // "winkeyer" | "icom" | "tci"
keyWKEscClears = "winkeyer.esc_clears_call" // ESC also clears the callsign
keyWKSendOnType = "winkeyer.send_on_type" // key characters live as typed
@@ -459,6 +459,12 @@ type App struct {
opLat float64
opLon float64
opSet bool
opCall string // active profile callsign, cached for outbound UDP emitters
// Dedup for the frequency/mode outbound UDP emitters (PstRotator, N1MM): only
// send when the frequency or mode actually changes.
udpLastFreqHz int64
udpLastMode string
}
// gridToLatLon parses a Maidenhead locator (4 or 6 chars) and returns the
@@ -534,6 +540,7 @@ func (a *App) refreshOperatorGrid() {
if err != nil {
return
}
a.opCall = strings.ToUpper(strings.TrimSpace(p.Callsign))
lat, lon, ok := gridToLatLon(p.MyGrid)
if !ok {
return
@@ -543,6 +550,36 @@ func (a *App) refreshOperatorGrid() {
a.opSet = true
}
// emitRadioUDP forwards the rig's frequency/mode to any enabled outbound UDP
// emitters (PstRotator, N1MM RadioInfo). Deduped so it only fires when the
// operating frequency or mode actually changes. The send runs off the CAT
// goroutine so a slow/failed datagram can't stall polling.
func (a *App) emitRadioUDP(s cat.RigState) {
if a.udp == nil {
return
}
rx := s.FreqHz
if s.RxFreqHz != 0 { // split: RxFreqHz is the active/listening VFO
rx = s.RxFreqHz
}
if rx <= 0 {
return
}
if rx == a.udpLastFreqHz && s.Mode == a.udpLastMode {
return
}
a.udpLastFreqHz = rx
a.udpLastMode = s.Mode
rs := udp.RadioState{
StationName: a.opCall,
OpCall: a.opCall,
RxFreqHz: rx,
TxFreqHz: s.FreqHz,
Mode: s.Mode,
}
go a.udp.EmitRadioState(rs)
}
// dxccAdapter bridges *dxcc.Manager to the lookup.DXCCResolver interface
// without making the lookup package import dxcc.
type dxccAdapter struct{ m *dxcc.Manager }
@@ -726,11 +763,13 @@ func (a *App) startup(ctx context.Context) {
fmt.Printf("OpsLog: clublog cty.xml loaded — %d exceptions (%s)\n", n, d)
}
}()
// CAT manager: emit pushes state to the frontend via Wails events.
// CAT manager: emit pushes state to the frontend via Wails events, and
// forwards frequency/mode to any outbound UDP emitters (PstRotator, N1MM).
a.cat = cat.NewManager(func(s cat.RigState) {
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "cat:state", s)
}
a.emitRadioUDP(s)
})
a.reloadCAT()
@@ -1583,6 +1622,10 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
a.extsvc.OnQSOLogged(id)
}
a.maybeAutoSendEQSL(q)
// Forward the ADIF of this QSO to any outbound "ADIF message" UDP rows.
if a.udp != nil {
go a.udp.EmitLoggedADIF(adif.SingleRecordADIF(q))
}
}
return id, err
}
@@ -7866,6 +7909,42 @@ func (a *App) IcomSetXITOn(on bool) error {
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetXITOn(on) })
}
// IcomSendCW keys a CW message through the rig's internal keyer (CI-V 0x17).
func (a *App) IcomSendCW(text string) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
err := a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SendCW(text) })
if err != nil {
applog.Printf("icom cw: IcomSendCW(%q) failed: %v", text, err)
}
return err
}
// IcomStopCW aborts the CW message currently being sent.
func (a *App) IcomStopCW() error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.StopCW() })
}
// IcomSetKeySpeed sets the CW keyer speed in WPM.
func (a *App) IcomSetKeySpeed(wpm int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetKeySpeed(wpm) })
}
// IcomSetBreakIn sets CW break-in (0=OFF, 1=SEMI, 2=FULL).
func (a *App) IcomSetBreakIn(mode int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetBreakIn(mode) })
}
func (a *App) IcomSetPTT(on bool) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
@@ -9092,7 +9171,7 @@ type WKMacro struct {
type WinkeyerSettings struct {
Enabled bool `json:"enabled"`
winkeyer.Config
Engine string `json:"engine"` // keyer backend: "winkeyer" | "tci"
Engine string `json:"engine"` // keyer backend: "winkeyer" | "icom" (rig keyer via CI-V) | "tci"
EscClearsCall bool `json:"esc_clears_call"` // ESC also resets the callsign
SendOnType bool `json:"send_on_type"` // key chars live as typed
Macros []WKMacro `json:"macros"`