diff --git a/CMakeLists.txt b/CMakeLists.txt index 3978e28d..d236b01e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -668,6 +668,7 @@ set(GUI_SOURCES src/gui/DvkPanel.cpp src/gui/AmpApplet.cpp src/gui/MeterApplet.cpp + src/gui/RadeApplet.cpp src/gui/HealthApplet.cpp src/gui/PersistentDialog.cpp src/gui/ProfileManagerDialog.cpp diff --git a/src/gui/AppletPanel.cpp b/src/gui/AppletPanel.cpp index 88e88fef..9849cfc8 100644 --- a/src/gui/AppletPanel.cpp +++ b/src/gui/AppletPanel.cpp @@ -30,6 +30,9 @@ #include "ShackSwitchApplet.h" #include "MeterApplet.h" #include "HealthApplet.h" +#ifdef HAVE_RADE +#include "RadeApplet.h" +#endif #ifdef HAVE_MQTT #include "MqttApplet.h" #endif @@ -852,6 +855,11 @@ AppletPanel::AppletPanel(QWidget* parent) : QWidget(parent) m_meterApplet = new MeterApplet; m_appletOrder.append(makeEntry("MTR", "Meters", m_meterApplet, false, m_drawer, m_drawerLayout)); +#ifdef HAVE_RADE + m_radeApplet = new RadeApplet; + m_appletOrder.append(makeEntry("RADE", "RADE Status", m_radeApplet, false, m_drawer, m_drawerLayout)); +#endif + m_healthApplet = new HealthApplet; m_appletOrder.append(makeEntry("HLTH", "Antenna Health", m_healthApplet, false, m_drawer, m_drawerLayout)); diff --git a/src/gui/AppletPanel.h b/src/gui/AppletPanel.h index ef1c54c0..b4fbf42d 100644 --- a/src/gui/AppletPanel.h +++ b/src/gui/AppletPanel.h @@ -53,6 +53,9 @@ class MeterApplet; class HealthApplet; class MqttApplet; class FavoritesPickerDialog; +#ifdef HAVE_RADE +class RadeApplet; +#endif // AppletPanel — right-side panel with a row of toggle buttons at the top, // an S-Meter gauge below them, and a scrollable stack of applets. @@ -119,6 +122,9 @@ class AppletPanel : public QWidget { ShackSwitchApplet* ssApplet() { return m_ssApplet; } MeterApplet* meterApplet() { return m_meterApplet; } HealthApplet* healthApplet() { return m_healthApplet; } +#ifdef HAVE_RADE + RadeApplet* radeApplet() { return m_radeApplet; } +#endif #ifdef HAVE_MQTT MqttApplet* mqttApplet() { return m_mqttApplet; } #endif @@ -278,6 +284,9 @@ class AppletPanel : public QWidget { ShackSwitchApplet* m_ssApplet{nullptr}; MeterApplet* m_meterApplet{nullptr}; HealthApplet* m_healthApplet{nullptr}; +#ifdef HAVE_RADE + RadeApplet* m_radeApplet{nullptr}; +#endif #ifdef HAVE_MQTT MqttApplet* m_mqttApplet{nullptr}; #endif diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 236481f5..8b2264f7 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -199,6 +199,7 @@ #include "core/SpotModeResolver.h" #ifdef HAVE_RADE #include "core/RADEEngine.h" +#include "RadeApplet.h" #endif #if defined(Q_OS_MAC) #include "core/VirtualAudioBridge.h" @@ -17372,6 +17373,19 @@ void MainWindow::activateRADE(int sliceId) } } + if (auto* applet = m_appletPanel->radeApplet()) { + applet->setRadeActive(true); + applet->setRadeSynced(false); + connect(m_radeEngine, &RADEEngine::syncChanged, + applet, &RadeApplet::setRadeSynced); + connect(m_radeEngine, &RADEEngine::snrChanged, + applet, &RadeApplet::setRadeSnr); + connect(m_radeEngine, &RADEEngine::freqOffsetChanged, + applet, &RadeApplet::setRadeFreqOffset); + connect(m_radeEngine, &RADEEngine::eooCallsignReceived, + applet, &RadeApplet::setRadeCallsign, Qt::QueuedConnection); + } + // Store far-end callsign received in EOO frame for display / future use. connect(m_radeEngine, &RADEEngine::eooCallsignReceived, this, [this](const QString& callsign) { @@ -17424,6 +17438,17 @@ void MainWindow::deactivateRADE() m_radioModel.setDigitalVoiceTxSlice(-1); m_audio->clearTxAccumulators(); // flush stale RADE modem data m_appletPanel->phoneCwApplet()->setRadeActive(false); + + if (auto* applet = m_appletPanel->radeApplet()) { + applet->setRadeActive(false); + if (m_radeEngine) { + disconnect(m_radeEngine, &RADEEngine::syncChanged, applet, nullptr); + disconnect(m_radeEngine, &RADEEngine::snrChanged, applet, nullptr); + disconnect(m_radeEngine, &RADEEngine::freqOffsetChanged, applet, nullptr); + disconnect(m_radeEngine, &RADEEngine::eooCallsignReceived, applet, nullptr); + } + } + // For hardware mics, reset to full gain — the radio controls hardware levels. // PC mic keeps its PcMicGain so SSB sessions are unaffected. if (m_radioModel.transmitModel().micSelection() != "PC") { @@ -17512,6 +17537,13 @@ void MainWindow::activateFdvDisplay(int sliceId) } } +#ifdef HAVE_RADE + if (auto* applet = m_appletPanel->radeApplet()) { + applet->setRadeActive(true, QStringLiteral("FreeDV")); + applet->setRadeSynced(false); + } +#endif + m_fdvSnrMeterIndex = m_radioModel.meterModel() .findMeter("EXT_WVF", "FreeDV_SNR"); @@ -17538,6 +17570,11 @@ void MainWindow::deactivateFdvDisplay() } } +#ifdef HAVE_RADE + if (auto* applet = m_appletPanel->radeApplet()) + applet->setRadeActive(false); +#endif + disconnect(&m_radioModel.meterModel(), &MeterModel::meterUpdated, this, &MainWindow::onFdvMeterUpdated); disconnect(&m_radioModel, &RadioModel::metersChanged, @@ -17565,9 +17602,18 @@ void MainWindow::onFdvMeterUpdated(int index, float value) if (synced != m_fdvSynced) { m_fdvSynced = synced; vfo->setRadeSynced(synced); +#ifdef HAVE_RADE + if (auto* applet = m_appletPanel->radeApplet()) + applet->setRadeSynced(synced); +#endif } - if (synced) + if (synced) { vfo->setRadeSnr(value); +#ifdef HAVE_RADE + if (auto* applet = m_appletPanel->radeApplet()) + applet->setRadeSnr(value); +#endif + } } void MainWindow::onFdvMetersChanged() diff --git a/src/gui/RadeApplet.cpp b/src/gui/RadeApplet.cpp new file mode 100644 index 00000000..7382e2f4 --- /dev/null +++ b/src/gui/RadeApplet.cpp @@ -0,0 +1,164 @@ +#ifdef HAVE_RADE + +#include "RadeApplet.h" +#include "core/ThemeManager.h" + +#include +#include +#include + +namespace { +// Base style for muted-grey informational labels (SNR value, offset, "SNR" caption). +// Signal-specific colors (#e0e040 / #00ff88) are substituted into this template +// by setRadeSnr; all other uses take the grey directly. +constexpr auto kMutedStyle = + "QLabel { color: #8090a0; font-size: 10px;" + " background: transparent; border: none; padding: 0; margin: 0; }"; +} // namespace + +namespace AetherSDR { + +RadeApplet::RadeApplet(QWidget* parent) + : QWidget(parent) +{ + theme::setContainer(this, QStringLiteral("applet/rade")); + + auto* vbox = new QVBoxLayout(this); + vbox->setContentsMargins(4, 4, 4, 4); + vbox->setSpacing(2); + + m_inactiveLabel = new QLabel(tr("RADE inactive")); + m_inactiveLabel->setStyleSheet( + "QLabel { color: #506070; font-size: 10px; " + "background: transparent; border: none; }"); + m_inactiveLabel->setAlignment(Qt::AlignCenter); + vbox->addWidget(m_inactiveLabel); + + m_dataRows = new QWidget; + m_dataRows->setAttribute(Qt::WA_TranslucentBackground); + auto* dataVbox = new QVBoxLayout(m_dataRows); + dataVbox->setContentsMargins(0, 0, 0, 0); + dataVbox->setSpacing(3); + + m_statusLabel = new QLabel; + m_statusLabel->setTextFormat(Qt::RichText); + ThemeManager::instance().applyStyleSheet(m_statusLabel, + "QLabel { color: {{color.accent}}; font-size: 10px; font-weight: bold;" + " background: transparent; border: none; padding: 0; margin: 0; }"); + dataVbox->addWidget(m_statusLabel); + + { + auto* row = new QHBoxLayout; + row->setContentsMargins(0, 0, 0, 0); + row->setSpacing(4); + + auto* snrLbl = new QLabel(tr("SNR")); + snrLbl->setStyleSheet(kMutedStyle); + + m_snrLabel = new QLabel(QStringLiteral("---")); + m_snrLabel->setStyleSheet(kMutedStyle); + + row->addWidget(snrLbl); + row->addWidget(m_snrLabel); + row->addStretch(); + dataVbox->addLayout(row); + } + + m_callsignLabel = new QLabel; + ThemeManager::instance().applyStyleSheet(m_callsignLabel, + "QLabel { color: {{color.accent}}; font-size: 10px; font-weight: bold;" + " background: transparent; border: none; padding: 0; margin: 0; }"); + m_callsignLabel->hide(); + dataVbox->addWidget(m_callsignLabel); + + m_offsetLabel = new QLabel; + m_offsetLabel->setStyleSheet(kMutedStyle); + m_offsetLabel->hide(); + dataVbox->addWidget(m_offsetLabel); + + dataVbox->addStretch(); + m_dataRows->hide(); + vbox->addWidget(m_dataRows); + vbox->addStretch(); +} + +void RadeApplet::setRadeActive(bool on, const QString& label) +{ + m_active = on; + m_modeLabel = label.isEmpty() ? QStringLiteral("RADE") : label; + + m_inactiveLabel->setVisible(!on); + m_dataRows->setVisible(on); + + if (!on) { + m_statusLabel->setText({}); + m_snrLabel->setText(QStringLiteral("---")); + m_snrLabel->setStyleSheet(kMutedStyle); + m_callsignLabel->hide(); + m_callsignLabel->clear(); + m_offsetLabel->hide(); + } else { + // Show initial unsynced state — syncChanged fires once audio starts flowing + const QString led = QStringLiteral(""); + m_statusLabel->setText(m_modeLabel + QLatin1Char(' ') + led); + m_callsignLabel->clear(); + m_callsignLabel->hide(); + m_offsetLabel->hide(); + } +} + +void RadeApplet::setRadeSynced(bool synced) +{ + if (!m_active) return; + + const QString led = synced + ? QStringLiteral("") + : QStringLiteral(""); + m_statusLabel->setText(m_modeLabel + QLatin1Char(' ') + led); + + if (!synced) { + m_snrLabel->setStyleSheet(kMutedStyle); + m_snrLabel->setText(QStringLiteral("---")); + m_offsetLabel->hide(); + } else { + // New sync — clear callsign from previous transmission + m_callsignLabel->clear(); + m_callsignLabel->hide(); + } +} + +void RadeApplet::setRadeSnr(float snrDb) +{ + if (!m_active) return; + const QString color = (snrDb < 5.0f) ? QStringLiteral("#e0e040") + : QStringLiteral("#00ff88"); + m_snrLabel->setStyleSheet( + QString("QLabel { color: %1; font-size: 10px;" + " background: transparent; border: none; padding: 0; margin: 0; }") + .arg(color)); + m_snrLabel->setText(QString("%1dB").arg(qRound(snrDb))); +} + +void RadeApplet::setRadeFreqOffset(float hz) +{ + if (!m_active) return; + const QString sign = (hz >= 0) ? QStringLiteral("+") : QString{}; + m_offsetLabel->setText( + QString("%1%2Hz").arg(sign).arg(static_cast(hz))); + m_offsetLabel->show(); +} + +void RadeApplet::setRadeCallsign(const QString& callsign) +{ + if (callsign.isEmpty()) { + m_callsignLabel->clear(); + m_callsignLabel->hide(); + } else { + m_callsignLabel->setText(callsign); + m_callsignLabel->show(); + } +} + +} // namespace AetherSDR + +#endif // HAVE_RADE diff --git a/src/gui/RadeApplet.h b/src/gui/RadeApplet.h new file mode 100644 index 00000000..3b24ce15 --- /dev/null +++ b/src/gui/RadeApplet.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef HAVE_RADE + +#include + +class QLabel; + +namespace AetherSDR { + +// Mirrors VfoWidget RADE status in the sidebar. Signal-driven from RADEEngine — no separate timer. +class RadeApplet : public QWidget { + Q_OBJECT +public: + explicit RadeApplet(QWidget* parent = nullptr); + +public slots: + void setRadeActive(bool on, const QString& label = {}); + + void setRadeSynced(bool synced); + void setRadeSnr(float snrDb); + void setRadeFreqOffset(float hz); + void setRadeCallsign(const QString& callsign); + +private: + QLabel* m_statusLabel{nullptr}; + QLabel* m_snrLabel{nullptr}; + QLabel* m_callsignLabel{nullptr}; + QLabel* m_offsetLabel{nullptr}; + QWidget* m_dataRows{nullptr}; + QLabel* m_inactiveLabel{nullptr}; + + QString m_modeLabel; + bool m_active{false}; +}; + +} // namespace AetherSDR + +#endif // HAVE_RADE