1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-07-25 19:44:12 -04:00
Files
sdrangel/plugins/channelrx/demodmeshcore/meshcoredemodgui.cpp
T
Robin Getz 409e1f1a00 Remove redundant ui null check in MeshcoreDemodGUI & MeshtasticDemodGUI
The ui object is created during MeshcoreDemodGUI and MeshtasticDemodGUI
construction and remains valid for the lifetime of the widget. Remove
the unnecessary null check to avoid misleading static analysis tools
into assuming ui can be null.

This resolves a Coverity false positive where the later call to
updateDechirpModeUI() was reported as a possible null dereference
in both places.

Signed-off-by: Robin Getz <rgetz503@gmail.com>
2026-07-20 23:33:44 -04:00

3373 lines
121 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2015 John Greb <hexameron@spam.no> //
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "device/deviceuiset.h"
#include "device/deviceapi.h"
#include <QScrollBar>
#include <QDebug>
#include <QCoreApplication>
#include <QEventLoop>
#include <QDateTime>
#include <QComboBox>
#include <QCheckBox>
#include <QPushButton>
#include <QHeaderView>
#include <QAbstractItemView>
#include <QList>
#include <QMap>
#include <QPlainTextEdit>
#include <QSplitter>
#include <QTabWidget>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <algorithm>
#include <cmath>
#include <functional>
#include <limits>
#include "ui_meshcoredemodgui.h"
#include "dsp/spectrumvis.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "dsp/devicesamplesource.h"
#include "dsp/devicesamplemimo.h"
#include "gui/glspectrum.h"
#include "gui/glspectrumgui.h"
#include "gui/basicchannelsettingsdialog.h"
#include "gui/dialpopup.h"
#include "gui/dialogpositioner.h"
#include "plugin/pluginapi.h"
#include "channel/channelwebapiutils.h"
#include "util/db.h"
#include "maincore.h"
#include <QGraphicsOpacityEffect>
#include "meshcoredemod.h"
#include "meshcoredemodmsg.h"
#include "meshcoredemodgui.h"
#include "meshcorekeysdialog.h"
#include "meshcorepacket.h"
namespace
{
static const int kMeshAutoLockCandidateTimeoutMs = 12000;
static const int kMeshAutoLockArmTimeoutMs = 12000;
static const int kMeshAutoLockMinObservationsPerCandidate = 3;
static const int kMeshAutoLockMinSourceObservationsPerCandidate = 6;
static const int kMeshAutoLockMinDecodeSamplesForApply = 3;
static const double kMeshAutoLockMinDecodeAverageForApply = 0.5;
static const double kMeshAutoLockActivityP2NThresholdDb = 4.0;
static const int kMeshAutoLockOffsetMultipliers[] = {
0, -1, 1, -2, 2, -3, 3, -4, 4, -6, 6, -8, 8, -10, 10, -12, 12, -16, 16, -24, 24, -32, 32, -48, 48, -64, 64
};
static const int kTreeRawKeyRole = Qt::UserRole;
static const int kTreeDisplayLabelRole = Qt::UserRole + 1;
static const int kTreeRawValueRole = Qt::UserRole + 2;
static const int kTreeMessageKeyRole = Qt::UserRole + 3;
void alignTextViewToLatestLineLeft(QPlainTextEdit *textView)
{
if (!textView) {
return;
}
QScrollBar *verticalScroll = textView->verticalScrollBar();
if (verticalScroll) {
verticalScroll->setValue(verticalScroll->maximum());
}
QScrollBar *horizontalScroll = textView->horizontalScrollBar();
if (horizontalScroll) {
horizontalScroll->setValue(horizontalScroll->minimum());
}
}
void alignTreeViewToLatestEntryLeft(QTreeWidget *treeWidget, QTreeWidgetItem *item)
{
if (!treeWidget) {
return;
}
if (item) {
treeWidget->scrollToItem(item);
}
QScrollBar *verticalScroll = treeWidget->verticalScrollBar();
if (verticalScroll) {
verticalScroll->setValue(verticalScroll->maximum());
}
QScrollBar *horizontalScroll = treeWidget->horizontalScrollBar();
if (horizontalScroll) {
horizontalScroll->setValue(horizontalScroll->minimum());
}
}
}
MeshcoreDemodGUI* MeshcoreDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel)
{
MeshcoreDemodGUI* gui = new MeshcoreDemodGUI(pluginAPI, deviceUISet, rxChannel);
return gui;
}
void MeshcoreDemodGUI::destroy()
{
delete this;
}
void MeshcoreDemodGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings(true);
}
QByteArray MeshcoreDemodGUI::serialize() const
{
return m_settings.serialize();
}
bool MeshcoreDemodGUI::deserialize(const QByteArray& data)
{
resetLoRaStatus();
if (m_settings.deserialize(data))
{
displaySettings();
applySettings(true);
return true;
}
else
{
resetToDefaults();
return false;
}
}
bool MeshcoreDemodGUI::handleMessage(const Message& message)
{
if (DSPSignalNotification::match(message))
{
DSPSignalNotification& notif = (DSPSignalNotification&) message;
m_deviceCenterFrequency = notif.getCenterFrequency();
int basebandSampleRate = notif.getSampleRate();
qDebug() << "MeshcoreDemodGUI::handleMessage: DSPSignalNotification: m_basebandSampleRate: " << basebandSampleRate;
if (basebandSampleRate != m_basebandSampleRate)
{
m_basebandSampleRate = basebandSampleRate;
setBandwidths();
}
ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2);
ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2));
updateAbsoluteCenterFrequency();
if (m_remoteTcpReconnectAutoApplyPending)
{
m_remoteTcpReconnectAutoApplyPending = false;
m_remoteTcpReconnectAutoApplyWaitTicks = 0;
qInfo() << "MeshcoreDemodGUI::handleMessage: DSPSignalNotification after RemoteTCP reconnect - reapplying Meshcore profile";
QMetaObject::invokeMethod(this, &MeshcoreDemodGUI::applyMeshcoreProfileFromSelection, Qt::QueuedConnection);
}
return true;
}
else if (MeshcoreDemodMsg::MsgReportDecodeBytes::match(message))
{
// Populates the upper unstructured view including raw bytes in hex
const MeshcoreDemodMsg::MsgReportDecodeBytes& msg = (MeshcoreDemodMsg::MsgReportDecodeBytes&) message;
handleMeshAutoLockObservation(msg);
showLoRaMessage(message);
return true;
}
else if (MeshcoreDemodMsg::MsgReportDecodeString::match(message))
{
// Populates the lower structured tree view with decoded fields
showTextMessage(message);
return true;
}
else if (MeshcoreDemod::MsgConfigureMeshcoreDemod::match(message))
{
qDebug("MeshcoreDemodGUI::handleMessage: NFMDemod::MsgConfigureMeshcoreDemod");
const MeshcoreDemod::MsgConfigureMeshcoreDemod& cfg = (MeshcoreDemod::MsgConfigureMeshcoreDemod&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
ui->spectrumGUI->updateSettings();
m_channelMarker.updateSettings(static_cast<const ChannelMarker*>(m_settings.m_channelMarker));
displaySettings();
blockApplySettings(false);
return true;
}
else
{
return false;
}
}
void MeshcoreDemodGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()) != 0)
{
if (handleMessage(*message)) {
delete message;
}
}
}
void MeshcoreDemodGUI::channelMarkerChangedByCursor()
{
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void MeshcoreDemodGUI::on_deltaFrequency_changed(qint64 value)
{
focusedSettings().m_inputFrequencyOffset = value;
if (m_focusedPipelineIndex == 0) {
m_channelMarker.setCenterFrequency(value);
updateAbsoluteCenterFrequency();
}
applyFocusedPipelineSettings();
}
void MeshcoreDemodGUI::channelMarkerHighlightedByCursor()
{
setHighlighted(m_channelMarker.getHighlighted());
}
void MeshcoreDemodGUI::on_BW_valueChanged(int value)
{
auto& s = focusedSettings();
if (value < 0) {
s.m_bandwidthIndex = 0;
} else if (value < MeshcoreDemodSettings::nbBandwidths) {
s.m_bandwidthIndex = value;
} else {
s.m_bandwidthIndex = MeshcoreDemodSettings::nbBandwidths - 1;
}
int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex];
ui->BWText->setText(QString("%1 Hz").arg(thisBW));
if (m_focusedPipelineIndex == 0) {
m_channelMarker.setBandwidth(thisBW);
ui->glSpectrum->setSampleRate(thisBW);
ui->glSpectrum->setCenterFrequency(thisBW/2);
}
applyFocusedPipelineSettings();
}
void MeshcoreDemodGUI::on_Spread_valueChanged(int value)
{
focusedSettings().m_spreadFactor = value;
ui->SpreadText->setText(tr("%1").arg(value));
if (m_focusedPipelineIndex == 0) {
ui->spectrumGUI->setFFTSize(m_settings.m_spreadFactor);
}
applyFocusedPipelineSettings();
}
void MeshcoreDemodGUI::on_deBits_valueChanged(int value)
{
focusedSettings().m_deBits = value;
ui->deBitsText->setText(tr("%1").arg(value));
applyFocusedPipelineSettings();
}
void MeshcoreDemodGUI::on_preambleChirps_valueChanged(int value)
{
focusedSettings().m_preambleChirps = value;
ui->preambleChirpsText->setText(tr("%1").arg(value));
applyFocusedPipelineSettings();
}
void MeshcoreDemodGUI::on_mute_toggled(bool checked)
{
m_settings.m_decodeActive = !checked;
applySettings();
}
void MeshcoreDemodGUI::on_clear_clicked(bool checked)
{
(void) checked;
clearPipelineViews();
setDechirpInspectionMode(false);
}
void MeshcoreDemodGUI::on_eomSquelch_valueChanged(int value)
{
m_settings.m_eomSquelchTenths = value;
displaySquelch();
applySettings();
}
void MeshcoreDemodGUI::on_messageLength_valueChanged(int value)
{
m_settings.m_nbSymbolsMax = value;
ui->messageLengthText->setText(tr("%1").arg(m_settings.m_nbSymbolsMax));
applySettings();
}
void MeshcoreDemodGUI::on_udpSend_stateChanged(int state)
{
m_settings.m_sendViaUDP = (state == Qt::Checked);
applySettings();
}
void MeshcoreDemodGUI::on_udpSendJson_stateChanged(int state)
{
m_settings.m_sendJsonViaUDP = (state == Qt::Checked);
applySettings();
}
void MeshcoreDemodGUI::on_udpAddress_editingFinished()
{
m_settings.m_udpAddress = ui->udpAddress->text();
applySettings();
}
void MeshcoreDemodGUI::on_udpPort_editingFinished()
{
bool ok;
quint16 udpPort = ui->udpPort->text().toInt(&ok);
if((!ok) || (udpPort < 1024)) {
udpPort = 9998;
}
m_settings.m_udpPort = udpPort;
ui->udpPort->setText(tr("%1").arg(m_settings.m_udpPort));
applySettings();
}
void MeshcoreDemodGUI::on_invertRamps_stateChanged(int state)
{
focusedSettings().m_invertRamps = (state == Qt::Checked);
applyFocusedPipelineSettings();
}
void MeshcoreDemodGUI::on_meshRegion_currentIndexChanged(int index)
{
(void) index;
if (m_meshControlsUpdating) {
return;
}
rebuildMeshcoreChannelOptions();
applyMeshcoreProfileFromSelection();
}
void MeshcoreDemodGUI::on_meshPreset_currentIndexChanged(int index)
{
(void) index;
if (m_meshControlsUpdating) {
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
ui->deltaFrequency->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined frequency offset and is the last item
rebuildMeshcoreChannelOptions();
applyMeshcoreProfileFromSelection();
}
void MeshcoreDemodGUI::on_meshChannel_currentIndexChanged(int index)
{
(void) index;
if (m_meshControlsUpdating) {
return;
}
applyMeshcoreProfileFromSelection();
}
void MeshcoreDemodGUI::on_meshApply_clicked(bool checked)
{
(void) checked;
if (m_meshControlsUpdating) {
return;
}
// Rebuild first so region/preset changes refresh the valid channel list before applying.
rebuildMeshcoreChannelOptions();
applyMeshcoreProfileFromSelection();
}
void MeshcoreDemodGUI::on_meshKeys_clicked(bool checked)
{
(void) checked;
editMeshcoreKeys();
}
void MeshcoreDemodGUI::on_meshAutoSampleRate_toggled(bool checked)
{
if (m_meshControlsUpdating) {
return;
}
m_settings.m_meshcoreAutoSampleRate = checked;
applySettings();
if (checked) {
applyMeshcoreProfileFromSelection();
} else {
displayStatus(tr("MESH CFG|auto input tuning disabled"));
}
}
void MeshcoreDemodGUI::on_meshAutoLock_clicked(bool checked)
{
if (checked) {
startMeshAutoLock();
} else {
stopMeshAutoLock(true);
}
}
void MeshcoreDemodGUI::startMeshAutoLock()
{
if (m_meshAutoLockActive) {
return;
}
const int bandwidthHz = MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex];
const int sf = std::max(1, m_settings.m_spreadFactor);
const int symbolBins = 1 << std::min(15, sf);
const int stepHz = std::max(100, bandwidthHz / symbolBins);
const bool invertOrder[] = {m_settings.m_invertRamps, !m_settings.m_invertRamps};
m_meshAutoLockCandidates.clear();
m_meshAutoLockBaseOffsetHz = m_settings.m_inputFrequencyOffset;
m_meshAutoLockBaseInvert = m_settings.m_invertRamps;
m_meshAutoLockBaseDeBits = m_settings.m_deBits;
QVector<int> deCandidates;
deCandidates.push_back(m_settings.m_deBits);
// SDR decode compatibility scan:
// for high SF profiles, also probe DE=0 and DE=2 even if the profile selects one of them.
if (sf >= 11)
{
if (std::find(deCandidates.begin(), deCandidates.end(), 0) == deCandidates.end()) {
deCandidates.push_back(0);
}
if (std::find(deCandidates.begin(), deCandidates.end(), 2) == deCandidates.end()) {
deCandidates.push_back(2);
}
}
for (bool invert : invertOrder)
{
for (int multiplier : kMeshAutoLockOffsetMultipliers)
{
for (int deBits : deCandidates)
{
MeshAutoLockCandidate candidate;
candidate.inputOffsetHz = m_meshAutoLockBaseOffsetHz + multiplier * stepHz;
candidate.invertRamps = invert;
candidate.deBits = deBits;
candidate.score = 0.0;
candidate.samples = 0;
candidate.sourceScore = 0.0;
candidate.sourceSamples = 0;
candidate.syncWordZeroCount = 0;
candidate.headerParityOkOrFixCount = 0;
candidate.headerCRCCount = 0;
candidate.payloadCRCCount = 0;
candidate.earlyEOMCount = 0;
m_meshAutoLockCandidates.push_back(candidate);
}
}
}
if (m_meshAutoLockCandidates.isEmpty())
{
displayStatus(tr("MESH LOCK|no candidates generated"));
ui->meshAutoLock->blockSignals(true);
ui->meshAutoLock->setChecked(false);
ui->meshAutoLock->blockSignals(false);
return;
}
m_meshAutoLockActive = true;
m_meshAutoLockCandidateIndex = 0;
m_meshAutoLockObservedSamplesForCandidate = 0;
m_meshAutoLockObservedSourceSamplesForCandidate = 0;
m_meshAutoLockTotalDecodeSamples = 0;
m_meshAutoLockTrafficSeen = false;
m_meshAutoLockActivityTicks = 0;
m_meshAutoLockArmStartMs = QDateTime::currentMSecsSinceEpoch();
m_meshAutoLockCandidateStartMs = QDateTime::currentMSecsSinceEpoch();
ui->meshAutoLock->setText(tr("Locking..."));
applyMeshAutoLockCandidate(m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex], true);
QString deSummary;
for (int i = 0; i < deCandidates.size(); ++i)
{
if (!deSummary.isEmpty()) {
deSummary += "/";
}
deSummary += QString::number(deCandidates[i]);
}
displayStatus(tr("MESH LOCK|armed %1 candidates step=%2Hz de=%3. Waiting for on-air activity before scanning.")
.arg(m_meshAutoLockCandidates.size())
.arg(stepHz)
.arg(deSummary));
}
void MeshcoreDemodGUI::stopMeshAutoLock(bool keepBestCandidate)
{
if (!m_meshAutoLockActive && !ui->meshAutoLock->isChecked())
{
return;
}
int bestIndex = -1;
int bestFallbackIndex = -1;
double bestWeightedScore = -std::numeric_limits<double>::infinity();
double bestFallbackWeightedScore = -std::numeric_limits<double>::infinity();
if (keepBestCandidate)
{
for (int i = 0; i < m_meshAutoLockCandidates.size(); ++i)
{
const MeshAutoLockCandidate& candidate = m_meshAutoLockCandidates[i];
const bool hasStrongDecodeEvidence = (candidate.payloadCRCCount > 0) || (candidate.headerCRCCount > 0);
const bool hasDecodeSamples = (candidate.samples >= kMeshAutoLockMinDecodeSamplesForApply) || hasStrongDecodeEvidence;
const bool hasSourceSamples = candidate.sourceSamples > 0;
if (!hasDecodeSamples && !hasSourceSamples) {
continue;
}
const double averageDecodeScore = candidate.samples > 0 ? (candidate.score / candidate.samples) : -12.0;
const double averageSourceScore = hasSourceSamples ? (candidate.sourceScore / candidate.sourceSamples) : -2.0;
const bool hasCRCBackedDecode = candidate.payloadCRCCount > 0;
const bool hasHeaderBackedDecode = candidate.headerCRCCount > 0;
const bool decodeEvidence = hasCRCBackedDecode || hasHeaderBackedDecode;
// Hard floor: no auto-apply on weak/noisy candidates that never show valid header/payload structure.
if (!decodeEvidence) {
continue;
}
const double confidenceBoost = std::min(candidate.samples, 8) * 0.25
+ std::min(candidate.sourceSamples, 20) * 0.03;
const double weightedScore = (averageDecodeScore * 1.1)
+ (averageSourceScore * 0.2)
+ (candidate.headerCRCCount * 4.0)
+ (candidate.payloadCRCCount * 8.0)
+ confidenceBoost;
const bool strongDecodeEvidence = hasCRCBackedDecode
|| (hasHeaderBackedDecode
&& (averageDecodeScore >= kMeshAutoLockMinDecodeAverageForApply));
if (strongDecodeEvidence && (weightedScore > bestWeightedScore))
{
bestWeightedScore = weightedScore;
bestIndex = i;
}
else if (hasHeaderBackedDecode && (averageDecodeScore >= -2.0) && (weightedScore > bestFallbackWeightedScore))
{
bestFallbackWeightedScore = weightedScore;
bestFallbackIndex = i;
}
}
}
m_meshAutoLockActive = false;
m_meshAutoLockCandidateIndex = 0;
m_meshAutoLockCandidateStartMs = 0;
m_meshAutoLockObservedSamplesForCandidate = 0;
m_meshAutoLockObservedSourceSamplesForCandidate = 0;
m_meshAutoLockTotalDecodeSamples = 0;
m_meshAutoLockTrafficSeen = false;
m_meshAutoLockActivityTicks = 0;
m_meshAutoLockArmStartMs = 0;
ui->meshAutoLock->blockSignals(true);
ui->meshAutoLock->setChecked(false);
ui->meshAutoLock->setText(tr("Auto Lock"));
ui->meshAutoLock->blockSignals(false);
if (keepBestCandidate && (bestIndex >= 0 || bestFallbackIndex >= 0))
{
const bool provisional = bestIndex < 0;
const MeshAutoLockCandidate& best = m_meshAutoLockCandidates[provisional ? bestFallbackIndex : bestIndex];
applyMeshAutoLockCandidate(best, true);
const double avgScore = best.samples > 0 ? (best.score / best.samples) : 0.0;
const double avgSourceScore = best.sourceSamples > 0 ? (best.sourceScore / best.sourceSamples) : 0.0;
const double syncRatio = best.samples > 0 ? (100.0 * best.syncWordZeroCount / best.samples) : 0.0;
displayStatus(tr("MESH LOCK|applied %1candidate df=%2Hz inv=%3 de=%4 decode=%5/%6 source=%7/%8 sync00=%9% hc=%10 crc=%11")
.arg(provisional ? "provisional " : "best ")
.arg(best.inputOffsetHz)
.arg(best.invertRamps ? "on" : "off")
.arg(best.deBits)
.arg(avgScore, 0, 'f', 2)
.arg(best.samples)
.arg(avgSourceScore, 0, 'f', 2)
.arg(best.sourceSamples)
.arg(syncRatio, 0, 'f', 1)
.arg(best.headerCRCCount)
.arg(best.payloadCRCCount));
}
else
{
MeshAutoLockCandidate baseCandidate;
baseCandidate.inputOffsetHz = m_meshAutoLockBaseOffsetHz;
baseCandidate.invertRamps = m_meshAutoLockBaseInvert;
baseCandidate.deBits = m_meshAutoLockBaseDeBits;
baseCandidate.score = 0.0;
baseCandidate.samples = 0;
baseCandidate.sourceScore = 0.0;
baseCandidate.sourceSamples = 0;
baseCandidate.syncWordZeroCount = 0;
baseCandidate.headerParityOkOrFixCount = 0;
baseCandidate.headerCRCCount = 0;
baseCandidate.payloadCRCCount = 0;
baseCandidate.earlyEOMCount = 0;
applyMeshAutoLockCandidate(baseCandidate, true);
displayStatus(tr("MESH LOCK|stopped (no decode-backed lock). Baseline restored."));
}
}
void MeshcoreDemodGUI::applyMeshAutoLockCandidate(const MeshAutoLockCandidate& candidate, bool applySettingsNow)
{
m_settings.m_inputFrequencyOffset = candidate.inputOffsetHz;
m_settings.m_invertRamps = candidate.invertRamps;
m_settings.m_deBits = candidate.deBits;
m_channelMarker.blockSignals(true);
m_channelMarker.setCenterFrequency(candidate.inputOffsetHz);
m_channelMarker.blockSignals(false);
ui->deltaFrequency->blockSignals(true);
ui->deltaFrequency->setValue(candidate.inputOffsetHz);
ui->deltaFrequency->blockSignals(false);
ui->invertRamps->blockSignals(true);
ui->invertRamps->setChecked(candidate.invertRamps);
ui->invertRamps->blockSignals(false);
ui->deBits->blockSignals(true);
ui->deBits->setValue(candidate.deBits);
ui->deBits->blockSignals(false);
ui->deBitsText->setText(tr("%1").arg(candidate.deBits));
updateAbsoluteCenterFrequency();
if (applySettingsNow) {
applySettings();
}
}
void MeshcoreDemodGUI::handleMeshAutoLockObservation(const MeshcoreDemodMsg::MsgReportDecodeBytes& msg)
{
if (!m_meshAutoLockActive) {
return;
}
if ((m_meshAutoLockCandidateIndex < 0) || (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size())) {
return;
}
MeshAutoLockCandidate& candidate = m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex];
const double snrDb = msg.getSingalDb() - msg.getNoiseDb();
const double clippedSnr = std::max(-10.0, std::min(30.0, static_cast<double>(snrDb)));
double score = clippedSnr * 0.2;
const bool earlyEOM = msg.getEarlyEOM();
if (earlyEOM) {
score -= 8.0;
} else {
score += 3.0;
}
const int headerParityStatus = msg.getHeaderParityStatus();
if (headerParityStatus == (int) MeshcoreDemodSettings::ParityOK) {
score += 8.0;
} else if (headerParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) {
score += 5.0;
} else if (headerParityStatus == (int) MeshcoreDemodSettings::ParityError) {
score -= 7.0;
}
const bool headerCRCStatus = msg.getHeaderCRCStatus();
if (headerCRCStatus) {
score += 10.0;
} else {
score -= 8.0;
}
const int payloadParityStatus = msg.getPayloadParityStatus();
const bool payloadCRCStatus = msg.getPayloadCRCStatus();
if (!earlyEOM)
{
if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityOK) {
score += 6.0;
} else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) {
score += 3.0;
} else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityError) {
score -= 4.0;
}
if (payloadCRCStatus) {
score += 12.0;
} else {
score -= 4.0;
}
}
const bool syncWordZero = msg.getSyncWord() == 0x00;
candidate.score += score;
candidate.samples++;
candidate.syncWordZeroCount += syncWordZero ? 1 : 0;
candidate.headerParityOkOrFixCount += (headerParityStatus == (int) MeshcoreDemodSettings::ParityOK
|| headerParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) ? 1 : 0;
candidate.headerCRCCount += headerCRCStatus ? 1 : 0;
candidate.payloadCRCCount += payloadCRCStatus ? 1 : 0;
candidate.earlyEOMCount += earlyEOM ? 1 : 0;
m_meshAutoLockObservedSamplesForCandidate++;
m_meshAutoLockTotalDecodeSamples++;
if (!earlyEOM && headerCRCStatus && payloadCRCStatus)
{
displayStatus(tr("MESH LOCK|strong lock found (HF/HC/CRC good), finishing scan"));
stopMeshAutoLock(true);
}
}
void MeshcoreDemodGUI::handleMeshAutoLockSourceObservation()
{
if (!m_meshAutoLockActive) {
return;
}
if ((m_meshAutoLockCandidateIndex < 0) || (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size())) {
return;
}
MeshAutoLockCandidate& candidate = m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex];
const double totalPower = std::max(1e-12, m_meshcoreDemod->getTotalPower());
const double noisePower = std::max(1e-12, m_meshcoreDemod->getCurrentNoiseLevel());
const double totalDb = CalcDb::dbPower(totalPower);
const double noiseDb = CalcDb::dbPower(noisePower);
const double p2nDb = std::max(-20.0, std::min(40.0, totalDb - noiseDb));
const bool demodActive = m_meshcoreDemod->getDemodActive();
if (!m_meshAutoLockTrafficSeen)
{
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
const bool sourceActive = p2nDb >= kMeshAutoLockActivityP2NThresholdDb;
if (sourceActive) {
m_meshAutoLockActivityTicks++;
}
if (demodActive) {
m_meshAutoLockActivityTicks++;
}
if (!sourceActive && !demodActive && (m_meshAutoLockActivityTicks > 0)) {
m_meshAutoLockActivityTicks--;
}
if (m_meshAutoLockActivityTicks >= 3)
{
m_meshAutoLockTrafficSeen = true;
m_meshAutoLockCandidateStartMs = nowMs;
m_meshAutoLockObservedSamplesForCandidate = 0;
m_meshAutoLockObservedSourceSamplesForCandidate = 0;
displayStatus(tr("MESH LOCK|traffic detected, starting scan"));
}
else if ((m_meshAutoLockArmStartMs > 0) && ((nowMs - m_meshAutoLockArmStartMs) >= kMeshAutoLockArmTimeoutMs))
{
m_meshAutoLockTrafficSeen = true;
m_meshAutoLockCandidateStartMs = nowMs;
m_meshAutoLockObservedSamplesForCandidate = 0;
m_meshAutoLockObservedSourceSamplesForCandidate = 0;
displayStatus(tr("MESH LOCK|no clear activity detected. Starting scan anyway."));
}
else
{
return;
}
}
// Source-only quality proxy:
// - prefer sustained demod activity
// - prefer clearer power/noise separation
double sourceScore = (demodActive ? 0.7 : -0.1) + (p2nDb * 0.03);
if (p2nDb < 1.0) {
sourceScore -= 0.3;
}
candidate.sourceScore += sourceScore;
candidate.sourceSamples++;
m_meshAutoLockObservedSourceSamplesForCandidate++;
}
void MeshcoreDemodGUI::advanceMeshAutoLock()
{
if (!m_meshAutoLockActive) {
return;
}
if (!m_meshAutoLockTrafficSeen) {
return;
}
if ((m_meshAutoLockCandidateIndex < 0) || (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size()))
{
stopMeshAutoLock(true);
return;
}
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
const bool enoughObservations = m_meshAutoLockObservedSamplesForCandidate >= kMeshAutoLockMinObservationsPerCandidate;
const bool timedOut = (nowMs - m_meshAutoLockCandidateStartMs) >= kMeshAutoLockCandidateTimeoutMs;
if (m_meshAutoLockTotalDecodeSamples == 0)
{
int sourceCount = 0;
double minSourceAvg = std::numeric_limits<double>::infinity();
double maxSourceAvg = -std::numeric_limits<double>::infinity();
for (const MeshAutoLockCandidate& candidate : m_meshAutoLockCandidates)
{
if (candidate.sourceSamples < kMeshAutoLockMinSourceObservationsPerCandidate) {
continue;
}
const double sourceAvg = candidate.sourceScore / candidate.sourceSamples;
minSourceAvg = std::min(minSourceAvg, sourceAvg);
maxSourceAvg = std::max(maxSourceAvg, sourceAvg);
sourceCount++;
}
// Source-only scoring is not discriminative: avoid sweeping every candidate and restore baseline.
if ((sourceCount >= 6) && ((maxSourceAvg - minSourceAvg) < 0.15))
{
displayStatus(tr("MESH LOCK|source-only signal is flat/inconclusive. Stopping early."));
stopMeshAutoLock(false);
return;
}
}
// Move to next candidate only if we have decode evidence or dwell timeout elapsed.
if (!enoughObservations && !timedOut) {
return;
}
m_meshAutoLockCandidateIndex++;
m_meshAutoLockObservedSamplesForCandidate = 0;
m_meshAutoLockObservedSourceSamplesForCandidate = 0;
m_meshAutoLockCandidateStartMs = nowMs;
if (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size())
{
stopMeshAutoLock(true);
return;
}
const MeshAutoLockCandidate& candidate = m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex];
applyMeshAutoLockCandidate(candidate, true);
displayStatus(tr("MESH LOCK|candidate %1/%2 df=%3Hz inv=%4 de=%5")
.arg(m_meshAutoLockCandidateIndex + 1)
.arg(m_meshAutoLockCandidates.size())
.arg(candidate.inputOffsetHz)
.arg(candidate.invertRamps ? "on" : "off")
.arg(candidate.deBits));
}
void MeshcoreDemodGUI::editMeshcoreKeys()
{
MeshcoreKeysDialog dialog(this);
dialog.setKeySpecList(m_settings.m_meshcoreKeySpecList);
if (dialog.exec() != QDialog::Accepted) {
return;
}
m_settings.m_meshcoreKeySpecList = dialog.getKeySpecList();
const bool hasCustomKeys = !m_settings.m_meshcoreKeySpecList.isEmpty();
ui->meshKeys->setText(hasCustomKeys ? tr("Keys*") : tr("Keys..."));
ui->meshKeys->setToolTip(hasCustomKeys ?
tr("Custom Meshcore decode keys configured. Click to edit.") :
tr("Open Meshcore key manager."));
applySettings();
if (m_settings.m_meshcoreKeySpecList.isEmpty()) {
displayStatus(tr("MESH KEYS|using environment/default key set"));
} else {
displayStatus(tr("MESH KEYS|custom key set saved"));
}
}
// ────────────────────────────────────────────────────────────────────────────
// Multi-pipeline helpers
// ────────────────────────────────────────────────────────────────────────────
MeshcoreDemodSettings& MeshcoreDemodGUI::focusedSettings()
{
if (m_focusedPipelineIndex > 0 && m_focusedPipelineIndex <= m_extraPipelineSettings.size()) {
return m_extraPipelineSettings[m_focusedPipelineIndex - 1];
}
return m_settings;
}
const MeshcoreDemodSettings& MeshcoreDemodGUI::focusedSettings() const
{
if (m_focusedPipelineIndex > 0 && m_focusedPipelineIndex <= m_extraPipelineSettings.size()) {
return m_extraPipelineSettings[m_focusedPipelineIndex - 1];
}
return m_settings;
}
int MeshcoreDemodGUI::pipelineCount() const
{
return 1 + m_extraPipelineSettings.size();
}
void MeshcoreDemodGUI::updateConfControls()
{
const int count = pipelineCount();
ui->conf->blockSignals(true);
ui->conf->setMaximum(std::max(0, count - 1));
ui->conf->setValue(m_focusedPipelineIndex);
ui->conf->blockSignals(false);
ui->confId->setText(QString::number(m_focusedPipelineIndex));
ui->confAdd->setEnabled(count < kMaxPipelines);
ui->confDel->setEnabled(m_focusedPipelineIndex > 0); // primary (0) cannot be deleted
}
void MeshcoreDemodGUI::loadFocusedSettingsToControls()
{
const auto& s = focusedSettings();
const int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex];
blockApplySettings(true);
m_meshControlsUpdating = true;
ui->deltaFrequency->setValue(s.m_inputFrequencyOffset);
ui->BW->setValue(s.m_bandwidthIndex);
ui->BWText->setText(QString("%1 Hz").arg(thisBW));
ui->Spread->setValue(s.m_spreadFactor);
ui->SpreadText->setText(tr("%1").arg(s.m_spreadFactor));
ui->deBits->setValue(s.m_deBits);
ui->deBitsText->setText(tr("%1").arg(s.m_deBits));
ui->preambleChirps->setValue(s.m_preambleChirps);
ui->preambleChirpsText->setText(tr("%1").arg(s.m_preambleChirps));
ui->invertRamps->setChecked(s.m_invertRamps);
int regionIndex = ui->meshRegion->findText(s.m_meshcoreRegionCode);
if (regionIndex < 0) { regionIndex = ui->meshRegion->findText("EU_868"); }
if (regionIndex < 0) { regionIndex = 0; }
ui->meshRegion->setCurrentIndex(regionIndex);
int presetIndex = ui->meshPreset->findText(s.m_meshcorePresetName);
if (presetIndex < 0) { presetIndex = ui->meshPreset->findText("EU_NARROW"); }
if (presetIndex < 0) { presetIndex = 0; }
ui->meshPreset->setCurrentIndex(presetIndex);
m_meshControlsUpdating = false;
blockApplySettings(false);
rebuildMeshcoreChannelOptions();
m_meshControlsUpdating = true;
int channelIndex = ui->meshChannel->findData(s.m_meshcoreChannelIndex);
if (channelIndex < 0) { channelIndex = 0; }
ui->meshChannel->setCurrentIndex(channelIndex);
m_meshControlsUpdating = false;
updateControlAvailabilityHints();
}
void MeshcoreDemodGUI::applyFocusedPipelineSettings(bool force)
{
if (m_focusedPipelineIndex == 0) {
applySettings(force);
} else {
pushExtraPipelineSettingsToDemod();
}
}
void MeshcoreDemodGUI::pushExtraPipelineSettingsToDemod()
{
if (!m_doApplySettings) {
return;
}
MeshcoreDemod::MsgSetExtraPipelineSettings* msg =
MeshcoreDemod::MsgSetExtraPipelineSettings::create(m_extraPipelineSettings);
m_meshcoreDemod->getInputMessageQueue()->push(msg);
}
// ────────────────────────────────────────────────────────────────────────────
// Conf-dial / add / delete slot handlers
// ────────────────────────────────────────────────────────────────────────────
void MeshcoreDemodGUI::on_conf_valueChanged(int value)
{
const auto maxFocus = static_cast<int>(m_extraPipelineSettings.size());
const int newFocus = std::clamp(value, 0, maxFocus);
if (newFocus == m_focusedPipelineIndex) {
return;
}
m_focusedPipelineIndex = newFocus;
ui->confId->setText(QString::number(m_focusedPipelineIndex));
ui->confDel->setEnabled(m_focusedPipelineIndex > 0);
loadFocusedSettingsToControls();
}
void MeshcoreDemodGUI::on_confAdd_clicked(bool checked)
{
(void) checked;
if (pipelineCount() >= kMaxPipelines) {
return;
}
// Seed the new pipeline with a copy of the currently focused settings.
m_extraPipelineSettings.append(focusedSettings());
m_focusedPipelineIndex = m_extraPipelineSettings.size(); // focus the new pipeline
updateConfControls();
loadFocusedSettingsToControls();
pushExtraPipelineSettingsToDemod();
displayStatus(tr("CONF|pipeline %1 added (copy of CONF%2)")
.arg(m_focusedPipelineIndex)
.arg(m_focusedPipelineIndex - 1));
}
void MeshcoreDemodGUI::on_confDel_clicked(bool checked)
{
(void) checked;
if (m_focusedPipelineIndex == 0 || m_extraPipelineSettings.isEmpty()) {
return;
}
const int removed = m_focusedPipelineIndex;
m_extraPipelineSettings.removeAt(m_focusedPipelineIndex - 1);
m_focusedPipelineIndex = std::max(0, m_focusedPipelineIndex - 1);
updateConfControls();
loadFocusedSettingsToControls();
pushExtraPipelineSettingsToDemod();
displayStatus(tr("CONF|pipeline %1 removed").arg(removed));
}
int MeshcoreDemodGUI::findBandwidthIndex(int bandwidthHz) const
{
int bestIndex = -1;
int bestDelta = 1 << 30;
for (int i = 0; i < MeshcoreDemodSettings::nbBandwidths; ++i)
{
const int delta = std::abs(MeshcoreDemodSettings::bandwidths[i] - bandwidthHz);
if (delta < bestDelta)
{
bestDelta = delta;
bestIndex = i;
}
}
return bestIndex;
}
bool MeshcoreDemodGUI::retuneDeviceToFrequency(qint64 centerFrequencyHz)
{
if (!m_deviceUISet || !m_deviceUISet->m_deviceAPI) {
return false;
}
DeviceAPI* deviceAPI = m_deviceUISet->m_deviceAPI;
if (deviceAPI->getDeviceSourceEngine() && deviceAPI->getSampleSource())
{
deviceAPI->getSampleSource()->setCenterFrequency(centerFrequencyHz);
return true;
}
if (deviceAPI->getDeviceMIMOEngine() && deviceAPI->getSampleMIMO())
{
deviceAPI->getSampleMIMO()->setSourceCenterFrequency(centerFrequencyHz, m_settings.m_streamIndex);
return true;
}
return false;
}
bool MeshcoreDemodGUI::autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary, int* newBasebandSampleRateOut)
{
summary.clear();
if (!m_meshcoreDemod) {
return false;
}
const int deviceSetIndex = m_meshcoreDemod->getDeviceSetIndex();
if (deviceSetIndex < 0) {
return false;
}
int devSampleRate = 0;
int log2Decim = 0;
QString sourceProtocol;
if (!ChannelWebAPIUtils::getDevSampleRate(deviceSetIndex, devSampleRate)
|| !ChannelWebAPIUtils::getSoftDecim(deviceSetIndex, log2Decim))
{
summary = "auto sample-rate control: unsupported by source";
return false;
}
if (devSampleRate <= 0) {
summary = "auto sample-rate control: invalid device sample-rate";
return false;
}
if (log2Decim < 0) {
log2Decim = 0;
}
const bool hasSourceProtocol = ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "protocol", sourceProtocol);
const bool isSpyServerProtocol = hasSourceProtocol && (sourceProtocol.compare("Spy Server", Qt::CaseInsensitive) == 0);
const int initialDevSampleRate = devSampleRate;
const int initialLog2Decim = log2Decim;
const int minEffectiveRate = std::max(500000, bandwidthHz * 4); // Keep margin above 2*BW for robust LoRa decode.
int newLog2Decim = log2Decim;
const int maxLog2Decim = 16; // Practical upper bound for software decimation controls.
// If current decimation undershoots the required effective rate, lower it first.
while ((newLog2Decim > 0) && ((devSampleRate >> newLog2Decim) < minEffectiveRate)) {
newLog2Decim--;
}
// Then push decimation as high as possible while keeping enough effective sample-rate.
while ((newLog2Decim < maxLog2Decim) && ((devSampleRate >> (newLog2Decim + 1)) >= minEffectiveRate)) {
newLog2Decim++;
}
if (newLog2Decim != log2Decim)
{
if (!ChannelWebAPIUtils::setSoftDecim(deviceSetIndex, newLog2Decim)) {
newLog2Decim = log2Decim;
}
}
if ((devSampleRate >> newLog2Decim) < minEffectiveRate && !isSpyServerProtocol)
{
const qint64 requiredDevRate = static_cast<qint64>(minEffectiveRate) << newLog2Decim;
if ((requiredDevRate > 0) && (requiredDevRate <= std::numeric_limits<int>::max()))
{
ChannelWebAPIUtils::setDevSampleRate(deviceSetIndex, static_cast<int>(requiredDevRate));
}
}
int finalDevSampleRate = devSampleRate;
int finalLog2Decim = newLog2Decim;
ChannelWebAPIUtils::getDevSampleRate(deviceSetIndex, finalDevSampleRate);
ChannelWebAPIUtils::getSoftDecim(deviceSetIndex, finalLog2Decim);
if (finalLog2Decim < 0) {
finalLog2Decim = 0;
}
int finalEffectiveRate = finalDevSampleRate >> finalLog2Decim;
bool channelSampleRateSynced = false;
bool channelDecimationDisabled = false;
bool dcBlockSupported = false;
bool iqCorrectionSupported = false;
bool agcSupported = false;
bool dcBlockEnabled = false;
bool iqCorrectionEnabled = false;
bool agcEnabled = false;
bool dcBlockApplied = false;
bool iqCorrectionApplied = false;
bool agcApplied = false;
int channelSampleRate = 0;
// Some sources (for example RemoteTCPInput) need channel sample-rate to be patched
// explicitly when decimation/sample-rate changes over WebAPI.
if (ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "channelSampleRate", channelSampleRate))
{
int channelDecimation = 0;
if (ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "channelDecimation", channelDecimation) && (channelDecimation != 0)) {
channelDecimationDisabled = ChannelWebAPIUtils::patchDeviceSetting(deviceSetIndex, "channelDecimation", 0);
}
if (channelSampleRate != finalEffectiveRate)
{
channelSampleRateSynced = ChannelWebAPIUtils::patchDeviceSetting(deviceSetIndex, "channelSampleRate", finalEffectiveRate);
if (channelSampleRateSynced) {
channelSampleRate = finalEffectiveRate;
}
}
finalEffectiveRate = channelSampleRate;
}
// Input-quality autotune for sources exposing these keys (e.g. RemoteTCPInput).
int settingValue = 0;
if (ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "dcBlock", settingValue))
{
dcBlockSupported = true;
if (settingValue == 0) {
dcBlockApplied = ChannelWebAPIUtils::patchDeviceSetting(deviceSetIndex, "dcBlock", 1);
dcBlockEnabled = dcBlockApplied;
} else {
dcBlockEnabled = true;
}
}
if (ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "iqCorrection", settingValue))
{
iqCorrectionSupported = true;
if (settingValue == 0) {
iqCorrectionApplied = ChannelWebAPIUtils::patchDeviceSetting(deviceSetIndex, "iqCorrection", 1);
iqCorrectionEnabled = iqCorrectionApplied;
} else {
iqCorrectionEnabled = true;
}
}
if (!isSpyServerProtocol && ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "agc", settingValue))
{
agcSupported = true;
if (settingValue == 0) {
agcApplied = ChannelWebAPIUtils::patchDeviceSetting(deviceSetIndex, "agc", 1);
agcEnabled = agcApplied;
} else {
agcEnabled = true;
}
}
if (newBasebandSampleRateOut) {
*newBasebandSampleRateOut = finalEffectiveRate;
}
const bool belowTarget = finalEffectiveRate < minEffectiveRate;
const bool changed = (finalDevSampleRate != initialDevSampleRate)
|| (finalLog2Decim != initialLog2Decim)
|| channelSampleRateSynced
|| channelDecimationDisabled
|| dcBlockApplied
|| iqCorrectionApplied
|| agcApplied;
summary = QString("effective sample-rate=%1Hz device sample-rate=%2Hz decimation=2^%3 required minimum=%4Hz%5")
.arg(finalEffectiveRate)
.arg(finalDevSampleRate)
.arg(finalLog2Decim)
.arg(minEffectiveRate)
.arg(belowTarget ? " (below target)" : "");
if (isSpyServerProtocol) {
summary += " source=SpyServer(fixed dev sample-rate)";
}
if (channelDecimationDisabled || channelSampleRateSynced) {
summary += " channel sample-rate synced";
}
summary += QString(" dcBlock=%1 iqCorrection=%2 agc=%3")
.arg(dcBlockSupported ? (dcBlockEnabled ? "on" : "off") : "n/a")
.arg(iqCorrectionSupported ? (iqCorrectionEnabled ? "on" : "off") : "n/a")
.arg(agcSupported ? (agcEnabled ? "on" : "off") : (isSpyServerProtocol ? "n/a(SpyServer)" : "n/a"));
return changed;
}
void MeshcoreDemodGUI::applyMeshcoreProfileFromSelection()
{
const QString region = ui->meshRegion->currentText();
const QString preset = ui->meshPreset->currentText();
const int meshChannel = ui->meshChannel->currentData().toInt();
const int channelNum = meshChannel + 1; // planner expects 1-based channel_num
if (region.isEmpty() || preset.isEmpty()) {
return;
}
auto& s = focusedSettings();
const bool isPrimary = (m_focusedPipelineIndex == 0);
// 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 (s.m_meshcoreRegionCode != region)
{
s.m_meshcoreRegionCode = region;
selectionStateChanged = true;
}
if (s.m_meshcorePresetName != preset)
{
s.m_meshcorePresetName = preset;
selectionStateChanged = true;
}
const int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex];
QString sampleRateSummary;
bool sampleRateChanged = false;
int newBasebandSampleRate = 0;
// Sample rate auto-tune is a device-level operation — primary pipeline only.
if (isPrimary && m_settings.m_meshcoreAutoSampleRate) {
sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary, &newBasebandSampleRate);
}
if (isPrimary && sampleRateChanged && newBasebandSampleRate > m_basebandSampleRate) {
m_basebandSampleRate = newBasebandSampleRate;
setBandwidths();
}
if (selectionStateChanged || sampleRateChanged) {
applyFocusedPipelineSettings();
}
const QString statusMsg = tr("MESH CFG|%1USER preset: BW=%2 Hz SF=%3 DE=%4 preamble=%5%6")
.arg(isPrimary ? QString() : QString("CONF%1 ").arg(m_focusedPipelineIndex))
.arg(thisBW)
.arg(s.m_spreadFactor)
.arg(s.m_deBits)
.arg(s.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);
modemmeshcore::TxRadioSettings meshRadio;
QString error;
if (!modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error))
{
qWarning() << "MeshcoreDemodGUI::applyMeshcoreProfileFromSelection:" << error;
return;
}
bool changed = false;
bool selectionStateChanged = false;
if (s.m_meshcoreRegionCode != region)
{
s.m_meshcoreRegionCode = region;
selectionStateChanged = true;
}
if (s.m_meshcorePresetName != preset)
{
s.m_meshcorePresetName = preset;
selectionStateChanged = true;
}
if (s.m_meshcoreChannelIndex != meshChannel)
{
s.m_meshcoreChannelIndex = meshChannel;
selectionStateChanged = true;
}
const int bwIndex = findBandwidthIndex(meshRadio.bandwidthHz);
if (bwIndex >= 0 && bwIndex != s.m_bandwidthIndex)
{
s.m_bandwidthIndex = bwIndex;
changed = true;
}
if (meshRadio.spreadFactor > 0 && meshRadio.spreadFactor != s.m_spreadFactor)
{
s.m_spreadFactor = meshRadio.spreadFactor;
changed = true;
}
if (meshRadio.deBits != s.m_deBits)
{
s.m_deBits = meshRadio.deBits;
changed = true;
}
if (meshRadio.parityBits > 0 && meshRadio.parityBits != s.m_nbParityBits)
{
s.m_nbParityBits = meshRadio.parityBits;
changed = true;
}
const int meshPreambleChirps = meshRadio.preambleChirps;
if (s.m_preambleChirps != static_cast<unsigned int>(meshPreambleChirps))
{
s.m_preambleChirps = static_cast<unsigned int>(meshPreambleChirps);
changed = true;
}
if (meshRadio.hasCenterFrequency)
{
if (isPrimary)
{
// Primary pipeline: try to retune the actual SDR device.
if (retuneDeviceToFrequency(meshRadio.centerFrequencyHz))
{
m_deviceCenterFrequency = meshRadio.centerFrequencyHz;
if (s.m_inputFrequencyOffset != 0)
{
s.m_inputFrequencyOffset = 0;
changed = true;
}
}
else if (m_deviceCenterFrequency != 0)
{
const qint64 wantedOffset = meshRadio.centerFrequencyHz - m_deviceCenterFrequency;
if (wantedOffset != s.m_inputFrequencyOffset)
{
s.m_inputFrequencyOffset = static_cast<int>(wantedOffset);
changed = true;
}
}
else
{
qWarning() << "MeshcoreDemodGUI::applyMeshcoreProfileFromSelection: cannot retune device and device center frequency unknown";
}
}
else
{
// Secondary pipeline: device frequency is owned by the primary; compute the input
// frequency offset relative to the current device center frequency.
if (m_deviceCenterFrequency != 0)
{
const qint64 wantedOffset = meshRadio.centerFrequencyHz - m_deviceCenterFrequency;
if (wantedOffset != s.m_inputFrequencyOffset)
{
s.m_inputFrequencyOffset = static_cast<int>(wantedOffset);
changed = true;
}
}
}
}
const int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex];
QString sampleRateSummary;
bool sampleRateChanged = false;
int newBasebandSampleRate = 0;
// Sample rate auto-tune is a device-level operation — primary pipeline only.
if (isPrimary)
{
if (m_settings.m_meshcoreAutoSampleRate) {
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.
if (sampleRateChanged && newBasebandSampleRate > m_basebandSampleRate) {
m_basebandSampleRate = newBasebandSampleRate;
setBandwidths();
}
}
if (!changed && !sampleRateChanged && !selectionStateChanged) {
return;
}
qInfo() << "MeshcoreDemodGUI::applyMeshcoreProfileFromSelection:"
<< (isPrimary ? "primary" : QString("CONF%1").arg(m_focusedPipelineIndex))
<< meshRadio.summary
<< sampleRateSummary;
QString status = tr("MESH CFG|%1region=%2 preset=%3 ch=%4 %5")
.arg(isPrimary ? QString() : QString("CONF%1 ").arg(m_focusedPipelineIndex))
.arg(region)
.arg(preset)
.arg(meshChannel)
.arg(meshRadio.summary);
status += QString(" preamble=%1").arg(meshPreambleChirps);
if (!sampleRateSummary.isEmpty()) {
status += " " + sampleRateSummary;
}
if (!changed)
{
applyFocusedPipelineSettings();
displayStatus(status);
return;
}
// Update channel marker (always primary; it represents the channel on the device display).
if (isPrimary)
{
m_channelMarker.blockSignals(true);
m_channelMarker.setCenterFrequency(s.m_inputFrequencyOffset);
m_channelMarker.setBandwidth(thisBW);
m_channelMarker.blockSignals(false);
}
blockApplySettings(true);
ui->deltaFrequency->setValue(s.m_inputFrequencyOffset);
ui->BW->setValue(s.m_bandwidthIndex);
ui->BWText->setText(QString("%1 Hz").arg(thisBW));
ui->Spread->setValue(s.m_spreadFactor);
ui->SpreadText->setText(tr("%1").arg(s.m_spreadFactor));
ui->deBits->setValue(s.m_deBits);
ui->deBitsText->setText(tr("%1").arg(s.m_deBits));
ui->preambleChirps->setValue(s.m_preambleChirps);
ui->preambleChirpsText->setText(tr("%1").arg(s.m_preambleChirps));
ui->fecParity->setValue(s.m_nbParityBits);
ui->fecParityText->setText(tr("%1").arg(s.m_nbParityBits));
blockApplySettings(false);
updateControlAvailabilityHints();
if (isPrimary)
{
ui->glSpectrum->setSampleRate(thisBW);
ui->glSpectrum->setCenterFrequency(thisBW/2);
updateAbsoluteCenterFrequency();
}
applyFocusedPipelineSettings();
displayStatus(status);
}
void MeshcoreDemodGUI::setupMeshcoreAutoProfileControls()
{
// Hide Region and Channel controls — region is implicit in each
// preset's frequency, and group channels are managed via the keys
// dialog rather than a numbered Channel selector. Preset combo +
// Apply button stay visible (see
// modemmeshcore::command::applyMeshcorePreset).
if (ui->meshRegionLabel) ui->meshRegionLabel->hide();
if (ui->meshRegion) ui->meshRegion->hide();
if (ui->meshChannelLabel) ui->meshChannelLabel->hide();
if (ui->meshChannel) ui->meshChannel->hide();
// Keep the still-applicable controls (meshKeys, meshAutoLock,
// meshAutoSampleRate) — they govern the LoRa PHY auto-tune + the
// operator's identity/contact/channel store, both of which apply to
// MeshCore unchanged.
ui->meshAutoSampleRate->setChecked(m_settings.m_meshcoreAutoSampleRate);
QObject::connect(ui->meshKeys, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_meshKeys_clicked);
QObject::connect(ui->meshAutoLock, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_meshAutoLock_clicked);
QObject::connect(ui->meshAutoSampleRate, &QCheckBox::toggled, this, &MeshcoreDemodGUI::on_meshAutoSampleRate_toggled);
}
void MeshcoreDemodGUI::rebuildMeshcoreChannelOptions()
{
const QString region = ui->meshRegion->currentText();
const QString preset = ui->meshPreset->currentText();
const int previousChannel = ui->meshChannel->currentData().toInt();
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 applyMeshcoreProfileFromSelection 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
// → rebuildMeshcoreChannelOptions → 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("Meshcore channel number (zero-based, shown with center frequency)"));
int added = 0;
for (int meshChannel = 0; meshChannel <= 200; ++meshChannel)
{
modemmeshcore::TxRadioSettings meshRadio;
QString error;
const int channelNum = meshChannel + 1; // planner expects 1-based channel_num
const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum);
if (!modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error))
{
if (added > 0) {
break;
} else {
continue;
}
}
const QString label = meshRadio.hasCenterFrequency
? QString("%1 (%2 MHz)").arg(meshChannel).arg(meshRadio.centerFrequencyHz / 1000000.0, 0, 'f', 3)
: QString::number(meshChannel);
ui->meshChannel->addItem(label, meshChannel);
added++;
}
if (added == 0) {
ui->meshChannel->addItem("0", 0);
}
ui->meshChannel->setToolTip(tr("Meshcore channel number (%1 available for %2/%3)")
.arg(added)
.arg(region)
.arg(preset));
int restoreIndex = ui->meshChannel->findData(previousChannel);
if (restoreIndex < 0) {
restoreIndex = 0;
}
ui->meshChannel->setCurrentIndex(restoreIndex);
m_meshControlsUpdating = false;
qInfo() << "MeshcoreDemodGUI::rebuildMeshcoreChannelOptions:"
<< "region=" << region
<< "preset=" << preset
<< "channels=" << added;
}
void MeshcoreDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
getRollupContents()->saveState(m_rollupState);
applySettings();
}
void MeshcoreDemodGUI::onMenuDialogCalled(const QPoint &p)
{
if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings)
{
BasicChannelSettingsDialog dialog(&m_channelMarker, this);
dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex);
dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex);
dialog.setDefaultTitle(m_displayedName);
if (m_deviceUISet->m_deviceMIMOEngine)
{
dialog.setNumberOfStreams(m_meshcoreDemod->getNumberOfDeviceStreams());
dialog.setStreamIndex(m_settings.m_streamIndex);
}
dialog.move(p);
new DialogPositioner(&dialog, false);
dialog.exec();
m_settings.m_rgbColor = m_channelMarker.getColor().rgb();
m_settings.m_title = m_channelMarker.getTitle();
m_settings.m_useReverseAPI = dialog.useReverseAPI();
m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex();
m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex();
setWindowTitle(m_settings.m_title);
setTitle(m_channelMarker.getTitle());
setTitleColor(m_settings.m_rgbColor);
if (m_deviceUISet->m_deviceMIMOEngine)
{
m_settings.m_streamIndex = dialog.getSelectedStreamIndex();
m_channelMarker.clearStreamIndexes();
m_channelMarker.addStreamIndex(m_settings.m_streamIndex);
updateIndexLabel();
}
applySettings();
}
resetContextMenuType();
}
MeshcoreDemodGUI::MeshcoreDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
ChannelGUI(parent),
ui(new Ui::MeshcoreDemodGUI),
m_pluginAPI(pluginAPI),
m_deviceUISet(deviceUISet),
m_channelMarker(this),
m_deviceCenterFrequency(0),
m_basebandSampleRate(250000),
m_doApplySettings(true),
m_pipelineTabs(nullptr),
m_meshControlsUpdating(false),
m_meshAutoLockActive(false),
m_meshAutoLockCandidateIndex(0),
m_meshAutoLockCandidateStartMs(0),
m_meshAutoLockObservedSamplesForCandidate(0),
m_meshAutoLockObservedSourceSamplesForCandidate(0),
m_meshAutoLockTotalDecodeSamples(0),
m_meshAutoLockTrafficSeen(false),
m_meshAutoLockActivityTicks(0),
m_meshAutoLockArmStartMs(0),
m_meshAutoLockBaseOffsetHz(0),
m_meshAutoLockBaseInvert(false),
m_meshAutoLockBaseDeBits(0),
m_remoteTcpReconnectAutoApplyPending(false),
m_remoteTcpReconnectAutoApplyWaitTicks(0),
m_remoteTcpLastRunningState(false),
m_dechirpInspectionActive(false),
m_replayPendingHasSelection(false),
m_replaySelectionQueued(false),
m_pipelineMessageSequence(0),
m_focusedPipelineIndex(0),
m_tickCount(0)
{
setAttribute(Qt::WA_DeleteOnClose, true);
m_helpURL = "plugins/channelrx/demodmeshcore/readme.md";
RollupContents *rollupContents = getRollupContents();
ui->setupUi(rollupContents);
setupMeshcoreAutoProfileControls();
QObject::connect(ui->conf, QOverload<int>::of(&QDial::valueChanged), this, &MeshcoreDemodGUI::on_conf_valueChanged);
QObject::connect(ui->confAdd, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_confAdd_clicked);
QObject::connect(ui->confDel, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_confDel_clicked);
updateConfControls();
setupPipelineViews();
QObject::connect(ui->dechirpLiveFollow, &QPushButton::clicked, this, [this](bool) {
setDechirpInspectionMode(false);
});
updateDechirpModeUI();
// Mark major sections as vertically expanding so RollupContents does not clamp max height.
ui->verticalLayoutWidget_2->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
ui->spectrumContainer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
if (m_pipelineTabs) {
m_pipelineTabs->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
setSizePolicy(rollupContents->sizePolicy());
rollupContents->arrangeRollups();
connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
m_meshcoreDemod = (MeshcoreDemod*) rxChannel;
m_spectrumVis = m_meshcoreDemod->getSpectrumVis();
m_spectrumVis->setGLSpectrum(ui->glSpectrum);
m_meshcoreDemod->setMessageQueueToGUI(getInputMessageQueue());
connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick()));
SpectrumSettings spectrumSettings = m_spectrumVis->getSettings();
// Meshcore dechirp view defaults: keep lower pane active so replay-on-click
// always has a visible target.
spectrumSettings.m_displayWaterfall = true;
spectrumSettings.m_display3DSpectrogram = false;
spectrumSettings.m_displayCurrent = true;
spectrumSettings.m_displayHistogram = false;
spectrumSettings.m_displayMaxHold = false;
spectrumSettings.m_averagingMode = SpectrumSettings::AvgModeNone;
spectrumSettings.m_refLevel = -10.0f;
spectrumSettings.m_powerRange = 45.0f;
SpectrumVis::MsgConfigureSpectrumVis *msg = SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false);
m_spectrumVis->getInputMessageQueue()->push(msg);
ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03)));
ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999);
ui->deltaFrequency->setToolTip(tr("Offset from device center frequency (Hz)."));
ui->deltaFrequencyLabel->setToolTip(tr("Frequency offset control for the demodulator channel."));
ui->deltaUnits->setToolTip(tr("Frequency unit for the offset control."));
ui->BW->setToolTip(tr("LoRa bandwidth selection. Meshcore presets auto-set this."));
ui->bwLabel->setToolTip(tr("LoRa bandwidth selector."));
ui->BWText->setToolTip(tr("Current LoRa bandwidth in Hz."));
ui->Spread->setToolTip(tr("LoRa spreading factor (SF). Higher SF increases range but lowers rate."));
ui->spreadLabel->setToolTip(tr("LoRa spreading factor selector."));
ui->SpreadText->setToolTip(tr("Current spreading factor value."));
ui->deBits->setToolTip(tr("Low data-rate optimization bits (DE)."));
ui->deBitsLabel->setToolTip(tr("Low data-rate optimization setting."));
ui->deBitsText->setToolTip(tr("Current low data-rate optimization value."));
ui->preambleChirps->setToolTip(tr("LoRa preamble chirps. MeshCore: 32 for SF<9, 16 for SF>8 (SF-based auto-set on preset)."));
ui->preambleChirpsLabel->setToolTip(tr("Expected LoRa preamble length in chirps."));
ui->preambleChirpsText->setToolTip(tr("Current preamble chirp value."));
ui->mute->setToolTip(tr("Disable decoder output."));
ui->clear->setToolTip(tr("Clear decoded message log."));
ui->eomSquelch->setToolTip(tr("End-of-message squelch threshold."));
ui->eomSquelchLabel->setToolTip(tr("End-of-message squelch level."));
ui->eomSquelchText->setToolTip(tr("Current end-of-message squelch value."));
ui->messageLength->setToolTip(tr("Maximum payload symbol length when auto is disabled."));
ui->messageLengthLabel->setToolTip(tr("Maximum payload symbol length."));
ui->messageLengthText->setToolTip(tr("Current payload symbol length setting."));
ui->fecParity->setToolTip(tr("LoRa coding rate parity denominator (CR)."));
ui->fecParityLabel->setToolTip(tr("LoRa coding rate parity setting."));
ui->fecParityText->setToolTip(tr("Current coding rate parity value."));
ui->packetLength->setToolTip(tr("Fixed packet length for implicit-header mode."));
ui->packetLengthLabel->setToolTip(tr("Fixed packet length for implicit header mode."));
ui->packetLengthText->setToolTip(tr("Current fixed packet length."));
ui->invertRamps->setToolTip(tr("Invert chirp ramp direction. Disabled"));
ui->invertRamps->setEnabled(false);
ui->messageLabel->setToolTip(tr("Decoded output area."));
ui->udpSend->setToolTip(tr("Forward decoded payload bytes to UDP."));
ui->udpAddress->setToolTip(tr("Destination UDP address for forwarded payloads."));
ui->udpPort->setToolTip(tr("Destination UDP port for forwarded payloads."));
ui->udpSeparator->setToolTip(tr("UDP forwarding controls."));
ui->glSpectrum->setToolTip(tr("De-chirped spectrum view of the selected LoRa channel."));
ui->spectrumGUI->setToolTip(tr("Spectrum and waterfall display controls."));
ui->headerHammingStatus->setToolTip(tr("Header FEC status indicator."));
ui->headerCRCStatus->setToolTip(tr("Header CRC status indicator."));
ui->payloadFECStatus->setToolTip(tr("Payload FEC status indicator."));
ui->payloadCRCStatus->setToolTip(tr("Payload CRC status indicator."));
ui->channelPower->setToolTip(tr("Estimated channel power."));
ui->nLabel->setToolTip(tr("Estimated symbol count."));
ui->nText->setToolTip(tr("Current estimated symbol count."));
ui->nbSymbolsText->setToolTip(tr("Current raw LoRa symbol counter."));
ui->nbCodewordsText->setToolTip(tr("Current raw LoRa codeword counter."));
ui->sLabel->setToolTip(tr("Estimated codeword count."));
ui->sText->setToolTip(tr("Current estimated codeword count."));
ui->snrLabel->setToolTip(tr("Estimated signal-to-noise ratio."));
ui->snrText->setToolTip(tr("Current estimated SNR."));
ui->sUnits->setToolTip(tr("Unit for SNR."));
ui->symbolsCodewordsSeparator->setToolTip(tr("Separator between symbol and codeword counters."));
m_channelMarker.setMovable(true);
m_channelMarker.setVisible(true);
connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor()));
connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor()));
m_deviceUISet->addChannelMarker(&m_channelMarker);
ui->spectrumGUI->setBuddies(m_spectrumVis, ui->glSpectrum);
m_settings.setChannelMarker(&m_channelMarker);
m_settings.setSpectrumGUI(ui->spectrumGUI);
m_settings.setRollupState(&m_rollupState);
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
setBandwidths();
displaySettings();
makeUIConnections();
resetLoRaStatus();
applySettings(true);
// On first creation, combo signals haven't fired yet. Apply selected Meshcore profile once.
// 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, &MeshcoreDemodGUI::applyMeshcoreProfileFromSelection, Qt::QueuedConnection);
DialPopup::addPopupsToChildDials(this);
m_resizer.enableChildMouseTracking();
}
MeshcoreDemodGUI::~MeshcoreDemodGUI()
{
delete ui;
}
void MeshcoreDemodGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void MeshcoreDemodGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
setTitleColor(m_channelMarker.getColor());
MeshcoreDemod::MsgConfigureMeshcoreDemod* message = MeshcoreDemod::MsgConfigureMeshcoreDemod::create( m_settings, force);
m_meshcoreDemod->getInputMessageQueue()->push(message);
}
}
void MeshcoreDemodGUI::updateControlAvailabilityHints()
{
const bool loRaMode = m_settings.m_codingScheme == MeshcoreDemodSettings::CodingLoRa;
const bool explicitHeaderMode = loRaMode && MeshcoreDemodSettings::m_hasHeader;
const QString fftWindowEnabledTip = tr("FFT window used by the de-chirping stage.");
const QString fftWindowDisabledTip = tr("Ignored in LoRa mode. The LoRa demodulator uses a fixed internal FFT window.");
const QString messageLengthAutoEnabledTip = tr("Auto-detect payload symbol length from headers.");
const QString messageLengthAutoDisabledTip = tr("Disabled in LoRa explicit-header mode. Payload length is decoded from the LoRa header.");
const QString messageLengthDefaultTip = tr("Maximum payload symbol length when auto is disabled.");
const QString messageLengthHeaderTip = tr("Maximum payload symbol clamp in LoRa explicit-header mode. Header still provides nominal payload length.");
const QString messageLengthTip = explicitHeaderMode ? messageLengthHeaderTip : messageLengthDefaultTip;
ui->messageLength->setToolTip(messageLengthTip);
ui->messageLengthLabel->setToolTip(messageLengthTip);
ui->messageLengthText->setToolTip(messageLengthTip);
const bool isUserPreset = focusedSettings().m_meshcorePresetName.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);
ui->deltaFrequency->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 = !MeshcoreDemodSettings::m_hasHeader;
ui->fecParity->setEnabled(headerControlsEnabled);
ui->packetLength->setEnabled(headerControlsEnabled);
const QString fecParityEnabledTip = tr("LoRa coding rate parity denominator (CR).");
const QString fecParityDisabledTip = tr("Disabled in explicit-header mode. Coding rate is decoded from the LoRa header.");
const QString fecParityTip = headerControlsEnabled ? fecParityEnabledTip : fecParityDisabledTip;
ui->fecParity->setToolTip(fecParityTip);
ui->fecParityLabel->setToolTip(fecParityTip);
ui->fecParityText->setToolTip(fecParityTip);
const QString crcEnabledTip = tr("Expect payload CRC.");
const QString crcDisabledTip = tr("Disabled in explicit-header mode. CRC expectation is decoded from the LoRa header.");
const QString packetLengthEnabledTip = tr("Fixed packet length for implicit-header mode.");
const QString packetLengthDisabledTip = tr("Disabled in explicit-header mode. Payload length is decoded from the LoRa header.");
const QString packetLengthTip = headerControlsEnabled ? packetLengthEnabledTip : packetLengthDisabledTip;
ui->packetLength->setToolTip(packetLengthTip);
ui->packetLengthLabel->setToolTip(packetLengthTip);
ui->packetLengthText->setToolTip(packetLengthTip);
}
void MeshcoreDemodGUI::displaySettings()
{
const auto& s = focusedSettings();
// Primary pipeline bandwidth for channel marker and spectrum display.
const int thisBWPrimary = MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex];
// Focused pipeline bandwidth for the BW slider and its label.
const int thisBWFocused = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex];
// Channel marker always reflects the primary pipeline.
m_channelMarker.blockSignals(true);
m_channelMarker.setTitle(m_settings.m_title);
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
m_channelMarker.setBandwidth(thisBWPrimary);
m_channelMarker.blockSignals(false);
m_channelMarker.setColor(m_settings.m_rgbColor);
setTitleColor(m_settings.m_rgbColor);
setTitle(m_channelMarker.getTitle());
ui->glSpectrum->setSampleRate(thisBWPrimary);
ui->glSpectrum->setCenterFrequency(thisBWPrimary/2);
blockApplySettings(true);
// LoRa controls reflect the focused pipeline.
ui->deltaFrequency->setValue(s.m_inputFrequencyOffset);
ui->BWText->setText(QString("%1 Hz").arg(thisBWFocused));
ui->BW->setValue(s.m_bandwidthIndex);
ui->Spread->setValue(s.m_spreadFactor);
ui->SpreadText->setText(tr("%1").arg(s.m_spreadFactor));
ui->deBits->setValue(s.m_deBits);
ui->deBitsText->setText(tr("%1").arg(s.m_deBits));
ui->preambleChirps->setValue(s.m_preambleChirps);
ui->preambleChirpsText->setText(tr("%1").arg(s.m_preambleChirps));
// Global settings always from primary.
ui->messageLengthText->setText(tr("%1").arg(m_settings.m_nbSymbolsMax));
ui->messageLength->setValue(m_settings.m_nbSymbolsMax);
ui->udpSend->setChecked(m_settings.m_sendViaUDP);
ui->udpSendJson->setChecked(m_settings.m_sendJsonViaUDP);
ui->udpAddress->setText(m_settings.m_udpAddress);
ui->udpPort->setText(tr("%1").arg(m_settings.m_udpPort));
ui->invertRamps->setChecked(s.m_invertRamps);
displaySquelch();
updateIndexLabel();
getRollupContents()->restoreState(m_rollupState);
updateAbsoluteCenterFrequency();
const bool hasCustomKeys = !m_settings.m_meshcoreKeySpecList.trimmed().isEmpty();
ui->meshKeys->setText(hasCustomKeys ? tr("Keys*") : tr("Keys..."));
ui->meshKeys->setToolTip(hasCustomKeys ?
tr("Custom Meshcore decode keys configured. Click to edit.") :
tr("Open Meshcore key manager."));
m_meshControlsUpdating = true;
ui->meshAutoSampleRate->setChecked(m_settings.m_meshcoreAutoSampleRate);
m_meshControlsUpdating = false;
ui->meshAutoLock->blockSignals(true);
ui->meshAutoLock->setChecked(m_meshAutoLockActive);
ui->meshAutoLock->setText(m_meshAutoLockActive ? tr("Locking...") : tr("Auto Lock"));
ui->meshAutoLock->blockSignals(false);
m_meshControlsUpdating = true;
// Mesh region/preset/channel reflect the focused pipeline's selection.
int regionIndex = ui->meshRegion->findText(s.m_meshcoreRegionCode);
if (regionIndex < 0) {
regionIndex = ui->meshRegion->findText("EU_868");
}
if (regionIndex < 0) {
regionIndex = 0;
}
ui->meshRegion->setCurrentIndex(regionIndex);
ui->meshRegion->setEnabled(s.m_meshcorePresetName != "USER");
ui->BW->setEnabled(s.m_meshcorePresetName == "USER");
ui->Spread->setEnabled(s.m_meshcorePresetName == "USER");
ui->deBits->setEnabled(s.m_meshcorePresetName == "USER");
ui->preambleChirps->setEnabled(s.m_meshcorePresetName == "USER");
int presetIndex = ui->meshPreset->findText(s.m_meshcorePresetName);
if (presetIndex < 0) {
presetIndex = ui->meshPreset->findText("EU_NARROW");
}
if (presetIndex < 0) {
presetIndex = 0;
}
ui->meshPreset->setCurrentIndex(presetIndex);
m_meshControlsUpdating = false;
rebuildMeshcoreChannelOptions();
m_meshControlsUpdating = true;
int channelIndex = ui->meshChannel->findData(s.m_meshcoreChannelIndex);
if (channelIndex < 0) {
channelIndex = 0;
}
ui->meshChannel->setCurrentIndex(channelIndex);
m_meshControlsUpdating = false;
updateControlAvailabilityHints();
updateConfControls();
blockApplySettings(false);
}
void MeshcoreDemodGUI::displaySquelch()
{
ui->eomSquelch->setValue(m_settings.m_eomSquelchTenths);
if (m_settings.m_eomSquelchTenths == ui->eomSquelch->maximum()) {
ui->eomSquelchText->setText("---");
} else {
ui->eomSquelchText->setText(tr("%1").arg(m_settings.m_eomSquelchTenths / 10.0, 0, 'f', 1));
}
}
void MeshcoreDemodGUI::displayLoRaStatus(int headerParityStatus, bool headerCRCStatus, int payloadParityStatus, bool payloadCRCStatus)
{
if (MeshcoreDemodSettings::m_hasHeader && (headerParityStatus == (int) MeshcoreDemodSettings::ParityOK)) {
ui->headerHammingStatus->setStyleSheet("QLabel { background-color : green; }");
} else if (MeshcoreDemodSettings::m_hasHeader && (headerParityStatus == (int) MeshcoreDemodSettings::ParityError)) {
ui->headerHammingStatus->setStyleSheet("QLabel { background-color : red; }");
} else if (MeshcoreDemodSettings::m_hasHeader && (headerParityStatus == (int) MeshcoreDemodSettings::ParityCorrected)) {
ui->headerHammingStatus->setStyleSheet("QLabel { background-color : blue; }");
} else {
ui->headerHammingStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
}
if (MeshcoreDemodSettings::m_hasHeader && headerCRCStatus) {
ui->headerCRCStatus->setStyleSheet("QLabel { background-color : green; }");
} else if (MeshcoreDemodSettings::m_hasHeader && !headerCRCStatus) {
ui->headerCRCStatus->setStyleSheet("QLabel { background-color : red; }");
} else {
ui->headerCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
}
if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityOK) {
ui->payloadFECStatus->setStyleSheet("QLabel { background-color : green; }");
} else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityError) {
ui->payloadFECStatus->setStyleSheet("QLabel { background-color : red; }");
} else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) {
ui->payloadFECStatus->setStyleSheet("QLabel { background-color : blue; }");
} else {
ui->payloadFECStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
}
if (payloadCRCStatus) {
ui->payloadCRCStatus->setStyleSheet("QLabel { background-color : green; }");
} else {
ui->payloadCRCStatus->setStyleSheet("QLabel { background-color : red; }");
}
}
void MeshcoreDemodGUI::resetLoRaStatus()
{
ui->headerHammingStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
ui->headerCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
ui->payloadFECStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
ui->payloadCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }");
ui->nbSymbolsText->setText("---");
ui->nbCodewordsText->setText("---");
}
void MeshcoreDemodGUI::setBandwidths()
{
int maxBandwidth = m_basebandSampleRate/MeshcoreDemodSettings::oversampling;
int maxIndex = 0;
for (; (maxIndex < MeshcoreDemodSettings::nbBandwidths) && (MeshcoreDemodSettings::bandwidths[maxIndex] <= maxBandwidth); maxIndex++)
{}
if (maxIndex != 0)
{
qDebug("MeshcoreDemodGUI::setBandwidths: avl: %d max: %d", maxBandwidth, MeshcoreDemodSettings::bandwidths[maxIndex-1]);
ui->BW->setMaximum(maxIndex - 1);
int index = ui->BW->value();
ui->BWText->setText(QString("%1 Hz").arg(MeshcoreDemodSettings::bandwidths[index]));
}
}
void MeshcoreDemodGUI::setupPipelineViews()
{
m_pipelineTabs = ui->pipelineTabs;
ensurePipelineView(-1, "All");
}
MeshcoreDemodGUI::PipelineView& MeshcoreDemodGUI::ensurePipelineView(int pipelineId, const QString& pipelineName)
{
auto it = m_pipelineViews.find(pipelineId);
if (it != m_pipelineViews.end()) {
return it.value();
}
PipelineView view;
view.tabWidget = new QWidget(m_pipelineTabs);
QVBoxLayout *layout = new QVBoxLayout(view.tabWidget);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(2);
QSplitter *splitter = new QSplitter(Qt::Vertical, view.tabWidget);
splitter->setChildrenCollapsible(false);
view.logText = new QPlainTextEdit(splitter);
view.logText->setReadOnly(true);
view.logText->setLineWrapMode(QPlainTextEdit::NoWrap);
QFont monoLog = view.logText->font();
monoLog.setFamily("Liberation Mono");
view.logText->setFont(monoLog);
view.treeWidget = new QTreeWidget(splitter);
view.treeWidget->setColumnCount(2);
view.treeWidget->setHeaderLabels(QStringList() << "Field" << "Value");
view.treeWidget->header()->setStretchLastSection(true);
view.treeWidget->setAlternatingRowColors(true);
view.treeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
QObject::connect(view.treeWidget, &QTreeWidget::itemSelectionChanged, this, &MeshcoreDemodGUI::onPipelineTreeSelectionChanged);
QObject::connect(view.treeWidget, &QTreeWidget::itemClicked, this, &MeshcoreDemodGUI::onPipelineTreeSelectionChanged);
QObject::connect(view.treeWidget, &QTreeWidget::currentItemChanged, this, [this, tree=view.treeWidget](QTreeWidgetItem*, QTreeWidgetItem*) {
queueReplayForTree(tree);
});
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 2);
layout->addWidget(splitter);
const QString tabLabel = pipelineName.trimmed().isEmpty() ? QString("P%1").arg(pipelineId) : pipelineName;
m_pipelineTabs->addTab(view.tabWidget, tabLabel);
m_pipelineViews.insert(pipelineId, view);
return m_pipelineViews[pipelineId];
}
void MeshcoreDemodGUI::clearPipelineViews()
{
setDechirpInspectionMode(false);
for (auto it = m_pipelineViews.begin(); it != m_pipelineViews.end(); ++it)
{
if (it.value().logText) {
it.value().logText->clear();
}
if (it.value().treeWidget) {
it.value().treeWidget->clear();
}
}
m_dechirpSnapshots.clear();
m_dechirpSnapshotOrder.clear();
m_dechirpSelectedMessageKey.clear();
m_replayPendingMessageKey.clear();
m_replayPendingHasSelection = false;
m_replaySelectionQueued = false;
m_pipelineMessageKeyByBase.clear();
m_pipelinePendingMessageKeysByBase.clear();
m_pipelineMessageSequence = 0;
}
QString MeshcoreDemodGUI::buildPipelineMessageBaseKey(int pipelineId, uint32_t frameId, const QString& timestamp) const
{
if (frameId != 0U) {
return QString("%1|frame:%2").arg(pipelineId).arg(frameId);
}
const QString ts = timestamp.trimmed().isEmpty()
? QStringLiteral("no-ts")
: timestamp.trimmed();
return QString("%1|%2").arg(pipelineId).arg(ts);
}
QString MeshcoreDemodGUI::allocatePipelineMessageKey(const QString& baseKey)
{
++m_pipelineMessageSequence;
const QString key = QString("%1#%2").arg(baseKey).arg(m_pipelineMessageSequence);
m_pipelineMessageKeyByBase[baseKey] = key;
m_pipelinePendingMessageKeysByBase[baseKey].push_back(key);
return key;
}
QString MeshcoreDemodGUI::resolvePipelineMessageKey(const QString& baseKey) const
{
auto pendingIt = m_pipelinePendingMessageKeysByBase.constFind(baseKey);
if ((pendingIt != m_pipelinePendingMessageKeysByBase.constEnd()) && !pendingIt.value().isEmpty()) {
return pendingIt.value().front();
}
auto it = m_pipelineMessageKeyByBase.constFind(baseKey);
return it == m_pipelineMessageKeyByBase.constEnd() ? QString() : it.value();
}
void MeshcoreDemodGUI::consumePipelineMessageKey(const QString& baseKey, const QString& key)
{
auto pendingIt = m_pipelinePendingMessageKeysByBase.find(baseKey);
if (pendingIt == m_pipelinePendingMessageKeysByBase.end()) {
return;
}
QVector<QString>& pendingKeys = pendingIt.value();
const int idx = pendingKeys.indexOf(key);
if (idx >= 0) {
pendingKeys.removeAt(idx);
}
if (pendingKeys.isEmpty()) {
m_pipelinePendingMessageKeysByBase.erase(pendingIt);
}
}
void MeshcoreDemodGUI::rememberLoRaDechirpSnapshot(
const MeshcoreDemodMsg::MsgReportDecodeBytes& msg,
const QString& messageKey
)
{
const std::vector<std::vector<float>>& lines = msg.getDechirpedSpectrum();
if (lines.empty()) {
return;
}
if (messageKey.trimmed().isEmpty()) {
return;
}
const QString key = messageKey;
DechirpSnapshot snapshot;
snapshot.fftSize = static_cast<int>(lines.front().size());
snapshot.lines = lines;
m_dechirpSnapshots[key] = snapshot;
const int existingIndex = m_dechirpSnapshotOrder.indexOf(key);
if (existingIndex >= 0) {
m_dechirpSnapshotOrder.removeAt(existingIndex);
}
m_dechirpSnapshotOrder.push_back(key);
static constexpr int kMaxStoredSnapshots = 256;
while (m_dechirpSnapshotOrder.size() > kMaxStoredSnapshots)
{
const QString oldestKey = m_dechirpSnapshotOrder.front();
m_dechirpSnapshotOrder.pop_front();
m_dechirpSnapshots.remove(oldestKey);
clearTreeMessageKeyReferences(oldestKey);
if (oldestKey == m_dechirpSelectedMessageKey) {
setDechirpInspectionMode(false);
}
}
}
void MeshcoreDemodGUI::setDechirpInspectionMode(bool enabled)
{
if (m_dechirpInspectionActive == enabled)
{
updateDechirpModeUI();
return;
}
m_dechirpInspectionActive = enabled;
if (enabled)
{
if (m_spectrumVis) {
m_spectrumVis->setGLSpectrum(nullptr);
}
}
else
{
m_dechirpSelectedMessageKey.clear();
if (m_spectrumVis && ui->glSpectrum)
{
m_spectrumVis->setGLSpectrum(ui->glSpectrum);
const SpectrumSettings spectrumSettings = m_spectrumVis->getSettings();
ui->glSpectrum->setDisplayWaterfall(spectrumSettings.m_displayWaterfall);
ui->glSpectrum->setDisplay3DSpectrogram(spectrumSettings.m_display3DSpectrogram);
}
}
updateDechirpModeUI();
}
void MeshcoreDemodGUI::updateDechirpModeUI()
{
ui->dechirpLiveFollow->setEnabled(m_dechirpInspectionActive);
ui->dechirpLiveFollow->setText(tr("Live"));
ui->dechirpLiveFollow->setToolTip(m_dechirpInspectionActive
? tr("Return de-chirped spectrum to live follow mode.")
: tr("Already in live follow mode."));
}
void MeshcoreDemodGUI::queueReplayForTree(QTreeWidget *treeWidget)
{
m_replayPendingMessageKey.clear();
m_replayPendingHasSelection = false;
if (treeWidget)
{
const QList<QTreeWidgetItem*> selectedItems = treeWidget->selectedItems();
QTreeWidgetItem *root = selectedItems.isEmpty() ? treeWidget->currentItem() : selectedItems.first();
if (root)
{
while (root->parent()) {
root = root->parent();
}
m_replayPendingHasSelection = true;
m_replayPendingMessageKey = root->data(0, kTreeMessageKeyRole).toString();
}
}
if (m_replaySelectionQueued) {
return;
}
m_replaySelectionQueued = true;
QMetaObject::invokeMethod(this, [this]() { processQueuedReplay(); }, Qt::QueuedConnection);
}
void MeshcoreDemodGUI::processQueuedReplay()
{
m_replaySelectionQueued = false;
const bool hasSelection = m_replayPendingHasSelection;
const QString key = m_replayPendingMessageKey;
m_replayPendingHasSelection = false;
m_replayPendingMessageKey.clear();
if (!hasSelection)
{
setDechirpInspectionMode(false);
return;
}
if (key.isEmpty())
{
setDechirpInspectionMode(false);
return;
}
auto it = m_dechirpSnapshots.constFind(key);
if (it == m_dechirpSnapshots.constEnd())
{
setDechirpInspectionMode(false);
return;
}
setDechirpInspectionMode(true);
m_dechirpSelectedMessageKey = key;
replayDechirpSnapshot(it.value());
}
void MeshcoreDemodGUI::hardResetDechirpDisplayBuffers()
{
if (!ui || !ui->glSpectrum) {
return;
}
bool wantWaterfall = true;
bool want3DSpectrogram = false;
if (m_spectrumVis)
{
const SpectrumSettings spectrumSettings = m_spectrumVis->getSettings();
wantWaterfall = spectrumSettings.m_displayWaterfall;
want3DSpectrogram = spectrumSettings.m_display3DSpectrogram;
}
if (!wantWaterfall && !want3DSpectrogram) {
wantWaterfall = true;
}
ui->glSpectrum->setDisplayWaterfall(false);
ui->glSpectrum->setDisplay3DSpectrogram(false);
QCoreApplication::processEvents(
QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers,
1
);
ui->glSpectrum->setDisplayWaterfall(wantWaterfall);
ui->glSpectrum->setDisplay3DSpectrogram(want3DSpectrogram);
QCoreApplication::processEvents(
QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers,
1
);
}
void MeshcoreDemodGUI::clearTreeMessageKeyReferences(const QString& messageKey)
{
if (messageKey.trimmed().isEmpty()) {
return;
}
for (auto it = m_pipelineViews.begin(); it != m_pipelineViews.end(); ++it)
{
QTreeWidget *treeWidget = it.value().treeWidget;
if (!treeWidget) {
continue;
}
for (int i = 0; i < treeWidget->topLevelItemCount(); ++i)
{
QTreeWidgetItem *item = treeWidget->topLevelItem(i);
if (!item) {
continue;
}
if (item->data(0, kTreeMessageKeyRole).toString() == messageKey)
{
item->setData(0, kTreeMessageKeyRole, QString());
item->setToolTip(0, tr("Dechirp snapshot no longer available for this row."));
}
}
}
}
void MeshcoreDemodGUI::replayDechirpSnapshot(const DechirpSnapshot& snapshot)
{
if (!ui || !ui->glSpectrum || snapshot.lines.empty()) {
return;
}
if (m_spectrumVis)
{
SpectrumSettings spectrumSettings = m_spectrumVis->getSettings();
if (!spectrumSettings.m_displayWaterfall && !spectrumSettings.m_display3DSpectrogram)
{
spectrumSettings.m_displayWaterfall = true;
SpectrumVis::MsgConfigureSpectrumVis *msg =
SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false);
m_spectrumVis->getInputMessageQueue()->push(msg);
}
}
const int fftSize = snapshot.fftSize > 0 ? snapshot.fftSize : static_cast<int>(snapshot.lines.front().size());
if (fftSize <= 0) {
return;
}
hardResetDechirpDisplayBuffers();
std::vector<Real> line(static_cast<size_t>(fftSize), static_cast<Real>(-120.0f));
const int spectrumHeight = (ui && ui->glSpectrum) ? ui->glSpectrum->height() : 0;
const int clearLines = std::max(128, std::min(2048, spectrumHeight > 0 ? spectrumHeight + 64 : 768));
const int replayChunkLines = 8;
auto flushReplayChunk = [this]() {
ui->glSpectrum->repaint();
QCoreApplication::processEvents(
QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers,
1
);
};
auto feedLineToSpectrum = [&](const std::vector<float>* powers) {
if (powers == nullptr)
{
std::fill(line.begin(), line.end(), static_cast<Real>(-120.0f));
}
else
{
std::fill(line.begin(), line.end(), static_cast<Real>(-120.0f));
const int count = std::min(fftSize, static_cast<int>(powers->size()));
for (int i = 0; i < count; ++i)
{
const float power = std::max((*powers)[static_cast<size_t>(i)], 1e-12f);
line[static_cast<size_t>(i)] = static_cast<Real>(10.0f * std::log10(power));
}
}
ui->glSpectrum->newSpectrum(line.data(), fftSize);
};
// Prime the GL spectrum so pending size/layout changes are applied before replay.
flushReplayChunk();
int lineCounter = 0;
for (int i = 0; i < clearLines; ++i)
{
feedLineToSpectrum(nullptr);
if ((++lineCounter % replayChunkLines) == 0) {
flushReplayChunk();
}
}
for (const std::vector<float>& powers : snapshot.lines)
{
feedLineToSpectrum(&powers);
if ((++lineCounter % replayChunkLines) == 0) {
flushReplayChunk();
}
}
flushReplayChunk();
}
void MeshcoreDemodGUI::onPipelineTreeSelectionChanged()
{
QTreeWidget *treeWidget = qobject_cast<QTreeWidget*>(sender());
if (!treeWidget) {
return;
}
queueReplayForTree(treeWidget);
}
void MeshcoreDemodGUI::appendPipelineLogLine(int pipelineId, const QString& pipelineName, const QString& line)
{
auto appendLine = [&line](PipelineView& view) {
if (!view.logText) {
return;
}
view.logText->appendPlainText(line);
alignTextViewToLatestLineLeft(view.logText);
};
PipelineView& targetView = ensurePipelineView(pipelineId, pipelineName);
appendLine(targetView);
if (pipelineId != -1)
{
PipelineView& allView = ensurePipelineView(-1, "All");
if (allView.logText != targetView.logText) {
appendLine(allView);
}
}
}
void MeshcoreDemodGUI::appendPipelineStatusLine(int pipelineId, const QString& pipelineName, const QString& status)
{
appendPipelineLogLine(pipelineId, pipelineName, QString(">%1").arg(status));
}
void MeshcoreDemodGUI::appendPipelineBytes(int pipelineId, const QString& pipelineName, const QByteArray& bytes)
{
QStringList lines;
QString line;
for (int i = 0; i < bytes.size(); ++i)
{
const unsigned int b = static_cast<unsigned int>(static_cast<unsigned char>(bytes.at(i)));
if ((i % 16) == 0) {
line = QString("%1|").arg(i, 3, 10, QChar('0'));
}
line += QString("%1").arg(b, 2, 16, QChar('0'));
if ((i % 16) == 15)
{
lines.append(line);
}
else if ((i % 4) == 3)
{
line += "|";
}
else
{
line += " ";
}
}
if ((bytes.size() % 16) != 0 && !line.isEmpty()) {
lines.append(line);
}
for (const QString& l : lines) {
appendPipelineLogLine(pipelineId, pipelineName, l);
}
}
void MeshcoreDemodGUI::appendPipelineTreeFields(
int pipelineId,
const QString& pipelineName,
const QString& messageTitle,
const QVector<QPair<QString, QString>>& fields,
const QString& messageKey
)
{
auto fieldValue = [&fields](const QString& path) -> QString {
for (const QPair<QString, QString>& field : fields)
{
if (field.first == path) {
return field.second;
}
}
return QString();
};
auto parseBool = [](const QString& value, bool& ok) -> bool {
const QString lower = value.trimmed().toLower();
if ((lower == "true") || (lower == "1") || (lower == "yes"))
{
ok = true;
return true;
}
if ((lower == "false") || (lower == "0") || (lower == "no"))
{
ok = true;
return false;
}
ok = false;
return false;
};
const QString portName = fieldValue("data.port_name");
const QString portNum = fieldValue("data.portnum");
const QString payloadLength = fieldValue("data.payload_len");
const QString decryptedValue = fieldValue("decode.decrypted");
const QString payloadText = fieldValue("data.text");
const QString payloadHex = fieldValue("data.payload_hex");
const QString viaMqttValue = fieldValue("header.via_mqtt");
QString messageType = portName;
if (messageType.isEmpty() && !portNum.isEmpty()) {
messageType = QString("PORT_%1").arg(portNum);
}
QString source;
bool viaMqttOk = false;
const bool viaMqtt = parseBool(viaMqttValue, viaMqttOk);
if (viaMqttOk) {
source = viaMqtt ? "mqtt" : "radio";
}
bool decryptedOk = false;
const bool decrypted = parseBool(decryptedValue, decryptedOk);
QString payloadPreview;
if (decryptedOk && decrypted)
{
payloadPreview = !payloadText.isEmpty() ? payloadText : payloadHex;
payloadPreview.replace('\n', ' ');
payloadPreview.replace('\r', ' ');
if (payloadPreview.size() > 96) {
payloadPreview = payloadPreview.left(96) + "...";
}
}
QStringList rootSummaryParts;
if (!messageType.isEmpty()) {
rootSummaryParts << QString("type=%1").arg(messageType);
}
if (!source.isEmpty()) {
rootSummaryParts << QString("source=%1").arg(source);
}
if (!payloadLength.isEmpty()) {
rootSummaryParts << QString("len=%1").arg(payloadLength);
}
if (decryptedOk) {
rootSummaryParts << QString("decrypted=%1").arg(decrypted ? "yes" : "no");
}
if (!payloadPreview.isEmpty()) {
rootSummaryParts << QString("payload=\"%1\"").arg(payloadPreview);
}
const QString rootSummary = rootSummaryParts.join(" ");
auto compactSummaryValue = [](QString value, int maxLen = 64) -> QString {
value.replace('\n', ' ');
value.replace('\r', ' ');
value = value.trimmed();
if (value.size() > maxLen) {
return value.left(maxLen) + "...";
}
return value;
};
auto splitCamelCase = [](const QString& token) -> QString {
QString out;
out.reserve(token.size() + 8);
for (int i = 0; i < token.size(); ++i)
{
const QChar ch = token.at(i);
const QChar prev = (i > 0) ? token.at(i - 1) : QChar();
const bool breakBeforeUpper = (i > 0)
&& ch.isUpper()
&& (prev.isLower() || prev.isDigit());
if (breakBeforeUpper) {
out += ' ';
}
out += ch;
}
return out;
};
auto humanizeProtoField = [&](const QString& rawName) -> QString {
static const QMap<QString, QString> kTokenMap = {
{"id", "ID"},
{"uid", "UID"},
{"snr", "SNR"},
{"rssi", "RSSI"},
{"rx", "RX"},
{"tx", "TX"},
{"utc", "UTC"},
{"gps", "GPS"},
{"lat", "Latitude"},
{"lon", "Longitude"},
{"lng", "Longitude"},
{"alt", "Altitude"},
{"deg", "Degrees"},
{"num", "Count"},
{"secs", "Seconds"},
{"hz", "Hz"}
};
QStringList prettyParts;
const QStringList snakeParts = rawName.split('_', Qt::SkipEmptyParts);
for (const QString& snakePart : snakeParts)
{
const QString camelSplit = splitCamelCase(snakePart);
const QStringList words = camelSplit.split(' ', Qt::SkipEmptyParts);
for (const QString& word : words)
{
const QString lower = word.toLower();
const auto mapIt = kTokenMap.find(lower);
if (mapIt != kTokenMap.end())
{
prettyParts.append(mapIt.value());
}
else
{
QString normalized = lower;
if (!normalized.isEmpty()) {
normalized[0] = normalized[0].toUpper();
}
prettyParts.append(normalized);
}
}
}
if (prettyParts.isEmpty()) {
return rawName;
}
return prettyParts.join(' ');
};
auto formatFieldLabel = [&](const QString& rawName) -> QString {
if (rawName.isEmpty()) {
return rawName;
}
bool isIndex = false;
rawName.toInt(&isIndex);
if (isIndex) {
return QString("Item (%1)").arg(rawName);
}
return QString("%1 (%2)").arg(humanizeProtoField(rawName), rawName);
};
auto formatLabeledValue = [&](const QString& rawName, const QString& value, int maxLen = 120) -> QString {
const QString compactValue = compactSummaryValue(value, maxLen);
const QString label = formatFieldLabel(rawName);
if (label.isEmpty()) {
return compactValue;
}
if (compactValue.isEmpty()) {
return label;
}
return QString("%1: %2").arg(label, compactValue);
};
auto populateTree = [&](PipelineView& view) {
if (!view.treeWidget) {
return;
}
QTreeWidgetItem *root = nullptr;
if (!messageKey.isEmpty())
{
for (int i = 0; i < view.treeWidget->topLevelItemCount(); ++i)
{
QTreeWidgetItem *candidate = view.treeWidget->topLevelItem(i);
if (!candidate) {
continue;
}
if (candidate->data(0, kTreeMessageKeyRole).toString() == messageKey)
{
root = candidate;
break;
}
}
}
if (!root) {
root = new QTreeWidgetItem(view.treeWidget);
} else {
while (root->childCount() > 0) {
delete root->takeChild(0);
}
}
root->setText(0, messageTitle);
root->setText(1, rootSummary);
const bool snapshotKnown = messageKey.isEmpty() || m_dechirpSnapshots.contains(messageKey);
if (!snapshotKnown)
{
// Keep row selectable for structured content but prevent stale replay lookup.
root->setData(0, kTreeMessageKeyRole, QString());
root->setToolTip(0, tr("No dechirp snapshot is available for this decoded row."));
}
else
{
root->setData(0, kTreeMessageKeyRole, messageKey);
}
for (const QPair<QString, QString>& field : fields)
{
QStringList pathParts = field.first.split('.', Qt::SkipEmptyParts);
if (pathParts.isEmpty())
{
const QString fieldLabel = formatFieldLabel(field.first);
const QString labeledValue = formatLabeledValue(field.first, field.second);
QTreeWidgetItem *leaf = new QTreeWidgetItem(QStringList() << fieldLabel << labeledValue);
leaf->setData(0, kTreeRawKeyRole, field.first);
leaf->setData(0, kTreeDisplayLabelRole, fieldLabel);
leaf->setData(1, kTreeRawValueRole, field.second);
const QString formatted = formatLabeledValue(field.first, field.second, 120);
leaf->setToolTip(0, formatted);
leaf->setToolTip(1, formatted);
root->addChild(leaf);
continue;
}
QTreeWidgetItem *parent = root;
for (int i = 0; i < pathParts.size(); ++i)
{
const QString& part = pathParts.at(i);
QTreeWidgetItem *child = nullptr;
for (int c = 0; c < parent->childCount(); ++c)
{
QTreeWidgetItem *candidate = parent->child(c);
QString candidateRaw = candidate->data(0, kTreeRawKeyRole).toString();
if (candidateRaw.isEmpty()) {
candidateRaw = candidate->text(0);
}
if (candidateRaw == part)
{
child = candidate;
break;
}
}
if (!child)
{
const QString fieldLabel = formatFieldLabel(part);
child = new QTreeWidgetItem(QStringList() << fieldLabel);
child->setData(0, kTreeRawKeyRole, part);
child->setData(0, kTreeDisplayLabelRole, fieldLabel);
parent->addChild(child);
}
if (i == pathParts.size() - 1)
{
const QString fieldLabel = child->data(0, kTreeDisplayLabelRole).toString().isEmpty()
? formatFieldLabel(part)
: child->data(0, kTreeDisplayLabelRole).toString();
const QString formatted = formatLabeledValue(part, field.second, 120);
child->setData(0, kTreeDisplayLabelRole, fieldLabel);
child->setData(1, kTreeRawValueRole, field.second);
child->setText(1, formatLabeledValue(part, field.second));
child->setToolTip(0, formatted);
child->setToolTip(1, formatted);
}
parent = child;
}
}
std::function<QString(QTreeWidgetItem*)> computeNodeSummary = [&](QTreeWidgetItem *item) -> QString {
if (!item) {
return QString();
}
QStringList parts;
const int maxParts = 4;
for (int i = 0; i < item->childCount(); ++i)
{
QTreeWidgetItem *child = item->child(i);
if (!child) {
continue;
}
QString key = child->data(0, kTreeDisplayLabelRole).toString().trimmed();
if (key.isEmpty()) {
key = child->text(0).trimmed();
}
QString value = child->data(1, kTreeRawValueRole).toString().trimmed();
if (value.isEmpty() && (child->childCount() == 0)) {
value = child->text(1).trimmed();
}
QString part;
if (!value.isEmpty())
{
part = key.isEmpty() ? compactSummaryValue(value) : QString("%1: %2").arg(key, compactSummaryValue(value));
}
else if (child->childCount() > 0)
{
const QString nested = computeNodeSummary(child);
if (!nested.isEmpty()) {
part = key.isEmpty() ? nested : QString("%1: {%2}").arg(key, nested);
} else {
part = key;
}
}
else
{
part = key;
}
if (!part.isEmpty()) {
parts.append(part);
}
if (parts.size() >= maxParts) {
break;
}
}
if (item->childCount() > parts.size()) {
parts.append("...");
}
return parts.join(" ");
};
std::function<void(QTreeWidgetItem*, bool)> applyNodeSummaries = [&](QTreeWidgetItem *item, bool isRoot) {
if (!item) {
return;
}
for (int i = 0; i < item->childCount(); ++i) {
applyNodeSummaries(item->child(i), false);
}
if (item->childCount() == 0) {
return;
}
const QString derivedSummary = computeNodeSummary(item);
if (derivedSummary.isEmpty()) {
return;
}
if (isRoot)
{
if (item->text(1).trimmed().isEmpty()) {
item->setText(1, derivedSummary);
} else {
item->setToolTip(1, item->text(1));
}
}
else if (item->text(1).trimmed().isEmpty())
{
item->setText(1, derivedSummary);
item->setToolTip(1, derivedSummary);
}
};
applyNodeSummaries(root, true);
root->setExpanded(true);
alignTreeViewToLatestEntryLeft(view.treeWidget, root);
};
PipelineView& targetView = ensurePipelineView(pipelineId, pipelineName);
populateTree(targetView);
if (pipelineId != -1)
{
PipelineView& allView = ensurePipelineView(-1, "All");
if (allView.treeWidget != targetView.treeWidget) {
populateTree(allView);
}
}
}
void MeshcoreDemodGUI::showLoRaMessage(const Message& message)
{
const MeshcoreDemodMsg::MsgReportDecodeBytes& msg = (MeshcoreDemodMsg::MsgReportDecodeBytes&) message;
const int pipelineId = msg.getPipelineId();
const QString messageBaseKey = buildPipelineMessageBaseKey(pipelineId, msg.getFrameId(), msg.getMsgTimestamp());
const QString messageKey = allocatePipelineMessageKey(messageBaseKey);
rememberLoRaDechirpSnapshot(msg, messageKey);
const QString pipelineName = msg.getPipelineName().trimmed().isEmpty()
? (pipelineId < 0 ? QString("Main") : QString("P%1").arg(pipelineId))
: msg.getPipelineName();
QByteArray bytes = msg.getBytes();
QString syncWordStr((tr("%1").arg(msg.getSyncWord(), 2, 16, QChar('0'))));
ui->sText->setText(tr("%1").arg(msg.getSingalDb(), 0, 'f', 1));
ui->snrText->setText(tr("%1").arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1));
unsigned int packetLength;
ui->fecParity->setValue(msg.getNbParityBits());
ui->fecParityText->setText(tr("%1").arg(msg.getNbParityBits()));
ui->packetLength->setValue(msg.getPacketSize());
ui->packetLengthText->setText(tr("%1").arg(msg.getPacketSize()));
packetLength = msg.getPacketSize();
QDateTime dt = QDateTime::currentDateTime();
QString dateStr = dt.toString("HH:mm:ss");
if (msg.getEarlyEOM())
{
QString loRaStatus = tr("%1 %2 S:%3 SN:%4 HF:%5 HC:%6 EOM:too early")
.arg(dateStr)
.arg(syncWordStr)
.arg(msg.getSingalDb(), 0, 'f', 1)
.arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1)
.arg(getParityStr(msg.getHeaderParityStatus()))
.arg(msg.getHeaderCRCStatus() ? "ok" : "err");
appendPipelineStatusLine(pipelineId, pipelineName, loRaStatus);
displayLoRaStatus(msg.getHeaderParityStatus(), msg.getHeaderCRCStatus(), (int) MeshcoreDemodSettings::ParityUndefined, true);
ui->payloadCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); // reset payload CRC
}
else
{
QString loRaHeader = tr("%1 %2 S:%3 SN:%4 HF:%5 HC:%6 FEC:%7 CRC:%8")
.arg(dateStr)
.arg(syncWordStr)
.arg(msg.getSingalDb(), 0, 'f', 1)
.arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1)
.arg(getParityStr(msg.getHeaderParityStatus()))
.arg(msg.getHeaderCRCStatus() ? "ok" : "err")
.arg(getParityStr(msg.getPayloadParityStatus()))
.arg(msg.getPayloadCRCStatus() ? "ok" : "err");
appendPipelineStatusLine(pipelineId, pipelineName, loRaHeader);
appendPipelineBytes(pipelineId, pipelineName, bytes);
QByteArray bytesCopy(bytes);
bytesCopy.truncate(packetLength);
bytesCopy.replace('\0', " ");
QString str = QString(bytesCopy.toStdString().c_str());
QString textHeader(tr("%1 (%2)").arg(dateStr).arg(syncWordStr));
appendPipelineLogLine(pipelineId, pipelineName, QString("TXT|%1 %2").arg(textHeader, str));
displayLoRaStatus(msg.getHeaderParityStatus(), msg.getHeaderCRCStatus(), msg.getPayloadParityStatus(), msg.getPayloadCRCStatus());
}
// Always create/update a selectable row per LoRa frame so every frame can
// be selected for dechirp replay, even when higher-layer Meshcore parsing
// does not yield structured fields.
QVector<QPair<QString, QString>> fallbackFields;
const QByteArray payloadBytes = bytes.left(static_cast<int>(packetLength));
const QString payloadHex = QString::fromLatin1(payloadBytes.toHex());
fallbackFields.append(qMakePair(QString("header.via_mqtt"), QString("false")));
fallbackFields.append(qMakePair(QString("data.port_name"), QString("LORA_FRAME")));
fallbackFields.append(qMakePair(QString("decode.frame_id"), QString::number(msg.getFrameId())));
fallbackFields.append(qMakePair(QString("decode.status"), msg.getEarlyEOM() ? QString("early_eom") : (msg.getPayloadCRCStatus() ? QString("ok") : QString("crc_error"))));
fallbackFields.append(qMakePair(QString("decode.sync_word"), QString("0x%1").arg(msg.getSyncWord(), 2, 16, QChar('0'))));
fallbackFields.append(qMakePair(QString("decode.signal_db"), QString::number(msg.getSingalDb(), 'f', 1)));
fallbackFields.append(qMakePair(QString("decode.snr_db"), QString::number(msg.getSingalDb() - msg.getNoiseDb(), 'f', 1)));
fallbackFields.append(qMakePair(QString("decode.header_parity"), getParityStr(msg.getHeaderParityStatus())));
fallbackFields.append(qMakePair(QString("decode.header_crc"), msg.getHeaderCRCStatus() ? QString("ok") : QString("err")));
fallbackFields.append(qMakePair(QString("decode.payload_parity"), getParityStr(msg.getPayloadParityStatus())));
fallbackFields.append(qMakePair(QString("decode.payload_crc"), msg.getPayloadCRCStatus() ? QString("ok") : QString("err")));
fallbackFields.append(qMakePair(QString("decode.early_eom"), msg.getEarlyEOM() ? QString("true") : QString("false")));
fallbackFields.append(qMakePair(QString("decode.decrypted"), QString("false")));
fallbackFields.append(qMakePair(QString("data.payload_len"), QString::number(packetLength)));
fallbackFields.append(qMakePair(QString("data.payload_hex"), payloadHex));
appendPipelineTreeFields(
pipelineId,
pipelineName,
QString("%1 %2").arg(dateStr, pipelineName),
fallbackFields,
messageKey
);
ui->nbSymbolsText->setText(tr("%1").arg(msg.getNbSymbols()));
ui->nbCodewordsText->setText(tr("%1").arg(msg.getNbCodewords()));
}
void MeshcoreDemodGUI::showTextMessage(const Message& message)
{
const MeshcoreDemodMsg::MsgReportDecodeString& msg = (MeshcoreDemodMsg::MsgReportDecodeString&) message;
const int pipelineId = msg.getPipelineId();
const QString pipelineName = msg.getPipelineName().trimmed().isEmpty()
? (pipelineId < 0 ? QString("Main") : QString("P%1").arg(pipelineId))
: msg.getPipelineName();
QDateTime dt = QDateTime::currentDateTime();
QString dateStr = dt.toString("HH:mm:ss");
ui->sText->setText(tr("%1").arg(msg.getSingalDb(), 0, 'f', 1));
ui->snrText->setText(tr("%1").arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1));
QString status = tr("%1 S:%2 SN:%3")
.arg(dateStr)
.arg(msg.getSingalDb(), 0, 'f', 1)
.arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1);
appendPipelineStatusLine(pipelineId, pipelineName, status);
appendPipelineLogLine(pipelineId, pipelineName, QString("TXT|%1").arg(msg.getString()));
if (msg.hasStructuredFields())
{
const QString title = QString("%1 %2").arg(dateStr, pipelineName);
const QString messageBaseKey = buildPipelineMessageBaseKey(pipelineId, msg.getFrameId(), msg.getMsgTimestamp());
QString messageKey = resolvePipelineMessageKey(messageBaseKey);
if (messageKey.isEmpty()) {
messageKey = allocatePipelineMessageKey(messageBaseKey);
}
appendPipelineTreeFields(pipelineId, pipelineName, title, msg.getStructuredFields(), messageKey);
if (!messageKey.isEmpty()) {
consumePipelineMessageKey(messageBaseKey, messageKey);
}
}
}
void MeshcoreDemodGUI::displayText(const QString& text)
{
appendPipelineLogLine(-1, "All", QString("TXT|%1").arg(text));
}
void MeshcoreDemodGUI::displayBytes(const QByteArray& bytes)
{
appendPipelineBytes(-1, "All", bytes);
}
void MeshcoreDemodGUI::displayStatus(const QString& status)
{
appendPipelineStatusLine(-1, "All", status);
qInfo() << "MeshcoreDemodGUI::displayStatus:" << status;
}
QString MeshcoreDemodGUI::getParityStr(int parityStatus)
{
if (parityStatus == (int) MeshcoreDemodSettings::ParityError) {
return "err";
} else if (parityStatus == (int) MeshcoreDemodSettings::ParityCorrected) {
return "fix";
} else if (parityStatus == (int) MeshcoreDemodSettings::ParityOK) {
return "ok";
} else {
return "n/a";
}
}
void MeshcoreDemodGUI::tick()
{
handleMeshAutoLockSourceObservation();
advanceMeshAutoLock();
if (m_deviceUISet && m_deviceUISet->m_deviceAPI)
{
const bool isRemoteTcpInput = (m_deviceUISet->m_deviceAPI->getHardwareId() == "RemoteTCPInput");
if (isRemoteTcpInput)
{
const bool running = (m_deviceUISet->m_deviceAPI->state(m_settings.m_streamIndex) == DeviceAPI::StRunning);
if (running && !m_remoteTcpLastRunningState)
{
m_remoteTcpReconnectAutoApplyPending = true;
m_remoteTcpReconnectAutoApplyWaitTicks = 0;
qInfo() << "MeshcoreDemodGUI::tick: RemoteTCP input running - waiting for first DSP notification to reapply Meshcore profile";
}
if (m_remoteTcpReconnectAutoApplyPending && running)
{
m_remoteTcpReconnectAutoApplyWaitTicks++;
if (m_remoteTcpReconnectAutoApplyWaitTicks >= 20)
{
m_remoteTcpReconnectAutoApplyPending = false;
m_remoteTcpReconnectAutoApplyWaitTicks = 0;
qInfo() << "MeshcoreDemodGUI::tick: RemoteTCP reconnect fallback timeout - reapplying Meshcore profile";
QMetaObject::invokeMethod(this, &MeshcoreDemodGUI::applyMeshcoreProfileFromSelection, Qt::QueuedConnection);
}
}
if (!running)
{
m_remoteTcpReconnectAutoApplyPending = false;
m_remoteTcpReconnectAutoApplyWaitTicks = 0;
}
m_remoteTcpLastRunningState = running;
}
else
{
m_remoteTcpReconnectAutoApplyPending = false;
m_remoteTcpReconnectAutoApplyWaitTicks = 0;
m_remoteTcpLastRunningState = false;
}
}
if (m_tickCount < 10)
{
m_tickCount++;
}
else
{
m_tickCount = 0;
ui->nText->setText(tr("%1").arg(CalcDb::dbPower(m_meshcoreDemod->getCurrentNoiseLevel()), 0, 'f', 1));
ui->channelPower->setText(tr("%1 dB").arg(CalcDb::dbPower(m_meshcoreDemod->getTotalPower()), 0, 'f', 1));
if (m_meshcoreDemod->getDemodActive()) {
ui->mute->setStyleSheet("QToolButton { background-color : green; }");
} else {
ui->mute->setStyleSheet("QToolButton { background:rgb(79,79,79); }");
}
}
}
void MeshcoreDemodGUI::makeUIConnections()
{
QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &MeshcoreDemodGUI::on_deltaFrequency_changed);
QObject::connect(ui->BW, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_BW_valueChanged);
QObject::connect(ui->Spread, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_Spread_valueChanged);
QObject::connect(ui->deBits, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_deBits_valueChanged);
QObject::connect(ui->preambleChirps, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_preambleChirps_valueChanged);
QObject::connect(ui->mute, &QToolButton::toggled, this, &MeshcoreDemodGUI::on_mute_toggled);
QObject::connect(ui->clear, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_clear_clicked);
QObject::connect(ui->eomSquelch, &QDial::valueChanged, this, &MeshcoreDemodGUI::on_eomSquelch_valueChanged);
QObject::connect(ui->messageLength, &QDial::valueChanged, this, &MeshcoreDemodGUI::on_messageLength_valueChanged);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
QObject::connect(ui->udpSend, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_udpSend_stateChanged(static_cast<int>(state)); });
QObject::connect(ui->udpSendJson, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_udpSendJson_stateChanged(static_cast<int>(state)); });
#else
QObject::connect(ui->udpSend, &QCheckBox::stateChanged, this, &MeshcoreDemodGUI::on_udpSend_stateChanged);
QObject::connect(ui->udpSendJson, &QCheckBox::stateChanged, this, &MeshcoreDemodGUI::on_udpSendJson_stateChanged);
#endif
QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &MeshcoreDemodGUI::on_udpAddress_editingFinished);
QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &MeshcoreDemodGUI::on_udpPort_editingFinished);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
QObject::connect(ui->invertRamps, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_invertRamps_stateChanged(static_cast<int>(state)); });
#else
QObject::connect(ui->invertRamps, &QCheckBox::stateChanged, this, &MeshcoreDemodGUI::on_invertRamps_stateChanged);
#endif
}
void MeshcoreDemodGUI::updateAbsoluteCenterFrequency()
{
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
}