1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-06-26 13:33:23 -04:00

meshcore: add MeshCore protocol RX/TX channel plugins

This commit is contained in:
Tom Hensel
2026-06-09 23:08:56 +02:00
parent 964bc0994d
commit 92552f6b4f
69 changed files with 26529 additions and 0 deletions
+54
View File
@@ -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)
+330
View File
@@ -0,0 +1,330 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QCryptographicHash>
#include <QtEndian>
#include <cmath>
#include <cstring>
namespace modemmeshcore
{
namespace builders
{
namespace
{
// little-endian uint32 -> 4 bytes
QByteArray u32LE(uint32_t v)
{
QByteArray out(4, '\0');
qToLittleEndian(v, reinterpret_cast<uchar*>(out.data()));
return out;
}
QByteArray i32LE(int32_t v)
{
QByteArray out(4, '\0');
qToLittleEndian(v, reinterpret_cast<uchar*>(out.data()));
return out;
}
QByteArray u16LE(uint16_t v)
{
QByteArray out(2, '\0');
qToLittleEndian(v, reinterpret_cast<uchar*>(out.data()));
return out;
}
} // namespace
uint8_t makeHeader(uint8_t routeType, uint8_t payloadType, uint8_t version)
{
return static_cast<uint8_t>(
((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<char>(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<char>(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<uint8_t>(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<char>(flags));
if (opts.latLon) {
const int32_t latI = static_cast<int32_t>(std::lround(opts.latLon->first * 1e6));
const int32_t lonI = static_cast<int32_t>(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<char>(((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<char>(((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
+178
View File
@@ -0,0 +1,178 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QByteArray>
#include <QString>
#include <optional>
#include <utility>
#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<std::pair<uint16_t, uint16_t>> 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<std::pair<double, double>> 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<std::pair<uint16_t, uint16_t>> 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<std::pair<uint16_t, uint16_t>> 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<std::pair<uint16_t, uint16_t>> 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<std::pair<uint16_t, uint16_t>> 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<std::pair<uint16_t, uint16_t>> 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_
+546
View File
@@ -0,0 +1,546 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QDateTime>
#include <QStringList>
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<QString, QString>& 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<QString, QString>& kv)
{
const QString ts = get(kv, "ts");
if (ts.isEmpty()) {
return static_cast<uint32_t>(QDateTime::currentSecsSinceEpoch());
}
uint32_t v = 0;
return toUInt(ts, v) ? v : 0;
}
} // namespace
bool tokenize(const QString& body, QMap<QString, QString>& 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<QString, QString>& 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<int>(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<int>(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<int>(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<uint8_t>(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<qint64>(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<int>(pr);
settings.hasLoRaParams = true;
changed = true;
}
if (changed) {
settings.hasCommand = true;
}
return true;
}
namespace
{
bool buildAdvertFromKv(const QMap<QString, QString>& 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<QString, QString>& 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<uint8_t>(txtType);
opts.attempt = static_cast<uint8_t>(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<QString, QString>& 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<QString, QString>& 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<uint8_t>(txtType);
opts.attempt = static_cast<uint8_t>(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<QString, QString>& 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<QString, QString>& 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
+73
View File
@@ -0,0 +1,73 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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=<hex64>; text=Hello //
// MESHCORE: type=grp_txt; channel=public; text=Hello group //
// MESHCORE: type=ack; dest=<hex64>; msg_hash=<hex8> //
// //
// 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 <QByteArray>
#include <QMap>
#include <QString>
#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<QString, QString>& 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<QString, QString>& 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:<utf8>). 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<QString, QString>& kv,
QByteArray& frame,
QString& summary,
QString& error);
} // namespace command
} // namespace modemmeshcore
#endif // MODEMMESHCORE_MESHCORE_COMMAND_H_
+280
View File
@@ -0,0 +1,280 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QCryptographicHash>
#include <algorithm>
#include <cstring>
namespace modemmeshcore
{
namespace detail
{
// ---- scalar clamping ----------------------------------------------------------
QByteArray clampScalar(const QByteArray& scalar)
{
if (scalar.size() != kPubKeySize) {
return QByteArray();
}
QByteArray out(scalar);
auto* p = reinterpret_cast<uint8_t*>(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<const uint8_t*>(seed.constData()),
static_cast<size_t>(seed.size()));
// Clamp the first 32 bytes (becomes the Ed25519 scalar).
hash[0] &= 248;
hash[31] &= 63;
hash[31] |= 64;
return QByteArray(reinterpret_cast<const char*>(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<uint8_t*>(pub32.data()),
reinterpret_cast<uint8_t*>(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<const uint8_t*>(otherPub32.constData()));
uint8_t shared[32];
crypto_x25519(shared,
reinterpret_cast<const uint8_t*>(expanded.constData()),
curvePk);
return QByteArray(reinterpret_cast<const char*>(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<char>(static_cast<uint8_t>(k[i]) ^ 0x36);
outerPad[i] = static_cast<char>(static_cast<uint8_t>(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<const uint8_t*>(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<const uint8_t*>(padded.constData() + off),
reinterpret_cast<uint8_t*>(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<const uint8_t*>(shared.constData()), kCipherKeySize)) {
return QByteArray();
}
QByteArray plaintext(ciphertext.size(), '\0');
for (int off = 0; off < ciphertext.size(); off += kCipherBlockSize) {
aes.decryptBlock(reinterpret_cast<const uint8_t*>(ciphertext.constData() + off),
reinterpret_cast<uint8_t*>(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<uint8_t*>(seedCopy.data()));
QByteArray signature(kSignatureSize, '\0');
crypto_ed25519_sign(reinterpret_cast<uint8_t*>(signature.data()),
mcSecretKey,
reinterpret_cast<const uint8_t*>(message.constData()),
static_cast<size_t>(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<const uint8_t*>(signature.constData()),
reinterpret_cast<const uint8_t*>(pub32.constData()),
reinterpret_cast<const uint8_t*>(message.constData()),
static_cast<size_t>(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<uint8_t>(((hashSize - 1) << kPathModeShift) | (hashCount & kPathCountMask));
}
} // namespace modemmeshcore
+81
View File
@@ -0,0 +1,81 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QByteArray>
#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_
+489
View File
@@ -0,0 +1,489 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QStringList>
#include <QtEndian>
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<uint32_t>(reinterpret_cast<const uchar*>(wire.constData() + off));
}
int32_t readI32LE(const QByteArray& wire, int off)
{
if (off + 4 > wire.size()) {
return 0;
}
return qFromLittleEndian<int32_t>(reinterpret_cast<const uchar*>(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<uint32_t>(reinterpret_cast<const uchar*>(plaintext.constData()));
const uint8_t flags = static_cast<uint8_t>(plaintext[4]);
txtType = static_cast<uint8_t>(flags >> 2);
attempt = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(wire[off]);
const PathLen p = decodePathLen(static_cast<uint8_t>(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<KeySpec>& 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:<name> 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:<name> 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<uint8_t>(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<KeySpec>& 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<KeySpec>& 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<KeySpec>& 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<uint32_t>(reinterpret_cast<const uchar*>(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
+113
View File
@@ -0,0 +1,113 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QByteArray>
#include <QString>
#include <QVector>
#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=<hex64> our 32-byte Ed25519 seed
// contact:<name>=<hex64> a known peer's 32-byte Ed25519 pubkey
// channel:<name>=<hex16|hex32|name=public> 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<KeySpec>& 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<KeySpec>& 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<KeySpec>& 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<KeySpec>& keys,
DecodeResult& out);
} // namespace decoder
} // namespace modemmeshcore
#endif // MODEMMESHCORE_MESHCORE_DECODER_H_
+144
View File
@@ -0,0 +1,144 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QByteArray>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRandomGenerator>
#include <QSaveFile>
#include <QStandardPaths>
#include <QString>
#include <QtGlobal>
#include "meshcore_crypto.h"
#ifndef Q_OS_WIN
#include <sys/stat.h>
#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<quint32*>(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
+83
View File
@@ -0,0 +1,83 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QByteArray>
#include <QString>
#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: <AppDataLocation>/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_
+165
View File
@@ -0,0 +1,165 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QDebug>
#include <QProcessEnvironment>
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<int>(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<QString, QString> 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<decoder::KeySpec> 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<decoder::KeySpec> 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<QString, QString> 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
+193
View File
@@ -0,0 +1,193 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef MODEMMESHCORE_MESHCOREPACKET_H_
#define MODEMMESHCORE_MESHCOREPACKET_H_
#include <QByteArray>
#include <QProcessEnvironment>
#include <QString>
#include <QtGlobal>
#include <QVector>
#include <stdint.h>
#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<Field> 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_
+500
View File
@@ -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
// <https://creativecommons.org/publicdomain/zero/1.0/>
#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
+140
View File
@@ -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
// <https://creativecommons.org/publicdomain/zero/1.0/>
#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
File diff suppressed because it is too large Load Diff
+321
View File
@@ -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
// <https://creativecommons.org/publicdomain/zero/1.0/>
#ifndef MONOCYPHER_H
#define MONOCYPHER_H
#include <stddef.h>
#include <stdint.h>
#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
+60
View File
@@ -0,0 +1,60 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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=<hex64>; 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 <QByteArray>
#include <QString>
#include <cstdio>
#include <cstdlib>
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;
}
+564
View File
@@ -0,0 +1,564 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
// //
// 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 <QByteArray>
#include <QMap>
#include <cstdio>
#include <cstdlib>
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<char>(static_cast<uint8_t>(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<char>(static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(pkt[0]) & kRouteMask) == RouteDirect,
"TXT_MSG route=Direct");
REQUIRE(((static_cast<uint8_t>(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask)
== PayloadTxt, "TXT_MSG payload=Txt");
REQUIRE(static_cast<uint8_t>(pkt[1]) == 0, "TXT_MSG path_len=0");
// dest_hash and src_hash at offsets 2 and 3
REQUIRE(static_cast<uint8_t>(pkt[2]) == static_cast<uint8_t>(pubB[0]),
"TXT_MSG dest_hash = pubB[0]");
REQUIRE(static_cast<uint8_t>(pkt[3]) == static_cast<uint8_t>(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<uint8_t>(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask)
== PayloadAnonReq, "ANON_REQ payload=AnonReq");
// dest_hash at 2, sender_pub at 3..3+32
REQUIRE(static_cast<uint8_t>(pkt[2]) == static_cast<uint8_t>(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<uint8_t>(pkt[0]) >> kPayloadTypeShift) & kPayloadTypeMask)
== PayloadGrpTxt, "GRP_TXT payload=GrpTxt");
REQUIRE(static_cast<uint8_t>(pkt[2]) == static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(pkt[2]) == static_cast<uint8_t>(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<QString, QString> 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<QString, QString> 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<uint8_t>(frame[0]) & kRouteMask) == RouteFlood,
"advert command default route=Flood");
REQUIRE(((static_cast<uint8_t>(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<uint8_t>(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;
}
+319
View File
@@ -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 <array>
#include <cstdint>
#include <cstring>
#include <vector>
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<int>(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<uint8_t, 16> 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<uint8_t, 16> 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<uint8_t> m_roundKey;
static uint8_t xtime(uint8_t x)
{
return static_cast<uint8_t>((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<uint8_t, 256> 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<uint8_t>(mul(a0, 2) ^ mul(a1, 3) ^ a2 ^ a3);
col[1] = static_cast<uint8_t>(a0 ^ mul(a1, 2) ^ mul(a2, 3) ^ a3);
col[2] = static_cast<uint8_t>(a0 ^ a1 ^ mul(a2, 2) ^ mul(a3, 3));
col[3] = static_cast<uint8_t>(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<uint8_t, 256> 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<uint8_t>(mul(a0, 0x0e) ^ mul(a1, 0x0b) ^ mul(a2, 0x0d) ^ mul(a3, 0x09));
col[1] = static_cast<uint8_t>(mul(a0, 0x09) ^ mul(a1, 0x0e) ^ mul(a2, 0x0b) ^ mul(a3, 0x0d));
col[2] = static_cast<uint8_t>(mul(a0, 0x0d) ^ mul(a1, 0x09) ^ mul(a2, 0x0e) ^ mul(a3, 0x0b));
col[3] = static_cast<uint8_t>(mul(a0, 0x0b) ^ mul(a1, 0x0d) ^ mul(a2, 0x09) ^ mul(a3, 0x0e));
}
}
static uint32_t subWord(uint32_t w)
{
return (static_cast<uint32_t>(sub(static_cast<uint8_t>((w >> 24) & 0xFF))) << 24)
| (static_cast<uint32_t>(sub(static_cast<uint8_t>((w >> 16) & 0xFF))) << 16)
| (static_cast<uint32_t>(sub(static_cast<uint8_t>((w >> 8) & 0xFF))) << 8)
| static_cast<uint32_t>(sub(static_cast<uint8_t>( w & 0xFF)));
}
static uint32_t rotWord(uint32_t w)
{
return (w << 8) | (w >> 24);
}
void keyExpansion(const uint8_t* key)
{
static const std::array<uint8_t, 11> rcon = {
0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36
};
const int words = 4 * (m_nr + 1);
std::vector<uint32_t> w(words, 0);
for (int i = 0; i < m_nk; ++i)
{
w[i] = (static_cast<uint32_t>(key[4 * i]) << 24)
| (static_cast<uint32_t>(key[4 * i + 1]) << 16)
| (static_cast<uint32_t>(key[4 * i + 2]) << 8)
| static_cast<uint32_t>(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<uint32_t>(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<uint8_t>((w[i] >> 24) & 0xFF);
m_roundKey[4 * i + 1] = static_cast<uint8_t>((w[i] >> 16) & 0xFF);
m_roundKey[4 * i + 2] = static_cast<uint8_t>((w[i] >> 8) & 0xFF);
m_roundKey[4 * i + 3] = static_cast<uint8_t>( w[i] & 0xFF);
}
}
};
} // namespace modemmeshcore
#endif // MODEMMESHCORE_TINY_AES_H_
@@ -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_PROPERTY:${TARGET_NAME},RUNTIME_OUTPUT_DIRECTORY>/${TARGET_NAME}stripped.pdb CONFIGURATIONS Release DESTINATION ${INSTALL_FOLDER} RENAME ${TARGET_NAME}.pdb )
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()
File diff suppressed because it is too large Load Diff
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2015 John Greb <hexameron@spam.no> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// (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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREDEMOD_H
#define INCLUDE_MESHCOREDEMOD_H
#include <vector>
#include <QNetworkRequest>
#include <QVector>
#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<MeshcoreDemodSettings>& getSettingsList() const { return m_settingsList; }
static MsgSetExtraPipelineSettings* create(const QVector<MeshcoreDemodSettings>& settingsList)
{
return new MsgSetExtraPipelineSettings(settingsList);
}
private:
QVector<MeshcoreDemodSettings> m_settingsList;
explicit MsgSetExtraPipelineSettings(const QVector<MeshcoreDemodSettings>& 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<PipelineConfig> m_pipelineConfigs;
std::vector<PipelineRuntime> 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<uint8_t> 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<PipelineConfig>& configs);
void stopPipelines();
void applyPipelineRuntimeSettings(PipelineRuntime& runtime, const MeshcoreDemodSettings& settings, bool force);
bool pipelineLayoutMatches(const std::vector<PipelineConfig>& configs) const;
void syncPipelinesWithSettings(const MeshcoreDemodSettings& settings, bool force);
void applyExtraPipelineSettings(const QVector<MeshcoreDemodSettings>& settingsList, bool force = false);
void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response);
void webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const MeshcoreDemodSettings& settings, bool force);
void sendChannelSettings(
const QList<ObjectPipe*>& pipes,
QList<QString>& channelSettingsKeys,
const MeshcoreDemodSettings& settings,
bool force
);
void webapiFormatChannelSettings(
QList<QString>& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
const MeshcoreDemodSettings& settings,
bool force
);
private slots:
void networkManagerFinished(QNetworkReply *reply);
void handleIndexInDeviceSetChanged(int index);
};
#endif // INCLUDE_MESHCOREDEMOD_H
@@ -0,0 +1,190 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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()
);
}
@@ -0,0 +1,89 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREDEMODBASEBAND_H
#define INCLUDE_MESHCOREDEMODBASEBAND_H
#include <QObject>
#include <QRecursiveMutex>
#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
@@ -0,0 +1,397 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QTime>
#include <algorithm>
#include <cmath>
#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<unsigned short>& 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<unsigned short>& 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<double>(std::max(1U, bandwidth))) > 16.0;
const int denom = static_cast<int>(spreadFactor) - (ldro ? 2 : 0);
if (denom > 0)
{
const int numerator =
2 * static_cast<int>(packetLength)
- static_cast<int>(spreadFactor)
+ 2
+ 5 // explicit header path (!impl_head)
+ (hasCRC ? 4 : 0);
const int payloadBlocks = std::max(0, static_cast<int>(std::ceil(static_cast<double>(numerator) / static_cast<double>(denom))));
expectedSymbols = 8U + static_cast<unsigned int>(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<std::vector<float>>& 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<unsigned short> 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<int>(shifted[i]);
const int v = (s + delta) % static_cast<int>(mod);
shifted[i] = static_cast<unsigned short>(v < 0 ? (v + static_cast<int>(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<long long>(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<long long>(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;
}
}
}
@@ -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 <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREDEMODDECODER_H
#define INCLUDE_MESHCOREDEMODDECODER_H
#include <vector>
#include <QObject>
#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<unsigned short>& 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
@@ -0,0 +1,682 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <algorithm>
#include "meshcoredemodsettings.h"
#include "meshcoredemoddecoderlora.h"
void MeshcoreDemodDecoderLoRa::decodeHeader(
const std::vector<unsigned short>& 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<uint16_t> symbols(headerSymbols);
std::copy(inSymbols.begin(), inSymbols.begin() + headerSymbols, symbols.begin());
//gray encode
for (auto &sym : symbols) {
sym = binaryToGray16(sym);
}
std::vector<uint8_t> 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<unsigned short>& 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<unsigned int>(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<unsigned int>(inSymbols.size()) / payloadBlockSymbols;
numSymbols = payloadBlocks * payloadBlockSymbols;
numCodewords = payloadBlocks * payloadNbSymbolBits;
}
if (numSymbols < headerSymbols)
{
earlyEOM = true;
return;
}
std::vector<uint16_t> 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<uint8_t> 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<uint8_t> 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<uint16_t>(crc ^ static_cast<uint8_t>(bytes[dOfs + packetLength - 1U]));
crc = static_cast<uint16_t>(crc ^ (static_cast<uint16_t>(static_cast<uint8_t>(bytes[dOfs + packetLength - 2U])) << 8));
const uint16_t packetCRC = static_cast<uint16_t>(static_cast<uint8_t>(bytes[dOfs + packetLength]))
| (static_cast<uint16_t>(static_cast<uint8_t>(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<uint16_t>(crc ^ static_cast<uint8_t>(bytes[packetLength - 1U]));
crc = static_cast<uint16_t>(crc ^ (static_cast<uint16_t>(static_cast<uint8_t>(bytes[packetLength - 2U])) << 8));
const uint16_t packetCRC = static_cast<uint16_t>(static_cast<uint8_t>(bytes[packetLength]))
| (static_cast<uint16_t>(static_cast<uint8_t>(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<std::vector<float>>& inMagnitudes,
const std::vector<unsigned short>& 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<unsigned int>(inSymbols.size() - headerSymbols)
: 0U;
const unsigned int payloadBlocks = payloadSymbols / payloadBlockSymbols;
numSymbols = headerSymbols + payloadBlocks * payloadBlockSymbols;
}
else
{
const unsigned int payloadBlocks = static_cast<unsigned int>(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<double>(std::max(1U, bandwidth))) > 16.0;
std::vector<std::vector<float>> llrs(numSymbols, std::vector<float>(spreadFactor, 0.0f));
for (unsigned int symIdx = 0; symIdx < numSymbols; symIdx++)
{
const std::vector<float>& 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<float>::lowest();
float maxX0 = std::numeric_limits<float>::lowest();
for (unsigned int n = 0; n < N; n++)
{
unsigned int s = static_cast<unsigned int>(modInt(static_cast<int>(n) - 1, static_cast<int>(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<uint8_t>& nibbles) {
if (sfApp == 0U) {
return false;
}
std::vector<std::vector<float>> interBin(cwLen, std::vector<float>(sfApp, 0.0f));
std::vector<std::vector<float>> deinterBin(sfApp, std::vector<float>(cwLen, 0.0f));
for (unsigned int i = 0; i < cwLen; i++)
{
const std::vector<float>& 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<unsigned int>(modInt(static_cast<int>(i) - static_cast<int>(j) - 1, static_cast<int>(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<uint8_t> 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<uint8_t> 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<uint8_t>(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<uint16_t>(crc ^ bytes[packetLength - 1U]);
crc = static_cast<uint16_t>(crc ^ (static_cast<uint16_t>(bytes[packetLength - 2U]) << 8));
const uint16_t packetCRC = static_cast<uint16_t>(bytes[packetLength])
| (static_cast<uint16_t>(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);
}
}
@@ -0,0 +1,483 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREDEMODDECODERLORA_H
#define INCLUDE_MESHCOREDEMODDECODERLORA_H
#include <cmath>
#include <limits>
#include <vector>
#include <QByteArray>
class MeshcoreDemodDecoderLoRa
{
public:
static void decodeBytes(
QByteArray& bytes,
const std::vector<unsigned short>& 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<std::vector<float>>& inMagnitudes,
const std::vector<unsigned short>& 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<unsigned short>& 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<int>(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<int>(nbSymbolBits); j++)
{
const uint8_t bit = (sym >> (static_cast<int>(nbSymbolBits) - 1 - j)) & 0x1;
const int row = ((i - j - 1) % static_cast<int>(nbSymbolBits) + static_cast<int>(nbSymbolBits)) % static_cast<int>(nbSymbolBits);
codewords[cwOff + static_cast<unsigned int>(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<unsigned int>(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<uint8_t>(codeword[3]),
static_cast<uint8_t>(codeword[2]),
static_cast<uint8_t>(codeword[1]),
static_cast<uint8_t>(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<int>(s0) + (static_cast<int>(s1) << 1) + (static_cast<int>(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<unsigned char>((nibbleBits[0] << 3) | (nibbleBits[1] << 2) | (nibbleBits[2] << 1) | nibbleBits[3]);
}
static inline unsigned char decodeCodewordSoft(const std::vector<float>& 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<float>::lowest();
unsigned int bestIdx = 0U;
for (unsigned int n = 0; n < 16U; n++)
{
const unsigned char cw = static_cast<unsigned char>(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<unsigned char>(cwLUT[bestIdx] >> 4);
return static_cast<unsigned char>(
(((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<uint16_t>((crc << 1) ^ 0x1021);
} else {
crc = static_cast<uint16_t>(crc << 1);
}
b <<= 1;
}
}
return crc;
}
};
#endif // INCLUDE_MESHCOREDEMODDECODERLORA_H
File diff suppressed because it is too large Load Diff
@@ -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 <hexameron@spam.no> //
// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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 <vector>
#include <QMap>
#include <QVector>
#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<int, PipelineView> 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<MeshAutoLockCandidate> 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<std::vector<float>> lines;
};
QMap<QString, DechirpSnapshot> m_dechirpSnapshots;
QVector<QString> m_dechirpSnapshotOrder;
bool m_dechirpInspectionActive;
QString m_dechirpSelectedMessageKey;
QString m_replayPendingMessageKey;
bool m_replayPendingHasSelection;
bool m_replaySelectionQueued;
QMap<QString, QString> m_pipelineMessageKeyByBase;
QMap<QString, QVector<QString>> m_pipelinePendingMessageKeysByBase;
quint64 m_pipelineMessageSequence;
// Multi-pipeline management
static constexpr int kMaxPipelines = 4;
int m_focusedPipelineIndex; //!< 0 = primary; 1-3 = extra (volatile) pipelines
QVector<MeshcoreDemodSettings> 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<QPair<QString, QString>>& 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
File diff suppressed because it is too large Load Diff
@@ -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 <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "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)
@@ -0,0 +1,545 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREDEMODMSG_H
#define INCLUDE_MESHCOREDEMODMSG_H
#include <vector>
#include <QObject>
#include <QPair>
#include <QVector>
#include "util/message.h"
#include "meshcoredemodsettings.h"
namespace MeshcoreDemodMsg
{
class MsgDecodeSymbols : public Message {
MESSAGE_CLASS_DECLARATION
public:
const std::vector<unsigned short>& getSymbols() const { return m_symbols; }
const std::vector<std::vector<float>>& getMagnitudes() const { return m_magnitudes; }
const std::vector<std::vector<float>>& 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<float>& magnitudes) {
m_magnitudes.push_back(magnitudes);
}
void pushBackDechirpedSpectrumLine(const std::vector<float>& spectrumLine) {
m_dechirpedSpectrum.push_back(spectrumLine);
}
void dropFront(unsigned int count)
{
const unsigned int symbolsDrop = std::min<unsigned int>(count, static_cast<unsigned int>(m_symbols.size()));
m_symbols.erase(m_symbols.begin(), m_symbols.begin() + symbolsDrop);
const unsigned int magnitudesDrop = std::min<unsigned int>(count, static_cast<unsigned int>(m_magnitudes.size()));
m_magnitudes.erase(m_magnitudes.begin(), m_magnitudes.begin() + magnitudesDrop);
const unsigned int spectrumDrop = std::min<unsigned int>(count, static_cast<unsigned int>(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<unsigned short> symbols) {
return new MsgDecodeSymbols(symbols);
}
private:
std::vector<unsigned short> m_symbols;
std::vector<std::vector<float>> m_magnitudes;
std::vector<std::vector<float>> 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<unsigned short> 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<unsigned short>& 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<unsigned short>& 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<unsigned short> 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<unsigned short>& 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<std::vector<float>>& 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<std::vector<float>>& 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<std::vector<float>> 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<QPair<QString, QString>>& 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<QPair<QString, QString>>& 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<QPair<QString, QString>> 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
@@ -0,0 +1,388 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2017-2018, 2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2021 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#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;
}
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2021 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELRX_DEMODMESHCORE_MESHCOREDEMODSETTINGS_H_
#define PLUGINS_CHANNELRX_DEMODMESHCORE_MESHCOREDEMODSETTINGS_H_
#include <QByteArray>
#include <QString>
#include <stdint.h>
#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_ */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,202 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREDEMODSINK_H
#define INCLUDE_MESHCOREDEMODSINK_H
#include <vector>
#include <queue>
#include <deque>
#include <cstdint>
#include <QtGlobal>
#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_settings.m_spreadFactor); }
double getTotalPower() const { return m_magsqTotalAvg.instantAverage() / (1<<m_settings.m_spreadFactor); }
private:
enum LoRaFrameSyncState
{
LoRaStateDetect,
LoRaStateSync,
LoRaStateSFOCompensation
};
enum LoRaSyncState
{
LoRaSyncNetId1,
LoRaSyncNetId2,
LoRaSyncDownchirp1,
LoRaSyncDownchirp2,
LoRaSyncQuarterDown
};
MeshcoreDemodSettings m_settings;
bool m_demodActive;
MeshcoreDemodMsg::MsgDecodeSymbols *m_decodeMsg;
MessageQueue *m_decoderMsgQueue;
int m_bandwidth;
int m_channelSampleRate;
int m_channelFrequencyOffset;
qint64 m_deviceCenterFrequency;
static constexpr unsigned int m_minRequiredPreambleChirps = 4; //!< Lower bound for preamble validation chirps
static constexpr unsigned int m_maxRequiredPreambleChirps = 64; //!< Upper bound for preamble validation chirps
static constexpr unsigned int m_loRaFFTInterpolation = 1; //!< Canonical gr-lora_sdr-like FFT binning for LoRa
FFTEngine *m_fft;
int m_fftSequence;
Complex *m_downChirps;
Complex *m_upChirps;
Complex *m_spectrumLine;
unsigned int m_requiredPreambleChirps;
double m_magsqMax;
MovingAverageUtil<double, double, 10> m_magsqOnAvg;
MovingAverageUtil<double, double, 10> m_magsqOffAvg;
MovingAverageUtil<double, double, 10> m_magsqTotalAvg;
std::queue<double> 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<Complex> m_loRaSampleFifo;
std::vector<Complex> m_loRaInDown;
std::vector<Complex> m_loRaPreambleRaw;
std::vector<Complex> m_loRaPreambleRawUp;
std::vector<Complex> m_loRaPreambleUpchirps;
std::vector<Complex> m_loRaRefUpchirp;
std::vector<Complex> m_loRaRefDownchirp;
std::vector<Complex> m_loRaCFOFracCorrec;
std::vector<Complex> m_loRaPayloadDownchirp;
std::vector<Complex> m_loRaSymbCorr;
std::vector<Complex> m_loRaNetIdSamp;
std::vector<Complex> m_loRaAdditionalSymbolSamp;
std::vector<int> m_loRaPreambleVals;
std::vector<int> 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<float> *symbolMagnitudes = nullptr,
bool publishSpectrum = false
);
float estimateLoRaCFOFracBernier(const Complex *samples);
float estimateLoRaSTOFrac();
void buildLoRaPayloadDownchirp();
void finalizeLoRaFrame();
};
#endif // INCLUDE_MESHCOREDEMODSINK_H
@@ -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 <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "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;
}
@@ -0,0 +1,49 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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
@@ -0,0 +1,87 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QMessageBox>
#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;
}
@@ -0,0 +1,47 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREKEYSDIALOG_H
#define INCLUDE_MESHCOREKEYSDIALOG_H
#include <QDialog>
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
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MeshcoreKeysDialog</class>
<widget class="QDialog" name="MeshcoreKeysDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>760</width>
<height>460</height>
</rect>
</property>
<property name="windowTitle">
<string>Meshcore Keys</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="helpLabel">
<property name="text">
<string>MeshCore key spec — semicolon-separated entries:
channel:&lt;name&gt;=&lt;32hex&gt; 16-byte channel PSK in hex
channel:public=public built-in public channel PSK
identity=&lt;64hex&gt; 32-byte Ed25519 public key
contact:&lt;name&gt;=&lt;64hex&gt; known peer pubkey with name
Example: channel:public=public;identity=aabbccdd...</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="keyEditor">
<property name="toolTip">
<string>Enter one or more key specs used to decrypt Meshcore packets.</string>
</property>
<property name="placeholderText">
<string>channel:public=public;contact:Alice=aabbccdd...</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="statusLayout">
<item>
<widget class="QPushButton" name="validate">
<property name="toolTip">
<string>Validate key syntax and count without saving.</string>
</property>
<property name="text">
<string>Validate</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="toolTip">
<string>Validation status for the current key list.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>MeshcoreKeysDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>MeshcoreKeysDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -0,0 +1,88 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015-2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2019 Davide Gerhard <rainbow@irh.it> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QtPlugin>
#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
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2015 John Greb <hexameron@spam.no> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREPLUGIN_H
#define INCLUDE_MESHCOREPLUGIN_H
#include <QObject>
#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
+56
View File
@@ -0,0 +1,56 @@
<h1>MeshCore demodulator plugin</h1>
<h2>Introduction</h2>
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.
<h2>Status</h2>
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.
<h2>References</h2>
- 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)
@@ -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_PROPERTY:${TARGET_NAME},RUNTIME_OUTPUT_DIRECTORY>/${TARGET_NAME}stripped.pdb CONFIGURATIONS Release DESTINATION ${INSTALL_FOLDER} RENAME ${TARGET_NAME}.pdb )
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()
@@ -0,0 +1,889 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020-2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// Copyright (C) 2021 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QTime>
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUdpSocket>
#include <QNetworkDatagram>
#include <QBuffer>
#include <QThread>
#include "SWGChannelSettings.h"
#include "SWGWorkspaceInfo.h"
#include "SWGChannelReport.h"
#include "SWGChannelActions.h"
#include "SWGChirpChatModReport.h"
#include "SWGMeshcoreModActions.h"
#include <stdio.h>
#include <complex.h>
#include <algorithm>
#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<<m_settings.m_spreadFactor)*1000.0) / MeshcoreModSettings::bandwidths[m_settings.m_bandwidthIndex];
if (getMessageQueueToGUI())
{
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, m_symbols.size());
getMessageQueueToGUI()->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<QString> 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<<settings.m_spreadFactor)*1000.0) / MeshcoreModSettings::bandwidths[settings.m_bandwidthIndex];
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, m_symbols.size());
getMessageQueueToGUI()->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<<settings.m_spreadFactor)*1000.0) / MeshcoreModSettings::bandwidths[settings.m_bandwidthIndex];
if (getMessageQueueToGUI())
{
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, m_symbols.size());
getMessageQueueToGUI()->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<ObjectPipe*> 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<<m_settings.m_spreadFactor) * 250.0) / MeshcoreModSettings::bandwidths[m_settings.m_bandwidthIndex];
float controlMs = (4*m_settings.m_preambleChirps + 8 + 9) * fourthsMs; // preamble + sync word + SFD
response.getMeshtasticModReport()->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<QString>& 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<ObjectPipe*>& pipes,
QList<QString>& channelSettingsKeys,
const MeshcoreModSettings& settings,
bool force)
{
for (const auto& pipe : pipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(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<QString>& 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<unsigned short> symbols;
m_encoder.encodeBytes(datagram.data(), symbols);
payloadMsg = MeshcoreModBaseband::MsgConfigureMeshcoreModPayload::create(symbols);
if (payloadMsg)
{
m_basebandSource->getInputMessageQueue()->push(payloadMsg);
m_currentPayloadTime = (symbols.size()*(1<<m_settings.m_spreadFactor)*1000.0) / MeshcoreModSettings::bandwidths[m_settings.m_bandwidthIndex];
if (getMessageQueueToGUI())
{
MsgReportPayloadTime *rpt = MsgReportPayloadTime::create(m_currentPayloadTime, symbols.size());
getMessageQueueToGUI()->push(rpt);
}
}
}
}
+228
View File
@@ -0,0 +1,228 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2017-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020-2021 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMOD_H_
#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMOD_H_
#include <vector>
#include <iostream>
#include <fstream>
#include <QRecursiveMutex>
#include <QNetworkRequest>
#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<unsigned short> 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<QString>& channelSettingsKeys, const MeshcoreModSettings& settings, bool force);
void sendChannelSettings(
const QList<ObjectPipe*>& pipes,
QList<QString>& channelSettingsKeys,
const MeshcoreModSettings& settings,
bool force
);
void webapiFormatChannelSettings(
QList<QString>& 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_ */
@@ -0,0 +1,204 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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();
}
@@ -0,0 +1,120 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREMODBASEBAND_H
#define INCLUDE_MESHCOREMODBASEBAND_H
#include <QObject>
#include <QRecursiveMutex>
#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<unsigned short>& getPayload() const { return m_payload; }
static MsgConfigureMeshcoreModPayload* create() {
return new MsgConfigureMeshcoreModPayload();
}
static MsgConfigureMeshcoreModPayload* create(const std::vector<unsigned short>& payload) {
return new MsgConfigureMeshcoreModPayload(payload);
}
private:
std::vector<unsigned short> m_payload;
MsgConfigureMeshcoreModPayload() : // This is empty payload notification
Message()
{}
MsgConfigureMeshcoreModPayload(const std::vector<unsigned short>& 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
@@ -0,0 +1,255 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDateTime>
#include <QDebug>
#include <QStringList>
#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<quint64>(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<unsigned short>& 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<unsigned short>& 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<unsigned short>& 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
);
}
@@ -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 <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODER_H_
#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODER_H_
#include <vector>
#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<unsigned short>& symbols);
void encode(MeshcoreModSettings settings, std::vector<unsigned short>& symbols);
private:
// LoRa functions
void encodeBytesLoRa(const QByteArray& bytes, std::vector<unsigned short>& 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_
@@ -0,0 +1,225 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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<char>(0));
bytes.append(static_cast<char>(0));
return;
}
uint16_t crc = 0x0000;
for (int i = 0; i < bytes.size() - 2; i++) {
crc ^= static_cast<uint16_t>(static_cast<uint8_t>(bytes[i])) << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = static_cast<uint16_t>((crc << 1) ^ 0x1021);
} else {
crc = static_cast<uint16_t>(crc << 1);
}
}
}
crc ^= static_cast<uint16_t>(static_cast<uint8_t>(bytes[bytes.size() - 1]));
crc ^= static_cast<uint16_t>(static_cast<uint8_t>(bytes[bytes.size() - 2])) << 8;
bytes.append(static_cast<char>(crc & 0xff));
bytes.append(static_cast<char>((crc >> 8) & 0xff));
}
void MeshcoreModEncoderLoRa::encodeBytes(
const QByteArray& bytes,
std::vector<unsigned short>& 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<uint8_t> codewords(numCodewords);
if (hasHeader)
{
std::vector<uint8_t> 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<uint8_t>((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<uint8_t> nibbles(totalPayloadNibbles, 0);
const uint8_t *rawBytes = reinterpret_cast<const uint8_t*>(bytes.data());
for (unsigned int i = 0; i < totalPayloadNibbles; i++)
{
unsigned int byteIdx = i / 2;
if (byteIdx < static_cast<unsigned int>(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<bool>((symbols[i] >> b) & 1);
}
symbols[i] |= (static_cast<unsigned short>(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);
}
}
@@ -0,0 +1,301 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODERLORA_H_
#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODENCODERLORA_H_
#include <vector>
#include <QByteArray>
class MeshcoreModEncoderLoRa
{
public:
static void addChecksum(QByteArray& bytes);
static void encodeBytes(
const QByteArray& bytes,
std::vector<unsigned short>& 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_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,161 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Alejandro Aleman //
// Copyright (C) 2016-2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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<double, double, 20> 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_ */
File diff suppressed because it is too large Load Diff
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2019 Davide Gerhard <rainbow@irh.it> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QtPlugin>
#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();
}
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2015 John Greb <hexameron@spam.no> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREMODPLUGIN_H
#define INCLUDE_MESHCOREMODPLUGIN_H
#include <QObject>
#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
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2021 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#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<MessageType>(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;
}
@@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2021 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODSETTINGS_H_
#define PLUGINS_CHANNELTX_MODMESHCORE_MESHCOREMODSETTINGS_H_
#include <QByteArray>
#include <QString>
#include <stdint.h>
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-<short-pubkey>"
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_ */
@@ -0,0 +1,429 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QStringList>
#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<<sf;
unsigned int symbolRange = fftLength/(1<<deBits);
m_symbols.clear();
for (unsigned int seq = 0; seq < 1; seq++)
{
for (unsigned int symbol = 0; symbol < symbolRange; symbol += symbolRange/4)
{
m_symbols.push_back(symbol);
m_symbols.push_back(symbol+1);
}
}
}
void MeshcoreModSource::reset()
{
m_chirp = 0;
m_chirp0 = 0;
m_sampleCounter = 0;
m_fftCounter = 0;
m_chirpCount = 0;
}
void MeshcoreModSource::pull(SampleVector::iterator begin, unsigned int nbSamples)
{
std::for_each(
begin,
begin + nbSamples,
[this](Sample& s) {
pullOne(s);
}
);
}
void MeshcoreModSource::pullOne(Sample& sample)
{
if (m_settings.m_channelMute)
{
sample.m_real = 0.0f;
sample.m_imag = 0.0f;
m_magsq = 0.0;
return;
}
Complex ci;
if (m_interpolatorDistance > 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<unsigned int>(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<unsigned short>(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<unsigned short>& 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
}
@@ -0,0 +1,114 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Alejandro Aleman //
// Copyright (C) 2019-2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MESHCOREMODSOURCE_H
#define INCLUDE_MESHCOREMODSOURCE_H
#include <QMutex>
#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<unsigned short>& 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<unsigned short> 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<Real> m_bandpass;
double m_magsq;
MovingAverageUtil<double, double, 16> 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
@@ -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 <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "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;
}
@@ -0,0 +1,49 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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
+77
View File
@@ -0,0 +1,77 @@
<h1>MeshCore modulator plugin</h1>
<h2>Introduction</h2>
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.
<h2>Status</h2>
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.
<h2>MESHCORE: command syntax</h2>
Send a MeshCore wire packet by setting the channel's "Text message" to a
line starting with `MESHCORE:`:
```
MESHCORE: type=advert; seed=<hex64>; name=NodeA
MESHCORE: type=txt_msg; seed=<hex64>; dest=<hex64>; text=Hello
MESHCORE: type=anon_req; seed=<hex64>; dest=<hex64>; data=<hex>
MESHCORE: type=grp_txt; channel=public; text=Hello group
MESHCORE: type=ack; dest=<hex64>; msg_hash=<hex8>
```
Optional radio overrides on any line: `sf=8`, `bw=62500`, `cr=8` (for 4/8),
`sync=0x12`, `freq=869.618M`, `preamble=8`.
<h2>References</h2>
- 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)
@@ -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 <QJsonDocument>
#include <QJsonArray>
#include <QObject>
#include <QDebug>
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;
}
}
@@ -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 <QJsonObject>
#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_ */