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 } }