fix(amp): couple a chosen GROUP, not every amplifier

The first version was a single switch meaning "command them all", and that is
wrong the moment a station has three: two SPE on a combiner and a PowerGenius
on another antenna would all go into OPERATE together, keying an amplifier that
has nothing to do with the pair.

It is now a set. Each amplifier is ticked into the group or not, the group is
stored as a list of ids, and an amplifier outside it keeps its own buttons —
which is the entire point of it being a set.

A group of fewer than two members is stored as none: one amplifier coupled to
itself would make every command fan out to a single member for ever.

A remembered member that is no longer running is skipped rather than failing
the command — deleting one amplifier must not break the button on the other.

An amplifier saved without an id cannot join, and the panel says so instead of
quietly omitting it from the list.
This commit is contained in:
2026-08-13 15:24:43 +02:00
parent 345be94c65
commit fabd1becce
7 changed files with 154 additions and 74 deletions
+55 -18
View File
@@ -16230,7 +16230,7 @@ func (a *App) AmpOperate(id string, on bool) error {
// call this, and a coupling implemented in one of them would be missing from
// the other — which on a combiner means one amplifier keyed and one not.
var firstErr error
for _, tid := range a.ampTargets(id, a.GetAmpsLinked()) {
for _, tid := range a.ampTargets(id, a.GetLinkedAmps()) {
if err := a.ampOperateOne(tid, on); err != nil && firstErr == nil {
firstErr = err
}
@@ -16263,10 +16263,11 @@ func (a *App) AmpPower(id string, on bool) (err error) {
applog.Printf("amp %s: power %v failed: %v", id, on, err)
}
}()
linked := a.GetAmpsLinked()
linked := a.GetLinkedAmps()
targets := a.ampTargets(id, linked)
var firstErr error
for _, tid := range a.ampTargets(id, linked) {
if e := a.ampPowerOne(tid, on, linked); e != nil && firstErr == nil {
for _, tid := range targets {
if e := a.ampPowerOne(tid, on, len(targets) > 1); e != nil && firstErr == nil {
firstErr = e
}
}
@@ -17850,12 +17851,36 @@ func (a *App) SetCompactHeight(h int) {
// what must not happen — the combiner would see power on one input only.
const keyAmpsLinked = "amps.linked"
// GetAmpsLinked reports whether the amplifiers are commanded together.
func (a *App) GetAmpsLinked() bool { return a.settingOr(keyAmpsLinked, "") == "1" }
// GetLinkedAmps returns the ids of the amplifiers commanded together.
//
// A SET, not a global flag. A station can have a combiner pair AND a third
// amplifier that has nothing to do with it — two SPE on the combiner and a
// PowerGenius on another antenna — and a global switch would send that third
// one into OPERATE alongside them.
func (a *App) GetLinkedAmps() []string {
out := []string{}
for _, id := range strings.Split(a.settingOr(keyAmpsLinked, ""), ",") {
if id = strings.TrimSpace(id); id != "" {
out = append(out, id)
}
}
return out
}
// SetAmpsLinked turns coupling on or off.
func (a *App) SetAmpsLinked(on bool) error {
a.setSetting(keyAmpsLinked, boolStr(on))
// SetLinkedAmps records which amplifiers are coupled.
func (a *App) SetLinkedAmps(ids []string) error {
clean := make([]string, 0, len(ids))
for _, id := range ids {
if id = strings.TrimSpace(id); id != "" {
clean = append(clean, id)
}
}
// One amplifier coupled to itself is not a group; store nothing rather than
// a set that would make ampTargets fan out to a single member for ever.
if len(clean) < 2 {
clean = nil
}
a.setSetting(keyAmpsLinked, strings.Join(clean, ","))
return nil
}
@@ -17865,21 +17890,33 @@ func (a *App) SetAmpsLinked(on bool) error {
// The METERS are deliberately untouched by this — each amplifier keeps its own
// bars. Two amps combined are still two amps, and an operator watching for one
// of them to run away needs to see them apart.
func (a *App) ampTargets(id string, linked bool) []string {
if !linked {
func (a *App) ampTargets(id string, linked []string) []string {
inGroup := false
for _, l := range linked {
if l == id {
inGroup = true
break
}
}
// An amplifier outside the group keeps its own buttons to itself — that is
// the whole point of the group being a set.
if !inGroup {
return []string{id}
}
a.ampsMu.Lock()
defer a.ampsMu.Unlock()
out := make([]string, 0, len(a.ampInsts))
out := make([]string, 0, len(linked))
// The one that was asked for goes FIRST, so a partial failure still did what
// the operator clicked before it stopped.
if _, ok := a.ampInsts[id]; ok {
out = append(out, id)
}
for k := range a.ampInsts {
if k != id {
out = append(out, k)
out = append(out, id)
for _, l := range linked {
if l == id {
continue
}
// Only amplifiers actually running: a group remembering one that has been
// deleted or switched off must not fail the whole command for it.
if _, ok := a.ampInsts[l]; ok {
out = append(out, l)
}
}
return out