Automatically switches the Station Control relay boards from the rig's current frequency / band. Each relay carries one rule: off (manual), a frequency window (ON inside [lo,hi] kHz, OFF outside), or a set of bands (ON on those bands, OFF elsewhere). Evaluated on every CAT frequency/band change; a relay is only switched when its desired state actually changed, so tuning within a range doesn't hammer the board. A cached atomic flag keeps the CAT hot path a no-op when the feature is off (important during FT8 slice churn). Saving re-applies from the live frequency so a changed rule takes effect immediately. New Settings → Hardware → Relay auto-control section: master enable plus a per-relay mode (Off / Frequency / Band) with kHz range inputs or band chips, per configured relay board. i18n EN + FR. Azimuth/Time modes (the other two PstRotator tabs) are left for later.
155 lines
4.4 KiB
Go
155 lines
4.4 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
|
|
}
|
|
|
|
// applyRelayAuto evaluates every rule against the current frequency/band and
|
|
// switches only the relays whose desired state changed since the last apply.
|
|
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
|
|
|
|
changed := false
|
|
for _, r := range cfg.Rules {
|
|
if r.Relay < 1 {
|
|
continue
|
|
}
|
|
var want bool
|
|
switch r.Mode {
|
|
case "freq":
|
|
if r.FreqLoKHz <= 0 && r.FreqHiKHz <= 0 {
|
|
continue // unconfigured range → leave the relay alone
|
|
}
|
|
lo, hi := r.FreqLoKHz, r.FreqHiKHz
|
|
if hi < lo {
|
|
lo, hi = hi, lo
|
|
}
|
|
want = khz >= lo && khz <= hi
|
|
case "band":
|
|
if len(r.Bands) == 0 {
|
|
continue
|
|
}
|
|
want = bandInList(r.Bands, band)
|
|
default:
|
|
continue // "off"/empty → not managed
|
|
}
|
|
|
|
key := relayAutoKey(r.DeviceID, r.Relay)
|
|
if last, ok := a.relayAutoLast[key]; ok && last == want {
|
|
continue // no change → don't hammer the board
|
|
}
|
|
if err := a.StationSetRelay(r.DeviceID, r.Relay, want); err != nil {
|
|
applog.Printf("relay auto: set %s relay %d = %v failed: %v", r.DeviceID, r.Relay, want, err)
|
|
continue // don't cache a failed write — retry next change
|
|
}
|
|
a.relayAutoLast[key] = want
|
|
changed = true
|
|
}
|
|
|
|
if changed && a.ctx != nil {
|
|
wruntime.EventsEmit(a.ctx, "station:relay_auto", nil) // nudge the Station Control UI to re-poll
|
|
}
|
|
}
|