Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/HERMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2961,6 +2961,19 @@ Backend pan ids are now translated through a first-seen-order allocator and stay
OPAQUE in both directions — `RadioModel` does not parse a family's naming scheme,
and a backend does not learn about the `0xE1000000` stream-id space.

HL2 RF gain is stored per band in the radio's `OperatingState` document.
On reconnect, the backend restores the start band's entry (or its default for
an unvisited band). The display mirrors that value; the legacy
`DisplayRfGain_hl2` setting is ignored during startup so it cannot overwrite
the band's entry (#5400). The `RfGain` client-settings domain remains enabled:
`RadioStateMemory` needs it to load and save the per-band map.

An explicit `lnaGainDb` connection parameter can temporarily override the live
gain without replacing an existing start-band entry (#5402). A band change
restores the new band's gain; an operator gain change updates the current band.
All panadapters still share the one hardware LNA. Flex and Icom restoration
behavior is unchanged.

### 20.10 The dynamic lifecycle: receivers come and go while the radio runs

The count was fixed at connect, from a persisted setting. It is now the
Expand Down
60 changes: 53 additions & 7 deletions src/core/backends/hl2/Hl2Backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "core/backends/hl2/Hl2RxDsp.h"
#include "core/backends/hl2/Hl2TxDsp.h"
#include "core/backends/hl2/Hl2BandMemoryPolicy.h"
#include "core/backends/hl2/Hl2OverloadPolicy.h"
#include "core/backends/hl2/Hl2DspSetupPolicy.h"
#include "core/backends/hl2/Hl2TxLevelPolicy.h"
Expand Down Expand Up @@ -1713,11 +1714,29 @@ void Hl2Backend::connectRadio(const RadioConnectRequest& request)
// Per-band memory (RFC #4603 PR 3): the session comes up with the start
// band's remembered LNA. The explicit param still wins via the guard.
m_currentBandKey = hl2::bandKeyForHz(startFreqHz);
if (m_haveRestoredState
&& !request.params.contains(QStringLiteral("lnaGainDb"))) {
m_lnaGainDb = qBound(kLnaGainMinDb,
m_lnaDbByBand.value(m_currentBandKey, m_lnaDefaultDb),
kLnaGainMaxDb);
{
const bool paramPresent =
request.params.contains(QStringLiteral("lnaGainDb"));
const bool hasStored = m_lnaDbByBand.contains(m_currentBandKey);
const AetherSDR::hl2::ConnectLna seed = AetherSDR::hl2::connectLna(
m_haveRestoredState, hasStored,
m_lnaDbByBand.value(m_currentBandKey, m_lnaDefaultDb),
paramPresent,
request.params.value(QStringLiteral("lnaGainDb")).toInt(),
m_lnaDefaultDb, kLnaGainMinDb, kLnaGainMaxDb);
// Only take the live value when the policy actually had something to
// say: with no restored state and no param it returns the default,
// which must not stamp on a value the lines above already settled.
if (m_haveRestoredState || paramPresent) {
m_lnaGainDb = seed.liveDb;
}
m_lnaSessionPin = seed.sessionPin;
if (m_lnaSessionPin) {
qCInfo(lcHl2) << "HL2: lnaGainDb param pins" << m_lnaGainDb
<< "dB for this session;" << m_currentBandKey
<< "keeps its stored"
<< m_lnaDbByBand.value(m_currentBandKey) << "dB";
}
}
// Seed the DRIVE from the start band's memory and echo it upward NOW —
// before linkUp — so TransmitModel carries the restored value when its
Expand Down Expand Up @@ -3007,6 +3026,9 @@ void Hl2Backend::setPanRfGain(const QString& panId, int gainDb)
qCInfo(lcHl2) << "HL2 LNA gain:" << m_lnaGainDb << "dB (requested" << gainDb << ")";

// The operator's gain belongs to the band they set it on (RFC #4603 PR 3).
// This is also what ends a session pin: the value is now the operator's
// own choice for this band, so the band memory is theirs to overwrite.
m_lnaSessionPin = false;
Comment thread
Ozy311 marked this conversation as resolved.
if (!m_currentBandKey.isEmpty())
m_lnaDbByBand.insert(m_currentBandKey, m_lnaGainDb);
notifyOperatingStateChanged();
Expand Down Expand Up @@ -4303,6 +4325,7 @@ void Hl2Backend::applyRestoredState(const RestoredRadioState& state)
m_driveByBand.clear();
m_lnaDefaultDb = 20; // Hl2Backend.h: m_lnaGainDb's constructed default
m_lnaGainDb = 20;
m_lnaSessionPin = false;
m_driveDefaultPercent = -1;
m_rfPowerPercent = 100; // TransmitModel's session default
m_sampleRateHz = 48000; // construction default — radio B must not
Expand Down Expand Up @@ -4571,7 +4594,22 @@ RestoredRadioState Hl2Backend::currentOperatingState() const
for (auto it = m_driveByBand.constBegin(); it != m_driveByBand.constEnd(); ++it)
driveByBand.insert(it.key(), it.value());
if (!m_currentBandKey.isEmpty()) {
lnaByBand.insert(m_currentBandKey, m_lnaGainDb);
// THE SAME PRESERVATION RULE AS THE WRITE-BACK, and it has to be here
// too. This snapshot is taken on a debounced store that any unrelated
// action schedules -- a same-band tune, a mode change, a filter change
// -- so it reaches the band map long BEFORE the first band change.
// Protecting only rememberCurrentBandState() left the session pin free
// to be persisted through this path: restore 20 m at -12, connect with
// lnaGainDb=20, tune within 20 m, and the capture stored 20 for 20 m.
// (#5402 review, Ozy311.)
//
// One policy, two call sites asking it -- not two copies of the rule.
lnaByBand.insert(
m_currentBandKey,
AetherSDR::hl2::bandMemoryWriteback(
m_lnaGainDb, m_lnaSessionPin,
m_lnaDbByBand.contains(m_currentBandKey),
m_lnaDbByBand.value(m_currentBandKey)));
driveByBand.insert(m_currentBandKey, m_rfPowerPercent);
}

Expand Down Expand Up @@ -4637,7 +4675,12 @@ void Hl2Backend::rememberCurrentBandState()
{
if (m_currentBandKey.isEmpty())
return;
m_lnaDbByBand.insert(m_currentBandKey, m_lnaGainDb);
m_lnaDbByBand.insert(
m_currentBandKey,
AetherSDR::hl2::bandMemoryWriteback(
Comment thread
Ozy311 marked this conversation as resolved.
m_lnaGainDb, m_lnaSessionPin,
m_lnaDbByBand.contains(m_currentBandKey),
m_lnaDbByBand.value(m_currentBandKey)));
m_driveByBand.insert(m_currentBandKey, m_rfPowerPercent);
}

Expand All @@ -4652,6 +4695,9 @@ void Hl2Backend::applyPerBandStateFor(double freqHz, const char* reason)
// review: the drive that makes 5 W on 80 m is not polite on 10 m, so a
// band change must never carry the old band's drive along.
rememberCurrentBandState();
// Clear only AFTER writeback preserves the start band. Later bands must
// record their own gains normally.
m_lnaSessionPin = false;
Comment thread
Ozy311 marked this conversation as resolved.
const QString oldBand = m_currentBandKey;
m_currentBandKey = newBand;

Expand Down
4 changes: 4 additions & 0 deletions src/core/backends/hl2/Hl2Backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,10 @@ class Hl2Backend : public IRadioBackend {
QMap<QString, int> m_lnaDbByBand;
QMap<QString, int> m_driveByBand;
int m_lnaDefaultDb = 20; // matches m_lnaGainDb's own default
// The connect param pinned a gain that the start band did not have stored.
// Live value honoured, persistence refused: see Hl2BandMemoryPolicy.h.
// Cleared when the operator changes gain or leaves the start band.
bool m_lnaSessionPin = false;
Comment thread
Ozy311 marked this conversation as resolved.
int m_driveDefaultPercent = -1; // <0: no restored default; leave drive alone
QString m_currentBandKey;
// True while band-memory / restore code drives setTxPower() itself: the
Expand Down
96 changes: 96 additions & 0 deletions src/core/backends/hl2/Hl2BandMemoryPolicy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#pragma once

// Per-band LNA memory: which value a session comes up on, and which value the
// band memory records when the operator leaves that band.
//
// These are two separate questions and the backend previously answered only the
// first. The second is where the defect lives: a connect that pins the LNA via
// the namespaced lnaGainDb param diverges the live value from the start band's
// stored entry, and the FIRST band change then writes the live value back over
// that entry (Hl2Backend::rememberCurrentBandState). The operator's calibration
// for that band is gone, replaced by a number that was only ever meant to hold
// for one session.
//
// Symptom, and why it is worth a header: the loss is silent and it is delayed.
// Nothing is wrong at connect — the pinned value is what was asked for. The
// stored entry dies later, on an unrelated action, and the next session comes
// up on the pinned value as though the operator had chosen it. By the time a
// band sounds wrong there is nothing left on disk that says what it used to be.
//
// They live in a header, evaluated by Hl2Backend rather than copied into it, so
// the suite exercises the SAME expressions the backend runs — the reasoning
// Hl2TxLevelPolicy.h states, and the same reason it applies here: a test
// against a re-typed copy of this decision would agree with itself while the
// backend kept the bug.
//
// See Hl2Backend::connectRadio and ::applyPerBandStateFor for the surrounding
// ordering; this header is the decision only.

namespace AetherSDR::hl2 {

// A clamp local to this header so the decision is testable without pulling in
// the backend's translation unit. Mirrors qBound's argument order.
constexpr int clampDb(int minDb, int v, int maxDb)
{
return v < minDb ? minDb : (v > maxDb ? maxDb : v);
}

// What a session comes up on for the start band.
struct ConnectLna {
int liveDb = 0;
// TRUE when liveDb came from the connect param while the start band ALSO
// had a stored entry — i.e. the live value is a session pin that the
// operator never chose for this band. Purely informational to the caller;
// it is bandMemoryWriteback below that decides what it costs.
bool sessionPin = false;
};

inline ConnectLna connectLna(bool haveRestoredState,
bool hasStoredEntry, int storedDb,
bool paramPresent, int paramDb,
int defaultDb, int minDb, int maxDb)
{
ConnectLna out;
// The explicit param still wins the LIVE value. That precedence is
// deliberate and documented at the call site: an automation or test caller
// pins the gain outright, and a stored entry must not silently ignore what
// the caller asked for. This header does not reverse it.
if (paramPresent) {
Comment thread
Ozy311 marked this conversation as resolved.
// Preserve the pre-existing explicit-parameter behavior; this PR
// changes persistence, not the connect parameter's range handling.
out.liveDb = paramDb;
out.sessionPin = haveRestoredState && hasStoredEntry && paramDb != storedDb;
return out;
}
if (haveRestoredState) {
out.liveDb = clampDb(minDb, hasStoredEntry ? storedDb : defaultDb, maxDb);
return out;
}
out.liveDb = defaultDb;
return out;
}

// What rememberCurrentBandState() should record for the band being left.
//
// Normally the live value: leaving a band records what the operator set while
// they were on it, which is the whole point of the memory.
//
// The exception is a session pin. That value came from the connect param, not
// from the operator acting on this band, and the band already had an entry of
// its own — so recording it would overwrite a calibration with a number nobody
// chose for this band. The stored entry is kept instead.
//
// Note what this deliberately does NOT do: it does not make the pin invisible.
// The live gain stays pinned, the radio runs at the requested value, and every
// pan is told about it. Only the persistence is refused, because persistence is
// the part that outlives the session that asked for it.
inline int bandMemoryWriteback(int liveDb, bool sessionPin,
bool hasStoredEntry, int storedDb)
{
if (sessionPin && hasStoredEntry) {
return storedDb;
}
return liveDb;
}

} // namespace AetherSDR::hl2
17 changes: 7 additions & 10 deletions src/gui/MainWindow_Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "PhoneCwApplet.h"
#include "SpectrumOverlayMenu.h"
#include "RfGainPresentation.h"
#include "RfGainRestore.h"
#include "core/backends/ConnectionSharingPolicy.h" // in-use share gate (#4448), shared with ConnectionPanel
#include "core/backends/sim/SimBackend.h" // demo owns its audio — see wirePanStreamRxAudioSinks
#include "core/CwSidetoneGenerator.h"
Expand Down Expand Up @@ -1667,19 +1668,15 @@ void MainWindow::wirePanLifecycle()
const bool clientOwnsRfGain =
m_radioModel.backendCapabilities().clientSettingsDomains.testFlag(
RadioCapabilities::ClientSettingsDomain::RfGain);
// Flex deliberately declares no client-owned RF-gain domain: the
// radio persists and reports its pan gain. Sim likewise regenerates
// its scene. Only a backend that explicitly delegates this domain
// (currently HL2) may receive a saved client replay.
const bool restoreSavedRfGain = clientOwnsRfGain && haveSavedRfGain;
PanadapterModel* activePan = m_radioModel.activePanadapter();
const int rfGain = restoreSavedRfGain
? s.value(rfGainKey).toInt()
: (activePan ? activePan->rfGain() : 0);
m_radioModel.setPanWnb(wnbOn);
m_radioModel.setPanWnbLevel(wnbLevel);
if (restoreSavedRfGain)
m_radioModel.setPanRfGain(rfGain);
const int rfGain = restoreLegacyRfGain(
m_radioModel.backendCapabilities().family, clientOwnsRfGain,
haveSavedRfGain ? std::optional<int>(s.value(rfGainKey).toInt())
: std::nullopt,
activePan ? activePan->rfGain() : 0,
[this](int gain) { m_radioModel.setPanRfGain(gain); });
sw->setWnbActive(wnbOn);
sw->setRfGain(rfGain);
sw->overlayMenu()->setWnbState(wnbOn, wnbLevel);
Expand Down
23 changes: 23 additions & 0 deletions src/gui/RfGainRestore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <QStringView>
#include <optional>

namespace AetherSDR {

// HL2 restores its per-band gain through RadioStateMemory. Its legacy display
// value has no band identity and must not be replayed as an operator change
// (#5400). Keep the existing replay rules for every other family.
template<typename SetGain>
int restoreLegacyRfGain(QStringView family, bool clientOwnsGain,
std::optional<int> savedGain, int currentGain,
SetGain setGain)
{
if (family != u"hl2" && clientOwnsGain && savedGain.has_value()) {
setGain(*savedGain);
return *savedGain;
}
return currentGain;
}

} // namespace AetherSDR
Loading
Loading