/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2023 Edouard Griffiths, F4EXB // // Copyright (C) 2015 John Greb // // Copyright (C) 2020 Kacper Michajłow // // Copyright (C) 2021 Jon Beniston, M7RCE // // (c) 2015 John Greb // // (c) 2020 Edouard Griffiths, F4EXB // // // // 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 . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include #include #include #include "SWGChannelSettings.h" #include "SWGWorkspaceInfo.h" #include "SWGChannelReport.h" #include "SWGMeshtasticDemodReport.h" #include "dsp/dspcommands.h" #include "device/deviceapi.h" #include "util/ax25.h" #include "util/db.h" #include "maincore.h" #include "channel/channelwebapiutils.h" #include "meshtasticdemodmsg.h" #include "meshtasticdemoddecoder.h" #include "meshtasticdemod.h" #include "meshtasticpacket.h" MESSAGE_CLASS_DEFINITION(MeshtasticDemod::MsgConfigureMeshtasticDemod, Message) MESSAGE_CLASS_DEFINITION(MeshtasticDemod::MsgSetExtraPipelineSettings, Message) const char* const MeshtasticDemod::m_channelIdURI = "sdrangel.channel.meshtasticdemod"; const char* const MeshtasticDemod::m_channelId = "MeshtasticDemod"; MeshtasticDemod::MeshtasticDemod(DeviceAPI* deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), m_deviceAPI(deviceAPI), m_running(false), m_spectrumVis(SDR_RX_SCALEF), m_basebandSampleRate(0), m_basebandCenterFrequency(0), m_haveBasebandCenterFrequency(false), m_lastMsgSignalDb(0.0), m_lastMsgNoiseDb(0.0), m_lastMsgSyncWord(0), m_lastMsgPacketLength(0), m_lastMsgNbParityBits(0), m_lastMsgHasCRC(false), m_lastMsgNbSymbols(0), m_lastMsgNbCodewords(0), m_lastMsgEarlyEOM(false), m_lastMsgHeaderCRC(false), m_lastMsgHeaderParityStatus(0), m_lastMsgPayloadCRC(false), m_lastMsgPayloadParityStatus(0), m_udpSink(this, 256) { setObjectName(m_channelId); applySettings(m_settings, true); m_deviceAPI->addChannelSink(this); m_deviceAPI->addChannelSinkAPI(this); m_networkManager = new QNetworkAccessManager(); QObject::connect( this, &ChannelAPI::indexInDeviceSetChanged, this, &MeshtasticDemod::handleIndexInDeviceSetChanged ); start(); } MeshtasticDemod::~MeshtasticDemod() { delete m_networkManager; m_deviceAPI->removeChannelSinkAPI(this); m_deviceAPI->removeChannelSink(this, true); stop(); } void MeshtasticDemod::setDeviceAPI(DeviceAPI *deviceAPI) { if (deviceAPI != m_deviceAPI) { m_deviceAPI->removeChannelSinkAPI(this); m_deviceAPI->removeChannelSink(this, false); m_deviceAPI = deviceAPI; m_deviceAPI->addChannelSink(this); m_deviceAPI->addChannelSinkAPI(this); } } uint32_t MeshtasticDemod::getNumberOfDeviceStreams() const { return m_deviceAPI->getNbSourceStreams(); } void MeshtasticDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool pO) { (void) pO; if (!m_running) { return; } for (PipelineRuntime& runtime : m_pipelines) { if (runtime.basebandSink) { runtime.basebandSink->feed(begin, end); } } } int MeshtasticDemod::findBandwidthIndexForHz(int bandwidthHz) const { if (bandwidthHz <= 0) { return -1; } int exactIndex = -1; int nearestIndex = -1; qint64 nearestDelta = std::numeric_limits::max(); for (int i = 0; i < MeshtasticDemodSettings::nbBandwidths; ++i) { const int bw = MeshtasticDemodSettings::bandwidths[i]; if (bw == bandwidthHz) { exactIndex = i; break; } const qint64 delta = std::abs(static_cast(bw) - static_cast(bandwidthHz)); if (delta < nearestDelta) { nearestDelta = delta; nearestIndex = i; } } return exactIndex >= 0 ? exactIndex : nearestIndex; } MeshtasticDemodSettings MeshtasticDemod::makePipelineSettingsFromMeshRadio( const MeshtasticDemodSettings& baseSettings, const QString& presetName, const modemmeshtastic::TxRadioSettings& meshRadio, qint64 selectedPresetFrequencyHz, bool haveSelectedPresetFrequency ) const { MeshtasticDemodSettings out = baseSettings; out.m_spreadFactor = meshRadio.spreadFactor; out.m_deBits = meshRadio.deBits; out.m_nbParityBits = meshRadio.parityBits; out.m_meshtasticPresetName = presetName; out.m_preambleChirps = meshRadio.preambleChirps; const int bandwidthIndex = findBandwidthIndexForHz(meshRadio.bandwidthHz); if (bandwidthIndex >= 0) { out.m_bandwidthIndex = bandwidthIndex; } if (meshRadio.hasCenterFrequency) { if (m_haveBasebandCenterFrequency) { out.m_inputFrequencyOffset = static_cast(meshRadio.centerFrequencyHz - m_basebandCenterFrequency); } else if (haveSelectedPresetFrequency) { out.m_inputFrequencyOffset = baseSettings.m_inputFrequencyOffset + static_cast(meshRadio.centerFrequencyHz - selectedPresetFrequencyHz); } else { out.m_inputFrequencyOffset = baseSettings.m_inputFrequencyOffset; } } return out; } void MeshtasticDemod::makePipelineConfigFromSettings(int configId, PipelineConfig& config, const MeshtasticDemodSettings& settings) const { // USER preset: all LoRa parameters are user-controlled; skip derivation from the mesh radio table. if (settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0) { config.id = configId; config.name = QString("Cnf_%1").arg(configId); config.presetName = settings.m_meshtasticPresetName; config.settings = settings; return; } const QString region = settings.m_meshtasticRegionCode.trimmed().isEmpty() ? QString("US") : settings.m_meshtasticRegionCode.trimmed(); const int channelNum = std::max(1, settings.m_meshtasticChannelIndex + 1); modemmeshtastic::TxRadioSettings meshRadio; QString error; const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3") .arg(settings.m_meshtasticPresetName.trimmed().isEmpty() ? QString("LONG_FAST") : settings.m_meshtasticPresetName.trimmed().toUpper()) .arg(region) .arg(channelNum); qint64 selectedPresetFrequencyHz = 0; bool haveSelectedPresetFrequency = false; if (modemmeshtastic::Packet::deriveTxRadioSettings(command, meshRadio, error) && meshRadio.hasCenterFrequency) { selectedPresetFrequencyHz = meshRadio.centerFrequencyHz; haveSelectedPresetFrequency = true; } config.id = configId; config.name = QString("Cnf_%1").arg(configId); config.presetName = settings.m_meshtasticPresetName; config.settings = makePipelineSettingsFromMeshRadio( settings, config.presetName, meshRadio, selectedPresetFrequencyHz, haveSelectedPresetFrequency ); } void MeshtasticDemod::applyPipelineRuntimeSettings(PipelineRuntime& runtime, const MeshtasticDemodSettings& settings, bool force) { runtime.settings = settings; if (runtime.decoder) { runtime.decoder->setCodingScheme(MeshtasticDemodSettings::m_codingScheme); runtime.decoder->setNbSymbolBits(settings.m_spreadFactor, settings.m_deBits); runtime.decoder->setLoRaHasHeader(MeshtasticDemodSettings::m_hasHeader); runtime.decoder->setLoRaHasCRC(MeshtasticDemodSettings::m_hasCRC); runtime.decoder->setLoRaParityBits(settings.m_nbParityBits); runtime.decoder->setLoRaPacketLength(settings.m_packetLength); runtime.decoder->setLoRaBandwidth(MeshtasticDemodSettings::bandwidths[settings.m_bandwidthIndex]); } if (runtime.basebandSink) { MeshtasticDemodBaseband::MsgConfigureMeshtasticDemodBaseband *msg = MeshtasticDemodBaseband::MsgConfigureMeshtasticDemodBaseband::create(settings, force); runtime.basebandSink->getInputMessageQueue()->push(msg); } } void MeshtasticDemod::startPipelines(const std::vector& configs) { m_pipelines.clear(); m_pipelines.reserve(configs.size()); for (const PipelineConfig& config : configs) { PipelineRuntime runtime; runtime.id = config.id; runtime.name = config.name; runtime.presetName = config.presetName; runtime.settings = config.settings; runtime.decoderThread = new QThread(); runtime.decoder = new MeshtasticDemodDecoder(); runtime.decoder->setOutputMessageQueue(getInputMessageQueue()); runtime.decoder->setPipelineMetadata(runtime.id, runtime.name, runtime.presetName); runtime.decoder->moveToThread(runtime.decoderThread); QObject::connect(runtime.decoderThread, &QThread::finished, runtime.decoder, &QObject::deleteLater); runtime.decoderThread->start(); runtime.basebandThread = new QThread(); runtime.basebandSink = new MeshtasticDemodBaseband(); if (config.id == 0) { runtime.basebandSink->setSpectrumSink(&m_spectrumVis); } runtime.basebandSink->setDecoderMessageQueue(runtime.decoder->getInputMessageQueue()); runtime.decoder->setHeaderFeedbackMessageQueue(runtime.basebandSink->getInputMessageQueue()); runtime.basebandSink->moveToThread(runtime.basebandThread); QObject::connect(runtime.basebandThread, &QThread::finished, runtime.basebandSink, &QObject::deleteLater); if (m_basebandSampleRate != 0) { runtime.basebandSink->setBasebandSampleRate(m_basebandSampleRate); } runtime.basebandSink->reset(); runtime.basebandSink->setFifoLabel(QString("%1[%2]").arg(m_channelId).arg(config.name)); runtime.basebandThread->start(); applyPipelineRuntimeSettings(runtime, runtime.settings, true); m_pipelines.push_back(runtime); } } void MeshtasticDemod::stopPipelines() { for (PipelineRuntime& runtime : m_pipelines) { if (runtime.basebandThread) { runtime.basebandThread->exit(); runtime.basebandThread->wait(); delete runtime.basebandThread; runtime.basebandThread = nullptr; } if (runtime.decoderThread) { runtime.decoderThread->exit(); runtime.decoderThread->wait(); delete runtime.decoderThread; runtime.decoderThread = nullptr; } runtime.basebandSink = nullptr; runtime.decoder = nullptr; } m_pipelines.clear(); } bool MeshtasticDemod::pipelineLayoutMatches(const std::vector& configs) const { if (configs.size() != m_pipelines.size()) { return false; } for (size_t i = 0; i < configs.size(); ++i) { if ((configs[i].id != m_pipelines[i].id) || (configs[i].presetName != m_pipelines[i].presetName)) { return false; } } return true; } void MeshtasticDemod::syncPipelinesWithSettings(const MeshtasticDemodSettings& settings, bool force) { if (!m_running) { return; } // Rebuild pipeline configs from the new settings so that region/preset/channel // changes are reflected in the single pipeline entry. for (size_t i = 0; i < m_pipelineConfigs.size(); ++i) { makePipelineConfigFromSettings(m_pipelineConfigs[i].id, m_pipelineConfigs[i], settings); } for (size_t i = 0; i < m_pipelineConfigs.size(); ++i) { m_pipelines[i].id = m_pipelineConfigs[i].id; m_pipelines[i].name = m_pipelineConfigs[i].name; m_pipelines[i].presetName = m_pipelineConfigs[i].presetName; if (m_pipelines[i].decoder) { m_pipelines[i].decoder->setPipelineMetadata(m_pipelineConfigs[i].id, m_pipelineConfigs[i].name, m_pipelineConfigs[i].presetName); } applyPipelineRuntimeSettings(m_pipelines[i], m_pipelineConfigs[i].settings, force); } } void MeshtasticDemod::applyExtraPipelineSettings(const QVector& settingsList, bool force) { if (!m_running) { return; } // Remove all secondary configs (indices 1+), keeping only the primary (index 0). while (m_pipelineConfigs.size() > 1) { m_pipelineConfigs.pop_back(); } // Build new secondary configs from the provided settings list. // Settings are used as-is (the GUI has already applied preset derivation and frequency offsets). for (int i = 0; i < settingsList.size(); ++i) { PipelineConfig config; const int configId = i + 1; config.id = configId; config.name = QString("Cnf_%1").arg(configId); config.presetName = settingsList[i].m_meshtasticPresetName; config.settings = settingsList[i]; m_pipelineConfigs.push_back(config); } // Determine if the layout (pipeline count / IDs) has changed. const bool layoutChanged = !pipelineLayoutMatches(m_pipelineConfigs); if (layoutChanged) { // Stop and destroy all secondary runtimes (primary at index 0 stays). for (int i = static_cast(m_pipelines.size()) - 1; i >= 1; --i) { PipelineRuntime& rt = m_pipelines[i]; if (rt.basebandThread) { rt.basebandThread->exit(); rt.basebandThread->wait(); delete rt.basebandThread; rt.basebandThread = nullptr; } if (rt.decoderThread) { rt.decoderThread->exit(); rt.decoderThread->wait(); delete rt.decoderThread; rt.decoderThread = nullptr; } rt.basebandSink = nullptr; rt.decoder = nullptr; } while (m_pipelines.size() > 1) { m_pipelines.pop_back(); } // Start fresh secondary runtimes. for (int i = 1; i < static_cast(m_pipelineConfigs.size()); ++i) { const PipelineConfig& config = m_pipelineConfigs[i]; PipelineRuntime runtime; runtime.id = config.id; runtime.name = config.name; runtime.presetName = config.presetName; runtime.settings = config.settings; runtime.decoderThread = new QThread(); runtime.decoder = new MeshtasticDemodDecoder(); runtime.decoder->setOutputMessageQueue(getInputMessageQueue()); runtime.decoder->setPipelineMetadata(runtime.id, runtime.name, runtime.presetName); runtime.decoder->moveToThread(runtime.decoderThread); QObject::connect(runtime.decoderThread, &QThread::finished, runtime.decoder, &QObject::deleteLater); runtime.decoderThread->start(); runtime.basebandThread = new QThread(); runtime.basebandSink = new MeshtasticDemodBaseband(); // Secondary pipelines (id != 0) do not own the spectrum visualiser. runtime.basebandSink->setDecoderMessageQueue(runtime.decoder->getInputMessageQueue()); runtime.decoder->setHeaderFeedbackMessageQueue(runtime.basebandSink->getInputMessageQueue()); runtime.basebandSink->moveToThread(runtime.basebandThread); QObject::connect(runtime.basebandThread, &QThread::finished, runtime.basebandSink, &QObject::deleteLater); if (m_basebandSampleRate != 0) { runtime.basebandSink->setBasebandSampleRate(m_basebandSampleRate); } runtime.basebandSink->reset(); runtime.basebandSink->setFifoLabel(QString("%1[%2]").arg(m_channelId).arg(config.name)); runtime.basebandThread->start(); applyPipelineRuntimeSettings(runtime, runtime.settings, true); m_pipelines.push_back(runtime); } } else { // Layout unchanged — just update settings for each secondary pipeline. for (int i = 1; i < static_cast(m_pipelineConfigs.size()); ++i) { m_pipelines[i].id = m_pipelineConfigs[i].id; m_pipelines[i].name = m_pipelineConfigs[i].name; m_pipelines[i].presetName = m_pipelineConfigs[i].presetName; if (m_pipelines[i].decoder) { m_pipelines[i].decoder->setPipelineMetadata(m_pipelineConfigs[i].id, m_pipelineConfigs[i].name, m_pipelineConfigs[i].presetName); } applyPipelineRuntimeSettings(m_pipelines[i], m_pipelineConfigs[i].settings, force); } } qDebug() << "MeshtasticDemod::applyExtraPipelineSettings: total pipelines=" << m_pipelines.size(); } void MeshtasticDemod::start() { if (m_running) { return; } qDebug() << "MeshtasticDemod::start"; m_pipelineConfigs.emplace_back(); m_currentPipelineId = 0; makePipelineConfigFromSettings(m_currentPipelineId, m_pipelineConfigs.back(), m_settings); startPipelines(m_pipelineConfigs); SpectrumSettings spectrumSettings = m_spectrumVis.getSettings(); spectrumSettings.m_ssb = true; SpectrumVis::MsgConfigureSpectrumVis *msg = SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false); m_spectrumVis.getInputMessageQueue()->push(msg); m_running = true; } void MeshtasticDemod::stop() { if (!m_running) { return; } qDebug() << "MeshtasticDemod::stop"; m_running = false; stopPipelines(); m_pipelineConfigs.clear(); } namespace { QString syncWordToPacketType(uint8_t syncWord) { switch (syncWord) { case 0x2B: return QStringLiteral("meshtastic"); case 0x12: return QStringLiteral("lorawan"); case 0x43: return QStringLiteral("helium"); case 0x00: return QStringLiteral("unset"); default: return QStringLiteral("custom"); } } QString parityStatusToStr(int status) { switch (status) { case (int) MeshtasticDemodSettings::ParityOK: return QStringLiteral("ok"); case (int) MeshtasticDemodSettings::ParityCorrected: return QStringLiteral("fix"); case (int) MeshtasticDemodSettings::ParityError: return QStringLiteral("err"); default: return QStringLiteral("n/a"); } } QString getMeshField(const modemmeshtastic::DecodeResult& result, const QString& path) { for (const auto& field : result.fields) { if (field.path == path) { return field.value; } } return QString(); } } // namespace QString MeshtasticDemod::buildMeshtasticJsonPacket( const MeshtasticDemodMsg::MsgReportDecodeBytes& msg, const modemmeshtastic::DecodeResult& meshResult) const { QJsonObject root; // Timestamps const QDateTime now = QDateTime::currentDateTime(); root["timestamp"] = now.toString("yyyy-MM-ddTHH:mm:ss.zzz"); root["timestamp_unix"] = now.toMSecsSinceEpoch(); // RF QJsonObject rf; rf["center_freq_hz"] = static_cast( m_basebandCenterFrequency + m_settings.m_inputFrequencyOffset); rf["bandwidth_hz"] = MeshtasticDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; rf["spreading_factor"] = m_settings.m_spreadFactor; rf["signal_db"] = msg.getSingalDb(); rf["noise_db"] = msg.getNoiseDb(); rf["snr_db"] = msg.getSingalDb() - msg.getNoiseDb(); root["rf"] = rf; // LoRa const uint8_t syncWord = static_cast(msg.getSyncWord()); const QString packetType = syncWordToPacketType(syncWord); QJsonObject lora; lora["packet_type"] = packetType; lora["sync_word"] = QString("0x%1").arg(syncWord, 2, 16, QChar('0')); lora["header_fec"] = parityStatusToStr(msg.getHeaderParityStatus()); lora["header_crc"] = msg.getHeaderCRCStatus() ? QStringLiteral("ok") : QStringLiteral("err"); if (msg.getEarlyEOM()) { lora["payload_fec"] = QStringLiteral("n/a"); lora["payload_crc"] = QStringLiteral("n/a"); } else { lora["payload_fec"] = parityStatusToStr(msg.getPayloadParityStatus()); lora["payload_crc"] = msg.getPayloadCRCStatus() ? QStringLiteral("ok") : QStringLiteral("err"); } lora["early_eom"] = msg.getEarlyEOM(); lora["packet_length"] = static_cast(msg.getPacketSize()); lora["nb_symbols"] = static_cast(msg.getNbSymbols()); lora["nb_codewords"] = static_cast(msg.getNbCodewords()); lora["payload_hex"] = QString(msg.getBytes().left( static_cast(msg.getPacketSize())).toHex()); root["lora"] = lora; // Meshtastic section — only for sync word 0x2B if (syncWord == 0x2B) { QJsonObject mesh; const bool headerCrcOk = msg.getHeaderCRCStatus(); const QString channelHash = getMeshField(meshResult, "header.channel_hash"); if (headerCrcOk && !channelHash.isEmpty()) { mesh["channel_hash"] = channelHash; } else { mesh["channel_hash"] = QStringLiteral("unknown"); } const QString packetId = getMeshField(meshResult, "header.id"); if (headerCrcOk && !packetId.isEmpty()) { mesh["packet_id"] = packetId; } else { mesh["packet_id"] = QStringLiteral("unknown"); } const QString hopStart = getMeshField(meshResult, "header.hop_start"); const QString hopLimit = getMeshField(meshResult, "header.hop_limit"); const QString relayNode = getMeshField(meshResult, "header.relay_node"); if (headerCrcOk) { if (!hopStart.isEmpty()) { mesh["hop_start"] = hopStart.toInt(); } else { mesh["hop_start"] = QStringLiteral("unknown"); } if (!hopLimit.isEmpty()) { mesh["hop_limit"] = hopLimit.toInt(); } else { mesh["hop_limit"] = QStringLiteral("unknown"); } if (!hopStart.isEmpty() && !hopLimit.isEmpty()) { mesh["hops_consumed"] = hopStart.toInt() - hopLimit.toInt(); } else { mesh["hops_consumed"] = QStringLiteral("unknown"); } if (!relayNode.isEmpty()) { mesh["relay_node"] = relayNode.toInt(); } else { mesh["relay_node"] = QStringLiteral("unknown"); } } else { mesh["hop_start"] = QStringLiteral("unknown"); mesh["hop_limit"] = QStringLiteral("unknown"); mesh["hops_consumed"] = QStringLiteral("unknown"); mesh["relay_node"] = QStringLiteral("unknown"); } const QString decodePath = getMeshField(meshResult, "decode.path"); const QString keyLabel = getMeshField(meshResult, "decode.key_label"); QString decryption; QString keyLabelOut; if (decodePath == QStringLiteral("plain")) { decryption = QStringLiteral("plaintext"); keyLabelOut = QStringLiteral("no_key"); mesh["hash_matching_index"] = QStringLiteral("none"); } else if (decodePath == QStringLiteral("aes_ctr_be")) { decryption = QStringLiteral("decrypted"); keyLabelOut = keyLabel.isEmpty() ? QStringLiteral("unknown_key") : keyLabel; mesh["hash_matching_index"] = 0; } else { decryption = QStringLiteral("not_decrypted"); keyLabelOut = QStringLiteral("unknown_key"); mesh["hash_matching_index"] = QStringLiteral("none"); } mesh["decryption"] = decryption; mesh["key_label"] = keyLabelOut; mesh["parsed"] = meshResult.dataDecoded; if (meshResult.dataDecoded) { mesh["channel_type"] = m_settings.m_meshtasticPresetName; QJsonObject fields; for (const auto& field : meshResult.fields) { if (field.path.startsWith(QStringLiteral("decode."))) { continue; } fields[field.path] = field.value; } mesh["fields"] = fields; } root["meshtastic"] = mesh; } return QString(QJsonDocument(root).toJson(QJsonDocument::Indented)); } bool MeshtasticDemod::handleMessage(const Message& cmd) { if (MsgConfigureMeshtasticDemod::match(cmd)) { qDebug() << "MeshtasticDemod::handleMessage: MsgConfigureMeshtasticDemod"; MsgConfigureMeshtasticDemod& cfg = (MsgConfigureMeshtasticDemod&) cmd; MeshtasticDemodSettings settings = cfg.getSettings(); applySettings(settings, cfg.getForce()); return true; } else if (MsgSetExtraPipelineSettings::match(cmd)) { qDebug() << "MeshtasticDemod::handleMessage: MsgSetExtraPipelineSettings"; const auto& msg = static_cast(cmd); applyExtraPipelineSettings(msg.getSettingsList(), false); return true; } else if (MeshtasticDemodMsg::MsgReportDecodeBytes::match(cmd)) { qDebug() << "MeshtasticDemod::handleMessage: MsgReportDecodeBytes"; MeshtasticDemodMsg::MsgReportDecodeBytes& msg = (MeshtasticDemodMsg::MsgReportDecodeBytes&) cmd; m_lastMsgSignalDb = msg.getSingalDb(); m_lastMsgNoiseDb = msg.getNoiseDb(); m_lastMsgSyncWord = msg.getSyncWord(); m_lastMsgTimestamp = msg.getMsgTimestamp(); m_lastMsgBytes = msg.getBytes(); m_lastMsgPacketLength = msg.getPacketSize(); m_lastMsgNbParityBits = msg.getNbParityBits(); m_lastMsgHasCRC = msg.getHasCRC(); m_lastMsgNbSymbols = msg.getNbSymbols(); m_lastMsgNbCodewords = msg.getNbCodewords(); m_lastMsgEarlyEOM = msg.getEarlyEOM(); m_lastMsgHeaderCRC = msg.getHeaderCRCStatus(); m_lastMsgHeaderParityStatus = msg.getHeaderParityStatus(); m_lastMsgPayloadCRC = msg.getPayloadCRCStatus(); m_lastMsgPayloadParityStatus = msg.getPayloadParityStatus(); m_lastMsgPipelineName = msg.getPipelineName(); m_lastFrameType = QStringLiteral("LORA_FRAME"); QByteArray bytesCopy(m_lastMsgBytes); bytesCopy.truncate(m_lastMsgPacketLength); bytesCopy.replace('\0', " "); m_lastMsgString = QString(bytesCopy.toStdString().c_str()); if (m_settings.m_sendViaUDP) { uint8_t *bytes = reinterpret_cast(m_lastMsgBytes.data()); m_udpSink.writeUnbuffered(bytes, m_lastMsgPacketLength); } if (getMessageQueueToGUI()) { getMessageQueueToGUI()->push(new MeshtasticDemodMsg::MsgReportDecodeBytes(msg)); // make a copy } modemmeshtastic::DecodeResult meshResult; if (modemmeshtastic::Packet::decodeFrame(m_lastMsgBytes, meshResult, m_settings.m_meshtasticKeySpecList)) { m_lastMsgString = meshResult.summary; for (const modemmeshtastic::DecodeResult::Field& field : meshResult.fields) { if (field.path == QStringLiteral("data.port_name")) { m_lastFrameType = field.value; break; } } qInfo() << "MeshtasticDemod::handleMessage:" << meshResult.summary; if (meshResult.dataDecoded && getMessageQueueToGUI()) { MeshtasticDemodMsg::MsgReportDecodeString *meshMsg = MeshtasticDemodMsg::MsgReportDecodeString::create(meshResult.summary); meshMsg->setFrameId(msg.getFrameId()); meshMsg->setSyncWord(msg.getSyncWord()); meshMsg->setSignalDb(msg.getSingalDb()); meshMsg->setNoiseDb(msg.getNoiseDb()); meshMsg->setMsgTimestamp(msg.getMsgTimestamp()); meshMsg->setPipelineMetadata(msg.getPipelineId(), msg.getPipelineName(), msg.getPipelinePreset()); QVector> structuredFields; structuredFields.reserve(meshResult.fields.size()); for (const modemmeshtastic::DecodeResult::Field& field : meshResult.fields) { structuredFields.append(qMakePair(field.path, field.value)); } meshMsg->setStructuredFields(structuredFields); getMessageQueueToGUI()->push(meshMsg); } } // Is this an APRS packet? // As per: https://github.com/oe3cjb/TTGO-T-Beam-LoRa-APRS/blob/master/lib/BG_RF95/BG_RF95.cpp // There is a 3 byte header for LoRa APRS packets. Addressing follows in ASCII: srccall>dst: int colonIdx = m_lastMsgBytes.indexOf(':'); int greaterThanIdx = m_lastMsgBytes.indexOf('>'); if ( (m_lastMsgBytes[0] == '<') && (greaterThanIdx != -1) && (colonIdx != -1) && ((m_lastMsgHasCRC && m_lastMsgPayloadCRC) || !m_lastMsgHasCRC) ) { QByteArray packet; // Extract addresses const char *d = m_lastMsgBytes.data(); QString srcString = QString::fromLatin1(d + 3, greaterThanIdx - 3); QString dstString = QString::fromLatin1(d + greaterThanIdx + 1, colonIdx - greaterThanIdx - 1); // Convert to AX.25 format packet.append(AX25Packet::encodeAddress(dstString)); packet.append(AX25Packet::encodeAddress(srcString, 1)); packet.append(3); packet.append(-16); // 0xf0 packet.append(m_lastMsgBytes.mid(colonIdx+1)); if (!m_lastMsgHasCRC) { packet.append((char)0); // dummy crc packet.append((char)0); } // Forward to APRS and other packet features QList packetsPipes; MainCore::instance()->getMessagePipes().getMessagePipes(this, "packets", packetsPipes); for (const auto& pipe : packetsPipes) { MessageQueue *messageQueue = qobject_cast(pipe->m_element); MainCore::MsgPacket *msg = MainCore::MsgPacket::create(this, packet, QDateTime::currentDateTime()); messageQueue->push(msg); } } if (m_settings.m_sendJsonViaUDP) { const QString json = buildMeshtasticJsonPacket(msg, meshResult); const QByteArray jsonBytes = json.toUtf8(); m_udpSink.writeUnbuffered( reinterpret_cast(jsonBytes.constData()), jsonBytes.size()); } return true; } else if (MeshtasticDemodMsg::MsgReportDecodeString::match(cmd)) { qDebug() << "MeshtasticDemod::handleMessage: MsgReportDecodeString"; MeshtasticDemodMsg::MsgReportDecodeString& msg = (MeshtasticDemodMsg::MsgReportDecodeString&) cmd; m_lastMsgSignalDb = msg.getSingalDb(); m_lastMsgNoiseDb = msg.getNoiseDb(); m_lastMsgSyncWord = msg.getSyncWord(); m_lastMsgTimestamp = msg.getMsgTimestamp(); m_lastMsgString = msg.getString(); if (m_settings.m_sendViaUDP) { const QByteArray& byteArray = m_lastMsgString.toUtf8(); const uint8_t *bytes = reinterpret_cast(byteArray.data()); m_udpSink.writeUnbuffered(bytes, byteArray.size()); } if (getMessageQueueToGUI()) { getMessageQueueToGUI()->push(new MeshtasticDemodMsg::MsgReportDecodeString(msg)); // make a copy } return true; } else if (DSPSignalNotification::match(cmd)) { DSPSignalNotification& notif = (DSPSignalNotification&) cmd; m_basebandSampleRate = notif.getSampleRate(); m_basebandCenterFrequency = notif.getCenterFrequency(); m_haveBasebandCenterFrequency = true; qDebug() << "MeshtasticDemod::handleMessage: DSPSignalNotification: m_basebandSampleRate: " << m_basebandSampleRate; // Forward to the sink if (m_running) { for (PipelineRuntime& runtime : m_pipelines) { if (runtime.basebandSink) { DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy runtime.basebandSink->getInputMessageQueue()->push(rep); } } // Frequency-dependent offsets may need update when source center changes. syncPipelinesWithSettings(m_settings, true); } if (getMessageQueueToGUI()) { getMessageQueueToGUI()->push(new DSPSignalNotification(notif)); // make a copy } return true; } else { return false; } } void MeshtasticDemod::setCenterFrequency(qint64 frequency) { MeshtasticDemodSettings settings = m_settings; settings.m_inputFrequencyOffset = frequency; applySettings(settings, false); if (m_guiMessageQueue) // forward to GUI if any { MsgConfigureMeshtasticDemod *msgToGUI = MsgConfigureMeshtasticDemod::create(settings, false); m_guiMessageQueue->push(msgToGUI); } } QByteArray MeshtasticDemod::serialize() const { return m_settings.serialize(); } bool MeshtasticDemod::deserialize(const QByteArray& data) { if (m_settings.deserialize(data)) { MsgConfigureMeshtasticDemod *msg = MsgConfigureMeshtasticDemod::create(m_settings, true); m_inputMessageQueue.push(msg); return true; } else { m_settings.resetToDefaults(); MsgConfigureMeshtasticDemod *msg = MsgConfigureMeshtasticDemod::create(m_settings, true); m_inputMessageQueue.push(msg); return false; } } void MeshtasticDemod::applySettings(MeshtasticDemodSettings settings, bool force) { qDebug() << "MeshtasticDemod::applySettings:" << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset << " m_bandwidthIndex: " << settings.m_bandwidthIndex << " m_spreadFactor: " << settings.m_spreadFactor << " m_deBits: " << settings.m_deBits << " m_codingScheme: " << MeshtasticDemodSettings::m_codingScheme << " m_hasHeader: " << MeshtasticDemodSettings::m_hasHeader << " m_hasCRC: " << MeshtasticDemodSettings::m_hasCRC << " m_nbParityBits: " << settings.m_nbParityBits << " m_packetLength: " << settings.m_packetLength << " m_autoNbSymbolsMax: " << MeshtasticDemodSettings::m_autoNbSymbolsMax << " m_sendViaUDP: " << settings.m_sendViaUDP << " m_sendJsonViaUDP: " << settings.m_sendJsonViaUDP << " m_udpAddress: " << settings.m_udpAddress << " m_udpPort: " << settings.m_udpPort << " m_meshtasticKeySpecList: " << settings.m_meshtasticKeySpecList << " m_decodeActive: " << settings.m_decodeActive << " m_eomSquelchTenths: " << settings.m_eomSquelchTenths << " m_nbSymbolsMax: " << settings.m_nbSymbolsMax << " m_preambleChirps: " << settings.m_preambleChirps << " m_streamIndex: " << settings.m_streamIndex << " m_useReverseAPI: " << settings.m_useReverseAPI << " m_invertRamps: " << settings.m_invertRamps << " m_rgbColor: " << settings.m_rgbColor << " m_title: " << settings.m_title << " m_meshtasticPresetName: " << settings.m_meshtasticPresetName << " force: " << force; QList reverseAPIKeys; if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { reverseAPIKeys.append("inputFrequencyOffset"); } if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) || force) { reverseAPIKeys.append("bandwidthIndex"); DSPSignalNotification *msg = new DSPSignalNotification( MeshtasticDemodSettings::bandwidths[settings.m_bandwidthIndex], 0); m_spectrumVis.getInputMessageQueue()->push(msg); } if ((settings.m_spreadFactor != m_settings.m_spreadFactor) || force) { reverseAPIKeys.append("spreadFactor"); } if ((settings.m_deBits != m_settings.m_deBits) || force) { reverseAPIKeys.append("deBits"); } if ((settings.m_nbParityBits != m_settings.m_nbParityBits) || force) { reverseAPIKeys.append("nbParityBits"); } if ((settings.m_packetLength != m_settings.m_packetLength) || force) { reverseAPIKeys.append("packetLength"); } if ((settings.m_decodeActive != m_settings.m_decodeActive) || force) { reverseAPIKeys.append("decodeActive"); } if ((settings.m_eomSquelchTenths != m_settings.m_eomSquelchTenths) || force) { reverseAPIKeys.append("eomSquelchTenths"); } if ((settings.m_nbSymbolsMax != m_settings.m_nbSymbolsMax) || force) { reverseAPIKeys.append("nbSymbolsMax"); } if ((settings.m_preambleChirps != m_settings.m_preambleChirps) || force) { reverseAPIKeys.append("preambleChirps"); } if ((settings.m_rgbColor != m_settings.m_rgbColor) || force) { reverseAPIKeys.append("rgbColor"); } if ((settings.m_title != m_settings.m_title) || force) { reverseAPIKeys.append("title"); } if ((settings.m_sendViaUDP != m_settings.m_sendViaUDP) || force) { reverseAPIKeys.append("sendViaUDP"); } if ((settings.m_sendJsonViaUDP != m_settings.m_sendJsonViaUDP) || force) { reverseAPIKeys.append("sendJsonViaUDP"); } if ((settings.m_invertRamps != m_settings.m_invertRamps) || force) { reverseAPIKeys.append("invertRamps"); } if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) { reverseAPIKeys.append("udpAddress"); m_udpSink.setAddress(settings.m_udpAddress); } if ((settings.m_udpPort != m_settings.m_udpPort) || force) { reverseAPIKeys.append("udpPort"); m_udpSink.setPort(settings.m_udpPort); } if ((settings.m_meshtasticKeySpecList != m_settings.m_meshtasticKeySpecList) || force) { reverseAPIKeys.append("meshtasticKeySpecList"); } if (m_settings.m_streamIndex != settings.m_streamIndex) { if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only { m_deviceAPI->removeChannelSinkAPI(this); m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex); m_deviceAPI->addChannelSink(this, settings.m_streamIndex); m_deviceAPI->addChannelSinkAPI(this); m_settings.m_streamIndex = settings.m_streamIndex; // make sure ChannelAPI::getStreamIndex() is consistent emit streamIndexChanged(settings.m_streamIndex); } reverseAPIKeys.append("streamIndex"); } if (m_running) { syncPipelinesWithSettings(settings, force); } // Copy LoRa params derived from the preset (bandwidth, spread factor, etc.) back into // settings so that m_settings and the GUI stay in sync with what was actually applied. // Skip for USER preset: those parameters are controlled entirely by the user via the GUI. if (m_running && !m_pipelineConfigs.empty() && settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) != 0) { const MeshtasticDemodSettings& derived = m_pipelineConfigs[0].settings; const bool bwChanged = (settings.m_bandwidthIndex != derived.m_bandwidthIndex); settings.m_spreadFactor = derived.m_spreadFactor; settings.m_deBits = derived.m_deBits; settings.m_nbParityBits = derived.m_nbParityBits; settings.m_preambleChirps = derived.m_preambleChirps; settings.m_bandwidthIndex = derived.m_bandwidthIndex; settings.m_inputFrequencyOffset = derived.m_inputFrequencyOffset; if (bwChanged) { auto *bwMsg = new DSPSignalNotification( MeshtasticDemodSettings::bandwidths[settings.m_bandwidthIndex], 0); m_spectrumVis.getInputMessageQueue()->push(bwMsg); } } if (settings.m_useReverseAPI) { bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); } QList pipes; MainCore::instance()->getMessagePipes().getMessagePipes(this, "settings", pipes); if (pipes.size() > 0) { sendChannelSettings(pipes, reverseAPIKeys, settings, force); } m_settings = settings; // Forward preset-derived settings back to GUI so controls (e.g. BW slider) reflect // the values actually applied. Skip for USER preset: no parameters were derived, so // there is nothing to sync back — and echoing would trigger an infinite apply loop // (GUI apply → demod echo → GUI displaySettings → rebuildMeshtasticChannelOptions // → queued apply → …). const bool isUserPreset = m_settings.m_meshtasticPresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0; if (!isUserPreset && getMessageQueueToGUI()) { MsgConfigureMeshtasticDemod *msgToGUI = MsgConfigureMeshtasticDemod::create(m_settings, false); getMessageQueueToGUI()->push(msgToGUI); } } int MeshtasticDemod::webapiSettingsGet( SWGSDRangel::SWGChannelSettings& response, QString& errorMessage) { (void) errorMessage; response.setMeshtasticDemodSettings(new SWGSDRangel::SWGMeshtasticDemodSettings()); response.getMeshtasticDemodSettings()->init(); webapiFormatChannelSettings(response, m_settings); return 200; } int MeshtasticDemod::webapiWorkspaceGet( SWGSDRangel::SWGWorkspaceInfo& response, QString& errorMessage) { (void) errorMessage; response.setIndex(m_settings.m_workspaceIndex); return 200; } int MeshtasticDemod::webapiSettingsPutPatch( bool force, const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response, QString& errorMessage) { (void) errorMessage; MeshtasticDemodSettings settings = m_settings; webapiUpdateChannelSettings(settings, channelSettingsKeys, response); MsgConfigureMeshtasticDemod *msg = MsgConfigureMeshtasticDemod::create(settings, force); m_inputMessageQueue.push(msg); if (m_guiMessageQueue) // forward to GUI if any { MsgConfigureMeshtasticDemod *msgToGUI = MsgConfigureMeshtasticDemod::create(settings, force); m_guiMessageQueue->push(msgToGUI); } webapiFormatChannelSettings(response, settings); return 200; } void MeshtasticDemod::webapiUpdateChannelSettings( MeshtasticDemodSettings& settings, const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response) { if (channelSettingsKeys.contains("inputFrequencyOffset")) { settings.m_inputFrequencyOffset = response.getMeshtasticDemodSettings()->getInputFrequencyOffset(); } if (channelSettingsKeys.contains("bandwidthIndex")) { settings.m_bandwidthIndex = response.getMeshtasticDemodSettings()->getBandwidthIndex(); } if (channelSettingsKeys.contains("spreadFactor")) { settings.m_spreadFactor = response.getMeshtasticDemodSettings()->getSpreadFactor(); } if (channelSettingsKeys.contains("deBits")) { settings.m_deBits = response.getMeshtasticDemodSettings()->getDeBits(); } if (channelSettingsKeys.contains("decodeActive")) { settings.m_decodeActive = response.getMeshtasticDemodSettings()->getDecodeActive() != 0; } if (channelSettingsKeys.contains("eomSquelchTenths")) { settings.m_eomSquelchTenths = response.getMeshtasticDemodSettings()->getEomSquelchTenths(); } if (channelSettingsKeys.contains("nbSymbolsMax")) { settings.m_nbSymbolsMax = response.getMeshtasticDemodSettings()->getNbSymbolsMax(); } if (channelSettingsKeys.contains("preambleChirps")) { settings.m_preambleChirps = response.getMeshtasticDemodSettings()->getPreambleChirps(); } if (channelSettingsKeys.contains("nbParityBits")) { settings.m_nbParityBits = response.getMeshtasticDemodSettings()->getNbParityBits(); } if (channelSettingsKeys.contains("packetLength")) { settings.m_packetLength = response.getMeshtasticDemodSettings()->getPacketLength(); } if (channelSettingsKeys.contains("sendViaUDP")) { settings.m_sendViaUDP = response.getMeshtasticDemodSettings()->getSendViaUdp() != 0; } if (channelSettingsKeys.contains("sendJsonViaUDP")) { settings.m_sendJsonViaUDP = response.getMeshtasticDemodSettings()->getSendJsonViaUdp() != 0; } if (channelSettingsKeys.contains("udpAddress")) { settings.m_udpAddress = *response.getMeshtasticDemodSettings()->getUdpAddress(); } if (channelSettingsKeys.contains("udpPort")) { uint16_t port = response.getMeshtasticDemodSettings()->getUdpPort(); settings.m_udpPort = port < 1024 ? 1024 : port; } if (channelSettingsKeys.contains("invertRamps")) { settings.m_invertRamps = response.getMeshtasticDemodSettings()->getInvertRamps() != 0; } if (channelSettingsKeys.contains("rgbColor")) { settings.m_rgbColor = response.getMeshtasticDemodSettings()->getRgbColor(); } if (channelSettingsKeys.contains("title")) { settings.m_title = *response.getMeshtasticDemodSettings()->getTitle(); } if (channelSettingsKeys.contains("streamIndex")) { settings.m_streamIndex = response.getMeshtasticDemodSettings()->getStreamIndex(); } if (channelSettingsKeys.contains("useReverseAPI")) { settings.m_useReverseAPI = response.getMeshtasticDemodSettings()->getUseReverseApi() != 0; } if (channelSettingsKeys.contains("reverseAPIAddress")) { settings.m_reverseAPIAddress = *response.getMeshtasticDemodSettings()->getReverseApiAddress(); } if (channelSettingsKeys.contains("reverseAPIPort")) { settings.m_reverseAPIPort = response.getMeshtasticDemodSettings()->getReverseApiPort(); } if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { settings.m_reverseAPIDeviceIndex = response.getMeshtasticDemodSettings()->getReverseApiDeviceIndex(); } if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { settings.m_reverseAPIChannelIndex = response.getMeshtasticDemodSettings()->getReverseApiChannelIndex(); } if (settings.m_spectrumGUI && channelSettingsKeys.contains("spectrumConfig")) { settings.m_spectrumGUI->updateFrom(channelSettingsKeys, response.getMeshtasticDemodSettings()->getSpectrumConfig()); } if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getMeshtasticDemodSettings()->getChannelMarker()); } if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { settings.m_rollupState->updateFrom(channelSettingsKeys, response.getMeshtasticDemodSettings()->getRollupState()); } } int MeshtasticDemod::webapiReportGet( SWGSDRangel::SWGChannelReport& response, QString& errorMessage) { (void) errorMessage; response.setMeshtasticDemodReport(new SWGSDRangel::SWGMeshtasticDemodReport()); response.getMeshtasticDemodReport()->init(); webapiFormatChannelReport(response); return 200; } void MeshtasticDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const MeshtasticDemodSettings& settings) { response.getMeshtasticDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); response.getMeshtasticDemodSettings()->setBandwidthIndex(settings.m_bandwidthIndex); response.getMeshtasticDemodSettings()->setSpreadFactor(settings.m_spreadFactor); response.getMeshtasticDemodSettings()->setDeBits(settings.m_deBits); response.getMeshtasticDemodSettings()->setDecodeActive(settings.m_decodeActive ? 1 : 0); response.getMeshtasticDemodSettings()->setEomSquelchTenths(settings.m_eomSquelchTenths); response.getMeshtasticDemodSettings()->setNbSymbolsMax(settings.m_nbSymbolsMax); response.getMeshtasticDemodSettings()->setPreambleChirps(settings.m_preambleChirps); response.getMeshtasticDemodSettings()->setNbParityBits(settings.m_nbParityBits); response.getMeshtasticDemodSettings()->setPacketLength(settings.m_packetLength); response.getMeshtasticDemodSettings()->setSendViaUdp(settings.m_sendViaUDP ? 1 : 0); response.getMeshtasticDemodSettings()->setSendJsonViaUdp(settings.m_sendJsonViaUDP ? 1 : 0); response.getMeshtasticDemodSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0); if (response.getMeshtasticDemodSettings()->getUdpAddress()) { *response.getMeshtasticDemodSettings()->getUdpAddress() = settings.m_udpAddress; } else { response.getMeshtasticDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); } response.getMeshtasticDemodSettings()->setUdpPort(settings.m_udpPort); response.getMeshtasticDemodSettings()->setRgbColor(settings.m_rgbColor); if (response.getMeshtasticDemodSettings()->getTitle()) { *response.getMeshtasticDemodSettings()->getTitle() = settings.m_title; } else { response.getMeshtasticDemodSettings()->setTitle(new QString(settings.m_title)); } response.getMeshtasticDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); if (response.getMeshtasticDemodSettings()->getReverseApiAddress()) { *response.getMeshtasticDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; } else { response.getMeshtasticDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); } response.getMeshtasticDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); response.getMeshtasticDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); response.getMeshtasticDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); if (settings.m_spectrumGUI) { if (response.getMeshtasticDemodSettings()->getSpectrumConfig()) { settings.m_spectrumGUI->formatTo(response.getMeshtasticDemodSettings()->getSpectrumConfig()); } else { SWGSDRangel::SWGGLSpectrum *swgGLSpectrum = new SWGSDRangel::SWGGLSpectrum(); settings.m_spectrumGUI->formatTo(swgGLSpectrum); response.getMeshtasticDemodSettings()->setSpectrumConfig(swgGLSpectrum); } } if (settings.m_channelMarker) { if (response.getMeshtasticDemodSettings()->getChannelMarker()) { settings.m_channelMarker->formatTo(response.getMeshtasticDemodSettings()->getChannelMarker()); } else { SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); settings.m_channelMarker->formatTo(swgChannelMarker); response.getMeshtasticDemodSettings()->setChannelMarker(swgChannelMarker); } } if (settings.m_rollupState) { if (response.getMeshtasticDemodSettings()->getRollupState()) { settings.m_rollupState->formatTo(response.getMeshtasticDemodSettings()->getRollupState()); } else { SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); settings.m_rollupState->formatTo(swgRollupState); response.getMeshtasticDemodSettings()->setRollupState(swgRollupState); } } } void MeshtasticDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { if (m_running && !m_pipelines.empty() && m_pipelines[0].basebandSink) { response.getMeshtasticDemodReport()->setChannelSampleRate(m_pipelines[0].basebandSink->getChannelSampleRate()); } response.getMeshtasticDemodReport()->setChannelPowerDb(CalcDb::dbPower(getTotalPower())); response.getMeshtasticDemodReport()->setSignalPowerDb(m_lastMsgSignalDb); response.getMeshtasticDemodReport()->setNoisePowerDb(CalcDb::dbPower(getCurrentNoiseLevel())); response.getMeshtasticDemodReport()->setSnrPowerDb(m_lastMsgSignalDb - m_lastMsgNoiseDb); response.getMeshtasticDemodReport()->setNbParityBits(m_lastMsgNbParityBits); response.getMeshtasticDemodReport()->setPacketLength(m_lastMsgPacketLength); response.getMeshtasticDemodReport()->setNbSymbols(m_lastMsgNbSymbols); response.getMeshtasticDemodReport()->setNbCodewords(m_lastMsgNbCodewords); response.getMeshtasticDemodReport()->setHeaderParityStatus(m_lastMsgHeaderParityStatus); response.getMeshtasticDemodReport()->setHeaderCrcStatus(m_lastMsgHeaderCRC); response.getMeshtasticDemodReport()->setPayloadParityStatus(m_lastMsgPayloadParityStatus); response.getMeshtasticDemodReport()->setPayloadCrcStatus(m_lastMsgPayloadCRC); response.getMeshtasticDemodReport()->setMessageTimestamp(new QString(m_lastMsgTimestamp)); response.getMeshtasticDemodReport()->setMessageString(new QString(m_lastMsgString)); response.getMeshtasticDemodReport()->setFrameType(new QString(m_lastFrameType)); response.getMeshtasticDemodReport()->setChannelType(new QString(m_lastMsgPipelineName)); response.getMeshtasticDemodReport()->setDecoding(getDemodActive() ? 1 : 0); response.getMeshtasticDemodReport()->setMessageBytes(new QList); QList *bytesStr = response.getMeshtasticDemodReport()->getMessageBytes(); for (QByteArray::const_iterator it = m_lastMsgBytes.begin(); it != m_lastMsgBytes.end(); ++it) { unsigned char b = *it; bytesStr->push_back(new QString(tr("%1").arg(b, 2, 16, QChar('0')))); } } void MeshtasticDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const MeshtasticDemodSettings& settings, bool force) { SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); const QUrl channelSettingsURL = ChannelWebAPIUtils::buildChannelSettingsURL( settings.m_reverseAPIAddress, settings.m_reverseAPIPort, settings.m_reverseAPIDeviceIndex, settings.m_reverseAPIChannelIndex); m_networkRequest.setUrl(channelSettingsURL); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); buffer->setParent(reply); delete swgChannelSettings; } void MeshtasticDemod::sendChannelSettings( const QList& pipes, QList& channelSettingsKeys, const MeshtasticDemodSettings& settings, bool force) { for (const auto& pipe : pipes) { MessageQueue *messageQueue = qobject_cast(pipe->m_element); if (messageQueue) { SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); MainCore::MsgChannelSettings *msg = MainCore::MsgChannelSettings::create( this, channelSettingsKeys, swgChannelSettings, force ); messageQueue->push(msg); } } } void MeshtasticDemod::webapiFormatChannelSettings( QList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings *swgChannelSettings, const MeshtasticDemodSettings& settings, bool force ) { swgChannelSettings->setDirection(0); // Single sink (Rx) swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); swgChannelSettings->setChannelType(new QString(m_channelId)); swgChannelSettings->setMeshtasticDemodSettings(new SWGSDRangel::SWGMeshtasticDemodSettings()); SWGSDRangel::SWGMeshtasticDemodSettings *swgMeshtasticDemodSettings = swgChannelSettings->getMeshtasticDemodSettings(); // transfer data that has been modified. When force is on transfer all data except reverse API data if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { swgMeshtasticDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); } if (channelSettingsKeys.contains("bandwidthIndex") || force) { swgMeshtasticDemodSettings->setBandwidthIndex(settings.m_bandwidthIndex); } if (channelSettingsKeys.contains("spreadFactor") || force) { swgMeshtasticDemodSettings->setSpreadFactor(settings.m_spreadFactor); } if (channelSettingsKeys.contains("deBits") || force) { swgMeshtasticDemodSettings->setDeBits(settings.m_deBits); } if (channelSettingsKeys.contains("decodeActive") || force) { swgMeshtasticDemodSettings->setDecodeActive(settings.m_decodeActive ? 1 : 0); } if (channelSettingsKeys.contains("eomSquelchTenths") || force) { swgMeshtasticDemodSettings->setEomSquelchTenths(settings.m_eomSquelchTenths); } if (channelSettingsKeys.contains("nbSymbolsMax") || force) { swgMeshtasticDemodSettings->setNbSymbolsMax(settings.m_nbSymbolsMax); } if (channelSettingsKeys.contains("preambleChirps") || force) { swgMeshtasticDemodSettings->setPreambleChirps(settings.m_preambleChirps); } if (channelSettingsKeys.contains("nbParityBits") || force) { swgMeshtasticDemodSettings->setNbParityBits(settings.m_nbParityBits); } if (channelSettingsKeys.contains("packetLength") || force) { swgMeshtasticDemodSettings->setPacketLength(settings.m_packetLength); } if (channelSettingsKeys.contains("sendViaUDP") || force) { swgMeshtasticDemodSettings->setSendViaUdp(settings.m_sendViaUDP ? 1 : 0); } if (channelSettingsKeys.contains("sendJsonViaUDP") || force) { swgMeshtasticDemodSettings->setSendJsonViaUdp(settings.m_sendJsonViaUDP ? 1 : 0); } if (channelSettingsKeys.contains("udpAddress") || force) { swgMeshtasticDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); } if (channelSettingsKeys.contains("udpPort") || force) { swgMeshtasticDemodSettings->setUdpPort(settings.m_udpPort); } if (channelSettingsKeys.contains("invertRamps") || force) { swgMeshtasticDemodSettings->setInvertRamps(settings.m_invertRamps ? 1 : 0); } if (channelSettingsKeys.contains("rgbColor") || force) { swgMeshtasticDemodSettings->setRgbColor(settings.m_rgbColor); } if (channelSettingsKeys.contains("title") || force) { swgMeshtasticDemodSettings->setTitle(new QString(settings.m_title)); } if (settings.m_spectrumGUI && (channelSettingsKeys.contains("spectrumConfig") || force)) { SWGSDRangel::SWGGLSpectrum *swgGLSpectrum = new SWGSDRangel::SWGGLSpectrum(); settings.m_spectrumGUI->formatTo(swgGLSpectrum); swgMeshtasticDemodSettings->setSpectrumConfig(swgGLSpectrum); } if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) { SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); settings.m_channelMarker->formatTo(swgChannelMarker); swgMeshtasticDemodSettings->setChannelMarker(swgChannelMarker); } if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) { SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); settings.m_rollupState->formatTo(swgRollupState); swgMeshtasticDemodSettings->setRollupState(swgRollupState); } } void MeshtasticDemod::networkManagerFinished(QNetworkReply *reply) { QNetworkReply::NetworkError replyError = reply->error(); if (replyError) { qWarning() << "MeshtasticDemod::networkManagerFinished:" << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); } else { QString answer = reply->readAll(); answer.chop(1); // remove last \n qDebug("MeshtasticDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } reply->deleteLater(); } bool MeshtasticDemod::getDemodActive() const { if (!m_running) { return false; } for (const PipelineRuntime& runtime : m_pipelines) { if (runtime.basebandSink && runtime.basebandSink->getDemodActive()) { return true; } } return false; } double MeshtasticDemod::getCurrentNoiseLevel() const { if (!m_running) { return 0.0; } double level = 0.0; for (const PipelineRuntime& runtime : m_pipelines) { if (runtime.basebandSink) { level = std::max(level, runtime.basebandSink->getCurrentNoiseLevel()); } } return level; } double MeshtasticDemod::getTotalPower() const { if (!m_running) { return 0.0; } double level = 0.0; for (const PipelineRuntime& runtime : m_pipelines) { if (runtime.basebandSink) { level = std::max(level, runtime.basebandSink->getTotalPower()); } } return level; } void MeshtasticDemod::handleIndexInDeviceSetChanged(int index) { if (!m_running || (index < 0)) { return; } for (const PipelineRuntime& runtime : m_pipelines) { if (!runtime.basebandSink) { continue; } QString fifoLabel = QString("%1 [%2:%3 %4]") .arg(m_channelId) .arg(m_deviceAPI->getDeviceSetIndex()) .arg(index) .arg(runtime.name); runtime.basebandSink->setFifoLabel(fifoLabel); } }