feat: amplifier band-follow — answer the amp's frequency polls
On its CAT/AUX connector an ACOM is the MASTER: it polls a transceiver and changes band from the reply, so nothing can be pushed to it. internal/catemu answers those polls on a second serial port, in ACOM command set 5 (Kenwood / Elecraft): FA;, FB;, IF; and ID;, and nothing else — a wrong-length reply is worse than none, it desynchronises the amp's parser for the following poll. Also offered for SPE. Some amps do not poll at all but read the radio↔PC CAT line in parallel; those never hear a responder, so an optional unprompted send (500/1000 ms) covers them. The TX frequency is what is sent: in split the amp must be tuned where we transmit. Frame lengths are pinned by tests — the failure they guard against is silent and only shows up as an amp that mistunes. Untested on hardware.
This commit is contained in:
@@ -30,6 +30,7 @@ import (
|
||||
"hamlog/internal/backup"
|
||||
"hamlog/internal/cabrillo"
|
||||
"hamlog/internal/cat"
|
||||
"hamlog/internal/catemu"
|
||||
"hamlog/internal/clublog"
|
||||
"hamlog/internal/cluster"
|
||||
"hamlog/internal/contest"
|
||||
@@ -917,6 +918,10 @@ func (a *App) startup(ctx context.Context) {
|
||||
wruntime.EventsEmit(a.ctx, "cat:state", s)
|
||||
}
|
||||
a.emitRadioUDP(s)
|
||||
// Feed the frequency to any amplifier we are pretending to be a radio
|
||||
// for. Just two atomic stores per amp — the reply itself is built when
|
||||
// the amp polls, so a fast-tuning VFO costs nothing here.
|
||||
a.feedAmpBandFollow(s)
|
||||
// Drive station relays by the current frequency/band (PstRotator-style
|
||||
// automatic control). Cheap cached-flag check keeps this a no-op when the
|
||||
// feature is off; when on, run off this callback so a slow relay board never
|
||||
@@ -13343,13 +13348,26 @@ type AmpConfig struct {
|
||||
Port int `json:"port"`
|
||||
ComPort string `json:"com_port"`
|
||||
Baud int `json:"baud"`
|
||||
|
||||
// Band-follow: an ACOM is the MASTER on its CAT/AUX connector — it polls a
|
||||
// transceiver and changes band from the reply, so following OpsLog means
|
||||
// answering those polls on a SECOND serial port (independent of the one used
|
||||
// above for metering; both run at once). See internal/catemu.
|
||||
FreqOut bool `json:"freq_out"`
|
||||
FreqComPort string `json:"freq_com_port"`
|
||||
FreqBaud int `json:"freq_baud"`
|
||||
// FreqBroadcastMs > 0 also SENDS the frequency unprompted at that interval,
|
||||
// for an amplifier that listens to a transceiver's CAT stream instead of
|
||||
// polling it. 0 = answer polls only.
|
||||
FreqBroadcastMs int `json:"freq_broadcast_ms"`
|
||||
}
|
||||
|
||||
type ampInst struct {
|
||||
cfg AmpConfig
|
||||
pgxl *powergenius.Client
|
||||
spe *spe.Client
|
||||
acom *acom.Client
|
||||
cfg AmpConfig
|
||||
pgxl *powergenius.Client
|
||||
spe *spe.Client
|
||||
acom *acom.Client
|
||||
catemu *catemu.Server // Kenwood-format responder for band-follow (ACOM)
|
||||
}
|
||||
|
||||
func (i *ampInst) stopAll() {
|
||||
@@ -13362,6 +13380,9 @@ func (i *ampInst) stopAll() {
|
||||
if i.acom != nil {
|
||||
i.acom.Stop()
|
||||
}
|
||||
if i.catemu != nil {
|
||||
i.catemu.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// ampTypeLabel is the default display name for an amp type.
|
||||
@@ -13491,12 +13512,61 @@ func (a *App) startAmps() {
|
||||
a.spe = inst.spe
|
||||
}
|
||||
}
|
||||
// Band-follow on a SECOND serial port, for any amp that takes its band from
|
||||
// a transceiver CAT link (ACOM and SPE both do). Independent of the control
|
||||
// transport above, so metering and band-follow run at the same time.
|
||||
if c.FreqOut && strings.TrimSpace(c.FreqComPort) != "" {
|
||||
inst.catemu = catemu.New(catemu.Config{
|
||||
ComPort: c.FreqComPort, Baud: c.FreqBaud,
|
||||
BroadcastMs: c.FreqBroadcastMs,
|
||||
}, applog.Printf)
|
||||
if st := a.cat.State(); st.Connected {
|
||||
inst.catemu.SetFrequency(st.FreqHz)
|
||||
inst.catemu.SetMode(st.Mode)
|
||||
}
|
||||
inst.catemu.Start()
|
||||
applog.Printf("amp %s: band-follow on %s at %d baud (Kenwood format, broadcast=%dms)",
|
||||
c.Name, c.FreqComPort, c.FreqBaud, c.FreqBroadcastMs)
|
||||
}
|
||||
a.ampsMu.Lock()
|
||||
a.ampInsts[c.ID] = inst
|
||||
a.ampsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// feedAmpBandFollow pushes the rig's frequency/mode to every amplifier we are
|
||||
// emulating a transceiver for. Called on each CAT state change.
|
||||
//
|
||||
// The TX frequency is what the amp must follow: in split it has to be tuned
|
||||
// for where we transmit, not where we listen.
|
||||
func (a *App) feedAmpBandFollow(s cat.RigState) {
|
||||
if !s.Connected || s.FreqHz <= 0 {
|
||||
return
|
||||
}
|
||||
a.ampsMu.Lock()
|
||||
defer a.ampsMu.Unlock()
|
||||
for _, inst := range a.ampInsts {
|
||||
if inst.catemu != nil {
|
||||
inst.catemu.SetFrequency(s.FreqHz)
|
||||
inst.catemu.SetMode(s.Mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAmpBandFollowStatus returns the band-follow link state per amplifier id,
|
||||
// so the settings panel can show whether the amp is actually polling us.
|
||||
func (a *App) GetAmpBandFollowStatus() map[string]catemu.Status {
|
||||
out := map[string]catemu.Status{}
|
||||
a.ampsMu.Lock()
|
||||
defer a.ampsMu.Unlock()
|
||||
for id, inst := range a.ampInsts {
|
||||
if inst.catemu != nil {
|
||||
out[id] = inst.catemu.GetStatus()
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AmpStatus is one amp's live state for the UI poll — exactly one of the
|
||||
// per-family payloads is set, per the amp's type.
|
||||
type AmpStatus struct {
|
||||
|
||||
Reference in New Issue
Block a user