From fc23cd2cc8482be83cf83a5564d920fcaef7d5d4 Mon Sep 17 00:00:00 2001 From: f4exb Date: Wed, 25 Mar 2026 01:17:55 +0100 Subject: [PATCH] Meshtastic demod: implemented USER preset allowing arbitrary modulation characteristics and frequencies --- .../demodmeshtastic/meshtasticdemod.cpp | 22 ++- .../demodmeshtastic/meshtasticdemodgui.cpp | 134 ++++++++++++++++-- .../demodmeshtastic/meshtasticdemodgui.h | 2 +- .../demodmeshtastic/meshtasticdemodgui.ui | 5 + 4 files changed, 148 insertions(+), 15 deletions(-) diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp index 2a62816d1..fa52ee823 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp @@ -326,6 +326,16 @@ std::vector MeshtasticDemod::buildPipelineConfi void MeshtasticDemod::makePipelineConfigFromSettings(int configId, PipelineConfig& config, const MeshtasticDemodSettings& settings) const { + // USER preset: all LoRa parameters are user-controlled; skip derivation from the mesh radio table. + if (settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0) + { + config.id = configId; + config.name = QString("CONF%1").arg(configId); + config.presetName = settings.m_meshtasticPresetName; + config.settings = settings; + return; + } + const QString region = settings.m_meshtasticRegionCode.trimmed().isEmpty() ? QString("US") : settings.m_meshtasticRegionCode.trimmed(); @@ -923,8 +933,10 @@ void MeshtasticDemod::applySettings(MeshtasticDemodSettings settings, bool force // Copy LoRa params derived from the preset (bandwidth, spread factor, etc.) back into // settings so that m_settings and the GUI stay in sync with what was actually applied. + // Skip for USER preset: those parameters are controlled entirely by the user via the GUI. if (m_running && !m_pipelineConfigs.empty() && - settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa && + settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) != 0) { const MeshtasticDemodSettings& derived = m_pipelineConfigs[0].settings; const bool bwChanged = (settings.m_bandwidthIndex != derived.m_bandwidthIndex); @@ -964,8 +976,12 @@ void MeshtasticDemod::applySettings(MeshtasticDemodSettings settings, bool force m_settings = settings; // Forward preset-derived settings back to GUI so controls (e.g. BW slider) reflect - // the values actually applied. - if (getMessageQueueToGUI()) + // the values actually applied. Skip for USER preset: no parameters were derived, so + // there is nothing to sync back — and echoing would trigger an infinite apply loop + // (GUI apply → demod echo → GUI displaySettings → rebuildMeshtasticChannelOptions + // → queued apply → …). + const bool isUserPreset = m_settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0; + if (!isUserPreset && getMessageQueueToGUI()) { MsgConfigureMeshtasticDemod *msgToGUI = MsgConfigureMeshtasticDemod::create(m_settings, false); getMessageQueueToGUI()->push(msgToGUI); diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp index effdb4888..9ab8ff4f8 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp @@ -59,6 +59,8 @@ #include "util/db.h" #include "maincore.h" +#include + #include "meshtasticdemod.h" #include "meshtasticdemodmsg.h" #include "meshtasticdemodgui.h" @@ -385,6 +387,12 @@ void MeshtasticDemodGUI::on_meshPreset_currentIndexChanged(int index) return; } + ui->meshRegion->setEnabled(index != ui->meshPreset->count() - 1); // USER preset has no region and is the last item + ui->BW->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined bandwidth and is the last item + ui->Spread->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined spread and is the last item + ui->deBits->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined deBits and is the last item + ui->preambleChirps->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined preambleChirps and is the last item + rebuildMeshtasticChannelOptions(); applyMeshtasticProfileFromSelection(); } @@ -984,7 +992,7 @@ bool MeshtasticDemodGUI::retuneDeviceToFrequency(qint64 centerFrequencyHz) return false; } -bool MeshtasticDemodGUI::autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary) +bool MeshtasticDemodGUI::autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary, int* newBasebandSampleRateOut) { summary.clear(); @@ -1133,6 +1141,10 @@ bool MeshtasticDemodGUI::autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, Q } } + if (newBasebandSampleRateOut) { + *newBasebandSampleRateOut = finalEffectiveRate; + } + const bool belowTarget = finalEffectiveRate < minEffectiveRate; const bool changed = (finalDevSampleRate != initialDevSampleRate) || (finalLog2Decim != initialLog2Decim) @@ -1174,6 +1186,53 @@ void MeshtasticDemodGUI::applyMeshtasticProfileFromSelection() return; } + // USER preset: all LoRa parameters and frequency are controlled manually from the GUI. + // Skip auto-configuration entirely; just persist the selection and optionally auto-tune sample rate. + if (preset == "USER") + { + bool selectionStateChanged = false; + + if (m_settings.m_meshtasticRegionCode != region) + { + m_settings.m_meshtasticRegionCode = region; + selectionStateChanged = true; + } + + if (m_settings.m_meshtasticPresetName != preset) + { + m_settings.m_meshtasticPresetName = preset; + selectionStateChanged = true; + } + + const int thisBW = MeshtasticDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; + QString sampleRateSummary; + bool sampleRateChanged = false; + int newBasebandSampleRate = 0; + + if (m_settings.m_meshtasticAutoSampleRate) { + sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary, &newBasebandSampleRate); + } + + if (sampleRateChanged && newBasebandSampleRate > m_basebandSampleRate) { + m_basebandSampleRate = newBasebandSampleRate; + setBandwidths(); + } + + if (selectionStateChanged || sampleRateChanged) { + applySettings(); + } + + const QString statusMsg = tr("MESH CFG|USER preset: BW=%1 Hz SF=%2 DE=%3 preamble=%4%5") + .arg(thisBW) + .arg(m_settings.m_spreadFactor) + .arg(m_settings.m_deBits) + .arg(m_settings.m_preambleChirps) + .arg(sampleRateSummary.isEmpty() ? QString() : " " + sampleRateSummary); + updateControlAvailabilityHints(); + displayStatus(statusMsg); + return; + } + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum); modemmeshtastic::TxRadioSettings meshRadio; QString error; @@ -1265,12 +1324,21 @@ void MeshtasticDemodGUI::applyMeshtasticProfileFromSelection() QString sampleRateSummary; bool sampleRateChanged = false; + int newBasebandSampleRate = 0; if (m_settings.m_meshtasticAutoSampleRate) { - sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary); + sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary, &newBasebandSampleRate); } else { sampleRateSummary = "auto sample-rate control: disabled"; } + // If the device sample rate was just raised, update m_basebandSampleRate immediately + // so that setBandwidths() can widen the BW slider maximum before we write to it. + // Without this, the slider silently clamps the desired BW to the old (too-small) max. + if (sampleRateChanged && newBasebandSampleRate > m_basebandSampleRate) { + m_basebandSampleRate = newBasebandSampleRate; + setBandwidths(); + } + if (!changed && !sampleRateChanged && !selectionStateChanged) { return; } @@ -1357,6 +1425,25 @@ void MeshtasticDemodGUI::rebuildMeshtasticChannelOptions() m_meshControlsUpdating = true; ui->meshChannel->clear(); + // USER preset: channel selection is not applicable — the user sets all parameters manually + if (preset == "USER") + { + ui->meshChannel->addItem(tr("(user-defined)"), 0); + ui->meshChannel->setEnabled(false); + ui->meshChannel->setToolTip(tr("Not applicable in USER preset. All LoRa parameters and frequency are set manually.")); + m_meshControlsUpdating = false; + // Do NOT queue applyMeshtasticProfileFromSelection here: this function is + // called from displaySettings() on every demod echo-back, and queueing an + // apply would create an infinite loop (apply → demod → echo → displaySettings + // → rebuildMeshtasticChannelOptions → apply → …). + // Initial and explicit applies happen via the constructor's queued call and + // direct user actions (Apply button, region/preset combo changes). + return; + } + + ui->meshChannel->setEnabled(true); + ui->meshChannel->setToolTip(tr("Meshtastic channel number (zero-based, shown with center frequency)")); + int added = 0; for (int meshChannel = 0; meshChannel <= 200; ++meshChannel) { @@ -1401,14 +1488,6 @@ void MeshtasticDemodGUI::rebuildMeshtasticChannelOptions() << "region=" << region << "preset=" << preset << "channels=" << added; - - // Ensure the rebuilt combo state is actually applied, even when the rebuild - // was triggered from code paths where index-change handlers are suppressed. - QMetaObject::invokeMethod(this, [this]() { - if (!m_meshControlsUpdating) { - applyMeshtasticProfileFromSelection(); - } - }, Qt::QueuedConnection); } void MeshtasticDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) @@ -1621,7 +1700,9 @@ MeshtasticDemodGUI::MeshtasticDemodGUI(PluginAPI* pluginAPI, DeviceUISet *device resetLoRaStatus(); applySettings(true); // On first creation, combo signals haven't fired yet. Apply selected Meshtastic profile once. - applyMeshtasticProfileFromSelection(); + // Use a queued connection so this runs after SDRangel calls deserialize() on the newly created + // object — ensuring the saved preset/settings are in effect before any device retuning occurs. + QMetaObject::invokeMethod(this, &MeshtasticDemodGUI::applyMeshtasticProfileFromSelection, Qt::QueuedConnection); DialPopup::addPopupsToChildDials(this); m_resizer.enableChildMouseTracking(); } @@ -1664,6 +1745,31 @@ void MeshtasticDemodGUI::updateControlAvailabilityHints() ui->messageLengthLabel->setToolTip(messageLengthTip); ui->messageLengthText->setToolTip(messageLengthTip); + const bool isUserPreset = m_settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0; + ui->meshRegion->setEnabled(!isUserPreset); + ui->BW->setEnabled(isUserPreset); + ui->Spread->setEnabled(isUserPreset); + ui->deBits->setEnabled(isUserPreset); + ui->preambleChirps->setEnabled(isUserPreset); + + // Apply an opacity effect to give a clear greyed-out appearance when disabled, + // because the platform or dark-theme style may not provide enough visual contrast. + auto setSliderDimmed = [](QSlider* slider, bool dimmed) { + if (dimmed) { + if (!qobject_cast(slider->graphicsEffect())) { + auto* effect = new QGraphicsOpacityEffect(slider); + effect->setOpacity(0.35); + slider->setGraphicsEffect(effect); + } + } else { + slider->setGraphicsEffect(nullptr); + } + }; + setSliderDimmed(ui->BW, !isUserPreset); + setSliderDimmed(ui->Spread, !isUserPreset); + setSliderDimmed(ui->deBits, !isUserPreset); + setSliderDimmed(ui->preambleChirps, !isUserPreset); + const bool headerControlsEnabled = !m_settings.m_hasHeader; ui->fecParity->setEnabled(headerControlsEnabled); ui->packetLength->setEnabled(headerControlsEnabled); @@ -1760,6 +1866,12 @@ void MeshtasticDemodGUI::displaySettings() } ui->meshRegion->setCurrentIndex(regionIndex); + ui->meshRegion->setEnabled(m_settings.m_meshtasticPresetName != "USER"); + ui->BW->setEnabled(m_settings.m_meshtasticPresetName == "USER"); + ui->Spread->setEnabled(m_settings.m_meshtasticPresetName == "USER"); + ui->deBits->setEnabled(m_settings.m_meshtasticPresetName == "USER"); + ui->preambleChirps->setEnabled(m_settings.m_meshtasticPresetName == "USER"); + int presetIndex = ui->meshPreset->findData(m_settings.m_meshtasticPresetName); if (presetIndex < 0) { presetIndex = ui->meshPreset->findData("LONG_FAST"); diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h index 06c70a3ef..aad0d3cde 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h @@ -208,7 +208,7 @@ private: void setupMeshtasticAutoProfileControls(); void rebuildMeshtasticChannelOptions(); bool retuneDeviceToFrequency(qint64 centerFrequencyHz); - bool autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary); + bool autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary, int* newBasebandSampleRateOut = nullptr); int findBandwidthIndex(int bandwidthHz) const; void applyMeshtasticProfileFromSelection(); void editMeshtasticKeys(); diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui index 95cb95777..986200a99 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui @@ -643,6 +643,11 @@ SHORT_TURBO + + + USER + +