fix(amps): combined amplifiers are commanded together, power level included
Two faults in the combiner coupling, both reported from the operating position. THE POWER LEVEL WAS NEVER COUPLED. ON, OFF and OPERATE fanned out to the group; L/M/H did not — it was simply the command nobody had linked. Two combined amplifiers left at different power levels feed the combiner unevenly, which is the thing the coupling exists to prevent. THE COMMANDS DID NOT LEAVE TOGETHER. The second amplifier was commanded only once the first had answered, and an SPE answers over its own link in its own time. The combiner heard power appear on one input before the other and beeped about it, on every OFF and every ON. Each target now gets a goroutine, all parked on one channel until every one is ready; closing it releases them together. That is the difference between "start one, then start the other" and "both leave at once" — they have separate clients and separate connections, so nothing downstream re-serialises them. It matters most on the power level, which is not one command at all: an SPE has no "set level", so the driver taps the POWER key and waits for the amp to report the new one before tapping again — up to three taps, up to two seconds each. One after the other, the pair would sit at different levels for six seconds. A single amplifier still runs inline: no goroutine, no barrier, nothing new to go wrong for the operators who have one amp. The one that was clicked stays first, because its failure is the one worth reporting.
This commit is contained in:
@@ -16740,13 +16740,55 @@ func (a *App) AmpOperate(id string, on bool) error {
|
||||
// Fan out here rather than in the UI: the card and the docked widget both
|
||||
// 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.GetLinkedAmps()) {
|
||||
if err := a.ampOperateOne(tid, on); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
return a.ampFanOut(a.ampTargets(id, a.GetLinkedAmps()), func(tid string) error {
|
||||
return a.ampOperateOne(tid, on)
|
||||
})
|
||||
}
|
||||
|
||||
// ampFanOut runs one command against every amplifier AT THE SAME TIME.
|
||||
//
|
||||
// Sequentially, the second amplifier of a combined pair was commanded only once
|
||||
// the first had answered — and an SPE answers over its own link, in its own
|
||||
// time. The combiner heard power appear on one input before the other and
|
||||
// complained about it, on every OFF and every ON.
|
||||
//
|
||||
// So each target gets a goroutine, and they are all parked on the same channel
|
||||
// until every one of them is ready. Closing it releases them together: the
|
||||
// difference between "start one, then start the other" and "both leave at
|
||||
// once". They have separate clients and separate connections, so nothing
|
||||
// downstream re-serialises them.
|
||||
//
|
||||
// A single target — the ordinary case of one amplifier — runs inline. No
|
||||
// goroutine, no barrier, nothing to go wrong for the operators who have one amp.
|
||||
func (a *App) ampFanOut(targets []string, do func(id string) error) error {
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(targets) == 1 {
|
||||
return do(targets[0])
|
||||
}
|
||||
errs := make([]error, len(targets))
|
||||
start := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
for i, id := range targets {
|
||||
wg.Add(1)
|
||||
go func(i int, id string) {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
errs[i] = do(id)
|
||||
}(i, id)
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
// The amplifier the operator actually clicked is first, and its failure is
|
||||
// the one worth reporting: "the amp I pressed did not respond" beats the
|
||||
// same message about its silent partner.
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ampOperateOne(id string, on bool) error {
|
||||
@@ -16774,15 +16816,11 @@ func (a *App) AmpPower(id string, on bool) (err error) {
|
||||
applog.Printf("amp %s: power %v failed: %v", id, on, err)
|
||||
}
|
||||
}()
|
||||
linked := a.GetLinkedAmps()
|
||||
targets := a.ampTargets(id, linked)
|
||||
var firstErr error
|
||||
for _, tid := range targets {
|
||||
if e := a.ampPowerOne(tid, on, len(targets) > 1); e != nil && firstErr == nil {
|
||||
firstErr = e
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
targets := a.ampTargets(id, a.GetLinkedAmps())
|
||||
multi := len(targets) > 1
|
||||
return a.ampFanOut(targets, func(tid string) error {
|
||||
return a.ampPowerOne(tid, on, multi)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) ampPowerOne(id string, on, linked bool) error {
|
||||
@@ -16812,12 +16850,31 @@ func (a *App) ampPowerOne(id string, on, linked bool) error {
|
||||
}
|
||||
|
||||
// AmpPowerLevel selects the output power level — SPE only (L/M/H).
|
||||
//
|
||||
// Coupled like ON, OFF and OPERATE, which it was not: two combined amplifiers
|
||||
// left at different power levels feed the combiner unevenly, which is the thing
|
||||
// the coupling exists to prevent. It was simply the command nobody had linked.
|
||||
//
|
||||
// Running the pair concurrently matters more here than anywhere else. Setting a
|
||||
// level is not one command: an SPE has no "set level" at all, so the driver taps
|
||||
// the POWER key and waits for the amp to report the new level before tapping
|
||||
// again — up to three taps, up to two seconds each. One after the other, the two
|
||||
// amplifiers would sit at different levels for as long as six seconds.
|
||||
func (a *App) AmpPowerLevel(id, level string) error {
|
||||
inst := a.ampInstByID(id)
|
||||
if inst == nil || inst.spe == nil {
|
||||
return fmt.Errorf("power level is an SPE feature")
|
||||
}
|
||||
return inst.spe.SetPowerLevel(level)
|
||||
targets := a.ampTargets(id, a.GetLinkedAmps())
|
||||
multi := len(targets) > 1
|
||||
return a.ampFanOut(targets, func(tid string) error {
|
||||
inst := a.ampInstByID(tid)
|
||||
if inst == nil || inst.spe == nil {
|
||||
// A PowerGenius sitting in the group has no L/M/H, and saying so
|
||||
// would make a successful SPE pair look like a failure.
|
||||
if multi {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("power level is an SPE feature")
|
||||
}
|
||||
return inst.spe.SetPowerLevel(level)
|
||||
})
|
||||
}
|
||||
|
||||
// AmpFanMode sets the fan mode — PGXL only (STANDARD/CONTEST/BROADCAST).
|
||||
|
||||
Reference in New Issue
Block a user