diff --git a/CMakeLists.txt b/CMakeLists.txt index 51a68dd8b..cd0b0443f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,7 @@ option(ENABLE_CHANNELRX_DEMODM17 "Enable channelrx demodm17 plugin" ON) option(ENABLE_CHANNELRX_FILESINK "Enable channelrx filesink plugin" ON) option(ENABLE_CHANNELRX_DEMODFREEDV "Enable channelrx demodfreedv plugin" ON) option(ENABLE_CHANNELRX_DEMODCHIRPCHAT "Enable channelrx demodchirpchat plugin" ON) +option(ENABLE_CHANNELRX_DEMODMESHTASTIC "Enable channelrx demodmeshtastic plugin" ON) option(ENABLE_CHANNELRX_REMOTESINK "Enable channelrx remotesink plugin" ON) option(ENABLE_CHANNELRX_REMOTETCPSINK "Enable channelrx remotetcpsink plugin" ON) option(ENABLE_CHANNELRX_DEMODSSB "Enable channelrx demodssb plugin" ON) @@ -171,6 +172,7 @@ option(ENABLE_CHANNELTX_MODPACKET "Enable channeltx modpacket plugin" ON) option(ENABLE_CHANNELTX_MODSSB "Enable channeltx modssb plugin" ON) option(ENABLE_CHANNELTX_UDPSOURCE "Enable channeltx udpsource plugin" ON) option(ENABLE_CHANNELTX_MODCHIRPCHAT "Enable channeltx modchirpchat plugin" ON) +option(ENABLE_CHANNELTX_MODMESHTASTIC "Enable channeltx modmeshtastic plugin" ON) option(ENABLE_CHANNELTX_MODWFM "Enable channeltx modwfm plugin" ON) option(ENABLE_CHANNELTX_MODATV "Enable channeltx modatv plugin" ON) option(ENABLE_CHANNELTX_MOD802.15.4 "Enable channeltx mod802.15.4 plugin" ON) diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 59783572c..4307ccf0c 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -85,6 +85,12 @@ else() message(STATUS "Not building demodchirpchat (ENABLE_CHANNELRX_DEMODCHIRPCHAT=${ENABLE_CHANNELRX_DEMODCHIRPCHAT})") endif() +if (ENABLE_CHANNELRX_DEMODMESHTASTIC) + add_subdirectory(demodmeshtastic) +else() + message(STATUS "Not building demodmeshtastic (ENABLE_CHANNELRX_DEMODMESHTASTIC=${ENABLE_CHANNELRX_DEMODMESHTASTIC})") +endif() + if (ENABLE_CHANNELRX_DEMODVOR) add_subdirectory(demodvor) else() diff --git a/plugins/channelrx/demodmeshtastic/CMakeLists.txt b/plugins/channelrx/demodmeshtastic/CMakeLists.txt new file mode 100644 index 000000000..27966dc4e --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/CMakeLists.txt @@ -0,0 +1,90 @@ +project(meshtastic) + +if (FT8_SUPPORT) + set(meshtastic_FT8_LIB ft8) + set(meshtastic_FT8_INCLUDE ${CMAKE_SOURCE_DIR}/ft8) +endif() + +set(meshtastic_SOURCES + meshtasticdemod.cpp + meshtasticdemodsettings.cpp + meshtasticdemodsink.cpp + meshtasticdemodbaseband.cpp + meshtasticplugin.cpp + meshtasticdemoddecoder.cpp + meshtasticdemoddecodertty.cpp + meshtasticdemoddecoderascii.cpp + meshtasticdemoddecoderlora.cpp + meshtasticdemoddecoderft.cpp + meshtasticdemodmsg.cpp + ${CMAKE_SOURCE_DIR}/plugins/meshtasticcommon/meshtasticpacket.cpp +) + +set(meshtastic_HEADERS + meshtasticdemod.h + meshtasticdemodsettings.h + meshtasticdemodsink.h + meshtasticdemodbaseband.h + meshtasticdemoddecoder.h + meshtasticdemoddecodertty.h + meshtasticdemoddecoderascii.h + meshtasticdemoddecoderlora.h + meshtasticdemoddecoderft.h + meshtasticdemodmsg.h + meshtasticplugin.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${meshtastic_FT8_INCLUDE} + ${CMAKE_SOURCE_DIR}/plugins/meshtasticcommon +) + +if(NOT SERVER_MODE) + set(meshtastic_SOURCES + ${meshtastic_SOURCES} + meshtasticdemodgui.cpp + meshtasticdemodgui.ui + ) + set(meshtastic_HEADERS + ${meshtastic_HEADERS} + meshtasticdemodgui.h + ) + set(TARGET_NAME ${PLUGINS_PREFIX}demodmeshtastic) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME ${PLUGINSSRV_PREFIX}demodmeshtasticsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + + +if(NOT Qt6_FOUND) + add_library(${TARGET_NAME} ${meshtastic_SOURCES}) +else() + qt_add_plugin(${TARGET_NAME} CLASS_NAME MeshtasticPlugin ${meshtastic_SOURCES}) +endif() + +if(NOT BUILD_SHARED_LIBS) + set_property(GLOBAL APPEND PROPERTY STATIC_PLUGINS_PROPERTY ${TARGET_NAME}) +endif() + +target_link_libraries(${TARGET_NAME} PRIVATE + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger + ${meshtastic_FT8_LIB} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $/${TARGET_NAME}stripped.pdb CONFIGURATIONS Release DESTINATION ${INSTALL_FOLDER} RENAME ${TARGET_NAME}.pdb ) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp new file mode 100644 index 000000000..9ca1761aa --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp @@ -0,0 +1,1457 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGChannelReport.h" +#include "SWGChirpChatDemodReport.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) + +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 Meshtastic::TxRadioSettings& meshRadio, + qint64 selectedPresetFrequencyHz, + bool haveSelectedPresetFrequency +) const +{ + MeshtasticDemodSettings out = baseSettings; + out.m_codingScheme = MeshtasticDemodSettings::CodingLoRa; + out.m_hasHeader = true; + out.m_hasCRC = true; + 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; +} + +std::vector MeshtasticDemod::buildPipelineConfigs(const MeshtasticDemodSettings& settings) const +{ + std::vector configs; + + if (settings.m_codingScheme != MeshtasticDemodSettings::CodingLoRa) + { + PipelineConfig config; + config.id = 0; + config.name = "Main"; + config.presetName = "MAIN"; + config.settings = settings; + configs.push_back(config); + return configs; + } + + static const std::array kPresetOrder = { + "LONG_FAST", + "LONG_SLOW", + "LONG_MODERATE", + "LONG_TURBO", + "MEDIUM_FAST", + "MEDIUM_SLOW", + "SHORT_FAST", + "SHORT_SLOW", + "SHORT_TURBO" + }; + + const QString selectedPreset = settings.m_meshtasticPresetName.trimmed().isEmpty() + ? QString("LONG_FAST") + : settings.m_meshtasticPresetName.trimmed().toUpper(); + + QStringList orderedPresets; + orderedPresets.append(selectedPreset); + + for (const char *preset : kPresetOrder) + { + const QString p(preset); + + if (!orderedPresets.contains(p)) { + orderedPresets.append(p); + } + } + + const QString region = settings.m_meshtasticRegionCode.trimmed().isEmpty() + ? QString("US") + : settings.m_meshtasticRegionCode.trimmed(); + const int channelNum = std::max(1, settings.m_meshtasticChannelIndex + 1); + + qint64 selectedPresetFrequencyHz = 0; + bool haveSelectedPresetFrequency = false; + { + Meshtastic::TxRadioSettings selectedMeshRadio; + QString error; + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3") + .arg(selectedPreset) + .arg(region) + .arg(channelNum); + + if (Meshtastic::Packet::deriveTxRadioSettings(command, selectedMeshRadio, error) && selectedMeshRadio.hasCenterFrequency) + { + selectedPresetFrequencyHz = selectedMeshRadio.centerFrequencyHz; + haveSelectedPresetFrequency = true; + } + } + + int id = 0; + + for (const QString& presetName : orderedPresets) + { + Meshtastic::TxRadioSettings meshRadio; + QString error; + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3") + .arg(presetName) + .arg(region) + .arg(channelNum); + + if (!Meshtastic::Packet::deriveTxRadioSettings(command, meshRadio, error)) + { + qDebug() << "MeshtasticDemod::buildPipelineConfigs: skip preset" << presetName << ":" << error; + continue; + } + + if (!meshRadio.hasLoRaParams) { + continue; + } + + PipelineConfig config; + config.id = id++; + config.name = presetName; + config.presetName = presetName; + config.settings = makePipelineSettingsFromMeshRadio( + settings, + presetName, + meshRadio, + selectedPresetFrequencyHz, + haveSelectedPresetFrequency + ); + configs.push_back(config); + } + + if (configs.empty()) + { + PipelineConfig config; + config.id = 0; + config.name = "Main"; + config.presetName = "MAIN"; + config.settings = settings; + configs.push_back(config); + } + + return configs; +} + +void MeshtasticDemod::applyPipelineRuntimeSettings(PipelineRuntime& runtime, const MeshtasticDemodSettings& settings, bool force) +{ + runtime.settings = settings; + + if (runtime.decoder) + { + runtime.decoder->setCodingScheme(settings.m_codingScheme); + runtime.decoder->setNbSymbolBits(settings.m_spreadFactor, settings.m_deBits); + runtime.decoder->setLoRaHasHeader(settings.m_hasHeader); + runtime.decoder->setLoRaHasCRC(settings.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; + } + + const std::vector configs = buildPipelineConfigs(settings); + + if (!pipelineLayoutMatches(configs)) + { + stopPipelines(); + startPipelines(configs); + return; + } + + for (size_t i = 0; i < configs.size(); ++i) + { + m_pipelines[i].id = configs[i].id; + m_pipelines[i].name = configs[i].name; + m_pipelines[i].presetName = configs[i].presetName; + + if (m_pipelines[i].decoder) { + m_pipelines[i].decoder->setPipelineMetadata(configs[i].id, configs[i].name, configs[i].presetName); + } + + applyPipelineRuntimeSettings(m_pipelines[i], configs[i].settings, force); + } +} + +void MeshtasticDemod::start() +{ + if (m_running) { + return; + } + + qDebug() << "MeshtasticDemod::start"; + const std::vector configs = buildPipelineConfigs(m_settings); + startPipelines(configs); + + 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(); +} + +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 (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(); + + if (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + { + 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(); + + 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 + } + + Meshtastic::DecodeResult meshResult; + + if (Meshtastic::Packet::decodeFrame(m_lastMsgBytes, meshResult, m_settings.m_meshtasticKeySpecList)) + { + 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 Meshtastic::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); + } + } + + // In explicit-header LoRa mode, frame length is already derived from header + // and may legitimately vary across packets. Auto-clamping nbSymbolsMax to the + // first short frame breaks subsequent longer frames. + if (m_settings.m_autoNbSymbolsMax + && !((m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) && m_settings.m_hasHeader)) + { + MeshtasticDemodSettings settings = m_settings; + settings.m_nbSymbolsMax = m_lastMsgNbSymbols; + applySettings(settings); + + if (getMessageQueueToGUI()) // forward to GUI if any + { + MsgConfigureMeshtasticDemod *msgToGUI = MsgConfigureMeshtasticDemod::create(settings, false); + getMessageQueueToGUI()->push(msgToGUI); + } + } + } + + 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 (MeshtasticDemodMsg::MsgReportDecodeFT::match(cmd)) + { + qDebug() << "MeshtasticDemod::handleMessage: MsgReportDecodeFT"; + MeshtasticDemodMsg::MsgReportDecodeFT& msg = (MeshtasticDemodMsg::MsgReportDecodeFT&) cmd; + m_lastMsgSignalDb = msg.getSingalDb(); + m_lastMsgNoiseDb = msg.getNoiseDb(); + m_lastMsgSyncWord = msg.getSyncWord(); + m_lastMsgTimestamp = msg.getMsgTimestamp(); + m_lastMsgString = msg.getMessage(); // for now we do not handle message components (call1, ...) + int nbSymbolBits = m_settings.m_spreadFactor - m_settings.m_deBits; + m_lastMsgNbSymbols = (174 / nbSymbolBits) + ((174 % nbSymbolBits) == 0 ? 0 : 1); + + if (m_settings.m_autoNbSymbolsMax) + { + MeshtasticDemodSettings settings = m_settings; + settings.m_nbSymbolsMax = m_lastMsgNbSymbols; + applySettings(settings); + + if (getMessageQueueToGUI()) // forward to GUI if any + { + MsgConfigureMeshtasticDemod *msgToGUI = MsgConfigureMeshtasticDemod::create(settings, false); + getMessageQueueToGUI()->push(msgToGUI); + } + } + + 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::MsgReportDecodeFT(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(const 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: " << settings.m_codingScheme + << " m_hasHeader: " << settings.m_hasHeader + << " m_hasCRC: " << settings.m_hasCRC + << " m_nbParityBits: " << settings.m_nbParityBits + << " m_packetLength: " << settings.m_packetLength + << " m_autoNbSymbolsMax: " << settings.m_autoNbSymbolsMax + << " m_sendViaUDP: " << settings.m_sendViaUDP + << " 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_fftWindow: " << settings.m_fftWindow + << " m_invertRamps: " << settings.m_invertRamps + << " m_rgbColor: " << settings.m_rgbColor + << " m_title: " << settings.m_title + << " 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_fftWindow != m_settings.m_fftWindow) || force) { + reverseAPIKeys.append("fftWindow"); + } + + if ((settings.m_codingScheme != m_settings.m_codingScheme) || force) + { + reverseAPIKeys.append("codingScheme"); + } + + if ((settings.m_hasHeader != m_settings.m_hasHeader) || force) + { + reverseAPIKeys.append("hasHeader"); + } + + if ((settings.m_hasCRC != m_settings.m_hasCRC) || force) + { + reverseAPIKeys.append("hasCRC"); + } + + 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_autoNbSymbolsMax != m_settings.m_autoNbSymbolsMax) || force) { + reverseAPIKeys.append("autoNbSymbolsMax"); + } + 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); + } + + 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; +} + +int MeshtasticDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatDemodSettings(new SWGSDRangel::SWGChirpChatDemodSettings()); + response.getChirpChatDemodSettings()->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.getChirpChatDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("bandwidthIndex")) { + settings.m_bandwidthIndex = response.getChirpChatDemodSettings()->getBandwidthIndex(); + } + if (channelSettingsKeys.contains("spreadFactor")) { + settings.m_spreadFactor = response.getChirpChatDemodSettings()->getSpreadFactor(); + } + if (channelSettingsKeys.contains("deBits")) { + settings.m_deBits = response.getChirpChatDemodSettings()->getDeBits(); + } + if (channelSettingsKeys.contains("fftWindow")) { + settings.m_fftWindow = (FFTWindow::Function) response.getChirpChatDemodSettings()->getFftWindow(); + } + if (channelSettingsKeys.contains("codingScheme")) { + settings.m_codingScheme = (MeshtasticDemodSettings::CodingScheme) response.getChirpChatDemodSettings()->getCodingScheme(); + } + if (channelSettingsKeys.contains("decodeActive")) { + settings.m_decodeActive = response.getChirpChatDemodSettings()->getDecodeActive() != 0; + } + if (channelSettingsKeys.contains("eomSquelchTenths")) { + settings.m_eomSquelchTenths = response.getChirpChatDemodSettings()->getEomSquelchTenths(); + } + if (channelSettingsKeys.contains("nbSymbolsMax")) { + settings.m_nbSymbolsMax = response.getChirpChatDemodSettings()->getNbSymbolsMax(); + } + if (channelSettingsKeys.contains("autoNbSymbolsMax")) { + settings.m_autoNbSymbolsMax = response.getChirpChatDemodSettings()->getAutoNbSymbolsMax() != 0; + } + if (channelSettingsKeys.contains("preambleChirps")) { + settings.m_preambleChirps = response.getChirpChatDemodSettings()->getPreambleChirps(); + } + if (channelSettingsKeys.contains("nbParityBits")) { + settings.m_nbParityBits = response.getChirpChatDemodSettings()->getNbParityBits(); + } + if (channelSettingsKeys.contains("packetLength")) { + settings.m_packetLength = response.getChirpChatDemodSettings()->getPacketLength(); + } + if (channelSettingsKeys.contains("hasCRC")) { + settings.m_hasCRC = response.getChirpChatDemodSettings()->getHasCrc() != 0; + } + if (channelSettingsKeys.contains("hasHeader")) { + settings.m_hasHeader = response.getChirpChatDemodSettings()->getHasHeader() != 0; + } + if (channelSettingsKeys.contains("sendViaUDP")) { + settings.m_sendViaUDP = response.getChirpChatDemodSettings()->getSendViaUdp() != 0; + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getChirpChatDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) + { + uint16_t port = response.getChirpChatDemodSettings()->getUdpPort(); + settings.m_udpPort = port < 1024 ? 1024 : port; + } + if (channelSettingsKeys.contains("invertRamps")) { + settings.m_invertRamps = response.getChirpChatDemodSettings()->getInvertRamps() != 0; + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getChirpChatDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getChirpChatDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getChirpChatDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getChirpChatDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getChirpChatDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getChirpChatDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getChirpChatDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getChirpChatDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_spectrumGUI && channelSettingsKeys.contains("spectrumConfig")) { + settings.m_spectrumGUI->updateFrom(channelSettingsKeys, response.getChirpChatDemodSettings()->getSpectrumConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getChirpChatDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getChirpChatDemodSettings()->getRollupState()); + } +} + +int MeshtasticDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatDemodReport(new SWGSDRangel::SWGChirpChatDemodReport()); + response.getChirpChatDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void MeshtasticDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const MeshtasticDemodSettings& settings) +{ + response.getChirpChatDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getChirpChatDemodSettings()->setBandwidthIndex(settings.m_bandwidthIndex); + response.getChirpChatDemodSettings()->setSpreadFactor(settings.m_spreadFactor); + response.getChirpChatDemodSettings()->setDeBits(settings.m_deBits); + response.getChirpChatDemodSettings()->setFftWindow((int) settings.m_fftWindow); + response.getChirpChatDemodSettings()->setCodingScheme((int) settings.m_codingScheme); + response.getChirpChatDemodSettings()->setDecodeActive(settings.m_decodeActive ? 1 : 0); + response.getChirpChatDemodSettings()->setEomSquelchTenths(settings.m_eomSquelchTenths); + response.getChirpChatDemodSettings()->setNbSymbolsMax(settings.m_nbSymbolsMax); + response.getChirpChatDemodSettings()->setAutoNbSymbolsMax(settings.m_autoNbSymbolsMax ? 1 : 0); + response.getChirpChatDemodSettings()->setPreambleChirps(settings.m_preambleChirps); + response.getChirpChatDemodSettings()->setNbParityBits(settings.m_nbParityBits); + response.getChirpChatDemodSettings()->setPacketLength(settings.m_packetLength); + response.getChirpChatDemodSettings()->setHasCrc(settings.m_hasCRC ? 1 : 0); + response.getChirpChatDemodSettings()->setHasHeader(settings.m_hasHeader ? 1 : 0); + response.getChirpChatDemodSettings()->setSendViaUdp(settings.m_sendViaUDP ? 1 : 0); + response.getChirpChatDemodSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0); + + if (response.getChirpChatDemodSettings()->getUdpAddress()) { + *response.getChirpChatDemodSettings()->getUdpAddress() = settings.m_udpAddress; + } else { + response.getChirpChatDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + } + + response.getChirpChatDemodSettings()->setUdpPort(settings.m_udpPort); + response.getChirpChatDemodSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getChirpChatDemodSettings()->getTitle()) { + *response.getChirpChatDemodSettings()->getTitle() = settings.m_title; + } else { + response.getChirpChatDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getChirpChatDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getChirpChatDemodSettings()->getReverseApiAddress()) { + *response.getChirpChatDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getChirpChatDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getChirpChatDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getChirpChatDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getChirpChatDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_spectrumGUI) + { + if (response.getChirpChatDemodSettings()->getSpectrumConfig()) + { + settings.m_spectrumGUI->formatTo(response.getChirpChatDemodSettings()->getSpectrumConfig()); + } + else + { + SWGSDRangel::SWGGLSpectrum *swgGLSpectrum = new SWGSDRangel::SWGGLSpectrum(); + settings.m_spectrumGUI->formatTo(swgGLSpectrum); + response.getChirpChatDemodSettings()->setSpectrumConfig(swgGLSpectrum); + } + } + + if (settings.m_channelMarker) + { + if (response.getChirpChatDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getChirpChatDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getChirpChatDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getChirpChatDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getChirpChatDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getChirpChatDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void MeshtasticDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + if (m_running && !m_pipelines.empty() && m_pipelines[0].basebandSink) { + response.getChirpChatDemodReport()->setChannelSampleRate(m_pipelines[0].basebandSink->getChannelSampleRate()); + } + + response.getChirpChatDemodReport()->setChannelPowerDb(CalcDb::dbPower(getTotalPower())); + response.getChirpChatDemodReport()->setSignalPowerDb(m_lastMsgSignalDb); + response.getChirpChatDemodReport()->setNoisePowerDb(CalcDb::dbPower(getCurrentNoiseLevel())); + response.getChirpChatDemodReport()->setSnrPowerDb(m_lastMsgSignalDb - m_lastMsgNoiseDb); + response.getChirpChatDemodReport()->setHasCrc(m_lastMsgHasCRC); + response.getChirpChatDemodReport()->setNbParityBits(m_lastMsgNbParityBits); + response.getChirpChatDemodReport()->setPacketLength(m_lastMsgPacketLength); + response.getChirpChatDemodReport()->setNbSymbols(m_lastMsgNbSymbols); + response.getChirpChatDemodReport()->setNbCodewords(m_lastMsgNbCodewords); + response.getChirpChatDemodReport()->setHeaderParityStatus(m_lastMsgHeaderParityStatus); + response.getChirpChatDemodReport()->setHeaderCrcStatus(m_lastMsgHeaderCRC); + response.getChirpChatDemodReport()->setPayloadParityStatus(m_lastMsgPayloadParityStatus); + response.getChirpChatDemodReport()->setPayloadCrcStatus(m_lastMsgPayloadCRC); + response.getChirpChatDemodReport()->setMessageTimestamp(new QString(m_lastMsgTimestamp)); + response.getChirpChatDemodReport()->setMessageString(new QString(m_lastMsgString)); + response.getChirpChatDemodReport()->setDecoding(getDemodActive() ? 1 : 0); + + response.getChirpChatDemodReport()->setMessageBytes(new QList); + QList *bytesStr = response.getChirpChatDemodReport()->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->setChirpChatDemodSettings(new SWGSDRangel::SWGChirpChatDemodSettings()); + SWGSDRangel::SWGChirpChatDemodSettings *swgMeshtasticDemodSettings = swgChannelSettings->getChirpChatDemodSettings(); + + // 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("fftWindow") || force) { + swgMeshtasticDemodSettings->setFftWindow((int) settings.m_fftWindow); + } + if (channelSettingsKeys.contains("codingScheme") || force) { + swgMeshtasticDemodSettings->setCodingScheme((int) settings.m_codingScheme); + } + 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("autoNbSymbolsMax") || force) { + swgMeshtasticDemodSettings->setAutoNbSymbolsMax(settings.m_autoNbSymbolsMax ? 1 : 0); + } + 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("hasCRC") || force) { + swgMeshtasticDemodSettings->setHasCrc(settings.m_hasCRC ? 1 : 0); + } + if (channelSettingsKeys.contains("hasHeader") || force) { + swgMeshtasticDemodSettings->setHasHeader(settings.m_hasHeader ? 1 : 0); + } + if (channelSettingsKeys.contains("sendViaUDP") || force) { + swgMeshtasticDemodSettings->setSendViaUdp(settings.m_sendViaUDP ? 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); + } +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemod.h b/plugins/channelrx/demodmeshtastic/meshtasticdemod.h new file mode 100644 index 000000000..33773ea18 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemod.h @@ -0,0 +1,228 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2017, 2019-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2015 John Greb // +// Copyright (C) 2020 Kacper Michajłow // +// (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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMOD_H +#define INCLUDE_MESHTASTICDEMOD_H + +#include + +#include + +#include "dsp/basebandsamplesink.h" +#include "dsp/spectrumvis.h" +#include "channel/channelapi.h" +#include "util/message.h" +#include "util/udpsinkutil.h" + +#include "meshtasticdemodbaseband.h" + +class QNetworkAccessManager; +class QNetworkReply; +class DeviceAPI; +class QThread; +class ObjectPipe; +class MeshtasticDemodDecoder; +namespace Meshtastic { struct TxRadioSettings; } + +class MeshtasticDemod : public BasebandSampleSink, public ChannelAPI { +public: + class MsgConfigureMeshtasticDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshtasticDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshtasticDemod* create(const MeshtasticDemodSettings& settings, bool force) + { + return new MsgConfigureMeshtasticDemod(settings, force); + } + + private: + MeshtasticDemodSettings m_settings; + bool m_force; + + MsgConfigureMeshtasticDemod(const MeshtasticDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + MeshtasticDemod(DeviceAPI* deviceAPI); + virtual ~MeshtasticDemod(); + virtual void destroy() { delete this; } + virtual void setDeviceAPI(DeviceAPI *deviceAPI); + virtual DeviceAPI *getDeviceAPI() { return m_deviceAPI; } + SpectrumVis *getSpectrumVis() { return &m_spectrumVis; } + + using BasebandSampleSink::feed; + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); } + virtual QString getSinkName() { return objectName(); } + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + virtual void setCenterFrequency(qint64 frequency); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + virtual int getStreamIndex() const { return m_settings.m_streamIndex; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return 0; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const MeshtasticDemodSettings& settings); + + static void webapiUpdateChannelSettings( + MeshtasticDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + bool getDemodActive() const; + double getCurrentNoiseLevel() const; + double getTotalPower() const; + + uint32_t getNumberOfDeviceStreams() const; + + static const char* const m_channelIdURI; + static const char* const m_channelId; + +private: + struct PipelineConfig + { + int id = -1; + QString name; + QString presetName; + MeshtasticDemodSettings settings; + }; + + struct PipelineRuntime + { + int id = -1; + QString name; + QString presetName; + MeshtasticDemodSettings settings; + QThread *basebandThread = nullptr; + QThread *decoderThread = nullptr; + MeshtasticDemodBaseband *basebandSink = nullptr; + MeshtasticDemodDecoder *decoder = nullptr; + }; + + DeviceAPI *m_deviceAPI; + std::vector m_pipelines; + bool m_running; + MeshtasticDemodSettings m_settings; + SpectrumVis m_spectrumVis; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_basebandCenterFrequency; + bool m_haveBasebandCenterFrequency; + float m_lastMsgSignalDb; + float m_lastMsgNoiseDb; + int m_lastMsgSyncWord; + int m_lastMsgPacketLength; + int m_lastMsgNbParityBits; + bool m_lastMsgHasCRC; + int m_lastMsgNbSymbols; + int m_lastMsgNbCodewords; + bool m_lastMsgEarlyEOM; + bool m_lastMsgHeaderCRC; + int m_lastMsgHeaderParityStatus; + bool m_lastMsgPayloadCRC; + int m_lastMsgPayloadParityStatus; + QString m_lastMsgTimestamp; + QString m_lastMsgString; + QByteArray m_lastMsgBytes; + UDPSinkUtil m_udpSink; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + virtual bool handleMessage(const Message& cmd); + void applySettings(const MeshtasticDemodSettings& settings, bool force = false); + std::vector buildPipelineConfigs(const MeshtasticDemodSettings& settings) const; + MeshtasticDemodSettings makePipelineSettingsFromMeshRadio( + const MeshtasticDemodSettings& baseSettings, + const QString& presetName, + const Meshtastic::TxRadioSettings& meshRadio, + qint64 selectedPresetFrequencyHz, + bool haveSelectedPresetFrequency + ) const; + int findBandwidthIndexForHz(int bandwidthHz) const; + void startPipelines(const std::vector& configs); + void stopPipelines(); + void applyPipelineRuntimeSettings(PipelineRuntime& runtime, const MeshtasticDemodSettings& settings, bool force); + bool pipelineLayoutMatches(const std::vector& configs) const; + void syncPipelinesWithSettings(const MeshtasticDemodSettings& settings, bool force); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const MeshtasticDemodSettings& settings, bool force); + void sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshtasticDemodSettings& settings, + bool force + ); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshtasticDemodSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); +}; + +#endif // INCLUDE_MESHTASTICDEMOD_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodbaseband.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodbaseband.cpp new file mode 100644 index 000000000..6b6fab18e --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodbaseband.cpp @@ -0,0 +1,190 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "meshtasticdemodbaseband.h" +#include "meshtasticdemodmsg.h" + +MESSAGE_CLASS_DEFINITION(MeshtasticDemodBaseband::MsgConfigureMeshtasticDemodBaseband, Message) + +MeshtasticDemodBaseband::MeshtasticDemodBaseband() : + m_channelizer(&m_sink) +{ + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + + qDebug("MeshtasticDemodBaseband::MeshtasticDemodBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &MeshtasticDemodBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +MeshtasticDemodBaseband::~MeshtasticDemodBaseband() +{ +} + +void MeshtasticDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void MeshtasticDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void MeshtasticDemodBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0)) + { + SampleVector::iterator part1begin; + SampleVector::iterator part1end; + SampleVector::iterator part2begin; + SampleVector::iterator part2end; + + std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end); + + // first part of FIFO data + if (part1begin != part1end) { + m_channelizer.feed(part1begin, part1end); + } + + // second part of FIFO data (used when block wraps around) + if(part2begin != part2end) { + m_channelizer.feed(part2begin, part2end); + } + + m_sampleFifo.readCommit((unsigned int) count); + } +} + +void MeshtasticDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool MeshtasticDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshtasticDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMeshtasticDemodBaseband& cfg = (MsgConfigureMeshtasticDemodBaseband&) cmd; + qDebug() << "MeshtasticDemodBaseband::handleMessage: MsgConfigureMeshtasticDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "MeshtasticDemodBaseband::handleMessage: DSPSignalNotification:" + << " basebandSampleRate:" << notif.getSampleRate() + << " centerFrequency:" << notif.getCenterFrequency(); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer.setBasebandSampleRate(notif.getSampleRate()); + m_sink.setDeviceCenterFrequency(notif.getCenterFrequency()); + m_sink.applyChannelSettings( + m_channelizer.getChannelSampleRate(), + MeshtasticDemodSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer.getChannelFrequencyOffset() + ); + + return true; + } + else if (MeshtasticDemodMsg::MsgLoRaHeaderFeedback::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MeshtasticDemodMsg::MsgLoRaHeaderFeedback& feedback = (MeshtasticDemodMsg::MsgLoRaHeaderFeedback&) cmd; + qDebug("MeshtasticDemodBaseband::handleMessage: header feedback frameId=%u valid=%d expected=%u", + feedback.getFrameId(), feedback.isValid() ? 1 : 0, feedback.getExpectedSymbols()); + m_sink.applyLoRaHeaderFeedback( + feedback.getFrameId(), + feedback.isValid(), + feedback.getHasCRC(), + feedback.getNbParityBits(), + feedback.getPacketLength(), + feedback.getLdro(), + feedback.getExpectedSymbols(), + feedback.getHeaderParityStatus(), + feedback.getHeaderCRCStatus() + ); + return true; + } + else + { + return false; + } +} + +void MeshtasticDemodBaseband::applySettings(const MeshtasticDemodSettings& settings, bool force) +{ + if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) + || (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer.setChannelization( + MeshtasticDemodSettings::bandwidths[settings.m_bandwidthIndex]*MeshtasticDemodSettings::oversampling, + settings.m_inputFrequencyOffset + ); + m_sink.applyChannelSettings( + m_channelizer.getChannelSampleRate(), + MeshtasticDemodSettings::bandwidths[settings.m_bandwidthIndex], + m_channelizer.getChannelFrequencyOffset() + ); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int MeshtasticDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer.getChannelSampleRate(); +} + + +void MeshtasticDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer.setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings( + m_channelizer.getChannelSampleRate(), + MeshtasticDemodSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer.getChannelFrequencyOffset() + ); +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodbaseband.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodbaseband.h new file mode 100644 index 000000000..2ec9809a0 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodbaseband.h @@ -0,0 +1,89 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODBASEBAND_H +#define INCLUDE_MESHTASTICDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/downchannelizer.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "meshtasticdemodsink.h" + +class MeshtasticDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureMeshtasticDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshtasticDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshtasticDemodBaseband* create(const MeshtasticDemodSettings& settings, bool force) + { + return new MsgConfigureMeshtasticDemodBaseband(settings, force); + } + + private: + MeshtasticDemodSettings m_settings; + bool m_force; + + MsgConfigureMeshtasticDemodBaseband(const MeshtasticDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + MeshtasticDemodBaseband(); + ~MeshtasticDemodBaseband(); + void reset(); + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + int getChannelSampleRate() const; + bool getDemodActive() const { return m_sink.getDemodActive(); } + double getCurrentNoiseLevel() const { return m_sink.getCurrentNoiseLevel(); } + double getTotalPower() const { return m_sink.getTotalPower(); } + void setBasebandSampleRate(int sampleRate); + void setDecoderMessageQueue(MessageQueue *messageQueue) { m_sink.setDecoderMessageQueue(messageQueue); } + void setSpectrumSink(BasebandSampleSink* spectrumSink) { m_sink.setSpectrumSink(spectrumSink); } + void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer m_channelizer; + MeshtasticDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MeshtasticDemodSettings m_settings; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void applySettings(const MeshtasticDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_MESHTASTICDEMODBASEBAND_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoder.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoder.cpp new file mode 100644 index 000000000..4d2f5d5c8 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoder.cpp @@ -0,0 +1,504 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (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 "meshtasticdemoddecoder.h" +#include "meshtasticdemoddecodertty.h" +#include "meshtasticdemoddecoderascii.h" +#include "meshtasticdemoddecoderlora.h" +#include "meshtasticdemoddecoderft.h" +#include "meshtasticdemodmsg.h" + +MeshtasticDemodDecoder::MeshtasticDemodDecoder() : + m_codingScheme(MeshtasticDemodSettings::CodingTTY), + m_spreadFactor(0U), + m_deBits(0U), + m_nbSymbolBits(5), + m_nbParityBits(1), + m_hasCRC(true), + m_hasHeader(true), + m_packetLength(0U), + m_loRaBandwidth(250000U), + m_nbSymbols(0U), + m_nbCodewords(0U), + m_earlyEOM(false), + m_headerParityStatus((int) MeshtasticDemodSettings::ParityUndefined), + m_headerCRCStatus(false), + m_payloadParityStatus((int) MeshtasticDemodSettings::ParityUndefined), + m_payloadCRCStatus(false), + m_pipelineId(-1), + m_outputMessageQueue(nullptr), + m_headerFeedbackMessageQueue(nullptr) +{ + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +MeshtasticDemodDecoder::~MeshtasticDemodDecoder() +{} + +void MeshtasticDemodDecoder::setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits) +{ + m_spreadFactor = spreadFactor; + + if (deBits >= spreadFactor) { + m_deBits = m_spreadFactor - 1; + } else { + m_deBits = deBits; + } + + m_nbSymbolBits = m_spreadFactor - m_deBits; +} + +void MeshtasticDemodDecoder::decodeSymbols(const std::vector& symbols, QString& str) +{ + switch(m_codingScheme) + { + case MeshtasticDemodSettings::CodingTTY: + if (m_nbSymbolBits == 5) { + MeshtasticDemodDecoderTTY::decodeSymbols(symbols, str); + } + break; + case MeshtasticDemodSettings::CodingASCII: + if (m_nbSymbolBits == 7) { + MeshtasticDemodDecoderASCII::decodeSymbols(symbols, str); + } + break; + default: + break; + } +} + +void MeshtasticDemodDecoder::decodeSymbols(const std::vector& symbols, QByteArray& bytes) +{ + switch(m_codingScheme) + { + case MeshtasticDemodSettings::CodingLoRa: + if (m_nbSymbolBits >= 5) + { + unsigned int headerNbSymbolBits; + + if (m_hasHeader && (m_spreadFactor > 2U)) { + headerNbSymbolBits = m_spreadFactor - 2U; + } else { + headerNbSymbolBits = m_nbSymbolBits; + } + + MeshtasticDemodDecoderLoRa::decodeBytes( + bytes, + symbols, + m_nbSymbolBits, + headerNbSymbolBits, + m_hasHeader, + m_hasCRC, + m_nbParityBits, + m_packetLength, + m_earlyEOM, + m_headerParityStatus, + m_headerCRCStatus, + m_payloadParityStatus, + m_payloadCRCStatus + ); + + MeshtasticDemodDecoderLoRa::getCodingMetrics( + m_nbSymbolBits, + headerNbSymbolBits, + m_nbParityBits, + m_packetLength, + m_hasHeader, + m_hasCRC, + m_nbSymbols, + m_nbCodewords + ); + } + break; + default: + break; + } +} + +void MeshtasticDemodDecoder::decodeSymbols( //!< For FT coding scheme + const std::vector>& mags, // vector of symbols magnitudes + int nbSymbolBits, //!< number of bits per symbol + std::string& msg, //!< formatted message + std::string& call1, //!< 1st callsign or shorthand + std::string& call2, //!< 2nd callsign + std::string& loc, //!< locator, report or shorthand + bool& reply //!< true if message is a reply report +) +{ + if (m_codingScheme != MeshtasticDemodSettings::CodingFT) { + return; + } + + MeshtasticDemodDecoderFT::decodeSymbols( + mags, + nbSymbolBits, + msg, + call1, + call2, + loc, + reply, + m_payloadParityStatus, + m_payloadCRCStatus + ); +} + +bool MeshtasticDemodDecoder::handleMessage(const Message& cmd) +{ + if (MeshtasticDemodMsg::MsgLoRaHeaderProbe::match(cmd)) + { + MeshtasticDemodMsg::MsgLoRaHeaderProbe& msg = (MeshtasticDemodMsg::MsgLoRaHeaderProbe&) cmd; + const std::vector& symbols = msg.getSymbols(); + + bool hasCRC = msg.getHasCRC(); + unsigned int nbParityBits = m_nbParityBits; + unsigned int packetLength = m_packetLength; + int headerParityStatus = (int) MeshtasticDemodSettings::ParityUndefined; + bool headerCRCStatus = false; + bool ldro = false; + unsigned int expectedSymbols = 0U; + bool valid = false; + + if (symbols.size() >= 8U && msg.getHasHeader()) + { + MeshtasticDemodDecoderLoRa::decodeHeader( + symbols, + msg.getHeaderNbSymbolBits(), + hasCRC, + nbParityBits, + packetLength, + headerParityStatus, + headerCRCStatus + ); + + if (headerCRCStatus && (packetLength > 0U) && (nbParityBits >= 1U) && (nbParityBits <= 4U)) + { + const unsigned int spreadFactor = msg.getSpreadFactor(); + const unsigned int bandwidth = msg.getBandwidth() > 0U ? msg.getBandwidth() : m_loRaBandwidth; + ldro = ((1U << spreadFactor) * 1000.0 / static_cast(std::max(1U, bandwidth))) > 16.0; + const int denom = static_cast(spreadFactor) - (ldro ? 2 : 0); + + if (denom > 0) + { + const int numerator = + 2 * static_cast(packetLength) + - static_cast(spreadFactor) + + 2 + + 5 // explicit header path (!impl_head) + + (hasCRC ? 4 : 0); + const int payloadBlocks = std::max(0, static_cast(std::ceil(static_cast(numerator) / static_cast(denom)))); + expectedSymbols = 8U + static_cast(payloadBlocks) * (4U + nbParityBits); + valid = expectedSymbols >= 8U; + } + } + } + + if (m_headerFeedbackMessageQueue) + { + MeshtasticDemodMsg::MsgLoRaHeaderFeedback *feedback = MeshtasticDemodMsg::MsgLoRaHeaderFeedback::create( + msg.getFrameId(), + valid, + hasCRC, + nbParityBits, + packetLength, + ldro, + expectedSymbols, + headerParityStatus, + headerCRCStatus + ); + m_headerFeedbackMessageQueue->push(feedback); + qDebug("MeshtasticDemodDecoder::handleMessage: header probe frameId=%u valid=%d len=%u CR=%u expected=%u", + msg.getFrameId(), valid ? 1 : 0, packetLength, nbParityBits, expectedSymbols); + } + + return true; + } + else if (MeshtasticDemodMsg::MsgDecodeSymbols::match(cmd)) + { + qDebug("MeshtasticDemodDecoder::handleMessage: MsgDecodeSymbols"); + MeshtasticDemodMsg::MsgDecodeSymbols& msg = (MeshtasticDemodMsg::MsgDecodeSymbols&) cmd; + float msgSignalDb = msg.getSingalDb(); + float msgNoiseDb = msg.getNoiseDb(); + unsigned int msgSyncWord = msg.getSyncWord(); + QDateTime dt = QDateTime::currentDateTime(); + QString msgTimestamp = dt.toString(Qt::ISODateWithMs); + + if (m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + { + QByteArray msgBytes; + const std::vector>& msgMags = msg.getMagnitudes(); + const bool canSoftDecode = !msgMags.empty() + && (msgMags.size() >= msg.getSymbols().size()) + && (m_spreadFactor >= 5U) + && (m_loRaBandwidth > 0U); + + struct LoRaDecodeState + { + QByteArray bytes; + bool hasCRC; + unsigned int nbParityBits; + unsigned int packetLength; + unsigned int nbSymbols; + unsigned int nbCodewords; + bool earlyEOM; + int headerParityStatus; + bool headerCRCStatus; + int payloadParityStatus; + bool payloadCRCStatus; + }; + + auto captureLoRaState = [this](const QByteArray& bytes) -> LoRaDecodeState { + LoRaDecodeState s; + s.bytes = bytes; + s.hasCRC = m_hasCRC; + s.nbParityBits = m_nbParityBits; + s.packetLength = m_packetLength; + s.nbSymbols = m_nbSymbols; + s.nbCodewords = m_nbCodewords; + s.earlyEOM = m_earlyEOM; + s.headerParityStatus = m_headerParityStatus; + s.headerCRCStatus = m_headerCRCStatus; + s.payloadParityStatus = m_payloadParityStatus; + s.payloadCRCStatus = m_payloadCRCStatus; + return s; + }; + + auto restoreLoRaState = [this, &msgBytes](const LoRaDecodeState& s) { + msgBytes = s.bytes; + m_hasCRC = s.hasCRC; + m_nbParityBits = s.nbParityBits; + m_packetLength = s.packetLength; + m_nbSymbols = s.nbSymbols; + m_nbCodewords = s.nbCodewords; + m_earlyEOM = s.earlyEOM; + m_headerParityStatus = s.headerParityStatus; + m_headerCRCStatus = s.headerCRCStatus; + m_payloadParityStatus = s.payloadParityStatus; + m_payloadCRCStatus = s.payloadCRCStatus; + }; + + if (canSoftDecode) + { + unsigned int headerNbSymbolBits; + + if (m_hasHeader && (m_spreadFactor > 2U)) { + headerNbSymbolBits = m_spreadFactor - 2U; + } else { + headerNbSymbolBits = m_nbSymbolBits; + } + + MeshtasticDemodDecoderLoRa::decodeBytesSoft( + msgBytes, + msgMags, + msg.getSymbols(), + m_spreadFactor, + m_loRaBandwidth, + m_nbSymbolBits, + headerNbSymbolBits, + m_hasHeader, + m_hasCRC, + m_nbParityBits, + m_packetLength, + m_earlyEOM, + m_headerParityStatus, + m_headerCRCStatus, + m_payloadParityStatus, + m_payloadCRCStatus + ); + + MeshtasticDemodDecoderLoRa::getCodingMetrics( + m_nbSymbolBits, + headerNbSymbolBits, + m_nbParityBits, + m_packetLength, + m_hasHeader, + m_hasCRC, + m_nbSymbols, + m_nbCodewords + ); + + const LoRaDecodeState softState = captureLoRaState(msgBytes); + + // Soft path is canonical for gr-lora_sdr, but if this approximation misses CRC + // on noisy captures, retry hard decode once and keep whichever path validates. + if (m_hasCRC && !m_payloadCRCStatus) + { + QByteArray hardBytes; + decodeSymbols(msg.getSymbols(), hardBytes); // hard path updates decoder state + const LoRaDecodeState hardState = captureLoRaState(hardBytes); + + if (hardState.payloadCRCStatus) { + restoreLoRaState(hardState); + } else { + restoreLoRaState(softState); + } + } + } + else + { + decodeSymbols(msg.getSymbols(), msgBytes); + } + + if (m_hasCRC && !m_payloadCRCStatus && (m_spreadFactor >= 5U)) + { + const LoRaDecodeState baseState = captureLoRaState(msgBytes); + const unsigned int headerNbSymbolBits = (m_hasHeader && (m_spreadFactor > 2U)) + ? (m_spreadFactor - 2U) + : m_nbSymbolBits; + bool recovered = false; + + for (int delta : {-1, 1}) + { + std::vector shifted = msg.getSymbols(); + + for (size_t i = 0; i < shifted.size(); i++) + { + const bool isHeader = m_hasHeader && (i < 8U); + const unsigned int bits = isHeader ? headerNbSymbolBits : m_nbSymbolBits; + const unsigned int mod = 1U << std::max(1U, bits); + const int s = static_cast(shifted[i]); + const int v = (s + delta) % static_cast(mod); + shifted[i] = static_cast(v < 0 ? (v + static_cast(mod)) : v); + } + + QByteArray shiftedBytes; + decodeSymbols(shifted, shiftedBytes); // hard-path decode with adjusted symbol indices + const LoRaDecodeState shiftedState = captureLoRaState(shiftedBytes); + + if (shiftedState.payloadCRCStatus) + { + restoreLoRaState(shiftedState); + recovered = true; + break; + } + } + + if (!recovered) { + restoreLoRaState(baseState); + } + } + + qDebug( + "MeshtasticDemodDecoder::handleMessage: decode symbols=%zu bytes=%lld earlyEOM=%d hCRC=%d pCRC=%d hParity=%d pParity=%d", + msg.getSymbols().size(), + static_cast(msgBytes.size()), + m_earlyEOM ? 1 : 0, + m_headerCRCStatus ? 1 : 0, + m_payloadCRCStatus ? 1 : 0, + m_headerParityStatus, + m_payloadParityStatus + ); + + if (m_outputMessageQueue) + { + qDebug( + "MeshtasticDemodDecoder::handleMessage: push report ts=%s bytes=%lld pCRC=%d", + qPrintable(msgTimestamp), + static_cast(msgBytes.size()), + m_payloadCRCStatus ? 1 : 0 + ); + MeshtasticDemodMsg::MsgReportDecodeBytes *outputMsg = MeshtasticDemodMsg::MsgReportDecodeBytes::create(msgBytes); + outputMsg->setFrameId(msg.getFrameId()); + outputMsg->setSyncWord(msgSyncWord); + outputMsg->setSignalDb(msgSignalDb); + outputMsg->setNoiseDb(msgNoiseDb); + outputMsg->setMsgTimestamp(msgTimestamp); + outputMsg->setPacketSize(getPacketLength()); + outputMsg->setNbParityBits(getNbParityBits()); + outputMsg->setHasCRC(getHasCRC()); + outputMsg->setNbSymbols(getNbSymbols()); + outputMsg->setNbCodewords(getNbCodewords()); + outputMsg->setEarlyEOM(getEarlyEOM()); + outputMsg->setHeaderParityStatus(getHeaderParityStatus()); + outputMsg->setHeaderCRCStatus(getHeaderCRCStatus()); + outputMsg->setPayloadParityStatus(getPayloadParityStatus()); + outputMsg->setPayloadCRCStatus(getPayloadCRCStatus()); + outputMsg->setPipelineMetadata(m_pipelineId, m_pipelineName, m_pipelinePreset); + outputMsg->setDechirpedSpectrum(msg.getDechirpedSpectrum()); + m_outputMessageQueue->push(outputMsg); + } + } + else if (m_codingScheme == MeshtasticDemodSettings::CodingFT) + { + std::string fmsg, call1, call2, loc; + bool reply; + decodeSymbols( + msg.getMagnitudes(), + m_nbSymbolBits, + fmsg, + call1, + call2, + loc, + reply + ); + + if (m_outputMessageQueue) + { + MeshtasticDemodMsg::MsgReportDecodeFT *outputMsg = MeshtasticDemodMsg::MsgReportDecodeFT::create(); + outputMsg->setSyncWord(msgSyncWord); + outputMsg->setSignalDb(msgSignalDb); + outputMsg->setNoiseDb(msgNoiseDb); + outputMsg->setMsgTimestamp(msgTimestamp); + outputMsg->setMessage(QString(fmsg.c_str())); + outputMsg->setCall1(QString(call1.c_str())); + outputMsg->setCall2(QString(call2.c_str())); + outputMsg->setLoc(QString(loc.c_str())); + outputMsg->setReply(reply); + outputMsg->setPayloadParityStatus(getPayloadParityStatus()); + outputMsg->setPayloadCRCStatus(getPayloadCRCStatus()); + outputMsg->setPipelineMetadata(m_pipelineId, m_pipelineName, m_pipelinePreset); + m_outputMessageQueue->push(outputMsg); + } + } + else + { + QString msgString; + decodeSymbols(msg.getSymbols(), msgString); + + if (m_outputMessageQueue) + { + MeshtasticDemodMsg::MsgReportDecodeString *outputMsg = MeshtasticDemodMsg::MsgReportDecodeString::create(msgString); + outputMsg->setFrameId(msg.getFrameId()); + outputMsg->setSyncWord(msgSyncWord); + outputMsg->setSignalDb(msgSignalDb); + outputMsg->setNoiseDb(msgNoiseDb); + outputMsg->setMsgTimestamp(msgTimestamp); + outputMsg->setPipelineMetadata(m_pipelineId, m_pipelineName, m_pipelinePreset); + m_outputMessageQueue->push(outputMsg); + } + } + + return true; + } + + return false; +} + +void MeshtasticDemodDecoder::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoder.h b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoder.h new file mode 100644 index 000000000..6c2e14810 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoder.h @@ -0,0 +1,106 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2016-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODDECODER_H +#define INCLUDE_MESHTASTICDEMODDECODER_H + +#include + +#include + +#include "util/messagequeue.h" +#include "meshtasticdemodsettings.h" + +class MeshtasticDemodDecoder : public QObject +{ + Q_OBJECT +public: + MeshtasticDemodDecoder(); + ~MeshtasticDemodDecoder(); + + void setCodingScheme(MeshtasticDemodSettings::CodingScheme codingScheme) { m_codingScheme = codingScheme; } + void setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits); + void setLoRaParityBits(unsigned int parityBits) { m_nbParityBits = parityBits; } + void setLoRaHasHeader(bool hasHeader) { m_hasHeader = hasHeader; } + void setLoRaHasCRC(bool hasCRC) { m_hasCRC = hasCRC; } + void setLoRaPacketLength(unsigned int packetLength) { m_packetLength = packetLength; } + void setLoRaBandwidth(unsigned int bandwidth) { m_loRaBandwidth = bandwidth; } + void setPipelineMetadata(int pipelineId, const QString& pipelineName, const QString& pipelinePreset) + { + m_pipelineId = pipelineId; + m_pipelineName = pipelineName; + m_pipelinePreset = pipelinePreset; + } + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setOutputMessageQueue(MessageQueue *messageQueue) { m_outputMessageQueue = messageQueue; } + void setHeaderFeedbackMessageQueue(MessageQueue *messageQueue) { m_headerFeedbackMessageQueue = messageQueue; } + +private: + bool handleMessage(const Message& cmd); + void decodeSymbols(const std::vector& symbols, QString& str); //!< For ASCII and TTY + void decodeSymbols(const std::vector& symbols, QByteArray& bytes); //!< For raw bytes (original LoRa) + void decodeSymbols( //!< For FT coding scheme + const std::vector>& mags, // vector of symbols magnitudes + int nbSymbolBits, //!< number of bits per symbol + std::string& msg, //!< formatted message + std::string& call1, //!< 1st callsign or shorthand + std::string& call2, //!< 2nd callsign + std::string& loc, //!< locator, report or shorthand + bool& reply //!< true if message is a reply report + ); + unsigned int getNbParityBits() const { return m_nbParityBits; } + unsigned int getPacketLength() const { return m_packetLength; } + bool getHasCRC() const { return m_hasCRC; } + unsigned int getNbSymbols() const { return m_nbSymbols; } + unsigned int getNbCodewords() const { return m_nbCodewords; } + bool getEarlyEOM() const { return m_earlyEOM; } + int getHeaderParityStatus() const { return m_headerParityStatus; } + bool getHeaderCRCStatus() const { return m_headerCRCStatus; } + int getPayloadParityStatus() const { return m_payloadParityStatus; } + bool getPayloadCRCStatus() const { return m_payloadCRCStatus; } + + MeshtasticDemodSettings::CodingScheme m_codingScheme; + unsigned int m_spreadFactor; + unsigned int m_deBits; + unsigned int m_nbSymbolBits; + // LoRa attributes + unsigned int m_nbParityBits; //!< 1 to 4 Hamming FEC bits for 4 payload bits + bool m_hasCRC; + bool m_hasHeader; + unsigned int m_packetLength; + unsigned int m_loRaBandwidth; + unsigned int m_nbSymbols; //!< Number of encoded symbols: this is only dependent of nbSymbolBits, nbParityBits, packetLength, hasHeader and hasCRC + unsigned int m_nbCodewords; //!< Number of encoded codewords: this is only dependent of nbSymbolBits, nbParityBits, packetLength, hasHeader and hasCRC + bool m_earlyEOM; + int m_headerParityStatus; + bool m_headerCRCStatus; + int m_payloadParityStatus; + bool m_payloadCRCStatus; + int m_pipelineId; + QString m_pipelineName; + QString m_pipelinePreset; + MessageQueue m_inputMessageQueue; + MessageQueue *m_outputMessageQueue; + MessageQueue *m_headerFeedbackMessageQueue; + +private slots: + void handleInputMessages(); +}; + +#endif // INCLUDE_MESHTASTICDEMODDECODER_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderascii.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderascii.cpp new file mode 100644 index 000000000..e6cc4d1d2 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderascii.cpp @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 "meshtasticdemoddecoderascii.h" + +void MeshtasticDemodDecoderASCII::decodeSymbols(const std::vector& symbols, QString& str) +{ + std::vector::const_iterator it = symbols.begin(); + QByteArray bytes; + + for (; it != symbols.end(); ++it) { + bytes.push_back(*it & 0x7F); + } + + str = QString(bytes.toStdString().c_str()); +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderascii.h b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderascii.h new file mode 100644 index 000000000..92d459fd1 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderascii.h @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODDECODERASCII_H +#define INCLUDE_MESHTASTICDEMODDECODERASCII_H + +#include +#include + +class MeshtasticDemodDecoderASCII +{ +public: + static void decodeSymbols(const std::vector& symbols, QString& str); +}; + +#endif // INCLUDE_MESHTASTICDEMODDECODERASCII_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderft.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderft.cpp new file mode 100644 index 000000000..b8c7d9e82 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderft.cpp @@ -0,0 +1,187 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "meshtasticdemodsettings.h" +#include "meshtasticdemoddecoderft.h" + +#ifndef HAS_FT8 +void MeshtasticDemodDecoderFT::decodeSymbols( + const std::vector>& mags, // vector of symbols magnitudes + int nbSymbolBits, //!< number of bits per symbol + QString& msg, //!< formatted message + QString& call1, //!< 1st callsign or shorthand + QString& call2, //!< 2nd callsign + QString& loc, //!< locator, report or shorthand + bool& reply //!< true if message is a reply report +) +{ + qWarning("MeshtasticDemodDecoderFT::decodeSymbols: not implemented"); +} +#else + +#include "ft8.h" +#include "packing.h" + +void MeshtasticDemodDecoderFT::decodeSymbols( + const std::vector>& mags, // vector of symbols magnitudes + int nbSymbolBits, //!< number of bits per symbol + std::string& msg, //!< formatted message + std::string& call1, //!< 1st callsign or shorthand + std::string& call2, //!< 2nd callsign + std::string& loc, //!< locator, report or shorthand + bool& reply, //!< true if message is a reply report + int& payloadParityStatus, + bool& payloadCRCStatus +) +{ + if (mags.size()*nbSymbolBits < 174) + { + qWarning("MeshtasticDemodDecoderFT::decodeSymbols: insufficient number of symbols for FT payload"); + return; + } + + FT8::FT8Params params; + int r174[174]; + std::string comments; + payloadParityStatus = (int) MeshtasticDemodSettings::ParityOK; + payloadCRCStatus = false; + std::vector> magsp = mags; + + qDebug("MeshtasticDemodDecoderFT::decodeSymbols: try decode with symbol shift 0"); + int res = decodeWithShift(params, magsp, nbSymbolBits, r174, comments); + + if (res == 0) + { + std::vector> magsn = mags; + int shiftcount = 0; + + while ((res == 0) && (shiftcount < 7)) + { + qDebug("MeshtasticDemodDecoderFT::decodeSymbols: try decode with symbol shift %d", shiftcount + 1); + res = decodeWithShift(params, magsp, nbSymbolBits, r174, comments, 1); + + if (res == 0) + { + qDebug("MeshtasticDemodDecoderFT::decodeSymbols: try decode with symbol shift -%d", shiftcount + 1); + res = decodeWithShift(params, magsn, nbSymbolBits, r174, comments, -1); + } + + shiftcount++; + } + } + + if (res == 0) + { + if (comments == "LDPC fail") + { + qWarning("MeshtasticDemodDecoderFT::decodeSymbols: LDPC failed"); + payloadParityStatus = (int) MeshtasticDemodSettings::ParityError; + } + else if (comments == "OSD fail") + { + qWarning("MeshtasticDemodDecoderFT::decodeSymbols: OSD failed"); + payloadParityStatus = (int) MeshtasticDemodSettings::ParityError; + } + else if (comments == "CRC fail") + { + qWarning("MeshtasticDemodDecoderFT::decodeSymbols: CRC failed"); + } + else + { + qWarning("MeshtasticDemodDecoderFT::decodeSymbols: decode failed for unknown reason"); + payloadParityStatus = (int) MeshtasticDemodSettings::ParityUndefined; + } + + return; + } + + payloadCRCStatus = true; + FT8::Packing packing; + std::string msgType; + msg = packing.unpack(r174, call1, call2, loc, msgType); + reply = false; + + if ((msgType == "0.3") || (msgType == "0.3")) { + reply = r174[56] != 0; + } + if ((msgType == "1") || (msgType == "2")) { + reply = r174[58] != 0; + } + if ((msgType == "3")) { + reply = r174[57] != 0; + } + if ((msgType == "5")) { + reply = r174[34] != 0; + } +} + +int MeshtasticDemodDecoderFT::decodeWithShift( + FT8::FT8Params& params, + std::vector>& mags, + int nbSymbolBits, + int *r174, + std::string& comments, + int shift +) +{ + if (shift > 0) + { + for (unsigned int si = 0; si < mags.size(); si++) + { + for (int bini = (1< 0; bini--) + { + float x = mags[si][bini - 1]; + mags[si][bini - 1] = mags[si][bini]; + mags[si][bini] = x; + } + } + } + + if (shift < 0) + { + for (unsigned int si = 0; si < mags.size(); si++) + { + for (int bini = 0; bini < (1<0 for 0, <0 for 1) + std::fill(lls, lls+mags.size()*nbSymbolBits, 0.0); + FT8::FT8::soft_decode_mags(params, mags, nbSymbolBits, lls); + deinterleave174(lls); + int ret = FT8::FT8::decode(lls, r174, params, 0, comments); + delete[] lls; + return ret; +} + +void MeshtasticDemodDecoderFT::deinterleave174(float ll174[]) +{ + // 174 = 2*3*29 + float t174[174]; + std::copy(ll174, ll174+174, t174); + + for (int i = 0; i < 174; i++) { + ll174[i] = t174[(i%6)*29 + (i%29)]; + } +} + +#endif // HAS_FT8 diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderft.h b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderft.h new file mode 100644 index 000000000..47b38dd46 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderft.h @@ -0,0 +1,64 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODDECODERFT_H +#define INCLUDE_MESHTASTICDEMODDECODERFT_H + +#include +#include + +namespace FT8 { + class FT8Params; +} + +class MeshtasticDemodDecoderFT +{ +public: + enum ParityStatus + { + ParityUndefined, + ParityError, + ParityCorrected, + ParityOK + }; + + static void decodeSymbols( + const std::vector>& mags, // vector of symbols magnitudes + int nbSymbolBits, //!< number of bits per symbol + std::string& msg, //!< formatted message + std::string& call1, //!< 1st callsign or shorthand + std::string& call2, //!< 2nd callsign + std::string& loc, //!< locator, report or shorthand + bool& reply, //!< true if message is a reply report + int& payloadParityStatus, + bool& payloadCRCStatus + ); + +private: + static int decodeWithShift( + FT8::FT8Params& params, + std::vector>& mags, + int nbSymbolBits, + int *r174, + std::string& comments, + int shift = 0 + ); + static void deinterleave174(float ll174[]); +}; + + +#endif diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp new file mode 100644 index 000000000..474c6c34c --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp @@ -0,0 +1,682 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Inspired by: https://github.com/myriadrf/LoRa-SDR // +// // +// 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 "meshtasticdemodsettings.h" +#include "meshtasticdemoddecoderlora.h" + +void MeshtasticDemodDecoderLoRa::decodeHeader( + const std::vector& inSymbols, + unsigned int headerNbSymbolBits, + bool& hasCRC, + unsigned int& nbParityBits, + unsigned int& packetLength, + int& headerParityStatus, + bool& headerCRCStatus +) +{ + // with header (H: header 8-bit codeword P: payload-8 bit codeword): + // nbSymbolBits = 5 |H|H|H|H|H| codewords => 8 symbols (always) : static headerSymbols = 8 + // nbSymbolBits = 7 |H|H|H|H|H|P|P| + // without header (P: payload 8-bit codeword): + // nbSymbolBits = 5 |P|P|P|P|P| codewords => 8 symbols (always) + // nbSymbolBits = 7 |P|P|P|P|P|P|P| + // Actual header is always represented with 5 8-bit codewords : static headerCodewords = 5 + // These 8-bit codewords are encoded with Hamming(4,8) FEC : static headerParityBits = 4 + + std::vector symbols(headerSymbols); + std::copy(inSymbols.begin(), inSymbols.begin() + headerSymbols, symbols.begin()); + + //gray encode + for (auto &sym : symbols) { + sym = binaryToGray16(sym); + } + + std::vector codewords(headerNbSymbolBits); + + // Header symbols de-interleave thus headerSymbols (8) symbols into nbSymbolBits (5..12) codewords using header FEC (4/8) + diagonalDeinterleaveSx(symbols.data(), headerSymbols, codewords.data(), headerNbSymbolBits, headerParityBits); + + bool error = false; + bool bad = false; + uint8_t bytes[3]; + + // decode actual header inside 8-bit codewords header with 4/8 FEC (5 first codewords) + bytes[0] = decodeHamming84sx(codewords[1], error, bad) & 0xf; + bytes[0] |= decodeHamming84sx(codewords[0], error, bad) << 4; // length + + bytes[1] = decodeHamming84sx(codewords[2], error, bad) & 0xf; // coding rate and crc enable + + bytes[2] = decodeHamming84sx(codewords[4], error, bad) & 0xf; + bytes[2] |= decodeHamming84sx(codewords[3], error, bad) << 4; // checksum + + bytes[2] ^= headerChecksum(bytes); + + if (bad) + { + headerParityStatus = (int) MeshtasticDemodSettings::ParityError; + } + else + { + if (error) { + headerParityStatus = (int) MeshtasticDemodSettings::ParityCorrected; + } else { + headerParityStatus = (int) MeshtasticDemodSettings::ParityOK; + } + + if (((bytes[2] & 0x1F) != 0) || (bytes[0] == 0)) { + headerCRCStatus = false; + } else { + headerCRCStatus = true; + } + } + + hasCRC = (bytes[1] & 1) != 0; + nbParityBits = (bytes[1] >> 1) & 0x7; + packetLength = bytes[0]; +} + +void MeshtasticDemodDecoderLoRa::decodeBytes( + QByteArray& inBytes, + const std::vector& inSymbols, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + bool hasHeader, + bool& hasCRC, + unsigned int& nbParityBits, + unsigned int& packetLength, + bool& earlyEOM, + int& headerParityStatus, + bool& headerCRCStatus, + int& payloadParityStatus, + bool& payloadCRCStatus +) +{ + payloadCRCStatus = false; + + // need at least a header (8 symbols of 8 bit codewords) whether an actual header is sent or not + if (inSymbols.size() < headerSymbols) + { + qDebug("MeshtasticDemodDecoderLoRa::decodeBytes: need at least %u symbols for header", headerSymbols); + earlyEOM = true; + return; + } + else + { + earlyEOM = false; + } + + if (hasHeader) + { + if (headerNbSymbolBits < headerCodewords) + { + qDebug("MeshtasticDemodDecoderLoRa::decodeBytes: invalid header symbol bits: %u", headerNbSymbolBits); + earlyEOM = true; + headerCRCStatus = false; + return; + } + + decodeHeader( + inSymbols, + headerNbSymbolBits, + hasCRC, + nbParityBits, + packetLength, + headerParityStatus, + headerCRCStatus + ); + + // Match gr-lora_sdr behavior: on explicit-header checksum failure, + // do not continue payload decoding for this frame attempt. + if (!headerCRCStatus) + { + earlyEOM = true; + return; + } + } + + qDebug("MeshtasticDemodDecoderLoRa::decodeBytes: crc: %s nbParityBits: %u packetLength: %u payloadSFbits: %u headerSFbits: %u", + hasCRC ? "on": "off", nbParityBits, packetLength, payloadNbSymbolBits, headerNbSymbolBits); + + if (nbParityBits > 4) + { + qDebug("MeshtasticDemodDecoderLoRa::decodeBytes: invalid parity bits in header: %u", nbParityBits); + earlyEOM = true; + headerCRCStatus = false; + return; + } + + const unsigned int payloadBlockSymbols = 4 + nbParityBits; + unsigned int numSymbols = 0; + unsigned int numCodewords = 0; + + if (hasHeader) + { + const unsigned int payloadSymbols = inSymbols.size() > headerSymbols + ? static_cast(inSymbols.size() - headerSymbols) + : 0U; + const unsigned int payloadBlocks = payloadSymbols / payloadBlockSymbols; + + numSymbols = headerSymbols + payloadBlocks * payloadBlockSymbols; + numCodewords = headerNbSymbolBits + payloadBlocks * payloadNbSymbolBits; + } + else + { + const unsigned int payloadBlocks = static_cast(inSymbols.size()) / payloadBlockSymbols; + numSymbols = payloadBlocks * payloadBlockSymbols; + numCodewords = payloadBlocks * payloadNbSymbolBits; + } + + if (numSymbols < headerSymbols) + { + earlyEOM = true; + return; + } + + std::vector symbols(numSymbols); + std::copy_n(inSymbols.begin(), numSymbols, symbols.begin()); + + //gray encode, when SF > PPM, depad the LSBs with rounding + for (auto &sym : symbols) { + sym = binaryToGray16(sym); + } + + std::vector codewords(numCodewords); + + // deinterleave / dewhiten the symbols into codewords + unsigned int sOfs = 0; + unsigned int cOfs = 0; + + // The first 8 LoRa symbols are always protected with 4/8 FEC. + // In explicit-header mode this first block is interleaved over SF-2 bits + // (header + first payload nibbles), while the remaining payload uses the + // configured payload symbol width. + if (hasHeader) + { + diagonalDeinterleaveSx(symbols.data(), headerSymbols, codewords.data(), headerNbSymbolBits, headerParityBits); + + cOfs = headerNbSymbolBits; + sOfs = headerSymbols; + + if (numSymbols > sOfs) + { + diagonalDeinterleaveSx(symbols.data() + sOfs, numSymbols - sOfs, codewords.data() + cOfs, payloadNbSymbolBits, nbParityBits); + } + } + else + { + diagonalDeinterleaveSx(symbols.data(), numSymbols, codewords.data(), payloadNbSymbolBits, nbParityBits); + } + + // Now we have nbSymbolBits 8-bit codewords (4/8 FEC) possibly containing the actual header followed by the rest of payload codewords with their own FEC (4/5..4/8) + + std::vector bytes((codewords.size()+1) / 2); + unsigned int dOfs = 0; + cOfs = 0; + + // Payload byte count plus optional outer CRC bytes; include header bytes + // only for explicit-header mode. + unsigned int dataLength = packetLength + (hasCRC ? 2 : 0); + if (hasHeader) { + dataLength += 3; + } + + if (hasHeader) + { + cOfs = headerCodewords; + dOfs = 6; + } + else + { + cOfs = 0; + dOfs = 0; + } + + if (dataLength > bytes.size()) + { + qDebug("MeshtasticDemodDecoderLoRa::decodeBytes: not enough data %lu vs %u", bytes.size(), dataLength); + earlyEOM = true; + return; + } + + // decode the rest of the payload inside 8-bit codewords header with 4/8 FEC + bool error = false; + bool bad = false; + + const unsigned int firstBlockCodewords = hasHeader ? headerNbSymbolBits : payloadNbSymbolBits; + + for (; cOfs < firstBlockCodewords; cOfs++, dOfs++) + { + if (dOfs % 2 == 1) { + bytes[dOfs/2] |= decodeHamming84sx(codewords[cOfs], error, bad) << 4; + } else { + bytes[dOfs/2] = decodeHamming84sx(codewords[cOfs], error, bad) & 0xf; + } + } + + if (dOfs % 2 == 1) // decode the start of the payload codewords with their own FEC when not on an even boundary + { + if (nbParityBits == 1) { + bytes[dOfs/2] |= checkParity54(codewords[cOfs++], error) << 4; + } else if (nbParityBits == 2) { + bytes[dOfs/2] |= checkParity64(codewords[cOfs++], error) << 4; + } else if (nbParityBits == 3){ + bytes[dOfs/2] |= decodeHamming74sx(codewords[cOfs++], error) << 4; + } else if (nbParityBits == 4){ + bytes[dOfs/2] |= decodeHamming84sx(codewords[cOfs++], error, bad) << 4; + } else { + bytes[dOfs/2] |= codewords[cOfs++] << 4; + } + + dOfs++; + } + + dOfs /= 2; + + // decode the rest of the payload codewords with their own FEC + + if (nbParityBits == 1) + { + for (unsigned int i = dOfs; i < dataLength; i++) + { + bytes[i] = checkParity54(codewords[cOfs++],error); + bytes[i] |= checkParity54(codewords[cOfs++], error) << 4; + } + } + else if (nbParityBits == 2) + { + for (unsigned int i = dOfs; i < dataLength; i++) + { + bytes[i] = checkParity64(codewords[cOfs++], error); + bytes[i] |= checkParity64(codewords[cOfs++],error) << 4; + } + } + else if (nbParityBits == 3) + { + for (unsigned int i = dOfs; i < dataLength; i++) + { + bytes[i] = decodeHamming74sx(codewords[cOfs++], error) & 0xf; + bytes[i] |= decodeHamming74sx(codewords[cOfs++], error) << 4; + } + } + else if (nbParityBits == 4) + { + for (unsigned int i = dOfs; i < dataLength; i++) + { + bytes[i] = decodeHamming84sx(codewords[cOfs++], error, bad) & 0xf; + bytes[i] |= decodeHamming84sx(codewords[cOfs++], error, bad) << 4; + } + } + else + { + for (unsigned int i = dOfs; i < dataLength; i++) + { + bytes[i] = codewords[cOfs++] & 0xf; + bytes[i] |= codewords[cOfs++] << 4; + } + } + + // LoRa payload dewhitening is applied after FEC decode and excludes the CRC bytes. + const unsigned int payloadByteOfs = hasHeader ? 3U : 0U; + if (packetLength > 0U && (payloadByteOfs + packetLength) <= bytes.size()) { + dewhitenPayloadBytes(bytes.data() + payloadByteOfs, packetLength); + } + + if (bad) { + payloadParityStatus = (int) MeshtasticDemodSettings::ParityError; + } else if (error) { + payloadParityStatus = (int) MeshtasticDemodSettings::ParityCorrected; + } else { + payloadParityStatus = (int) MeshtasticDemodSettings::ParityOK; + } + + // finalization: + // adjust offsets dpending on header and CRC presence + // compute and verify payload CRC if present + + if (hasHeader) + { + dOfs = 3; // skip header + dataLength -= 3; // remove header + + if (hasCRC) // always compute crc if present skipping the header + { + if ((packetLength >= 2U) && ((dOfs + packetLength + 2U) <= bytes.size())) + { + // Match gr-lora_sdr crc_verif: + // crc16(first pay_len-2 bytes) XOR last 2 bytes inside payload + // compare against trailing CRC bytes. + uint16_t crc = crc16gr(bytes.data() + dOfs, packetLength - 2U); + crc = static_cast(crc ^ static_cast(bytes[dOfs + packetLength - 1U])); + crc = static_cast(crc ^ (static_cast(static_cast(bytes[dOfs + packetLength - 2U])) << 8)); + const uint16_t packetCRC = static_cast(static_cast(bytes[dOfs + packetLength])) + | (static_cast(static_cast(bytes[dOfs + packetLength + 1U])) << 8); + + payloadCRCStatus = (crc == packetCRC); + } + else + { + payloadCRCStatus = false; + } + } + else + { + payloadCRCStatus = true; + } + } + else + { + dOfs = 0; // no header to skip + + if (hasCRC) + { + if ((packetLength >= 2U) && ((packetLength + 2U) <= bytes.size())) + { + uint16_t crc = crc16gr(bytes.data(), packetLength - 2U); + crc = static_cast(crc ^ static_cast(bytes[packetLength - 1U])); + crc = static_cast(crc ^ (static_cast(static_cast(bytes[packetLength - 2U])) << 8)); + const uint16_t packetCRC = static_cast(static_cast(bytes[packetLength])) + | (static_cast(static_cast(bytes[packetLength + 1U])) << 8); + payloadCRCStatus = (crc == packetCRC); + } + else + { + payloadCRCStatus = false; + } + } + else + { + payloadCRCStatus = true; + } + } + + inBytes.resize(packetLength); + std::copy(bytes.data() + dOfs, bytes.data() + dOfs + packetLength, inBytes.data()); +} + +void MeshtasticDemodDecoderLoRa::decodeBytesSoft( + QByteArray& inBytes, + const std::vector>& inMagnitudes, + const std::vector& inSymbols, + unsigned int spreadFactor, + unsigned int bandwidth, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + bool hasHeader, + bool& hasCRC, + unsigned int& nbParityBits, + unsigned int& packetLength, + bool& earlyEOM, + int& headerParityStatus, + bool& headerCRCStatus, + int& payloadParityStatus, + bool& payloadCRCStatus +) +{ + payloadCRCStatus = false; + payloadParityStatus = (int) MeshtasticDemodSettings::ParityUndefined; + + if (inSymbols.size() < headerSymbols) + { + earlyEOM = true; + return; + } + else + { + earlyEOM = false; + } + + if (hasHeader) + { + if (headerNbSymbolBits < headerCodewords) + { + earlyEOM = true; + headerCRCStatus = false; + return; + } + + decodeHeader( + inSymbols, + headerNbSymbolBits, + hasCRC, + nbParityBits, + packetLength, + headerParityStatus, + headerCRCStatus + ); + + if (!headerCRCStatus) + { + earlyEOM = true; + return; + } + } + + if ((nbParityBits < 1U) || (nbParityBits > 4U)) + { + earlyEOM = true; + headerCRCStatus = false; + return; + } + + const unsigned int payloadBlockSymbols = 4U + nbParityBits; + unsigned int numSymbols = 0U; + + if (hasHeader) + { + const unsigned int payloadSymbols = inSymbols.size() > headerSymbols + ? static_cast(inSymbols.size() - headerSymbols) + : 0U; + const unsigned int payloadBlocks = payloadSymbols / payloadBlockSymbols; + numSymbols = headerSymbols + payloadBlocks * payloadBlockSymbols; + } + else + { + const unsigned int payloadBlocks = static_cast(inSymbols.size()) / payloadBlockSymbols; + numSymbols = payloadBlocks * payloadBlockSymbols; + } + + if ((numSymbols < headerSymbols) || (inMagnitudes.size() < numSymbols)) + { + earlyEOM = true; + return; + } + + const unsigned int N = 1U << spreadFactor; + const bool ldro = ((1U << spreadFactor) * 1000.0 / static_cast(std::max(1U, bandwidth))) > 16.0; + std::vector> llrs(numSymbols, std::vector(spreadFactor, 0.0f)); + + for (unsigned int symIdx = 0; symIdx < numSymbols; symIdx++) + { + const std::vector& mags = inMagnitudes[symIdx]; + + if (mags.size() < N) + { + earlyEOM = true; + return; + } + + const bool isHeaderSym = hasHeader && (symIdx < headerSymbols); + const bool ldroSym = (!isHeaderSym) && ldro; + const unsigned int symbolDiv = (isHeaderSym || ldroSym) ? 4U : 1U; + + for (unsigned int bit = 0; bit < spreadFactor; bit++) + { + float maxX1 = std::numeric_limits::lowest(); + float maxX0 = std::numeric_limits::lowest(); + + for (unsigned int n = 0; n < N; n++) + { + unsigned int s = static_cast(modInt(static_cast(n) - 1, static_cast(N))); + s /= symbolDiv; + s = s ^ (s >> 1U); + const float v = mags[n]; + + if ((s & (1U << bit)) != 0U) { + maxX1 = std::max(maxX1, v); + } else { + maxX0 = std::max(maxX0, v); + } + } + + if (!std::isfinite(maxX1)) { maxX1 = 0.0f; } + if (!std::isfinite(maxX0)) { maxX0 = 0.0f; } + llrs[symIdx][spreadFactor - 1U - bit] = maxX1 - maxX0; + } + } + + auto decodeSoftBlock = [&llrs, spreadFactor](unsigned int symOfs, unsigned int cwLen, unsigned int sfApp, unsigned int crApp, std::vector& nibbles) { + if (sfApp == 0U) { + return false; + } + + std::vector> interBin(cwLen, std::vector(sfApp, 0.0f)); + std::vector> deinterBin(sfApp, std::vector(cwLen, 0.0f)); + + for (unsigned int i = 0; i < cwLen; i++) + { + const std::vector& symLlr = llrs[symOfs + i]; + const unsigned int start = (spreadFactor > sfApp) ? (spreadFactor - sfApp) : 0U; + + for (unsigned int j = 0; j < sfApp; j++) { + interBin[i][j] = symLlr[start + j]; + } + } + + for (unsigned int i = 0; i < cwLen; i++) + { + for (unsigned int j = 0; j < sfApp; j++) + { + const unsigned int row = static_cast(modInt(static_cast(i) - static_cast(j) - 1, static_cast(sfApp))); + deinterBin[row][i] = interBin[i][j]; + } + } + + for (unsigned int row = 0; row < sfApp; row++) { + nibbles.push_back(decodeCodewordSoft(deinterBin[row], crApp)); + } + + return true; + }; + + std::vector nibbles; + nibbles.reserve((packetLength + (hasCRC ? 2U : 0U) * 2U) + 16U); + unsigned int symOfs = 0U; + + if (hasHeader) + { + if (symOfs + headerSymbols > numSymbols) { + earlyEOM = true; + return; + } + + if (!decodeSoftBlock(symOfs, 8U, headerNbSymbolBits, 4U, nibbles)) { + earlyEOM = true; + return; + } + + symOfs += headerSymbols; + } + + while (symOfs + payloadBlockSymbols <= numSymbols) + { + if (!decodeSoftBlock(symOfs, payloadBlockSymbols, payloadNbSymbolBits, nbParityBits, nibbles)) { + earlyEOM = true; + return; + } + + symOfs += payloadBlockSymbols; + } + + const unsigned int dataByteLen = packetLength + (hasCRC ? 2U : 0U); + const unsigned int nibbleOfs = hasHeader ? 5U : 0U; + const unsigned int neededNibbles = nibbleOfs + dataByteLen * 2U; + + if (nibbles.size() < neededNibbles) + { + earlyEOM = true; + return; + } + + std::vector bytes(dataByteLen, 0U); + + for (unsigned int i = 0; i < dataByteLen; i++) + { + const uint8_t low = nibbles[nibbleOfs + i * 2U] & 0x0FU; + const uint8_t high = nibbles[nibbleOfs + i * 2U + 1U] & 0x0FU; + bytes[i] = static_cast(low | (high << 4)); + } + + if (packetLength > 0U) { + dewhitenPayloadBytes(bytes.data(), packetLength); + } + + if (hasCRC) + { + if ((packetLength >= 2U) && (dataByteLen >= packetLength + 2U)) + { + uint16_t crc = crc16gr(bytes.data(), packetLength - 2U); + crc = static_cast(crc ^ bytes[packetLength - 1U]); + crc = static_cast(crc ^ (static_cast(bytes[packetLength - 2U]) << 8)); + const uint16_t packetCRC = static_cast(bytes[packetLength]) + | (static_cast(bytes[packetLength + 1U]) << 8); + payloadCRCStatus = (crc == packetCRC); + } + else + { + payloadCRCStatus = false; + } + } + else + { + payloadCRCStatus = true; + } + + inBytes.resize(packetLength); + std::copy(bytes.begin(), bytes.begin() + packetLength, inBytes.begin()); +} + +void MeshtasticDemodDecoderLoRa::getCodingMetrics( + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + unsigned int nbParityBits, + unsigned int packetLength, + bool hasHeader, + bool hasCRC, + unsigned int& numSymbols, + unsigned int& numCodewords +) +{ + if (hasHeader) + { + const unsigned int payloadNibbles = (packetLength + (hasCRC ? 2 : 0)) * 2; + const unsigned int firstPayloadNibbles = headerNbSymbolBits > headerCodewords ? (headerNbSymbolBits - headerCodewords) : 0; + const unsigned int remainingPayloadNibbles = payloadNibbles > firstPayloadNibbles ? (payloadNibbles - firstPayloadNibbles) : 0; + const unsigned int payloadBlocks = remainingPayloadNibbles > 0 ? roundUp(remainingPayloadNibbles, payloadNbSymbolBits) / payloadNbSymbolBits : 0; + + numCodewords = headerNbSymbolBits + payloadBlocks * payloadNbSymbolBits; + numSymbols = headerSymbols + payloadBlocks * (4 + nbParityBits); + } + else + { + numCodewords = roundUp((packetLength + (hasCRC ? 2 : 0)) * 2, payloadNbSymbolBits); + numSymbols = headerSymbols + (numCodewords / payloadNbSymbolBits - 1) * (4 + nbParityBits); + } +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h new file mode 100644 index 000000000..00e32c896 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h @@ -0,0 +1,483 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Inspired by: https://github.com/myriadrf/LoRa-SDR // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODDECODERLORA_H +#define INCLUDE_MESHTASTICDEMODDECODERLORA_H + +#include +#include +#include +#include + +class MeshtasticDemodDecoderLoRa +{ +public: + static void decodeBytes( + QByteArray& bytes, + const std::vector& inSymbols, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + bool hasHeader, + bool& hasCRC, + unsigned int& nbParityBits, + unsigned int& packetLength, + bool& earlyEOM, + int& headerParityStatus, + bool& headerCRCStatus, + int& payloadParityStatus, + bool& payloadCRCStatus + ); + + static void decodeBytesSoft( + QByteArray& bytes, + const std::vector>& inMagnitudes, + const std::vector& inSymbols, + unsigned int spreadFactor, + unsigned int bandwidth, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + bool hasHeader, + bool& hasCRC, + unsigned int& nbParityBits, + unsigned int& packetLength, + bool& earlyEOM, + int& headerParityStatus, + bool& headerCRCStatus, + int& payloadParityStatus, + bool& payloadCRCStatus + ); + + static void getCodingMetrics( + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + unsigned int nbParityBits, + unsigned int packetLength, + bool hasHeader, + bool hasCRC, + unsigned int& numSymbols, + unsigned int& numCodewords + ); + + static void decodeHeader( + const std::vector& inSymbols, + unsigned int headerNbSymbolBits, + bool& hasCRC, + unsigned int& nbParityBits, + unsigned int& packetLength, + int& headerParityStatus, + bool& headerCRCStatus + ); + +private: + static constexpr unsigned int headerParityBits = 4; + static constexpr unsigned int headerSymbols = 8; + static constexpr unsigned int headerCodewords = 5; + + /*********************************************************************** + * Round functions + **********************************************************************/ + static inline unsigned roundUp(unsigned num, unsigned factor) + { + return ((num + factor - 1) / factor) * factor; + } + + /*********************************************************************** + * https://en.wikipedia.org/wiki/Gray_code + **********************************************************************/ + + /* + * This function converts an unsigned binary + * number to reflected binary Gray code. + * + * The operator >> is shift right. The operator ^ is exclusive or. + */ + static inline unsigned short binaryToGray16(unsigned short num) + { + return num ^ (num >> 1); + } + + static inline int modInt(int a, int b) + { + if (b <= 0) { + return 0; + } + + return (a % b + b) % b; + } + + /*********************************************************************** + * Diagonal deinterleaver + **********************************************************************/ + static inline void diagonalDeinterleaveSx( + const uint16_t *symbols, + const unsigned int numSymbols, + uint8_t *codewords, + const unsigned int nbSymbolBits, + const unsigned int nbParityBits) + { + const int cwLen = 4 + static_cast(nbParityBits); + + for (unsigned int x = 0; x < numSymbols / (4 + nbParityBits); x++) + { + const unsigned int cwOff = x*nbSymbolBits; + const unsigned int symOff = x*(4U + nbParityBits); + + for (int i = 0; i < cwLen; i++) + { + const uint16_t sym = symbols[symOff + i]; + + for (int j = 0; j < static_cast(nbSymbolBits); j++) + { + const uint8_t bit = (sym >> (static_cast(nbSymbolBits) - 1 - j)) & 0x1; + const int row = ((i - j - 1) % static_cast(nbSymbolBits) + static_cast(nbSymbolBits)) % static_cast(nbSymbolBits); + codewords[cwOff + static_cast(row)] |= (bit << (cwLen - 1 - i)); + } + } + } + } + + /*********************************************************************** + * Dewhitening sequence used by LoRa payload bytes. + * Matches the sequence used by gr-lora_sdr. + **********************************************************************/ + static constexpr uint8_t s_whiteningSeq[] = { + 0xFF, 0xFE, 0xFC, 0xF8, 0xF0, 0xE1, 0xC2, 0x85, 0x0B, 0x17, 0x2F, 0x5E, 0xBC, 0x78, 0xF1, 0xE3, + 0xC6, 0x8D, 0x1A, 0x34, 0x68, 0xD0, 0xA0, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x11, 0x23, 0x47, + 0x8E, 0x1C, 0x38, 0x71, 0xE2, 0xC4, 0x89, 0x12, 0x25, 0x4B, 0x97, 0x2E, 0x5C, 0xB8, 0x70, 0xE0, + 0xC0, 0x81, 0x03, 0x06, 0x0C, 0x19, 0x32, 0x64, 0xC9, 0x92, 0x24, 0x49, 0x93, 0x26, 0x4D, 0x9B, + 0x37, 0x6E, 0xDC, 0xB9, 0x72, 0xE4, 0xC8, 0x90, 0x20, 0x41, 0x82, 0x05, 0x0A, 0x15, 0x2B, 0x56, + 0xAD, 0x5B, 0xB6, 0x6D, 0xDA, 0xB5, 0x6B, 0xD6, 0xAC, 0x59, 0xB2, 0x65, 0xCB, 0x96, 0x2C, 0x58, + 0xB0, 0x61, 0xC3, 0x87, 0x0F, 0x1F, 0x3E, 0x7D, 0xFB, 0xF6, 0xED, 0xDB, 0xB7, 0x6F, 0xDE, 0xBD, + 0x7A, 0xF5, 0xEB, 0xD7, 0xAE, 0x5D, 0xBA, 0x74, 0xE8, 0xD1, 0xA2, 0x44, 0x88, 0x10, 0x21, 0x43, + 0x86, 0x0D, 0x1B, 0x36, 0x6C, 0xD8, 0xB1, 0x63, 0xC7, 0x8F, 0x1E, 0x3C, 0x79, 0xF3, 0xE7, 0xCE, + 0x9C, 0x39, 0x73, 0xE6, 0xCC, 0x98, 0x31, 0x62, 0xC5, 0x8B, 0x16, 0x2D, 0x5A, 0xB4, 0x69, 0xD2, + 0xA4, 0x48, 0x91, 0x22, 0x45, 0x8A, 0x14, 0x29, 0x52, 0xA5, 0x4A, 0x95, 0x2A, 0x54, 0xA9, 0x53, + 0xA7, 0x4E, 0x9D, 0x3B, 0x77, 0xEE, 0xDD, 0xBB, 0x76, 0xEC, 0xD9, 0xB3, 0x67, 0xCF, 0x9E, 0x3D, + 0x7B, 0xF7, 0xEF, 0xDF, 0xBF, 0x7E, 0xFD, 0xFA, 0xF4, 0xE9, 0xD3, 0xA6, 0x4C, 0x99, 0x33, 0x66, + 0xCD, 0x9A, 0x35, 0x6A, 0xD4, 0xA8, 0x51, 0xA3, 0x46, 0x8C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, + 0x0E, 0x1D, 0x3A, 0x75, 0xEA, 0xD5, 0xAA, 0x55, 0xAB, 0x57, 0xAF, 0x5F, 0xBE, 0x7C, 0xF9, 0xF2, + 0xE5, 0xCA, 0x94, 0x28, 0x50, 0xA1, 0x42, 0x84, 0x09, 0x13, 0x27, 0x4F, 0x9F, 0x3F, 0x7F + }; + + static inline void dewhitenPayloadBytes(uint8_t* payload, unsigned int length) + { + const unsigned int whiteningSize = static_cast(sizeof(s_whiteningSeq) / sizeof(s_whiteningSeq[0])); + + for (unsigned int i = 0; i < length; ++i) { + payload[i] ^= s_whiteningSeq[i % whiteningSize]; + } + } + + /*********************************************************************** + * Canonical gr-lora_sdr hard-decision codeword decoder. + * crApp is the LoRa coding-rate parity bits count in [1..4]. + **********************************************************************/ + static inline unsigned char decodeCodewordHard(const unsigned char b, unsigned int crApp, bool &error, bool &bad) + { + if ((crApp < 1U) || (crApp > 4U)) { + bad = true; + return b & 0xF; + } + + const unsigned int cwLen = 4U + crApp; + bool codeword[8] = {false, false, false, false, false, false, false, false}; + + for (unsigned int i = 0; i < cwLen; i++) { + codeword[i] = ((b >> (cwLen - 1U - i)) & 0x1U) != 0U; + } + + // hamming_dec nibble ordering: {codeword[3], codeword[2], codeword[1], codeword[0]}. + uint8_t nibbleBits[4] = { + static_cast(codeword[3]), + static_cast(codeword[2]), + static_cast(codeword[1]), + static_cast(codeword[0]) + }; + + switch (crApp) + { + case 4: + { + int ones = 0; + for (unsigned int i = 0; i < cwLen; i++) { + ones += codeword[i] ? 1 : 0; + } + + if ((ones % 2) == 0) { + break; // do not correct even-weight patterns + } + + // fall through to crApp=3 syndrome logic + } + // no break + case 3: + { + const bool s0 = codeword[0] ^ codeword[1] ^ codeword[2] ^ codeword[4]; + const bool s1 = codeword[1] ^ codeword[2] ^ codeword[3] ^ codeword[5]; + const bool s2 = codeword[0] ^ codeword[1] ^ codeword[3] ^ codeword[6]; + const int syndrome = static_cast(s0) + (static_cast(s1) << 1) + (static_cast(s2) << 2); + + if (syndrome != 0) { + error = true; + } + + switch (syndrome) + { + case 5: nibbleBits[3] ^= 0x1U; break; + case 7: nibbleBits[2] ^= 0x1U; break; + case 3: nibbleBits[1] ^= 0x1U; break; + case 6: nibbleBits[0] ^= 0x1U; break; + default: break; + } + break; + } + case 2: + { + const bool s0 = codeword[0] ^ codeword[1] ^ codeword[2] ^ codeword[4]; + const bool s1 = codeword[1] ^ codeword[2] ^ codeword[3] ^ codeword[5]; + if (s0 || s1) { + error = true; + } + break; + } + case 1: + default: + { + int ones = 0; + for (unsigned int i = 0; i < cwLen; i++) { + ones += codeword[i] ? 1 : 0; + } + if ((ones % 2) == 0) { + error = true; + } + break; + } + } + + bad = false; + return static_cast((nibbleBits[0] << 3) | (nibbleBits[1] << 2) | (nibbleBits[2] << 1) | nibbleBits[3]); + } + + static inline unsigned char decodeCodewordSoft(const std::vector& codewordLLR, unsigned int crApp) + { + static const unsigned char cwLUT[16] = { + 0, 23, 45, 58, 78, 89, 99, 116, + 139, 156, 166, 177, 197, 210, 232, 255 + }; + static const unsigned char cwLUTCr5[16] = { + 0, 24, 40, 48, 72, 80, 96, 120, + 136, 144, 160, 184, 192, 216, 232, 240 + }; + + if ((crApp < 1U) || (crApp > 4U)) { + return 0; + } + + const unsigned int cwLen = 4U + crApp; + const unsigned char *lut = (crApp == 1U) ? cwLUTCr5 : cwLUT; + float bestScore = std::numeric_limits::lowest(); + unsigned int bestIdx = 0U; + + for (unsigned int n = 0; n < 16U; n++) + { + const unsigned char cw = static_cast(lut[n] >> (8U - cwLen)); + float score = 0.0f; + + for (unsigned int j = 0; j < cwLen; j++) + { + const bool bit = ((cw >> (cwLen - 1U - j)) & 0x1U) != 0U; + const float v = std::fabs(codewordLLR[j]); + score += (((bit && (codewordLLR[j] > 0.0f)) || (!bit && (codewordLLR[j] < 0.0f))) ? v : -v); + } + + if (score > bestScore) { + bestScore = score; + bestIdx = n; + } + } + + const unsigned char dataNibbleSoft = static_cast(cwLUT[bestIdx] >> 4); + return static_cast( + (((dataNibbleSoft & 0x1U) != 0U) << 3) | + (((dataNibbleSoft & 0x2U) != 0U) << 2) | + (((dataNibbleSoft & 0x4U) != 0U) << 1) | + ((dataNibbleSoft & 0x8U) != 0U) + ); + } + + /*********************************************************************** + * Decode 8 bits into a 4 bit word with single bit correction. + * Set error to true when a parity error was detected. + **********************************************************************/ + static inline unsigned char decodeHamming84sx(const unsigned char b, bool &error, bool &bad) + { + return decodeCodewordHard(b, 4U, error, bad); + } + + /*********************************************************************** + * Simple 8-bit checksum routine + **********************************************************************/ + static inline uint8_t checksum8(const uint8_t *p, const size_t len) + { + uint8_t acc = 0; + + for (size_t i = 0; i < len; i++) + { + acc = (acc >> 1) + ((acc & 0x1) << 7); //rotate + acc += p[i]; //add + } + + return acc; + } + + static inline uint8_t headerChecksum(const uint8_t *h) + { + auto a0 = (h[0] >> 4) & 0x1; + auto a1 = (h[0] >> 5) & 0x1; + auto a2 = (h[0] >> 6) & 0x1; + auto a3 = (h[0] >> 7) & 0x1; + + auto b0 = (h[0] >> 0) & 0x1; + auto b1 = (h[0] >> 1) & 0x1; + auto b2 = (h[0] >> 2) & 0x1; + auto b3 = (h[0] >> 3) & 0x1; + + auto c0 = (h[1] >> 0) & 0x1; + auto c1 = (h[1] >> 1) & 0x1; + auto c2 = (h[1] >> 2) & 0x1; + auto c3 = (h[1] >> 3) & 0x1; + + uint8_t res; + res = (a0 ^ a1 ^ a2 ^ a3) << 4; + res |= (a3 ^ b1 ^ b2 ^ b3 ^ c0) << 3; + res |= (a2 ^ b0 ^ b3 ^ c1 ^ c3) << 2; + res |= (a1 ^ b0 ^ b2 ^ c0 ^ c1 ^ c2) << 1; + res |= a0 ^ b1 ^ c0 ^ c1 ^ c2 ^ c3; + + return res; + } + + /*********************************************************************** + * Check parity for 5/4 code. + * return true if parity is valid. + **********************************************************************/ + static inline unsigned char checkParity54(const unsigned char b, bool &error) + { + bool bad = false; + return decodeCodewordHard(b, 1U, error, bad); + } + + /*********************************************************************** + * Check parity for 6/4 code. + * return true if parity is valid. + **********************************************************************/ + static inline unsigned char checkParity64(const unsigned char b, bool &error) + { + bool bad = false; + return decodeCodewordHard(b, 2U, error, bad); + } + + /*********************************************************************** + * Decode 7 bits into a 4 bit word with single bit correction. + * Non standard version used in sx1272. + * Set error to true when a parity error was detected + * Non correctable errors are indistinguishable from single or no errors + * therefore no 'bad' variable is proposed + **********************************************************************/ + static inline unsigned char decodeHamming74sx(const unsigned char b, bool &error) + { + bool bad = false; + return decodeCodewordHard(b, 3U, error, bad); + } + + /*********************************************************************** + * CRC reverse engineered from Sx1272 data stream. + * Modified CCITT crc with masking of the output with an 8bit lfsr + **********************************************************************/ + static inline uint16_t crc16sx(uint16_t crc, const uint16_t poly) + { + for (int i = 0; i < 8; i++) + { + if (crc & 0x8000) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + } + + return crc; + } + + static inline uint8_t xsum8(uint8_t t) + { + t ^= t >> 4; + t ^= t >> 2; + t ^= t >> 1; + + return (t & 1); + } + + static inline uint16_t sx1272DataChecksum(const uint8_t *data, int length) + { + uint16_t res = 0; + uint8_t v = 0xff; + uint16_t crc = 0; + + for (int i = 0; i < length; i++) + { + crc = crc16sx(res, 0x1021); + v = xsum8(v & 0xB8) | (v << 1); + res = crc ^ data[i]; + } + + res ^= v; + v = xsum8(v & 0xB8) | (v << 1); + res ^= v << 8; + + return res; + } + + /** + * CRC routine used by gr-lora_sdr crc_verif block. + */ + static inline uint16_t crc16gr(const uint8_t *data, unsigned int length) + { + uint16_t crc = 0x0000; + + for (unsigned int i = 0; i < length; i++) + { + uint8_t b = data[i]; + + for (unsigned char j = 0; j < 8; j++) + { + if ((((crc & 0x8000) >> 8) ^ (b & 0x80)) != 0) { + crc = static_cast((crc << 1) ^ 0x1021); + } else { + crc = static_cast(crc << 1); + } + + b <<= 1; + } + } + + return crc; + } +}; + +#endif // INCLUDE_MESHTASTICDEMODDECODERLORA_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecodertty.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecodertty.cpp new file mode 100644 index 000000000..0dbd4eb3e --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecodertty.cpp @@ -0,0 +1,67 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (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 "meshtasticdemoddecodertty.h" + +const char MeshtasticDemodDecoderTTY::ttyLetters[32] = { + '_', 'E', '\n', 'A', ' ', 'S', 'I', 'U', + '\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K', + 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', + 'O', 'B', 'G', ' ', 'M', 'X', 'V', ' ' +}; + +const char MeshtasticDemodDecoderTTY::ttyFigures[32] = { // U.S. standard + '_', '3', '\n', '-', ' ', '\a', '8', '7', + '\r', '$', '4', '\'', ',', '!', ':', '(', + '5', '"', ')', '2', '#', '6', '0', '1', + '9', '?', '&', ' ', '.', '/', ';', ' ' +}; + +void MeshtasticDemodDecoderTTY::decodeSymbols(const std::vector& symbols, QString& str) +{ + std::vector::const_iterator it = symbols.begin(); + QByteArray bytes; + TTYState ttyState = TTYLetters; + + for (; it != symbols.end(); ++it) + { + char ttyChar = *it & 0x1F; + + if (ttyChar == lettersTag) { + ttyState = TTYLetters; + } else if (ttyChar == figuresTag) { + ttyState = TTYFigures; + } + else + { + signed char asciiChar = -1; + + if (ttyState == TTYLetters) { + asciiChar = ttyLetters[(int) ttyChar]; + } else if (ttyState == TTYFigures) { + asciiChar = ttyFigures[(int) ttyChar]; + } + + if (asciiChar >= 0) { + bytes.push_back(asciiChar); + } + } + } + + str = QString(bytes.toStdString().c_str()); +} + diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecodertty.h b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecodertty.h new file mode 100644 index 000000000..178a2f7ec --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecodertty.h @@ -0,0 +1,44 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODDECODERTTY_H +#define INCLUDE_MESHTASTICDEMODDECODERTTY_H + +#include +#include + +class MeshtasticDemodDecoderTTY +{ +public: + static void decodeSymbols(const std::vector& symbols, QString& str); + +private: + enum TTYState + { + TTYLetters, + TTYFigures + }; + + static const char ttyLetters[32]; + static const char ttyFigures[32]; + static const char lettersTag = 0x1f; + static const char figuresTag = 0x1b; +}; + +#endif // INCLUDE_MESHTASTICDEMODDECODERTTY_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp new file mode 100644 index 000000000..dbdf06e18 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp @@ -0,0 +1,3522 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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) 2021-2023 Jon Beniston, M7RCE // +// // +// 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 "device/deviceuiset.h" +#include "device/deviceapi.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui_meshtasticdemodgui.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 "meshtasticdemod.h" +#include "meshtasticdemodmsg.h" +#include "meshtasticdemodgui.h" +#include "meshtasticpacket.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()); + } + } +} + +MeshtasticDemodGUI* MeshtasticDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + MeshtasticDemodGUI* gui = new MeshtasticDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void MeshtasticDemodGUI::destroy() +{ + delete this; +} + +void MeshtasticDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray MeshtasticDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshtasticDemodGUI::deserialize(const QByteArray& data) +{ + resetLoRaStatus(); + + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool MeshtasticDemodGUI::handleMessage(const Message& message) +{ + if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_deviceCenterFrequency = notif.getCenterFrequency(); + int basebandSampleRate = notif.getSampleRate(); + qDebug() << "MeshtasticDemodGUI::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() << "MeshtasticDemodGUI::handleMessage: DSPSignalNotification after RemoteTCP reconnect - reapplying Meshtastic profile"; + QMetaObject::invokeMethod(this, &MeshtasticDemodGUI::applyMeshtasticProfileFromSelection, Qt::QueuedConnection); + } + + return true; + } + else if (MeshtasticDemodMsg::MsgReportDecodeBytes::match(message)) + { + const MeshtasticDemodMsg::MsgReportDecodeBytes& msg = (MeshtasticDemodMsg::MsgReportDecodeBytes&) message; + handleMeshAutoLockObservation(msg); + + if (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) { + showLoRaMessage(message); + } + + return true; + } + else if (MeshtasticDemodMsg::MsgReportDecodeString::match(message)) + { + if ((m_settings.m_codingScheme == MeshtasticDemodSettings::CodingASCII) + || (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingTTY) + || (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa)) { + showTextMessage(message); + } + + return true; + } + else if (MeshtasticDemodMsg::MsgReportDecodeFT::match(message)) + { + if (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingFT) { + showFTMessage(message); + } + + return true; + } + else if (MeshtasticDemod::MsgConfigureMeshtasticDemod::match(message)) + { + qDebug("MeshtasticDemodGUI::handleMessage: NFMDemod::MsgConfigureMeshtasticDemod"); + const MeshtasticDemod::MsgConfigureMeshtasticDemod& cfg = (MeshtasticDemod::MsgConfigureMeshtasticDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + ui->spectrumGUI->updateSettings(); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + + return true; + } + else + { + return false; + } +} + +void MeshtasticDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void MeshtasticDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void MeshtasticDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void MeshtasticDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void MeshtasticDemodGUI::on_BW_valueChanged(int value) +{ + if (value < 0) { + m_settings.m_bandwidthIndex = 0; + } else if (value < MeshtasticDemodSettings::nbBandwidths) { + m_settings.m_bandwidthIndex = value; + } else { + m_settings.m_bandwidthIndex = MeshtasticDemodSettings::nbBandwidths - 1; + } + + int thisBW = MeshtasticDemodSettings::bandwidths[value]; + ui->BWText->setText(QString("%1 Hz").arg(thisBW)); + m_channelMarker.setBandwidth(thisBW); + ui->glSpectrum->setSampleRate(thisBW); + ui->glSpectrum->setCenterFrequency(thisBW/2); + + applySettings(); +} + +void MeshtasticDemodGUI::on_Spread_valueChanged(int value) +{ + m_settings.m_spreadFactor = value; + ui->SpreadText->setText(tr("%1").arg(value)); + ui->spectrumGUI->setFFTSize(m_settings.m_spreadFactor); + + applySettings(); +} + +void MeshtasticDemodGUI::on_deBits_valueChanged(int value) +{ + m_settings.m_deBits = value; + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + applySettings(); +} + +void MeshtasticDemodGUI::on_fftWindow_currentIndexChanged(int index) +{ + m_settings.m_fftWindow = (FFTWindow::Function) index; + applySettings(); +} + +void MeshtasticDemodGUI::on_preambleChirps_valueChanged(int value) +{ + m_settings.m_preambleChirps = value; + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + applySettings(); +} + +void MeshtasticDemodGUI::on_scheme_currentIndexChanged(int index) +{ + m_settings.m_codingScheme = (MeshtasticDemodSettings::CodingScheme) index; + + if (m_settings.m_codingScheme != MeshtasticDemodSettings::CodingLoRa) { + resetLoRaStatus(); + } + + updateControlAvailabilityHints(); + applySettings(); +} + +void MeshtasticDemodGUI::on_mute_toggled(bool checked) +{ + m_settings.m_decodeActive = !checked; + applySettings(); +} + +void MeshtasticDemodGUI::on_clear_clicked(bool checked) +{ + (void) checked; + clearPipelineViews(); + setDechirpInspectionMode(false); +} + +void MeshtasticDemodGUI::on_eomSquelch_valueChanged(int value) +{ + m_settings.m_eomSquelchTenths = value; + displaySquelch(); + applySettings(); +} + +void MeshtasticDemodGUI::on_messageLength_valueChanged(int value) +{ + m_settings.m_nbSymbolsMax = value; + ui->messageLengthText->setText(tr("%1").arg(m_settings.m_nbSymbolsMax)); + applySettings(); +} + +void MeshtasticDemodGUI::on_messageLengthAuto_stateChanged(int state) +{ + m_settings.m_autoNbSymbolsMax = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticDemodGUI::on_header_stateChanged(int state) +{ + m_settings.m_hasHeader = (state == Qt::Checked); + + if (!m_settings.m_hasHeader) // put back values from settings + { + ui->fecParity->blockSignals(true); + ui->crc->blockSignals(true); + ui->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->crc->setChecked(m_settings.m_hasCRC); + ui->fecParity->blockSignals(false); + ui->crc->blockSignals(false); + } + + updateControlAvailabilityHints(); + + applySettings(); +} + +void MeshtasticDemodGUI::on_fecParity_valueChanged(int value) +{ + m_settings.m_nbParityBits = value; + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + applySettings(); +} + +void MeshtasticDemodGUI::on_crc_stateChanged(int state) +{ + m_settings.m_hasCRC = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticDemodGUI::on_packetLength_valueChanged(int value) +{ + m_settings.m_packetLength = value; + ui->packetLengthText->setText(tr("%1").arg(m_settings.m_packetLength)); + applySettings(); +} + +void MeshtasticDemodGUI::on_udpSend_stateChanged(int state) +{ + m_settings.m_sendViaUDP = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void MeshtasticDemodGUI::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 MeshtasticDemodGUI::on_invertRamps_stateChanged(int state) +{ + m_settings.m_invertRamps = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticDemodGUI::on_meshRegion_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + rebuildMeshtasticChannelOptions(); + applyMeshtasticProfileFromSelection(); +} + +void MeshtasticDemodGUI::on_meshPreset_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + rebuildMeshtasticChannelOptions(); + applyMeshtasticProfileFromSelection(); +} + +void MeshtasticDemodGUI::on_meshChannel_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + applyMeshtasticProfileFromSelection(); +} + +void MeshtasticDemodGUI::on_meshApply_clicked(bool checked) +{ + (void) checked; + + if (m_meshControlsUpdating) { + return; + } + + // Rebuild first so region/preset changes refresh the valid channel list before applying. + rebuildMeshtasticChannelOptions(); + applyMeshtasticProfileFromSelection(); +} + +void MeshtasticDemodGUI::on_meshKeys_clicked(bool checked) +{ + (void) checked; + editMeshtasticKeys(); +} + +void MeshtasticDemodGUI::on_meshAutoSampleRate_toggled(bool checked) +{ + if (m_meshControlsUpdating) { + return; + } + + m_settings.m_meshtasticAutoSampleRate = checked; + applySettings(); + + if (checked) { + applyMeshtasticProfileFromSelection(); + } else { + displayStatus(tr("MESH CFG|auto input tuning disabled")); + } +} + +void MeshtasticDemodGUI::on_meshAutoLock_clicked(bool checked) +{ + if (checked) { + startMeshAutoLock(); + } else { + stopMeshAutoLock(true); + } +} + +void MeshtasticDemodGUI::startMeshAutoLock() +{ + if (m_meshAutoLockActive) { + return; + } + + if (m_settings.m_codingScheme != MeshtasticDemodSettings::CodingLoRa) + { + displayStatus(tr("MESH LOCK|switch decoder scheme to LoRa before auto-lock")); + + if (m_meshAutoLockButton) + { + m_meshAutoLockButton->blockSignals(true); + m_meshAutoLockButton->setChecked(false); + m_meshAutoLockButton->blockSignals(false); + } + + return; + } + + const int bandwidthHz = MeshtasticDemodSettings::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 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")); + if (m_meshAutoLockButton) + { + m_meshAutoLockButton->blockSignals(true); + m_meshAutoLockButton->setChecked(false); + m_meshAutoLockButton->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(); + + if (m_meshAutoLockButton) { + m_meshAutoLockButton->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 MeshtasticDemodGUI::stopMeshAutoLock(bool keepBestCandidate) +{ + if (!m_meshAutoLockActive && (!m_meshAutoLockButton || !m_meshAutoLockButton->isChecked())) + { + return; + } + + int bestIndex = -1; + int bestFallbackIndex = -1; + double bestWeightedScore = -std::numeric_limits::infinity(); + double bestFallbackWeightedScore = -std::numeric_limits::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; + + if (m_meshAutoLockButton) + { + m_meshAutoLockButton->blockSignals(true); + m_meshAutoLockButton->setChecked(false); + m_meshAutoLockButton->setText(tr("Auto Lock")); + m_meshAutoLockButton->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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::handleMeshAutoLockObservation(const MeshtasticDemodMsg::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(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) MeshtasticDemodSettings::ParityOK) { + score += 8.0; + } else if (headerParityStatus == (int) MeshtasticDemodSettings::ParityCorrected) { + score += 5.0; + } else if (headerParityStatus == (int) MeshtasticDemodSettings::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) MeshtasticDemodSettings::ParityOK) { + score += 6.0; + } else if (payloadParityStatus == (int) MeshtasticDemodSettings::ParityCorrected) { + score += 3.0; + } else if (payloadParityStatus == (int) MeshtasticDemodSettings::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) MeshtasticDemodSettings::ParityOK + || headerParityStatus == (int) MeshtasticDemodSettings::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 MeshtasticDemodGUI::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_chirpChatDemod->getTotalPower()); + const double noisePower = std::max(1e-12, m_chirpChatDemod->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_chirpChatDemod->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 MeshtasticDemodGUI::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::infinity(); + double maxSourceAvg = -std::numeric_limits::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 MeshtasticDemodGUI::editMeshtasticKeys() +{ + QDialog dialog(this); + dialog.setWindowTitle(tr("Meshtastic Keys")); + dialog.resize(760, 460); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* helpLabel = new QLabel(tr( + "One key entry per line (or comma/semicolon separated).\n" + "Formats: default, none, simple1..simple10, hex:, b64:, base64:, raw hex, raw base64.\n" + "Optional channel mapping: channelName=keySpec (example: LongFast=default)."), + &dialog); + helpLabel->setWordWrap(true); + layout->addWidget(helpLabel); + + QTextEdit* keyEditor = new QTextEdit(&dialog); + keyEditor->setPlainText(m_settings.m_meshtasticKeySpecList); + keyEditor->setToolTip(tr("Enter one or more key specs used to decrypt Meshtastic packets.")); + keyEditor->setPlaceholderText("LongFast=default\nnone\nLongSlow=hex:00112233445566778899aabbccddeeff"); + layout->addWidget(keyEditor, 1); + + QHBoxLayout* statusLayout = new QHBoxLayout(); + QPushButton* validateButton = new QPushButton(tr("Validate"), &dialog); + validateButton->setToolTip(tr("Validate key syntax and count without saving.")); + QLabel* statusLabel = new QLabel(&dialog); + statusLabel->setToolTip(tr("Validation status for the current key list.")); + statusLabel->setWordWrap(true); + statusLayout->addWidget(validateButton); + statusLayout->addWidget(statusLabel, 1); + layout->addLayout(statusLayout); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + layout->addWidget(buttons); + + auto validateInput = [keyEditor, statusLabel]() -> bool { + const QString keyText = keyEditor->toPlainText().trimmed(); + + if (keyText.isEmpty()) + { + statusLabel->setStyleSheet("QLabel { color: #bbbbbb; }"); + statusLabel->setText(QObject::tr("No custom keys set. Decoder will use environment/default keys.")); + return true; + } + + QString error; + int keyCount = 0; + + if (!Meshtastic::Packet::validateKeySpecList(keyText, error, &keyCount)) + { + statusLabel->setStyleSheet("QLabel { color: #ff5555; }"); + statusLabel->setText(QObject::tr("Invalid key list: %1").arg(error)); + return false; + } + + statusLabel->setStyleSheet("QLabel { color: #7cd67c; }"); + statusLabel->setText(QObject::tr("Valid: %1 key(s) parsed").arg(keyCount)); + return true; + }; + + QObject::connect(validateButton, &QPushButton::clicked, &dialog, [validateInput]() { + validateInput(); + }); + + QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, [this, &dialog, keyEditor, validateInput]() { + if (!validateInput()) + { + QMessageBox::warning(this, tr("Invalid Keys"), tr("Fix the Meshtastic key list before saving.")); + return; + } + + m_settings.m_meshtasticKeySpecList = keyEditor->toPlainText().trimmed(); + + if (m_meshKeysButton) + { + const bool hasCustomKeys = !m_settings.m_meshtasticKeySpecList.isEmpty(); + m_meshKeysButton->setText(hasCustomKeys ? tr("Keys*") : tr("Keys...")); + m_meshKeysButton->setToolTip(hasCustomKeys ? + tr("Custom Meshtastic decode keys configured. Click to edit.") : + tr("Open Meshtastic key manager.")); + } + + applySettings(); + + if (m_settings.m_meshtasticKeySpecList.isEmpty()) { + displayStatus(tr("MESH KEYS|using environment/default key set")); + } else { + displayStatus(tr("MESH KEYS|custom key set saved")); + } + + dialog.accept(); + }); + + QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + validateInput(); + dialog.exec(); +} + +int MeshtasticDemodGUI::findBandwidthIndex(int bandwidthHz) const +{ + int bestIndex = -1; + int bestDelta = 1 << 30; + + for (int i = 0; i < MeshtasticDemodSettings::nbBandwidths; ++i) + { + const int delta = std::abs(MeshtasticDemodSettings::bandwidths[i] - bandwidthHz); + if (delta < bestDelta) + { + bestDelta = delta; + bestIndex = i; + } + } + + return bestIndex; +} + +bool MeshtasticDemodGUI::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 MeshtasticDemodGUI::autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary) +{ + summary.clear(); + + if (!m_chirpChatDemod) { + return false; + } + + const int deviceSetIndex = m_chirpChatDemod->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(minEffectiveRate) << newLog2Decim; + + if ((requiredDevRate > 0) && (requiredDevRate <= std::numeric_limits::max())) + { + ChannelWebAPIUtils::setDevSampleRate(deviceSetIndex, static_cast(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; + } + } + + 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 MeshtasticDemodGUI::applyMeshtasticProfileFromSelection() +{ + if (!m_meshRegionCombo || !m_meshPresetCombo || !m_meshChannelCombo) { + return; + } + + const QString region = m_meshRegionCombo->currentData().toString(); + const QString preset = m_meshPresetCombo->currentData().toString(); + const int meshChannel = m_meshChannelCombo->currentData().toInt(); + const int channelNum = meshChannel + 1; // planner expects 1-based channel_num + + if (region.isEmpty() || preset.isEmpty()) { + return; + } + + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum); + Meshtastic::TxRadioSettings meshRadio; + QString error; + + if (!Meshtastic::Packet::deriveTxRadioSettings(command, meshRadio, error)) + { + qWarning() << "MeshtasticDemodGUI::applyMeshtasticProfileFromSelection:" << error; + return; + } + + bool changed = false; + bool selectionStateChanged = false; + + if (m_settings.m_meshtasticRegionCode != region) + { + m_settings.m_meshtasticRegionCode = region; + selectionStateChanged = true; + } + if (m_settings.m_meshtasticPresetName != preset) + { + m_settings.m_meshtasticPresetName = preset; + selectionStateChanged = true; + } + if (m_settings.m_meshtasticChannelIndex != meshChannel) + { + m_settings.m_meshtasticChannelIndex = meshChannel; + selectionStateChanged = true; + } + + if (m_settings.m_codingScheme != MeshtasticDemodSettings::CodingLoRa) + { + m_settings.m_codingScheme = MeshtasticDemodSettings::CodingLoRa; + changed = true; + } + + const int bwIndex = findBandwidthIndex(meshRadio.bandwidthHz); + if (bwIndex >= 0 && bwIndex != m_settings.m_bandwidthIndex) + { + m_settings.m_bandwidthIndex = bwIndex; + changed = true; + } + + if (meshRadio.spreadFactor > 0 && meshRadio.spreadFactor != m_settings.m_spreadFactor) + { + m_settings.m_spreadFactor = meshRadio.spreadFactor; + changed = true; + } + + if (meshRadio.deBits != m_settings.m_deBits) + { + m_settings.m_deBits = meshRadio.deBits; + changed = true; + } + + if (meshRadio.parityBits > 0 && meshRadio.parityBits != m_settings.m_nbParityBits) + { + m_settings.m_nbParityBits = meshRadio.parityBits; + changed = true; + } + + if (!m_settings.m_hasHeader) + { + m_settings.m_hasHeader = true; + changed = true; + } + + if (!m_settings.m_hasCRC) + { + m_settings.m_hasCRC = true; + changed = true; + } + + const int meshPreambleChirps = meshRadio.preambleChirps; + if (m_settings.m_preambleChirps != static_cast(meshPreambleChirps)) + { + m_settings.m_preambleChirps = static_cast(meshPreambleChirps); + changed = true; + } + + if (meshRadio.hasCenterFrequency) + { + if (retuneDeviceToFrequency(meshRadio.centerFrequencyHz)) + { + m_deviceCenterFrequency = meshRadio.centerFrequencyHz; + if (m_settings.m_inputFrequencyOffset != 0) + { + m_settings.m_inputFrequencyOffset = 0; + changed = true; + } + } + else if (m_deviceCenterFrequency != 0) + { + const qint64 wantedOffset = meshRadio.centerFrequencyHz - m_deviceCenterFrequency; + if (wantedOffset != m_settings.m_inputFrequencyOffset) + { + m_settings.m_inputFrequencyOffset = static_cast(wantedOffset); + changed = true; + } + } + else + { + qWarning() << "MeshtasticDemodGUI::applyMeshtasticProfileFromSelection: cannot retune device and device center frequency unknown"; + } + } + + const int thisBW = MeshtasticDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; + QString sampleRateSummary; + bool sampleRateChanged = false; + + if (m_settings.m_meshtasticAutoSampleRate) { + sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary); + } else { + sampleRateSummary = "auto sample-rate control: disabled"; + } + + if (!changed && !sampleRateChanged && !selectionStateChanged) { + return; + } + + qInfo() << "MeshtasticDemodGUI::applyMeshtasticProfileFromSelection:" << meshRadio.summary + << sampleRateSummary; + + QString status = tr("MESH CFG|region=%1 preset=%2 ch=%3 %4") + .arg(region) + .arg(preset) + .arg(meshChannel) + .arg(meshRadio.summary); + + status += QString(" preamble=%1").arg(meshPreambleChirps); + + if (!sampleRateSummary.isEmpty()) { + status += " " + sampleRateSummary; + } + + if (!changed) + { + applySettings(); + displayStatus(status); + return; + } + + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBW); + m_channelMarker.blockSignals(false); + + blockApplySettings(true); + ui->deltaFrequency->setValue(m_settings.m_inputFrequencyOffset); + ui->BW->setValue(m_settings.m_bandwidthIndex); + ui->BWText->setText(QString("%1 Hz").arg(thisBW)); + ui->Spread->setValue(m_settings.m_spreadFactor); + ui->SpreadText->setText(tr("%1").arg(m_settings.m_spreadFactor)); + ui->deBits->setValue(m_settings.m_deBits); + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + ui->preambleChirps->setValue(m_settings.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + ui->scheme->setCurrentIndex((int) m_settings.m_codingScheme); + ui->header->setChecked(m_settings.m_hasHeader); + ui->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->crc->setChecked(m_settings.m_hasCRC); + blockApplySettings(false); + updateControlAvailabilityHints(); + + ui->glSpectrum->setSampleRate(thisBW); + ui->glSpectrum->setCenterFrequency(thisBW/2); + + updateAbsoluteCenterFrequency(); + applySettings(); + displayStatus(status); +} + +void MeshtasticDemodGUI::setupMeshtasticAutoProfileControls() +{ + QHBoxLayout* meshLayout = new QHBoxLayout(); + meshLayout->setSpacing(2); + + QLabel* regionLabel = new QLabel("Region", this); + regionLabel->setToolTip("Meshtastic region (defines allowed frequency band)"); + m_meshRegionCombo = new QComboBox(this); + m_meshRegionCombo->setToolTip("Meshtastic region. Combined with preset/channel to auto-apply LoRa receive parameters."); + m_meshRegionCombo->addItem("US", "US"); + m_meshRegionCombo->addItem("EU_433", "EU_433"); + m_meshRegionCombo->addItem("EU_868", "EU_868"); + m_meshRegionCombo->addItem("ANZ", "ANZ"); + m_meshRegionCombo->addItem("JP", "JP"); + m_meshRegionCombo->addItem("CN", "CN"); + m_meshRegionCombo->addItem("KR", "KR"); + m_meshRegionCombo->addItem("TW", "TW"); + m_meshRegionCombo->addItem("IN", "IN"); + m_meshRegionCombo->addItem("TH", "TH"); + m_meshRegionCombo->addItem("BR_902", "BR_902"); + m_meshRegionCombo->addItem("LORA_24", "LORA_24"); + + QLabel* presetLabel = new QLabel("Preset", this); + presetLabel->setToolTip("Meshtastic modem preset (LongFast, MediumSlow, ...)"); + m_meshPresetCombo = new QComboBox(this); + m_meshPresetCombo->setToolTip("Meshtastic modem preset. Applies LoRa BW/SF/CR/DE and header/CRC expectations."); + m_meshPresetCombo->addItem("LONG_FAST", "LONG_FAST"); + m_meshPresetCombo->addItem("LONG_SLOW", "LONG_SLOW"); + m_meshPresetCombo->addItem("LONG_MODERATE", "LONG_MODERATE"); + m_meshPresetCombo->addItem("LONG_TURBO", "LONG_TURBO"); + m_meshPresetCombo->addItem("MEDIUM_FAST", "MEDIUM_FAST"); + m_meshPresetCombo->addItem("MEDIUM_SLOW", "MEDIUM_SLOW"); + m_meshPresetCombo->addItem("SHORT_FAST", "SHORT_FAST"); + m_meshPresetCombo->addItem("SHORT_SLOW", "SHORT_SLOW"); + m_meshPresetCombo->addItem("SHORT_TURBO", "SHORT_TURBO"); + + QLabel* channelLabel = new QLabel("Channel", this); + channelLabel->setToolTip("Meshtastic channel number (zero-based)"); + m_meshChannelCombo = new QComboBox(this); + m_meshChannelCombo->setToolTip("Meshtastic channel number (zero-based, shown with center frequency)"); + m_meshApplyButton = new QPushButton("Apply", this); + m_meshApplyButton->setToolTip("Apply the currently selected Meshtastic region/preset/channel profile now."); + m_meshKeysButton = new QPushButton("Keys...", this); + m_meshKeysButton->setToolTip("Open key manager to configure Meshtastic decryption keys (hex/base64/default/simple)."); + m_meshAutoLockButton = new QPushButton("Auto Lock", this); + m_meshAutoLockButton->setCheckable(true); + m_meshAutoLockButton->setToolTip( + "Scan Invert + frequency offset candidates and keep the best lock.\n" + "Arms and waits for on-air activity, then scans candidates.\n" + "Scores using decode quality plus source-side intensity (demod activity and power/noise)."); + m_meshAutoSampleRateCheck = new QCheckBox("Auto Input Tune", this); + m_meshAutoSampleRateCheck->setChecked(m_settings.m_meshtasticAutoSampleRate); + m_meshAutoSampleRateCheck->setToolTip( + "Automatically tune source parameters for the selected LoRa profile.\n" + "Includes sample-rate/decimation and, where supported, dcBlock/iqCorrection/agc."); + + meshLayout->addWidget(regionLabel); + meshLayout->addWidget(m_meshRegionCombo, 1); + meshLayout->addWidget(presetLabel); + meshLayout->addWidget(m_meshPresetCombo, 1); + meshLayout->addWidget(channelLabel); + meshLayout->addWidget(m_meshChannelCombo); + meshLayout->addWidget(m_meshApplyButton); + meshLayout->addWidget(m_meshKeysButton); + meshLayout->addWidget(m_meshAutoLockButton); + meshLayout->addWidget(m_meshAutoSampleRateCheck); + + ui->payloadLayout->insertLayout(0, meshLayout); + + QObject::connect(m_meshRegionCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticDemodGUI::on_meshRegion_currentIndexChanged); + QObject::connect(m_meshPresetCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticDemodGUI::on_meshPreset_currentIndexChanged); + QObject::connect(m_meshChannelCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticDemodGUI::on_meshChannel_currentIndexChanged); + QObject::connect(m_meshApplyButton, &QPushButton::clicked, this, &MeshtasticDemodGUI::on_meshApply_clicked); + QObject::connect(m_meshKeysButton, &QPushButton::clicked, this, &MeshtasticDemodGUI::on_meshKeys_clicked); + QObject::connect(m_meshAutoLockButton, &QPushButton::clicked, this, &MeshtasticDemodGUI::on_meshAutoLock_clicked); + QObject::connect(m_meshAutoSampleRateCheck, &QCheckBox::toggled, this, &MeshtasticDemodGUI::on_meshAutoSampleRate_toggled); + + rebuildMeshtasticChannelOptions(); +} + +void MeshtasticDemodGUI::rebuildMeshtasticChannelOptions() +{ + if (!m_meshRegionCombo || !m_meshPresetCombo || !m_meshChannelCombo) { + return; + } + + const QString region = m_meshRegionCombo->currentData().toString(); + const QString preset = m_meshPresetCombo->currentData().toString(); + const int previousChannel = m_meshChannelCombo->currentData().toInt(); + + m_meshControlsUpdating = true; + m_meshChannelCombo->clear(); + + int added = 0; + for (int meshChannel = 0; meshChannel <= 200; ++meshChannel) + { + Meshtastic::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 (!Meshtastic::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); + + m_meshChannelCombo->addItem(label, meshChannel); + added++; + } + + if (added == 0) { + m_meshChannelCombo->addItem("0", 0); + } + + m_meshChannelCombo->setToolTip(tr("Meshtastic channel number (%1 available for %2/%3)") + .arg(added) + .arg(region) + .arg(preset)); + int restoreIndex = m_meshChannelCombo->findData(previousChannel); + if (restoreIndex < 0) { + restoreIndex = 0; + } + m_meshChannelCombo->setCurrentIndex(restoreIndex); + m_meshControlsUpdating = false; + + qInfo() << "MeshtasticDemodGUI::rebuildMeshtasticChannelOptions:" + << "region=" << region + << "preset=" << preset + << "channels=" << added; + + // Ensure the rebuilt combo state is actually applied, even when the rebuild + // was triggered from code paths where index-change handlers are suppressed. + QMetaObject::invokeMethod(this, [this]() { + if (!m_meshControlsUpdating) { + applyMeshtasticProfileFromSelection(); + } + }, Qt::QueuedConnection); +} + +void MeshtasticDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void MeshtasticDemodGUI::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_chirpChatDemod->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(); +} + +MeshtasticDemodGUI::MeshtasticDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::MeshtasticDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_basebandSampleRate(250000), + m_doApplySettings(true), + m_meshRegionCombo(nullptr), + m_meshPresetCombo(nullptr), + m_meshChannelCombo(nullptr), + m_meshApplyButton(nullptr), + m_meshKeysButton(nullptr), + m_meshAutoLockButton(nullptr), + m_dechirpLiveFollowButton(nullptr), + m_meshAutoSampleRateCheck(nullptr), + 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_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demodmeshtastic/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setupMeshtasticAutoProfileControls(); + setupPipelineViews(); + if (ui->messageLayout) + { + m_dechirpLiveFollowButton = new QPushButton(tr("Live"), this); + m_dechirpLiveFollowButton->setAutoDefault(false); + m_dechirpLiveFollowButton->setMaximumSize(QSize(42, 24)); + m_dechirpLiveFollowButton->setToolTip(tr("Return de-chirped spectrum to live follow mode.")); + ui->messageLayout->insertWidget(2, m_dechirpLiveFollowButton); + QObject::connect(m_dechirpLiveFollowButton, &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); + } else { + ui->messageText->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_chirpChatDemod = (MeshtasticDemod*) rxChannel; + m_spectrumVis = m_chirpChatDemod->getSpectrumVis(); + m_spectrumVis->setGLSpectrum(ui->glSpectrum); + m_chirpChatDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + SpectrumSettings spectrumSettings = m_spectrumVis->getSettings(); + // Meshtastic 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. Meshtastic 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->fftWindow->setToolTip(tr("FFT window used by the LoRa de-chirping stage.")); + ui->fftWindowLabel->setToolTip(tr("FFT window function.")); + ui->preambleChirps->setToolTip(tr("Expected LoRa preamble chirp count. Meshtastic profiles default to 17 (sub-GHz) or 12 (2.4 GHz).")); + ui->preambleChirpsLabel->setToolTip(tr("Expected LoRa preamble length in chirps.")); + ui->preambleChirpsText->setToolTip(tr("Current preamble chirp value.")); + ui->scheme->setToolTip(tr("Decoder mode. Use LoRa for Meshtastic traffic.")); + ui->schemeLabel->setToolTip(tr("Select decoding scheme.")); + 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->messageLengthAuto->setToolTip(tr("Auto-detect payload symbol length from headers.")); + ui->messageLengthLabel->setToolTip(tr("Maximum payload symbol length.")); + ui->messageLengthText->setToolTip(tr("Current payload symbol length setting.")); + ui->header->setToolTip(tr("Assume explicit LoRa header mode.")); + 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->crc->setToolTip(tr("Expect payload CRC.")); + 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.")); + ui->messageText->setToolTip(tr("Decoded packet and status log.")); + 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->loraLabel->setToolTip(tr("LoRa header/payload status indicators.")); + ui->symbolsCodewordsSeparator->setToolTip(tr("Separator between symbol and codeword counters.")); + + ui->messageText->setReadOnly(true); + ui->messageText->setReadOnly(true); + + 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 Meshtastic profile once. + applyMeshtasticProfileFromSelection(); + DialPopup::addPopupsToChildDials(this); + m_resizer.enableChildMouseTracking(); +} + +MeshtasticDemodGUI::~MeshtasticDemodGUI() +{ + delete ui; +} + +void MeshtasticDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void MeshtasticDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + setTitleColor(m_channelMarker.getColor()); + MeshtasticDemod::MsgConfigureMeshtasticDemod* message = MeshtasticDemod::MsgConfigureMeshtasticDemod::create( m_settings, force); + m_chirpChatDemod->getInputMessageQueue()->push(message); + } +} + +void MeshtasticDemodGUI::updateControlAvailabilityHints() +{ + const bool loRaMode = m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa; + const bool explicitHeaderMode = loRaMode && m_settings.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."); + ui->fftWindow->setEnabled(!loRaMode); + ui->fftWindow->setToolTip(loRaMode ? fftWindowDisabledTip : fftWindowEnabledTip); + ui->fftWindowLabel->setToolTip(loRaMode ? fftWindowDisabledTip : tr("FFT window function.")); + + 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."); + ui->messageLengthAuto->setEnabled(!explicitHeaderMode); + ui->messageLengthAuto->setToolTip(explicitHeaderMode ? messageLengthAutoDisabledTip : messageLengthAutoEnabledTip); + + 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 headerControlsEnabled = !m_settings.m_hasHeader; + ui->fecParity->setEnabled(headerControlsEnabled); + ui->crc->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."); + ui->crc->setToolTip(headerControlsEnabled ? crcEnabledTip : crcDisabledTip); + + 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 MeshtasticDemodGUI::displaySettings() +{ + int thisBW = MeshtasticDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; + + m_channelMarker.blockSignals(true); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBW); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); + setTitleColor(m_settings.m_rgbColor); + setTitle(m_channelMarker.getTitle()); + + ui->glSpectrum->setSampleRate(thisBW); + ui->glSpectrum->setCenterFrequency(thisBW/2); + + blockApplySettings(true); + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + ui->BWText->setText(QString("%1 Hz").arg(thisBW)); + ui->BW->setValue(m_settings.m_bandwidthIndex); + ui->Spread->setValue(m_settings.m_spreadFactor); + ui->SpreadText->setText(tr("%1").arg(m_settings.m_spreadFactor)); + ui->deBits->setValue(m_settings.m_deBits); + ui->fftWindow->setCurrentIndex((int) m_settings.m_fftWindow); + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + ui->preambleChirps->setValue(m_settings.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + ui->scheme->setCurrentIndex((int) m_settings.m_codingScheme); + 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->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(tr("%1").arg(m_settings.m_udpPort)); + ui->header->setChecked(m_settings.m_hasHeader); + + if (!m_settings.m_hasHeader) + { + ui->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->crc->setChecked(m_settings.m_hasCRC); + ui->packetLength->setValue(m_settings.m_packetLength); + ui->spectrumGUI->setFFTSize(m_settings.m_spreadFactor); + } + + ui->messageLengthAuto->setChecked(m_settings.m_autoNbSymbolsMax); + ui->invertRamps->setChecked(m_settings.m_invertRamps); + + displaySquelch(); + updateIndexLabel(); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + + if (m_meshKeysButton) + { + const bool hasCustomKeys = !m_settings.m_meshtasticKeySpecList.trimmed().isEmpty(); + m_meshKeysButton->setText(hasCustomKeys ? tr("Keys*") : tr("Keys...")); + m_meshKeysButton->setToolTip(hasCustomKeys ? + tr("Custom Meshtastic decode keys configured. Click to edit.") : + tr("Open Meshtastic key manager.")); + } + + if (m_meshAutoSampleRateCheck) + { + m_meshControlsUpdating = true; + m_meshAutoSampleRateCheck->setChecked(m_settings.m_meshtasticAutoSampleRate); + m_meshControlsUpdating = false; + } + + if (m_meshAutoLockButton) + { + m_meshAutoLockButton->blockSignals(true); + m_meshAutoLockButton->setChecked(m_meshAutoLockActive); + m_meshAutoLockButton->setText(m_meshAutoLockActive ? tr("Locking...") : tr("Auto Lock")); + m_meshAutoLockButton->blockSignals(false); + } + + if (m_meshRegionCombo && m_meshPresetCombo && m_meshChannelCombo) + { + m_meshControlsUpdating = true; + + int regionIndex = m_meshRegionCombo->findData(m_settings.m_meshtasticRegionCode); + if (regionIndex < 0) { + regionIndex = m_meshRegionCombo->findData("US"); + } + if (regionIndex < 0) { + regionIndex = 0; + } + m_meshRegionCombo->setCurrentIndex(regionIndex); + + int presetIndex = m_meshPresetCombo->findData(m_settings.m_meshtasticPresetName); + if (presetIndex < 0) { + presetIndex = m_meshPresetCombo->findData("LONG_FAST"); + } + if (presetIndex < 0) { + presetIndex = 0; + } + m_meshPresetCombo->setCurrentIndex(presetIndex); + m_meshControlsUpdating = false; + + rebuildMeshtasticChannelOptions(); + + m_meshControlsUpdating = true; + int channelIndex = m_meshChannelCombo->findData(m_settings.m_meshtasticChannelIndex); + if (channelIndex < 0) { + channelIndex = 0; + } + m_meshChannelCombo->setCurrentIndex(channelIndex); + m_meshControlsUpdating = false; + } + + updateControlAvailabilityHints(); + blockApplySettings(false); +} + +void MeshtasticDemodGUI::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 MeshtasticDemodGUI::displayLoRaStatus(int headerParityStatus, bool headerCRCStatus, int payloadParityStatus, bool payloadCRCStatus) +{ + if (m_settings.m_hasHeader && (headerParityStatus == (int) MeshtasticDemodSettings::ParityOK)) { + ui->headerHammingStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (m_settings.m_hasHeader && (headerParityStatus == (int) MeshtasticDemodSettings::ParityError)) { + ui->headerHammingStatus->setStyleSheet("QLabel { background-color : red; }"); + } else if (m_settings.m_hasHeader && (headerParityStatus == (int) MeshtasticDemodSettings::ParityCorrected)) { + ui->headerHammingStatus->setStyleSheet("QLabel { background-color : blue; }"); + } else { + ui->headerHammingStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + } + + if (m_settings.m_hasHeader && headerCRCStatus) { + ui->headerCRCStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (m_settings.m_hasHeader && !headerCRCStatus) { + ui->headerCRCStatus->setStyleSheet("QLabel { background-color : red; }"); + } else { + ui->headerCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + } + + if (payloadParityStatus == (int) MeshtasticDemodSettings::ParityOK) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (payloadParityStatus == (int) MeshtasticDemodSettings::ParityError) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : red; }"); + } else if (payloadParityStatus == (int) MeshtasticDemodSettings::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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::displayFTStatus(int payloadParityStatus, bool payloadCRCStatus) +{ + if (payloadParityStatus == (int) MeshtasticDemodSettings::ParityOK) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (payloadParityStatus == (int) MeshtasticDemodSettings::ParityError) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : red; }"); + } else if (payloadParityStatus == (int) MeshtasticDemodSettings::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 MeshtasticDemodGUI::setBandwidths() +{ + int maxBandwidth = m_basebandSampleRate/MeshtasticDemodSettings::oversampling; + int maxIndex = 0; + + for (; (maxIndex < MeshtasticDemodSettings::nbBandwidths) && (MeshtasticDemodSettings::bandwidths[maxIndex] <= maxBandwidth); maxIndex++) + {} + + if (maxIndex != 0) + { + qDebug("MeshtasticDemodGUI::setBandwidths: avl: %d max: %d", maxBandwidth, MeshtasticDemodSettings::bandwidths[maxIndex-1]); + ui->BW->setMaximum(maxIndex - 1); + int index = ui->BW->value(); + ui->BWText->setText(QString("%1 Hz").arg(MeshtasticDemodSettings::bandwidths[index])); + } +} + +void MeshtasticDemodGUI::setupPipelineViews() +{ + if (!ui->textLayout || m_pipelineTabs) { + return; + } + + ui->textLayout->removeWidget(ui->messageText); + ui->messageText->hide(); + + m_pipelineTabs = new QTabWidget(this); + m_pipelineTabs->setTabPosition(QTabWidget::West); + m_pipelineTabs->setMovable(false); + m_pipelineTabs->setDocumentMode(true); + m_pipelineTabs->setMinimumHeight(ui->messageText->minimumHeight()); + ui->textLayout->addWidget(m_pipelineTabs); + + ensurePipelineView(-1, "All"); +} + +MeshtasticDemodGUI::PipelineView& MeshtasticDemodGUI::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, &MeshtasticDemodGUI::onPipelineTreeSelectionChanged); + QObject::connect(view.treeWidget, &QTreeWidget::itemClicked, this, &MeshtasticDemodGUI::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 MeshtasticDemodGUI::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(); + } + } + + if (ui->messageText) { + ui->messageText->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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::consumePipelineMessageKey(const QString& baseKey, const QString& key) +{ + auto pendingIt = m_pipelinePendingMessageKeysByBase.find(baseKey); + + if (pendingIt == m_pipelinePendingMessageKeysByBase.end()) { + return; + } + + QVector& pendingKeys = pendingIt.value(); + const int idx = pendingKeys.indexOf(key); + + if (idx >= 0) { + pendingKeys.removeAt(idx); + } + + if (pendingKeys.isEmpty()) { + m_pipelinePendingMessageKeysByBase.erase(pendingIt); + } +} + +void MeshtasticDemodGUI::rememberLoRaDechirpSnapshot( + const MeshtasticDemodMsg::MsgReportDecodeBytes& msg, + const QString& messageKey +) +{ + const std::vector>& lines = msg.getDechirpedSpectrum(); + + if (lines.empty()) { + return; + } + + if (messageKey.trimmed().isEmpty()) { + return; + } + + const QString key = messageKey; + DechirpSnapshot snapshot; + snapshot.fftSize = static_cast(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 MeshtasticDemodGUI::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 && 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 MeshtasticDemodGUI::updateDechirpModeUI() +{ + if (!m_dechirpLiveFollowButton) { + return; + } + + m_dechirpLiveFollowButton->setEnabled(m_dechirpInspectionActive); + m_dechirpLiveFollowButton->setText(tr("Live")); + m_dechirpLiveFollowButton->setToolTip(m_dechirpInspectionActive + ? tr("Return de-chirped spectrum to live follow mode.") + : tr("Already in live follow mode.")); +} + +void MeshtasticDemodGUI::queueReplayForTree(QTreeWidget *treeWidget) +{ + m_replayPendingMessageKey.clear(); + m_replayPendingHasSelection = false; + + if (treeWidget) + { + const QList 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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::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 MeshtasticDemodGUI::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(snapshot.lines.front().size()); + + if (fftSize <= 0) { + return; + } + hardResetDechirpDisplayBuffers(); + + std::vector line(static_cast(fftSize), static_cast(-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* powers) { + if (powers == nullptr) + { + std::fill(line.begin(), line.end(), static_cast(-120.0f)); + } + else + { + std::fill(line.begin(), line.end(), static_cast(-120.0f)); + const int count = std::min(fftSize, static_cast(powers->size())); + + for (int i = 0; i < count; ++i) + { + const float power = std::max((*powers)[static_cast(i)], 1e-12f); + line[static_cast(i)] = static_cast(10.0f * std::log10(power)); + } + } + + ui->glSpectrum->newSpectrum(line.data(), fftSize, 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& powers : snapshot.lines) + { + feedLineToSpectrum(&powers); + + if ((++lineCounter % replayChunkLines) == 0) { + flushReplayChunk(); + } + } + + flushReplayChunk(); +} + +void MeshtasticDemodGUI::onPipelineTreeSelectionChanged() +{ + QTreeWidget *treeWidget = qobject_cast(sender()); + + if (!treeWidget) { + return; + } + + queueReplayForTree(treeWidget); +} + +void MeshtasticDemodGUI::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 MeshtasticDemodGUI::appendPipelineStatusLine(int pipelineId, const QString& pipelineName, const QString& status) +{ + appendPipelineLogLine(pipelineId, pipelineName, QString(">%1").arg(status)); +} + +void MeshtasticDemodGUI::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(static_cast(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 MeshtasticDemodGUI::appendPipelineTreeFields( + int pipelineId, + const QString& pipelineName, + const QString& messageTitle, + const QVector>& fields, + const QString& messageKey +) +{ + auto fieldValue = [&fields](const QString& path) -> QString { + for (const QPair& 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 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& 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 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 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 MeshtasticDemodGUI::showLoRaMessage(const Message& message) +{ + const MeshtasticDemodMsg::MsgReportDecodeBytes& msg = (MeshtasticDemodMsg::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; + + if (m_settings.m_hasHeader) + { + ui->fecParity->setValue(msg.getNbParityBits()); + ui->fecParityText->setText(tr("%1").arg(msg.getNbParityBits())); + ui->crc->setChecked(msg.getHasCRC()); + ui->packetLength->setValue(msg.getPacketSize()); + ui->packetLengthText->setText(tr("%1").arg(msg.getPacketSize())); + packetLength = msg.getPacketSize(); + } + else + { + packetLength = m_settings.m_packetLength; + } + + 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) MeshtasticDemodSettings::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 Meshtastic parsing + // does not yield structured fields. + QVector> fallbackFields; + const QByteArray payloadBytes = bytes.left(static_cast(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 MeshtasticDemodGUI::showTextMessage(const Message& message) +{ + const MeshtasticDemodMsg::MsgReportDecodeString& msg = (MeshtasticDemodMsg::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() && (m_settings.m_codingScheme != MeshtasticDemodSettings::CodingLoRa)) { + messageKey = allocatePipelineMessageKey(messageBaseKey); + } + + appendPipelineTreeFields(pipelineId, pipelineName, title, msg.getStructuredFields(), messageKey); + + if (!messageKey.isEmpty()) { + consumePipelineMessageKey(messageBaseKey, messageKey); + } + } +} + +void MeshtasticDemodGUI::showFTMessage(const Message& message) +{ + const MeshtasticDemodMsg::MsgReportDecodeFT& msg = (MeshtasticDemodMsg::MsgReportDecodeFT&) 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 FEC:%4 CRC:%5") + .arg(dateStr) + .arg(msg.getSingalDb(), 0, 'f', 1) + .arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1) + .arg(getParityStr(msg.getPayloadParityStatus())) + .arg(msg.getPayloadCRCStatus() ? "ok" : "err"); + + appendPipelineStatusLine(pipelineId, pipelineName, status); + appendPipelineLogLine(pipelineId, pipelineName, QString("TXT|%1").arg(msg.getMessage())); // We do not show constituents of the message (call1, ...) + displayFTStatus(msg.getPayloadParityStatus(), msg.getPayloadCRCStatus()); +} + +void MeshtasticDemodGUI::displayText(const QString& text) +{ + if (m_pipelineTabs) + { + appendPipelineLogLine(-1, "All", QString("TXT|%1").arg(text)); + return; + } + + QTextCursor cursor = ui->messageText->textCursor(); + cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); + if (!ui->messageText->document()->isEmpty()) { + cursor.insertText("\n"); + } + cursor.insertText(tr("TXT|%1").arg(text)); + alignTextViewToLatestLineLeft(ui->messageText); +} + +void MeshtasticDemodGUI::displayBytes(const QByteArray& bytes) +{ + if (m_pipelineTabs) + { + appendPipelineBytes(-1, "All", bytes); + return; + } + + QTextCursor cursor = ui->messageText->textCursor(); + cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); + + if (!ui->messageText->document()->isEmpty()) { + cursor.insertText("\n"); + } + + QByteArray::const_iterator it = bytes.begin(); + unsigned int i = 0; + + for (;it != bytes.end(); ++it, i++) + { + unsigned char b = *it; + + if (i%16 == 0) { + cursor.insertText(tr("%1|").arg(i, 3, 10, QChar('0'))); + } + + cursor.insertText(tr("%1").arg(b, 2, 16, QChar('0'))); + + if (i%16 == 15) { + cursor.insertText("\n"); + } else if (i%4 == 3) { + cursor.insertText("|"); + } else { + cursor.insertText(" "); + } + } + + alignTextViewToLatestLineLeft(ui->messageText); +} + +void MeshtasticDemodGUI::displayStatus(const QString& status) +{ + if (m_pipelineTabs) + { + appendPipelineStatusLine(-1, "All", status); + qInfo() << "MeshtasticDemodGUI::displayStatus:" << status; + return; + } + + QTextCursor cursor = ui->messageText->textCursor(); + cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); + + if (!ui->messageText->document()->isEmpty()) { + cursor.insertText("\n"); + } + + cursor.insertText(tr(">%1").arg(status)); + alignTextViewToLatestLineLeft(ui->messageText); + qInfo() << "MeshtasticDemodGUI::displayStatus:" << status; +} + +QString MeshtasticDemodGUI::getParityStr(int parityStatus) +{ + if (parityStatus == (int) MeshtasticDemodSettings::ParityError) { + return "err"; + } else if (parityStatus == (int) MeshtasticDemodSettings::ParityCorrected) { + return "fix"; + } else if (parityStatus == (int) MeshtasticDemodSettings::ParityOK) { + return "ok"; + } else { + return "n/a"; + } +} + +void MeshtasticDemodGUI::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() << "MeshtasticDemodGUI::tick: RemoteTCP input running - waiting for first DSP notification to reapply Meshtastic profile"; + } + + if (m_remoteTcpReconnectAutoApplyPending && running) + { + m_remoteTcpReconnectAutoApplyWaitTicks++; + + if (m_remoteTcpReconnectAutoApplyWaitTicks >= 20) + { + m_remoteTcpReconnectAutoApplyPending = false; + m_remoteTcpReconnectAutoApplyWaitTicks = 0; + qInfo() << "MeshtasticDemodGUI::tick: RemoteTCP reconnect fallback timeout - reapplying Meshtastic profile"; + QMetaObject::invokeMethod(this, &MeshtasticDemodGUI::applyMeshtasticProfileFromSelection, 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_chirpChatDemod->getCurrentNoiseLevel()), 0, 'f', 1)); + ui->channelPower->setText(tr("%1 dB").arg(CalcDb::dbPower(m_chirpChatDemod->getTotalPower()), 0, 'f', 1)); + + if (m_chirpChatDemod->getDemodActive()) { + ui->mute->setStyleSheet("QToolButton { background-color : green; }"); + } else { + ui->mute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + } + } +} + +void MeshtasticDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &MeshtasticDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->BW, &QSlider::valueChanged, this, &MeshtasticDemodGUI::on_BW_valueChanged); + QObject::connect(ui->Spread, &QSlider::valueChanged, this, &MeshtasticDemodGUI::on_Spread_valueChanged); + QObject::connect(ui->deBits, &QSlider::valueChanged, this, &MeshtasticDemodGUI::on_deBits_valueChanged); + QObject::connect(ui->fftWindow, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticDemodGUI::on_fftWindow_currentIndexChanged); + QObject::connect(ui->preambleChirps, &QSlider::valueChanged, this, &MeshtasticDemodGUI::on_preambleChirps_valueChanged); + QObject::connect(ui->scheme, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticDemodGUI::on_scheme_currentIndexChanged); + QObject::connect(ui->mute, &QToolButton::toggled, this, &MeshtasticDemodGUI::on_mute_toggled); + QObject::connect(ui->clear, &QPushButton::clicked, this, &MeshtasticDemodGUI::on_clear_clicked); + QObject::connect(ui->eomSquelch, &QDial::valueChanged, this, &MeshtasticDemodGUI::on_eomSquelch_valueChanged); + QObject::connect(ui->messageLength, &QDial::valueChanged, this, &MeshtasticDemodGUI::on_messageLength_valueChanged); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QObject::connect(ui->messageLengthAuto, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_messageLengthAuto_stateChanged(static_cast(state)); }); + QObject::connect(ui->header, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_header_stateChanged(static_cast(state)); }); +#else + QObject::connect(ui->messageLengthAuto, &QCheckBox::stateChanged, this, &MeshtasticDemodGUI::on_messageLengthAuto_stateChanged); + QObject::connect(ui->header, &QCheckBox::stateChanged, this, &MeshtasticDemodGUI::on_header_stateChanged); +#endif + QObject::connect(ui->fecParity, &QDial::valueChanged, this, &MeshtasticDemodGUI::on_fecParity_valueChanged); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QObject::connect(ui->crc, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_crc_stateChanged(static_cast(state)); }); +#else + QObject::connect(ui->crc, &QCheckBox::stateChanged, this, &MeshtasticDemodGUI::on_crc_stateChanged); +#endif + QObject::connect(ui->packetLength, &QDial::valueChanged, this, &MeshtasticDemodGUI::on_packetLength_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(state)); }); +#else + QObject::connect(ui->udpSend, &QCheckBox::stateChanged, this, &MeshtasticDemodGUI::on_udpSend_stateChanged); +#endif + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &MeshtasticDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &MeshtasticDemodGUI::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(state)); }); +#else + QObject::connect(ui->invertRamps, &QCheckBox::stateChanged, this, &MeshtasticDemodGUI::on_invertRamps_stateChanged); +#endif +} + +void MeshtasticDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h new file mode 100644 index 000000000..e5dae6b14 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.h @@ -0,0 +1,251 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2014 John Greb // +// Copyright (C) 2015-2020, 2022 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODGUI_H +#define INCLUDE_MESHTASTICDEMODGUI_H + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include +#include +#include + +#include "meshtasticdemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class MeshtasticDemod; +class SpectrumVis; +class BasebandSampleSink; +class QComboBox; +class QCheckBox; +class QPushButton; +class QTabWidget; +class QPlainTextEdit; +class QTreeWidget; +class QTreeWidgetItem; +namespace MeshtasticDemodMsg { class MsgReportDecodeBytes; } + +namespace Ui { + class MeshtasticDemodGUI; +} + +class MeshtasticDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static MeshtasticDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceAPI, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual void setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; }; + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; }; + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; }; + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; }; + virtual QString getTitle() const { return m_settings.m_title; }; + virtual QColor getTitleColor() const { return m_settings.m_rgbColor; }; + virtual void zetHidden(bool hidden) { m_settings.m_hidden = hidden; } + virtual bool getHidden() const { return m_settings.m_hidden; } + virtual ChannelMarker& getChannelMarker() { return m_channelMarker; } + virtual int getStreamIndex() const { return m_settings.m_streamIndex; } + virtual void setStreamIndex(int streamIndex) { m_settings.m_streamIndex = streamIndex; } + +private slots: + void channelMarkerChangedByCursor(); + void on_deltaFrequency_changed(qint64 value); + void on_BW_valueChanged(int value); + void on_Spread_valueChanged(int value); + void on_deBits_valueChanged(int value); + void on_fftWindow_currentIndexChanged(int index); + void on_preambleChirps_valueChanged(int value); + void on_scheme_currentIndexChanged(int index); + void on_mute_toggled(bool checked); + void on_clear_clicked(bool checked); + void on_eomSquelch_valueChanged(int value); + void on_messageLength_valueChanged(int value); + void on_messageLengthAuto_stateChanged(int state); + void on_header_stateChanged(int state); + void on_fecParity_valueChanged(int value); + void on_crc_stateChanged(int state); + void on_packetLength_valueChanged(int value); + void on_udpSend_stateChanged(int state); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + void on_invertRamps_stateChanged(int state); + void on_meshRegion_currentIndexChanged(int index); + void on_meshPreset_currentIndexChanged(int index); + void on_meshChannel_currentIndexChanged(int index); + void on_meshApply_clicked(bool checked); + void on_meshKeys_clicked(bool checked); + void on_meshAutoSampleRate_toggled(bool checked); + void on_meshAutoLock_clicked(bool checked); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void channelMarkerHighlightedByCursor(); + void handleInputMessages(); + void onPipelineTreeSelectionChanged(); + void tick(); + +private: + Ui::MeshtasticDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + MeshtasticDemodSettings m_settings; + qint64 m_deviceCenterFrequency; + int m_basebandSampleRate; + bool m_doApplySettings; + + MeshtasticDemod* m_chirpChatDemod; + SpectrumVis* m_spectrumVis; + QComboBox* m_meshRegionCombo; + QComboBox* m_meshPresetCombo; + QComboBox* m_meshChannelCombo; + QPushButton* m_meshApplyButton; + QPushButton* m_meshKeysButton; + QPushButton* m_meshAutoLockButton; + QPushButton* m_dechirpLiveFollowButton; + QCheckBox* m_meshAutoSampleRateCheck; + struct PipelineView + { + QWidget *tabWidget = nullptr; + QPlainTextEdit *logText = nullptr; + QTreeWidget *treeWidget = nullptr; + }; + QTabWidget *m_pipelineTabs; + QMap m_pipelineViews; + bool m_meshControlsUpdating; + struct MeshAutoLockCandidate { + int inputOffsetHz; + bool invertRamps; + int deBits; + double score; + int samples; + double sourceScore; + int sourceSamples; + int syncWordZeroCount; + int headerParityOkOrFixCount; + int headerCRCCount; + int payloadCRCCount; + int earlyEOMCount; + }; + QVector m_meshAutoLockCandidates; + bool m_meshAutoLockActive; + int m_meshAutoLockCandidateIndex; + qint64 m_meshAutoLockCandidateStartMs; + int m_meshAutoLockObservedSamplesForCandidate; + int m_meshAutoLockObservedSourceSamplesForCandidate; + int m_meshAutoLockTotalDecodeSamples; + bool m_meshAutoLockTrafficSeen; + int m_meshAutoLockActivityTicks; + qint64 m_meshAutoLockArmStartMs; + int m_meshAutoLockBaseOffsetHz; + bool m_meshAutoLockBaseInvert; + int m_meshAutoLockBaseDeBits; + bool m_remoteTcpReconnectAutoApplyPending; + int m_remoteTcpReconnectAutoApplyWaitTicks; + bool m_remoteTcpLastRunningState; + struct DechirpSnapshot + { + int fftSize = 0; + std::vector> lines; + }; + QMap m_dechirpSnapshots; + QVector m_dechirpSnapshotOrder; + bool m_dechirpInspectionActive; + QString m_dechirpSelectedMessageKey; + QString m_replayPendingMessageKey; + bool m_replayPendingHasSelection; + bool m_replaySelectionQueued; + QMap m_pipelineMessageKeyByBase; + QMap> m_pipelinePendingMessageKeysByBase; + quint64 m_pipelineMessageSequence; + MessageQueue m_inputMessageQueue; + unsigned int m_tickCount; + + explicit MeshtasticDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~MeshtasticDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void updateControlAvailabilityHints(); + void displaySquelch(); + void setBandwidths(); + void showLoRaMessage(const Message& message); //!< For LoRa coding scheme + void showTextMessage(const Message& message); //!< For TTY and ASCII + void showFTMessage(const Message& message); //!< For FT coding scheme + void setupPipelineViews(); + PipelineView& ensurePipelineView(int pipelineId, const QString& pipelineName); + void clearPipelineViews(); + void appendPipelineLogLine(int pipelineId, const QString& pipelineName, const QString& line); + void appendPipelineStatusLine(int pipelineId, const QString& pipelineName, const QString& status); + void appendPipelineBytes(int pipelineId, const QString& pipelineName, const QByteArray& bytes); + void appendPipelineTreeFields( + int pipelineId, + const QString& pipelineName, + const QString& messageTitle, + const QVector>& fields, + const QString& messageKey + ); + void displayText(const QString& text); + void displayBytes(const QByteArray& bytes); + void displayStatus(const QString& status); + void displayLoRaStatus(int headerParityStatus, bool headerCRCStatus, int payloadParityStatus, bool payloadCRCStatus); + void displayFTStatus(int payloadParityStatus, bool payloadCRCStatus); + QString getParityStr(int parityStatus); + void resetLoRaStatus(); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + void setupMeshtasticAutoProfileControls(); + void rebuildMeshtasticChannelOptions(); + bool retuneDeviceToFrequency(qint64 centerFrequencyHz); + bool autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary); + int findBandwidthIndex(int bandwidthHz) const; + void applyMeshtasticProfileFromSelection(); + void editMeshtasticKeys(); + void startMeshAutoLock(); + void stopMeshAutoLock(bool keepBestCandidate); + void applyMeshAutoLockCandidate(const MeshAutoLockCandidate& candidate, bool applySettingsNow); + void handleMeshAutoLockObservation(const MeshtasticDemodMsg::MsgReportDecodeBytes& msg); + void handleMeshAutoLockSourceObservation(); + void advanceMeshAutoLock(); + QString buildPipelineMessageBaseKey(int pipelineId, uint32_t frameId, const QString& timestamp) const; + QString allocatePipelineMessageKey(const QString& baseKey); + QString resolvePipelineMessageKey(const QString& baseKey) const; + void consumePipelineMessageKey(const QString& baseKey, const QString& key); + void rememberLoRaDechirpSnapshot(const MeshtasticDemodMsg::MsgReportDecodeBytes& msg, const QString& messageKey); + void setDechirpInspectionMode(bool enabled); + void updateDechirpModeUI(); + void queueReplayForTree(QTreeWidget *treeWidget); + void processQueuedReplay(); + void hardResetDechirpDisplayBuffers(); + void clearTreeMessageKeyReferences(const QString& messageKey); + void replayDechirpSnapshot(const DechirpSnapshot& snapshot); +}; + +#endif // INCLUDE_MESHTASTICDEMODGUI_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui new file mode 100644 index 000000000..6e1e05546 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.ui @@ -0,0 +1,1286 @@ + + + MeshtasticDemodGUI + + + + 0 + 0 + 579 + 680 + + + + + 532 + 680 + + + + + Liberation Sans + 9 + + + + Meshtastic Demodulator + + + + + 0 + 10 + 571 + 91 + + + + RF/demod settings + + + + 2 + + + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 60 + 0 + + + + De-chirped channel power + + + Qt::RightToLeft + + + -100.0 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + BW + + + + + + + Bandwidth + + + 0 + + + 10 + + + 1 + + + 5 + + + Qt::Horizontal + + + + + + + 500000 Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + N + + + + + + + De-chirped noise maximum power + + + -50.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + S + + + + + + + + 30 + 0 + + + + De-chirped signal maximum power + + + -50.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + / + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 25 + 0 + + + + De-chirped Signal to Noise Ratio + + + -10.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + FFTW + + + + + + + + Liberation Sans + 8 + + + + + Bart + + + + + B-H + + + + + FT + + + + + Ham + + + + + Han + + + + + Rec + + + + + Kai + + + + + Black + + + + + B-H7 + + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + SF + + + + + + + Spreading factor + + + 7 + + + 12 + + + 1 + + + 10 + + + 10 + + + Qt::Horizontal + + + + + + + + 20 + 0 + + + + 10 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + DE + + + + + + + Distance Enhancement bits i.e. log2 of number of FFT bins per effective sample + + + 0 + + + 4 + + + 1 + + + 0 + + + 0 + + + Qt::Horizontal + + + + + + + + 15 + 0 + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + Pre + + + + + + + Expected number of preamble chirps + + + 4 + + + 32 + + + 1 + + + 8 + + + 8 + + + Qt::Horizontal + + + + + + + + 20 + 0 + + + + 8 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Invert preamble, SFD and payload ramps + + + Inv + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + 100 + 572 + 301 + + + + Payload + + + + 2 + + + + + + + Scheme + + + + + + + + LoRa + + + + + ASCII + + + + + TTY + + + + + FT + + + + + + + + Run/Stop decoder + + + + + + + :/stop.png + :/play.png:/stop.png + + + true + + + + + + + EOM + + + + + + + + 22 + 22 + + + + End Of Message squelch factor + + + 40 + + + 121 + + + 1 + + + 60 + + + + + + + 10.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + ML + + + + + + + + 22 + 22 + + + + Message (payload) length in number of symbols + + + 8 + + + 255 + + + 1 + + + 127 + + + + + + + 255 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + Set message length in symbols automatically to provided message length (LoRa and FT only) + + + Auto + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + LoRa + + + + + + + Expect header (explicit) - disables manual FEC and CRC + + + HDR + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + FEC + + + + + + + + 22 + 22 + + + + Number of FEC parity bits (0 to 4) for Hamming code + + + 4 + + + 1 + + + 1 + + + + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + CRC appended to payload + + + CRC + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + + 22 + 22 + + + + Payload packet length in number of bytes or characters + + + 225 + + + 1 + + + 30 + + + + + + + Pkt + + + + + + + 255 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + Number of codewords in the payload with header and CRC + + + --- + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + / + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Number of symbols in the payload with header and CRC + + + --- + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 5 + 0 + + + + Qt::Vertical + + + + + + + Header FEC parity status + + + HF + + + + + + + Header CRC status + + + HC + + + + + + + Payload FEC parity status + + + FEC + + + + + + + Payload CRC status + + + CRC + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 6 + + + 6 + + + + + + + Msg + + + + + + + + 24 + 24 + + + + Clear text + + + + + + + :/sweep.png:/sweep.png + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 200 + + + + + Liberation Mono + 9 + + + + + + + + + + + + Send message via UDP + + + Qt::RightToLeft + + + UDP + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 9998 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 410 + 571 + 260 + + + + + 373 + 0 + + + + De-chirped Spectrum + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 230 + + + + + + + + + + + + + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + GLSpectrum + QWidget +
gui/glspectrum.h
+ 1 +
+ + GLSpectrumGUI + QWidget +
gui/glspectrumgui.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+
+ + + + +
diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodmsg.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodmsg.cpp new file mode 100644 index 000000000..cd6ca57c5 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodmsg.cpp @@ -0,0 +1,27 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 "meshtasticdemodmsg.h" + +MESSAGE_CLASS_DEFINITION(MeshtasticDemodMsg::MsgDecodeSymbols, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticDemodMsg::MsgLoRaHeaderProbe, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticDemodMsg::MsgLoRaHeaderFeedback, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticDemodMsg::MsgReportDecodeBytes, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticDemodMsg::MsgReportDecodeString, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticDemodMsg::MsgReportDecodeFT, Message) diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodmsg.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodmsg.h new file mode 100644 index 000000000..e5ddb9290 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodmsg.h @@ -0,0 +1,534 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODMSG_H +#define INCLUDE_MESHTASTICDEMODMSG_H + +#include + +#include +#include +#include +#include "util/message.h" + +#include "meshtasticdemodsettings.h" + +namespace MeshtasticDemodMsg +{ + class MsgDecodeSymbols : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const std::vector& getSymbols() const { return m_symbols; } + const std::vector>& getMagnitudes() const { return m_magnitudes; } + const std::vector>& getDechirpedSpectrum() const { return m_dechirpedSpectrum; } + uint32_t getFrameId() const { return m_frameId; } + unsigned int getSyncWord() const { return m_syncWord; } + float getSingalDb() const { return m_signalDb; } + float getNoiseDb() const { return m_noiseDb; } + + void pushBackSymbol(unsigned short symbol) { + m_symbols.push_back(symbol); + } + void popSymbol() { + m_symbols.pop_back(); + } + void setSyncWord(unsigned char syncWord) { + m_syncWord = syncWord; + } + void setSignalDb(float db) { + m_signalDb = db; + } + void setNoiseDb(float db) { + m_noiseDb = db; + } + void setFrameId(uint32_t frameId) { + m_frameId = frameId; + } + + void pushBackMagnitudes(const std::vector& magnitudes) { + m_magnitudes.push_back(magnitudes); + } + void pushBackDechirpedSpectrumLine(const std::vector& spectrumLine) { + m_dechirpedSpectrum.push_back(spectrumLine); + } + + static MsgDecodeSymbols* create() { + return new MsgDecodeSymbols(); + } + static MsgDecodeSymbols* create(const std::vector symbols) { + return new MsgDecodeSymbols(symbols); + } + + private: + std::vector m_symbols; + std::vector> m_magnitudes; + std::vector> m_dechirpedSpectrum; + uint32_t m_frameId; + unsigned int m_syncWord; + float m_signalDb; + float m_noiseDb; + + MsgDecodeSymbols() : //!< create an empty message + Message(), + m_frameId(0), + m_syncWord(0), + m_signalDb(0.0), + m_noiseDb(0.0) + {} + MsgDecodeSymbols(const std::vector symbols) : //!< create a message with symbols copy + Message(), + m_frameId(0), + m_syncWord(0), + m_signalDb(0.0), + m_noiseDb(0.0) + { m_symbols = symbols; } + }; + + class MsgLoRaHeaderProbe : public Message { + MESSAGE_CLASS_DECLARATION + + public: + uint32_t getFrameId() const { return m_frameId; } + const std::vector& getSymbols() const { return m_symbols; } + unsigned int getPayloadNbSymbolBits() const { return m_payloadNbSymbolBits; } + unsigned int getHeaderNbSymbolBits() const { return m_headerNbSymbolBits; } + unsigned int getSpreadFactor() const { return m_spreadFactor; } + unsigned int getBandwidth() const { return m_bandwidth; } + bool getHasHeader() const { return m_hasHeader; } + bool getHasCRC() const { return m_hasCRC; } + + static MsgLoRaHeaderProbe* create( + uint32_t frameId, + const std::vector& symbols, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + unsigned int spreadFactor, + unsigned int bandwidth, + bool hasHeader, + bool hasCRC + ) { + return new MsgLoRaHeaderProbe( + frameId, + symbols, + payloadNbSymbolBits, + headerNbSymbolBits, + spreadFactor, + bandwidth, + hasHeader, + hasCRC + ); + } + + private: + uint32_t m_frameId; + std::vector m_symbols; + unsigned int m_payloadNbSymbolBits; + unsigned int m_headerNbSymbolBits; + unsigned int m_spreadFactor; + unsigned int m_bandwidth; + bool m_hasHeader; + bool m_hasCRC; + + MsgLoRaHeaderProbe( + uint32_t frameId, + const std::vector& symbols, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + unsigned int spreadFactor, + unsigned int bandwidth, + bool hasHeader, + bool hasCRC + ) : + Message(), + m_frameId(frameId), + m_symbols(symbols), + m_payloadNbSymbolBits(payloadNbSymbolBits), + m_headerNbSymbolBits(headerNbSymbolBits), + m_spreadFactor(spreadFactor), + m_bandwidth(bandwidth), + m_hasHeader(hasHeader), + m_hasCRC(hasCRC) + {} + }; + + class MsgLoRaHeaderFeedback : public Message { + MESSAGE_CLASS_DECLARATION + + public: + uint32_t getFrameId() const { return m_frameId; } + bool isValid() const { return m_valid; } + bool getHasCRC() const { return m_hasCRC; } + unsigned int getNbParityBits() const { return m_nbParityBits; } + unsigned int getPacketLength() const { return m_packetLength; } + bool getLdro() const { return m_ldro; } + unsigned int getExpectedSymbols() const { return m_expectedSymbols; } + int getHeaderParityStatus() const { return m_headerParityStatus; } + bool getHeaderCRCStatus() const { return m_headerCRCStatus; } + + static MsgLoRaHeaderFeedback* create( + uint32_t frameId, + bool valid, + bool hasCRC, + unsigned int nbParityBits, + unsigned int packetLength, + bool ldro, + unsigned int expectedSymbols, + int headerParityStatus, + bool headerCRCStatus + ) { + return new MsgLoRaHeaderFeedback( + frameId, + valid, + hasCRC, + nbParityBits, + packetLength, + ldro, + expectedSymbols, + headerParityStatus, + headerCRCStatus + ); + } + + private: + uint32_t m_frameId; + bool m_valid; + bool m_hasCRC; + unsigned int m_nbParityBits; + unsigned int m_packetLength; + bool m_ldro; + unsigned int m_expectedSymbols; + int m_headerParityStatus; + bool m_headerCRCStatus; + + MsgLoRaHeaderFeedback( + uint32_t frameId, + bool valid, + bool hasCRC, + unsigned int nbParityBits, + unsigned int packetLength, + bool ldro, + unsigned int expectedSymbols, + int headerParityStatus, + bool headerCRCStatus + ) : + Message(), + m_frameId(frameId), + m_valid(valid), + m_hasCRC(hasCRC), + m_nbParityBits(nbParityBits), + m_packetLength(packetLength), + m_ldro(ldro), + m_expectedSymbols(expectedSymbols), + m_headerParityStatus(headerParityStatus), + m_headerCRCStatus(headerCRCStatus) + {} + }; + + class MsgReportDecodeBytes : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QByteArray& getBytes() const { return m_bytes; } + uint32_t getFrameId() const { return m_frameId; } + unsigned int getSyncWord() const { return m_syncWord; } + float getSingalDb() const { return m_signalDb; } + float getNoiseDb() const { return m_noiseDb; } + const QString& getMsgTimestamp() const { return m_msgTimestamp; } + unsigned int getPacketSize() const { return m_packetSize; } + unsigned int getNbParityBits() const { return m_nbParityBits; } + unsigned int getNbSymbols() const { return m_nbSymbols; } + unsigned int getNbCodewords() const { return m_nbCodewords; } + bool getHasCRC() const { return m_hasCRC; } + bool getEarlyEOM() const { return m_earlyEOM; } + int getHeaderParityStatus() const { return m_headerParityStatus; } + bool getHeaderCRCStatus() const { return m_headerCRCStatus; } + int getPayloadParityStatus() const { return m_payloadParityStatus; } + bool getPayloadCRCStatus() const { return m_payloadCRCStatus; } + int getPipelineId() const { return m_pipelineId; } + const QString& getPipelineName() const { return m_pipelineName; } + const QString& getPipelinePreset() const { return m_pipelinePreset; } + const std::vector>& getDechirpedSpectrum() const { return m_dechirpedSpectrum; } + + static MsgReportDecodeBytes* create(const QByteArray& bytes) { + return new MsgReportDecodeBytes(bytes); + } + void setSyncWord(unsigned int syncWord) { + m_syncWord = syncWord; + } + void setFrameId(uint32_t frameId) { + m_frameId = frameId; + } + void setSignalDb(float db) { + m_signalDb = db; + } + void setNoiseDb(float db) { + m_noiseDb = db; + } + void setMsgTimestamp(const QString& ts) { + m_msgTimestamp = ts; + } + void setPacketSize(unsigned int packetSize) { + m_packetSize = packetSize; + } + void setNbParityBits(unsigned int nbParityBits) { + m_nbParityBits = nbParityBits; + } + void setNbSymbols(unsigned int nbSymbols) { + m_nbSymbols = nbSymbols; + } + void setNbCodewords(unsigned int nbCodewords) { + m_nbCodewords = nbCodewords; + } + void setHasCRC(bool hasCRC) { + m_hasCRC = hasCRC; + } + void setEarlyEOM(bool earlyEOM) { + m_earlyEOM = earlyEOM; + } + void setHeaderParityStatus(int headerParityStatus) { + m_headerParityStatus = headerParityStatus; + } + void setHeaderCRCStatus(bool headerCRCStatus) { + m_headerCRCStatus = headerCRCStatus; + } + void setPayloadParityStatus(int payloadParityStatus) { + m_payloadParityStatus = payloadParityStatus; + } + void setPayloadCRCStatus(bool payloadCRCStatus) { + m_payloadCRCStatus = payloadCRCStatus; + } + void setPipelineMetadata(int pipelineId, const QString& pipelineName, const QString& pipelinePreset) { + m_pipelineId = pipelineId; + m_pipelineName = pipelineName; + m_pipelinePreset = pipelinePreset; + } + void setDechirpedSpectrum(const std::vector>& dechirpedSpectrum) { + m_dechirpedSpectrum = dechirpedSpectrum; + } + + private: + QByteArray m_bytes; + uint32_t m_frameId; + unsigned int m_syncWord; + float m_signalDb; + float m_noiseDb; + QString m_msgTimestamp; + unsigned int m_packetSize; + unsigned int m_nbParityBits; + unsigned int m_nbSymbols; + unsigned int m_nbCodewords; + bool m_hasCRC; + bool m_earlyEOM; + int m_headerParityStatus; + bool m_headerCRCStatus; + int m_payloadParityStatus; + bool m_payloadCRCStatus; + int m_pipelineId; + QString m_pipelineName; + QString m_pipelinePreset; + std::vector> m_dechirpedSpectrum; + + MsgReportDecodeBytes(const QByteArray& bytes) : + Message(), + m_bytes(bytes), + m_frameId(0), + m_syncWord(0), + m_signalDb(0.0), + m_noiseDb(0.0), + m_packetSize(0), + m_nbParityBits(0), + m_nbSymbols(0), + m_nbCodewords(0), + m_hasCRC(false), + m_earlyEOM(false), + m_headerParityStatus(false), + m_headerCRCStatus(false), + m_payloadParityStatus((int) MeshtasticDemodSettings::ParityUndefined), + m_payloadCRCStatus(false), + m_pipelineId(-1) + { } + }; + + class MsgReportDecodeString : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getString() const { return m_str; } + uint32_t getFrameId() const { return m_frameId; } + unsigned int getSyncWord() const { return m_syncWord; } + float getSingalDb() const { return m_signalDb; } + float getNoiseDb() const { return m_noiseDb; } + const QString& getMsgTimestamp() const { return m_msgTimestamp; } + int getPipelineId() const { return m_pipelineId; } + const QString& getPipelineName() const { return m_pipelineName; } + const QString& getPipelinePreset() const { return m_pipelinePreset; } + const QVector>& getStructuredFields() const { return m_structuredFields; } + bool hasStructuredFields() const { return !m_structuredFields.isEmpty(); } + + static MsgReportDecodeString* create(const QString& str) + { + return new MsgReportDecodeString(str); + } + void setSyncWord(unsigned int syncWord) { + m_syncWord = syncWord; + } + void setFrameId(uint32_t frameId) { + m_frameId = frameId; + } + void setSignalDb(float db) { + m_signalDb = db; + } + void setNoiseDb(float db) { + m_noiseDb = db; + } + void setMsgTimestamp(const QString& ts) { + m_msgTimestamp = ts; + } + void setPipelineMetadata(int pipelineId, const QString& pipelineName, const QString& pipelinePreset) { + m_pipelineId = pipelineId; + m_pipelineName = pipelineName; + m_pipelinePreset = pipelinePreset; + } + void setStructuredFields(const QVector>& fields) { + m_structuredFields = fields; + } + void addStructuredField(const QString& path, const QString& value) { + m_structuredFields.append(qMakePair(path, value)); + } + + private: + QString m_str; + uint32_t m_frameId; + unsigned int m_syncWord; + float m_signalDb; + float m_noiseDb; + QString m_msgTimestamp; + int m_pipelineId; + QString m_pipelineName; + QString m_pipelinePreset; + QVector> m_structuredFields; + + MsgReportDecodeString(const QString& str) : + Message(), + m_str(str), + m_frameId(0), + m_syncWord(0), + m_signalDb(0.0), + m_noiseDb(0.0), + m_pipelineId(-1) + { } + }; + + class MsgReportDecodeFT : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getMessage() const { return m_message; } + const QString& getCall1() const { return m_call1; } + const QString& getCall2() const { return m_call2; } + const QString& getLoc() const { return m_loc; } + bool isReply() const { return m_reply; } + bool isFreeText() const { return m_freeText; } + unsigned int getSyncWord() const { return m_syncWord; } + float getSingalDb() const { return m_signalDb; } + float getNoiseDb() const { return m_noiseDb; } + const QString& getMsgTimestamp() const { return m_msgTimestamp; } + int getPayloadParityStatus() const { return m_payloadParityStatus; } + bool getPayloadCRCStatus() const { return m_payloadCRCStatus; } + int getPipelineId() const { return m_pipelineId; } + const QString& getPipelineName() const { return m_pipelineName; } + const QString& getPipelinePreset() const { return m_pipelinePreset; } + + static MsgReportDecodeFT* create() + { + return new MsgReportDecodeFT(); + } + void setMessage(const QString& message) { + m_message = message; + } + void setCall1(const QString& call1) { + m_call1 = call1; + } + void setCall2(const QString& call2) { + m_call2 = call2; + } + void setLoc(const QString& loc) { + m_loc = loc; + } + void setReply(bool reply) { + m_reply = reply; + } + void setFreeText(bool freeText) { + m_freeText = freeText; + } + void setSyncWord(unsigned int syncWord) { + m_syncWord = syncWord; + } + void setSignalDb(float db) { + m_signalDb = db; + } + void setNoiseDb(float db) { + m_noiseDb = db; + } + void setMsgTimestamp(const QString& ts) { + m_msgTimestamp = ts; + } + void setPayloadParityStatus(int payloadParityStatus) { + m_payloadParityStatus = payloadParityStatus; + } + void setPayloadCRCStatus(bool payloadCRCStatus) { + m_payloadCRCStatus = payloadCRCStatus; + } + void setPipelineMetadata(int pipelineId, const QString& pipelineName, const QString& pipelinePreset) { + m_pipelineId = pipelineId; + m_pipelineName = pipelineName; + m_pipelinePreset = pipelinePreset; + } + + private: + QString m_message; + QString m_call1; + QString m_call2; + QString m_loc; + bool m_reply; + bool m_freeText; + unsigned int m_syncWord; + float m_signalDb; + float m_noiseDb; + QString m_msgTimestamp; + int m_payloadParityStatus; + bool m_payloadCRCStatus; + int m_pipelineId; + QString m_pipelineName; + QString m_pipelinePreset; + + MsgReportDecodeFT() : + Message(), + m_reply(false), + m_freeText(false), + m_syncWord(0), + m_signalDb(0.0), + m_noiseDb(0.0), + m_payloadParityStatus((int) MeshtasticDemodSettings::ParityUndefined), + m_payloadCRCStatus(false), + m_pipelineId(-1) + { } + }; +} + +#endif // INCLUDE_MESHTASTICDEMODMSG_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodsettings.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodsettings.cpp new file mode 100644 index 000000000..c5fc15a0d --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodsettings.cpp @@ -0,0 +1,400 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017-2018, 2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "meshtasticdemodsettings.h" + +const int MeshtasticDemodSettings::bandwidths[] = { + 325, // 384k / 1024 + 488, // 500k / 1024 + 750, // 384k / 512 + 1500, // 384k / 256 + 2604, // 333k / 128 + 3125, // 400k / 128 + 3906, // 500k / 128 + 5208, // 333k / 64 + 6250, // 400k / 64 + 7813, // 500k / 64 + 10417, // 333k / 32 + 12500, // 400k / 32 + 15625, // 500k / 32 + 20833, // 333k / 16 + 25000, // 400k / 16 + 31250, // 500k / 16 + 41667, // 333k / 8 + 50000, // 400k / 8 + 62500, // 500k / 8 + 83333, // 333k / 4 + 100000, // 400k / 4 + 125000, // 500k / 4 + 166667, // 333k / 2 + 200000, // 400k / 2 + 250000, // 500k / 2 + 333333, // 333k / 1 + 400000, // 400k / 1 + 500000 // 500k / 1 +}; +const int MeshtasticDemodSettings::nbBandwidths = 3*8 + 4; +// Keep frame-sync input at >=4x BW (matches gr-lora_sdr os_factor=4 expectations) +// so SF11/SF12 Meshtastic presets retain enough timing resolution. +const int MeshtasticDemodSettings::oversampling = 4; + +MeshtasticDemodSettings::MeshtasticDemodSettings() : + m_inputFrequencyOffset(0), + m_channelMarker(0), + m_spectrumGUI(0), + m_rollupState(0) +{ + resetToDefaults(); +} + +void MeshtasticDemodSettings::resetToDefaults() +{ + m_bandwidthIndex = 5; + m_spreadFactor = 7; + m_deBits = 0; + m_codingScheme = CodingLoRa; + m_decodeActive = true; + m_fftWindow = FFTWindow::Rectangle; + m_eomSquelchTenths = 60; + m_nbSymbolsMax = 255; + m_autoNbSymbolsMax = false; + m_preambleChirps = 17; + m_packetLength = 237; + m_nbParityBits = 1; + m_hasCRC = true; + m_hasHeader = true; + m_sendViaUDP = false; + m_invertRamps = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_meshtasticKeySpecList.clear(); + m_meshtasticAutoSampleRate = true; + m_meshtasticRegionCode = "US"; + m_meshtasticPresetName = "LONG_FAST"; + m_meshtasticChannelIndex = 0; + m_rgbColor = QColor(255, 0, 255).rgb(); + m_title = "Meshtastic Demodulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + m_workspaceIndex = 0; + m_hidden = false; +} + +QByteArray MeshtasticDemodSettings::serialize() const +{ + SimpleSerializer s(3); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_bandwidthIndex); + s.writeS32(3, m_spreadFactor); + + if (m_spectrumGUI) { + s.writeBlob(4, m_spectrumGUI->serialize()); + } + + if (m_channelMarker) { + s.writeBlob(5, m_channelMarker->serialize()); + } + + s.writeString(6, m_title); + s.writeS32(7, m_deBits); + s.writeS32(8, m_codingScheme); + s.writeBool(9, m_decodeActive); + s.writeS32(10, m_eomSquelchTenths); + s.writeU32(11, m_nbSymbolsMax); + s.writeS32(12, m_packetLength); + s.writeS32(13, m_nbParityBits); + s.writeBool(14, m_hasCRC); + s.writeBool(15, m_hasHeader); + s.writeU32(17, m_preambleChirps); + s.writeS32(18, (int) m_fftWindow); + s.writeBool(19, m_invertRamps); + s.writeBool(20, m_useReverseAPI); + s.writeString(21, m_reverseAPIAddress); + s.writeU32(22, m_reverseAPIPort); + s.writeU32(23, m_reverseAPIDeviceIndex); + s.writeU32(24, m_reverseAPIChannelIndex); + s.writeS32(25, m_streamIndex); + s.writeBool(26, m_sendViaUDP); + s.writeString(27, m_udpAddress); + s.writeU32(28, m_udpPort); + + if (m_rollupState) { + s.writeBlob(29, m_rollupState->serialize()); + } + + s.writeS32(30, m_workspaceIndex); + s.writeBlob(31, m_geometryBytes); + s.writeBool(32, m_hidden); + s.writeString(33, m_meshtasticKeySpecList); + s.writeBool(34, m_meshtasticAutoSampleRate); + s.writeString(35, m_meshtasticRegionCode); + s.writeString(36, m_meshtasticPresetName); + s.writeS32(37, m_meshtasticChannelIndex); + + return s.final(); +} + +bool MeshtasticDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if ((d.getVersion() == 1) || (d.getVersion() == 2) || (d.getVersion() == 3)) + { + QByteArray bytetmp; + int tmp; + unsigned int utmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_bandwidthIndex, 0); + d.readS32(3, &m_spreadFactor, 0); + + if (m_spectrumGUI) + { + d.readBlob(4, &bytetmp); + m_spectrumGUI->deserialize(bytetmp); + } + + if (m_channelMarker) + { + d.readBlob(5, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readString(6, &m_title, "Meshtastic Demodulator"); + d.readS32(7, &m_deBits, 0); + d.readS32(8, &tmp); + m_codingScheme = (CodingScheme) tmp; + d.readBool(9, &m_decodeActive, true); + d.readS32(10, &m_eomSquelchTenths, 60); + d.readU32(11, &m_nbSymbolsMax, 255); + d.readS32(12, &m_packetLength, 237); + d.readS32(13, &m_nbParityBits, 1); + d.readBool(14, &m_hasCRC, true); + d.readBool(15, &m_hasHeader, true); + d.readU32(17, &m_preambleChirps, 17); + d.readS32(18, &tmp, (int) FFTWindow::Rectangle); + m_fftWindow = (FFTWindow::Function) tmp; + d.readBool(19, &m_invertRamps, false); + d.readBool(20, &m_useReverseAPI, false); + d.readString(21, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(22, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(23, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(24, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readS32(25, &m_streamIndex, 0); + d.readBool(26, &m_sendViaUDP, false); + d.readString(27, &m_udpAddress, "127.0.0.1"); + d.readU32(28, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + + if (m_rollupState) + { + d.readBlob(29, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(30, &m_workspaceIndex, 0); + d.readBlob(31, &m_geometryBytes); + d.readBool(32, &m_hidden, false); + d.readString(33, &m_meshtasticKeySpecList, ""); + d.readBool(34, &m_meshtasticAutoSampleRate, true); + d.readString(35, &m_meshtasticRegionCode, "US"); + d.readString(36, &m_meshtasticPresetName, "LONG_FAST"); + d.readS32(37, &m_meshtasticChannelIndex, 0); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void MeshtasticDemodSettings::applySettings(const QStringList& settingsKeys, const MeshtasticDemodSettings& settings) +{ + if (settingsKeys.contains("inputFrequencyOffset")) + m_inputFrequencyOffset = settings.m_inputFrequencyOffset; + if (settingsKeys.contains("bandwidthIndex")) + m_bandwidthIndex = settings.m_bandwidthIndex; + if (settingsKeys.contains("spreadFactor")) + m_spreadFactor = settings.m_spreadFactor; + if (settingsKeys.contains("deBits")) + m_deBits = settings.m_deBits; + if (settingsKeys.contains("fftWindow")) + m_fftWindow = settings.m_fftWindow; + if (settingsKeys.contains("codingScheme")) + m_codingScheme = settings.m_codingScheme; + if (settingsKeys.contains("decodeActive")) + m_decodeActive = settings.m_decodeActive; + if (settingsKeys.contains("eomSquelchTenths")) + m_eomSquelchTenths = settings.m_eomSquelchTenths; + if (settingsKeys.contains("nbSymbolsMax")) + m_nbSymbolsMax = settings.m_nbSymbolsMax; + if (settingsKeys.contains("preambleChirps")) + m_preambleChirps = settings.m_preambleChirps; + if (settingsKeys.contains("nbParityBits")) + m_nbParityBits = settings.m_nbParityBits; + if (settingsKeys.contains("packetLength")) + m_packetLength = settings.m_packetLength; + if (settingsKeys.contains("hasCRC")) + m_hasCRC = settings.m_hasCRC; + if (settingsKeys.contains("hasHeader")) + m_hasHeader = settings.m_hasHeader; + if (settingsKeys.contains("sendViaUDP")) + m_sendViaUDP = settings.m_sendViaUDP; + if (settingsKeys.contains("invertRamps")) + m_invertRamps = settings.m_invertRamps; + if (settingsKeys.contains("udpAddress")) + m_udpAddress = settings.m_udpAddress; + if (settingsKeys.contains("udpPort")) + m_udpPort = settings.m_udpPort; + if (settingsKeys.contains("meshtasticKeySpecList")) + m_meshtasticKeySpecList = settings.m_meshtasticKeySpecList; + if (settingsKeys.contains("meshtasticAutoSampleRate")) + m_meshtasticAutoSampleRate = settings.m_meshtasticAutoSampleRate; + if (settingsKeys.contains("meshtasticRegionCode")) + m_meshtasticRegionCode = settings.m_meshtasticRegionCode; + if (settingsKeys.contains("meshtasticPresetName")) + m_meshtasticPresetName = settings.m_meshtasticPresetName; + if (settingsKeys.contains("meshtasticChannelIndex")) + m_meshtasticChannelIndex = settings.m_meshtasticChannelIndex; + if (settingsKeys.contains("useReverseAPI")) + m_useReverseAPI = settings.m_useReverseAPI; + if (settingsKeys.contains("reverseAPIAddress")) + m_reverseAPIAddress = settings.m_reverseAPIAddress; + if (settingsKeys.contains("reverseAPIPort")) + m_reverseAPIPort = settings.m_reverseAPIPort; + if (settingsKeys.contains("reverseAPIDeviceIndex")) + m_reverseAPIDeviceIndex = settings.m_reverseAPIDeviceIndex; + if (settingsKeys.contains("reverseAPIChannelIndex")) + m_reverseAPIChannelIndex = settings.m_reverseAPIChannelIndex; + if (settingsKeys.contains("streamIndex")) + m_streamIndex = settings.m_streamIndex; +} + +QString MeshtasticDemodSettings::getDebugString(const QStringList& settingsKeys, bool force) const +{ + QString debug; + + if (force || settingsKeys.contains("inputFrequencyOffset")) + debug += QString("InputFrequencyOffset: %1 ").arg(m_inputFrequencyOffset); + if (force || settingsKeys.contains("bandwidthIndex")) + debug += QString("BandwidthIndex: %1 ").arg(m_bandwidthIndex); + if (force || settingsKeys.contains("spreadFactor")) + debug += QString("SpreadFactor: %1 ").arg(m_spreadFactor); + if (force || settingsKeys.contains("deBits")) + debug += QString("DEBits: %1 ").arg(m_deBits); + if (force || settingsKeys.contains("fftWindow")) + debug += QString("FFTWindow: %1 ").arg((int) m_fftWindow); + if (force || settingsKeys.contains("codingScheme")) + debug += QString("CodingScheme: %1 ").arg((int) m_codingScheme); + if (force || settingsKeys.contains("decodeActive")) + debug += QString("DecodeActive: %1 ").arg(m_decodeActive); + if (force || settingsKeys.contains("eomSquelchTenths")) + debug += QString("EOMSquelchTenths: %1 ").arg(m_eomSquelchTenths); + if (force || settingsKeys.contains("nbSymbolsMax")) + debug += QString("NbSymbolsMax: %1 ").arg(m_nbSymbolsMax); + if (force || settingsKeys.contains("preambleChirps")) + debug += QString("PreambleChirps: %1 ").arg(m_preambleChirps); + if (force || settingsKeys.contains("nbParityBits")) + debug += QString("NbParityBits: %1 ").arg(m_nbParityBits); + if (force || settingsKeys.contains("packetLength")) + debug += QString("PacketLength: %1 ").arg(m_packetLength); + if (force || settingsKeys.contains("hasCRC")) + debug += QString("HasCRC: %1 ").arg(m_hasCRC); + if (force || settingsKeys.contains("hasHeader")) + debug += QString("HasHeader: %1 ").arg(m_hasHeader); + if (force || settingsKeys.contains("sendViaUDP")) + debug += QString("SendViaUDP: %1 ").arg(m_sendViaUDP); + if (force || settingsKeys.contains("invertRamps")) + debug += QString("InvertRamps: %1 ").arg(m_invertRamps); + if (force || settingsKeys.contains("udpAddress")) + debug += QString("UDPAddress: %1 ").arg(m_udpAddress); + if (force || settingsKeys.contains("udpPort")) + debug += QString("UDPPort: %1 ").arg(m_udpPort); + if (force || settingsKeys.contains("meshtasticKeySpecList")) + debug += QString("MeshtasticKeySpecList: %1 ").arg(m_meshtasticKeySpecList); + if (force || settingsKeys.contains("meshtasticAutoSampleRate")) + debug += QString("MeshtasticAutoSampleRate: %1 ").arg(m_meshtasticAutoSampleRate); + if (force || settingsKeys.contains("meshtasticRegionCode")) + debug += QString("MeshtasticRegionCode: %1 ").arg(m_meshtasticRegionCode); + if (force || settingsKeys.contains("meshtasticPresetName")) + debug += QString("MeshtasticPresetName: %1 ").arg(m_meshtasticPresetName); + if (force || settingsKeys.contains("meshtasticChannelIndex")) + debug += QString("MeshtasticChannelIndex: %1 ").arg(m_meshtasticChannelIndex); + if (force || settingsKeys.contains("useReverseAPI")) + debug += QString("UseReverseAPI: %1 ").arg(m_useReverseAPI); + if (force || settingsKeys.contains("reverseAPIAddress")) + debug += QString("ReverseAPIAddress: %1 ").arg(m_reverseAPIAddress); + if (force || settingsKeys.contains("reverseAPIPort")) + debug += QString("ReverseAPIPort: %1 ").arg(m_reverseAPIPort); + if (force || settingsKeys.contains("reverseAPIDeviceIndex")) + debug += QString("ReverseAPIDeviceIndex: %1 ").arg(m_reverseAPIDeviceIndex); + if (force || settingsKeys.contains("reverseAPIChannelIndex")) + debug += QString("ReverseAPIChannelIndex: %1 ").arg(m_reverseAPIChannelIndex); + if (force || settingsKeys.contains("streamIndex")) + debug += QString("StreamIndex: %1 ").arg(m_streamIndex); + return debug; +} + +unsigned int MeshtasticDemodSettings::getNbSFDFourths() const +{ + switch (m_codingScheme) + { + case CodingLoRa: + return 9; + default: + return 8; + } +} + +bool MeshtasticDemodSettings::hasSyncWord() const +{ + // Keep sync-symbol handling enabled for live-air compatibility. + // The extracted sync value can still be 0x00 when the on-air flow uses [0,0]. + return m_codingScheme == CodingLoRa; +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodsettings.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodsettings.h new file mode 100644 index 000000000..e8eabe3c6 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodsettings.h @@ -0,0 +1,111 @@ + +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELRX_DEMODMESHTASTIC_MESHTASTICDEMODSETTINGS_H_ +#define PLUGINS_CHANNELRX_DEMODMESHTASTIC_MESHTASTICDEMODSETTINGS_H_ + +#include +#include + +#include + +#include "dsp/fftwindow.h" + +class Serializable; + +struct MeshtasticDemodSettings +{ + enum CodingScheme + { + CodingLoRa, //!< Standard LoRa + CodingASCII, //!< plain ASCII (7 bits) + CodingTTY, //!< plain TTY (5 bits) + CodingFT //!< FT8/4 scheme (payload 174 bits LDPC) + }; + + enum ParityStatus + { + ParityUndefined, + ParityError, + ParityCorrected, + ParityOK + }; + + int m_inputFrequencyOffset; + int m_bandwidthIndex; + int m_spreadFactor; + int m_deBits; //!< Low data rate optimize (DE) bits + FFTWindow::Function m_fftWindow; + CodingScheme m_codingScheme; + bool m_decodeActive; + int m_eomSquelchTenths; //!< Squelch factor to trigger end of message (/10) + unsigned int m_nbSymbolsMax; //!< Maximum number of symbols in a payload + bool m_autoNbSymbolsMax; //!< Set maximum number of symbols in a payload automatically using last message value + unsigned int m_preambleChirps; //!< Number of expected preamble chirps + int m_nbParityBits; //!< Hamming parity bits (LoRa) + int m_packetLength; //!< Payload packet length in bytes or characters (LoRa) + bool m_hasCRC; //!< Payload has CRC (LoRa) + bool m_hasHeader; //!< Header present before actual payload (LoRa) + bool m_sendViaUDP; //!< Send decoded message via UDP + bool m_invertRamps; //!< Invert chirp ramps vs standard LoRa (up/down/up is standard) + QString m_udpAddress; //!< UDP address where to send message + uint16_t m_udpPort; //!< UDP port where to send message + QString m_meshtasticKeySpecList; //!< Optional per-channel Meshtastic decode key list + bool m_meshtasticAutoSampleRate; //!< Auto-tune source sample rate/decimation for selected Meshtastic profile + QString m_meshtasticRegionCode; //!< UI-selected Meshtastic region code (US, EU_868, ...) + QString m_meshtasticPresetName; //!< UI-selected Meshtastic preset (LONG_FAST, ...) + int m_meshtasticChannelIndex; //!< UI-selected Meshtastic channel index (zero-based) + uint32_t m_rgbColor; + QString m_title; + int m_streamIndex; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + Serializable *m_channelMarker; + Serializable *m_spectrumGUI; + Serializable *m_rollupState; + + static const int bandwidths[]; + static const int nbBandwidths; + static const int oversampling; + + MeshtasticDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void setSpectrumGUI(Serializable *spectrumGUI) { m_spectrumGUI = spectrumGUI; } + unsigned int getNbSFDFourths() const; //!< Get the number of SFD period fourths (depends on coding scheme) + bool hasSyncWord() const; //!< Only LoRa has a syncword (for the moment) + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void applySettings(const QStringList& settingsKeys, const MeshtasticDemodSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; +}; + + + +#endif /* PLUGINS_CHANNELRX_DEMODMESHTASTIC_MESHTASTICDEMODSETTINGS_H_ */ diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodsink.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodsink.cpp new file mode 100644 index 000000000..6efbce09d --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodsink.cpp @@ -0,0 +1,1972 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-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 "dsp/dsptypes.h" +#include "dsp/basebandsamplesink.h" +#include "dsp/dspengine.h" +#include "dsp/fftfactory.h" +#include "dsp/fftengine.h" +#include "util/db.h" + +#include "meshtasticdemodmsg.h" +#include "meshtasticdemoddecoderlora.h" +#include "meshtasticdemodsink.h" + +MeshtasticDemodSink::MeshtasticDemodSink() : + m_decodeMsg(nullptr), + m_decoderMsgQueue(nullptr), + m_fftSequence(-1), + m_fftSFDSequence(-1), + m_downChirps(nullptr), + m_upChirps(nullptr), + m_spectrumLine(nullptr), + m_headerLocked(false), + m_expectedSymbols(0), + m_waitHeaderFeedback(false), + m_headerFeedbackWaitSteps(0U), + m_loRaFrameId(0U), + m_osFactor(MeshtasticDemodSettings::oversampling > 0 ? MeshtasticDemodSettings::oversampling : 1), + m_osCenterPhase((MeshtasticDemodSettings::oversampling > 1 ? MeshtasticDemodSettings::oversampling / 2 : 0)), + m_osCounter(0), + m_loRaState(LoRaStateDetect), + m_loRaSyncState(LoRaSyncNetId1), + m_loRaSymbolCnt(1), + m_loRaBinIdx(0), + m_loRaKHat(0), + m_loRaDownVal(0), + m_loRaCFOInt(0), + m_loRaNetIdOff(0), + m_loRaAdditionalUpchirps(0), + m_loRaUpSymbToUse(0), + m_loRaRequiredUpchirps(0), + m_loRaSymbolSpan(0), + m_loRaFrameSymbolCount(0), + m_loRaCFOFrac(0.0f), + m_loRaSTOFrac(0.0f), + m_loRaSFOHat(0.0f), + m_loRaSFOCum(0.0f), + m_loRaCFOSTOEstimated(false), + m_loRaReceivedHeader(false), + m_loRaOneSymbolOff(false), + m_spectrumSink(nullptr), + m_spectrumBuffer(nullptr) +{ + m_demodActive = false; + m_bandwidth = MeshtasticDemodSettings::bandwidths[0]; + m_channelSampleRate = 96000; + m_channelFrequencyOffset = 0; + m_deviceCenterFrequency = 0; + m_nco.setFreq(m_channelFrequencyOffset, m_channelSampleRate); + m_interpolator.create(16, m_channelSampleRate, m_bandwidth / 1.9f); + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) m_bandwidth; + m_sampleDistanceRemain = 0; + const unsigned int ctorConfiguredPreamble = m_settings.m_preambleChirps > 0U + ? m_settings.m_preambleChirps + : m_minRequiredPreambleChirps; + const unsigned int ctorTargetRequired = ctorConfiguredPreamble > 3U + ? (ctorConfiguredPreamble - 3U) + : m_minRequiredPreambleChirps; + m_requiredPreambleChirps = std::max( + m_minRequiredPreambleChirps, + std::min(ctorTargetRequired, m_maxRequiredPreambleChirps) + ); + m_fftInterpolation = (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + ? m_loRaFFTInterpolation + : m_legacyFFTInterpolation; + + m_state = ChirpChatStateReset; + m_chirp = 0; + m_chirp0 = 0; + + initSF(m_settings.m_spreadFactor, m_settings.m_deBits, m_settings.m_fftWindow); +} + +MeshtasticDemodSink::~MeshtasticDemodSink() +{ + FFTFactory *fftFactory = DSPEngine::instance()->getFFTFactory(); + + if (m_fftSequence >= 0) + { + fftFactory->releaseEngine(m_interpolatedFFTLength, false, m_fftSequence); + fftFactory->releaseEngine(m_interpolatedFFTLength, false, m_fftSFDSequence); + } + + delete[] m_downChirps; + delete[] m_upChirps; + delete[] m_spectrumBuffer; + delete[] m_spectrumLine; +} + +void MeshtasticDemodSink::initSF(unsigned int sf, unsigned int deBits, FFTWindow::Function fftWindow) +{ + if (m_downChirps) { + delete[] m_downChirps; + } + if (m_upChirps) { + delete[] m_upChirps; + } + if (m_spectrumBuffer) { + delete[] m_spectrumBuffer; + } + if (m_spectrumLine) { + delete[] m_spectrumLine; + } + + FFTFactory *fftFactory = DSPEngine::instance()->getFFTFactory(); + + if (m_fftSequence >= 0) + { + fftFactory->releaseEngine(m_interpolatedFFTLength, false, m_fftSequence); + fftFactory->releaseEngine(m_interpolatedFFTLength, false, m_fftSFDSequence); + } + + m_nbSymbols = 1 << sf; + m_nbSymbolsEff = 1 << (sf - deBits); + m_deLength = 1 << deBits; + m_fftLength = m_nbSymbols; + m_fftWindow.create(fftWindow, m_fftLength); + m_fftWindow.setKaiserAlpha(M_PI); + m_interpolatedFFTLength = m_fftInterpolation*m_fftLength; + m_preambleTolerance = std::max(1, (m_deLength*static_cast(m_fftInterpolation))/2); + m_fftSequence = fftFactory->getEngine(m_interpolatedFFTLength, false, &m_fft); + m_fftSFDSequence = fftFactory->getEngine(m_interpolatedFFTLength, false, &m_fftSFD); + m_state = ChirpChatStateReset; + m_sfdSkip = m_fftLength / 4; + m_downChirps = new Complex[2*m_nbSymbols]; // Each table is 2 chirps long to allow processing from arbitrary offsets. + m_upChirps = new Complex[2*m_nbSymbols]; + m_spectrumBuffer = new Complex[m_nbSymbols]; + m_spectrumLine = new Complex[m_nbSymbols]; + std::fill(m_spectrumLine, m_spectrumLine+m_nbSymbols, Complex(std::polar(1e-6*SDR_RX_SCALED, 0.0))); + m_loRaSymbolSpan = m_nbSymbols * m_osFactor; + m_loRaRequiredUpchirps = m_requiredPreambleChirps; + m_loRaUpSymbToUse = (m_loRaRequiredUpchirps > 0U) ? static_cast(m_loRaRequiredUpchirps - 1U) : 0; + m_loRaInDown.assign(m_nbSymbols, Complex{0.0f, 0.0f}); + m_loRaPreambleRaw.assign(m_nbSymbols * m_loRaRequiredUpchirps, Complex{0.0f, 0.0f}); + m_loRaPreambleRawUp.assign((m_settings.m_preambleChirps + 3U) * m_loRaSymbolSpan, Complex{0.0f, 0.0f}); + m_loRaPreambleUpchirps.assign(m_nbSymbols * m_loRaRequiredUpchirps, Complex{0.0f, 0.0f}); + m_loRaCFOFracCorrec.assign(m_nbSymbols, Complex{1.0f, 0.0f}); + m_loRaPayloadDownchirp.assign(m_nbSymbols, Complex{1.0f, 0.0f}); + m_loRaSymbCorr.assign(m_nbSymbols, Complex{0.0f, 0.0f}); + m_loRaNetIdSamp.assign((m_loRaSymbolSpan * 5U) / 2U + m_loRaSymbolSpan, Complex{0.0f, 0.0f}); + m_loRaAdditionalSymbolSamp.assign(m_loRaSymbolSpan * 2U, Complex{0.0f, 0.0f}); + m_loRaPreambleVals.assign(m_loRaRequiredUpchirps, 0); + m_loRaNetIds.assign(2, 0); + m_loRaSampleFifo.clear(); + m_loRaState = LoRaStateDetect; + m_loRaSyncState = LoRaSyncNetId1; + m_loRaSymbolCnt = 1; + m_loRaBinIdx = 0; + m_loRaKHat = 0; + m_loRaAdditionalUpchirps = 0; + m_loRaCFOSTOEstimated = false; + m_loRaReceivedHeader = false; + m_loRaFrameSymbolCount = 0; + + // Canonical gr-lora_sdr reference chirps (utilities::build_ref_chirps, id=0, os_factor=1). + for (unsigned int i = 0; i < m_fftLength; i++) + { + const double n = static_cast(i); + const double N = static_cast(m_nbSymbols); + const double phase = 2.0 * M_PI * ((n * n) / (2.0 * N) - 0.5 * n); + m_upChirps[i] = Complex(std::cos(phase), std::sin(phase)); + m_downChirps[i] = std::conj(m_upChirps[i]); + } + + // Duplicate table to allow processing from arbitrary offsets + std::copy(m_downChirps, m_downChirps+m_fftLength, m_downChirps+m_fftLength); + std::copy(m_upChirps, m_upChirps+m_fftLength, m_upChirps+m_fftLength); +} + +void MeshtasticDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + + for (SampleVector::const_iterator it = begin; it < end; ++it) + { + Complex c(it->real() / SDR_RX_SCALEF, it->imag() / SDR_RX_SCALEF); + c *= m_nco.nextIQ(); + + if (m_interpolator.decimate(&m_sampleDistanceRemain, c, &ci)) + { + if (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + { + processSample(ci); + } + else if (m_osFactor <= 1U) + { + processSample(ci); + } + else + { + if ((m_osCounter % m_osFactor) == m_osCenterPhase) { + processSample(ci); + } + m_osCounter++; + } + m_sampleDistanceRemain += m_interpolatorDistance; + } + } +} + +void MeshtasticDemodSink::processSample(const Complex& ci) +{ + if (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + { + processSampleLoRa(ci); + return; + } + + if (m_state == ChirpChatStateReset) // start over + { + m_demodActive = false; + reset(); + std::queue().swap(m_magsqQueue); // this clears the queue + m_state = ChirpChatStateDetectPreamble; + } + else if (m_state == ChirpChatStateDetectPreamble) // look for preamble + { + m_fft->in()[m_fftCounter++] = ci * (m_settings.m_invertRamps ? m_upChirps[m_chirp] : m_downChirps[m_chirp]); // de-chirp the preamble ramp + + if (m_fftCounter == m_fftLength) + { + m_fftWindow.apply(m_fft->in()); + std::fill(m_fft->in()+m_fftLength, m_fft->in()+m_interpolatedFFTLength, Complex{0.0, 0.0}); + m_fft->transform(); + m_fftCounter = 0; + double magsq, magsqTotal; + + unsigned int imax = argmax( + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsq, + magsqTotal, + m_spectrumBuffer, + m_fftInterpolation + ) / m_fftInterpolation; + + // When ramps are inverted, FFT output interpretation is reversed + if (m_settings.m_invertRamps) { + imax = (m_nbSymbols - imax) % m_nbSymbols; + } + + if (m_magsqQueue.size() > m_settings.m_preambleChirps) { + m_magsqQueue.pop(); + } + + m_magsqTotalAvg(magsqTotal); + m_magsqQueue.push(magsq); + + if (m_havePrevPreambleBin) + { + const int delta = circularBinDelta(imax, m_prevPreambleBin); + + if (std::abs(delta) <= m_preambleTolerance) + { + m_preambleConsecutive++; + m_preambleBinHistory.push_back(imax); + } + else + { + m_preambleConsecutive = 1; + m_preambleBinHistory.clear(); + m_preambleBinHistory.push_back(imax); + } + } + else + { + m_havePrevPreambleBin = true; + m_preambleConsecutive = 1; + m_preambleBinHistory.clear(); + m_preambleBinHistory.push_back(imax); + } + + if (m_preambleBinHistory.size() > m_requiredPreambleChirps) { + m_preambleBinHistory.pop_front(); + } + + m_prevPreambleBin = imax; + + // gr-lora_sdr-style rolling detect: lock after enough consecutive near-equal upchirps. + if ((m_preambleConsecutive >= m_requiredPreambleChirps) && (magsq > 1e-9)) + { + const unsigned int preambleBin = getPreambleModeBin(); + + if (m_spectrumSink) { + m_spectrumSink->feed(m_spectrumBuffer, m_nbSymbols); + } + + qInfo("MeshtasticDemodSink::processSample: preamble found: %u|%f (consecutive=%u)", + preambleBin, magsq, m_preambleConsecutive); + m_chirp = preambleBin; + m_fftCounter = m_chirp; + m_chirp0 = 0; + m_chirpCount = 0; + m_state = ChirpChatStatePreambleResyc; + } + else if (!m_magsqQueue.empty()) + { + m_magsqOffAvg(m_magsqQueue.front()); + } + } + } + else if (m_state == ChirpChatStatePreambleResyc) + { + m_fftCounter++; + + if (m_fftCounter == m_fftLength) + { + if (m_spectrumSink) { + m_spectrumSink->feed(m_spectrumLine, m_nbSymbols); + } + + m_fftCounter = 0; + m_demodActive = true; + m_state = ChirpChatStatePreamble; + } + } + else if (m_state == ChirpChatStatePreamble) // preamble found look for SFD start + { + m_fft->in()[m_fftCounter] = ci * (m_settings.m_invertRamps ? m_upChirps[m_chirp] : m_downChirps[m_chirp]); // de-chirp the preamble ramp + m_fftSFD->in()[m_fftCounter] = ci * (m_settings.m_invertRamps ? m_downChirps[m_chirp] : m_upChirps[m_chirp]); // de-chirp the SFD ramp + m_fftCounter++; + + if (m_fftCounter == m_fftLength) + { + m_fftWindow.apply(m_fft->in()); + std::fill(m_fft->in()+m_fftLength, m_fft->in()+m_interpolatedFFTLength, Complex{0.0, 0.0}); + m_fft->transform(); + + m_fftWindow.apply(m_fftSFD->in()); + std::fill(m_fftSFD->in()+m_fftLength, m_fftSFD->in()+m_interpolatedFFTLength, Complex{0.0, 0.0}); + m_fftSFD->transform(); + + m_fftCounter = 0; + double magsqPre, magsqSFD; + double magsqTotal, magsqSFDTotal; + + unsigned int imaxSFD = argmax( + m_fftSFD->out(), + m_fftInterpolation, + m_fftLength, + magsqSFD, + magsqTotal, + nullptr, + m_fftInterpolation + ) / m_fftInterpolation; + + unsigned int imax = argmax( + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsqPre, + magsqSFDTotal, + m_spectrumBuffer, + m_fftInterpolation + ) / m_fftInterpolation; + + // When ramps are inverted, FFT output interpretation is reversed + if (m_settings.m_invertRamps) { + imax = (m_nbSymbols - imax) % m_nbSymbols; + } + + if (m_chirpCount < m_maxSFDSearchChirps) + { + m_preambleHistory[m_chirpCount] = imax; + m_chirpCount++; + } + else + { + // Protect against history overflow when long preambles are configured. + qWarning("MeshtasticDemodSink::processSample: SFD search history overflow (%u >= %u)", + m_chirpCount, m_maxSFDSearchChirps); + m_state = ChirpChatStateReset; + return; + } + const double preDrop = magsqPre - magsqSFD; + const double dropRatio = (magsqSFD > 1e-18) ? (-preDrop / magsqSFD) : 0.0; + const bool sfdDominant = magsqSFD > (magsqPre * 1.05); // less strict than legacy 50% jump + const bool sfdBinAligned = (imaxSFD <= 2U) || (imaxSFD >= (m_nbSymbols - 2U)); + + if (sfdDominant && sfdBinAligned) // preamble -> SFD transition candidate + { + m_magsqTotalAvg(magsqSFDTotal); + + if (m_chirpCount < 1 + (m_settings.hasSyncWord() ? 2 : 0)) // too early + { + m_state = ChirpChatStateReset; + qDebug("MeshtasticDemodSink::processSample: SFD search: signal drop is too early"); + } + else + { + if (m_settings.hasSyncWord()) + { + m_syncWord = round(m_preambleHistory[m_chirpCount-2] / 8.0); + m_syncWord += 16 * round(m_preambleHistory[m_chirpCount-3] / 8.0); + qInfo("MeshtasticDemodSink::processSample: SFD found: pre=%4u|%11.6f sfd=%4u|%11.6f ratio=%8.4f sync=%x", + imax, magsqPre, imaxSFD, magsqSFD, dropRatio, m_syncWord); + } + else + { + m_syncWord = 0; + qInfo("MeshtasticDemodSink::processSample: SFD found: pre=%4u|%11.6f sfd=%4u|%11.6f ratio=%8.4f", + imax, magsqPre, imaxSFD, magsqSFD, dropRatio); + } + + int sadj = 0; + int nadj = 0; + int zadj; + int sfdSkip = m_sfdSkip; + + for (unsigned int i = 0; i < m_chirpCount - 1 - (m_settings.hasSyncWord() ? 2 : 0); i++) + { + sadj += m_preambleHistory[i] > m_nbSymbols/2 ? m_preambleHistory[i] - m_nbSymbols : m_preambleHistory[i]; + nadj++; + } + + zadj = nadj == 0 ? 0 : sadj / nadj; + zadj = zadj < -(sfdSkip/2) ? -(sfdSkip/2) : zadj > sfdSkip/2 ? sfdSkip/2 : zadj; + qDebug("MeshtasticDemodSink::processSample: zero adjust: %d (%d)", zadj, nadj); + + m_sfdSkipCounter = 0; + m_fftCounter = m_fftLength - m_sfdSkip + zadj; + m_chirp += zadj; + m_state = ChirpChatStateSkipSFD; //ChirpChatStateSlideSFD; + } + } + else // SFD missed start over + { + const unsigned int preambleForWindow = std::max(m_settings.m_preambleChirps, m_requiredPreambleChirps); + unsigned int sfdSearchWindow = preambleForWindow - m_requiredPreambleChirps + 2U; + sfdSearchWindow = std::max( + m_requiredPreambleChirps, + std::min(sfdSearchWindow, m_maxSFDSearchChirps) + ); + + if (m_chirpCount <= sfdSearchWindow) { + if (m_spectrumSink) { + m_spectrumSink->feed(m_spectrumBuffer, m_nbSymbols); + } + + qDebug("MeshtasticDemodSink::processSample: SFD search: pre=%4u|%11.6f sfd=%4u|%11.6f ratio=%8.4f", + imax, magsqPre, imaxSFD, magsqSFD, dropRatio); + m_magsqTotalAvg(magsqTotal); + m_magsqOnAvg(magsqPre); + return; + } + + qDebug("MeshtasticDemodSink::processSample: SFD search: number of possible chirps exceeded"); + m_magsqTotalAvg(magsqTotal); + m_state = ChirpChatStateReset; + } + } + } + else if (m_state == ChirpChatStateSkipSFD) // Just skip the rest of SFD + { + m_fftCounter++; + + if (m_fftCounter == m_fftLength) + { + m_fftCounter = m_fftLength - m_sfdSkip; + m_sfdSkipCounter++; + + if (m_sfdSkipCounter == m_settings.getNbSFDFourths() - 4U) // SFD chips fourths less one full period + { + qInfo("MeshtasticDemodSink::processSample: SFD skipped"); + m_chirp = m_chirp0; + m_fftCounter = 0; + m_chirpCount = 0; + m_magsqMax = 0.0; + m_decodeMsg = MeshtasticDemodMsg::MsgDecodeSymbols::create(); + m_decodeMsg->setFrameId(0U); + m_decodeMsg->setSyncWord(m_syncWord); + clearSpectrumHistoryForNewFrame(); + m_state = ChirpChatStateReadPayload; + } + } + } + else if (m_state == ChirpChatStateReadPayload) + { + m_fft->in()[m_fftCounter] = ci * (m_settings.m_invertRamps ? m_upChirps[m_chirp] : m_downChirps[m_chirp]); + m_fftCounter++; + + if (m_fftCounter == m_fftLength) + { + m_fftWindow.apply(m_fft->in()); + std::fill(m_fft->in()+m_fftLength, m_fft->in()+m_interpolatedFFTLength, Complex{0.0, 0.0}); + m_fft->transform(); + m_fftCounter = 0; + double magsq, magsqTotal; + unsigned short symbol; + + if (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingFT) + { + std::vector magnitudes; + symbol = evalSymbol( + extractMagnitudes( + magnitudes, + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsq, + magsqTotal, + m_spectrumBuffer, + m_fftInterpolation + ) + ) % m_nbSymbolsEff; + m_decodeMsg->pushBackSymbol(symbol); + m_decodeMsg->pushBackMagnitudes(magnitudes); + } + else + { + int imax; + + if (m_settings.m_deBits > 0) + { + double magSqNoise; + imax = argmaxSpreaded( + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsq, + magSqNoise, + magsqTotal, + m_spectrumBuffer, + m_fftInterpolation + ); + } + else + { + imax = argmax( + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsq, + magsqTotal, + m_spectrumBuffer, + m_fftInterpolation + ); + } + + if (m_settings.m_invertRamps) { + imax = (m_nbSymbols * m_fftInterpolation - imax) % (m_nbSymbols * m_fftInterpolation); + } + + const bool headerSymbol = (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + && m_settings.m_hasHeader + && (m_chirpCount < 8U); + symbol = evalSymbol(imax, headerSymbol) % m_nbSymbolsEff; + m_decodeMsg->pushBackSymbol(symbol); + } + + if (m_spectrumSink) { + m_spectrumSink->feed(m_spectrumBuffer, m_nbSymbols); + } + + if (magsq > m_magsqMax) { + m_magsqMax = magsq; + } + + m_magsqTotalAvg(magsq); + + const bool inHeaderBlock = (m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + && m_settings.m_hasHeader + && (m_chirpCount < 8U); + + if (m_headerLocked) + { + // Header-locked: accept every symbol, terminate at exact expected count + qDebug("MeshtasticDemodSink::processSample: symbol %02u: %4u|%11.6f (%u/%u locked)", + m_chirpCount, symbol, magsq, m_chirpCount + 1, m_expectedSymbols); + m_magsqOnAvg(magsq); + m_chirpCount++; + + if (m_chirpCount >= m_expectedSymbols) + { + qInfo("MeshtasticDemodSink::processSample: header-locked frame complete (%u symbols)", m_chirpCount); + m_state = ChirpChatStateReset; + m_decodeMsg->setSignalDb(CalcDb::dbPower(m_magsqOnAvg.asDouble() / (1<setNoiseDb(CalcDb::dbPower(m_magsqOffAvg.asDouble() / (1<push(m_decodeMsg); + } else { + delete m_decodeMsg; + } + } + } + else if (inHeaderBlock + || (m_chirpCount == 0) + || (m_settings.m_eomSquelchTenths == 121) + || ((m_settings.m_eomSquelchTenths*magsq)/10.0 > m_magsqMax)) + { + qDebug("MeshtasticDemodSink::processSample: symbol %02u: %4u|%11.6f", m_chirpCount, symbol, magsq); + m_magsqOnAvg(magsq); + m_chirpCount++; + + // Attempt header lock after the 8-symbol header block + if (m_chirpCount == 8 + && m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa + && m_settings.m_hasHeader) + { + tryHeaderLock(); + } + + if (m_chirpCount > m_settings.m_nbSymbolsMax) + { + qInfo("MeshtasticDemodSink::processSample: message length reached"); + m_state = ChirpChatStateReset; + m_decodeMsg->setSignalDb(CalcDb::dbPower(m_magsqOnAvg.asDouble() / (1<setNoiseDb(CalcDb::dbPower(m_magsqOffAvg.asDouble() / (1<push(m_decodeMsg); + } else { + delete m_decodeMsg; + } + } + } + else + { + qInfo("MeshtasticDemodSink::processSample: end of message (EOM squelch)"); + m_state = ChirpChatStateReset; + m_decodeMsg->popSymbol(); + m_decodeMsg->setSignalDb(CalcDb::dbPower(m_magsqOnAvg.asDouble() / (1<setNoiseDb(CalcDb::dbPower(m_magsqOffAvg.asDouble() / (1<push(m_decodeMsg); + } else { + delete m_decodeMsg; + } + } + } + } + else + { + m_state = ChirpChatStateReset; + } + + m_chirp++; + + if (m_chirp >= m_chirp0 + m_nbSymbols) { + m_chirp = m_chirp0; + } +} + +void MeshtasticDemodSink::reset() +{ + resetLoRaFrameSync(); + m_chirp = 0; + m_chirp0 = 0; + m_fftCounter = 0; + m_preambleConsecutive = 0; + m_havePrevPreambleBin = false; + m_prevPreambleBin = 0; + m_preambleBinHistory.clear(); + m_sfdSkipCounter = 0; + m_syncWord = 0; + m_headerLocked = false; + m_expectedSymbols = 0; + m_waitHeaderFeedback = false; + m_headerFeedbackWaitSteps = 0; + m_osCounter = 0; +} + +unsigned int MeshtasticDemodSink::argmax( + const Complex *fftBins, + unsigned int fftMult, + unsigned int fftLength, + double& magsqMax, + double& magsqTotal, + Complex *specBuffer, + unsigned int specDecim) +{ + magsqMax = 0.0; + magsqTotal = 0.0; + unsigned int imax = 0; + double magSum = 0.0; + std::vector spectrumBucketPowers; + + if (specBuffer) { + spectrumBucketPowers.reserve((fftMult * fftLength) / std::max(1U, specDecim)); + } + + for (unsigned int i = 0; i < fftMult*fftLength; i++) + { + double magsq = std::norm(fftBins[i]); + magsqTotal += magsq; + + if (magsq > magsqMax) + { + imax = i; + magsqMax = magsq; + } + + if (specBuffer) + { + magSum += magsq; + + if (i % specDecim == specDecim - 1) + { + spectrumBucketPowers.push_back(magSum); + magSum = 0.0; + } + } + } + + const double magsqAvgRaw = magsqTotal / static_cast(fftMult * fftLength); + magsqTotal = magsqAvgRaw; + + if (specBuffer && !spectrumBucketPowers.empty()) + { + const double noisePerBucket = magsqAvgRaw * static_cast(std::max(1U, specDecim)); + const double floorCut = noisePerBucket * 1.05; // suppress steady floor + const double boost = 12.0; // emphasize peaks over residual floor + + for (size_t i = 0; i < spectrumBucketPowers.size(); ++i) + { + const double enhancedPower = std::max(0.0, spectrumBucketPowers[i] - floorCut) * boost; + const double specAmp = std::sqrt(enhancedPower) * static_cast(m_nbSymbols); + specBuffer[i] = Complex(std::polar(specAmp, 0.0)); + } + } + + return imax; +} + +unsigned int MeshtasticDemodSink::extractMagnitudes( + std::vector& magnitudes, + const Complex *fftBins, + unsigned int fftMult, + unsigned int fftLength, + double& magsqMax, + double& magsqTotal, + Complex *specBuffer, + unsigned int specDecim) +{ + magsqMax = 0.0; + magsqTotal = 0.0; + unsigned int imax = 0; + double magSum = 0.0; + std::vector spectrumBucketPowers; + + if (specBuffer) { + spectrumBucketPowers.reserve((fftMult * fftLength) / std::max(1U, specDecim)); + } + + unsigned int spread = fftMult * (1< magsqMax) + { + imax = (i/spread)*spread; + magsqMax = magnitude; + } + + magnitudes.push_back(magnitude); + magnitude = 0.0; + } + + if (specBuffer) + { + magSum += magsq; + + if (i % specDecim == specDecim - 1) + { + spectrumBucketPowers.push_back(magSum); + magSum = 0.0; + } + } + } + + const double magsqAvgRaw = magsqTotal / static_cast(fftMult * fftLength); + magsqTotal = magsqAvgRaw; + + if (specBuffer && !spectrumBucketPowers.empty()) + { + const double noisePerBucket = magsqAvgRaw * static_cast(std::max(1U, specDecim)); + const double floorCut = noisePerBucket * 1.05; + const double boost = 12.0; + + for (size_t i = 0; i < spectrumBucketPowers.size(); ++i) + { + const double enhancedPower = std::max(0.0, spectrumBucketPowers[i] - floorCut) * boost; + const double specAmp = std::sqrt(enhancedPower) * static_cast(m_nbSymbols); + specBuffer[i] = Complex(std::polar(specAmp, 0.0)); + } + } + + return imax; +} + +unsigned int MeshtasticDemodSink::argmaxSpreaded( + const Complex *fftBins, + unsigned int fftMult, + unsigned int fftLength, + double& magsqMax, + double& magsqNoise, + double& magsqTotal, + Complex *specBuffer, + unsigned int specDecim) +{ + magsqMax = 0.0; + magsqNoise = 0.0; + magsqTotal = 0.0; + unsigned int imax = 0; + double magSum = 0.0; + std::vector spectrumBucketPowers; + + if (specBuffer) { + spectrumBucketPowers.reserve((fftMult * fftLength) / std::max(1U, specDecim)); + } + + unsigned int nbsymbols = 1<<(m_settings.m_spreadFactor - m_settings.m_deBits); + unsigned int spread = fftMult * (1< magsqMax) + { + imax = (i/spread)*spread; + magsqMax = magSymbol; + } + + magsqNoise += magSymbol; + magSymbol = 0.0; + } + + if (specBuffer) + { + magSum += magsq; + + if (i % specDecim == specDecim - 1) + { + spectrumBucketPowers.push_back(magSum); + magSum = 0.0; + } + } + } + + const double magsqAvgRaw = magsqTotal / static_cast(fftMult * fftLength); + + if (specBuffer && !spectrumBucketPowers.empty()) + { + const double noisePerBucket = magsqAvgRaw * static_cast(std::max(1U, specDecim)); + const double floorCut = noisePerBucket * 1.05; + const double boost = 12.0; + + for (size_t i = 0; i < spectrumBucketPowers.size(); ++i) + { + const double enhancedPower = std::max(0.0, spectrumBucketPowers[i] - floorCut) * boost; + const double specAmp = std::sqrt(enhancedPower) * static_cast(m_nbSymbols); + specBuffer[i] = Complex(std::polar(specAmp, 0.0)); + } + } + + magsqNoise -= magsqMax; + magsqNoise /= (nbsymbols - 1); + magsqTotal /= nbsymbols; + // magsqNoise /= fftLength; + // magsqTotal /= fftMult*fftLength; + + return imax; +} + +void MeshtasticDemodSink::decimateSpectrum(Complex *in, Complex *out, unsigned int size, unsigned int decimation) +{ + for (unsigned int i = 0; i < size; i++) + { + if (i % decimation == 0) { + out[i/decimation] = in[i]; + } + } +} + +int MeshtasticDemodSink::toSigned(int u, int intSize) +{ + if (u > intSize/2) { + return u - intSize; + } else { + return u; + } +} + +int MeshtasticDemodSink::circularBinDelta(unsigned int current, unsigned int previous) const +{ + const int bins = static_cast(m_nbSymbols); + if (bins <= 0) { + return 0; + } + + int delta = static_cast(current) - static_cast(previous); + const int half = bins / 2; + + if (delta > half) { + delta -= bins; + } else if (delta < -half) { + delta += bins; + } + + return delta; +} + +unsigned int MeshtasticDemodSink::getPreambleModeBin() const +{ + if (m_preambleBinHistory.empty()) { + return 0U; + } + + std::vector counts(m_nbSymbols, 0U); + unsigned int bestBin = m_preambleBinHistory.back() % m_nbSymbols; + unsigned int bestCount = 0U; + + for (unsigned int bin : m_preambleBinHistory) + { + const unsigned int b = bin % m_nbSymbols; + const unsigned int c = ++counts[b]; + + if (c > bestCount) + { + bestCount = c; + bestBin = b; + } + } + + return bestBin; +} + +unsigned int MeshtasticDemodSink::evalSymbol(unsigned int rawSymbol, bool headerSymbol) +{ + unsigned int spread = m_fftInterpolation * (1U << m_settings.m_deBits); + const unsigned int symbolBins = m_fftInterpolation * m_nbSymbols; + + if (symbolBins == 0U) { + return rawSymbol; + } + + // In gr-lora_sdr, explicit-header symbols are always reduced by 2 extra bits + // (sf_app = sf-2), independently of payload LDRO selection. + if (headerSymbol) + { + const int de = m_settings.m_deBits; + if (de < 2) { + spread <<= (2 - de); + } + } + + // Match gr-lora_sdr hard-decoding symbol mapping: + // s = mod(raw_bin - 1, 2^SF * os_factor) / (os_factor * 2^DE) + const unsigned int shifted = (rawSymbol + symbolBins - 1U) % symbolBins; + + if (spread == 0U) { + return shifted; + } + + return shifted / spread; +} + +void MeshtasticDemodSink::tryHeaderLock() +{ + const std::vector& symbols = m_decodeMsg->getSymbols(); + + if (symbols.size() < 8) { + return; + } + + const unsigned int sf = m_settings.m_spreadFactor; + + if (sf < 7) { + return; + } + + const unsigned int headerNbSymbolBits = sf - 2U; + + if (headerNbSymbolBits < 5) { + return; + } + + bool hasCRC = true; + unsigned int nbParityBits = 1U; + unsigned int packetLength = 0U; + int headerParityStatus = (int) MeshtasticDemodSettings::ParityUndefined; + bool headerCRCStatus = false; + + MeshtasticDemodDecoderLoRa::decodeHeader( + symbols, + headerNbSymbolBits, + hasCRC, + nbParityBits, + packetLength, + headerParityStatus, + headerCRCStatus + ); + + if (!headerCRCStatus || packetLength == 0U || nbParityBits < 1U || nbParityBits > 4U) + { + qDebug("MeshtasticDemodSink::tryHeaderLock: header invalid (CRC=%d len=%u CR=%u parity=%d)", + headerCRCStatus ? 1 : 0, + packetLength, + nbParityBits, + headerParityStatus); + return; + } + + const double symbolDurationMs = (double)(1U << sf) * 1000.0 / (double)m_bandwidth; + const bool ldro = symbolDurationMs > 16.0; + const unsigned int sfDenom = sf - (ldro ? 2U : 0U); + + // gr-lora_sdr formula: symb_numb = 8 + ceil(max(0, 2*pay_len - sf + 2 + 5 + has_crc*4) / (sf - 2*ldro)) * (4 + cr) + const int numerator = 2 * (int)packetLength - (int)sf + 2 + 5 + (hasCRC ? 4 : 0); + unsigned int payloadBlocks = 0; + + if (numerator > 0 && sfDenom > 0) { + payloadBlocks = ((unsigned int)numerator + sfDenom - 1U) / sfDenom; + } + + m_expectedSymbols = 8U + payloadBlocks * (4U + nbParityBits); + + if (m_expectedSymbols > m_settings.m_nbSymbolsMax) + { + qDebug("MeshtasticDemodSink::tryHeaderLock: expected %u > max %u, falling back to EOM", + m_expectedSymbols, m_settings.m_nbSymbolsMax); + return; + } + + m_headerLocked = true; + + qDebug("MeshtasticDemodSink::tryHeaderLock: LOCKED len=%u CR=%u CRC=%s LDRO=%s expected=%u symbols", + packetLength, nbParityBits, hasCRC ? "on" : "off", ldro ? "on" : "off", m_expectedSymbols); +} + +bool MeshtasticDemodSink::sendLoRaHeaderProbe() +{ + if (!m_decodeMsg || !m_decoderMsgQueue) { + return false; + } + + const std::vector& symbols = m_decodeMsg->getSymbols(); + + if (symbols.size() < 8U || !m_settings.m_hasHeader) { + return false; + } + + std::vector headerSymbols(symbols.begin(), symbols.begin() + 8); + const unsigned int payloadNbSymbolBits = (m_settings.m_spreadFactor > m_settings.m_deBits) + ? (m_settings.m_spreadFactor - m_settings.m_deBits) + : 1U; + const unsigned int headerNbSymbolBits = (static_cast(m_settings.m_spreadFactor) > 2U) + ? (m_settings.m_spreadFactor - 2U) + : payloadNbSymbolBits; + + MeshtasticDemodMsg::MsgLoRaHeaderProbe *probe = MeshtasticDemodMsg::MsgLoRaHeaderProbe::create( + m_loRaFrameId, + headerSymbols, + payloadNbSymbolBits, + headerNbSymbolBits, + m_settings.m_spreadFactor, + static_cast(std::max(1, m_bandwidth)), + m_settings.m_hasHeader, + m_settings.m_hasCRC + ); + m_decoderMsgQueue->push(probe); + return true; +} + +void MeshtasticDemodSink::applyLoRaHeaderFeedback( + uint32_t frameId, + bool valid, + bool hasCRC, + unsigned int nbParityBits, + unsigned int packetLength, + bool ldro, + unsigned int expectedSymbols, + int headerParityStatus, + bool headerCRCStatus) +{ + (void) hasCRC; + (void) ldro; + (void) headerParityStatus; + + if ((m_settings.m_codingScheme != MeshtasticDemodSettings::CodingLoRa) + || (m_loRaState != LoRaStateSFOCompensation)) + { + return; + } + + if (frameId != m_loRaFrameId) { + return; + } + + m_waitHeaderFeedback = false; + m_headerFeedbackWaitSteps = 0; + + if (!valid || !headerCRCStatus || (packetLength == 0U) || (nbParityBits < 1U) || (nbParityBits > 4U)) + { + qDebug("MeshtasticDemodSink::applyLoRaHeaderFeedback: invalid header -> reset frame"); + resetLoRaFrameSync(); + return; + } + + if (expectedSymbols > m_settings.m_nbSymbolsMax) + { + qDebug("MeshtasticDemodSink::applyLoRaHeaderFeedback: expected %u > max %u, fallback to EOM", + expectedSymbols, m_settings.m_nbSymbolsMax); + return; + } + + m_expectedSymbols = expectedSymbols; + m_headerLocked = true; + m_loRaReceivedHeader = true; +} + +int MeshtasticDemodSink::loRaMod(int a, int b) const +{ + if (b <= 0) { + return 0; + } + + return (a % b + b) % b; +} + +int MeshtasticDemodSink::loRaRound(float number) const +{ + return (number > 0.0f) ? static_cast(number + 0.5f) : static_cast(std::ceil(number - 0.5f)); +} + +void MeshtasticDemodSink::resetLoRaFrameSync() +{ + if ((m_settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + && m_decodeMsg + && (m_loRaState != LoRaStateDetect)) + { + delete m_decodeMsg; + m_decodeMsg = nullptr; + } + + m_loRaState = LoRaStateDetect; + m_loRaSyncState = LoRaSyncNetId1; + m_loRaSampleFifo.clear(); + m_loRaSymbolCnt = 1; + m_loRaBinIdx = 0; + m_loRaKHat = 0; + m_loRaDownVal = 0; + m_loRaCFOInt = 0; + m_loRaNetIdOff = 0; + m_loRaAdditionalUpchirps = 0; + m_loRaCFOFrac = 0.0f; + m_loRaSTOFrac = 0.0f; + m_loRaSFOHat = 0.0f; + m_loRaSFOCum = 0.0f; + m_loRaCFOSTOEstimated = false; + m_loRaReceivedHeader = false; + m_loRaOneSymbolOff = false; + m_loRaFrameSymbolCount = 0; + m_demodActive = false; + m_headerLocked = false; + m_expectedSymbols = 0; + m_waitHeaderFeedback = false; + m_headerFeedbackWaitSteps = 0; + m_syncWord = 0; + + if (!m_loRaPreambleVals.empty()) { + std::fill(m_loRaPreambleVals.begin(), m_loRaPreambleVals.end(), 0); + } + + if (!m_loRaCFOFracCorrec.empty()) { + std::fill(m_loRaCFOFracCorrec.begin(), m_loRaCFOFracCorrec.end(), Complex{1.0f, 0.0f}); + } +} + +void MeshtasticDemodSink::clearSpectrumHistoryForNewFrame() +{ + if (!m_spectrumSink || !m_spectrumLine) { + return; + } + + // Insert a short floor separator between frames to make packet boundaries + // visible even when consecutive packets arrive with a short idle gap. + static constexpr unsigned int kSeparatorLines = 16U; + for (unsigned int i = 0; i < kSeparatorLines; ++i) { + m_spectrumSink->feed(m_spectrumLine, m_nbSymbols); + } +} + +unsigned int MeshtasticDemodSink::getLoRaSymbolVal( + const Complex *samples, + const Complex *refChirp, + std::vector *symbolMagnitudes, + bool publishSpectrum +) +{ + for (unsigned int i = 0; i < m_nbSymbols; i++) { + m_fft->in()[i] = samples[i] * refChirp[i]; + } + + // Canonical gr-lora_sdr demod uses a rectangular symbol window for + // frame_sync/fft_demod symbol decisions. Do not apply user-selected FFT + // windows in LoRa mode, otherwise header symbols drift and CRC checks fail. + + if (m_interpolatedFFTLength > m_fftLength) { + std::fill(m_fft->in() + m_fftLength, m_fft->in() + m_interpolatedFFTLength, Complex{0.0f, 0.0f}); + } + + m_fft->transform(); + + if (symbolMagnitudes) + { + symbolMagnitudes->assign(m_nbSymbols, 0.0f); + + for (unsigned int i = 0; i < m_nbSymbols; i++) { + (*symbolMagnitudes)[i] = static_cast(std::norm(m_fft->out()[i])); + } + } + + double magsq = 0.0; + double magsqTotal = 0.0; + const bool canCaptureSpectrum = (m_spectrumBuffer != nullptr); + const bool publishSpectrumNow = publishSpectrum && (m_spectrumSink != nullptr) && canCaptureSpectrum; + const unsigned int imax = argmax( + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsq, + magsqTotal, + canCaptureSpectrum ? m_spectrumBuffer : nullptr, + m_fftInterpolation + ); + + if (publishSpectrumNow) { + m_spectrumSink->feed(m_spectrumBuffer, m_nbSymbols); + } + + return imax / m_fftInterpolation; +} + +float MeshtasticDemodSink::estimateLoRaCFOFracBernier(const Complex *samples) +{ + if (m_loRaUpSymbToUse <= 1) { + return 0.0f; + } + + std::vector k0(m_loRaUpSymbToUse, 0); + std::vector k0Mag(m_loRaUpSymbToUse, 0.0); + std::vector fftVal(m_loRaUpSymbToUse * m_nbSymbols); + std::vector dechirped(m_nbSymbols); + + for (int i = 0; i < m_loRaUpSymbToUse; i++) + { + const Complex *sym = samples + i * m_nbSymbols; + + for (unsigned int j = 0; j < m_nbSymbols; j++) { + dechirped[j] = sym[j] * m_downChirps[j]; + m_fft->in()[j] = dechirped[j]; + } + + if (m_interpolatedFFTLength > m_fftLength) { + std::fill(m_fft->in() + m_fftLength, m_fft->in() + m_interpolatedFFTLength, Complex{0.0f, 0.0f}); + } + + m_fft->transform(); + + double magsq = 0.0; + double magsqTotal = 0.0; + const unsigned int imax = argmax( + m_fft->out(), + m_fftInterpolation, + m_fftLength, + magsq, + magsqTotal, + nullptr, + m_fftInterpolation + ) / m_fftInterpolation; + k0[i] = static_cast(imax); + k0Mag[i] = magsq; + + for (unsigned int j = 0; j < m_nbSymbols; j++) { + fftVal[j + i * m_nbSymbols] = m_fft->out()[j]; + } + } + + const int idxMax = k0[std::distance(k0Mag.begin(), std::max_element(k0Mag.begin(), k0Mag.end()))]; + Complex fourCum(0.0f, 0.0f); + + for (int i = 0; i < m_loRaUpSymbToUse - 1; i++) { + fourCum += fftVal[idxMax + m_nbSymbols * i] * std::conj(fftVal[idxMax + m_nbSymbols * (i + 1)]); + } + + const float cfoFrac = -std::arg(fourCum) / (2.0f * static_cast(M_PI)); + const unsigned int corrCount = static_cast(m_loRaUpSymbToUse) * m_nbSymbols; + + for (unsigned int n = 0; n < corrCount && n < m_loRaPreambleUpchirps.size(); n++) + { + const float phase = -2.0f * static_cast(M_PI) * cfoFrac * static_cast(n) / static_cast(m_nbSymbols); + m_loRaPreambleUpchirps[n] = samples[n] * Complex(std::cos(phase), std::sin(phase)); + } + + return cfoFrac; +} + +float MeshtasticDemodSink::estimateLoRaSTOFrac() +{ + if (m_loRaUpSymbToUse <= 0) { + return 0.0f; + } + + FFTFactory *fftFactory = DSPEngine::instance()->getFFTFactory(); + FFTEngine *fft2N = nullptr; + const unsigned int fft2NLen = 2U * m_nbSymbols; + const int fft2NSeq = fftFactory->getEngine(fft2NLen, false, &fft2N); + std::vector fftMagSq(fft2NLen, 0.0); + std::vector dechirped(m_nbSymbols); + + for (int i = 0; i < m_loRaUpSymbToUse; i++) + { + const Complex *sym = m_loRaPreambleUpchirps.data() + i * m_nbSymbols; + + for (unsigned int j = 0; j < m_nbSymbols; j++) { + dechirped[j] = sym[j] * m_downChirps[j]; + fft2N->in()[j] = dechirped[j]; + } + + std::fill(fft2N->in() + m_nbSymbols, fft2N->in() + fft2NLen, Complex{0.0f, 0.0f}); + fft2N->transform(); + + for (unsigned int j = 0; j < fft2NLen; j++) { + fftMagSq[j] += std::norm(fft2N->out()[j]); + } + } + + fftFactory->releaseEngine(fft2NLen, false, fft2NSeq); + + const int k0 = static_cast(std::distance(fftMagSq.begin(), std::max_element(fftMagSq.begin(), fftMagSq.end()))); + const double Y_1 = fftMagSq[loRaMod(k0 - 1, static_cast(fft2NLen))]; + const double Y0 = fftMagSq[k0]; + const double Y1 = fftMagSq[loRaMod(k0 + 1, static_cast(fft2NLen))]; + const double u = 64.0 * m_nbSymbols / 406.5506497; + const double v = u * 2.4674; + const double wa = (Y1 - Y_1) / (u * (Y1 + Y_1) + v * Y0 + 1e-12); + const double ka = wa * m_nbSymbols / M_PI; + const double kres = std::fmod((k0 + ka) / 2.0, 1.0); + + return static_cast(kres - (kres > 0.5 ? 1.0 : 0.0)); +} + +void MeshtasticDemodSink::buildLoRaPayloadDownchirp() +{ + const int N = static_cast(m_nbSymbols); + const int id = loRaMod(m_loRaCFOInt, N); + + for (int n = 0; n < N; n++) + { + const int nFold = N - id; + const double nD = static_cast(n); + const double ND = static_cast(N); + double phase; + + if (n < nFold) { + phase = 2.0 * M_PI * ((nD * nD) / (2.0 * ND) + (static_cast(id) / ND - 0.5) * nD); + } else { + phase = 2.0 * M_PI * ((nD * nD) / (2.0 * ND) + (static_cast(id) / ND - 1.5) * nD); + } + + const Complex up(std::cos(phase), std::sin(phase)); + Complex ref = m_settings.m_invertRamps ? up : std::conj(up); + const float cfoPhase = -2.0f * static_cast(M_PI) * m_loRaCFOFrac * static_cast(n) / static_cast(N); + ref *= Complex(std::cos(cfoPhase), std::sin(cfoPhase)); + m_loRaPayloadDownchirp[n] = ref; + } +} + +void MeshtasticDemodSink::finalizeLoRaFrame() +{ + if (!m_decodeMsg) { + resetLoRaFrameSync(); + return; + } + + qDebug( + "MeshtasticDemodSink::finalizeLoRaFrame: frameId=%u symbols=%u headerLocked=%d expected=%u", + m_loRaFrameId, + m_loRaFrameSymbolCount, + m_headerLocked ? 1 : 0, + m_expectedSymbols + ); + + m_decodeMsg->setSignalDb(CalcDb::dbPower(m_magsqOnAvg.asDouble() / (1 << m_settings.m_spreadFactor))); + m_decodeMsg->setNoiseDb(CalcDb::dbPower(m_magsqOffAvg.asDouble() / (1 << m_settings.m_spreadFactor))); + + if (m_decoderMsgQueue && m_settings.m_decodeActive) { + m_decoderMsgQueue->push(m_decodeMsg); + } else { + delete m_decodeMsg; + } + + m_decodeMsg = nullptr; + resetLoRaFrameSync(); +} + +void MeshtasticDemodSink::processSampleLoRa(const Complex& ci) +{ + m_loRaSampleFifo.push_back(ci); + + while (true) + { + const unsigned int needed = (m_loRaState == LoRaStateSync) ? (3U * m_loRaSymbolSpan) : m_loRaSymbolSpan; + + if (m_loRaSampleFifo.size() < needed) { + return; + } + + int consumed = processLoRaFrameSyncStep(); + + if (consumed <= 0) { + consumed = 1; + } + + consumed = std::min(consumed, static_cast(m_loRaSampleFifo.size())); + + for (int i = 0; i < consumed; i++) { + m_loRaSampleFifo.pop_front(); + } + } +} + +int MeshtasticDemodSink::processLoRaFrameSyncStep() +{ + const int stoShift = loRaRound(m_loRaSTOFrac * static_cast(m_osFactor)); + + for (unsigned int ii = 0; ii < m_nbSymbols; ii++) + { + int idx = static_cast(m_osCenterPhase + m_osFactor * ii) - stoShift; + idx = std::max(0, std::min(idx, static_cast(m_loRaSymbolSpan) - 1)); + m_loRaInDown[ii] = m_loRaSampleFifo[static_cast(idx)]; + } + + if (m_loRaState == LoRaStateDetect) + { + const Complex *detectRef = m_settings.m_invertRamps ? m_upChirps : m_downChirps; + // Keep last decoded packet visible until next frame starts. + const int binNew = static_cast(getLoRaSymbolVal(m_loRaInDown.data(), detectRef, nullptr, false)); + const int detectDelta = std::abs(loRaMod(std::abs(binNew - m_loRaBinIdx) + 1, static_cast(m_nbSymbols)) - 1); + const bool isConsecutive = (detectDelta <= 1); + double symbolPower = 0.0; + + for (const Complex &s : m_loRaInDown) { + symbolPower += std::norm(s); + } + + symbolPower /= std::max(1U, m_nbSymbols); + m_magsqTotalAvg(symbolPower); + + if (isConsecutive) + { + if (m_loRaSymbolCnt == 1 && !m_loRaPreambleVals.empty()) { + m_loRaPreambleVals[0] = m_loRaBinIdx; + } + + if ((m_loRaSymbolCnt >= 0) && (m_loRaSymbolCnt < static_cast(m_loRaPreambleVals.size()))) { + m_loRaPreambleVals[m_loRaSymbolCnt] = binNew; + } + + const size_t sOfs = static_cast(m_loRaSymbolCnt) * m_nbSymbols; + if (sOfs + m_nbSymbols <= m_loRaPreambleRaw.size()) { + std::copy_n(m_loRaInDown.begin(), m_nbSymbols, m_loRaPreambleRaw.begin() + sOfs); + } + + const size_t upOfs = static_cast(m_loRaSymbolCnt) * m_loRaSymbolSpan; + if (upOfs + m_loRaSymbolSpan <= m_loRaPreambleRawUp.size()) { + std::copy_n(m_loRaSampleFifo.begin(), m_loRaSymbolSpan, m_loRaPreambleRawUp.begin() + upOfs); + } + + m_loRaSymbolCnt++; + } + else + { + m_magsqOffAvg(symbolPower); + + if (m_loRaPreambleRaw.size() >= m_nbSymbols) { + std::copy_n(m_loRaInDown.begin(), m_nbSymbols, m_loRaPreambleRaw.begin()); + } + + if (m_loRaPreambleRawUp.size() >= m_loRaSymbolSpan) { + std::copy_n(m_loRaSampleFifo.begin(), m_loRaSymbolSpan, m_loRaPreambleRawUp.begin()); + } + + m_loRaSymbolCnt = 1; + } + + m_loRaBinIdx = binNew; + + if ((m_loRaSymbolCnt >= static_cast(m_loRaRequiredUpchirps)) + && !m_loRaPreambleVals.empty()) + { + m_loRaAdditionalUpchirps = 0; + m_loRaState = LoRaStateSync; + m_loRaSyncState = LoRaSyncNetId1; + m_loRaSymbolCnt = 0; + m_loRaCFOSTOEstimated = false; + + std::vector hist(m_nbSymbols, 0U); + unsigned int bestBin = 0U; + unsigned int bestCount = 0U; + + for (int v : m_loRaPreambleVals) + { + const unsigned int b = static_cast(loRaMod(v, static_cast(m_nbSymbols))); + const unsigned int c = ++hist[b]; + + if (c > bestCount) { + bestCount = c; + bestBin = b; + } + } + + m_loRaKHat = static_cast(bestBin); + const int netStart = static_cast(0.75f * static_cast(m_loRaSymbolSpan)) - m_loRaKHat * static_cast(m_osFactor); + + for (unsigned int i = 0; i < m_loRaSymbolSpan / 4U; i++) + { + const int src = std::max(0, std::min(netStart + static_cast(i), static_cast(m_loRaSampleFifo.size()) - 1)); + if (i < m_loRaNetIdSamp.size()) { + m_loRaNetIdSamp[i] = m_loRaSampleFifo[static_cast(src)]; + } + } + + return static_cast(m_osFactor * (m_nbSymbols - bestBin)); + } + + return static_cast(m_loRaSymbolSpan); + } + + if (m_loRaState == LoRaStateSync) + { + if (!m_loRaCFOSTOEstimated) + { + const int cfoStart = std::max(0, static_cast(m_nbSymbols) - m_loRaKHat); + + if (cfoStart < static_cast(m_loRaPreambleRaw.size())) { + m_loRaCFOFrac = estimateLoRaCFOFracBernier(m_loRaPreambleRaw.data() + cfoStart); + } else { + m_loRaCFOFrac = 0.0f; + } + + m_loRaSTOFrac = estimateLoRaSTOFrac(); + + for (unsigned int n = 0; n < m_nbSymbols; n++) + { + const float phase = -2.0f * static_cast(M_PI) * m_loRaCFOFrac * static_cast(n) / static_cast(m_nbSymbols); + m_loRaCFOFracCorrec[n] = Complex(std::cos(phase), std::sin(phase)); + } + + m_loRaCFOSTOEstimated = true; + } + + for (unsigned int i = 0; i < m_nbSymbols; i++) { + m_loRaSymbCorr[i] = m_loRaInDown[i] * m_loRaCFOFracCorrec[i]; + } + + const Complex *syncRef = m_settings.m_invertRamps ? m_upChirps : m_downChirps; + const int binIdx = static_cast(getLoRaSymbolVal(m_loRaSymbCorr.data(), syncRef, nullptr, true)); + + switch (m_loRaSyncState) + { + case LoRaSyncNetId1: + if ((binIdx == 0) || (binIdx == 1) || (binIdx == static_cast(m_nbSymbols) - 1)) + { + const size_t dstOfs = static_cast(m_loRaRequiredUpchirps + m_loRaAdditionalUpchirps) * m_loRaSymbolSpan; + + if (dstOfs + m_loRaSymbolSpan <= m_loRaPreambleRawUp.size()) { + std::copy_n(m_loRaSampleFifo.begin(), m_loRaSymbolSpan, m_loRaPreambleRawUp.begin() + dstOfs); + } + + m_loRaAdditionalUpchirps = std::min(m_loRaAdditionalUpchirps + 1, 3); + } + else + { + m_loRaSyncState = LoRaSyncNetId2; + m_loRaNetIds[0] = binIdx; + } + break; + case LoRaSyncNetId2: + m_loRaSyncState = LoRaSyncDownchirp1; + m_loRaNetIds[1] = binIdx; + break; + case LoRaSyncDownchirp1: + m_loRaSyncState = LoRaSyncDownchirp2; + break; + case LoRaSyncDownchirp2: + m_loRaDownVal = static_cast(getLoRaSymbolVal(m_loRaSymbCorr.data(), m_settings.m_invertRamps ? m_downChirps : m_upChirps, nullptr, true)); + if (m_loRaAdditionalSymbolSamp.size() >= m_loRaSymbolSpan) { + std::copy_n(m_loRaSampleFifo.begin(), m_loRaSymbolSpan, m_loRaAdditionalSymbolSamp.begin()); + } + m_loRaSyncState = LoRaSyncQuarterDown; + break; + case LoRaSyncQuarterDown: + default: + if (m_loRaAdditionalSymbolSamp.size() >= 2U * m_loRaSymbolSpan) { + std::copy_n(m_loRaSampleFifo.begin(), m_loRaSymbolSpan, m_loRaAdditionalSymbolSamp.begin() + m_loRaSymbolSpan); + } + + if (static_cast(m_loRaDownVal) < m_nbSymbols / 2U) { + m_loRaCFOInt = static_cast(std::floor(m_loRaDownVal / 2.0)); + } else { + m_loRaCFOInt = static_cast(std::floor((m_loRaDownVal - static_cast(m_nbSymbols)) / 2.0)); + } + + const unsigned int upSymCount = std::min( + static_cast(std::max(0, m_loRaUpSymbToUse)), + static_cast(m_loRaPreambleUpchirps.size() / std::max(1U, m_nbSymbols)) + ); + const unsigned int corrLen = upSymCount * m_nbSymbols; + + if (corrLen > 0U) + { + const int cfoIntMod = loRaMod(m_loRaCFOInt, static_cast(m_nbSymbols)); + std::rotate( + m_loRaPreambleUpchirps.begin(), + m_loRaPreambleUpchirps.begin() + cfoIntMod, + m_loRaPreambleUpchirps.begin() + corrLen + ); + + std::vector cfoIntCorrec(corrLen, Complex{1.0f, 0.0f}); + for (unsigned int n = 0; n < corrLen; n++) + { + const float phase = -2.0f * static_cast(M_PI) + * static_cast(m_loRaCFOInt) + * static_cast(n) + / static_cast(m_nbSymbols); + cfoIntCorrec[n] = Complex(std::cos(phase), std::sin(phase)); + } + + for (unsigned int n = 0; n < corrLen; n++) { + m_loRaPreambleUpchirps[n] *= cfoIntCorrec[n]; + } + + if (m_deviceCenterFrequency > 0) { + m_loRaSFOHat = + (static_cast(m_loRaCFOInt) + m_loRaCFOFrac) + * static_cast(m_bandwidth) + / static_cast(m_deviceCenterFrequency); + } else { + m_loRaSFOHat = 0.0f; + } + + std::vector sfoCorrec(corrLen, Complex{1.0f, 0.0f}); + const double clkOff = static_cast(m_loRaSFOHat) / static_cast(m_nbSymbols); + const double fs = static_cast(m_bandwidth); + const double fsP = fs * (1.0 - clkOff); + const int N = static_cast(m_nbSymbols); + + for (unsigned int n = 0; n < corrLen; n++) + { + const double nMod = static_cast(loRaMod(static_cast(n), N)); + const double nFloor = std::floor(static_cast(n) / static_cast(N)); + const double q1 = (nMod * nMod) / (2.0 * static_cast(N)) + * ((m_bandwidth / fsP) * (m_bandwidth / fsP) - (m_bandwidth / fs) * (m_bandwidth / fs)); + const double q2 = (nFloor * ((m_bandwidth / fsP) * (m_bandwidth / fsP) - (m_bandwidth / fsP)) + + m_bandwidth / 2.0 * (1.0 / fs - 1.0 / fsP)) * nMod; + const double phase = -2.0 * M_PI * (q1 + q2); + sfoCorrec[n] = Complex(std::cos(phase), std::sin(phase)); + } + + for (unsigned int n = 0; n < corrLen; n++) { + m_loRaPreambleUpchirps[n] *= sfoCorrec[n]; + } + + const float tmpSto = estimateLoRaSTOFrac(); + const float diffSto = m_loRaSTOFrac - tmpSto; + + if (std::abs(diffSto) <= (static_cast(m_osFactor) - 1.0f) / static_cast(m_osFactor)) { + m_loRaSTOFrac = tmpSto; + } + + std::vector netIdsDec(2U * m_nbSymbols, Complex{0.0f, 0.0f}); + const int startOff = static_cast(m_osFactor / 2U) + - loRaRound(m_loRaSTOFrac * static_cast(m_osFactor)) + + static_cast(m_osFactor) + * (static_cast(0.25f * static_cast(m_nbSymbols)) + m_loRaCFOInt); + + for (unsigned int i = 0; i < 2U * m_nbSymbols; i++) + { + const int idx = std::max( + 0, + std::min(startOff + static_cast(i * m_osFactor), static_cast(m_loRaNetIdSamp.size()) - 1) + ); + netIdsDec[i] = m_loRaNetIdSamp[static_cast(idx)]; + } + + for (unsigned int i = 0; i < 2U * m_nbSymbols; i++) + { + netIdsDec[i] *= cfoIntCorrec[i % std::max(1U, corrLen)]; + + const float phase = -2.0f * static_cast(M_PI) + * m_loRaCFOFrac + * static_cast(i % m_nbSymbols) + / static_cast(m_nbSymbols); + netIdsDec[i] *= Complex(std::cos(phase), std::sin(phase)); + } + + const int netid1 = static_cast(getLoRaSymbolVal(netIdsDec.data(), m_settings.m_invertRamps ? m_upChirps : m_downChirps)); + const int netid2 = static_cast(getLoRaSymbolVal(netIdsDec.data() + m_nbSymbols, m_settings.m_invertRamps ? m_upChirps : m_downChirps)); + m_loRaNetIds[0] = netid1; + m_loRaNetIds[1] = netid2; + m_loRaNetIdOff = netid1; + } + else + { + if (m_deviceCenterFrequency > 0) { + m_loRaSFOHat = + (static_cast(m_loRaCFOInt) + m_loRaCFOFrac) + * static_cast(m_bandwidth) + / static_cast(m_deviceCenterFrequency); + } else { + m_loRaSFOHat = 0.0f; + } + } + + buildLoRaPayloadDownchirp(); + m_loRaFrameId++; + m_decodeMsg = MeshtasticDemodMsg::MsgDecodeSymbols::create(); + m_decodeMsg->setFrameId(m_loRaFrameId); + m_decodeMsg->setSyncWord(0U); + clearSpectrumHistoryForNewFrame(); + m_loRaFrameSymbolCount = 0U; + m_magsqMax = 0.0; + m_headerLocked = false; + m_expectedSymbols = 0; + m_waitHeaderFeedback = false; + m_headerFeedbackWaitSteps = 0U; + m_demodActive = true; + m_loRaSTOFrac += m_loRaSFOHat * 4.25f; + if (std::abs(m_loRaSTOFrac) > 0.5f) { + m_loRaSTOFrac += (m_loRaSTOFrac > 0.0f) ? -1.0f : 1.0f; + } + const float stoQuant = static_cast(loRaRound(m_loRaSTOFrac * static_cast(m_osFactor))); + m_loRaSFOCum = ((m_loRaSTOFrac * static_cast(m_osFactor)) - stoQuant) / static_cast(m_osFactor); + m_loRaState = LoRaStateSFOCompensation; + m_loRaSyncState = LoRaSyncNetId1; + return std::max(1, static_cast(m_loRaSymbolSpan / 4U + static_cast(m_osFactor) * m_loRaCFOInt)); + } + + return static_cast(m_loRaSymbolSpan); + } + + if (!m_decodeMsg) + { + resetLoRaFrameSync(); + return static_cast(m_loRaSymbolSpan); + } + + if (m_settings.m_hasHeader + && !m_headerLocked + && (m_loRaFrameSymbolCount >= 8U) + && m_waitHeaderFeedback) + { + const unsigned int maxWaitSteps = std::max(1U, m_headerFeedbackMaxWaitSteps); + + if (++m_headerFeedbackWaitSteps > maxWaitSteps) { + // Safety fallback when async feedback is delayed. + m_waitHeaderFeedback = false; + qDebug("MeshtasticDemodSink::processLoRaFrameSyncStep: header feedback timeout -> local fallback"); + tryHeaderLock(); + } + } + + std::vector symbolMags; + const unsigned int rawSymbol = getLoRaSymbolVal(m_loRaInDown.data(), m_loRaPayloadDownchirp.data(), &symbolMags, true); + const bool headerSymbol = m_settings.m_hasHeader && (m_loRaFrameSymbolCount < 8U); + const unsigned short symbol = evalSymbol(rawSymbol, headerSymbol) % m_nbSymbolsEff; + m_decodeMsg->pushBackSymbol(symbol); + m_decodeMsg->pushBackMagnitudes(symbolMags); + + if (m_spectrumBuffer) + { + std::vector spectrumLine; + spectrumLine.reserve(m_nbSymbols); + + for (unsigned int i = 0; i < m_nbSymbols; ++i) { + spectrumLine.push_back(static_cast(std::norm(m_spectrumBuffer[i]))); + } + + m_decodeMsg->pushBackDechirpedSpectrumLine(spectrumLine); + } + + double magsq = 0.0; + for (const Complex &s : m_loRaInDown) { + magsq += std::norm(s); + } + magsq /= std::max(1U, m_nbSymbols); + + if (magsq > m_magsqMax) { + m_magsqMax = magsq; + } + + m_magsqTotalAvg(magsq); + m_magsqOnAvg(magsq); + m_loRaFrameSymbolCount++; + + if (!m_headerLocked + && m_settings.m_hasHeader + && (m_loRaFrameSymbolCount == 8U)) + { + if (sendLoRaHeaderProbe()) { + m_waitHeaderFeedback = true; + m_headerFeedbackWaitSteps = 0U; + } else { + tryHeaderLock(); + } + } + + if (m_headerLocked) + { + if (m_loRaFrameSymbolCount >= m_expectedSymbols) { + finalizeLoRaFrame(); + } + } + else if (m_loRaFrameSymbolCount >= m_settings.m_nbSymbolsMax) + { + finalizeLoRaFrame(); + } + + int itemsToConsume = static_cast(m_loRaSymbolSpan); + + if (std::abs(m_loRaSFOCum) > (1.0f / (2.0f * static_cast(m_osFactor)))) + { + const int step = std::signbit(m_loRaSFOCum) ? -1 : 1; + itemsToConsume -= step; + m_loRaSFOCum -= step * (1.0f / static_cast(m_osFactor)); + } + + m_loRaSFOCum += m_loRaSFOHat; + return std::max(1, itemsToConsume); +} + +void MeshtasticDemodSink::applyChannelSettings(int channelSampleRate, int bandwidth, int channelFrequencyOffset, bool force) +{ + qDebug() << "MeshtasticDemodSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset + << " bandwidth: " << bandwidth; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) || + (channelSampleRate != m_channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || + (bandwidth != m_bandwidth) || force) + { + const int targetFrameSyncRate = std::max(1, bandwidth * static_cast(m_osFactor)); + // Keep the anti-alias/channel filter narrow around the configured LoRa bandwidth. + // A too-wide cutoff destabilizes preamble bin tracking in DETECT. + m_interpolator.create(16, channelSampleRate, m_bandwidth / 1.9f); + m_interpolatorDistance = (Real) channelSampleRate / (Real) targetFrameSyncRate; + m_sampleDistanceRemain = 0; + m_osCounter = 0; + qDebug() << "MeshtasticDemodSink::applyChannelSettings: m_interpolator.create:" + << " m_interpolatorDistance: " << m_interpolatorDistance + << " targetFrameSyncRate: " << targetFrameSyncRate + << " osFactor: " << m_osFactor; + } + + m_channelSampleRate = channelSampleRate; + m_bandwidth = bandwidth; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void MeshtasticDemodSink::applySettings(const MeshtasticDemodSettings& settings, bool force) +{ + qDebug() << "MeshtasticDemodSink::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_bandwidthIndex: " << settings.m_bandwidthIndex + << " m_spreadFactor: " << settings.m_spreadFactor + << " m_rgbColor: " << settings.m_rgbColor + << " m_title: " << settings.m_title + << " force: " << force; + + const unsigned int desiredFFTInterpolation = (settings.m_codingScheme == MeshtasticDemodSettings::CodingLoRa) + ? m_loRaFFTInterpolation + : m_legacyFFTInterpolation; + const bool fftInterpChanged = desiredFFTInterpolation != m_fftInterpolation; + + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) + || (settings.m_deBits != m_settings.m_deBits) + || (settings.m_fftWindow != m_settings.m_fftWindow) + || fftInterpChanged + || force) + { + m_fftInterpolation = desiredFFTInterpolation; + initSF(settings.m_spreadFactor, settings.m_deBits, settings.m_fftWindow); + } + + const unsigned int configuredPreamble = settings.m_preambleChirps > 0U + ? settings.m_preambleChirps + : m_minRequiredPreambleChirps; + const unsigned int targetRequired = configuredPreamble > 3U + ? (configuredPreamble - 3U) + : m_minRequiredPreambleChirps; + m_requiredPreambleChirps = std::max( + m_minRequiredPreambleChirps, + std::min(targetRequired, m_maxRequiredPreambleChirps) + ); + qDebug() << "MeshtasticDemodSink::applySettings:" + << " requiredPreambleChirps: " << m_requiredPreambleChirps + << " configuredPreamble: " << settings.m_preambleChirps + << " fftInterpolation: " << m_fftInterpolation; + + m_settings = settings; + m_loRaRequiredUpchirps = m_requiredPreambleChirps; + m_loRaUpSymbToUse = (m_loRaRequiredUpchirps > 0U) ? static_cast(m_loRaRequiredUpchirps - 1U) : 0; + m_loRaPreambleVals.assign(m_loRaRequiredUpchirps, 0); + m_loRaPreambleRaw.assign(m_nbSymbols * m_loRaRequiredUpchirps, Complex{0.0f, 0.0f}); + m_loRaPreambleUpchirps.assign(m_nbSymbols * m_loRaRequiredUpchirps, Complex{0.0f, 0.0f}); + m_loRaPreambleRawUp.assign((m_settings.m_preambleChirps + 3U) * m_loRaSymbolSpan, Complex{0.0f, 0.0f}); + m_loRaCFOFracCorrec.assign(m_nbSymbols, Complex{1.0f, 0.0f}); + m_loRaPayloadDownchirp.assign(m_nbSymbols, Complex{1.0f, 0.0f}); + m_loRaSymbCorr.assign(m_nbSymbols, Complex{0.0f, 0.0f}); + m_loRaNetIdSamp.assign((m_loRaSymbolSpan * 5U) / 2U + m_loRaSymbolSpan, Complex{0.0f, 0.0f}); + m_loRaAdditionalSymbolSamp.assign(m_loRaSymbolSpan * 2U, Complex{0.0f, 0.0f}); + resetLoRaFrameSync(); +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodsink.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodsink.h new file mode 100644 index 000000000..a16855e3d --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodsink.h @@ -0,0 +1,258 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMODSINK_H +#define INCLUDE_MESHTASTICDEMODSINK_H + +#include +#include +#include +#include +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/fftwindow.h" +#include "util/movingaverage.h" + +#include "meshtasticdemodsettings.h" + +class BasebandSampleSink; +class FFTEngine; +namespace MeshtasticDemodMsg { + class MsgDecodeSymbols; +} +class MessageQueue; + +class MeshtasticDemodSink : public ChannelSampleSink { +public: + MeshtasticDemodSink(); + ~MeshtasticDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + bool getDemodActive() const { return m_demodActive; } + void setDecoderMessageQueue(MessageQueue *messageQueue) { m_decoderMsgQueue = messageQueue; } + void setSpectrumSink(BasebandSampleSink* spectrumSink) { m_spectrumSink = spectrumSink; } + void setDeviceCenterFrequency(qint64 centerFrequency) { m_deviceCenterFrequency = centerFrequency; } + void applyLoRaHeaderFeedback( + uint32_t frameId, + bool valid, + bool hasCRC, + unsigned int nbParityBits, + unsigned int packetLength, + bool ldro, + unsigned int expectedSymbols, + int headerParityStatus, + bool headerCRCStatus + ); + void applyChannelSettings(int channelSampleRate, int bandwidth, int channelFrequencyOffset, bool force = false); + void applySettings(const MeshtasticDemodSettings& settings, bool force = false); + double getCurrentNoiseLevel() const { return m_magsqOffAvg.instantAverage() / (1< m_preambleBinHistory; //!< Rolling preamble bins for mode (k_hat) estimation + unsigned int m_preambleConsecutive; //!< Consecutive upchirp count in DETECT state + bool m_havePrevPreambleBin; + unsigned int m_prevPreambleBin; + unsigned int m_requiredPreambleChirps; + unsigned int m_preambleHistory[m_maxSFDSearchChirps]; + unsigned int m_syncWord; + double m_magsqMax; + MovingAverageUtil m_magsqOnAvg; + MovingAverageUtil m_magsqOffAvg; + MovingAverageUtil m_magsqTotalAvg; + std::queue m_magsqQueue; + unsigned int m_chirpCount; //!< Generic chirp counter + unsigned int m_sfdSkip; //!< Number of samples in a SFD skip or slide (1/4) period + unsigned int m_sfdSkipCounter; //!< Counter of skip or slide periods + + bool m_headerLocked; //!< True when header decode succeeded and we have a deterministic symbol budget + unsigned int m_expectedSymbols; //!< Total expected symbols (header + payload) from header decode + bool m_waitHeaderFeedback; + unsigned int m_headerFeedbackWaitSteps; + uint32_t m_loRaFrameId; + static constexpr unsigned int m_headerFeedbackMaxWaitSteps = 128; + + unsigned int m_osFactor; //!< Oversampling factor at frame-sync input (gr-lora_sdr os_factor) + unsigned int m_osCenterPhase; //!< Selected downsample phase inside oversampled symbol + unsigned int m_osCounter; //!< Oversampled sample counter + LoRaFrameSyncState m_loRaState; + LoRaSyncState m_loRaSyncState; + std::deque m_loRaSampleFifo; + std::vector m_loRaInDown; + std::vector m_loRaPreambleRaw; + std::vector m_loRaPreambleRawUp; + std::vector m_loRaPreambleUpchirps; + std::vector m_loRaRefUpchirp; + std::vector m_loRaRefDownchirp; + std::vector m_loRaCFOFracCorrec; + std::vector m_loRaPayloadDownchirp; + std::vector m_loRaSymbCorr; + std::vector m_loRaNetIdSamp; + std::vector m_loRaAdditionalSymbolSamp; + std::vector m_loRaPreambleVals; + std::vector m_loRaNetIds; + int m_loRaSymbolCnt; + int m_loRaBinIdx; + int m_loRaKHat; + int m_loRaDownVal; + int m_loRaCFOInt; + int m_loRaNetIdOff; + int m_loRaAdditionalUpchirps; + int m_loRaUpSymbToUse; + unsigned int m_loRaRequiredUpchirps; + unsigned int m_loRaSymbolSpan; + unsigned int m_loRaFrameSymbolCount; + float m_loRaCFOFrac; + float m_loRaSTOFrac; + float m_loRaSFOHat; + float m_loRaSFOCum; + bool m_loRaCFOSTOEstimated; + bool m_loRaReceivedHeader; + bool m_loRaOneSymbolOff; + + NCO m_nco; + Interpolator m_interpolator; + Real m_sampleDistanceRemain; + Real m_interpolatorDistance; + + BasebandSampleSink* m_spectrumSink; + Complex *m_spectrumBuffer; + + unsigned int m_nbSymbols; //!< Number of symbols = length of base FFT + unsigned int m_nbSymbolsEff; //!< effective symbols considering DE bits + unsigned int m_fftLength; //!< Length of base FFT + unsigned int m_fftInterpolation; //!< FFT interpolation factor (LoRa=1, legacy modes=4) + unsigned int m_interpolatedFFTLength; //!< Length of interpolated FFT + int m_deLength; //!< Number of FFT bins collated to represent one symbol + int m_preambleTolerance; //!< Number of FFT bins to collate when looking for preamble + + void processSample(const Complex& ci); + void initSF(unsigned int sf, unsigned int deBits, FFTWindow::Function fftWindow); //!< Init tables, FFTs, depending on spread factor + void reset(); + unsigned int argmax( + const Complex *fftBins, + unsigned int fftMult, + unsigned int fftLength, + double& magsqMax, + double& magSqTotal, + Complex *specBuffer, + unsigned int specDecim + ); + unsigned int argmaxSpreaded( //!< count energy in adjacent bins for same symbol (needs DE bits > 0) + const Complex *fftBins, + unsigned int fftMult, + unsigned int fftLength, + double& magsqMax, + double& magsqNoise, + double& magSqTotal, + Complex *specBuffer, + unsigned int specDecim + ); + unsigned int extractMagnitudes( + std::vector& magnitudes, + const Complex *fftBins, + unsigned int fftMult, + unsigned int fftLength, + double& magsqMax, + double& magSqTotal, + Complex *specBuffer, + unsigned int specDecim + ); + void decimateSpectrum(Complex *in, Complex *out, unsigned int size, unsigned int decimation); + int toSigned(int u, int intSize); + int circularBinDelta(unsigned int current, unsigned int previous) const; + unsigned int getPreambleModeBin() const; + unsigned int evalSymbol(unsigned int rawSymbol, bool headerSymbol = false); + void tryHeaderLock(); //!< Attempt inline header decode after 8 symbols to determine expected frame length + bool sendLoRaHeaderProbe(); + void processSampleLoRa(const Complex& ci); + int processLoRaFrameSyncStep(); + void resetLoRaFrameSync(); + void clearSpectrumHistoryForNewFrame(); + int loRaMod(int a, int b) const; + int loRaRound(float number) const; + unsigned int getLoRaSymbolVal( + const Complex *samples, + const Complex *refChirp, + std::vector *symbolMagnitudes = nullptr, + bool publishSpectrum = false + ); + float estimateLoRaCFOFracBernier(const Complex *samples); + float estimateLoRaSTOFrac(); + void buildLoRaPayloadDownchirp(); + void finalizeLoRaFrame(); +}; + +#endif // INCLUDE_MESHTASTICDEMODSINK_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodwebapiadapter.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodwebapiadapter.cpp new file mode 100644 index 000000000..7f2fd7bf7 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 "SWGChannelSettings.h" +#include "meshtasticdemod.h" +#include "meshtasticdemodwebapiadapter.h" + +MeshtasticDemodWebAPIAdapter::MeshtasticDemodWebAPIAdapter() +{} + +MeshtasticDemodWebAPIAdapter::~MeshtasticDemodWebAPIAdapter() +{} + +int MeshtasticDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatDemodSettings(new SWGSDRangel::SWGChirpChatDemodSettings()); + response.getChirpChatDemodSettings()->init(); + MeshtasticDemod::webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int MeshtasticDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + MeshtasticDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + MeshtasticDemod::webapiFormatChannelSettings(response, m_settings); + return 200; +} diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodwebapiadapter.h b/plugins/channelrx/demodmeshtastic/meshtasticdemodwebapiadapter.h new file mode 100644 index 000000000..67320c065 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodwebapiadapter.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICDEMOD_WEBAPIADAPTER_H +#define INCLUDE_MESHTASTICDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "meshtasticdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class MeshtasticDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + MeshtasticDemodWebAPIAdapter(); + virtual ~MeshtasticDemodWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + MeshtasticDemodSettings m_settings; +}; + +#endif // INCLUDE_MESHTASTICDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodmeshtastic/meshtasticplugin.cpp b/plugins/channelrx/demodmeshtastic/meshtasticplugin.cpp new file mode 100644 index 000000000..78bca9ce8 --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticplugin.cpp @@ -0,0 +1,88 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2019 Davide Gerhard // +// Copyright (C) 2020 Kacper Michajłow // +// // +// 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 "plugin/pluginapi.h" + +#include "meshtasticplugin.h" +#ifndef SERVER_MODE +#include "meshtasticdemodgui.h" +#endif +#include "meshtasticdemod.h" + +const PluginDescriptor MeshtasticPlugin::m_pluginDescriptor = { + MeshtasticDemod::m_channelId, + QStringLiteral("Meshtastic Demodulator"), + QStringLiteral("7.23.1"), + QStringLiteral("(c) Edouard Griffiths, F4EXB"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +MeshtasticPlugin::MeshtasticPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& MeshtasticPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void MeshtasticPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register demodulator + m_pluginAPI->registerRxChannel(MeshtasticDemod::m_channelIdURI, MeshtasticDemod::m_channelId, this); +} + +void MeshtasticPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + MeshtasticDemod *instance = new MeshtasticDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* MeshtasticPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return nullptr; +} +#else +ChannelGUI* MeshtasticPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return MeshtasticDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif diff --git a/plugins/channelrx/demodmeshtastic/meshtasticplugin.h b/plugins/channelrx/demodmeshtastic/meshtasticplugin.h new file mode 100644 index 000000000..292ef1aac --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/meshtasticplugin.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2015 John Greb // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICPLUGIN_H +#define INCLUDE_MESHTASTICPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class MeshtasticPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.meshtasticdemod") + +public: + explicit MeshtasticPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_LoRaPLUGIN_H diff --git a/plugins/channelrx/demodmeshtastic/readme.md b/plugins/channelrx/demodmeshtastic/readme.md new file mode 100644 index 000000000..e8b5e693d --- /dev/null +++ b/plugins/channelrx/demodmeshtastic/readme.md @@ -0,0 +1,384 @@ +

Meshtastic demodulator plugin

+ +

Introduction

+ +This plugin can be used to demodulate and decode transmissions based on Chirp Spread Spectrum (CSS). The basic idea is to transform each symbol of a MFSK modulation to an ascending frequency ramp shifted in time. It could equally be a descending ramp but this one is reserved to detect a break in the preamble sequence (synchronization). This plugin has been designed to work in conjunction with the ChirpChat modulator plugin that should be used ideally on the transmission side. + +It has clearly been inspired by the LoRa technique but is designed for experimentation and extension to other protocols mostly inspired by amateur radio techniques using chirp modulation to transmit symbols. Thanks to the MFSK to chirp translation it is possible to adapt any MFSK based mode. + +LoRa is a property of Semtech and the details of the protocol are not made public. However a LoRa compatible protocol has been implemented based on the reverse engineering performed by the community. It is mainly based on the work done in https://github.com/myriadrf/LoRa-SDR. You can find more information about LoRa and chirp modulation here: + + - To get an idea of what is LoRa: [here](https://www.link-labs.com/blog/what-is-lora) + - A detailed inspection of LoRa modulation and protocol: [here](https://static1.squarespace.com/static/54cecce7e4b054df1848b5f9/t/57489e6e07eaa0105215dc6c/1464376943218/Reversing-Lora-Knight.pdf) + +⚠ Only spread factors of 11 and 12 are working in LoRa mode with the distance enhancement active (DE=2) + +Transmissions from the RN2483 module (SF=11 and SF=12 with DE=2) could be successfully received. It has not been tested with Semtech SX127x hardware. This LoRa decoder is designed for experimentation. For production grade applications it is recommended to use dedicated hardware instead. + +Modulation characteristics from LoRa have been augmented with more bandwidths and FFT bin collations (DE factor). Plain TTY and ASCII have also been added and there are plans to add some more complex typically amateur radio MFSK based modes like JT65. + +In any case it is recommended to use a non zero distance enhancement factor for successful transmissions. + +Note: this plugin is officially supported since version 6. + +

Meshtastic decode mode

+ +In LoRa coding mode, decoded payload bytes are automatically tested as Meshtastic over-the-air frames (16-byte radio header + protobuf `Data` payload). When a frame is decoded successfully, an additional text line is appended in the message window with a `MESH RX|...` summary. + +Key handling for decryption supports: + + - `none` / plaintext + - Meshtastic shorthand keys (`default`, `simple0..10`) + - Explicit `hex:` and `base64:` keys + - Multiple keys from environment + +Environment variables: + + - `SDRANGEL_MESHTASTIC_KEYS`: comma-separated key specs. You can optionally bind a channel name using `ChannelName=KeySpec`. + - `SDRANGEL_MESHTASTIC_CHANNEL_NAME`: default channel name used when `SDRANGEL_MESHTASTIC_KEYS` is not set (default: `LongFast`). + - `SDRANGEL_MESHTASTIC_KEY`: default key used when `SDRANGEL_MESHTASTIC_KEYS` is not set (default: `default`). + +UI key manager: + + - Use the **Keys...** button next to `Region/Preset/Channel` to set a per-channel key list. + - Entries accept the same formats as environment variables and support `channelName=keySpec`. + - When a custom list is saved, it is persisted in the channel settings and takes precedence over environment variables for this demodulator instance. + +Examples: + + - `SDRANGEL_MESHTASTIC_KEYS=\"LongFast=default,Ops=hex:00112233445566778899aabbccddeeff,none\"` + - `SDRANGEL_MESHTASTIC_KEYS=\"base64:2PG7OiApB1nwvP+rz05pAQ==\"` + +

Meshtastic quick profile

+ +The demodulator includes Meshtastic profile controls in the RF settings section: + + - **Region**: Meshtastic region code (`US`, `EU_868`, ...) + - **Preset**: Meshtastic modem preset (`LONG_FAST`, ...) + - **Channel**: Meshtastic channel number, shown as Meshtastic-style zero-based index and auto-populated for selected region/preset + - **Auto Input Tune**: when enabled, tries to set source sample-rate/decimation automatically for the selected LoRa bandwidth + - **Auto Lock**: arms first and waits for on-air activity, then scans `Inv` + frequency offset candidates. For SF11/SF12 it also probes `DE=0` and `DE=2` as compatibility candidates. It scores using decode quality plus source-side intensity (demod activity + power/noise), but only auto-applies candidates with decode evidence (header/payload CRC backed); otherwise it restores baseline settings. + +When either selection changes, the demodulator auto-applies LoRa decode parameters for the selected profile: + + - bandwidth + - spread factor + - DE bits + - preamble length (16 on sub-GHz presets, 12 on 2.4 GHz) + - FEC parity bits + - header/CRC expectations + +If the device center frequency is known, the channel offset is also auto-centered to the selected region/preset default channel. +The demodulator also attempts to auto-tune the device sample rate/decimation (when supported by the device) so effective baseband rate is suitable for the selected LoRa bandwidth. + +

Interface

+ +The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md) + +![Meshtastic Demodulator plugin GUI](../../../doc/img/MeshtasticDemod_plugin.png) + +

1: Frequency shift from center frequency of reception

+ +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +

2: De-chirped channel power

+ +This is the total power in the FFT of the de-chirped signal in dB. When no ChirpChat signal is detected this corresponds to the power received in the bandwidth (3). It will show a significant increase in presence of a ChirpChat signal that can be detected. + +

3: Bandwidth

+ +This is the bandwidth of the ChirpChat signal. Similarly to LoRa the signal sweeps between the lower and the upper frequency of this bandwidth. The sample rate of the ChirpChat signal in seconds is exactly one over this bandwidth in Hertz. + +In the LoRa standard there are 2 base bandwidths: 500 and 333.333 kHz. A 400 kHz base has been added. Possible bandwidths are obtained by a division of these base bandwidths by a power of two from 1 to 64. Extra divisor of 128 is provided to achieve smaller bandwidths that can fit in a SSB channel. Finally special divisors from a 384 kHz base are provided to allow even more narrow bandwidths. + +Thus available bandwidths are: + + - **500000** (500000 / 1) Hz + - **400000** (400000 / 1) Hz not in LoRa standard + - **333333** (333333 / 1) Hz + - **250000** (500000 / 2) Hz + - **200000** (400000 / 2) Hz not in LoRa standard + - **166667** (333333 / 2) Hz + - **125000** (500000 / 4) Hz + - **100000** (400000 / 4) Hz not in LoRa standard + - **83333** (333333 / 4) Hz + - **62500** (500000 / 8) Hz + - **50000** (400000 / 8) Hz not in LoRa standard + - **41667** (333333 / 8) Hz + - **31250** (500000 / 16) Hz + - **25000** (400000 / 16) Hz not in LoRa standard + - **20833** (333333 / 16) Hz + - **15625** (500000 / 32) Hz + - **12500** (400000 / 32) Hz not in LoRa standard + - **10417** (333333 / 32) Hz + - **7813** (500000 / 64) Hz + - **6250** (400000 / 64) Hz not in LoRa standard + - **5208** (333333 / 64) Hz + - **3906** (500000 / 128) Hz not in LoRa standard + - **3125** (400000 / 128) Hz not in LoRa standard + - **2604** (333333 / 128) Hz not in LoRa standard + - **1500** (384000 / 256) Hz not in LoRa standard + - **750** (384000 / 512) Hz not in LoRa standard + - **488** (500000 / 1024) Hz not in LoRa standard + - **375** (384000 / 1024) Hz not in LoRa standard + +The ChirpChat signal is oversampled by two therefore it needs a baseband of at least twice the bandwidth. This drives the maximum value on the slider automatically. + +

4: De-chirped noise maximum power

+ +This is the maximum power received in one FFT bin (the argmax bin) in dB when no signal is detected. It is averaged over 10 values. + +

5. De-chirped signal maximum power

+ +This is the maximum power received in one FFT bin (the argmax bin) in dB when a signal is detected. It is averaged over 10 values. + +

6: De-chirped signal over noise ratio

+ +The noise level reference is the noise maximum power just before the detected signal starts and the signal level the signal maximum power just before the detected signal stops. To get a significant reading you have to adjust correctly the number of preamble chirps (9) and the End Of Message squelch level (A.3) and/or the message length (A.4) so that signal boundaries are determined correctly. + +Decode errors are very likely to happen when this value falls below 4 dB. + +

7: FFT Window

+ +A choice of FFT Windows to apply to the FFT performed on the de-chirped signal is provided. These are the same windows as those used in the spectrum display. The effect of windowing is to reduce the spill over in adjacent bins at the expense of a flatter top and fatter main lobe. When the purpose is frequency detection this is not what is desired necessarily and thus the "Rectangular" window (i.e. no window) should be chosen. However a variety of windows is provided to experiment with. Experimentally the best alternative to "Rectangular" is "Kaiser" then "Bartlett" and "Hanning". The complete list is: + + - **Bart**: Bartlett + - **B-H**: 4 term Blackman-Harris + - **FT**: Flat Top + - **Ham**: Hamming + - **Han**: Hanning + - **Rec**: Rectangular (no window) + - **Kai**: Kaiser with α = π + - **Black**: Blackman (3 term) + - **B-H7**: 7 term Blackman-Harris + +

8: Spread Factor

+ +This is the Spread Factor parameter of the ChirpChat signal. This is the log2 of the FFT size used over the bandwidth (3). The number of symbols is 2SF-DE where SF is the spread factor and DE the Distance Enhancement factor (8) + +

9: Distance Enhancement factor

+ +The LoRa standard specifies 0 (no DE) or 2 (DE active). The ChirpChat DE range is extended to all values between 0 and 4 bits. + +The LoRa standard also specifies that the LowDataRateOptimizatio flag (thus DE=2 vs DE=0 here) should be set when the symbol time defined as BW / 2^SF exceeds 16 ms (See section 4.1.1.6 of the SX127x datasheet). In practice this happens for SF=11 and SF=12 and large enough bandwidths (you can do the maths). + +Here this value is the log2 of the number of FFT bins used for one symbol. Extending the number of FFT bins per symbol decreases the probability to detect the wrong symbol as an adjacent bin. It can also overcome frequency or sampling time drift on long messages particularly for small bandwidths. + +In practice it is difficult to make correct decodes if only one FFT bin is used to code one symbol (DE=0) therefore it is recommended to use a DE factor of 2 or more. With medium SNR DE=1 can still achieve good results. + +

10: Number of expected preamble chirps

+ +This is the number of chirps expected in the preamble and has to be agreed between the transmitter and receiver. + +

15: Invert chirp ramps

+ +The LoRa standard is up-chirps for the preamble, down-chirps for the SFD and up-chirps for the payload. + +When you check this option it inverts the direction of the chirps thus becoming down-chirps for the preamble, up-chirps for the SFD and down-chirps for the payload. + +

A: Payload controls and indicators

+ +![Meshtastic Demodulator payload controls](../../../doc/img/MeshtasticDemod_payload.png) + +

A.1: Coding scheme

+ +In addition to the LoRa standard plain ASCII and TTY have been added for pure text messages. ASCII and TTY have no form of FEC. + + - **LoRa**: LoRa standard (see LoRa documentation) + - **ASCII**: This is plain 7 bit ASCII coded characters. It needs exactly 7 effective bits per symbols (SF - DE = 7) + - **TTY**: Baudot (Teletype) 5 bit encoded characters. It needs exactly 5 effective bits per symbols (SF - DE = 5) + - **FT**: FT8/4 protocol. The 174 payload bits are packed into chirp symbols with zero padding if necessary + +

A.2: Start/Stop decoder

+ +You can suspend and resume decoding activity using this button. This is useful if you want to freeze the payload content display. + +

A.3: End Of Message squelch

+ +This is used to determine the end of message automatically. It can be de-activated by turning the button completely to the right (as shown on the picture). In this case it relies on the message length set with (A.4). + +During payload detection the maximum power value in the FFT (at argmax) Pmax is stored and compared to the current argmax power value Pi if SEOM is this squelch value the end of message is detected if SEOM × Si < Smax + +

A.4: Expected message length in symbols

+ +This is the expected number of symbols in a message. When a header is present in the payload it should match the size given in the header (A.11). + +

A.5: Auto message length

+ +LoRa and DT modes only. Set message length (A.4) equal to the number of symbols specified (or implied for FT) in the message just received. When messages are sent repeatedly this helps adjusting in possible message length changes automatically. + +

A.6: Sync word

+ +This is the message 1 byte sync word displayed in hexadecimal. + +

A.7: Expect header in message

+ +LoRa mode only. Use this checkbox to tell if you expect or not a header in the message. + +

A.8: Number of FEC parity bits

+ +LoRa mode only. This is the number of parity bits in the Hamming code used in the FEC. The standard values are 1 to 4 for H(4,5) to H(4,8) encoding. 0 is a non-standard value to specify no FEC. + +When a header is expected this control is disabled because the value used is the one found in the header. + +In FT8 mode there is no FEC as FEC is handled within the FT payload (with LDPC) + +

A.9: Payload CRC presence

+ +LoRa mode: Use this checkbox to tell if you expect a 2 byte CRC at the end of the payload. FT mode: there is always a CRC. + +LoRa: When a header is expected this control is disabled because the value used is the one found in the header. + +

A.10: Packet length

+ +This is the expected packet length in bytes without header and CRC. For FT this is the number of symbols. + +LoRa: When a header is expected this control is disabled because the value used is the one found in the header. + +

A.11: Number of symbols and codewords

+ +This is the number of symbols (left of slash) and codewords (right of slash) used for the payload including header and CRC. + +

A.12: Header FEC indicator (LoRa)

+ +Header uses a H(4,8) FEC. The color of the indicator gives the status of header parity checks: + + - **Grey**: undefined + - **Red**: unrecoverable error + - **Blue**: recovered error + - **Green**: no errors + +

A.13: Header CRC indicator (LoRa)

+ +The header has a one byte CRC. The color of this indicator gives the CRC status: + + - **Green**: CRC OK + - **Red**: CRC error + +

A.14: Payload FEC indicator (LoRa and FT)

+ +The color of the indicator gives the status of payload parity checks: + + - **Grey**: undefined + - **Red**: unrecoverable error. H(4,7) cannot distinguish between recoverable and unrecoverable error. Therefore this is never displayed for H(4,7). For FT it means that LDPC decoding failed. + - **Blue**: recovered error (LoRa only) + - **Green**: no errors + +

A.15: Payload CRC indicator (LoRa and FT)

+ +The payload can have a two byte CRC. The color of this indicator gives the CRC status: + + - **Grey**: No CRC (LoRa) + - **Green**: CRC OK + - **Red**: CRC error + +

11: Clear message window

+ +Use this push button to clear the message window (12) + +

12: Message window

+ +This is where the message and status data are displayed. The display varies if the coding scheme is purely text based (TTY, ASCII, FT) or text/binary mixed based (LoRa). The text vs binary consideration concerns the content of the message not the way it is transmitted on air that is by itself binary. + +

12.a: Text messages

+ +The format of a message line is the following: + +![Meshtastic Demodulator message string window](../../../doc/img/MeshtasticDemod_message_string.png) + + - 1: Timestamp in HH:MM:SS format + - 2: Signal level. Same as (5) for the current message + - 3: Signal Over Noise. Same as (6) for the current message + - 4: Start of text indicator + - 5: Text + +

12.b: Binary messages

+ +![Meshtastic Demodulator message bytes window](../../../doc/img/MeshtasticDemod_message_binary.png) + + - 1: Timestamp in HH:NN:SS format + - 2: Sync word. This is the sync word (byte) displayed in hex. Corresponds to (A.5) in the current message. + - 3: De-chirped signal level. This is the de-chirped signal level in dB. Corresponds to (5) in the current message. + - 4: De-chirped signal to noise ratio. This is the de-chirped signal to noise ratio in dB. Corresponds to (6) in the current message. + - 5: Header FEC status. Corresponds to (A.12) indicator in the current message: + - **n/a**: unknown or not applicable + - **err**: unrecoverable error + - **fix**: corrected error + - **ok**: OK + - 6: Header CRC status. Corresponds to (A.13) indicator in the current message + - **ok**: CRC OK + - **err**: CRC error + - **n/a**: not applicable + - 7: Payload FEC status. Corresponds to (A.14) indicator in the current message. If the end of message is reached before expectation then `ERR: too early` is displayed instead and no payload CRC status (next) is displayed. + - **n/a**: unknown or not applicable + - **err**: unrecoverable error + - **fix**: corrected error + - **ok**: OK + - 8: Payload CRC status. Corresponds to (A.15) indicator in the current message: + - **ok**: CRC OK + - **err**: CRC error + - **n/a**: not applicable + - 9: Displacement at start of line. 16 bytes in 4 groups of 4 bytes are displayed per line starting with the displacement in decimal. + - 10: Bytes group. This is a group of 4 bytes displayed as hexadecimal values. The payload is displayed with its possible CRC and without the header. + - 11: Message as text with "TXT" as prefix indicating it is the translation of the message to character representation + +

13: Send message via UDP

+ +Select to send the decoded message via UDP. + +

14: UDP address and port

+ +This is the UDP address and port to where the decoded message is sent when (12) is selected. + +

B: De-chirped spectrum

+ +This is the spectrum of the de-chirped signal when a ChirpChat signal can be decoded. Details on the spectrum view and controls can be found [here](../../../sdrgui/gui/spectrum.md) + +The frequency span corresponds to the bandwidth of the ChirpChat signal (3). Default FFT size is 2SF where SF is the spread factor (7). + +Sequences of successful ChirpChat signal demodulation are separated by blank lines (generated with a string of high value bins). + +Controls are the usual controls of spectrum displays with the following restrictions: + + - The window type is non operating because the FFT window is chosen by (7) + - The FFT size can be changed however it is set to 2SF where SF is the spread factor and thus displays correctly + + +

Common LoRa settings

+ +CR is the code rate and translates to FEC according to the following table + +| CR | FEC | +| --- | --- | +| 4/5 | 1 | +| 4/6 | 2 | +| 4/7 | 3 | +| 4/8 | 4 | + +When DE is on set DE to 2 else 0 + +

Generic

+ +| Use Case | SF | CR | BW (kHz) | DE Enabled? | Notes | +| --------------------- | --------- | ------- | -------- | ----------- | ----------------------------------------------------------- | +| High rate/short range | SF7 | 4/5 | 500 | Off | Fastest, urban/close devices | +| Balanced/general IoT | SF7–SF9 | 4/5–4/6 | 125–250 | Off | TTN default, good for gateways | +| Long range/rural | SF10–SF12 | 4/6–4/8 | 125 | On | Max sensitivity (-137 dBm), slow airtime | +| Extreme range | SF12 | 4/8 | 125 | On | Best for weak signals, long packets | + +

Meshtastic

+ +

Quick facts

+ + - Uses 0x2B sync byte + - In Europe the default channel is centered on 869.525 MHz + +

Presets table

+ +| Preset | SF | CR | BW (kHz) | DE? | Use Case | +| ------------------- | -- | ---- | -------- | --- | ---------------------------------------------------- | +| LONG_FAST (default) | 11 | 4/8 | 250 | On | Long range, moderate speed | +| MEDIUM_SLOW | 10 | 4/7 | 250 | On | Balanced range/reliability​ | +| SHORT_FAST | 9 | 4/7 | 250 | Off | Urban/short hops, faster​ | +| SHORT_TURBO | 7 | 4/5 | 500 | Off | Max speed, shortest range (region-limited)​ | +| LONG_SLOW | 12 | 4/8 | 125 | On | Extreme range, slowest meshtastic​ | diff --git a/plugins/channeltx/CMakeLists.txt b/plugins/channeltx/CMakeLists.txt index f4d28a31b..064c5c9dc 100644 --- a/plugins/channeltx/CMakeLists.txt +++ b/plugins/channeltx/CMakeLists.txt @@ -20,6 +20,10 @@ if (ENABLE_CHANNELTX_MODCHIRPCHAT) add_subdirectory(modchirpchat) endif() +if (ENABLE_CHANNELTX_MODMESHTASTIC) + add_subdirectory(modmeshtastic) +endif() + if (ENABLE_CHANNELTX_MODNFM) add_subdirectory(modnfm) endif() diff --git a/plugins/channeltx/modmeshtastic/CMakeLists.txt b/plugins/channeltx/modmeshtastic/CMakeLists.txt new file mode 100644 index 000000000..9d19b24a7 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/CMakeLists.txt @@ -0,0 +1,90 @@ +project(modmeshtastic) + +if (FT8_SUPPORT) + set(meshtasticmod_FT8_LIB ft8) + set(meshtasticmod_FT8_INCLUDE ${CMAKE_SOURCE_DIR}/ft8) +endif() + +set(modmeshtastic_SOURCES + meshtasticmod.cpp + meshtasticmodsettings.cpp + meshtasticmodsource.cpp + meshtasticmodbaseband.cpp + meshtasticmodplugin.cpp + meshtasticmodencoder.cpp + meshtasticmodencodertty.cpp + meshtasticmodencoderascii.cpp + meshtasticmodencoderlora.cpp + meshtasticmodencoderft.cpp + meshtasticmodwebapiadapter.cpp + ${CMAKE_SOURCE_DIR}/plugins/meshtasticcommon/meshtasticpacket.cpp +) + +set(modmeshtastic_HEADERS + meshtasticmod.h + meshtasticmodsettings.h + meshtasticmodsource.h + meshtasticmodbaseband.h + meshtasticmodplugin.h + meshtasticmodencoder.h + meshtasticmodencodertty.h + meshtasticmodencoderascii.h + meshtasticmodencoderlora.h + meshtasticmodencoderft.h + meshtasticmodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${meshtasticmod_FT8_INCLUDE} + ${CMAKE_SOURCE_DIR}/plugins/meshtasticcommon +) + +if(NOT SERVER_MODE) + set(modmeshtastic_SOURCES + ${modmeshtastic_SOURCES} + meshtasticmodgui.cpp + meshtasticmodgui.ui + ) + set(modmeshtastic_HEADERS + ${modmeshtastic_HEADERS} + meshtasticmodgui.h + ) + + set(TARGET_NAME ${PLUGINS_PREFIX}modmeshtastic) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME ${PLUGINSSRV_PREFIX}modmeshtasticsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +if(NOT Qt6_FOUND) + add_library(${TARGET_NAME} ${modmeshtastic_SOURCES}) +else() + qt_add_plugin(${TARGET_NAME} CLASS_NAME MeshtasticModPlugin ${modmeshtastic_SOURCES}) +endif() + +if(NOT BUILD_SHARED_LIBS) + set_property(GLOBAL APPEND PROPERTY STATIC_PLUGINS_PROPERTY ${TARGET_NAME}) +endif() + +target_link_libraries(${TARGET_NAME} PRIVATE + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger + ${meshtasticmod_FT8_LIB} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $/${TARGET_NAME}stripped.pdb CONFIGURATIONS Release DESTINATION ${INSTALL_FOLDER} RENAME ${TARGET_NAME}.pdb ) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channeltx/modmeshtastic/meshtasticmod.cpp b/plugins/channeltx/modmeshtastic/meshtasticmod.cpp new file mode 100644 index 000000000..0f85c0731 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmod.cpp @@ -0,0 +1,1089 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020-2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Kacper Michajłow // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGChannelReport.h" +#include "SWGChirpChatModReport.h" + +#include +#include +#include + +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "settings/serializable.h" +#include "util/db.h" +#include "maincore.h" +#include "channel/channelwebapiutils.h" + +#include "meshtasticmodbaseband.h" +#include "meshtasticmod.h" + +MESSAGE_CLASS_DEFINITION(MeshtasticMod::MsgConfigureMeshtasticMod, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticMod::MsgReportPayloadTime, Message) + +const char* const MeshtasticMod::m_channelIdURI = "sdrangel.channeltx.modmeshtastic"; +const char* const MeshtasticMod::m_channelId = "MeshtasticMod"; + +MeshtasticMod::MeshtasticMod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), + m_deviceAPI(deviceAPI), + m_currentPayloadTime(0.0), + m_sampleRate(48000), + m_udpSocket(nullptr) +{ + setObjectName(m_channelId); + + m_thread = new QThread(this); + m_basebandSource = new MeshtasticModBaseband(); + m_basebandSource->moveToThread(m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSource(this); + m_deviceAPI->addChannelSourceAPI(this); + + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &MeshtasticMod::networkManagerFinished + ); +} + +MeshtasticMod::~MeshtasticMod() +{ + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &MeshtasticMod::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this, true); + stop(); + delete m_basebandSource; + delete m_thread; +} + +void MeshtasticMod::setDeviceAPI(DeviceAPI *deviceAPI) +{ + if (deviceAPI != m_deviceAPI) + { + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this, false); + m_deviceAPI = deviceAPI; + m_deviceAPI->addChannelSource(this); + m_deviceAPI->addChannelSinkAPI(this); + } +} + +void MeshtasticMod::start() +{ + qDebug("MeshtasticMod::start"); + m_basebandSource->reset(); + m_thread->start(); +} + +void MeshtasticMod::stop() +{ + qDebug("MeshtasticMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void MeshtasticMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); +} + +bool MeshtasticMod::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshtasticMod::match(cmd)) + { + MsgConfigureMeshtasticMod& cfg = (MsgConfigureMeshtasticMod&) cmd; + qDebug() << "MeshtasticMod::handleMessage: MsgConfigureMeshtasticMod"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "MeshtasticMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + + // Forward to the GUI + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(new DSPSignalNotification(notif)); + } + + return true; + } + else + { + return false; + } +} + +void MeshtasticMod::setCenterFrequency(qint64 frequency) +{ + MeshtasticModSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMeshtasticMod *msgToGUI = MsgConfigureMeshtasticMod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void MeshtasticMod::applySettings(const MeshtasticModSettings& settings, bool force) +{ + qDebug() << "MeshtasticMod::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_bandwidthIndex + << " bandwidth: " << MeshtasticModSettings::bandwidths[settings.m_bandwidthIndex] + << " m_channelMute: " << settings.m_channelMute + << " m_beaconMessage: " << settings.m_beaconMessage + << " m_cqMessage: " << settings.m_cqMessage + << " m_replyMessage: " << settings.m_replyMessage + << " m_reportMessage:" << settings.m_reportMessage + << " m_replyReportMessage: " << settings.m_replyReportMessage + << " m_rrrMessage: " << settings.m_rrrMessage + << " m_73message: " << settings.m_73Message + << " m_qsoTextMessage: " << settings.m_qsoTextMessage + << " m_textMessage: " << settings.m_textMessage + << " m_bytesMessage: " << settings.m_bytesMessage.toHex() + << " m_spreadFactor: " << settings.m_spreadFactor + << " m_deBits: " << settings.m_deBits + << " m_codingScheme: " << settings.m_codingScheme + << " m_nbParityBits: " << settings.m_nbParityBits + << " m_hasCRC: " << settings.m_hasCRC + << " m_hasHeader: " << settings.m_hasHeader + << " m_messageType: " << settings.m_messageType + << " m_preambleChirps: " << settings.m_preambleChirps + << " m_quietMillis: " << settings.m_quietMillis + << " m_messageRepeat: " << settings.m_messageRepeat + << " m_udpEnabled: " << settings.m_udpEnabled + << " m_udpAddress: " << settings.m_udpAddress + << " m_udpPort: " << settings.m_udpPort + << " m_invertRamps: " << settings.m_invertRamps + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIAddress: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " 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"); + } + if ((settings.m_channelMute != m_settings.m_channelMute) || force) { + reverseAPIKeys.append("channelMute"); + } + 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_spreadFactor != m_settings.m_spreadFactor) + || (settings.m_deBits != m_settings.m_deBits) || force) { + m_encoder.setNbSymbolBits(settings.m_spreadFactor, settings.m_deBits); + } + + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) + || (settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) || force) + { + if (getMessageQueueToGUI()) + { + m_currentPayloadTime = (m_symbols.size()*(1<push(rpt); + } + } + + if ((settings.m_codingScheme != m_settings.m_codingScheme) || force) + { + reverseAPIKeys.append("codingScheme"); + m_encoder.setCodingScheme(settings.m_codingScheme); + } + + if ((settings.m_nbParityBits != m_settings.m_nbParityBits || force)) + { + reverseAPIKeys.append("nbParityBits"); + m_encoder.setLoRaParityBits(settings.m_nbParityBits); + } + + if ((settings.m_hasCRC != m_settings.m_hasCRC) || force) + { + reverseAPIKeys.append("hasCRC"); + m_encoder.setLoRaHasCRC(settings.m_hasCRC); + } + + if ((settings.m_hasHeader != m_settings.m_hasHeader) || force) + { + reverseAPIKeys.append("hasHeader"); + m_encoder.setLoRaHasHeader(settings.m_hasHeader); + } + + if ((settings.m_messageType != m_settings.m_messageType) || force) { + reverseAPIKeys.append("messageType"); + } + if ((settings.m_beaconMessage != m_settings.m_beaconMessage) || force) { + reverseAPIKeys.append("beaconMessage"); + } + if ((settings.m_cqMessage != m_settings.m_cqMessage) || force) { + reverseAPIKeys.append("cqMessage"); + } + if ((settings.m_replyMessage != m_settings.m_replyMessage) || force) { + reverseAPIKeys.append("replyMessage"); + } + if ((settings.m_reportMessage != m_settings.m_reportMessage) || force) { + reverseAPIKeys.append("reportMessage"); + } + if ((settings.m_replyReportMessage != m_settings.m_replyReportMessage) || force) { + reverseAPIKeys.append("replyReportMessage"); + } + if ((settings.m_rrrMessage != m_settings.m_rrrMessage) || force) { + reverseAPIKeys.append("rrrMessage"); + } + if ((settings.m_73Message != m_settings.m_73Message) || force) { + reverseAPIKeys.append("73Message"); + } + if ((settings.m_qsoTextMessage != m_settings.m_qsoTextMessage) || force) { + reverseAPIKeys.append("qsoTextMessage"); + } + if ((settings.m_textMessage != m_settings.m_textMessage) || force) { + reverseAPIKeys.append("textMessage"); + } + if ((settings.m_bytesMessage != m_settings.m_bytesMessage) || force) { + reverseAPIKeys.append("bytesMessage"); + } + if ((settings.m_preambleChirps != m_settings.m_preambleChirps) || force) { + reverseAPIKeys.append("preambleChirps"); + } + if ((settings.m_quietMillis != m_settings.m_quietMillis) || force) { + reverseAPIKeys.append("quietMillis"); + } + if ((settings.m_invertRamps != m_settings.m_invertRamps) || force) { + reverseAPIKeys.append("invertRamps"); + } + + MeshtasticModBaseband::MsgConfigureMeshtasticModPayload *payloadMsg = nullptr; + + if ((settings.m_messageType == MeshtasticModSettings::MessageNone) + && ((settings.m_messageType != m_settings.m_messageType) || force)) + { + payloadMsg = MeshtasticModBaseband::MsgConfigureMeshtasticModPayload::create(); + } + else if ((settings.m_messageType != m_settings.m_messageType) + || (settings.m_beaconMessage != m_settings.m_beaconMessage) + || (settings.m_cqMessage != m_settings.m_cqMessage) + || (settings.m_replyMessage != m_settings.m_replyMessage) + || (settings.m_reportMessage != m_settings.m_reportMessage) + || (settings.m_replyReportMessage != m_settings.m_replyReportMessage) + || (settings.m_rrrMessage != m_settings.m_rrrMessage) + || (settings.m_73Message != m_settings.m_73Message) + || (settings.m_qsoTextMessage != m_settings.m_qsoTextMessage) + || (settings.m_textMessage != m_settings.m_textMessage) + || (settings.m_bytesMessage != m_settings.m_bytesMessage) || force) + { + m_symbols.clear(); + m_encoder.encode(settings, m_symbols); + payloadMsg = MeshtasticModBaseband::MsgConfigureMeshtasticModPayload::create(m_symbols); + } + + if (payloadMsg) + { + m_basebandSource->getInputMessageQueue()->push(payloadMsg); + m_currentPayloadTime = (m_symbols.size()*(1<push(rpt); + } + } + + if ((settings.m_messageRepeat != m_settings.m_messageRepeat) || force) { + reverseAPIKeys.append("messageRepeat"); + } + if ((settings.m_udpEnabled != m_settings.m_udpEnabled) || force) { + reverseAPIKeys.append("udpEnabled"); + } + if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) { + reverseAPIKeys.append("udpAddress"); + } + if ((settings.m_udpPort != m_settings.m_udpPort) || force) { + reverseAPIKeys.append("udpPort"); + } + + if ( (settings.m_udpEnabled != m_settings.m_udpEnabled) + || (settings.m_udpAddress != m_settings.m_udpAddress) + || (settings.m_udpPort != m_settings.m_udpPort) + || force) + { + if (settings.m_udpEnabled) + openUDP(settings); + else + closeUDP(); + } + + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this, false, m_settings.m_streamIndex); + m_deviceAPI->addChannelSource(this, settings.m_streamIndex); + m_deviceAPI->addChannelSourceAPI(this); + m_settings.m_streamIndex = settings.m_streamIndex; // make sure ChannelAPI::getStreamIndex() is consistent + emit streamIndexChanged(settings.m_streamIndex); + } + + reverseAPIKeys.append("streamIndex"); + } + + MeshtasticModBaseband::MsgConfigureMeshtasticModBaseband *msg = + MeshtasticModBaseband::MsgConfigureMeshtasticModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + + 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; +} + +QByteArray MeshtasticMod::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshtasticMod::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureMeshtasticMod *msg = MsgConfigureMeshtasticMod::create(m_settings, true); + m_inputMessageQueue.push(msg); + + return success; +} + +int MeshtasticMod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + response.getChirpChatModSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int MeshtasticMod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int MeshtasticMod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + MeshtasticModSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureMeshtasticMod *msg = MsgConfigureMeshtasticMod::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMeshtasticMod *msgToGUI = MsgConfigureMeshtasticMod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void MeshtasticMod::webapiUpdateChannelSettings( + MeshtasticModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getChirpChatModSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("bandwidthIndex")) { + settings.m_bandwidthIndex = response.getChirpChatModSettings()->getBandwidthIndex(); + } + if (channelSettingsKeys.contains("spreadFactor")) { + settings.m_spreadFactor = response.getChirpChatModSettings()->getSpreadFactor(); + } + if (channelSettingsKeys.contains("deBits")) { + settings.m_deBits = response.getChirpChatModSettings()->getDeBits(); + } + if (channelSettingsKeys.contains("preambleChirps")) { + settings.m_preambleChirps = response.getChirpChatModSettings()->getPreambleChirps(); + } + if (channelSettingsKeys.contains("quietMillis")) { + settings.m_quietMillis = response.getChirpChatModSettings()->getQuietMillis(); + } + if (channelSettingsKeys.contains("syncWord")) { + settings.m_syncWord = response.getChirpChatModSettings()->getSyncWord(); + } + if (channelSettingsKeys.contains("syncWord")) { + settings.m_syncWord = response.getChirpChatModSettings()->getSyncWord(); + } + if (channelSettingsKeys.contains("channelMute")) { + settings.m_channelMute = response.getChirpChatModSettings()->getChannelMute() != 0; + } + if (channelSettingsKeys.contains("codingScheme")) { + settings.m_codingScheme = (MeshtasticModSettings::CodingScheme) response.getChirpChatModSettings()->getCodingScheme(); + } + if (channelSettingsKeys.contains("nbParityBits")) { + settings.m_nbParityBits = response.getChirpChatModSettings()->getNbParityBits(); + } + if (channelSettingsKeys.contains("hasCRC")) { + settings.m_hasCRC = response.getChirpChatModSettings()->getHasCrc() != 0; + } + if (channelSettingsKeys.contains("hasHeader")) { + settings.m_hasHeader = response.getChirpChatModSettings()->getHasHeader() != 0; + } + if (channelSettingsKeys.contains("myCall")) { + settings.m_myCall = *response.getChirpChatModSettings()->getMyCall(); + } + if (channelSettingsKeys.contains("urCall")) { + settings.m_urCall = *response.getChirpChatModSettings()->getUrCall(); + } + if (channelSettingsKeys.contains("myLoc")) { + settings.m_myLoc = *response.getChirpChatModSettings()->getMyLoc(); + } + if (channelSettingsKeys.contains("myRpt")) { + settings.m_myRpt = *response.getChirpChatModSettings()->getMyRpt(); + } + if (channelSettingsKeys.contains("messageType")) { + settings.m_messageType = (MeshtasticModSettings::MessageType) response.getChirpChatModSettings()->getMessageType(); + } + if (channelSettingsKeys.contains("beaconMessage")) { + settings.m_beaconMessage = *response.getChirpChatModSettings()->getBeaconMessage(); + } + if (channelSettingsKeys.contains("cqMessage")) { + settings.m_cqMessage = *response.getChirpChatModSettings()->getCqMessage(); + } + if (channelSettingsKeys.contains("replyMessage")) { + settings.m_replyMessage = *response.getChirpChatModSettings()->getReplyMessage(); + } + if (channelSettingsKeys.contains("reportMessage")) { + settings.m_reportMessage = *response.getChirpChatModSettings()->getReportMessage(); + } + if (channelSettingsKeys.contains("replyReportMessage")) { + settings.m_replyReportMessage = *response.getChirpChatModSettings()->getReplyReportMessage(); + } + if (channelSettingsKeys.contains("rrrMessage")) { + settings.m_rrrMessage = *response.getChirpChatModSettings()->getRrrMessage(); + } + if (channelSettingsKeys.contains("message73")) { + settings.m_73Message = *response.getChirpChatModSettings()->getMessage73(); + } + if (channelSettingsKeys.contains("qsoTextMessage")) { + settings.m_qsoTextMessage = *response.getChirpChatModSettings()->getQsoTextMessage(); + } + if (channelSettingsKeys.contains("textMessage")) { + settings.m_textMessage = *response.getChirpChatModSettings()->getTextMessage(); + } + if (channelSettingsKeys.contains("bytesMessage")) + { + const QList *bytesStr = response.getChirpChatModSettings()->getBytesMessage(); + settings.m_bytesMessage.clear(); + + for (QList::const_iterator it = bytesStr->begin(); it != bytesStr->end(); ++it) + { + bool bStatus = false; + unsigned int byteInt = (**it).toUInt(&bStatus, 16); + + if (bStatus) { + settings.m_bytesMessage.append((char) (byteInt % 256)); + } + } + } + if (channelSettingsKeys.contains("messageRepeat")) { + settings.m_messageRepeat = response.getChirpChatModSettings()->getMessageRepeat(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getChirpChatModSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getChirpChatModSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getChirpChatModSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("invertRamps")) { + settings.m_invertRamps = response.getChirpChatModSettings()->getInvertRamps(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getChirpChatModSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getChirpChatModSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getChirpChatModSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getChirpChatModSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getChirpChatModSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getChirpChatModSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getChirpChatModSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getChirpChatModSettings()->getReverseApiChannelIndex(); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getChirpChatModSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getChirpChatModSettings()->getChannelMarker()); + } +} + +int MeshtasticMod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModReport(new SWGSDRangel::SWGChirpChatModReport()); + response.getChirpChatModReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void MeshtasticMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const MeshtasticModSettings& settings) +{ + response.getChirpChatModSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getChirpChatModSettings()->setBandwidthIndex(settings.m_bandwidthIndex); + response.getChirpChatModSettings()->setSpreadFactor(settings.m_spreadFactor); + response.getChirpChatModSettings()->setDeBits(settings.m_deBits); + response.getChirpChatModSettings()->setPreambleChirps(settings.m_preambleChirps); + response.getChirpChatModSettings()->setQuietMillis(settings.m_quietMillis); + response.getChirpChatModSettings()->setSyncWord(settings.m_syncWord); + response.getChirpChatModSettings()->setChannelMute(settings.m_channelMute ? 1 : 0); + response.getChirpChatModSettings()->setCodingScheme((int) settings.m_codingScheme); + response.getChirpChatModSettings()->setNbParityBits(settings.m_nbParityBits); + response.getChirpChatModSettings()->setHasCrc(settings.m_hasCRC ? 1 : 0); + response.getChirpChatModSettings()->setHasHeader(settings.m_hasHeader ? 1 : 0); + + if (response.getChirpChatModSettings()->getMyCall()) { + *response.getChirpChatModSettings()->getMyCall() = settings.m_myCall; + } else { + response.getChirpChatModSettings()->setMyCall(new QString(settings.m_myCall)); + } + + if (response.getChirpChatModSettings()->getUrCall()) { + *response.getChirpChatModSettings()->getUrCall() = settings.m_urCall; + } else { + response.getChirpChatModSettings()->setUrCall(new QString(settings.m_urCall)); + } + + if (response.getChirpChatModSettings()->getMyLoc()) { + *response.getChirpChatModSettings()->getMyLoc() = settings.m_myLoc; + } else { + response.getChirpChatModSettings()->setMyLoc(new QString(settings.m_myLoc)); + } + + if (response.getChirpChatModSettings()->getMyRpt()) { + *response.getChirpChatModSettings()->getMyRpt() = settings.m_myRpt; + } else { + response.getChirpChatModSettings()->setMyRpt(new QString(settings.m_myRpt)); + } + + response.getChirpChatModSettings()->setMessageType((int) settings.m_messageType); + + if (response.getChirpChatModSettings()->getBeaconMessage()) { + *response.getChirpChatModSettings()->getBeaconMessage() = settings.m_beaconMessage; + } else { + response.getChirpChatModSettings()->setBeaconMessage(new QString(settings.m_beaconMessage)); + } + + if (response.getChirpChatModSettings()->getCqMessage()) { + *response.getChirpChatModSettings()->getCqMessage() = settings.m_cqMessage; + } else { + response.getChirpChatModSettings()->setCqMessage(new QString(settings.m_cqMessage)); + } + + if (response.getChirpChatModSettings()->getReplyMessage()) { + *response.getChirpChatModSettings()->getReplyMessage() = settings.m_replyMessage; + } else { + response.getChirpChatModSettings()->setReplyMessage(new QString(settings.m_replyMessage)); + } + + if (response.getChirpChatModSettings()->getReportMessage()) { + *response.getChirpChatModSettings()->getReportMessage() = settings.m_reportMessage; + } else { + response.getChirpChatModSettings()->setReportMessage(new QString(settings.m_reportMessage)); + } + + if (response.getChirpChatModSettings()->getReplyReportMessage()) { + *response.getChirpChatModSettings()->getReplyReportMessage() = settings.m_replyReportMessage; + } else { + response.getChirpChatModSettings()->setReplyReportMessage(new QString(settings.m_replyReportMessage)); + } + + if (response.getChirpChatModSettings()->getRrrMessage()) { + *response.getChirpChatModSettings()->getRrrMessage() = settings.m_rrrMessage; + } else { + response.getChirpChatModSettings()->setRrrMessage(new QString(settings.m_rrrMessage)); + } + + if (response.getChirpChatModSettings()->getMessage73()) { + *response.getChirpChatModSettings()->getMessage73() = settings.m_73Message; + } else { + response.getChirpChatModSettings()->setMessage73(new QString(settings.m_73Message)); + } + + if (response.getChirpChatModSettings()->getQsoTextMessage()) { + *response.getChirpChatModSettings()->getQsoTextMessage() = settings.m_qsoTextMessage; + } else { + response.getChirpChatModSettings()->setQsoTextMessage(new QString(settings.m_qsoTextMessage)); + } + + if (response.getChirpChatModSettings()->getTextMessage()) { + *response.getChirpChatModSettings()->getTextMessage() = settings.m_textMessage; + } else { + response.getChirpChatModSettings()->setTextMessage(new QString(settings.m_textMessage)); + } + + response.getChirpChatModSettings()->setBytesMessage(new QList); + QList *bytesStr = response.getChirpChatModSettings()->getBytesMessage(); + + for (QByteArray::const_iterator it = settings.m_bytesMessage.begin(); it != settings.m_bytesMessage.end(); ++it) + { + unsigned char b = *it; + bytesStr->push_back(new QString(tr("%1").arg(b, 2, 16, QChar('0')))); + } + + response.getChirpChatModSettings()->setMessageRepeat(settings.m_messageRepeat); + response.getChirpChatModSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getChirpChatModSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getChirpChatModSettings()->setUdpPort(settings.m_udpPort); + response.getChirpChatModSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0); + + response.getChirpChatModSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getChirpChatModSettings()->getTitle()) { + *response.getChirpChatModSettings()->getTitle() = settings.m_title; + } else { + response.getChirpChatModSettings()->setTitle(new QString(settings.m_title)); + } + + response.getChirpChatModSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getChirpChatModSettings()->getReverseApiAddress()) { + *response.getChirpChatModSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getChirpChatModSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getChirpChatModSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getChirpChatModSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getChirpChatModSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_channelMarker) + { + if (response.getChirpChatModSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getChirpChatModSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getChirpChatModSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getChirpChatModSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getChirpChatModSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getChirpChatModSettings()->setRollupState(swgRollupState); + } + } +} + +void MeshtasticMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + response.getChirpChatModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); + response.getChirpChatModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); + float fourthsMs = ((1<setPayloadTimeMs(m_currentPayloadTime); + response.getChirpChatModReport()->setTotalTimeMs(m_currentPayloadTime + controlMs); + response.getChirpChatModReport()->setSymbolTimeMs(4.0 * fourthsMs); + response.getChirpChatModReport()->setPlaying(getModulatorActive() ? 1 : 0); +} + +void MeshtasticMod::webapiReverseSendSettings(QList& channelSettingsKeys, const MeshtasticModSettings& 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 MeshtasticMod::sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshtasticModSettings& 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 MeshtasticMod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshtasticModSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(1); // single source (Tx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString(m_channelId)); + swgChannelSettings->setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + SWGSDRangel::SWGChirpChatModSettings *swgMeshtasticModSettings = swgChannelSettings->getChirpChatModSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgMeshtasticModSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("bandwidthIndex") || force) { + swgMeshtasticModSettings->setBandwidthIndex(settings.m_bandwidthIndex); + } + if (channelSettingsKeys.contains("spreadFactor") || force) { + swgMeshtasticModSettings->setSpreadFactor(settings.m_spreadFactor); + } + if (channelSettingsKeys.contains("deBits") || force) { + swgMeshtasticModSettings->setDeBits(settings.m_deBits); + } + if (channelSettingsKeys.contains("preambleChirps") || force) { + swgMeshtasticModSettings->setPreambleChirps(settings.m_preambleChirps); + } + if (channelSettingsKeys.contains("quietMillis") || force) { + swgMeshtasticModSettings->setQuietMillis(settings.m_quietMillis); + } + if (channelSettingsKeys.contains("syncWord") || force) { + swgMeshtasticModSettings->setSyncWord(settings.m_syncWord); + } + if (channelSettingsKeys.contains("channelMute") || force) { + swgMeshtasticModSettings->setChannelMute(settings.m_channelMute ? 1 : 0); + } + if (channelSettingsKeys.contains("codingScheme") || force) { + swgMeshtasticModSettings->setCodingScheme((int) settings.m_codingScheme); + } + if (channelSettingsKeys.contains("nbParityBits") || force) { + swgMeshtasticModSettings->setNbParityBits(settings.m_nbParityBits); + } + if (channelSettingsKeys.contains("hasCRC") || force) { + swgMeshtasticModSettings->setHasCrc(settings.m_hasCRC ? 1 : 0); + } + if (channelSettingsKeys.contains("hasHeader") || force) { + swgMeshtasticModSettings->setHasHeader(settings.m_hasHeader ? 1 : 0); + } + if (channelSettingsKeys.contains("myCall") || force) { + swgMeshtasticModSettings->setMyCall(new QString(settings.m_myCall)); + } + if (channelSettingsKeys.contains("urCall") || force) { + swgMeshtasticModSettings->setUrCall(new QString(settings.m_urCall)); + } + if (channelSettingsKeys.contains("myLoc") || force) { + swgMeshtasticModSettings->setMyLoc(new QString(settings.m_myLoc)); + } + if (channelSettingsKeys.contains("myRpt") || force) { + swgMeshtasticModSettings->setMyRpt(new QString(settings.m_myRpt)); + } + if (channelSettingsKeys.contains("messageType") || force) { + swgMeshtasticModSettings->setMessageType((int) settings.m_messageType); + } + if (channelSettingsKeys.contains("beaconMessage") || force) { + swgMeshtasticModSettings->setBeaconMessage(new QString(settings.m_beaconMessage)); + } + if (channelSettingsKeys.contains("cqMessage") || force) { + swgMeshtasticModSettings->setCqMessage(new QString(settings.m_cqMessage)); + } + if (channelSettingsKeys.contains("replyMessage") || force) { + swgMeshtasticModSettings->setReplyMessage(new QString(settings.m_replyMessage)); + } + if (channelSettingsKeys.contains("reportMessage") || force) { + swgMeshtasticModSettings->setReportMessage(new QString(settings.m_reportMessage)); + } + if (channelSettingsKeys.contains("replyReportMessage") || force) { + swgMeshtasticModSettings->setReplyReportMessage(new QString(settings.m_replyReportMessage)); + } + if (channelSettingsKeys.contains("rrrMessage") || force) { + swgMeshtasticModSettings->setRrrMessage(new QString(settings.m_rrrMessage)); + } + if (channelSettingsKeys.contains("message73") || force) { + swgMeshtasticModSettings->setMessage73(new QString(settings.m_73Message)); + } + if (channelSettingsKeys.contains("qsoTextMessage") || force) { + swgMeshtasticModSettings->setQsoTextMessage(new QString(settings.m_qsoTextMessage)); + } + if (channelSettingsKeys.contains("textMessage") || force) { + swgMeshtasticModSettings->setTextMessage(new QString(settings.m_textMessage)); + } + + if (channelSettingsKeys.contains("bytesMessage") || force) + { + swgMeshtasticModSettings->setBytesMessage(new QList); + QList *bytesStr = swgMeshtasticModSettings-> getBytesMessage(); + + for (QByteArray::const_iterator it = settings.m_bytesMessage.begin(); it != settings.m_bytesMessage.end(); ++it) + { + unsigned char b = *it; + bytesStr->push_back(new QString(tr("%1").arg(b, 2, 16, QChar('0')))); + } + } + + if (channelSettingsKeys.contains("messageRepeat") || force) { + swgMeshtasticModSettings->setMessageRepeat(settings.m_messageRepeat); + } + + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgMeshtasticModSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgMeshtasticModSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgMeshtasticModSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("invertRamps") || force) { + swgMeshtasticModSettings->setInvertRamps(settings.m_invertRamps ? 1 : 0); + } + + if (channelSettingsKeys.contains("rgbColor") || force) { + swgMeshtasticModSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgMeshtasticModSettings->setTitle(new QString(settings.m_title)); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgMeshtasticModSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgMeshtasticModSettings->setRollupState(swgRollupState); + } +} + +void MeshtasticMod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "MeshtasticMod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("MeshtasticMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +double MeshtasticMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +void MeshtasticMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} + +uint32_t MeshtasticMod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSinkStreams(); +} + +bool MeshtasticMod::getModulatorActive() const +{ + return m_basebandSource->getActive(); +} + +void MeshtasticMod::openUDP(const MeshtasticModSettings& settings) +{ + closeUDP(); + m_udpSocket = new QUdpSocket(); + if (!m_udpSocket->bind(QHostAddress(settings.m_udpAddress), settings.m_udpPort)) + qCritical() << "MeshtasticMod::openUDP: Failed to bind to port " << settings.m_udpAddress << ":" << settings.m_udpPort << ". Error: " << m_udpSocket->error(); + else + qDebug() << "MeshtasticMod::openUDP: Listening for packets on " << settings.m_udpAddress << ":" << settings.m_udpPort; + connect(m_udpSocket, &QUdpSocket::readyRead, this, &MeshtasticMod::udpRx); +} + +void MeshtasticMod::closeUDP() +{ + if (m_udpSocket != nullptr) + { + disconnect(m_udpSocket, &QUdpSocket::readyRead, this, &MeshtasticMod::udpRx); + delete m_udpSocket; + m_udpSocket = nullptr; + } +} + +void MeshtasticMod::udpRx() +{ + while (m_udpSocket->hasPendingDatagrams()) + { + QNetworkDatagram datagram = m_udpSocket->receiveDatagram(); + MeshtasticModBaseband::MsgConfigureMeshtasticModPayload *payloadMsg = nullptr; + std::vector symbols; + + m_encoder.encodeBytes(datagram.data(), symbols); + payloadMsg = MeshtasticModBaseband::MsgConfigureMeshtasticModPayload::create(symbols); + + if (payloadMsg) + { + m_basebandSource->getInputMessageQueue()->push(payloadMsg); + m_currentPayloadTime = (symbols.size()*(1<push(rpt); + } + } + } +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmod.h b/plugins/channeltx/modmeshtastic/meshtasticmod.h new file mode 100644 index 000000000..7955c56ae --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmod.h @@ -0,0 +1,206 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2020-2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Kacper Michajłow // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMOD_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMOD_H_ + +#include +#include +#include + +#include +#include + +#include "dsp/basebandsamplesource.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "meshtasticmodsettings.h" +#include "meshtasticmodencoder.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class QUdpSocket; +class DeviceAPI; +class CWKeyer; +class MeshtasticModBaseband; +class ObjectPipe; + +class MeshtasticMod : public BasebandSampleSource, public ChannelAPI { +public: + class MsgConfigureMeshtasticMod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshtasticModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshtasticMod* create(const MeshtasticModSettings& settings, bool force) + { + return new MsgConfigureMeshtasticMod(settings, force); + } + + private: + MeshtasticModSettings m_settings; + bool m_force; + + MsgConfigureMeshtasticMod(const MeshtasticModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgReportPayloadTime : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getPayloadTimeMs() const { return m_timeMs; } + std::size_t getNbSymbols() const { return m_nbSymbols; } + static MsgReportPayloadTime* create(float timeMs, std::size_t nbSymbols) { + return new MsgReportPayloadTime(timeMs, nbSymbols); + } + + private: + float m_timeMs; //!< time in milliseconds + std::size_t m_nbSymbols; //!< number of symbols + + MsgReportPayloadTime(float timeMs, std::size_t nbSymbols) : + Message(), + m_timeMs(timeMs), + m_nbSymbols(nbSymbols) + {} + }; + + //================================================================= + + MeshtasticMod(DeviceAPI *deviceAPI); + virtual ~MeshtasticMod(); + virtual void destroy() { delete this; } + virtual void setDeviceAPI(DeviceAPI *deviceAPI); + virtual DeviceAPI *getDeviceAPI() { return m_deviceAPI; } + + virtual void start(); + virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); + virtual void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); } + virtual QString getSourceName() { return objectName(); } + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + virtual void setCenterFrequency(qint64 frequency); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + virtual int getStreamIndex() const { return m_settings.m_streamIndex; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return m_settings.m_inputFrequencyOffset; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const MeshtasticModSettings& settings); + + static void webapiUpdateChannelSettings( + MeshtasticModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); + uint32_t getNumberOfDeviceStreams() const; + bool getModulatorActive() const; + + static const char* const m_channelIdURI; + static const char* const m_channelId; + +private: + DeviceAPI* m_deviceAPI; + QThread *m_thread; + MeshtasticModBaseband* m_basebandSource; + MeshtasticModEncoder m_encoder; // TODO: check if it needs to be on its own thread + MeshtasticModSettings m_settings; + float m_currentPayloadTime; + std::vector m_symbols; + + SampleVector m_sampleBuffer; + QRecursiveMutex m_settingsMutex; + + int m_sampleRate; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + QUdpSocket *m_udpSocket; + + virtual bool handleMessage(const Message& cmd); + void applySettings(const MeshtasticModSettings& settings, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const MeshtasticModSettings& settings, bool force); + void sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshtasticModSettings& settings, + bool force + ); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshtasticModSettings& settings, + bool force + ); + void openUDP(const MeshtasticModSettings& settings); + void closeUDP(); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void udpRx(); +}; + + +#endif /* PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMOD_H_ */ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodbaseband.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodbaseband.cpp new file mode 100644 index 000000000..5b33fef1a --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodbaseband.cpp @@ -0,0 +1,201 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 "dsp/upchannelizer.h" +#include "dsp/dspcommands.h" + +#include "meshtasticmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(MeshtasticModBaseband::MsgConfigureMeshtasticModBaseband, Message) +MESSAGE_CLASS_DEFINITION(MeshtasticModBaseband::MsgConfigureMeshtasticModPayload, Message) + +MeshtasticModBaseband::MeshtasticModBaseband() +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpChannelizer(&m_source); + + qDebug("MeshtasticModBaseband::MeshtasticModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &MeshtasticModBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +MeshtasticModBaseband::~MeshtasticModBaseband() +{ + delete m_channelizer; +} + +void MeshtasticModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void MeshtasticModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void MeshtasticModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + qreal rmsLevel, peakLevel; + int numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void MeshtasticModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void MeshtasticModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool MeshtasticModBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshtasticModBaseband::match(cmd)) + { + qDebug() << "MeshtasticModBaseband::handleMessage: MsgConfigureMeshtasticModBaseband"; + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMeshtasticModBaseband& cfg = (MsgConfigureMeshtasticModBaseband&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureMeshtasticModPayload::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMeshtasticModPayload& cfg = (MsgConfigureMeshtasticModPayload&) cmd; + qDebug() << "MeshtasticModBaseband::handleMessage: MsgConfigureMeshtasticModPayload:" << cfg.getPayload().size(); + m_source.setSymbols(cfg.getPayload()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + qDebug() << "MeshtasticModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + MeshtasticModSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer->getChannelFrequencyOffset() + ); + + return true; + } + else + { + return false; + } +} + +void MeshtasticModBaseband::applySettings(const MeshtasticModSettings& settings, bool force) +{ + if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) + || (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + int thisBW = MeshtasticModSettings::bandwidths[settings.m_bandwidthIndex]; + m_channelizer->setChannelization( + thisBW * MeshtasticModSettings::oversampling, + settings.m_inputFrequencyOffset + ); + m_source.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + thisBW, + m_channelizer->getChannelFrequencyOffset() + ); + } + + m_source.applySettings(settings, force); + + m_settings = settings; +} + +int MeshtasticModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodbaseband.h b/plugins/channeltx/modmeshtastic/meshtasticmodbaseband.h new file mode 100644 index 000000000..c22213e3f --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodbaseband.h @@ -0,0 +1,120 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICMODBASEBAND_H +#define INCLUDE_MESHTASTICMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "meshtasticmodsource.h" + +class UpChannelizer; + +class MeshtasticModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureMeshtasticModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshtasticModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshtasticModBaseband* create(const MeshtasticModSettings& settings, bool force) + { + return new MsgConfigureMeshtasticModBaseband(settings, force); + } + + private: + MeshtasticModSettings m_settings; + bool m_force; + + MsgConfigureMeshtasticModBaseband(const MeshtasticModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureMeshtasticModPayload : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const std::vector& getPayload() const { return m_payload; } + + static MsgConfigureMeshtasticModPayload* create() { + return new MsgConfigureMeshtasticModPayload(); + } + static MsgConfigureMeshtasticModPayload* create(const std::vector& payload) { + return new MsgConfigureMeshtasticModPayload(payload); + } + + private: + std::vector m_payload; + + MsgConfigureMeshtasticModPayload() : // This is empty payload notification + Message() + {} + MsgConfigureMeshtasticModPayload(const std::vector& payload) : + Message() + { m_payload = payload; } + }; + + MeshtasticModBaseband(); + ~MeshtasticModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + bool getActive() const { return m_source.getActive(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpChannelizer *m_channelizer; + MeshtasticModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MeshtasticModSettings m_settings; + QRecursiveMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const MeshtasticModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_MESHTASTICMODBASEBAND_H diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoder.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodencoder.cpp new file mode 100644 index 000000000..8c7f0d58e --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoder.cpp @@ -0,0 +1,181 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (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 "meshtasticmodencoder.h" +#include "meshtasticmodencodertty.h" +#include "meshtasticmodencoderascii.h" +#include "meshtasticmodencoderlora.h" +#include "meshtasticmodencoderft.h" +#include "meshtasticpacket.h" + +MeshtasticModEncoder::MeshtasticModEncoder() : + m_codingScheme(MeshtasticModSettings::CodingTTY), + m_nbSymbolBits(5), + m_nbParityBits(1), + m_hasCRC(true), + m_hasHeader(true) +{} + +MeshtasticModEncoder::~MeshtasticModEncoder() +{} + +void MeshtasticModEncoder::setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits) +{ + m_spreadFactor = spreadFactor; + + if (deBits >= spreadFactor) { + m_deBits = m_spreadFactor - 1; + } else { + m_deBits = deBits; + } + + m_nbSymbolBits = m_spreadFactor - m_deBits; +} + +void MeshtasticModEncoder::encode(MeshtasticModSettings settings, std::vector& symbols) +{ + if (settings.m_codingScheme == MeshtasticModSettings::CodingFT) + { + MeshtasticModEncoderFT::encodeMsg( + settings.m_myCall, + settings.m_urCall, + settings.m_myLoc, + settings.m_myRpt, + settings.m_textMessage, + settings.m_messageType, + m_nbSymbolBits, + symbols + ); + } + else + { + if (settings.m_messageType == MeshtasticModSettings::MessageBytes) { + encodeBytes(settings.m_bytesMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageBeacon) { + encodeString(settings.m_beaconMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageCQ) { + encodeString(settings.m_cqMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageReply) { + encodeString(settings.m_replyMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageReport) { + encodeString(settings.m_reportMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageReplyReport) { + encodeString(settings.m_replyReportMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageRRR) { + encodeString(settings.m_rrrMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::Message73) { + encodeString(settings.m_73Message, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageQSOText) { + encodeString(settings.m_qsoTextMessage, symbols); + } else if (settings.m_messageType == MeshtasticModSettings::MessageText) { + encodeString(settings.m_textMessage, symbols); + } + } +} + +void MeshtasticModEncoder::encodeString(const QString& str, std::vector& symbols) +{ + switch (m_codingScheme) + { + case MeshtasticModSettings::CodingTTY: + if (m_nbSymbolBits == 5) { + MeshtasticModEncoderTTY::encodeString(str, symbols); + } + break; + case MeshtasticModSettings::CodingASCII: + if (m_nbSymbolBits == 7) { + MeshtasticModEncoderASCII::encodeString(str, symbols); + } + break; + case MeshtasticModSettings::CodingLoRa: + if (m_nbSymbolBits >= 5) + { + QByteArray bytes; + QString summary; + QString error; + + if (Meshtastic::Packet::isCommand(str)) + { + if (!Meshtastic::Packet::buildFrameFromCommand(str, bytes, summary, error)) + { + qWarning() << "MeshtasticModEncoder::encodeString: Meshtastic command error:" << error; + return; + } + + qInfo() << "MeshtasticModEncoder::encodeString:" << summary; + } + else + { + bytes = str.toUtf8(); + } + + encodeBytesLoRa(bytes, symbols); + } + break; + default: + break; + } +} + +void MeshtasticModEncoder::encodeBytes(const QByteArray& bytes, std::vector& symbols) +{ + switch (m_codingScheme) + { + case MeshtasticModSettings::CodingLoRa: + { + QByteArray payload(bytes); + + if (Meshtastic::Packet::isCommand(QString::fromUtf8(bytes))) + { + QString summary; + QString error; + + if (!Meshtastic::Packet::buildFrameFromCommand(QString::fromUtf8(bytes), payload, summary, error)) + { + qWarning() << "MeshtasticModEncoder::encodeBytes: Meshtastic command error:" << error; + return; + } + + qInfo() << "MeshtasticModEncoder::encodeBytes:" << summary; + } + + encodeBytesLoRa(payload, symbols); + break; + } + default: + break; + }; +} + +void MeshtasticModEncoder::encodeBytesLoRa(const QByteArray& bytes, std::vector& symbols) +{ + QByteArray payload(bytes); + + if ((payload.size() + (m_hasCRC ? 2 : 0)) > 255) + { + qWarning() << "MeshtasticModEncoder::encodeBytesLoRa: payload too large:" << payload.size(); + return; + } + + if (m_hasCRC) { + MeshtasticModEncoderLoRa::addChecksum(payload); + } + + MeshtasticModEncoderLoRa::encodeBytes(payload, symbols, m_nbSymbolBits, m_hasHeader, m_hasCRC, m_nbParityBits); +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoder.h b/plugins/channeltx/modmeshtastic/meshtasticmodencoder.h new file mode 100644 index 000000000..453a1908c --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoder.h @@ -0,0 +1,57 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2016-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODER_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODER_H_ + +#include +#include "meshtasticmodsettings.h" + +class MeshtasticModEncoder +{ +public: + MeshtasticModEncoder(); + ~MeshtasticModEncoder(); + + void setCodingScheme(MeshtasticModSettings::CodingScheme codingScheme) { m_codingScheme = codingScheme; } + void setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits); + void setLoRaParityBits(unsigned int parityBits) { m_nbParityBits = parityBits; } + void setLoRaHasHeader(bool hasHeader) { m_hasHeader = hasHeader; } + void setLoRaHasCRC(bool hasCRC) { m_hasCRC = hasCRC; } + void encodeBytes(const QByteArray& bytes, std::vector& symbols); + void encode(MeshtasticModSettings settings, std::vector& symbols); + +private: + void encodeString(const QString& str, std::vector& symbols); + // LoRa functions + void encodeBytesLoRa(const QByteArray& bytes, std::vector& symbols); + + // General attributes + MeshtasticModSettings::CodingScheme m_codingScheme; + unsigned int m_spreadFactor; + unsigned int m_deBits; + unsigned int m_nbSymbolBits; + // LoRa attributes + unsigned int m_nbParityBits; //!< 1 to 4 Hamming FEC bits for 4 payload bits + bool m_hasCRC; + bool m_hasHeader; +}; + +#endif // PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODER_H_ + diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoderascii.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodencoderascii.cpp new file mode 100644 index 000000000..8f8469d59 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoderascii.cpp @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 "meshtasticmodencoderascii.h" + +void MeshtasticModEncoderASCII::encodeString(const QString& str, std::vector& symbols) +{ + QByteArray asciiStr = str.toUtf8(); + QByteArray::const_iterator it = asciiStr.begin(); + + for (; it != asciiStr.end(); ++it) { + symbols.push_back(*it & 0x7F); + } +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoderascii.h b/plugins/channeltx/modmeshtastic/meshtasticmodencoderascii.h new file mode 100644 index 000000000..28fea3b1d --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoderascii.h @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERASCII_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERASCII_H_ + +#include +#include + +class MeshtasticModEncoderASCII +{ +public: + static void encodeString(const QString& str, std::vector& symbols); +}; + +#endif // PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERASCII_H_ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoderft.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodencoderft.cpp new file mode 100644 index 000000000..5d29dac8e --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoderft.cpp @@ -0,0 +1,246 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "meshtasticmodencoderft.h" + +#ifndef HAS_FT8 +void MeshtasticModEncoderFT::encodeMsg( + const QString& myCall, + const QString& urCall, + const QString& myLocator, + const QString& myReport, + const QString& textMessage, + MeshtasticModSettings::MessageType messageType, + unsigned int nbSymbolBits, + std::vector& symbols +) +{ + qDebug("MeshtasticModEncoderFT::encodeMsg: not implemented"); +} +#else + +#include "ft8.h" +#include "packing.h" + + +void MeshtasticModEncoderFT::encodeMsg( + const QString& myCall, + const QString& urCall, + const QString& myLocator, + const QString& myReport, + const QString& textMessage, + MeshtasticModSettings::MessageType messageType, + unsigned int nbSymbolBits, + std::vector& symbols +) +{ + int a174[174]; // FT payload is 174 bits + + if (messageType == MeshtasticModSettings::MessageNone) { + return; // do nothing + } else if (messageType == MeshtasticModSettings::MessageBeacon) { + encodeMsgBeaconOrCQ(myCall, myLocator, "DE", a174); + } else if (messageType == MeshtasticModSettings::MessageCQ) { + encodeMsgBeaconOrCQ(myCall, myLocator, "CQ", a174); + } else if (messageType == MeshtasticModSettings::MessageReply) { + encodeMsgReply(myCall, urCall, myLocator, a174); + } else if (messageType == MeshtasticModSettings::MessageReport) { + encodeMsgReport(myCall, urCall, myReport, 0, a174); + } else if (messageType == MeshtasticModSettings::MessageReplyReport) { + encodeMsgReport(myCall, urCall, myReport, 1, a174); + } else if (messageType == MeshtasticModSettings::MessageRRR) { + encodeMsgReport(myCall, urCall, "RRR", 1, a174); + } else if (messageType == MeshtasticModSettings::Message73) { + encodeMsgReport(myCall, urCall, "73", 1, a174); + } else { + encodeTextMsg(textMessage, a174); + } + + int allBits = ((174 / nbSymbolBits) + (174 % nbSymbolBits == 0 ? 0 : 1))*nbSymbolBits; // ensures zero bits padding + int iBit; + int symbol = 0; + + interleave174(a174); + + for (int i = 0; i < allBits; i++) + { + iBit = nbSymbolBits - (i % nbSymbolBits) - 1; // MSB first + + if (i < 174) { + symbol += a174[i] * (1<> 1); // Gray code + symbols.push_back(symbol); + symbol = 0; + } + } +} + +void MeshtasticModEncoderFT::encodeTextMsg(const QString& text, int a174[]) +{ + int a77[77]; + std::fill(a77, a77 + 77, 0); + QString sentMsg = text.rightJustified(13, ' ', true); + + if (!FT8::Packing::packfree(a77, sentMsg.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeTextMsg: failed to encode free text message (%s)", qPrintable(sentMsg)); + return; + } + + FT8::FT8::encode(a174, a77); +} + +void MeshtasticModEncoderFT::encodeMsgBeaconOrCQ(const QString& myCall, const QString& myLocator, const QString& shorthand, int a174[]) +{ + int c28_1, c28_2, g15; + + if (!FT8::Packing::packcall_std(c28_1, shorthand.toUpper().toStdString())) // + { + qDebug("MeshtasticModEncoderFT::encodeMsgBeaconOrCQ: failed to encode call1 (%s)", qPrintable(shorthand)); + return; + } + + if (!FT8::Packing::packcall_std(c28_2, myCall.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgBeaconOrCQ: failed to encode call2 (%s)", qPrintable(myCall)); + return; + } + + if (myLocator.size() < 4) + { + qDebug("MeshtasticModEncoderFT::encodeMsgBeaconOrCQ: locator invalid (%s)", qPrintable(myLocator)); + return; + } + + if (!FT8::Packing::packgrid(g15, myLocator.left(4).toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgBeaconOrCQ: failed to encode locator (%s)", qPrintable(myLocator)); + return; + } + + int a77[77]; + std::fill(a77, a77 + 77, 0); + FT8::Packing::pack1(a77, c28_1, c28_2, g15, 0); + FT8::FT8::encode(a174, a77); +} + +void MeshtasticModEncoderFT::encodeMsgReply(const QString& myCall, const QString& urCall, const QString& myLocator, int a174[]) +{ + int c28_1, c28_2, g15; + + if (!FT8::Packing::packcall_std(c28_1, urCall.toUpper().toStdString())) // + { + qDebug("MeshtasticModEncoderFT::encodeMsgReply: failed to encode call1 (%s)", qPrintable(urCall)); + return; + } + + if (!FT8::Packing::packcall_std(c28_2, myCall.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgReply: failed to encode call2 (%s)", qPrintable(myCall)); + return; + } + + if (myLocator.size() < 4) + { + qDebug("MeshtasticModEncoderFT::encodeMsgReply: locator invalid (%s)", qPrintable(myLocator)); + return; + } + + if (!FT8::Packing::packgrid(g15, myLocator.left(4).toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgReply: failed to encode locator (%s)", qPrintable(myLocator)); + return; + } + + int a77[77]; + std::fill(a77, a77 + 77, 0); + FT8::Packing::pack1(a77, c28_1, c28_2, g15, 0); + FT8::FT8::encode(a174, a77); +} + +void MeshtasticModEncoderFT::encodeMsgReport(const QString& myCall, const QString& urCall, const QString& myReport, int reply, int a174[]) +{ + int c28_1, c28_2, g15; + + if (!FT8::Packing::packcall_std(c28_1, urCall.toUpper().toStdString())) // + { + qDebug("MeshtasticModEncoderFT::encodeMsgReport: failed to encode call1 (%s)", qPrintable(urCall)); + return; + } + + if (!FT8::Packing::packcall_std(c28_2, myCall.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgReport: failed to encode call2 (%s)", qPrintable(myCall)); + return; + } + + if (!FT8::Packing::packgrid(g15, myReport.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgReport: failed to encode report (%s)", qPrintable(myReport)); + return; + } + + int a77[77]; + std::fill(a77, a77 + 77, 0); + FT8::Packing::pack1(a77, c28_1, c28_2, g15, reply); + FT8::FT8::encode(a174, a77); +} + +void MeshtasticModEncoderFT::encodeMsgFinish(const QString& myCall, const QString& urCall, const QString& shorthand, int a174[]) +{ + int c28_1, c28_2, g15; + + if (!FT8::Packing::packcall_std(c28_1, urCall.toUpper().toStdString())) // + { + qDebug("MeshtasticModEncoderFT::encodeMsgFinish: failed to encode call1 (%s)", qPrintable(urCall)); + return; + } + + if (!FT8::Packing::packcall_std(c28_2, myCall.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgFinish: failed to encode call2 (%s)", qPrintable(myCall)); + return; + } + + if (!FT8::Packing::packgrid(g15, shorthand.toUpper().toStdString())) + { + qDebug("MeshtasticModEncoderFT::encodeMsgFinish: failed to encode shorthand (%s)", qPrintable(shorthand)); + return; + } + + int a77[77]; + std::fill(a77, a77 + 77, 0); + FT8::Packing::pack1(a77, c28_1, c28_2, g15, 0); + FT8::FT8::encode(a174, a77); +} + +void MeshtasticModEncoderFT::interleave174(int a174[]) +{ + // 174 = 2*3*29 + int t174[174]; + std::copy(a174, a174+174, t174); + + for (int i = 0; i < 174; i++) { + a174[i] = t174[(i%6)*29 + (i%29)]; + } +} + +#endif // HAS_FT8 diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoderft.h b/plugins/channeltx/modmeshtastic/meshtasticmodencoderft.h new file mode 100644 index 000000000..0d8c6dc81 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoderft.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODEFT_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODEFT_H_ + +#include +#include + +#include "meshtasticmodsettings.h" + +class MeshtasticModEncoderFT +{ +public: + static void encodeMsg( + const QString& myCall, + const QString& urCall, + const QString& myLocator, + const QString& myReport, + const QString& textMessage, + MeshtasticModSettings::MessageType messageType, + unsigned int nbSymbolBits, + std::vector& symbols + ); + +private: + static void encodeTextMsg(const QString& text, int a174[]); + static void encodeMsgBeaconOrCQ(const QString& myCall, const QString& myLocator, const QString& shorthand, int a174[]); + static void encodeMsgReply(const QString& myCall, const QString& urCall, const QString& myLocator, int a174[]); + static void encodeMsgReport(const QString& myCall, const QString& urCall, const QString& myReport, int reply, int a174[]); + static void encodeMsgFinish(const QString& myCall, const QString& urCall, const QString& shorthand, int a174[]); + static void interleave174(int a174[]); +}; + +#endif // PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODEFT_H_ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoderlora.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodencoderlora.cpp new file mode 100644 index 000000000..8bf31c153 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoderlora.cpp @@ -0,0 +1,148 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Inspired by: https://github.com/myriadrf/LoRa-SDR // +// // +// 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 "meshtasticmodencoderlora.h" + +void MeshtasticModEncoderLoRa::addChecksum(QByteArray& bytes) +{ + uint16_t crc = sx1272DataChecksum(reinterpret_cast(bytes.data()), bytes.size()); + bytes.append(crc & 0xff); + bytes.append((crc >> 8) & 0xff); +} + +void MeshtasticModEncoderLoRa::encodeBytes( + const QByteArray& bytes, + std::vector& symbols, + unsigned int nbSymbolBits, + bool hasHeader, + bool hasCRC, + unsigned int nbParityBits +) +{ + if (nbSymbolBits < 5) { + return; + } + + const unsigned int numCodewords = roundUp(bytes.size()*2 + (hasHeader ? headerCodewords : 0), nbSymbolBits); // uses payload + CRC for encoding size + unsigned int cOfs = 0; + unsigned int dOfs = 0; + + std::vector codewords(numCodewords); + + if (hasHeader) + { + std::vector hdr(3); + unsigned int payloadSize = bytes.size() - (hasCRC ? 2 : 0); // actual payload size is without CRC + hdr[0] = payloadSize % 256; + hdr[1] = (hasCRC ? 1 : 0) | (nbParityBits << 1); + hdr[2] = headerChecksum(hdr.data()); + + // Nibble decomposition and parity bit(s) addition. LSNibble first. + codewords[cOfs++] = encodeHamming84sx(hdr[0] >> 4); + codewords[cOfs++] = encodeHamming84sx(hdr[0] & 0xf); // length + codewords[cOfs++] = encodeHamming84sx(hdr[1] & 0xf); // crc / fec info + codewords[cOfs++] = encodeHamming84sx(hdr[2] >> 4); // checksum + codewords[cOfs++] = encodeHamming84sx(hdr[2] & 0xf); + } + + unsigned int headerSize = cOfs; + + // fill nbSymbolBits codewords with 8 bit codewords using payload data (ecode and whiten) + encodeFec(codewords, 4, cOfs, dOfs, reinterpret_cast(bytes.data()), nbSymbolBits - headerSize); + Sx1272ComputeWhitening(codewords.data() + headerSize, nbSymbolBits - headerSize, 0, headerParityBits); + + // encode and whiten the rest of the payload with 4 + nbParityBits bits codewords + if (numCodewords > nbSymbolBits) + { + unsigned int cOfs2 = cOfs; + encodeFec(codewords, nbParityBits, cOfs, dOfs, reinterpret_cast(bytes.data()), numCodewords - nbSymbolBits); + Sx1272ComputeWhitening(codewords.data() + cOfs2, numCodewords - nbSymbolBits, nbSymbolBits - headerSize, nbParityBits); + } + + // header is always coded with 8 bits and yields exactly 8 symbols (headerSymbols) + const unsigned int numSymbols = headerSymbols + (numCodewords / nbSymbolBits - 1) * (4 + nbParityBits); + + // interleave the codewords into symbols + symbols.clear(); + symbols.resize(numSymbols); + diagonalInterleaveSx(codewords.data(), nbSymbolBits, symbols.data(), nbSymbolBits, headerParityBits); + + if (numCodewords > nbSymbolBits) { + diagonalInterleaveSx(codewords.data() + nbSymbolBits, numCodewords - nbSymbolBits, symbols.data() + headerSymbols, nbSymbolBits, nbParityBits); + } + + // gray decode + for (auto &sym : symbols) { + sym = grayToBinary16(sym); + } +} + +void MeshtasticModEncoderLoRa::encodeFec( + std::vector &codewords, + unsigned int nbParityBits, + unsigned int& cOfs, + unsigned int& dOfs, + const uint8_t *bytes, + const unsigned int codewordCount +) +{ + for (unsigned int i = 0; i < codewordCount; i++, dOfs++) + { + if (nbParityBits == 1) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeParity54(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeParity54(bytes[dOfs/2] & 0xf); + } + } + else if (nbParityBits == 2) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeParity64(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeParity64(bytes[dOfs/2] & 0xf); + } + } + else if (nbParityBits == 3) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeHamming74sx(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeHamming74sx(bytes[dOfs/2] & 0xf); + } + } + else if (nbParityBits == 4) + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = encodeHamming84sx(bytes[dOfs/2] >> 4); + } else { + codewords[cOfs++] = encodeHamming84sx(bytes[dOfs/2] & 0xf); + } + } + else + { + if (dOfs % 2 == 1) { + codewords[cOfs++] = bytes[dOfs/2] >> 4; + } else { + codewords[cOfs++] = bytes[dOfs/2] & 0xf; + } + } + } +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencoderlora.h b/plugins/channeltx/modmeshtastic/meshtasticmodencoderlora.h new file mode 100644 index 000000000..d4b6c8d54 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencoderlora.h @@ -0,0 +1,274 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// Inspired by: https://github.com/myriadrf/LoRa-SDR // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERLORA_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERLORA_H_ + +#include +#include + +class MeshtasticModEncoderLoRa +{ +public: + static void addChecksum(QByteArray& bytes); + static void encodeBytes( + const QByteArray& bytes, + std::vector& symbols, + unsigned int nbSymbolBits, + bool hasHeader, + bool hasCRC, + unsigned int nbParityBits + ); + +private: + static void encodeFec( + std::vector &codewords, + unsigned int nbParityBits, + unsigned int& cOfs, + unsigned int& dOfs, + const uint8_t *bytes, + const unsigned int codewordCount + ); + + static constexpr unsigned int headerParityBits = 4; + static constexpr unsigned int headerSymbols = 8; + static constexpr unsigned int headerCodewords = 5; + + /*********************************************************************** + * Round functions + **********************************************************************/ + static inline unsigned roundUp(unsigned num, unsigned factor) + { + return ((num + factor - 1) / factor) * factor; + } + + /*********************************************************************** + * Encode a 4 bit word into a 8 bits with parity + * Non standard version used in sx1272. + * https://en.wikipedia.org/wiki/Hamming_code + **********************************************************************/ + static inline unsigned char encodeHamming84sx(const unsigned char x) + { + auto d0 = (x >> 0) & 0x1; + auto d1 = (x >> 1) & 0x1; + auto d2 = (x >> 2) & 0x1; + auto d3 = (x >> 3) & 0x1; + + unsigned char b = x & 0xf; + b |= (d0 ^ d1 ^ d2) << 4; + b |= (d1 ^ d2 ^ d3) << 5; + b |= (d0 ^ d1 ^ d3) << 6; + b |= (d0 ^ d2 ^ d3) << 7; + return b; + } + + /*********************************************************************** + * Encode a 4 bit word into a 7 bits with parity. + * Non standard version used in sx1272. + **********************************************************************/ + static inline unsigned char encodeHamming74sx(const unsigned char x) + { + auto d0 = (x >> 0) & 0x1; + auto d1 = (x >> 1) & 0x1; + auto d2 = (x >> 2) & 0x1; + auto d3 = (x >> 3) & 0x1; + + unsigned char b = x & 0xf; + b |= (d0 ^ d1 ^ d2) << 4; + b |= (d1 ^ d2 ^ d3) << 5; + b |= (d0 ^ d1 ^ d3) << 6; + return b; + } + + /*********************************************************************** + * Encode a 4 bit word into a 6 bits with parity. + **********************************************************************/ + static inline unsigned char encodeParity64(const unsigned char b) + { + auto x = b ^ (b >> 1) ^ (b >> 2); + auto y = x ^ b ^ (b >> 3); + return ((x & 1) << 4) | ((y & 1) << 5) | (b & 0xf); + } + + /*********************************************************************** + * Encode a 4 bit word into a 5 bits with parity. + **********************************************************************/ + static inline unsigned char encodeParity54(const unsigned char b) + { + auto x = b ^ (b >> 2); + x = x ^ (x >> 1); + return (b & 0xf) | ((x << 4) & 0x10); + } + + /*********************************************************************** + * CRC reverse engineered from Sx1272 data stream. + * Modified CCITT crc with masking of the output with an 8bit lfsr + **********************************************************************/ + static inline uint16_t crc16sx(uint16_t crc, const uint16_t poly) + { + for (int i = 0; i < 8; i++) + { + if (crc & 0x8000) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + } + + return crc; + } + + static inline uint8_t xsum8(uint8_t t) + { + t ^= t >> 4; + t ^= t >> 2; + t ^= t >> 1; + + return (t & 1); + } + + static inline uint16_t sx1272DataChecksum(const uint8_t *data, int length) + { + uint16_t res = 0; + uint8_t v = 0xff; + uint16_t crc = 0; + + for (int i = 0; i < length; i++) + { + crc = crc16sx(res, 0x1021); + v = xsum8(v & 0xB8) | (v << 1); + res = crc ^ data[i]; + } + + res ^= v; + v = xsum8(v & 0xB8) | (v << 1); + res ^= v << 8; + + return res; + } + + /*********************************************************************** + * Specific checksum for header + **********************************************************************/ + static inline uint8_t headerChecksum(const uint8_t *h) + { + auto a0 = (h[0] >> 4) & 0x1; + auto a1 = (h[0] >> 5) & 0x1; + auto a2 = (h[0] >> 6) & 0x1; + auto a3 = (h[0] >> 7) & 0x1; + + auto b0 = (h[0] >> 0) & 0x1; + auto b1 = (h[0] >> 1) & 0x1; + auto b2 = (h[0] >> 2) & 0x1; + auto b3 = (h[0] >> 3) & 0x1; + + auto c0 = (h[1] >> 0) & 0x1; + auto c1 = (h[1] >> 1) & 0x1; + auto c2 = (h[1] >> 2) & 0x1; + auto c3 = (h[1] >> 3) & 0x1; + + uint8_t res; + res = (a0 ^ a1 ^ a2 ^ a3) << 4; + res |= (a3 ^ b1 ^ b2 ^ b3 ^ c0) << 3; + res |= (a2 ^ b0 ^ b3 ^ c1 ^ c3) << 2; + res |= (a1 ^ b0 ^ b2 ^ c0 ^ c1 ^ c2) << 1; + res |= a0 ^ b1 ^ c0 ^ c1 ^ c2 ^ c3; + + return res; + } + + /*********************************************************************** + * Whitening generator reverse engineered from Sx1272 data stream. + * Each bit of a codeword is combined with the output from a different position in the whitening sequence. + **********************************************************************/ + static inline void Sx1272ComputeWhitening(uint8_t *buffer, uint16_t bufferSize, const int bitOfs, const int nbParityBits) + { + static const int ofs0[8] = {6,4,2,0,-112,-114,-302,-34 }; // offset into sequence for each bit + static const int ofs1[5] = {6,4,2,0,-360 }; // different offsets used for single parity mode (1 == nbParityBits) + static const int whiten_len = 510; // length of whitening sequence + static const uint64_t whiten_seq[8] = { // whitening sequence + 0x0102291EA751AAFFL,0xD24B050A8D643A17L,0x5B279B671120B8F4L,0x032B37B9F6FB55A2L, + 0x994E0F87E95E2D16L,0x7CBCFC7631984C26L,0x281C8E4F0DAEF7F9L,0x1741886EB7733B15L + }; + const int *ofs = (1 == nbParityBits) ? ofs1 : ofs0; + int i, j; + + for (j = 0; j < bufferSize; j++) + { + uint8_t x = 0; + + for (i = 0; i < 4 + nbParityBits; i++) + { + int t = (ofs[i] + j + bitOfs + whiten_len) % whiten_len; + + if (whiten_seq[t >> 6] & ((uint64_t)1 << (t & 0x3F))) { + x |= 1 << i; + } + } + + buffer[j] ^= x; + } + } + + /*********************************************************************** + * Diagonal interleaver + deinterleaver + **********************************************************************/ + static inline void diagonalInterleaveSx( + const uint8_t *codewords, + const size_t numCodewords, + uint16_t *symbols, + const size_t nbSymbolBits, + const size_t nbParityBits + ) + { + for (size_t x = 0; x < numCodewords / nbSymbolBits; x++) + { + const size_t cwOff = x*nbSymbolBits; + const size_t symOff = x*(4 + nbParityBits); + + for (size_t k = 0; k < 4 + nbParityBits; k++) + { + for (size_t m = 0; m < nbSymbolBits; m++) + { + const size_t i = (m + k + nbSymbolBits) % nbSymbolBits; + const auto bit = (codewords[cwOff + i] >> k) & 0x1; + symbols[symOff + k] |= (bit << m); + } + } + } + } + + /*********************************************************************** + * https://en.wikipedia.org/wiki/Gray_code + **********************************************************************/ + + /* + * A more efficient version, for Gray codes of 16 or fewer bits. + */ + static inline unsigned short grayToBinary16(unsigned short num) + { + num = num ^ (num >> 8); + num = num ^ (num >> 4); + num = num ^ (num >> 2); + num = num ^ (num >> 1); + return num; + } +}; + +#endif // PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERLORA_H_ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencodertty.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodencodertty.cpp new file mode 100644 index 000000000..10ef8d47d --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencodertty.cpp @@ -0,0 +1,133 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (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 "meshtasticmodencodertty.h" + +const signed char MeshtasticModEncoderTTY::asciiToTTYLetters[128] = { +// '\x00' '\x01' '\x02' '\x03' '\x04' '\x05' '\x06' '\x07' + 0x00, -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '\x08' '\t' '\n' '\x0b' '\x0c' '\r' '\x0e' '\x0f' + -1 , -1 , 0x02, -1 , -1 , 0x08, -1 , -1 , +// '\x10' '\x11' '\x12' '\x13' '\x14' '\x15' '\x16' '\x17' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '\x18' '\x19' '\x1a' '\x1b' '\x1c' '\x1d' '\x1e' '\x1f' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// ' ' '!' '"' '#' '$' '%' '&' "'" + 0x04, -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '(' ')' '*' '+' ',' '-' '.' '/' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '0' '1' '2' '3' '4' '5' '6' '7' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '8' '9' ':' ';' '<' '=' '>' '?' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '@' 'A' 'B' 'C' 'D' 'E' 'F' 'G' + -1 , 0x03, 0x19, 0x0e, 0x09, 0x01, 0x0d, 0x1a, +// 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' + 0x14, 0x06, 0x0b, 0x0f, 0x12, 0x1c, 0x0c, 0x18, +// 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' + 0x16, 0x17, 0x0a, 0x05, 0x10, 0x07, 0x1e, 0x13, +// 'X' 'Y' 'Z' '[' '\\' ']' '^' '_' + 0x1d, 0x15, 0x11, -1 , -1 , -1 , -1 , -1 , +// '`' 'a' 'b' 'c' 'd' 'e' 'f' 'g' + -1 , 0x03, 0x19, 0x0e, 0x09, 0x01, 0x0d, 0x1a, +// 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' + 0x14, 0x06, 0x0b, 0x0f, 0x12, 0x1c, 0x0c, 0x18, +// 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' + 0x16, 0x17, 0x0a, 0x05, 0x10, 0x07, 0x1e, 0x13, +// 'x' 'y' 'z' '{' '|' '}' '~' '\x7f' + 0x1d, 0x15, 0x11, -1 , -1 , -1 , -1 , -1 + }; + +const signed char MeshtasticModEncoderTTY::asciiToTTYFigures[128] = { +// '\x00' '\x01' '\x02' '\x03' '\x04' '\x05' '\x06' '\x07' + 0x00, -1 , -1 , -1 , -1 , -1 , -1 , 0x05, +// '\x08' '\t' '\n' '\x0b' '\x0c' '\r' '\x0e' '\x0f' + -1 , -1 , 0x02, -1 , -1 , 0x08, -1 , -1 , +// '\x10' '\x11' '\x12' '\x13' '\x14' '\x15' '\x16' '\x17' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// '\x18' '\x19' '\x1a' '\x1b' '\x1c' '\x1d' '\x1e' '\x1f' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// ' ' '!' '"' '#' '$' '%' '&' "'" + 0x04, 0x0d, 0x11, 0x14, 0x09, -1 , 0x1a, -1 , +// '(' ')' '*' '+' ',' '-' '.' '/' + 0x0f, 0x12, -1 , -1 , 0x0c, 0x03, 0x1c, 0x1d, +// '0' '1' '2' '3' '4' '5' '6' '7' + 0x16, 0x17, 0x13, 0x01, 0x0a, 0x10, 0x15, 0x07, +// '8' '9' ':' ';' '<' '=' '>' '?' + 0x06, 0x18, 0x0e, 0x1e, -1 , -1 , -1 , 0x19, +// '@' 'A' 'B' 'C' 'D' 'E' 'F' 'G' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'X' 'Y' 'Z' '[' '\\' ']' '^' '_' + -1 , -1 , -1 , -1 , 0x0b, -1 , -1 , -1 , +// '`' 'a' 'b' 'c' 'd' 'e' 'f' 'g' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , +// 'x' 'y' 'z' '{' '|' '}' '~' '\x7f' + -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 + }; + +void MeshtasticModEncoderTTY::encodeString(const QString& str, std::vector& symbols) +{ + TTYState ttyState = TTYLetters; + QByteArray asciiStr = str.toUtf8(); + QByteArray::const_iterator it = asciiStr.begin(); + + for (; it != asciiStr.end(); ++it) + { + char asciiChar = *it & 0x7F; + int ttyLetter = asciiToTTYLetters[(int) asciiChar]; + int ttyFigure = asciiToTTYFigures[(int) asciiChar]; + + if (ttyLetter < 0) + { + if (ttyFigure >= 0) + { + if (ttyState != TTYFigures) + { + symbols.push_back(ttyFigures); + ttyState = TTYFigures; + } + + symbols.push_back(ttyFigure); + } // else skip + } + else + { + if (ttyFigure >= 0) + { + symbols.push_back(ttyFigure); // same TTY character no state change + } + else + { + if (ttyState != TTYLetters) + { + symbols.push_back(ttyLetters); + ttyState = TTYLetters; + } + + symbols.push_back(ttyLetter); + } + } + } +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodencodertty.h b/plugins/channeltx/modmeshtastic/meshtasticmodencodertty.h new file mode 100644 index 000000000..649b5c47a --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodencodertty.h @@ -0,0 +1,44 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERTTY_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERTTY_H_ + +#include +#include + +class MeshtasticModEncoderTTY +{ +public: + static void encodeString(const QString& str, std::vector& symbols); + +private: + enum TTYState + { + TTYLetters, + TTYFigures + }; + + static const signed char asciiToTTYLetters[128]; + static const signed char asciiToTTYFigures[128]; + static const char ttyLetters = 0x1f; + static const char ttyFigures = 0x1b; +}; + +#endif // PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODENCODERTTY_H_ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodgui.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodgui.cpp new file mode 100644 index 000000000..f4b033c0e --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodgui.cpp @@ -0,0 +1,880 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020-2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2021-2023 Jon Beniston, M7RCE // +// // +// 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 "device/deviceuiset.h" +#include "plugin/pluginapi.h" +#include "util/db.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/dialpopup.h" +#include "gui/dialogpositioner.h" +#include "maincore.h" + +#include "ui_meshtasticmodgui.h" +#include "meshtasticmodgui.h" +#include "meshtasticpacket.h" + + +MeshtasticModGUI* MeshtasticModGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx) +{ + MeshtasticModGUI* gui = new MeshtasticModGUI(pluginAPI, deviceUISet, channelTx); + return gui; +} + +void MeshtasticModGUI::destroy() +{ + delete this; +} + +void MeshtasticModGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray MeshtasticModGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshtasticModGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool MeshtasticModGUI::handleMessage(const Message& message) +{ + if (MeshtasticMod::MsgConfigureMeshtasticMod::match(message)) + { + const MeshtasticMod::MsgConfigureMeshtasticMod& cfg = (MeshtasticMod::MsgConfigureMeshtasticMod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (MeshtasticMod::MsgReportPayloadTime::match(message)) + { + const MeshtasticMod::MsgReportPayloadTime& rpt = (MeshtasticMod::MsgReportPayloadTime&) message; + float fourthsMs = ((1<timeMessageLengthText->setText(tr("%1").arg(rpt.getNbSymbols())); + ui->timePayloadText->setText(tr("%1 ms").arg(QString::number(rpt.getPayloadTimeMs(), 'f', 0))); + ui->timeTotalText->setText(tr("%1 ms").arg(QString::number(rpt.getPayloadTimeMs() + controlMs, 'f', 0))); + ui->timeSymbolText->setText(tr("%1 ms").arg(QString::number(4.0*fourthsMs, 'f', 1))); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_deviceCenterFrequency = notif.getCenterFrequency(); + int basebandSampleRate = notif.getSampleRate(); + qDebug() << "MeshtasticModGUI::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(); + + return true; + } + else + { + return false; + } +} + +void MeshtasticModGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void MeshtasticModGUI::handleSourceMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +QString MeshtasticModGUI::getActivePayloadText() const +{ + switch (m_settings.m_messageType) + { + case MeshtasticModSettings::MessageBeacon: + return m_settings.m_beaconMessage; + case MeshtasticModSettings::MessageCQ: + return m_settings.m_cqMessage; + case MeshtasticModSettings::MessageReply: + return m_settings.m_replyMessage; + case MeshtasticModSettings::MessageReport: + return m_settings.m_reportMessage; + case MeshtasticModSettings::MessageReplyReport: + return m_settings.m_replyReportMessage; + case MeshtasticModSettings::MessageRRR: + return m_settings.m_rrrMessage; + case MeshtasticModSettings::Message73: + return m_settings.m_73Message; + case MeshtasticModSettings::MessageQSOText: + return m_settings.m_qsoTextMessage; + case MeshtasticModSettings::MessageText: + return m_settings.m_textMessage; + case MeshtasticModSettings::MessageBytes: + return QString::fromUtf8(m_settings.m_bytesMessage); + default: + return QString(); + } +} + +int MeshtasticModGUI::findBandwidthIndex(int bandwidthHz) const +{ + int bestIndex = -1; + int bestDelta = 1 << 30; + + for (int i = 0; i < MeshtasticModSettings::nbBandwidths; ++i) + { + const int delta = std::abs(MeshtasticModSettings::bandwidths[i] - bandwidthHz); + if (delta < bestDelta) + { + bestDelta = delta; + bestIndex = i; + } + } + + return bestIndex; +} + +void MeshtasticModGUI::applyMeshtasticRadioSettingsIfPresent(const QString& payloadText) +{ + if (m_settings.m_codingScheme != MeshtasticModSettings::CodingLoRa) { + return; + } + + if (!Meshtastic::Packet::isCommand(payloadText)) { + return; + } + + Meshtastic::TxRadioSettings meshRadio; + QString error; + if (!Meshtastic::Packet::deriveTxRadioSettings(payloadText, meshRadio, error)) + { + qWarning() << "MeshtasticModGUI::applyMeshtasticRadioSettingsIfPresent:" << error; + return; + } + + bool changed = false; + const int bwIndex = findBandwidthIndex(meshRadio.bandwidthHz); + if (bwIndex >= 0 && bwIndex != m_settings.m_bandwidthIndex) { + m_settings.m_bandwidthIndex = bwIndex; + changed = true; + } + + if (meshRadio.spreadFactor > 0 && meshRadio.spreadFactor != m_settings.m_spreadFactor) { + m_settings.m_spreadFactor = meshRadio.spreadFactor; + changed = true; + } + + if (meshRadio.parityBits > 0 && meshRadio.parityBits != m_settings.m_nbParityBits) { + m_settings.m_nbParityBits = meshRadio.parityBits; + changed = true; + } + + if (meshRadio.deBits != m_settings.m_deBits) { + m_settings.m_deBits = meshRadio.deBits; + changed = true; + } + + if (meshRadio.syncWord != m_settings.m_syncWord) { + m_settings.m_syncWord = meshRadio.syncWord; + changed = true; + } + + if (meshRadio.hasCenterFrequency) + { + if (m_deviceCenterFrequency != 0) + { + const qint64 wantedOffset = meshRadio.centerFrequencyHz - m_deviceCenterFrequency; + if (wantedOffset != m_settings.m_inputFrequencyOffset) + { + m_settings.m_inputFrequencyOffset = static_cast(wantedOffset); + changed = true; + } + } + else + { + qWarning() << "MeshtasticModGUI::applyMeshtasticRadioSettingsIfPresent: device center frequency unknown, cannot auto-center"; + } + } + + if (!changed) { + return; + } + + qInfo() << "MeshtasticModGUI::applyMeshtasticRadioSettingsIfPresent:" << meshRadio.summary; + + const int thisBW = MeshtasticModSettings::bandwidths[m_settings.m_bandwidthIndex]; + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBW); + m_channelMarker.blockSignals(false); + + blockApplySettings(true); + ui->deltaFrequency->setValue(m_settings.m_inputFrequencyOffset); + ui->bw->setValue(m_settings.m_bandwidthIndex); + ui->bwText->setText(QString("%1 Hz").arg(thisBW)); + ui->spread->setValue(m_settings.m_spreadFactor); + ui->spreadText->setText(tr("%1").arg(m_settings.m_spreadFactor)); + ui->deBits->setValue(m_settings.m_deBits); + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + ui->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->syncWord->setText(tr("%1").arg(m_settings.m_syncWord, 2, 16)); + blockApplySettings(false); + + updateAbsoluteCenterFrequency(); +} + +void MeshtasticModGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void MeshtasticModGUI::on_bw_valueChanged(int value) +{ + if (value < 0) { + m_settings.m_bandwidthIndex = 0; + } else if (value < MeshtasticModSettings::nbBandwidths) { + m_settings.m_bandwidthIndex = value; + } else { + m_settings.m_bandwidthIndex = MeshtasticModSettings::nbBandwidths - 1; + } + + int thisBW = MeshtasticModSettings::bandwidths[value]; + ui->bwText->setText(QString("%1 Hz").arg(thisBW)); + m_channelMarker.setBandwidth(thisBW); + + applySettings(); +} + +void MeshtasticModGUI::on_channelMute_toggled(bool checked) +{ + m_settings.m_channelMute = checked; + applySettings(); +} + +void MeshtasticModGUI::on_spread_valueChanged(int value) +{ + m_settings.m_spreadFactor = value; + ui->spreadText->setText(tr("%1").arg(value)); + + applySettings(); +} + +void MeshtasticModGUI::on_deBits_valueChanged(int value) +{ + m_settings.m_deBits = value; + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + applySettings(); +} + +void MeshtasticModGUI::on_preambleChirps_valueChanged(int value) +{ + m_settings.m_preambleChirps = value; + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + applySettings(); +} + +void MeshtasticModGUI::on_idleTime_valueChanged(int value) +{ + m_settings.m_quietMillis = value * 100; + ui->idleTimeText->setText(tr("%1").arg(m_settings.m_quietMillis / 1000.0, 0, 'f', 1)); + applySettings(); +} + +void MeshtasticModGUI::on_syncWord_editingFinished() +{ + bool ok; + unsigned int syncWord = ui->syncWord->text().toUInt(&ok, 16); + + if (ok) + { + m_settings.m_syncWord = syncWord > 255 ? 0 : syncWord; + applySettings(); + } +} + +void MeshtasticModGUI::on_scheme_currentIndexChanged(int index) +{ + m_settings.m_codingScheme = (MeshtasticModSettings::CodingScheme) index; + ui->fecParity->setEnabled(m_settings.m_codingScheme == MeshtasticModSettings::CodingLoRa); + ui->crc->setEnabled(m_settings.m_codingScheme == MeshtasticModSettings::CodingLoRa); + ui->header->setEnabled(m_settings.m_codingScheme == MeshtasticModSettings::CodingLoRa); + applySettings(); +} + +void MeshtasticModGUI::on_fecParity_valueChanged(int value) +{ + m_settings.m_nbParityBits = value; + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + applySettings(); +} + +void MeshtasticModGUI::on_crc_stateChanged(int state) +{ + m_settings.m_hasCRC = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticModGUI::on_header_stateChanged(int state) +{ + m_settings.m_hasHeader = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticModGUI::on_myCall_editingFinished() +{ + m_settings.m_myCall = ui->myCall->text(); + applySettings(); +} + +void MeshtasticModGUI::on_urCall_editingFinished() +{ + m_settings.m_urCall = ui->urCall->text(); + applySettings(); +} + +void MeshtasticModGUI::on_myLocator_editingFinished() +{ + m_settings.m_myLoc = ui->myLocator->text(); + applySettings(); +} + +void MeshtasticModGUI::on_report_editingFinished() +{ + m_settings.m_myRpt = ui->report->text(); + applySettings(); +} + +void MeshtasticModGUI::on_msgType_currentIndexChanged(int index) +{ + m_settings.m_messageType = (MeshtasticModSettings::MessageType) index; + displayCurrentPayloadMessage(); + applyMeshtasticRadioSettingsIfPresent(getActivePayloadText()); + applySettings(); +} + +void MeshtasticModGUI::on_resetMessages_clicked(bool checked) +{ + (void) checked; + m_settings.setDefaultTemplates(); + displayCurrentPayloadMessage(); + applySettings(); +} + +void MeshtasticModGUI::on_playMessage_clicked(bool checked) +{ + (void) checked; + applyMeshtasticRadioSettingsIfPresent(getActivePayloadText()); + // Switch to message None then back to current message type to trigger sending process + MeshtasticModSettings::MessageType msgType = m_settings.m_messageType; + m_settings.m_messageType = MeshtasticModSettings::MessageNone; + applySettings(); + m_settings.m_messageType = msgType; + applySettings(); +} + +void MeshtasticModGUI::on_repeatMessage_valueChanged(int value) +{ + m_settings.m_messageRepeat = value; + ui->repeatText->setText(tr("%1").arg(m_settings.m_messageRepeat)); + applySettings(); +} + +void MeshtasticModGUI::on_generateMessages_clicked(bool checked) +{ + (void) checked; + m_settings.generateMessages(); + displayCurrentPayloadMessage(); + applySettings(); +} + +void MeshtasticModGUI::on_messageText_editingFinished() +{ + if (m_settings.m_messageType == MeshtasticModSettings::MessageBeacon) { + m_settings.m_beaconMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageCQ) { + m_settings.m_cqMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageReply) { + m_settings.m_replyMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageReport) { + m_settings.m_reportMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageReplyReport) { + m_settings.m_replyReportMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageRRR) { + m_settings.m_rrrMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::Message73) { + m_settings.m_73Message = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageQSOText) { + m_settings.m_qsoTextMessage = ui->messageText->toPlainText(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageText) { + m_settings.m_textMessage = ui->messageText->toPlainText(); + } + + applyMeshtasticRadioSettingsIfPresent(getActivePayloadText()); + applySettings(); +} + +void MeshtasticModGUI::on_hexText_editingFinished() +{ + m_settings.m_bytesMessage = QByteArray::fromHex(ui->hexText->text().toLatin1()); + applyMeshtasticRadioSettingsIfPresent(getActivePayloadText()); + applySettings(); +} + +void MeshtasticModGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void MeshtasticModGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void MeshtasticModGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void MeshtasticModGUI::on_invertRamps_stateChanged(int state) +{ + m_settings.m_invertRamps = (state == Qt::Checked); + applySettings(); +} + +void MeshtasticModGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void MeshtasticModGUI::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_chirpChatMod->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(); +} + +MeshtasticModGUI::MeshtasticModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::MeshtasticModGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_basebandSampleRate(125000), + m_doApplySettings(true), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channeltx/modmeshtastic/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + 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_chirpChatMod = (MeshtasticMod*) channelTx; + m_chirpChatMod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + 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 modulator channel.")); + ui->deltaUnits->setToolTip(tr("Frequency unit for the offset control.")); + ui->bw->setToolTip(tr("LoRa transmit bandwidth.")); + ui->bwLabel->setToolTip(tr("LoRa transmit bandwidth selector.")); + ui->bwText->setToolTip(tr("Current LoRa transmit bandwidth in Hz.")); + ui->spread->setToolTip(tr("LoRa spreading factor (SF).")); + 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 chirp count.")); + ui->preambleChirpsLabel->setToolTip(tr("LoRa preamble chirp count selector.")); + ui->preambleChirpsText->setToolTip(tr("Current preamble chirp value.")); + ui->idleTime->setToolTip(tr("Silence interval between repeated messages (x0.1s).")); + ui->idleTimeLabel->setToolTip(tr("Idle interval between repeated transmissions.")); + ui->idleTimeText->setToolTip(tr("Current idle interval in seconds.")); + ui->syncWord->setToolTip(tr("LoRa sync word in hexadecimal (00-ff).")); + ui->syncLabel->setToolTip(tr("LoRa sync word.")); + ui->scheme->setToolTip(tr("Encoder mode. Use LoRa for Meshtastic-compatible payloads.")); + ui->schemeLabel->setToolTip(tr("Select encoding scheme.")); + 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->crc->setToolTip(tr("Append payload CRC.")); + ui->header->setToolTip(tr("Use explicit LoRa header mode.")); + ui->channelMute->setToolTip(tr("Mute this channel output.")); + ui->myCall->setToolTip(tr("Source callsign used by template messages.")); + ui->myCallLabel->setToolTip(tr("Source callsign field.")); + ui->urCall->setToolTip(tr("Destination callsign used by template messages.")); + ui->urCallLabel->setToolTip(tr("Destination callsign field.")); + ui->myLocator->setToolTip(tr("Source locator used by template messages.")); + ui->myLocatorLabel->setToolTip(tr("Source locator field.")); + ui->report->setToolTip(tr("Signal report used by template messages.")); + ui->reportLabel->setToolTip(tr("Signal report field.")); + ui->msgType->setToolTip(tr("Select which payload template is edited/transmitted.")); + ui->msgTypeLabel->setToolTip(tr("Payload template type.")); + ui->resetMessages->setToolTip(tr("Reset payload templates to defaults.")); + ui->playMessage->setToolTip(tr("Queue one transmission of current message type.")); + ui->repeatMessage->setToolTip(tr("Number of repetitions for each triggered transmission.")); + ui->repeatLabel->setToolTip(tr("Transmission repetition count.")); + ui->generateMessages->setToolTip(tr("Generate standardized payload templates from current fields.")); + ui->messageText->setToolTip(tr("Text payload editor. Meshtastic MESH: commands can auto-apply radio settings.")); + ui->msgLabel->setToolTip(tr("Message payload editor.")); + ui->hexText->setToolTip(tr("Raw hexadecimal payload bytes.")); + ui->hexLabel->setToolTip(tr("Hexadecimal payload editor.")); + ui->udpEnabled->setToolTip(tr("Receive message payloads from UDP input.")); + ui->udpAddress->setToolTip(tr("UDP listen address for incoming payloads.")); + ui->udpPort->setToolTip(tr("UDP listen port for incoming payloads.")); + ui->udpSeparator->setToolTip(tr("UDP input controls.")); + ui->invertRamps->setToolTip(tr("Invert chirp ramp direction.")); + ui->channelPower->setToolTip(tr("Estimated channel output power.")); + ui->timesLabel->setToolTip(tr("Estimated timing values for current LoRa frame.")); + ui->timeSymbolText->setToolTip(tr("Estimated LoRa symbol time.")); + ui->timeSymbolLabel->setToolTip(tr("LoRa symbol time estimate.")); + ui->timeMessageLengthText->setToolTip(tr("Estimated payload symbol count.")); + ui->timeMessageLengthLabel->setToolTip(tr("Payload symbol count estimate.")); + ui->timePayloadText->setToolTip(tr("Estimated payload airtime.")); + ui->timePayloadLabel->setToolTip(tr("Payload airtime estimate.")); + ui->timeTotalText->setToolTip(tr("Estimated total airtime including preamble/control.")); + ui->timeTotalLabel->setToolTip(tr("Total frame airtime estimate.")); + ui->repeatText->setToolTip(tr("Current repetition count.")); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::red); + m_channelMarker.setBandwidth(12500); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setTitle("Meshtastic Modulator"); + m_channelMarker.setSourceOrSinkStream(false); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + + m_settings.setChannelMarker(&m_channelMarker); + m_settings.setRollupState(&m_rollupState); + + setBandwidths(); + displaySettings(); + makeUIConnections(); + applySettings(); + DialPopup::addPopupsToChildDials(this); + m_resizer.enableChildMouseTracking(); +} + +MeshtasticModGUI::~MeshtasticModGUI() +{ + delete ui; +} + +void MeshtasticModGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void MeshtasticModGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + MeshtasticMod::MsgConfigureMeshtasticMod *msg = MeshtasticMod::MsgConfigureMeshtasticMod::create(m_settings, force); + m_chirpChatMod->getInputMessageQueue()->push(msg); + } +} + +void MeshtasticModGUI::displaySettings() +{ + int thisBW = MeshtasticModSettings::bandwidths[m_settings.m_bandwidthIndex]; + + m_channelMarker.blockSignals(true); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBW); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); + setTitleColor(m_settings.m_rgbColor); + + setWindowTitle(m_channelMarker.getTitle()); + setTitle(m_channelMarker.getTitle()); + updateIndexLabel(); + displayCurrentPayloadMessage(); + displayBinaryMessage(); + + ui->fecParity->setEnabled(m_settings.m_codingScheme == MeshtasticModSettings::CodingLoRa); + ui->crc->setEnabled(m_settings.m_codingScheme == MeshtasticModSettings::CodingLoRa); + ui->header->setEnabled(m_settings.m_codingScheme == MeshtasticModSettings::CodingLoRa); + + blockApplySettings(true); + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + ui->bwText->setText(QString("%1 Hz").arg(thisBW)); + ui->bw->setValue(m_settings.m_bandwidthIndex); + ui->spread->setValue(m_settings.m_spreadFactor); + ui->spreadText->setText(tr("%1").arg(m_settings.m_spreadFactor)); + ui->deBits->setValue(m_settings.m_deBits); + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + ui->preambleChirps->setValue(m_settings.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + ui->idleTime->setValue(m_settings.m_quietMillis / 100); + ui->idleTimeText->setText(tr("%1").arg(m_settings.m_quietMillis / 1000.0, 0, 'f', 1)); + ui->syncWord->setText((tr("%1").arg(m_settings.m_syncWord, 2, 16))); + ui->channelMute->setChecked(m_settings.m_channelMute); + ui->scheme->setCurrentIndex((int) m_settings.m_codingScheme); + ui->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->crc->setChecked(m_settings.m_hasCRC); + ui->header->setChecked(m_settings.m_hasHeader); + ui->myCall->setText(m_settings.m_myCall); + ui->urCall->setText(m_settings.m_urCall); + ui->myLocator->setText(m_settings.m_myLoc); + ui->report->setText(m_settings.m_myRpt); + ui->repeatMessage->setValue(m_settings.m_messageRepeat); + ui->repeatText->setText(tr("%1").arg(m_settings.m_messageRepeat)); + ui->msgType->setCurrentIndex((int) m_settings.m_messageType); + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + ui->invertRamps->setChecked(m_settings.m_invertRamps); + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); +} + +void MeshtasticModGUI::displayCurrentPayloadMessage() +{ + ui->messageText->blockSignals(true); + + if (m_settings.m_messageType == MeshtasticModSettings::MessageNone) { + ui->messageText->clear(); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageBeacon) { + ui->messageText->setText(m_settings.m_beaconMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageCQ) { + ui->messageText->setText(m_settings.m_cqMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageReply) { + ui->messageText->setText(m_settings.m_replyMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageReport) { + ui->messageText->setText(m_settings.m_reportMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageReplyReport) { + ui->messageText->setText(m_settings.m_replyReportMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageRRR) { + ui->messageText->setText(m_settings.m_rrrMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::Message73) { + ui->messageText->setText(m_settings.m_73Message); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageQSOText) { + ui->messageText->setText(m_settings.m_qsoTextMessage); + } else if (m_settings.m_messageType == MeshtasticModSettings::MessageText) { + ui->messageText->setText(m_settings.m_textMessage); + } + + ui->messageText->blockSignals(false); +} + +void MeshtasticModGUI::displayBinaryMessage() +{ + ui->hexText->setText(m_settings.m_bytesMessage.toHex()); +} + +void MeshtasticModGUI::setBandwidths() +{ + int maxBandwidth = m_basebandSampleRate / MeshtasticModSettings::oversampling; + int maxIndex = 0; + + for (; (maxIndex < MeshtasticModSettings::nbBandwidths) && (MeshtasticModSettings::bandwidths[maxIndex] <= maxBandwidth); maxIndex++) + {} + + if (maxIndex != 0) + { + qDebug("MeshtasticModGUI::setBandwidths: avl: %d max: %d", maxBandwidth, MeshtasticModSettings::bandwidths[maxIndex-1]); + ui->bw->setMaximum(maxIndex - 1); + int index = ui->bw->value(); + ui->bwText->setText(QString("%1 Hz").arg(MeshtasticModSettings::bandwidths[index])); + } +} + +void MeshtasticModGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void MeshtasticModGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void MeshtasticModGUI::tick() +{ + if (m_tickCount < 10) + { + m_tickCount++; + } + else + { + m_tickCount = 0; + double powDb = CalcDb::dbPower(m_chirpChatMod->getMagSq()); + m_channelPowerDbAvg(powDb); + ui->channelPower->setText(tr("%1 dB").arg(m_channelPowerDbAvg.asDouble(), 0, 'f', 1)); + + if (m_chirpChatMod->getModulatorActive()) { + ui->playMessage->setStyleSheet("QPushButton { background-color : green; }"); + } else { + ui->playMessage->setStyleSheet("QPushButton { background:rgb(79,79,79); }"); + } + } +} + +void MeshtasticModGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &MeshtasticModGUI::on_deltaFrequency_changed); + QObject::connect(ui->bw, &QSlider::valueChanged, this, &MeshtasticModGUI::on_bw_valueChanged); + QObject::connect(ui->spread, &QSlider::valueChanged, this, &MeshtasticModGUI::on_spread_valueChanged); + QObject::connect(ui->deBits, &QSlider::valueChanged, this, &MeshtasticModGUI::on_deBits_valueChanged); + QObject::connect(ui->preambleChirps, &QSlider::valueChanged, this, &MeshtasticModGUI::on_preambleChirps_valueChanged); + QObject::connect(ui->idleTime, &QSlider::valueChanged, this, &MeshtasticModGUI::on_idleTime_valueChanged); + QObject::connect(ui->syncWord, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_syncWord_editingFinished); + QObject::connect(ui->channelMute, &QToolButton::toggled, this, &MeshtasticModGUI::on_channelMute_toggled); + QObject::connect(ui->scheme, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticModGUI::on_scheme_currentIndexChanged); + QObject::connect(ui->fecParity, &QDial::valueChanged, this, &MeshtasticModGUI::on_fecParity_valueChanged); + QObject::connect(ui->crc, &QCheckBox::stateChanged, this, &MeshtasticModGUI::on_crc_stateChanged); + QObject::connect(ui->header, &QCheckBox::stateChanged, this, &MeshtasticModGUI::on_header_stateChanged); + QObject::connect(ui->myCall, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_myCall_editingFinished); + QObject::connect(ui->urCall, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_urCall_editingFinished); + QObject::connect(ui->myLocator, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_myLocator_editingFinished); + QObject::connect(ui->report, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_report_editingFinished); + QObject::connect(ui->msgType, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshtasticModGUI::on_msgType_currentIndexChanged); + QObject::connect(ui->resetMessages, &QPushButton::clicked, this, &MeshtasticModGUI::on_resetMessages_clicked); + QObject::connect(ui->playMessage, &QPushButton::clicked, this, &MeshtasticModGUI::on_playMessage_clicked); + QObject::connect(ui->repeatMessage, &QDial::valueChanged, this, &MeshtasticModGUI::on_repeatMessage_valueChanged); + QObject::connect(ui->generateMessages, &QPushButton::clicked, this, &MeshtasticModGUI::on_generateMessages_clicked); + QObject::connect(ui->messageText, &CustomTextEdit::editingFinished, this, &MeshtasticModGUI::on_messageText_editingFinished); + QObject::connect(ui->hexText, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_hexText_editingFinished); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &MeshtasticModGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &MeshtasticModGUI::on_udpPort_editingFinished); + QObject::connect(ui->invertRamps, &QCheckBox::stateChanged, this, &MeshtasticModGUI::on_invertRamps_stateChanged); +} + +void MeshtasticModGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodgui.h b/plugins/channeltx/modmeshtastic/meshtasticmodgui.h new file mode 100644 index 000000000..b03782e67 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodgui.h @@ -0,0 +1,135 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2021-2022 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODLORA_LORAMODGUI_H_ +#define PLUGINS_CHANNELTX_MODLORA_LORAMODGUI_H_ + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/movingaverage.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" + +#include "meshtasticmod.h" +#include "meshtasticmodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSource; + +namespace Ui { + class MeshtasticModGUI; +} + +class MeshtasticModGUI : public ChannelGUI { + Q_OBJECT + +public: + static MeshtasticModGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual void setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; }; + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; }; + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; }; + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; }; + virtual QString getTitle() const { return m_settings.m_title; }; + virtual QColor getTitleColor() const { return m_settings.m_rgbColor; }; + virtual void zetHidden(bool hidden) { m_settings.m_hidden = hidden; } + virtual bool getHidden() const { return m_settings.m_hidden; } + virtual ChannelMarker& getChannelMarker() { return m_channelMarker; } + virtual int getStreamIndex() const { return m_settings.m_streamIndex; } + virtual void setStreamIndex(int streamIndex) { m_settings.m_streamIndex = streamIndex; } + +public slots: + void channelMarkerChangedByCursor(); + +private: + Ui::MeshtasticModGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + MeshtasticModSettings m_settings; + qint64 m_deviceCenterFrequency; + int m_basebandSampleRate; + bool m_doApplySettings; + + MeshtasticMod* m_chirpChatMod; + MovingAverageUtil m_channelPowerDbAvg; + + std::size_t m_tickCount; + MessageQueue m_inputMessageQueue; + + explicit MeshtasticModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent = nullptr); + virtual ~MeshtasticModGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayCurrentPayloadMessage(); + void displayBinaryMessage(); + void setBandwidths(); + QString getActivePayloadText() const; + int findBandwidthIndex(int bandwidthHz) const; + void applyMeshtasticRadioSettingsIfPresent(const QString& payloadText); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + + void leaveEvent(QEvent*); + void enterEvent(EnterEventType*); + +private slots: + void handleSourceMessages(); + void on_deltaFrequency_changed(qint64 value); + void on_bw_valueChanged(int value); + void on_spread_valueChanged(int value); + void on_deBits_valueChanged(int value); + void on_preambleChirps_valueChanged(int value); + void on_idleTime_valueChanged(int value); + void on_syncWord_editingFinished(); + void on_channelMute_toggled(bool checked); + void on_scheme_currentIndexChanged(int index); + void on_fecParity_valueChanged(int value); + void on_crc_stateChanged(int state); + void on_header_stateChanged(int state); + void on_myCall_editingFinished(); + void on_urCall_editingFinished(); + void on_myLocator_editingFinished(); + void on_report_editingFinished(); + void on_msgType_currentIndexChanged(int index); + void on_resetMessages_clicked(bool checked); + void on_playMessage_clicked(bool checked); + void on_repeatMessage_valueChanged(int value); + void on_generateMessages_clicked(bool checked); + void on_messageText_editingFinished(); + void on_hexText_editingFinished(); + void on_udpEnabled_clicked(bool checked); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + void on_invertRamps_stateChanged(int state); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void tick(); +}; + +#endif /* PLUGINS_CHANNELTX_MODLORA_LORAMODGUI_H_ */ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodgui.ui b/plugins/channeltx/modmeshtastic/meshtasticmodgui.ui new file mode 100644 index 000000000..1384d3750 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodgui.ui @@ -0,0 +1,1260 @@ + + + MeshtasticModGUI + + + + 0 + 0 + 429 + 573 + + + + + 0 + 0 + + + + + 392 + 180 + + + + + 560 + 16777215 + + + + + Liberation Sans + 9 + + + + Meshtastic Modulator + + + + + 0 + 0 + 421 + 131 + + + + RF/mod/coder settings + + + + 2 + + + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 60 + 0 + + + + Channel power + + + Qt::RightToLeft + + + -100.0 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Mute/Unmute channel + + + ... + + + + :/txon.png + :/txoff.png:/txon.png + + + true + + + + + + + + + + + BW + + + + + + + Bandwidth + + + 0 + + + 10 + + + 1 + + + 5 + + + Qt::Horizontal + + + + + + + + 80 + 0 + + + + 7813 Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Invert preamble, SFD and payload ramps + + + Inv + + + + + + + + + + + + 22 + 0 + + + + SF + + + + + + + + 90 + 0 + + + + Spreading factor + + + 7 + + + 12 + + + 1 + + + 10 + + + 10 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 10 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 22 + 0 + + + + DE + + + + + + + + 90 + 0 + + + + Low data rate optimize (DE) bits + + + 0 + + + 4 + + + 1 + + + 0 + + + 0 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + 6 + + + + + + 22 + 0 + + + + Pre + + + + + + + + 90 + 0 + + + + Number of preamble chirps + + + 4 + + + 32 + + + 1 + + + 8 + + + 8 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 8 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 22 + 0 + + + + Idle + + + + + + + + 90 + 0 + + + + Idle time between packets (s) + + + 1 + + + 900 + + + 1 + + + 10 + + + 10 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 60.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 140 + 421 + 401 + + + + Payload + + + + 2 + + + + + + + Scheme + + + + + + + + LoRa + + + + + ASCII + + + + + TTY + + + + + FT + + + + + + + + FEC + + + + + + + + 22 + 22 + + + + Number of FEC parity bits (0 to 4) for Hamming code + + + 4 + + + 1 + + + 1 + + + + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Append CRC to payload + + + CRC + + + + + + + Header (explicit) + + + HDR + + + + + + + Sync + + + + + + + + 30 + 16777215 + + + + Qt::ClickFocus + + + Sync word (1 byte hex) + + + HH + + + 00 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + MyCall + + + + + + + Qt::ClickFocus + + + Caller callsign + + + + + + 10 + + + + + + + + 50 + 0 + + + + YourCall + + + + + + + Qt::ClickFocus + + + Callee callsign + + + + + + 10 + + + + + + + + + + + MyLoc + + + + + + + Qt::ClickFocus + + + Caller QRA locator + + + + + + 10 + + + + + + + + 50 + 0 + + + + Report + + + + + + + Qt::ClickFocus + + + Report to callee + + + + + + 10 + + + + + + + + + 6 + + + 6 + + + + + Type + + + + + + + Message type + + + + None + + + + + Beacon + + + + + CQ + + + + + Reply + + + + + Report + + + + + R-Report + + + + + RRR + + + + + 73 + + + + + QSO Text + + + + + Text + + + + + Bytes + + + + + Test + + + + + + + + Restore default message templates + + + + + + + :/recycle.png:/recycle.png + + + + + + + Play message + + + + + + + :/play.png:/play.png + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Repeat + + + + + + + + 22 + 22 + + + + Message repetition (0 for infinite) + + + 20 + + + 1 + + + 1 + + + + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 40 + 16777215 + + + + Generate standard messages + + + Gen + + + + + + + + + + + + + Msg + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + Hex + + + + + + + + + + + + + + Time + + + + + + + Tsym + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Payload time in milliseconds + + + 0000.0 ms + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + ML + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Tpay + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Payload time in milliseconds + + + 00000 ms + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Ttot + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Total transmission time in milliseconds + + + 00000 ms + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Forward messages received via UDP + + + Qt::RightToLeft + + + UDP + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + UDP address to listen for messages to forward on + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + UDP port to listen for messages to forward on + + + 00000 + + + 9997 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + CustomTextEdit + QTextEdit +
gui/customtextedit.h
+
+
+ + + + +
diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodplugin.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodplugin.cpp new file mode 100644 index 000000000..5df2da423 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodplugin.cpp @@ -0,0 +1,96 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2019 Davide Gerhard // +// Copyright (C) 2020 Kacper Michajłow // +// // +// 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 "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "meshtasticmodgui.h" +#endif +#include "meshtasticmod.h" +#include "meshtasticmodwebapiadapter.h" +#include "meshtasticmodplugin.h" + +const PluginDescriptor MeshtasticModPlugin::m_pluginDescriptor = { + MeshtasticMod::m_channelId, + QStringLiteral("Meshtastic Modulator"), + QStringLiteral("7.23.1"), + QStringLiteral("(c) Edouard Griffiths, F4EXB"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +MeshtasticModPlugin::MeshtasticModPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& MeshtasticModPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void MeshtasticModPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register LoRa modulator + m_pluginAPI->registerTxChannel(MeshtasticMod::m_channelIdURI, MeshtasticMod::m_channelId, this); +} + +void MeshtasticModPlugin::createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + MeshtasticMod *instance = new MeshtasticMod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* MeshtasticModPlugin::createTxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSource *txChannel) const +{ + (void) deviceUISet; + (void) txChannel; + return nullptr; +} +#else +ChannelGUI* MeshtasticModPlugin::createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *txChannel) const +{ + return MeshtasticModGUI::create(m_pluginAPI, deviceUISet, txChannel); +} +#endif + +ChannelWebAPIAdapter* MeshtasticModPlugin::createChannelWebAPIAdapter() const +{ + return new MeshtasticModWebAPIAdapter(); +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodplugin.h b/plugins/channeltx/modmeshtastic/meshtasticmodplugin.h new file mode 100644 index 000000000..1ea3f7037 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodplugin.h @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2017, 2019-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2015 John Greb // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICMODPLUGIN_H +#define INCLUDE_MESHTASTICMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSource; + +class MeshtasticModPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channeltx.modmeshtastic") + +public: + explicit MeshtasticModPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_MESHTASTICMODPLUGIN_H diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodsettings.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodsettings.cpp new file mode 100644 index 000000000..cea9b7db6 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodsettings.cpp @@ -0,0 +1,492 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "meshtasticmodsettings.h" + +const int MeshtasticModSettings::bandwidths[] = { + 325, // 384k / 1024 + 488, // 500k / 1024 + 750, // 384k / 512 + 1500, // 384k / 256 + 2604, // 333k / 128 + 3125, // 400k / 128 + 3906, // 500k / 128 + 5208, // 333k / 64 + 6250, // 400k / 64 + 7813, // 500k / 64 + 10417, // 333k / 32 + 12500, // 400k / 32 + 15625, // 500k / 32 + 20833, // 333k / 16 + 25000, // 400k / 16 + 31250, // 500k / 16 + 41667, // 333k / 8 + 50000, // 400k / 8 + 62500, // 500k / 8 + 83333, // 333k / 4 + 100000, // 400k / 4 + 125000, // 500k / 4 + 166667, // 333k / 2 + 200000, // 400k / 2 + 250000, // 500k / 2 + 333333, // 333k / 1 + 400000, // 400k / 1 + 500000 // 500k / 1 +}; +const int MeshtasticModSettings::nbBandwidths = 3*8 + 4; +const int MeshtasticModSettings::oversampling = 4; + +MeshtasticModSettings::MeshtasticModSettings() : + m_inputFrequencyOffset(0), + m_channelMarker(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void MeshtasticModSettings::resetToDefaults() +{ + m_bandwidthIndex = 5; + m_spreadFactor = 7; + m_deBits = 0; + m_preambleChirps = 8; + m_quietMillis = 1000; + m_codingScheme = CodingLoRa; + m_nbParityBits = 1; + m_hasCRC = true; + m_hasHeader = true; + m_textMessage = "Hello LoRa"; + m_myCall = "MYCALL"; + m_urCall = "URCALL"; + m_myLoc = "AA00AA"; + m_myRpt = "59"; + m_syncWord = 0x34; + m_channelMute = false; + m_messageRepeat = 1; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9998; + m_invertRamps = false; + m_rgbColor = QColor(255, 0, 255).rgb(); + m_title = "Meshtastic Modulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + m_workspaceIndex = 0; + m_hidden = false; + + setDefaultTemplates(); +} + +void MeshtasticModSettings::setDefaultTemplates() +{ + // %1: myCall %2: urCall %3: myLoc %4: report + m_beaconMessage = "VVV DE %1 %2"; // Beacon + m_cqMessage = "CQ DE %1 %2"; // caller calls CQ + m_replyMessage = "%1 %2 %3"; // Reply to CQ from caller + m_reportMessage = "%1 %2 %3"; // Report to caller + m_replyReportMessage = "%1 %2 R%3"; // Report to callee + m_rrrMessage = "%1 %2 RRR"; // RRR to callee + m_73Message = "%1 %2 73"; // 73 to caller + m_qsoTextMessage = "%1 %2 %3"; // Freeflow message to caller - %3 is m_textMessage +} + +void MeshtasticModSettings::generateMessages() +{ + m_beaconMessage = m_beaconMessage + .arg(m_myCall).arg(m_myLoc); + m_cqMessage = m_cqMessage + .arg(m_myCall).arg(m_myLoc); + m_replyMessage = m_replyMessage + .arg(m_urCall).arg(m_myCall).arg(m_myLoc); + m_reportMessage = m_reportMessage + .arg(m_urCall).arg(m_myCall).arg(m_myRpt); + m_replyReportMessage = m_replyReportMessage + .arg(m_urCall).arg(m_myCall).arg(m_myRpt); + m_rrrMessage = m_rrrMessage + .arg(m_urCall).arg(m_myCall); + m_73Message = m_73Message + .arg(m_urCall).arg(m_myCall); + m_qsoTextMessage = m_qsoTextMessage + .arg(m_urCall).arg(m_myCall).arg(m_textMessage); +} + +unsigned int MeshtasticModSettings::getNbSFDFourths() const +{ + switch (m_codingScheme) + { + case CodingLoRa: + return 9; + default: + return 8; + } +} + +bool MeshtasticModSettings::hasSyncWord() const +{ + return m_codingScheme == CodingLoRa; +} + +QByteArray MeshtasticModSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_bandwidthIndex); + s.writeS32(3, m_spreadFactor); + s.writeS32(4, m_codingScheme); + + if (m_channelMarker) { + s.writeBlob(5, m_channelMarker->serialize()); + } + + s.writeString(6, m_title); + s.writeS32(7, m_deBits); + s.writeBool(8, m_channelMute); + s.writeU32(9, m_syncWord); + s.writeU32(10, m_preambleChirps); + s.writeS32(11, m_quietMillis); + s.writeBool(12, m_invertRamps); + s.writeString(20, m_beaconMessage); + s.writeString(21, m_cqMessage); + s.writeString(22, m_replyMessage); + s.writeString(23, m_reportMessage); + s.writeString(24, m_replyReportMessage); + s.writeString(25, m_rrrMessage); + s.writeString(26, m_73Message); + s.writeString(27, m_qsoTextMessage); + s.writeString(28, m_textMessage); + s.writeBlob(29, m_bytesMessage); + s.writeS32(30, (int) m_messageType); + s.writeS32(31, m_nbParityBits); + s.writeBool(32, m_hasCRC); + s.writeBool(33, m_hasHeader); + s.writeString(40, m_myCall); + s.writeString(41, m_urCall); + s.writeString(42, m_myLoc); + s.writeString(43, m_myRpt); + s.writeS32(44, m_messageRepeat); + s.writeBool(50, m_useReverseAPI); + s.writeString(51, m_reverseAPIAddress); + s.writeU32(52, m_reverseAPIPort); + s.writeU32(53, m_reverseAPIDeviceIndex); + s.writeU32(54, m_reverseAPIChannelIndex); + s.writeS32(55, m_streamIndex); + s.writeBool(56, m_udpEnabled); + s.writeString(57, m_udpAddress); + s.writeU32(58, m_udpPort); + + if (m_rollupState) { + s.writeBlob(59, m_rollupState->serialize()); + } + + s.writeS32(60, m_workspaceIndex); + s.writeBlob(61, m_geometryBytes); + s.writeBool(62, m_hidden); + + return s.final(); +} + +bool MeshtasticModSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + unsigned int utmp; + int tmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_bandwidthIndex, 0); + d.readS32(3, &m_spreadFactor, 0); + d.readS32(4, &tmp, 0); + m_codingScheme = (CodingScheme) tmp; + + if (m_channelMarker) + { + d.readBlob(5, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readString(6, &m_title, "LoRa Demodulator"); + d.readS32(7, &m_deBits, 0); + d.readBool(8, &m_channelMute, false); + d.readU32(9, &utmp, 0x34); + m_syncWord = utmp > 255 ? 0 : utmp; + d.readU32(10, &m_preambleChirps, 8); + d.readS32(11, &m_quietMillis, 1000); + d.readBool(11, &m_useReverseAPI, false); + d.readBool(12, &m_invertRamps, false); + d.readString(20, &m_beaconMessage, "VVV DE %1 %2"); + d.readString(21, &m_cqMessage, "CQ DE %1 %2"); + d.readString(22, &m_replyMessage, "%2 %1 %3"); + d.readString(23, &m_reportMessage, "%2 %1 %3"); + d.readString(24, &m_replyReportMessage, "%2 %1 R%3"); + d.readString(25, &m_rrrMessage, "%2 %1 RRR"); + d.readString(26, &m_73Message, "%2 %1 73"); + d.readString(27, &m_qsoTextMessage, "%2 %1 Hello LoRa"); + d.readString(28, &m_textMessage, "Hello LoRa"); + d.readBlob(29, &m_bytesMessage); + d.readS32(30, &tmp, 0); + m_messageType = (MessageType) tmp; + d.readS32(31, &m_nbParityBits, 1); + d.readBool(32, &m_hasCRC, true); + d.readBool(33, &m_hasHeader, true); + d.readString(40, &m_myCall, "MYCALL"); + d.readString(41, &m_urCall, "URCALL"); + d.readString(42, &m_myLoc, "AA00AA"); + d.readString(43, &m_myRpt, "59"); + d.readS32(44, &m_messageRepeat, 1); + d.readBool(50, &m_useReverseAPI, false); + d.readString(51, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(52, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(53, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(54, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readS32(55, &m_streamIndex, 0); + + d.readBool(56, &m_udpEnabled); + d.readString(57, &m_udpAddress, "127.0.0.1"); + d.readU32(58, &utmp); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9998; + } + + if (m_rollupState) + { + d.readBlob(59, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(60, &m_workspaceIndex, 0); + d.readBlob(61, &m_geometryBytes); + d.readBool(62, &m_hidden, false); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void MeshtasticModSettings::applySettings(const QStringList& settingsKeys, const MeshtasticModSettings& settings) +{ + if (settingsKeys.contains("inputFrequencyOffset")) + m_inputFrequencyOffset = settings.m_inputFrequencyOffset; + if (settingsKeys.contains("bandwidthIndex")) + m_bandwidthIndex = settings.m_bandwidthIndex; + if (settingsKeys.contains("spreadFactor")) + m_spreadFactor = settings.m_spreadFactor; + if (settingsKeys.contains("deBits")) + m_deBits = settings.m_deBits; + if (settingsKeys.contains("codingScheme")) + m_codingScheme = settings.m_codingScheme; + if (settingsKeys.contains("preambleChirps")) + m_preambleChirps = settings.m_preambleChirps; + if (settingsKeys.contains("quietMillis")) + m_quietMillis = settings.m_quietMillis; + if (settingsKeys.contains("invertRamps")) + m_invertRamps = settings.m_invertRamps; + if (settingsKeys.contains("syncWord")) + m_syncWord = settings.m_syncWord; + if (settingsKeys.contains("channelMute")) + m_channelMute = settings.m_channelMute; + if (settingsKeys.contains("title")) + m_title = settings.m_title; + if (settingsKeys.contains("udpEnabled")) + m_udpEnabled = settings.m_udpEnabled; + if (settingsKeys.contains("udpAddress")) + m_udpAddress = settings.m_udpAddress; + if (settingsKeys.contains("udpPort")) + m_udpPort = settings.m_udpPort; + if (settingsKeys.contains("streamIndex")) + m_streamIndex = settings.m_streamIndex; + if (settingsKeys.contains("useReverseAPI")) + m_useReverseAPI = settings.m_useReverseAPI; + if (settingsKeys.contains("reverseAPIAddress")) + m_reverseAPIAddress = settings.m_reverseAPIAddress; + if (settingsKeys.contains("reverseAPIPort")) + m_reverseAPIPort = settings.m_reverseAPIPort; + if (settingsKeys.contains("reverseAPIDeviceIndex")) + m_reverseAPIDeviceIndex = settings.m_reverseAPIDeviceIndex; + if (settingsKeys.contains("reverseAPIChannelIndex")) + m_reverseAPIChannelIndex = settings.m_reverseAPIChannelIndex; + if (settingsKeys.contains("workspaceIndex")) + m_workspaceIndex = settings.m_workspaceIndex; + if (settingsKeys.contains("geometryBytes")) + m_geometryBytes = settings.m_geometryBytes; + if (settingsKeys.contains("hidden")) + m_hidden = settings.m_hidden; + if (settingsKeys.contains("channelMarker") && m_channelMarker && settings.m_channelMarker) + m_channelMarker->deserialize(settings.m_channelMarker->serialize()); + if (settingsKeys.contains("rollupState") && m_rollupState && settings.m_rollupState) + m_rollupState->deserialize(settings.m_rollupState->serialize()); + if (settingsKeys.contains("beaconMessage")) + m_beaconMessage = settings.m_beaconMessage; + if (settingsKeys.contains("cqMessage")) + m_cqMessage = settings.m_cqMessage; + if (settingsKeys.contains("replyMessage")) + m_replyMessage = settings.m_replyMessage; + if (settingsKeys.contains("reportMessage")) + m_reportMessage = settings.m_reportMessage; + if (settingsKeys.contains("replyReportMessage")) + m_replyReportMessage = settings.m_replyReportMessage; + if (settingsKeys.contains("rrrMessage")) + m_rrrMessage = settings.m_rrrMessage; + if (settingsKeys.contains("73Message")) + m_73Message = settings.m_73Message; + if (settingsKeys.contains("qsoTextMessage")) + m_qsoTextMessage = settings.m_qsoTextMessage; + if (settingsKeys.contains("textMessage")) + m_textMessage = settings.m_textMessage; + if (settingsKeys.contains("bytesMessage")) + m_bytesMessage = settings.m_bytesMessage; + if (settingsKeys.contains("messageType")) + m_messageType = settings.m_messageType; + if (settingsKeys.contains("nbParityBits")) + m_nbParityBits = settings.m_nbParityBits; + if (settingsKeys.contains("hasCRC")) + m_hasCRC = settings.m_hasCRC; + if (settingsKeys.contains("hasHeader")) + m_hasHeader = settings.m_hasHeader; + if (settingsKeys.contains("myCall")) + m_myCall = settings.m_myCall; + if (settingsKeys.contains("urCall")) + m_urCall = settings.m_urCall; + if (settingsKeys.contains("myLoc")) + m_myLoc = settings.m_myLoc; + if (settingsKeys.contains("myRpt")) + m_myRpt = settings.m_myRpt; + if (settingsKeys.contains("messageRepeat")) + m_messageRepeat = settings.m_messageRepeat; +} + +QString MeshtasticModSettings::getDebugString(const QStringList& settingsKeys, bool force) const +{ + QString debug; + if (settingsKeys.contains("inputFrequencyOffset") || force) + debug += QString("Input Frequency Offset: %1\n").arg(m_inputFrequencyOffset); + if (settingsKeys.contains("bandwidthIndex") || force) + debug += QString("Bandwidth Index: %1\n").arg(m_bandwidthIndex); + if (settingsKeys.contains("spreadFactor") || force) + debug += QString("Spread Factor: %1\n").arg(m_spreadFactor); + if (settingsKeys.contains("deBits") || force) + debug += QString("DE Bits: %1\n").arg(m_deBits); + if (settingsKeys.contains("codingScheme") || force) + debug += QString("Coding Scheme: %1\n").arg(m_codingScheme); + if (settingsKeys.contains("preambleChirps") || force) + debug += QString("Preamble Chirps: %1\n").arg(m_preambleChirps); + if (settingsKeys.contains("quietMillis") || force) + debug += QString("Quiet Millis: %1\n").arg(m_quietMillis); + if (settingsKeys.contains("invertRamps") || force) + debug += QString("Invert Ramps: %1\n").arg(m_invertRamps); + if (settingsKeys.contains("syncWord") || force) + debug += QString("Sync Word: %1\n").arg(m_syncWord); + if (settingsKeys.contains("channelMute") || force) + debug += QString("Channel Mute: %1\n").arg(m_channelMute); + if (settingsKeys.contains("title") || force) + debug += QString("Title: %1\n").arg(m_title); + if (settingsKeys.contains("udpEnabled") || force) + debug += QString("UDP Enabled: %1\n").arg(m_udpEnabled); + if (settingsKeys.contains("udpAddress") || force) + debug += QString("UDP Address: %1\n").arg(m_udpAddress); + if (settingsKeys.contains("udpPort") || force) + debug += QString("UDP Port: %1\n").arg(m_udpPort); + if (settingsKeys.contains("streamIndex") || force) + debug += QString("Stream Index: %1\n").arg(m_streamIndex); + if (settingsKeys.contains("useReverseAPI") || force) + debug += QString("Use Reverse API: %1\n").arg(m_useReverseAPI); + if (settingsKeys.contains("reverseAPIAddress") || force) + debug += QString("Reverse API Address: %1\n").arg(m_reverseAPIAddress); + if (settingsKeys.contains("reverseAPIPort") || force) + debug += QString("Reverse API Port: %1\n").arg(m_reverseAPIPort); + if (settingsKeys.contains("reverseAPIDeviceIndex") || force) + debug += QString("Reverse API Device Index: %1\n").arg(m_reverseAPIDeviceIndex); + if (settingsKeys.contains("reverseAPIChannelIndex") || force) + debug += QString("Reverse API Channel Index: %1\n").arg(m_reverseAPIChannelIndex); + if (settingsKeys.contains("workspaceIndex") || force) + debug += QString("Workspace Index: %1\n").arg(m_workspaceIndex); + if (settingsKeys.contains("hidden") || force) + debug += QString("Hidden: %1\n").arg(m_hidden); + if (settingsKeys.contains("beaconMessage") || force) + debug += QString("Beacon Message: %1\n").arg(m_beaconMessage); + if (settingsKeys.contains("cqMessage") || force) + debug += QString("CQ Message: %1\n").arg(m_cqMessage); + if (settingsKeys.contains("replyMessage") || force) + debug += QString("Reply Message: %1\n").arg(m_replyMessage); + if (settingsKeys.contains("reportMessage") || force) + debug += QString("Report Message: %1\n").arg(m_reportMessage); + if (settingsKeys.contains("replyReportMessage") || force) + debug += QString("Reply Report Message: %1\n").arg(m_replyReportMessage); + if (settingsKeys.contains("rrrMessage") || force) + debug += QString("RRR Message: %1\n").arg(m_rrrMessage); + if (settingsKeys.contains("73Message") || force) + debug += QString("73 Message: %1\n").arg(m_73Message); + if (settingsKeys.contains("qsoTextMessage") || force) + debug += QString("QSO Text Message: %1\n").arg(m_qsoTextMessage); + if (settingsKeys.contains("textMessage") || force) + debug += QString("Text Message: %1\n").arg(m_textMessage); + if (settingsKeys.contains("messageType") || force) + debug += QString("Message Type: %1\n").arg(m_messageType); + if (settingsKeys.contains("nbParityBits") || force) + debug += QString("Number of Parity Bits: %1\n").arg(m_nbParityBits); + if (settingsKeys.contains("hasCRC") || force) + debug += QString("Has CRC: %1\n").arg(m_hasCRC); + if (settingsKeys.contains("hasHeader") || force) + debug += QString("Has Header: %1\n").arg(m_hasHeader); + if (settingsKeys.contains("myCall") || force) + debug += QString("My Call: %1\n").arg(m_myCall); + if (settingsKeys.contains("urCall") || force) + debug += QString("UR Call: %1\n").arg(m_urCall); + if (settingsKeys.contains("myLoc") || force) + debug += QString("My Loc: %1\n").arg(m_myLoc); + if (settingsKeys.contains("myRpt") || force) + debug += QString("My Rpt: %1\n").arg(m_myRpt); + if (settingsKeys.contains("messageRepeat") || force) + debug += QString("Message Repeat: %1\n").arg(m_messageRepeat); + return debug; +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodsettings.h b/plugins/channeltx/modmeshtastic/meshtasticmodsettings.h new file mode 100644 index 000000000..0fb154935 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodsettings.h @@ -0,0 +1,123 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODSETTINGS_H_ +#define PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODSETTINGS_H_ + +#include +#include + +#include + +class Serializable; + +struct MeshtasticModSettings +{ + enum CodingScheme + { + CodingLoRa, //!< Standard LoRa + CodingASCII, //!< plain ASCII (7 bits) + CodingTTY, //!< plain TTY (5 bits) + CodingFT //!< FT8/4 scheme (payload 174 bits LDPC) + }; + + enum MessageType + { + MessageNone, + MessageBeacon, + MessageCQ, + MessageReply, + MessageReport, + MessageReplyReport, + MessageRRR, + Message73, + MessageQSOText, + MessageText, + MessageBytes + }; + + int m_inputFrequencyOffset; + int m_bandwidthIndex; + int m_spreadFactor; + int m_deBits; //!< Low data rate optimize (DE) bits + unsigned int m_preambleChirps; //!< Number of preamble chirps + int m_quietMillis; //!< Number of milliseconds to pause between transmissions + int m_nbParityBits; //!< Hamming parity bits (LoRa) + bool m_hasCRC; //!< Payload has CRC (LoRa) + bool m_hasHeader; //!< Header present before actual payload (LoRa) + unsigned char m_syncWord; + bool m_channelMute; + CodingScheme m_codingScheme; + QString m_myCall; //!< QSO mode: my callsign + QString m_urCall; //!< QSO mode: your callsign + QString m_myLoc; //!< QSO mode: my locator + QString m_myRpt; //!< QSO mode: my report + MessageType m_messageType; + QString m_beaconMessage; + QString m_cqMessage; + QString m_replyMessage; + QString m_reportMessage; + QString m_replyReportMessage; + QString m_rrrMessage; + QString m_73Message; + QString m_qsoTextMessage; + QString m_textMessage; + QByteArray m_bytesMessage; + int m_messageRepeat; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + bool m_invertRamps; //!< Invert chirp ramps vs standard LoRa (up/down/up is standard) + uint32_t m_rgbColor; + QString m_title; + int m_streamIndex; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + Serializable *m_channelMarker; + Serializable *m_rollupState; + + static const int bandwidths[]; + static const int nbBandwidths; + static const int oversampling; + + MeshtasticModSettings(); + void resetToDefaults(); + void setDefaultTemplates(); + void generateMessages(); + unsigned int getNbSFDFourths() const; //!< Get the number of SFD period fourths (depends on coding scheme) + bool hasSyncWord() const; //!< Only LoRa has a syncword (for the moment) + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void applySettings(const QStringList& settingsKeys, const MeshtasticModSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; +}; + + + +#endif /* PLUGINS_CHANNELTX_MODMESHTASTIC_MESHTASTICMODSETTINGS_H_ */ diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodsource.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodsource.cpp new file mode 100644 index 000000000..c99fc8aab --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodsource.cpp @@ -0,0 +1,410 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (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 "meshtasticmodsource.h" + +const int MeshtasticModSource::m_levelNbSamples = 480; // every 10ms + +MeshtasticModSource::MeshtasticModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_phaseIncrements(nullptr), + m_repeatCount(0), + m_active(false), + m_modPhasor(0.0f), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f) +{ + m_magsq = 0.0; + + initSF(m_settings.m_spreadFactor); + initTest(m_settings.m_spreadFactor, m_settings.m_deBits); + reset(); + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +MeshtasticModSource::~MeshtasticModSource() +{ + delete[] m_phaseIncrements; +} + +void MeshtasticModSource::initSF(unsigned int sf) +{ + m_fftLength = 1 << sf; + m_state = ChirpChatStateIdle; + m_quarterSamples = (m_fftLength/4)*MeshtasticModSettings::oversampling; + + float halfAngle = M_PI/MeshtasticModSettings::oversampling; + float phase = -halfAngle; + + if (m_phaseIncrements) { + delete[] m_phaseIncrements; + } + + m_phaseIncrements = new double[2*m_fftLength*MeshtasticModSettings::oversampling]; + phase = -halfAngle; + + for (unsigned int i = 0; i < m_fftLength*MeshtasticModSettings::oversampling; i++) + { + m_phaseIncrements[i] = phase; + phase += (2*halfAngle) / (m_fftLength*MeshtasticModSettings::oversampling); + } + + std::copy( + m_phaseIncrements, + m_phaseIncrements+m_fftLength*MeshtasticModSettings::oversampling, + m_phaseIncrements+m_fftLength*MeshtasticModSettings::oversampling + ); +} + +void MeshtasticModSource::initTest(unsigned int sf, unsigned int deBits) +{ + unsigned int fftLength = 1< 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + + if (!(m_state == ChirpChatStateIdle)) + { + double magsq = std::norm(ci); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + } + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void MeshtasticModSource::modulateSample() +{ + if (m_state == ChirpChatStateIdle) + { + m_modSample = Complex{0.0, 0.0}; + m_sampleCounter++; + + if (m_sampleCounter == m_quietSamples*MeshtasticModSettings::oversampling) // done with quiet time + { + m_chirp0 = 0; + m_chirp = m_fftLength*MeshtasticModSettings::oversampling - 1; + + if (m_symbols.size() != 0) // some payload to transmit + { + if (m_settings.m_messageRepeat == 0) // infinite + { + m_state = ChirpChatStatePreamble; + m_active = true; + } + else + { + if (m_repeatCount != 0) + { + m_repeatCount--; + m_state = ChirpChatStatePreamble; + m_active = true; + } + else + { + m_active = false; + } + } + } + else + { + m_active = false; + } + } + } + else if (m_state == ChirpChatStatePreamble) + { + m_modPhasor += (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // preamble chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + + if (m_fftCounter == m_fftLength*MeshtasticModSettings::oversampling) + { + m_chirpCount++; + m_fftCounter = 0; + + if (m_chirpCount == m_settings.m_preambleChirps) + { + m_chirpCount = 0; + + if (m_settings.hasSyncWord()) + { + m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8; + m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1; + m_state = ChirpChatStateSyncWord; + } + else + { + m_sampleCounter = 0; + m_chirp0 = 0; + m_chirp = m_fftLength*MeshtasticModSettings::oversampling - 1; + m_state = ChirpChatStateSFD; + } + } + } + } + else if (m_state == ChirpChatStateSyncWord) + { + m_modPhasor += (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // sync chirps same orientation as preamble + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + + if (m_fftCounter == m_fftLength*MeshtasticModSettings::oversampling) + { + m_chirpCount++; + m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8; + m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1; + m_fftCounter = 0; + + if (m_chirpCount == 2) + { + m_sampleCounter = 0; + m_chirpCount = 0; + m_chirp0 = 0; + m_chirp = m_fftLength*MeshtasticModSettings::oversampling - 1; + m_state = ChirpChatStateSFD; + } + } + } + else if (m_state == ChirpChatStateSFD) + { + m_modPhasor -= (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // SFD chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + m_sampleCounter++; + + if (m_fftCounter == m_fftLength*MeshtasticModSettings::oversampling) + { + m_chirp0 = 0; + m_chirp = m_fftLength*MeshtasticModSettings::oversampling - 1; + m_fftCounter = 0; + } + + if (m_sampleCounter == m_quarterSamples) + { + m_chirpCount++; + m_sampleCounter = 0; + } + + if (m_chirpCount == m_settings.getNbSFDFourths()) + { + m_fftCounter = 0; + m_chirpCount = 0; + m_chirp0 = encodeSymbol(m_symbols[m_chirpCount]); + m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1; + m_state = ChirpChatStatePayload; + } + } + else if (m_state == ChirpChatStatePayload) + { + m_modPhasor += (m_settings.m_invertRamps ? -1 : 1) * m_phaseIncrements[m_chirp]; // payload chirps + m_modSample = Complex(std::polar(0.891235351562 * SDR_TX_SCALED, m_modPhasor)); + m_fftCounter++; + + if (m_fftCounter == m_fftLength*MeshtasticModSettings::oversampling) + { + m_chirpCount++; + + if (m_chirpCount == m_symbols.size()) + { + reset(); + m_state = ChirpChatStateIdle; + } + else + { + m_chirp0 = encodeSymbol(m_symbols[m_chirpCount]); + m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1; + m_fftCounter = 0; + } + } + } + + // limit phasor range to ]-pi,pi] + if (m_modPhasor > M_PI) { + m_modPhasor -= (2.0f * M_PI); + } + + m_chirp++; + + if (m_chirp >= (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling) { + m_chirp = m_chirp0*MeshtasticModSettings::oversampling; + } +} + +unsigned short MeshtasticModSource::encodeSymbol(unsigned short symbol) +{ + if (m_settings.m_deBits == 0) { + return symbol; + } + + unsigned int deWidth = 1<& symbols) +{ + m_symbols = symbols; + qDebug("MeshtasticModSource::setSymbols: m_symbols: %lu", m_symbols.size()); + m_repeatCount = m_settings.m_messageRepeat; + m_state = ChirpChatStateIdle; // first reset to idle + reset(); + m_sampleCounter = m_quietSamples*MeshtasticModSettings::oversampling - 1; // start immediately +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodsource.h b/plugins/channeltx/modmeshtastic/meshtasticmodsource.h new file mode 100644 index 000000000..7905b3b15 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodsource.h @@ -0,0 +1,112 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICMODSOURCE_H +#define INCLUDE_MESHTASTICMODSOURCE_H + +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" + +#include "meshtasticmodsettings.h" + +class MeshtasticModSource : public ChannelSampleSource +{ +public: + MeshtasticModSource(); + virtual ~MeshtasticModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) { (void) nbSamples; } + + double getMagSq() const { return m_magsq; } + void getLevels(qreal& rmsLevel, qreal& peakLevel, int& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevelOut; + numSamples = m_levelNbSamples; + } + void applySettings(const MeshtasticModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int bandwidth, int channelFrequencyOffset, bool force = false); + void setSymbols(const std::vector& symbols); + bool getActive() const { return m_active; } + +private: + enum ChirpChatState + { + ChirpChatStateIdle, //!< Quiet time + ChirpChatStatePreamble, //!< Transmit preamble + ChirpChatStateSyncWord, //!< Transmit sync word + ChirpChatStateSFD, //!< Transmit SFD + ChirpChatStatePayload //!< Tramsmoit payload + }; + + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_bandwidth; + MeshtasticModSettings m_settings; + + ChirpChatState m_state; + double *m_phaseIncrements; + std::vector m_symbols; + unsigned int m_fftLength; //!< chirp length in samples + unsigned int m_chirp; //!< actual chirp index in chirps table + unsigned int m_chirp0; //!< half index of chirp start in chirps table + unsigned int m_sampleCounter; //!< actual sample counter + unsigned int m_fftCounter; //!< chirp sample counter + unsigned int m_chirpCount; //!< chirp or quarter chirp counter + unsigned int m_quietSamples; //!< number of samples during quiet period + unsigned int m_quarterSamples; //!< number of samples in a quarter chirp + unsigned int m_repeatCount; //!< message repetition counter + bool m_active; //!< modulator is in a sending sequence (including periodic quiet times) + + NCO m_carrierNco; + double m_modPhasor; //!< baseband modulator phasor + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + Bandpass m_bandpass; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_levelCalcCount; + qreal m_rmsLevel; + qreal m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + static const int m_levelNbSamples; + + void initSF(unsigned int sf); //!< Init tables, FFTs, depending on spread factor + void initTest(unsigned int sf, unsigned int deBits); + void reset(); + void calculateLevel(Real& sample); + void modulateSample(); + unsigned short encodeSymbol(unsigned short symbol); //!< Encodes symbol with possible DE bits spacing +}; + +#endif // INCLUDE_MESHTASTICMODSOURCE_H diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodwebapiadapter.cpp b/plugins/channeltx/modmeshtastic/meshtasticmodwebapiadapter.cpp new file mode 100644 index 000000000..d8122c853 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-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 "SWGChannelSettings.h" +#include "meshtasticmod.h" +#include "meshtasticmodwebapiadapter.h" + +MeshtasticModWebAPIAdapter::MeshtasticModWebAPIAdapter() +{} + +MeshtasticModWebAPIAdapter::~MeshtasticModWebAPIAdapter() +{} + +int MeshtasticModWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + response.getChirpChatModSettings()->init(); + MeshtasticMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int MeshtasticModWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + MeshtasticMod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + MeshtasticMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} diff --git a/plugins/channeltx/modmeshtastic/meshtasticmodwebapiadapter.h b/plugins/channeltx/modmeshtastic/meshtasticmodwebapiadapter.h new file mode 100644 index 000000000..3924e789a --- /dev/null +++ b/plugins/channeltx/modmeshtastic/meshtasticmodwebapiadapter.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MESHTASTICMOD_WEBAPIADAPTER_H +#define INCLUDE_MESHTASTICMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "meshtasticmodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class MeshtasticModWebAPIAdapter : public ChannelWebAPIAdapter { +public: + MeshtasticModWebAPIAdapter(); + virtual ~MeshtasticModWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + MeshtasticModSettings m_settings; +}; + +#endif // INCLUDE_LORAMOD_WEBAPIADAPTER_H diff --git a/plugins/channeltx/modmeshtastic/readme.md b/plugins/channeltx/modmeshtastic/readme.md new file mode 100644 index 000000000..17caa3db2 --- /dev/null +++ b/plugins/channeltx/modmeshtastic/readme.md @@ -0,0 +1,272 @@ +

Meshtastic modulator plugin

+ +

Introduction

+ +This plugin can be used to code and modulate a transmission signal based on Chirp Spread Spectrum (CSS). The basic idea is to transform each symbol of a MFSK modulation to an ascending frequency ramp shifted in time. It could equally be a descending ramp but this one is reserved to detect a break in the preamble sequence (synchronization). This plugin has been designed to work in conjunction with the ChirpChat demodulator plugin that should be used ideally on the reception side. + +It has clearly been inspired by the LoRa technique but is designed for experimentation and extension to other protocols mostly inspired by amateur radio techniques using chirp modulation to transmit symbols. Thanks to the MFSK to chirp translation it is possible to adapt any MFSK based mode. + +LoRa is a property of Semtech and the details of the protocol are not made public. However a LoRa compatible protocol has been implemented based on the reverse engineering performed by the community. It is mainly based on the work done in https://github.com/myriadrf/LoRa-SDR. You can find more information about LoRa and chirp modulation here: + + - To get an idea of what is LoRa: [here](https://www.link-labs.com/blog/what-is-lora) + - A detailed inspection of LoRa modulation and protocol: [here](https://static1.squarespace.com/static/54cecce7e4b054df1848b5f9/t/57489e6e07eaa0105215dc6c/1464376943218/Reversing-Lora-Knight.pdf) + +This LoRa encoder is designed for experimentation. For production grade applications it is recommended to use dedicated hardware instead. + +Modulation characteristics from LoRa have been augmented with more bandwidths and FFT bin collations (DE factor). Plain TTY and ASCII have also been added that match character value to symbols directly. The FT protocol used in FT8 and FT4 is introduced packing the 174 bits payload into (SF -DE) bits symbols. There are plans to add some more of these typically amateur radio MFSK based modes like JT65. + +

Meshtastic frame mode

+ +In LoRa coding mode, if the message text starts with `MESH:` the plugin will build a full Meshtastic over-the-air frame (16-byte header + protobuf `Data` payload) and encrypt it with AES-CTR when enabled. + +Quick example (Text mode): + +`MESH:from=0x11223344;to=0xffffffff;id=0x1234;channel_name=LongFast;key=default;port=TEXT;text=hello mesh;want_ack=1;hop_limit=3` + +Supported fields: + + - Header: `to`, `from`, `id`, `hop_limit`, `hop_start`, `want_ack`, `via_mqtt`, `channel_hash` (or `channel`), `channel_name`, `next_hop`, `relay_node` + - Preset helper: `preset` / `modem_preset` (`LONG_FAST`, `LONG_SLOW`, `LONG_TURBO`, `LONG_MODERATE`, `MEDIUM_FAST`, `MEDIUM_SLOW`, `SHORT_FAST`, `SHORT_SLOW`, `SHORT_TURBO`) maps to channel naming/hash defaults + - Radio planner: `region`, `channel_num`, `frequency`/`freq`/`freq_hz`, `frequency_offset`/`frequency_offset_hz` + - Data protobuf: `port`/`portnum`, `text`, `payload_hex`, `payload_base64`/`payload_b64`, `want_response`, `dest`, `source`, `request_id`, `reply_id`, `emoji`, `bitfield` + - Crypto: `key`/`psk` (`default`, `none`, `simple0..10`, `hex:<...>`, `base64:<...>`), `encrypt` (`true|false|auto`) + +Notes: + + - If `encrypt=true` then `key` must resolve to 16 or 32 bytes. + - If `channel_hash` is not provided, it is derived from `channel_name` and `key` using Meshtastic hash rules. + - In GUI mode, when the active message is a valid `MESH:` command, the plugin auto-applies LoRa settings (`BW/SF/CR/DE`, `syncword=0x2B`) from `modem_preset`. + - If `region` (+ optional `channel_num`) or explicit `frequency` is present, GUI mode also auto-centers the channel when device center frequency is known. + - If a `MESH:` command is invalid it is rejected (no payload is emitted), and an error is logged. + +

Interface

+ +The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md) + +![Meshtastic Modulator plugin GUI](../../../doc/img/MeshtasticMod_plugin.png) + +

1: Frequency shift from center frequency of reception

+ +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +

2: Channel power

+ +The signal is frequency modulated with a constant envelope hence this value should be constant. To prevent possible overshoots the signal is reduced by 1 dB from the full scale. Thus this should always display `-1 dB`. + +

3: Channel mute

+ +Use this button to mute/unmute transmission. + +

4: Bandwidth

+ +This is the bandwidth of the ChirpChat signal. Similarly to LoRa the signal sweeps between the lower and the upper frequency of this bandwidth. The sample rate of the ChirpChat signal in seconds is exactly one over this bandwidth in Hertz. + +In the LoRa standard there are 2 base bandwidths: 500 and 333.333 kHz. A 400 kHz base has been added. Possible bandwidths are obtained by a division of these base bandwidths by a power of two from 1 to 64. Extra divisor of 128 is provided to achieve smaller bandwidths that can fit in a SSB channel. Finally special divisors from a 384 kHz base are provided to allow even more narrow bandwidths. + +Thus available bandwidths are: + + - **500000** (500000 / 1) Hz + - **400000** (400000 / 1) Hz not in LoRa standard + - **333333** (333333 / 1) Hz + - **250000** (500000 / 2) Hz + - **200000** (400000 / 2) Hz not in LoRa standard + - **166667** (333333 / 2) Hz + - **125000** (500000 / 4) Hz + - **100000** (400000 / 4) Hz not in LoRa standard + - **83333** (333333 / 4) Hz + - **62500** (500000 / 8) Hz + - **50000** (400000 / 8) Hz not in LoRa standard + - **41667** (333333 / 8) Hz + - **31250** (500000 / 16) Hz + - **25000** (400000 / 16) Hz not in LoRa standard + - **20833** (333333 / 16) Hz + - **15625** (500000 / 32) Hz + - **12500** (400000 / 32) Hz not in LoRa standard + - **10417** (333333 / 32) Hz + - **7813** (500000 / 64) Hz + - **6250** (400000 / 64) Hz not in LoRa standard + - **5208** (333333 / 64) Hz + - **3906** (500000 / 128) Hz not in LoRa standard + - **3125** (400000 / 128) Hz not in LoRa standard + - **2604** (333333 / 128) Hz not in LoRa standard + - **1500** (384000 / 256) Hz not in LoRa standard + - **750** (384000 / 512) Hz not in LoRa standard + - **488** (500000 / 1024) Hz not in LoRa standard + - **375** (384000 / 1024) Hz not in LoRa standard + +The ChirpChat signal is oversampled by four therefore it needs a baseband of at least four times the bandwidth. This drives the maximum value on the slider automatically. + +

16: Invert chirp ramps

+ +The LoRa standard is up-chirps for the preamble, down-chirps for the SFD and up-chirps for the payload. + +When you check this option it inverts the direction of the chirps thus becoming down-chirps for the preamble, up-chirps for the SFD and down-chirps for the payload. + +

5: Spread Factor

+ +This is the Spread Factor parameter of the ChirpChat signal. This is the log2 of the possible frequency shifts used over the bandwidth (3). The number of symbols is 2SF-DE where SF is the spread factor and DE the Distance Enhancement factor (6). + +

6: Distance Enhancement factor

+ +The LoRa standard specifies 0 (no DE) or 2 (DE active). The ChirpChat range is extended to all values between 0 and 4 bits. + +The LoRa standard also specifies that the LowDataRateOptimization flag (thus DE=2 vs DE=0 here) should be set when the symbol time defined as BW / 2^SF exceeds 16 ms (See section 4.1.1.6 of the SX127x datasheet). In practice this happens for SF=11 and SF=12 and large enough bandwidths (you can do the maths). + +Here this value is the log2 of the number of frequency shifts separating two consecutive shifts that represent a symbol. On the receiving side this decreases the probability to detect the wrong symbol as an adjacent FFT bin. It can also overcome frequency or sampling time drift on long messages particularly for small bandwidths. + +In practice it is difficult on the Rx side to make correct decodes if only one FFT bin is used to code one symbol (DE=0). It is therefore recommended to use a factor of 1 or more. + +

8: Number of preamble chirps

+ +This is the number of preamble chirps to transmit that are used for the Rx to synchronize. The LoRa standard specifies it can be between 2 and 65535. Here it is limited to the 4 to 20 range that corresponds to realistic values. The RN2483 uses 6 preamble chirps. You may use 12 preamble chirps or more to facilitate signal acquisition with poor SNR on the Rx side. + +

9: Idle time between transmissions

+ +When sending a message repeatedly this is the time between the end of one transmission and the start of the next transmission. + +

10: Message and encoding details

+ +![Meshtastic Modulator plugin GUI](../../../doc/img/MeshtasticMod_payload.png) + +ChirpChat is primarily designed to make QSOs in the amateur radio sense. To be efficient the messages have to be kept short and minimal therefore the standard exchange follows WSJT scheme and is reflected in the sequence of messages you can follow with the message selection combo (10.9): CQ, Reply to CQ, Report to callee, Report to caller (R-Report), RRR and 73. + +To populate messages you can specify your callsign (10.5), the other party callsign (10.6), your QRA locator (10.7) and a signal report (10.8) + +

10.1: Modulation scheme

+ + - **LoRa**: LoRa compatible + - **ASCII**: 7 bit plain ASCII without FEC and CRC. Requires exactly 7 bit effective samples thus SF-DE = 7 where SF is the spreading factor (5) and DE the distance enhancement factor (6) + - **TTY**: 5 bit Baudot (Teletype) without FEC and CRC. Requires exactly 5 bit effective samples thus SF-DE = 5 where SF is the spreading factor (5) and DE the distance enhancement factor (6) + - **FT**: FT8/FT4 coding is applied using data in (10.5) to (10.8) to encode the 174 bit message payload with CRC and FEC as per FT8/FT4 protocol using a type 1 (standard) type of message. Note that the report (10.8) must comply with the FT rule (coded "-35" to "+99" with a leading 0 for the number) and would usually represent the integer part of the S/N ratio in the ChirpChat demodulator receiver. Calls should not be prefixed nor suffixed and the first 4 characters of the locator must represent a valid 4 character grid square. Plain text messages (13 characters) are also supported with the 0.0 type of message using the text entered in the message box (11). These 174 bits are packed into (SF - DE) bits symbols padded with zero bits if necessary. For the details of the FT protocol see: https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf For example for SF=9 and DE=3 we have 6 bits per symbols so the 174 bits are packed in exactly 29 symbols this should appear in the message length ML (13) + +

10.2: Number of FEC parity bits (LoRa)

+ +This is a LoRa specific feature. Each byte of the payload is split into two four bit nibbles and Hamming code of various "strength" in number of parity bits can be applied to these nibbles. The number of parity bits can vary from 1 to 4. 0 (no FEC) has been added but is not part of the LoRa original standard: + + - **0**: no FEC + - **1**: 1 bit parity thus Hamming H(4,5) applies + - **2**: 2 bit parity thus Hamming H(4,6) applies + - **3**: 3 bit parity thus Hamming H(4,7) applies + - **4**: 4 bit parity thus Hamming H(4,8) applies + +

10.3: Append two byte CRC to payload (LoRa)

+ +This is a LoRa specific feature. A 2 bytes CRC can be appended to the payload. + +

10.4: Send a header at the start of the payload (LoRa)

+ +This is a LoRa specific feature and is also known as explicit (with header) or implicit (without header) modes. In explicit mode a header with net payload length in bytes, presence of a CRC and number of parity bits is prepended to the actual payload. This header has a 1 byte CRC and is coded with H(4,8) FEC. + +

10.5: My callsign (QSO mode)

+ +Enter your callsign so it can populate message placeholders (See next) + +

10.6: Your callsign (QSO mode)

+ +Enter the other party callsign so it can populate message placeholders (See next) + +

10.7: My locator (QSO mode)

+ +Enter your Maidenhead QRA locator so it can populate message placeholders (See next) + +

10.8: My report (QSO mode)

+ +Enter the signal report you will send to the other party so it can populate message placeholders (See next) + +

10.9: Message selector

+ +This lets you choose which pre-formatted message to send: + + - **None**: empty message. In fact this is used to make a transition to trigger sending of the same message again. It is used internally by the "play" button (11) and can be used with the REST API. + - **Beacon**: a beacon message + - **CQ**: (QSO mode) CQ general call message + - **Reply**: (QSO mode) reply to a CQ call + - **Report**: (QSO mode) signal report to the callee of a CQ call + - **R-Report**: (QSO mode) signal report to the caller of a CQ call + - **RRR**: (QSO mode) report received confirmation to the callee + - **73**: (QSO mode) confirmation back to the caller and closing the QSO + - **QSO text**: (QSO mode) free form message with callsigns + - **Text**: plain text + - **Bytes**: binary message in the form of a string of bytes. Use the hex window (12) to specify the message + +In FT mode standard FT type messages are generated regardless of placeholders based on MyCall, YourCall, MyLoc, Report and Msg data (entered while in "Text" format). Locators are 4 character grids i.e. only the 4 first characters are taken. Reports must be valid FT reports from -35 to 99 coded as < sign>< zero padded value> (e.g -12, -04, +00, +04, +12) : + + - **Beacon**: DE < MyCall > < MyLoc > + - **CQ**: CQ < MyCall> < MyLoc > + - **Reply**: < YourCall > < MyCall > < MyLoc > + - **Report**: < YourCall > < MyCall > < Report > + - **R-Report**: < YourCall > < MyCall > R< Report > + - **RRR**: < YourCall > < MyCall > RRR + - **73**: < YourCall > < MyCall > 73 + - **QSO text**: < Msg > + - **Text**: < Msg > + - **Bytes**: < Msg > + +

10.10: Revert to standard messages

+ +Reformat all predefined messages in standard messages with placeholders. The Generate button (13) replaces the placeholders with the given QSO elements (10.5 to 10.8) + + - **Beacon**: `VVV DE %1 %2` + - **CQ**: `CQ DE %1 %2` + - **Reply**: `%1 %2 %3` + - **Report**: `%1 %2 %3` + - **R-Reply**: `%1 %2 R%3` + - **RRR**: `%1 %2 RRR` + - **73**: `%1 %2 73` + - **QSO text**: `%1 %2 %3` + +

10.11 Play current message immediately

+ +This starts playing the current selected message immediately. It may be necessary to use this button after a change of modulation and/or coding parameters. + +

10.12: Number of message repetitions

+ +The message is repeated this number of times (use 0 for infinite). The end of one message sequence and the start of the next is separated by the delay specified with the "Idle" slidebar (9) + +

10.13: Generate messages

+ +This applies the QSO elements (10.5 to 10.8) to the placeholders in messages to generate the final messages: + + - **Beacon**: `VVV DE %1 %2`: `%1` is my call (10.5) and `%2` is my locator (10.7) + - **CQ**: `CQ DE %1 %2`: `%1` is my call (10.5) and `%2` is my locator (10.7) + - **Reply**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my locator (10.7) + - **Report**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my report (10.8) + - **R-Reply**: `%1 %2 R%3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my report (10.8) + - **RRR**: `%1 %2 RRR`: `%1` is your call (10.6) and `%2` is my call (10.5) + - **73**: `%1 %2 73`: `%1` is your call (10.6) and `%2` is my call (10.5) + - **QSO text**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is the text specified as the free form text message + +

10.14: Sync word

+ +This is a LoRa specific feature and is the sync word (byte) to transmit entered as a 2 nibble hexadecimal number. + +

11: Message text

+ +This window lets you edit the message selected in (10.9). You can use `%n` placeholders that depend on the type of message selected. + + - **Beacon**: `%1` is my callsign and `%2` is my locator + - **CQ message**: `%1` is my callsign and `%2` is my locator + - **Reply**: `%1` is the other callsign, `%2` is my callsign and `%3` is my locator + - **Report**: `%1` is the other callsign, `%2` is my callsign and `%3` is my report + - **R-Report**: `%1` is the other callsign, `%2` is my callsign and `%3` is my report + - **RRR**: `%1` is the other callsign and `%2` is my callsign + - **73**: `%1` is the other callsign and `%2` is my callsign + - **QSO Text**: `%1` is the other callsign, `%2` is my callsign and `%3` is the free text message + - **Text**: free text message no placeholders + - **Bytes**: binary message no placeholders + +

12: Message bytes

+ +Use this line editor to specify the hex string used as the bytes message. + +

13: Symbol time and message length

+ +This is the duration of a symbol or chirp in milliseconds followed by the message length in the number of symbols + +

14: Payload time

+ +This is the duration of the message payload in milliseconds. It excludes the preamble, the sync word and synchronization (SFD) sequence. + +

15: Total time

+ +This is the duration of the full message in milliseconds. diff --git a/plugins/meshtasticcommon/meshtasticpacket.cpp b/plugins/meshtasticcommon/meshtasticpacket.cpp new file mode 100644 index 000000000..b53ed26e3 --- /dev/null +++ b/plugins/meshtasticcommon/meshtasticpacket.cpp @@ -0,0 +1,2167 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 // +// SPDX-License-Identifier: GPL-3.0-or-later // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshtasticpacket.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Meshtastic +{ +namespace +{ + +constexpr uint8_t kFlagHopLimitMask = 0x07; +constexpr uint8_t kFlagWantAckMask = 0x08; +constexpr uint8_t kFlagViaMqttMask = 0x10; +constexpr uint8_t kFlagHopStartMask = 0xE0; +constexpr int kHeaderLength = 16; +constexpr uint32_t kBroadcastNode = 0xFFFFFFFFu; + +struct Header +{ + uint32_t to = kBroadcastNode; + uint32_t from = 0; + uint32_t id = 0; + uint8_t flags = 0; + uint8_t channel = 0; + uint8_t nextHop = 0; + uint8_t relayNode = 0; +}; + +struct DataFields +{ + bool hasPortnum = false; + uint32_t portnum = 0; + QByteArray payload; + + bool wantResponse = false; + + bool hasDest = false; + uint32_t dest = 0; + + bool hasSource = false; + uint32_t source = 0; + + bool hasRequestId = false; + uint32_t requestId = 0; + + bool hasReplyId = false; + uint32_t replyId = 0; + + bool hasEmoji = false; + uint32_t emoji = 0; + + bool hasBitfield = false; + uint32_t bitfield = 0; +}; + +struct KeyEntry +{ + QString label; + QString channelName; + QByteArray key; + bool hasExpectedHash = false; + uint8_t expectedHash = 0; +}; + +struct CommandConfig +{ + Header header; + DataFields data; + bool encrypt = true; + QByteArray key; + QString keyLabel; + QString channelName; + QString presetName; + bool hasRegion = false; + QString regionName; + bool hasChannelNum = false; + uint32_t channelNum = 0; // 1-based + bool hasOverrideFrequencyMHz = false; + double overrideFrequencyMHz = 0.0; + double frequencyOffsetMHz = 0.0; + bool hasFrequencyOffsetMHz = false; +}; + +struct RegionBand +{ + const char* name; + double freqStartMHz; + double freqEndMHz; + double spacingMHz; + bool wideLora; +}; + +static const uint8_t kDefaultChannelKey[16] = { + 0xD4, 0xF1, 0xBB, 0x3A, 0x20, 0x29, 0x07, 0x59, + 0xF0, 0xBC, 0xFF, 0xAB, 0xCF, 0x4E, 0x69, 0x01 +}; + +// Port numbers from meshtastic/portnums.proto (high value subset is accepted as numeric fallback). +static QMap makePortMap() +{ + QMap m; + m.insert("UNKNOWN_APP", 0); + m.insert("TEXT", 1); + m.insert("TEXT_MESSAGE_APP", 1); + m.insert("REMOTE_HARDWARE_APP", 2); + m.insert("POSITION_APP", 3); + m.insert("NODEINFO_APP", 4); + m.insert("ROUTING_APP", 5); + m.insert("ADMIN_APP", 6); + m.insert("TEXT_MESSAGE_COMPRESSED_APP", 7); + m.insert("WAYPOINT_APP", 8); + m.insert("AUDIO_APP", 9); + m.insert("DETECTION_SENSOR_APP", 10); + m.insert("ALERT_APP", 11); + m.insert("KEY_VERIFICATION_APP", 12); + m.insert("REPLY_APP", 32); + m.insert("IP_TUNNEL_APP", 33); + m.insert("PAXCOUNTER_APP", 34); + m.insert("STORE_FORWARD_PLUSPLUS_APP", 35); + m.insert("NODE_STATUS_APP", 36); + m.insert("SERIAL_APP", 64); + m.insert("STORE_FORWARD_APP", 65); + m.insert("RANGE_TEST_APP", 66); + m.insert("TELEMETRY_APP", 67); + m.insert("ZPS_APP", 68); + m.insert("SIMULATOR_APP", 69); + m.insert("TRACEROUTE_APP", 70); + m.insert("NEIGHBORINFO_APP", 71); + m.insert("ATAK_PLUGIN", 72); + return m; +} + +static const QMap kPortMap = makePortMap(); + +static const RegionBand kRegionBands[] = { + {"US", 902.0, 928.0, 0.0, false}, + {"EU_433", 433.0, 434.0, 0.0, false}, + {"EU_868", 869.4, 869.65, 0.0, false}, + {"CN", 470.0, 510.0, 0.0, false}, + {"JP", 920.5, 923.5, 0.0, false}, + {"ANZ", 915.0, 928.0, 0.0, false}, + {"ANZ_433", 433.05, 434.79, 0.0, false}, + {"RU", 868.7, 869.2, 0.0, false}, + {"KR", 920.0, 923.0, 0.0, false}, + {"TW", 920.0, 925.0, 0.0, false}, + {"IN", 865.0, 867.0, 0.0, false}, + {"NZ_865", 864.0, 868.0, 0.0, false}, + {"TH", 920.0, 925.0, 0.0, false}, + {"UA_433", 433.0, 434.7, 0.0, false}, + {"UA_868", 868.0, 868.6, 0.0, false}, + {"MY_433", 433.0, 435.0, 0.0, false}, + {"MY_919", 919.0, 924.0, 0.0, false}, + {"SG_923", 917.0, 925.0, 0.0, false}, + {"PH_433", 433.0, 434.7, 0.0, false}, + {"PH_868", 868.0, 869.4, 0.0, false}, + {"PH_915", 915.0, 918.0, 0.0, false}, + {"KZ_433", 433.075, 434.775, 0.0, false}, + {"KZ_863", 863.0, 868.0, 0.0, false}, + {"NP_865", 865.0, 868.0, 0.0, false}, + {"BR_902", 902.0, 907.5, 0.0, false}, + {"LORA_24", 2400.0, 2483.5, 0.0, true} +}; + +static const int kRegionBandsCount = static_cast(sizeof(kRegionBands) / sizeof(kRegionBands[0])); + +static QString trimQuotes(const QString& s) +{ + QString out = s.trimmed(); + if ((out.startsWith('"') && out.endsWith('"')) || (out.startsWith('\'') && out.endsWith('\''))) { + out = out.mid(1, out.size() - 2); + } + return out; +} + +static bool parseBool(const QString& s, bool& out) +{ + const QString v = s.trimmed().toLower(); + + if (v == "1" || v == "true" || v == "yes" || v == "on") { + out = true; + return true; + } + + if (v == "0" || v == "false" || v == "no" || v == "off") { + out = false; + return true; + } + + return false; +} + +static bool parseUInt(const QString& s, uint64_t& out) +{ + QString v = s.trimmed(); + v.remove('_'); + + bool ok = false; + int base = 10; + + if (v.startsWith("0x") || v.startsWith("0X")) { + base = 16; + v = v.mid(2); + } + + out = v.toULongLong(&ok, base); + return ok; +} + +static bool parseDouble(const QString& s, double& out) +{ + QString v = s.trimmed(); + v.remove('_'); + bool ok = false; + out = v.toDouble(&ok); + return ok; +} + +static QString normalizeToken(const QString& value) +{ + QString v = value.trimmed().toUpper(); + v.replace('-', '_'); + v.replace(' ', '_'); + return v; +} + +static QByteArray normalizeHex(const QString& s) +{ + QByteArray out; + const QByteArray in = s.toLatin1(); + + for (char c : in) + { + const bool isHex = ((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F')); + if (isHex) { + out.append(c); + } + } + + return out; +} + +static bool parseHexBytes(const QString& s, QByteArray& out) +{ + QByteArray hex = normalizeHex(s); + + if (hex.isEmpty() || (hex.size() % 2) != 0) { + return false; + } + + out = QByteArray::fromHex(hex); + return !out.isEmpty(); +} + +static QByteArray expandSimpleKey(unsigned int simple) +{ + if (simple == 0) { + return QByteArray(); + } + + if (simple > 10) { + return QByteArray(); + } + + QByteArray key(reinterpret_cast(kDefaultChannelKey), sizeof(kDefaultChannelKey)); + + if (simple > 1) { + const int offset = static_cast(simple - 1); + key[15] = static_cast(static_cast(kDefaultChannelKey[15] + offset)); + } + + return key; +} + +static bool parseKeySpec(const QString& rawSpec, QByteArray& key, QString& label) +{ + QString spec = rawSpec.trimmed(); + const QString lower = spec.toLower(); + + if (lower.isEmpty() || lower == "default" || lower == "simple1") { + key = expandSimpleKey(1); + label = "default"; + return true; + } + + if (lower == "none" || lower == "unencrypted" || lower == "simple0" || lower == "0") { + key = QByteArray(); + label = "none"; + return true; + } + + if (lower.startsWith("simple")) + { + uint64_t n = 0; + if (!parseUInt(lower.mid(6), n)) { + return false; + } + + if (n > 10) { + return false; + } + + key = expandSimpleKey(static_cast(n)); + label = QString("simple%1").arg(n); + return true; + } + + if (lower.startsWith("hex:")) + { + if (!parseHexBytes(spec.mid(4), key)) { + return false; + } + + label = "hex"; + return key.size() == 16 || key.size() == 32; + } + + if (lower.startsWith("base64:") || lower.startsWith("b64:")) + { + const int p = spec.indexOf(':'); + key = QByteArray::fromBase64(spec.mid(p + 1).trimmed().toLatin1()); + + if (key.isEmpty()) { + return false; + } + + label = "base64"; + return key.size() == 16 || key.size() == 32; + } + + if (lower == "1" || lower == "2" || lower == "3" || lower == "4" || lower == "5" || lower == "6" || lower == "7" || lower == "8" || lower == "9" || lower == "10") + { + uint64_t n = 0; + parseUInt(lower, n); + key = expandSimpleKey(static_cast(n)); + label = QString("simple%1").arg(n); + return true; + } + + QByteArray parsed; + + if (parseHexBytes(spec, parsed) && (parsed.size() == 16 || parsed.size() == 32)) { + key = parsed; + label = "hex"; + return true; + } + + parsed = QByteArray::fromBase64(spec.toLatin1()); + + if (parsed.size() == 16 || parsed.size() == 32) { + key = parsed; + label = "base64"; + return true; + } + + return false; +} + +static uint32_t readU32LE(const char* p) +{ + return static_cast(static_cast(p[0])) + | (static_cast(static_cast(p[1])) << 8) + | (static_cast(static_cast(p[2])) << 16) + | (static_cast(static_cast(p[3])) << 24); +} + +static void writeU32LE(char* p, uint32_t v) +{ + p[0] = static_cast(v & 0xFF); + p[1] = static_cast((v >> 8) & 0xFF); + p[2] = static_cast((v >> 16) & 0xFF); + p[3] = static_cast((v >> 24) & 0xFF); +} + +static bool parseHeader(const QByteArray& frame, Header& h) +{ + if (frame.size() < kHeaderLength) { + return false; + } + + const char* p = frame.constData(); + h.to = readU32LE(p + 0); + h.from = readU32LE(p + 4); + h.id = readU32LE(p + 8); + h.flags = static_cast(p[12]); + h.channel = static_cast(p[13]); + h.nextHop = static_cast(p[14]); + h.relayNode = static_cast(p[15]); + return true; +} + +static QByteArray encodeHeader(const Header& h) +{ + QByteArray out(kHeaderLength, 0); + char* p = out.data(); + writeU32LE(p + 0, h.to); + writeU32LE(p + 4, h.from); + writeU32LE(p + 8, h.id); + p[12] = static_cast(h.flags); + p[13] = static_cast(h.channel); + p[14] = static_cast(h.nextHop); + p[15] = static_cast(h.relayNode); + return out; +} + +static bool readVarint(const QByteArray& bytes, int& pos, uint64_t& value) +{ + value = 0; + int shift = 0; + + while (pos < bytes.size() && shift <= 63) + { + const uint8_t b = static_cast(bytes[pos++]); + value |= static_cast(b & 0x7F) << shift; + + if ((b & 0x80) == 0) { + return true; + } + + shift += 7; + } + + return false; +} + +static bool readFixed32(const QByteArray& bytes, int& pos, uint32_t& value) +{ + if ((pos + 4) > bytes.size()) { + return false; + } + + value = readU32LE(bytes.constData() + pos); + pos += 4; + return true; +} + +static bool skipField(const QByteArray& bytes, int& pos, uint32_t wireType) +{ + switch (wireType) + { + case 0: { + uint64_t v = 0; + return readVarint(bytes, pos, v); + } + case 1: + if ((pos + 8) > bytes.size()) { + return false; + } + pos += 8; + return true; + case 2: { + uint64_t len = 0; + if (!readVarint(bytes, pos, len)) { + return false; + } + if (len > static_cast(bytes.size() - pos)) { + return false; + } + pos += static_cast(len); + return true; + } + case 5: + if ((pos + 4) > bytes.size()) { + return false; + } + pos += 4; + return true; + default: + return false; + } +} + +static bool parseData(const QByteArray& bytes, DataFields& d) +{ + int pos = 0; + + while (pos < bytes.size()) + { + uint64_t rawTag = 0; + + if (!readVarint(bytes, pos, rawTag)) { + return false; + } + + const uint32_t field = static_cast(rawTag >> 3); + const uint32_t wire = static_cast(rawTag & 0x7); + + switch (field) + { + case 1: { + if (wire != 0) { + return false; + } + + uint64_t v = 0; + if (!readVarint(bytes, pos, v)) { + return false; + } + + d.hasPortnum = true; + d.portnum = static_cast(v); + break; + } + + case 2: { + if (wire != 2) { + return false; + } + + uint64_t len = 0; + if (!readVarint(bytes, pos, len)) { + return false; + } + if (len > static_cast(bytes.size() - pos)) { + return false; + } + + d.payload = bytes.mid(pos, static_cast(len)); + pos += static_cast(len); + break; + } + + case 3: { + if (wire != 0) { + return false; + } + + uint64_t v = 0; + if (!readVarint(bytes, pos, v)) { + return false; + } + + d.wantResponse = (v != 0); + break; + } + + case 4: { + if (wire != 5) { + return false; + } + + uint32_t v = 0; + if (!readFixed32(bytes, pos, v)) { + return false; + } + + d.hasDest = true; + d.dest = v; + break; + } + + case 5: { + if (wire != 5) { + return false; + } + + uint32_t v = 0; + if (!readFixed32(bytes, pos, v)) { + return false; + } + + d.hasSource = true; + d.source = v; + break; + } + + case 6: { + if (wire != 5) { + return false; + } + + uint32_t v = 0; + if (!readFixed32(bytes, pos, v)) { + return false; + } + + d.hasRequestId = true; + d.requestId = v; + break; + } + + case 7: { + if (wire != 5) { + return false; + } + + uint32_t v = 0; + if (!readFixed32(bytes, pos, v)) { + return false; + } + + d.hasReplyId = true; + d.replyId = v; + break; + } + + case 8: { + if (wire != 5) { + return false; + } + + uint32_t v = 0; + if (!readFixed32(bytes, pos, v)) { + return false; + } + + d.hasEmoji = true; + d.emoji = v; + break; + } + + case 9: { + if (wire != 0) { + return false; + } + + uint64_t v = 0; + if (!readVarint(bytes, pos, v)) { + return false; + } + + d.hasBitfield = true; + d.bitfield = static_cast(v); + break; + } + + default: + if (!skipField(bytes, pos, wire)) { + return false; + } + break; + } + } + + return d.hasPortnum; +} + +static void writeVarint(QByteArray& out, uint64_t value) +{ + while (true) + { + uint8_t b = static_cast(value & 0x7F); + value >>= 7; + + if (value != 0) { + b |= 0x80; + out.append(static_cast(b)); + } else { + out.append(static_cast(b)); + break; + } + } +} + +static void writeTag(QByteArray& out, uint32_t field, uint32_t wire) +{ + const uint64_t tag = (static_cast(field) << 3) | static_cast(wire); + writeVarint(out, tag); +} + +static void writeFixed32(QByteArray& out, uint32_t v) +{ + char b[4]; + writeU32LE(b, v); + out.append(b, 4); +} + +static QByteArray encodeData(const DataFields& d) +{ + QByteArray out; + + writeTag(out, 1, 0); + writeVarint(out, d.portnum); + + if (!d.payload.isEmpty()) { + writeTag(out, 2, 2); + writeVarint(out, static_cast(d.payload.size())); + out.append(d.payload); + } + + if (d.wantResponse) { + writeTag(out, 3, 0); + writeVarint(out, 1); + } + + if (d.hasDest) { + writeTag(out, 4, 5); + writeFixed32(out, d.dest); + } + + if (d.hasSource) { + writeTag(out, 5, 5); + writeFixed32(out, d.source); + } + + if (d.hasRequestId) { + writeTag(out, 6, 5); + writeFixed32(out, d.requestId); + } + + if (d.hasReplyId) { + writeTag(out, 7, 5); + writeFixed32(out, d.replyId); + } + + if (d.hasEmoji) { + writeTag(out, 8, 5); + writeFixed32(out, d.emoji); + } + + if (d.hasBitfield) { + writeTag(out, 9, 0); + writeVarint(out, d.bitfield); + } + + return out; +} + +static uint8_t xorHash(const QByteArray& bytes) +{ + uint8_t h = 0; + + for (char c : bytes) { + h ^= static_cast(c); + } + + return h; +} + +static uint8_t generateChannelHash(const QString& channelName, const QByteArray& key) +{ + QByteArray name = channelName.toUtf8(); + + if (name.isEmpty()) { + name = "X"; + } + + return xorHash(name) ^ xorHash(key); +} + +// Tiny AES implementation adapted from tiny-AES-c (public domain / unlicense). +class AesCtx +{ +public: + bool init(const QByteArray& key) + { + if (key.size() != 16 && key.size() != 32) { + return false; + } + + m_nk = key.size() / 4; + m_nr = m_nk + 6; + const int words = 4 * (m_nr + 1); + m_roundKey.resize(words * 4); + keyExpansion(reinterpret_cast(key.constData())); + return true; + } + + void encryptBlock(const uint8_t in[16], uint8_t out[16]) const + { + uint8_t state[16]; + memcpy(state, in, 16); + + addRoundKey(state, 0); + + for (int round = 1; round < m_nr; ++round) + { + subBytes(state); + shiftRows(state); + mixColumns(state); + addRoundKey(state, round); + } + + subBytes(state); + shiftRows(state); + addRoundKey(state, m_nr); + + memcpy(out, state, 16); + } + +private: + int m_nk = 0; + int m_nr = 0; + std::vector m_roundKey; + + static uint8_t xtime(uint8_t x) + { + return static_cast((x << 1) ^ (((x >> 7) & 1) * 0x1B)); + } + + static uint8_t mul(uint8_t a, uint8_t b) + { + uint8_t res = 0; + uint8_t x = a; + uint8_t y = b; + + while (y) + { + if (y & 1) { + res ^= x; + } + + x = xtime(x); + y >>= 1; + } + + return res; + } + + static uint8_t sub(uint8_t x) + { + static const uint8_t sbox[256] = { + 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, + 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, + 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, + 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, + 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, + 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, + 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, + 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, + 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, + 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, + 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, + 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, + 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, + 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, + 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, + 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 + }; + + return sbox[x]; + } + + static void subBytes(uint8_t state[16]) + { + for (int i = 0; i < 16; ++i) { + state[i] = sub(state[i]); + } + } + + static void shiftRows(uint8_t state[16]) + { + uint8_t t; + + t = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = t; + + t = state[2]; + state[2] = state[10]; + state[10] = t; + t = state[6]; + state[6] = state[14]; + state[14] = t; + + t = state[3]; + state[3] = state[15]; + state[15] = state[11]; + state[11] = state[7]; + state[7] = t; + } + + static void mixColumns(uint8_t state[16]) + { + for (int c = 0; c < 4; ++c) + { + uint8_t* col = &state[c * 4]; + const uint8_t a0 = col[0]; + const uint8_t a1 = col[1]; + const uint8_t a2 = col[2]; + const uint8_t a3 = col[3]; + + col[0] = static_cast(mul(a0, 2) ^ mul(a1, 3) ^ a2 ^ a3); + col[1] = static_cast(a0 ^ mul(a1, 2) ^ mul(a2, 3) ^ a3); + col[2] = static_cast(a0 ^ a1 ^ mul(a2, 2) ^ mul(a3, 3)); + col[3] = static_cast(mul(a0, 3) ^ a1 ^ a2 ^ mul(a3, 2)); + } + } + + void addRoundKey(uint8_t state[16], int round) const + { + const uint8_t* rk = &m_roundKey[round * 16]; + for (int i = 0; i < 16; ++i) { + state[i] ^= rk[i]; + } + } + + static uint32_t subWord(uint32_t w) + { + return (static_cast(sub(static_cast((w >> 24) & 0xFF))) << 24) + | (static_cast(sub(static_cast((w >> 16) & 0xFF))) << 16) + | (static_cast(sub(static_cast((w >> 8) & 0xFF))) << 8) + | static_cast(sub(static_cast(w & 0xFF))); + } + + static uint32_t rotWord(uint32_t w) + { + return (w << 8) | (w >> 24); + } + + void keyExpansion(const uint8_t* key) + { + static const uint8_t rcon[11] = {0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36}; + + const int words = 4 * (m_nr + 1); + std::vector w(words, 0); + + for (int i = 0; i < m_nk; ++i) + { + w[i] = (static_cast(key[4 * i]) << 24) + | (static_cast(key[4 * i + 1]) << 16) + | (static_cast(key[4 * i + 2]) << 8) + | static_cast(key[4 * i + 3]); + } + + for (int i = m_nk; i < words; ++i) + { + uint32_t temp = w[i - 1]; + + if ((i % m_nk) == 0) { + temp = subWord(rotWord(temp)) ^ (static_cast(rcon[i / m_nk]) << 24); + } else if (m_nk > 6 && (i % m_nk) == 4) { + temp = subWord(temp); + } + + w[i] = w[i - m_nk] ^ temp; + } + + for (int i = 0; i < words; ++i) + { + m_roundKey[4 * i] = static_cast((w[i] >> 24) & 0xFF); + m_roundKey[4 * i + 1] = static_cast((w[i] >> 16) & 0xFF); + m_roundKey[4 * i + 2] = static_cast((w[i] >> 8) & 0xFF); + m_roundKey[4 * i + 3] = static_cast(w[i] & 0xFF); + } + } +}; + +enum class CounterMode +{ + BigEndian, + LittleEndian +}; + +static void incrementCounter4(uint8_t counter[16], CounterMode mode) +{ + if (mode == CounterMode::BigEndian) + { + for (int i = 15; i >= 12; --i) + { + counter[i] = static_cast(counter[i] + 1); + if (counter[i] != 0) { + break; + } + } + } + else + { + for (int i = 12; i <= 15; ++i) + { + counter[i] = static_cast(counter[i] + 1); + if (counter[i] != 0) { + break; + } + } + } +} + +static void initNonce(uint8_t nonce[16], uint32_t fromNode, uint32_t packetId) +{ + memset(nonce, 0, 16); + + const uint64_t packetId64 = packetId; + memcpy(nonce, &packetId64, sizeof(packetId64)); + memcpy(nonce + sizeof(packetId64), &fromNode, sizeof(fromNode)); +} + +static QByteArray aesCtrCrypt(const QByteArray& in, const QByteArray& key, uint32_t fromNode, uint32_t packetId, CounterMode mode) +{ + QByteArray out(in); + + if (key.isEmpty()) { + return out; + } + + AesCtx aes; + + if (!aes.init(key)) { + return QByteArray(); + } + + uint8_t counter[16]; + initNonce(counter, fromNode, packetId); + + int pos = 0; + while (pos < out.size()) + { + uint8_t keystream[16]; + aes.encryptBlock(counter, keystream); + + const int remain = std::min(16, static_cast(out.size() - pos)); + for (int i = 0; i < remain; ++i) { + out[pos + i] = static_cast(static_cast(out[pos + i]) ^ keystream[i]); + } + + pos += remain; + incrementCounter4(counter, mode); + } + + return out; +} + +static QString formatNode(uint32_t node) +{ + return QString("0x%1").arg(node, 8, 16, QChar('0')); +} + +static QString payloadToText(const QByteArray& payload) +{ + QString s = QString::fromUtf8(payload); + + if (s.isEmpty() && !payload.isEmpty()) { + return QString(); + } + + return s; +} + +static QString portToName(uint32_t p) +{ + for (auto it = kPortMap.constBegin(); it != kPortMap.constEnd(); ++it) + { + if (it.value() == p && it.key().endsWith("_APP")) { + return it.key(); + } + } + + return QString("PORT_%1").arg(p); +} + +static bool addKeyEntry( + std::vector& keys, + const QString& channelName, + const QString& keySpec, + QString* error = nullptr) +{ + QByteArray key; + QString label; + + if (!parseKeySpec(keySpec, key, label)) + { + if (error) { + *error = QString("invalid key spec '%1'").arg(keySpec); + } + return false; + } + + KeyEntry e; + e.key = key; + e.channelName = channelName; + e.label = channelName.isEmpty() ? label : QString("%1:%2").arg(channelName, label); + + if (!channelName.isEmpty()) + { + e.hasExpectedHash = true; + e.expectedHash = generateChannelHash(channelName, key); + } + + keys.push_back(e); + return true; +} + +static bool parseKeyListEntry( + const QString& rawEntry, + QString& channelName, + QString& keySpec, + QString* error = nullptr) +{ + const QString entry = rawEntry.trimmed(); + + if (entry.isEmpty() || entry.startsWith('#')) { + return false; + } + + QByteArray parsedKey; + QString parsedLabel; + + if (parseKeySpec(entry, parsedKey, parsedLabel)) + { + channelName.clear(); + keySpec = entry; + return true; + } + + const int eq = entry.indexOf('='); + + if (eq <= 0) + { + if (error) { + *error = QString("invalid key entry '%1' (use 'channel=key' or key only)").arg(entry); + } + return false; + } + + channelName = trimQuotes(entry.left(eq)); + keySpec = trimQuotes(entry.mid(eq + 1)); + + if (channelName.isEmpty()) + { + if (error) { + *error = QString("missing channel name in '%1'").arg(entry); + } + return false; + } + + if (keySpec.isEmpty()) + { + if (error) { + *error = QString("missing key spec in '%1'").arg(entry); + } + return false; + } + + return true; +} + +static bool parseKeySpecList( + const QString& rawList, + std::vector& keys, + QString* error = nullptr, + int* keyCount = nullptr, + bool strict = true) +{ + QString input = rawList; + input.replace('\r', '\n'); + input.replace(';', '\n'); + input.replace(',', '\n'); + + const QStringList entries = input.split('\n', Qt::SkipEmptyParts); + int parsedCount = 0; + + for (const QString& rawEntry : entries) + { + QString channelName; + QString keySpec; + QString entryError; + + const bool hasEntry = parseKeyListEntry(rawEntry, channelName, keySpec, &entryError); + + if (!hasEntry) + { + if (!entryError.isEmpty() && strict) + { + if (error) { + *error = entryError; + } + return false; + } + + continue; + } + + if (!addKeyEntry(keys, channelName, keySpec, &entryError)) + { + if (strict) + { + if (error) { + *error = QString("%1 in '%2'").arg(entryError, rawEntry.trimmed()); + } + return false; + } + continue; + } + + parsedCount++; + } + + if (keyCount) { + *keyCount = parsedCount; + } + + return true; +} + +static std::vector defaultKeysFromEnv() +{ + std::vector keys; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + + const QString envKeys = env.value("SDRANGEL_MESHTASTIC_KEYS").trimmed(); + + if (!envKeys.isEmpty()) + { + parseKeySpecList(envKeys, keys, nullptr, nullptr, false); + } + else + { + const QString channel = env.value("SDRANGEL_MESHTASTIC_CHANNEL_NAME", "LongFast").trimmed(); + const QString keySpec = env.value("SDRANGEL_MESHTASTIC_KEY", "default").trimmed(); + + addKeyEntry(keys, channel, keySpec); + addKeyEntry(keys, QString(), "none"); + } + + if (keys.empty()) { + addKeyEntry(keys, "LongFast", "default"); + addKeyEntry(keys, QString(), "none"); + } + + return keys; +} + +static bool parsePortValue(const QString& raw, uint32_t& port) +{ + uint64_t numeric = 0; + if (parseUInt(raw, numeric)) { + port = static_cast(numeric); + return true; + } + + const QString upper = raw.trimmed().toUpper(); + + if (kPortMap.contains(upper)) { + port = kPortMap.value(upper); + return true; + } + + if (kPortMap.contains(upper + "_APP")) { + port = kPortMap.value(upper + "_APP"); + return true; + } + + return false; +} + +static bool parsePresetName(const QString& presetValue, QString& presetName) +{ + QString p = normalizeToken(presetValue); + p.remove('_'); + + if (p == "LONGFAST") { presetName = "LONG_FAST"; return true; } + if (p == "LONGSLOW") { presetName = "LONG_SLOW"; return true; } + if (p == "LONGTURBO") { presetName = "LONG_TURBO"; return true; } + if (p == "LONGMODERATE") { presetName = "LONG_MODERATE"; return true; } + if (p == "MEDIUMFAST") { presetName = "MEDIUM_FAST"; return true; } + if (p == "MEDIUMSLOW") { presetName = "MEDIUM_SLOW"; return true; } + if (p == "SHORTFAST") { presetName = "SHORT_FAST"; return true; } + if (p == "SHORTSLOW") { presetName = "SHORT_SLOW"; return true; } + if (p == "SHORTTURBO") { presetName = "SHORT_TURBO"; return true; } + return false; +} + +static bool presetToChannelName(const QString& presetName, QString& channelName) +{ + if (presetName == "LONG_FAST") { channelName = "LongFast"; return true; } + if (presetName == "LONG_SLOW") { channelName = "LongSlow"; return true; } + if (presetName == "LONG_TURBO") { channelName = "LongTurbo"; return true; } + if (presetName == "LONG_MODERATE") { channelName = "LongModerate"; return true; } + if (presetName == "MEDIUM_FAST") { channelName = "MediumFast"; return true; } + if (presetName == "MEDIUM_SLOW") { channelName = "MediumSlow"; return true; } + if (presetName == "SHORT_FAST") { channelName = "ShortFast"; return true; } + if (presetName == "SHORT_SLOW") { channelName = "ShortSlow"; return true; } + if (presetName == "SHORT_TURBO") { channelName = "ShortTurbo"; return true; } + return false; +} + +static bool presetToParams(const QString& presetName, bool wideLora, int& bandwidthHz, int& spreadFactor, int& parityBits) +{ + int bwKHz = 0; + int sf = 0; + int cr = 0; + + if (presetName == "SHORT_TURBO") { bwKHz = wideLora ? 1625 : 500; cr = 5; sf = 7; } + else if (presetName == "SHORT_FAST") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 7; } + else if (presetName == "SHORT_SLOW") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 8; } + else if (presetName == "MEDIUM_FAST") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 9; } + else if (presetName == "MEDIUM_SLOW") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 10; } + else if (presetName == "LONG_TURBO") { bwKHz = wideLora ? 1625 : 500; cr = 8; sf = 11; } + else if (presetName == "LONG_MODERATE") { bwKHz = wideLora ? 406 : 125; cr = 8; sf = 11; } + else if (presetName == "LONG_SLOW") { bwKHz = wideLora ? 406 : 125; cr = 8; sf = 12; } + else if (presetName == "LONG_FAST") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 11; } + else { + return false; + } + + bandwidthHz = bwKHz * 1000; + spreadFactor = sf; + parityBits = std::max(1, std::min(4, cr - 4)); + return true; +} + +static QString presetToDisplayName(const QString& presetName) +{ + QString channelName; + if (presetToChannelName(presetName, channelName)) { + return channelName; + } + return QString("LongFast"); +} + +static uint32_t meshHashDjb2(const QString& s) +{ + const QByteArray bytes = s.toUtf8(); + uint32_t h = 5381u; + for (char c : bytes) { + h = ((h << 5) + h) + static_cast(c); + } + return h; +} + +static const RegionBand* findRegionBand(const QString& regionValue) +{ + QString r = normalizeToken(regionValue); + r.remove('_'); + + if (r == "EU868") { r = "EU_868"; } + else if (r == "EU433") { r = "EU_433"; } + else if (r == "NZ865") { r = "NZ_865"; } + else if (r == "UA868") { r = "UA_868"; } + else if (r == "UA433") { r = "UA_433"; } + else if (r == "MY433") { r = "MY_433"; } + else if (r == "MY919") { r = "MY_919"; } + else if (r == "SG923") { r = "SG_923"; } + else if (r == "PH433") { r = "PH_433"; } + else if (r == "PH868") { r = "PH_868"; } + else if (r == "PH915") { r = "PH_915"; } + else if (r == "KZ433") { r = "KZ_433"; } + else if (r == "KZ863") { r = "KZ_863"; } + else if (r == "NP865") { r = "NP_865"; } + else if (r == "BR902") { r = "BR_902"; } + else if (r == "ANZ433") { r = "ANZ_433"; } + else if (r == "LORA24") { r = "LORA_24"; } + else { r = normalizeToken(regionValue); } + + for (int i = 0; i < kRegionBandsCount; ++i) { + if (r == kRegionBands[i].name) { + return &kRegionBands[i]; + } + } + + return nullptr; +} + +static bool parseCommand(const QString& command, CommandConfig& cfg, QString& error) +{ + if (!Packet::isCommand(command)) { + error = "command must start with MESH:"; + return false; + } + + cfg.header.to = kBroadcastNode; + cfg.header.from = 0; + cfg.header.id = QRandomGenerator::global()->generate(); + cfg.header.channel = 0; + cfg.header.nextHop = 0; + cfg.header.relayNode = 0; + cfg.data.hasPortnum = true; + cfg.data.portnum = 1; // TEXT_MESSAGE_APP + cfg.data.payload.clear(); + cfg.encrypt = true; + cfg.key = expandSimpleKey(1); + cfg.keyLabel = "default"; + cfg.channelName = "LongFast"; + cfg.presetName = "LONG_FAST"; + bool encryptAuto = true; + + uint8_t hopLimit = 3; + uint8_t hopStart = 3; + bool wantAck = false; + bool viaMqtt = false; + + QString body = command.mid(5).trimmed(); + QStringList parts = body.split(';', Qt::SkipEmptyParts); + QStringList freeText; + + for (QString part : parts) + { + part = part.trimmed(); + if (part.isEmpty()) { + continue; + } + + const int sep = part.indexOf('='); + if (sep <= 0) + { + freeText.push_back(trimQuotes(part)); + continue; + } + + const QString key = part.left(sep).trimmed().toLower(); + const QString value = trimQuotes(part.mid(sep + 1)); + + uint64_t u = 0; + + if (key == "to") + { + if (!parseUInt(value, u)) { + error = "invalid to"; + return false; + } + cfg.header.to = static_cast(u); + } + else if (key == "from") + { + if (!parseUInt(value, u)) { + error = "invalid from"; + return false; + } + cfg.header.from = static_cast(u); + } + else if (key == "id") + { + if (!parseUInt(value, u)) { + error = "invalid id"; + return false; + } + cfg.header.id = static_cast(u); + } + else if (key == "port" || key == "portnum") + { + uint32_t p = 0; + if (!parsePortValue(value, p)) { + error = "invalid port/portnum"; + return false; + } + cfg.data.portnum = p; + cfg.data.hasPortnum = true; + } + else if (key == "text") + { + cfg.data.payload = value.toUtf8(); + } + else if (key == "payload_hex") + { + QByteArray p; + if (!parseHexBytes(value, p)) { + error = "invalid payload_hex"; + return false; + } + cfg.data.payload = p; + } + else if (key == "payload_b64" || key == "payload_base64") + { + QByteArray p = QByteArray::fromBase64(value.toLatin1()); + if (p.isEmpty() && !value.isEmpty()) { + error = "invalid payload_base64"; + return false; + } + cfg.data.payload = p; + } + else if (key == "want_response") + { + bool b = false; + if (!parseBool(value, b)) { + error = "invalid want_response"; + return false; + } + cfg.data.wantResponse = b; + } + else if (key == "dest") + { + if (!parseUInt(value, u)) { + error = "invalid dest"; + return false; + } + cfg.data.hasDest = true; + cfg.data.dest = static_cast(u); + } + else if (key == "source") + { + if (!parseUInt(value, u)) { + error = "invalid source"; + return false; + } + cfg.data.hasSource = true; + cfg.data.source = static_cast(u); + } + else if (key == "request_id") + { + if (!parseUInt(value, u)) { + error = "invalid request_id"; + return false; + } + cfg.data.hasRequestId = true; + cfg.data.requestId = static_cast(u); + } + else if (key == "reply_id") + { + if (!parseUInt(value, u)) { + error = "invalid reply_id"; + return false; + } + cfg.data.hasReplyId = true; + cfg.data.replyId = static_cast(u); + } + else if (key == "emoji") + { + if (!parseUInt(value, u)) { + error = "invalid emoji"; + return false; + } + cfg.data.hasEmoji = true; + cfg.data.emoji = static_cast(u); + } + else if (key == "bitfield") + { + if (!parseUInt(value, u)) { + error = "invalid bitfield"; + return false; + } + cfg.data.hasBitfield = true; + cfg.data.bitfield = static_cast(u); + } + else if (key == "hop_limit") + { + if (!parseUInt(value, u) || u > 7) { + error = "invalid hop_limit"; + return false; + } + hopLimit = static_cast(u); + } + else if (key == "hop_start") + { + if (!parseUInt(value, u) || u > 7) { + error = "invalid hop_start"; + return false; + } + hopStart = static_cast(u); + } + else if (key == "want_ack") + { + if (!parseBool(value, wantAck)) { + error = "invalid want_ack"; + return false; + } + } + else if (key == "via_mqtt") + { + if (!parseBool(value, viaMqtt)) { + error = "invalid via_mqtt"; + return false; + } + } + else if (key == "next_hop") + { + if (!parseUInt(value, u) || u > 255) { + error = "invalid next_hop"; + return false; + } + cfg.header.nextHop = static_cast(u); + } + else if (key == "relay_node") + { + if (!parseUInt(value, u) || u > 255) { + error = "invalid relay_node"; + return false; + } + cfg.header.relayNode = static_cast(u); + } + else if (key == "channel_hash" || key == "channel") + { + if (!parseUInt(value, u) || u > 255) { + error = "invalid channel_hash"; + return false; + } + cfg.header.channel = static_cast(u); + cfg.channelName.clear(); + } + else if (key == "channel_name") + { + cfg.channelName = value; + } + else if (key == "preset" || key == "modem_preset") + { + QString presetName; + if (!parsePresetName(value, presetName)) { + error = "invalid preset/modem_preset"; + return false; + } + + cfg.presetName = presetName; + presetToChannelName(cfg.presetName, cfg.channelName); + } + else if (key == "region" || key == "region_code") + { + const RegionBand* band = findRegionBand(value); + if (!band) { + error = "invalid region"; + return false; + } + + cfg.hasRegion = true; + cfg.regionName = band->name; + } + else if (key == "channel_num" || key == "slot") + { + if (!parseUInt(value, u) || u < 1 || u > 10000) { + error = "invalid channel_num"; + return false; + } + cfg.hasChannelNum = true; + cfg.channelNum = static_cast(u); + } + else if (key == "frequency" || key == "freq" || key == "override_frequency" || key == "frequency_mhz" || key == "freq_mhz") + { + double f = 0.0; + if (!parseDouble(value, f) || f <= 0.0) { + error = "invalid frequency"; + return false; + } + + if (f > 1000000.0) { + cfg.overrideFrequencyMHz = f / 1000000.0; + } else { + cfg.overrideFrequencyMHz = f; + } + + cfg.hasOverrideFrequencyMHz = true; + } + else if (key == "freq_hz" || key == "frequency_hz") + { + if (!parseUInt(value, u) || u < 1000000ull) { + error = "invalid frequency_hz"; + return false; + } + cfg.overrideFrequencyMHz = static_cast(u) / 1000000.0; + cfg.hasOverrideFrequencyMHz = true; + } + else if (key == "frequency_offset" || key == "freq_offset") + { + double off = 0.0; + if (!parseDouble(value, off)) { + error = "invalid frequency_offset"; + return false; + } + + // Meshtastic uses MHz for frequency offset. We also accept Hz-like values. + if (std::fabs(off) > 100000.0) { + off /= 1000000.0; + } + + cfg.frequencyOffsetMHz = off; + cfg.hasFrequencyOffsetMHz = true; + } + else if (key == "frequency_offset_hz" || key == "freq_offset_hz") + { + double offHz = 0.0; + if (!parseDouble(value, offHz)) { + error = "invalid frequency_offset_hz"; + return false; + } + cfg.frequencyOffsetMHz = offHz / 1000000.0; + cfg.hasFrequencyOffsetMHz = true; + } + else if (key == "key" || key == "psk") + { + QByteArray parsedKey; + QString keyLabel; + if (!parseKeySpec(value, parsedKey, keyLabel)) { + error = "invalid key/psk"; + return false; + } + cfg.key = parsedKey; + cfg.keyLabel = keyLabel; + + if (encryptAuto) { + cfg.encrypt = !cfg.key.isEmpty(); + } + } + else if (key == "encrypt") + { + const QString lower = value.toLower(); + if (lower == "auto") { + encryptAuto = true; + cfg.encrypt = !cfg.key.isEmpty(); + } + else + { + bool b = false; + if (!parseBool(value, b)) { + error = "invalid encrypt"; + return false; + } + encryptAuto = false; + cfg.encrypt = b; + } + } + else + { + error = QString("unknown key '%1'").arg(key); + return false; + } + } + + if (cfg.data.payload.isEmpty() && !freeText.isEmpty()) { + cfg.data.payload = freeText.join(';').toUtf8(); + } + + if (cfg.header.channel == 0 && !cfg.channelName.isEmpty()) { + cfg.header.channel = generateChannelHash(cfg.channelName, cfg.key); + } + + if (encryptAuto) { + cfg.encrypt = !cfg.key.isEmpty(); + } + + if (cfg.encrypt && cfg.key.isEmpty()) { + error = "encrypt=true but key resolves to none"; + return false; + } + + cfg.header.flags = static_cast(hopLimit & 0x07); + if (wantAck) { + cfg.header.flags |= kFlagWantAckMask; + } + if (viaMqtt) { + cfg.header.flags |= kFlagViaMqttMask; + } + cfg.header.flags |= static_cast((hopStart & 0x07) << 5); + + return true; +} + +static QString summarizeHeader(const Header& h) +{ + const int hopLimit = h.flags & kFlagHopLimitMask; + const int hopStart = (h.flags & kFlagHopStartMask) >> 5; + const bool wantAck = (h.flags & kFlagWantAckMask) != 0; + const bool viaMqtt = (h.flags & kFlagViaMqttMask) != 0; + + return QString("to=%1 from=%2 id=0x%3 ch=0x%4 hop=%5/%6 ack=%7 mqtt=%8 next=%9 relay=%10") + .arg(formatNode(h.to)) + .arg(formatNode(h.from)) + .arg(h.id, 8, 16, QChar('0')) + .arg(h.channel, 2, 16, QChar('0')) + .arg(hopLimit) + .arg(hopStart) + .arg(wantAck ? 1 : 0) + .arg(viaMqtt ? 1 : 0) + .arg(h.nextHop) + .arg(h.relayNode); +} + +static QString summarizeData(const DataFields& d) +{ + const QString portName = portToName(d.portnum); + QString s = QString("port=%1(%2)").arg(portName).arg(d.portnum); + + if (d.wantResponse) { + s += " want_response=1"; + } + + if (d.hasDest) { + s += QString(" dest=%1").arg(formatNode(d.dest)); + } + + if (d.hasSource) { + s += QString(" source=%1").arg(formatNode(d.source)); + } + + if (d.hasRequestId) { + s += QString(" request_id=0x%1").arg(d.requestId, 8, 16, QChar('0')); + } + + if (d.hasReplyId) { + s += QString(" reply_id=0x%1").arg(d.replyId, 8, 16, QChar('0')); + } + + if (d.hasEmoji) { + s += QString(" emoji=%1").arg(d.emoji); + } + + if (d.hasBitfield) { + s += QString(" bitfield=0x%1").arg(d.bitfield, 0, 16); + } + + const QString text = payloadToText(d.payload); + + if (!text.isEmpty()) { + s += QString(" text=\"%1\"").arg(text); + } else if (!d.payload.isEmpty()) { + const int n = std::min(32, static_cast(d.payload.size())); + s += QString(" payload_hex=%1").arg(QString(d.payload.left(n).toHex())); + if (d.payload.size() > n) { + s += "..."; + } + } else { + s += " payload="; + } + + return s; +} + +static void addDecodeField(DecodeResult& result, const QString& path, const QString& value) +{ + DecodeResult::Field f; + f.path = path; + f.value = value; + result.fields.append(f); +} + +static void addDecodeField(DecodeResult& result, const QString& path, uint32_t value) +{ + addDecodeField(result, path, QString::number(value)); +} + +static void addDecodeField(DecodeResult& result, const QString& path, bool value) +{ + addDecodeField(result, path, QString(value ? "true" : "false")); +} + +static void appendHeaderDecodeFields(const Header& h, DecodeResult& result) +{ + const int hopLimit = h.flags & kFlagHopLimitMask; + const int hopStart = (h.flags & kFlagHopStartMask) >> 5; + const bool wantAck = (h.flags & kFlagWantAckMask) != 0; + const bool viaMqtt = (h.flags & kFlagViaMqttMask) != 0; + + addDecodeField(result, "header.to", formatNode(h.to)); + addDecodeField(result, "header.from", formatNode(h.from)); + addDecodeField(result, "header.id", QString("0x%1").arg(h.id, 8, 16, QChar('0'))); + addDecodeField(result, "header.channel_hash", QString("0x%1").arg(h.channel, 2, 16, QChar('0'))); + addDecodeField(result, "header.hop_limit", QString::number(hopLimit)); + addDecodeField(result, "header.hop_start", QString::number(hopStart)); + addDecodeField(result, "header.want_ack", wantAck); + addDecodeField(result, "header.via_mqtt", viaMqtt); + addDecodeField(result, "header.next_hop", QString::number(h.nextHop)); + addDecodeField(result, "header.relay_node", QString::number(h.relayNode)); +} + +static void appendDataDecodeFields(const DataFields& d, DecodeResult& result) +{ + addDecodeField(result, "data.port_name", portToName(d.portnum)); + addDecodeField(result, "data.portnum", d.portnum); + addDecodeField(result, "data.want_response", d.wantResponse); + + if (d.hasDest) { + addDecodeField(result, "data.dest", formatNode(d.dest)); + } + if (d.hasSource) { + addDecodeField(result, "data.source", formatNode(d.source)); + } + if (d.hasRequestId) { + addDecodeField(result, "data.request_id", QString("0x%1").arg(d.requestId, 8, 16, QChar('0'))); + } + if (d.hasReplyId) { + addDecodeField(result, "data.reply_id", QString("0x%1").arg(d.replyId, 8, 16, QChar('0'))); + } + if (d.hasEmoji) { + addDecodeField(result, "data.emoji", QString::number(d.emoji)); + } + if (d.hasBitfield) { + addDecodeField(result, "data.bitfield", QString("0x%1").arg(d.bitfield, 0, 16)); + } + + addDecodeField(result, "data.payload_len", QString::number(d.payload.size())); + + const QString text = payloadToText(d.payload); + if (!text.isEmpty()) { + addDecodeField(result, "data.text", text); + } else if (!d.payload.isEmpty()) { + addDecodeField(result, "data.payload_hex", QString(d.payload.toHex())); + } +} + +static bool deriveTxRadioSettingsFromConfig(const CommandConfig& cfg, TxRadioSettings& settings, QString& error) +{ + settings = TxRadioSettings(); + settings.hasCommand = true; + settings.syncWord = 0x00; + + QString presetName = cfg.presetName; + if (presetName.isEmpty()) { + presetName = "LONG_FAST"; + } + + bool wideLora = false; + const RegionBand* region = nullptr; + if (cfg.hasRegion) { + region = findRegionBand(cfg.regionName); + if (!region) { + error = "invalid region"; + return false; + } + wideLora = region->wideLora; + } + + int bandwidthHz = 0; + int spreadFactor = 0; + int parityBits = 0; + if (!presetToParams(presetName, wideLora, bandwidthHz, spreadFactor, parityBits)) { + error = "invalid preset"; + return false; + } + + if (wideLora) { + error = "LORA_24 wide LoRa presets are not supported by ChirpChat"; + return false; + } + + settings.hasLoRaParams = true; + settings.bandwidthHz = bandwidthHz; + settings.spreadFactor = spreadFactor; + settings.parityBits = parityBits; + settings.preambleChirps = wideLora ? 12 : 17; + + const double symbolTimeSec = static_cast(1u << spreadFactor) / static_cast(bandwidthHz); + settings.deBits = (symbolTimeSec > 0.016) ? 2 : 0; // match classic LoRa low data rate optimization rule + + if (cfg.hasOverrideFrequencyMHz) + { + const double freqMHz = cfg.overrideFrequencyMHz + (cfg.hasFrequencyOffsetMHz ? cfg.frequencyOffsetMHz : 0.0); + settings.hasCenterFrequency = true; + settings.centerFrequencyHz = static_cast(std::llround(freqMHz * 1000000.0)); + } + else if (region) + { + const double bwMHz = static_cast(bandwidthHz) / 1000000.0; + const double slotWidthMHz = region->spacingMHz + bwMHz; + const double spanMHz = region->freqEndMHz - region->freqStartMHz; + const uint32_t numChannels = static_cast(std::floor(spanMHz / slotWidthMHz)); + + if (numChannels == 0) { + error = "region span too narrow for selected preset bandwidth"; + return false; + } + + uint32_t channelIndex = 0; + if (cfg.hasChannelNum) + { + if (cfg.channelNum < 1 || cfg.channelNum > numChannels) { + error = QString("channel_num out of range 1..%1").arg(numChannels); + return false; + } + channelIndex = cfg.channelNum - 1; + } + else + { + const QString displayName = presetToDisplayName(presetName); + channelIndex = meshHashDjb2(displayName) % numChannels; + } + + const double centerMHz = region->freqStartMHz + (bwMHz / 2.0) + (channelIndex * slotWidthMHz) + + (cfg.hasFrequencyOffsetMHz ? cfg.frequencyOffsetMHz : 0.0); + + settings.hasCenterFrequency = true; + settings.centerFrequencyHz = static_cast(std::llround(centerMHz * 1000000.0)); + } + + settings.summary = QString("preset=%1 sf=%2 cr=4/%3 bw=%4kHz de=%5") + .arg(presetName) + .arg(settings.spreadFactor) + .arg(settings.parityBits + 4) + .arg(settings.bandwidthHz / 1000) + .arg(settings.deBits); + settings.summary += QString(" preamble=%1").arg(settings.preambleChirps); + + if (region) { + settings.summary += QString(" region=%1").arg(region->name); + } + if (cfg.hasChannelNum) { + settings.summary += QString(" channel_num=%1").arg(cfg.channelNum); + } + if (settings.hasCenterFrequency) { + settings.summary += QString(" freq=%1MHz").arg(settings.centerFrequencyHz / 1000000.0, 0, 'f', 6); + } + + return true; +} + +} // namespace + +bool Packet::isCommand(const QString& text) +{ + return text.trimmed().startsWith("MESH:", Qt::CaseInsensitive); +} + +bool Packet::buildFrameFromCommand(const QString& command, QByteArray& frame, QString& summary, QString& error) +{ + CommandConfig cfg; + + if (!parseCommand(command, cfg, error)) { + return false; + } + + QByteArray payload = encodeData(cfg.data); + + if (cfg.encrypt) { + payload = aesCtrCrypt(payload, cfg.key, cfg.header.from, cfg.header.id, CounterMode::BigEndian); + if (payload.isEmpty()) { + error = "failed to encrypt payload"; + return false; + } + } + + frame = encodeHeader(cfg.header); + frame.append(payload); + + summary = QString("MESH TX|%1 key=%2 encrypt=%3 %4") + .arg(summarizeHeader(cfg.header)) + .arg(cfg.keyLabel) + .arg(cfg.encrypt ? 1 : 0) + .arg(summarizeData(cfg.data)); + + return true; +} + +bool Packet::decodeFrame(const QByteArray& frame, DecodeResult& result) +{ + return decodeFrame(frame, result, QString()); +} + +bool Packet::decodeFrame(const QByteArray& frame, DecodeResult& result, const QString& keySpecList) +{ + result = DecodeResult(); + + Header h; + if (!parseHeader(frame, h)) { + return false; + } + + result.isFrame = true; + appendHeaderDecodeFields(h, result); + + const QByteArray encryptedPayload = frame.mid(kHeaderLength); + addDecodeField(result, "decode.payload_encrypted_len", QString::number(encryptedPayload.size())); + + // 1) Plain decode attempt first (unencrypted / key-none packets) + DataFields data; + if (parseData(encryptedPayload, data)) + { + result.dataDecoded = true; + result.decrypted = false; + result.keyLabel = "none"; + result.summary = QString("MESH RX|%1 key=none %2") + .arg(summarizeHeader(h)) + .arg(summarizeData(data)); + addDecodeField(result, "decode.path", "plain"); + addDecodeField(result, "decode.key_label", result.keyLabel); + addDecodeField(result, "decode.decrypted", result.decrypted); + appendDataDecodeFields(data, result); + return true; + } + + // 2) Try configured keys (hash-matched keys first) + std::vector keys; + + if (!keySpecList.trimmed().isEmpty()) + { + QString error; + int keyCount = 0; + + if (!parseKeySpecList(keySpecList, keys, &error, &keyCount, true)) + { + qWarning() << "Meshtastic::Packet::decodeFrame: invalid keySpecList:" << error; + keys = defaultKeysFromEnv(); + } + else if (keyCount == 0) + { + keys = defaultKeysFromEnv(); + } + } + else + { + keys = defaultKeysFromEnv(); + } + + std::stable_sort(keys.begin(), keys.end(), [h](const KeyEntry& a, const KeyEntry& b) { + const int as = (a.hasExpectedHash && a.expectedHash == h.channel) ? 1 : 0; + const int bs = (b.hasExpectedHash && b.expectedHash == h.channel) ? 1 : 0; + return as > bs; + }); + + std::set tested; + + for (const KeyEntry& k : keys) + { + if (k.key.isEmpty()) { + continue; + } + + const QString fingerprint = QString("%1:%2").arg(QString(k.key.toHex()), k.label); + + if (tested.find(fingerprint) != tested.end()) { + continue; + } + tested.insert(fingerprint); + + const QByteArray plainBe = aesCtrCrypt(encryptedPayload, k.key, h.from, h.id, CounterMode::BigEndian); + if (!plainBe.isEmpty() && parseData(plainBe, data)) + { + result.dataDecoded = true; + result.decrypted = true; + result.keyLabel = k.label; + result.summary = QString("MESH RX|%1 key=%2 ctr=be %3") + .arg(summarizeHeader(h)) + .arg(k.label) + .arg(summarizeData(data)); + addDecodeField(result, "decode.path", "aes_ctr_be"); + addDecodeField(result, "decode.key_label", result.keyLabel); + addDecodeField(result, "decode.decrypted", result.decrypted); + appendDataDecodeFields(data, result); + return true; + } + + // Keep CTR mode strict to match Meshtastic reference decode path. + } + + result.summary = QString("MESH RX|%1 undecoded payload_len=%2") + .arg(summarizeHeader(h)) + .arg(encryptedPayload.size()); + addDecodeField(result, "decode.path", "undecoded"); + addDecodeField(result, "decode.key_label", "none"); + addDecodeField(result, "decode.decrypted", false); + + if (!encryptedPayload.isEmpty()) { + addDecodeField(result, "decode.payload_encrypted_hex", QString(encryptedPayload.toHex())); + } + + return true; +} + +bool Packet::validateKeySpecList(const QString& keySpecList, QString& error, int* keyCount) +{ + std::vector keys; + if (!parseKeySpecList(keySpecList, keys, &error, keyCount, true)) { + return false; + } + + if (keyCount && *keyCount == 0) + { + error = "no keys found"; + return false; + } + + return true; +} + +bool Packet::deriveTxRadioSettings(const QString& command, TxRadioSettings& settings, QString& error) +{ + CommandConfig cfg; + + if (!parseCommand(command, cfg, error)) { + return false; + } + + return deriveTxRadioSettingsFromConfig(cfg, settings, error); +} + +} // namespace Meshtastic diff --git a/plugins/meshtasticcommon/meshtasticpacket.h b/plugins/meshtasticcommon/meshtasticpacket.h new file mode 100644 index 000000000..178725de3 --- /dev/null +++ b/plugins/meshtasticcommon/meshtasticpacket.h @@ -0,0 +1,90 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 // +// SPDX-License-Identifier: GPL-3.0-or-later // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHIRPCHATCOMMON_MESHTASTICPACKET_H_ +#define PLUGINS_CHIRPCHATCOMMON_MESHTASTICPACKET_H_ + +#include +#include +#include +#include + +#include + +namespace Meshtastic +{ + +struct DecodeResult +{ + struct Field + { + QString path; + QString value; + }; + + bool isFrame = false; + bool dataDecoded = false; + bool decrypted = false; + QString keyLabel; + QString summary; + QVector fields; +}; + +struct TxRadioSettings +{ + bool hasCommand = false; + bool hasLoRaParams = false; + int bandwidthHz = 0; + int spreadFactor = 0; + int parityBits = 0; // 1..4 maps to CR 4/5 .. 4/8 + int deBits = 0; + uint8_t syncWord = 0x00; // Meshtastic_SDR/gr-lora_sdr reference flow uses [0,0] + int preambleChirps = 17; + + bool hasCenterFrequency = false; + qint64 centerFrequencyHz = 0; + + QString summary; +}; + +class Packet +{ +public: + static bool isCommand(const QString& text); + + static bool buildFrameFromCommand( + const QString& command, + QByteArray& frame, + QString& summary, + QString& error + ); + + static bool decodeFrame( + const QByteArray& frame, + DecodeResult& result + ); + + static bool decodeFrame( + const QByteArray& frame, + DecodeResult& result, + const QString& keySpecList + ); + + static bool validateKeySpecList( + const QString& keySpecList, + QString& error, + int* keyCount = nullptr + ); + + static bool deriveTxRadioSettings( + const QString& command, + TxRadioSettings& settings, + QString& error + ); +}; + +} // namespace Meshtastic + +#endif // PLUGINS_CHIRPCHATCOMMON_MESHTASTICPACKET_H_ diff --git a/plugins/samplesource/fileinput/fileinput.cpp b/plugins/samplesource/fileinput/fileinput.cpp index 647dd0689..aad6f5210 100644 --- a/plugins/samplesource/fileinput/fileinput.cpp +++ b/plugins/samplesource/fileinput/fileinput.cpp @@ -292,15 +292,15 @@ bool FileInput::start() QMutexLocker mutexLocker(&m_mutex); qDebug() << "FileInput::start"; -#ifdef ANDROID - m_inputFile.seek(0); -#else - if (m_ifstream.tellg() != (std::streampos)0) - { - m_ifstream.clear(); - m_ifstream.seekg(sizeof(FileRecord::Header), std::ios::beg); - } -#endif + #ifdef ANDROID + m_inputFile.seek(0); + #else + if (m_ifstream.tellg() != (std::streampos)0) + { + m_ifstream.clear(); + m_ifstream.seekg(m_dataStartPos, std::ios::beg); + } + #endif if (!m_sampleFifo.setSize(m_settings.m_accelerationFactor * m_sampleRate * sizeof(Sample))) { diff --git a/plugins/samplesource/remotetcpinput/remotetcpinput.cpp b/plugins/samplesource/remotetcpinput/remotetcpinput.cpp index c3db99062..4585b42ed 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinput.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinput.cpp @@ -228,7 +228,7 @@ bool RemoteTCPInput::handleMessage(const Message& message) { qDebug() << "RemoteTCPInput::handleMessage:" << message.getIdentifier(); const RemoteTCPInputTCPHandler::MsgReportConnection& report = (const RemoteTCPInputTCPHandler::MsgReportConnection&) message; - if (report.getConnected()) + if (!report.getConnected()) { qDebug() << "Disconnected - stopping DSP"; m_deviceAPI->stopDeviceEngine(); @@ -444,6 +444,9 @@ void RemoteTCPInput::webapiUpdateDeviceSettings( if (deviceSettingsKeys.contains("agc")) { settings.m_agc = response.getRemoteTcpInputSettings()->getAgc() != 0; } + if (deviceSettingsKeys.contains("gain")) { + settings.m_gain[0] = response.getRemoteTcpInputSettings()->getGain(); + } if (deviceSettingsKeys.contains("rfBW")) { settings.m_rfBW = response.getRemoteTcpInputSettings()->getRfBw(); } diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp index 823dd268b..1bc853ec6 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp @@ -341,10 +341,12 @@ void RemoteTCPInputGui::displayEnabled() bool enableSquelchEnable; bool enableSquelch; bool sdra; + bool spyServer; if (state == DeviceAPI::StRunning) { sdra = m_sdra; + spyServer = m_spyServer; remoteControl = m_remoteControl; enableMessages = !m_iqOnly; enableSquelchEnable = !m_iqOnly; @@ -353,6 +355,7 @@ void RemoteTCPInputGui::displayEnabled() else { sdra = m_settings.m_protocol == "SDRangel"; + spyServer = m_settings.m_protocol == "Spy Server"; remoteControl = m_settings.m_overrideRemoteSettings; enableMessages = false; enableSquelchEnable = m_settings.m_overrideRemoteSettings; @@ -371,18 +374,18 @@ void RemoteTCPInputGui::displayEnabled() ui->channelSampleRateLabel->setEnabled(m_settings.m_channelDecimation && sdra && remoteControl); ui->channelSampleRateUnit->setEnabled(m_settings.m_channelDecimation && sdra && remoteControl); - ui->devSampleRateLabel->setEnabled(!m_spyServer && remoteControl); - ui->devSampleRate->setEnabled(!m_spyServer && remoteControl); - ui->devSampleRateUnits->setEnabled(!m_spyServer && remoteControl); - ui->agc->setEnabled(!m_spyServer && remoteControl); - ui->rfBWLabel->setEnabled(!m_spyServer && remoteControl); - ui->rfBW->setEnabled(!m_spyServer && remoteControl); - ui->rfBWUnits->setEnabled(!m_spyServer && remoteControl); - ui->dcOffset->setEnabled(!m_spyServer && remoteControl); - ui->iqImbalance->setEnabled(!m_spyServer && remoteControl); - ui->ppm->setEnabled(!m_spyServer && remoteControl); - ui->ppmLabel->setEnabled(!m_spyServer && remoteControl); - ui->ppmText->setEnabled(!m_spyServer && remoteControl); + ui->devSampleRateLabel->setEnabled(!spyServer && remoteControl); + ui->devSampleRate->setEnabled(!spyServer && remoteControl); + ui->devSampleRateUnits->setEnabled(!spyServer && remoteControl); + ui->agc->setEnabled(!spyServer && remoteControl); + ui->rfBWLabel->setEnabled(!spyServer && remoteControl); + ui->rfBW->setEnabled(!spyServer && remoteControl); + ui->rfBWUnits->setEnabled(!spyServer && remoteControl); + ui->dcOffset->setEnabled(!spyServer && remoteControl); + ui->iqImbalance->setEnabled(!spyServer && remoteControl); + ui->ppm->setEnabled(!spyServer && remoteControl); + ui->ppmLabel->setEnabled(!spyServer && remoteControl); + ui->ppmText->setEnabled(!spyServer && remoteControl); ui->centerFrequency->setEnabled(remoteControl); ui->biasTee->setEnabled(remoteControl); diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index ec34ea163..b6754f555 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -43,6 +43,15 @@ #include "feature/featureset.h" #include "feature/feature.h" +namespace { +QString defaultReverseAPIScheme() +{ + // Keep reverse API compatibility with legacy host-only addresses. + static const char kHttpScheme[] = {'h', 't', 't', 'p', '\0'}; + return QString::fromLatin1(kHttpScheme); +} +} + bool ChannelWebAPIUtils::getDeviceSettings(unsigned int deviceIndex, SWGSDRangel::SWGDeviceSettings &deviceSettingsResponse, DeviceSet *&deviceSet) { QString errorResponse; @@ -1170,6 +1179,73 @@ bool ChannelWebAPIUtils::getDeviceSetting(unsigned int deviceIndex, const QStrin } } +bool ChannelWebAPIUtils::getDeviceSetting(unsigned int deviceIndex, const QString &setting, QString &value) +{ + SWGSDRangel::SWGDeviceSettings deviceSettingsResponse; + DeviceSet *deviceSet; + + if (getDeviceSettings(deviceIndex, deviceSettingsResponse, deviceSet)) + { + QJsonObject *jsonObj = deviceSettingsResponse.asJsonObject(); + bool result = WebAPIUtils::getSubObjectString(*jsonObj, setting, value); + delete jsonObj; + return result; + } + else + { + return false; + } +} + +QUrl ChannelWebAPIUtils::buildChannelSettingsURL( + const QString& reverseAPIAddress, + unsigned int reverseAPIPort, + unsigned int reverseAPIDeviceIndex, + unsigned int reverseAPIChannelIndex) +{ + const QString address = reverseAPIAddress.trimmed(); + QUrl parsedAddress = QUrl::fromUserInput(address); + + QString scheme = defaultReverseAPIScheme(); + QString host = address; + + if (parsedAddress.isValid() && !parsedAddress.host().isEmpty()) + { + host = parsedAddress.host(); + + if (!parsedAddress.scheme().isEmpty()) { + scheme = parsedAddress.scheme(); + } + + if ((parsedAddress.port() > 0) && (parsedAddress.port() <= 65535)) { + reverseAPIPort = static_cast(parsedAddress.port()); + } + } + else + { + const QUrl authorityOnly = QUrl::fromUserInput(QStringLiteral("//%1").arg(address)); + if (authorityOnly.isValid() && !authorityOnly.host().isEmpty()) + { + host = authorityOnly.host(); + + if ((authorityOnly.port() > 0) && (authorityOnly.port() <= 65535)) { + reverseAPIPort = static_cast(authorityOnly.port()); + } + } + } + + QUrl channelSettingsURL; + channelSettingsURL.setScheme(scheme); + channelSettingsURL.setHost(host); + channelSettingsURL.setPort(static_cast(reverseAPIPort)); + channelSettingsURL.setPath( + QString("/sdrangel/deviceset/%1/channel/%2/settings") + .arg(reverseAPIDeviceIndex) + .arg(reverseAPIChannelIndex)); + + return channelSettingsURL; +} + bool ChannelWebAPIUtils::getDeviceReportValue(unsigned int deviceIndex, const QString &key, QString &value) { SWGSDRangel::SWGDeviceReport deviceReport; diff --git a/sdrbase/channel/channelwebapiutils.h b/sdrbase/channel/channelwebapiutils.h index a13c9b24d..60d73a897 100644 --- a/sdrbase/channel/channelwebapiutils.h +++ b/sdrbase/channel/channelwebapiutils.h @@ -24,6 +24,7 @@ #include #include #include +#include #include "SWGDeviceSettings.h" #include "SWGDeviceReport.h" @@ -97,6 +98,12 @@ public: static bool satelliteAOS(const QString name, bool northToSouthPass, const QString &tle, QDateTime dateTime); static bool satelliteLOS(const QString name); static bool getDeviceSetting(unsigned int deviceIndex, const QString &setting, int &value); + static bool getDeviceSetting(unsigned int deviceIndex, const QString &setting, QString &value); + static QUrl buildChannelSettingsURL( + const QString& reverseAPIAddress, + unsigned int reverseAPIPort, + unsigned int reverseAPIDeviceIndex, + unsigned int reverseAPIChannelIndex); static bool getDeviceReportValue(unsigned int deviceIndex, const QString &key, QString &value); static bool getDeviceReportList(unsigned int deviceIndex, const QString &key, const QString &subKey, QList &values); static bool getDevicePosition(unsigned int deviceIndex, float& latitude, float& longitude, float& altitude); @@ -138,4 +145,3 @@ protected: }; #endif // SDRBASE_CHANNEL_CHANNELWEBAPIUTILS_H_ - diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 920828454..6b5dc791f 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -37,6 +37,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channel.chanalyzerng", "ChannelAnalyzerSettings"}, // remap {"org.f4exb.sdrangelove.channel.chanalyzer", "ChannelAnalyzerSettings"}, // remap {"sdrangel.channel.chirpchatdemod", "ChirpChatDemodSettings"}, + {"sdrangel.channel.meshtasticdemod", "ChirpChatDemodSettings"}, // alias: Meshtastic uses ChirpChatDemodSettings schema {"sdrangel.channel.modchirpchat", "ChirpChatModSettings"}, {"sdrangel.channel.demodatv", "ATVDemodSettings"}, {"sdrangel.channel.demoddatv", "DATVDemodSettings"}, @@ -162,6 +163,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"BFMDemod", "BFMDemodSettings"}, {"ChannelAnalyzer", "ChannelAnalyzerSettings"}, {"ChirpChatDemod", "ChirpChatDemodSettings"}, + {"MeshtasticDemod", "ChirpChatDemodSettings"}, // alias: Meshtastic uses ChirpChatDemodSettings schema {"ChirpChatMod", "ChirpChatModSettings"}, {"ChannelPower", "ChannelPowerSettings"}, {"DATVDemod", "DATVDemodSettings"},