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
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ set(GUI_SOURCES
src/gui/ConnectedStationsDialog.cpp
src/gui/SpectrumWidget.cpp
src/gui/SpectrumOverlayMenu.cpp
src/gui/FrequencyEntryParser.cpp
src/gui/SliceColorManager.cpp
src/gui/SliceLabel.cpp
src/gui/VfoWidget.cpp
Expand Down Expand Up @@ -1454,6 +1455,14 @@ add_executable(xvtr_policy_test
target_include_directories(xvtr_policy_test PRIVATE src)
target_link_libraries(xvtr_policy_test PRIVATE Qt6::Core)

add_executable(frequency_entry_parser_test
tests/frequency_entry_parser_test.cpp
src/gui/FrequencyEntryParser.cpp
)
target_include_directories(frequency_entry_parser_test PRIVATE src)
target_link_libraries(frequency_entry_parser_test PRIVATE Qt6::Core)
add_test(NAME frequency_entry_parser_test COMMAND frequency_entry_parser_test)

add_executable(radio_status_ownership_test
tests/radio_status_ownership_test.cpp
src/core/CommandParser.cpp
Expand Down
36 changes: 36 additions & 0 deletions src/gui/FrequencyEntryParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "FrequencyEntryParser.h"

namespace AetherSDR::FrequencyEntryParser {

QString normalizedMhzText(const QString& text)
{
QString clean = text.trimmed();
const int firstDot = clean.indexOf(QLatin1Char('.'));
if (firstDot >= 0) {
const QString beforeDot = clean.left(firstDot);
const QString afterDot = clean.mid(firstDot + 1).remove(QLatin1Char('.'));
clean = beforeDot + QLatin1Char('.') + afterDot;
}
return clean;
}

bool isExplicitMhzEntry(const QString& rawText, const QString& normalizedText)
{
const int dot = normalizedText.indexOf(QLatin1Char('.'));
if (dot < 0) {
return false;
}

// Display-style entries are already MHz.kHz.Hz, even when the current
// slice is not yet on an XVTR/high-frequency band.
if (rawText.count(QLatin1Char('.')) >= 2) {
return true;
}

// Single-dot entries with a normal MHz field are explicit MHz. Preserve
// the historic HF shortcut where `14225.0` means 14.225 MHz by treating
// five-plus leading digits as kHz-style input.
return dot <= 4;
}

} // namespace AetherSDR::FrequencyEntryParser
10 changes: 10 additions & 0 deletions src/gui/FrequencyEntryParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#include <QString>

namespace AetherSDR::FrequencyEntryParser {

QString normalizedMhzText(const QString& text);
bool isExplicitMhzEntry(const QString& rawText, const QString& normalizedText);

} // namespace AetherSDR::FrequencyEntryParser
123 changes: 116 additions & 7 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2979,6 +2979,14 @@ MainWindow::MainWindow(QWidget* parent)
connect(m_appletPanel->rxApplet(), &RxApplet::afGainChanged, this, [this](int v) {
if (auto* s = activeSlice()) s->setAudioGain(v);
});
connect(m_appletPanel->rxApplet(), &RxApplet::directEntryCommitted,
this, [this](double mhz, const QString& source) {
if (auto* s = activeSlice()) {
const QByteArray sourceUtf8 = source.toUtf8();
applyTuneRequest(s, mhz, TuneIntent::CommandedTargetCenter,
sourceUtf8.constData());
}
});

// ── Slice tab toggle: click A/B/C/D → switch active slice (#1278) ──
connect(m_appletPanel->rxApplet(), &RxApplet::sliceActivationRequested,
Expand Down Expand Up @@ -8556,7 +8564,7 @@ void MainWindow::onConnectionStateChanged(bool connected)
QVector<SpectrumOverlayMenu::XvtrBand> xvtrBands;
for (const auto& x : m_radioModel.xvtrList()) {
if (x.isValid)
xvtrBands.append({x.name, x.rfFreq});
xvtrBands.append({x.name, x.rfFreq, QString("X%1").arg(x.index)});
}
const ModelCapabilities caps = m_radioModel.capabilities();
for (auto* applet : m_panStack->allApplets()) {
Expand Down Expand Up @@ -9040,7 +9048,8 @@ bool MainWindow::activateMemorySpot(int memoryIndex, const QString& preferredPan
const QString currentBand = BandSettings::bandForFrequency(slice->frequency());
if (memoryBand != currentBand) {
const auto xvtrs = xvtrPolicyBandsFrom(m_radioModel.xvtrList());
const auto stackKeyResult = XvtrPolicy::resolveBandStackKey(memoryBand, xvtrs);
const auto stackKeyResult =
XvtrPolicy::resolveBandStackKey(memoryBand, xvtrs, m_radioModel.capabilities());
if (stackKeyResult.isSupported()) {
qCDebug(lcProtocol).noquote().nospace()
<< "MainWindow: memory recall preselecting band stack memory="
Expand Down Expand Up @@ -9761,6 +9770,59 @@ void MainWindow::mirrorDiversityChildFrequency(SliceModel* slice, double mhz)
}
}

MainWindow::BandStackPreselectResult MainWindow::preselectBandStackForTune(
SliceModel* slice, double mhz, const char* source)
{
if (!slice || slice->panId().isEmpty())
return BandStackPreselectResult::NotNeeded;
if (mhz <= 54.0 && slice->frequency() <= 54.0)
return BandStackPreselectResult::NotNeeded;

const QString targetBand = BandSettings::bandForFrequency(mhz);
const QString currentBand = BandSettings::bandForFrequency(slice->frequency());
if (targetBand == currentBand)
return BandStackPreselectResult::NotNeeded;

const auto xvtrs = xvtrPolicyBandsFrom(m_radioModel.xvtrList());
const auto stackKeyResult =
XvtrPolicy::resolveBandStackKey(targetBand, xvtrs, m_radioModel.capabilities());
if (!stackKeyResult.isSupported()) {
QString unsupportedReason = stackKeyResult.unsupportedReason;
if (mhz > 54.0 && xvtrs.isEmpty()) {
unsupportedReason =
QString("Band %1 requires a configured XVTR before Aether can tune it.")
.arg(targetBand);
}
qCWarning(lcProtocol).noquote().nospace()
<< "MainWindow: direct tune cannot preselect band stack source="
<< (source ? source : "(unknown)")
<< " pan=" << slice->panId()
<< " from_band=" << currentBand
<< " to_band=" << targetBand
<< " freq_mhz=" << QString::number(mhz, 'f', 6)
<< " reason=" << unsupportedReason
<< " available_xvtrs=" << xvtrListSummary(xvtrs);
statusBar()->showMessage(unsupportedReason, 5000);
return BandStackPreselectResult::Unsupported;
}

qCDebug(lcProtocol).noquote().nospace()
<< "MainWindow: direct tune preselecting band stack source="
<< (source ? source : "(unknown)")
<< " pan=" << slice->panId()
<< " from_band=" << currentBand
<< " to_band=" << targetBand
<< " key=" << stackKeyResult.key;
clearSwrSweepForBandChange(-1, slice->panId(), targetBand);
m_bandSettings.setCurrentBand(targetBand);
m_radioModel.sendCommand(
QString("display pan set %1 band=%2").arg(slice->panId(), stackKeyResult.key));
QTimer::singleShot(300, this, [this, panId = slice->panId()]() {
reassertUnmutedSliceAudioForPan(panId);
});
return BandStackPreselectResult::Selected;
}

void MainWindow::applyTuneRequest(SliceModel* slice, double mhz,
TuneIntent intent, const char* source)
{
Expand All @@ -9783,6 +9845,42 @@ void MainWindow::applyTuneRequest(SliceModel* slice, double mhz,
if (slice->sliceId() == m_activeSliceId && sw)
sw->setVfoFrequency(mhz);

const BandStackPreselectResult bandPreselect =
(intent == TuneIntent::CommandedTargetCenter)
? preselectBandStackForTune(slice, mhz, source)
: BandStackPreselectResult::NotNeeded;
if (bandPreselect == BandStackPreselectResult::Unsupported) {
if (slice->sliceId() == m_activeSliceId && sw)
sw->setVfoFrequency(oldFreqMhz);
return;
}

if (bandPreselect == BandStackPreselectResult::Selected) {
const int sliceId = slice->sliceId();
const QString sourceName = QString::fromUtf8(source ? source : "");
QTimer::singleShot(250, this, [this, sliceId, mhz, sourceName, oldFreqMhz]() {
auto* pendingSlice = m_radioModel.slice(sliceId);
if (!pendingSlice || pendingSlice->isLocked() || m_swrSweep.running)
return;
if (pendingSlice->sliceId() == m_activeSliceId) {
if (auto* pendingSw = spectrumForSlice(pendingSlice))
pendingSw->setVfoFrequency(mhz);
}
pendingSlice->tuneAndRecenter(mhz);
mirrorDiversityChildFrequency(pendingSlice, mhz);

const QByteArray sourceUtf8 = sourceName.toUtf8();
const char* delayedSource = sourceUtf8.constData();
const TuneCenteringResult result =
revealFrequencyIfNeeded(pendingSlice, mhz,
TuneIntent::CommandedTargetCenter,
delayedSource);
logTunePolicyDecision(delayedSource, TuneIntent::CommandedTargetCenter,
oldFreqMhz, mhz, result);
});
return;
}

slice->setFrequency(mhz);
mirrorDiversityChildFrequency(slice, mhz);

Expand Down Expand Up @@ -11155,7 +11253,8 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)

// ── Band selection ───────────────────────────────────────────────────
connect(menu, &SpectrumOverlayMenu::bandSelected,
this, [this, applet](const QString& bandName, double freqMhz, const QString& mode) {
this, [this, applet](const QString& bandName, double freqMhz, const QString& mode,
const QString& stackKeyHint) {
qDebug() << "MainWindow: switching to band" << bandName
<< "freq:" << freqMhz << "mode:" << mode;

Expand All @@ -11179,15 +11278,25 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)
// (0-based), not the radio's 1-based setup-order field (#2342).
// - WWV / GEN use numeric band-stack slots 33 / 34 from SmartSDR
// capture history (#1540/#1211).
// - Built-in 4m / 2m hardware bands use bare keys "4" / "2" only
// when the connected model reports those capabilities.
// - Configured XVTR buttons also pass explicit X<n> keys so a
// user XVTR named "4m" can still be selected on a radio that
// also has native 4m hardware.
//
// If no exact mapping exists, refuse the band change and leave the
// If no supported mapping exists, refuse the band change and leave the
// current slice/pan state untouched. Guessing is worse than failing
// visibly because a wrong tune destroys the very band-stack state this
// path exists to preserve.
const auto xvtrs = xvtrPolicyBandsFrom(m_radioModel.xvtrList());
const auto stackKeyResult = XvtrPolicy::resolveBandStackKey(bandName, xvtrs);
const QString stackKey = stackKeyResult.key;
QString unsupportedBandReason = stackKeyResult.unsupportedReason;
QString stackKey = stackKeyHint;
QString unsupportedBandReason;
if (stackKey.isEmpty()) {
const auto stackKeyResult =
XvtrPolicy::resolveBandStackKey(bandName, xvtrs, m_radioModel.capabilities());
stackKey = stackKeyResult.key;
unsupportedBandReason = stackKeyResult.unsupportedReason;
}

if (stackKey.isEmpty()) {
qCWarning(lcProtocol).noquote().nospace()
Expand Down
8 changes: 8 additions & 0 deletions src/gui/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ private slots:
RevealOffscreen,
};

enum class BandStackPreselectResult {
NotNeeded,
Selected,
Unsupported,
};

struct TuneCenteringResult {
double oldCenterMhz{0.0};
double newCenterMhz{0.0};
Expand Down Expand Up @@ -178,6 +184,8 @@ private slots:
SliceModel* activeSlice() const;
static const char* tuneIntentName(TuneIntent intent);
bool panFollowEnabled() const;
BandStackPreselectResult preselectBandStackForTune(SliceModel* slice, double mhz,
const char* source);
void applyTuneRequest(SliceModel* slice, double mhz,
TuneIntent intent, const char* source);
void applyPanRangeRequest(const QString& panId, double centerMhz,
Expand Down
15 changes: 7 additions & 8 deletions src/gui/RxApplet.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "RxApplet.h"
#include "FilterPassbandWidget.h"
#include "FrequencyEntryParser.h"
#include "GuardedSlider.h"
#include "ComboStyle.h"
#include "SliceColorManager.h"
Expand Down Expand Up @@ -465,16 +466,14 @@ void RxApplet::buildUI()
connect(m_freqEdit, &QLineEdit::returnPressed, this, [this] {
const QString text = m_freqEdit->text().trimmed();
if (!text.isEmpty() && m_slice) {
QString clean = text;
int firstDot = clean.indexOf('.');
if (firstDot >= 0) {
clean = clean.left(firstDot) + "." + clean.mid(firstDot + 1).remove('.');
}
QString clean = FrequencyEntryParser::normalizedMhzText(text);
bool ok = false;
double freqMhz = clean.toDouble(&ok);
const bool explicitMhzEntry = FrequencyEntryParser::isExplicitMhzEntry(text, clean);
const bool onXvtr = m_slice &&
(m_slice->rxAntenna().startsWith("XVT") || m_slice->frequency() > 54.0);
const double maxMhz = onXvtr ? 50000.0 : 54.0;
const bool highExplicitMhzEntry = ok && explicitMhzEntry && freqMhz > 54.0;
const double maxMhz = (onXvtr || highExplicitMhzEntry) ? 50000.0 : 54.0;
if (onXvtr) {
// 3-digit-band convenience (2m/70cm): 1446 → 144.6.
// Skip for 23cm/microwave — 1296 means 1296 MHz.
Expand All @@ -487,12 +486,12 @@ void RxApplet::buildUI()
freqMhz = clean.toDouble(&ok);
}
}
} else {
} else if (!highExplicitMhzEntry) {
if (ok && freqMhz > 54000.0) freqMhz /= 1e6;
else if (ok && freqMhz > 54.0) freqMhz /= 1e3;
}
if (ok && freqMhz >= 0.001 && freqMhz <= maxMhz)
m_slice->tuneAndRecenter(freqMhz);
emit directEntryCommitted(freqMhz, QStringLiteral("rx-direct-entry"));
}
m_freqStack->setCurrentIndex(0);
});
Expand Down
1 change: 1 addition & 0 deletions src/gui/RxApplet.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class RxApplet : public QWidget {
void autoSqlMarginDbChanged(int dB);
// Emitted when the radio reports a squelch state change (for spectrum line).
void squelchStateChanged(bool on, int level);
void directEntryCommitted(double mhz, const QString& source);

#ifdef HAVE_RADE
// Emitted when user selects/deselects RADE digital voice mode
Expand Down
10 changes: 6 additions & 4 deletions src/gui/SpectrumOverlayMenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1469,9 +1469,10 @@ void SpectrumOverlayMenu::setXvtrBands(const QVector<XvtrBand>& bands)
btn->setStyleSheet(xvtrBtnStyle);
const double freq = bands[i].rfFreqMhz;
const QString name = bands[i].name;
connect(btn, &QPushButton::clicked, this, [this, name, freq]() {
const QString stackKey = bands[i].stackKey;
connect(btn, &QPushButton::clicked, this, [this, name, freq, stackKey]() {
hideAllSubPanels();
emit bandSelected(name, freq, "USB");
emit bandSelected(name, freq, "USB", stackKey);
});
grid->addWidget(btn, row + i / 3, i % 3);
m_xvtrBandBtns.append(btn);
Expand Down Expand Up @@ -1561,8 +1562,9 @@ void SpectrumOverlayMenu::setXvtrBands(const QVector<XvtrBand>& bands)

const double freq = xvtr.rfFreqMhz;
const QString name = xvtr.name;
connect(btn, &QPushButton::clicked, this, [this, name, freq]() {
emit bandSelected(name, freq, "FM");
const QString stackKey = xvtr.stackKey;
connect(btn, &QPushButton::clicked, this, [this, name, freq, stackKey]() {
emit bandSelected(name, freq, "FM", stackKey);
m_xvtrPanel->hide();
m_xvtrPanelVisible = false;
});
Expand Down
9 changes: 6 additions & 3 deletions src/gui/SpectrumOverlayMenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class SpectrumOverlayMenu : public QWidget {
void syncNoiseFloorPosition(int pos);

// Populate XVTR band sub-panel
struct XvtrBand { QString name; double rfFreqMhz; };
struct XvtrBand { QString name; double rfFreqMhz; QString stackKey; };
void setXvtrBands(const QVector<XvtrBand>& bands);

// Surface the connected radio's built-in transverter bands (4m on
Expand Down Expand Up @@ -118,8 +118,11 @@ class SpectrumOverlayMenu : public QWidget {
void wfColorSchemeChanged(int scheme);
void noiseFloorPositionChanged(int pos);
void noiseFloorEnableChanged(bool on);
// Emitted when user selects a band from the sub-panel.
void bandSelected(const QString& bandName, double freqMhz, const QString& mode);
// Emitted when user selects a band from the sub-panel. stackKeyHint is
// populated only when the clicked control already knows the exact Flex
// band-stack key, such as a configured XVTR button.
void bandSelected(const QString& bandName, double freqMhz, const QString& mode,
const QString& stackKeyHint = {});
// Emitted when user clicks XVTR button to open Radio Setup XVTR tab.
void xvtrSetupRequested();
// Emitted when WNB toggle changes.
Expand Down
Loading
Loading