From 92552f6b4f892303c1927aa1593300b3af1b65ca Mon Sep 17 00:00:00 2001 From: Tom Hensel Date: Tue, 9 Jun 2026 23:08:56 +0200 Subject: [PATCH] meshcore: add MeshCore protocol RX/TX channel plugins --- modemmeshcore/CMakeLists.txt | 54 + modemmeshcore/meshcore_builders.cpp | 330 ++ modemmeshcore/meshcore_builders.h | 178 + modemmeshcore/meshcore_command.cpp | 546 +++ modemmeshcore/meshcore_command.h | 73 + modemmeshcore/meshcore_crypto.cpp | 280 ++ modemmeshcore/meshcore_crypto.h | 81 + modemmeshcore/meshcore_decoder.cpp | 489 +++ modemmeshcore/meshcore_decoder.h | 113 + modemmeshcore/meshcore_identity.cpp | 144 + modemmeshcore/meshcore_identity.h | 83 + modemmeshcore/meshcorepacket.cpp | 165 + modemmeshcore/meshcorepacket.h | 193 + modemmeshcore/monocypher-ed25519.c | 500 +++ modemmeshcore/monocypher-ed25519.h | 140 + modemmeshcore/monocypher.c | 2957 +++++++++++++++ modemmeshcore/monocypher.h | 321 ++ modemmeshcore/test/cli.cpp | 60 + modemmeshcore/test/smoke.cpp | 564 +++ modemmeshcore/tiny_aes.h | 319 ++ .../channelrx/demodmeshcore/CMakeLists.txt | 83 + .../channelrx/demodmeshcore/meshcoredemod.cpp | 1727 +++++++++ .../channelrx/demodmeshcore/meshcoredemod.h | 255 ++ .../demodmeshcore/meshcoredemodbaseband.cpp | 190 + .../demodmeshcore/meshcoredemodbaseband.h | 89 + .../demodmeshcore/meshcoredemoddecoder.cpp | 397 ++ .../demodmeshcore/meshcoredemoddecoder.h | 96 + .../meshcoredemoddecoderlora.cpp | 682 ++++ .../demodmeshcore/meshcoredemoddecoderlora.h | 483 +++ .../demodmeshcore/meshcoredemodgui.cpp | 3372 +++++++++++++++++ .../demodmeshcore/meshcoredemodgui.h | 252 ++ .../demodmeshcore/meshcoredemodgui.ui | 1518 ++++++++ .../demodmeshcore/meshcoredemodmsg.cpp | 27 + .../demodmeshcore/meshcoredemodmsg.h | 545 +++ .../demodmeshcore/meshcoredemodsettings.cpp | 388 ++ .../demodmeshcore/meshcoredemodsettings.h | 108 + .../demodmeshcore/meshcoredemodsink.cpp | 1332 +++++++ .../demodmeshcore/meshcoredemodsink.h | 202 + .../meshcoredemodwebapiadapter.cpp | 52 + .../meshcoredemodwebapiadapter.h | 49 + .../demodmeshcore/meshcorekeysdialog.cpp | 87 + .../demodmeshcore/meshcorekeysdialog.h | 47 + .../demodmeshcore/meshcorekeysdialog.ui | 115 + .../demodmeshcore/meshcoreplugin.cpp | 88 + .../channelrx/demodmeshcore/meshcoreplugin.h | 50 + plugins/channelrx/demodmeshcore/readme.md | 56 + plugins/channeltx/modmeshcore/CMakeLists.txt | 78 + plugins/channeltx/modmeshcore/meshcoremod.cpp | 889 +++++ plugins/channeltx/modmeshcore/meshcoremod.h | 228 ++ .../modmeshcore/meshcoremodbaseband.cpp | 204 + .../modmeshcore/meshcoremodbaseband.h | 120 + .../modmeshcore/meshcoremodencoder.cpp | 255 ++ .../modmeshcore/meshcoremodencoder.h | 52 + .../modmeshcore/meshcoremodencoderlora.cpp | 225 ++ .../modmeshcore/meshcoremodencoderlora.h | 301 ++ .../channeltx/modmeshcore/meshcoremodgui.cpp | 1280 +++++++ .../channeltx/modmeshcore/meshcoremodgui.h | 161 + .../channeltx/modmeshcore/meshcoremodgui.ui | 1225 ++++++ .../modmeshcore/meshcoremodplugin.cpp | 96 + .../channeltx/modmeshcore/meshcoremodplugin.h | 51 + .../modmeshcore/meshcoremodsettings.cpp | 481 +++ .../modmeshcore/meshcoremodsettings.h | 118 + .../modmeshcore/meshcoremodsource.cpp | 429 +++ .../channeltx/modmeshcore/meshcoremodsource.h | 114 + .../modmeshcore/meshcoremodwebapiadapter.cpp | 52 + .../modmeshcore/meshcoremodwebapiadapter.h | 49 + plugins/channeltx/modmeshcore/readme.md | 77 + .../code/qt5/client/SWGMeshcoreModActions.cpp | 107 + .../code/qt5/client/SWGMeshcoreModActions.h | 57 + 69 files changed, 26529 insertions(+) create mode 100644 modemmeshcore/CMakeLists.txt create mode 100644 modemmeshcore/meshcore_builders.cpp create mode 100644 modemmeshcore/meshcore_builders.h create mode 100644 modemmeshcore/meshcore_command.cpp create mode 100644 modemmeshcore/meshcore_command.h create mode 100644 modemmeshcore/meshcore_crypto.cpp create mode 100644 modemmeshcore/meshcore_crypto.h create mode 100644 modemmeshcore/meshcore_decoder.cpp create mode 100644 modemmeshcore/meshcore_decoder.h create mode 100644 modemmeshcore/meshcore_identity.cpp create mode 100644 modemmeshcore/meshcore_identity.h create mode 100644 modemmeshcore/meshcorepacket.cpp create mode 100644 modemmeshcore/meshcorepacket.h create mode 100644 modemmeshcore/monocypher-ed25519.c create mode 100644 modemmeshcore/monocypher-ed25519.h create mode 100644 modemmeshcore/monocypher.c create mode 100644 modemmeshcore/monocypher.h create mode 100644 modemmeshcore/test/cli.cpp create mode 100644 modemmeshcore/test/smoke.cpp create mode 100644 modemmeshcore/tiny_aes.h create mode 100644 plugins/channelrx/demodmeshcore/CMakeLists.txt create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemod.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemod.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodbaseband.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodbaseband.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemoddecoder.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemoddecoder.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodgui.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodgui.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodgui.ui create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodmsg.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodmsg.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodsettings.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodsettings.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodsink.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodsink.h create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.h create mode 100644 plugins/channelrx/demodmeshcore/meshcorekeysdialog.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcorekeysdialog.h create mode 100644 plugins/channelrx/demodmeshcore/meshcorekeysdialog.ui create mode 100644 plugins/channelrx/demodmeshcore/meshcoreplugin.cpp create mode 100644 plugins/channelrx/demodmeshcore/meshcoreplugin.h create mode 100644 plugins/channelrx/demodmeshcore/readme.md create mode 100644 plugins/channeltx/modmeshcore/CMakeLists.txt create mode 100644 plugins/channeltx/modmeshcore/meshcoremod.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremod.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodbaseband.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodbaseband.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodencoder.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodencoder.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodencoderlora.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodencoderlora.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodgui.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodgui.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodgui.ui create mode 100644 plugins/channeltx/modmeshcore/meshcoremodplugin.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodplugin.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodsettings.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodsettings.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodsource.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodsource.h create mode 100644 plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.cpp create mode 100644 plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.h create mode 100644 plugins/channeltx/modmeshcore/readme.md create mode 100644 swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.h diff --git a/modemmeshcore/CMakeLists.txt b/modemmeshcore/CMakeLists.txt new file mode 100644 index 000000000..3864e9d10 --- /dev/null +++ b/modemmeshcore/CMakeLists.txt @@ -0,0 +1,54 @@ +project (modemmeshcore) + +set(modemmeshcore_SOURCES + meshcorepacket.cpp + meshcore_crypto.cpp + meshcore_builders.cpp + meshcore_command.cpp + meshcore_decoder.cpp + meshcore_identity.cpp + monocypher.c + monocypher-ed25519.c +) + +set(modemmeshcore_HEADERS + meshcorepacket.h + meshcore_crypto.h + meshcore_builders.h + meshcore_command.h + meshcore_decoder.h + meshcore_identity.h + monocypher.h + monocypher-ed25519.h + tiny_aes.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/exports +) + +add_library(modemmeshcore + ${modemmeshcore_SOURCES} +) + +# Vendored Monocypher (BSD-2-Clause OR CC0-1.0) is intentionally untouched; +# silence sdrangel's strict-warning policy for the two .c files. +set_source_files_properties(monocypher.c monocypher-ed25519.c + PROPERTIES COMPILE_FLAGS "-Wno-unused-function -Wno-unused-parameter" +) + +target_link_libraries(modemmeshcore + Qt::Core +) + +install(TARGETS modemmeshcore DESTINATION ${INSTALL_LIB_DIR}) + +# Smoke test executable. Not installed; run from build dir to validate the +# crypto + builder round-trip after edits to the lib. +add_executable(modemmeshcore_smoke EXCLUDE_FROM_ALL test/smoke.cpp) +target_link_libraries(modemmeshcore_smoke PRIVATE modemmeshcore Qt::Core) + +# CLI tool: prints wire packet hex for a MESHCORE: command line. Used for +# golden-vector parity tests against the gr4-lora python meshcore_tx.py. +add_executable(modmeshcore_cli EXCLUDE_FROM_ALL test/cli.cpp) +target_link_libraries(modmeshcore_cli PRIVATE modemmeshcore Qt::Core) diff --git a/modemmeshcore/meshcore_builders.cpp b/modemmeshcore/meshcore_builders.cpp new file mode 100644 index 000000000..f0bd14417 --- /dev/null +++ b/modemmeshcore/meshcore_builders.cpp @@ -0,0 +1,330 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore wire-packet builder implementations. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcore_builders.h" + +#include "meshcorepacket.h" +#include "meshcore_crypto.h" + +#include +#include + +#include +#include + +namespace modemmeshcore +{ +namespace builders +{ + +namespace +{ + +// little-endian uint32 -> 4 bytes +QByteArray u32LE(uint32_t v) +{ + QByteArray out(4, '\0'); + qToLittleEndian(v, reinterpret_cast(out.data())); + return out; +} + +QByteArray i32LE(int32_t v) +{ + QByteArray out(4, '\0'); + qToLittleEndian(v, reinterpret_cast(out.data())); + return out; +} + +QByteArray u16LE(uint16_t v) +{ + QByteArray out(2, '\0'); + qToLittleEndian(v, reinterpret_cast(out.data())); + return out; +} + +} // namespace + +uint8_t makeHeader(uint8_t routeType, uint8_t payloadType, uint8_t version) +{ + return static_cast( + ((version & kVersionMask) << kVersionShift) + | ((payloadType & kPayloadTypeMask) << kPayloadTypeShift) + | (routeType & kRouteMask)); +} + +QByteArray buildWirePacket(uint8_t header, const QByteArray& payload, const WireOptions& opts) +{ + if (opts.pathHashSize < 1 || opts.pathHashSize > 3) { + return QByteArray(); + } + if (opts.path.size() % opts.pathHashSize != 0) { + return QByteArray(); // path length must be a multiple of hash size + } + + const int hashCount = opts.path.size() / opts.pathHashSize; + if (hashCount > 63) { + return QByteArray(); // path_count field is 6 bits + } + + QByteArray out; + out.reserve(1 + (opts.transport ? 4 : 0) + 1 + opts.path.size() + payload.size()); + + // [0] header + out.append(static_cast(header)); + + // [1..4] optional transport_codes (LE u16, u16) + if (opts.transport) { + out.append(u16LE(opts.transport->first)); + out.append(u16LE(opts.transport->second)); + } + + // [N] path_len byte (encodes hash size + count) + out.append(static_cast(encodePathLen(hashCount, opts.pathHashSize))); + + // [N+1 .. N+P] path + out.append(opts.path); + + // [N+1+P ..] payload + out.append(payload); + + return out; +} + +QByteArray buildAdvert(const QByteArray& seed, const QByteArray& pubKey, const AdvertOptions& opts) +{ + if (seed.size() != kPubKeySize || pubKey.size() != kPubKeySize) { + return QByteArray(); + } + + // ---- compute flags ---- + uint8_t flags = static_cast(opts.nodeType & 0x0F); + if (opts.latLon) { + flags |= AdvertHasLocation; + } + if (!opts.name.isEmpty()) { + flags |= AdvertHasName; + } + + // ---- assemble app_data ---- + QByteArray appData; + appData.reserve(1 + (opts.latLon ? 8 : 0) + opts.name.toUtf8().size()); + appData.append(static_cast(flags)); + if (opts.latLon) { + const int32_t latI = static_cast(std::lround(opts.latLon->first * 1e6)); + const int32_t lonI = static_cast(std::lround(opts.latLon->second * 1e6)); + appData.append(i32LE(latI)); + appData.append(i32LE(lonI)); + } + if (!opts.name.isEmpty()) { + appData.append(opts.name.toUtf8()); + } + + // ---- sign over (pubkey || ts || app_data) ---- + const QByteArray tsBytes = u32LE(opts.timestamp); + QByteArray message; + message.reserve(kPubKeySize + 4 + appData.size()); + message.append(pubKey); + message.append(tsBytes); + message.append(appData); + + const QByteArray signature = detail::signEd25519(seed, message); + if (signature.size() != kSignatureSize) { + return QByteArray(); + } + + // ---- assemble ADVERT body: [pubkey(32)][ts(4)][signature(64)][app_data] ---- + QByteArray body; + body.reserve(kPubKeySize + 4 + kSignatureSize + appData.size()); + body.append(pubKey); + body.append(tsBytes); + body.append(signature); + body.append(appData); + + // ---- wrap in wire envelope ---- + WireOptions wireOpts; + wireOpts.transport = opts.transport; + return buildWirePacket( + makeHeader(opts.routeType, PayloadAdvert), + body, + wireOpts); +} + +// ---- TXT_MSG ------------------------------------------------------------------ + +QByteArray buildTxtMsg(const QByteArray& seed, + const QByteArray& destPub32, + const QByteArray& text, + const TxtMsgOptions& opts) +{ + if (seed.size() != kPubKeySize || destPub32.size() != kPubKeySize) { + return QByteArray(); + } + + const QByteArray secret = detail::sharedSecret(seed, destPub32); + if (secret.size() != kPubKeySize) { + return QByteArray(); + } + + // plaintext: [ts(4 LE)] [txt_type<<2 | attempt(1)] [text] + QByteArray plaintext; + plaintext.reserve(4 + 1 + text.size()); + plaintext.append(u32LE(opts.timestamp)); + plaintext.append(static_cast(((opts.txtType & 0x3F) << 2) | (opts.attempt & 0x03))); + plaintext.append(text); + + const QByteArray encrypted = detail::encryptThenMac(secret, plaintext); + if (encrypted.isEmpty()) { + return QByteArray(); + } + + const QByteArray myPub = detail::derivePubKey(seed); + if (myPub.size() != kPubKeySize) { + return QByteArray(); + } + + // body: [dest_hash(1)] [src_hash(1)] [MAC(2) || ciphertext] + QByteArray body; + body.reserve(1 + 1 + encrypted.size()); + body.append(destPub32.left(1)); + body.append(myPub.left(1)); + body.append(encrypted); + + WireOptions wireOpts; + wireOpts.transport = opts.transport; + return buildWirePacket(makeHeader(opts.routeType, PayloadTxt), body, wireOpts); +} + +// ---- ANON_REQ ----------------------------------------------------------------- + +QByteArray buildAnonReq(const QByteArray& seed, + const QByteArray& destPub32, + const QByteArray& data, + const AnonReqOptions& opts) +{ + if (seed.size() != kPubKeySize || destPub32.size() != kPubKeySize) { + return QByteArray(); + } + + const QByteArray secret = detail::sharedSecret(seed, destPub32); + if (secret.size() != kPubKeySize) { + return QByteArray(); + } + + // plaintext: [ts(4)] [data] + QByteArray plaintext; + plaintext.reserve(4 + data.size()); + plaintext.append(u32LE(opts.timestamp)); + plaintext.append(data); + + const QByteArray encrypted = detail::encryptThenMac(secret, plaintext); + if (encrypted.isEmpty()) { + return QByteArray(); + } + + const QByteArray myPub = detail::derivePubKey(seed); + if (myPub.size() != kPubKeySize) { + return QByteArray(); + } + + // body: [dest_hash(1)] [sender_pub(32)] [MAC(2) || ciphertext] + QByteArray body; + body.reserve(1 + kPubKeySize + encrypted.size()); + body.append(destPub32.left(1)); + body.append(myPub); + body.append(encrypted); + + WireOptions wireOpts; + wireOpts.transport = opts.transport; + return buildWirePacket(makeHeader(opts.routeType, PayloadAnonReq), body, wireOpts); +} + +// ---- GroupChannel + GRP_TXT --------------------------------------------------- + +GroupChannel GroupChannel::fromPsk(const QString& name, const QByteArray& pskRaw) +{ + GroupChannel ch; + if (pskRaw.size() != 16 && pskRaw.size() != 32) { + return ch; // invalid -> isValid() == false + } + ch.name = name; + ch.pskRaw = pskRaw; + + // Zero-pad PSK to 32 bytes (used as HMAC key). + ch.secret = pskRaw; + if (ch.secret.size() < kPubKeySize) { + ch.secret.append(kPubKeySize - ch.secret.size(), '\0'); + } + + // Channel hash = SHA-256(pskRaw)[0:1] + ch.hash = QCryptographicHash::hash(pskRaw, QCryptographicHash::Sha256).left(1); + return ch; +} + +QByteArray buildGrpTxt(const GroupChannel& channel, + const QByteArray& text, + const GrpTxtOptions& opts) +{ + if (!channel.isValid()) { + return QByteArray(); + } + + QByteArray plaintext; + plaintext.reserve(4 + 1 + text.size()); + plaintext.append(u32LE(opts.timestamp)); + plaintext.append(static_cast(((opts.txtType & 0x3F) << 2) | (opts.attempt & 0x03))); + plaintext.append(text); + + const QByteArray encrypted = detail::encryptThenMac(channel.secret, plaintext); + if (encrypted.isEmpty()) { + return QByteArray(); + } + + // body: [channel_hash(1)] [MAC(2) || ciphertext] + QByteArray body; + body.reserve(1 + encrypted.size()); + body.append(channel.hash); + body.append(encrypted); + + WireOptions wireOpts; + wireOpts.transport = opts.transport; + return buildWirePacket(makeHeader(opts.routeType, PayloadGrpTxt), body, wireOpts); +} + +// ---- ACK ---------------------------------------------------------------------- + +QByteArray buildAck(const QByteArray& destPub32, + const QByteArray& msgHash4, + const AckOptions& opts) +{ + if (destPub32.size() != kPubKeySize || msgHash4.size() != 4) { + return QByteArray(); + } + + QByteArray body; + body.reserve(1 + 4); + body.append(destPub32.left(1)); + body.append(msgHash4); + + WireOptions wireOpts; + wireOpts.transport = opts.transport; + return buildWirePacket(makeHeader(opts.routeType, PayloadAck), body, wireOpts); +} + +// ---- Public channel PSK ------------------------------------------------------- + +QByteArray publicChannelPsk() +{ + static const QByteArray kPsk = QByteArray::fromHex("8b3387e9c5cdea6ac9e5edbaa115cd72"); + return kPsk; +} + +} // namespace builders +} // namespace modemmeshcore diff --git a/modemmeshcore/meshcore_builders.h b/modemmeshcore/meshcore_builders.h new file mode 100644 index 000000000..d26b84a82 --- /dev/null +++ b/modemmeshcore/meshcore_builders.h @@ -0,0 +1,178 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore wire-packet builders. Each function returns a complete OTA packet // +// ready to be handed to the LoRa PHY encoder. // +// // +// Wire envelope (build_wire_packet in gr4-lora/meshcore_tx.py): // +// [header(1)] [opt transport(4)] [path_len(1)] [path(N)] [payload(M)] // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef MODEMMESHCORE_MESHCORE_BUILDERS_H_ +#define MODEMMESHCORE_MESHCORE_BUILDERS_H_ + +#include +#include + +#include +#include + +#include "export.h" + +namespace modemmeshcore +{ +namespace builders +{ + +// ---- header byte -------------------------------------------------------------- + +// Build the MeshCore header byte: version[7:6] | payload_type[5:2] | route[1:0] +MODEMMESHCORE_API uint8_t makeHeader(uint8_t routeType, uint8_t payloadType, uint8_t version = 0); + +// ---- wire envelope ------------------------------------------------------------ + +struct WireOptions +{ + QByteArray path; // 0..63*hashSize bytes + int pathHashSize = 1; // 1, 2, or 3 + std::optional> transport; // 4-byte transport block +}; + +// Compose the full OTA packet from header byte + per-payload-type body. +MODEMMESHCORE_API QByteArray buildWirePacket( + uint8_t header, + const QByteArray& payload, + const WireOptions& opts = WireOptions{}); + +// ---- ADVERT ------------------------------------------------------------------- + +struct AdvertOptions +{ + uint8_t nodeType = 0x01; // AdvertNodeChat + QString name; // empty -> no name flag + std::optional> latLon; // (lat, lon) in degrees + uint32_t timestamp = 0; // 0 -> caller must inject; 0 stays as-is + uint8_t routeType = 0x01; // RouteFlood (default per MeshCore convention) + std::optional> transport; +}; + +// Build a signed ADVERT wire packet from a 32-byte Ed25519 seed. +// +// Body layout: [pubkey(32)][timestamp(4 LE)][signature(64)][app_data] +// app_data = [flags(1)][opt lat(4 LE)+lon(4 LE)][opt name(UTF-8)] +// signature is over: pubkey || timestamp || app_data +// +// Returns the OTA-ready packet, empty on error. +MODEMMESHCORE_API QByteArray buildAdvert( + const QByteArray& seed, + const QByteArray& pubKey, + const AdvertOptions& opts); + +// ---- TXT_MSG (encrypted to a known contact via ECDH) -------------------------- + +enum TxtType : uint8_t +{ + TxtTypePlain = 0x00, + TxtTypeCli = 0x01, + TxtTypeSigned = 0x02, +}; + +struct TxtMsgOptions +{ + uint8_t txtType = TxtTypePlain; // upper 6 bits of txt_type_attempt byte + uint8_t attempt = 0; // lower 2 bits (0..3) + uint32_t timestamp = 0; + uint8_t routeType = 0x02; // RouteDirect default for known contacts + std::optional> transport; +}; + +// Encrypted text message to a known contact identified by destPub32. +// +// Body: [dest_hash(1)] [src_hash(1)] [MAC(2)] [AES-128-ECB ciphertext] +// plaintext: [ts(4 LE)] [txt_type<<2 | attempt(1)] [text bytes] +// +// dest_hash = destPub32[0:1], src_hash = derivePubKey(seed)[0:1]. +MODEMMESHCORE_API QByteArray buildTxtMsg( + const QByteArray& seed, + const QByteArray& destPub32, + const QByteArray& text, + const TxtMsgOptions& opts); + +// ---- ANON_REQ (encrypted with sender pubkey in cleartext) --------------------- + +struct AnonReqOptions +{ + uint32_t timestamp = 0; + uint8_t routeType = 0x02; // RouteDirect default + std::optional> transport; +}; + +// Body: [dest_hash(1)] [sender_pub(32)] [MAC(2)] [AES-128-ECB ciphertext] +// plaintext: [ts(4 LE)] [data] +MODEMMESHCORE_API QByteArray buildAnonReq( + const QByteArray& seed, + const QByteArray& destPub32, + const QByteArray& data, + const AnonReqOptions& opts); + +// ---- GRP_TXT (group channel pre-shared key) ----------------------------------- + +// A MeshCore group channel: 16- or 32-byte PSK identified by a 1-byte hash. +struct MODEMMESHCORE_API GroupChannel +{ + QString name; // human label + QByteArray pskRaw; // 16 or 32 raw bytes + QByteArray secret; // pskRaw zero-padded to 32 bytes (HMAC key) + QByteArray hash; // SHA-256(pskRaw)[0:1] + + static GroupChannel fromPsk(const QString& name, const QByteArray& pskRaw); + bool isValid() const { return !pskRaw.isEmpty() && hash.size() == 1; } +}; + +struct GrpTxtOptions +{ + uint8_t txtType = TxtTypePlain; + uint8_t attempt = 0; + uint32_t timestamp = 0; + uint8_t routeType = 0x01; // RouteFlood default for group channels + std::optional> transport; +}; + +// Body: [channel_hash(1)] [MAC(2)] [AES-128-ECB ciphertext] +// plaintext: [ts(4 LE)] [txt_type<<2 | attempt(1)] [text bytes] +// +// MeshCore firmware convention: text is formatted as "SenderName: message". +MODEMMESHCORE_API QByteArray buildGrpTxt( + const GroupChannel& channel, + const QByteArray& text, + const GrpTxtOptions& opts); + +// ---- ACK (control plaintext, no encryption) ----------------------------------- + +struct AckOptions +{ + uint8_t routeType = 0x02; // RouteDirect default + std::optional> transport; +}; + +// Body: [dest_hash(1)] [msg_hash(4)] +MODEMMESHCORE_API QByteArray buildAck( + const QByteArray& destPub32, + const QByteArray& msgHash4, + const AckOptions& opts); + +// ---- Public-channel helper (fixed PSK convention) ----------------------------- + +// MeshCore firmware fixes the "public" channel PSK at this 16-byte value and +// always seats it at channel index 0 in firmware/companion-app contact lists. +MODEMMESHCORE_API QByteArray publicChannelPsk(); + +} // namespace builders +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_MESHCORE_BUILDERS_H_ diff --git a/modemmeshcore/meshcore_command.cpp b/modemmeshcore/meshcore_command.cpp new file mode 100644 index 000000000..385c6992d --- /dev/null +++ b/modemmeshcore/meshcore_command.cpp @@ -0,0 +1,546 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MESHCORE: command parser implementation. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcore_command.h" + +#include "meshcore_builders.h" +#include "meshcore_crypto.h" + +#include +#include + +namespace modemmeshcore +{ +namespace command +{ + +namespace +{ + +QString lower(const QString& s) { return s.toLower(); } + +// Parse a hex string into bytes; empty on parse error. +QByteArray hex(const QString& v) +{ + QByteArray b = QByteArray::fromHex(v.toLatin1()); + return b; +} + +// Resolve a key (case-insensitive). Empty if missing. +QString get(const QMap& kv, const char* key) +{ + auto it = kv.find(QString::fromLatin1(key)); + return (it != kv.end()) ? it.value() : QString(); +} + +bool toUInt(const QString& s, uint32_t& out) +{ + bool ok = false; + if (s.startsWith("0x", Qt::CaseInsensitive)) { + out = s.mid(2).toUInt(&ok, 16); + } else { + out = s.toUInt(&ok, 10); + } + return ok; +} + +bool toRouteType(const QString& s, uint8_t& out) +{ + const QString l = lower(s); + if (l == "t_flood") { out = RouteTFlood; return true; } + if (l == "flood") { out = RouteFlood; return true; } + if (l == "direct") { out = RouteDirect; return true; } + if (l == "t_direct") { out = RouteTDirect; return true; } + return false; +} + +uint32_t resolveTimestamp(const QMap& kv) +{ + const QString ts = get(kv, "ts"); + if (ts.isEmpty()) { + return static_cast(QDateTime::currentSecsSinceEpoch()); + } + uint32_t v = 0; + return toUInt(ts, v) ? v : 0; +} + +} // namespace + +bool tokenize(const QString& body, QMap& kv, QString& error) +{ + kv.clear(); + const QStringList tokens = body.split(';', Qt::SkipEmptyParts); + for (const QString& raw : tokens) + { + const QString tok = raw.trimmed(); + if (tok.isEmpty()) { + continue; + } + const int eq = tok.indexOf('='); + if (eq <= 0) { + error = QString("malformed token (expected key=value): '%1'").arg(tok); + return false; + } + const QString k = tok.left(eq).trimmed().toLower(); + const QString v = tok.mid(eq + 1).trimmed(); + if (k.isEmpty()) { + error = QString("empty key in token: '%1'").arg(tok); + return false; + } + kv.insert(k, v); + } + return true; +} + +namespace +{ + +// MeshCore regional preset table. +// +// Entries below are the well-known regional defaults the MeshCore +// community uses. They map directly to the firmware CLI's +// `set radio freq_MHz,bw_kHz,sf,cr` form (cr is encoded as 5..8 for +// 4/5..4/8 — `cr` here is therefore parityBits + 4). Default profile +// is `EU_NARROW` (MeshCore EU/UK Narrow / Recommended channel). +struct MeshcorePresetEntry +{ + const char* key; // canonical preset key (uppercase, snake_case) + const char* displayName; // human-readable label (for logs/summary) + qint64 freqHz; + int bandwidthHz; + int spreadFactor; + int parityBits; // CR = parityBits + 4 +}; + +constexpr MeshcorePresetEntry kMeshcorePresets[] = { + {"EU_NARROW", "EU/UK Narrow (Recommended)", 869618000, 62500, 8, 4}, + {"EU_LONG_RANGE", "EU/UK Long Range", 869525000, 250000, 11, 1}, + {"EU_MEDIUM_RANGE", "EU/UK Medium Range", 869525000, 250000, 10, 1}, + {"AU", "Australia", 915800000, 250000, 10, 1}, + {"AU_VICTORIA", "Australia: Victoria", 916575000, 62500, 7, 4}, + {"CZ_NARROW", "Czech Republic Narrow", 869525000, 62500, 7, 1}, + {"EU_433_LONG_RANGE", "EU 433 MHz Long Range", 433650000, 250000, 11, 1}, + {"NZ", "New Zealand", 917375000, 250000, 11, 1}, + {"NZ_NARROW", "New Zealand Narrow", 917375000, 62500, 7, 1}, + {"PT_433", "Portugal 433", 433375000, 62500, 9, 2}, + {"PT_868", "Portugal 868", 869618000, 62500, 7, 2}, + {"CH", "Switzerland", 869618000, 62500, 8, 4}, + {"USA", "USA / Canada", 910525000, 62500, 7, 1}, + {"VN", "Vietnam", 920250000, 250000, 11, 1}, +}; + +bool applyMeshcorePreset(const QString& preset, TxRadioSettings& settings, QString& error) +{ + const QByteArray key = preset.toUpper().toLatin1(); + if (key == "USER") { + return true; // USER preset = no overrides; caller's individual keys win + } + for (const auto& entry : kMeshcorePresets) { + if (key == entry.key) { + settings.centerFrequencyHz = entry.freqHz; + settings.hasCenterFrequency = true; + settings.bandwidthHz = entry.bandwidthHz; + settings.spreadFactor = entry.spreadFactor; + settings.parityBits = entry.parityBits; + settings.hasLoRaParams = true; + settings.summary = QString("MeshCore preset %1").arg(entry.displayName); + return true; + } + } + error = QString("invalid MeshCore preset: '%1'").arg(preset); + return false; +} + +} // namespace + +bool applyRadioParams(const QMap& kv, TxRadioSettings& settings, QString& error) +{ + bool changed = false; + + // Apply MeshCore regional preset first (if present), then let individual + // sf/bw/cr/freq/sync/preamble keys override specific fields. + const QString presetS = get(kv, "preset"); + if (!presetS.isEmpty()) { + if (!applyMeshcorePreset(presetS, settings, error)) { + return false; + } + // SF-dependent preamble preamble: SF < 9 → 32, SF > 8 → 16. + // Matches upstream meshcore-py commit 2026-06. + if (settings.spreadFactor < 9) { settings.preambleChirps = 32; } + else { settings.preambleChirps = 16; } + changed = true; + } + + const QString sfS = get(kv, "sf"); + if (!sfS.isEmpty()) { + uint32_t sf = 0; + if (!toUInt(sfS, sf) || sf < 7 || sf > 12) { + error = QString("invalid sf: '%1' (expected 7..12)").arg(sfS); + return false; + } + settings.spreadFactor = static_cast(sf); + settings.hasLoRaParams = true; + changed = true; + } + + const QString bwS = get(kv, "bw"); + if (!bwS.isEmpty()) { + uint32_t bw = 0; + if (!toUInt(bwS, bw) || bw == 0) { + error = QString("invalid bw: '%1' (expected Hz)").arg(bwS); + return false; + } + settings.bandwidthHz = static_cast(bw); + settings.hasLoRaParams = true; + changed = true; + } + + const QString crS = get(kv, "cr"); + if (!crS.isEmpty()) { + uint32_t cr = 0; + if (!toUInt(crS, cr) || cr < 5 || cr > 8) { + error = QString("invalid cr: '%1' (expected 5..8 for 4/5..4/8)").arg(crS); + return false; + } + settings.parityBits = static_cast(cr) - 4; // 5 -> 1, 8 -> 4 + settings.hasLoRaParams = true; + changed = true; + } + + const QString syncS = get(kv, "sync"); + if (!syncS.isEmpty()) { + uint32_t sync = 0; + if (!toUInt(syncS, sync) || sync > 0xFF) { + error = QString("invalid sync: '%1'").arg(syncS); + return false; + } + settings.syncWord = static_cast(sync); + settings.hasLoRaParams = true; + changed = true; + } + + const QString freqS = get(kv, "freq"); + if (!freqS.isEmpty()) { + // Accept "869618000" (Hz) or "869.618M" / "869.618MHz" + QString s = freqS; + s.replace("Hz", "", Qt::CaseInsensitive); + const bool isMhz = s.contains('M', Qt::CaseInsensitive); + s.replace("M", "", Qt::CaseInsensitive); + bool ok = false; + const double v = s.toDouble(&ok); + if (!ok || v <= 0) { + error = QString("invalid freq: '%1'").arg(freqS); + return false; + } + settings.centerFrequencyHz = static_cast(isMhz ? v * 1.0e6 : v); + settings.hasCenterFrequency = true; + changed = true; + } + + const QString prS = get(kv, "preamble"); + if (!prS.isEmpty()) { + uint32_t pr = 0; + if (!toUInt(prS, pr) || pr == 0 || pr > 1024) { + error = QString("invalid preamble: '%1'").arg(prS); + return false; + } + settings.preambleChirps = static_cast(pr); + settings.hasLoRaParams = true; + changed = true; + } + + if (changed) { + settings.hasCommand = true; + } + return true; +} + +namespace +{ + +bool buildAdvertFromKv(const QMap& kv, + QByteArray& frame, QString& summary, QString& error) +{ + const QByteArray seed = hex(get(kv, "seed")); + if (seed.size() != kPubKeySize) { + error = "advert: seed (hex 32 bytes) is required"; + return false; + } + const QByteArray pub = detail::derivePubKey(seed); + if (pub.size() != kPubKeySize) { + error = "advert: failed to derive public key"; + return false; + } + + builders::AdvertOptions opts; + opts.timestamp = resolveTimestamp(kv); + opts.name = get(kv, "name"); + + const QString latS = get(kv, "lat"); + const QString lonS = get(kv, "lon"); + if (!latS.isEmpty() && !lonS.isEmpty()) { + bool okLat = false, okLon = false; + const double lat = latS.toDouble(&okLat); + const double lon = lonS.toDouble(&okLon); + if (!okLat || !okLon) { + error = "advert: invalid lat/lon"; + return false; + } + opts.latLon = std::make_pair(lat, lon); + } + + const QString routeS = get(kv, "route"); + if (!routeS.isEmpty()) { + if (!toRouteType(routeS, opts.routeType)) { + error = QString("advert: invalid route '%1'").arg(routeS); + return false; + } + } + + frame = builders::buildAdvert(seed, pub, opts); + if (frame.isEmpty()) { + error = "advert: build failed"; + return false; + } + summary = QString("MESHCORE TX|advert pub=%1 ts=%2 name=\"%3\"") + .arg(QString::fromLatin1(pub.toHex().left(16))) + .arg(opts.timestamp) + .arg(opts.name); + return true; +} + +bool buildTxtMsgFromKv(const QMap& kv, + QByteArray& frame, QString& summary, QString& error) +{ + const QByteArray seed = hex(get(kv, "seed")); + const QByteArray dest = hex(get(kv, "dest")); + const QString text = get(kv, "text"); + if (seed.size() != kPubKeySize) { + error = "txt_msg: seed (hex 32 bytes) required"; + return false; + } + if (dest.size() != kPubKeySize) { + error = "txt_msg: dest (hex 32 bytes) required"; + return false; + } + if (text.isEmpty()) { + error = "txt_msg: text required"; + return false; + } + + builders::TxtMsgOptions opts; + opts.timestamp = resolveTimestamp(kv); + + uint32_t txtType = 0, attempt = 0; + const QString tt = get(kv, "txt_type"); + if (!tt.isEmpty() && (!toUInt(tt, txtType) || txtType > 0x3F)) { + error = "txt_msg: invalid txt_type"; + return false; + } + const QString at = get(kv, "attempt"); + if (!at.isEmpty() && (!toUInt(at, attempt) || attempt > 3)) { + error = "txt_msg: invalid attempt (0..3)"; + return false; + } + opts.txtType = static_cast(txtType); + opts.attempt = static_cast(attempt); + + const QString routeS = get(kv, "route"); + if (!routeS.isEmpty() && !toRouteType(routeS, opts.routeType)) { + error = QString("txt_msg: invalid route '%1'").arg(routeS); + return false; + } + + frame = builders::buildTxtMsg(seed, dest, text.toUtf8(), opts); + if (frame.isEmpty()) { + error = "txt_msg: build failed"; + return false; + } + summary = QString("MESHCORE TX|txt_msg dest=%1 ts=%2 text=\"%3\"") + .arg(QString::fromLatin1(dest.toHex().left(16))) + .arg(opts.timestamp) + .arg(text); + return true; +} + +bool buildAnonReqFromKv(const QMap& kv, + QByteArray& frame, QString& summary, QString& error) +{ + const QByteArray seed = hex(get(kv, "seed")); + const QByteArray dest = hex(get(kv, "dest")); + if (seed.size() != kPubKeySize) { + error = "anon_req: seed required"; + return false; + } + if (dest.size() != kPubKeySize) { + error = "anon_req: dest required"; + return false; + } + + QByteArray data; + const QString dataHex = get(kv, "data"); + const QString dataTxt = get(kv, "text"); + if (!dataHex.isEmpty()) { + data = hex(dataHex); + if (data.isEmpty()) { + error = "anon_req: invalid data hex"; + return false; + } + } else if (!dataTxt.isEmpty()) { + data = dataTxt.toUtf8(); + } else { + error = "anon_req: data or text required"; + return false; + } + + builders::AnonReqOptions opts; + opts.timestamp = resolveTimestamp(kv); + const QString routeS = get(kv, "route"); + if (!routeS.isEmpty() && !toRouteType(routeS, opts.routeType)) { + error = QString("anon_req: invalid route '%1'").arg(routeS); + return false; + } + + frame = builders::buildAnonReq(seed, dest, data, opts); + if (frame.isEmpty()) { + error = "anon_req: build failed"; + return false; + } + summary = QString("MESHCORE TX|anon_req dest=%1 ts=%2 len=%3") + .arg(QString::fromLatin1(dest.toHex().left(16))) + .arg(opts.timestamp) + .arg(data.size()); + return true; +} + +bool buildGrpTxtFromKv(const QMap& kv, + QByteArray& frame, QString& summary, QString& error) +{ + const QString text = get(kv, "text"); + if (text.isEmpty()) { + error = "grp_txt: text required"; + return false; + } + + QByteArray psk; + QString chanName = get(kv, "channel"); + const QString pskS = get(kv, "channel_psk"); + if (!pskS.isEmpty()) { + psk = hex(pskS); + if (psk.size() != 16 && psk.size() != 32) { + error = "grp_txt: channel_psk must be 16 or 32 hex bytes"; + return false; + } + if (chanName.isEmpty()) { + chanName = "psk"; + } + } else if (chanName.compare("public", Qt::CaseInsensitive) == 0) { + psk = builders::publicChannelPsk(); + } else { + error = "grp_txt: channel_psk or channel=public required"; + return false; + } + + const builders::GroupChannel ch = builders::GroupChannel::fromPsk(chanName, psk); + if (!ch.isValid()) { + error = "grp_txt: invalid channel"; + return false; + } + + builders::GrpTxtOptions opts; + opts.timestamp = resolveTimestamp(kv); + + uint32_t txtType = 0, attempt = 0; + const QString tt = get(kv, "txt_type"); + if (!tt.isEmpty() && (!toUInt(tt, txtType) || txtType > 0x3F)) { + error = "grp_txt: invalid txt_type"; + return false; + } + const QString at = get(kv, "attempt"); + if (!at.isEmpty() && (!toUInt(at, attempt) || attempt > 3)) { + error = "grp_txt: invalid attempt"; + return false; + } + opts.txtType = static_cast(txtType); + opts.attempt = static_cast(attempt); + + const QString routeS = get(kv, "route"); + if (!routeS.isEmpty() && !toRouteType(routeS, opts.routeType)) { + error = QString("grp_txt: invalid route '%1'").arg(routeS); + return false; + } + + frame = builders::buildGrpTxt(ch, text.toUtf8(), opts); + if (frame.isEmpty()) { + error = "grp_txt: build failed"; + return false; + } + summary = QString("MESHCORE TX|grp_txt channel=\"%1\" hash=%2 ts=%3 text=\"%4\"") + .arg(chanName) + .arg(QString::fromLatin1(ch.hash.toHex())) + .arg(opts.timestamp) + .arg(text); + return true; +} + +bool buildAckFromKv(const QMap& kv, + QByteArray& frame, QString& summary, QString& error) +{ + const QByteArray dest = hex(get(kv, "dest")); + const QByteArray msgHash = hex(get(kv, "msg_hash")); + if (dest.size() != kPubKeySize) { + error = "ack: dest required"; + return false; + } + if (msgHash.size() != 4) { + error = "ack: msg_hash must be 4 hex bytes"; + return false; + } + + builders::AckOptions opts; + const QString routeS = get(kv, "route"); + if (!routeS.isEmpty() && !toRouteType(routeS, opts.routeType)) { + error = QString("ack: invalid route '%1'").arg(routeS); + return false; + } + + frame = builders::buildAck(dest, msgHash, opts); + if (frame.isEmpty()) { + error = "ack: build failed"; + return false; + } + summary = QString("MESHCORE TX|ack dest=%1 hash=%2") + .arg(QString::fromLatin1(dest.toHex().left(16))) + .arg(QString::fromLatin1(msgHash.toHex())); + return true; +} + +} // namespace + +bool buildFrameByType(const QString& typeName, + const QMap& kv, + QByteArray& frame, + QString& summary, + QString& error) +{ + const QString t = typeName.toLower(); + if (t == "advert") { return buildAdvertFromKv(kv, frame, summary, error); } + if (t == "txt_msg") { return buildTxtMsgFromKv(kv, frame, summary, error); } + if (t == "anon_req") { return buildAnonReqFromKv(kv, frame, summary, error); } + if (t == "grp_txt") { return buildGrpTxtFromKv(kv, frame, summary, error); } + if (t == "ack") { return buildAckFromKv(kv, frame, summary, error); } + error = QString("unknown type: '%1' (expected advert|txt_msg|anon_req|grp_txt|ack)") + .arg(typeName); + return false; +} + +} // namespace command +} // namespace modemmeshcore diff --git a/modemmeshcore/meshcore_command.h b/modemmeshcore/meshcore_command.h new file mode 100644 index 000000000..afdacc096 --- /dev/null +++ b/modemmeshcore/meshcore_command.h @@ -0,0 +1,73 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MESHCORE: command-line parser. Translates a single line like // +// // +// MESHCORE: type=advert; name=SDRangelTest; lat=53.55; lon=9.99 // +// MESHCORE: type=txt_msg; dest=; text=Hello // +// MESHCORE: type=grp_txt; channel=public; text=Hello group // +// MESHCORE: type=ack; dest=; msg_hash= // +// // +// into a wire packet (via builders::*) and / or a TxRadioSettings override. // +// // +// Tokens are split on ';'. Each token is a key=value pair (key compared // +// case-insensitively). Whitespace around tokens and around '=' is trimmed. // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef MODEMMESHCORE_MESHCORE_COMMAND_H_ +#define MODEMMESHCORE_MESHCORE_COMMAND_H_ + +#include +#include +#include + +#include "export.h" +#include "meshcorepacket.h" + +namespace modemmeshcore +{ +namespace command +{ + +// Tokenize a "key1=val1; key2=val2; ..." string into a case-insensitive map. +// Returns true on parse OK, sets error on malformed input. +MODEMMESHCORE_API bool tokenize( + const QString& body, + QMap& kv, + QString& error); + +// Apply common radio-param keys (sf, bw, cr, sync, route, freq, preamble) to +// the given TxRadioSettings. Unknown keys are ignored (handled by caller). +// Returns true on success, false on invalid value (sets error). +MODEMMESHCORE_API bool applyRadioParams( + const QMap& kv, + TxRadioSettings& settings, + QString& error); + +// Build a wire packet from a parsed token map. The "type" key must already +// have been validated by the caller. Sets summary on success. +// +// Required tokens by type: +// advert : seed (hex64). Optional: name, lat, lon, ts, route. +// txt_msg : seed, dest (hex64), text. Optional: txt_type, attempt, ts, route. +// anon_req : seed, dest (hex64), data (hex or text:). Optional: ts, route. +// grp_txt : channel_psk (hex16/32) OR channel=public, text. Optional ts, route. +// ack : dest (hex64), msg_hash (hex8). Optional route. +// +// Returns true and fills frame on success, false sets error. +MODEMMESHCORE_API bool buildFrameByType( + const QString& typeName, + const QMap& kv, + QByteArray& frame, + QString& summary, + QString& error); + +} // namespace command +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_MESHCORE_COMMAND_H_ diff --git a/modemmeshcore/meshcore_crypto.cpp b/modemmeshcore/meshcore_crypto.cpp new file mode 100644 index 000000000..921abcd10 --- /dev/null +++ b/modemmeshcore/meshcore_crypto.cpp @@ -0,0 +1,280 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore crypto primitives. Wraps vendored Monocypher (Ed25519 + X25519 + // +// SHA-512) and tiny-AES (AES-128 ECB) behind a Qt-friendly QByteArray API. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcore_crypto.h" +#include "meshcorepacket.h" +#include "tiny_aes.h" + +#include "monocypher.h" +#include "monocypher-ed25519.h" + +#include + +#include +#include + +namespace modemmeshcore +{ +namespace detail +{ + +// ---- scalar clamping ---------------------------------------------------------- + +QByteArray clampScalar(const QByteArray& scalar) +{ + if (scalar.size() != kPubKeySize) { + return QByteArray(); + } + + QByteArray out(scalar); + auto* p = reinterpret_cast(out.data()); + p[0] &= 248; + p[31] &= 63; + p[31] |= 64; + return out; +} + +// ---- expanded key (SHA-512(seed) + clamp) ------------------------------------- + +QByteArray expandedKey(const QByteArray& seed) +{ + if (seed.size() != kPubKeySize) { + return QByteArray(); + } + + uint8_t hash[64]; + crypto_sha512(hash, reinterpret_cast(seed.constData()), + static_cast(seed.size())); + + // Clamp the first 32 bytes (becomes the Ed25519 scalar). + hash[0] &= 248; + hash[31] &= 63; + hash[31] |= 64; + + return QByteArray(reinterpret_cast(hash), 64); +} + +// ---- public key derivation ---------------------------------------------------- +// +// Note: Monocypher's crypto_ed25519_key_pair produces a 64-byte secret_key +// in its own internal layout (NOT MeshCore's expandedKey form). We use it +// here only to obtain the public key — the secret_key is discarded. + +QByteArray derivePubKey(const QByteArray& seed) +{ + if (seed.size() != kPubKeySize) { + return QByteArray(); + } + + QByteArray seedCopy(seed); // crypto_ed25519_key_pair wipes the seed buffer + uint8_t mcSecretKey[64]; + QByteArray pub32(kPubKeySize, '\0'); + crypto_ed25519_key_pair(mcSecretKey, + reinterpret_cast(pub32.data()), + reinterpret_cast(seedCopy.data())); + return pub32; +} + +// ---- ECDH shared secret ------------------------------------------------------- + +QByteArray sharedSecret(const QByteArray& seed, const QByteArray& otherPub32) +{ + if (seed.size() != kPubKeySize || otherPub32.size() != kPubKeySize) { + return QByteArray(); + } + + // Build MeshCore's expanded private key and take its first 32 bytes as the + // X25519 scalar. expandedKey() handles SHA-512 + clamp. + const QByteArray expanded = expandedKey(seed); + if (expanded.size() != kPrvKeySize) { + return QByteArray(); + } + + // Convert the peer's Ed25519 public key (Edwards form) into its X25519 + // counterpart (Montgomery form). Pure curve map, hash-independent. + uint8_t curvePk[32]; + crypto_eddsa_to_x25519(curvePk, + reinterpret_cast(otherPub32.constData())); + + uint8_t shared[32]; + crypto_x25519(shared, + reinterpret_cast(expanded.constData()), + curvePk); + return QByteArray(reinterpret_cast(shared), 32); +} + +// ---- HMAC-SHA256 (RFC 2104) over Qt's QCryptographicHash::Sha256 -------------- + +QByteArray hmacSha256(const QByteArray& key, const QByteArray& data) +{ + constexpr int blockSize = 64; // SHA-256 block size + + QByteArray k(key); + if (k.size() > blockSize) { + k = QCryptographicHash::hash(k, QCryptographicHash::Sha256); + } + if (k.size() < blockSize) { + k.append(blockSize - k.size(), '\0'); + } + + QByteArray innerPad(blockSize, '\0'); + QByteArray outerPad(blockSize, '\0'); + for (int i = 0; i < blockSize; ++i) { + innerPad[i] = static_cast(static_cast(k[i]) ^ 0x36); + outerPad[i] = static_cast(static_cast(k[i]) ^ 0x5C); + } + + QCryptographicHash inner(QCryptographicHash::Sha256); + inner.addData(innerPad); + inner.addData(data); + + QCryptographicHash outer(QCryptographicHash::Sha256); + outer.addData(outerPad); + outer.addData(inner.result()); + return outer.result(); +} + +// ---- encrypt-then-MAC --------------------------------------------------------- + +QByteArray encryptThenMac(const QByteArray& shared, const QByteArray& plaintext) +{ + if (shared.size() < kPubKeySize) { + return QByteArray(); + } + + AesCtx aes; + if (!aes.init(reinterpret_cast(shared.constData()), kCipherKeySize)) { + return QByteArray(); + } + + // Pad plaintext to a 16-byte boundary with 0x00. + QByteArray padded(plaintext); + const int pad = (kCipherBlockSize - padded.size() % kCipherBlockSize) % kCipherBlockSize; + if (pad > 0) { + padded.append(pad, '\0'); + } + + // ECB block-by-block. + QByteArray ciphertext(padded.size(), '\0'); + for (int off = 0; off < padded.size(); off += kCipherBlockSize) { + aes.encryptBlock(reinterpret_cast(padded.constData() + off), + reinterpret_cast(ciphertext.data() + off)); + } + + // 2-byte truncated HMAC over ciphertext. + QByteArray macKey = shared.left(kPubKeySize); + QByteArray mac = hmacSha256(macKey, ciphertext).left(kCipherMacSize); + + QByteArray out; + out.reserve(mac.size() + ciphertext.size()); + out.append(mac); + out.append(ciphertext); + return out; +} + +// ---- MAC-then-decrypt --------------------------------------------------------- + +QByteArray macThenDecrypt(const QByteArray& shared, const QByteArray& macThenCipher) +{ + if (shared.size() < kPubKeySize) { + return QByteArray(); + } + if (macThenCipher.size() < kCipherMacSize + kCipherBlockSize) { + return QByteArray(); + } + if ((macThenCipher.size() - kCipherMacSize) % kCipherBlockSize != 0) { + return QByteArray(); // ciphertext must be a whole number of blocks + } + + QByteArray macReceived = macThenCipher.left(kCipherMacSize); + QByteArray ciphertext = macThenCipher.mid(kCipherMacSize); + + QByteArray macKey = shared.left(kPubKeySize); + QByteArray macComputed = hmacSha256(macKey, ciphertext).left(kCipherMacSize); + if (macReceived != macComputed) { + return QByteArray(); // MAC mismatch + } + + AesCtx aes; + if (!aes.init(reinterpret_cast(shared.constData()), kCipherKeySize)) { + return QByteArray(); + } + + QByteArray plaintext(ciphertext.size(), '\0'); + for (int off = 0; off < ciphertext.size(); off += kCipherBlockSize) { + aes.decryptBlock(reinterpret_cast(ciphertext.constData() + off), + reinterpret_cast(plaintext.data() + off)); + } + return plaintext; +} + +// ---- Ed25519 sign / verify ---------------------------------------------------- + +QByteArray signEd25519(const QByteArray& seed, const QByteArray& message) +{ + if (seed.size() != kPubKeySize) { + return QByteArray(); + } + + // Materialize Monocypher's 64-byte secret_key from the seed. + // (Discard the public key it computes — caller already has it via + // derivePubKey if needed.) + QByteArray seedCopy(seed); // crypto_ed25519_key_pair wipes the seed buffer + uint8_t mcSecretKey[64]; + uint8_t mcPub[32]; + crypto_ed25519_key_pair(mcSecretKey, + mcPub, + reinterpret_cast(seedCopy.data())); + + QByteArray signature(kSignatureSize, '\0'); + crypto_ed25519_sign(reinterpret_cast(signature.data()), + mcSecretKey, + reinterpret_cast(message.constData()), + static_cast(message.size())); + return signature; +} + +bool verifyEd25519(const QByteArray& signature, const QByteArray& pub32, const QByteArray& message) +{ + if (signature.size() != kSignatureSize || pub32.size() != kPubKeySize) { + return false; + } + const int rc = crypto_ed25519_check( + reinterpret_cast(signature.constData()), + reinterpret_cast(pub32.constData()), + reinterpret_cast(message.constData()), + static_cast(message.size())); + return rc == 0; +} + +} // namespace detail + +// ---- public protocol helpers -------------------------------------------------- + +PathLen decodePathLen(uint8_t raw) +{ + PathLen p; + p.hashCount = raw & kPathCountMask; + p.hashSize = ((raw >> kPathModeShift) & 0x03) + 1; + p.totalBytes = p.hashCount * p.hashSize; + return p; +} + +uint8_t encodePathLen(int hashCount, int hashSize) +{ + if (hashCount < 0 || hashCount > 63 || hashSize < 1 || hashSize > 3) { + return 0; // invalid -> empty path + } + return static_cast(((hashSize - 1) << kPathModeShift) | (hashCount & kPathCountMask)); +} + +} // namespace modemmeshcore diff --git a/modemmeshcore/meshcore_crypto.h b/modemmeshcore/meshcore_crypto.h new file mode 100644 index 000000000..58cc6d785 --- /dev/null +++ b/modemmeshcore/meshcore_crypto.h @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore crypto primitives. Internal to modemmeshcore — exposed in // +// modemmeshcore::detail so unit tests can exercise them directly. // +// // +// Wire-format reference: github.com/meshcore-dev/MeshCore (MIT) and the // +// gr4-lora python implementation under scripts/src/lora/core/meshcore_crypto.py // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef MODEMMESHCORE_MESHCORE_CRYPTO_H_ +#define MODEMMESHCORE_MESHCORE_CRYPTO_H_ + +#include + +#include "export.h" + +namespace modemmeshcore +{ +namespace detail +{ + +// Curve25519 scalar clamping (RFC 7748 §5). +// ba[0] &= 248 // clear low 3 bits +// ba[31] &= 63 // clear top 2 bits +// ba[31] |= 64 // set bit 254 +// Returns the clamped 32-byte scalar (or empty on size mismatch). +MODEMMESHCORE_API QByteArray clampScalar(const QByteArray& scalar); + +// Expand a 32-byte seed into MeshCore's 64-byte private key: +// SHA-512(seed), clamp first 32 bytes, leave second 32 bytes raw. +// Returns 64 bytes (or empty on size mismatch). +MODEMMESHCORE_API QByteArray expandedKey(const QByteArray& seed); + +// Derive the 32-byte Ed25519 public key from a 32-byte seed. +MODEMMESHCORE_API QByteArray derivePubKey(const QByteArray& seed); + +// Compute MeshCore ECDH shared secret via X25519 from our seed and the peer's +// Ed25519 public key. Internally builds expandedKey(seed) and uses its first +// 32 bytes (clamped scalar) for the X25519 multiply. +// Returns 32-byte shared secret on success, empty on validation failure. +MODEMMESHCORE_API QByteArray sharedSecret(const QByteArray& seed, + const QByteArray& otherPub32); + +// AES-128-ECB encrypt then HMAC-SHA256 MAC (truncated to 2 bytes). +// plaintext is zero-padded to 16-byte boundary before encryption. +// key = sharedSecret[0..16] +// mac = HMAC-SHA256(sharedSecret[0..32], ciphertext)[0..2] +// Returns: mac(2) || ciphertext(N*16). Empty on input error. +MODEMMESHCORE_API QByteArray encryptThenMac(const QByteArray& sharedSecret, + const QByteArray& plaintext); + +// Inverse of encryptThenMac. Verifies MAC, decrypts, returns plaintext +// (still zero-padded). Empty on MAC failure or input error. +MODEMMESHCORE_API QByteArray macThenDecrypt(const QByteArray& sharedSecret, + const QByteArray& macThenCipher); + +// Sign a message with the MeshCore identity (32-byte seed). +// Returns 64-byte signature, empty on input-size error. +MODEMMESHCORE_API QByteArray signEd25519(const QByteArray& seed, + const QByteArray& message); + +// Verify Ed25519 signature. +MODEMMESHCORE_API bool verifyEd25519(const QByteArray& signature, + const QByteArray& pub32, + const QByteArray& message); + +// Standard HMAC-SHA256 (RFC 2104) over Qt's QCryptographicHash::Sha256. +// Returns 32-byte digest. Used for ACK hashing too. +MODEMMESHCORE_API QByteArray hmacSha256(const QByteArray& key, + const QByteArray& data); + +} // namespace detail +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_MESHCORE_CRYPTO_H_ diff --git a/modemmeshcore/meshcore_decoder.cpp b/modemmeshcore/meshcore_decoder.cpp new file mode 100644 index 000000000..0b2c3cdb2 --- /dev/null +++ b/modemmeshcore/meshcore_decoder.cpp @@ -0,0 +1,489 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore wire-packet decoder implementation. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcore_decoder.h" + +#include "meshcore_builders.h" +#include "meshcore_crypto.h" + +#include +#include + +namespace modemmeshcore +{ +namespace decoder +{ + +namespace +{ + +void addField(DecodeResult& out, const QString& path, const QString& value) +{ + DecodeResult::Field f; + f.path = path; + f.value = value; + out.fields.append(f); +} + +void addField(DecodeResult& out, const QString& path, int v) +{ + addField(out, path, QString::number(v)); +} + +QString hexOf(const QByteArray& bytes) { return QString::fromLatin1(bytes.toHex()); } + +uint32_t readU32LE(const QByteArray& wire, int off) +{ + if (off + 4 > wire.size()) { + return 0; + } + return qFromLittleEndian(reinterpret_cast(wire.constData() + off)); +} + +int32_t readI32LE(const QByteArray& wire, int off) +{ + if (off + 4 > wire.size()) { + return 0; + } + return qFromLittleEndian(reinterpret_cast(wire.constData() + off)); +} + +// txt_type/attempt is upper 6 bits / lower 2 bits of one byte; same layout +// for TXT_MSG and GRP_TXT. +QString txtTypeName(uint8_t txtType) +{ + switch (txtType) { + case 0x00: return QStringLiteral("plain"); + case 0x01: return QStringLiteral("cli"); + case 0x02: return QStringLiteral("signed"); + default: return QString::number(txtType); + } +} + +// Common decrypt + extract for TXT_MSG / GRP_TXT plaintext layout. +// ts(4 LE) | txt_type<<2|attempt(1) | text || pad +bool extractTxtPlaintext(const QByteArray& plaintext, + uint32_t& timestamp, uint8_t& txtType, + uint8_t& attempt, QString& text) +{ + if (plaintext.size() < 5) { + return false; + } + timestamp = qFromLittleEndian(reinterpret_cast(plaintext.constData())); + const uint8_t flags = static_cast(plaintext[4]); + txtType = static_cast(flags >> 2); + attempt = static_cast(flags & 0x03); + + QByteArray body = plaintext.mid(5); + // Strip trailing zero padding from the encryption block alignment. + while (!body.isEmpty() && body.endsWith('\0')) { + body.chop(1); + } + text = QString::fromUtf8(body); + return true; +} + +} // namespace + +// ---- Header parser ------------------------------------------------------------ + +bool parseHeader(const QByteArray& wire, ParsedHeader& hdr) +{ + if (wire.size() < 2) { + return false; + } + const uint8_t b = static_cast(wire[0]); + hdr.routeType = b & kRouteMask; + hdr.payloadType = (b >> kPayloadTypeShift) & kPayloadTypeMask; + hdr.version = (b >> kVersionShift) & kVersionMask; + hdr.hasTransport = (hdr.routeType == RouteTFlood) || (hdr.routeType == RouteTDirect); + + int off = 1; + if (hdr.hasTransport) { + if (off + 4 > wire.size()) { + return false; + } + off += 4; + } + if (off >= wire.size()) { + return false; + } + hdr.pathLenByte = static_cast(wire[off]); + const PathLen p = decodePathLen(static_cast(hdr.pathLenByte)); + off += 1 + p.totalBytes; + if (off > wire.size()) { + return false; + } + hdr.payloadOffset = off; + return true; +} + +// ---- Key spec list ------------------------------------------------------------ + +bool parseKeySpecList(const QString& spec, QVector& outKeys, + QString& error, bool validateOnly) +{ + outKeys.clear(); + error.clear(); + + const QStringList tokens = spec.split(';', Qt::SkipEmptyParts); + for (const QString& raw : tokens) + { + const QString tok = raw.trimmed(); + if (tok.isEmpty()) { + continue; + } + const int eq = tok.indexOf('='); + if (eq <= 0) { + error = QString("malformed key entry: '%1' (expected key=value)").arg(tok); + return false; + } + const QString lhs = tok.left(eq).trimmed(); + const QString rhs = tok.mid(eq + 1).trimmed(); + + const int colon = lhs.indexOf(':'); + QString prefix = (colon < 0) ? lhs.toLower() : lhs.left(colon).trimmed().toLower(); + QString name = (colon < 0) ? QString() : lhs.mid(colon + 1).trimmed(); + + KeySpec ks; + ks.name = name; + + // 'channel:public' is a special-case alias for the public PSK. + if (prefix == "channel" && rhs.compare("public", Qt::CaseInsensitive) == 0) { + ks.kind = KeySpec::Channel; + if (!validateOnly) { + ks.data = builders::publicChannelPsk(); + ks.name = name.isEmpty() ? QStringLiteral("public") : name; + } + outKeys.append(ks); + continue; + } + + QByteArray bytes = QByteArray::fromHex(rhs.toLatin1()); + if (bytes.isEmpty()) { + error = QString("invalid hex value for '%1'").arg(lhs); + return false; + } + + if (prefix == "identity") { + if (bytes.size() != kPubKeySize) { + error = QString("identity must be %1 hex bytes, got %2") + .arg(kPubKeySize).arg(bytes.size()); + return false; + } + ks.kind = KeySpec::Identity; + } else if (prefix == "contact") { + if (bytes.size() != kPubKeySize) { + error = QString("contact:%1 must be %2 hex bytes, got %3") + .arg(name).arg(kPubKeySize).arg(bytes.size()); + return false; + } + if (name.isEmpty()) { + error = "contact: requires a name"; + return false; + } + ks.kind = KeySpec::Contact; + } else if (prefix == "channel") { + if (bytes.size() != 16 && bytes.size() != 32) { + error = QString("channel:%1 must be 16 or 32 hex bytes, got %2") + .arg(name).arg(bytes.size()); + return false; + } + if (name.isEmpty()) { + error = "channel: requires a name"; + return false; + } + ks.kind = KeySpec::Channel; + } else { + error = QString("unknown key prefix: '%1' (expected identity|contact|channel)").arg(prefix); + return false; + } + + if (!validateOnly) { + ks.data = bytes; + } + outKeys.append(ks); + } + return true; +} + +// ---- ADVERT decoder ----------------------------------------------------------- + +bool decodeAdvert(const QByteArray& wire, const ParsedHeader& hdr, DecodeResult& out) +{ + if (hdr.payloadType != PayloadAdvert) { + return false; + } + + const int off = hdr.payloadOffset; + const int needBase = kPubKeySize + 4 + kSignatureSize + 1; // pub + ts + sig + flags + if (off + needBase > wire.size()) { + return false; + } + + const QByteArray pubKey = wire.mid(off, kPubKeySize); + const uint32_t ts = readU32LE(wire, off + kPubKeySize); + const QByteArray sig = wire.mid(off + kPubKeySize + 4, kSignatureSize); + + const int appOff = off + kPubKeySize + 4 + kSignatureSize; + const uint8_t flags = static_cast(wire[appOff]); + QByteArray appData = wire.mid(appOff); + + addField(out, "advert.pubkey", hexOf(pubKey)); + addField(out, "advert.timestamp", QString::number(ts)); + addField(out, "advert.flags", QString("0x%1").arg(flags, 2, 16, QChar('0'))); + + int nameOff = appOff + 1; + if (flags & AdvertHasLocation) { + if (nameOff + 8 <= wire.size()) { + const int32_t latI = readI32LE(wire, nameOff); + const int32_t lonI = readI32LE(wire, nameOff + 4); + addField(out, "advert.lat", QString::number(latI / 1e6, 'f', 6)); + addField(out, "advert.lon", QString::number(lonI / 1e6, 'f', 6)); + } + nameOff += 8; + } + if (flags & AdvertHasFeature1) { nameOff += 2; } + if (flags & AdvertHasFeature2) { nameOff += 2; } + if ((flags & AdvertHasName) && nameOff < wire.size()) { + QByteArray rawName = wire.mid(nameOff, 32); + while (!rawName.isEmpty() && rawName.endsWith('\0')) { + rawName.chop(1); + } + addField(out, "advert.name", QString::fromUtf8(rawName)); + } + + // Verify Ed25519 signature: sign target = pubkey || ts || appData + QByteArray msg; + msg.reserve(kPubKeySize + 4 + appData.size()); + msg.append(pubKey); + msg.append(wire.mid(off + kPubKeySize, 4)); + msg.append(appData); + const bool sigOk = detail::verifyEd25519(sig, pubKey, msg); + addField(out, "advert.signature_valid", sigOk ? QStringLiteral("true") : QStringLiteral("false")); + + out.dataDecoded = true; + out.summary = QString("MESHCORE RX|advert pub=%1 ts=%2%3") + .arg(hexOf(pubKey).left(16)) + .arg(ts) + .arg(sigOk ? QString() : QStringLiteral(" SIG_INVALID")); + return true; +} + +// ---- ACK decoder -------------------------------------------------------------- + +bool decodeAck(const QByteArray& wire, const ParsedHeader& hdr, DecodeResult& out) +{ + if (hdr.payloadType != PayloadAck) { + return false; + } + const int off = hdr.payloadOffset; + if (off + 1 + 4 > wire.size()) { + return false; + } + const QByteArray destHash = wire.mid(off, 1); + const QByteArray msgHash = wire.mid(off + 1, 4); + addField(out, "ack.dest_hash", hexOf(destHash)); + addField(out, "ack.msg_hash", hexOf(msgHash)); + out.dataDecoded = true; + out.summary = QString("MESHCORE RX|ack dest=%1 hash=%2") + .arg(hexOf(destHash)) + .arg(hexOf(msgHash)); + return true; +} + +// ---- TXT_MSG decoder ---------------------------------------------------------- + +bool decodeTxtMsg(const QByteArray& wire, const ParsedHeader& hdr, + const QVector& keys, DecodeResult& out) +{ + if (hdr.payloadType != PayloadTxt) { + return false; + } + + const int off = hdr.payloadOffset; + if (off + 2 + kCipherMacSize + kCipherBlockSize > wire.size()) { + return false; + } + + const QByteArray destHash = wire.mid(off, 1); + const QByteArray srcHash = wire.mid(off + 1, 1); + const QByteArray macThenCt = wire.mid(off + 2); + + addField(out, "txt.dest_hash", hexOf(destHash)); + addField(out, "txt.src_hash", hexOf(srcHash)); + + out.dataDecoded = true; + out.summary = QString("MESHCORE RX|txt_msg dest=%1 src=%2") + .arg(hexOf(destHash)) + .arg(hexOf(srcHash)); + + // Find our identity (if any) — required for ECDH trial-decryption. + QByteArray ourSeed; + for (const KeySpec& k : keys) { + if (k.kind == KeySpec::Identity) { ourSeed = k.data; break; } + } + if (ourSeed.isEmpty()) { + return true; // Without identity we can only show the envelope. + } + + // Trial-decrypt against every known contact pubkey. + for (const KeySpec& k : keys) { + if (k.kind != KeySpec::Contact) { + continue; + } + const QByteArray secret = detail::sharedSecret(ourSeed, k.data); + if (secret.size() != kPubKeySize) { + continue; + } + const QByteArray pt = detail::macThenDecrypt(secret, macThenCt); + if (pt.isEmpty()) { + continue; + } + uint32_t ts; uint8_t txtType, attempt; QString text; + if (!extractTxtPlaintext(pt, ts, txtType, attempt, text)) { + continue; + } + addField(out, "txt.timestamp", QString::number(ts)); + addField(out, "txt.txt_type", txtTypeName(txtType)); + addField(out, "txt.attempt", attempt); + addField(out, "txt.text", text); + addField(out, "txt.sender_pubkey", hexOf(k.data)); + addField(out, "txt.sender_name", k.name); + out.decrypted = true; + out.keyLabel = QString("contact:%1").arg(k.name); + out.summary = QString("MESHCORE RX|txt_msg from=%1 ts=%2 text=\"%3\"") + .arg(k.name).arg(ts).arg(text); + return true; + } + + return true; // envelope decoded, decryption failed (no matching contact) +} + +// ---- GRP_TXT decoder ---------------------------------------------------------- + +bool decodeGrpTxt(const QByteArray& wire, const ParsedHeader& hdr, + const QVector& keys, DecodeResult& out) +{ + if (hdr.payloadType != PayloadGrpTxt) { + return false; + } + + const int off = hdr.payloadOffset; + if (off + 1 + kCipherMacSize + kCipherBlockSize > wire.size()) { + return false; + } + + const QByteArray channelHash = wire.mid(off, 1); + const QByteArray macThenCt = wire.mid(off + 1); + + addField(out, "grp.channel_hash", hexOf(channelHash)); + out.dataDecoded = true; + out.summary = QString("MESHCORE RX|grp_txt hash=%1").arg(hexOf(channelHash)); + + // Trial-decrypt against every channel PSK. + for (const KeySpec& k : keys) { + if (k.kind != KeySpec::Channel) { + continue; + } + const builders::GroupChannel gc = builders::GroupChannel::fromPsk(k.name, k.data); + if (!gc.isValid() || gc.hash != channelHash) { + continue; + } + const QByteArray pt = detail::macThenDecrypt(gc.secret, macThenCt); + if (pt.isEmpty()) { + continue; + } + uint32_t ts; uint8_t txtType, attempt; QString text; + if (!extractTxtPlaintext(pt, ts, txtType, attempt, text)) { + continue; + } + addField(out, "grp.channel", k.name); + addField(out, "grp.timestamp", QString::number(ts)); + addField(out, "grp.txt_type", txtTypeName(txtType)); + addField(out, "grp.attempt", attempt); + addField(out, "grp.text", text); + out.decrypted = true; + out.keyLabel = QString("channel:%1").arg(k.name); + out.summary = QString("MESHCORE RX|grp_txt channel=%1 ts=%2 text=\"%3\"") + .arg(k.name).arg(ts).arg(text); + return true; + } + + return true; +} + +// ---- ANON_REQ decoder --------------------------------------------------------- + +bool decodeAnonReq(const QByteArray& wire, const ParsedHeader& hdr, + const QVector& keys, DecodeResult& out) +{ + if (hdr.payloadType != PayloadAnonReq) { + return false; + } + const int off = hdr.payloadOffset; + if (off + 1 + kPubKeySize + kCipherMacSize + kCipherBlockSize > wire.size()) { + return false; + } + + const QByteArray destHash = wire.mid(off, 1); + const QByteArray senderPub = wire.mid(off + 1, kPubKeySize); + const QByteArray macThenCt = wire.mid(off + 1 + kPubKeySize); + + addField(out, "anon.dest_hash", hexOf(destHash)); + addField(out, "anon.sender_pubkey", hexOf(senderPub)); + + out.dataDecoded = true; + out.summary = QString("MESHCORE RX|anon_req dest=%1 sender=%2") + .arg(hexOf(destHash)) + .arg(hexOf(senderPub).left(16)); + + // Need our identity to derive the ECDH secret. + QByteArray ourSeed; + for (const KeySpec& k : keys) { + if (k.kind == KeySpec::Identity) { ourSeed = k.data; break; } + } + if (ourSeed.isEmpty()) { + return true; + } + + const QByteArray secret = detail::sharedSecret(ourSeed, senderPub); + if (secret.size() != kPubKeySize) { + return true; + } + const QByteArray pt = detail::macThenDecrypt(secret, macThenCt); + if (pt.isEmpty()) { + return true; + } + if (pt.size() < 4) { + return true; + } + + const uint32_t ts = qFromLittleEndian(reinterpret_cast(pt.constData())); + QByteArray data = pt.mid(4); + while (!data.isEmpty() && data.endsWith('\0')) { + data.chop(1); + } + + addField(out, "anon.timestamp", QString::number(ts)); + addField(out, "anon.data_hex", hexOf(data)); + // Show as text if it parses as UTF-8 cleanly + addField(out, "anon.text", QString::fromUtf8(data)); + out.decrypted = true; + out.summary = QString("MESHCORE RX|anon_req from=%1 ts=%2 len=%3") + .arg(hexOf(senderPub).left(16)) + .arg(ts) + .arg(data.size()); + return true; +} + +} // namespace decoder +} // namespace modemmeshcore diff --git a/modemmeshcore/meshcore_decoder.h b/modemmeshcore/meshcore_decoder.h new file mode 100644 index 000000000..c4dbdba18 --- /dev/null +++ b/modemmeshcore/meshcore_decoder.h @@ -0,0 +1,113 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore wire-packet decoder. Inverse of meshcore_builders. // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef MODEMMESHCORE_MESHCORE_DECODER_H_ +#define MODEMMESHCORE_MESHCORE_DECODER_H_ + +#include +#include +#include + +#include "export.h" +#include "meshcorepacket.h" + +namespace modemmeshcore +{ +namespace decoder +{ + +// Parsed envelope header (fields populated from the first 1+optional-4+1+N +// bytes on the wire). +struct ParsedHeader +{ + uint8_t routeType = 0; + uint8_t payloadType = 0; + uint8_t version = 0; + bool hasTransport = false; + int pathLenByte = 0; // raw path_len byte + int payloadOffset = 0; // byte offset into the wire packet at which + // the payload-type-specific body begins +}; + +// Returns true on success, false if the wire packet is too short or +// malformed. Populates hdr. +MODEMMESHCORE_API bool parseHeader(const QByteArray& wire, ParsedHeader& hdr); + +// Key spec list — the keys an operator has loaded for the RX side. Used by +// Packet::decodeFrame to trial-decrypt incoming encrypted frames. +// +// String format (case-insensitive keys, semicolon-separated entries): +// identity= our 32-byte Ed25519 seed +// contact:= a known peer's 32-byte Ed25519 pubkey +// channel:= a group channel PSK +// +// Example: +// identity=01..20; contact:alice=ab..cd; channel:public=8b3387e9...cd72 +struct KeySpec +{ + enum Kind { Identity, Contact, Channel }; + Kind kind = Identity; + QString name; // identifier (empty for Identity, non-empty for Contact/Channel) + QByteArray data; // raw bytes +}; + +// Parse the key spec list. Returns true on success. Sets error on failure. +// If validateOnly, don't store data into outKeys. +MODEMMESHCORE_API bool parseKeySpecList( + const QString& spec, + QVector& outKeys, + QString& error, + bool validateOnly = false); + +// ---- per-payload decoders ----------------------------------------------------- + +// ADVERT (no key needed). Populates fields: +// advert.pubkey (hex) +// advert.timestamp +// advert.flags +// advert.name (if HasName flag set) +// advert.lat / advert.lon (if HasLocation flag set) +// advert.signature_valid (Ed25519 verify) +MODEMMESHCORE_API bool decodeAdvert(const QByteArray& wire, const ParsedHeader& hdr, DecodeResult& out); + +// ACK (no key needed). Populates: +// ack.dest_hash (hex 1 byte) +// ack.msg_hash (hex 4 bytes) +MODEMMESHCORE_API bool decodeAck(const QByteArray& wire, const ParsedHeader& hdr, DecodeResult& out); + +// TXT_MSG. Trial-decrypts against (identity, contact pubkey) pairs. +// On success: txt.text, txt.sender_pubkey (hex), txt.timestamp, txt.txt_type +// Always populates: txt.dest_hash, txt.src_hash +MODEMMESHCORE_API bool decodeTxtMsg(const QByteArray& wire, + const ParsedHeader& hdr, + const QVector& keys, + DecodeResult& out); + +// GRP_TXT. Trial-decrypts against channel PSKs. +// On success: grp.text, grp.channel, grp.timestamp, grp.txt_type +// Always: grp.channel_hash +MODEMMESHCORE_API bool decodeGrpTxt(const QByteArray& wire, + const ParsedHeader& hdr, + const QVector& keys, + DecodeResult& out); + +// ANON_REQ. Decrypts using identity + sender pubkey embedded in the packet. +// On success: anon.text/data, anon.sender_pubkey, anon.timestamp +// Always: anon.dest_hash +MODEMMESHCORE_API bool decodeAnonReq(const QByteArray& wire, + const ParsedHeader& hdr, + const QVector& keys, + DecodeResult& out); + +} // namespace decoder +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_MESHCORE_DECODER_H_ diff --git a/modemmeshcore/meshcore_identity.cpp b/modemmeshcore/meshcore_identity.cpp new file mode 100644 index 000000000..b9e82f0a6 --- /dev/null +++ b/modemmeshcore/meshcore_identity.cpp @@ -0,0 +1,144 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcore_identity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "meshcore_crypto.h" + +#ifndef Q_OS_WIN +#include +#endif + +namespace modemmeshcore +{ +namespace identity +{ + +namespace +{ + +constexpr int kSeedSize = 32; +constexpr int kPubSize = 32; +constexpr int kFileSize = kSeedSize + kPubSize; + +bool ensureParentDir(const QString& path) +{ + QFileInfo fi(path); + QDir parent = fi.dir(); + if (parent.exists()) return true; + return parent.mkpath(QStringLiteral(".")); +} + +void chmodOwnerOnly(const QString& path) +{ +#ifndef Q_OS_WIN + ::chmod(QFile::encodeName(path).constData(), S_IRUSR | S_IWUSR); +#else + Q_UNUSED(path); +#endif +} + +} // anonymous + +QString defaultIdentityPath() +{ + QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (base.isEmpty()) { + base = QDir::homePath(); + } + return QDir::cleanPath(base + QStringLiteral("/meshcore/identity.bin")); +} + +Identity loadIdentity(const QString& path) +{ + Identity id; + QFile f(path); + if (!f.exists()) return id; + if (!f.open(QIODevice::ReadOnly)) return id; + const QByteArray blob = f.readAll(); + f.close(); + if (blob.size() != kFileSize) return id; + + QByteArray seed = blob.left(kSeedSize); + QByteArray pub = blob.mid(kSeedSize, kPubSize); + + // Validate seed→pub re-derivation matches stored pub. + QByteArray derived = detail::derivePubKey(seed); + if (derived.size() != kPubSize || derived != pub) { + return id; + } + + id.seed = seed; + id.pub = pub; + return id; +} + +bool saveIdentity(const Identity& id, const QString& path) +{ + if (!id.isValid()) return false; + if (!ensureParentDir(path)) return false; + + QSaveFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false; + QByteArray blob; + blob.reserve(kFileSize); + blob.append(id.seed); + blob.append(id.pub); + if (f.write(blob) != kFileSize) { + f.cancelWriting(); + return false; + } + if (!f.commit()) return false; + + chmodOwnerOnly(path); + return true; +} + +Identity generateIdentity() +{ + QByteArray seed(kSeedSize, Qt::Uninitialized); + QRandomGenerator* rng = QRandomGenerator::system(); + // Fill 32 bytes from the system CSPRNG via 8 quint32 draws. + quint32* p32 = reinterpret_cast(seed.data()); + for (int i = 0; i < kSeedSize / 4; ++i) { + p32[i] = rng->generate(); + } + Identity id; + id.seed = seed; + id.pub = detail::derivePubKey(seed); + return id; +} + +Identity loadOrCreateIdentity(const QString& path) +{ + Identity id = loadIdentity(path); + if (id.isValid()) return id; + id = generateIdentity(); + saveIdentity(id, path); + return id; +} + +QString defaultNodeNameFor(const Identity& id) +{ + if (!id.isValid()) return QStringLiteral("SDRangel"); + return QStringLiteral("SDRangel-") + id.pubShort(); +} + +} // namespace identity +} // namespace modemmeshcore diff --git a/modemmeshcore/meshcore_identity.h b/modemmeshcore/meshcore_identity.h new file mode 100644 index 000000000..d7109f85b --- /dev/null +++ b/modemmeshcore/meshcore_identity.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore on-host identity store. Persistent 32-byte Ed25519 seed + // +// derived 32-byte public key, loaded from / saved to a 64-byte file at // +// AppDataLocation/meshcore/identity.bin. // +// // +// Shared between modmeshcore (TX seed source for builders::buildAdvert / // +// buildTxtMsg / ...) and demodmeshcore (default identity= entry in the // +// KeysDialog so trial-decrypt against our prv key works without manual setup). // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef MODEMMESHCORE_MESHCORE_IDENTITY_H_ +#define MODEMMESHCORE_MESHCORE_IDENTITY_H_ + +#include +#include + +#include "export.h" + +namespace modemmeshcore +{ +namespace identity +{ + +struct MODEMMESHCORE_API Identity +{ + QByteArray seed; // 32 bytes (Ed25519 seed; treated as private) + QByteArray pub; // 32 bytes (derived from seed) + + bool isValid() const + { + return seed.size() == 32 && pub.size() == 32; + } + + // Helpers — convenience for callers that want hex strings. + QString seedHex() const { return QString::fromLatin1(seed.toHex()); } + QString pubHex() const { return QString::fromLatin1(pub.toHex()); } + + // First 8 hex chars of the public key — used for the default node name + // suffix ("SDRangel-73ccef30") and any short-display label. + QString pubShort() const + { + return pub.size() == 32 ? QString::fromLatin1(pub.left(4).toHex()) : QString(); + } +}; + +// Default on-disk path: /meshcore/identity.bin +// macOS: ~/Library/Application Support/SDRangel/meshcore/identity.bin +// Linux: ~/.local/share/SDRangel/meshcore/identity.bin +// Windows: %APPDATA%/SDRangel/meshcore/identity.bin +MODEMMESHCORE_API QString defaultIdentityPath(); + +// Load existing identity. Returns an invalid Identity (isValid() == false) +// if the file is missing, wrong size (must be 64), or fails the +// seed → pub re-derivation check. +MODEMMESHCORE_API Identity loadIdentity(const QString& path); + +// Save identity to disk. Creates parent directories as needed. Sets file +// mode 0600 on POSIX (best-effort on Windows). Returns false on I/O error +// or invalid input. +MODEMMESHCORE_API bool saveIdentity(const Identity& id, const QString& path); + +// Generate a new random Identity. Uses QRandomGenerator::system() (cryptographic +// quality on platforms that support it). Always returns a valid Identity. +MODEMMESHCORE_API Identity generateIdentity(); + +// Load if file exists and is valid; otherwise generate + save + return. +// Common entry point for both RX and TX. +MODEMMESHCORE_API Identity loadOrCreateIdentity(const QString& path); + +// Format a default node name from a public key: "SDRangel-<8hex>". +MODEMMESHCORE_API QString defaultNodeNameFor(const Identity& id); + +} // namespace identity +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_MESHCORE_IDENTITY_H_ diff --git a/modemmeshcore/meshcorepacket.cpp b/modemmeshcore/meshcorepacket.cpp new file mode 100644 index 000000000..33ed0d77a --- /dev/null +++ b/modemmeshcore/meshcorepacket.cpp @@ -0,0 +1,165 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// MeshCore packet entry point. Wires the public Packet API to the internal // +// command parser + builders + (eventually) decoder. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcorepacket.h" +#include "meshcore_command.h" +#include "meshcore_decoder.h" + +#include +#include + +namespace modemmeshcore +{ + +namespace +{ + +constexpr const char* kCommandPrefix = "MESHCORE:"; + +// Strip the leading "MESHCORE:" prefix and return the remaining body. +QString stripPrefix(const QString& cmd) +{ + QString t = cmd.trimmed(); + if (t.startsWith(QString::fromLatin1(kCommandPrefix), Qt::CaseInsensitive)) { + return t.mid(static_cast(strlen(kCommandPrefix))).trimmed(); + } + return QString(); +} + +} // namespace + +QString Packet::defaultKeysFromEnv() +{ + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + const QString keys = env.value(QStringLiteral("SDRANGEL_MESHCORE_KEYS")).trimmed(); + if (!keys.isEmpty()) { + qInfo("modemmeshcore::Packet::defaultKeysFromEnv: using SDRANGEL_MESHCORE_KEYS"); + return keys; + } + return {}; +} + +bool Packet::isCommand(const QString& text) +{ + return text.trimmed().startsWith(QString::fromLatin1(kCommandPrefix), Qt::CaseInsensitive); +} + +bool Packet::buildFrameFromCommand(const QString& command, QByteArray& frame, QString& summary, QString& error) +{ + const QString body = stripPrefix(command); + if (body.isEmpty()) { + error = "missing MESHCORE: prefix or empty body"; + return false; + } + + QMap kv; + if (!command::tokenize(body, kv, error)) { + return false; + } + + auto typeIt = kv.find(QStringLiteral("type")); + if (typeIt == kv.end()) { + error = "missing 'type=' (expected advert|txt_msg|anon_req|grp_txt|ack)"; + return false; + } + + return command::buildFrameByType(typeIt.value(), kv, frame, summary, error); +} + +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(); + + decoder::ParsedHeader hdr; + if (!decoder::parseHeader(frame, hdr)) { + return false; + } + result.isFrame = true; + result.routeType = hdr.routeType; + result.payloadType = hdr.payloadType; + result.payloadVer = hdr.version; + + QVector keys; + if (!keySpecList.isEmpty()) { + QString tmpErr; + if (!decoder::parseKeySpecList(keySpecList, keys, tmpErr, /*validateOnly=*/false)) { + // Bad key list — keep going on the envelope-only path. + qWarning() << "modemmeshcore::Packet::decodeFrame: invalid key list:" << tmpErr; + keys.clear(); + } + } + + switch (hdr.payloadType) + { + case PayloadAdvert: return decoder::decodeAdvert(frame, hdr, result); + case PayloadAck: return decoder::decodeAck(frame, hdr, result); + case PayloadTxt: return decoder::decodeTxtMsg(frame, hdr, keys, result); + case PayloadGrpTxt: return decoder::decodeGrpTxt(frame, hdr, keys, result); + case PayloadAnonReq: return decoder::decodeAnonReq(frame, hdr, keys, result); + default: + // Envelope decoded; payload-type-specific decoder not implemented yet. + return true; + } +} + +bool Packet::validateKeySpecList(const QString& keySpecList, QString& error, int* keyCount) +{ + QVector keys; + if (!decoder::parseKeySpecList(keySpecList, keys, error, /*validateOnly=*/true)) { + if (keyCount) { + *keyCount = 0; + } + return false; + } + if (keyCount) { + *keyCount = keys.size(); + } + if (keys.isEmpty()) { + error = "no keys found"; + return false; + } + error.clear(); + return true; +} + +bool Packet::deriveTxRadioSettings(const QString& command, TxRadioSettings& settings, QString& error) +{ + settings = TxRadioSettings(); // MeshCore EU defaults + settings.summary = QStringLiteral("MeshCore EU defaults"); + + if (!isCommand(command)) { + return true; // not a command -> defaults stand + } + + const QString body = stripPrefix(command); + QMap kv; + QString tokErr; + if (!command::tokenize(body, kv, tokErr)) { + error = tokErr; + return false; + } + + if (!command::applyRadioParams(kv, settings, error)) { + return false; + } + if (settings.hasCommand) { + settings.summary = QStringLiteral("MeshCore radio override applied"); + } + return true; +} + +} // namespace modemmeshcore diff --git a/modemmeshcore/meshcorepacket.h b/modemmeshcore/meshcorepacket.h new file mode 100644 index 000000000..1e19dd7fd --- /dev/null +++ b/modemmeshcore/meshcorepacket.h @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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 MODEMMESHCORE_MESHCOREPACKET_H_ +#define MODEMMESHCORE_MESHCOREPACKET_H_ + +#include +#include +#include +#include +#include + +#include + +#include "export.h" + +namespace modemmeshcore +{ + +// MeshCore default radio params (EU primary): 869.618 MHz / 62.5 kHz / SF 8 / CR 8 +constexpr int kDefaultBandwidthHz = 62500; +constexpr int kDefaultSpreadFactor = 8; +constexpr int kDefaultParityBits = 4; // 4 -> CR 4/8 +constexpr uint8_t kDefaultSyncWord = 0x12; // MeshCore wire sync (verify against firmware) +constexpr int kDefaultPreambleChirps = 16; // MeshCore minimum: 16 for SF>8, 32 for SF<9 +constexpr qint64 kDefaultCenterFrequencyHz = 869618000LL; + +// ---- Crypto sizes (MeshCore wire spec) ---- +constexpr int kPubKeySize = 32; // Ed25519 / X25519 public key +constexpr int kPrvKeySize = 64; // expanded Ed25519 private key +constexpr int kSignatureSize = 64; // Ed25519 signature +constexpr int kCipherKeySize = 16; // AES-128 +constexpr int kCipherBlockSize = 16; +constexpr int kCipherMacSize = 2; // HMAC-SHA256 truncated to 2 bytes + +// ---- Header byte (first OTA byte) layout ---- +// bits [1:0] route_type (4 values) +// bits [5:2] payload_type (12 named + reserved) +// bits [7:6] version (currently 0) +constexpr uint8_t kRouteMask = 0x03; +constexpr int kPayloadTypeShift = 2; +constexpr uint8_t kPayloadTypeMask = 0x0F; +constexpr int kVersionShift = 6; +constexpr uint8_t kVersionMask = 0x03; + +enum RouteType : uint8_t +{ + RouteTFlood = 0x00, + RouteFlood = 0x01, + RouteDirect = 0x02, + RouteTDirect = 0x03, +}; + +enum PayloadType : uint8_t +{ + PayloadReq = 0x00, + PayloadResp = 0x01, + PayloadTxt = 0x02, + PayloadAck = 0x03, + PayloadAdvert = 0x04, + PayloadGrpTxt = 0x05, + PayloadGrpData = 0x06, + PayloadAnonReq = 0x07, + PayloadPath = 0x08, + PayloadTrace = 0x09, + PayloadMulti = 0x0A, + PayloadCtrl = 0x0B, + PayloadRawCustom = 0x0F, +}; + +// ---- Path packing ---- +// path_len byte: +// bits [7:6] hash size selector: 0=>1B, 1=>2B, 2=>3B, 3=>reserved +// bits [5:0] hash count (0..63) +// total path bytes on wire = hash_count * hash_size +constexpr uint8_t kPathModeMask = 0xC0; +constexpr int kPathModeShift = 6; +constexpr uint8_t kPathCountMask = 0x3F; + +struct PathLen +{ + int hashCount; // number of hop hashes (0..63) + int hashSize; // bytes per hash (1, 2, or 3) + int totalBytes; // hashCount * hashSize +}; + +MODEMMESHCORE_API PathLen decodePathLen(uint8_t raw); +MODEMMESHCORE_API uint8_t encodePathLen(int hashCount, int hashSize); + +// ---- ADVERT app-data flags ---- +enum AdvertFlags : uint8_t +{ + AdvertNodeChat = 0x01, + AdvertNodeRepeater = 0x02, + AdvertNodeRoom = 0x03, + AdvertNodeSensor = 0x04, + AdvertHasLocation = 0x10, + AdvertHasFeature1 = 0x20, + AdvertHasFeature2 = 0x40, + AdvertHasName = 0x80, +}; + +struct MODEMMESHCORE_API DecodeResult +{ + struct Field + { + QString path; + QString value; + }; + + bool isFrame = false; + bool dataDecoded = false; // any meaningful payload-level decode produced + bool decrypted = false; + uint8_t routeType = 0; + uint8_t payloadType = 0; + uint8_t payloadVer = 0; + QString keyLabel; + QString summary; + QVector fields; +}; + +struct MODEMMESHCORE_API TxRadioSettings +{ + bool hasCommand = false; + bool hasLoRaParams = false; + int bandwidthHz = kDefaultBandwidthHz; + int spreadFactor = kDefaultSpreadFactor; + int parityBits = kDefaultParityBits; + int deBits = 0; + uint8_t syncWord = kDefaultSyncWord; + int preambleChirps = kDefaultPreambleChirps; + + bool hasCenterFrequency = false; + qint64 centerFrequencyHz = kDefaultCenterFrequencyHz; + + QString summary; +}; + +class MODEMMESHCORE_API 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 QString defaultKeysFromEnv(); + + static bool deriveTxRadioSettings( + const QString& command, + TxRadioSettings& settings, + QString& error + ); +}; + +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_MESHCOREPACKET_H_ diff --git a/modemmeshcore/monocypher-ed25519.c b/modemmeshcore/monocypher-ed25519.c new file mode 100644 index 000000000..9ab8046f3 --- /dev/null +++ b/modemmeshcore/monocypher-ed25519.c @@ -0,0 +1,500 @@ +// Monocypher version __git__ +// +// This file is dual-licensed. Choose whichever licence you want from +// the two licences listed below. +// +// The first licence is a regular 2-clause BSD licence. The second licence +// is the CC-0 from Creative Commons. It is intended to release Monocypher +// to the public domain. The BSD licence serves as a fallback option. +// +// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0 +// +// ------------------------------------------------------------------------ +// +// Copyright (c) 2017-2019, Loup Vaillant +// All rights reserved. +// +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ------------------------------------------------------------------------ +// +// Written in 2017-2019 by Loup Vaillant +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// + +#include "monocypher-ed25519.h" + +#ifdef MONOCYPHER_CPP_NAMESPACE +namespace MONOCYPHER_CPP_NAMESPACE { +#endif + +///////////////// +/// Utilities /// +///////////////// +#define FOR(i, min, max) for (size_t i = min; i < max; i++) +#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_] +#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0 +#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx))) +#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer)) +#define MIN(a, b) ((a) <= (b) ? (a) : (b)) +typedef uint8_t u8; +typedef uint64_t u64; + +// Returns the smallest positive integer y such that +// (x + y) % pow_2 == 0 +// Basically, it's how many bytes we need to add to "align" x. +// Only works when pow_2 is a power of 2. +// Note: we use ~x+1 instead of -x to avoid compiler warnings +static size_t align(size_t x, size_t pow_2) +{ + return (~x + 1) & (pow_2 - 1); +} + +static u64 load64_be(const u8 s[8]) +{ + return((u64)s[0] << 56) + | ((u64)s[1] << 48) + | ((u64)s[2] << 40) + | ((u64)s[3] << 32) + | ((u64)s[4] << 24) + | ((u64)s[5] << 16) + | ((u64)s[6] << 8) + | (u64)s[7]; +} + +static void store64_be(u8 out[8], u64 in) +{ + out[0] = (u8)(in >> 56); + out[1] = (u8)(in >> 48); + out[2] = (u8)(in >> 40); + out[3] = (u8)(in >> 32); + out[4] = (u8)(in >> 24); + out[5] = (u8)(in >> 16); + out[6] = (u8)(in >> 8); + out[7] = (u8) in ; +} + +static void load64_be_buf (u64 *dst, const u8 *src, size_t size) { + FOR(i, 0, size) { dst[i] = load64_be(src + i*8); } +} + +/////////////// +/// SHA 512 /// +/////////////// +static u64 rot(u64 x, int c ) { return (x >> c) | (x << (64 - c)); } +static u64 ch (u64 x, u64 y, u64 z) { return (x & y) ^ (~x & z); } +static u64 maj(u64 x, u64 y, u64 z) { return (x & y) ^ ( x & z) ^ (y & z); } +static u64 big_sigma0(u64 x) { return rot(x, 28) ^ rot(x, 34) ^ rot(x, 39); } +static u64 big_sigma1(u64 x) { return rot(x, 14) ^ rot(x, 18) ^ rot(x, 41); } +static u64 lit_sigma0(u64 x) { return rot(x, 1) ^ rot(x, 8) ^ (x >> 7); } +static u64 lit_sigma1(u64 x) { return rot(x, 19) ^ rot(x, 61) ^ (x >> 6); } + +static const u64 K[80] = { + 0x428a2f98d728ae22,0x7137449123ef65cd,0xb5c0fbcfec4d3b2f,0xe9b5dba58189dbbc, + 0x3956c25bf348b538,0x59f111f1b605d019,0x923f82a4af194f9b,0xab1c5ed5da6d8118, + 0xd807aa98a3030242,0x12835b0145706fbe,0x243185be4ee4b28c,0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f,0x80deb1fe3b1696b1,0x9bdc06a725c71235,0xc19bf174cf692694, + 0xe49b69c19ef14ad2,0xefbe4786384f25e3,0x0fc19dc68b8cd5b5,0x240ca1cc77ac9c65, + 0x2de92c6f592b0275,0x4a7484aa6ea6e483,0x5cb0a9dcbd41fbd4,0x76f988da831153b5, + 0x983e5152ee66dfab,0xa831c66d2db43210,0xb00327c898fb213f,0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2,0xd5a79147930aa725,0x06ca6351e003826f,0x142929670a0e6e70, + 0x27b70a8546d22ffc,0x2e1b21385c26c926,0x4d2c6dfc5ac42aed,0x53380d139d95b3df, + 0x650a73548baf63de,0x766a0abb3c77b2a8,0x81c2c92e47edaee6,0x92722c851482353b, + 0xa2bfe8a14cf10364,0xa81a664bbc423001,0xc24b8b70d0f89791,0xc76c51a30654be30, + 0xd192e819d6ef5218,0xd69906245565a910,0xf40e35855771202a,0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8,0x1e376c085141ab53,0x2748774cdf8eeb99,0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63,0x4ed8aa4ae3418acb,0x5b9cca4f7763e373,0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc,0x78a5636f43172f60,0x84c87814a1f0ab72,0x8cc702081a6439ec, + 0x90befffa23631e28,0xa4506cebde82bde9,0xbef9a3f7b2c67915,0xc67178f2e372532b, + 0xca273eceea26619c,0xd186b8c721c0c207,0xeada7dd6cde0eb1e,0xf57d4f7fee6ed178, + 0x06f067aa72176fba,0x0a637dc5a2c898a6,0x113f9804bef90dae,0x1b710b35131c471b, + 0x28db77f523047d84,0x32caab7b40c72493,0x3c9ebe0a15c9bebc,0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6,0x597f299cfc657e2a,0x5fcb6fab3ad6faec,0x6c44198c4a475817 +}; + +static void sha512_compress(crypto_sha512_ctx *ctx) +{ + u64 a = ctx->hash[0]; u64 b = ctx->hash[1]; + u64 c = ctx->hash[2]; u64 d = ctx->hash[3]; + u64 e = ctx->hash[4]; u64 f = ctx->hash[5]; + u64 g = ctx->hash[6]; u64 h = ctx->hash[7]; + + FOR (j, 0, 16) { + u64 in = K[j] + ctx->input[j]; + u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in; + u64 t2 = big_sigma0(a) + maj(a, b, c); + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + size_t i16 = 0; + FOR(i, 1, 5) { + i16 += 16; + FOR (j, 0, 16) { + ctx->input[j] += lit_sigma1(ctx->input[(j- 2) & 15]); + ctx->input[j] += lit_sigma0(ctx->input[(j-15) & 15]); + ctx->input[j] += ctx->input[(j- 7) & 15]; + u64 in = K[i16 + j] + ctx->input[j]; + u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in; + u64 t2 = big_sigma0(a) + maj(a, b, c); + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + } + + ctx->hash[0] += a; ctx->hash[1] += b; + ctx->hash[2] += c; ctx->hash[3] += d; + ctx->hash[4] += e; ctx->hash[5] += f; + ctx->hash[6] += g; ctx->hash[7] += h; +} + +// Write 1 input byte +static void sha512_set_input(crypto_sha512_ctx *ctx, u8 input) +{ + size_t word = ctx->input_idx >> 3; + size_t byte = ctx->input_idx & 7; + ctx->input[word] |= (u64)input << (8 * (7 - byte)); +} + +// Increment a 128-bit "word". +static void sha512_incr(u64 x[2], u64 y) +{ + x[1] += y; + if (x[1] < y) { + x[0]++; + } +} + +void crypto_sha512_init(crypto_sha512_ctx *ctx) +{ + ctx->hash[0] = 0x6a09e667f3bcc908; + ctx->hash[1] = 0xbb67ae8584caa73b; + ctx->hash[2] = 0x3c6ef372fe94f82b; + ctx->hash[3] = 0xa54ff53a5f1d36f1; + ctx->hash[4] = 0x510e527fade682d1; + ctx->hash[5] = 0x9b05688c2b3e6c1f; + ctx->hash[6] = 0x1f83d9abfb41bd6b; + ctx->hash[7] = 0x5be0cd19137e2179; + ctx->input_size[0] = 0; + ctx->input_size[1] = 0; + ctx->input_idx = 0; + ZERO(ctx->input, 16); +} + +void crypto_sha512_update(crypto_sha512_ctx *ctx, + const u8 *message, size_t message_size) +{ + // Avoid undefined NULL pointer increments with empty messages + if (message_size == 0) { + return; + } + + // Align ourselves with word boundaries + if ((ctx->input_idx & 7) != 0) { + size_t nb_bytes = MIN(align(ctx->input_idx, 8), message_size); + FOR (i, 0, nb_bytes) { + sha512_set_input(ctx, message[i]); + ctx->input_idx++; + } + message += nb_bytes; + message_size -= nb_bytes; + } + + // Align ourselves with block boundaries + if ((ctx->input_idx & 127) != 0) { + size_t nb_words = MIN(align(ctx->input_idx, 128), message_size) >> 3; + load64_be_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words); + ctx->input_idx += nb_words << 3; + message += nb_words << 3; + message_size -= nb_words << 3; + } + + // Compress block if needed + if (ctx->input_idx == 128) { + sha512_incr(ctx->input_size, 1024); // size is in bits + sha512_compress(ctx); + ctx->input_idx = 0; + ZERO(ctx->input, 16); + } + + // Process the message block by block + FOR (i, 0, message_size >> 7) { // number of blocks + load64_be_buf(ctx->input, message, 16); + sha512_incr(ctx->input_size, 1024); // size is in bits + sha512_compress(ctx); + ctx->input_idx = 0; + ZERO(ctx->input, 16); + message += 128; + } + message_size &= 127; + + if (message_size != 0) { + // Remaining words + size_t nb_words = message_size >> 3; + load64_be_buf(ctx->input, message, nb_words); + ctx->input_idx += nb_words << 3; + message += nb_words << 3; + message_size -= nb_words << 3; + + // Remaining bytes + FOR (i, 0, message_size) { + sha512_set_input(ctx, message[i]); + ctx->input_idx++; + } + } +} + +void crypto_sha512_final(crypto_sha512_ctx *ctx, u8 hash[64]) +{ + // Add padding bit + if (ctx->input_idx == 0) { + ZERO(ctx->input, 16); + } + sha512_set_input(ctx, 128); + + // Update size + sha512_incr(ctx->input_size, ctx->input_idx * 8); + + // Compress penultimate block (if any) + if (ctx->input_idx > 111) { + sha512_compress(ctx); + ZERO(ctx->input, 14); + } + // Compress last block + ctx->input[14] = ctx->input_size[0]; + ctx->input[15] = ctx->input_size[1]; + sha512_compress(ctx); + + // Copy hash to output (big endian) + FOR (i, 0, 8) { + store64_be(hash + i*8, ctx->hash[i]); + } + + WIPE_CTX(ctx); +} + +void crypto_sha512(u8 hash[64], const u8 *message, size_t message_size) +{ + crypto_sha512_ctx ctx; + crypto_sha512_init (&ctx); + crypto_sha512_update(&ctx, message, message_size); + crypto_sha512_final (&ctx, hash); +} + +//////////////////// +/// HMAC SHA 512 /// +//////////////////// +void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx, + const u8 *key, size_t key_size) +{ + // hash key if it is too long + if (key_size > 128) { + crypto_sha512(ctx->key, key, key_size); + key = ctx->key; + key_size = 64; + } + // Compute inner key: padded key XOR 0x36 + FOR (i, 0, key_size) { ctx->key[i] = key[i] ^ 0x36; } + FOR (i, key_size, 128) { ctx->key[i] = 0x36; } + // Start computing inner hash + crypto_sha512_init (&ctx->ctx); + crypto_sha512_update(&ctx->ctx, ctx->key, 128); +} + +void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx, + const u8 *message, size_t message_size) +{ + crypto_sha512_update(&ctx->ctx, message, message_size); +} + +void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, u8 hmac[64]) +{ + // Finish computing inner hash + crypto_sha512_final(&ctx->ctx, hmac); + // Compute outer key: padded key XOR 0x5c + FOR (i, 0, 128) { + ctx->key[i] ^= 0x36 ^ 0x5c; + } + // Compute outer hash + crypto_sha512_init (&ctx->ctx); + crypto_sha512_update(&ctx->ctx, ctx->key , 128); + crypto_sha512_update(&ctx->ctx, hmac, 64); + crypto_sha512_final (&ctx->ctx, hmac); // outer hash + WIPE_CTX(ctx); +} + +void crypto_sha512_hmac(u8 hmac[64], const u8 *key, size_t key_size, + const u8 *message, size_t message_size) +{ + crypto_sha512_hmac_ctx ctx; + crypto_sha512_hmac_init (&ctx, key, key_size); + crypto_sha512_hmac_update(&ctx, message, message_size); + crypto_sha512_hmac_final (&ctx, hmac); +} + +//////////////////// +/// HKDF SHA 512 /// +//////////////////// +void crypto_sha512_hkdf_expand(u8 *okm, size_t okm_size, + const u8 *prk, size_t prk_size, + const u8 *info, size_t info_size) +{ + int not_first = 0; + u8 ctr = 1; + u8 blk[64]; + + while (okm_size > 0) { + size_t out_size = MIN(okm_size, sizeof(blk)); + + crypto_sha512_hmac_ctx ctx; + crypto_sha512_hmac_init(&ctx, prk , prk_size); + if (not_first) { + // For some reason HKDF uses some kind of CBC mode. + // For some reason CTR mode alone wasn't enough. + // Like what, they didn't trust HMAC in 2010? Really?? + crypto_sha512_hmac_update(&ctx, blk , sizeof(blk)); + } + crypto_sha512_hmac_update(&ctx, info, info_size); + crypto_sha512_hmac_update(&ctx, &ctr, 1); + crypto_sha512_hmac_final(&ctx, blk); + + COPY(okm, blk, out_size); + + not_first = 1; + okm += out_size; + okm_size -= out_size; + ctr++; + } +} + +void crypto_sha512_hkdf(u8 *okm , size_t okm_size, + const u8 *ikm , size_t ikm_size, + const u8 *salt, size_t salt_size, + const u8 *info, size_t info_size) +{ + // Extract + u8 prk[64]; + crypto_sha512_hmac(prk, salt, salt_size, ikm, ikm_size); + + // Expand + crypto_sha512_hkdf_expand(okm, okm_size, prk, sizeof(prk), info, info_size); +} + +/////////////// +/// Ed25519 /// +/////////////// +void crypto_ed25519_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32]) +{ + u8 a[64]; + COPY(a, seed, 32); // a[ 0..31] = seed + crypto_wipe(seed, 32); + COPY(secret_key, a, 32); // secret key = seed + crypto_sha512(a, a, 32); // a[ 0..31] = scalar + crypto_eddsa_trim_scalar(a, a); // a[ 0..31] = trimmed scalar + crypto_eddsa_scalarbase(public_key, a); // public key = [trimmed scalar]B + COPY(secret_key + 32, public_key, 32); // secret key includes public half + WIPE_BUFFER(a); +} + +static void hash_reduce(u8 h[32], + const u8 *a, size_t a_size, + const u8 *b, size_t b_size, + const u8 *c, size_t c_size, + const u8 *d, size_t d_size) +{ + u8 hash[64]; + crypto_sha512_ctx ctx; + crypto_sha512_init (&ctx); + crypto_sha512_update(&ctx, a, a_size); + crypto_sha512_update(&ctx, b, b_size); + crypto_sha512_update(&ctx, c, c_size); + crypto_sha512_update(&ctx, d, d_size); + crypto_sha512_final (&ctx, hash); + crypto_eddsa_reduce(h, hash); +} + +static void ed25519_dom_sign(u8 signature[64], const u8 secret_key[64], + const u8 *dom, size_t dom_size, + const u8 *message, size_t message_size) +{ + u8 a[64]; // secret scalar and prefix + u8 r[32]; // secret deterministic "random" nonce + u8 h[32]; // publically verifiable hash of the message (not wiped) + u8 R[32]; // first half of the signature (allows overlapping inputs) + const u8 *pk = secret_key + 32; + + crypto_sha512(a, secret_key, 32); + crypto_eddsa_trim_scalar(a, a); + hash_reduce(r, dom, dom_size, a + 32, 32, message, message_size, 0, 0); + crypto_eddsa_scalarbase(R, r); + hash_reduce(h, dom, dom_size, R, 32, pk, 32, message, message_size); + COPY(signature, R, 32); + crypto_eddsa_mul_add(signature + 32, h, a, r); + + WIPE_BUFFER(a); + WIPE_BUFFER(r); +} + +void crypto_ed25519_sign(u8 signature [64], const u8 secret_key[64], + const u8 *message, size_t message_size) +{ + ed25519_dom_sign(signature, secret_key, 0, 0, message, message_size); +} + +int crypto_ed25519_check(const u8 signature[64], const u8 public_key[32], + const u8 *msg, size_t msg_size) +{ + u8 h_ram[32]; + hash_reduce(h_ram, signature, 32, public_key, 32, msg, msg_size, 0, 0); + return crypto_eddsa_check_equation(signature, public_key, h_ram); +} + +static const u8 domain[34] = "SigEd25519 no Ed25519 collisions\1"; + +void crypto_ed25519_ph_sign(uint8_t signature[64], const uint8_t secret_key[64], + const uint8_t message_hash[64]) +{ + ed25519_dom_sign(signature, secret_key, domain, sizeof(domain), + message_hash, 64); +} + +int crypto_ed25519_ph_check(const uint8_t sig[64], const uint8_t pk[32], + const uint8_t msg_hash[64]) +{ + u8 h_ram[32]; + hash_reduce(h_ram, domain, sizeof(domain), sig, 32, pk, 32, msg_hash, 64); + return crypto_eddsa_check_equation(sig, pk, h_ram); +} + + +#ifdef MONOCYPHER_CPP_NAMESPACE +} +#endif diff --git a/modemmeshcore/monocypher-ed25519.h b/modemmeshcore/monocypher-ed25519.h new file mode 100644 index 000000000..d7aa00412 --- /dev/null +++ b/modemmeshcore/monocypher-ed25519.h @@ -0,0 +1,140 @@ +// Monocypher version __git__ +// +// This file is dual-licensed. Choose whichever licence you want from +// the two licences listed below. +// +// The first licence is a regular 2-clause BSD licence. The second licence +// is the CC-0 from Creative Commons. It is intended to release Monocypher +// to the public domain. The BSD licence serves as a fallback option. +// +// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0 +// +// ------------------------------------------------------------------------ +// +// Copyright (c) 2017-2019, Loup Vaillant +// All rights reserved. +// +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ------------------------------------------------------------------------ +// +// Written in 2017-2019 by Loup Vaillant +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// + +#ifndef ED25519_H +#define ED25519_H + +#include "monocypher.h" + +#ifdef MONOCYPHER_CPP_NAMESPACE +namespace MONOCYPHER_CPP_NAMESPACE { +#elif defined(__cplusplus) +extern "C" { +#endif + +//////////////////////// +/// Type definitions /// +//////////////////////// + +// Do not rely on the size or content on any of those types, +// they may change without notice. +typedef struct { + uint64_t hash[8]; + uint64_t input[16]; + uint64_t input_size[2]; + size_t input_idx; +} crypto_sha512_ctx; + +typedef struct { + uint8_t key[128]; + crypto_sha512_ctx ctx; +} crypto_sha512_hmac_ctx; + + +// SHA 512 +// ------- +void crypto_sha512_init (crypto_sha512_ctx *ctx); +void crypto_sha512_update(crypto_sha512_ctx *ctx, + const uint8_t *message, size_t message_size); +void crypto_sha512_final (crypto_sha512_ctx *ctx, uint8_t hash[64]); +void crypto_sha512(uint8_t hash[64], + const uint8_t *message, size_t message_size); + +// SHA 512 HMAC +// ------------ +void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx, + const uint8_t *key, size_t key_size); +void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx, + const uint8_t *message, size_t message_size); +void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, uint8_t hmac[64]); +void crypto_sha512_hmac(uint8_t hmac[64], + const uint8_t *key , size_t key_size, + const uint8_t *message, size_t message_size); + +// SHA 512 HKDF +// ------------ +void crypto_sha512_hkdf_expand(uint8_t *okm, size_t okm_size, + const uint8_t *prk, size_t prk_size, + const uint8_t *info, size_t info_size); +void crypto_sha512_hkdf(uint8_t *okm , size_t okm_size, + const uint8_t *ikm , size_t ikm_size, + const uint8_t *salt, size_t salt_size, + const uint8_t *info, size_t info_size); + +// Ed25519 +// ------- +// Signatures (EdDSA with curve25519 + SHA-512) +// -------------------------------------------- +void crypto_ed25519_key_pair(uint8_t secret_key[64], + uint8_t public_key[32], + uint8_t seed[32]); +void crypto_ed25519_sign(uint8_t signature [64], + const uint8_t secret_key[64], + const uint8_t *message, size_t message_size); +int crypto_ed25519_check(const uint8_t signature [64], + const uint8_t public_key[32], + const uint8_t *message, size_t message_size); + +// Pre-hash variants +void crypto_ed25519_ph_sign(uint8_t signature [64], + const uint8_t secret_key [64], + const uint8_t message_hash[64]); +int crypto_ed25519_ph_check(const uint8_t signature [64], + const uint8_t public_key [32], + const uint8_t message_hash[64]); + +#ifdef __cplusplus +} +#endif + +#endif // ED25519_H diff --git a/modemmeshcore/monocypher.c b/modemmeshcore/monocypher.c new file mode 100644 index 000000000..23bc0127c --- /dev/null +++ b/modemmeshcore/monocypher.c @@ -0,0 +1,2957 @@ +// Monocypher version __git__ +// +// This file is dual-licensed. Choose whichever licence you want from +// the two licences listed below. +// +// The first licence is a regular 2-clause BSD licence. The second licence +// is the CC-0 from Creative Commons. It is intended to release Monocypher +// to the public domain. The BSD licence serves as a fallback option. +// +// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0 +// +// ------------------------------------------------------------------------ +// +// Copyright (c) 2017-2020, Loup Vaillant +// All rights reserved. +// +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ------------------------------------------------------------------------ +// +// Written in 2017-2020 by Loup Vaillant +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// + +#include "monocypher.h" + +#ifdef MONOCYPHER_CPP_NAMESPACE +namespace MONOCYPHER_CPP_NAMESPACE { +#endif + +///////////////// +/// Utilities /// +///////////////// +#define FOR_T(type, i, start, end) for (type i = (start); i < (end); i++) +#define FOR(i, start, end) FOR_T(size_t, i, start, end) +#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_] +#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0 +#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx))) +#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer)) +#define MIN(a, b) ((a) <= (b) ? (a) : (b)) +#define MAX(a, b) ((a) >= (b) ? (a) : (b)) + +typedef int8_t i8; +typedef uint8_t u8; +typedef int16_t i16; +typedef uint32_t u32; +typedef int32_t i32; +typedef int64_t i64; +typedef uint64_t u64; + +static const u8 zero[128] = {0}; + +// returns the smallest positive integer y such that +// (x + y) % pow_2 == 0 +// Basically, y is the "gap" missing to align x. +// Only works when pow_2 is a power of 2. +// Note: we use ~x+1 instead of -x to avoid compiler warnings +static size_t gap(size_t x, size_t pow_2) +{ + return (~x + 1) & (pow_2 - 1); +} + +static u32 load24_le(const u8 s[3]) +{ + return + ((u32)s[0] << 0) | + ((u32)s[1] << 8) | + ((u32)s[2] << 16); +} + +static u32 load32_le(const u8 s[4]) +{ + return + ((u32)s[0] << 0) | + ((u32)s[1] << 8) | + ((u32)s[2] << 16) | + ((u32)s[3] << 24); +} + +static u64 load64_le(const u8 s[8]) +{ + return load32_le(s) | ((u64)load32_le(s+4) << 32); +} + +static void store32_le(u8 out[4], u32 in) +{ + out[0] = (u8)(in ); + out[1] = (u8)(in >> 8); + out[2] = (u8)(in >> 16); + out[3] = (u8)(in >> 24); +} + +static void store64_le(u8 out[8], u64 in) +{ + store32_le(out , (u32)(in )); + store32_le(out + 4, (u32)(in >> 32)); +} + +static void load32_le_buf (u32 *dst, const u8 *src, size_t size) { + FOR(i, 0, size) { dst[i] = load32_le(src + i*4); } +} +static void load64_le_buf (u64 *dst, const u8 *src, size_t size) { + FOR(i, 0, size) { dst[i] = load64_le(src + i*8); } +} +static void store32_le_buf(u8 *dst, const u32 *src, size_t size) { + FOR(i, 0, size) { store32_le(dst + i*4, src[i]); } +} +static void store64_le_buf(u8 *dst, const u64 *src, size_t size) { + FOR(i, 0, size) { store64_le(dst + i*8, src[i]); } +} + +static u64 rotr64(u64 x, u64 n) { return (x >> n) ^ (x << (64 - n)); } +static u32 rotl32(u32 x, u32 n) { return (x << n) ^ (x >> (32 - n)); } + +static int neq0(u64 diff) +{ + // constant time comparison to zero + // return diff != 0 ? -1 : 0 + u64 half = (diff >> 32) | ((u32)diff); // half < 2^32 + u64 eq0 = 1 & ((half - 1) >> 32); // half == 0 ? 1 : 0 + return (int)eq0 - 1; // half == 0 ? 0 : -1 +} + +static u64 x16(const u8 a[16], const u8 b[16]) +{ + return (load64_le(a + 0) ^ load64_le(b + 0)) + | (load64_le(a + 8) ^ load64_le(b + 8)); +} +static u64 x32(const u8 a[32],const u8 b[32]){return x16(a,b)| x16(a+16, b+16);} +static u64 x64(const u8 a[64],const u8 b[64]){return x32(a,b)| x32(a+32, b+32);} +int crypto_verify16(const u8 a[16], const u8 b[16]){ return neq0(x16(a, b)); } +int crypto_verify32(const u8 a[32], const u8 b[32]){ return neq0(x32(a, b)); } +int crypto_verify64(const u8 a[64], const u8 b[64]){ return neq0(x64(a, b)); } + +void crypto_wipe(void *secret, size_t size) +{ + volatile u8 *v_secret = (u8*)secret; + ZERO(v_secret, size); +} + +///////////////// +/// Chacha 20 /// +///////////////// +#define QUARTERROUND(a, b, c, d) \ + a += b; d = rotl32(d ^ a, 16); \ + c += d; b = rotl32(b ^ c, 12); \ + a += b; d = rotl32(d ^ a, 8); \ + c += d; b = rotl32(b ^ c, 7) + +static void chacha20_rounds(u32 out[16], const u32 in[16]) +{ + // The temporary variables make Chacha20 10% faster. + u32 t0 = in[ 0]; u32 t1 = in[ 1]; u32 t2 = in[ 2]; u32 t3 = in[ 3]; + u32 t4 = in[ 4]; u32 t5 = in[ 5]; u32 t6 = in[ 6]; u32 t7 = in[ 7]; + u32 t8 = in[ 8]; u32 t9 = in[ 9]; u32 t10 = in[10]; u32 t11 = in[11]; + u32 t12 = in[12]; u32 t13 = in[13]; u32 t14 = in[14]; u32 t15 = in[15]; + + FOR (i, 0, 10) { // 20 rounds, 2 rounds per loop. + QUARTERROUND(t0, t4, t8 , t12); // column 0 + QUARTERROUND(t1, t5, t9 , t13); // column 1 + QUARTERROUND(t2, t6, t10, t14); // column 2 + QUARTERROUND(t3, t7, t11, t15); // column 3 + QUARTERROUND(t0, t5, t10, t15); // diagonal 0 + QUARTERROUND(t1, t6, t11, t12); // diagonal 1 + QUARTERROUND(t2, t7, t8 , t13); // diagonal 2 + QUARTERROUND(t3, t4, t9 , t14); // diagonal 3 + } + out[ 0] = t0; out[ 1] = t1; out[ 2] = t2; out[ 3] = t3; + out[ 4] = t4; out[ 5] = t5; out[ 6] = t6; out[ 7] = t7; + out[ 8] = t8; out[ 9] = t9; out[10] = t10; out[11] = t11; + out[12] = t12; out[13] = t13; out[14] = t14; out[15] = t15; +} + +static const u8 *chacha20_constant = (const u8*)"expand 32-byte k"; // 16 bytes + +void crypto_chacha20_h(u8 out[32], const u8 key[32], const u8 in [16]) +{ + u32 block[16]; + load32_le_buf(block , chacha20_constant, 4); + load32_le_buf(block + 4, key , 8); + load32_le_buf(block + 12, in , 4); + + chacha20_rounds(block, block); + + // prevent reversal of the rounds by revealing only half of the buffer. + store32_le_buf(out , block , 4); // constant + store32_le_buf(out+16, block+12, 4); // counter and nonce + WIPE_BUFFER(block); +} + +u64 crypto_chacha20_djb(u8 *cipher_text, const u8 *plain_text, + size_t text_size, const u8 key[32], const u8 nonce[8], + u64 ctr) +{ + u32 input[16]; + load32_le_buf(input , chacha20_constant, 4); + load32_le_buf(input + 4, key , 8); + load32_le_buf(input + 14, nonce , 2); + input[12] = (u32) ctr; + input[13] = (u32)(ctr >> 32); + + // Whole blocks + u32 pool[16]; + size_t nb_blocks = text_size >> 6; + FOR (i, 0, nb_blocks) { + chacha20_rounds(pool, input); + if (plain_text != NULL) { + FOR (j, 0, 16) { + u32 p = pool[j] + input[j]; + store32_le(cipher_text, p ^ load32_le(plain_text)); + cipher_text += 4; + plain_text += 4; + } + } else { + FOR (j, 0, 16) { + u32 p = pool[j] + input[j]; + store32_le(cipher_text, p); + cipher_text += 4; + } + } + input[12]++; + if (input[12] == 0) { + input[13]++; + } + } + text_size &= 63; + + // Last (incomplete) block + if (text_size > 0) { + if (plain_text == NULL) { + plain_text = zero; + } + chacha20_rounds(pool, input); + u8 tmp[64]; + FOR (i, 0, 16) { + store32_le(tmp + i*4, pool[i] + input[i]); + } + FOR (i, 0, text_size) { + cipher_text[i] = tmp[i] ^ plain_text[i]; + } + WIPE_BUFFER(tmp); + } + ctr = input[12] + ((u64)input[13] << 32) + (text_size > 0); + + WIPE_BUFFER(pool); + WIPE_BUFFER(input); + return ctr; +} + +u32 crypto_chacha20_ietf(u8 *cipher_text, const u8 *plain_text, + size_t text_size, + const u8 key[32], const u8 nonce[12], u32 ctr) +{ + u64 big_ctr = ctr + ((u64)load32_le(nonce) << 32); + return (u32)crypto_chacha20_djb(cipher_text, plain_text, text_size, + key, nonce + 4, big_ctr); +} + +u64 crypto_chacha20_x(u8 *cipher_text, const u8 *plain_text, + size_t text_size, + const u8 key[32], const u8 nonce[24], u64 ctr) +{ + u8 sub_key[32]; + crypto_chacha20_h(sub_key, key, nonce); + ctr = crypto_chacha20_djb(cipher_text, plain_text, text_size, + sub_key, nonce + 16, ctr); + WIPE_BUFFER(sub_key); + return ctr; +} + +///////////////// +/// Poly 1305 /// +///////////////// + +// h = (h + c) * r +// preconditions: +// ctx->h <= 4_ffffffff_ffffffff_ffffffff_ffffffff +// ctx->r <= 0ffffffc_0ffffffc_0ffffffc_0fffffff +// end <= 1 +// Postcondition: +// ctx->h <= 4_ffffffff_ffffffff_ffffffff_ffffffff +static void poly_blocks(crypto_poly1305_ctx *ctx, const u8 *in, + size_t nb_blocks, unsigned end) +{ + // Local all the things! + const u32 r0 = ctx->r[0]; + const u32 r1 = ctx->r[1]; + const u32 r2 = ctx->r[2]; + const u32 r3 = ctx->r[3]; + const u32 rr0 = (r0 >> 2) * 5; // lose 2 bits... + const u32 rr1 = (r1 >> 2) + r1; // rr1 == (r1 >> 2) * 5 + const u32 rr2 = (r2 >> 2) + r2; // rr1 == (r2 >> 2) * 5 + const u32 rr3 = (r3 >> 2) + r3; // rr1 == (r3 >> 2) * 5 + const u32 rr4 = r0 & 3; // ...recover 2 bits + u32 h0 = ctx->h[0]; + u32 h1 = ctx->h[1]; + u32 h2 = ctx->h[2]; + u32 h3 = ctx->h[3]; + u32 h4 = ctx->h[4]; + + FOR (i, 0, nb_blocks) { + // h + c, without carry propagation + const u64 s0 = (u64)h0 + load32_le(in); in += 4; + const u64 s1 = (u64)h1 + load32_le(in); in += 4; + const u64 s2 = (u64)h2 + load32_le(in); in += 4; + const u64 s3 = (u64)h3 + load32_le(in); in += 4; + const u32 s4 = h4 + end; + + // (h + c) * r, without carry propagation + const u64 x0 = s0*r0+ s1*rr3+ s2*rr2+ s3*rr1+ s4*rr0; + const u64 x1 = s0*r1+ s1*r0 + s2*rr3+ s3*rr2+ s4*rr1; + const u64 x2 = s0*r2+ s1*r1 + s2*r0 + s3*rr3+ s4*rr2; + const u64 x3 = s0*r3+ s1*r2 + s2*r1 + s3*r0 + s4*rr3; + const u32 x4 = s4*rr4; + + // partial reduction modulo 2^130 - 5 + const u32 u5 = (u32)(x3 >> 32) + x4; // u5 <= 7ffffff5 + const u64 u0 = (u32)(u5 >> 2) * 5 + (x0 & 0xffffffff); + const u64 u1 = (u32)(u0 >> 32) + (x1 & 0xffffffff) + (x0 >> 32); + const u64 u2 = (u32)(u1 >> 32) + (x2 & 0xffffffff) + (x1 >> 32); + const u64 u3 = (u32)(u2 >> 32) + (x3 & 0xffffffff) + (x2 >> 32); + const u32 u4 = (u32)(u3 >> 32) + (u5 & 3); // u4 <= 4 + + // Update the hash + h0 = u0 & 0xffffffff; + h1 = u1 & 0xffffffff; + h2 = u2 & 0xffffffff; + h3 = u3 & 0xffffffff; + h4 = u4; + } + ctx->h[0] = h0; + ctx->h[1] = h1; + ctx->h[2] = h2; + ctx->h[3] = h3; + ctx->h[4] = h4; +} + +void crypto_poly1305_init(crypto_poly1305_ctx *ctx, const u8 key[32]) +{ + ZERO(ctx->h, 5); // Initial hash is zero + ctx->c_idx = 0; + // load r and pad (r has some of its bits cleared) + load32_le_buf(ctx->r , key , 4); + load32_le_buf(ctx->pad, key+16, 4); + FOR (i, 0, 1) { ctx->r[i] &= 0x0fffffff; } + FOR (i, 1, 4) { ctx->r[i] &= 0x0ffffffc; } +} + +void crypto_poly1305_update(crypto_poly1305_ctx *ctx, + const u8 *message, size_t message_size) +{ + // Avoid undefined NULL pointer increments with empty messages + if (message_size == 0) { + return; + } + + // Align ourselves with block boundaries + size_t aligned = MIN(gap(ctx->c_idx, 16), message_size); + FOR (i, 0, aligned) { + ctx->c[ctx->c_idx] = *message; + ctx->c_idx++; + message++; + message_size--; + } + + // If block is complete, process it + if (ctx->c_idx == 16) { + poly_blocks(ctx, ctx->c, 1, 1); + ctx->c_idx = 0; + } + + // Process the message block by block + size_t nb_blocks = message_size >> 4; + poly_blocks(ctx, message, nb_blocks, 1); + message += nb_blocks << 4; + message_size &= 15; + + // remaining bytes (we never complete a block here) + FOR (i, 0, message_size) { + ctx->c[ctx->c_idx] = message[i]; + ctx->c_idx++; + } +} + +void crypto_poly1305_final(crypto_poly1305_ctx *ctx, u8 mac[16]) +{ + // Process the last block (if any) + // We move the final 1 according to remaining input length + // (this will add less than 2^130 to the last input block) + if (ctx->c_idx != 0) { + ZERO(ctx->c + ctx->c_idx, 16 - ctx->c_idx); + ctx->c[ctx->c_idx] = 1; + poly_blocks(ctx, ctx->c, 1, 0); + } + + // check if we should subtract 2^130-5 by performing the + // corresponding carry propagation. + u64 c = 5; + FOR (i, 0, 4) { + c += ctx->h[i]; + c >>= 32; + } + c += ctx->h[4]; + c = (c >> 2) * 5; // shift the carry back to the beginning + // c now indicates how many times we should subtract 2^130-5 (0 or 1) + FOR (i, 0, 4) { + c += (u64)ctx->h[i] + ctx->pad[i]; + store32_le(mac + i*4, (u32)c); + c = c >> 32; + } + WIPE_CTX(ctx); +} + +void crypto_poly1305(u8 mac[16], const u8 *message, + size_t message_size, const u8 key[32]) +{ + crypto_poly1305_ctx ctx; + crypto_poly1305_init (&ctx, key); + crypto_poly1305_update(&ctx, message, message_size); + crypto_poly1305_final (&ctx, mac); +} + +//////////////// +/// BLAKE2 b /// +//////////////// +static const u64 iv[8] = { + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, +}; + +static void blake2b_compress(crypto_blake2b_ctx *ctx, int is_last_block) +{ + static const u8 sigma[12][16] = { + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, + { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 }, + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 }, + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 }, + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 }, + { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 }, + { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 }, + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 }, + { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, + }; + + // increment input offset + u64 *x = ctx->input_offset; + size_t y = ctx->input_idx; + x[0] += y; + if (x[0] < y) { + x[1]++; + } + + // init work vector + u64 v0 = ctx->hash[0]; u64 v8 = iv[0]; + u64 v1 = ctx->hash[1]; u64 v9 = iv[1]; + u64 v2 = ctx->hash[2]; u64 v10 = iv[2]; + u64 v3 = ctx->hash[3]; u64 v11 = iv[3]; + u64 v4 = ctx->hash[4]; u64 v12 = iv[4] ^ ctx->input_offset[0]; + u64 v5 = ctx->hash[5]; u64 v13 = iv[5] ^ ctx->input_offset[1]; + u64 v6 = ctx->hash[6]; u64 v14 = iv[6] ^ (u64)~(is_last_block - 1); + u64 v7 = ctx->hash[7]; u64 v15 = iv[7]; + + // mangle work vector + u64 *input = ctx->input; +#define BLAKE2_G(a, b, c, d, x, y) \ + a += b + x; d = rotr64(d ^ a, 32); \ + c += d; b = rotr64(b ^ c, 24); \ + a += b + y; d = rotr64(d ^ a, 16); \ + c += d; b = rotr64(b ^ c, 63) +#define BLAKE2_ROUND(i) \ + BLAKE2_G(v0, v4, v8 , v12, input[sigma[i][ 0]], input[sigma[i][ 1]]); \ + BLAKE2_G(v1, v5, v9 , v13, input[sigma[i][ 2]], input[sigma[i][ 3]]); \ + BLAKE2_G(v2, v6, v10, v14, input[sigma[i][ 4]], input[sigma[i][ 5]]); \ + BLAKE2_G(v3, v7, v11, v15, input[sigma[i][ 6]], input[sigma[i][ 7]]); \ + BLAKE2_G(v0, v5, v10, v15, input[sigma[i][ 8]], input[sigma[i][ 9]]); \ + BLAKE2_G(v1, v6, v11, v12, input[sigma[i][10]], input[sigma[i][11]]); \ + BLAKE2_G(v2, v7, v8 , v13, input[sigma[i][12]], input[sigma[i][13]]); \ + BLAKE2_G(v3, v4, v9 , v14, input[sigma[i][14]], input[sigma[i][15]]) + +#ifdef BLAKE2_NO_UNROLLING + FOR (i, 0, 12) { + BLAKE2_ROUND(i); + } +#else + BLAKE2_ROUND(0); BLAKE2_ROUND(1); BLAKE2_ROUND(2); BLAKE2_ROUND(3); + BLAKE2_ROUND(4); BLAKE2_ROUND(5); BLAKE2_ROUND(6); BLAKE2_ROUND(7); + BLAKE2_ROUND(8); BLAKE2_ROUND(9); BLAKE2_ROUND(10); BLAKE2_ROUND(11); +#endif + + // update hash + ctx->hash[0] ^= v0 ^ v8; ctx->hash[1] ^= v1 ^ v9; + ctx->hash[2] ^= v2 ^ v10; ctx->hash[3] ^= v3 ^ v11; + ctx->hash[4] ^= v4 ^ v12; ctx->hash[5] ^= v5 ^ v13; + ctx->hash[6] ^= v6 ^ v14; ctx->hash[7] ^= v7 ^ v15; +} + +void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size, + const u8 *key, size_t key_size) +{ + // initial hash + COPY(ctx->hash, iv, 8); + ctx->hash[0] ^= 0x01010000 ^ (key_size << 8) ^ hash_size; + + ctx->input_offset[0] = 0; // beginning of the input, no offset + ctx->input_offset[1] = 0; // beginning of the input, no offset + ctx->hash_size = hash_size; + ctx->input_idx = 0; + ZERO(ctx->input, 16); + + // if there is a key, the first block is that key (padded with zeroes) + if (key_size > 0) { + u8 key_block[128] = {0}; + COPY(key_block, key, key_size); + // same as calling crypto_blake2b_update(ctx, key_block , 128) + load64_le_buf(ctx->input, key_block, 16); + ctx->input_idx = 128; + } +} + +void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size) +{ + crypto_blake2b_keyed_init(ctx, hash_size, 0, 0); +} + +void crypto_blake2b_update(crypto_blake2b_ctx *ctx, + const u8 *message, size_t message_size) +{ + // Avoid undefined NULL pointer increments with empty messages + if (message_size == 0) { + return; + } + + // Align with word boundaries + if ((ctx->input_idx & 7) != 0) { + size_t nb_bytes = MIN(gap(ctx->input_idx, 8), message_size); + size_t word = ctx->input_idx >> 3; + size_t byte = ctx->input_idx & 7; + FOR (i, 0, nb_bytes) { + ctx->input[word] |= (u64)message[i] << ((byte + i) << 3); + } + ctx->input_idx += nb_bytes; + message += nb_bytes; + message_size -= nb_bytes; + } + + // Align with block boundaries (faster than byte by byte) + if ((ctx->input_idx & 127) != 0) { + size_t nb_words = MIN(gap(ctx->input_idx, 128), message_size) >> 3; + load64_le_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words); + ctx->input_idx += nb_words << 3; + message += nb_words << 3; + message_size -= nb_words << 3; + } + + // Process block by block + size_t nb_blocks = message_size >> 7; + FOR (i, 0, nb_blocks) { + if (ctx->input_idx == 128) { + blake2b_compress(ctx, 0); + } + load64_le_buf(ctx->input, message, 16); + message += 128; + ctx->input_idx = 128; + } + message_size &= 127; + + if (message_size != 0) { + // Compress block & flush input buffer as needed + if (ctx->input_idx == 128) { + blake2b_compress(ctx, 0); + ctx->input_idx = 0; + } + if (ctx->input_idx == 0) { + ZERO(ctx->input, 16); + } + // Fill remaining words (faster than byte by byte) + size_t nb_words = message_size >> 3; + load64_le_buf(ctx->input, message, nb_words); + ctx->input_idx += nb_words << 3; + message += nb_words << 3; + message_size -= nb_words << 3; + + // Fill remaining bytes + FOR (i, 0, message_size) { + size_t word = ctx->input_idx >> 3; + size_t byte = ctx->input_idx & 7; + ctx->input[word] |= (u64)message[i] << (byte << 3); + ctx->input_idx++; + } + } +} + +void crypto_blake2b_final(crypto_blake2b_ctx *ctx, u8 *hash) +{ + blake2b_compress(ctx, 1); // compress the last block + size_t hash_size = MIN(ctx->hash_size, 64); + size_t nb_words = hash_size >> 3; + store64_le_buf(hash, ctx->hash, nb_words); + FOR (i, nb_words << 3, hash_size) { + hash[i] = (ctx->hash[i >> 3] >> (8 * (i & 7))) & 0xff; + } + WIPE_CTX(ctx); +} + +void crypto_blake2b_keyed(u8 *hash, size_t hash_size, + const u8 *key, size_t key_size, + const u8 *message, size_t message_size) +{ + crypto_blake2b_ctx ctx; + crypto_blake2b_keyed_init(&ctx, hash_size, key, key_size); + crypto_blake2b_update (&ctx, message, message_size); + crypto_blake2b_final (&ctx, hash); +} + +void crypto_blake2b(u8 *hash, size_t hash_size, const u8 *msg, size_t msg_size) +{ + crypto_blake2b_keyed(hash, hash_size, 0, 0, msg, msg_size); +} + +////////////// +/// Argon2 /// +////////////// +// references to R, Z, Q etc. come from the spec + +// Argon2 operates on 1024 byte blocks. +typedef struct { u64 a[128]; } blk; + +// updates a BLAKE2 hash with a 32 bit word, little endian. +static void blake_update_32(crypto_blake2b_ctx *ctx, u32 input) +{ + u8 buf[4]; + store32_le(buf, input); + crypto_blake2b_update(ctx, buf, 4); + WIPE_BUFFER(buf); +} + +static void blake_update_32_buf(crypto_blake2b_ctx *ctx, + const u8 *buf, u32 size) +{ + blake_update_32(ctx, size); + crypto_blake2b_update(ctx, buf, size); +} + + +static void copy_block(blk *o,const blk*in){FOR(i, 0, 128) o->a[i] = in->a[i];} +static void xor_block(blk *o,const blk*in){FOR(i, 0, 128) o->a[i] ^= in->a[i];} + +// Hash with a virtually unlimited digest size. +// Doesn't extract more entropy than the base hash function. +// Mainly used for filling a whole kilobyte block with pseudo-random bytes. +// (One could use a stream cipher with a seed hash as the key, but +// this would introduce another dependency —and point of failure.) +static void extended_hash(u8 *digest, u32 digest_size, + const u8 *input , u32 input_size) +{ + crypto_blake2b_ctx ctx; + crypto_blake2b_init (&ctx, MIN(digest_size, 64)); + blake_update_32 (&ctx, digest_size); + crypto_blake2b_update(&ctx, input, input_size); + crypto_blake2b_final (&ctx, digest); + + if (digest_size > 64) { + // the conversion to u64 avoids integer overflow on + // ludicrously big hash sizes. + u32 r = (u32)(((u64)digest_size + 31) >> 5) - 2; + u32 i = 1; + u32 in = 0; + u32 out = 32; + while (i < r) { + // Input and output overlap. This is intentional + crypto_blake2b(digest + out, 64, digest + in, 64); + i += 1; + in += 32; + out += 32; + } + crypto_blake2b(digest + out, digest_size - (32 * r), digest + in , 64); + } +} + +#define LSB(x) ((u64)(u32)x) +#define G(a, b, c, d) \ + a += b + ((LSB(a) * LSB(b)) << 1); d ^= a; d = rotr64(d, 32); \ + c += d + ((LSB(c) * LSB(d)) << 1); b ^= c; b = rotr64(b, 24); \ + a += b + ((LSB(a) * LSB(b)) << 1); d ^= a; d = rotr64(d, 16); \ + c += d + ((LSB(c) * LSB(d)) << 1); b ^= c; b = rotr64(b, 63) +#define ROUND(v0, v1, v2, v3, v4, v5, v6, v7, \ + v8, v9, v10, v11, v12, v13, v14, v15) \ + G(v0, v4, v8, v12); G(v1, v5, v9, v13); \ + G(v2, v6, v10, v14); G(v3, v7, v11, v15); \ + G(v0, v5, v10, v15); G(v1, v6, v11, v12); \ + G(v2, v7, v8, v13); G(v3, v4, v9, v14) + +// Core of the compression function G. Computes Z from R in place. +static void g_rounds(blk *b) +{ + // column rounds (work_block = Q) + for (int i = 0; i < 128; i += 16) { + ROUND(b->a[i ], b->a[i+ 1], b->a[i+ 2], b->a[i+ 3], + b->a[i+ 4], b->a[i+ 5], b->a[i+ 6], b->a[i+ 7], + b->a[i+ 8], b->a[i+ 9], b->a[i+10], b->a[i+11], + b->a[i+12], b->a[i+13], b->a[i+14], b->a[i+15]); + } + // row rounds (b = Z) + for (int i = 0; i < 16; i += 2) { + ROUND(b->a[i ], b->a[i+ 1], b->a[i+ 16], b->a[i+ 17], + b->a[i+32], b->a[i+33], b->a[i+ 48], b->a[i+ 49], + b->a[i+64], b->a[i+65], b->a[i+ 80], b->a[i+ 81], + b->a[i+96], b->a[i+97], b->a[i+112], b->a[i+113]); + } +} + +const crypto_argon2_extras crypto_argon2_no_extras = { 0, 0, 0, 0 }; + +void crypto_argon2(u8 *hash, u32 hash_size, void *work_area, + crypto_argon2_config config, + crypto_argon2_inputs inputs, + crypto_argon2_extras extras) +{ + const u32 segment_size = config.nb_blocks / config.nb_lanes / 4; + const u32 lane_size = segment_size * 4; + const u32 nb_blocks = lane_size * config.nb_lanes; // rounding down + + // work area seen as blocks (must be suitably aligned) + blk *blocks = (blk*)work_area; + { + u8 initial_hash[72]; // 64 bytes plus 2 words for future hashes + crypto_blake2b_ctx ctx; + crypto_blake2b_init (&ctx, 64); + blake_update_32 (&ctx, config.nb_lanes ); // p: number of "threads" + blake_update_32 (&ctx, hash_size); + blake_update_32 (&ctx, config.nb_blocks); + blake_update_32 (&ctx, config.nb_passes); + blake_update_32 (&ctx, 0x13); // v: version number + blake_update_32 (&ctx, config.algorithm); // y: Argon2i, Argon2d... + blake_update_32_buf (&ctx, inputs.pass, inputs.pass_size); + blake_update_32_buf (&ctx, inputs.salt, inputs.salt_size); + blake_update_32_buf (&ctx, extras.key, extras.key_size); + blake_update_32_buf (&ctx, extras.ad, extras.ad_size); + crypto_blake2b_final(&ctx, initial_hash); // fill 64 first bytes only + + // fill first 2 blocks of each lane + u8 hash_area[1024]; + FOR_T(u32, l, 0, config.nb_lanes) { + FOR_T(u32, i, 0, 2) { + store32_le(initial_hash + 64, i); // first additional word + store32_le(initial_hash + 68, l); // second additional word + extended_hash(hash_area, 1024, initial_hash, 72); + load64_le_buf(blocks[l * lane_size + i].a, hash_area, 128); + } + } + + WIPE_BUFFER(initial_hash); + WIPE_BUFFER(hash_area); + } + + // Argon2i and Argon2id start with constant time indexing + int constant_time = config.algorithm != CRYPTO_ARGON2_D; + + // Fill (and re-fill) the rest of the blocks + // + // Note: even though each segment within the same slice can be + // computed in parallel, (one thread per lane), we are computing + // them sequentially, because Monocypher doesn't support threads. + // + // Yet optimal performance (and therefore security) requires one + // thread per lane. The only reason Monocypher supports multiple + // lanes is compatibility. + blk tmp; + FOR_T(u32, pass, 0, config.nb_passes) { + FOR_T(u32, slice, 0, 4) { + // On the first slice of the first pass, + // blocks 0 and 1 are already filled, hence pass_offset. + u32 pass_offset = pass == 0 && slice == 0 ? 2 : 0; + u32 slice_offset = slice * segment_size; + + // Argon2id switches back to non-constant time indexing + // after the first two slices of the first pass + if (slice == 2 && config.algorithm == CRYPTO_ARGON2_ID) { + constant_time = 0; + } + + // Each iteration of the following loop may be performed in + // a separate thread. All segments must be fully completed + // before we start filling the next slice. + FOR_T(u32, segment, 0, config.nb_lanes) { + blk index_block; + u32 index_ctr = 1; + FOR_T (u32, block, pass_offset, segment_size) { + // Current and previous blocks + u32 lane_offset = segment * lane_size; + blk *segment_start = blocks + lane_offset + slice_offset; + blk *current = segment_start + block; + blk *previous = + block == 0 && slice_offset == 0 + ? segment_start + lane_size - 1 + : segment_start + block - 1; + + u64 index_seed; + if (constant_time) { + if (block == pass_offset || (block % 128) == 0) { + // Fill or refresh deterministic indices block + + // seed the beginning of the block... + ZERO(index_block.a, 128); + index_block.a[0] = pass; + index_block.a[1] = segment; + index_block.a[2] = slice; + index_block.a[3] = nb_blocks; + index_block.a[4] = config.nb_passes; + index_block.a[5] = config.algorithm; + index_block.a[6] = index_ctr; + index_ctr++; + + // ... then shuffle it + copy_block(&tmp, &index_block); + g_rounds (&index_block); + xor_block (&index_block, &tmp); + copy_block(&tmp, &index_block); + g_rounds (&index_block); + xor_block (&index_block, &tmp); + } + index_seed = index_block.a[block % 128]; + } else { + index_seed = previous->a[0]; + } + + // Establish the reference set. *Approximately* comprises: + // - The last 3 slices (if they exist yet) + // - The already constructed blocks in the current segment + u32 next_slice = ((slice + 1) % 4) * segment_size; + u32 window_start = pass == 0 ? 0 : next_slice; + u32 nb_segments = pass == 0 ? slice : 3; + u32 lane = + pass == 0 && slice == 0 + ? segment + : (u32)(index_seed >> 32) % config.nb_lanes; + u32 window_size = + nb_segments * segment_size + + (lane == segment ? block-1 : + block == 0 ? (u32)-1 : 0); + + // Find reference block + u64 j1 = index_seed & 0xffffffff; // block selector + u64 x = (j1 * j1) >> 32; + u64 y = (window_size * x) >> 32; + u64 z = (window_size - 1) - y; + u32 ref = (u32)((window_start + z) % lane_size); + u32 index = lane * lane_size + ref; + blk *reference = blocks + index; + + // Shuffle the previous & reference block + // into the current block + copy_block(&tmp, previous); + xor_block (&tmp, reference); + if (pass == 0) { copy_block(current, &tmp); } + else { xor_block (current, &tmp); } + g_rounds (&tmp); + xor_block (current, &tmp); + } + } + } + } + + // Wipe temporary block + volatile u64* p = tmp.a; + ZERO(p, 128); + + // XOR last blocks of each lane + blk *last_block = blocks + lane_size - 1; + FOR_T (u32, lane, 1, config.nb_lanes) { + blk *next_block = last_block + lane_size; + xor_block(next_block, last_block); + last_block = next_block; + } + + // Serialize last block + u8 final_block[1024]; + store64_le_buf(final_block, last_block->a, 128); + + // Wipe work area + p = (u64*)work_area; + ZERO(p, 128 * nb_blocks); + + // Hash the very last block with H' into the output hash + extended_hash(hash, hash_size, final_block, 1024); + WIPE_BUFFER(final_block); +} + +//////////////////////////////////// +/// Arithmetic modulo 2^255 - 19 /// +//////////////////////////////////// +// Originally taken from SUPERCOP's ref10 implementation. +// A bit bigger than TweetNaCl, over 4 times faster. + +// field element +typedef i32 fe[10]; + +// field constants +// +// fe_one : 1 +// sqrtm1 : sqrt(-1) +// d : -121665 / 121666 +// D2 : 2 * -121665 / 121666 +// lop_x, lop_y: low order point in Edwards coordinates +// ufactor : -sqrt(-1) * 2 +// A2 : 486662^2 (A squared) +static const fe fe_one = {1}; +static const fe sqrtm1 = { + -32595792, -7943725, 9377950, 3500415, 12389472, + -272473, -25146209, -2005654, 326686, 11406482, +}; +static const fe d = { + -10913610, 13857413, -15372611, 6949391, 114729, + -8787816, -6275908, -3247719, -18696448, -12055116, +}; +static const fe D2 = { + -21827239, -5839606, -30745221, 13898782, 229458, + 15978800, -12551817, -6495438, 29715968, 9444199, +}; +static const fe lop_x = { + 21352778, 5345713, 4660180, -8347857, 24143090, + 14568123, 30185756, -12247770, -33528939, 8345319, +}; +static const fe lop_y = { + -6952922, -1265500, 6862341, -7057498, -4037696, + -5447722, 31680899, -15325402, -19365852, 1569102, +}; +static const fe ufactor = { + -1917299, 15887451, -18755900, -7000830, -24778944, + 544946, -16816446, 4011309, -653372, 10741468, +}; +static const fe A2 = { + 12721188, 3529, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static void fe_0(fe h) { ZERO(h , 10); } +static void fe_1(fe h) { h[0] = 1; ZERO(h+1, 9); } + +static void fe_copy(fe h,const fe f ){FOR(i,0,10) h[i] = f[i]; } +static void fe_neg (fe h,const fe f ){FOR(i,0,10) h[i] = -f[i]; } +static void fe_add (fe h,const fe f,const fe g){FOR(i,0,10) h[i] = f[i] + g[i];} +static void fe_sub (fe h,const fe f,const fe g){FOR(i,0,10) h[i] = f[i] - g[i];} + +static void fe_cswap(fe f, fe g, int b) +{ + i32 mask = -b; // -1 = 0xffffffff + FOR (i, 0, 10) { + i32 x = (f[i] ^ g[i]) & mask; + f[i] = f[i] ^ x; + g[i] = g[i] ^ x; + } +} + +static void fe_ccopy(fe f, const fe g, int b) +{ + i32 mask = -b; // -1 = 0xffffffff + FOR (i, 0, 10) { + i32 x = (f[i] ^ g[i]) & mask; + f[i] = f[i] ^ x; + } +} + + +// Signed carry propagation +// ------------------------ +// +// Let t be a number. It can be uniquely decomposed thus: +// +// t = h*2^26 + l +// such that -2^25 <= l < 2^25 +// +// Let c = (t + 2^25) / 2^26 (rounded down) +// c = (h*2^26 + l + 2^25) / 2^26 (rounded down) +// c = h + (l + 2^25) / 2^26 (rounded down) +// c = h (exactly) +// Because 0 <= l + 2^25 < 2^26 +// +// Let u = t - c*2^26 +// u = h*2^26 + l - h*2^26 +// u = l +// Therefore, -2^25 <= u < 2^25 +// +// Additionally, if |t| < x, then |h| < x/2^26 (rounded down) +// +// Notations: +// - In C, 1<<25 means 2^25. +// - In C, x>>25 means floor(x / (2^25)). +// - All of the above applies with 25 & 24 as well as 26 & 25. +// +// +// Note on negative right shifts +// ----------------------------- +// +// In C, x >> n, where x is a negative integer, is implementation +// defined. In practice, all platforms do arithmetic shift, which is +// equivalent to division by 2^26, rounded down. Some compilers, like +// GCC, even guarantee it. +// +// If we ever stumble upon a platform that does not propagate the sign +// bit (we won't), visible failures will show at the slightest test, and +// the signed shifts can be replaced by the following: +// +// typedef struct { i64 x:39; } s25; +// typedef struct { i64 x:38; } s26; +// i64 shift25(i64 x) { s25 s; s.x = ((u64)x)>>25; return s.x; } +// i64 shift26(i64 x) { s26 s; s.x = ((u64)x)>>26; return s.x; } +// +// Current compilers cannot optimise this, causing a 30% drop in +// performance. Fairly expensive for something that never happens. +// +// +// Precondition +// ------------ +// +// |t0| < 2^63 +// |t1|..|t9| < 2^62 +// +// Algorithm +// --------- +// c = t0 + 2^25 / 2^26 -- |c| <= 2^36 +// t0 -= c * 2^26 -- |t0| <= 2^25 +// t1 += c -- |t1| <= 2^63 +// +// c = t4 + 2^25 / 2^26 -- |c| <= 2^36 +// t4 -= c * 2^26 -- |t4| <= 2^25 +// t5 += c -- |t5| <= 2^63 +// +// c = t1 + 2^24 / 2^25 -- |c| <= 2^38 +// t1 -= c * 2^25 -- |t1| <= 2^24 +// t2 += c -- |t2| <= 2^63 +// +// c = t5 + 2^24 / 2^25 -- |c| <= 2^38 +// t5 -= c * 2^25 -- |t5| <= 2^24 +// t6 += c -- |t6| <= 2^63 +// +// c = t2 + 2^25 / 2^26 -- |c| <= 2^37 +// t2 -= c * 2^26 -- |t2| <= 2^25 < 1.1 * 2^25 (final t2) +// t3 += c -- |t3| <= 2^63 +// +// c = t6 + 2^25 / 2^26 -- |c| <= 2^37 +// t6 -= c * 2^26 -- |t6| <= 2^25 < 1.1 * 2^25 (final t6) +// t7 += c -- |t7| <= 2^63 +// +// c = t3 + 2^24 / 2^25 -- |c| <= 2^38 +// t3 -= c * 2^25 -- |t3| <= 2^24 < 1.1 * 2^24 (final t3) +// t4 += c -- |t4| <= 2^25 + 2^38 < 2^39 +// +// c = t7 + 2^24 / 2^25 -- |c| <= 2^38 +// t7 -= c * 2^25 -- |t7| <= 2^24 < 1.1 * 2^24 (final t7) +// t8 += c -- |t8| <= 2^63 +// +// c = t4 + 2^25 / 2^26 -- |c| <= 2^13 +// t4 -= c * 2^26 -- |t4| <= 2^25 < 1.1 * 2^25 (final t4) +// t5 += c -- |t5| <= 2^24 + 2^13 < 1.1 * 2^24 (final t5) +// +// c = t8 + 2^25 / 2^26 -- |c| <= 2^37 +// t8 -= c * 2^26 -- |t8| <= 2^25 < 1.1 * 2^25 (final t8) +// t9 += c -- |t9| <= 2^63 +// +// c = t9 + 2^24 / 2^25 -- |c| <= 2^38 +// t9 -= c * 2^25 -- |t9| <= 2^24 < 1.1 * 2^24 (final t9) +// t0 += c * 19 -- |t0| <= 2^25 + 2^38*19 < 2^44 +// +// c = t0 + 2^25 / 2^26 -- |c| <= 2^18 +// t0 -= c * 2^26 -- |t0| <= 2^25 < 1.1 * 2^25 (final t0) +// t1 += c -- |t1| <= 2^24 + 2^18 < 1.1 * 2^24 (final t1) +// +// Postcondition +// ------------- +// |t0|, |t2|, |t4|, |t6|, |t8| < 1.1 * 2^25 +// |t1|, |t3|, |t5|, |t7|, |t9| < 1.1 * 2^24 +#define FE_CARRY \ + i64 c; \ + c = (t0 + ((i64)1<<25)) >> 26; t0 -= c * ((i64)1 << 26); t1 += c; \ + c = (t4 + ((i64)1<<25)) >> 26; t4 -= c * ((i64)1 << 26); t5 += c; \ + c = (t1 + ((i64)1<<24)) >> 25; t1 -= c * ((i64)1 << 25); t2 += c; \ + c = (t5 + ((i64)1<<24)) >> 25; t5 -= c * ((i64)1 << 25); t6 += c; \ + c = (t2 + ((i64)1<<25)) >> 26; t2 -= c * ((i64)1 << 26); t3 += c; \ + c = (t6 + ((i64)1<<25)) >> 26; t6 -= c * ((i64)1 << 26); t7 += c; \ + c = (t3 + ((i64)1<<24)) >> 25; t3 -= c * ((i64)1 << 25); t4 += c; \ + c = (t7 + ((i64)1<<24)) >> 25; t7 -= c * ((i64)1 << 25); t8 += c; \ + c = (t4 + ((i64)1<<25)) >> 26; t4 -= c * ((i64)1 << 26); t5 += c; \ + c = (t8 + ((i64)1<<25)) >> 26; t8 -= c * ((i64)1 << 26); t9 += c; \ + c = (t9 + ((i64)1<<24)) >> 25; t9 -= c * ((i64)1 << 25); t0 += c * 19; \ + c = (t0 + ((i64)1<<25)) >> 26; t0 -= c * ((i64)1 << 26); t1 += c; \ + h[0]=(i32)t0; h[1]=(i32)t1; h[2]=(i32)t2; h[3]=(i32)t3; h[4]=(i32)t4; \ + h[5]=(i32)t5; h[6]=(i32)t6; h[7]=(i32)t7; h[8]=(i32)t8; h[9]=(i32)t9 + +// Decodes a field element from a byte buffer. +// mask specifies how many bits we ignore. +// Traditionally we ignore 1. It's useful for EdDSA, +// which uses that bit to denote the sign of x. +// Elligator however uses positive representatives, +// which means ignoring 2 bits instead. +static void fe_frombytes_mask(fe h, const u8 s[32], unsigned nb_mask) +{ + u32 mask = 0xffffff >> nb_mask; + i64 t0 = load32_le(s); // t0 < 2^32 + i64 t1 = load24_le(s + 4) << 6; // t1 < 2^30 + i64 t2 = load24_le(s + 7) << 5; // t2 < 2^29 + i64 t3 = load24_le(s + 10) << 3; // t3 < 2^27 + i64 t4 = load24_le(s + 13) << 2; // t4 < 2^26 + i64 t5 = load32_le(s + 16); // t5 < 2^32 + i64 t6 = load24_le(s + 20) << 7; // t6 < 2^31 + i64 t7 = load24_le(s + 23) << 5; // t7 < 2^29 + i64 t8 = load24_le(s + 26) << 4; // t8 < 2^28 + i64 t9 = (load24_le(s + 29) & mask) << 2; // t9 < 2^25 + FE_CARRY; // Carry precondition OK +} + +static void fe_frombytes(fe h, const u8 s[32]) +{ + fe_frombytes_mask(h, s, 1); +} + + +// Precondition +// |h[0]|, |h[2]|, |h[4]|, |h[6]|, |h[8]| < 1.1 * 2^25 +// |h[1]|, |h[3]|, |h[5]|, |h[7]|, |h[9]| < 1.1 * 2^24 +// +// Therefore, |h| < 2^255-19 +// There are two possibilities: +// +// - If h is positive, all we need to do is reduce its individual +// limbs down to their tight positive range. +// - If h is negative, we also need to add 2^255-19 to it. +// Or just remove 19 and chop off any excess bit. +static void fe_tobytes(u8 s[32], const fe h) +{ + i32 t[10]; + COPY(t, h, 10); + i32 q = (19 * t[9] + (((i32) 1) << 24)) >> 25; + // |t9| < 1.1 * 2^24 + // -1.1 * 2^24 < t9 < 1.1 * 2^24 + // -21 * 2^24 < 19 * t9 < 21 * 2^24 + // -2^29 < 19 * t9 + 2^24 < 2^29 + // -2^29 / 2^25 < (19 * t9 + 2^24) / 2^25 < 2^29 / 2^25 + // -16 < (19 * t9 + 2^24) / 2^25 < 16 + FOR (i, 0, 5) { + q += t[2*i ]; q >>= 26; // q = 0 or -1 + q += t[2*i+1]; q >>= 25; // q = 0 or -1 + } + // q = 0 iff h >= 0 + // q = -1 iff h < 0 + // Adding q * 19 to h reduces h to its proper range. + q *= 19; // Shift carry back to the beginning + FOR (i, 0, 5) { + t[i*2 ] += q; q = t[i*2 ] >> 26; t[i*2 ] -= q * ((i32)1 << 26); + t[i*2+1] += q; q = t[i*2+1] >> 25; t[i*2+1] -= q * ((i32)1 << 25); + } + // h is now fully reduced, and q represents the excess bit. + + store32_le(s + 0, ((u32)t[0] >> 0) | ((u32)t[1] << 26)); + store32_le(s + 4, ((u32)t[1] >> 6) | ((u32)t[2] << 19)); + store32_le(s + 8, ((u32)t[2] >> 13) | ((u32)t[3] << 13)); + store32_le(s + 12, ((u32)t[3] >> 19) | ((u32)t[4] << 6)); + store32_le(s + 16, ((u32)t[5] >> 0) | ((u32)t[6] << 25)); + store32_le(s + 20, ((u32)t[6] >> 7) | ((u32)t[7] << 19)); + store32_le(s + 24, ((u32)t[7] >> 13) | ((u32)t[8] << 12)); + store32_le(s + 28, ((u32)t[8] >> 20) | ((u32)t[9] << 6)); + + WIPE_BUFFER(t); +} + +// Precondition +// ------------- +// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26 +// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25 +// +// |g0|, |g2|, |g4|, |g6|, |g8| < 1.65 * 2^26 +// |g1|, |g3|, |g5|, |g7|, |g9| < 1.65 * 2^25 +static void fe_mul_small(fe h, const fe f, i32 g) +{ + i64 t0 = f[0] * (i64) g; i64 t1 = f[1] * (i64) g; + i64 t2 = f[2] * (i64) g; i64 t3 = f[3] * (i64) g; + i64 t4 = f[4] * (i64) g; i64 t5 = f[5] * (i64) g; + i64 t6 = f[6] * (i64) g; i64 t7 = f[7] * (i64) g; + i64 t8 = f[8] * (i64) g; i64 t9 = f[9] * (i64) g; + // |t0|, |t2|, |t4|, |t6|, |t8| < 1.65 * 2^26 * 2^31 < 2^58 + // |t1|, |t3|, |t5|, |t7|, |t9| < 1.65 * 2^25 * 2^31 < 2^57 + + FE_CARRY; // Carry precondition OK +} + +// Precondition +// ------------- +// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26 +// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25 +// +// |g0|, |g2|, |g4|, |g6|, |g8| < 1.65 * 2^26 +// |g1|, |g3|, |g5|, |g7|, |g9| < 1.65 * 2^25 +static void fe_mul(fe h, const fe f, const fe g) +{ + // Everything is unrolled and put in temporary variables. + // We could roll the loop, but that would make curve25519 twice as slow. + i32 f0 = f[0]; i32 f1 = f[1]; i32 f2 = f[2]; i32 f3 = f[3]; i32 f4 = f[4]; + i32 f5 = f[5]; i32 f6 = f[6]; i32 f7 = f[7]; i32 f8 = f[8]; i32 f9 = f[9]; + i32 g0 = g[0]; i32 g1 = g[1]; i32 g2 = g[2]; i32 g3 = g[3]; i32 g4 = g[4]; + i32 g5 = g[5]; i32 g6 = g[6]; i32 g7 = g[7]; i32 g8 = g[8]; i32 g9 = g[9]; + i32 F1 = f1*2; i32 F3 = f3*2; i32 F5 = f5*2; i32 F7 = f7*2; i32 F9 = f9*2; + i32 G1 = g1*19; i32 G2 = g2*19; i32 G3 = g3*19; + i32 G4 = g4*19; i32 G5 = g5*19; i32 G6 = g6*19; + i32 G7 = g7*19; i32 G8 = g8*19; i32 G9 = g9*19; + // |F1|, |F3|, |F5|, |F7|, |F9| < 1.65 * 2^26 + // |G0|, |G2|, |G4|, |G6|, |G8| < 2^31 + // |G1|, |G3|, |G5|, |G7|, |G9| < 2^30 + + i64 t0 = f0*(i64)g0 + F1*(i64)G9 + f2*(i64)G8 + F3*(i64)G7 + f4*(i64)G6 + + F5*(i64)G5 + f6*(i64)G4 + F7*(i64)G3 + f8*(i64)G2 + F9*(i64)G1; + i64 t1 = f0*(i64)g1 + f1*(i64)g0 + f2*(i64)G9 + f3*(i64)G8 + f4*(i64)G7 + + f5*(i64)G6 + f6*(i64)G5 + f7*(i64)G4 + f8*(i64)G3 + f9*(i64)G2; + i64 t2 = f0*(i64)g2 + F1*(i64)g1 + f2*(i64)g0 + F3*(i64)G9 + f4*(i64)G8 + + F5*(i64)G7 + f6*(i64)G6 + F7*(i64)G5 + f8*(i64)G4 + F9*(i64)G3; + i64 t3 = f0*(i64)g3 + f1*(i64)g2 + f2*(i64)g1 + f3*(i64)g0 + f4*(i64)G9 + + f5*(i64)G8 + f6*(i64)G7 + f7*(i64)G6 + f8*(i64)G5 + f9*(i64)G4; + i64 t4 = f0*(i64)g4 + F1*(i64)g3 + f2*(i64)g2 + F3*(i64)g1 + f4*(i64)g0 + + F5*(i64)G9 + f6*(i64)G8 + F7*(i64)G7 + f8*(i64)G6 + F9*(i64)G5; + i64 t5 = f0*(i64)g5 + f1*(i64)g4 + f2*(i64)g3 + f3*(i64)g2 + f4*(i64)g1 + + f5*(i64)g0 + f6*(i64)G9 + f7*(i64)G8 + f8*(i64)G7 + f9*(i64)G6; + i64 t6 = f0*(i64)g6 + F1*(i64)g5 + f2*(i64)g4 + F3*(i64)g3 + f4*(i64)g2 + + F5*(i64)g1 + f6*(i64)g0 + F7*(i64)G9 + f8*(i64)G8 + F9*(i64)G7; + i64 t7 = f0*(i64)g7 + f1*(i64)g6 + f2*(i64)g5 + f3*(i64)g4 + f4*(i64)g3 + + f5*(i64)g2 + f6*(i64)g1 + f7*(i64)g0 + f8*(i64)G9 + f9*(i64)G8; + i64 t8 = f0*(i64)g8 + F1*(i64)g7 + f2*(i64)g6 + F3*(i64)g5 + f4*(i64)g4 + + F5*(i64)g3 + f6*(i64)g2 + F7*(i64)g1 + f8*(i64)g0 + F9*(i64)G9; + i64 t9 = f0*(i64)g9 + f1*(i64)g8 + f2*(i64)g7 + f3*(i64)g6 + f4*(i64)g5 + + f5*(i64)g4 + f6*(i64)g3 + f7*(i64)g2 + f8*(i64)g1 + f9*(i64)g0; + // t0 < 0.67 * 2^61 + // t1 < 0.41 * 2^61 + // t2 < 0.52 * 2^61 + // t3 < 0.32 * 2^61 + // t4 < 0.38 * 2^61 + // t5 < 0.22 * 2^61 + // t6 < 0.23 * 2^61 + // t7 < 0.13 * 2^61 + // t8 < 0.09 * 2^61 + // t9 < 0.03 * 2^61 + + FE_CARRY; // Everything below 2^62, Carry precondition OK +} + +// Precondition +// ------------- +// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26 +// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25 +// +// Note: we could use fe_mul() for this, but this is significantly faster +static void fe_sq(fe h, const fe f) +{ + i32 f0 = f[0]; i32 f1 = f[1]; i32 f2 = f[2]; i32 f3 = f[3]; i32 f4 = f[4]; + i32 f5 = f[5]; i32 f6 = f[6]; i32 f7 = f[7]; i32 f8 = f[8]; i32 f9 = f[9]; + i32 f0_2 = f0*2; i32 f1_2 = f1*2; i32 f2_2 = f2*2; i32 f3_2 = f3*2; + i32 f4_2 = f4*2; i32 f5_2 = f5*2; i32 f6_2 = f6*2; i32 f7_2 = f7*2; + i32 f5_38 = f5*38; i32 f6_19 = f6*19; i32 f7_38 = f7*38; + i32 f8_19 = f8*19; i32 f9_38 = f9*38; + // |f0_2| , |f2_2| , |f4_2| , |f6_2| , |f8_2| < 1.65 * 2^27 + // |f1_2| , |f3_2| , |f5_2| , |f7_2| , |f9_2| < 1.65 * 2^26 + // |f5_38|, |f6_19|, |f7_38|, |f8_19|, |f9_38| < 2^31 + + i64 t0 = f0 *(i64)f0 + f1_2*(i64)f9_38 + f2_2*(i64)f8_19 + + f3_2*(i64)f7_38 + f4_2*(i64)f6_19 + f5 *(i64)f5_38; + i64 t1 = f0_2*(i64)f1 + f2 *(i64)f9_38 + f3_2*(i64)f8_19 + + f4 *(i64)f7_38 + f5_2*(i64)f6_19; + i64 t2 = f0_2*(i64)f2 + f1_2*(i64)f1 + f3_2*(i64)f9_38 + + f4_2*(i64)f8_19 + f5_2*(i64)f7_38 + f6 *(i64)f6_19; + i64 t3 = f0_2*(i64)f3 + f1_2*(i64)f2 + f4 *(i64)f9_38 + + f5_2*(i64)f8_19 + f6 *(i64)f7_38; + i64 t4 = f0_2*(i64)f4 + f1_2*(i64)f3_2 + f2 *(i64)f2 + + f5_2*(i64)f9_38 + f6_2*(i64)f8_19 + f7 *(i64)f7_38; + i64 t5 = f0_2*(i64)f5 + f1_2*(i64)f4 + f2_2*(i64)f3 + + f6 *(i64)f9_38 + f7_2*(i64)f8_19; + i64 t6 = f0_2*(i64)f6 + f1_2*(i64)f5_2 + f2_2*(i64)f4 + + f3_2*(i64)f3 + f7_2*(i64)f9_38 + f8 *(i64)f8_19; + i64 t7 = f0_2*(i64)f7 + f1_2*(i64)f6 + f2_2*(i64)f5 + + f3_2*(i64)f4 + f8 *(i64)f9_38; + i64 t8 = f0_2*(i64)f8 + f1_2*(i64)f7_2 + f2_2*(i64)f6 + + f3_2*(i64)f5_2 + f4 *(i64)f4 + f9 *(i64)f9_38; + i64 t9 = f0_2*(i64)f9 + f1_2*(i64)f8 + f2_2*(i64)f7 + + f3_2*(i64)f6 + f4 *(i64)f5_2; + // t0 < 0.67 * 2^61 + // t1 < 0.41 * 2^61 + // t2 < 0.52 * 2^61 + // t3 < 0.32 * 2^61 + // t4 < 0.38 * 2^61 + // t5 < 0.22 * 2^61 + // t6 < 0.23 * 2^61 + // t7 < 0.13 * 2^61 + // t8 < 0.09 * 2^61 + // t9 < 0.03 * 2^61 + + FE_CARRY; +} + +// Parity check. Returns 0 if even, 1 if odd +static int fe_isodd(const fe f) +{ + u8 s[32]; + fe_tobytes(s, f); + u8 isodd = s[0] & 1; + WIPE_BUFFER(s); + return isodd; +} + +// Returns 1 if equal, 0 if not equal +static int fe_isequal(const fe f, const fe g) +{ + u8 fs[32]; + u8 gs[32]; + fe_tobytes(fs, f); + fe_tobytes(gs, g); + int isdifferent = crypto_verify32(fs, gs); + WIPE_BUFFER(fs); + WIPE_BUFFER(gs); + return 1 + isdifferent; +} + +// Inverse square root. +// Returns true if x is a square, false otherwise. +// After the call: +// isr = sqrt(1/x) if x is a non-zero square. +// isr = sqrt(sqrt(-1)/x) if x is not a square. +// isr = 0 if x is zero. +// We do not guarantee the sign of the square root. +// +// Notes: +// Let quartic = x^((p-1)/4) +// +// x^((p-1)/2) = chi(x) +// quartic^2 = chi(x) +// quartic = sqrt(chi(x)) +// quartic = 1 or -1 or sqrt(-1) or -sqrt(-1) +// +// Note that x is a square if quartic is 1 or -1 +// There are 4 cases to consider: +// +// if quartic = 1 (x is a square) +// then x^((p-1)/4) = 1 +// x^((p-5)/4) * x = 1 +// x^((p-5)/4) = 1/x +// x^((p-5)/8) = sqrt(1/x) or -sqrt(1/x) +// +// if quartic = -1 (x is a square) +// then x^((p-1)/4) = -1 +// x^((p-5)/4) * x = -1 +// x^((p-5)/4) = -1/x +// x^((p-5)/8) = sqrt(-1) / sqrt(x) +// x^((p-5)/8) * sqrt(-1) = sqrt(-1)^2 / sqrt(x) +// x^((p-5)/8) * sqrt(-1) = -1/sqrt(x) +// x^((p-5)/8) * sqrt(-1) = -sqrt(1/x) or sqrt(1/x) +// +// if quartic = sqrt(-1) (x is not a square) +// then x^((p-1)/4) = sqrt(-1) +// x^((p-5)/4) * x = sqrt(-1) +// x^((p-5)/4) = sqrt(-1)/x +// x^((p-5)/8) = sqrt(sqrt(-1)/x) or -sqrt(sqrt(-1)/x) +// +// Note that the product of two non-squares is always a square: +// For any non-squares a and b, chi(a) = -1 and chi(b) = -1. +// Since chi(x) = x^((p-1)/2), chi(a)*chi(b) = chi(a*b) = 1. +// Therefore a*b is a square. +// +// Since sqrt(-1) and x are both non-squares, their product is a +// square, and we can compute their square root. +// +// if quartic = -sqrt(-1) (x is not a square) +// then x^((p-1)/4) = -sqrt(-1) +// x^((p-5)/4) * x = -sqrt(-1) +// x^((p-5)/4) = -sqrt(-1)/x +// x^((p-5)/8) = sqrt(-sqrt(-1)/x) +// x^((p-5)/8) = sqrt( sqrt(-1)/x) * sqrt(-1) +// x^((p-5)/8) * sqrt(-1) = sqrt( sqrt(-1)/x) * sqrt(-1)^2 +// x^((p-5)/8) * sqrt(-1) = sqrt( sqrt(-1)/x) * -1 +// x^((p-5)/8) * sqrt(-1) = -sqrt(sqrt(-1)/x) or sqrt(sqrt(-1)/x) +static int invsqrt(fe isr, const fe x) +{ + fe t0, t1, t2; + + // t0 = x^((p-5)/8) + // Can be achieved with a simple double & add ladder, + // but it would be slower. + fe_sq(t0, x); + fe_sq(t1,t0); fe_sq(t1, t1); fe_mul(t1, x, t1); + fe_mul(t0, t0, t1); + fe_sq(t0, t0); fe_mul(t0, t1, t0); + fe_sq(t1, t0); FOR (i, 1, 5) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); + fe_sq(t1, t0); FOR (i, 1, 10) { fe_sq(t1, t1); } fe_mul(t1, t1, t0); + fe_sq(t2, t1); FOR (i, 1, 20) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); + fe_sq(t1, t1); FOR (i, 1, 10) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); + fe_sq(t1, t0); FOR (i, 1, 50) { fe_sq(t1, t1); } fe_mul(t1, t1, t0); + fe_sq(t2, t1); FOR (i, 1, 100) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); + fe_sq(t1, t1); FOR (i, 1, 50) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); + fe_sq(t0, t0); FOR (i, 1, 2) { fe_sq(t0, t0); } fe_mul(t0, t0, x); + + // quartic = x^((p-1)/4) + i32 *quartic = t1; + fe_sq (quartic, t0); + fe_mul(quartic, quartic, x); + + i32 *check = t2; + fe_0 (check); int z0 = fe_isequal(x , check); + fe_1 (check); int p1 = fe_isequal(quartic, check); + fe_neg(check, check ); int m1 = fe_isequal(quartic, check); + fe_neg(check, sqrtm1); int ms = fe_isequal(quartic, check); + + // if quartic == -1 or sqrt(-1) + // then isr = x^((p-1)/4) * sqrt(-1) + // else isr = x^((p-1)/4) + fe_mul(isr, t0, sqrtm1); + fe_ccopy(isr, t0, 1 - (m1 | ms)); + + WIPE_BUFFER(t0); + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); + return p1 | m1 | z0; +} + +// Inverse in terms of inverse square root. +// Requires two additional squarings to get rid of the sign. +// +// 1/x = x * (+invsqrt(x^2))^2 +// = x * (-invsqrt(x^2))^2 +// +// A fully optimised exponentiation by p-1 would save 6 field +// multiplications, but it would require more code. +static void fe_invert(fe out, const fe x) +{ + fe tmp; + fe_sq(tmp, x); + invsqrt(tmp, tmp); + fe_sq(tmp, tmp); + fe_mul(out, tmp, x); + WIPE_BUFFER(tmp); +} + +// trim a scalar for scalar multiplication +void crypto_eddsa_trim_scalar(u8 out[32], const u8 in[32]) +{ + COPY(out, in, 32); + out[ 0] &= 248; + out[31] &= 127; + out[31] |= 64; +} + +// get bit from scalar at position i +static int scalar_bit(const u8 s[32], int i) +{ + if (i < 0) { return 0; } // handle -1 for sliding windows + return (s[i>>3] >> (i&7)) & 1; +} + +/////////////// +/// X-25519 /// Taken from SUPERCOP's ref10 implementation. +/////////////// +static void scalarmult(u8 q[32], const u8 scalar[32], const u8 p[32], + int nb_bits) +{ + // computes the scalar product + fe x1; + fe_frombytes(x1, p); + + // computes the actual scalar product (the result is in x2 and z2) + fe x2, z2, x3, z3, t0, t1; + // Montgomery ladder + // In projective coordinates, to avoid divisions: x = X / Z + // We don't care about the y coordinate, it's only 1 bit of information + fe_1(x2); fe_0(z2); // "zero" point + fe_copy(x3, x1); fe_1(z3); // "one" point + int swap = 0; + for (int pos = nb_bits-1; pos >= 0; --pos) { + // constant time conditional swap before ladder step + int b = scalar_bit(scalar, pos); + swap ^= b; // xor trick avoids swapping at the end of the loop + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + swap = b; // anticipates one last swap after the loop + + // Montgomery ladder step: replaces (P2, P3) by (P2*2, P2+P3) + // with differential addition + fe_sub(t0, x3, z3); + fe_sub(t1, x2, z2); + fe_add(x2, x2, z2); + fe_add(z2, x3, z3); + fe_mul(z3, t0, x2); + fe_mul(z2, z2, t1); + fe_sq (t0, t1 ); + fe_sq (t1, x2 ); + fe_add(x3, z3, z2); + fe_sub(z2, z3, z2); + fe_mul(x2, t1, t0); + fe_sub(t1, t1, t0); + fe_sq (z2, z2 ); + fe_mul_small(z3, t1, 121666); + fe_sq (x3, x3 ); + fe_add(t0, t0, z3); + fe_mul(z3, x1, z2); + fe_mul(z2, t1, t0); + } + // last swap is necessary to compensate for the xor trick + // Note: after this swap, P3 == P2 + P1. + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + + // normalises the coordinates: x == X / Z + fe_invert(z2, z2); + fe_mul(x2, x2, z2); + fe_tobytes(q, x2); + + WIPE_BUFFER(x1); + WIPE_BUFFER(x2); WIPE_BUFFER(z2); WIPE_BUFFER(t0); + WIPE_BUFFER(x3); WIPE_BUFFER(z3); WIPE_BUFFER(t1); +} + +void crypto_x25519(u8 raw_shared_secret[32], + const u8 your_secret_key [32], + const u8 their_public_key [32]) +{ + // restrict the possible scalar values + u8 e[32]; + crypto_eddsa_trim_scalar(e, your_secret_key); + scalarmult(raw_shared_secret, e, their_public_key, 255); + WIPE_BUFFER(e); +} + +void crypto_x25519_public_key(u8 public_key[32], + const u8 secret_key[32]) +{ + static const u8 base_point[32] = {9}; + crypto_x25519(public_key, secret_key, base_point); +} + +/////////////////////////// +/// Arithmetic modulo L /// +/////////////////////////// +static const u32 L[8] = { + 0x5cf5d3ed, 0x5812631a, 0xa2f79cd6, 0x14def9de, + 0x00000000, 0x00000000, 0x00000000, 0x10000000, +}; + +// p = a*b + p +static void multiply(u32 p[16], const u32 a[8], const u32 b[8]) +{ + FOR (i, 0, 8) { + u64 carry = 0; + FOR (j, 0, 8) { + carry += p[i+j] + (u64)a[i] * b[j]; + p[i+j] = (u32)carry; + carry >>= 32; + } + p[i+8] = (u32)carry; + } +} + +static int is_above_l(const u32 x[8]) +{ + // We work with L directly, in a 2's complement encoding + // (-L == ~L + 1) + u64 carry = 1; + FOR (i, 0, 8) { + carry += (u64)x[i] + (~L[i] & 0xffffffff); + carry >>= 32; + } + return (int)carry; // carry is either 0 or 1 +} + +// Final reduction modulo L, by conditionally removing L. +// if x < l , then r = x +// if l <= x 2*l, then r = x-l +// otherwise the result will be wrong +static void remove_l(u32 r[8], const u32 x[8]) +{ + u64 carry = (u64)is_above_l(x); + u32 mask = ~(u32)carry + 1; // carry == 0 or 1 + FOR (i, 0, 8) { + carry += (u64)x[i] + (~L[i] & mask); + r[i] = (u32)carry; + carry >>= 32; + } +} + +// Full reduction modulo L (Barrett reduction) +static void mod_l(u8 reduced[32], const u32 x[16]) +{ + static const u32 r[9] = { + 0x0a2c131b,0xed9ce5a3,0x086329a7,0x2106215d, + 0xffffffeb,0xffffffff,0xffffffff,0xffffffff,0xf, + }; + // xr = x * r + u32 xr[25] = {0}; + FOR (i, 0, 9) { + u64 carry = 0; + FOR (j, 0, 16) { + carry += xr[i+j] + (u64)r[i] * x[j]; + xr[i+j] = (u32)carry; + carry >>= 32; + } + xr[i+16] = (u32)carry; + } + // xr = floor(xr / 2^512) * L + // Since the result is guaranteed to be below 2*L, + // it is enough to only compute the first 256 bits. + // The division is performed by saying xr[i+16]. (16 * 32 = 512) + ZERO(xr, 8); + FOR (i, 0, 8) { + u64 carry = 0; + FOR (j, 0, 8-i) { + carry += xr[i+j] + (u64)xr[i+16] * L[j]; + xr[i+j] = (u32)carry; + carry >>= 32; + } + } + // xr = x - xr + u64 carry = 1; + FOR (i, 0, 8) { + carry += (u64)x[i] + (~xr[i] & 0xffffffff); + xr[i] = (u32)carry; + carry >>= 32; + } + // Final reduction modulo L (conditional subtraction) + remove_l(xr, xr); + store32_le_buf(reduced, xr, 8); + + WIPE_BUFFER(xr); +} + +void crypto_eddsa_reduce(u8 reduced[32], const u8 expanded[64]) +{ + u32 x[16]; + load32_le_buf(x, expanded, 16); + mod_l(reduced, x); + WIPE_BUFFER(x); +} + +// r = (a * b) + c +void crypto_eddsa_mul_add(u8 r[32], + const u8 a[32], const u8 b[32], const u8 c[32]) +{ + u32 A[8]; load32_le_buf(A, a, 8); + u32 B[8]; load32_le_buf(B, b, 8); + u32 p[16]; load32_le_buf(p, c, 8); ZERO(p + 8, 8); + multiply(p, A, B); + mod_l(r, p); + WIPE_BUFFER(p); + WIPE_BUFFER(A); + WIPE_BUFFER(B); +} + +/////////////// +/// Ed25519 /// +/////////////// + +// Point (group element, ge) in a twisted Edwards curve, +// in extended projective coordinates. +// ge : x = X/Z, y = Y/Z, T = XY/Z +// ge_cached : Yp = X+Y, Ym = X-Y, T2 = T*D2 +// ge_precomp: Z = 1 +typedef struct { fe X; fe Y; fe Z; fe T; } ge; +typedef struct { fe Yp; fe Ym; fe Z; fe T2; } ge_cached; +typedef struct { fe Yp; fe Ym; fe T2; } ge_precomp; + +static void ge_zero(ge *p) +{ + fe_0(p->X); + fe_1(p->Y); + fe_1(p->Z); + fe_0(p->T); +} + +static void ge_tobytes(u8 s[32], const ge *h) +{ + fe recip, x, y; + fe_invert(recip, h->Z); + fe_mul(x, h->X, recip); + fe_mul(y, h->Y, recip); + fe_tobytes(s, y); + s[31] ^= (u8)fe_isodd(x) << 7; + + WIPE_BUFFER(recip); + WIPE_BUFFER(x); + WIPE_BUFFER(y); +} + +// h = -s, where s is a point encoded in 32 bytes +// +// Variable time! Inputs must not be secret! +// => Use only to *check* signatures. +// +// From the specifications: +// The encoding of s contains y and the sign of x +// x = sqrt((y^2 - 1) / (d*y^2 + 1)) +// In extended coordinates: +// X = x, Y = y, Z = 1, T = x*y +// +// Note that num * den is a square iff num / den is a square +// If num * den is not a square, the point was not on the curve. +// From the above: +// Let num = y^2 - 1 +// Let den = d*y^2 + 1 +// x = sqrt((y^2 - 1) / (d*y^2 + 1)) +// x = sqrt(num / den) +// x = sqrt(num^2 / (num * den)) +// x = num * sqrt(1 / (num * den)) +// +// Therefore, we can just compute: +// num = y^2 - 1 +// den = d*y^2 + 1 +// isr = invsqrt(num * den) // abort if not square +// x = num * isr +// Finally, negate x if its sign is not as specified. +static int ge_frombytes_neg_vartime(ge *h, const u8 s[32]) +{ + fe_frombytes(h->Y, s); + fe_1(h->Z); + fe_sq (h->T, h->Y); // t = y^2 + fe_mul(h->X, h->T, d ); // x = d*y^2 + fe_sub(h->T, h->T, h->Z); // t = y^2 - 1 + fe_add(h->X, h->X, h->Z); // x = d*y^2 + 1 + fe_mul(h->X, h->T, h->X); // x = (y^2 - 1) * (d*y^2 + 1) + int is_square = invsqrt(h->X, h->X); + if (!is_square) { + return -1; // Not on the curve, abort + } + fe_mul(h->X, h->T, h->X); // x = sqrt((y^2 - 1) / (d*y^2 + 1)) + if (fe_isodd(h->X) == (s[31] >> 7)) { + fe_neg(h->X, h->X); + } + fe_mul(h->T, h->X, h->Y); + return 0; +} + +static void ge_cache(ge_cached *c, const ge *p) +{ + fe_add (c->Yp, p->Y, p->X); + fe_sub (c->Ym, p->Y, p->X); + fe_copy(c->Z , p->Z ); + fe_mul (c->T2, p->T, D2 ); +} + +// Internal buffers are not wiped! Inputs must not be secret! +// => Use only to *check* signatures. +static void ge_add(ge *s, const ge *p, const ge_cached *q) +{ + fe a, b; + fe_add(a , p->Y, p->X ); + fe_sub(b , p->Y, p->X ); + fe_mul(a , a , q->Yp); + fe_mul(b , b , q->Ym); + fe_add(s->Y, a , b ); + fe_sub(s->X, a , b ); + + fe_add(s->Z, p->Z, p->Z ); + fe_mul(s->Z, s->Z, q->Z ); + fe_mul(s->T, p->T, q->T2); + fe_add(a , s->Z, s->T ); + fe_sub(b , s->Z, s->T ); + + fe_mul(s->T, s->X, s->Y); + fe_mul(s->X, s->X, b ); + fe_mul(s->Y, s->Y, a ); + fe_mul(s->Z, a , b ); +} + +// Internal buffers are not wiped! Inputs must not be secret! +// => Use only to *check* signatures. +static void ge_sub(ge *s, const ge *p, const ge_cached *q) +{ + ge_cached neg; + fe_copy(neg.Ym, q->Yp); + fe_copy(neg.Yp, q->Ym); + fe_copy(neg.Z , q->Z ); + fe_neg (neg.T2, q->T2); + ge_add(s, p, &neg); +} + +static void ge_madd(ge *s, const ge *p, const ge_precomp *q, fe a, fe b) +{ + fe_add(a , p->Y, p->X ); + fe_sub(b , p->Y, p->X ); + fe_mul(a , a , q->Yp); + fe_mul(b , b , q->Ym); + fe_add(s->Y, a , b ); + fe_sub(s->X, a , b ); + + fe_add(s->Z, p->Z, p->Z ); + fe_mul(s->T, p->T, q->T2); + fe_add(a , s->Z, s->T ); + fe_sub(b , s->Z, s->T ); + + fe_mul(s->T, s->X, s->Y); + fe_mul(s->X, s->X, b ); + fe_mul(s->Y, s->Y, a ); + fe_mul(s->Z, a , b ); +} + +// Internal buffers are not wiped! Inputs must not be secret! +// => Use only to *check* signatures. +static void ge_msub(ge *s, const ge *p, const ge_precomp *q, fe a, fe b) +{ + ge_precomp neg; + fe_copy(neg.Ym, q->Yp); + fe_copy(neg.Yp, q->Ym); + fe_neg (neg.T2, q->T2); + ge_madd(s, p, &neg, a, b); +} + +static void ge_double(ge *s, const ge *p, ge *q) +{ + fe_sq (q->X, p->X); + fe_sq (q->Y, p->Y); + fe_sq (q->Z, p->Z); // qZ = pZ^2 + fe_mul_small(q->Z, q->Z, 2); // qZ = pZ^2 * 2 + fe_add(q->T, p->X, p->Y); + fe_sq (s->T, q->T); + fe_add(q->T, q->Y, q->X); + fe_sub(q->Y, q->Y, q->X); + fe_sub(q->X, s->T, q->T); + fe_sub(q->Z, q->Z, q->Y); + + fe_mul(s->X, q->X , q->Z); + fe_mul(s->Y, q->T , q->Y); + fe_mul(s->Z, q->Y , q->Z); + fe_mul(s->T, q->X , q->T); +} + +// 5-bit signed window in cached format (Niels coordinates, Z=1) +static const ge_precomp b_window[8] = { + {{25967493,-14356035,29566456,3660896,-12694345, + 4014787,27544626,-11754271,-6079156,2047605,}, + {-12545711,934262,-2722910,3049990,-727428, + 9406986,12720692,5043384,19500929,-15469378,}, + {-8738181,4489570,9688441,-14785194,10184609, + -12363380,29287919,11864899,-24514362,-4438546,},}, + {{15636291,-9688557,24204773,-7912398,616977, + -16685262,27787600,-14772189,28944400,-1550024,}, + {16568933,4717097,-11556148,-1102322,15682896, + -11807043,16354577,-11775962,7689662,11199574,}, + {30464156,-5976125,-11779434,-15670865,23220365, + 15915852,7512774,10017326,-17749093,-9920357,},}, + {{10861363,11473154,27284546,1981175,-30064349, + 12577861,32867885,14515107,-15438304,10819380,}, + {4708026,6336745,20377586,9066809,-11272109, + 6594696,-25653668,12483688,-12668491,5581306,}, + {19563160,16186464,-29386857,4097519,10237984, + -4348115,28542350,13850243,-23678021,-15815942,},}, + {{5153746,9909285,1723747,-2777874,30523605, + 5516873,19480852,5230134,-23952439,-15175766,}, + {-30269007,-3463509,7665486,10083793,28475525, + 1649722,20654025,16520125,30598449,7715701,}, + {28881845,14381568,9657904,3680757,-20181635, + 7843316,-31400660,1370708,29794553,-1409300,},}, + {{-22518993,-6692182,14201702,-8745502,-23510406, + 8844726,18474211,-1361450,-13062696,13821877,}, + {-6455177,-7839871,3374702,-4740862,-27098617, + -10571707,31655028,-7212327,18853322,-14220951,}, + {4566830,-12963868,-28974889,-12240689,-7602672, + -2830569,-8514358,-10431137,2207753,-3209784,},}, + {{-25154831,-4185821,29681144,7868801,-6854661, + -9423865,-12437364,-663000,-31111463,-16132436,}, + {25576264,-2703214,7349804,-11814844,16472782, + 9300885,3844789,15725684,171356,6466918,}, + {23103977,13316479,9739013,-16149481,817875, + -15038942,8965339,-14088058,-30714912,16193877,},}, + {{-33521811,3180713,-2394130,14003687,-16903474, + -16270840,17238398,4729455,-18074513,9256800,}, + {-25182317,-4174131,32336398,5036987,-21236817, + 11360617,22616405,9761698,-19827198,630305,}, + {-13720693,2639453,-24237460,-7406481,9494427, + -5774029,-6554551,-15960994,-2449256,-14291300,},}, + {{-3151181,-5046075,9282714,6866145,-31907062, + -863023,-18940575,15033784,25105118,-7894876,}, + {-24326370,15950226,-31801215,-14592823,-11662737, + -5090925,1573892,-2625887,2198790,-15804619,}, + {-3099351,10324967,-2241613,7453183,-5446979, + -2735503,-13812022,-16236442,-32461234,-12290683,},}, +}; + +// Incremental sliding windows (left to right) +// Based on Roberto Maria Avanzi[2005] +typedef struct { + i16 next_index; // position of the next signed digit + i8 next_digit; // next signed digit (odd number below 2^window_width) + u8 next_check; // point at which we must check for a new window +} slide_ctx; + +static void slide_init(slide_ctx *ctx, const u8 scalar[32]) +{ + // scalar is guaranteed to be below L, either because we checked (s), + // or because we reduced it modulo L (h_ram). L is under 2^253, so + // so bits 253 to 255 are guaranteed to be zero. No need to test them. + // + // Note however that L is very close to 2^252, so bit 252 is almost + // always zero. If we were to start at bit 251, the tests wouldn't + // catch the off-by-one error (constructing one that does would be + // prohibitively expensive). + // + // We should still check bit 252, though. + int i = 252; + while (i > 0 && scalar_bit(scalar, i) == 0) { + i--; + } + ctx->next_check = (u8)(i + 1); + ctx->next_index = -1; + ctx->next_digit = -1; +} + +static int slide_step(slide_ctx *ctx, int width, int i, const u8 scalar[32]) +{ + if (i == ctx->next_check) { + if (scalar_bit(scalar, i) == scalar_bit(scalar, i - 1)) { + ctx->next_check--; + } else { + // compute digit of next window + int w = MIN(width, i + 1); + int v = -(scalar_bit(scalar, i) << (w-1)); + FOR_T (int, j, 0, w-1) { + v += scalar_bit(scalar, i-(w-1)+j) << j; + } + v += scalar_bit(scalar, i-w); + int lsb = v & (~v + 1); // smallest bit of v + int s = // log2(lsb) + (((lsb & 0xAA) != 0) << 0) | + (((lsb & 0xCC) != 0) << 1) | + (((lsb & 0xF0) != 0) << 2); + ctx->next_index = (i16)(i-(w-1)+s); + ctx->next_digit = (i8) (v >> s ); + ctx->next_check -= (u8) w; + } + } + return i == ctx->next_index ? ctx->next_digit: 0; +} + +#define P_W_WIDTH 3 // Affects the size of the stack +#define B_W_WIDTH 5 // Affects the size of the binary +#define P_W_SIZE (1<<(P_W_WIDTH-2)) + +int crypto_eddsa_check_equation(const u8 signature[64], const u8 public_key[32], + const u8 h[32]) +{ + ge minus_A; // -public_key + ge minus_R; // -first_half_of_signature + const u8 *s = signature + 32; + + // Check that A and R are on the curve + // Check that 0 <= S < L (prevents malleability) + // *Allow* non-cannonical encoding for A and R + { + u32 s32[8]; + load32_le_buf(s32, s, 8); + if (ge_frombytes_neg_vartime(&minus_A, public_key) || + ge_frombytes_neg_vartime(&minus_R, signature) || + is_above_l(s32)) { + return -1; + } + } + + // look-up table for minus_A + ge_cached lutA[P_W_SIZE]; + { + ge minus_A2, tmp; + ge_double(&minus_A2, &minus_A, &tmp); + ge_cache(&lutA[0], &minus_A); + FOR (i, 1, P_W_SIZE) { + ge_add(&tmp, &minus_A2, &lutA[i-1]); + ge_cache(&lutA[i], &tmp); + } + } + + // sum = [s]B - [h]A + // Merged double and add ladder, fused with sliding + slide_ctx h_slide; slide_init(&h_slide, h); + slide_ctx s_slide; slide_init(&s_slide, s); + int i = MAX(h_slide.next_check, s_slide.next_check); + ge *sum = &minus_A; // reuse minus_A for the sum + ge_zero(sum); + while (i >= 0) { + ge tmp; + ge_double(sum, sum, &tmp); + int h_digit = slide_step(&h_slide, P_W_WIDTH, i, h); + int s_digit = slide_step(&s_slide, B_W_WIDTH, i, s); + if (h_digit > 0) { ge_add(sum, sum, &lutA[ h_digit / 2]); } + if (h_digit < 0) { ge_sub(sum, sum, &lutA[-h_digit / 2]); } + fe t1, t2; + if (s_digit > 0) { ge_madd(sum, sum, b_window + s_digit/2, t1, t2); } + if (s_digit < 0) { ge_msub(sum, sum, b_window + -s_digit/2, t1, t2); } + i--; + } + + // Compare [8](sum-R) and the zero point + // The multiplication by 8 eliminates any low-order component + // and ensures consistency with batched verification. + ge_cached cached; + u8 check[32]; + static const u8 zero_point[32] = {1}; // Point of order 1 + ge_cache(&cached, &minus_R); + ge_add(sum, sum, &cached); + ge_double(sum, sum, &minus_R); // reuse minus_R as temporary + ge_double(sum, sum, &minus_R); // reuse minus_R as temporary + ge_double(sum, sum, &minus_R); // reuse minus_R as temporary + ge_tobytes(check, sum); + return crypto_verify32(check, zero_point); +} + +// 5-bit signed comb in cached format (Niels coordinates, Z=1) +static const ge_precomp b_comb_low[8] = { + {{-6816601,-2324159,-22559413,124364,18015490, + 8373481,19993724,1979872,-18549925,9085059,}, + {10306321,403248,14839893,9633706,8463310, + -8354981,-14305673,14668847,26301366,2818560,}, + {-22701500,-3210264,-13831292,-2927732,-16326337, + -14016360,12940910,177905,12165515,-2397893,},}, + {{-12282262,-7022066,9920413,-3064358,-32147467, + 2927790,22392436,-14852487,2719975,16402117,}, + {-7236961,-4729776,2685954,-6525055,-24242706, + -15940211,-6238521,14082855,10047669,12228189,}, + {-30495588,-12893761,-11161261,3539405,-11502464, + 16491580,-27286798,-15030530,-7272871,-15934455,},}, + {{17650926,582297,-860412,-187745,-12072900, + -10683391,-20352381,15557840,-31072141,-5019061,}, + {-6283632,-2259834,-4674247,-4598977,-4089240, + 12435688,-31278303,1060251,6256175,10480726,}, + {-13871026,2026300,-21928428,-2741605,-2406664, + -8034988,7355518,15733500,-23379862,7489131,},}, + {{6883359,695140,23196907,9644202,-33430614, + 11354760,-20134606,6388313,-8263585,-8491918,}, + {-7716174,-13605463,-13646110,14757414,-19430591, + -14967316,10359532,-11059670,-21935259,12082603,}, + {-11253345,-15943946,10046784,5414629,24840771, + 8086951,-6694742,9868723,15842692,-16224787,},}, + {{9639399,11810955,-24007778,-9320054,3912937, + -9856959,996125,-8727907,-8919186,-14097242,}, + {7248867,14468564,25228636,-8795035,14346339, + 8224790,6388427,-7181107,6468218,-8720783,}, + {15513115,15439095,7342322,-10157390,18005294, + -7265713,2186239,4884640,10826567,7135781,},}, + {{-14204238,5297536,-5862318,-6004934,28095835, + 4236101,-14203318,1958636,-16816875,3837147,}, + {-5511166,-13176782,-29588215,12339465,15325758, + -15945770,-8813185,11075932,-19608050,-3776283,}, + {11728032,9603156,-4637821,-5304487,-7827751, + 2724948,31236191,-16760175,-7268616,14799772,},}, + {{-28842672,4840636,-12047946,-9101456,-1445464, + 381905,-30977094,-16523389,1290540,12798615,}, + {27246947,-10320914,14792098,-14518944,5302070, + -8746152,-3403974,-4149637,-27061213,10749585,}, + {25572375,-6270368,-15353037,16037944,1146292, + 32198,23487090,9585613,24714571,-1418265,},}, + {{19844825,282124,-17583147,11004019,-32004269, + -2716035,6105106,-1711007,-21010044,14338445,}, + {8027505,8191102,-18504907,-12335737,25173494, + -5923905,15446145,7483684,-30440441,10009108,}, + {-14134701,-4174411,10246585,-14677495,33553567, + -14012935,23366126,15080531,-7969992,7663473,},}, +}; + +static const ge_precomp b_comb_high[8] = { + {{33055887,-4431773,-521787,6654165,951411, + -6266464,-5158124,6995613,-5397442,-6985227,}, + {4014062,6967095,-11977872,3960002,8001989, + 5130302,-2154812,-1899602,-31954493,-16173976,}, + {16271757,-9212948,23792794,731486,-25808309, + -3546396,6964344,-4767590,10976593,10050757,},}, + {{2533007,-4288439,-24467768,-12387405,-13450051, + 14542280,12876301,13893535,15067764,8594792,}, + {20073501,-11623621,3165391,-13119866,13188608, + -11540496,-10751437,-13482671,29588810,2197295,}, + {-1084082,11831693,6031797,14062724,14748428, + -8159962,-20721760,11742548,31368706,13161200,},}, + {{2050412,-6457589,15321215,5273360,25484180, + 124590,-18187548,-7097255,-6691621,-14604792,}, + {9938196,2162889,-6158074,-1711248,4278932, + -2598531,-22865792,-7168500,-24323168,11746309,}, + {-22691768,-14268164,5965485,9383325,20443693, + 5854192,28250679,-1381811,-10837134,13717818,},}, + {{-8495530,16382250,9548884,-4971523,-4491811, + -3902147,6182256,-12832479,26628081,10395408,}, + {27329048,-15853735,7715764,8717446,-9215518, + -14633480,28982250,-5668414,4227628,242148,}, + {-13279943,-7986904,-7100016,8764468,-27276630, + 3096719,29678419,-9141299,3906709,11265498,},}, + {{11918285,15686328,-17757323,-11217300,-27548967, + 4853165,-27168827,6807359,6871949,-1075745,}, + {-29002610,13984323,-27111812,-2713442,28107359, + -13266203,6155126,15104658,3538727,-7513788,}, + {14103158,11233913,-33165269,9279850,31014152, + 4335090,-1827936,4590951,13960841,12787712,},}, + {{1469134,-16738009,33411928,13942824,8092558, + -8778224,-11165065,1437842,22521552,-2792954,}, + {31352705,-4807352,-25327300,3962447,12541566, + -9399651,-27425693,7964818,-23829869,5541287,}, + {-25732021,-6864887,23848984,3039395,-9147354, + 6022816,-27421653,10590137,25309915,-1584678,},}, + {{-22951376,5048948,31139401,-190316,-19542447, + -626310,-17486305,-16511925,-18851313,-12985140,}, + {-9684890,14681754,30487568,7717771,-10829709, + 9630497,30290549,-10531496,-27798994,-13812825,}, + {5827835,16097107,-24501327,12094619,7413972, + 11447087,28057551,-1793987,-14056981,4359312,},}, + {{26323183,2342588,-21887793,-1623758,-6062284, + 2107090,-28724907,9036464,-19618351,-13055189,}, + {-29697200,14829398,-4596333,14220089,-30022969, + 2955645,12094100,-13693652,-5941445,7047569,}, + {-3201977,14413268,-12058324,-16417589,-9035655, + -7224648,9258160,1399236,30397584,-5684634,},}, +}; + +static void lookup_add(ge *p, ge_precomp *tmp_c, fe tmp_a, fe tmp_b, + const ge_precomp comb[8], const u8 scalar[32], int i) +{ + u8 teeth = (u8)((scalar_bit(scalar, i) ) + + (scalar_bit(scalar, i + 32) << 1) + + (scalar_bit(scalar, i + 64) << 2) + + (scalar_bit(scalar, i + 96) << 3)); + u8 high = teeth >> 3; + u8 index = (teeth ^ (high - 1)) & 7; + FOR (j, 0, 8) { + i32 select = 1 & (((j ^ index) - 1) >> 8); + fe_ccopy(tmp_c->Yp, comb[j].Yp, select); + fe_ccopy(tmp_c->Ym, comb[j].Ym, select); + fe_ccopy(tmp_c->T2, comb[j].T2, select); + } + fe_neg(tmp_a, tmp_c->T2); + fe_cswap(tmp_c->T2, tmp_a , high ^ 1); + fe_cswap(tmp_c->Yp, tmp_c->Ym, high ^ 1); + ge_madd(p, p, tmp_c, tmp_a, tmp_b); +} + +// p = [scalar]B, where B is the base point +static void ge_scalarmult_base(ge *p, const u8 scalar[32]) +{ + // twin 4-bits signed combs, from Mike Hamburg's + // Fast and compact elliptic-curve cryptography (2012) + // 1 / 2 modulo L + static const u8 half_mod_L[32] = { + 247,233,122,46,141,49,9,44,107,206,123,81,239,124,111,10, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8, + }; + // (2^256 - 1) / 2 modulo L + static const u8 half_ones[32] = { + 142,74,204,70,186,24,118,107,184,231,190,57,250,173,119,99, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,7, + }; + + // All bits set form: 1 means 1, 0 means -1 + u8 s_scalar[32]; + crypto_eddsa_mul_add(s_scalar, scalar, half_mod_L, half_ones); + + // Double and add ladder + fe tmp_a, tmp_b; // temporaries for addition + ge_precomp tmp_c; // temporary for comb lookup + ge tmp_d; // temporary for doubling + fe_1(tmp_c.Yp); + fe_1(tmp_c.Ym); + fe_0(tmp_c.T2); + + // Save a double on the first iteration + ge_zero(p); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_low , s_scalar, 31); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_high, s_scalar, 31+128); + // Regular double & add for the rest + for (int i = 30; i >= 0; i--) { + ge_double(p, p, &tmp_d); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_low , s_scalar, i); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_high, s_scalar, i+128); + } + // Note: we could save one addition at the end if we assumed the + // scalar fit in 252 bits. Which it does in practice if it is + // selected at random. However, non-random, non-hashed scalars + // *can* overflow 252 bits in practice. Better account for that + // than leaving that kind of subtle corner case. + + WIPE_BUFFER(tmp_a); WIPE_CTX(&tmp_d); + WIPE_BUFFER(tmp_b); WIPE_CTX(&tmp_c); + WIPE_BUFFER(s_scalar); +} + +void crypto_eddsa_scalarbase(u8 point[32], const u8 scalar[32]) +{ + ge P; + ge_scalarmult_base(&P, scalar); + ge_tobytes(point, &P); + WIPE_CTX(&P); +} + +void crypto_eddsa_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32]) +{ + // To allow overlaps, observable writes happen in this order: + // 1. seed + // 2. secret_key + // 3. public_key + u8 a[64]; + COPY(a, seed, 32); + crypto_wipe(seed, 32); + COPY(secret_key, a, 32); + crypto_blake2b(a, 64, a, 32); + crypto_eddsa_trim_scalar(a, a); + crypto_eddsa_scalarbase(secret_key + 32, a); + COPY(public_key, secret_key + 32, 32); + WIPE_BUFFER(a); +} + +static void hash_reduce(u8 h[32], + const u8 *a, size_t a_size, + const u8 *b, size_t b_size, + const u8 *c, size_t c_size) +{ + u8 hash[64]; + crypto_blake2b_ctx ctx; + crypto_blake2b_init (&ctx, 64); + crypto_blake2b_update(&ctx, a, a_size); + crypto_blake2b_update(&ctx, b, b_size); + crypto_blake2b_update(&ctx, c, c_size); + crypto_blake2b_final (&ctx, hash); + crypto_eddsa_reduce(h, hash); +} + +// Digital signature of a message with from a secret key. +// +// The secret key comprises two parts: +// - The seed that generates the key (secret_key[ 0..31]) +// - The public key (secret_key[32..63]) +// +// The seed and the public key are bundled together to make sure users +// don't use mismatched seeds and public keys, which would instantly +// leak the secret scalar and allow forgeries (allowing this to happen +// has resulted in critical vulnerabilities in the wild). +// +// The seed is hashed to derive the secret scalar and a secret prefix. +// The sole purpose of the prefix is to generate a secret random nonce. +// The properties of that nonce must be as follows: +// - Unique: we need a different one for each message. +// - Secret: third parties must not be able to predict it. +// - Random: any detectable bias would break all security. +// +// There are two ways to achieve these properties. The obvious one is +// to simply generate a random number. Here that would be a parameter +// (Monocypher doesn't have an RNG). It works, but then users may reuse +// the nonce by accident, which _also_ leaks the secret scalar and +// allows forgeries. This has happened in the wild too. +// +// This is no good, so instead we generate that nonce deterministically +// by reducing modulo L a hash of the secret prefix and the message. +// The secret prefix makes the nonce unpredictable, the message makes it +// unique, and the hash/reduce removes all bias. +// +// The cost of that safety is hashing the message twice. If that cost +// is unacceptable, there are two alternatives: +// +// - Signing a hash of the message instead of the message itself. This +// is fine as long as the hash is collision resistant. It is not +// compatible with existing "pure" signatures, but at least it's safe. +// +// - Using a random nonce. Please exercise **EXTREME CAUTION** if you +// ever do that. It is absolutely **critical** that the nonce is +// really an unbiased random number between 0 and L-1, never reused, +// and wiped immediately. +// +// To lower the likelihood of complete catastrophe if the RNG is +// either flawed or misused, you can hash the RNG output together with +// the secret prefix and the beginning of the message, and use the +// reduction of that hash instead of the RNG output itself. It's not +// foolproof (you'd need to hash the whole message) but it helps. +// +// Signing a message involves the following operations: +// +// scalar, prefix = HASH(secret_key) +// r = HASH(prefix || message) % L +// R = [r]B +// h = HASH(R || public_key || message) % L +// S = ((h * a) + r) % L +// signature = R || S +void crypto_eddsa_sign(u8 signature [64], const u8 secret_key[64], + const u8 *message, size_t message_size) +{ + u8 a[64]; // secret scalar and prefix + u8 r[32]; // secret deterministic "random" nonce + u8 h[32]; // publically verifiable hash of the message (not wiped) + u8 R[32]; // first half of the signature (allows overlapping inputs) + + crypto_blake2b(a, 64, secret_key, 32); + crypto_eddsa_trim_scalar(a, a); + hash_reduce(r, a + 32, 32, message, message_size, 0, 0); + crypto_eddsa_scalarbase(R, r); + hash_reduce(h, R, 32, secret_key + 32, 32, message, message_size); + COPY(signature, R, 32); + crypto_eddsa_mul_add(signature + 32, h, a, r); + + WIPE_BUFFER(a); + WIPE_BUFFER(r); +} + +// To check the signature R, S of the message M with the public key A, +// there are 3 steps: +// +// compute h = HASH(R || A || message) % L +// check that A is on the curve. +// check that R == [s]B - [h]A +// +// The last two steps are done in crypto_eddsa_check_equation() +int crypto_eddsa_check(const u8 signature[64], const u8 public_key[32], + const u8 *message, size_t message_size) +{ + u8 h[32]; + hash_reduce(h, signature, 32, public_key, 32, message, message_size); + return crypto_eddsa_check_equation(signature, public_key, h); +} + +///////////////////////// +/// EdDSA <--> X25519 /// +///////////////////////// +void crypto_eddsa_to_x25519(u8 x25519[32], const u8 eddsa[32]) +{ + // (u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x) + // Only converting y to u, the sign of x is ignored. + fe t1, t2; + fe_frombytes(t2, eddsa); + fe_add(t1, fe_one, t2); + fe_sub(t2, fe_one, t2); + fe_invert(t2, t2); + fe_mul(t1, t1, t2); + fe_tobytes(x25519, t1); + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); +} + +void crypto_x25519_to_eddsa(u8 eddsa[32], const u8 x25519[32]) +{ + // (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1)) + // Only converting u to y, x is assumed positive. + fe t1, t2; + fe_frombytes(t2, x25519); + fe_sub(t1, t2, fe_one); + fe_add(t2, t2, fe_one); + fe_invert(t2, t2); + fe_mul(t1, t1, t2); + fe_tobytes(eddsa, t1); + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); +} + +///////////////////////////////////////////// +/// Dirty ephemeral public key generation /// +///////////////////////////////////////////// + +// Those functions generates a public key, *without* clearing the +// cofactor. Sending that key over the network leaks 3 bits of the +// private key. Use only to generate ephemeral keys that will be hidden +// with crypto_curve_to_hidden(). +// +// The public key is otherwise compatible with crypto_x25519(), which +// properly clears the cofactor. +// +// Note that the distribution of the resulting public keys is almost +// uniform. Flipping the sign of the v coordinate (not provided by this +// function), covers the entire key space almost perfectly, where +// "almost" means a 2^-128 bias (undetectable). This uniformity is +// needed to ensure the proper randomness of the resulting +// representatives (once we apply crypto_curve_to_hidden()). +// +// Recall that Curve25519 has order C = 2^255 + e, with e < 2^128 (not +// to be confused with the prime order of the main subgroup, L, which is +// 8 times less than that). +// +// Generating all points would require us to multiply a point of order C +// (the base point plus any point of order 8) by all scalars from 0 to +// C-1. Clamping limits us to scalars between 2^254 and 2^255 - 1. But +// by negating the resulting point at random, we also cover scalars from +// -2^255 + 1 to -2^254 (which modulo C is congruent to e+1 to 2^254 + e). +// +// In practice: +// - Scalars from 0 to e + 1 are never generated +// - Scalars from 2^255 to 2^255 + e are never generated +// - Scalars from 2^254 + 1 to 2^254 + e are generated twice +// +// Since e < 2^128, detecting this bias requires observing over 2^100 +// representatives from a given source (this will never happen), *and* +// recovering enough of the private key to determine that they do, or do +// not, belong to the biased set (this practically requires solving +// discrete logarithm, which is conjecturally intractable). +// +// In practice, this means the bias is impossible to detect. + +// s + (x*L) % 8*L +// Guaranteed to fit in 256 bits iff s fits in 255 bits. +// L < 2^253 +// x%8 < 2^3 +// L * (x%8) < 2^255 +// s < 2^255 +// s + L * (x%8) < 2^256 +static void add_xl(u8 s[32], u8 x) +{ + u64 mod8 = x & 7; + u64 carry = 0; + FOR (i , 0, 8) { + carry = carry + load32_le(s + 4*i) + L[i] * mod8; + store32_le(s + 4*i, (u32)carry); + carry >>= 32; + } +} + +// "Small" dirty ephemeral key. +// Use if you need to shrink the size of the binary, and can afford to +// slow down by a factor of two (compared to the fast version) +// +// This version works by decoupling the cofactor from the main factor. +// +// - The trimmed scalar determines the main factor +// - The clamped bits of the scalar determine the cofactor. +// +// Cofactor and main factor are combined into a single scalar, which is +// then multiplied by a point of order 8*L (unlike the base point, which +// has prime order). That "dirty" base point is the addition of the +// regular base point (9), and a point of order 8. +void crypto_x25519_dirty_small(u8 public_key[32], const u8 secret_key[32]) +{ + // Base point of order 8*L + // Raw scalar multiplication with it does not clear the cofactor, + // and the resulting public key will reveal 3 bits of the scalar. + // + // The low order component of this base point has been chosen + // to yield the same results as crypto_x25519_dirty_fast(). + static const u8 dirty_base_point[32] = { + 0xd8, 0x86, 0x1a, 0xa2, 0x78, 0x7a, 0xd9, 0x26, + 0x8b, 0x74, 0x74, 0xb6, 0x82, 0xe3, 0xbe, 0xc3, + 0xce, 0x36, 0x9a, 0x1e, 0x5e, 0x31, 0x47, 0xa2, + 0x6d, 0x37, 0x7c, 0xfd, 0x20, 0xb5, 0xdf, 0x75, + }; + // separate the main factor & the cofactor of the scalar + u8 scalar[32]; + crypto_eddsa_trim_scalar(scalar, secret_key); + + // Separate the main factor and the cofactor + // + // The scalar is trimmed, so its cofactor is cleared. The three + // least significant bits however still have a main factor. We must + // remove it for X25519 compatibility. + // + // cofactor = lsb * L (modulo 8*L) + // combined = scalar + cofactor (modulo 8*L) + add_xl(scalar, secret_key[0]); + scalarmult(public_key, scalar, dirty_base_point, 256); + WIPE_BUFFER(scalar); +} + +// Select low order point +// We're computing the [cofactor]lop scalar multiplication, where: +// +// cofactor = tweak & 7. +// lop = (lop_x, lop_y) +// lop_x = sqrt((sqrt(d + 1) + 1) / d) +// lop_y = -lop_x * sqrtm1 +// +// The low order point has order 8. There are 4 such points. We've +// chosen the one whose both coordinates are positive (below p/2). +// The 8 low order points are as follows: +// +// [0]lop = ( 0 , 1 ) +// [1]lop = ( lop_x , lop_y) +// [2]lop = ( sqrt(-1), -0 ) +// [3]lop = ( lop_x , -lop_y) +// [4]lop = (-0 , -1 ) +// [5]lop = (-lop_x , -lop_y) +// [6]lop = (-sqrt(-1), 0 ) +// [7]lop = (-lop_x , lop_y) +// +// The x coordinate is either 0, sqrt(-1), lop_x, or their opposite. +// The y coordinate is either 0, -1 , lop_y, or their opposite. +// The pattern for both is the same, except for a rotation of 2 (modulo 8) +// +// This helper function captures the pattern, and we can use it thus: +// +// select_lop(x, lop_x, sqrtm1, cofactor); +// select_lop(y, lop_y, fe_one, cofactor + 2); +// +// This is faster than an actual scalar multiplication, +// and requires less code than naive constant time look up. +static void select_lop(fe out, const fe x, const fe k, u8 cofactor) +{ + fe tmp; + fe_0(out); + fe_ccopy(out, k , (cofactor >> 1) & 1); // bit 1 + fe_ccopy(out, x , (cofactor >> 0) & 1); // bit 0 + fe_neg (tmp, out); + fe_ccopy(out, tmp, (cofactor >> 2) & 1); // bit 2 + WIPE_BUFFER(tmp); +} + +// "Fast" dirty ephemeral key +// We use this one by default. +// +// This version works by performing a regular scalar multiplication, +// then add a low order point. The scalar multiplication is done in +// Edwards space for more speed (*2 compared to the "small" version). +// The cost is a bigger binary for programs that don't also sign messages. +void crypto_x25519_dirty_fast(u8 public_key[32], const u8 secret_key[32]) +{ + // Compute clean scalar multiplication + u8 scalar[32]; + ge pk; + crypto_eddsa_trim_scalar(scalar, secret_key); + ge_scalarmult_base(&pk, scalar); + + // Compute low order point + fe t1, t2; + select_lop(t1, lop_x, sqrtm1, secret_key[0]); + select_lop(t2, lop_y, fe_one, secret_key[0] + 2); + ge_precomp low_order_point; + fe_add(low_order_point.Yp, t2, t1); + fe_sub(low_order_point.Ym, t2, t1); + fe_mul(low_order_point.T2, t2, t1); + fe_mul(low_order_point.T2, low_order_point.T2, D2); + + // Add low order point to the public key + ge_madd(&pk, &pk, &low_order_point, t1, t2); + + // Convert to Montgomery u coordinate (we ignore the sign) + fe_add(t1, pk.Z, pk.Y); + fe_sub(t2, pk.Z, pk.Y); + fe_invert(t2, t2); + fe_mul(t1, t1, t2); + + fe_tobytes(public_key, t1); + + WIPE_BUFFER(t1); WIPE_CTX(&pk); + WIPE_BUFFER(t2); WIPE_CTX(&low_order_point); + WIPE_BUFFER(scalar); +} + +/////////////////// +/// Elligator 2 /// +/////////////////// +static const fe A = {486662}; + +// Elligator direct map +// +// Computes the point corresponding to a representative, encoded in 32 +// bytes (little Endian). Since positive representatives fits in 254 +// bits, The two most significant bits are ignored. +// +// From the paper: +// w = -A / (fe(1) + non_square * r^2) +// e = chi(w^3 + A*w^2 + w) +// u = e*w - (fe(1)-e)*(A//2) +// v = -e * sqrt(u^3 + A*u^2 + u) +// +// We ignore v because we don't need it for X25519 (the Montgomery +// ladder only uses u). +// +// Note that e is either 0, 1 or -1 +// if e = 0 u = 0 and v = 0 +// if e = 1 u = w +// if e = -1 u = -w - A = w * non_square * r^2 +// +// Let r1 = non_square * r^2 +// Let r2 = 1 + r1 +// Note that r2 cannot be zero, -1/non_square is not a square. +// We can (tediously) verify that: +// w^3 + A*w^2 + w = (A^2*r1 - r2^2) * A / r2^3 +// Therefore: +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) * 1 +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) * chi(r2^6) +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3) * r2^6) +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * A * r2^3) +// Corollary: +// e = 1 if (A^2*r1 - r2^2) * A * r2^3) is a non-zero square +// e = -1 if (A^2*r1 - r2^2) * A * r2^3) is not a square +// Note that w^3 + A*w^2 + w (and therefore e) can never be zero: +// w^3 + A*w^2 + w = w * (w^2 + A*w + 1) +// w^3 + A*w^2 + w = w * (w^2 + A*w + A^2/4 - A^2/4 + 1) +// w^3 + A*w^2 + w = w * (w + A/2)^2 - A^2/4 + 1) +// which is zero only if: +// w = 0 (impossible) +// (w + A/2)^2 = A^2/4 - 1 (impossible, because A^2/4-1 is not a square) +// +// Let isr = invsqrt((A^2*r1 - r2^2) * A * r2^3) +// isr = sqrt(1 / ((A^2*r1 - r2^2) * A * r2^3)) if e = 1 +// isr = sqrt(sqrt(-1) / ((A^2*r1 - r2^2) * A * r2^3)) if e = -1 +// +// if e = 1 +// let u1 = -A * (A^2*r1 - r2^2) * A * r2^2 * isr^2 +// u1 = w +// u1 = u +// +// if e = -1 +// let ufactor = -non_square * sqrt(-1) * r^2 +// let vfactor = sqrt(ufactor) +// let u2 = -A * (A^2*r1 - r2^2) * A * r2^2 * isr^2 * ufactor +// u2 = w * -1 * -non_square * r^2 +// u2 = w * non_square * r^2 +// u2 = u +void crypto_elligator_map(u8 curve[32], const u8 hidden[32]) +{ + fe r, u, t1, t2, t3; + fe_frombytes_mask(r, hidden, 2); // r is encoded in 254 bits. + fe_sq(r, r); + fe_add(t1, r, r); + fe_add(u, t1, fe_one); + fe_sq (t2, u); + fe_mul(t3, A2, t1); + fe_sub(t3, t3, t2); + fe_mul(t3, t3, A); + fe_mul(t1, t2, u); + fe_mul(t1, t3, t1); + int is_square = invsqrt(t1, t1); + fe_mul(u, r, ufactor); + fe_ccopy(u, fe_one, is_square); + fe_sq (t1, t1); + fe_mul(u, u, A); + fe_mul(u, u, t3); + fe_mul(u, u, t2); + fe_mul(u, u, t1); + fe_neg(u, u); + fe_tobytes(curve, u); + + WIPE_BUFFER(t1); WIPE_BUFFER(r); + WIPE_BUFFER(t2); WIPE_BUFFER(u); + WIPE_BUFFER(t3); +} + +// Elligator inverse map +// +// Computes the representative of a point, if possible. If not, it does +// nothing and returns -1. Note that the success of the operation +// depends only on the point (more precisely its u coordinate). The +// tweak parameter is used only upon success +// +// The tweak should be a random byte. Beyond that, its contents are an +// implementation detail. Currently, the tweak comprises: +// - Bit 1 : sign of the v coordinate (0 if positive, 1 if negative) +// - Bit 2-5: not used +// - Bits 6-7: random padding +// +// From the paper: +// Let sq = -non_square * u * (u+A) +// if sq is not a square, or u = -A, there is no mapping +// Assuming there is a mapping: +// if v is positive: r = sqrt(-u / (non_square * (u+A))) +// if v is negative: r = sqrt(-(u+A) / (non_square * u )) +// +// We compute isr = invsqrt(-non_square * u * (u+A)) +// if it wasn't a square, abort. +// else, isr = sqrt(-1 / (non_square * u * (u+A)) +// +// If v is positive, we return isr * u: +// isr * u = sqrt(-1 / (non_square * u * (u+A)) * u +// isr * u = sqrt(-u / (non_square * (u+A)) +// +// If v is negative, we return isr * (u+A): +// isr * (u+A) = sqrt(-1 / (non_square * u * (u+A)) * (u+A) +// isr * (u+A) = sqrt(-(u+A) / (non_square * u) +int crypto_elligator_rev(u8 hidden[32], const u8 public_key[32], u8 tweak) +{ + fe t1, t2, t3; + fe_frombytes(t1, public_key); // t1 = u + + fe_add(t2, t1, A); // t2 = u + A + fe_mul(t3, t1, t2); + fe_mul_small(t3, t3, -2); + int is_square = invsqrt(t3, t3); // t3 = sqrt(-1 / non_square * u * (u+A)) + if (is_square) { + // The only variable time bit. This ultimately reveals how many + // tries it took us to find a representable key. + // This does not affect security as long as we try keys at random. + + fe_ccopy (t1, t2, tweak & 1); // multiply by u if v is positive, + fe_mul (t3, t1, t3); // multiply by u+A otherwise + fe_mul_small(t1, t3, 2); + fe_neg (t2, t3); + fe_ccopy (t3, t2, fe_isodd(t1)); + fe_tobytes(hidden, t3); + + // Pad with two random bits + hidden[31] |= tweak & 0xc0; + } + + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); + WIPE_BUFFER(t3); + return is_square - 1; +} + +void crypto_elligator_key_pair(u8 hidden[32], u8 secret_key[32], u8 seed[32]) +{ + u8 pk [32]; // public key + u8 buf[64]; // seed + representative + COPY(buf + 32, seed, 32); + do { + crypto_chacha20_djb(buf, 0, 64, buf+32, zero, 0); + crypto_x25519_dirty_fast(pk, buf); // or the "small" version + } while(crypto_elligator_rev(buf+32, pk, buf[32])); + // Note that the return value of crypto_elligator_rev() is + // independent from its tweak parameter. + // Therefore, buf[32] is not actually reused. Either we loop one + // more time and buf[32] is used for the new seed, or we succeeded, + // and buf[32] becomes the tweak parameter. + + crypto_wipe(seed, 32); + COPY(hidden , buf + 32, 32); + COPY(secret_key, buf , 32); + WIPE_BUFFER(buf); + WIPE_BUFFER(pk); +} + +/////////////////////// +/// Scalar division /// +/////////////////////// + +// Montgomery reduction. +// Divides x by (2^256), and reduces the result modulo L +// +// Precondition: +// x < L * 2^256 +// Constants: +// r = 2^256 (makes division by r trivial) +// k = (r * (1/r) - 1) // L (1/r is computed modulo L ) +// Algorithm: +// s = (x * k) % r +// t = x + s*L (t is always a multiple of r) +// u = (t/r) % L (u is always below 2*L, conditional subtraction is enough) +static void redc(u32 u[8], u32 x[16]) +{ + static const u32 k[8] = { + 0x12547e1b, 0xd2b51da3, 0xfdba84ff, 0xb1a206f2, + 0xffa36bea, 0x14e75438, 0x6fe91836, 0x9db6c6f2, + }; + + // s = x * k (modulo 2^256) + // This is cheaper than the full multiplication. + u32 s[8] = {0}; + FOR (i, 0, 8) { + u64 carry = 0; + FOR (j, 0, 8-i) { + carry += s[i+j] + (u64)x[i] * k[j]; + s[i+j] = (u32)carry; + carry >>= 32; + } + } + u32 t[16] = {0}; + multiply(t, s, L); + + // t = t + x + u64 carry = 0; + FOR (i, 0, 16) { + carry += (u64)t[i] + x[i]; + t[i] = (u32)carry; + carry >>= 32; + } + + // u = (t / 2^256) % L + // Note that t / 2^256 is always below 2*L, + // So a constant time conditional subtraction is enough + remove_l(u, t+8); + + WIPE_BUFFER(s); + WIPE_BUFFER(t); +} + +void crypto_x25519_inverse(u8 blind_salt [32], const u8 private_key[32], + const u8 curve_point[32]) +{ + static const u8 Lm2[32] = { // L - 2 + 0xeb, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, + 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + }; + // 1 in Montgomery form + u32 m_inv [8] = { + 0x8d98951d, 0xd6ec3174, 0x737dcf70, 0xc6ef5bf4, + 0xfffffffe, 0xffffffff, 0xffffffff, 0x0fffffff, + }; + + u8 scalar[32]; + crypto_eddsa_trim_scalar(scalar, private_key); + + // Convert the scalar in Montgomery form + // m_scl = scalar * 2^256 (modulo L) + u32 m_scl[8]; + { + u32 tmp[16]; + ZERO(tmp, 8); + load32_le_buf(tmp+8, scalar, 8); + mod_l(scalar, tmp); + load32_le_buf(m_scl, scalar, 8); + WIPE_BUFFER(tmp); // Wipe ASAP to save stack space + } + + // Compute the inverse + u32 product[16]; + for (int i = 252; i >= 0; i--) { + ZERO(product, 16); + multiply(product, m_inv, m_inv); + redc(m_inv, product); + if (scalar_bit(Lm2, i)) { + ZERO(product, 16); + multiply(product, m_inv, m_scl); + redc(m_inv, product); + } + } + // Convert the inverse *out* of Montgomery form + // scalar = m_inv / 2^256 (modulo L) + COPY(product, m_inv, 8); + ZERO(product + 8, 8); + redc(m_inv, product); + store32_le_buf(scalar, m_inv, 8); // the *inverse* of the scalar + + // Clear the cofactor of scalar: + // cleared = scalar * (3*L + 1) (modulo 8*L) + // cleared = scalar + scalar * 3 * L (modulo 8*L) + // Note that (scalar * 3) is reduced modulo 8, so we only need the + // first byte. + add_xl(scalar, scalar[0] * 3); + + // Recall that 8*L < 2^256. However it is also very close to + // 2^255. If we spanned the ladder over 255 bits, random tests + // wouldn't catch the off-by-one error. + scalarmult(blind_salt, scalar, curve_point, 256); + + WIPE_BUFFER(scalar); WIPE_BUFFER(m_scl); + WIPE_BUFFER(product); WIPE_BUFFER(m_inv); +} + +//////////////////////////////// +/// Authenticated encryption /// +//////////////////////////////// +static void lock_auth(u8 mac[16], const u8 auth_key[32], + const u8 *ad , size_t ad_size, + const u8 *cipher_text, size_t text_size) +{ + u8 sizes[16]; // Not secret, not wiped + store64_le(sizes + 0, ad_size); + store64_le(sizes + 8, text_size); + crypto_poly1305_ctx poly_ctx; // auto wiped... + crypto_poly1305_init (&poly_ctx, auth_key); + crypto_poly1305_update(&poly_ctx, ad , ad_size); + crypto_poly1305_update(&poly_ctx, zero , gap(ad_size, 16)); + crypto_poly1305_update(&poly_ctx, cipher_text, text_size); + crypto_poly1305_update(&poly_ctx, zero , gap(text_size, 16)); + crypto_poly1305_update(&poly_ctx, sizes , 16); + crypto_poly1305_final (&poly_ctx, mac); // ...here +} + +void crypto_aead_init_x(crypto_aead_ctx *ctx, + u8 const key[32], const u8 nonce[24]) +{ + crypto_chacha20_h(ctx->key, key, nonce); + COPY(ctx->nonce, nonce + 16, 8); + ctx->counter = 0; +} + +void crypto_aead_init_djb(crypto_aead_ctx *ctx, + const u8 key[32], const u8 nonce[8]) +{ + COPY(ctx->key , key , 32); + COPY(ctx->nonce, nonce, 8); + ctx->counter = 0; +} + +void crypto_aead_init_ietf(crypto_aead_ctx *ctx, + const u8 key[32], const u8 nonce[12]) +{ + COPY(ctx->key , key , 32); + COPY(ctx->nonce, nonce + 4, 8); + ctx->counter = (u64)load32_le(nonce) << 32; +} + +void crypto_aead_write(crypto_aead_ctx *ctx, u8 *cipher_text, u8 mac[16], + const u8 *ad, size_t ad_size, + const u8 *plain_text, size_t text_size) +{ + u8 auth_key[64]; // the last 32 bytes are used for rekeying. + crypto_chacha20_djb(auth_key, 0, 64, ctx->key, ctx->nonce, ctx->counter); + crypto_chacha20_djb(cipher_text, plain_text, text_size, + ctx->key, ctx->nonce, ctx->counter + 1); + lock_auth(mac, auth_key, ad, ad_size, cipher_text, text_size); + COPY(ctx->key, auth_key + 32, 32); + WIPE_BUFFER(auth_key); +} + +int crypto_aead_read(crypto_aead_ctx *ctx, u8 *plain_text, const u8 mac[16], + const u8 *ad, size_t ad_size, + const u8 *cipher_text, size_t text_size) +{ + u8 auth_key[64]; // the last 32 bytes are used for rekeying. + u8 real_mac[16]; + crypto_chacha20_djb(auth_key, 0, 64, ctx->key, ctx->nonce, ctx->counter); + lock_auth(real_mac, auth_key, ad, ad_size, cipher_text, text_size); + int mismatch = crypto_verify16(mac, real_mac); + if (!mismatch) { + crypto_chacha20_djb(plain_text, cipher_text, text_size, + ctx->key, ctx->nonce, ctx->counter + 1); + COPY(ctx->key, auth_key + 32, 32); + } + WIPE_BUFFER(auth_key); + WIPE_BUFFER(real_mac); + return mismatch; +} + +void crypto_aead_lock(u8 *cipher_text, u8 mac[16], const u8 key[32], + const u8 nonce[24], const u8 *ad, size_t ad_size, + const u8 *plain_text, size_t text_size) +{ + crypto_aead_ctx ctx; + crypto_aead_init_x(&ctx, key, nonce); + crypto_aead_write(&ctx, cipher_text, mac, ad, ad_size, + plain_text, text_size); + crypto_wipe(&ctx, sizeof(ctx)); +} + +int crypto_aead_unlock(u8 *plain_text, const u8 mac[16], const u8 key[32], + const u8 nonce[24], const u8 *ad, size_t ad_size, + const u8 *cipher_text, size_t text_size) +{ + crypto_aead_ctx ctx; + crypto_aead_init_x(&ctx, key, nonce); + int mismatch = crypto_aead_read(&ctx, plain_text, mac, ad, ad_size, + cipher_text, text_size); + crypto_wipe(&ctx, sizeof(ctx)); + return mismatch; +} + +#ifdef MONOCYPHER_CPP_NAMESPACE +} +#endif diff --git a/modemmeshcore/monocypher.h b/modemmeshcore/monocypher.h new file mode 100644 index 000000000..cf635e88e --- /dev/null +++ b/modemmeshcore/monocypher.h @@ -0,0 +1,321 @@ +// Monocypher version __git__ +// +// This file is dual-licensed. Choose whichever licence you want from +// the two licences listed below. +// +// The first licence is a regular 2-clause BSD licence. The second licence +// is the CC-0 from Creative Commons. It is intended to release Monocypher +// to the public domain. The BSD licence serves as a fallback option. +// +// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0 +// +// ------------------------------------------------------------------------ +// +// Copyright (c) 2017-2019, Loup Vaillant +// All rights reserved. +// +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ------------------------------------------------------------------------ +// +// Written in 2017-2019 by Loup Vaillant +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// + +#ifndef MONOCYPHER_H +#define MONOCYPHER_H + +#include +#include + +#ifdef MONOCYPHER_CPP_NAMESPACE +namespace MONOCYPHER_CPP_NAMESPACE { +#elif defined(__cplusplus) +extern "C" { +#endif + +// Constant time comparisons +// ------------------------- + +// Return 0 if a and b are equal, -1 otherwise +int crypto_verify16(const uint8_t a[16], const uint8_t b[16]); +int crypto_verify32(const uint8_t a[32], const uint8_t b[32]); +int crypto_verify64(const uint8_t a[64], const uint8_t b[64]); + + +// Erase sensitive data +// -------------------- +void crypto_wipe(void *secret, size_t size); + + +// Authenticated encryption +// ------------------------ +void crypto_aead_lock(uint8_t *cipher_text, + uint8_t mac [16], + const uint8_t key [32], + const uint8_t nonce[24], + const uint8_t *ad, size_t ad_size, + const uint8_t *plain_text, size_t text_size); +int crypto_aead_unlock(uint8_t *plain_text, + const uint8_t mac [16], + const uint8_t key [32], + const uint8_t nonce[24], + const uint8_t *ad, size_t ad_size, + const uint8_t *cipher_text, size_t text_size); + +// Authenticated stream +// -------------------- +typedef struct { + uint64_t counter; + uint8_t key[32]; + uint8_t nonce[8]; +} crypto_aead_ctx; + +void crypto_aead_init_x(crypto_aead_ctx *ctx, + const uint8_t key[32], const uint8_t nonce[24]); +void crypto_aead_init_djb(crypto_aead_ctx *ctx, + const uint8_t key[32], const uint8_t nonce[8]); +void crypto_aead_init_ietf(crypto_aead_ctx *ctx, + const uint8_t key[32], const uint8_t nonce[12]); + +void crypto_aead_write(crypto_aead_ctx *ctx, + uint8_t *cipher_text, + uint8_t mac[16], + const uint8_t *ad , size_t ad_size, + const uint8_t *plain_text, size_t text_size); +int crypto_aead_read(crypto_aead_ctx *ctx, + uint8_t *plain_text, + const uint8_t mac[16], + const uint8_t *ad , size_t ad_size, + const uint8_t *cipher_text, size_t text_size); + + +// General purpose hash (BLAKE2b) +// ------------------------------ + +// Direct interface +void crypto_blake2b(uint8_t *hash, size_t hash_size, + const uint8_t *message, size_t message_size); + +void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size, + const uint8_t *key, size_t key_size, + const uint8_t *message, size_t message_size); + +// Incremental interface +typedef struct { + // Do not rely on the size or contents of this type, + // for they may change without notice. + uint64_t hash[8]; + uint64_t input_offset[2]; + uint64_t input[16]; + size_t input_idx; + size_t hash_size; +} crypto_blake2b_ctx; + +void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size); +void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size, + const uint8_t *key, size_t key_size); +void crypto_blake2b_update(crypto_blake2b_ctx *ctx, + const uint8_t *message, size_t message_size); +void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash); + + +// Password key derivation (Argon2) +// -------------------------------- +#define CRYPTO_ARGON2_D 0 +#define CRYPTO_ARGON2_I 1 +#define CRYPTO_ARGON2_ID 2 + +typedef struct { + uint32_t algorithm; // Argon2d, Argon2i, Argon2id + uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes + uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i) + uint32_t nb_lanes; // parallelism level (single threaded anyway) +} crypto_argon2_config; + +typedef struct { + const uint8_t *pass; + const uint8_t *salt; + uint32_t pass_size; + uint32_t salt_size; // 16 bytes recommended +} crypto_argon2_inputs; + +typedef struct { + const uint8_t *key; // may be NULL if no key + const uint8_t *ad; // may be NULL if no additional data + uint32_t key_size; // 0 if no key (32 bytes recommended otherwise) + uint32_t ad_size; // 0 if no additional data +} crypto_argon2_extras; + +extern const crypto_argon2_extras crypto_argon2_no_extras; + +void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area, + crypto_argon2_config config, + crypto_argon2_inputs inputs, + crypto_argon2_extras extras); + + +// Key exchange (X-25519) +// ---------------------- + +// Shared secrets are not quite random. +// Hash them to derive an actual shared key. +void crypto_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]); +void crypto_x25519(uint8_t raw_shared_secret[32], + const uint8_t your_secret_key [32], + const uint8_t their_public_key [32]); + +// Conversion to EdDSA +void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]); + +// scalar "division" +// Used for OPRF. Be aware that exponential blinding is less secure +// than Diffie-Hellman key exchange. +void crypto_x25519_inverse(uint8_t blind_salt [32], + const uint8_t private_key[32], + const uint8_t curve_point[32]); + +// "Dirty" versions of x25519_public_key(). +// Use with crypto_elligator_rev(). +// Leaks 3 bits of the private key. +void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]); +void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]); + + +// Signatures +// ---------- + +// EdDSA with curve25519 + BLAKE2b +void crypto_eddsa_key_pair(uint8_t secret_key[64], + uint8_t public_key[32], + uint8_t seed[32]); +void crypto_eddsa_sign(uint8_t signature [64], + const uint8_t secret_key[64], + const uint8_t *message, size_t message_size); +int crypto_eddsa_check(const uint8_t signature [64], + const uint8_t public_key[32], + const uint8_t *message, size_t message_size); + +// Conversion to X25519 +void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]); + +// EdDSA building blocks +void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]); +void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]); +void crypto_eddsa_mul_add(uint8_t r[32], + const uint8_t a[32], + const uint8_t b[32], + const uint8_t c[32]); +void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]); +int crypto_eddsa_check_equation(const uint8_t signature[64], + const uint8_t public_key[32], + const uint8_t h_ram[32]); + + +// Chacha20 +// -------- + +// Specialised hash. +// Used to hash X25519 shared secrets. +void crypto_chacha20_h(uint8_t out[32], + const uint8_t key[32], + const uint8_t in [16]); + +// Unauthenticated stream cipher. +// Don't forget to add authentication. +uint64_t crypto_chacha20_djb(uint8_t *cipher_text, + const uint8_t *plain_text, + size_t text_size, + const uint8_t key[32], + const uint8_t nonce[8], + uint64_t ctr); +uint32_t crypto_chacha20_ietf(uint8_t *cipher_text, + const uint8_t *plain_text, + size_t text_size, + const uint8_t key[32], + const uint8_t nonce[12], + uint32_t ctr); +uint64_t crypto_chacha20_x(uint8_t *cipher_text, + const uint8_t *plain_text, + size_t text_size, + const uint8_t key[32], + const uint8_t nonce[24], + uint64_t ctr); + + +// Poly 1305 +// --------- + +// This is a *one time* authenticator. +// Disclosing the mac reveals the key. +// See crypto_lock() on how to use it properly. + +// Direct interface +void crypto_poly1305(uint8_t mac[16], + const uint8_t *message, size_t message_size, + const uint8_t key[32]); + +// Incremental interface +typedef struct { + // Do not rely on the size or contents of this type, + // for they may change without notice. + uint8_t c[16]; // chunk of the message + size_t c_idx; // How many bytes are there in the chunk. + uint32_t r [4]; // constant multiplier (from the secret key) + uint32_t pad[4]; // random number added at the end (from the secret key) + uint32_t h [5]; // accumulated hash +} crypto_poly1305_ctx; + +void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]); +void crypto_poly1305_update(crypto_poly1305_ctx *ctx, + const uint8_t *message, size_t message_size); +void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]); + + +// Elligator 2 +// ----------- + +// Elligator mappings proper +void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]); +int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32], + uint8_t tweak); + +// Easy to use key pair generation +void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32], + uint8_t seed[32]); + +#ifdef __cplusplus +} +#endif + +#endif // MONOCYPHER_H diff --git a/modemmeshcore/test/cli.cpp b/modemmeshcore/test/cli.cpp new file mode 100644 index 000000000..7bc2540a9 --- /dev/null +++ b/modemmeshcore/test/cli.cpp @@ -0,0 +1,60 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// modmeshcore_cli — print the wire packet for a given MESHCORE: command line. // +// // +// Usage: // +// modmeshcore_cli "MESHCORE: type=advert; seed=; name=Test; ts=42" // +// // +// Prints the OTA-ready packet as hex on stdout, plus a one-line summary on // +// stderr. Used for golden-vector parity tests against the python // +// gr4-lora/scripts/src/lora/tools/meshcore_tx.py implementation. // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcorepacket.h" + +#include +#include + +#include +#include + +int main(int argc, char** argv) +{ + if (argc != 2) { + std::fprintf(stderr, + "usage: %s \"MESHCORE: type=...; ...\"\n" + "examples:\n" + " %s \"MESHCORE: type=advert; seed=0102030405060708090a0b0c0d0e0f10" + "111213141516171819""1a1b1c1d1e1f20; name=Test; ts=1700000000\"\n" + " %s \"MESHCORE: type=grp_txt; channel=public; text=Hello group; " + "ts=1700000000\"\n", + argv[0], argv[0], argv[0]); + return 2; + } + + const QString command = QString::fromUtf8(argv[1]); + + if (!modemmeshcore::Packet::isCommand(command)) { + std::fprintf(stderr, "error: argument is not a MESHCORE: command\n"); + return 2; + } + + QByteArray frame; + QString summary, error; + if (!modemmeshcore::Packet::buildFrameFromCommand(command, frame, summary, error)) { + std::fprintf(stderr, "error: %s\n", error.toUtf8().constData()); + return 1; + } + + std::fprintf(stderr, "%s\n", summary.toUtf8().constData()); + std::fprintf(stderr, "size: %d bytes\n", frame.size()); + std::fwrite(frame.toHex().constData(), 1, frame.toHex().size(), stdout); + std::fputc('\n', stdout); + return 0; +} diff --git a/modemmeshcore/test/smoke.cpp b/modemmeshcore/test/smoke.cpp new file mode 100644 index 000000000..563d528e9 --- /dev/null +++ b/modemmeshcore/test/smoke.cpp @@ -0,0 +1,564 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Tom Hensel // +// // +// 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. // +// // +// modemmeshcore smoke test. Exercises the crypto + ADVERT builder round-trip. // +// No QtTest infra yet — this is a standalone executable that aborts on first // +// inconsistency. Run from build dir: ./modemmeshcore/modemmeshcore_smoke // +/////////////////////////////////////////////////////////////////////////////////// + +#include "meshcorepacket.h" +#include "meshcore_crypto.h" +#include "meshcore_builders.h" +#include "meshcore_command.h" +#include "meshcore_decoder.h" + +#include +#include + +#include +#include + +namespace +{ + +#define REQUIRE(cond, msg) do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL: %s (line %d): %s\n", msg, __LINE__, #cond); \ + std::abort(); \ + } \ + std::printf(" ok: %s\n", msg); \ + } while (0) + +QByteArray fromHex(const char* hex) { return QByteArray::fromHex(QByteArray(hex)); } + +void testCryptoRoundtrip() +{ + using namespace modemmeshcore; + + // Deterministic 32-byte seed (test vector). + const QByteArray seed = fromHex("0102030405060708090a0b0c0d0e0f10111213141516171819" + "1a1b1c1d1e1f20"); + REQUIRE(seed.size() == kPubKeySize, "seed is 32 bytes"); + + const QByteArray pub32 = detail::derivePubKey(seed); + REQUIRE(pub32.size() == kPubKeySize, "pub32 derived from seed is 32 bytes"); + + // expandedKey(seed) is MeshCore's 64-byte form (clamp(SHA512(seed)[0..32]) || ..). + const QByteArray expanded = detail::expandedKey(seed); + REQUIRE(expanded.size() == kPrvKeySize, "expandedKey is 64 bytes"); + + // ECDH to self: computing shared secret with our own pub key should succeed + // and produce 32 bytes. (Round-trip validation: same secret on both sides.) + const QByteArray sharedSelf = detail::sharedSecret(seed, pub32); + REQUIRE(sharedSelf.size() == kPubKeySize, "ECDH self-shared secret is 32B"); + + // Encrypt-then-MAC round-trip with the self-shared secret. + const QByteArray plaintext = QByteArray("Hello MeshCore!"); + const QByteArray encrypted = detail::encryptThenMac(sharedSelf, plaintext); + REQUIRE(!encrypted.isEmpty(), "encryptThenMac produced output"); + REQUIRE(encrypted.size() >= kCipherMacSize + kCipherBlockSize, + "encryptThenMac output >= 18 bytes"); + + const QByteArray decrypted = detail::macThenDecrypt(sharedSelf, encrypted); + REQUIRE(!decrypted.isEmpty(), "macThenDecrypt validated MAC"); + // decrypted is zero-padded; the prefix must equal the plaintext + REQUIRE(decrypted.startsWith(plaintext), "decrypted starts with plaintext"); + + // MAC tamper: flip one byte in the ciphertext, decrypt should fail + QByteArray tampered = encrypted; + tampered[5] = static_cast(static_cast(tampered[5]) ^ 0x01); + const QByteArray badDecrypt = detail::macThenDecrypt(sharedSelf, tampered); + REQUIRE(badDecrypt.isEmpty(), "tampered MAC rejected"); + + // Ed25519 sign / verify + const QByteArray msg = QByteArray("MeshCore signing test message"); + const QByteArray sig = detail::signEd25519(seed, msg); + REQUIRE(sig.size() == kSignatureSize, "signature is 64 bytes"); + REQUIRE(detail::verifyEd25519(sig, pub32, msg), "signature verifies under pub32"); + + // Tampered message must fail verification + QByteArray badMsg = msg; + badMsg[0] = static_cast(static_cast(badMsg[0]) ^ 0x01); + REQUIRE(!detail::verifyEd25519(sig, pub32, badMsg), "tampered msg fails verify"); +} + +void testAdvertBuilder() +{ + using namespace modemmeshcore; + + const QByteArray seed = fromHex("0102030405060708090a0b0c0d0e0f10111213141516171819" + "1a1b1c1d1e1f20"); + const QByteArray pub32 = detail::derivePubKey(seed); + + builders::AdvertOptions opts; + opts.nodeType = AdvertNodeChat; + opts.name = QStringLiteral("SDRangelTest"); + opts.timestamp = 1700000000U; + opts.routeType = RouteFlood; + + const QByteArray packet = builders::buildAdvert(seed, pub32, opts); + REQUIRE(!packet.isEmpty(), "ADVERT packet built"); + + // Minimum size: header(1) + path_len(1) + body(pub32+ts4+sig64+flags1+name) + const int expectedMin = 1 + 1 + kPubKeySize + 4 + kSignatureSize + 1 + + opts.name.toUtf8().size(); + REQUIRE(packet.size() == expectedMin, "ADVERT exact size matches spec"); + + // Header byte: route=Flood(1), payload=Advert(4), version=0 + const uint8_t header = static_cast(packet[0]); + REQUIRE((header & kRouteMask) == RouteFlood, "header route = Flood"); + REQUIRE(((header >> kPayloadTypeShift) & kPayloadTypeMask) == PayloadAdvert, + "header payload = Advert"); + + // path_len byte: empty path -> 0 + REQUIRE(static_cast(packet[1]) == 0, "path_len = 0 (no path)"); + + // pubkey at offset 2 must equal pub32 + REQUIRE(packet.mid(2, kPubKeySize) == pub32, "pubkey at offset 2 matches"); +} + +void testPathLenEncoding() +{ + using namespace modemmeshcore; + + // 1-byte hash mode, 0 hashes -> 0x00 + REQUIRE(encodePathLen(0, 1) == 0x00, "encode(0,1) == 0x00"); + // 1-byte hash mode, 5 hashes -> 0x05 + REQUIRE(encodePathLen(5, 1) == 0x05, "encode(5,1) == 0x05"); + // 2-byte hash mode, 3 hashes -> 0x40 | 3 = 0x43 + REQUIRE(encodePathLen(3, 2) == 0x43, "encode(3,2) == 0x43"); + // 3-byte hash mode, 1 hash -> 0x80 | 1 = 0x81 + REQUIRE(encodePathLen(1, 3) == 0x81, "encode(1,3) == 0x81"); + + // Round-trip + const PathLen p1 = decodePathLen(0x05); + REQUIRE(p1.hashCount == 5 && p1.hashSize == 1 && p1.totalBytes == 5, + "decode(0x05) = {5,1,5}"); + const PathLen p2 = decodePathLen(0x43); + REQUIRE(p2.hashCount == 3 && p2.hashSize == 2 && p2.totalBytes == 6, + "decode(0x43) = {3,2,6}"); + const PathLen p3 = decodePathLen(0x81); + REQUIRE(p3.hashCount == 1 && p3.hashSize == 3 && p3.totalBytes == 3, + "decode(0x81) = {1,3,3}"); +} + +} // namespace + +void testEncryptedBuilders() +{ + using namespace modemmeshcore; + namespace b = modemmeshcore::builders; + + const QByteArray seedA = fromHex("0102030405060708090a0b0c0d0e0f10111213141516171819" + "1a1b1c1d1e1f20"); + const QByteArray seedB = fromHex("a1a2a3a4a5a6a7a8a9aaabacadaeafb0" + "b1b2b3b4b5b6b7b8b9babbbcbdbebfc0"); + const QByteArray pubA = detail::derivePubKey(seedA); + const QByteArray pubB = detail::derivePubKey(seedB); + REQUIRE(pubA.size() == kPubKeySize, "pubA derived"); + REQUIRE(pubB.size() == kPubKeySize, "pubB derived"); + + // ECDH symmetry: shared(A->B) == shared(B->A) + const QByteArray sharedAB = detail::sharedSecret(seedA, pubB); + const QByteArray sharedBA = detail::sharedSecret(seedB, pubA); + REQUIRE(!sharedAB.isEmpty() && sharedAB == sharedBA, "ECDH symmetric"); + + // ---- TXT_MSG: A -> B ---- + { + b::TxtMsgOptions opts; + opts.timestamp = 1700000001U; + opts.routeType = RouteDirect; + const QByteArray text = QByteArray("Hello from A"); + const QByteArray pkt = b::buildTxtMsg(seedA, pubB, text, opts); + REQUIRE(!pkt.isEmpty(), "TXT_MSG built"); + + // Header byte: route=Direct, payload=Txt + REQUIRE((static_cast(pkt[0]) & kRouteMask) == RouteDirect, + "TXT_MSG route=Direct"); + REQUIRE(((static_cast(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask) + == PayloadTxt, "TXT_MSG payload=Txt"); + REQUIRE(static_cast(pkt[1]) == 0, "TXT_MSG path_len=0"); + + // dest_hash and src_hash at offsets 2 and 3 + REQUIRE(static_cast(pkt[2]) == static_cast(pubB[0]), + "TXT_MSG dest_hash = pubB[0]"); + REQUIRE(static_cast(pkt[3]) == static_cast(pubA[0]), + "TXT_MSG src_hash = pubA[0]"); + + // RX-side: B decrypts using shared secret. + const QByteArray macThenCt = pkt.mid(4); + const QByteArray plaintext = detail::macThenDecrypt(sharedBA, macThenCt); + REQUIRE(!plaintext.isEmpty(), "TXT_MSG decrypts under B's secret"); + // plaintext layout: ts(4) || flags(1) || text || pad + REQUIRE(plaintext.size() >= 5 + text.size(), "TXT_MSG plaintext length OK"); + REQUIRE(plaintext.mid(5, text.size()) == text, + "TXT_MSG plaintext text matches"); + } + + // ---- ANON_REQ: A -> B ---- + { + b::AnonReqOptions opts; + opts.timestamp = 1700000002U; + const QByteArray data = QByteArray("anon-req payload"); + const QByteArray pkt = b::buildAnonReq(seedA, pubB, data, opts); + REQUIRE(!pkt.isEmpty(), "ANON_REQ built"); + + REQUIRE(((static_cast(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask) + == PayloadAnonReq, "ANON_REQ payload=AnonReq"); + + // dest_hash at 2, sender_pub at 3..3+32 + REQUIRE(static_cast(pkt[2]) == static_cast(pubB[0]), + "ANON_REQ dest_hash = pubB[0]"); + REQUIRE(pkt.mid(3, kPubKeySize) == pubA, "ANON_REQ sender_pub at offset 3"); + + // RX-side decrypt + const QByteArray macThenCt = pkt.mid(3 + kPubKeySize); + const QByteArray plaintext = detail::macThenDecrypt(sharedBA, macThenCt); + REQUIRE(!plaintext.isEmpty(), "ANON_REQ decrypts"); + REQUIRE(plaintext.mid(4, data.size()) == data, "ANON_REQ data matches"); + } + + // ---- GRP_TXT: public channel ---- + { + const QByteArray psk = b::publicChannelPsk(); + REQUIRE(psk.size() == 16, "public PSK is 16 bytes"); + const b::GroupChannel pub = b::GroupChannel::fromPsk("public", psk); + REQUIRE(pub.isValid(), "public channel valid"); + REQUIRE(pub.hash.size() == 1, "channel hash is 1 byte"); + REQUIRE(pub.secret.size() == kPubKeySize, "channel secret zero-padded to 32"); + + b::GrpTxtOptions opts; + opts.timestamp = 1700000003U; + const QByteArray text = QByteArray("Test: public message"); + const QByteArray pkt = b::buildGrpTxt(pub, text, opts); + REQUIRE(!pkt.isEmpty(), "GRP_TXT built"); + + REQUIRE(((static_cast(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask) + == PayloadGrpTxt, "GRP_TXT payload=GrpTxt"); + REQUIRE(static_cast(pkt[2]) == static_cast(pub.hash[0]), + "GRP_TXT channel_hash matches"); + + // Decrypt with channel secret + const QByteArray macThenCt = pkt.mid(3); + const QByteArray plaintext = detail::macThenDecrypt(pub.secret, macThenCt); + REQUIRE(!plaintext.isEmpty(), "GRP_TXT decrypts under channel secret"); + REQUIRE(plaintext.mid(5, text.size()) == text, "GRP_TXT text matches"); + } + + // ---- ACK ---- + { + const QByteArray msgHash = fromHex("deadbeef"); + b::AckOptions opts; + const QByteArray pkt = b::buildAck(pubA, msgHash, opts); + REQUIRE(!pkt.isEmpty(), "ACK built"); + REQUIRE(((static_cast(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask) + == PayloadAck, "ACK payload=Ack"); + REQUIRE(pkt.size() == 1 + 1 + 1 + 4, "ACK size = header + path_len + dest + msg_hash"); + REQUIRE(static_cast(pkt[2]) == static_cast(pubA[0]), + "ACK dest_hash = pubA[0]"); + REQUIRE(pkt.mid(3, 4) == msgHash, "ACK msg_hash matches"); + } +} + +void testCommandParser() +{ + using namespace modemmeshcore; + + // ---- isCommand ---- + REQUIRE(Packet::isCommand("MESHCORE: type=advert"), "isCommand uppercase prefix"); + REQUIRE(Packet::isCommand("meshcore:type=ack"), "isCommand lowercase no space"); + REQUIRE(!Packet::isCommand("Hello world"), "isCommand rejects plain text"); + + // ---- tokenize ---- + { + QMap kv; + QString err; + REQUIRE(command::tokenize("a=1; b=hello world; c=0xff", kv, err), + "tokenize ok"); + REQUIRE(kv.value("a") == "1", "tokenize a=1"); + REQUIRE(kv.value("b") == "hello world", "tokenize whitespace value"); + REQUIRE(kv.value("c") == "0xff", "tokenize hex value"); + REQUIRE(kv.size() == 3, "tokenize 3 entries"); + } + { + QMap kv; + QString err; + REQUIRE(!command::tokenize("a=1; bad-token", kv, err), + "tokenize rejects malformed"); + REQUIRE(!err.isEmpty(), "tokenize sets error"); + } + + // ---- ADVERT via command ---- + const QByteArray seedHex = "0102030405060708090a0b0c0d0e0f10" + "111213141516171819""1a1b1c1d1e1f20"; + { + QString cmd = QStringLiteral("MESHCORE: type=advert; seed=") + + QString::fromLatin1(seedHex) + + QStringLiteral("; name=NodeA; ts=1700000100"); + QByteArray frame; + QString summary, err; + REQUIRE(Packet::buildFrameFromCommand(cmd, frame, summary, err), + "advert command builds"); + REQUIRE(!frame.isEmpty(), "advert frame non-empty"); + // Header: route=Flood, payload=Advert + REQUIRE((static_cast(frame[0]) & kRouteMask) == RouteFlood, + "advert command default route=Flood"); + REQUIRE(((static_cast(frame[0]) >> kPayloadTypeShift) & kPayloadTypeMask) + == PayloadAdvert, + "advert command payload=Advert"); + REQUIRE(summary.contains("advert"), "advert summary mentions type"); + } + + // ---- TXT_MSG via command ---- + { + // Generate a destination identity + const QByteArray destSeed = QByteArray::fromHex( + "a1a2a3a4a5a6a7a8a9aaabacadaeafb0" + "b1b2b3b4b5b6b7b8b9babbbcbdbebfc0"); + const QByteArray destPub = detail::derivePubKey(destSeed); + + QString cmd = QStringLiteral("MESHCORE: type=txt_msg; seed=") + + QString::fromLatin1(seedHex) + + QStringLiteral("; dest=") + + QString::fromLatin1(destPub.toHex()) + + QStringLiteral("; text=Hello via parser; ts=1700000200"); + QByteArray frame; + QString summary, err; + REQUIRE(Packet::buildFrameFromCommand(cmd, frame, summary, err), + "txt_msg command builds"); + REQUIRE(((static_cast(frame[0]) >> kPayloadTypeShift) & kPayloadTypeMask) + == PayloadTxt, "txt_msg payload=Txt"); + } + + // ---- GRP_TXT via channel=public ---- + { + QString cmd = QStringLiteral("MESHCORE: type=grp_txt; channel=public; " + "text=Hello group; ts=1700000300"); + QByteArray frame; + QString summary, err; + REQUIRE(Packet::buildFrameFromCommand(cmd, frame, summary, err), + "grp_txt public builds"); + REQUIRE(summary.contains("grp_txt"), "grp_txt summary"); + } + + // ---- ACK ---- + { + const QByteArray destSeed = QByteArray::fromHex( + "a1a2a3a4a5a6a7a8a9aaabacadaeafb0" + "b1b2b3b4b5b6b7b8b9babbbcbdbebfc0"); + const QByteArray destPub = detail::derivePubKey(destSeed); + QString cmd = QStringLiteral("MESHCORE: type=ack; dest=") + + QString::fromLatin1(destPub.toHex()) + + QStringLiteral("; msg_hash=deadbeef"); + QByteArray frame; + QString summary, err; + REQUIRE(Packet::buildFrameFromCommand(cmd, frame, summary, err), + "ack command builds"); + REQUIRE(frame.size() == 1 + 1 + 1 + 4, "ack exact size"); + } + + // ---- error cases ---- + { + QByteArray frame; + QString summary, err; + REQUIRE(!Packet::buildFrameFromCommand("MESHCORE: name=foo", frame, summary, err), + "missing type rejected"); + REQUIRE(err.contains("type"), "error mentions type"); + } + { + QByteArray frame; + QString summary, err; + REQUIRE(!Packet::buildFrameFromCommand("MESHCORE: type=advert; seed=00", + frame, summary, err), + "short seed rejected"); + REQUIRE(err.contains("seed"), "error mentions seed"); + } + + // ---- deriveTxRadioSettings ---- + { + TxRadioSettings settings; + QString err; + REQUIRE(Packet::deriveTxRadioSettings( + "MESHCORE: type=advert; sf=8; bw=62500; cr=8; sync=0x12; freq=869.618M", + settings, err), + "deriveTxRadioSettings parses"); + REQUIRE(settings.spreadFactor == 8, "sf=8 applied"); + REQUIRE(settings.bandwidthHz == 62500, "bw=62500 applied"); + REQUIRE(settings.parityBits == 4, "cr=8 -> parity=4"); + REQUIRE(settings.syncWord == 0x12, "sync=0x12 applied"); + REQUIRE(settings.centerFrequencyHz == 869618000, "freq=869.618M applied"); + REQUIRE(settings.hasCommand, "hasCommand set"); + REQUIRE(settings.hasLoRaParams, "hasLoRaParams set"); + REQUIRE(settings.hasCenterFrequency, "hasCenterFrequency set"); + } + { + TxRadioSettings settings; + QString err; + REQUIRE(Packet::deriveTxRadioSettings("Hello world", settings, err), + "non-command -> defaults OK"); + REQUIRE(settings.bandwidthHz == kDefaultBandwidthHz, "default BW"); + REQUIRE(settings.spreadFactor == kDefaultSpreadFactor, "default SF"); + REQUIRE(settings.parityBits == kDefaultParityBits, "default parity"); + } +} + +QString findField(const modemmeshcore::DecodeResult& r, const QString& path) +{ + for (const auto& f : r.fields) { + if (f.path == path) return f.value; + } + return QString(); +} + +void testRoundtripDecode() +{ + using namespace modemmeshcore; + namespace b = modemmeshcore::builders; + + const QByteArray seedA = fromHex("0102030405060708090a0b0c0d0e0f10" + "111213141516171819""1a1b1c1d1e1f20"); + const QByteArray seedB = fromHex("a1a2a3a4a5a6a7a8a9aaabacadaeafb0" + "b1b2b3b4b5b6b7b8b9babbbcbdbebfc0"); + const QByteArray pubA = detail::derivePubKey(seedA); + const QByteArray pubB = detail::derivePubKey(seedB); + + // ---- ADVERT round-trip ---- + { + b::AdvertOptions o; + o.timestamp = 1700001000U; + o.name = "NodeA"; + o.routeType = RouteFlood; + const QByteArray pkt = b::buildAdvert(seedA, pubA, o); + REQUIRE(!pkt.isEmpty(), "advert built"); + + DecodeResult r; + REQUIRE(Packet::decodeFrame(pkt, r), "advert decoded"); + REQUIRE(r.isFrame, "advert isFrame"); + REQUIRE(r.payloadType == PayloadAdvert, "advert payloadType=Advert"); + REQUIRE(r.dataDecoded, "advert dataDecoded"); + REQUIRE(findField(r, "advert.pubkey") == QString::fromLatin1(pubA.toHex()), + "advert.pubkey roundtrip"); + REQUIRE(findField(r, "advert.timestamp") == "1700001000", + "advert.timestamp roundtrip"); + REQUIRE(findField(r, "advert.name") == "NodeA", "advert.name roundtrip"); + REQUIRE(findField(r, "advert.signature_valid") == "true", + "advert signature verifies"); + } + + // ---- TXT_MSG round-trip A -> B (B's identity + A as contact) ---- + { + b::TxtMsgOptions o; + o.timestamp = 1700002000U; + o.routeType = RouteDirect; + const QByteArray text = QByteArray("Hello B from A"); + const QByteArray pkt = b::buildTxtMsg(seedA, pubB, text, o); + REQUIRE(!pkt.isEmpty(), "txt_msg built A->B"); + + const QString keys = QString("identity=") + QString::fromLatin1(seedB.toHex()) + + "; contact:alice=" + QString::fromLatin1(pubA.toHex()); + DecodeResult r; + REQUIRE(Packet::decodeFrame(pkt, r, keys), "txt_msg decoded"); + REQUIRE(r.payloadType == PayloadTxt, "txt_msg payloadType=Txt"); + REQUIRE(r.decrypted, "txt_msg decrypted"); + REQUIRE(findField(r, "txt.text") == text, "txt.text roundtrip"); + REQUIRE(findField(r, "txt.sender_name") == "alice", "txt.sender_name"); + REQUIRE(r.keyLabel == "contact:alice", "txt keyLabel"); + } + + // Wrong-key path: B has a key list with no matching contact + { + const QByteArray text = QByteArray("Hello"); + b::TxtMsgOptions o; + o.timestamp = 1700002001U; + const QByteArray pkt = b::buildTxtMsg(seedA, pubB, text, o); + + const QString keys = QString("identity=") + QString::fromLatin1(seedB.toHex()); + DecodeResult r; + REQUIRE(Packet::decodeFrame(pkt, r, keys), "txt_msg envelope decoded"); + REQUIRE(!r.decrypted, "txt_msg not decrypted (no matching contact)"); + } + + // ---- GRP_TXT round-trip ---- + { + const QByteArray psk = b::publicChannelPsk(); + const b::GroupChannel pub = b::GroupChannel::fromPsk("public", psk); + b::GrpTxtOptions o; + o.timestamp = 1700003000U; + const QByteArray text = QByteArray("Public group test"); + const QByteArray pkt = b::buildGrpTxt(pub, text, o); + REQUIRE(!pkt.isEmpty(), "grp_txt built"); + + const QString keys = "channel:public=public"; + DecodeResult r; + REQUIRE(Packet::decodeFrame(pkt, r, keys), "grp_txt decoded"); + REQUIRE(r.payloadType == PayloadGrpTxt, "grp_txt payloadType=GrpTxt"); + REQUIRE(r.decrypted, "grp_txt decrypted"); + REQUIRE(findField(r, "grp.text") == text, "grp.text roundtrip"); + REQUIRE(findField(r, "grp.channel") == "public", "grp.channel matches"); + } + + // ---- ANON_REQ round-trip ---- + { + const QByteArray data = QByteArray("anon-payload"); + b::AnonReqOptions o; + o.timestamp = 1700004000U; + const QByteArray pkt = b::buildAnonReq(seedA, pubB, data, o); + const QString keys = QString("identity=") + QString::fromLatin1(seedB.toHex()); + DecodeResult r; + REQUIRE(Packet::decodeFrame(pkt, r, keys), "anon_req decoded"); + REQUIRE(r.payloadType == PayloadAnonReq, "anon_req payloadType=AnonReq"); + REQUIRE(r.decrypted, "anon_req decrypted"); + REQUIRE(findField(r, "anon.text") == QString::fromUtf8(data), "anon.text roundtrip"); + } + + // ---- ACK round-trip ---- + { + const QByteArray msgHash = fromHex("cafebabe"); + b::AckOptions o; + const QByteArray pkt = b::buildAck(pubA, msgHash, o); + DecodeResult r; + REQUIRE(Packet::decodeFrame(pkt, r), "ack decoded"); + REQUIRE(r.payloadType == PayloadAck, "ack payloadType=Ack"); + REQUIRE(findField(r, "ack.msg_hash") == "cafebabe", "ack.msg_hash roundtrip"); + } + + // ---- validateKeySpecList ---- + { + QString err; int n = -1; + const QString good = QString("identity=") + QString::fromLatin1(seedA.toHex()) + + "; channel:public=public"; + REQUIRE(Packet::validateKeySpecList(good, err, &n), "valid key list"); + REQUIRE(n == 2, "valid key list count = 2"); + + REQUIRE(!Packet::validateKeySpecList("identity=ff", err, &n), + "short identity rejected"); + REQUIRE(!Packet::validateKeySpecList("", err, &n), "empty rejected"); + } +} + +int main(int argc, char** argv) +{ + (void)argc; + (void)argv; + + std::printf("modemmeshcore smoke test\n"); + std::printf("[1] testPathLenEncoding\n"); + testPathLenEncoding(); + std::printf("[2] testCryptoRoundtrip\n"); + testCryptoRoundtrip(); + std::printf("[3] testAdvertBuilder\n"); + testAdvertBuilder(); + std::printf("[4] testEncryptedBuilders\n"); + testEncryptedBuilders(); + std::printf("[5] testCommandParser\n"); + testCommandParser(); + std::printf("[6] testRoundtripDecode\n"); + testRoundtripDecode(); + std::printf("\nALL OK\n"); + return 0; +} diff --git a/modemmeshcore/tiny_aes.h b/modemmeshcore/tiny_aes.h new file mode 100644 index 000000000..5586c126d --- /dev/null +++ b/modemmeshcore/tiny_aes.h @@ -0,0 +1,319 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Tiny AES adapted from tiny-AES-c (public domain / unlicense). // +// Original: https://github.com/kokke/tiny-AES-c // +// This file imports only the AES-128 (and AES-256) block primitive used by // +// MeshCore's AES-128-ECB mode. CTR / CBC wrappers are intentionally omitted — // +// MeshCore does ECB only, on payload boundaries. // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef MODEMMESHCORE_TINY_AES_H_ +#define MODEMMESHCORE_TINY_AES_H_ + +#include +#include +#include +#include + +namespace modemmeshcore +{ + +// Single-block AES context. Supports AES-128 (16-byte key) and AES-256 (32-byte key). +// MeshCore uses AES-128 only. +class AesCtx +{ +public: + bool init(const uint8_t* key, std::size_t keyLen) + { + if (keyLen != 16 && keyLen != 32) { + return false; + } + + m_nk = static_cast(keyLen) / 4; + m_nr = m_nk + 6; + const int words = 4 * (m_nr + 1); + m_roundKey.resize(words * 4); + keyExpansion(key); + return true; + } + + void encryptBlock(const uint8_t in[16], uint8_t out[16]) const + { + std::array state; + std::memcpy(state.data(), in, 16); + + addRoundKey(state.data(), 0); + + for (int round = 1; round < m_nr; ++round) + { + subBytes(state.data()); + shiftRows(state.data()); + mixColumns(state.data()); + addRoundKey(state.data(), round); + } + + subBytes(state.data()); + shiftRows(state.data()); + addRoundKey(state.data(), m_nr); + + std::memcpy(out, state.data(), 16); + } + + void decryptBlock(const uint8_t in[16], uint8_t out[16]) const + { + std::array state; + std::memcpy(state.data(), in, 16); + + addRoundKey(state.data(), m_nr); + + for (int round = m_nr - 1; round >= 1; --round) + { + invShiftRows(state.data()); + invSubBytes(state.data()); + addRoundKey(state.data(), round); + invMixColumns(state.data()); + } + + invShiftRows(state.data()); + invSubBytes(state.data()); + addRoundKey(state.data(), 0); + + std::memcpy(out, state.data(), 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 std::array sbox = { + 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 uint8_t invSub(uint8_t x) + { + static const std::array rsbox = { + 0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, + 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, + 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, + 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, + 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, + 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, + 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, + 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, + 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, + 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, + 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, + 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, + 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, + 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, + 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, + 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d + }; + return rsbox[x]; + } + + static void invSubBytes(uint8_t state[16]) + { + for (int i = 0; i < 16; ++i) { + state[i] = invSub(state[i]); + } + } + + static void invShiftRows(uint8_t state[16]) + { + uint8_t t; + + // row 1: shift right 1 + t = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = t; + + // row 2: shift right 2 (same as left 2) + t = state[2]; + state[2] = state[10]; + state[10] = t; + t = state[6]; + state[6] = state[14]; + state[14] = t; + + // row 3: shift right 3 (same as left 1) + t = state[3]; + state[3] = state[7]; + state[7] = state[11]; + state[11] = state[15]; + state[15] = t; + } + + static void invMixColumns(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, 0x0e) ^ mul(a1, 0x0b) ^ mul(a2, 0x0d) ^ mul(a3, 0x09)); + col[1] = static_cast(mul(a0, 0x09) ^ mul(a1, 0x0e) ^ mul(a2, 0x0b) ^ mul(a3, 0x0d)); + col[2] = static_cast(mul(a0, 0x0d) ^ mul(a1, 0x09) ^ mul(a2, 0x0e) ^ mul(a3, 0x0b)); + col[3] = static_cast(mul(a0, 0x0b) ^ mul(a1, 0x0d) ^ mul(a2, 0x09) ^ mul(a3, 0x0e)); + } + } + + 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 std::array rcon = { + 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); + } + } +}; + +} // namespace modemmeshcore + +#endif // MODEMMESHCORE_TINY_AES_H_ diff --git a/plugins/channelrx/demodmeshcore/CMakeLists.txt b/plugins/channelrx/demodmeshcore/CMakeLists.txt new file mode 100644 index 000000000..97ecf9f3f --- /dev/null +++ b/plugins/channelrx/demodmeshcore/CMakeLists.txt @@ -0,0 +1,83 @@ +project(meshcore) + +set(meshcore_SOURCES + meshcoredemod.cpp + meshcoredemodsettings.cpp + meshcoredemodsink.cpp + meshcoredemodbaseband.cpp + meshcoreplugin.cpp + meshcoredemoddecoder.cpp + meshcoredemoddecoderlora.cpp + meshcoredemodmsg.cpp + meshcoredemodwebapiadapter.cpp +) + +set(meshcore_HEADERS + meshcoredemod.h + meshcoredemodsettings.h + meshcoredemodsink.h + meshcoredemodbaseband.h + meshcoredemoddecoder.h + meshcoredemoddecoderlora.h + meshcoredemodmsg.h + meshcoreplugin.h + meshcoredemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${CMAKE_SOURCE_DIR}/modemmeshcore +) + +if(NOT SERVER_MODE) + set(meshcore_SOURCES + ${meshcore_SOURCES} + meshcoredemodgui.cpp + meshcoredemodgui.ui + meshcorekeysdialog.cpp + meshcorekeysdialog.ui + ) + set(meshcore_HEADERS + ${meshcore_HEADERS} + meshcoredemodgui.h + meshcorekeysdialog.h + ) + set(TARGET_NAME ${PLUGINS_PREFIX}demodmeshcore) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME ${PLUGINSSRV_PREFIX}demodmeshcoresrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + + +if(NOT Qt6_FOUND) + add_library(${TARGET_NAME} ${meshcore_SOURCES}) +else() + qt_add_plugin(${TARGET_NAME} CLASS_NAME MeshcorePlugin) + target_sources(${TARGET_NAME} PRIVATE ${meshcore_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 + modemmeshcore +) + +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/demodmeshcore/meshcoredemod.cpp b/plugins/channelrx/demodmeshcore/meshcoredemod.cpp new file mode 100644 index 000000000..53cb666cd --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemod.cpp @@ -0,0 +1,1727 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2023 Edouard Griffiths, F4EXB // +// Copyright (C) 2015 John Greb // +// Copyright (C) 2020 Kacper Michajłow // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// (c) 2015 John Greb // +// (c) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGChannelReport.h" +#include "SWGMeshtasticDemodReport.h" + +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "util/ax25.h" +#include "util/db.h" +#include "maincore.h" +#include "channel/channelwebapiutils.h" + +#include "meshcoredemodmsg.h" +#include "meshcoredemoddecoder.h" +#include "meshcoredemod.h" +#include "meshcorepacket.h" +#include "meshcore_identity.h" + +MESSAGE_CLASS_DEFINITION(MeshcoreDemod::MsgConfigureMeshcoreDemod, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreDemod::MsgSetExtraPipelineSettings, Message) + +const char* const MeshcoreDemod::m_channelIdURI = "sdrangel.channel.meshcoredemod"; +const char* const MeshcoreDemod::m_channelId = "MeshcoreDemod"; + +MeshcoreDemod::MeshcoreDemod(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, + &MeshcoreDemod::handleIndexInDeviceSetChanged + ); + + start(); +} + +MeshcoreDemod::~MeshcoreDemod() +{ + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this, true); + stop(); +} + +void MeshcoreDemod::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 MeshcoreDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void MeshcoreDemod::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 MeshcoreDemod::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 < MeshcoreDemodSettings::nbBandwidths; ++i) + { + const int bw = MeshcoreDemodSettings::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; +} + +MeshcoreDemodSettings MeshcoreDemod::makePipelineSettingsFromMeshRadio( + const MeshcoreDemodSettings& baseSettings, + const QString& presetName, + const modemmeshcore::TxRadioSettings& meshRadio, + qint64 selectedPresetFrequencyHz, + bool haveSelectedPresetFrequency +) const +{ + MeshcoreDemodSettings out = baseSettings; + out.m_spreadFactor = meshRadio.spreadFactor; + out.m_deBits = meshRadio.deBits; + out.m_nbParityBits = meshRadio.parityBits; + out.m_meshcorePresetName = presetName; + out.m_preambleChirps = meshRadio.preambleChirps; + + const int bandwidthIndex = findBandwidthIndexForHz(meshRadio.bandwidthHz); + + if (bandwidthIndex >= 0) { + out.m_bandwidthIndex = bandwidthIndex; + } + + if (meshRadio.hasCenterFrequency) + { + if (m_haveBasebandCenterFrequency) + { + out.m_inputFrequencyOffset = static_cast(meshRadio.centerFrequencyHz - m_basebandCenterFrequency); + } + else if (haveSelectedPresetFrequency) + { + out.m_inputFrequencyOffset = baseSettings.m_inputFrequencyOffset + + static_cast(meshRadio.centerFrequencyHz - selectedPresetFrequencyHz); + } + else + { + out.m_inputFrequencyOffset = baseSettings.m_inputFrequencyOffset; + } + } + + return out; +} + +void MeshcoreDemod::makePipelineConfigFromSettings(int configId, PipelineConfig& config, const MeshcoreDemodSettings& settings) const +{ + // USER preset: all LoRa parameters are user-controlled; skip derivation from the mesh radio table. + if (settings.m_meshcorePresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0) + { + config.id = configId; + config.name = QString("Cnf_%1").arg(configId); + config.presetName = settings.m_meshcorePresetName; + config.settings = settings; + return; + } + + const QString region = settings.m_meshcoreRegionCode.trimmed().isEmpty() + ? QString("EU_868") + : settings.m_meshcoreRegionCode.trimmed(); + const int channelNum = std::max(1, settings.m_meshcoreChannelIndex + 1); + + modemmeshcore::TxRadioSettings meshRadio; + QString error; + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3") + .arg(settings.m_meshcorePresetName.trimmed().isEmpty() ? QString("EU_NARROW") : settings.m_meshcorePresetName.trimmed().toUpper()) + .arg(region) + .arg(channelNum); + qint64 selectedPresetFrequencyHz = 0; + bool haveSelectedPresetFrequency = false; + + if (modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error) && meshRadio.hasCenterFrequency) + { + selectedPresetFrequencyHz = meshRadio.centerFrequencyHz; + haveSelectedPresetFrequency = true; + } + + config.id = configId; + config.name = QString("Cnf_%1").arg(configId); + config.presetName = settings.m_meshcorePresetName; + config.settings = makePipelineSettingsFromMeshRadio( + settings, + config.presetName, + meshRadio, + selectedPresetFrequencyHz, + haveSelectedPresetFrequency + ); +} + +void MeshcoreDemod::applyPipelineRuntimeSettings(PipelineRuntime& runtime, const MeshcoreDemodSettings& settings, bool force) +{ + runtime.settings = settings; + + if (runtime.decoder) + { + runtime.decoder->setCodingScheme(MeshcoreDemodSettings::m_codingScheme); + runtime.decoder->setNbSymbolBits(settings.m_spreadFactor, settings.m_deBits); + runtime.decoder->setLoRaHasHeader(MeshcoreDemodSettings::m_hasHeader); + runtime.decoder->setLoRaHasCRC(MeshcoreDemodSettings::m_hasCRC); + runtime.decoder->setLoRaParityBits(settings.m_nbParityBits); + runtime.decoder->setLoRaPacketLength(settings.m_packetLength); + runtime.decoder->setLoRaBandwidth(MeshcoreDemodSettings::bandwidths[settings.m_bandwidthIndex]); + } + + if (runtime.basebandSink) + { + MeshcoreDemodBaseband::MsgConfigureMeshcoreDemodBaseband *msg = + MeshcoreDemodBaseband::MsgConfigureMeshcoreDemodBaseband::create(settings, force); + runtime.basebandSink->getInputMessageQueue()->push(msg); + } +} + +void MeshcoreDemod::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 MeshcoreDemodDecoder(); + 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 MeshcoreDemodBaseband(); + + 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 MeshcoreDemod::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 MeshcoreDemod::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 MeshcoreDemod::syncPipelinesWithSettings(const MeshcoreDemodSettings& settings, bool force) +{ + if (!m_running) { + return; + } + + // Rebuild pipeline configs from the new settings so that region/preset/channel + // changes are reflected in the single pipeline entry. + for (size_t i = 0; i < m_pipelineConfigs.size(); ++i) { + makePipelineConfigFromSettings(m_pipelineConfigs[i].id, m_pipelineConfigs[i], settings); + } + + for (size_t i = 0; i < m_pipelineConfigs.size(); ++i) + { + m_pipelines[i].id = m_pipelineConfigs[i].id; + m_pipelines[i].name = m_pipelineConfigs[i].name; + m_pipelines[i].presetName = m_pipelineConfigs[i].presetName; + + if (m_pipelines[i].decoder) { + m_pipelines[i].decoder->setPipelineMetadata(m_pipelineConfigs[i].id, m_pipelineConfigs[i].name, m_pipelineConfigs[i].presetName); + } + + applyPipelineRuntimeSettings(m_pipelines[i], m_pipelineConfigs[i].settings, force); + } +} + +void MeshcoreDemod::applyExtraPipelineSettings(const QVector& settingsList, bool force) +{ + if (!m_running) { + return; + } + + // Remove all secondary configs (indices 1+), keeping only the primary (index 0). + while (m_pipelineConfigs.size() > 1) { + m_pipelineConfigs.pop_back(); + } + + // Build new secondary configs from the provided settings list. + // Settings are used as-is (the GUI has already applied preset derivation and frequency offsets). + for (int i = 0; i < settingsList.size(); ++i) + { + PipelineConfig config; + const int configId = i + 1; + config.id = configId; + config.name = QString("Cnf_%1").arg(configId); + config.presetName = settingsList[i].m_meshcorePresetName; + config.settings = settingsList[i]; + m_pipelineConfigs.push_back(config); + } + + // Determine if the layout (pipeline count / IDs) has changed. + const bool layoutChanged = !pipelineLayoutMatches(m_pipelineConfigs); + + if (layoutChanged) + { + // Stop and destroy all secondary runtimes (primary at index 0 stays). + for (int i = static_cast(m_pipelines.size()) - 1; i >= 1; --i) + { + PipelineRuntime& rt = m_pipelines[i]; + if (rt.basebandThread) + { + rt.basebandThread->exit(); + rt.basebandThread->wait(); + delete rt.basebandThread; + rt.basebandThread = nullptr; + } + if (rt.decoderThread) + { + rt.decoderThread->exit(); + rt.decoderThread->wait(); + delete rt.decoderThread; + rt.decoderThread = nullptr; + } + rt.basebandSink = nullptr; + rt.decoder = nullptr; + } + while (m_pipelines.size() > 1) { + m_pipelines.pop_back(); + } + + // Start fresh secondary runtimes. + for (int i = 1; i < static_cast(m_pipelineConfigs.size()); ++i) + { + const PipelineConfig& config = m_pipelineConfigs[i]; + PipelineRuntime runtime; + runtime.id = config.id; + runtime.name = config.name; + runtime.presetName = config.presetName; + runtime.settings = config.settings; + + runtime.decoderThread = new QThread(); + runtime.decoder = new MeshcoreDemodDecoder(); + 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 MeshcoreDemodBaseband(); + // Secondary pipelines (id != 0) do not own the spectrum visualiser. + runtime.basebandSink->setDecoderMessageQueue(runtime.decoder->getInputMessageQueue()); + runtime.decoder->setHeaderFeedbackMessageQueue(runtime.basebandSink->getInputMessageQueue()); + runtime.basebandSink->moveToThread(runtime.basebandThread); + QObject::connect(runtime.basebandThread, &QThread::finished, runtime.basebandSink, &QObject::deleteLater); + + if (m_basebandSampleRate != 0) { + runtime.basebandSink->setBasebandSampleRate(m_basebandSampleRate); + } + runtime.basebandSink->reset(); + runtime.basebandSink->setFifoLabel(QString("%1[%2]").arg(m_channelId).arg(config.name)); + runtime.basebandThread->start(); + + applyPipelineRuntimeSettings(runtime, runtime.settings, true); + m_pipelines.push_back(runtime); + } + } + else + { + // Layout unchanged — just update settings for each secondary pipeline. + for (int i = 1; i < static_cast(m_pipelineConfigs.size()); ++i) + { + m_pipelines[i].id = m_pipelineConfigs[i].id; + m_pipelines[i].name = m_pipelineConfigs[i].name; + m_pipelines[i].presetName = m_pipelineConfigs[i].presetName; + if (m_pipelines[i].decoder) { + m_pipelines[i].decoder->setPipelineMetadata(m_pipelineConfigs[i].id, m_pipelineConfigs[i].name, m_pipelineConfigs[i].presetName); + } + applyPipelineRuntimeSettings(m_pipelines[i], m_pipelineConfigs[i].settings, force); + } + } + + qDebug() << "MeshcoreDemod::applyExtraPipelineSettings: total pipelines=" << m_pipelines.size(); +} + +void MeshcoreDemod::start() +{ + if (m_running) { + return; + } + + qDebug() << "MeshcoreDemod::start"; + m_pipelineConfigs.emplace_back(); + m_currentPipelineId = 0; + makePipelineConfigFromSettings(m_currentPipelineId, m_pipelineConfigs.back(), m_settings); + startPipelines(m_pipelineConfigs); + + SpectrumSettings spectrumSettings = m_spectrumVis.getSettings(); + spectrumSettings.m_ssb = true; + SpectrumVis::MsgConfigureSpectrumVis *msg = SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false); + m_spectrumVis.getInputMessageQueue()->push(msg); + + m_running = true; +} + +void MeshcoreDemod::stop() +{ + if (!m_running) { + return; + } + + qDebug() << "MeshcoreDemod::stop"; + m_running = false; + stopPipelines(); + m_pipelineConfigs.clear(); +} + +namespace { + +QString syncWordToPacketType(uint8_t syncWord) +{ + switch (syncWord) + { + case 0x2B: return QStringLiteral("chirpchat"); + case 0x12: return QStringLiteral("meshcore"); + case 0x43: return QStringLiteral("helium"); + case 0x00: return QStringLiteral("unset"); + default: return QStringLiteral("custom"); + } +} + +QString parityStatusToStr(int status) +{ + switch (status) + { + case (int) MeshcoreDemodSettings::ParityOK: return QStringLiteral("ok"); + case (int) MeshcoreDemodSettings::ParityCorrected: return QStringLiteral("fix"); + case (int) MeshcoreDemodSettings::ParityError: return QStringLiteral("err"); + default: return QStringLiteral("n/a"); + } +} + +QString getMeshField(const modemmeshcore::DecodeResult& result, const QString& path) +{ + for (const auto& field : result.fields) + { + if (field.path == path) { + return field.value; + } + } + return QString(); +} + +} // namespace + +QString MeshcoreDemod::buildMeshcoreJsonPacket( + const MeshcoreDemodMsg::MsgReportDecodeBytes& msg, + const modemmeshcore::DecodeResult& meshResult) const +{ + QJsonObject root; + + // Timestamps + const QDateTime now = QDateTime::currentDateTime(); + root["timestamp"] = now.toString("yyyy-MM-ddTHH:mm:ss.zzz"); + root["timestamp_unix"] = now.toMSecsSinceEpoch(); + + // RF + QJsonObject rf; + rf["center_freq_hz"] = static_cast( + m_basebandCenterFrequency + m_settings.m_inputFrequencyOffset); + rf["bandwidth_hz"] = MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; + rf["spreading_factor"] = m_settings.m_spreadFactor; + rf["signal_db"] = msg.getSingalDb(); + rf["noise_db"] = msg.getNoiseDb(); + rf["snr_db"] = msg.getSingalDb() - msg.getNoiseDb(); + root["rf"] = rf; + + // LoRa + const uint8_t syncWord = static_cast(msg.getSyncWord()); + const QString packetType = syncWordToPacketType(syncWord); + + QJsonObject lora; + lora["packet_type"] = packetType; + lora["sync_word"] = QString("0x%1").arg(syncWord, 2, 16, QChar('0')); + lora["header_fec"] = parityStatusToStr(msg.getHeaderParityStatus()); + lora["header_crc"] = msg.getHeaderCRCStatus() ? QStringLiteral("ok") : QStringLiteral("err"); + + if (msg.getEarlyEOM()) + { + lora["payload_fec"] = QStringLiteral("n/a"); + lora["payload_crc"] = QStringLiteral("n/a"); + } + else + { + lora["payload_fec"] = parityStatusToStr(msg.getPayloadParityStatus()); + lora["payload_crc"] = msg.getPayloadCRCStatus() ? QStringLiteral("ok") : QStringLiteral("err"); + } + + lora["early_eom"] = msg.getEarlyEOM(); + lora["packet_length"] = static_cast(msg.getPacketSize()); + lora["nb_symbols"] = static_cast(msg.getNbSymbols()); + lora["nb_codewords"] = static_cast(msg.getNbCodewords()); + lora["payload_hex"] = QString(msg.getBytes().left( + static_cast(msg.getPacketSize())).toHex()); + root["lora"] = lora; + + // MeshCore section — sync word 0x12. Emits decoded fields from + // modemmeshcore::Packet::decodeFrame: payload type, decrypted plaintext + // when our identity / channel PSK was the recipient, ADVERT name+pubkey, + // etc. Operators reading the UDP JSON now see *what* was sent on-air, + // not just the raw ciphertext under `lora.payload_hex`. + if (syncWord == 0x12) + { + QJsonObject mesh; + mesh["decoded"] = meshResult.isFrame; + mesh["decrypted"] = meshResult.decrypted; + if (!meshResult.summary.isEmpty()) { + mesh["summary"] = meshResult.summary; + } + if (!meshResult.keyLabel.isEmpty()) { + mesh["key_label"] = meshResult.keyLabel; + } + QJsonObject fields; + for (const auto& field : meshResult.fields) { + fields[field.path] = field.value; + } + mesh["fields"] = fields; + root["meshcore"] = mesh; + } + // Meshtastic-format report (sync word 0x2B) — emitted only when the + // demodulator is fed Meshtastic traffic on a non-MeshCore sync word. + // Useful for diagnostic / cross-protocol debugging. + else if (syncWord == 0x2B) + { + QJsonObject mesh; + const bool headerCrcOk = msg.getHeaderCRCStatus(); + + const QString channelHash = getMeshField(meshResult, "header.channel_hash"); + if (headerCrcOk && !channelHash.isEmpty()) { + mesh["channel_hash"] = channelHash; + } else { + mesh["channel_hash"] = QStringLiteral("unknown"); + } + + const QString packetId = getMeshField(meshResult, "header.id"); + if (headerCrcOk && !packetId.isEmpty()) { + mesh["packet_id"] = packetId; + } else { + mesh["packet_id"] = QStringLiteral("unknown"); + } + + const QString hopStart = getMeshField(meshResult, "header.hop_start"); + const QString hopLimit = getMeshField(meshResult, "header.hop_limit"); + const QString relayNode = getMeshField(meshResult, "header.relay_node"); + + if (headerCrcOk) + { + if (!hopStart.isEmpty()) { + mesh["hop_start"] = hopStart.toInt(); + } else { + mesh["hop_start"] = QStringLiteral("unknown"); + } + + if (!hopLimit.isEmpty()) { + mesh["hop_limit"] = hopLimit.toInt(); + } else { + mesh["hop_limit"] = QStringLiteral("unknown"); + } + + if (!hopStart.isEmpty() && !hopLimit.isEmpty()) { + mesh["hops_consumed"] = hopStart.toInt() - hopLimit.toInt(); + } else { + mesh["hops_consumed"] = QStringLiteral("unknown"); + } + + if (!relayNode.isEmpty()) { + mesh["relay_node"] = relayNode.toInt(); + } else { + mesh["relay_node"] = QStringLiteral("unknown"); + } + } + else + { + mesh["hop_start"] = QStringLiteral("unknown"); + mesh["hop_limit"] = QStringLiteral("unknown"); + mesh["hops_consumed"] = QStringLiteral("unknown"); + mesh["relay_node"] = QStringLiteral("unknown"); + } + + const QString decodePath = getMeshField(meshResult, "decode.path"); + const QString keyLabel = getMeshField(meshResult, "decode.key_label"); + + QString decryption; + QString keyLabelOut; + + if (decodePath == QStringLiteral("plain")) + { + decryption = QStringLiteral("plaintext"); + keyLabelOut = QStringLiteral("no_key"); + mesh["hash_matching_index"] = QStringLiteral("none"); + } + else if (decodePath == QStringLiteral("aes_ctr_be")) + { + decryption = QStringLiteral("decrypted"); + keyLabelOut = keyLabel.isEmpty() ? QStringLiteral("unknown_key") : keyLabel; + mesh["hash_matching_index"] = 0; + } + else + { + decryption = QStringLiteral("not_decrypted"); + keyLabelOut = QStringLiteral("unknown_key"); + mesh["hash_matching_index"] = QStringLiteral("none"); + } + + mesh["decryption"] = decryption; + mesh["key_label"] = keyLabelOut; + mesh["parsed"] = meshResult.dataDecoded; + + if (meshResult.dataDecoded) + { + mesh["channel_type"] = m_settings.m_meshcorePresetName; + + QJsonObject fields; + for (const auto& field : meshResult.fields) + { + if (field.path.startsWith(QStringLiteral("decode."))) { + continue; + } + fields[field.path] = field.value; + } + mesh["fields"] = fields; + } + + root["meshcore"] = mesh; + } + + return QString(QJsonDocument(root).toJson(QJsonDocument::Indented)); +} + +bool MeshcoreDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshcoreDemod::match(cmd)) + { + qDebug() << "MeshcoreDemod::handleMessage: MsgConfigureMeshcoreDemod"; + MsgConfigureMeshcoreDemod& cfg = (MsgConfigureMeshcoreDemod&) cmd; + MeshcoreDemodSettings settings = cfg.getSettings(); + applySettings(settings, cfg.getForce()); + + return true; + } + else if (MsgSetExtraPipelineSettings::match(cmd)) + { + qDebug() << "MeshcoreDemod::handleMessage: MsgSetExtraPipelineSettings"; + const auto& msg = static_cast(cmd); + applyExtraPipelineSettings(msg.getSettingsList(), false); + return true; + } + else if (MeshcoreDemodMsg::MsgReportDecodeBytes::match(cmd)) + { + qDebug() << "MeshcoreDemod::handleMessage: MsgReportDecodeBytes"; + MeshcoreDemodMsg::MsgReportDecodeBytes& msg = (MeshcoreDemodMsg::MsgReportDecodeBytes&) cmd; + + m_lastMsgSignalDb = msg.getSingalDb(); + m_lastMsgNoiseDb = msg.getNoiseDb(); + m_lastMsgSyncWord = msg.getSyncWord(); + m_lastMsgTimestamp = msg.getMsgTimestamp(); + + m_lastMsgBytes = msg.getBytes(); + m_lastMsgPacketLength = msg.getPacketSize(); + m_lastMsgNbParityBits = msg.getNbParityBits(); + m_lastMsgHasCRC = msg.getHasCRC(); + m_lastMsgNbSymbols = msg.getNbSymbols(); + m_lastMsgNbCodewords = msg.getNbCodewords(); + m_lastMsgEarlyEOM = msg.getEarlyEOM(); + m_lastMsgHeaderCRC = msg.getHeaderCRCStatus(); + m_lastMsgHeaderParityStatus = msg.getHeaderParityStatus(); + m_lastMsgPayloadCRC = msg.getPayloadCRCStatus(); + m_lastMsgPayloadParityStatus = msg.getPayloadParityStatus(); + m_lastMsgPipelineName = msg.getPipelineName(); + m_lastFrameType = QStringLiteral("LORA_FRAME"); + + QByteArray bytesCopy(m_lastMsgBytes); + bytesCopy.truncate(m_lastMsgPacketLength); + bytesCopy.replace('\0', " "); + m_lastMsgString = QString(bytesCopy.toStdString().c_str()); + + if (m_settings.m_sendViaUDP) + { + uint8_t *bytes = reinterpret_cast(m_lastMsgBytes.data()); + m_udpSink.writeUnbuffered(bytes, m_lastMsgPacketLength); + } + + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(new MeshcoreDemodMsg::MsgReportDecodeBytes(msg)); // make a copy + } + + modemmeshcore::DecodeResult meshResult; + + // Augment the user-provided key spec list with sensible defaults so + // common-case decoding works out-of-the-box: + // identity= — trial-decrypts TXT_MSG / ANON_REQ + // addressed to our pubkey + // channel:public=8b3387e9... — fixed MeshCore Public channel PSK + // per firmware convention; lets + // GRP_TXT on the public group decode + // without the operator typing it + // in the Keys dialog. + // Both are no-ops if the operator already supplied them. + // + // Environment override: SDRANGEL_MESHCORE_KEYS prepends key specs, + // useful for CI/headless testing. GUI-specified keys come next so + // they can still contribute (both env and GUI keys are tried). + QString envKeys = modemmeshcore::Packet::defaultKeysFromEnv(); + QString effectiveKeys = envKeys.isEmpty() + ? m_settings.m_meshcoreKeySpecList + : (m_settings.m_meshcoreKeySpecList.isEmpty() + ? envKeys + : envKeys + QStringLiteral("; ") + m_settings.m_meshcoreKeySpecList); + auto appendSep = [&](){ + if (!effectiveKeys.isEmpty() + && !effectiveKeys.endsWith(QChar(';'))) { + effectiveKeys.append(QStringLiteral("; ")); + } + }; + if (!effectiveKeys.contains(QStringLiteral("identity="), Qt::CaseInsensitive)) + { + modemmeshcore::identity::Identity self = + modemmeshcore::identity::loadOrCreateIdentity( + modemmeshcore::identity::defaultIdentityPath()); + if (self.isValid()) + { + appendSep(); + effectiveKeys.append(QStringLiteral("identity=")); + effectiveKeys.append(self.seedHex()); + } + } + if (!effectiveKeys.contains(QStringLiteral("channel:public"), Qt::CaseInsensitive)) + { + appendSep(); + effectiveKeys.append(QStringLiteral("channel:public=8b3387e9c5cdea6ac9e5edbaa115cd72")); + } + + if (modemmeshcore::Packet::decodeFrame(m_lastMsgBytes, meshResult, effectiveKeys)) + { + m_lastMsgString = meshResult.summary; + + for (const modemmeshcore::DecodeResult::Field& field : meshResult.fields) + { + if (field.path == QStringLiteral("data.port_name")) + { + m_lastFrameType = field.value; + break; + } + } + + qInfo() << "MeshcoreDemod::handleMessage:" << meshResult.summary; + + if (meshResult.dataDecoded && getMessageQueueToGUI()) + { + MeshcoreDemodMsg::MsgReportDecodeString *meshMsg = MeshcoreDemodMsg::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 modemmeshcore::DecodeResult::Field& field : meshResult.fields) { + structuredFields.append(qMakePair(field.path, field.value)); + } + + meshMsg->setStructuredFields(structuredFields); + getMessageQueueToGUI()->push(meshMsg); + } + } + + // Is this an APRS packet? + // As per: https://github.com/oe3cjb/TTGO-T-Beam-LoRa-APRS/blob/master/lib/BG_RF95/BG_RF95.cpp + // There is a 3 byte header for LoRa APRS packets. Addressing follows in ASCII: srccall>dst: + int colonIdx = m_lastMsgBytes.indexOf(':'); + int greaterThanIdx = m_lastMsgBytes.indexOf('>'); + if ( (m_lastMsgBytes[0] == '<') + && (greaterThanIdx != -1) + && (colonIdx != -1) + && ((m_lastMsgHasCRC && m_lastMsgPayloadCRC) || !m_lastMsgHasCRC) + ) + { + QByteArray packet; + + // Extract addresses + const char *d = m_lastMsgBytes.data(); + QString srcString = QString::fromLatin1(d + 3, greaterThanIdx - 3); + QString dstString = QString::fromLatin1(d + greaterThanIdx + 1, colonIdx - greaterThanIdx - 1); + + // Convert to AX.25 format + packet.append(AX25Packet::encodeAddress(dstString)); + packet.append(AX25Packet::encodeAddress(srcString, 1)); + packet.append(3); + packet.append(-16); // 0xf0 + packet.append(m_lastMsgBytes.mid(colonIdx+1)); + if (!m_lastMsgHasCRC) + { + packet.append((char)0); // dummy crc + packet.append((char)0); + } + + // Forward to APRS and other packet features + QList packetsPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(this, "packets", packetsPipes); + + for (const auto& pipe : packetsPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + MainCore::MsgPacket *msg = MainCore::MsgPacket::create(this, packet, QDateTime::currentDateTime()); + messageQueue->push(msg); + } + } + + if (m_settings.m_sendJsonViaUDP) + { + const QString json = buildMeshcoreJsonPacket(msg, meshResult); + const QByteArray jsonBytes = json.toUtf8(); + m_udpSink.writeUnbuffered( + reinterpret_cast(jsonBytes.constData()), + jsonBytes.size()); + } + + return true; + } + else if (MeshcoreDemodMsg::MsgReportDecodeString::match(cmd)) + { + qDebug() << "MeshcoreDemod::handleMessage: MsgReportDecodeString"; + MeshcoreDemodMsg::MsgReportDecodeString& msg = (MeshcoreDemodMsg::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 MeshcoreDemodMsg::MsgReportDecodeString(msg)); // make a copy + } + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_basebandCenterFrequency = notif.getCenterFrequency(); + m_haveBasebandCenterFrequency = true; + qDebug() << "MeshcoreDemod::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 MeshcoreDemod::setCenterFrequency(qint64 frequency) +{ + MeshcoreDemodSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMeshcoreDemod *msgToGUI = MsgConfigureMeshcoreDemod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +QByteArray MeshcoreDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshcoreDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureMeshcoreDemod *msg = MsgConfigureMeshcoreDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureMeshcoreDemod *msg = MsgConfigureMeshcoreDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void MeshcoreDemod::applySettings(MeshcoreDemodSettings settings, bool force) +{ + qDebug() << "MeshcoreDemod::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_bandwidthIndex: " << settings.m_bandwidthIndex + << " m_spreadFactor: " << settings.m_spreadFactor + << " m_deBits: " << settings.m_deBits + << " m_codingScheme: " << MeshcoreDemodSettings::m_codingScheme + << " m_hasHeader: " << MeshcoreDemodSettings::m_hasHeader + << " m_hasCRC: " << MeshcoreDemodSettings::m_hasCRC + << " m_nbParityBits: " << settings.m_nbParityBits + << " m_packetLength: " << settings.m_packetLength + << " m_autoNbSymbolsMax: " << MeshcoreDemodSettings::m_autoNbSymbolsMax + << " m_sendViaUDP: " << settings.m_sendViaUDP + << " m_sendJsonViaUDP: " << settings.m_sendJsonViaUDP + << " m_udpAddress: " << settings.m_udpAddress + << " m_udpPort: " << settings.m_udpPort + << " m_meshcoreKeySpecList: " << settings.m_meshcoreKeySpecList + << " m_decodeActive: " << settings.m_decodeActive + << " m_eomSquelchTenths: " << settings.m_eomSquelchTenths + << " m_nbSymbolsMax: " << settings.m_nbSymbolsMax + << " m_preambleChirps: " << settings.m_preambleChirps + << " m_streamIndex: " << settings.m_streamIndex + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_invertRamps: " << settings.m_invertRamps + << " m_rgbColor: " << settings.m_rgbColor + << " m_title: " << settings.m_title + << " m_meshcorePresetName: " << settings.m_meshcorePresetName + << " 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( + MeshcoreDemodSettings::bandwidths[settings.m_bandwidthIndex], + 0); + m_spectrumVis.getInputMessageQueue()->push(msg); + } + + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) || force) { + reverseAPIKeys.append("spreadFactor"); + } + if ((settings.m_deBits != m_settings.m_deBits) || force) { + reverseAPIKeys.append("deBits"); + } + + if ((settings.m_nbParityBits != m_settings.m_nbParityBits) || force) + { + reverseAPIKeys.append("nbParityBits"); + } + + if ((settings.m_packetLength != m_settings.m_packetLength) || force) + { + reverseAPIKeys.append("packetLength"); + } + + if ((settings.m_decodeActive != m_settings.m_decodeActive) || force) { + reverseAPIKeys.append("decodeActive"); + } + if ((settings.m_eomSquelchTenths != m_settings.m_eomSquelchTenths) || force) { + reverseAPIKeys.append("eomSquelchTenths"); + } + if ((settings.m_nbSymbolsMax != m_settings.m_nbSymbolsMax) || force) { + reverseAPIKeys.append("nbSymbolsMax"); + } + if ((settings.m_preambleChirps != m_settings.m_preambleChirps) || force) { + reverseAPIKeys.append("preambleChirps"); + } + if ((settings.m_rgbColor != m_settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + if ((settings.m_title != m_settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((settings.m_sendViaUDP != m_settings.m_sendViaUDP) || force) { + reverseAPIKeys.append("sendViaUDP"); + } + if ((settings.m_sendJsonViaUDP != m_settings.m_sendJsonViaUDP) || force) { + reverseAPIKeys.append("sendJsonViaUDP"); + } + if ((settings.m_invertRamps != m_settings.m_invertRamps) || force) { + reverseAPIKeys.append("invertRamps"); + } + + if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) + { + reverseAPIKeys.append("udpAddress"); + m_udpSink.setAddress(settings.m_udpAddress); + } + + if ((settings.m_udpPort != m_settings.m_udpPort) || force) + { + reverseAPIKeys.append("udpPort"); + m_udpSink.setPort(settings.m_udpPort); + } + + if ((settings.m_meshcoreKeySpecList != m_settings.m_meshcoreKeySpecList) || force) { + reverseAPIKeys.append("meshcoreKeySpecList"); + } + + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSink(this, settings.m_streamIndex); + m_deviceAPI->addChannelSinkAPI(this); + m_settings.m_streamIndex = settings.m_streamIndex; // make sure ChannelAPI::getStreamIndex() is consistent + emit streamIndexChanged(settings.m_streamIndex); + } + + reverseAPIKeys.append("streamIndex"); + } + + if (m_running) { + syncPipelinesWithSettings(settings, force); + } + + // Copy LoRa params derived from the preset (bandwidth, spread factor, etc.) back into + // settings so that m_settings and the GUI stay in sync with what was actually applied. + // Skip for USER preset: those parameters are controlled entirely by the user via the GUI. + if (m_running && !m_pipelineConfigs.empty() && + settings.m_meshcorePresetName.trimmed().compare("USER", Qt::CaseInsensitive) != 0) + { + const MeshcoreDemodSettings& derived = m_pipelineConfigs[0].settings; + const bool bwChanged = (settings.m_bandwidthIndex != derived.m_bandwidthIndex); + + settings.m_spreadFactor = derived.m_spreadFactor; + settings.m_deBits = derived.m_deBits; + settings.m_nbParityBits = derived.m_nbParityBits; + settings.m_preambleChirps = derived.m_preambleChirps; + settings.m_bandwidthIndex = derived.m_bandwidthIndex; + settings.m_inputFrequencyOffset = derived.m_inputFrequencyOffset; + + if (bwChanged) + { + auto *bwMsg = new DSPSignalNotification( + MeshcoreDemodSettings::bandwidths[settings.m_bandwidthIndex], 0); + m_spectrumVis.getInputMessageQueue()->push(bwMsg); + } + } + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + QList pipes; + MainCore::instance()->getMessagePipes().getMessagePipes(this, "settings", pipes); + + if (pipes.size() > 0) { + sendChannelSettings(pipes, reverseAPIKeys, settings, force); + } + + m_settings = settings; + + // Forward preset-derived settings back to GUI so controls (e.g. BW slider) reflect + // the values actually applied. Skip for USER preset: no parameters were derived, so + // there is nothing to sync back — and echoing would trigger an infinite apply loop + // (GUI apply → demod echo → GUI displaySettings → rebuildMeshcoreChannelOptions + // → queued apply → …). + const bool isUserPreset = m_settings.m_meshcorePresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0; + if (!isUserPreset && getMessageQueueToGUI()) + { + MsgConfigureMeshcoreDemod *msgToGUI = MsgConfigureMeshcoreDemod::create(m_settings, false); + getMessageQueueToGUI()->push(msgToGUI); + } +} + +int MeshcoreDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setMeshtasticDemodSettings(new SWGSDRangel::SWGMeshtasticDemodSettings()); + response.getMeshtasticDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int MeshcoreDemod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int MeshcoreDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + MeshcoreDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureMeshcoreDemod *msg = MsgConfigureMeshcoreDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMeshcoreDemod *msgToGUI = MsgConfigureMeshcoreDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void MeshcoreDemod::webapiUpdateChannelSettings( + MeshcoreDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getMeshtasticDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("bandwidthIndex")) { + settings.m_bandwidthIndex = response.getMeshtasticDemodSettings()->getBandwidthIndex(); + } + if (channelSettingsKeys.contains("spreadFactor")) { + settings.m_spreadFactor = response.getMeshtasticDemodSettings()->getSpreadFactor(); + } + if (channelSettingsKeys.contains("deBits")) { + settings.m_deBits = response.getMeshtasticDemodSettings()->getDeBits(); + } + if (channelSettingsKeys.contains("decodeActive")) { + settings.m_decodeActive = response.getMeshtasticDemodSettings()->getDecodeActive() != 0; + } + if (channelSettingsKeys.contains("eomSquelchTenths")) { + settings.m_eomSquelchTenths = response.getMeshtasticDemodSettings()->getEomSquelchTenths(); + } + if (channelSettingsKeys.contains("nbSymbolsMax")) { + settings.m_nbSymbolsMax = response.getMeshtasticDemodSettings()->getNbSymbolsMax(); + } + if (channelSettingsKeys.contains("preambleChirps")) { + settings.m_preambleChirps = response.getMeshtasticDemodSettings()->getPreambleChirps(); + } + if (channelSettingsKeys.contains("nbParityBits")) { + settings.m_nbParityBits = response.getMeshtasticDemodSettings()->getNbParityBits(); + } + if (channelSettingsKeys.contains("packetLength")) { + settings.m_packetLength = response.getMeshtasticDemodSettings()->getPacketLength(); + } + if (channelSettingsKeys.contains("sendViaUDP")) { + settings.m_sendViaUDP = response.getMeshtasticDemodSettings()->getSendViaUdp() != 0; + } + if (channelSettingsKeys.contains("sendJsonViaUDP")) { + settings.m_sendJsonViaUDP = response.getMeshtasticDemodSettings()->getSendJsonViaUdp() != 0; + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getMeshtasticDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) + { + uint16_t port = response.getMeshtasticDemodSettings()->getUdpPort(); + settings.m_udpPort = port < 1024 ? 1024 : port; + } + if (channelSettingsKeys.contains("invertRamps")) { + settings.m_invertRamps = response.getMeshtasticDemodSettings()->getInvertRamps() != 0; + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getMeshtasticDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getMeshtasticDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getMeshtasticDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getMeshtasticDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getMeshtasticDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getMeshtasticDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getMeshtasticDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getMeshtasticDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_spectrumGUI && channelSettingsKeys.contains("spectrumConfig")) { + settings.m_spectrumGUI->updateFrom(channelSettingsKeys, response.getMeshtasticDemodSettings()->getSpectrumConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getMeshtasticDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getMeshtasticDemodSettings()->getRollupState()); + } +} + +int MeshcoreDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setMeshtasticDemodReport(new SWGSDRangel::SWGMeshtasticDemodReport()); + response.getMeshtasticDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void MeshcoreDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const MeshcoreDemodSettings& settings) +{ + response.getMeshtasticDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getMeshtasticDemodSettings()->setBandwidthIndex(settings.m_bandwidthIndex); + response.getMeshtasticDemodSettings()->setSpreadFactor(settings.m_spreadFactor); + response.getMeshtasticDemodSettings()->setDeBits(settings.m_deBits); + response.getMeshtasticDemodSettings()->setDecodeActive(settings.m_decodeActive ? 1 : 0); + response.getMeshtasticDemodSettings()->setEomSquelchTenths(settings.m_eomSquelchTenths); + response.getMeshtasticDemodSettings()->setNbSymbolsMax(settings.m_nbSymbolsMax); + response.getMeshtasticDemodSettings()->setPreambleChirps(settings.m_preambleChirps); + response.getMeshtasticDemodSettings()->setNbParityBits(settings.m_nbParityBits); + response.getMeshtasticDemodSettings()->setPacketLength(settings.m_packetLength); + response.getMeshtasticDemodSettings()->setSendViaUdp(settings.m_sendViaUDP ? 1 : 0); + response.getMeshtasticDemodSettings()->setSendJsonViaUdp(settings.m_sendJsonViaUDP ? 1 : 0); + response.getMeshtasticDemodSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0); + + if (response.getMeshtasticDemodSettings()->getUdpAddress()) { + *response.getMeshtasticDemodSettings()->getUdpAddress() = settings.m_udpAddress; + } else { + response.getMeshtasticDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + } + + response.getMeshtasticDemodSettings()->setUdpPort(settings.m_udpPort); + response.getMeshtasticDemodSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getMeshtasticDemodSettings()->getTitle()) { + *response.getMeshtasticDemodSettings()->getTitle() = settings.m_title; + } else { + response.getMeshtasticDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getMeshtasticDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getMeshtasticDemodSettings()->getReverseApiAddress()) { + *response.getMeshtasticDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getMeshtasticDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getMeshtasticDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getMeshtasticDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getMeshtasticDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_spectrumGUI) + { + if (response.getMeshtasticDemodSettings()->getSpectrumConfig()) + { + settings.m_spectrumGUI->formatTo(response.getMeshtasticDemodSettings()->getSpectrumConfig()); + } + else + { + SWGSDRangel::SWGGLSpectrum *swgGLSpectrum = new SWGSDRangel::SWGGLSpectrum(); + settings.m_spectrumGUI->formatTo(swgGLSpectrum); + response.getMeshtasticDemodSettings()->setSpectrumConfig(swgGLSpectrum); + } + } + + if (settings.m_channelMarker) + { + if (response.getMeshtasticDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getMeshtasticDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getMeshtasticDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getMeshtasticDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getMeshtasticDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getMeshtasticDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void MeshcoreDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + if (m_running && !m_pipelines.empty() && m_pipelines[0].basebandSink) { + response.getMeshtasticDemodReport()->setChannelSampleRate(m_pipelines[0].basebandSink->getChannelSampleRate()); + } + + response.getMeshtasticDemodReport()->setChannelPowerDb(CalcDb::dbPower(getTotalPower())); + response.getMeshtasticDemodReport()->setSignalPowerDb(m_lastMsgSignalDb); + response.getMeshtasticDemodReport()->setNoisePowerDb(CalcDb::dbPower(getCurrentNoiseLevel())); + response.getMeshtasticDemodReport()->setSnrPowerDb(m_lastMsgSignalDb - m_lastMsgNoiseDb); + response.getMeshtasticDemodReport()->setNbParityBits(m_lastMsgNbParityBits); + response.getMeshtasticDemodReport()->setPacketLength(m_lastMsgPacketLength); + response.getMeshtasticDemodReport()->setNbSymbols(m_lastMsgNbSymbols); + response.getMeshtasticDemodReport()->setNbCodewords(m_lastMsgNbCodewords); + response.getMeshtasticDemodReport()->setHeaderParityStatus(m_lastMsgHeaderParityStatus); + response.getMeshtasticDemodReport()->setHeaderCrcStatus(m_lastMsgHeaderCRC); + response.getMeshtasticDemodReport()->setPayloadParityStatus(m_lastMsgPayloadParityStatus); + response.getMeshtasticDemodReport()->setPayloadCrcStatus(m_lastMsgPayloadCRC); + response.getMeshtasticDemodReport()->setMessageTimestamp(new QString(m_lastMsgTimestamp)); + response.getMeshtasticDemodReport()->setMessageString(new QString(m_lastMsgString)); + response.getMeshtasticDemodReport()->setFrameType(new QString(m_lastFrameType)); + response.getMeshtasticDemodReport()->setChannelType(new QString(m_lastMsgPipelineName)); + response.getMeshtasticDemodReport()->setDecoding(getDemodActive() ? 1 : 0); + + response.getMeshtasticDemodReport()->setMessageBytes(new QList); + QList *bytesStr = response.getMeshtasticDemodReport()->getMessageBytes(); + + for (QByteArray::const_iterator it = m_lastMsgBytes.begin(); it != m_lastMsgBytes.end(); ++it) + { + unsigned char b = *it; + bytesStr->push_back(new QString(tr("%1").arg(b, 2, 16, QChar('0')))); + } +} + +void MeshcoreDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const MeshcoreDemodSettings& 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 MeshcoreDemod::sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshcoreDemodSettings& 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 MeshcoreDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshcoreDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString(m_channelId)); + swgChannelSettings->setMeshtasticDemodSettings(new SWGSDRangel::SWGMeshtasticDemodSettings()); + SWGSDRangel::SWGMeshtasticDemodSettings *swgMeshcoreDemodSettings = swgChannelSettings->getMeshtasticDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgMeshcoreDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("bandwidthIndex") || force) { + swgMeshcoreDemodSettings->setBandwidthIndex(settings.m_bandwidthIndex); + } + if (channelSettingsKeys.contains("spreadFactor") || force) { + swgMeshcoreDemodSettings->setSpreadFactor(settings.m_spreadFactor); + } + if (channelSettingsKeys.contains("deBits") || force) { + swgMeshcoreDemodSettings->setDeBits(settings.m_deBits); + } + if (channelSettingsKeys.contains("decodeActive") || force) { + swgMeshcoreDemodSettings->setDecodeActive(settings.m_decodeActive ? 1 : 0); + } + if (channelSettingsKeys.contains("eomSquelchTenths") || force) { + swgMeshcoreDemodSettings->setEomSquelchTenths(settings.m_eomSquelchTenths); + } + if (channelSettingsKeys.contains("nbSymbolsMax") || force) { + swgMeshcoreDemodSettings->setNbSymbolsMax(settings.m_nbSymbolsMax); + } + if (channelSettingsKeys.contains("preambleChirps") || force) { + swgMeshcoreDemodSettings->setPreambleChirps(settings.m_preambleChirps); + } + if (channelSettingsKeys.contains("nbParityBits") || force) { + swgMeshcoreDemodSettings->setNbParityBits(settings.m_nbParityBits); + } + if (channelSettingsKeys.contains("packetLength") || force) { + swgMeshcoreDemodSettings->setPacketLength(settings.m_packetLength); + } + if (channelSettingsKeys.contains("sendViaUDP") || force) { + swgMeshcoreDemodSettings->setSendViaUdp(settings.m_sendViaUDP ? 1 : 0); + } + if (channelSettingsKeys.contains("sendJsonViaUDP") || force) { + swgMeshcoreDemodSettings->setSendJsonViaUdp(settings.m_sendJsonViaUDP ? 1 : 0); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgMeshcoreDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgMeshcoreDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("invertRamps") || force) { + swgMeshcoreDemodSettings->setInvertRamps(settings.m_invertRamps ? 1 : 0); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgMeshcoreDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgMeshcoreDemodSettings->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); + swgMeshcoreDemodSettings->setSpectrumConfig(swgGLSpectrum); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgMeshcoreDemodSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgMeshcoreDemodSettings->setRollupState(swgRollupState); + } +} + +void MeshcoreDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "MeshcoreDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("MeshcoreDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +bool MeshcoreDemod::getDemodActive() const +{ + if (!m_running) { + return false; + } + + for (const PipelineRuntime& runtime : m_pipelines) + { + if (runtime.basebandSink && runtime.basebandSink->getDemodActive()) { + return true; + } + } + + return false; +} + +double MeshcoreDemod::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 MeshcoreDemod::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 MeshcoreDemod::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/demodmeshcore/meshcoredemod.h b/plugins/channelrx/demodmeshcore/meshcoredemod.h new file mode 100644 index 000000000..9d64280ed --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemod.h @@ -0,0 +1,255 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MESHCOREDEMOD_H +#define INCLUDE_MESHCOREDEMOD_H + +#include + +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "dsp/spectrumvis.h" +#include "channel/channelapi.h" +#include "util/message.h" +#include "util/udpsinkutil.h" + +#include "meshcoredemodbaseband.h" + +class QNetworkAccessManager; +class QNetworkReply; +class DeviceAPI; +class QThread; +class ObjectPipe; +class MeshcoreDemodDecoder; +namespace MeshcoreDemodMsg { class MsgReportDecodeBytes; } +namespace modemmeshcore { struct TxRadioSettings; struct DecodeResult; } + +class MeshcoreDemod : public BasebandSampleSink, public ChannelAPI { +public: + // Carries the full settings for each extra (non-primary) pipeline. Sent from GUI to demod + // whenever the secondary pipeline list changes. Index 0 in the vector = pipeline id 1, etc. + class MsgSetExtraPipelineSettings : public Message { + MESSAGE_CLASS_DECLARATION + public: + const QVector& getSettingsList() const { return m_settingsList; } + static MsgSetExtraPipelineSettings* create(const QVector& settingsList) + { + return new MsgSetExtraPipelineSettings(settingsList); + } + private: + QVector m_settingsList; + explicit MsgSetExtraPipelineSettings(const QVector& settingsList) : + Message(), m_settingsList(settingsList) + { } + }; + + class MsgConfigureMeshcoreDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshcoreDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshcoreDemod* create(const MeshcoreDemodSettings& settings, bool force) + { + return new MsgConfigureMeshcoreDemod(settings, force); + } + + private: + MeshcoreDemodSettings m_settings; + bool m_force; + + MsgConfigureMeshcoreDemod(const MeshcoreDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + MeshcoreDemod(DeviceAPI* deviceAPI); + virtual ~MeshcoreDemod(); + 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 MeshcoreDemodSettings& settings); + + static void webapiUpdateChannelSettings( + MeshcoreDemodSettings& 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; + MeshcoreDemodSettings settings; + }; + + struct PipelineRuntime + { + int id = -1; + QString name; + QString presetName; + MeshcoreDemodSettings settings; + QThread *basebandThread = nullptr; + QThread *decoderThread = nullptr; + MeshcoreDemodBaseband *basebandSink = nullptr; + MeshcoreDemodDecoder *decoder = nullptr; + }; + + DeviceAPI *m_deviceAPI; + std::vector m_pipelineConfigs; + std::vector m_pipelines; + int m_currentPipelineId; + bool m_running; + MeshcoreDemodSettings 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_lastMsgPipelineName; + QString m_lastMsgTimestamp; + QString m_lastMsgString; + QString m_lastFrameType; + QByteArray m_lastMsgBytes; + UDPSinkUtil m_udpSink; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + virtual bool handleMessage(const Message& cmd); + void applySettings(MeshcoreDemodSettings settings, bool force = false); + QString buildMeshcoreJsonPacket( + const MeshcoreDemodMsg::MsgReportDecodeBytes& msg, + const modemmeshcore::DecodeResult& meshResult) const; + void makePipelineConfigFromSettings(int configId, PipelineConfig& config, const MeshcoreDemodSettings& settings) const; + MeshcoreDemodSettings makePipelineSettingsFromMeshRadio( + const MeshcoreDemodSettings& baseSettings, + const QString& presetName, + const modemmeshcore::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 MeshcoreDemodSettings& settings, bool force); + bool pipelineLayoutMatches(const std::vector& configs) const; + void syncPipelinesWithSettings(const MeshcoreDemodSettings& settings, bool force); + void applyExtraPipelineSettings(const QVector& settingsList, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const MeshcoreDemodSettings& settings, bool force); + void sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshcoreDemodSettings& settings, + bool force + ); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshcoreDemodSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); +}; + +#endif // INCLUDE_MESHCOREDEMOD_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodbaseband.cpp b/plugins/channelrx/demodmeshcore/meshcoredemodbaseband.cpp new file mode 100644 index 000000000..a6e5637c3 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodbaseband.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 "meshcoredemodbaseband.h" +#include "meshcoredemodmsg.h" + +MESSAGE_CLASS_DEFINITION(MeshcoreDemodBaseband::MsgConfigureMeshcoreDemodBaseband, Message) + +MeshcoreDemodBaseband::MeshcoreDemodBaseband() : + m_channelizer(&m_sink) +{ + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + + qDebug("MeshcoreDemodBaseband::MeshcoreDemodBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &MeshcoreDemodBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +MeshcoreDemodBaseband::~MeshcoreDemodBaseband() +{ +} + +void MeshcoreDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void MeshcoreDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void MeshcoreDemodBaseband::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 MeshcoreDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool MeshcoreDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshcoreDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMeshcoreDemodBaseband& cfg = (MsgConfigureMeshcoreDemodBaseband&) cmd; + qDebug() << "MeshcoreDemodBaseband::handleMessage: MsgConfigureMeshcoreDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "MeshcoreDemodBaseband::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(), + MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer.getChannelFrequencyOffset() + ); + + return true; + } + else if (MeshcoreDemodMsg::MsgLoRaHeaderFeedback::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MeshcoreDemodMsg::MsgLoRaHeaderFeedback& feedback = (MeshcoreDemodMsg::MsgLoRaHeaderFeedback&) cmd; + qDebug("MeshcoreDemodBaseband::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 MeshcoreDemodBaseband::applySettings(const MeshcoreDemodSettings& settings, bool force) +{ + if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) + || (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer.setChannelization( + MeshcoreDemodSettings::bandwidths[settings.m_bandwidthIndex]*MeshcoreDemodSettings::oversampling, + settings.m_inputFrequencyOffset + ); + m_sink.applyChannelSettings( + m_channelizer.getChannelSampleRate(), + MeshcoreDemodSettings::bandwidths[settings.m_bandwidthIndex], + m_channelizer.getChannelFrequencyOffset() + ); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int MeshcoreDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer.getChannelSampleRate(); +} + + +void MeshcoreDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer.setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings( + m_channelizer.getChannelSampleRate(), + MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer.getChannelFrequencyOffset() + ); +} diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodbaseband.h b/plugins/channelrx/demodmeshcore/meshcoredemodbaseband.h new file mode 100644 index 000000000..9baf715f0 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodbaseband.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_MESHCOREDEMODBASEBAND_H +#define INCLUDE_MESHCOREDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/downchannelizer.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "meshcoredemodsink.h" + +class MeshcoreDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureMeshcoreDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshcoreDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshcoreDemodBaseband* create(const MeshcoreDemodSettings& settings, bool force) + { + return new MsgConfigureMeshcoreDemodBaseband(settings, force); + } + + private: + MeshcoreDemodSettings m_settings; + bool m_force; + + MsgConfigureMeshcoreDemodBaseband(const MeshcoreDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + MeshcoreDemodBaseband(); + ~MeshcoreDemodBaseband(); + 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; + MeshcoreDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MeshcoreDemodSettings m_settings; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void applySettings(const MeshcoreDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_MESHCOREDEMODBASEBAND_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemoddecoder.cpp b/plugins/channelrx/demodmeshcore/meshcoredemoddecoder.cpp new file mode 100644 index 000000000..61af980e1 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemoddecoder.cpp @@ -0,0 +1,397 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoredemoddecoder.h" +#include "meshcoredemoddecoderlora.h" +#include "meshcoredemodmsg.h" + +MeshcoreDemodDecoder::MeshcoreDemodDecoder() : + m_codingScheme(MeshcoreDemodSettings::CodingLoRa), + 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) MeshcoreDemodSettings::ParityUndefined), + m_headerCRCStatus(false), + m_payloadParityStatus((int) MeshcoreDemodSettings::ParityUndefined), + m_payloadCRCStatus(false), + m_pipelineId(-1), + m_outputMessageQueue(nullptr), + m_headerFeedbackMessageQueue(nullptr) +{ + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +MeshcoreDemodDecoder::~MeshcoreDemodDecoder() +{} + +void MeshcoreDemodDecoder::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 MeshcoreDemodDecoder::decodeSymbols(const std::vector& symbols, QByteArray& bytes) +{ + if (m_nbSymbolBits >= 5) + { + unsigned int headerNbSymbolBits; + + if (m_hasHeader && (m_spreadFactor > 2U)) { + headerNbSymbolBits = m_spreadFactor - 2U; + } else { + headerNbSymbolBits = m_nbSymbolBits; + } + + MeshcoreDemodDecoderLoRa::decodeBytes( + bytes, + symbols, + m_nbSymbolBits, + headerNbSymbolBits, + m_hasHeader, + m_hasCRC, + m_nbParityBits, + m_packetLength, + m_earlyEOM, + m_headerParityStatus, + m_headerCRCStatus, + m_payloadParityStatus, + m_payloadCRCStatus + ); + + MeshcoreDemodDecoderLoRa::getCodingMetrics( + m_nbSymbolBits, + headerNbSymbolBits, + m_nbParityBits, + m_packetLength, + m_hasHeader, + m_hasCRC, + m_nbSymbols, + m_nbCodewords + ); + } +} + +bool MeshcoreDemodDecoder::handleMessage(const Message& cmd) +{ + if (MeshcoreDemodMsg::MsgLoRaHeaderProbe::match(cmd)) + { + MeshcoreDemodMsg::MsgLoRaHeaderProbe& msg = (MeshcoreDemodMsg::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) MeshcoreDemodSettings::ParityUndefined; + bool headerCRCStatus = false; + bool ldro = false; + unsigned int expectedSymbols = 0U; + bool valid = false; + + if (symbols.size() >= 8U && msg.getHasHeader()) + { + MeshcoreDemodDecoderLoRa::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) + { + MeshcoreDemodMsg::MsgLoRaHeaderFeedback *feedback = MeshcoreDemodMsg::MsgLoRaHeaderFeedback::create( + msg.getFrameId(), + valid, + hasCRC, + nbParityBits, + packetLength, + ldro, + expectedSymbols, + headerParityStatus, + headerCRCStatus + ); + m_headerFeedbackMessageQueue->push(feedback); + qDebug("MeshcoreDemodDecoder::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 (MeshcoreDemodMsg::MsgDecodeSymbols::match(cmd)) + { + qDebug("MeshcoreDemodDecoder::handleMessage: MsgDecodeSymbols"); + MeshcoreDemodMsg::MsgDecodeSymbols& msg = (MeshcoreDemodMsg::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); + + 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; + } + + MeshcoreDemodDecoderLoRa::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 + ); + + MeshcoreDemodDecoderLoRa::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( + "MeshcoreDemodDecoder::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( + "MeshcoreDemodDecoder::handleMessage: push report name=%s ts=%s bytes=%lld pCRC=%d", + qPrintable(m_pipelineName), + qPrintable(msgTimestamp), + static_cast(msgBytes.size()), + m_payloadCRCStatus ? 1 : 0 + ); + MeshcoreDemodMsg::MsgReportDecodeBytes *outputMsg = MeshcoreDemodMsg::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); + } + + return true; + } + + return false; +} + +void MeshcoreDemodDecoder::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} diff --git a/plugins/channelrx/demodmeshcore/meshcoredemoddecoder.h b/plugins/channelrx/demodmeshcore/meshcoredemoddecoder.h new file mode 100644 index 000000000..873811939 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemoddecoder.h @@ -0,0 +1,96 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MESHCOREDEMODDECODER_H +#define INCLUDE_MESHCOREDEMODDECODER_H + +#include + +#include + +#include "util/messagequeue.h" +#include "meshcoredemodsettings.h" + +class MeshcoreDemodDecoder : public QObject +{ + Q_OBJECT +public: + MeshcoreDemodDecoder(); + ~MeshcoreDemodDecoder(); + + void setCodingScheme(MeshcoreDemodSettings::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, QByteArray& bytes); //!< For raw bytes (original LoRa) + 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; } + + MeshcoreDemodSettings::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_MESHCOREDEMODDECODER_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.cpp b/plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.cpp new file mode 100644 index 000000000..49d97f9c5 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.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 "meshcoredemodsettings.h" +#include "meshcoredemoddecoderlora.h" + +void MeshcoreDemodDecoderLoRa::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) MeshcoreDemodSettings::ParityError; + } + else + { + if (error) { + headerParityStatus = (int) MeshcoreDemodSettings::ParityCorrected; + } else { + headerParityStatus = (int) MeshcoreDemodSettings::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 MeshcoreDemodDecoderLoRa::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("MeshcoreDemodDecoderLoRa::decodeBytes: need at least %u symbols for header", headerSymbols); + earlyEOM = true; + return; + } + else + { + earlyEOM = false; + } + + if (hasHeader) + { + if (headerNbSymbolBits < headerCodewords) + { + qDebug("MeshcoreDemodDecoderLoRa::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("MeshcoreDemodDecoderLoRa::decodeBytes: crc: %s nbParityBits: %u packetLength: %u payloadSFbits: %u headerSFbits: %u", + hasCRC ? "on": "off", nbParityBits, packetLength, payloadNbSymbolBits, headerNbSymbolBits); + + if (nbParityBits > 4) + { + qDebug("MeshcoreDemodDecoderLoRa::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("MeshcoreDemodDecoderLoRa::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) MeshcoreDemodSettings::ParityError; + } else if (error) { + payloadParityStatus = (int) MeshcoreDemodSettings::ParityCorrected; + } else { + payloadParityStatus = (int) MeshcoreDemodSettings::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 MeshcoreDemodDecoderLoRa::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) MeshcoreDemodSettings::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 MeshcoreDemodDecoderLoRa::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/demodmeshcore/meshcoredemoddecoderlora.h b/plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.h new file mode 100644 index 000000000..d5033c680 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemoddecoderlora.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_MESHCOREDEMODDECODERLORA_H +#define INCLUDE_MESHCOREDEMODDECODERLORA_H + +#include +#include +#include +#include + +class MeshcoreDemodDecoderLoRa +{ +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_MESHCOREDEMODDECODERLORA_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodgui.cpp b/plugins/channelrx/demodmeshcore/meshcoredemodgui.cpp new file mode 100644 index 000000000..a095c8dab --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodgui.cpp @@ -0,0 +1,3372 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ui_meshcoredemodgui.h" +#include "dsp/spectrumvis.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/devicesamplesource.h" +#include "dsp/devicesamplemimo.h" +#include "gui/glspectrum.h" +#include "gui/glspectrumgui.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/dialpopup.h" +#include "gui/dialogpositioner.h" +#include "plugin/pluginapi.h" +#include "channel/channelwebapiutils.h" +#include "util/db.h" +#include "maincore.h" + +#include + +#include "meshcoredemod.h" +#include "meshcoredemodmsg.h" +#include "meshcoredemodgui.h" +#include "meshcorekeysdialog.h" +#include "meshcorepacket.h" + +namespace +{ + static const int kMeshAutoLockCandidateTimeoutMs = 12000; + static const int kMeshAutoLockArmTimeoutMs = 12000; + static const int kMeshAutoLockMinObservationsPerCandidate = 3; + static const int kMeshAutoLockMinSourceObservationsPerCandidate = 6; + static const int kMeshAutoLockMinDecodeSamplesForApply = 3; + static const double kMeshAutoLockMinDecodeAverageForApply = 0.5; + static const double kMeshAutoLockActivityP2NThresholdDb = 4.0; + static const int kMeshAutoLockOffsetMultipliers[] = { + 0, -1, 1, -2, 2, -3, 3, -4, 4, -6, 6, -8, 8, -10, 10, -12, 12, -16, 16, -24, 24, -32, 32, -48, 48, -64, 64 + }; + static const int kTreeRawKeyRole = Qt::UserRole; + static const int kTreeDisplayLabelRole = Qt::UserRole + 1; + static const int kTreeRawValueRole = Qt::UserRole + 2; + static const int kTreeMessageKeyRole = Qt::UserRole + 3; + + void alignTextViewToLatestLineLeft(QPlainTextEdit *textView) + { + if (!textView) { + return; + } + + QScrollBar *verticalScroll = textView->verticalScrollBar(); + + if (verticalScroll) { + verticalScroll->setValue(verticalScroll->maximum()); + } + + QScrollBar *horizontalScroll = textView->horizontalScrollBar(); + + if (horizontalScroll) { + horizontalScroll->setValue(horizontalScroll->minimum()); + } + } + + void alignTreeViewToLatestEntryLeft(QTreeWidget *treeWidget, QTreeWidgetItem *item) + { + if (!treeWidget) { + return; + } + + if (item) { + treeWidget->scrollToItem(item); + } + + QScrollBar *verticalScroll = treeWidget->verticalScrollBar(); + + if (verticalScroll) { + verticalScroll->setValue(verticalScroll->maximum()); + } + + QScrollBar *horizontalScroll = treeWidget->horizontalScrollBar(); + + if (horizontalScroll) { + horizontalScroll->setValue(horizontalScroll->minimum()); + } + } +} + +MeshcoreDemodGUI* MeshcoreDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + MeshcoreDemodGUI* gui = new MeshcoreDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void MeshcoreDemodGUI::destroy() +{ + delete this; +} + +void MeshcoreDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray MeshcoreDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshcoreDemodGUI::deserialize(const QByteArray& data) +{ + resetLoRaStatus(); + + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool MeshcoreDemodGUI::handleMessage(const Message& message) +{ + if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_deviceCenterFrequency = notif.getCenterFrequency(); + int basebandSampleRate = notif.getSampleRate(); + qDebug() << "MeshcoreDemodGUI::handleMessage: DSPSignalNotification: m_basebandSampleRate: " << basebandSampleRate; + + if (basebandSampleRate != m_basebandSampleRate) + { + m_basebandSampleRate = basebandSampleRate; + setBandwidths(); + } + + ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); + ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2)); + updateAbsoluteCenterFrequency(); + + if (m_remoteTcpReconnectAutoApplyPending) + { + m_remoteTcpReconnectAutoApplyPending = false; + m_remoteTcpReconnectAutoApplyWaitTicks = 0; + qInfo() << "MeshcoreDemodGUI::handleMessage: DSPSignalNotification after RemoteTCP reconnect - reapplying Meshcore profile"; + QMetaObject::invokeMethod(this, &MeshcoreDemodGUI::applyMeshcoreProfileFromSelection, Qt::QueuedConnection); + } + + return true; + } + else if (MeshcoreDemodMsg::MsgReportDecodeBytes::match(message)) + { + // Populates the upper unstructured view including raw bytes in hex + const MeshcoreDemodMsg::MsgReportDecodeBytes& msg = (MeshcoreDemodMsg::MsgReportDecodeBytes&) message; + handleMeshAutoLockObservation(msg); + showLoRaMessage(message); + return true; + } + else if (MeshcoreDemodMsg::MsgReportDecodeString::match(message)) + { + // Populates the lower structured tree view with decoded fields + showTextMessage(message); + return true; + } + else if (MeshcoreDemod::MsgConfigureMeshcoreDemod::match(message)) + { + qDebug("MeshcoreDemodGUI::handleMessage: NFMDemod::MsgConfigureMeshcoreDemod"); + const MeshcoreDemod::MsgConfigureMeshcoreDemod& cfg = (MeshcoreDemod::MsgConfigureMeshcoreDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + ui->spectrumGUI->updateSettings(); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + + return true; + } + else + { + return false; + } +} + +void MeshcoreDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void MeshcoreDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void MeshcoreDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + focusedSettings().m_inputFrequencyOffset = value; + if (m_focusedPipelineIndex == 0) { + m_channelMarker.setCenterFrequency(value); + updateAbsoluteCenterFrequency(); + } + applyFocusedPipelineSettings(); +} + +void MeshcoreDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void MeshcoreDemodGUI::on_BW_valueChanged(int value) +{ + auto& s = focusedSettings(); + if (value < 0) { + s.m_bandwidthIndex = 0; + } else if (value < MeshcoreDemodSettings::nbBandwidths) { + s.m_bandwidthIndex = value; + } else { + s.m_bandwidthIndex = MeshcoreDemodSettings::nbBandwidths - 1; + } + + int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex]; + ui->BWText->setText(QString("%1 Hz").arg(thisBW)); + if (m_focusedPipelineIndex == 0) { + m_channelMarker.setBandwidth(thisBW); + ui->glSpectrum->setSampleRate(thisBW); + ui->glSpectrum->setCenterFrequency(thisBW/2); + } + + applyFocusedPipelineSettings(); +} + +void MeshcoreDemodGUI::on_Spread_valueChanged(int value) +{ + focusedSettings().m_spreadFactor = value; + ui->SpreadText->setText(tr("%1").arg(value)); + if (m_focusedPipelineIndex == 0) { + ui->spectrumGUI->setFFTSize(m_settings.m_spreadFactor); + } + + applyFocusedPipelineSettings(); +} + +void MeshcoreDemodGUI::on_deBits_valueChanged(int value) +{ + focusedSettings().m_deBits = value; + ui->deBitsText->setText(tr("%1").arg(value)); + applyFocusedPipelineSettings(); +} + +void MeshcoreDemodGUI::on_preambleChirps_valueChanged(int value) +{ + focusedSettings().m_preambleChirps = value; + ui->preambleChirpsText->setText(tr("%1").arg(value)); + applyFocusedPipelineSettings(); +} + +void MeshcoreDemodGUI::on_mute_toggled(bool checked) +{ + m_settings.m_decodeActive = !checked; + applySettings(); +} + +void MeshcoreDemodGUI::on_clear_clicked(bool checked) +{ + (void) checked; + clearPipelineViews(); + setDechirpInspectionMode(false); +} + +void MeshcoreDemodGUI::on_eomSquelch_valueChanged(int value) +{ + m_settings.m_eomSquelchTenths = value; + displaySquelch(); + applySettings(); +} + +void MeshcoreDemodGUI::on_messageLength_valueChanged(int value) +{ + m_settings.m_nbSymbolsMax = value; + ui->messageLengthText->setText(tr("%1").arg(m_settings.m_nbSymbolsMax)); + applySettings(); +} + +void MeshcoreDemodGUI::on_udpSend_stateChanged(int state) +{ + m_settings.m_sendViaUDP = (state == Qt::Checked); + applySettings(); +} + +void MeshcoreDemodGUI::on_udpSendJson_stateChanged(int state) +{ + m_settings.m_sendJsonViaUDP = (state == Qt::Checked); + applySettings(); +} + +void MeshcoreDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void MeshcoreDemodGUI::on_udpPort_editingFinished() +{ + bool ok; + quint16 udpPort = ui->udpPort->text().toInt(&ok); + + if((!ok) || (udpPort < 1024)) { + udpPort = 9998; + } + + m_settings.m_udpPort = udpPort; + ui->udpPort->setText(tr("%1").arg(m_settings.m_udpPort)); + applySettings(); +} + +void MeshcoreDemodGUI::on_invertRamps_stateChanged(int state) +{ + focusedSettings().m_invertRamps = (state == Qt::Checked); + applyFocusedPipelineSettings(); +} + +void MeshcoreDemodGUI::on_meshRegion_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + rebuildMeshcoreChannelOptions(); + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreDemodGUI::on_meshPreset_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + ui->meshRegion->setEnabled(index != ui->meshPreset->count() - 1); // USER preset has no region and is the last item + ui->BW->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined bandwidth and is the last item + ui->Spread->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined spread and is the last item + ui->deBits->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined deBits and is the last item + ui->preambleChirps->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined preambleChirps and is the last item + ui->deltaFrequency->setEnabled(index == ui->meshPreset->count() - 1); // USER preset has user-defined frequency offset and is the last item + + rebuildMeshcoreChannelOptions(); + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreDemodGUI::on_meshChannel_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreDemodGUI::on_meshApply_clicked(bool checked) +{ + (void) checked; + + if (m_meshControlsUpdating) { + return; + } + + // Rebuild first so region/preset changes refresh the valid channel list before applying. + rebuildMeshcoreChannelOptions(); + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreDemodGUI::on_meshKeys_clicked(bool checked) +{ + (void) checked; + editMeshcoreKeys(); +} + +void MeshcoreDemodGUI::on_meshAutoSampleRate_toggled(bool checked) +{ + if (m_meshControlsUpdating) { + return; + } + + m_settings.m_meshcoreAutoSampleRate = checked; + applySettings(); + + if (checked) { + applyMeshcoreProfileFromSelection(); + } else { + displayStatus(tr("MESH CFG|auto input tuning disabled")); + } +} + +void MeshcoreDemodGUI::on_meshAutoLock_clicked(bool checked) +{ + if (checked) { + startMeshAutoLock(); + } else { + stopMeshAutoLock(true); + } +} + +void MeshcoreDemodGUI::startMeshAutoLock() +{ + if (m_meshAutoLockActive) { + return; + } + + const int bandwidthHz = MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; + const int sf = std::max(1, m_settings.m_spreadFactor); + const int symbolBins = 1 << std::min(15, sf); + const int stepHz = std::max(100, bandwidthHz / symbolBins); + const bool invertOrder[] = {m_settings.m_invertRamps, !m_settings.m_invertRamps}; + + m_meshAutoLockCandidates.clear(); + m_meshAutoLockBaseOffsetHz = m_settings.m_inputFrequencyOffset; + m_meshAutoLockBaseInvert = m_settings.m_invertRamps; + m_meshAutoLockBaseDeBits = m_settings.m_deBits; + + QVector deCandidates; + deCandidates.push_back(m_settings.m_deBits); + + // SDR decode compatibility scan: + // for high SF profiles, also probe DE=0 and DE=2 even if the profile selects one of them. + if (sf >= 11) + { + if (std::find(deCandidates.begin(), deCandidates.end(), 0) == deCandidates.end()) { + deCandidates.push_back(0); + } + if (std::find(deCandidates.begin(), deCandidates.end(), 2) == deCandidates.end()) { + deCandidates.push_back(2); + } + } + + for (bool invert : invertOrder) + { + for (int multiplier : kMeshAutoLockOffsetMultipliers) + { + for (int deBits : deCandidates) + { + MeshAutoLockCandidate candidate; + candidate.inputOffsetHz = m_meshAutoLockBaseOffsetHz + multiplier * stepHz; + candidate.invertRamps = invert; + candidate.deBits = deBits; + candidate.score = 0.0; + candidate.samples = 0; + candidate.sourceScore = 0.0; + candidate.sourceSamples = 0; + candidate.syncWordZeroCount = 0; + candidate.headerParityOkOrFixCount = 0; + candidate.headerCRCCount = 0; + candidate.payloadCRCCount = 0; + candidate.earlyEOMCount = 0; + m_meshAutoLockCandidates.push_back(candidate); + } + } + } + + if (m_meshAutoLockCandidates.isEmpty()) + { + displayStatus(tr("MESH LOCK|no candidates generated")); + ui->meshAutoLock->blockSignals(true); + ui->meshAutoLock->setChecked(false); + ui->meshAutoLock->blockSignals(false); + return; + } + + m_meshAutoLockActive = true; + m_meshAutoLockCandidateIndex = 0; + m_meshAutoLockObservedSamplesForCandidate = 0; + m_meshAutoLockObservedSourceSamplesForCandidate = 0; + m_meshAutoLockTotalDecodeSamples = 0; + m_meshAutoLockTrafficSeen = false; + m_meshAutoLockActivityTicks = 0; + m_meshAutoLockArmStartMs = QDateTime::currentMSecsSinceEpoch(); + m_meshAutoLockCandidateStartMs = QDateTime::currentMSecsSinceEpoch(); + + ui->meshAutoLock->setText(tr("Locking...")); + + applyMeshAutoLockCandidate(m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex], true); + + QString deSummary; + for (int i = 0; i < deCandidates.size(); ++i) + { + if (!deSummary.isEmpty()) { + deSummary += "/"; + } + deSummary += QString::number(deCandidates[i]); + } + + displayStatus(tr("MESH LOCK|armed %1 candidates step=%2Hz de=%3. Waiting for on-air activity before scanning.") + .arg(m_meshAutoLockCandidates.size()) + .arg(stepHz) + .arg(deSummary)); +} + +void MeshcoreDemodGUI::stopMeshAutoLock(bool keepBestCandidate) +{ + if (!m_meshAutoLockActive && !ui->meshAutoLock->isChecked()) + { + return; + } + + int bestIndex = -1; + int bestFallbackIndex = -1; + double bestWeightedScore = -std::numeric_limits::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; + + ui->meshAutoLock->blockSignals(true); + ui->meshAutoLock->setChecked(false); + ui->meshAutoLock->setText(tr("Auto Lock")); + ui->meshAutoLock->blockSignals(false); + + if (keepBestCandidate && (bestIndex >= 0 || bestFallbackIndex >= 0)) + { + const bool provisional = bestIndex < 0; + const MeshAutoLockCandidate& best = m_meshAutoLockCandidates[provisional ? bestFallbackIndex : bestIndex]; + applyMeshAutoLockCandidate(best, true); + const double avgScore = best.samples > 0 ? (best.score / best.samples) : 0.0; + const double avgSourceScore = best.sourceSamples > 0 ? (best.sourceScore / best.sourceSamples) : 0.0; + const double syncRatio = best.samples > 0 ? (100.0 * best.syncWordZeroCount / best.samples) : 0.0; + displayStatus(tr("MESH LOCK|applied %1candidate df=%2Hz inv=%3 de=%4 decode=%5/%6 source=%7/%8 sync00=%9% hc=%10 crc=%11") + .arg(provisional ? "provisional " : "best ") + .arg(best.inputOffsetHz) + .arg(best.invertRamps ? "on" : "off") + .arg(best.deBits) + .arg(avgScore, 0, 'f', 2) + .arg(best.samples) + .arg(avgSourceScore, 0, 'f', 2) + .arg(best.sourceSamples) + .arg(syncRatio, 0, 'f', 1) + .arg(best.headerCRCCount) + .arg(best.payloadCRCCount)); + } + else + { + MeshAutoLockCandidate baseCandidate; + baseCandidate.inputOffsetHz = m_meshAutoLockBaseOffsetHz; + baseCandidate.invertRamps = m_meshAutoLockBaseInvert; + baseCandidate.deBits = m_meshAutoLockBaseDeBits; + baseCandidate.score = 0.0; + baseCandidate.samples = 0; + baseCandidate.sourceScore = 0.0; + baseCandidate.sourceSamples = 0; + baseCandidate.syncWordZeroCount = 0; + baseCandidate.headerParityOkOrFixCount = 0; + baseCandidate.headerCRCCount = 0; + baseCandidate.payloadCRCCount = 0; + baseCandidate.earlyEOMCount = 0; + applyMeshAutoLockCandidate(baseCandidate, true); + displayStatus(tr("MESH LOCK|stopped (no decode-backed lock). Baseline restored.")); + } +} + +void MeshcoreDemodGUI::applyMeshAutoLockCandidate(const MeshAutoLockCandidate& candidate, bool applySettingsNow) +{ + m_settings.m_inputFrequencyOffset = candidate.inputOffsetHz; + m_settings.m_invertRamps = candidate.invertRamps; + m_settings.m_deBits = candidate.deBits; + + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(candidate.inputOffsetHz); + m_channelMarker.blockSignals(false); + + ui->deltaFrequency->blockSignals(true); + ui->deltaFrequency->setValue(candidate.inputOffsetHz); + ui->deltaFrequency->blockSignals(false); + + ui->invertRamps->blockSignals(true); + ui->invertRamps->setChecked(candidate.invertRamps); + ui->invertRamps->blockSignals(false); + + ui->deBits->blockSignals(true); + ui->deBits->setValue(candidate.deBits); + ui->deBits->blockSignals(false); + ui->deBitsText->setText(tr("%1").arg(candidate.deBits)); + + updateAbsoluteCenterFrequency(); + + if (applySettingsNow) { + applySettings(); + } +} + +void MeshcoreDemodGUI::handleMeshAutoLockObservation(const MeshcoreDemodMsg::MsgReportDecodeBytes& msg) +{ + if (!m_meshAutoLockActive) { + return; + } + + if ((m_meshAutoLockCandidateIndex < 0) || (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size())) { + return; + } + + MeshAutoLockCandidate& candidate = m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex]; + const double snrDb = msg.getSingalDb() - msg.getNoiseDb(); + const double clippedSnr = std::max(-10.0, std::min(30.0, static_cast(snrDb))); + double score = clippedSnr * 0.2; + + const bool earlyEOM = msg.getEarlyEOM(); + if (earlyEOM) { + score -= 8.0; + } else { + score += 3.0; + } + + const int headerParityStatus = msg.getHeaderParityStatus(); + if (headerParityStatus == (int) MeshcoreDemodSettings::ParityOK) { + score += 8.0; + } else if (headerParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) { + score += 5.0; + } else if (headerParityStatus == (int) MeshcoreDemodSettings::ParityError) { + score -= 7.0; + } + + const bool headerCRCStatus = msg.getHeaderCRCStatus(); + if (headerCRCStatus) { + score += 10.0; + } else { + score -= 8.0; + } + + const int payloadParityStatus = msg.getPayloadParityStatus(); + const bool payloadCRCStatus = msg.getPayloadCRCStatus(); + + if (!earlyEOM) + { + if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityOK) { + score += 6.0; + } else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) { + score += 3.0; + } else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityError) { + score -= 4.0; + } + + if (payloadCRCStatus) { + score += 12.0; + } else { + score -= 4.0; + } + } + + const bool syncWordZero = msg.getSyncWord() == 0x00; + + candidate.score += score; + candidate.samples++; + candidate.syncWordZeroCount += syncWordZero ? 1 : 0; + candidate.headerParityOkOrFixCount += (headerParityStatus == (int) MeshcoreDemodSettings::ParityOK + || headerParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) ? 1 : 0; + candidate.headerCRCCount += headerCRCStatus ? 1 : 0; + candidate.payloadCRCCount += payloadCRCStatus ? 1 : 0; + candidate.earlyEOMCount += earlyEOM ? 1 : 0; + m_meshAutoLockObservedSamplesForCandidate++; + m_meshAutoLockTotalDecodeSamples++; + + if (!earlyEOM && headerCRCStatus && payloadCRCStatus) + { + displayStatus(tr("MESH LOCK|strong lock found (HF/HC/CRC good), finishing scan")); + stopMeshAutoLock(true); + } +} + +void MeshcoreDemodGUI::handleMeshAutoLockSourceObservation() +{ + if (!m_meshAutoLockActive) { + return; + } + + if ((m_meshAutoLockCandidateIndex < 0) || (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size())) { + return; + } + + MeshAutoLockCandidate& candidate = m_meshAutoLockCandidates[m_meshAutoLockCandidateIndex]; + const double totalPower = std::max(1e-12, m_meshcoreDemod->getTotalPower()); + const double noisePower = std::max(1e-12, m_meshcoreDemod->getCurrentNoiseLevel()); + const double totalDb = CalcDb::dbPower(totalPower); + const double noiseDb = CalcDb::dbPower(noisePower); + const double p2nDb = std::max(-20.0, std::min(40.0, totalDb - noiseDb)); + const bool demodActive = m_meshcoreDemod->getDemodActive(); + + if (!m_meshAutoLockTrafficSeen) + { + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + const bool sourceActive = p2nDb >= kMeshAutoLockActivityP2NThresholdDb; + + if (sourceActive) { + m_meshAutoLockActivityTicks++; + } + + if (demodActive) { + m_meshAutoLockActivityTicks++; + } + + if (!sourceActive && !demodActive && (m_meshAutoLockActivityTicks > 0)) { + m_meshAutoLockActivityTicks--; + } + + if (m_meshAutoLockActivityTicks >= 3) + { + m_meshAutoLockTrafficSeen = true; + m_meshAutoLockCandidateStartMs = nowMs; + m_meshAutoLockObservedSamplesForCandidate = 0; + m_meshAutoLockObservedSourceSamplesForCandidate = 0; + displayStatus(tr("MESH LOCK|traffic detected, starting scan")); + } + else if ((m_meshAutoLockArmStartMs > 0) && ((nowMs - m_meshAutoLockArmStartMs) >= kMeshAutoLockArmTimeoutMs)) + { + m_meshAutoLockTrafficSeen = true; + m_meshAutoLockCandidateStartMs = nowMs; + m_meshAutoLockObservedSamplesForCandidate = 0; + m_meshAutoLockObservedSourceSamplesForCandidate = 0; + displayStatus(tr("MESH LOCK|no clear activity detected. Starting scan anyway.")); + } + else + { + return; + } + } + + // Source-only quality proxy: + // - prefer sustained demod activity + // - prefer clearer power/noise separation + double sourceScore = (demodActive ? 0.7 : -0.1) + (p2nDb * 0.03); + if (p2nDb < 1.0) { + sourceScore -= 0.3; + } + + candidate.sourceScore += sourceScore; + candidate.sourceSamples++; + m_meshAutoLockObservedSourceSamplesForCandidate++; +} + +void MeshcoreDemodGUI::advanceMeshAutoLock() +{ + if (!m_meshAutoLockActive) { + return; + } + + if (!m_meshAutoLockTrafficSeen) { + return; + } + + if ((m_meshAutoLockCandidateIndex < 0) || (m_meshAutoLockCandidateIndex >= m_meshAutoLockCandidates.size())) + { + stopMeshAutoLock(true); + return; + } + + const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); + const bool enoughObservations = m_meshAutoLockObservedSamplesForCandidate >= kMeshAutoLockMinObservationsPerCandidate; + const bool timedOut = (nowMs - m_meshAutoLockCandidateStartMs) >= kMeshAutoLockCandidateTimeoutMs; + + if (m_meshAutoLockTotalDecodeSamples == 0) + { + int sourceCount = 0; + double minSourceAvg = std::numeric_limits::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 MeshcoreDemodGUI::editMeshcoreKeys() +{ + MeshcoreKeysDialog dialog(this); + dialog.setKeySpecList(m_settings.m_meshcoreKeySpecList); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + m_settings.m_meshcoreKeySpecList = dialog.getKeySpecList(); + + const bool hasCustomKeys = !m_settings.m_meshcoreKeySpecList.isEmpty(); + ui->meshKeys->setText(hasCustomKeys ? tr("Keys*") : tr("Keys...")); + ui->meshKeys->setToolTip(hasCustomKeys ? + tr("Custom Meshcore decode keys configured. Click to edit.") : + tr("Open Meshcore key manager.")); + + applySettings(); + + if (m_settings.m_meshcoreKeySpecList.isEmpty()) { + displayStatus(tr("MESH KEYS|using environment/default key set")); + } else { + displayStatus(tr("MESH KEYS|custom key set saved")); + } +} + +// ──────────────────────────────────────────────────────────────────────────── +// Multi-pipeline helpers +// ──────────────────────────────────────────────────────────────────────────── + +MeshcoreDemodSettings& MeshcoreDemodGUI::focusedSettings() +{ + if (m_focusedPipelineIndex > 0 && m_focusedPipelineIndex <= m_extraPipelineSettings.size()) { + return m_extraPipelineSettings[m_focusedPipelineIndex - 1]; + } + return m_settings; +} + +const MeshcoreDemodSettings& MeshcoreDemodGUI::focusedSettings() const +{ + if (m_focusedPipelineIndex > 0 && m_focusedPipelineIndex <= m_extraPipelineSettings.size()) { + return m_extraPipelineSettings[m_focusedPipelineIndex - 1]; + } + return m_settings; +} + +int MeshcoreDemodGUI::pipelineCount() const +{ + return 1 + m_extraPipelineSettings.size(); +} + +void MeshcoreDemodGUI::updateConfControls() +{ + const int count = pipelineCount(); + ui->conf->blockSignals(true); + ui->conf->setMaximum(std::max(0, count - 1)); + ui->conf->setValue(m_focusedPipelineIndex); + ui->conf->blockSignals(false); + ui->confId->setText(QString::number(m_focusedPipelineIndex)); + ui->confAdd->setEnabled(count < kMaxPipelines); + ui->confDel->setEnabled(m_focusedPipelineIndex > 0); // primary (0) cannot be deleted +} + +void MeshcoreDemodGUI::loadFocusedSettingsToControls() +{ + const auto& s = focusedSettings(); + const int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex]; + + blockApplySettings(true); + m_meshControlsUpdating = true; + + ui->deltaFrequency->setValue(s.m_inputFrequencyOffset); + ui->BW->setValue(s.m_bandwidthIndex); + ui->BWText->setText(QString("%1 Hz").arg(thisBW)); + ui->Spread->setValue(s.m_spreadFactor); + ui->SpreadText->setText(tr("%1").arg(s.m_spreadFactor)); + ui->deBits->setValue(s.m_deBits); + ui->deBitsText->setText(tr("%1").arg(s.m_deBits)); + ui->preambleChirps->setValue(s.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(s.m_preambleChirps)); + ui->invertRamps->setChecked(s.m_invertRamps); + + int regionIndex = ui->meshRegion->findText(s.m_meshcoreRegionCode); + if (regionIndex < 0) { regionIndex = ui->meshRegion->findText("EU_868"); } + if (regionIndex < 0) { regionIndex = 0; } + ui->meshRegion->setCurrentIndex(regionIndex); + + int presetIndex = ui->meshPreset->findText(s.m_meshcorePresetName); + if (presetIndex < 0) { presetIndex = ui->meshPreset->findText("EU_NARROW"); } + if (presetIndex < 0) { presetIndex = 0; } + ui->meshPreset->setCurrentIndex(presetIndex); + + m_meshControlsUpdating = false; + blockApplySettings(false); + + rebuildMeshcoreChannelOptions(); + + m_meshControlsUpdating = true; + int channelIndex = ui->meshChannel->findData(s.m_meshcoreChannelIndex); + if (channelIndex < 0) { channelIndex = 0; } + ui->meshChannel->setCurrentIndex(channelIndex); + m_meshControlsUpdating = false; + + updateControlAvailabilityHints(); +} + +void MeshcoreDemodGUI::applyFocusedPipelineSettings(bool force) +{ + if (m_focusedPipelineIndex == 0) { + applySettings(force); + } else { + pushExtraPipelineSettingsToDemod(); + } +} + +void MeshcoreDemodGUI::pushExtraPipelineSettingsToDemod() +{ + if (!m_doApplySettings) { + return; + } + MeshcoreDemod::MsgSetExtraPipelineSettings* msg = + MeshcoreDemod::MsgSetExtraPipelineSettings::create(m_extraPipelineSettings); + m_meshcoreDemod->getInputMessageQueue()->push(msg); +} + +// ──────────────────────────────────────────────────────────────────────────── +// Conf-dial / add / delete slot handlers +// ──────────────────────────────────────────────────────────────────────────── + +void MeshcoreDemodGUI::on_conf_valueChanged(int value) +{ + const auto maxFocus = static_cast(m_extraPipelineSettings.size()); + const int newFocus = std::clamp(value, 0, maxFocus); + + if (newFocus == m_focusedPipelineIndex) { + return; + } + m_focusedPipelineIndex = newFocus; + ui->confId->setText(QString::number(m_focusedPipelineIndex)); + ui->confDel->setEnabled(m_focusedPipelineIndex > 0); + loadFocusedSettingsToControls(); +} + +void MeshcoreDemodGUI::on_confAdd_clicked(bool checked) +{ + (void) checked; + if (pipelineCount() >= kMaxPipelines) { + return; + } + // Seed the new pipeline with a copy of the currently focused settings. + m_extraPipelineSettings.append(focusedSettings()); + m_focusedPipelineIndex = m_extraPipelineSettings.size(); // focus the new pipeline + updateConfControls(); + loadFocusedSettingsToControls(); + pushExtraPipelineSettingsToDemod(); + displayStatus(tr("CONF|pipeline %1 added (copy of CONF%2)") + .arg(m_focusedPipelineIndex) + .arg(m_focusedPipelineIndex - 1)); +} + +void MeshcoreDemodGUI::on_confDel_clicked(bool checked) +{ + (void) checked; + if (m_focusedPipelineIndex == 0 || m_extraPipelineSettings.isEmpty()) { + return; + } + const int removed = m_focusedPipelineIndex; + m_extraPipelineSettings.removeAt(m_focusedPipelineIndex - 1); + m_focusedPipelineIndex = std::max(0, m_focusedPipelineIndex - 1); + updateConfControls(); + loadFocusedSettingsToControls(); + pushExtraPipelineSettingsToDemod(); + displayStatus(tr("CONF|pipeline %1 removed").arg(removed)); +} + +int MeshcoreDemodGUI::findBandwidthIndex(int bandwidthHz) const +{ + int bestIndex = -1; + int bestDelta = 1 << 30; + + for (int i = 0; i < MeshcoreDemodSettings::nbBandwidths; ++i) + { + const int delta = std::abs(MeshcoreDemodSettings::bandwidths[i] - bandwidthHz); + if (delta < bestDelta) + { + bestDelta = delta; + bestIndex = i; + } + } + + return bestIndex; +} + +bool MeshcoreDemodGUI::retuneDeviceToFrequency(qint64 centerFrequencyHz) +{ + if (!m_deviceUISet || !m_deviceUISet->m_deviceAPI) { + return false; + } + + DeviceAPI* deviceAPI = m_deviceUISet->m_deviceAPI; + + if (deviceAPI->getDeviceSourceEngine() && deviceAPI->getSampleSource()) + { + deviceAPI->getSampleSource()->setCenterFrequency(centerFrequencyHz); + return true; + } + + if (deviceAPI->getDeviceMIMOEngine() && deviceAPI->getSampleMIMO()) + { + deviceAPI->getSampleMIMO()->setSourceCenterFrequency(centerFrequencyHz, m_settings.m_streamIndex); + return true; + } + + return false; +} + +bool MeshcoreDemodGUI::autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary, int* newBasebandSampleRateOut) +{ + summary.clear(); + + if (!m_meshcoreDemod) { + return false; + } + + const int deviceSetIndex = m_meshcoreDemod->getDeviceSetIndex(); + + if (deviceSetIndex < 0) { + return false; + } + + int devSampleRate = 0; + int log2Decim = 0; + QString sourceProtocol; + + if (!ChannelWebAPIUtils::getDevSampleRate(deviceSetIndex, devSampleRate) + || !ChannelWebAPIUtils::getSoftDecim(deviceSetIndex, log2Decim)) + { + summary = "auto sample-rate control: unsupported by source"; + return false; + } + + if (devSampleRate <= 0) { + summary = "auto sample-rate control: invalid device sample-rate"; + return false; + } + + if (log2Decim < 0) { + log2Decim = 0; + } + + const bool hasSourceProtocol = ChannelWebAPIUtils::getDeviceSetting(deviceSetIndex, "protocol", sourceProtocol); + const bool isSpyServerProtocol = hasSourceProtocol && (sourceProtocol.compare("Spy Server", Qt::CaseInsensitive) == 0); + const int initialDevSampleRate = devSampleRate; + const int initialLog2Decim = log2Decim; + const int minEffectiveRate = std::max(500000, bandwidthHz * 4); // Keep margin above 2*BW for robust LoRa decode. + + int newLog2Decim = log2Decim; + const int maxLog2Decim = 16; // Practical upper bound for software decimation controls. + + // If current decimation undershoots the required effective rate, lower it first. + while ((newLog2Decim > 0) && ((devSampleRate >> newLog2Decim) < minEffectiveRate)) { + newLog2Decim--; + } + + // Then push decimation as high as possible while keeping enough effective sample-rate. + while ((newLog2Decim < maxLog2Decim) && ((devSampleRate >> (newLog2Decim + 1)) >= minEffectiveRate)) { + newLog2Decim++; + } + + if (newLog2Decim != log2Decim) + { + if (!ChannelWebAPIUtils::setSoftDecim(deviceSetIndex, newLog2Decim)) { + newLog2Decim = log2Decim; + } + } + + if ((devSampleRate >> newLog2Decim) < minEffectiveRate && !isSpyServerProtocol) + { + const qint64 requiredDevRate = static_cast(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; + } + } + + if (newBasebandSampleRateOut) { + *newBasebandSampleRateOut = finalEffectiveRate; + } + + const bool belowTarget = finalEffectiveRate < minEffectiveRate; + const bool changed = (finalDevSampleRate != initialDevSampleRate) + || (finalLog2Decim != initialLog2Decim) + || channelSampleRateSynced + || channelDecimationDisabled + || dcBlockApplied + || iqCorrectionApplied + || agcApplied; + + summary = QString("effective sample-rate=%1Hz device sample-rate=%2Hz decimation=2^%3 required minimum=%4Hz%5") + .arg(finalEffectiveRate) + .arg(finalDevSampleRate) + .arg(finalLog2Decim) + .arg(minEffectiveRate) + .arg(belowTarget ? " (below target)" : ""); + + if (isSpyServerProtocol) { + summary += " source=SpyServer(fixed dev sample-rate)"; + } + if (channelDecimationDisabled || channelSampleRateSynced) { + summary += " channel sample-rate synced"; + } + summary += QString(" dcBlock=%1 iqCorrection=%2 agc=%3") + .arg(dcBlockSupported ? (dcBlockEnabled ? "on" : "off") : "n/a") + .arg(iqCorrectionSupported ? (iqCorrectionEnabled ? "on" : "off") : "n/a") + .arg(agcSupported ? (agcEnabled ? "on" : "off") : (isSpyServerProtocol ? "n/a(SpyServer)" : "n/a")); + + return changed; +} + +void MeshcoreDemodGUI::applyMeshcoreProfileFromSelection() +{ + const QString region = ui->meshRegion->currentText(); + const QString preset = ui->meshPreset->currentText(); + const int meshChannel = ui->meshChannel->currentData().toInt(); + const int channelNum = meshChannel + 1; // planner expects 1-based channel_num + + if (region.isEmpty() || preset.isEmpty()) { + return; + } + + auto& s = focusedSettings(); + const bool isPrimary = (m_focusedPipelineIndex == 0); + + // USER preset: all LoRa parameters and frequency are controlled manually from the GUI. + // Skip auto-configuration entirely; just persist the selection and optionally auto-tune sample rate. + if (preset == "USER") + { + bool selectionStateChanged = false; + + if (s.m_meshcoreRegionCode != region) + { + s.m_meshcoreRegionCode = region; + selectionStateChanged = true; + } + + if (s.m_meshcorePresetName != preset) + { + s.m_meshcorePresetName = preset; + selectionStateChanged = true; + } + + const int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex]; + QString sampleRateSummary; + bool sampleRateChanged = false; + int newBasebandSampleRate = 0; + + // Sample rate auto-tune is a device-level operation — primary pipeline only. + if (isPrimary && m_settings.m_meshcoreAutoSampleRate) { + sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary, &newBasebandSampleRate); + } + + if (isPrimary && sampleRateChanged && newBasebandSampleRate > m_basebandSampleRate) { + m_basebandSampleRate = newBasebandSampleRate; + setBandwidths(); + } + + if (selectionStateChanged || sampleRateChanged) { + applyFocusedPipelineSettings(); + } + + const QString statusMsg = tr("MESH CFG|%1USER preset: BW=%2 Hz SF=%3 DE=%4 preamble=%5%6") + .arg(isPrimary ? QString() : QString("CONF%1 ").arg(m_focusedPipelineIndex)) + .arg(thisBW) + .arg(s.m_spreadFactor) + .arg(s.m_deBits) + .arg(s.m_preambleChirps) + .arg(sampleRateSummary.isEmpty() ? QString() : " " + sampleRateSummary); + updateControlAvailabilityHints(); + displayStatus(statusMsg); + return; + } + + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum); + modemmeshcore::TxRadioSettings meshRadio; + QString error; + + if (!modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error)) + { + qWarning() << "MeshcoreDemodGUI::applyMeshcoreProfileFromSelection:" << error; + return; + } + + bool changed = false; + bool selectionStateChanged = false; + + if (s.m_meshcoreRegionCode != region) + { + s.m_meshcoreRegionCode = region; + selectionStateChanged = true; + } + if (s.m_meshcorePresetName != preset) + { + s.m_meshcorePresetName = preset; + selectionStateChanged = true; + } + if (s.m_meshcoreChannelIndex != meshChannel) + { + s.m_meshcoreChannelIndex = meshChannel; + selectionStateChanged = true; + } + + const int bwIndex = findBandwidthIndex(meshRadio.bandwidthHz); + if (bwIndex >= 0 && bwIndex != s.m_bandwidthIndex) + { + s.m_bandwidthIndex = bwIndex; + changed = true; + } + + if (meshRadio.spreadFactor > 0 && meshRadio.spreadFactor != s.m_spreadFactor) + { + s.m_spreadFactor = meshRadio.spreadFactor; + changed = true; + } + + if (meshRadio.deBits != s.m_deBits) + { + s.m_deBits = meshRadio.deBits; + changed = true; + } + + if (meshRadio.parityBits > 0 && meshRadio.parityBits != s.m_nbParityBits) + { + s.m_nbParityBits = meshRadio.parityBits; + changed = true; + } + + const int meshPreambleChirps = meshRadio.preambleChirps; + if (s.m_preambleChirps != static_cast(meshPreambleChirps)) + { + s.m_preambleChirps = static_cast(meshPreambleChirps); + changed = true; + } + + if (meshRadio.hasCenterFrequency) + { + if (isPrimary) + { + // Primary pipeline: try to retune the actual SDR device. + if (retuneDeviceToFrequency(meshRadio.centerFrequencyHz)) + { + m_deviceCenterFrequency = meshRadio.centerFrequencyHz; + if (s.m_inputFrequencyOffset != 0) + { + s.m_inputFrequencyOffset = 0; + changed = true; + } + } + else if (m_deviceCenterFrequency != 0) + { + const qint64 wantedOffset = meshRadio.centerFrequencyHz - m_deviceCenterFrequency; + if (wantedOffset != s.m_inputFrequencyOffset) + { + s.m_inputFrequencyOffset = static_cast(wantedOffset); + changed = true; + } + } + else + { + qWarning() << "MeshcoreDemodGUI::applyMeshcoreProfileFromSelection: cannot retune device and device center frequency unknown"; + } + } + else + { + // Secondary pipeline: device frequency is owned by the primary; compute the input + // frequency offset relative to the current device center frequency. + if (m_deviceCenterFrequency != 0) + { + const qint64 wantedOffset = meshRadio.centerFrequencyHz - m_deviceCenterFrequency; + if (wantedOffset != s.m_inputFrequencyOffset) + { + s.m_inputFrequencyOffset = static_cast(wantedOffset); + changed = true; + } + } + } + } + + const int thisBW = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex]; + QString sampleRateSummary; + bool sampleRateChanged = false; + + int newBasebandSampleRate = 0; + // Sample rate auto-tune is a device-level operation — primary pipeline only. + if (isPrimary) + { + if (m_settings.m_meshcoreAutoSampleRate) { + sampleRateChanged = autoTuneDeviceSampleRateForBandwidth(thisBW, sampleRateSummary, &newBasebandSampleRate); + } else { + sampleRateSummary = "auto sample-rate control: disabled"; + } + + // If the device sample rate was just raised, update m_basebandSampleRate immediately + // so that setBandwidths() can widen the BW slider maximum before we write to it. + if (sampleRateChanged && newBasebandSampleRate > m_basebandSampleRate) { + m_basebandSampleRate = newBasebandSampleRate; + setBandwidths(); + } + } + + if (!changed && !sampleRateChanged && !selectionStateChanged) { + return; + } + + qInfo() << "MeshcoreDemodGUI::applyMeshcoreProfileFromSelection:" + << (isPrimary ? "primary" : QString("CONF%1").arg(m_focusedPipelineIndex)) + << meshRadio.summary + << sampleRateSummary; + + QString status = tr("MESH CFG|%1region=%2 preset=%3 ch=%4 %5") + .arg(isPrimary ? QString() : QString("CONF%1 ").arg(m_focusedPipelineIndex)) + .arg(region) + .arg(preset) + .arg(meshChannel) + .arg(meshRadio.summary); + + status += QString(" preamble=%1").arg(meshPreambleChirps); + + if (!sampleRateSummary.isEmpty()) { + status += " " + sampleRateSummary; + } + + if (!changed) + { + applyFocusedPipelineSettings(); + displayStatus(status); + return; + } + + // Update channel marker (always primary; it represents the channel on the device display). + if (isPrimary) + { + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(s.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBW); + m_channelMarker.blockSignals(false); + } + + blockApplySettings(true); + ui->deltaFrequency->setValue(s.m_inputFrequencyOffset); + ui->BW->setValue(s.m_bandwidthIndex); + ui->BWText->setText(QString("%1 Hz").arg(thisBW)); + ui->Spread->setValue(s.m_spreadFactor); + ui->SpreadText->setText(tr("%1").arg(s.m_spreadFactor)); + ui->deBits->setValue(s.m_deBits); + ui->deBitsText->setText(tr("%1").arg(s.m_deBits)); + ui->preambleChirps->setValue(s.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(s.m_preambleChirps)); + ui->fecParity->setValue(s.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(s.m_nbParityBits)); + blockApplySettings(false); + updateControlAvailabilityHints(); + + if (isPrimary) + { + ui->glSpectrum->setSampleRate(thisBW); + ui->glSpectrum->setCenterFrequency(thisBW/2); + updateAbsoluteCenterFrequency(); + } + + applyFocusedPipelineSettings(); + displayStatus(status); +} + +void MeshcoreDemodGUI::setupMeshcoreAutoProfileControls() +{ + // Hide Region and Channel controls — region is implicit in each + // preset's frequency, and group channels are managed via the keys + // dialog rather than a numbered Channel selector. Preset combo + + // Apply button stay visible (see + // modemmeshcore::command::applyMeshcorePreset). + if (ui->meshRegionLabel) ui->meshRegionLabel->hide(); + if (ui->meshRegion) ui->meshRegion->hide(); + if (ui->meshChannelLabel) ui->meshChannelLabel->hide(); + if (ui->meshChannel) ui->meshChannel->hide(); + + // Keep the still-applicable controls (meshKeys, meshAutoLock, + // meshAutoSampleRate) — they govern the LoRa PHY auto-tune + the + // operator's identity/contact/channel store, both of which apply to + // MeshCore unchanged. + ui->meshAutoSampleRate->setChecked(m_settings.m_meshcoreAutoSampleRate); + + QObject::connect(ui->meshKeys, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_meshKeys_clicked); + QObject::connect(ui->meshAutoLock, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_meshAutoLock_clicked); + QObject::connect(ui->meshAutoSampleRate, &QCheckBox::toggled, this, &MeshcoreDemodGUI::on_meshAutoSampleRate_toggled); +} + +void MeshcoreDemodGUI::rebuildMeshcoreChannelOptions() +{ + const QString region = ui->meshRegion->currentText(); + const QString preset = ui->meshPreset->currentText(); + const int previousChannel = ui->meshChannel->currentData().toInt(); + + m_meshControlsUpdating = true; + ui->meshChannel->clear(); + + // USER preset: channel selection is not applicable — the user sets all parameters manually + if (preset == "USER") + { + ui->meshChannel->addItem(tr("(user-defined)"), 0); + ui->meshChannel->setEnabled(false); + ui->meshChannel->setToolTip(tr("Not applicable in USER preset. All LoRa parameters and frequency are set manually.")); + m_meshControlsUpdating = false; + // Do NOT queue applyMeshcoreProfileFromSelection here: this function is + // called from displaySettings() on every demod echo-back, and queueing an + // apply would create an infinite loop (apply → demod → echo → displaySettings + // → rebuildMeshcoreChannelOptions → apply → …). + // Initial and explicit applies happen via the constructor's queued call and + // direct user actions (Apply button, region/preset combo changes). + return; + } + + ui->meshChannel->setEnabled(true); + ui->meshChannel->setToolTip(tr("Meshcore channel number (zero-based, shown with center frequency)")); + + int added = 0; + for (int meshChannel = 0; meshChannel <= 200; ++meshChannel) + { + modemmeshcore::TxRadioSettings meshRadio; + QString error; + const int channelNum = meshChannel + 1; // planner expects 1-based channel_num + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum); + + if (!modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error)) + { + if (added > 0) { + break; + } else { + continue; + } + } + + const QString label = meshRadio.hasCenterFrequency + ? QString("%1 (%2 MHz)").arg(meshChannel).arg(meshRadio.centerFrequencyHz / 1000000.0, 0, 'f', 3) + : QString::number(meshChannel); + + ui->meshChannel->addItem(label, meshChannel); + added++; + } + + if (added == 0) { + ui->meshChannel->addItem("0", 0); + } + + ui->meshChannel->setToolTip(tr("Meshcore channel number (%1 available for %2/%3)") + .arg(added) + .arg(region) + .arg(preset)); + int restoreIndex = ui->meshChannel->findData(previousChannel); + if (restoreIndex < 0) { + restoreIndex = 0; + } + ui->meshChannel->setCurrentIndex(restoreIndex); + m_meshControlsUpdating = false; + + qInfo() << "MeshcoreDemodGUI::rebuildMeshcoreChannelOptions:" + << "region=" << region + << "preset=" << preset + << "channels=" << added; +} + +void MeshcoreDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void MeshcoreDemodGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.setDefaultTitle(m_displayedName); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + dialog.setNumberOfStreams(m_meshcoreDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + } + + dialog.move(p); + new DialogPositioner(&dialog, false); + dialog.exec(); + + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitle(m_channelMarker.getTitle()); + setTitleColor(m_settings.m_rgbColor); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + updateIndexLabel(); + } + + applySettings(); + } + + resetContextMenuType(); +} + +MeshcoreDemodGUI::MeshcoreDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::MeshcoreDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_basebandSampleRate(250000), + m_doApplySettings(true), + m_pipelineTabs(nullptr), + m_meshControlsUpdating(false), + m_meshAutoLockActive(false), + m_meshAutoLockCandidateIndex(0), + m_meshAutoLockCandidateStartMs(0), + m_meshAutoLockObservedSamplesForCandidate(0), + m_meshAutoLockObservedSourceSamplesForCandidate(0), + m_meshAutoLockTotalDecodeSamples(0), + m_meshAutoLockTrafficSeen(false), + m_meshAutoLockActivityTicks(0), + m_meshAutoLockArmStartMs(0), + m_meshAutoLockBaseOffsetHz(0), + m_meshAutoLockBaseInvert(false), + m_meshAutoLockBaseDeBits(0), + m_remoteTcpReconnectAutoApplyPending(false), + m_remoteTcpReconnectAutoApplyWaitTicks(0), + m_remoteTcpLastRunningState(false), + m_dechirpInspectionActive(false), + m_replayPendingHasSelection(false), + m_replaySelectionQueued(false), + m_pipelineMessageSequence(0), + m_focusedPipelineIndex(0), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demodmeshcore/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setupMeshcoreAutoProfileControls(); + QObject::connect(ui->conf, QOverload::of(&QDial::valueChanged), this, &MeshcoreDemodGUI::on_conf_valueChanged); + QObject::connect(ui->confAdd, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_confAdd_clicked); + QObject::connect(ui->confDel, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_confDel_clicked); + updateConfControls(); + setupPipelineViews(); + QObject::connect(ui->dechirpLiveFollow, &QPushButton::clicked, this, [this](bool) { + setDechirpInspectionMode(false); + }); + updateDechirpModeUI(); + // Mark major sections as vertically expanding so RollupContents does not clamp max height. + ui->verticalLayoutWidget_2->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + ui->spectrumContainer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + if (m_pipelineTabs) { + m_pipelineTabs->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } + setSizePolicy(rollupContents->sizePolicy()); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_meshcoreDemod = (MeshcoreDemod*) rxChannel; + m_spectrumVis = m_meshcoreDemod->getSpectrumVis(); + m_spectrumVis->setGLSpectrum(ui->glSpectrum); + m_meshcoreDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + SpectrumSettings spectrumSettings = m_spectrumVis->getSettings(); + // Meshcore dechirp view defaults: keep lower pane active so replay-on-click + // always has a visible target. + spectrumSettings.m_displayWaterfall = true; + spectrumSettings.m_display3DSpectrogram = false; + spectrumSettings.m_displayCurrent = true; + spectrumSettings.m_displayHistogram = false; + spectrumSettings.m_displayMaxHold = false; + spectrumSettings.m_averagingMode = SpectrumSettings::AvgModeNone; + spectrumSettings.m_refLevel = -10.0f; + spectrumSettings.m_powerRange = 45.0f; + SpectrumVis::MsgConfigureSpectrumVis *msg = SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false); + m_spectrumVis->getInputMessageQueue()->push(msg); + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->deltaFrequency->setToolTip(tr("Offset from device center frequency (Hz).")); + ui->deltaFrequencyLabel->setToolTip(tr("Frequency offset control for the demodulator channel.")); + ui->deltaUnits->setToolTip(tr("Frequency unit for the offset control.")); + ui->BW->setToolTip(tr("LoRa bandwidth selection. Meshcore presets auto-set this.")); + ui->bwLabel->setToolTip(tr("LoRa bandwidth selector.")); + ui->BWText->setToolTip(tr("Current LoRa bandwidth in Hz.")); + ui->Spread->setToolTip(tr("LoRa spreading factor (SF). Higher SF increases range but lowers rate.")); + ui->spreadLabel->setToolTip(tr("LoRa spreading factor selector.")); + ui->SpreadText->setToolTip(tr("Current spreading factor value.")); + ui->deBits->setToolTip(tr("Low data-rate optimization bits (DE).")); + ui->deBitsLabel->setToolTip(tr("Low data-rate optimization setting.")); + ui->deBitsText->setToolTip(tr("Current low data-rate optimization value.")); + ui->preambleChirps->setToolTip(tr("LoRa preamble chirps. MeshCore: 32 for SF<9, 16 for SF>8 (SF-based auto-set on preset).")); + ui->preambleChirpsLabel->setToolTip(tr("Expected LoRa preamble length in chirps.")); + ui->preambleChirpsText->setToolTip(tr("Current preamble chirp value.")); + ui->mute->setToolTip(tr("Disable decoder output.")); + ui->clear->setToolTip(tr("Clear decoded message log.")); + ui->eomSquelch->setToolTip(tr("End-of-message squelch threshold.")); + ui->eomSquelchLabel->setToolTip(tr("End-of-message squelch level.")); + ui->eomSquelchText->setToolTip(tr("Current end-of-message squelch value.")); + ui->messageLength->setToolTip(tr("Maximum payload symbol length when auto is disabled.")); + ui->messageLengthLabel->setToolTip(tr("Maximum payload symbol length.")); + ui->messageLengthText->setToolTip(tr("Current payload symbol length setting.")); + ui->fecParity->setToolTip(tr("LoRa coding rate parity denominator (CR).")); + ui->fecParityLabel->setToolTip(tr("LoRa coding rate parity setting.")); + ui->fecParityText->setToolTip(tr("Current coding rate parity value.")); + ui->packetLength->setToolTip(tr("Fixed packet length for implicit-header mode.")); + ui->packetLengthLabel->setToolTip(tr("Fixed packet length for implicit header mode.")); + ui->packetLengthText->setToolTip(tr("Current fixed packet length.")); + ui->invertRamps->setToolTip(tr("Invert chirp ramp direction. Disabled")); + ui->invertRamps->setEnabled(false); + ui->messageLabel->setToolTip(tr("Decoded output area.")); + ui->udpSend->setToolTip(tr("Forward decoded payload bytes to UDP.")); + ui->udpAddress->setToolTip(tr("Destination UDP address for forwarded payloads.")); + ui->udpPort->setToolTip(tr("Destination UDP port for forwarded payloads.")); + ui->udpSeparator->setToolTip(tr("UDP forwarding controls.")); + ui->glSpectrum->setToolTip(tr("De-chirped spectrum view of the selected LoRa channel.")); + ui->spectrumGUI->setToolTip(tr("Spectrum and waterfall display controls.")); + ui->headerHammingStatus->setToolTip(tr("Header FEC status indicator.")); + ui->headerCRCStatus->setToolTip(tr("Header CRC status indicator.")); + ui->payloadFECStatus->setToolTip(tr("Payload FEC status indicator.")); + ui->payloadCRCStatus->setToolTip(tr("Payload CRC status indicator.")); + ui->channelPower->setToolTip(tr("Estimated channel power.")); + ui->nLabel->setToolTip(tr("Estimated symbol count.")); + ui->nText->setToolTip(tr("Current estimated symbol count.")); + ui->nbSymbolsText->setToolTip(tr("Current raw LoRa symbol counter.")); + ui->nbCodewordsText->setToolTip(tr("Current raw LoRa codeword counter.")); + ui->sLabel->setToolTip(tr("Estimated codeword count.")); + ui->sText->setToolTip(tr("Current estimated codeword count.")); + ui->snrLabel->setToolTip(tr("Estimated signal-to-noise ratio.")); + ui->snrText->setToolTip(tr("Current estimated SNR.")); + ui->sUnits->setToolTip(tr("Unit for SNR.")); + ui->symbolsCodewordsSeparator->setToolTip(tr("Separator between symbol and codeword counters.")); + + m_channelMarker.setMovable(true); + m_channelMarker.setVisible(true); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + ui->spectrumGUI->setBuddies(m_spectrumVis, ui->glSpectrum); + + m_settings.setChannelMarker(&m_channelMarker); + m_settings.setSpectrumGUI(ui->spectrumGUI); + m_settings.setRollupState(&m_rollupState); + + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + setBandwidths(); + displaySettings(); + makeUIConnections(); + resetLoRaStatus(); + applySettings(true); + // On first creation, combo signals haven't fired yet. Apply selected Meshcore profile once. + // Use a queued connection so this runs after SDRangel calls deserialize() on the newly created + // object — ensuring the saved preset/settings are in effect before any device retuning occurs. + QMetaObject::invokeMethod(this, &MeshcoreDemodGUI::applyMeshcoreProfileFromSelection, Qt::QueuedConnection); + DialPopup::addPopupsToChildDials(this); + m_resizer.enableChildMouseTracking(); +} + +MeshcoreDemodGUI::~MeshcoreDemodGUI() +{ + delete ui; +} + +void MeshcoreDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void MeshcoreDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + setTitleColor(m_channelMarker.getColor()); + MeshcoreDemod::MsgConfigureMeshcoreDemod* message = MeshcoreDemod::MsgConfigureMeshcoreDemod::create( m_settings, force); + m_meshcoreDemod->getInputMessageQueue()->push(message); + } +} + +void MeshcoreDemodGUI::updateControlAvailabilityHints() +{ + const bool loRaMode = m_settings.m_codingScheme == MeshcoreDemodSettings::CodingLoRa; + const bool explicitHeaderMode = loRaMode && MeshcoreDemodSettings::m_hasHeader; + + const QString fftWindowEnabledTip = tr("FFT window used by the de-chirping stage."); + const QString fftWindowDisabledTip = tr("Ignored in LoRa mode. The LoRa demodulator uses a fixed internal FFT window."); + + const QString messageLengthAutoEnabledTip = tr("Auto-detect payload symbol length from headers."); + const QString messageLengthAutoDisabledTip = tr("Disabled in LoRa explicit-header mode. Payload length is decoded from the LoRa header."); + + const QString messageLengthDefaultTip = tr("Maximum payload symbol length when auto is disabled."); + const QString messageLengthHeaderTip = tr("Maximum payload symbol clamp in LoRa explicit-header mode. Header still provides nominal payload length."); + const QString messageLengthTip = explicitHeaderMode ? messageLengthHeaderTip : messageLengthDefaultTip; + ui->messageLength->setToolTip(messageLengthTip); + ui->messageLengthLabel->setToolTip(messageLengthTip); + ui->messageLengthText->setToolTip(messageLengthTip); + + const bool isUserPreset = focusedSettings().m_meshcorePresetName.trimmed().compare("USER", Qt::CaseInsensitive) == 0; + ui->meshRegion->setEnabled(!isUserPreset); + ui->BW->setEnabled(isUserPreset); + ui->Spread->setEnabled(isUserPreset); + ui->deBits->setEnabled(isUserPreset); + ui->preambleChirps->setEnabled(isUserPreset); + ui->deltaFrequency->setEnabled(isUserPreset); + + // Apply an opacity effect to give a clear greyed-out appearance when disabled, + // because the platform or dark-theme style may not provide enough visual contrast. + auto setSliderDimmed = [](QSlider* slider, bool dimmed) { + if (dimmed) { + if (!qobject_cast(slider->graphicsEffect())) { + auto* effect = new QGraphicsOpacityEffect(slider); + effect->setOpacity(0.35); + slider->setGraphicsEffect(effect); + } + } else { + slider->setGraphicsEffect(nullptr); + } + }; + setSliderDimmed(ui->BW, !isUserPreset); + setSliderDimmed(ui->Spread, !isUserPreset); + setSliderDimmed(ui->deBits, !isUserPreset); + setSliderDimmed(ui->preambleChirps, !isUserPreset); + + const bool headerControlsEnabled = !MeshcoreDemodSettings::m_hasHeader; + ui->fecParity->setEnabled(headerControlsEnabled); + ui->packetLength->setEnabled(headerControlsEnabled); + + const QString fecParityEnabledTip = tr("LoRa coding rate parity denominator (CR)."); + const QString fecParityDisabledTip = tr("Disabled in explicit-header mode. Coding rate is decoded from the LoRa header."); + const QString fecParityTip = headerControlsEnabled ? fecParityEnabledTip : fecParityDisabledTip; + ui->fecParity->setToolTip(fecParityTip); + ui->fecParityLabel->setToolTip(fecParityTip); + ui->fecParityText->setToolTip(fecParityTip); + + const QString crcEnabledTip = tr("Expect payload CRC."); + const QString crcDisabledTip = tr("Disabled in explicit-header mode. CRC expectation is decoded from the LoRa header."); + + const QString packetLengthEnabledTip = tr("Fixed packet length for implicit-header mode."); + const QString packetLengthDisabledTip = tr("Disabled in explicit-header mode. Payload length is decoded from the LoRa header."); + const QString packetLengthTip = headerControlsEnabled ? packetLengthEnabledTip : packetLengthDisabledTip; + ui->packetLength->setToolTip(packetLengthTip); + ui->packetLengthLabel->setToolTip(packetLengthTip); + ui->packetLengthText->setToolTip(packetLengthTip); +} + +void MeshcoreDemodGUI::displaySettings() +{ + const auto& s = focusedSettings(); + // Primary pipeline bandwidth for channel marker and spectrum display. + const int thisBWPrimary = MeshcoreDemodSettings::bandwidths[m_settings.m_bandwidthIndex]; + // Focused pipeline bandwidth for the BW slider and its label. + const int thisBWFocused = MeshcoreDemodSettings::bandwidths[s.m_bandwidthIndex]; + + // Channel marker always reflects the primary pipeline. + m_channelMarker.blockSignals(true); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(thisBWPrimary); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); + setTitleColor(m_settings.m_rgbColor); + setTitle(m_channelMarker.getTitle()); + + ui->glSpectrum->setSampleRate(thisBWPrimary); + ui->glSpectrum->setCenterFrequency(thisBWPrimary/2); + + blockApplySettings(true); + // LoRa controls reflect the focused pipeline. + ui->deltaFrequency->setValue(s.m_inputFrequencyOffset); + ui->BWText->setText(QString("%1 Hz").arg(thisBWFocused)); + ui->BW->setValue(s.m_bandwidthIndex); + ui->Spread->setValue(s.m_spreadFactor); + ui->SpreadText->setText(tr("%1").arg(s.m_spreadFactor)); + ui->deBits->setValue(s.m_deBits); + ui->deBitsText->setText(tr("%1").arg(s.m_deBits)); + ui->preambleChirps->setValue(s.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(s.m_preambleChirps)); + // Global settings always from primary. + ui->messageLengthText->setText(tr("%1").arg(m_settings.m_nbSymbolsMax)); + ui->messageLength->setValue(m_settings.m_nbSymbolsMax); + ui->udpSend->setChecked(m_settings.m_sendViaUDP); + ui->udpSendJson->setChecked(m_settings.m_sendJsonViaUDP); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(tr("%1").arg(m_settings.m_udpPort)); + ui->invertRamps->setChecked(s.m_invertRamps); + + displaySquelch(); + updateIndexLabel(); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + + const bool hasCustomKeys = !m_settings.m_meshcoreKeySpecList.trimmed().isEmpty(); + ui->meshKeys->setText(hasCustomKeys ? tr("Keys*") : tr("Keys...")); + ui->meshKeys->setToolTip(hasCustomKeys ? + tr("Custom Meshcore decode keys configured. Click to edit.") : + tr("Open Meshcore key manager.")); + + m_meshControlsUpdating = true; + ui->meshAutoSampleRate->setChecked(m_settings.m_meshcoreAutoSampleRate); + m_meshControlsUpdating = false; + + ui->meshAutoLock->blockSignals(true); + ui->meshAutoLock->setChecked(m_meshAutoLockActive); + ui->meshAutoLock->setText(m_meshAutoLockActive ? tr("Locking...") : tr("Auto Lock")); + ui->meshAutoLock->blockSignals(false); + + m_meshControlsUpdating = true; + // Mesh region/preset/channel reflect the focused pipeline's selection. + int regionIndex = ui->meshRegion->findText(s.m_meshcoreRegionCode); + if (regionIndex < 0) { + regionIndex = ui->meshRegion->findText("EU_868"); + } + if (regionIndex < 0) { + regionIndex = 0; + } + ui->meshRegion->setCurrentIndex(regionIndex); + + ui->meshRegion->setEnabled(s.m_meshcorePresetName != "USER"); + ui->BW->setEnabled(s.m_meshcorePresetName == "USER"); + ui->Spread->setEnabled(s.m_meshcorePresetName == "USER"); + ui->deBits->setEnabled(s.m_meshcorePresetName == "USER"); + ui->preambleChirps->setEnabled(s.m_meshcorePresetName == "USER"); + + int presetIndex = ui->meshPreset->findText(s.m_meshcorePresetName); + if (presetIndex < 0) { + presetIndex = ui->meshPreset->findText("EU_NARROW"); + } + if (presetIndex < 0) { + presetIndex = 0; + } + ui->meshPreset->setCurrentIndex(presetIndex); + m_meshControlsUpdating = false; + + rebuildMeshcoreChannelOptions(); + + m_meshControlsUpdating = true; + int channelIndex = ui->meshChannel->findData(s.m_meshcoreChannelIndex); + if (channelIndex < 0) { + channelIndex = 0; + } + ui->meshChannel->setCurrentIndex(channelIndex); + m_meshControlsUpdating = false; + + updateControlAvailabilityHints(); + updateConfControls(); + blockApplySettings(false); +} + +void MeshcoreDemodGUI::displaySquelch() +{ + ui->eomSquelch->setValue(m_settings.m_eomSquelchTenths); + + if (m_settings.m_eomSquelchTenths == ui->eomSquelch->maximum()) { + ui->eomSquelchText->setText("---"); + } else { + ui->eomSquelchText->setText(tr("%1").arg(m_settings.m_eomSquelchTenths / 10.0, 0, 'f', 1)); + } +} + +void MeshcoreDemodGUI::displayLoRaStatus(int headerParityStatus, bool headerCRCStatus, int payloadParityStatus, bool payloadCRCStatus) +{ + if (MeshcoreDemodSettings::m_hasHeader && (headerParityStatus == (int) MeshcoreDemodSettings::ParityOK)) { + ui->headerHammingStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (MeshcoreDemodSettings::m_hasHeader && (headerParityStatus == (int) MeshcoreDemodSettings::ParityError)) { + ui->headerHammingStatus->setStyleSheet("QLabel { background-color : red; }"); + } else if (MeshcoreDemodSettings::m_hasHeader && (headerParityStatus == (int) MeshcoreDemodSettings::ParityCorrected)) { + ui->headerHammingStatus->setStyleSheet("QLabel { background-color : blue; }"); + } else { + ui->headerHammingStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + } + + if (MeshcoreDemodSettings::m_hasHeader && headerCRCStatus) { + ui->headerCRCStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (MeshcoreDemodSettings::m_hasHeader && !headerCRCStatus) { + ui->headerCRCStatus->setStyleSheet("QLabel { background-color : red; }"); + } else { + ui->headerCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + } + + if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityOK) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : green; }"); + } else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityError) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : red; }"); + } else if (payloadParityStatus == (int) MeshcoreDemodSettings::ParityCorrected) { + ui->payloadFECStatus->setStyleSheet("QLabel { background-color : blue; }"); + } else { + ui->payloadFECStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + } + + if (payloadCRCStatus) { + ui->payloadCRCStatus->setStyleSheet("QLabel { background-color : green; }"); + } else { + ui->payloadCRCStatus->setStyleSheet("QLabel { background-color : red; }"); + } +} + +void MeshcoreDemodGUI::resetLoRaStatus() +{ + ui->headerHammingStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + ui->headerCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + ui->payloadFECStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + ui->payloadCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + ui->nbSymbolsText->setText("---"); + ui->nbCodewordsText->setText("---"); +} + +void MeshcoreDemodGUI::setBandwidths() +{ + int maxBandwidth = m_basebandSampleRate/MeshcoreDemodSettings::oversampling; + int maxIndex = 0; + + for (; (maxIndex < MeshcoreDemodSettings::nbBandwidths) && (MeshcoreDemodSettings::bandwidths[maxIndex] <= maxBandwidth); maxIndex++) + {} + + if (maxIndex != 0) + { + qDebug("MeshcoreDemodGUI::setBandwidths: avl: %d max: %d", maxBandwidth, MeshcoreDemodSettings::bandwidths[maxIndex-1]); + ui->BW->setMaximum(maxIndex - 1); + int index = ui->BW->value(); + ui->BWText->setText(QString("%1 Hz").arg(MeshcoreDemodSettings::bandwidths[index])); + } +} + +void MeshcoreDemodGUI::setupPipelineViews() +{ + m_pipelineTabs = ui->pipelineTabs; + ensurePipelineView(-1, "All"); +} + +MeshcoreDemodGUI::PipelineView& MeshcoreDemodGUI::ensurePipelineView(int pipelineId, const QString& pipelineName) +{ + auto it = m_pipelineViews.find(pipelineId); + + if (it != m_pipelineViews.end()) { + return it.value(); + } + + PipelineView view; + view.tabWidget = new QWidget(m_pipelineTabs); + QVBoxLayout *layout = new QVBoxLayout(view.tabWidget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(2); + + QSplitter *splitter = new QSplitter(Qt::Vertical, view.tabWidget); + splitter->setChildrenCollapsible(false); + + view.logText = new QPlainTextEdit(splitter); + view.logText->setReadOnly(true); + view.logText->setLineWrapMode(QPlainTextEdit::NoWrap); + QFont monoLog = view.logText->font(); + monoLog.setFamily("Liberation Mono"); + view.logText->setFont(monoLog); + + view.treeWidget = new QTreeWidget(splitter); + view.treeWidget->setColumnCount(2); + view.treeWidget->setHeaderLabels(QStringList() << "Field" << "Value"); + view.treeWidget->header()->setStretchLastSection(true); + view.treeWidget->setAlternatingRowColors(true); + view.treeWidget->setSelectionMode(QAbstractItemView::SingleSelection); + QObject::connect(view.treeWidget, &QTreeWidget::itemSelectionChanged, this, &MeshcoreDemodGUI::onPipelineTreeSelectionChanged); + QObject::connect(view.treeWidget, &QTreeWidget::itemClicked, this, &MeshcoreDemodGUI::onPipelineTreeSelectionChanged); + QObject::connect(view.treeWidget, &QTreeWidget::currentItemChanged, this, [this, tree=view.treeWidget](QTreeWidgetItem*, QTreeWidgetItem*) { + queueReplayForTree(tree); + }); + + splitter->setStretchFactor(0, 3); + splitter->setStretchFactor(1, 2); + layout->addWidget(splitter); + + const QString tabLabel = pipelineName.trimmed().isEmpty() ? QString("P%1").arg(pipelineId) : pipelineName; + m_pipelineTabs->addTab(view.tabWidget, tabLabel); + m_pipelineViews.insert(pipelineId, view); + return m_pipelineViews[pipelineId]; +} + +void MeshcoreDemodGUI::clearPipelineViews() +{ + setDechirpInspectionMode(false); + + for (auto it = m_pipelineViews.begin(); it != m_pipelineViews.end(); ++it) + { + if (it.value().logText) { + it.value().logText->clear(); + } + if (it.value().treeWidget) { + it.value().treeWidget->clear(); + } + } + + m_dechirpSnapshots.clear(); + m_dechirpSnapshotOrder.clear(); + m_dechirpSelectedMessageKey.clear(); + m_replayPendingMessageKey.clear(); + m_replayPendingHasSelection = false; + m_replaySelectionQueued = false; + m_pipelineMessageKeyByBase.clear(); + m_pipelinePendingMessageKeysByBase.clear(); + m_pipelineMessageSequence = 0; +} + +QString MeshcoreDemodGUI::buildPipelineMessageBaseKey(int pipelineId, uint32_t frameId, const QString& timestamp) const +{ + if (frameId != 0U) { + return QString("%1|frame:%2").arg(pipelineId).arg(frameId); + } + + const QString ts = timestamp.trimmed().isEmpty() + ? QStringLiteral("no-ts") + : timestamp.trimmed(); + return QString("%1|%2").arg(pipelineId).arg(ts); +} + +QString MeshcoreDemodGUI::allocatePipelineMessageKey(const QString& baseKey) +{ + ++m_pipelineMessageSequence; + const QString key = QString("%1#%2").arg(baseKey).arg(m_pipelineMessageSequence); + m_pipelineMessageKeyByBase[baseKey] = key; + m_pipelinePendingMessageKeysByBase[baseKey].push_back(key); + return key; +} + +QString MeshcoreDemodGUI::resolvePipelineMessageKey(const QString& baseKey) const +{ + auto pendingIt = m_pipelinePendingMessageKeysByBase.constFind(baseKey); + + if ((pendingIt != m_pipelinePendingMessageKeysByBase.constEnd()) && !pendingIt.value().isEmpty()) { + return pendingIt.value().front(); + } + + auto it = m_pipelineMessageKeyByBase.constFind(baseKey); + return it == m_pipelineMessageKeyByBase.constEnd() ? QString() : it.value(); +} + +void MeshcoreDemodGUI::consumePipelineMessageKey(const QString& baseKey, const QString& key) +{ + auto pendingIt = m_pipelinePendingMessageKeysByBase.find(baseKey); + + if (pendingIt == m_pipelinePendingMessageKeysByBase.end()) { + return; + } + + QVector& pendingKeys = pendingIt.value(); + const int idx = pendingKeys.indexOf(key); + + if (idx >= 0) { + pendingKeys.removeAt(idx); + } + + if (pendingKeys.isEmpty()) { + m_pipelinePendingMessageKeysByBase.erase(pendingIt); + } +} + +void MeshcoreDemodGUI::rememberLoRaDechirpSnapshot( + const MeshcoreDemodMsg::MsgReportDecodeBytes& msg, + const QString& messageKey +) +{ + const std::vector>& 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 MeshcoreDemodGUI::setDechirpInspectionMode(bool enabled) +{ + if (m_dechirpInspectionActive == enabled) + { + updateDechirpModeUI(); + return; + } + + m_dechirpInspectionActive = enabled; + + if (enabled) + { + if (m_spectrumVis) { + m_spectrumVis->setGLSpectrum(nullptr); + } + } + else + { + m_dechirpSelectedMessageKey.clear(); + + if (m_spectrumVis && ui && ui->glSpectrum) + { + m_spectrumVis->setGLSpectrum(ui->glSpectrum); + + const SpectrumSettings spectrumSettings = m_spectrumVis->getSettings(); + ui->glSpectrum->setDisplayWaterfall(spectrumSettings.m_displayWaterfall); + ui->glSpectrum->setDisplay3DSpectrogram(spectrumSettings.m_display3DSpectrogram); + } + } + + updateDechirpModeUI(); +} + +void MeshcoreDemodGUI::updateDechirpModeUI() +{ + ui->dechirpLiveFollow->setEnabled(m_dechirpInspectionActive); + ui->dechirpLiveFollow->setText(tr("Live")); + ui->dechirpLiveFollow->setToolTip(m_dechirpInspectionActive + ? tr("Return de-chirped spectrum to live follow mode.") + : tr("Already in live follow mode.")); +} + +void MeshcoreDemodGUI::queueReplayForTree(QTreeWidget *treeWidget) +{ + m_replayPendingMessageKey.clear(); + m_replayPendingHasSelection = false; + + if (treeWidget) + { + const QList selectedItems = treeWidget->selectedItems(); + QTreeWidgetItem *root = selectedItems.isEmpty() ? treeWidget->currentItem() : selectedItems.first(); + + if (root) + { + while (root->parent()) { + root = root->parent(); + } + + m_replayPendingHasSelection = true; + m_replayPendingMessageKey = root->data(0, kTreeMessageKeyRole).toString(); + } + } + + if (m_replaySelectionQueued) { + return; + } + + m_replaySelectionQueued = true; + QMetaObject::invokeMethod(this, [this]() { processQueuedReplay(); }, Qt::QueuedConnection); +} + +void MeshcoreDemodGUI::processQueuedReplay() +{ + m_replaySelectionQueued = false; + const bool hasSelection = m_replayPendingHasSelection; + const QString key = m_replayPendingMessageKey; + m_replayPendingHasSelection = false; + m_replayPendingMessageKey.clear(); + + if (!hasSelection) + { + setDechirpInspectionMode(false); + return; + } + + if (key.isEmpty()) + { + setDechirpInspectionMode(false); + return; + } + + auto it = m_dechirpSnapshots.constFind(key); + + if (it == m_dechirpSnapshots.constEnd()) + { + setDechirpInspectionMode(false); + return; + } + + setDechirpInspectionMode(true); + m_dechirpSelectedMessageKey = key; + replayDechirpSnapshot(it.value()); +} + +void MeshcoreDemodGUI::hardResetDechirpDisplayBuffers() +{ + if (!ui || !ui->glSpectrum) { + return; + } + + bool wantWaterfall = true; + bool want3DSpectrogram = false; + + if (m_spectrumVis) + { + const SpectrumSettings spectrumSettings = m_spectrumVis->getSettings(); + wantWaterfall = spectrumSettings.m_displayWaterfall; + want3DSpectrogram = spectrumSettings.m_display3DSpectrogram; + } + + if (!wantWaterfall && !want3DSpectrogram) { + wantWaterfall = true; + } + + ui->glSpectrum->setDisplayWaterfall(false); + ui->glSpectrum->setDisplay3DSpectrogram(false); + QCoreApplication::processEvents( + QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers, + 1 + ); + + ui->glSpectrum->setDisplayWaterfall(wantWaterfall); + ui->glSpectrum->setDisplay3DSpectrogram(want3DSpectrogram); + QCoreApplication::processEvents( + QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers, + 1 + ); +} + +void MeshcoreDemodGUI::clearTreeMessageKeyReferences(const QString& messageKey) +{ + if (messageKey.trimmed().isEmpty()) { + return; + } + + for (auto it = m_pipelineViews.begin(); it != m_pipelineViews.end(); ++it) + { + QTreeWidget *treeWidget = it.value().treeWidget; + + if (!treeWidget) { + continue; + } + + for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = treeWidget->topLevelItem(i); + + if (!item) { + continue; + } + + if (item->data(0, kTreeMessageKeyRole).toString() == messageKey) + { + item->setData(0, kTreeMessageKeyRole, QString()); + item->setToolTip(0, tr("Dechirp snapshot no longer available for this row.")); + } + } + } +} + +void MeshcoreDemodGUI::replayDechirpSnapshot(const DechirpSnapshot& snapshot) +{ + if (!ui || !ui->glSpectrum || snapshot.lines.empty()) { + return; + } + + if (m_spectrumVis) + { + SpectrumSettings spectrumSettings = m_spectrumVis->getSettings(); + + if (!spectrumSettings.m_displayWaterfall && !spectrumSettings.m_display3DSpectrogram) + { + spectrumSettings.m_displayWaterfall = true; + SpectrumVis::MsgConfigureSpectrumVis *msg = + SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false); + m_spectrumVis->getInputMessageQueue()->push(msg); + } + } + + const int fftSize = snapshot.fftSize > 0 ? snapshot.fftSize : static_cast(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); + }; + + // 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 MeshcoreDemodGUI::onPipelineTreeSelectionChanged() +{ + QTreeWidget *treeWidget = qobject_cast(sender()); + + if (!treeWidget) { + return; + } + + queueReplayForTree(treeWidget); +} + +void MeshcoreDemodGUI::appendPipelineLogLine(int pipelineId, const QString& pipelineName, const QString& line) +{ + auto appendLine = [&line](PipelineView& view) { + if (!view.logText) { + return; + } + + view.logText->appendPlainText(line); + alignTextViewToLatestLineLeft(view.logText); + }; + + PipelineView& targetView = ensurePipelineView(pipelineId, pipelineName); + appendLine(targetView); + + if (pipelineId != -1) + { + PipelineView& allView = ensurePipelineView(-1, "All"); + + if (allView.logText != targetView.logText) { + appendLine(allView); + } + } +} + +void MeshcoreDemodGUI::appendPipelineStatusLine(int pipelineId, const QString& pipelineName, const QString& status) +{ + appendPipelineLogLine(pipelineId, pipelineName, QString(">%1").arg(status)); +} + +void MeshcoreDemodGUI::appendPipelineBytes(int pipelineId, const QString& pipelineName, const QByteArray& bytes) +{ + QStringList lines; + QString line; + + for (int i = 0; i < bytes.size(); ++i) + { + const unsigned int b = static_cast(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 MeshcoreDemodGUI::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 MeshcoreDemodGUI::showLoRaMessage(const Message& message) +{ + const MeshcoreDemodMsg::MsgReportDecodeBytes& msg = (MeshcoreDemodMsg::MsgReportDecodeBytes&) message; + const int pipelineId = msg.getPipelineId(); + const QString messageBaseKey = buildPipelineMessageBaseKey(pipelineId, msg.getFrameId(), msg.getMsgTimestamp()); + const QString messageKey = allocatePipelineMessageKey(messageBaseKey); + rememberLoRaDechirpSnapshot(msg, messageKey); + const QString pipelineName = msg.getPipelineName().trimmed().isEmpty() + ? (pipelineId < 0 ? QString("Main") : QString("P%1").arg(pipelineId)) + : msg.getPipelineName(); + QByteArray bytes = msg.getBytes(); + QString syncWordStr((tr("%1").arg(msg.getSyncWord(), 2, 16, QChar('0')))); + + ui->sText->setText(tr("%1").arg(msg.getSingalDb(), 0, 'f', 1)); + ui->snrText->setText(tr("%1").arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1)); + unsigned int packetLength; + + ui->fecParity->setValue(msg.getNbParityBits()); + ui->fecParityText->setText(tr("%1").arg(msg.getNbParityBits())); + ui->packetLength->setValue(msg.getPacketSize()); + ui->packetLengthText->setText(tr("%1").arg(msg.getPacketSize())); + packetLength = msg.getPacketSize(); + + QDateTime dt = QDateTime::currentDateTime(); + QString dateStr = dt.toString("HH:mm:ss"); + + if (msg.getEarlyEOM()) + { + QString loRaStatus = tr("%1 %2 S:%3 SN:%4 HF:%5 HC:%6 EOM:too early") + .arg(dateStr) + .arg(syncWordStr) + .arg(msg.getSingalDb(), 0, 'f', 1) + .arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1) + .arg(getParityStr(msg.getHeaderParityStatus())) + .arg(msg.getHeaderCRCStatus() ? "ok" : "err"); + + appendPipelineStatusLine(pipelineId, pipelineName, loRaStatus); + displayLoRaStatus(msg.getHeaderParityStatus(), msg.getHeaderCRCStatus(), (int) MeshcoreDemodSettings::ParityUndefined, true); + ui->payloadCRCStatus->setStyleSheet("QLabel { background:rgb(79,79,79); }"); // reset payload CRC + } + else + { + QString loRaHeader = tr("%1 %2 S:%3 SN:%4 HF:%5 HC:%6 FEC:%7 CRC:%8") + .arg(dateStr) + .arg(syncWordStr) + .arg(msg.getSingalDb(), 0, 'f', 1) + .arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1) + .arg(getParityStr(msg.getHeaderParityStatus())) + .arg(msg.getHeaderCRCStatus() ? "ok" : "err") + .arg(getParityStr(msg.getPayloadParityStatus())) + .arg(msg.getPayloadCRCStatus() ? "ok" : "err"); + + appendPipelineStatusLine(pipelineId, pipelineName, loRaHeader); + appendPipelineBytes(pipelineId, pipelineName, bytes); + + QByteArray bytesCopy(bytes); + bytesCopy.truncate(packetLength); + bytesCopy.replace('\0', " "); + QString str = QString(bytesCopy.toStdString().c_str()); + QString textHeader(tr("%1 (%2)").arg(dateStr).arg(syncWordStr)); + appendPipelineLogLine(pipelineId, pipelineName, QString("TXT|%1 %2").arg(textHeader, str)); + displayLoRaStatus(msg.getHeaderParityStatus(), msg.getHeaderCRCStatus(), msg.getPayloadParityStatus(), msg.getPayloadCRCStatus()); + } + + // Always create/update a selectable row per LoRa frame so every frame can + // be selected for dechirp replay, even when higher-layer Meshcore parsing + // does not yield structured fields. + QVector> 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 MeshcoreDemodGUI::showTextMessage(const Message& message) +{ + const MeshcoreDemodMsg::MsgReportDecodeString& msg = (MeshcoreDemodMsg::MsgReportDecodeString&) message; + const int pipelineId = msg.getPipelineId(); + const QString pipelineName = msg.getPipelineName().trimmed().isEmpty() + ? (pipelineId < 0 ? QString("Main") : QString("P%1").arg(pipelineId)) + : msg.getPipelineName(); + + QDateTime dt = QDateTime::currentDateTime(); + QString dateStr = dt.toString("HH:mm:ss"); + ui->sText->setText(tr("%1").arg(msg.getSingalDb(), 0, 'f', 1)); + ui->snrText->setText(tr("%1").arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1)); + + QString status = tr("%1 S:%2 SN:%3") + .arg(dateStr) + .arg(msg.getSingalDb(), 0, 'f', 1) + .arg(msg.getSingalDb() - msg.getNoiseDb(), 0, 'f', 1); + + appendPipelineStatusLine(pipelineId, pipelineName, status); + appendPipelineLogLine(pipelineId, pipelineName, QString("TXT|%1").arg(msg.getString())); + + if (msg.hasStructuredFields()) + { + const QString title = QString("%1 %2").arg(dateStr, pipelineName); + const QString messageBaseKey = buildPipelineMessageBaseKey(pipelineId, msg.getFrameId(), msg.getMsgTimestamp()); + QString messageKey = resolvePipelineMessageKey(messageBaseKey); + + if (messageKey.isEmpty()) { + messageKey = allocatePipelineMessageKey(messageBaseKey); + } + + appendPipelineTreeFields(pipelineId, pipelineName, title, msg.getStructuredFields(), messageKey); + + if (!messageKey.isEmpty()) { + consumePipelineMessageKey(messageBaseKey, messageKey); + } + } +} + +void MeshcoreDemodGUI::displayText(const QString& text) +{ + appendPipelineLogLine(-1, "All", QString("TXT|%1").arg(text)); +} + +void MeshcoreDemodGUI::displayBytes(const QByteArray& bytes) +{ + appendPipelineBytes(-1, "All", bytes); +} + +void MeshcoreDemodGUI::displayStatus(const QString& status) +{ + appendPipelineStatusLine(-1, "All", status); + qInfo() << "MeshcoreDemodGUI::displayStatus:" << status; +} + +QString MeshcoreDemodGUI::getParityStr(int parityStatus) +{ + if (parityStatus == (int) MeshcoreDemodSettings::ParityError) { + return "err"; + } else if (parityStatus == (int) MeshcoreDemodSettings::ParityCorrected) { + return "fix"; + } else if (parityStatus == (int) MeshcoreDemodSettings::ParityOK) { + return "ok"; + } else { + return "n/a"; + } +} + +void MeshcoreDemodGUI::tick() +{ + handleMeshAutoLockSourceObservation(); + advanceMeshAutoLock(); + + if (m_deviceUISet && m_deviceUISet->m_deviceAPI) + { + const bool isRemoteTcpInput = (m_deviceUISet->m_deviceAPI->getHardwareId() == "RemoteTCPInput"); + + if (isRemoteTcpInput) + { + const bool running = (m_deviceUISet->m_deviceAPI->state(m_settings.m_streamIndex) == DeviceAPI::StRunning); + + if (running && !m_remoteTcpLastRunningState) + { + m_remoteTcpReconnectAutoApplyPending = true; + m_remoteTcpReconnectAutoApplyWaitTicks = 0; + qInfo() << "MeshcoreDemodGUI::tick: RemoteTCP input running - waiting for first DSP notification to reapply Meshcore profile"; + } + + if (m_remoteTcpReconnectAutoApplyPending && running) + { + m_remoteTcpReconnectAutoApplyWaitTicks++; + + if (m_remoteTcpReconnectAutoApplyWaitTicks >= 20) + { + m_remoteTcpReconnectAutoApplyPending = false; + m_remoteTcpReconnectAutoApplyWaitTicks = 0; + qInfo() << "MeshcoreDemodGUI::tick: RemoteTCP reconnect fallback timeout - reapplying Meshcore profile"; + QMetaObject::invokeMethod(this, &MeshcoreDemodGUI::applyMeshcoreProfileFromSelection, Qt::QueuedConnection); + } + } + + if (!running) + { + m_remoteTcpReconnectAutoApplyPending = false; + m_remoteTcpReconnectAutoApplyWaitTicks = 0; + } + + m_remoteTcpLastRunningState = running; + } + else + { + m_remoteTcpReconnectAutoApplyPending = false; + m_remoteTcpReconnectAutoApplyWaitTicks = 0; + m_remoteTcpLastRunningState = false; + } + } + + if (m_tickCount < 10) + { + m_tickCount++; + } + else + { + m_tickCount = 0; + + ui->nText->setText(tr("%1").arg(CalcDb::dbPower(m_meshcoreDemod->getCurrentNoiseLevel()), 0, 'f', 1)); + ui->channelPower->setText(tr("%1 dB").arg(CalcDb::dbPower(m_meshcoreDemod->getTotalPower()), 0, 'f', 1)); + + if (m_meshcoreDemod->getDemodActive()) { + ui->mute->setStyleSheet("QToolButton { background-color : green; }"); + } else { + ui->mute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + } + } +} + +void MeshcoreDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &MeshcoreDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->BW, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_BW_valueChanged); + QObject::connect(ui->Spread, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_Spread_valueChanged); + QObject::connect(ui->deBits, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_deBits_valueChanged); + QObject::connect(ui->preambleChirps, &QSlider::valueChanged, this, &MeshcoreDemodGUI::on_preambleChirps_valueChanged); + QObject::connect(ui->mute, &QToolButton::toggled, this, &MeshcoreDemodGUI::on_mute_toggled); + QObject::connect(ui->clear, &QPushButton::clicked, this, &MeshcoreDemodGUI::on_clear_clicked); + QObject::connect(ui->eomSquelch, &QDial::valueChanged, this, &MeshcoreDemodGUI::on_eomSquelch_valueChanged); + QObject::connect(ui->messageLength, &QDial::valueChanged, this, &MeshcoreDemodGUI::on_messageLength_valueChanged); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QObject::connect(ui->udpSend, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_udpSend_stateChanged(static_cast(state)); }); + QObject::connect(ui->udpSendJson, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_udpSendJson_stateChanged(static_cast(state)); }); +#else + QObject::connect(ui->udpSend, &QCheckBox::stateChanged, this, &MeshcoreDemodGUI::on_udpSend_stateChanged); + QObject::connect(ui->udpSendJson, &QCheckBox::stateChanged, this, &MeshcoreDemodGUI::on_udpSendJson_stateChanged); +#endif + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &MeshcoreDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &MeshcoreDemodGUI::on_udpPort_editingFinished); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QObject::connect(ui->invertRamps, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ on_invertRamps_stateChanged(static_cast(state)); }); +#else + QObject::connect(ui->invertRamps, &QCheckBox::stateChanged, this, &MeshcoreDemodGUI::on_invertRamps_stateChanged); +#endif +} + +void MeshcoreDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodgui.h b/plugins/channelrx/demodmeshcore/meshcoredemodgui.h new file mode 100644 index 000000000..76d433fbd --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodgui.h @@ -0,0 +1,252 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MESHCOREDEMODGUI_H +#define INCLUDE_MESHCOREDEMODGUI_H + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include +#include +#include + +#include "meshcoredemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class MeshcoreDemod; +class SpectrumVis; +class BasebandSampleSink; +class QComboBox; +class QCheckBox; +class QPushButton; +class QTabWidget; +class QPlainTextEdit; +class QTreeWidget; +class QTreeWidgetItem; +namespace MeshcoreDemodMsg { class MsgReportDecodeBytes; } + +namespace Ui { + class MeshcoreDemodGUI; +} + +class MeshcoreDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static MeshcoreDemodGUI* 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_preambleChirps_valueChanged(int value); + 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_udpSend_stateChanged(int state); + void on_udpSendJson_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 on_conf_valueChanged(int value); + void on_confAdd_clicked(bool checked); + void on_confDel_clicked(bool checked); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void channelMarkerHighlightedByCursor(); + void handleInputMessages(); + void onPipelineTreeSelectionChanged(); + void tick(); + +private: + Ui::MeshcoreDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + MeshcoreDemodSettings m_settings; + qint64 m_deviceCenterFrequency; + int m_basebandSampleRate; + bool m_doApplySettings; + + MeshcoreDemod* m_meshcoreDemod; + SpectrumVis* m_spectrumVis; + 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; + + // Multi-pipeline management + static constexpr int kMaxPipelines = 4; + int m_focusedPipelineIndex; //!< 0 = primary; 1-3 = extra (volatile) pipelines + QVector m_extraPipelineSettings; //!< Settings for extra pipelines; not persisted + + MessageQueue m_inputMessageQueue; + unsigned int m_tickCount; + + explicit MeshcoreDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~MeshcoreDemodGUI(); + + 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 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); + QString getParityStr(int parityStatus); + void resetLoRaStatus(); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + void setupMeshcoreAutoProfileControls(); + void rebuildMeshcoreChannelOptions(); + bool retuneDeviceToFrequency(qint64 centerFrequencyHz); + bool autoTuneDeviceSampleRateForBandwidth(int bandwidthHz, QString& summary, int* newBasebandSampleRateOut = nullptr); + int findBandwidthIndex(int bandwidthHz) const; + void applyMeshcoreProfileFromSelection(); + void editMeshcoreKeys(); + // Multi-pipeline helpers + MeshcoreDemodSettings& focusedSettings(); + const MeshcoreDemodSettings& focusedSettings() const; + int pipelineCount() const; + void updateConfControls(); + void loadFocusedSettingsToControls(); + void applyFocusedPipelineSettings(bool force = false); + void pushExtraPipelineSettingsToDemod(); + void startMeshAutoLock(); + void stopMeshAutoLock(bool keepBestCandidate); + void applyMeshAutoLockCandidate(const MeshAutoLockCandidate& candidate, bool applySettingsNow); + void handleMeshAutoLockObservation(const MeshcoreDemodMsg::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 MeshcoreDemodMsg::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_MESHCOREDEMODGUI_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodgui.ui b/plugins/channelrx/demodmeshcore/meshcoredemodgui.ui new file mode 100644 index 000000000..b9a0b2b40 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodgui.ui @@ -0,0 +1,1518 @@ + + + MeshcoreDemodGUI + + + + 0 + 0 + 838 + 680 + + + + + 605 + 680 + + + + + Liberation Sans + 9 + + + + MeshCore Demodulator + + + + + 0 + 10 + 601 + 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 + + + + + + + + + + + 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 + 825 + 316 + + + + Payload + + + + 2 + + + + + 2 + + + + + Meshcore region (defines allowed frequency band) + + + Region + + + + + + + Meshcore region. Combined with preset/channel to auto-apply LoRa receive parameters. + + + + US + + + + + EU_433 + + + + + EU_868 + + + + + ANZ + + + + + JP + + + + + CN + + + + + KR + + + + + TW + + + + + IN + + + + + TH + + + + + BR_902 + + + + + LORA_24 + + + + + + + + MeshCore regional preset (EU_NARROW, EU_LONG_RANGE, AU, USA, ...) + + + Preset + + + + + + + MeshCore regional preset. Applies frequency, bandwidth, spreading factor, and coding rate. + + + + EU_NARROW + + + + + EU_LONG_RANGE + + + + + EU_MEDIUM_RANGE + + + + + AU + + + + + AU_VICTORIA + + + + + CZ_NARROW + + + + + EU_433_LONG_RANGE + + + + + NZ + + + + + NZ_NARROW + + + + + PT_433 + + + + + PT_868 + + + + + CH + + + + + USA + + + + + VN + + + + + USER + + + + + + + + Meshcore channel number (zero-based) + + + Channel + + + + + + + Meshcore channel number (zero-based, shown with center frequency) + + + + + + + Apply the currently selected Meshcore region/preset/channel profile now. + + + Apply + + + + + + + Open key manager to configure Meshcore decryption keys (hex/base64/default/simple). + + + Keys... + + + + + + + Scan Invert + frequency offset candidates and keep the best lock. + Arms and waits for on-air activity, then scans candidates. + Scores using decode quality plus source-side intensity (demod activity and power/noise). + + + Auto Lock + + + true + + + + + + + Automatically tune source parameters for the selected LoRa profile. + Includes sample-rate/decimation and, where supported, dcBlock/iqCorrection/agc. + + + Auto Input Tune + + + + + + + + + + + Conf + + + + + + + + 24 + 24 + + + + Trace index (0 is X trace) + + + 0 + + + 1 + + + + + + + + 15 + 0 + + + + Configuration Id + + + 0 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 18 + 18 + + + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + + + 190 + 190 + 190 + + + + + + + + + Liberation Sans + 10 + + + + Add a new configuration + + + + + + + + + + + + 18 + 18 + + + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + + + 190 + 190 + 190 + + + + + + + + + Liberation Sans + 10 + + + + Remove current configuration + + + - + + + + + + + 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 + + + + + + + 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 + + + + + + + + 22 + 22 + + + + Payload packet length in number of bytes or characters + + + 225 + + + 1 + + + 30 + + + + + + + Pkt + + + + + + + 255 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 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 + + + + + + + + 42 + 24 + + + + Return de-chirped spectrum to live follow mode. + + + Live + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 200 + + + + QTabWidget::West + + + true + + + false + + + + + + + + + + + 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 + + + + + + + Send decoded message as JSON via UDP + + + Qt::RightToLeft + + + JSON + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + 410 + 601 + 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/demodmeshcore/meshcoredemodmsg.cpp b/plugins/channelrx/demodmeshcore/meshcoredemodmsg.cpp new file mode 100644 index 000000000..d5c826f53 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodmsg.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 "meshcoredemodmsg.h" + +MESSAGE_CLASS_DEFINITION(MeshcoreDemodMsg::MsgDecodeSymbols, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreDemodMsg::MsgLoRaHeaderProbe, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreDemodMsg::MsgLoRaHeaderFeedback, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreDemodMsg::MsgReportDecodeBytes, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreDemodMsg::MsgReportDecodeString, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreDemodMsg::MsgReportDecodeFT, Message) diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodmsg.h b/plugins/channelrx/demodmeshcore/meshcoredemodmsg.h new file mode 100644 index 000000000..bdad998f2 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodmsg.h @@ -0,0 +1,545 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MESHCOREDEMODMSG_H +#define INCLUDE_MESHCOREDEMODMSG_H + +#include + +#include +#include +#include +#include "util/message.h" + +#include "meshcoredemodsettings.h" + +namespace MeshcoreDemodMsg +{ + 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); + } + void dropFront(unsigned int count) + { + const unsigned int symbolsDrop = std::min(count, static_cast(m_symbols.size())); + m_symbols.erase(m_symbols.begin(), m_symbols.begin() + symbolsDrop); + + const unsigned int magnitudesDrop = std::min(count, static_cast(m_magnitudes.size())); + m_magnitudes.erase(m_magnitudes.begin(), m_magnitudes.begin() + magnitudesDrop); + + const unsigned int spectrumDrop = std::min(count, static_cast(m_dechirpedSpectrum.size())); + m_dechirpedSpectrum.erase(m_dechirpedSpectrum.begin(), m_dechirpedSpectrum.begin() + spectrumDrop); + } + + 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) MeshcoreDemodSettings::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) MeshcoreDemodSettings::ParityUndefined), + m_payloadCRCStatus(false), + m_pipelineId(-1) + { } + }; +} + +#endif // INCLUDE_MESHCOREDEMODMSG_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodsettings.cpp b/plugins/channelrx/demodmeshcore/meshcoredemodsettings.cpp new file mode 100644 index 000000000..7f507d4b2 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodsettings.cpp @@ -0,0 +1,388 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoredemodsettings.h" + +const int MeshcoreDemodSettings::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 MeshcoreDemodSettings::nbBandwidths = 3*8 + 4; +// Keep frame-sync input at >=4x BW (matches gr-lora_sdr os_factor=4 expectations) +// so SF11/SF12 Meshcore presets retain enough timing resolution. +const int MeshcoreDemodSettings::oversampling = 4; + +// Static settings values (not user-configurable) +const MeshcoreDemodSettings::CodingScheme MeshcoreDemodSettings::m_codingScheme = MeshcoreDemodSettings::CodingLoRa; +const bool MeshcoreDemodSettings::m_autoNbSymbolsMax = false; +const bool MeshcoreDemodSettings::m_hasHeader = true; +const bool MeshcoreDemodSettings::m_hasCRC = true; + +MeshcoreDemodSettings::MeshcoreDemodSettings() : + m_inputFrequencyOffset(0), + m_channelMarker(0), + m_spectrumGUI(0), + m_rollupState(0) +{ + resetToDefaults(); +} + +void MeshcoreDemodSettings::resetToDefaults() +{ + // MeshCore EU defaults — match MeshcoreModSettings::resetToDefaults + // so demod decodes mod's frames out of the box. Per user request: + // SF=8, BW=62.5 kHz, preamble=16, CR=4/8. + m_bandwidthIndex = 18; // 62500 Hz + m_spreadFactor = 8; + m_deBits = 0; + m_decodeActive = true; + m_eomSquelchTenths = 60; + m_nbSymbolsMax = 1023; + m_preambleChirps = 16; // MeshCore: 16 for SF>8, 32 for SF<9 (profile overrides) + m_packetLength = 237; + m_nbParityBits = 4; // CR 4/8 (MeshCore EU) + m_sendViaUDP = false; + m_sendJsonViaUDP = false; + m_invertRamps = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_meshcoreKeySpecList.clear(); + m_meshcoreAutoSampleRate = true; + // MeshCore EU/UK Narrow (Recommended) — 869.618 MHz / SF 8 / BW 62.5 kHz / + // CR 4/8. See modemmeshcore::command::applyMeshcorePreset for the full + // table of regional presets recognised by the radio-settings derivation. + m_meshcoreRegionCode = "EU_868"; + m_meshcorePresetName = "EU_NARROW"; + m_meshcoreChannelIndex = 0; + m_rgbColor = QColor(255, 0, 255).rgb(); + m_title = "MeshCore 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 MeshcoreDemodSettings::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.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.writeU32(17, m_preambleChirps); + 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); + s.writeBool(38, m_sendJsonViaUDP); + + 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_meshcoreKeySpecList); + s.writeBool(34, m_meshcoreAutoSampleRate); + s.writeString(35, m_meshcoreRegionCode); + s.writeString(36, m_meshcorePresetName); + s.writeS32(37, m_meshcoreChannelIndex); + + return s.final(); +} + +bool MeshcoreDemodSettings::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; + 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, "MeshCore Demodulator"); + d.readS32(7, &m_deBits, 0); + d.readBool(9, &m_decodeActive, true); + d.readS32(10, &m_eomSquelchTenths, 60); + d.readU32(11, &m_nbSymbolsMax, 1023); + d.readS32(12, &m_packetLength, 237); + d.readS32(13, &m_nbParityBits, 1); + d.readU32(17, &m_preambleChirps, 16); + 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; + } + + d.readBool(38, &m_sendJsonViaUDP, false); + + 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_meshcoreKeySpecList, ""); + d.readBool(34, &m_meshcoreAutoSampleRate, true); + d.readString(35, &m_meshcoreRegionCode, "EU_868"); + d.readString(36, &m_meshcorePresetName, "EU_NARROW"); + d.readS32(37, &m_meshcoreChannelIndex, 0); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void MeshcoreDemodSettings::applySettings(const QStringList& settingsKeys, const MeshcoreDemodSettings& 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("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("sendViaUDP")) + m_sendViaUDP = settings.m_sendViaUDP; + if (settingsKeys.contains("sendJsonViaUDP")) + m_sendJsonViaUDP = settings.m_sendJsonViaUDP; + 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("meshcoreKeySpecList")) + m_meshcoreKeySpecList = settings.m_meshcoreKeySpecList; + if (settingsKeys.contains("meshcoreAutoSampleRate")) + m_meshcoreAutoSampleRate = settings.m_meshcoreAutoSampleRate; + if (settingsKeys.contains("meshcoreRegionCode")) + m_meshcoreRegionCode = settings.m_meshcoreRegionCode; + if (settingsKeys.contains("meshcorePresetName")) + m_meshcorePresetName = settings.m_meshcorePresetName; + if (settingsKeys.contains("meshcoreChannelIndex")) + m_meshcoreChannelIndex = settings.m_meshcoreChannelIndex; + 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 MeshcoreDemodSettings::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("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("sendViaUDP")) + debug += QString("SendViaUDP: %1 ").arg(m_sendViaUDP); + if (force || settingsKeys.contains("sendJsonViaUDP")) + debug += QString("SendJsonViaUDP: %1 ").arg(m_sendJsonViaUDP); + 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("meshcoreKeySpecList")) + debug += QString("MeshcoreKeySpecList: %1 ").arg(m_meshcoreKeySpecList); + if (force || settingsKeys.contains("meshcoreAutoSampleRate")) + debug += QString("MeshcoreAutoSampleRate: %1 ").arg(m_meshcoreAutoSampleRate); + if (force || settingsKeys.contains("meshcoreRegionCode")) + debug += QString("MeshcoreRegionCode: %1 ").arg(m_meshcoreRegionCode); + if (force || settingsKeys.contains("meshcorePresetName")) + debug += QString("MeshcorePresetName: %1 ").arg(m_meshcorePresetName); + if (force || settingsKeys.contains("meshcoreChannelIndex")) + debug += QString("MeshcoreChannelIndex: %1 ").arg(m_meshcoreChannelIndex); + 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 MeshcoreDemodSettings::getNbSFDFourths() const +{ + switch (m_codingScheme) + { + case CodingLoRa: + return 9; + default: + return 8; + } +} + +bool MeshcoreDemodSettings::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/demodmeshcore/meshcoredemodsettings.h b/plugins/channelrx/demodmeshcore/meshcoredemodsettings.h new file mode 100644 index 000000000..4f4c18612 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodsettings.h @@ -0,0 +1,108 @@ + +/////////////////////////////////////////////////////////////////////////////////// +// 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_DEMODMESHCORE_MESHCOREDEMODSETTINGS_H_ +#define PLUGINS_CHANNELRX_DEMODMESHCORE_MESHCOREDEMODSETTINGS_H_ + +#include +#include + +#include + +#include "dsp/fftwindow.h" + +class Serializable; + +struct MeshcoreDemodSettings +{ + enum CodingScheme + { + CodingLoRa, //!< Standard LoRa + }; + + enum ParityStatus + { + ParityUndefined, + ParityError, + ParityCorrected, + ParityOK + }; + + int m_inputFrequencyOffset; + int m_bandwidthIndex; + int m_spreadFactor; + int m_deBits; //!< Low data rate optimize (DE) bits + static const 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 + static const 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) + static const bool m_hasCRC; //!< Payload has CRC (LoRa) + static const bool m_hasHeader; //!< Header present before actual payload (LoRa) + bool m_sendViaUDP; //!< Send decoded message via UDP + bool m_sendJsonViaUDP; //!< Send decoded message as JSON 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_meshcoreKeySpecList; //!< Optional per-channel Meshcore decode key list + bool m_meshcoreAutoSampleRate; //!< Auto-tune source sample rate/decimation for selected Meshcore profile + QString m_meshcoreRegionCode; //!< UI-selected Meshcore region code (US, EU_868, ...) + QString m_meshcorePresetName; //!< UI-selected MeshCore regional preset (EU_NARROW, AU, USA, ...) + int m_meshcoreChannelIndex; //!< UI-selected Meshcore 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; + + MeshcoreDemodSettings(); + 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 MeshcoreDemodSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; +}; + + + +#endif /* PLUGINS_CHANNELRX_DEMODMESHCORE_MESHCOREDEMODSETTINGS_H_ */ diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodsink.cpp b/plugins/channelrx/demodmeshcore/meshcoredemodsink.cpp new file mode 100644 index 000000000..e66d73ac2 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodsink.cpp @@ -0,0 +1,1332 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 + +#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 "meshcoredemodmsg.h" +#include "meshcoredemoddecoderlora.h" +#include "meshcoredemodsink.h" + +MeshcoreDemodSink::MeshcoreDemodSink() : + m_decodeMsg(nullptr), + m_decoderMsgQueue(nullptr), + m_fftSequence(-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(MeshcoreDemodSettings::oversampling > 0 ? MeshcoreDemodSettings::oversampling : 1), + m_osCenterPhase((MeshcoreDemodSettings::oversampling > 1 ? MeshcoreDemodSettings::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 = MeshcoreDemodSettings::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_loRaFFTInterpolation; + + initSF(m_settings.m_spreadFactor, m_settings.m_deBits); +} + +MeshcoreDemodSink::~MeshcoreDemodSink() +{ + FFTFactory *fftFactory = DSPEngine::instance()->getFFTFactory(); + + if (m_fftSequence >= 0) + { + fftFactory->releaseEngine(m_interpolatedFFTLength, false, m_fftSequence); + } + + delete[] m_downChirps; + delete[] m_upChirps; + delete[] m_spectrumBuffer; + delete[] m_spectrumLine; +} + +void MeshcoreDemodSink::initSF(unsigned int sf, unsigned int deBits) +{ + 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); + } + + m_nbSymbols = 1 << sf; + m_nbSymbolsEff = 1 << (sf - deBits); + m_fftLength = m_nbSymbols; + m_interpolatedFFTLength = m_fftInterpolation*m_fftLength; + m_fftSequence = fftFactory->getEngine(m_interpolatedFFTLength, false, &m_fft); + 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 MeshcoreDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + +#define MESHCORE_DISPATCH_SAMPLE(_ci) \ + do { \ + if (MeshcoreDemodSettings::m_codingScheme == MeshcoreDemodSettings::CodingLoRa) { \ + processSampleLoRa(_ci); \ + } else if (m_osFactor <= 1U) { \ + processSampleLoRa(_ci); \ + } else { \ + if ((m_osCounter % m_osFactor) == m_osCenterPhase) { \ + processSampleLoRa(_ci); \ + } \ + m_osCounter++; \ + } \ + m_sampleDistanceRemain += m_interpolatorDistance; \ + } while (0) + + 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(); + + // Interpolator::decimate() is decimate-only (per interpolator.h:40-41: + // "decimation factor should always be lower than 2 for proper work" — + // implicitly: distance ≥ 1.0). When channelSampleRate < BW * osFactor, + // distance < 1.0 and we need the upsample path (interpolate()), not + // decimate(). On-air bisection vs ChirpChatDemod (positive control, + // os=2 so distance always ≥ 1.0) showed mesh = 0 frames at distance=0.5 + // while ChirpChat decoded the same baseband. AMDemodSink:82-97 has the + // canonical dual-mode pattern; this matches it. + if (m_interpolatorDistance < 1.0f) + { + while (!m_interpolator.interpolate(&m_sampleDistanceRemain, c, &ci)) + { + MESHCORE_DISPATCH_SAMPLE(ci); + } + } + else + { + if (m_interpolator.decimate(&m_sampleDistanceRemain, c, &ci)) + { + MESHCORE_DISPATCH_SAMPLE(ci); + } + } + } + +#undef MESHCORE_DISPATCH_SAMPLE +} + +void MeshcoreDemodSink::reset() +{ + resetLoRaFrameSync(); + m_headerLocked = false; + m_expectedSymbols = 0; + m_waitHeaderFeedback = false; + m_headerFeedbackWaitSteps = 0; + m_osCounter = 0; +} + +unsigned int MeshcoreDemodSink::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 MeshcoreDemodSink::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 MeshcoreDemodSink::tryHeaderLock() +{ + if (!m_decodeMsg) { + return; + } + + 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; + } + + const unsigned int maxOffset = std::min(2U, static_cast(symbols.size()) - 8U); + const unsigned int headerSymbolMod = 1U << headerNbSymbolBits; + + for (unsigned int offset = 0U; offset <= maxOffset; offset++) + { + const std::vector baseHeaderSlice(symbols.begin() + offset, symbols.begin() + offset + 8U); + + for (int delta = -2; delta <= 2; delta++) + { + std::vector headerSlice(baseHeaderSlice); + + if (delta != 0) + { + for (auto& sym : headerSlice) { + const int shifted = loRaMod(static_cast(sym) + delta, static_cast(headerSymbolMod)); + sym = static_cast(shifted); + } + } + + bool hasCRC = true; + unsigned int nbParityBits = 1U; + unsigned int packetLength = 0U; + int headerParityStatus = (int) MeshcoreDemodSettings::ParityUndefined; + bool headerCRCStatus = false; + + MeshcoreDemodDecoderLoRa::decodeHeader( + headerSlice, + headerNbSymbolBits, + hasCRC, + nbParityBits, + packetLength, + headerParityStatus, + headerCRCStatus + ); + + if (!headerCRCStatus || packetLength == 0U || nbParityBits < 1U || nbParityBits > 4U) { + continue; + } + + 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; + } + + const unsigned int expectedSymbols = 8U + payloadBlocks * (4U + nbParityBits); + + if (expectedSymbols > m_settings.m_nbSymbolsMax) { + continue; + } + + if (offset > 0U) + { + m_decodeMsg->dropFront(offset); + m_loRaFrameSymbolCount = m_loRaFrameSymbolCount > offset + ? (m_loRaFrameSymbolCount - offset) + : 0U; + } + + m_expectedSymbols = expectedSymbols; + m_headerLocked = true; + + // qDebug("[LOOPBACK][RX] header_realign frameId=%u offset=%u delta=%d", m_loRaFrameId, offset, delta); + qDebug("MeshcoreDemodSink::tryHeaderLock: LOCKED len=%u CR=%u CRC=%s LDRO=%s expected=%u symbols offset=%u delta=%d", + packetLength, nbParityBits, hasCRC ? "on" : "off", ldro ? "on" : "off", m_expectedSymbols, offset, delta); + return; + } + } + + if (symbols.size() >= 10U) { + qDebug("MeshcoreDemodSink::tryHeaderLock: header invalid after offsets 0..%u deltas -2..2", maxOffset); + } +} + +bool MeshcoreDemodSink::sendLoRaHeaderProbe() +{ + if (!m_decodeMsg || !m_decoderMsgQueue) { + return false; + } + + const std::vector& symbols = m_decodeMsg->getSymbols(); + + if (symbols.size() < 8U) { + 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; + + MeshcoreDemodMsg::MsgLoRaHeaderProbe *probe = MeshcoreDemodMsg::MsgLoRaHeaderProbe::create( + m_loRaFrameId, + headerSymbols, + payloadNbSymbolBits, + headerNbSymbolBits, + m_settings.m_spreadFactor, + static_cast(std::max(1, m_bandwidth)), + MeshcoreDemodSettings::m_hasHeader, + MeshcoreDemodSettings::m_hasCRC + ); + m_decoderMsgQueue->push(probe); + + return true; +} + +void MeshcoreDemodSink::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_loRaState != LoRaStateSFOCompensation) + { + return; + } + + if (frameId != m_loRaFrameId) { + return; + } + + m_waitHeaderFeedback = false; + m_headerFeedbackWaitSteps = 0; + + if (!valid || !headerCRCStatus || (packetLength == 0U) || (nbParityBits < 1U) || (nbParityBits > 4U)) + { + qDebug("MeshcoreDemodSink::applyLoRaHeaderFeedback: invalid header -> reset frame"); + resetLoRaFrameSync(); + return; + } + + if (expectedSymbols > m_settings.m_nbSymbolsMax) + { + qDebug("MeshcoreDemodSink::applyLoRaHeaderFeedback: expected %u > max %u, fallback to EOM", + expectedSymbols, m_settings.m_nbSymbolsMax); + return; + } + + m_expectedSymbols = expectedSymbols; + m_headerLocked = true; + m_loRaReceivedHeader = true; +} + +int MeshcoreDemodSink::loRaMod(int a, int b) const +{ + if (b <= 0) { + return 0; + } + + return (a % b + b) % b; +} + +int MeshcoreDemodSink::loRaRound(float number) const +{ + return (number > 0.0f) ? static_cast(number + 0.5f) : static_cast(std::ceil(number - 0.5f)); +} + +void MeshcoreDemodSink::resetLoRaFrameSync() +{ + if (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; + + 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 MeshcoreDemodSink::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 MeshcoreDemodSink::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 MeshcoreDemodSink::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 MeshcoreDemodSink::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 MeshcoreDemodSink::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 MeshcoreDemodSink::finalizeLoRaFrame() +{ + if (!m_decodeMsg) { + resetLoRaFrameSync(); + return; + } + + qDebug( + "MeshcoreDemodSink::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 MeshcoreDemodSink::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 MeshcoreDemodSink::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)); + } + + // Preserve state-machine net ID bin indices for sync word extraction. + // The corrLen-based refinement overwrites m_loRaNetIds but may produce + // incorrect results when m_loRaNetIdSamp is not fully populated. + const int netIdBin0 = m_loRaNetIds[0]; + const int netIdBin1 = m_loRaNetIds[1]; + + 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 = MeshcoreDemodMsg::MsgDecodeSymbols::create(); + m_decodeMsg->setFrameId(m_loRaFrameId); + { + // LoRa sync word is encoded across two net-ID chirps. + // First chirp (index 0) carries the high nibble, second (index 1) the low nibble. + // Each nibble N is mapped to bin N*8. Formula: syncWord = low + 16*high. + // Use the coarse state-machine bin values (netIdBin0/1) which are reliably + // set from the FFT in LoRaSyncNetId1/2 states, not the refined values which + // require m_loRaNetIdSamp to be fully populated. + const unsigned int hiNibble = static_cast(std::round(static_cast(netIdBin0) / 8.0)) & 0xFU; + const unsigned int loNibble = static_cast(std::round(static_cast(netIdBin1) / 8.0)) & 0xFU; + m_decodeMsg->setSyncWord(loNibble + 16U * hiNibble); + } + 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); + } + + std::vector symbolMags; + const unsigned int rawSymbol = getLoRaSymbolVal(m_loRaInDown.data(), m_loRaPayloadDownchirp.data(), &symbolMags, true); + const bool headerSymbol = 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_loRaFrameSymbolCount >= 8U)) + { + 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 MeshcoreDemodSink::applyChannelSettings(int channelSampleRate, int bandwidth, int channelFrequencyOffset, bool force) +{ + qDebug() << "MeshcoreDemodSink::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)); + // Channel filter cutoff has to leave headroom above the signal's + // outer edge (BW/2) to absorb on-air CFO. BW/1.9 = ~1.05 * (BW/2) + // gives only ~5% margin and clips upper-leg chirp energy when CFO + // exceeds ~5 kHz (matches an empirical on-air cliff observed on + // a B210 + Heltec V3 at 869 MHz; ~10 ppm combined LO/TCXO mismatch + // ≈ 8.7 kHz CFO landed right at the cliff and produced ~10 % header + // decode + zero payload decode rate). ChirpChatDemod uses + // BW/1.25 = 1.6 * (BW/2) and works on the same hardware; CFO sweep + // on a clean test vector confirms the cliff moves from ~10 kHz to + // ~17 kHz with this cutoff. + // `bandwidth` is the function parameter (the new value); using + // `m_bandwidth` here would pick up the prior call's value, since + // m_bandwidth is only updated below at line ~1247. + m_interpolator.create(16, channelSampleRate, bandwidth / 1.25f); + m_interpolatorDistance = (Real) channelSampleRate / (Real) targetFrameSyncRate; + m_sampleDistanceRemain = 0; + m_osCounter = 0; + qDebug() << "MeshcoreDemodSink::applyChannelSettings: m_interpolator.create:" + << " m_interpolatorDistance: " << m_interpolatorDistance + << " targetFrameSyncRate: " << targetFrameSyncRate + << " osFactor: " << m_osFactor; + } + + m_channelSampleRate = channelSampleRate; + m_bandwidth = bandwidth; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void MeshcoreDemodSink::applySettings(const MeshcoreDemodSettings& settings, bool force) +{ + qDebug() << "MeshcoreDemodSink::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 = m_loRaFFTInterpolation; + const bool fftInterpChanged = desiredFFTInterpolation != m_fftInterpolation; + + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) + || (settings.m_deBits != m_settings.m_deBits) + || fftInterpChanged + || force) + { + m_fftInterpolation = desiredFFTInterpolation; + initSF(settings.m_spreadFactor, settings.m_deBits); + } + + 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() << "MeshcoreDemodSink::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/demodmeshcore/meshcoredemodsink.h b/plugins/channelrx/demodmeshcore/meshcoredemodsink.h new file mode 100644 index 000000000..b4401d030 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodsink.h @@ -0,0 +1,202 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MESHCOREDEMODSINK_H +#define INCLUDE_MESHCOREDEMODSINK_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 "meshcoredemodsettings.h" + +class BasebandSampleSink; +class FFTEngine; +namespace MeshcoreDemodMsg { + class MsgDecodeSymbols; +} +class MessageQueue; + +class MeshcoreDemodSink : public ChannelSampleSink { +public: + MeshcoreDemodSink(); + ~MeshcoreDemodSink(); + + 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 MeshcoreDemodSettings& settings, bool force = false); + double getCurrentNoiseLevel() const { return m_magsqOffAvg.instantAverage() / (1< m_magsqOnAvg; + MovingAverageUtil m_magsqOffAvg; + MovingAverageUtil m_magsqTotalAvg; + std::queue m_magsqQueue; + + 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 + + void initSF(unsigned int sf, unsigned int deBits); //!< 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 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_MESHCOREDEMODSINK_H diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.cpp b/plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.cpp new file mode 100644 index 000000000..278884f35 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.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 "meshcoredemod.h" +#include "meshcoredemodwebapiadapter.h" + +MeshcoreDemodWebAPIAdapter::MeshcoreDemodWebAPIAdapter() +{} + +MeshcoreDemodWebAPIAdapter::~MeshcoreDemodWebAPIAdapter() +{} + +int MeshcoreDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setMeshtasticDemodSettings(new SWGSDRangel::SWGMeshtasticDemodSettings()); + response.getMeshtasticDemodSettings()->init(); + MeshcoreDemod::webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int MeshcoreDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + MeshcoreDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + MeshcoreDemod::webapiFormatChannelSettings(response, m_settings); + return 200; +} diff --git a/plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.h b/plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.h new file mode 100644 index 000000000..724e6179e --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoredemodwebapiadapter.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_MESHCOREDEMOD_WEBAPIADAPTER_H +#define INCLUDE_MESHCOREDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "meshcoredemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class MeshcoreDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + MeshcoreDemodWebAPIAdapter(); + virtual ~MeshcoreDemodWebAPIAdapter(); + + 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: + MeshcoreDemodSettings m_settings; +}; + +#endif // INCLUDE_MESHCOREDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodmeshcore/meshcorekeysdialog.cpp b/plugins/channelrx/demodmeshcore/meshcorekeysdialog.cpp new file mode 100644 index 000000000..7e49137b7 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcorekeysdialog.cpp @@ -0,0 +1,87 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2026 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 "meshcorekeysdialog.h" +#include "ui_meshcorekeysdialog.h" +#include "meshcorepacket.h" + +MeshcoreKeysDialog::MeshcoreKeysDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::MeshcoreKeysDialog) +{ + ui->setupUi(this); + validateCurrentInput(); +} + +MeshcoreKeysDialog::~MeshcoreKeysDialog() +{ + delete ui; +} + +void MeshcoreKeysDialog::setKeySpecList(const QString& keySpecList) +{ + ui->keyEditor->setPlainText(keySpecList); + validateCurrentInput(); +} + +QString MeshcoreKeysDialog::getKeySpecList() const +{ + return ui->keyEditor->toPlainText().trimmed(); +} + +void MeshcoreKeysDialog::on_validate_clicked() +{ + validateCurrentInput(); +} + +void MeshcoreKeysDialog::accept() +{ + if (!validateCurrentInput()) + { + QMessageBox::warning(this, tr("Invalid Keys"), tr("Fix the Meshcore key list before saving.")); + return; + } + + QDialog::accept(); +} + +bool MeshcoreKeysDialog::validateCurrentInput() +{ + const QString keyText = ui->keyEditor->toPlainText().trimmed(); + + if (keyText.isEmpty()) + { + ui->statusLabel->setStyleSheet("QLabel { color: #bbbbbb; }"); + ui->statusLabel->setText(tr("No custom keys set. Decoder will use environment/default keys.")); + return true; + } + + QString error; + int keyCount = 0; + + if (!modemmeshcore::Packet::validateKeySpecList(keyText, error, &keyCount)) + { + ui->statusLabel->setStyleSheet("QLabel { color: #ff5555; }"); + ui->statusLabel->setText(tr("Invalid key list: %1").arg(error)); + return false; + } + + ui->statusLabel->setStyleSheet("QLabel { color: #7cd67c; }"); + ui->statusLabel->setText(tr("Valid: %1 key(s) parsed").arg(keyCount)); + return true; +} diff --git a/plugins/channelrx/demodmeshcore/meshcorekeysdialog.h b/plugins/channelrx/demodmeshcore/meshcorekeysdialog.h new file mode 100644 index 000000000..e44306fb7 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcorekeysdialog.h @@ -0,0 +1,47 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2026 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_MESHCOREKEYSDIALOG_H +#define INCLUDE_MESHCOREKEYSDIALOG_H + +#include + +namespace Ui { +class MeshcoreKeysDialog; +} + +class MeshcoreKeysDialog : public QDialog +{ + Q_OBJECT + +public: + explicit MeshcoreKeysDialog(QWidget* parent = nullptr); + ~MeshcoreKeysDialog() override; + + void setKeySpecList(const QString& keySpecList); + QString getKeySpecList() const; + +private slots: + void on_validate_clicked(); + void accept() override; + +private: + bool validateCurrentInput(); + + Ui::MeshcoreKeysDialog* ui; +}; + +#endif // INCLUDE_MESHCOREKEYSDIALOG_H diff --git a/plugins/channelrx/demodmeshcore/meshcorekeysdialog.ui b/plugins/channelrx/demodmeshcore/meshcorekeysdialog.ui new file mode 100644 index 000000000..14291d378 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcorekeysdialog.ui @@ -0,0 +1,115 @@ + + + MeshcoreKeysDialog + + + + 0 + 0 + 760 + 460 + + + + Meshcore Keys + + + + + + MeshCore key spec — semicolon-separated entries: + + channel:<name>=<32hex> 16-byte channel PSK in hex + channel:public=public built-in public channel PSK + identity=<64hex> 32-byte Ed25519 public key + contact:<name>=<64hex> known peer pubkey with name + + Example: channel:public=public;identity=aabbccdd... + + + true + + + + + + + Enter one or more key specs used to decrypt Meshcore packets. + + + channel:public=public;contact:Alice=aabbccdd... + + + + + + + + + Validate key syntax and count without saving. + + + Validate + + + + + + + Validation status for the current key list. + + + true + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + MeshcoreKeysDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MeshcoreKeysDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channelrx/demodmeshcore/meshcoreplugin.cpp b/plugins/channelrx/demodmeshcore/meshcoreplugin.cpp new file mode 100644 index 000000000..7569a140b --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoreplugin.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 "meshcoreplugin.h" +#ifndef SERVER_MODE +#include "meshcoredemodgui.h" +#endif +#include "meshcoredemod.h" + +const PluginDescriptor MeshcorePlugin::m_pluginDescriptor = { + MeshcoreDemod::m_channelId, + QStringLiteral("MeshCore Demodulator"), + QStringLiteral("7.24.0"), + QStringLiteral("(c) Edouard Griffiths, F4EXB"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +MeshcorePlugin::MeshcorePlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& MeshcorePlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void MeshcorePlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register demodulator + m_pluginAPI->registerRxChannel(MeshcoreDemod::m_channelIdURI, MeshcoreDemod::m_channelId, this); +} + +void MeshcorePlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + MeshcoreDemod *instance = new MeshcoreDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* MeshcorePlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return nullptr; +} +#else +ChannelGUI* MeshcorePlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return MeshcoreDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif diff --git a/plugins/channelrx/demodmeshcore/meshcoreplugin.h b/plugins/channelrx/demodmeshcore/meshcoreplugin.h new file mode 100644 index 000000000..aeb0a3834 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/meshcoreplugin.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_MESHCOREPLUGIN_H +#define INCLUDE_MESHCOREPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class MeshcorePlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.meshcoredemod") + +public: + explicit MeshcorePlugin(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_MESHCOREPLUGIN_H diff --git a/plugins/channelrx/demodmeshcore/readme.md b/plugins/channelrx/demodmeshcore/readme.md new file mode 100644 index 000000000..565bfde22 --- /dev/null +++ b/plugins/channelrx/demodmeshcore/readme.md @@ -0,0 +1,56 @@ +

MeshCore demodulator plugin

+ +

Introduction

+ +This plugin demodulates and decodes incoming MeshCore wire packets. It is the +RX counterpart of the MeshCore modulator plugin (`channeltx/modmeshcore`) +and the standalone `lora_trx` headless transceiver in gr4-lora. + +The decode pipeline is derived from the Meshtastic demodulator +(LoRa-SDR-based, configurable BW/SF/CR/sync). MeshCore EU defaults +are baked in: **869.618 MHz / 62.5 kHz / SF 8 / CR 4/8**, sync word +0x12. + +

Status

+ +Functional surfaces: +- LoRa PHY decode covering MeshCore EU radio params (preamble, sync, + CFO, soft demod), with the following adjustments: `m_nbSymbolsMax` + default raised to 1023 (256 symbols are needed for a 120-byte + ADVERT at SF=8 / CR=4/8; the prior default of 255 clipped this); + channel filter cutoff widened to BW/1.25; interpolator upsample + regime in `feed()`; `m_chirp` initialization aligned to the + standard LoRa upchirp. +- `Packet::decodeFrame` (in `modemmeshcore`) for ADVERT / TXT_MSG / + GRP_TXT / ANON_REQ / ACK / PATH / CTRL packets. +- ADVERT signature verification (Ed25519 via vendored Monocypher). +- Auto-augmented PSK for public channels. +- Optional UDP JSON sink (`sendJsonViaUDP`) for off-process + observation. +- Radio parameters (BW, SF, CR, preamble length, frequency offset) + exposed via combo controls. BW combo includes 62500 / 125000 / + 250000 Hz — the bandwidths used by the MeshCore firmware CLI's + `set radio freq_MHz,bw_kHz,sf,cr` command. +- Preset combo populated with the well-known MeshCore regional + defaults (EU_NARROW, EU_LONG_RANGE, EU_MEDIUM_RANGE, AU, + AU_VICTORIA, CZ_NARROW, EU_433_LONG_RANGE, NZ, NZ_NARROW, PT_433, + PT_868, CH, USA, VN, USER). Default is `EU_NARROW` (869.618 MHz / + SF 8 / BW 62.5 kHz / CR 4/8). Selecting a preset applies its + freq/BW/SF/CR via `modemmeshcore::command::applyMeshcorePreset`. +- The Region and Channel controls are hidden: region is implicit + in the preset frequency, and group channels are managed via the + keys dialog rather than a numbered Channel selector. + +Outstanding: +- WebAPI schema reuses `SWGMeshtasticDemodSettings` / + `SWGMeshtasticDemodReport` as a placeholder; a dedicated + `SWGMeshcoreDemod*` schema would require regenerating the SWG + bindings. + +

References

+ +- MeshCore protocol: https://github.com/meshcore-dev/MeshCore +- gr4-lora python decoder: `scripts/src/lora/decoders/meshcore.py` +- gr4-lora python crypto: `scripts/src/lora/core/meshcore_crypto.py` +- Vendored Monocypher: https://monocypher.org (BSD-2-Clause OR CC0-1.0) +- Vendored tiny-AES-c: https://github.com/kokke/tiny-AES-c (public domain) diff --git a/plugins/channeltx/modmeshcore/CMakeLists.txt b/plugins/channeltx/modmeshcore/CMakeLists.txt new file mode 100644 index 000000000..d3f1a50a4 --- /dev/null +++ b/plugins/channeltx/modmeshcore/CMakeLists.txt @@ -0,0 +1,78 @@ +project(modmeshcore) + +set(modmeshcore_SOURCES + meshcoremod.cpp + meshcoremodsettings.cpp + meshcoremodsource.cpp + meshcoremodbaseband.cpp + meshcoremodplugin.cpp + meshcoremodencoder.cpp + meshcoremodencoderlora.cpp + meshcoremodwebapiadapter.cpp +) + +set(modmeshcore_HEADERS + meshcoremod.h + meshcoremodsettings.h + meshcoremodsource.h + meshcoremodbaseband.h + meshcoremodplugin.h + meshcoremodencoder.h + meshcoremodencoderlora.h + meshcoremodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${CMAKE_SOURCE_DIR}/modemmeshcore +) + +if(NOT SERVER_MODE) + set(modmeshcore_SOURCES + ${modmeshcore_SOURCES} + meshcoremodgui.cpp + meshcoremodgui.ui + ) + set(modmeshcore_HEADERS + ${modmeshcore_HEADERS} + meshcoremodgui.h + ) + + set(TARGET_NAME ${PLUGINS_PREFIX}modmeshcore) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME ${PLUGINSSRV_PREFIX}modmeshcoresrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +if(NOT Qt6_FOUND) + add_library(${TARGET_NAME} ${modmeshcore_SOURCES}) +else() + qt_add_plugin(${TARGET_NAME} CLASS_NAME MeshcoreModPlugin) + target_sources(${TARGET_NAME} PRIVATE ${modmeshcore_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 + modemmeshcore +) + +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/modmeshcore/meshcoremod.cpp b/plugins/channeltx/modmeshcore/meshcoremod.cpp new file mode 100644 index 000000000..ba16f12cf --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremod.cpp @@ -0,0 +1,889 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "SWGChannelActions.h" +#include "SWGChirpChatModReport.h" +#include "SWGMeshcoreModActions.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 "meshcorepacket.h" +#include "meshcoremodbaseband.h" +#include "meshcoremod.h" + +MESSAGE_CLASS_DEFINITION(MeshcoreMod::MsgConfigureMeshcoreMod, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreMod::MsgReportPayloadTime, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreMod::MsgSendMessage, Message) + +const char* const MeshcoreMod::m_channelIdURI = "sdrangel.channeltx.modmeshcore"; +const char* const MeshcoreMod::m_channelId = "MeshcoreMod"; + +MeshcoreMod::MeshcoreMod(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 MeshcoreModBaseband(); + 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, + &MeshcoreMod::networkManagerFinished + ); +} + +MeshcoreMod::~MeshcoreMod() +{ + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &MeshcoreMod::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this, true); + stop(); + delete m_basebandSource; + delete m_thread; +} + +void MeshcoreMod::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 MeshcoreMod::start() +{ + qDebug("MeshcoreMod::start"); + m_basebandSource->reset(); + m_thread->start(); +} + +void MeshcoreMod::stop() +{ + qDebug("MeshcoreMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void MeshcoreMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); +} + +bool MeshcoreMod::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshcoreMod::match(cmd)) + { + MsgConfigureMeshcoreMod& cfg = (MsgConfigureMeshcoreMod&) cmd; + qDebug() << "MeshcoreMod::handleMessage: MsgConfigureMeshcoreMod"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgSendMessage::match(cmd)) + { + qDebug() << "MeshcoreMod::handleMessage: MsgSendMessage"; + sendCurrentSettingsMessage(); + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "MeshcoreMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + + // Forward to the GUI + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(new DSPSignalNotification(notif)); + } + + return true; + } + else + { + return false; + } +} + +void MeshcoreMod::sendMessage() +{ + m_inputMessageQueue.push(MsgSendMessage::create()); +} + +void MeshcoreMod::sendCurrentSettingsMessage() +{ + MeshcoreModBaseband::MsgConfigureMeshcoreModPayload *payloadMsg = nullptr; + + m_symbols.clear(); + m_encoder.encode(m_settings, m_symbols); + payloadMsg = MeshcoreModBaseband::MsgConfigureMeshcoreModPayload::create(m_symbols); + + if (payloadMsg) + { + m_basebandSource->getInputMessageQueue()->push(payloadMsg); + m_currentPayloadTime = (m_symbols.size()*(1<push(rpt); + } + } +} + +void MeshcoreMod::setCenterFrequency(qint64 frequency) +{ + MeshcoreModSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMeshcoreMod *msgToGUI = MsgConfigureMeshcoreMod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void MeshcoreMod::applySettings(const MeshcoreModSettings& incomingSettings, bool force) +{ + MeshcoreModSettings settings(incomingSettings); + + qDebug() << "MeshcoreMod::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_bandwidthIndex + << " bandwidth: " << MeshcoreModSettings::bandwidths[settings.m_bandwidthIndex] + << " m_channelMute: " << settings.m_channelMute + << " m_textMessage: " << settings.m_textMessage + << " m_bytesMessage: " << settings.m_bytesMessage.toHex() + << " m_spreadFactor: " << settings.m_spreadFactor + << " m_deBits: " << settings.m_deBits + << " m_codingScheme: " << MeshcoreModSettings::m_codingScheme + << " m_nbParityBits: " << settings.m_nbParityBits + << " m_hasCRC: " << MeshcoreModSettings::m_hasCRC + << " m_hasHeader: " << MeshcoreModSettings::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_syncWord: " << settings.m_syncWord + << " 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_nbParityBits != m_settings.m_nbParityBits || force)) + { + reverseAPIKeys.append("nbParityBits"); + m_encoder.setLoRaParityBits(settings.m_nbParityBits); + } + + 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"); + } + if ((settings.m_syncWord != m_settings.m_syncWord) || force) { + reverseAPIKeys.append("syncWord"); + } + + MeshcoreModBaseband::MsgConfigureMeshcoreModPayload *payloadMsg = nullptr; + + const bool reencodePayload = force + || settings.m_textMessage != m_settings.m_textMessage + || settings.m_messageType != m_settings.m_messageType + || settings.m_meshIdentityPath != m_settings.m_meshIdentityPath + || settings.m_meshNodeName != m_settings.m_meshNodeName + || settings.m_meshAdvertLocationEnabled != m_settings.m_meshAdvertLocationEnabled + || settings.m_meshAdvertLat != m_settings.m_meshAdvertLat + || settings.m_meshAdvertLon != m_settings.m_meshAdvertLon + || settings.m_meshDestPubKeyHex != m_settings.m_meshDestPubKeyHex + || settings.m_meshGroupChannelName != m_settings.m_meshGroupChannelName + || settings.m_meshGroupChannelPskHex != m_settings.m_meshGroupChannelPskHex + || settings.m_meshAckMsgHashHex != m_settings.m_meshAckMsgHashHex; + + if (reencodePayload) + { + m_symbols.clear(); + m_encoder.encode(settings, m_symbols); + payloadMsg = MeshcoreModBaseband::MsgConfigureMeshcoreModPayload::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"); + } + + MeshcoreModBaseband::MsgConfigureMeshcoreModBaseband *msg = + MeshcoreModBaseband::MsgConfigureMeshcoreModBaseband::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 MeshcoreMod::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshcoreMod::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureMeshcoreMod *msg = MsgConfigureMeshcoreMod::create(m_settings, true); + m_inputMessageQueue.push(msg); + + return success; +} + +int MeshcoreMod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setMeshtasticModSettings(new SWGSDRangel::SWGMeshtasticModSettings()); + response.getMeshtasticModSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int MeshcoreMod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int MeshcoreMod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + MeshcoreModSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureMeshcoreMod *msg = MsgConfigureMeshcoreMod::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMeshcoreMod *msgToGUI = MsgConfigureMeshcoreMod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void MeshcoreMod::webapiUpdateChannelSettings( + MeshcoreModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getMeshtasticModSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("bandwidthIndex")) { + settings.m_bandwidthIndex = response.getMeshtasticModSettings()->getBandwidthIndex(); + } + if (channelSettingsKeys.contains("spreadFactor")) { + settings.m_spreadFactor = response.getMeshtasticModSettings()->getSpreadFactor(); + } + if (channelSettingsKeys.contains("deBits")) { + settings.m_deBits = response.getMeshtasticModSettings()->getDeBits(); + } + if (channelSettingsKeys.contains("preambleChirps")) { + settings.m_preambleChirps = response.getMeshtasticModSettings()->getPreambleChirps(); + } + if (channelSettingsKeys.contains("quietMillis")) { + settings.m_quietMillis = response.getMeshtasticModSettings()->getQuietMillis(); + } + if (channelSettingsKeys.contains("syncWord")) { + settings.m_syncWord = response.getMeshtasticModSettings()->getSyncWord(); + } + if (channelSettingsKeys.contains("syncWord")) { + settings.m_syncWord = response.getMeshtasticModSettings()->getSyncWord(); + } + if (channelSettingsKeys.contains("channelMute")) { + settings.m_channelMute = response.getMeshtasticModSettings()->getChannelMute() != 0; + } + if (channelSettingsKeys.contains("nbParityBits")) { + settings.m_nbParityBits = response.getMeshtasticModSettings()->getNbParityBits(); + } + if (channelSettingsKeys.contains("textMessage")) { + settings.m_textMessage = *response.getMeshtasticModSettings()->getTextMessage(); + } + if (channelSettingsKeys.contains("messageRepeat")) { + settings.m_messageRepeat = response.getMeshtasticModSettings()->getMessageRepeat(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getMeshtasticModSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getMeshtasticModSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getMeshtasticModSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("invertRamps")) { + settings.m_invertRamps = response.getMeshtasticModSettings()->getInvertRamps(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getMeshtasticModSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getMeshtasticModSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getMeshtasticModSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getMeshtasticModSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getMeshtasticModSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getMeshtasticModSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getMeshtasticModSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getMeshtasticModSettings()->getReverseApiChannelIndex(); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getMeshtasticModSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getMeshtasticModSettings()->getRollupState()); + } +} + +int MeshcoreMod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setMeshtasticModReport(new SWGSDRangel::SWGMeshtasticModReport()); + response.getMeshtasticModReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +int MeshcoreMod::webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage) +{ + SWGSDRangel::SWGMeshcoreModActions *swgMeshcoreModActions = query.getMeshcoreModActions(); + + if (swgMeshcoreModActions) + { + if (channelActionsKeys.contains("sendNow") && (swgMeshcoreModActions->getSendNow() != 0)) + { + sendMessage(); + } + return 202; + } + else + { + errorMessage = "Missing MeshcoreModActions in query"; + return 400; + } +} + +void MeshcoreMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const MeshcoreModSettings& settings) +{ + response.getMeshtasticModSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getMeshtasticModSettings()->setBandwidthIndex(settings.m_bandwidthIndex); + response.getMeshtasticModSettings()->setSpreadFactor(settings.m_spreadFactor); + response.getMeshtasticModSettings()->setDeBits(settings.m_deBits); + response.getMeshtasticModSettings()->setPreambleChirps(settings.m_preambleChirps); + response.getMeshtasticModSettings()->setQuietMillis(settings.m_quietMillis); + response.getMeshtasticModSettings()->setSyncWord(settings.m_syncWord); + response.getMeshtasticModSettings()->setChannelMute(settings.m_channelMute ? 1 : 0); + response.getMeshtasticModSettings()->setNbParityBits(settings.m_nbParityBits); + + if (response.getMeshtasticModSettings()->getTextMessage()) { + *response.getMeshtasticModSettings()->getTextMessage() = settings.m_textMessage; + } else { + response.getMeshtasticModSettings()->setTextMessage(new QString(settings.m_textMessage)); + } + + response.getMeshtasticModSettings()->setMessageRepeat(settings.m_messageRepeat); + response.getMeshtasticModSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getMeshtasticModSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getMeshtasticModSettings()->setUdpPort(settings.m_udpPort); + response.getMeshtasticModSettings()->setInvertRamps(settings.m_invertRamps ? 1 : 0); + + response.getMeshtasticModSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getMeshtasticModSettings()->getTitle()) { + *response.getMeshtasticModSettings()->getTitle() = settings.m_title; + } else { + response.getMeshtasticModSettings()->setTitle(new QString(settings.m_title)); + } + + response.getMeshtasticModSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getMeshtasticModSettings()->getReverseApiAddress()) { + *response.getMeshtasticModSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getMeshtasticModSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getMeshtasticModSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getMeshtasticModSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getMeshtasticModSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_channelMarker) + { + if (response.getMeshtasticModSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getMeshtasticModSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getMeshtasticModSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getMeshtasticModSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getMeshtasticModSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getMeshtasticModSettings()->setRollupState(swgRollupState); + } + } +} + +void MeshcoreMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + response.getMeshtasticModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); + response.getMeshtasticModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); + float fourthsMs = ((1<setPayloadTimeMs(m_currentPayloadTime); + response.getMeshtasticModReport()->setTotalTimeMs(m_currentPayloadTime + controlMs); + response.getMeshtasticModReport()->setSymbolTimeMs(4.0 * fourthsMs); + response.getMeshtasticModReport()->setPlaying(getModulatorActive() ? 1 : 0); +} + +void MeshcoreMod::webapiReverseSendSettings(QList& channelSettingsKeys, const MeshcoreModSettings& 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 MeshcoreMod::sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshcoreModSettings& 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 MeshcoreMod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshcoreModSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(1); // single source (Tx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString(m_channelId)); + swgChannelSettings->setMeshtasticModSettings(new SWGSDRangel::SWGMeshtasticModSettings()); + SWGSDRangel::SWGMeshtasticModSettings *swgMeshcoreModSettings = swgChannelSettings->getMeshtasticModSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgMeshcoreModSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("bandwidthIndex") || force) { + swgMeshcoreModSettings->setBandwidthIndex(settings.m_bandwidthIndex); + } + if (channelSettingsKeys.contains("spreadFactor") || force) { + swgMeshcoreModSettings->setSpreadFactor(settings.m_spreadFactor); + } + if (channelSettingsKeys.contains("deBits") || force) { + swgMeshcoreModSettings->setDeBits(settings.m_deBits); + } + if (channelSettingsKeys.contains("preambleChirps") || force) { + swgMeshcoreModSettings->setPreambleChirps(settings.m_preambleChirps); + } + if (channelSettingsKeys.contains("quietMillis") || force) { + swgMeshcoreModSettings->setQuietMillis(settings.m_quietMillis); + } + if (channelSettingsKeys.contains("syncWord") || force) { + swgMeshcoreModSettings->setSyncWord(settings.m_syncWord); + } + if (channelSettingsKeys.contains("channelMute") || force) { + swgMeshcoreModSettings->setChannelMute(settings.m_channelMute ? 1 : 0); + } + if (channelSettingsKeys.contains("nbParityBits") || force) { + swgMeshcoreModSettings->setNbParityBits(settings.m_nbParityBits); + } + if (channelSettingsKeys.contains("textMessage") || force) { + swgMeshcoreModSettings->setTextMessage(new QString(settings.m_textMessage)); + } + + if (channelSettingsKeys.contains("messageRepeat") || force) { + swgMeshcoreModSettings->setMessageRepeat(settings.m_messageRepeat); + } + + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgMeshcoreModSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgMeshcoreModSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgMeshcoreModSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("invertRamps") || force) { + swgMeshcoreModSettings->setInvertRamps(settings.m_invertRamps ? 1 : 0); + } + + if (channelSettingsKeys.contains("rgbColor") || force) { + swgMeshcoreModSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgMeshcoreModSettings->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); + swgMeshcoreModSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgMeshcoreModSettings->setRollupState(swgRollupState); + } +} + +void MeshcoreMod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "MeshcoreMod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("MeshcoreMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +double MeshcoreMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +void MeshcoreMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} + +uint32_t MeshcoreMod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSinkStreams(); +} + +bool MeshcoreMod::getModulatorActive() const +{ + return m_basebandSource->getActive(); +} + +void MeshcoreMod::openUDP(const MeshcoreModSettings& settings) +{ + closeUDP(); + m_udpSocket = new QUdpSocket(); + if (!m_udpSocket->bind(QHostAddress(settings.m_udpAddress), settings.m_udpPort)) + qCritical() << "MeshcoreMod::openUDP: Failed to bind to port " << settings.m_udpAddress << ":" << settings.m_udpPort << ". Error: " << m_udpSocket->error(); + else + qDebug() << "MeshcoreMod::openUDP: Listening for packets on " << settings.m_udpAddress << ":" << settings.m_udpPort; + connect(m_udpSocket, &QUdpSocket::readyRead, this, &MeshcoreMod::udpRx); +} + +void MeshcoreMod::closeUDP() +{ + if (m_udpSocket != nullptr) + { + disconnect(m_udpSocket, &QUdpSocket::readyRead, this, &MeshcoreMod::udpRx); + delete m_udpSocket; + m_udpSocket = nullptr; + } +} + +void MeshcoreMod::udpRx() +{ + while (m_udpSocket->hasPendingDatagrams()) + { + QNetworkDatagram datagram = m_udpSocket->receiveDatagram(); + MeshcoreModBaseband::MsgConfigureMeshcoreModPayload *payloadMsg = nullptr; + std::vector symbols; + + m_encoder.encodeBytes(datagram.data(), symbols); + payloadMsg = MeshcoreModBaseband::MsgConfigureMeshcoreModPayload::create(symbols); + + if (payloadMsg) + { + m_basebandSource->getInputMessageQueue()->push(payloadMsg); + m_currentPayloadTime = (symbols.size()*(1<push(rpt); + } + } + } +} diff --git a/plugins/channeltx/modmeshcore/meshcoremod.h b/plugins/channeltx/modmeshcore/meshcoremod.h new file mode 100644 index 000000000..a50b1e4c8 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremod.h @@ -0,0 +1,228 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MODMESHCORE_MESHCOREMOD_H_ +#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMOD_H_ + +#include +#include +#include + +#include +#include + +#include "dsp/basebandsamplesource.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "meshcoremodsettings.h" +#include "meshcoremodencoder.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class QUdpSocket; +class DeviceAPI; +class CWKeyer; +class MeshcoreModBaseband; +class ObjectPipe; + +class MeshcoreMod : public BasebandSampleSource, public ChannelAPI { +public: + class MsgConfigureMeshcoreMod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshcoreModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshcoreMod* create(const MeshcoreModSettings& settings, bool force) + { + return new MsgConfigureMeshcoreMod(settings, force); + } + + private: + MeshcoreModSettings m_settings; + bool m_force; + + MsgConfigureMeshcoreMod(const MeshcoreModSettings& 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) + {} + }; + + class MsgSendMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + static MsgSendMessage* create() + { + return new MsgSendMessage(); + } + + private: + MsgSendMessage() : + Message() + {} + }; + + //================================================================= + + MeshcoreMod(DeviceAPI *deviceAPI); + virtual ~MeshcoreMod(); + 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); + + virtual int webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const MeshcoreModSettings& settings); + + static void webapiUpdateChannelSettings( + MeshcoreModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); + uint32_t getNumberOfDeviceStreams() const; + bool getModulatorActive() const; + void sendMessage(); + + static const char* const m_channelIdURI; + static const char* const m_channelId; + +private: + DeviceAPI* m_deviceAPI; + QThread *m_thread; + MeshcoreModBaseband* m_basebandSource; + MeshcoreModEncoder m_encoder; // TODO: check if it needs to be on its own thread + MeshcoreModSettings 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 MeshcoreModSettings& settings, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const MeshcoreModSettings& settings, bool force); + void sendChannelSettings( + const QList& pipes, + QList& channelSettingsKeys, + const MeshcoreModSettings& settings, + bool force + ); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const MeshcoreModSettings& settings, + bool force + ); + void openUDP(const MeshcoreModSettings& settings); + void closeUDP(); + void sendCurrentSettingsMessage(); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void udpRx(); +}; + + +#endif /* PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMOD_H_ */ diff --git a/plugins/channeltx/modmeshcore/meshcoremodbaseband.cpp b/plugins/channeltx/modmeshcore/meshcoremodbaseband.cpp new file mode 100644 index 000000000..9a0a4d202 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodbaseband.cpp @@ -0,0 +1,204 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoremodbaseband.h" + +MESSAGE_CLASS_DEFINITION(MeshcoreModBaseband::MsgConfigureMeshcoreModBaseband, Message) +MESSAGE_CLASS_DEFINITION(MeshcoreModBaseband::MsgConfigureMeshcoreModPayload, Message) + +MeshcoreModBaseband::MeshcoreModBaseband() +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpChannelizer(&m_source); + + qDebug("MeshcoreModBaseband::MeshcoreModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &MeshcoreModBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +MeshcoreModBaseband::~MeshcoreModBaseband() +{ + delete m_channelizer; +} + +void MeshcoreModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void MeshcoreModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + handleInputMessages(); + + 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 MeshcoreModBaseband::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 MeshcoreModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void MeshcoreModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool MeshcoreModBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureMeshcoreModBaseband::match(cmd)) + { + qDebug() << "MeshcoreModBaseband::handleMessage: MsgConfigureMeshcoreModBaseband"; + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMeshcoreModBaseband& cfg = (MsgConfigureMeshcoreModBaseband&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureMeshcoreModPayload::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMeshcoreModPayload& cfg = (MsgConfigureMeshcoreModPayload&) cmd; + qDebug() << "MeshcoreModBaseband::handleMessage: MsgConfigureMeshcoreModPayload:" << 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())); + qWarning() << "MeshcoreModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + MeshcoreModSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelizer->getChannelFrequencyOffset() + ); + qWarning() << "MeshcoreModBaseband::handleMessage: post-DSPSignalNotification channelSampleRate: " << m_channelizer->getChannelSampleRate(); + + return true; + } + else + { + return false; + } +} + +void MeshcoreModBaseband::applySettings(const MeshcoreModSettings& settings, bool force) +{ + if ((settings.m_bandwidthIndex != m_settings.m_bandwidthIndex) + || (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + int thisBW = MeshcoreModSettings::bandwidths[settings.m_bandwidthIndex]; + m_channelizer->setChannelization( + thisBW * MeshcoreModSettings::oversampling, + settings.m_inputFrequencyOffset + ); + m_source.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + thisBW, + m_channelizer->getChannelFrequencyOffset() + ); + } + + m_source.applySettings(settings, force); + + m_settings = settings; +} + +int MeshcoreModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodbaseband.h b/plugins/channeltx/modmeshcore/meshcoremodbaseband.h new file mode 100644 index 000000000..d64fe1d8d --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodbaseband.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_MESHCOREMODBASEBAND_H +#define INCLUDE_MESHCOREMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "meshcoremodsource.h" + +class UpChannelizer; + +class MeshcoreModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureMeshcoreModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MeshcoreModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureMeshcoreModBaseband* create(const MeshcoreModSettings& settings, bool force) + { + return new MsgConfigureMeshcoreModBaseband(settings, force); + } + + private: + MeshcoreModSettings m_settings; + bool m_force; + + MsgConfigureMeshcoreModBaseband(const MeshcoreModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureMeshcoreModPayload : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const std::vector& getPayload() const { return m_payload; } + + static MsgConfigureMeshcoreModPayload* create() { + return new MsgConfigureMeshcoreModPayload(); + } + static MsgConfigureMeshcoreModPayload* create(const std::vector& payload) { + return new MsgConfigureMeshcoreModPayload(payload); + } + + private: + std::vector m_payload; + + MsgConfigureMeshcoreModPayload() : // This is empty payload notification + Message() + {} + MsgConfigureMeshcoreModPayload(const std::vector& payload) : + Message() + { m_payload = payload; } + }; + + MeshcoreModBaseband(); + ~MeshcoreModBaseband(); + 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; + MeshcoreModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MeshcoreModSettings m_settings; + QRecursiveMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const MeshcoreModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_MESHCOREMODBASEBAND_H diff --git a/plugins/channeltx/modmeshcore/meshcoremodencoder.cpp b/plugins/channeltx/modmeshcore/meshcoremodencoder.cpp new file mode 100644 index 000000000..c2e00472d --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodencoder.cpp @@ -0,0 +1,255 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoremodencoder.h" +#include "meshcoremodencoderlora.h" +#include "meshcorepacket.h" +#include "meshcore_identity.h" + +namespace +{ + +// Translate `MeshcoreModSettings::MessageType` + identity into a single +// `MESHCORE: type=...; ...` command string that `Packet::buildFrameFromCommand` +// already knows how to render into wire bytes. All optional knobs (lat/lon, +// txt_type, route, ts) come from settings; the seed is loaded from the +// shared identity store on demand. +QString buildMeshcoreCommandFromSettings(const MeshcoreModSettings& s) +{ + auto loadSeedHex = [&]() -> QString { + const QString path = s.m_meshIdentityPath.isEmpty() + ? modemmeshcore::identity::defaultIdentityPath() + : s.m_meshIdentityPath; + modemmeshcore::identity::Identity id = + modemmeshcore::identity::loadOrCreateIdentity(path); + return id.isValid() ? id.seedHex() : QString(); + }; + + auto resolvedNodeName = [&]() -> QString { + if (!s.m_meshNodeName.isEmpty()) return s.m_meshNodeName; + const QString path = s.m_meshIdentityPath.isEmpty() + ? modemmeshcore::identity::defaultIdentityPath() + : s.m_meshIdentityPath; + modemmeshcore::identity::Identity id = + modemmeshcore::identity::loadOrCreateIdentity(path); + return modemmeshcore::identity::defaultNodeNameFor(id); + }; + + const quint64 now = static_cast(QDateTime::currentSecsSinceEpoch()); + QStringList toks; + + switch (s.m_messageType) + { + case MeshcoreModSettings::MessageText: + // Caller-managed: m_textMessage already carries either a literal or + // a MESHCORE: command. Encoder's existing path handles both. + return QString(); + + case MeshcoreModSettings::MessageAdvert: { + const QString seed = loadSeedHex(); + if (seed.isEmpty()) return QString(); + toks << "type=advert" + << "seed=" + seed + << "name=" + resolvedNodeName() + << "ts=" + QString::number(now) + << "route=flood"; + if (s.m_meshAdvertLocationEnabled) { + toks << QString("lat=%1").arg(s.m_meshAdvertLat, 0, 'f', 6); + toks << QString("lon=%1").arg(s.m_meshAdvertLon, 0, 'f', 6); + } + break; + } + + case MeshcoreModSettings::MessageTxtMsg: { + const QString seed = loadSeedHex(); + if (seed.isEmpty() || s.m_meshDestPubKeyHex.isEmpty()) return QString(); + toks << "type=txt_msg" + << "seed=" + seed + << "dest=" + s.m_meshDestPubKeyHex + << "ts=" + QString::number(now) + << "text=" + s.m_textMessage; + break; + } + + case MeshcoreModSettings::MessageGrpTxt: { + toks << "type=grp_txt"; + if (!s.m_meshGroupChannelPskHex.isEmpty()) { + toks << "channel_psk=" + s.m_meshGroupChannelPskHex; + } else { + toks << "channel=" + (s.m_meshGroupChannelName.isEmpty() + ? QStringLiteral("public") + : s.m_meshGroupChannelName); + } + toks << "ts=" + QString::number(now) + << "text=" + s.m_textMessage; + break; + } + + case MeshcoreModSettings::MessageAnonReq: { + const QString seed = loadSeedHex(); + if (seed.isEmpty() || s.m_meshDestPubKeyHex.isEmpty()) return QString(); + toks << "type=anon_req" + << "seed=" + seed + << "dest=" + s.m_meshDestPubKeyHex + << "ts=" + QString::number(now) + << "data=text:" + s.m_textMessage; + break; + } + + case MeshcoreModSettings::MessageAck: { + if (s.m_meshDestPubKeyHex.isEmpty() || s.m_meshAckMsgHashHex.isEmpty()) { + return QString(); + } + toks << "type=ack" + << "dest=" + s.m_meshDestPubKeyHex + << "msg_hash=" + s.m_meshAckMsgHashHex; + break; + } + } + + if (toks.isEmpty()) return QString(); + return QStringLiteral("MESHCORE: ") + toks.join(QStringLiteral("; ")); +} + +} // anonymous + +const MeshcoreModSettings::CodingScheme MeshcoreModEncoder::m_codingScheme = MeshcoreModSettings::CodingLoRa; +const bool MeshcoreModEncoder::m_hasCRC = true; +const bool MeshcoreModEncoder::m_hasHeader = true; + +MeshcoreModEncoder::MeshcoreModEncoder() : + m_nbSymbolBits(5), + m_nbParityBits(1) +{} + +MeshcoreModEncoder::~MeshcoreModEncoder() +{} + +void MeshcoreModEncoder::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 MeshcoreModEncoder::encode(MeshcoreModSettings settings, std::vector& symbols) +{ + if (m_nbSymbolBits < 5) { + return; + } + + QByteArray bytes; + QString summary; + QString error; + + // Synthesise a MESHCORE: command from m_messageType + identity store + + // settings fields. For MessageText we fall through to the legacy path + // which honours either a literal payload or a hand-typed MESHCORE: line + // in m_textMessage. + QString command; + if (settings.m_messageType != MeshcoreModSettings::MessageText) { + command = buildMeshcoreCommandFromSettings(settings); + } else if (modemmeshcore::Packet::isCommand(settings.m_textMessage)) { + command = settings.m_textMessage; + } + + if (!command.isEmpty()) + { + if (!modemmeshcore::Packet::buildFrameFromCommand(command, bytes, summary, error)) + { + qWarning() << "MeshcoreModEncoder::encode: Meshcore command error:" + << error << " command=" << command; + return; + } + qInfo() << "MeshcoreModEncoder::encode:" << summary; + } + else + { + bytes = settings.m_textMessage.toUtf8(); + } + + encodeBytesLoRa(bytes, symbols); +} + +void MeshcoreModEncoder::encodeBytes(const QByteArray& bytes, std::vector& symbols) +{ + switch (m_codingScheme) + { + case MeshcoreModSettings::CodingLoRa: + { + QByteArray payload(bytes); + + if (modemmeshcore::Packet::isCommand(QString::fromUtf8(bytes))) + { + QString summary; + QString error; + + if (!modemmeshcore::Packet::buildFrameFromCommand(QString::fromUtf8(bytes), payload, summary, error)) + { + qWarning() << "MeshcoreModEncoder::encodeBytes: Meshcore command error:" << error; + return; + } + + qInfo() << "MeshcoreModEncoder::encodeBytes:" << summary; + } + + encodeBytesLoRa(payload, symbols); + break; + } + default: + break; + }; +} + +void MeshcoreModEncoder::encodeBytesLoRa(const QByteArray& bytes, std::vector& symbols) +{ + QByteArray payload(bytes); + + if ((payload.size() + (m_hasCRC ? 2 : 0)) > 255) + { + qWarning() << "MeshcoreModEncoder::encodeBytesLoRa: payload too large:" << payload.size(); + return; + } + + if (m_hasCRC) { + MeshcoreModEncoderLoRa::addChecksum(payload); + } + + const unsigned int headerNbSymbolBits = (m_hasHeader && (m_spreadFactor > 2U)) + ? (m_spreadFactor - 2U) + : m_nbSymbolBits; + + MeshcoreModEncoderLoRa::encodeBytes( + payload, + symbols, + m_nbSymbolBits, + headerNbSymbolBits, + m_hasHeader, + m_hasCRC, + m_nbParityBits + ); +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodencoder.h b/plugins/channeltx/modmeshcore/meshcoremodencoder.h new file mode 100644 index 000000000..643316204 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodencoder.h @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MODMESHCORE_MESHCOREMODENCODER_H_ +#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODER_H_ + +#include +#include "meshcoremodsettings.h" + +class MeshcoreModEncoder +{ +public: + MeshcoreModEncoder(); + ~MeshcoreModEncoder(); + + void setNbSymbolBits(unsigned int spreadFactor, unsigned int deBits); + void setLoRaParityBits(unsigned int parityBits) { m_nbParityBits = parityBits; } + void encodeBytes(const QByteArray& bytes, std::vector& symbols); + void encode(MeshcoreModSettings settings, std::vector& symbols); + +private: + // LoRa functions + void encodeBytesLoRa(const QByteArray& bytes, std::vector& symbols); + + // General attributes + static const MeshcoreModSettings::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 + static const bool m_hasCRC; + static const bool m_hasHeader; +}; + +#endif // PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODER_H_ diff --git a/plugins/channeltx/modmeshcore/meshcoremodencoderlora.cpp b/plugins/channeltx/modmeshcore/meshcoremodencoderlora.cpp new file mode 100644 index 000000000..e4ca276dd --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodencoderlora.cpp @@ -0,0 +1,225 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoremodencoderlora.h" + +void MeshcoreModEncoderLoRa::addChecksum(QByteArray& bytes) +{ + // Standard LoRa payload CRC per lora_payload_crc (gr4-lora crc.hpp): + // 1. CRC-16-CCITT (0x1021) over first (size - 2) bytes + // 2. XOR result with last 2 bytes as uint16 LE + // This matches the SX1262 hardware CRC in explicit header mode. + if (bytes.size() < 2) { + bytes.append(static_cast(0)); + bytes.append(static_cast(0)); + return; + } + + uint16_t crc = 0x0000; + for (int i = 0; i < bytes.size() - 2; i++) { + crc ^= static_cast(static_cast(bytes[i])) << 8; + for (int j = 0; j < 8; j++) { + if (crc & 0x8000) { + crc = static_cast((crc << 1) ^ 0x1021); + } else { + crc = static_cast(crc << 1); + } + } + } + + crc ^= static_cast(static_cast(bytes[bytes.size() - 1])); + crc ^= static_cast(static_cast(bytes[bytes.size() - 2])) << 8; + + bytes.append(static_cast(crc & 0xff)); + bytes.append(static_cast((crc >> 8) & 0xff)); +} + +void MeshcoreModEncoderLoRa::encodeBytes( + const QByteArray& bytes, + std::vector& symbols, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + bool hasHeader, + bool hasCRC, + unsigned int nbParityBits +) +{ + if (payloadNbSymbolBits < 5) { + return; + } + + if (hasHeader && (headerNbSymbolBits < headerCodewords)) { + return; + } + + const unsigned int payloadNibbleCount = bytes.size() * 2U; + const unsigned int firstBlockCodewords = hasHeader ? headerNbSymbolBits : payloadNbSymbolBits; + const unsigned int headerSize = hasHeader ? headerCodewords : 0U; + const unsigned int payloadInFirstBlock = firstBlockCodewords > headerSize + ? std::min(payloadNibbleCount, firstBlockCodewords - headerSize) + : 0U; + const unsigned int remainingPayloadNibbles = payloadNibbleCount > payloadInFirstBlock + ? (payloadNibbleCount - payloadInFirstBlock) + : 0U; + const unsigned int remainingCodewords = remainingPayloadNibbles > 0U + ? roundUp(remainingPayloadNibbles, payloadNbSymbolBits) + : 0U; + const unsigned int numCodewords = firstBlockCodewords + remainingCodewords; + + 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); + // Standard LoRa header checksum per Tapparel & Burg Section III-A. + // XOR-based checksum from 3 header nibbles: + // n0 = length_hi, n1 = length_lo, n2 = (cr<<1)|has_crc + { + uint8_t n0 = (hdr[0] >> 4) & 0x0F; + uint8_t n1 = hdr[0] & 0x0F; + uint8_t n2 = hdr[1] & 0x0F; + bool a0 = (n0 >> 3) & 1, a1 = (n0 >> 2) & 1, a2 = (n0 >> 1) & 1, a3 = n0 & 1; + bool a4 = (n1 >> 3) & 1, a5 = (n1 >> 2) & 1, a6 = (n1 >> 1) & 1, a7 = n1 & 1; + bool a8 = (n2 >> 3) & 1, a9 = (n2 >> 2) & 1, a10 = (n2 >> 1) & 1, a11 = n2 & 1; + bool c4 = a0 ^ a1 ^ a2 ^ a3; + bool c3 = a0 ^ a4 ^ a5 ^ a6 ^ a11; + bool c2 = a1 ^ a4 ^ a7 ^ a8 ^ a10; + bool c1 = a2 ^ a5 ^ a7 ^ a9 ^ a10 ^ a11; + bool c0 = a3 ^ a6 ^ a8 ^ a9 ^ a10 ^ a11; + hdr[2] = static_cast((c4 << 4) | (c3 << 3) | (c2 << 2) | (c1 << 1) | c0); + } + + // 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); + } + + // Pre-FEC whitening: whiten data nibbles before Hamming FEC encoding. + // Standard LoRa order: payload -> whiten -> Hamming FEC -> interleave -> gray. + // CRC nibbles are NOT whitened — only actual payload data. + const unsigned int payloadNibblesOnly = bytes.size() * 2U - (hasCRC ? 4U : 0U); + const unsigned int totalPayloadNibbles = firstBlockCodewords > headerSize + ? (firstBlockCodewords - headerSize + remainingCodewords) + : 0U; + + if (totalPayloadNibbles > 0U) + { + std::vector nibbles(totalPayloadNibbles, 0); + const uint8_t *rawBytes = reinterpret_cast(bytes.data()); + + for (unsigned int i = 0; i < totalPayloadNibbles; i++) + { + unsigned int byteIdx = i / 2; + if (byteIdx < static_cast(bytes.size())) { + nibbles[i] = (i % 2 == 0) + ? (rawBytes[byteIdx] & 0xf) + : ((rawBytes[byteIdx] >> 4) & 0xf); + } + } + + // Whiten payload nibbles only (not CRC nibbles). + if (payloadNibblesOnly > 0) { + loRaWhitenNibbles(nibbles.data(), std::min(payloadNibblesOnly, totalPayloadNibbles), 0); + } + + // Fill first interleaver block (explicit header + first payload codewords) with 4/8 FEC. + if (firstBlockCodewords > headerSize) + { + const unsigned int payloadNibblesInFirst = firstBlockCodewords - headerSize; + + for (unsigned int i = 0; i < payloadNibblesInFirst; i++, dOfs++) { + codewords[cOfs++] = encodeHamming84sx(nibbles[i]); + } + } + + // Encode remaining payload blocks with payload coding rate. + if (remainingCodewords > 0U) + { + const unsigned int payloadNibblesInFirst = firstBlockCodewords - headerSize; + + for (unsigned int i = 0; i < remainingCodewords; i++, dOfs++) + { + uint8_t nib = nibbles[payloadNibblesInFirst + i]; + if (nbParityBits == 1) { + codewords[cOfs++] = encodeParity54(nib); + } else if (nbParityBits == 2) { + codewords[cOfs++] = encodeParity64(nib); + } else if (nbParityBits == 3) { + codewords[cOfs++] = encodeHamming74sx(nib); + } else { + codewords[cOfs++] = encodeHamming84sx(nib); + } + } + } + } + + const unsigned int numSymbols = hasHeader + ? (headerSymbols + (remainingCodewords / payloadNbSymbolBits) * (4U + nbParityBits)) + : ((numCodewords / payloadNbSymbolBits) * (4U + nbParityBits)); + + // interleave the codewords into symbols + symbols.clear(); + symbols.resize(numSymbols); + + if (hasHeader) + { + diagonalInterleaveSx(codewords.data(), firstBlockCodewords, symbols.data(), headerNbSymbolBits, headerParityBits); + + // Add even parity bit at position headerNbSymbolBits for each + // header symbol. Standard LoRa header uses reduced rate: + // sf_app = sf-2 data bits + 1 even parity bit + zero padding. + for (unsigned int i = 0; i < headerSymbols; i++) { + bool parity = false; + for (unsigned int b = 0; b < headerNbSymbolBits; b++) { + parity ^= static_cast((symbols[i] >> b) & 1); + } + symbols[i] |= (static_cast(parity) << headerNbSymbolBits); + } + + if (remainingCodewords > 0U) { + diagonalInterleaveSx( + codewords.data() + firstBlockCodewords, + remainingCodewords, + symbols.data() + headerSymbols, + payloadNbSymbolBits, + nbParityBits + ); + } + } + else + { + diagonalInterleaveSx(codewords.data(), numCodewords, symbols.data(), payloadNbSymbolBits, nbParityBits); + } + + // gray decode + for (auto &sym : symbols) { + sym = grayToBinary16(sym); + } +} + + diff --git a/plugins/channeltx/modmeshcore/meshcoremodencoderlora.h b/plugins/channeltx/modmeshcore/meshcoremodencoderlora.h new file mode 100644 index 000000000..2f90a080d --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodencoderlora.h @@ -0,0 +1,301 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MODMESHCORE_MESHCOREMODENCODERLORA_H_ +#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODERLORA_H_ + +#include +#include + +class MeshcoreModEncoderLoRa +{ +public: + static void addChecksum(QByteArray& bytes); + static void encodeBytes( + const QByteArray& bytes, + std::vector& symbols, + unsigned int payloadNbSymbolBits, + unsigned int headerNbSymbolBits, + bool hasHeader, + bool hasCRC, + unsigned int nbParityBits + ); + +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; + } + + /*********************************************************************** + * Standard LoRa Hamming(8,4) encode — LUT from gr4-lora hamming.hpp. + * Matches the SX1262 hardware FEC decoder. SDRangel's bit-level + * Hamming_84sx uses a different parity matrix and produces + * incompatible codewords. + **********************************************************************/ + static inline unsigned char encodeHamming84sx(const unsigned char x) + { + // Standard LoRa Hamming(8,4) codewords per Semtech/LoRa spec. + // Each 4-bit nibble maps to an 8-bit codeword (data + parity). + static constexpr unsigned char kHammingCW[16] = { + 0, 23, 45, 58, 78, 89, 99, 116, + 139, 156, 166, 177, 197, 210, 232, 255 + }; + return kHammingCW[x & 0xf]; + } + + /*********************************************************************** + * 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; + } + + /*********************************************************************** + * Standard LoRa whitening sequence (polynomial 0x21, 255 bytes). + * Derived from the LoRa LFSR: x^9 + x^5 + 1, init 0x100. + * Source: gr4-lora tables.hpp whitening_seq. + **********************************************************************/ + static constexpr uint8_t kLoRaWhiteningSeq[255] = { + 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 + }; + + /// Apply standard LoRa nibble whitening to payload nibbles (not CRC). + /// nibbles array contains 4-bit values per element. byteOffset is the + /// starting byte index into the whitening sequence. + static inline void loRaWhitenNibbles(uint8_t *nibbles, unsigned int count, unsigned int byteOffset) + { + for (unsigned int i = 0; i < count; i++) { + unsigned int byteIdx = (byteOffset + i / 2) % 255; + uint8_t mask = (i % 2 == 0) + ? (kLoRaWhiteningSeq[byteIdx] & 0x0F) + : ((kLoRaWhiteningSeq[byteIdx] >> 4) & 0x0F); + nibbles[i] ^= mask; + } + } + + /*********************************************************************** + * 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 = (k - m - 1 + 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_MODMESHCORE_MESHCOREMODENCODERLORA_H_ diff --git a/plugins/channeltx/modmeshcore/meshcoremodgui.cpp b/plugins/channeltx/modmeshcore/meshcoremodgui.cpp new file mode 100644 index 000000000..0b3adbd34 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodgui.cpp @@ -0,0 +1,1280 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Alejandro Aleman // +// Copyright (C) 2020-2026 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "meshcore_identity.h" + +#include "device/deviceuiset.h" +#include "device/deviceapi.h" +#include "dsp/devicesamplesink.h" +#include "dsp/devicesamplemimo.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_meshcoremodgui.h" +#include "meshcoremodgui.h" +#include "meshcorepacket.h" + + +MeshcoreModGUI* MeshcoreModGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx) +{ + MeshcoreModGUI* gui = new MeshcoreModGUI(pluginAPI, deviceUISet, channelTx); + return gui; +} + +void MeshcoreModGUI::destroy() +{ + delete this; +} + +void MeshcoreModGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray MeshcoreModGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool MeshcoreModGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool MeshcoreModGUI::handleMessage(const Message& message) +{ + if (MeshcoreMod::MsgConfigureMeshcoreMod::match(message)) + { + const MeshcoreMod::MsgConfigureMeshcoreMod& cfg = (MeshcoreMod::MsgConfigureMeshcoreMod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (MeshcoreMod::MsgReportPayloadTime::match(message)) + { + const MeshcoreMod::MsgReportPayloadTime& rpt = (MeshcoreMod::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() << "MeshcoreModGUI::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 MeshcoreModGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void MeshcoreModGUI::handleSourceMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +QString MeshcoreModGUI::getActivePayloadText() const +{ + switch (m_settings.m_messageType) + { + case MeshcoreModSettings::MessageText: + return m_settings.m_textMessage; + case MeshcoreModSettings::MessageGrpTxt: + case MeshcoreModSettings::MessageTxtMsg: + case MeshcoreModSettings::MessageAnonReq: + // For encrypted text payloads the same edit field carries the plaintext. + return m_settings.m_textMessage; + default: + return QString(); + } +} + +int MeshcoreModGUI::findBandwidthIndex(int bandwidthHz) const +{ + int bestIndex = -1; + int bestDelta = 1 << 30; + + for (int i = 0; i < MeshcoreModSettings::nbBandwidths; ++i) + { + const int delta = std::abs(MeshcoreModSettings::bandwidths[i] - bandwidthHz); + if (delta < bestDelta) + { + bestDelta = delta; + bestIndex = i; + } + } + + return bestIndex; +} + +bool MeshcoreModGUI::retuneDeviceToFrequency(qint64 centerFrequencyHz) +{ + if (!m_deviceUISet || !m_deviceUISet->m_deviceAPI) { + return false; + } + + DeviceAPI* deviceAPI = m_deviceUISet->m_deviceAPI; + + if (deviceAPI->getDeviceSinkEngine() && deviceAPI->getSampleSink()) + { + deviceAPI->getSampleSink()->setCenterFrequency(centerFrequencyHz); + return true; + } + + if (deviceAPI->getDeviceMIMOEngine() && deviceAPI->getSampleMIMO()) + { + deviceAPI->getSampleMIMO()->setSinkCenterFrequency(centerFrequencyHz, m_settings.m_streamIndex); + return true; + } + + return false; +} + +void MeshcoreModGUI::applyMeshcoreProfileFromSelection() +{ + const QString region = ui->meshRegion->currentText(); + const QString preset = ui->meshPreset->currentText(); + const int meshChannel = ui->meshChannel->currentData().toInt(); + const int channelNum = meshChannel + 1; // planner expects 1-based channel_num + + if (region.isEmpty() || preset.isEmpty()) { + return; + } + + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum); + modemmeshcore::TxRadioSettings meshRadio; + QString error; + + if (!modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error)) + { + qWarning() << "MeshcoreModGUI::applyMeshcoreProfileFromSelection:" << error; + return; + } + + bool changed = false; + bool selectionStateChanged = false; + + if (m_settings.m_meshcoreRegionCode != region) + { + m_settings.m_meshcoreRegionCode = region; + selectionStateChanged = true; + } + if (m_settings.m_meshcorePresetName != preset) + { + m_settings.m_meshcorePresetName = preset; + selectionStateChanged = true; + } + if (m_settings.m_meshcoreChannelIndex != meshChannel) + { + m_settings.m_meshcoreChannelIndex = meshChannel; + selectionStateChanged = 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; + } + + const int meshPreambleChirps = meshRadio.preambleChirps; + if (m_settings.m_preambleChirps != static_cast(meshPreambleChirps)) + { + m_settings.m_preambleChirps = static_cast(meshPreambleChirps); + changed = true; + } + + if (meshRadio.syncWord != m_settings.m_syncWord) + { + m_settings.m_syncWord = meshRadio.syncWord; + changed = true; + } + + if (meshRadio.hasCenterFrequency) + { + if (retuneDeviceToFrequency(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; + const qint64 maxOffset = m_basebandSampleRate / 2; + + if (std::abs(wantedOffset) <= maxOffset) + { + if (wantedOffset != m_settings.m_inputFrequencyOffset) + { + m_settings.m_inputFrequencyOffset = static_cast(wantedOffset); + changed = true; + } + } + else + { + qWarning() << "MeshcoreModGUI::applyMeshcoreProfileFromSelection: requested frequency" + << meshRadio.centerFrequencyHz + << "is out of channel offset range with current baseband sample rate"; + } + } + else + { + qWarning() << "MeshcoreModGUI::applyMeshcoreProfileFromSelection: device center frequency unknown, cannot auto-center"; + } + } + + if (!changed && !selectionStateChanged) { + return; + } + + qInfo() << "MeshcoreModGUI::applyMeshcoreProfileFromSelection:" << meshRadio.summary; + + if (!changed) + { + applySettings(); + return; + } + + const int thisBW = MeshcoreModSettings::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->preambleChirps->setValue(m_settings.m_preambleChirps); + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + 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(); + applySettings(); +} + +void MeshcoreModGUI::rebuildMeshcoreChannelOptions() +{ + const QString region = ui->meshRegion->currentText(); + const QString preset = ui->meshPreset->currentText(); + const int previousChannel = ui->meshChannel->currentData().toInt(); + + m_meshControlsUpdating = true; + ui->meshChannel->clear(); + + int added = 0; + for (int meshChannel = 0; meshChannel <= 200; ++meshChannel) + { + modemmeshcore::TxRadioSettings meshRadio; + QString error; + const int channelNum = meshChannel + 1; // planner expects 1-based channel_num + const QString command = QString("MESH:preset=%1;region=%2;channel_num=%3").arg(preset, region).arg(channelNum); + + if (!modemmeshcore::Packet::deriveTxRadioSettings(command, meshRadio, error)) + { + if (added > 0) { + break; + } else { + continue; + } + } + + const QString label = meshRadio.hasCenterFrequency + ? QString("%1 (%2 MHz)").arg(meshChannel).arg(meshRadio.centerFrequencyHz / 1000000.0, 0, 'f', 3) + : QString::number(meshChannel); + + ui->meshChannel->addItem(label, meshChannel); + added++; + } + + if (added == 0) { + ui->meshChannel->addItem("0", 0); + } + + ui->meshChannel->setToolTip(tr("Meshcore channel number (%1 available for %2/%3)") + .arg(added) + .arg(region) + .arg(preset)); + int restoreIndex = ui->meshChannel->findData(previousChannel); + if (restoreIndex < 0) { + restoreIndex = 0; + } + ui->meshChannel->setCurrentIndex(restoreIndex); + m_meshControlsUpdating = false; + + qInfo() << "MeshcoreModGUI::rebuildMeshcoreChannelOptions:" + << "region=" << region + << "preset=" << preset + << "channels=" << added; + + QMetaObject::invokeMethod(this, [this]() { + if (!m_meshControlsUpdating) { + applyMeshcoreProfileFromSelection(); + } + }, Qt::QueuedConnection); +} + +void MeshcoreModGUI::setupMeshcoreAutoProfileControls() +{ + // Hide Region and Channel controls — region is implicit in each + // preset's frequency, and MeshCore has no numbered Channel concept. + // Preset combo + Apply button stay visible (see + // modemmeshcore::command::applyMeshcorePreset). + if (ui->meshRegionLabel) ui->meshRegionLabel->hide(); + if (ui->meshRegion) ui->meshRegion->hide(); + if (ui->meshChannelLabel) ui->meshChannelLabel->hide(); + if (ui->meshChannel) ui->meshChannel->hide(); +} + +void MeshcoreModGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void MeshcoreModGUI::on_bw_valueChanged(int value) +{ + if (value < 0) { + m_settings.m_bandwidthIndex = 0; + } else if (value < MeshcoreModSettings::nbBandwidths) { + m_settings.m_bandwidthIndex = value; + } else { + m_settings.m_bandwidthIndex = MeshcoreModSettings::nbBandwidths - 1; + } + + int thisBW = MeshcoreModSettings::bandwidths[value]; + ui->bwText->setText(QString("%1 Hz").arg(thisBW)); + m_channelMarker.setBandwidth(thisBW); + + applySettings(); +} + +void MeshcoreModGUI::on_channelMute_toggled(bool checked) +{ + m_settings.m_channelMute = checked; + applySettings(); +} + +void MeshcoreModGUI::on_spread_valueChanged(int value) +{ + m_settings.m_spreadFactor = value; + ui->spreadText->setText(tr("%1").arg(value)); + + applySettings(); +} + +void MeshcoreModGUI::on_deBits_valueChanged(int value) +{ + m_settings.m_deBits = value; + ui->deBitsText->setText(tr("%1").arg(m_settings.m_deBits)); + applySettings(); +} + +void MeshcoreModGUI::on_preambleChirps_valueChanged(int value) +{ + m_settings.m_preambleChirps = value; + ui->preambleChirpsText->setText(tr("%1").arg(m_settings.m_preambleChirps)); + applySettings(); +} + +void MeshcoreModGUI::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 MeshcoreModGUI::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 MeshcoreModGUI::on_fecParity_valueChanged(int value) +{ + m_settings.m_nbParityBits = value; + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + applySettings(); +} + +void MeshcoreModGUI::on_playMessage_clicked(bool checked) +{ + (void) checked; + applySettings(); + m_meshcoreMod->sendMessage(); +} + +void MeshcoreModGUI::on_repeatMessage_valueChanged(int value) +{ + m_settings.m_messageRepeat = value; + ui->repeatText->setText(tr("%1").arg(m_settings.m_messageRepeat)); + applySettings(); +} + +void MeshcoreModGUI::on_messageText_editingFinished() +{ + switch (m_settings.m_messageType) + { + case MeshcoreModSettings::MessageText: + case MeshcoreModSettings::MessageGrpTxt: + case MeshcoreModSettings::MessageTxtMsg: + case MeshcoreModSettings::MessageAnonReq: + m_settings.m_textMessage = ui->messageText->toPlainText(); + break; + default: + break; + } + applySettings(); +} + +void MeshcoreModGUI::on_hexText_editingFinished() +{ + m_settings.m_bytesMessage = QByteArray::fromHex(ui->hexText->text().toLatin1()); + applySettings(); +} + +void MeshcoreModGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void MeshcoreModGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void MeshcoreModGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void MeshcoreModGUI::on_invertRamps_stateChanged(int state) +{ + m_settings.m_invertRamps = (state == Qt::Checked); + applySettings(); +} + +void MeshcoreModGUI::on_meshRegion_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + rebuildMeshcoreChannelOptions(); + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreModGUI::on_meshPreset_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + rebuildMeshcoreChannelOptions(); + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreModGUI::on_meshChannel_currentIndexChanged(int index) +{ + (void) index; + if (m_meshControlsUpdating) { + return; + } + + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreModGUI::on_meshApply_clicked(bool checked) +{ + (void) checked; + if (m_meshControlsUpdating) { + return; + } + + rebuildMeshcoreChannelOptions(); + applyMeshcoreProfileFromSelection(); +} + +void MeshcoreModGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void MeshcoreModGUI::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_meshcoreMod->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(); +} + +MeshcoreModGUI::MeshcoreModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::MeshcoreModGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_basebandSampleRate(125000), + m_doApplySettings(true), + m_meshControlsUpdating(false), + m_meshIdPanel(nullptr), + m_meshIdPubLabel(nullptr), + m_meshIdNodeNameEdit(nullptr), + m_meshIdGenerateButton(nullptr), + m_meshIdCopyPubkeyButton(nullptr), + m_meshIdMessageTypeCombo(nullptr), + m_meshIdDestPubEdit(nullptr), + m_meshIdChannelEdit(nullptr), + m_meshIdSendNowButton(nullptr), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channeltx/modmeshcore/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_meshcoreMod = (MeshcoreMod*) channelTx; + m_meshcoreMod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + ui->fecParity->setEnabled(true); + + 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->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->channelMute->setToolTip(tr("Mute this channel output.")); + 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->messageText->setToolTip(tr( + "Text payload editor. Lines starting with 'MESHCORE:' are parsed as " + "wire-packet commands and can override radio settings.\n" + "Examples:\n" + " MESHCORE: type=advert; seed=; name=Foo\n" + " MESHCORE: type=txt_msg; seed=; dest=; text=Hello\n" + " MESHCORE: type=grp_txt; channel=public; text=Hello group\n" + " MESHCORE: type=ack; dest=; msg_hash=\n" + "Optional radio overrides on any command: sf=, bw=, cr=, sync=, " + "freq=, preamble=")); + 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. Disabled")); + ui->invertRamps->setEnabled(false); + 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("MeshCore 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(); + setupMeshcoreAutoProfileControls(); + setupMeshcoreIdentityControls(); + displaySettings(); + makeUIConnections(); + applySettings(); + DialPopup::addPopupsToChildDials(this); + m_resizer.enableChildMouseTracking(); +} + +MeshcoreModGUI::~MeshcoreModGUI() +{ + delete ui; +} + +void MeshcoreModGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void MeshcoreModGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + MeshcoreMod::MsgConfigureMeshcoreMod *msg = MeshcoreMod::MsgConfigureMeshcoreMod::create(m_settings, force); + m_meshcoreMod->getInputMessageQueue()->push(msg); + } +} + +void MeshcoreModGUI::displaySettings() +{ + int thisBW = MeshcoreModSettings::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(MeshcoreModSettings::m_codingScheme == MeshcoreModSettings::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->fecParity->setValue(m_settings.m_nbParityBits); + ui->fecParityText->setText(tr("%1").arg(m_settings.m_nbParityBits)); + ui->repeatMessage->setValue(m_settings.m_messageRepeat); + ui->repeatText->setText(tr("%1").arg(m_settings.m_messageRepeat)); + 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); + + m_meshControlsUpdating = true; + + int regionIndex = ui->meshRegion->findText(m_settings.m_meshcoreRegionCode); + if (regionIndex < 0) { + regionIndex = ui->meshRegion->findText("EU_868"); + } + if (regionIndex < 0) { + regionIndex = 0; + } + ui->meshRegion->setCurrentIndex(regionIndex); + + int presetIndex = ui->meshPreset->findText(m_settings.m_meshcorePresetName); + if (presetIndex < 0) { + presetIndex = ui->meshPreset->findText("EU_NARROW"); + } + if (presetIndex < 0) { + presetIndex = 0; + } + ui->meshPreset->setCurrentIndex(presetIndex); + m_meshControlsUpdating = false; + + rebuildMeshcoreChannelOptions(); + + m_meshControlsUpdating = true; + int channelIndex = ui->meshChannel->findData(m_settings.m_meshcoreChannelIndex); + if (channelIndex < 0) { + channelIndex = 0; + } + ui->meshChannel->setCurrentIndex(channelIndex); + m_meshControlsUpdating = false; + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + + displayMeshcoreIdentity(); + updateMeshcoreMessageTypeFields(); + + blockApplySettings(false); +} + +void MeshcoreModGUI::displayCurrentPayloadMessage() +{ + ui->messageText->blockSignals(true); + + switch (m_settings.m_messageType) + { + case MeshcoreModSettings::MessageText: + case MeshcoreModSettings::MessageGrpTxt: + case MeshcoreModSettings::MessageTxtMsg: + case MeshcoreModSettings::MessageAnonReq: + ui->messageText->setText(m_settings.m_textMessage); + break; + case MeshcoreModSettings::MessageAdvert: + ui->messageText->setText(QString()); // ADVERT body driven by identity, not text + break; + case MeshcoreModSettings::MessageAck: + ui->messageText->setText(QString()); // ACK body driven by msg-hash field + break; + } + + ui->messageText->blockSignals(false); +} + +void MeshcoreModGUI::displayBinaryMessage() +{ + ui->hexText->setText(m_settings.m_bytesMessage.toHex()); +} + +void MeshcoreModGUI::setBandwidths() +{ + int maxBandwidth = m_basebandSampleRate / MeshcoreModSettings::oversampling; + int maxIndex = 0; + + for (; (maxIndex < MeshcoreModSettings::nbBandwidths) && (MeshcoreModSettings::bandwidths[maxIndex] <= maxBandwidth); maxIndex++) + {} + + if (maxIndex != 0) + { + qDebug("MeshcoreModGUI::setBandwidths: avl: %d max: %d", maxBandwidth, MeshcoreModSettings::bandwidths[maxIndex-1]); + ui->bw->setMaximum(maxIndex - 1); + int index = ui->bw->value(); + ui->bwText->setText(QString("%1 Hz").arg(MeshcoreModSettings::bandwidths[index])); + } +} + +void MeshcoreModGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void MeshcoreModGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void MeshcoreModGUI::tick() +{ + if (m_tickCount < 10) + { + m_tickCount++; + } + else + { + m_tickCount = 0; + double powDb = CalcDb::dbPower(m_meshcoreMod->getMagSq()); + m_channelPowerDbAvg(powDb); + ui->channelPower->setText(tr("%1 dB").arg(m_channelPowerDbAvg.asDouble(), 0, 'f', 1)); + + if (m_meshcoreMod->getModulatorActive()) { + ui->playMessage->setStyleSheet("QPushButton { background-color : green; }"); + } else { + ui->playMessage->setStyleSheet("QPushButton { background:rgb(79,79,79); }"); + } + } +} + +void MeshcoreModGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &MeshcoreModGUI::on_deltaFrequency_changed); + QObject::connect(ui->bw, &QSlider::valueChanged, this, &MeshcoreModGUI::on_bw_valueChanged); + QObject::connect(ui->spread, &QSlider::valueChanged, this, &MeshcoreModGUI::on_spread_valueChanged); + QObject::connect(ui->deBits, &QSlider::valueChanged, this, &MeshcoreModGUI::on_deBits_valueChanged); + QObject::connect(ui->preambleChirps, &QSlider::valueChanged, this, &MeshcoreModGUI::on_preambleChirps_valueChanged); + QObject::connect(ui->idleTime, &QSlider::valueChanged, this, &MeshcoreModGUI::on_idleTime_valueChanged); + QObject::connect(ui->syncWord, &QLineEdit::editingFinished, this, &MeshcoreModGUI::on_syncWord_editingFinished); + QObject::connect(ui->channelMute, &QToolButton::toggled, this, &MeshcoreModGUI::on_channelMute_toggled); + QObject::connect(ui->fecParity, &QDial::valueChanged, this, &MeshcoreModGUI::on_fecParity_valueChanged); + QObject::connect(ui->playMessage, &QPushButton::clicked, this, &MeshcoreModGUI::on_playMessage_clicked); + QObject::connect(ui->repeatMessage, &QDial::valueChanged, this, &MeshcoreModGUI::on_repeatMessage_valueChanged); + QObject::connect(ui->messageText, &CustomTextEdit::editingFinished, this, &MeshcoreModGUI::on_messageText_editingFinished); + QObject::connect(ui->hexText, &QLineEdit::editingFinished, this, &MeshcoreModGUI::on_hexText_editingFinished); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &MeshcoreModGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &MeshcoreModGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &MeshcoreModGUI::on_udpPort_editingFinished); + QObject::connect(ui->meshRegion, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshcoreModGUI::on_meshRegion_currentIndexChanged); + QObject::connect(ui->meshPreset, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshcoreModGUI::on_meshPreset_currentIndexChanged); + QObject::connect(ui->meshChannel, QOverload::of(&QComboBox::currentIndexChanged), this, &MeshcoreModGUI::on_meshChannel_currentIndexChanged); + QObject::connect(ui->meshApply, &QPushButton::clicked, this, &MeshcoreModGUI::on_meshApply_clicked); +} + +void MeshcoreModGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} + +// ---- MeshCore Identity / Message-Type panel ----------------------------------- +// +// Built programmatically and inserted into the existing rollup contents. +// Layout: a single QGroupBox "MeshCore Identity & Message" with: +// - Pubkey label (read-only, autogenerated on first launch) +// - Node name line edit (default "SDRangel-") +// - [Generate] / [Copy pubkey] buttons +// - Message-Type combo (Text/Advert/TxtMsg/GrpTxt/AnonReq/Ack) +// - Dest pubkey line edit (relevant for TxtMsg/AnonReq/Ack) +// - Channel name line edit (relevant for GrpTxt; default "public") +// - [Send Now] button — equivalent to playMessage but bypasses checked state +// +// Keeps the heavy 41-KB UI XML untouched. Region/preset/channel selectors +// already exist above this panel and govern the carrier PHY for ADVERT TX. + +void MeshcoreModGUI::setupMeshcoreIdentityControls() +{ + RollupContents *rollupContents = getRollupContents(); + if (!rollupContents) { + return; + } + + m_meshIdPanel = new QGroupBox(tr("MeshCore Identity & Message"), rollupContents); + m_meshIdPanel->setObjectName(QStringLiteral("meshIdPanel")); + + QFormLayout *form = new QFormLayout(m_meshIdPanel); + form->setContentsMargins(6, 6, 6, 6); + form->setHorizontalSpacing(6); + form->setVerticalSpacing(4); + + m_meshIdPubLabel = new QLabel(QStringLiteral("(loading…)"), m_meshIdPanel); + m_meshIdPubLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + m_meshIdPubLabel->setToolTip( + tr("Local Ed25519 pubkey (32 bytes, hex). Auto-generated on first use; " + "stored at AppDataLocation/meshcore/identity.bin.")); + form->addRow(tr("Pubkey"), m_meshIdPubLabel); + + m_meshIdNodeNameEdit = new QLineEdit(m_meshIdPanel); + m_meshIdNodeNameEdit->setPlaceholderText(QStringLiteral("SDRangel-")); + m_meshIdNodeNameEdit->setToolTip( + tr("Node name advertised in our ADVERT packets. Defaults to " + "'SDRangel-' when empty.")); + form->addRow(tr("Name"), m_meshIdNodeNameEdit); + + QHBoxLayout *btnRow = new QHBoxLayout(); + btnRow->setSpacing(4); + m_meshIdGenerateButton = new QPushButton(tr("Generate"), m_meshIdPanel); + m_meshIdGenerateButton->setToolTip( + tr("Replace the on-disk identity with a fresh keypair. Existing " + "contacts that learned our previous pubkey will not recognise us.")); + m_meshIdCopyPubkeyButton = new QPushButton(tr("Copy pubkey"), m_meshIdPanel); + m_meshIdCopyPubkeyButton->setToolTip(tr("Copy our pubkey hex to the clipboard.")); + btnRow->addWidget(m_meshIdGenerateButton); + btnRow->addWidget(m_meshIdCopyPubkeyButton); + btnRow->addStretch(1); + form->addRow(QString(), btnRow); + + m_meshIdMessageTypeCombo = new QComboBox(m_meshIdPanel); + m_meshIdMessageTypeCombo->addItem(tr("Text (raw / MESHCORE: command)"), + MeshcoreModSettings::MessageText); + m_meshIdMessageTypeCombo->addItem(tr("ADVERT (broadcast our identity)"), + MeshcoreModSettings::MessageAdvert); + m_meshIdMessageTypeCombo->addItem(tr("TXT_MSG (ECDH-encrypted DM)"), + MeshcoreModSettings::MessageTxtMsg); + m_meshIdMessageTypeCombo->addItem(tr("GRP_TXT (channel PSK)"), + MeshcoreModSettings::MessageGrpTxt); + m_meshIdMessageTypeCombo->addItem(tr("ANON_REQ (encrypted, sender pub embedded)"), + MeshcoreModSettings::MessageAnonReq); + m_meshIdMessageTypeCombo->addItem(tr("ACK (plaintext)"), + MeshcoreModSettings::MessageAck); + m_meshIdMessageTypeCombo->setToolTip( + tr("MeshCore wire packet type to encode + transmit. ADVERT broadcasts " + "our identity; the other types build packets from the dest pubkey " + "/ channel / text fields below.")); + form->addRow(tr("Type"), m_meshIdMessageTypeCombo); + + m_meshIdDestPubEdit = new QLineEdit(m_meshIdPanel); + m_meshIdDestPubEdit->setPlaceholderText(QStringLiteral("64 hex chars")); + m_meshIdDestPubEdit->setToolTip( + tr("Destination pubkey (64 hex). Used by TXT_MSG, ANON_REQ, ACK. " + "Paste from a companion's contact share.")); + form->addRow(tr("Dest pubkey"), m_meshIdDestPubEdit); + + m_meshIdChannelEdit = new QLineEdit(m_meshIdPanel); + m_meshIdChannelEdit->setPlaceholderText(QStringLiteral("public")); + m_meshIdChannelEdit->setToolTip( + tr("Group channel name for GRP_TXT messages (default 'public', " + "PSK 8b3387e9c5cdea6ac9e5edbaa115cd72).")); + form->addRow(tr("Channel"), m_meshIdChannelEdit); + + m_meshIdSendNowButton = new QPushButton(tr("Send now"), m_meshIdPanel); + m_meshIdSendNowButton->setToolTip( + tr("Re-encode the selected message type and queue one transmission. " + "Equivalent to clicking play, but reasserts repeat-count and " + "re-runs the encoder so the timestamp/identity are fresh.")); + form->addRow(QString(), m_meshIdSendNowButton); + + // Append the panel to the existing rollup vertical layout. + QLayout *outer = rollupContents->layout(); + if (outer) { + outer->addWidget(m_meshIdPanel); + } + + // Wire signals. + QObject::connect(m_meshIdMessageTypeCombo, + QOverload::of(&QComboBox::currentIndexChanged), + this, &MeshcoreModGUI::onMeshIdMessageTypeChanged); + QObject::connect(m_meshIdGenerateButton, &QPushButton::clicked, + this, &MeshcoreModGUI::onMeshIdGenerateClicked); + QObject::connect(m_meshIdCopyPubkeyButton, &QPushButton::clicked, + this, &MeshcoreModGUI::onMeshIdCopyPubkeyClicked); + QObject::connect(m_meshIdNodeNameEdit, &QLineEdit::editingFinished, + this, &MeshcoreModGUI::onMeshIdNodeNameEdited); + QObject::connect(m_meshIdDestPubEdit, &QLineEdit::editingFinished, + this, &MeshcoreModGUI::onMeshIdDestPubEdited); + QObject::connect(m_meshIdChannelEdit, &QLineEdit::editingFinished, + this, &MeshcoreModGUI::onMeshIdChannelEdited); + QObject::connect(m_meshIdSendNowButton, &QPushButton::clicked, + this, &MeshcoreModGUI::onMeshIdSendNowClicked); + + displayMeshcoreIdentity(); + updateMeshcoreMessageTypeFields(); + + rollupContents->arrangeRollups(); +} + +void MeshcoreModGUI::displayMeshcoreIdentity() +{ + if (!m_meshIdPanel) return; + + const QString path = m_settings.m_meshIdentityPath.isEmpty() + ? modemmeshcore::identity::defaultIdentityPath() + : m_settings.m_meshIdentityPath; + modemmeshcore::identity::Identity id = + modemmeshcore::identity::loadOrCreateIdentity(path); + + if (id.isValid()) { + m_meshIdPubLabel->setText(id.pubHex()); + } else { + m_meshIdPubLabel->setText(tr("(identity load failed)")); + } + + { + QSignalBlocker block(m_meshIdNodeNameEdit); + if (m_settings.m_meshNodeName.isEmpty()) { + m_meshIdNodeNameEdit->setText(modemmeshcore::identity::defaultNodeNameFor(id)); + } else { + m_meshIdNodeNameEdit->setText(m_settings.m_meshNodeName); + } + } + + { + QSignalBlocker block(m_meshIdMessageTypeCombo); + const int found = m_meshIdMessageTypeCombo->findData( + static_cast(m_settings.m_messageType)); + if (found >= 0) { + m_meshIdMessageTypeCombo->setCurrentIndex(found); + } + } + + { + QSignalBlocker block(m_meshIdDestPubEdit); + m_meshIdDestPubEdit->setText(m_settings.m_meshDestPubKeyHex); + } + { + QSignalBlocker block(m_meshIdChannelEdit); + m_meshIdChannelEdit->setText(m_settings.m_meshGroupChannelName); + } +} + +void MeshcoreModGUI::updateMeshcoreMessageTypeFields() +{ + if (!m_meshIdDestPubEdit || !m_meshIdChannelEdit) return; + const auto t = m_settings.m_messageType; + const bool needsDest = + (t == MeshcoreModSettings::MessageTxtMsg) + || (t == MeshcoreModSettings::MessageAnonReq) + || (t == MeshcoreModSettings::MessageAck); + const bool needsChannel = (t == MeshcoreModSettings::MessageGrpTxt); + m_meshIdDestPubEdit->setEnabled(needsDest); + m_meshIdChannelEdit->setEnabled(needsChannel); +} + +void MeshcoreModGUI::onMeshIdMessageTypeChanged(int index) +{ + if (!m_meshIdMessageTypeCombo) return; + const int raw = m_meshIdMessageTypeCombo->itemData(index).toInt(); + if (raw < MeshcoreModSettings::MessageText + || raw > MeshcoreModSettings::MessageAck) return; + m_settings.m_messageType = static_cast(raw); + updateMeshcoreMessageTypeFields(); + applySettings(); +} + +void MeshcoreModGUI::onMeshIdGenerateClicked() +{ + const QString path = m_settings.m_meshIdentityPath.isEmpty() + ? modemmeshcore::identity::defaultIdentityPath() + : m_settings.m_meshIdentityPath; + modemmeshcore::identity::Identity fresh = + modemmeshcore::identity::generateIdentity(); + if (!modemmeshcore::identity::saveIdentity(fresh, path)) { + qWarning() << "MeshcoreModGUI: saveIdentity failed for path:" << path; + return; + } + qInfo() << "MeshcoreModGUI: regenerated identity, new pubkey=" + << fresh.pubHex(); + displayMeshcoreIdentity(); + applySettings(true); // re-encode with new identity +} + +void MeshcoreModGUI::onMeshIdCopyPubkeyClicked() +{ + if (!m_meshIdPubLabel) return; + QClipboard *cb = QGuiApplication::clipboard(); + if (cb) { + cb->setText(m_meshIdPubLabel->text()); + } +} + +void MeshcoreModGUI::onMeshIdNodeNameEdited() +{ + if (!m_meshIdNodeNameEdit) return; + m_settings.m_meshNodeName = m_meshIdNodeNameEdit->text(); + applySettings(); +} + +void MeshcoreModGUI::onMeshIdDestPubEdited() +{ + if (!m_meshIdDestPubEdit) return; + m_settings.m_meshDestPubKeyHex = m_meshIdDestPubEdit->text().trimmed(); + applySettings(); +} + +void MeshcoreModGUI::onMeshIdChannelEdited() +{ + if (!m_meshIdChannelEdit) return; + QString name = m_meshIdChannelEdit->text().trimmed(); + if (name.isEmpty()) name = QStringLiteral("public"); + m_settings.m_meshGroupChannelName = name; + applySettings(); +} + +void MeshcoreModGUI::onMeshIdSendNowClicked() +{ + // Re-encode + queue one transmission. Mirrors play-button semantics but + // forces a re-encode so the freshly-rebuilt MESHCORE: command (with a + // current timestamp) is what hits the air. + applySettings(true); + if (!ui->playMessage->isChecked()) { + ui->playMessage->setChecked(true); + on_playMessage_clicked(true); + } else { + // Already in play state — toggle off then on so the encoder runs again. + ui->playMessage->setChecked(false); + on_playMessage_clicked(false); + ui->playMessage->setChecked(true); + on_playMessage_clicked(true); + } +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodgui.h b/plugins/channeltx/modmeshcore/meshcoremodgui.h new file mode 100644 index 000000000..772d9e5eb --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodgui.h @@ -0,0 +1,161 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Alejandro Aleman // +// Copyright (C) 2016-2026 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_MODMESHCORE_MESHCOREMODGUI_H_ +#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODGUI_H_ + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/movingaverage.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" + +#include "meshcoremod.h" +#include "meshcoremodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSource; +class QComboBox; +class QLabel; +class QLineEdit; +class QPushButton; +class QWidget; + +namespace Ui { + class MeshcoreModGUI; +} + +class MeshcoreModGUI : public ChannelGUI { + Q_OBJECT + +public: + static MeshcoreModGUI* 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::MeshcoreModGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + MeshcoreModSettings m_settings; + qint64 m_deviceCenterFrequency; + int m_basebandSampleRate; + bool m_doApplySettings; + bool m_meshControlsUpdating; + + // MeshCore Identity / Message-Type panel widgets (built programmatically + // in setupMeshcoreIdentityControls — avoids a heavy UI XML edit). + QWidget* m_meshIdPanel; + QLabel* m_meshIdPubLabel; + QLineEdit* m_meshIdNodeNameEdit; + QPushButton* m_meshIdGenerateButton; + QPushButton* m_meshIdCopyPubkeyButton; + QComboBox* m_meshIdMessageTypeCombo; + QLineEdit* m_meshIdDestPubEdit; + QLineEdit* m_meshIdChannelEdit; + QPushButton* m_meshIdSendNowButton; + + MeshcoreMod* m_meshcoreMod; + MovingAverageUtil m_channelPowerDbAvg; + + std::size_t m_tickCount; + MessageQueue m_inputMessageQueue; + + explicit MeshcoreModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent = nullptr); + virtual ~MeshcoreModGUI(); + + 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; + bool retuneDeviceToFrequency(qint64 centerFrequencyHz); + void setupMeshcoreAutoProfileControls(); + void rebuildMeshcoreChannelOptions(); + void applyMeshcoreProfileFromSelection(); + void setupMeshcoreIdentityControls(); + void displayMeshcoreIdentity(); + void updateMeshcoreMessageTypeFields(); + 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_fecParity_valueChanged(int value); + void on_playMessage_clicked(bool checked); + void on_repeatMessage_valueChanged(int value); + 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 on_meshRegion_currentIndexChanged(int index); + void on_meshPreset_currentIndexChanged(int index); + void on_meshChannel_currentIndexChanged(int index); + void on_meshApply_clicked(bool checked); + void onMeshIdMessageTypeChanged(int index); + void onMeshIdGenerateClicked(); + void onMeshIdCopyPubkeyClicked(); + void onMeshIdNodeNameEdited(); + void onMeshIdDestPubEdited(); + void onMeshIdChannelEdited(); + void onMeshIdSendNowClicked(); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void tick(); +}; + +#endif /* PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODGUI_H_ */ diff --git a/plugins/channeltx/modmeshcore/meshcoremodgui.ui b/plugins/channeltx/modmeshcore/meshcoremodgui.ui new file mode 100644 index 000000000..8469c89ff --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodgui.ui @@ -0,0 +1,1225 @@ + + + MeshcoreModGUI + + + + 0 + 0 + 650 + 548 + + + + + 0 + 0 + + + + + 650 + 180 + + + + + Liberation Sans + 9 + + + + MeshCore 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 + 951 + 401 + + + + Payload + + + + 2 + + + 10 + + + + + 2 + + + + + + 40 + 16777215 + + + + Meshcore region (defines allowed frequency band) + + + Region + + + + + + + + 80 + 16777215 + + + + Meshcore region. Combined with preset/channel to auto-apply LoRa receive parameters. + + + + US + + + + + EU_433 + + + + + EU_868 + + + + + ANZ + + + + + JP + + + + + CN + + + + + KR + + + + + TW + + + + + IN + + + + + TH + + + + + BR_902 + + + + + LORA_24 + + + + + + + + + 40 + 16777215 + + + + MeshCore regional preset (EU_NARROW, EU_LONG_RANGE, AU, USA, ...) + + + Preset + + + + + + + + 120 + 16777215 + + + + Meshcore modem preset. Applies LoRa BW/SF/CR/DE and header/CRC expectations. + + + + EU_NARROW + + + + + EU_LONG_RANGE + + + + + EU_MEDIUM_RANGE + + + + + AU + + + + + AU_VICTORIA + + + + + CZ_NARROW + + + + + EU_433_LONG_RANGE + + + + + NZ + + + + + NZ_NARROW + + + + + PT_433 + + + + + PT_868 + + + + + CH + + + + + USA + + + + + VN + + + + + USER + + + + + + + + + 50 + 16777215 + + + + Meshcore channel number (zero-based) + + + Channel + + + + + + + + 160 + 16777215 + + + + Meshcore channel number (zero-based, shown with center frequency) + + + + + + + + 40 + 16777215 + + + + Apply the currently selected Meshcore region/preset/channel profile now. + + + Apply + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + FEC + + + + + + + + 22 + 22 + + + + Number of FEC parity bits (0 to 4) for Hamming code + + + 4 + + + 1 + + + 1 + + + + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Sync + + + + + + + + 30 + 16777215 + + + + Qt::ClickFocus + + + Sync word (1 byte hex) + + + HH + + + 00 + + + + + + + Play message + + + + + + + :/play.png:/play.png + + + false + + + + + + + Repeat + + + + + + + + 22 + 22 + + + + Message repetition (0 for infinite) + + + 20 + + + 1 + + + 1 + + + + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + 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/modmeshcore/meshcoremodplugin.cpp b/plugins/channeltx/modmeshcore/meshcoremodplugin.cpp new file mode 100644 index 000000000..036d1008e --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodplugin.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 "meshcoremodgui.h" +#endif +#include "meshcoremod.h" +#include "meshcoremodwebapiadapter.h" +#include "meshcoremodplugin.h" + +const PluginDescriptor MeshcoreModPlugin::m_pluginDescriptor = { + MeshcoreMod::m_channelId, + QStringLiteral("MeshCore Modulator"), + QStringLiteral("7.24.0"), + QStringLiteral("(c) Edouard Griffiths, F4EXB"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +MeshcoreModPlugin::MeshcoreModPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& MeshcoreModPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void MeshcoreModPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register LoRa modulator + m_pluginAPI->registerTxChannel(MeshcoreMod::m_channelIdURI, MeshcoreMod::m_channelId, this); +} + +void MeshcoreModPlugin::createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + MeshcoreMod *instance = new MeshcoreMod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* MeshcoreModPlugin::createTxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSource *txChannel) const +{ + (void) deviceUISet; + (void) txChannel; + return nullptr; +} +#else +ChannelGUI* MeshcoreModPlugin::createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *txChannel) const +{ + return MeshcoreModGUI::create(m_pluginAPI, deviceUISet, txChannel); +} +#endif + +ChannelWebAPIAdapter* MeshcoreModPlugin::createChannelWebAPIAdapter() const +{ + return new MeshcoreModWebAPIAdapter(); +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodplugin.h b/plugins/channeltx/modmeshcore/meshcoremodplugin.h new file mode 100644 index 000000000..1cd378279 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodplugin.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_MESHCOREMODPLUGIN_H +#define INCLUDE_MESHCOREMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSource; + +class MeshcoreModPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channeltx.modmeshcore") + +public: + explicit MeshcoreModPlugin(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_MESHCOREMODPLUGIN_H diff --git a/plugins/channeltx/modmeshcore/meshcoremodsettings.cpp b/plugins/channeltx/modmeshcore/meshcoremodsettings.cpp new file mode 100644 index 000000000..24a38b93c --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodsettings.cpp @@ -0,0 +1,481 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoremodsettings.h" + +const int MeshcoreModSettings::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 MeshcoreModSettings::CodingScheme MeshcoreModSettings::m_codingScheme = MeshcoreModSettings::CodingLoRa; +const bool MeshcoreModSettings::m_hasCRC = true; +const bool MeshcoreModSettings::m_hasHeader = true; +const int MeshcoreModSettings::nbBandwidths = 3*8 + 4; +const int MeshcoreModSettings::oversampling = 4; + +MeshcoreModSettings::MeshcoreModSettings() : + m_inputFrequencyOffset(0), + m_channelMarker(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void MeshcoreModSettings::resetToDefaults() +{ + // MeshCore EU/UK Narrow defaults: 869.618 MHz / 62.5 kHz / SF 8 / CR 4/8. + // bandwidthIndex 18 -> 62500 Hz in the bandwidths[] table. + m_bandwidthIndex = 18; // 62500 Hz + m_spreadFactor = 8; + m_deBits = 0; + m_preambleChirps = 16; // MeshCore: 16 for SF>8, 32 for SF<9 (profile overrides) + m_quietMillis = 1000; + m_nbParityBits = 4; // CR 4/8 + m_syncWord = 0x12; // MeshCore wire sync (verify against firmware) + + // Default text is a self-contained MESHCORE: command — sending it from a + // fresh plugin instance broadcasts an ADVERT for the public group channel. + // Operator must replace `seed=00..` with their identity hex before TX. + m_textMessage = "MESHCORE: type=grp_txt; channel=public; text=Hello MeshCore"; + + m_channelMute = false; + m_messageRepeat = 1; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9998; + m_invertRamps = false; + m_meshcoreRegionCode = "EU_868"; + m_meshcorePresetName = "EU_NARROW"; // MeshCore EU/UK Narrow / Recommended + m_meshcoreChannelIndex = 0; + + // Default to ADVERT message type — sending a fresh plugin instance now + // broadcasts our identity rather than a hardcoded text payload. The + // identity itself is auto-generated on first use (see meshcore_identity.h). + m_messageType = MessageAdvert; + m_meshIdentityPath.clear(); // empty -> identity::defaultIdentityPath() + m_meshNodeName.clear(); // empty -> defaultNodeNameFor(loaded identity) + m_meshAdvertLocationEnabled = false; + m_meshAdvertLat = 0.0; + m_meshAdvertLon = 0.0; + m_meshAdvertIntervalSec = 0; // manual only by default + m_meshDestPubKeyHex.clear(); + m_meshGroupChannelName = QStringLiteral("public"); + m_meshGroupChannelPskHex.clear(); // empty -> built-in PUBLIC_CHANNEL_PSK + m_meshAckMsgHashHex.clear(); + + m_rgbColor = QColor(0, 200, 255).rgb(); // distinct from Meshtastic's magenta + m_title = "MeshCore 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; +} + +unsigned int MeshcoreModSettings::getNbSFDFourths() const +{ + switch (m_codingScheme) + { + case CodingLoRa: + return 9; + default: + return 8; + } +} + +bool MeshcoreModSettings::hasSyncWord() const +{ + return m_codingScheme == CodingLoRa; +} + +QByteArray MeshcoreModSettings::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(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.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); + s.writeString(63, m_meshcoreRegionCode); + s.writeString(64, m_meshcorePresetName); + s.writeS32(65, m_meshcoreChannelIndex); + + // MeshCore app-layer / identity (added with the dual-mode TX rework). + s.writeString(80, m_meshIdentityPath); + s.writeString(81, m_meshNodeName); + s.writeBool(82, m_meshAdvertLocationEnabled); + s.writeDouble(83, m_meshAdvertLat); + s.writeDouble(84, m_meshAdvertLon); + s.writeS32(85, m_meshAdvertIntervalSec); + s.writeString(86, m_meshDestPubKeyHex); + s.writeString(87, m_meshGroupChannelName); + s.writeString(88, m_meshGroupChannelPskHex); + s.writeString(89, m_meshAckMsgHashHex); + + return s.final(); +} + +bool MeshcoreModSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + unsigned int utmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_bandwidthIndex, 0); + d.readS32(3, &m_spreadFactor, 0); + + if (m_channelMarker) + { + d.readBlob(5, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readString(6, &m_title, "MeshCore Modulator"); + 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(8, &m_preambleChirps, 16); + d.readS32(11, &m_quietMillis, 1000); + d.readBool(12, &m_invertRamps, false); + d.readString(28, &m_textMessage, "Hello Meshcore"); + d.readBlob(29, &m_bytesMessage); + int msgTypeRaw = MessageAdvert; + d.readS32(30, &msgTypeRaw, MessageAdvert); + if (msgTypeRaw < MessageText || msgTypeRaw > MessageAck) { + msgTypeRaw = MessageAdvert; + } + m_messageType = static_cast(msgTypeRaw); + d.readS32(31, &m_nbParityBits, 1); + 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); + d.readString(63, &m_meshcoreRegionCode, "EU_868"); + d.readString(64, &m_meshcorePresetName, "EU_NARROW"); + d.readS32(65, &m_meshcoreChannelIndex, 0); + + d.readString(80, &m_meshIdentityPath, QString()); + d.readString(81, &m_meshNodeName, QString()); + d.readBool(82, &m_meshAdvertLocationEnabled, false); + d.readDouble(83, &m_meshAdvertLat, 0.0); + d.readDouble(84, &m_meshAdvertLon, 0.0); + d.readS32(85, &m_meshAdvertIntervalSec, 0); + d.readString(86, &m_meshDestPubKeyHex, QString()); + d.readString(87, &m_meshGroupChannelName, QStringLiteral("public")); + d.readString(88, &m_meshGroupChannelPskHex, QString()); + d.readString(89, &m_meshAckMsgHashHex, QString()); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void MeshcoreModSettings::applySettings(const QStringList& settingsKeys, const MeshcoreModSettings& 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("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("textMessage")) + m_textMessage = settings.m_textMessage; + if (settingsKeys.contains("bytesMessage")) + m_bytesMessage = settings.m_bytesMessage; + if (settingsKeys.contains("nbParityBits")) + m_nbParityBits = settings.m_nbParityBits; + if (settingsKeys.contains("messageRepeat")) + m_messageRepeat = settings.m_messageRepeat; + if (settingsKeys.contains("meshcoreRegionCode")) + m_meshcoreRegionCode = settings.m_meshcoreRegionCode; + if (settingsKeys.contains("meshcorePresetName")) + m_meshcorePresetName = settings.m_meshcorePresetName; + if (settingsKeys.contains("meshcoreChannelIndex")) + m_meshcoreChannelIndex = settings.m_meshcoreChannelIndex; + if (settingsKeys.contains("messageType")) + m_messageType = settings.m_messageType; + if (settingsKeys.contains("meshIdentityPath")) + m_meshIdentityPath = settings.m_meshIdentityPath; + if (settingsKeys.contains("meshNodeName")) + m_meshNodeName = settings.m_meshNodeName; + if (settingsKeys.contains("meshAdvertLocationEnabled")) + m_meshAdvertLocationEnabled = settings.m_meshAdvertLocationEnabled; + if (settingsKeys.contains("meshAdvertLat")) + m_meshAdvertLat = settings.m_meshAdvertLat; + if (settingsKeys.contains("meshAdvertLon")) + m_meshAdvertLon = settings.m_meshAdvertLon; + if (settingsKeys.contains("meshAdvertIntervalSec")) + m_meshAdvertIntervalSec = settings.m_meshAdvertIntervalSec; + if (settingsKeys.contains("meshDestPubKeyHex")) + m_meshDestPubKeyHex = settings.m_meshDestPubKeyHex; + if (settingsKeys.contains("meshGroupChannelName")) + m_meshGroupChannelName = settings.m_meshGroupChannelName; + if (settingsKeys.contains("meshGroupChannelPskHex")) + m_meshGroupChannelPskHex = settings.m_meshGroupChannelPskHex; + if (settingsKeys.contains("meshAckMsgHashHex")) + m_meshAckMsgHashHex = settings.m_meshAckMsgHashHex; +} + +QString MeshcoreModSettings::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("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("messageRepeat") || force) + debug += QString("Message Repeat: %1\n").arg(m_messageRepeat); + if (settingsKeys.contains("meshcoreRegionCode") || force) + debug += QString("Meshcore Region Code: %1\n").arg(m_meshcoreRegionCode); + if (settingsKeys.contains("meshcorePresetName") || force) + debug += QString("Meshcore Preset Name: %1\n").arg(m_meshcorePresetName); + if (settingsKeys.contains("meshcoreChannelIndex") || force) + debug += QString("Meshcore Channel Index: %1\n").arg(m_meshcoreChannelIndex); + if (settingsKeys.contains("meshIdentityPath") || force) + debug += QString("Mesh Identity Path: %1\n").arg(m_meshIdentityPath); + if (settingsKeys.contains("meshNodeName") || force) + debug += QString("Mesh Node Name: %1\n").arg(m_meshNodeName); + if (settingsKeys.contains("meshAdvertLocationEnabled") || force) + debug += QString("Mesh Advert Location Enabled: %1\n").arg(m_meshAdvertLocationEnabled); + if (settingsKeys.contains("meshAdvertLat") || force) + debug += QString("Mesh Advert Lat: %1\n").arg(m_meshAdvertLat); + if (settingsKeys.contains("meshAdvertLon") || force) + debug += QString("Mesh Advert Lon: %1\n").arg(m_meshAdvertLon); + if (settingsKeys.contains("meshAdvertIntervalSec") || force) + debug += QString("Mesh Advert Interval Sec: %1\n").arg(m_meshAdvertIntervalSec); + if (settingsKeys.contains("meshDestPubKeyHex") || force) + debug += QString("Mesh Dest Pubkey: %1\n").arg(m_meshDestPubKeyHex); + if (settingsKeys.contains("meshGroupChannelName") || force) + debug += QString("Mesh Group Channel Name: %1\n").arg(m_meshGroupChannelName); + if (settingsKeys.contains("meshGroupChannelPskHex") || force) + debug += QString("Mesh Group Channel PSK: %1\n").arg(m_meshGroupChannelPskHex); + if (settingsKeys.contains("meshAckMsgHashHex") || force) + debug += QString("Mesh ACK Msg Hash: %1\n").arg(m_meshAckMsgHashHex); + return debug; +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodsettings.h b/plugins/channeltx/modmeshcore/meshcoremodsettings.h new file mode 100644 index 000000000..39e8b4c82 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodsettings.h @@ -0,0 +1,118 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_MODMESHCORE_MESHCOREMODSETTINGS_H_ +#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODSETTINGS_H_ + +#include +#include + +#include + +class Serializable; + +struct MeshcoreModSettings +{ + enum CodingScheme + { + CodingLoRa, //!< Standard LoRa + }; + + enum MessageType + { + MessageText, //!< Plain text payload (raw, no MeshCore framing) + MessageAdvert, //!< Self-ADVERT — broadcasts our identity pubkey + name + (opt) location + MessageTxtMsg, //!< MeshCore TXT_MSG (encrypted to a known contact via ECDH) + MessageGrpTxt, //!< MeshCore GRP_TXT (encrypted with a group channel PSK) + MessageAnonReq, //!< MeshCore ANON_REQ (encrypted, sender pubkey embedded) + MessageAck //!< MeshCore ACK (plaintext) + }; + + 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) + static const bool m_hasCRC; //!< Payload has CRC (LoRa) + static const bool m_hasHeader; //!< Header present before actual payload (LoRa) + unsigned char m_syncWord; + bool m_channelMute; + static const CodingScheme m_codingScheme; + MessageType m_messageType; //!< user-selectable: Text/Advert/TxtMsg/GrpTxt/AnonReq/Ack + 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) + QString m_meshcoreRegionCode; //!< Meshcore region code (e.g. "US", "EU_868") + QString m_meshcorePresetName; //!< MeshCore regional preset name (e.g. "EU_NARROW") + int m_meshcoreChannelIndex; //!< Meshcore channel index (0-based) + + // MeshCore app-layer / identity fields. All optional; resetToDefaults() seats + // sensible values matching EU_868 + Public channel + auto-generated identity. + QString m_meshIdentityPath; //!< override identity file path (empty = AppDataLocation default) + QString m_meshNodeName; //!< ADVERT name field; defaults to "SDRangel-" + bool m_meshAdvertLocationEnabled; //!< include lat/lon in ADVERT (default false) + double m_meshAdvertLat; //!< ADVERT location latitude (degrees) + double m_meshAdvertLon; //!< ADVERT location longitude (degrees) + int m_meshAdvertIntervalSec; //!< auto-ADVERT interval seconds (0 = manual only) + QString m_meshDestPubKeyHex; //!< destination Ed25519 pubkey (hex64) for TxtMsg/AnonReq/Ack + QString m_meshGroupChannelName; //!< channel name for GRP_TXT (default "public") + QString m_meshGroupChannelPskHex; //!< channel PSK (hex16/32); empty = use built-in public PSK + QString m_meshAckMsgHashHex; //!< 4-byte msg hash (hex8) for ACK frames + + 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; + + MeshcoreModSettings(); + void resetToDefaults(); + 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 MeshcoreModSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; +}; + + + +#endif /* PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODSETTINGS_H_ */ diff --git a/plugins/channeltx/modmeshcore/meshcoremodsource.cpp b/plugins/channeltx/modmeshcore/meshcoremodsource.cpp new file mode 100644 index 000000000..9b9e39b79 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodsource.cpp @@ -0,0 +1,429 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "meshcoremodsource.h" + +const int MeshcoreModSource::m_levelNbSamples = 480; // every 10ms + +MeshcoreModSource::MeshcoreModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_bandwidth(MeshcoreModSettings::bandwidths[5]), + m_phaseIncrements(nullptr), + m_repeatCount(0), + m_txFrameToken(0U), + 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, + MeshcoreModSettings::bandwidths[m_settings.m_bandwidthIndex], + m_channelFrequencyOffset, + true + ); +} + +MeshcoreModSource::~MeshcoreModSource() +{ + delete[] m_phaseIncrements; +} + +void MeshcoreModSource::initSF(unsigned int sf) +{ + m_fftLength = 1 << sf; + m_state = ChirpChatStateIdle; + m_quarterSamples = (m_fftLength/4)*MeshcoreModSettings::oversampling; + + float halfAngle = M_PI/MeshcoreModSettings::oversampling; + float phase = -halfAngle; + + if (m_phaseIncrements) { + delete[] m_phaseIncrements; + } + + m_phaseIncrements = new double[2*m_fftLength*MeshcoreModSettings::oversampling]; + phase = -halfAngle; + + for (unsigned int i = 0; i < m_fftLength*MeshcoreModSettings::oversampling; i++) + { + m_phaseIncrements[i] = phase; + phase += (2*halfAngle) / (m_fftLength*MeshcoreModSettings::oversampling); + } + + std::copy( + m_phaseIncrements, + m_phaseIncrements+m_fftLength*MeshcoreModSettings::oversampling, + m_phaseIncrements+m_fftLength*MeshcoreModSettings::oversampling + ); +} + +void MeshcoreModSource::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 MeshcoreModSource::modulateSample() +{ + if (m_state == ChirpChatStateIdle) + { + m_modSample = Complex{0.0, 0.0}; + m_sampleCounter++; + + if (m_sampleCounter == m_quietSamples*MeshcoreModSettings::oversampling) // done with quiet time + { + m_chirp0 = 0; + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + + 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*MeshcoreModSettings::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; + // Standard LoRa: chirp index walks phaseIncrements[chirp0*os .. (chirp0+N)*os - 1]. + // Old init `(chirp0+N)*os - 1` triggered immediate wrap on first m_chirp++, + // causing first sample to read phaseInc[N*os-1] (high freq) before ramping + // up from phaseInc[0]. ~0.25 FFT-bin freq drift, breaking demod header CRC. + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + m_state = ChirpChatStateSyncWord; + } + else + { + m_sampleCounter = 0; + m_chirp0 = 0; + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + 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*MeshcoreModSettings::oversampling) + { + m_chirpCount++; + m_fftCounter = 0; + + if (m_chirpCount >= 2) + { + m_sampleCounter = 0; + m_chirpCount = 0; + m_chirp0 = 0; + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + m_state = ChirpChatStateSFD; + } + else + { + m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8; + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + } + } + } + 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*MeshcoreModSettings::oversampling) + { + m_chirp0 = 0; + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + 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], MeshcoreModSettings::m_hasHeader && (m_chirpCount < 8U)); + m_txFrameToken++; + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + 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*MeshcoreModSettings::oversampling) + { + m_chirpCount++; + + if (m_chirpCount == m_symbols.size()) + { + reset(); + m_state = ChirpChatStateIdle; + } + else + { + m_chirp0 = encodeSymbol(m_symbols[m_chirpCount], MeshcoreModSettings::m_hasHeader && (m_chirpCount < 8U)); + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + 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)*MeshcoreModSettings::oversampling) { + m_chirp = m_chirp0*MeshcoreModSettings::oversampling; + } +} + +unsigned short MeshcoreModSource::encodeSymbol(unsigned short symbol, bool headerSymbol) const +{ + auto deBits = static_cast(std::max(0, m_settings.m_deBits)); + + if (headerSymbol && deBits < 2U) { + deBits = 2U; + } + + const unsigned int deWidth = 1U << deBits; + const unsigned int symbolRange = std::max(1U, m_fftLength / std::max(1U, deWidth)); + const unsigned int baseSymbol = symbol % symbolRange; + const unsigned int rawSymbol = (deWidth * baseSymbol + 1U) % m_fftLength; // match demod evalSymbol shift (raw_bin - 1) + return static_cast(rawSymbol); +} + +void MeshcoreModSource::calculateLevel(Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(m_peakLevel, std::fabs(sample)); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void MeshcoreModSource::applySettings(const MeshcoreModSettings& settings, bool force) +{ + if ((settings.m_spreadFactor != m_settings.m_spreadFactor) + || (settings.m_deBits != m_settings.m_deBits) + || (settings.m_preambleChirps != m_settings.m_preambleChirps)|| force) + { + initSF(settings.m_spreadFactor); + initTest(settings.m_spreadFactor, settings.m_deBits); + reset(); + } + + if ((settings.m_quietMillis != m_settings.m_quietMillis) || force) + { + m_quietSamples = (m_bandwidth*settings.m_quietMillis) / 1000; + reset(); + } + + if ((settings.m_messageRepeat != m_settings.m_messageRepeat) || force) { + m_repeatCount = settings.m_messageRepeat; + } + + m_settings = settings; +} + +void MeshcoreModSource::applyChannelSettings(int channelSampleRate, int bandwidth, int channelFrequencyOffset, bool force) +{ + qWarning() << "MeshcoreModSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset + << " bandwidth: " << bandwidth + << " SR: " << bandwidth * MeshcoreModSettings::oversampling; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) + || (channelSampleRate != m_channelSampleRate) || force) + { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) + || (bandwidth != m_bandwidth) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) (bandwidth*MeshcoreModSettings::oversampling) / (Real) channelSampleRate; + m_interpolator.create(16, bandwidth, bandwidth / 2.2); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; + m_bandwidth = bandwidth; + m_quietSamples = (bandwidth*m_settings.m_quietMillis) / 1000; + m_state = ChirpChatStateIdle; + reset(); +} + +void MeshcoreModSource::setSymbols(const std::vector& symbols) +{ + m_symbols = symbols; + qDebug("MeshcoreModSource::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*MeshcoreModSettings::oversampling - 1; // start immediately +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodsource.h b/plugins/channeltx/modmeshcore/meshcoremodsource.h new file mode 100644 index 000000000..56a50a718 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodsource.h @@ -0,0 +1,114 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Alejandro Aleman // +// Copyright (C) 2019-2026 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_MESHCOREMODSOURCE_H +#define INCLUDE_MESHCOREMODSOURCE_H + +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" + +#include "meshcoremodsettings.h" + +class MeshcoreModSource : public ChannelSampleSource +{ +public: + MeshcoreModSource(); + virtual ~MeshcoreModSource(); + + 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 MeshcoreModSettings& 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 //!< Transmit payload + }; + + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_bandwidth; + MeshcoreModSettings 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 + unsigned int m_txFrameToken; //!< monotonically increasing loopback trace token + 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, bool headerSymbol) const; //!< Encodes symbol with payload/header DE spacing +}; + +#endif // INCLUDE_MESHCOREMODSOURCE_H diff --git a/plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.cpp b/plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.cpp new file mode 100644 index 000000000..32bc46a42 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.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 "meshcoremod.h" +#include "meshcoremodwebapiadapter.h" + +MeshcoreModWebAPIAdapter::MeshcoreModWebAPIAdapter() +{} + +MeshcoreModWebAPIAdapter::~MeshcoreModWebAPIAdapter() +{} + +int MeshcoreModWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setChirpChatModSettings(new SWGSDRangel::SWGChirpChatModSettings()); + response.getChirpChatModSettings()->init(); + MeshcoreMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int MeshcoreModWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + MeshcoreMod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + MeshcoreMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} diff --git a/plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.h b/plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.h new file mode 100644 index 000000000..6dadc5707 --- /dev/null +++ b/plugins/channeltx/modmeshcore/meshcoremodwebapiadapter.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_MESHCOREMOD_WEBAPIADAPTER_H +#define INCLUDE_MESHCOREMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "meshcoremodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class MeshcoreModWebAPIAdapter : public ChannelWebAPIAdapter { +public: + MeshcoreModWebAPIAdapter(); + virtual ~MeshcoreModWebAPIAdapter(); + + 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: + MeshcoreModSettings m_settings; +}; + +#endif // INCLUDE_MESHCOREMOD_WEBAPIADAPTER_H diff --git a/plugins/channeltx/modmeshcore/readme.md b/plugins/channeltx/modmeshcore/readme.md new file mode 100644 index 000000000..29a91bae6 --- /dev/null +++ b/plugins/channeltx/modmeshcore/readme.md @@ -0,0 +1,77 @@ +

MeshCore modulator plugin

+ +

Introduction

+ +This plugin codes and modulates a transmission signal based the LoRa Chirp +Spread Spectrum (CSS) modulation scheme with a MeshCore payload. + +MeshCore (https://github.com/meshcore-dev/MeshCore, MIT) is a mesh networking +protocol distinct from Meshtastic. The two share LoRa as the PHY layer but +differ at the packet layer (envelope, addressing, crypto, routing). This +plugin builds and transmits MeshCore wire packets — ADVERT, TXT_MSG, +ANON_REQ, GRP_TXT, ACK — using vendored crypto (Monocypher for Ed25519 + +X25519, tiny-AES-c for AES-128-ECB, Qt SHA-256 + hand-rolled HMAC-SHA256). + +The encode pipeline is derived from the Meshtastic modulator +(LoRa-SDR-based, configurable BW/SF/CR/sync). MeshCore EU defaults +are baked in: **869.618 MHz / 62.5 kHz / SF 8 / CR 4/8**, sync word +0x12. + +This is a companion to the MeshCore demodulator plugin +(`channelrx/demodmeshcore`) and the standalone `lora_trx` headless +transceiver in gr4-lora. + +

Status

+ +Functional surfaces: +- TX of ADVERT / TXT_MSG / ANON_REQ / GRP_TXT / ACK / PATH / CTRL + via the `MESHCORE:` command syntax below. +- LoRa PHY with MeshCore EU radio defaults. +- Identity store: an Ed25519 seed is persisted under the application + support directory, auto-loaded by `MeshcoreMod`, and used to sign + outgoing ADVERTs. Encoder output is bit-exact against gr4-lora's + `meshcore_tx.py::build_advert_raw` reference for the same seed and + timestamp. +- REST sendNow action (`SWGMeshcoreModActions`) for programmatic + single-packet TX. +- Radio parameters (BW, SF, CR, preamble length, frequency offset) + exposed via combo controls. BW combo includes 62500 / 125000 / + 250000 Hz — the bandwidths used by the MeshCore firmware CLI's + `set radio freq_MHz,bw_kHz,sf,cr` command. +- Preset combo populated with the well-known MeshCore regional + defaults (EU_NARROW, EU_LONG_RANGE, EU_MEDIUM_RANGE, AU, + AU_VICTORIA, CZ_NARROW, EU_433_LONG_RANGE, NZ, NZ_NARROW, PT_433, + PT_868, CH, USA, VN, USER). Default is `EU_NARROW` (869.618 MHz / + SF 8 / BW 62.5 kHz / CR 4/8). Selecting a preset applies its + freq/BW/SF/CR via `modemmeshcore::command::applyMeshcorePreset`. +- The Region and Channel controls are hidden: region is implicit + in the preset frequency, and MeshCore has no numbered Channel + concept. + +Outstanding: +- WebAPI schema reuses `SWGMeshtasticModSettings` as a placeholder; a + dedicated `SWGMeshcoreMod*` schema would require regenerating the + SWG bindings. + +

MESHCORE: command syntax

+ +Send a MeshCore wire packet by setting the channel's "Text message" to a +line starting with `MESHCORE:`: + +``` +MESHCORE: type=advert; seed=; name=NodeA +MESHCORE: type=txt_msg; seed=; dest=; text=Hello +MESHCORE: type=anon_req; seed=; dest=; data= +MESHCORE: type=grp_txt; channel=public; text=Hello group +MESHCORE: type=ack; dest=; msg_hash= +``` + +Optional radio overrides on any line: `sf=8`, `bw=62500`, `cr=8` (for 4/8), +`sync=0x12`, `freq=869.618M`, `preamble=8`. + +

References

+ +- MeshCore protocol: https://github.com/meshcore-dev/MeshCore +- gr4-lora python implementation: scripts/src/lora/core/meshcore_crypto.py +- Vendored Monocypher: https://monocypher.org (BSD-2-Clause OR CC0-1.0) +- Vendored tiny-AES-c: https://github.com/kokke/tiny-AES-c (public domain) diff --git a/swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.cpp b/swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.cpp new file mode 100644 index 000000000..7b5ce263a --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.cpp @@ -0,0 +1,107 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGMeshcoreModActions.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGMeshcoreModActions::SWGMeshcoreModActions(QString* json) { + init(); + this->fromJson(*json); +} + +SWGMeshcoreModActions::SWGMeshcoreModActions() { + send_now = 0; + m_send_now_isSet = false; +} + +SWGMeshcoreModActions::~SWGMeshcoreModActions() { + this->cleanup(); +} + +void +SWGMeshcoreModActions::init() { + send_now = 0; + m_send_now_isSet = false; +} + +void +SWGMeshcoreModActions::cleanup() { +} + +SWGMeshcoreModActions* +SWGMeshcoreModActions::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGMeshcoreModActions::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&send_now, pJson["sendNow"], "qint32", ""); + +} + +QString +SWGMeshcoreModActions::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGMeshcoreModActions::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_send_now_isSet){ + obj->insert("sendNow", QJsonValue(send_now)); + } + + return obj; +} + +qint32 +SWGMeshcoreModActions::getSendNow() { + return send_now; +} +void +SWGMeshcoreModActions::setSendNow(qint32 send_now) { + this->send_now = send_now; + this->m_send_now_isSet = true; +} + + +bool +SWGMeshcoreModActions::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_send_now_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.h b/swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.h new file mode 100644 index 000000000..c94e9bea4 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGMeshcoreModActions.h @@ -0,0 +1,57 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGMeshcoreModActions.h + * + * MeshcoreMod + */ + +#ifndef SWGMeshcoreModActions_H_ +#define SWGMeshcoreModActions_H_ + +#include + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGMeshcoreModActions: public SWGObject { +public: + SWGMeshcoreModActions(); + SWGMeshcoreModActions(QString* json); + virtual ~SWGMeshcoreModActions(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGMeshcoreModActions* fromJson(QString &jsonString) override; + + qint32 getSendNow(); + void setSendNow(qint32 send_now); + + + virtual bool isSet() override; + +private: + qint32 send_now; + bool m_send_now_isSet; + +}; + +} + +#endif /* SWGMeshcoreModActions_H_ */