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.
237 lines
7.6 KiB
Go
237 lines
7.6 KiB
Go
package main
|
|
|
|
// Relay auto-control: drives the Station Control relay boards automatically from
|
|
// the rig's current frequency / band — the equivalent of PstRotator's "Automatic
|
|
// Control". Each relay carries at most one rule:
|
|
// - "freq": ON while the frequency is inside [lo,hi] kHz, OFF otherwise;
|
|
// - "band": ON while the current band is one of the listed bands, OFF otherwise;
|
|
// - "off"/empty: not managed (left to manual control).
|
|
//
|
|
// Evaluated on every CAT frequency/band change. A relay is only switched when its
|
|
// desired state actually changed since the last apply, so a slow relay board isn't
|
|
// hammered while you tune within the same range.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
|
|
|
"hamlog/internal/applog"
|
|
)
|
|
|
|
const keyRelayAuto = "relayauto.config"
|
|
|
|
// RelayAutoRule is one relay's automatic-control rule.
|
|
type RelayAutoRule struct {
|
|
DeviceID string `json:"device_id"`
|
|
Relay int `json:"relay"` // 1-based
|
|
Mode string `json:"mode"` // "off" | "freq" | "band"
|
|
FreqLoKHz float64 `json:"freq_lo_khz"`
|
|
FreqHiKHz float64 `json:"freq_hi_khz"`
|
|
Bands []string `json:"bands"`
|
|
}
|
|
|
|
// RelayAutoConfig is the whole auto-control setup: a master switch + the rules.
|
|
type RelayAutoConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
Rules []RelayAutoRule `json:"rules"`
|
|
}
|
|
|
|
// GetRelayAuto returns the relay auto-control configuration for the settings UI.
|
|
func (a *App) GetRelayAuto() RelayAutoConfig {
|
|
var cfg RelayAutoConfig
|
|
if a.settings == nil {
|
|
return cfg
|
|
}
|
|
s, _ := a.settings.GetGlobal(a.ctx, keyRelayAuto)
|
|
if strings.TrimSpace(s) != "" {
|
|
_ = json.Unmarshal([]byte(s), &cfg)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// SaveRelayAuto persists the configuration and applies it immediately from the
|
|
// current rig state, so toggling a rule takes effect without waiting for the next
|
|
// frequency change.
|
|
func (a *App) SaveRelayAuto(cfg RelayAutoConfig) error {
|
|
if a.settings == nil {
|
|
return fmt.Errorf("db not initialized")
|
|
}
|
|
b, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := a.settings.SetGlobal(a.ctx, keyRelayAuto, string(b)); err != nil {
|
|
return err
|
|
}
|
|
a.relayAutoOn.Store(cfg.Enabled) // keep the CAT hot-path flag in sync
|
|
// Re-apply from the live frequency so a just-changed rule takes hold now. Also
|
|
// forget the last-applied cache so a rule the user just switched to "off" and
|
|
// back gets re-sent even if the value is unchanged.
|
|
a.relayAutoMu.Lock()
|
|
a.relayAutoLast = map[string]bool{}
|
|
a.relayAutoMu.Unlock()
|
|
if a.cat != nil {
|
|
st := a.cat.State()
|
|
go a.applyRelayAuto(st.FreqHz, st.Band)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func relayAutoKey(dev string, relay int) string { return dev + "|" + strconv.Itoa(relay) }
|
|
|
|
func bandInList(bands []string, band string) bool {
|
|
band = strings.ToLower(strings.TrimSpace(band))
|
|
if band == "" {
|
|
return false
|
|
}
|
|
for _, b := range bands {
|
|
if strings.ToLower(strings.TrimSpace(b)) == band {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// relayAction is one relay's computed desired state for this evaluation.
|
|
type relayAction struct {
|
|
dev string
|
|
relay int
|
|
want bool
|
|
}
|
|
|
|
// applyRelayAuto evaluates every rule against the current frequency/band and
|
|
// switches only the relays that are NOT already in the wanted position. Two things
|
|
// it deliberately does NOT do, which used to make the relay clunk on every
|
|
// launch/close:
|
|
// - Never acts on an UNKNOWN frequency/band. When the CAT disconnects (app close)
|
|
// the frequency drops to 0; reading that as "out of range" and switching the
|
|
// relay off — then back on at the next launch — was the whole bug.
|
|
// - Never commands a relay already in the right position: on the first evaluation
|
|
// after launch/save it reads the boards' LIVE state, so a relay that's already
|
|
// correct is left untouched instead of being re-sent.
|
|
func (a *App) applyRelayAuto(freqHz int64, band string) {
|
|
a.relayAutoMu.Lock()
|
|
defer a.relayAutoMu.Unlock()
|
|
|
|
cfg := a.GetRelayAuto()
|
|
if !cfg.Enabled || len(cfg.Rules) == 0 {
|
|
return
|
|
}
|
|
if a.relayAutoLast == nil {
|
|
a.relayAutoLast = map[string]bool{}
|
|
}
|
|
khz := float64(freqHz) / 1000.0
|
|
band = strings.TrimSpace(band)
|
|
|
|
// Compute desired states, skipping rules whose input is unknown right now.
|
|
var acts []relayAction
|
|
needLive := false
|
|
for _, r := range cfg.Rules {
|
|
if r.Relay < 1 {
|
|
continue
|
|
}
|
|
var want bool
|
|
switch r.Mode {
|
|
case "freq":
|
|
if freqHz <= 0 {
|
|
continue // no known frequency (CAT off/closing) → leave the relay as-is
|
|
}
|
|
if r.FreqLoKHz <= 0 && r.FreqHiKHz <= 0 {
|
|
continue // unconfigured range
|
|
}
|
|
lo, hi := r.FreqLoKHz, r.FreqHiKHz
|
|
if hi < lo {
|
|
lo, hi = hi, lo
|
|
}
|
|
want = khz >= lo && khz <= hi
|
|
case "band":
|
|
if band == "" {
|
|
continue // no known band → leave the relay as-is
|
|
}
|
|
if len(r.Bands) == 0 {
|
|
continue
|
|
}
|
|
want = bandInList(r.Bands, band)
|
|
default:
|
|
continue // "off"/empty → not managed
|
|
}
|
|
acts = append(acts, relayAction{r.DeviceID, r.Relay, want})
|
|
if _, ok := a.relayAutoLast[relayAutoKey(r.DeviceID, r.Relay)]; !ok {
|
|
needLive = true
|
|
}
|
|
}
|
|
if len(acts) == 0 {
|
|
return
|
|
}
|
|
|
|
// First evaluation after launch/save: read the boards' LIVE relay states once
|
|
// so we don't re-command a relay that's already in the wanted position.
|
|
var live map[string]bool
|
|
if needLive {
|
|
live = map[string]bool{}
|
|
for _, ds := range a.GetStationStatus() {
|
|
for _, rl := range ds.Relays {
|
|
live[relayAutoKey(ds.ID, rl.Number)] = rl.On
|
|
}
|
|
}
|
|
}
|
|
|
|
changed := false
|
|
for _, ac := range acts {
|
|
key := relayAutoKey(ac.dev, ac.relay)
|
|
cur, known := a.relayAutoLast[key]
|
|
if !known && live != nil {
|
|
cur, known = live[key]
|
|
}
|
|
if known && cur == ac.want {
|
|
a.relayAutoLast[key] = ac.want // already in position — record it, don't switch
|
|
continue
|
|
}
|
|
if err := a.StationSetRelay(ac.dev, ac.relay, ac.want); err != nil {
|
|
applog.Printf("relay auto: set %s relay %d = %v failed: %v", ac.dev, ac.relay, ac.want, err)
|
|
continue // don't cache a failed write — retry next change
|
|
}
|
|
a.relayAutoLast[key] = ac.want
|
|
changed = true
|
|
}
|
|
|
|
if changed && a.ctx != nil {
|
|
wruntime.EventsEmit(a.ctx, "station:relay_auto", nil) // nudge the Station Control UI to re-poll
|
|
}
|
|
}
|
|
|
|
// EntryBandChanged drives the band-following features from the QSO ENTRY band
|
|
// selector, for a station whose rig OpsLog does not control.
|
|
//
|
|
// Both of them — relay automatic control and the band-change outbound rows —
|
|
// hang off the rig state, and that is right when there IS a rig: changing Band
|
|
// in the entry strip pushes a QSY to it, the new state comes back through the
|
|
// CAT callback, and the relays follow from there. Without a CAT connection
|
|
// nothing is pushed and nothing comes back, so an operator with an antenna
|
|
// switch and no rig control changed band in OpsLog and watched the switch sit
|
|
// exactly where it was. Reported as automatic control not working; it was never
|
|
// told the band had changed.
|
|
//
|
|
// The frontend calls this 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 (logging an old contact off-frequency), and moving an
|
|
// antenna to match a QSO from last year is worse than doing nothing.
|
|
//
|
|
// The frequency is passed as unknown, deliberately: a band selector gives a
|
|
// band and nothing else, and rules written on a frequency RANGE must be left
|
|
// alone rather than evaluated against a made-up dial reading.
|
|
func (a *App) EntryBandChanged(band string) {
|
|
band = strings.TrimSpace(band)
|
|
if band == "" {
|
|
return
|
|
}
|
|
if a.relayAutoOn.Load() {
|
|
go a.applyRelayAuto(0, band)
|
|
}
|
|
a.emitBandChangeTrigger(band, "", 0)
|
|
}
|