diff --git a/relayauto.go b/relayauto.go index b940b42..314a643 100644 --- a/relayauto.go +++ b/relayauto.go @@ -96,8 +96,23 @@ func bandInList(bands []string, band string) bool { 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 whose desired state changed since the last apply. +// 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() @@ -110,8 +125,11 @@ func (a *App) applyRelayAuto(freqHz int64, band string) { a.relayAutoLast = map[string]bool{} } khz := float64(freqHz) / 1000.0 + band = strings.TrimSpace(band) - changed := false + // 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 @@ -119,8 +137,11 @@ func (a *App) applyRelayAuto(freqHz int64, band string) { 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 → leave the relay alone + continue // unconfigured range } lo, hi := r.FreqLoKHz, r.FreqHiKHz if hi < lo { @@ -128,6 +149,9 @@ func (a *App) applyRelayAuto(freqHz int64, band string) { } want = khz >= lo && khz <= hi case "band": + if band == "" { + continue // no known band → leave the relay as-is + } if len(r.Bands) == 0 { continue } @@ -135,16 +159,43 @@ func (a *App) applyRelayAuto(freqHz int64, band string) { 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 + acts = append(acts, relayAction{r.DeviceID, r.Relay, want}) + if _, ok := a.relayAutoLast[relayAutoKey(r.DeviceID, r.Relay)]; !ok { + needLive = true } - 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) + } + 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] = want + a.relayAutoLast[key] = ac.want changed = true }