Skip to content

Commit 03ddf48

Browse files
on8stclaudeten9876
authored
fix(hl2): a declined auto-gain arm explains itself where the operator is, not only in the log (#5825)
Closes #5817. The shape is the one proposed in that issue's triage, widened in review to every route an arm request can take. ## What the operator sees today Tick **Auto** with no stored gain for the current band and the constructed baseline sits above `kAutoRfGainMaxBaselineDb`. `setAutoRfGain` declines, and the checkbox **springs back to unticked with no explanation**. The wiring already reads `isArmed()` back, and that is correct — it is what stops the UI reporting a state the radio is not in (#5395). But a readback can only say *that* the request failed, and only after the operator's own click. The account of *why* — a well-formed sentence naming the baseline, the ceiling, and the remedy — went only to a `qWarning`. And two routes to an arm never pass through that click at all: the backend's own connect-time restore of a stored "on", and a bridge `pan autorfgain on`. On both, the checkbox reported the wrong state and a refusal was silent. On a fresh install that is the **first** tick of Auto: `kLnaDefaultGainDb` is 20 dB and the ceiling is 19 dB, so the shipped default is always refused. A bench whose stored gains sit at −12 never reproduces it, which is exactly why it went unnoticed here. ## The change The sentence was already being composed. It is now kept, every outcome of an arm request is announced on the seam, and one handler turns that into the checkbox state, a card where the operator is looking, and a channel a screen reader reads — **and takes it all back off again the moment the control arms.** | | | |---|---| | `src/core/backends/AutoRfGainControl.h` | `IAutoRfGainControl` gains `lastArmRefusalReason()`, **defaulted to empty** so no other backend changes; the contract now requires the backend to emit `autoRfGainArmSettled` after every outcome | | `src/core/backends/IRadioBackend.h` | new signal `autoRfGainArmSettled(bool armed)`: fired after a refusal, an arm and a disarm; not for a request that changed nothing | | `src/core/backends/hl2/Hl2Backend.{h,cpp}` | stores the sentence it already built — `tr()`'d, without the issue citation — **clears it on a successful arm and on the radio-swap reset**, and emits the settled signal on every outcome | | `src/models/RadioModel.{h,cpp}` | relays `autoRfGainArmSettled` to the GUI, caching nothing | | `src/gui/MainWindow.{h,cpp}` | `onAutoRfGainArmSettled`: reflects the outcome on every pan's copy of the checkbox; on a refusal, sets the description on all of them and raises the card and the spoken announcement once, on the active pan. The per-pan capability push also reads a standing reason, for a pan built after the connect-time restore settled | | `src/gui/MainWindow_Wiring.cpp` | the click lambda now only commands and reads back; the settled handler does the explaining | | `src/gui/SpectrumWidget.{h,cpp}` | `showNoticeCard(detail, id, ms)`: a transient card that takes the **caller's** id; the three card wrappers share one private builder | | `src/gui/SpectrumOverlayMenu.{h,cpp}` | `setAutoRfGainRefusalDescription()` puts the reason on the Auto checkbox's **accessible description**; arming, or hiding the control for a family without the loop, clears it and restores the help tooltip. `announceAutoRfGainRefusal()` speaks it once via `QAccessibleAnnouncementEvent` | | `src/core/AutomationServer.cpp` | `pan autorfgain on\|off` and its status form now report `armed` and `refusal` alongside `requested` | | `tests/spectrum_overlay_auto_rf_gain_refusal_test.cpp` | **new**, registered; the widget half | | `tests/hl2_gain_split_test.cpp` | the backend half: the reason, its clears, and the settled signal | ### On the panadapter, not the status bar, and not a dialog Not a dialog, because #4227 is stacked unclosable message boxes and a refusal the operator asked for by clicking does not warrant one. Not the status bar, because `MainWindow_Wiring`'s own `messageChanged` handler hides `m_statusBarContainer` wholesale on any non-empty message — so a timed notice there takes the TX indicator, PA temperature and supply voltage with it for its whole duration, to explain a checkbox. `MainWindow.cpp` and `MainWindow_Controllers.cpp` both route away from it citing #4649. ### Its own card id `showInterlockNotification` pins every card it raises to `interlock.active` on a latest-wins rule — deliberately, so the radio's authoritative denial supersedes a local preflight of the same refusal in place (#3999). A gain refusal is not the same thing: routed through it, the card would evict a live **Transmit disabled** card and be evicted by the next one. `showNoticeCard` takes the caller's id, which is the argument `showTxFilterNotification` already made for taking one of its own. The refusal's id is `autorfgain.refused`. ### One handler for every route `RadioModel::autoRfGainArmSettled` → `MainWindow::onAutoRfGainArmSettled`. Three routes reach an arm outcome and only one of them is a click: - **the operator's click** — the signal lands synchronously inside `setArmed()`; the lambda's readback afterwards is the #5395 guard for a backend that settled silently; - **the connect-time restore** in `Hl2Backend`, which settles inside its link-up handler after it has already emitted `connected()` — so the pans that would hear the signal may not exist yet, and the per-pan capability push reads the standing reason onto a late pan's description (no card: nothing just happened, that pan is catching up); - **a bridge `pan autorfgain on`**, which arms without going near a widget. ### And on a channel a screen reader actually reads The checkbox carries the sentence as its accessible **description** (what just happened to it) rather than its name (what it is), on every pan's copy. And it is **spoken once, at the moment it happens**: a description is read on arrival at a control, and on a refused tick the operator is already on it, so without the `QAccessibleAnnouncementEvent` the sentence would sit unread until they left and came back. The clipping indicator beside the box raises the same event for the same reason. One announcement, on the active pan, not one per copy. ### The clear lives in the widget, before the early return `SpectrumOverlayMenu::setAutoRfGainEnabled(true)` clears the description, and clears it **before** the function's "already there" early return: on the operator's retry the box is *checked* before the backend is asked, so when the arm succeeds `isChecked() == on` and the early return fires. A clear written after that return never runs on the one path it exists for. The clear is a **restore, not a blanking**: the tooltip goes back to the standing help text, hoisted into `autoRfGainHelpToolTip()` so the constructor and the clear share one literal. The backend clears its own copy too — on a successful arm, and on the radio-swap reset in `applyRestoredState()`, so radio B's control never reports a sentence composed about radio A's baseline. ### The sentence is no longer a log line It is shown on the panadapter and read out by a screen reader, so it is `tr()`'d, and the `#5354` citation moved to the `qWarning` — provenance for us, noise to an operator. ## No constant moves `kAutoRfGainMaxBaselineDb`, `kLnaDefaultGainDb` and the AD9866 fold question stay exactly where #5752 left them. Each of the three candidate "real" fixes settles the fold question by the back door, and #5752 merged explicitly **declining** to settle it. A visible refusal is the one change that does not take a position. ## Tests Two socket-free targets, both registered. **`hl2_gain_split_test`** — the backend half. The sentence names both numbers the operator needs and cites no issue number; a backend never asked has no reason to give; a successful arm clears it (asserted, not skipped); a radio swap clears it; and `autoRfGainArmSettled` fires on a refusal, an arm and a disarm, and not on a no-op. **`spectrum_overlay_auto_rf_gain_refusal_test`** — the widget half, offscreen, a plain `QWidget` parent. Walks refuse → retry → arm and asserts the description is *gone* afterwards and the help tooltip is *back*, with the help text captured from the widget rather than retyped; and that hiding the control for a family without the loop clears it. Each guard has been broken on purpose and the assertion that catches it named; the review threads carry the runs. ## What this does not cover - **No hardware.** Socket-free throughout; nothing keyed. That the card renders on a real refusal follows from the wiring and has not been watched on screen — the demo `SimBackend` returns no `IAutoRfGainControl`. - **`showNoticeCard` and `onAutoRfGainArmSettled` have no test of their own.** The overlay machinery underneath is covered by `panadapter_message_overlay_test`; `MainWindow` is not cheaply instantiable. - **Whether the pans exist by the time the connect-time restore settles** is not established; the capability-push catch-up is there for the case where they do not. - **The refusal itself is unchanged.** Whether 19 dB is the right ceiling, or 20 the right default, is the fold question and stays open. - **#5828** (an untick after a refusal is never recorded) is pre-existing, filed with a repro, and deliberately not folded in here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
1 parent 1360434 commit 03ddf48

17 files changed

Lines changed: 579 additions & 37 deletions

src/core/AutomationServer.cpp

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11486,10 +11486,16 @@ QJsonObject AutomationServer::doPan(const QString& action, const QString& arg)
1148611486
}
1148711487
const QString v = raw.toLower();
1148811488
if (v.isEmpty()) {
11489-
return QJsonObject{{QStringLiteral("ok"), true},
11489+
auto* status = radio->autoRfGain();
11490+
QJsonObject report{{QStringLiteral("ok"), true},
1149011491
{QStringLiteral("pan"), QStringLiteral("autorfgain")},
11491-
{QStringLiteral("available"), radio->autoRfGain() != nullptr},
11492+
{QStringLiteral("available"), status != nullptr},
1149211493
{QStringLiteral("requested"), false}};
11494+
if (status) {
11495+
report.insert(QStringLiteral("armed"), status->isArmed());
11496+
report.insert(QStringLiteral("refusal"), status->lastArmRefusalReason());
11497+
}
11498+
return report;
1149311499
}
1149411500
const bool on = (v == QLatin1String("on") || v == QLatin1String("true")
1149511501
|| v == QLatin1String("1"));
@@ -11505,13 +11511,16 @@ QJsonObject AutomationServer::doPan(const QString& action, const QString& arg)
1150511511
"pan autorfgain: this radio has no automatic RF gain control"));
1150611512
}
1150711513
ag->setArmed(on);
11508-
// DELIBERATELY NOT AN ECHO OF THE ARMED STATE. The backend may decline
11509-
// to arm and log why; reporting "requested" rather than "on" keeps this
11510-
// verb honest about the difference. Read `health` for what actually
11511-
// happened -- that is the row that comes from the backend.
11514+
// `requested` is what was asked and `armed` is what the backend did;
11515+
// they differ when it declined, and `refusal` then carries the same
11516+
// sentence the GUI shows (#5817). The verb used to report only the
11517+
// request, so a headless caller had to scrape the log to learn the
11518+
// arm never took.
1151211519
return QJsonObject{{QStringLiteral("ok"), true},
1151311520
{QStringLiteral("pan"), QStringLiteral("autorfgain")},
11514-
{QStringLiteral("requested"), on}};
11521+
{QStringLiteral("requested"), on},
11522+
{QStringLiteral("armed"), ag->isArmed()},
11523+
{QStringLiteral("refusal"), ag->lastArmRefusalReason()}};
1151511524
}
1151611525

1151711526
if (action == QLatin1String("float") || action == QLatin1String("dock")) {

src/core/backends/AutoRfGainControl.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,29 @@ class IAutoRfGainControl {
5656
// A backend may DECLINE to arm — the HL2 refuses above a baseline where its
5757
// gain axis is not trustworthy — so a caller must read `isArmed()` back
5858
// rather than assuming the request took.
59+
//
60+
// AND THE BACKEND MUST EMIT IRadioBackend::autoRfGainArmSettled after every
61+
// outcome, refusal included. A reader that polls isArmed() only after its
62+
// own click never learns about an arm settled elsewhere -- the backend's
63+
// own connect-time restore, a bridge verb -- and reports a control as off
64+
// while it holds the operator's gain down (#5817).
5965
virtual void setArmed(bool on) = 0;
6066
[[nodiscard]] virtual bool isArmed() const = 0;
6167

68+
// WHY IT DECLINED, in a sentence meant for the operator rather than a log.
69+
//
70+
// Reading `isArmed()` back tells a caller THAT the request did not take.
71+
// It cannot tell them why, and without the why the operator sees a checkbox
72+
// spring back to unticked with no explanation -- which on the HL2 is what
73+
// every fresh install does on its first tick of Auto, because a stored gain
74+
// is absent and the constructed baseline sits above the ceiling that gates
75+
// arming (#5817).
76+
//
77+
// Empty when the last attempt succeeded, or when a backend has no reason to
78+
// give. A caller shows it only after a readback has already shown the
79+
// request failed; it is not a status line.
80+
[[nodiscard]] virtual QString lastArmRefusalReason() const { return {}; }
81+
6282
// How far below the operator's own gain the control may go, in dB. The
6383
// second of exactly two numbers the operator owns; the first is the switch.
6484
// Everything else about such a loop is a decision they have no evidence to

src/core/backends/IRadioBackend.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,16 @@ class IRadioBackend : public QObject {
11301130
// behaves strangely. See FrontEndOverload.h.
11311131
void frontEndOverloadChanged(const AetherSDR::FrontEndOverload& state);
11321132

1133+
// AN ARM REQUEST ON autoRfGainControl() HAS SETTLED: `armed` is what the
1134+
// control is now doing. Emitted after EVERY outcome of setArmed() --
1135+
// refused (armed stays false; lastArmRefusalReason() says why), armed, and
1136+
// disarmed -- and not for a request that changed nothing. This is how a
1137+
// view learns about an arm it did not ask for: the connect-time restore
1138+
// inside the backend and a bridge `pan autorfgain on` both settle without
1139+
// passing through any GUI click, and a checkbox that only read isArmed()
1140+
// back after its own click reported the wrong state on both (#5817).
1141+
void autoRfGainArmSettled(bool armed);
1142+
11331143
// Normalized transmit-status delta (aetherd RFC 2.3 — TransmitModel
11341144
// touchpoint). Typed + compiler-checked; the backend populates only the
11351145
// fields the wire reported (across the transmit / interlock / ATU / APD /

src/core/backends/hl2/Hl2Backend.cpp

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6458,6 +6458,11 @@ void Hl2Backend::applyRestoredState(const RestoredRadioState& state)
64586458
// A radio swap ends the session, and an automatic control armed about radio
64596459
// A's antenna has nothing to say about radio B's.
64606460
m_autoRfGainEnabled = false;
6461+
// Nor does a refusal composed about radio A's baseline: the interface
6462+
// promises an empty reason from a backend that has not been asked, and
6463+
// radio B has not been. Left standing, any reader other than the toggle
6464+
// lambda would surface radio A's number as radio B's.
6465+
m_autoRfGainRefusal.clear();
64616466
m_autoGainState = AetherSDR::hl2::AutoGainState{};
64626467
m_autoGainConfig = AetherSDR::hl2::AutoGainConfig{};
64636468
m_autoGainMode = QStringLiteral("ramp");
@@ -7006,16 +7011,38 @@ void Hl2Backend::setAutoRfGain(bool on)
70067011
// so a radio that declined to arm keeps the operator's on
70077012
// recorded"; without this line nothing had recorded it.
70087013
m_autoRfGainWanted = true;
7009-
qWarning().noquote()
7010-
<< QStringLiteral(
7011-
"Hl2Backend: auto RF gain declined — the RF Gain baseline is "
7012-
"%1 dB and this radio's gain axis is not trusted above %2 dB "
7013-
"(#5354: +48 dB measures like +18 dB). Lower RF Gain to %2 dB "
7014-
"or below and try again. Your setting has not been changed.")
7014+
// COMPOSED ONCE AND KEPT, because the operator needs it more than
7015+
// the log does. Reading isArmed() back tells the GUI THAT this
7016+
// declined; only this sentence says why, and it already names the
7017+
// baseline, the ceiling and the remedy. Storing it is what lets the
7018+
// checkbox explain itself instead of springing back in silence
7019+
// (#5817).
7020+
//
7021+
// tr(), AND WITHOUT THE ISSUE NUMBER, because this sentence stopped
7022+
// being a log line the moment it was kept: it is shown on the
7023+
// panadapter and read out by a screen reader. "#5354" is provenance
7024+
// for us and noise to an operator, so it stays on the qWarning --
7025+
// which is where the next person debugging this actually looks --
7026+
// and the operator gets the baseline, the ceiling and the remedy.
7027+
m_autoRfGainRefusal = tr(
7028+
"Auto RF gain declined — the RF Gain baseline is "
7029+
"%1 dB and this radio's gain axis is not trusted above "
7030+
"%2 dB. Lower RF Gain to %2 dB or below and try again. "
7031+
"Your setting has not been changed.")
70157032
.arg(m_lnaGainDb)
70167033
.arg(kAutoRfGainMaxBaselineDb);
7034+
qWarning().noquote()
7035+
<< QStringLiteral("Hl2Backend: ") + m_autoRfGainRefusal
7036+
+ QStringLiteral(" (#5354: +48 dB measures like +18 dB)");
7037+
// SETTLED AS NOT ARMED, and said so. A refusal that only the
7038+
// caller's own readback could discover was invisible on the two
7039+
// routes that have no readback: the restore below and the bridge.
7040+
emit autoRfGainArmSettled(false);
70177041
return;
70187042
}
7043+
// CLEARED ON SUCCESS. A reason that outlived the refusal it describes
7044+
// would be shown against a later, unrelated failure.
7045+
m_autoRfGainRefusal.clear();
70197046
m_autoGainState = AetherSDR::hl2::AutoGainState{};
70207047
m_autoGainBandKey = m_currentBandKey;
70217048
m_autoGainBaselineDb = m_lnaGainDb;
@@ -7031,6 +7058,7 @@ void Hl2Backend::setAutoRfGain(bool on)
70317058
applyBandscopeForAutoGain();
70327059
qCInfo(lcHl2) << "HL2 auto RF gain: ARMED at baseline" << m_lnaGainDb
70337060
<< "dB, floor" << m_autoGainConfig.maxOffsetDb << "dB below";
7061+
emit autoRfGainArmSettled(true);
70347062
} else {
70357063
m_autoRfGainEnabled = false;
70367064
// The operator turning it OFF is a preference, and is persisted as one.
@@ -7046,6 +7074,7 @@ void Hl2Backend::setAutoRfGain(bool on)
70467074
setLnaAutoOffsetDb(0);
70477075
qCInfo(lcHl2) << "HL2 auto RF gain: disarmed, baseline" << m_lnaGainDb
70487076
<< "dB restored";
7077+
emit autoRfGainArmSettled(false);
70497078
}
70507079
}
70517080

src/core/backends/hl2/Hl2Backend.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ class Hl2Backend : public IRadioBackend, public IAutoRfGainControl {
250250
IAutoRfGainControl* autoRfGainControl() override { return this; }
251251
void setArmed(bool on) override { setAutoRfGain(on); }
252252
[[nodiscard]] bool isArmed() const override { return m_autoRfGainEnabled; }
253+
[[nodiscard]] QString lastArmRefusalReason() const override
254+
{
255+
return m_autoRfGainRefusal;
256+
}
253257
void setFloorDb(int floorDb) override { setAutoRfGainFloorDb(floorDb); }
254258
[[nodiscard]] int floorDb() const override { return autoRfGainFloorDb(); }
255259
[[nodiscard]] int maxFloorDb() const override { return kAutoRfGainFloorMaxDb; }
@@ -1037,6 +1041,10 @@ class Hl2Backend : public IRadioBackend, public IAutoRfGainControl {
10371041
// the control stays off, and the next connect from a baseline it trusts
10381042
// honours the operator without them having to ask twice.
10391043
bool m_autoRfGainWanted = false;
1044+
// Why the last arm attempt was declined, empty when it was not. See
1045+
// setAutoRfGain: composed where the refusal happens, cleared on a
1046+
// successful arm so it can never describe a different failure.
1047+
QString m_autoRfGainRefusal;
10401048
AetherSDR::hl2::AutoGainState m_autoGainState;
10411049
AetherSDR::hl2::AutoGainConfig m_autoGainConfig;
10421050

src/gui/MainWindow.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,12 @@ MainWindow::MainWindow(QWidget* parent)
14291429
});
14301430
connect(&m_radioModel, &RadioModel::interlockNotificationRequested,
14311431
this, &MainWindow::showPanadapterInterlockNotification);
1432+
// An automatic-gain arm request settled -- from the operator's click, the
1433+
// backend's own connect-time restore, or a bridge verb. One handler for all
1434+
// three, because the checkbox reading isArmed() back after its OWN click
1435+
// covered exactly one of them (#5817).
1436+
connect(&m_radioModel, &RadioModel::autoRfGainArmSettled,
1437+
this, &MainWindow::onAutoRfGainArmSettled);
14321438
// Goes on the panadapter as a transient card, NOT the status bar (#4649).
14331439
// A QStatusBar temporary message hides every permanent widget for its whole
14341440
// duration -- the TX indicator, PA temperature and supply voltage among
@@ -8007,8 +8013,18 @@ void MainWindow::applyRadioSideDspToPanDisplay(SpectrumWidget* sw) const
80078013
// did not reflect that would report a control as off while it was
80088014
// holding the operator's gain down.
80098015
auto* autoGain = m_radioModel.autoRfGain();
8016+
const bool armed = autoGain && autoGain->isArmed();
80108017
menu->setAutoRfGainAvailable(autoGain != nullptr);
8011-
menu->setAutoRfGainEnabled(autoGain && autoGain->isArmed());
8018+
menu->setAutoRfGainEnabled(armed);
8019+
// AND IF THE LAST REQUEST WAS REFUSED BEFORE THIS PAN EXISTED. The
8020+
// backend's connect-time restore settles inside its link-up handler,
8021+
// after it has emitted connected() and before the pans that would have
8022+
// heard autoRfGainArmSettled are built. The reason is still on the
8023+
// control, and the description is the only channel a late pan has for
8024+
// it. No card: nothing just happened, this pan is only catching up.
8025+
if (autoGain && !armed) {
8026+
menu->setAutoRfGainRefusalDescription(autoGain->lastArmRefusalReason());
8027+
}
80128028
}
80138029
// A MASK, not a rewrite: the operator's stored HW preference survives a
80148030
// session on a radio that has no hardware black level, and comes back by

src/gui/MainWindow.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,9 @@ private slots:
763763
void showPanadapterInterlockNotification(const QString& message,
764764
const QString& key = QString(),
765765
const QString& panId = QString());
766+
// RadioModel::autoRfGainArmSettled: reflect the outcome on every pan's copy
767+
// of the Auto checkbox, and explain a refusal once, on the active pan.
768+
void onAutoRfGainArmSettled(bool armed);
766769
void setActivePanApplet(PanadapterApplet* applet);
767770
void routeCwDecoderOutput();
768771
// Show a decoder panel on exactly one applet — the current decoder target —

src/gui/MainWindow_Wiring.cpp

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5481,14 +5481,21 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)
54815481
this, [this, sw](bool on) {
54825482
auto* autoGain = m_radioModel.autoRfGain();
54835483
if (autoGain) {
5484+
// The backend settles the request -- armed, or refused with a
5485+
// reason -- and says so through RadioModel::autoRfGainArmSettled.
5486+
// onAutoRfGainArmSettled turns that into the checkbox state, the
5487+
// card and the accessible description, and it is the one place all
5488+
// three routes to an arm meet: this click, the backend's own
5489+
// connect-time restore, and a bridge verb (#5817).
54845490
autoGain->setArmed(on);
54855491
}
5486-
// READ BACK WHAT ACTUALLY HAPPENED. The backend may DECLINE to arm --
5487-
// the HL2 refuses from a gain baseline inside the register region where
5488-
// #5354 measured +48 dB reading identically to +18 dB -- and a checkbox
5489-
// that stayed ticked over a control that is not running would be the
5490-
// #5395 defect exactly: a UI reporting one state while the radio is in
5491-
// another. So ask the control rather than assuming the request took.
5492+
// READ BACK WHAT ACTUALLY HAPPENED, regardless. The backend may DECLINE
5493+
// to arm -- the HL2 refuses from a gain baseline inside the register
5494+
// region where #5354 measured +48 dB reading identically to +18 dB --
5495+
// and a checkbox that stayed ticked over a control that is not running
5496+
// would be the #5395 defect exactly: a UI reporting one state while the
5497+
// radio is in another. The settled signal normally lands first, inside
5498+
// setArmed(); this is the guard for a backend that settled silently.
54925499
if (auto* m = sw->overlayMenu()) {
54935500
m->setAutoRfGainEnabled(autoGain && autoGain->isArmed());
54945501
}
@@ -5533,6 +5540,70 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)
55335540
refreshKiwiSdrWaterfallAvailability();
55345541
}
55355542

5543+
// AN ARM REQUEST SETTLED, from whichever route asked. Reflect it on every pan's
5544+
// copy of the control, and if it was a refusal, say why.
5545+
//
5546+
// ONE HANDLER FOR ALL ROUTES. The Auto checkbox used to learn the outcome only
5547+
// by reading isArmed() back after its own click. That covers the click and
5548+
// nothing else: the HL2 arms (or refuses) from its stored preference inside its
5549+
// own link-up handler, after it has already emitted connected(), and a bridge
5550+
// `pan autorfgain on` arms without going near a widget. On both, the checkbox
5551+
// reported the wrong state and a refusal was silent -- the #5817 springback,
5552+
// just not on a click (#5395 is the same defect seen from the other side).
5553+
//
5554+
// WHAT GOES WHERE. The armed state and the refusal DESCRIPTION go on every
5555+
// pan's checkbox, because the control is radio-wide and each pan carries a
5556+
// copy: the description is state, and a copy left saying "declined" over a
5557+
// running loop would be read out as such. setAutoRfGainEnabled(true) is what
5558+
// clears a standing description. The card and the spoken announcement go
5559+
// ONCE, on the active pan, where the operator is looking.
5560+
//
5561+
// ON THE PANADAPTER, NOT THE STATUS BAR, and not a dialog either. Not a dialog
5562+
// because #4227 is stacked unclosable message boxes, and a refusal the
5563+
// operator asked for by clicking does not warrant one. Not the status bar
5564+
// because of #4649: any non-empty temporary message hides m_statusBarContainer
5565+
// wholesale for its whole duration, taking the TX indicator, PA temperature
5566+
// and supply voltage with it -- MainWindow.cpp and MainWindow_Controllers.cpp
5567+
// both route away from it for the same reason. The card takes its OWN id
5568+
// rather than riding on the interlock's "interlock.active" latest-wins: a
5569+
// gain refusal must not evict a live "Transmit disabled" card, nor be evicted
5570+
// by one.
5571+
void MainWindow::onAutoRfGainArmSettled(bool armed)
5572+
{
5573+
if (!m_panStack) {
5574+
return;
5575+
}
5576+
auto* autoGain = m_radioModel.autoRfGain();
5577+
const QString why = (!armed && autoGain) ? autoGain->lastArmRefusalReason()
5578+
: QString();
5579+
const QStringList panIds = m_panStack->panIds();
5580+
for (const QString& panId : panIds) {
5581+
SpectrumWidget* sw = m_panStack->spectrum(panId);
5582+
auto* m = sw ? sw->overlayMenu() : nullptr;
5583+
if (!m) {
5584+
continue;
5585+
}
5586+
m->setAutoRfGainEnabled(armed);
5587+
if (!why.isEmpty()) {
5588+
m->setAutoRfGainRefusalDescription(why);
5589+
}
5590+
}
5591+
if (why.isEmpty()) {
5592+
return;
5593+
}
5594+
SpectrumWidget* where = m_panStack->activeSpectrum();
5595+
if (!where && !panIds.isEmpty()) {
5596+
where = m_panStack->spectrum(panIds.first());
5597+
}
5598+
if (!where) {
5599+
return;
5600+
}
5601+
where->showNoticeCard(why, QStringLiteral("autorfgain.refused"), 10000);
5602+
if (auto* m = where->overlayMenu()) {
5603+
m->announceAutoRfGainRefusal(why);
5604+
}
5605+
}
5606+
55365607
MainWindow::TuneCenteringResult MainWindow::revealFrequencyIfNeeded(
55375608
SliceModel* slice, double mhz, TuneIntent intent, const char* source,
55385609
double leftFlagEdgeOffsetMhz, double rightFlagEdgeOffsetMhz)

0 commit comments

Comments
 (0)