1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-03-31 20:25:38 -04:00

Meshtastic demod: implemented USER preset allowing arbitrary modulation characteristics and frequencies

This commit is contained in:
f4exb 2026-03-25 01:17:55 +01:00
parent 3c0277bfd8
commit fc23cd2cc8
4 changed files with 148 additions and 15 deletions

View File

@ -326,6 +326,16 @@ std::vector<MeshtasticDemod::PipelineConfig> 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);

View File

@ -59,6 +59,8 @@
#include "util/db.h"
#include "maincore.h"
#include <QGraphicsOpacityEffect>
#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<QGraphicsOpacityEffect*>(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");

View File

@ -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();

View File

@ -643,6 +643,11 @@
<string>SHORT_TURBO</string>
</property>
</item>
<item>
<property name="text">
<string>USER</string>
</property>
</item>
</widget>
</item>
<item>