mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-05-01 11:53:58 -04:00
Meshtastic: fixed Tx and strengthen Rx header processing
This commit is contained in:
parent
a0d779fb65
commit
a3f5ed6963
@ -1897,7 +1897,7 @@ static bool deriveTxRadioSettingsFromConfig(const CommandConfig& cfg, TxRadioSet
|
||||
{
|
||||
settings = TxRadioSettings();
|
||||
settings.hasCommand = true;
|
||||
settings.syncWord = 0x00;
|
||||
settings.syncWord = 0x2B;
|
||||
|
||||
QString presetName = cfg.presetName;
|
||||
if (presetName.isEmpty()) {
|
||||
|
||||
@ -55,7 +55,7 @@ struct MODEMMESHTASTIC_API TxRadioSettings
|
||||
int spreadFactor = 0;
|
||||
int parityBits = 0; // 1..4 maps to CR 4/5 .. 4/8
|
||||
int deBits = 0;
|
||||
uint8_t syncWord = 0x00; // Meshtastic_SDR/gr-lora_sdr reference flow uses [0,0]
|
||||
uint8_t syncWord = 0x2B; // Meshtastic LoRa private network sync word
|
||||
int preambleChirps = 17;
|
||||
|
||||
bool hasCenterFrequency = false;
|
||||
|
||||
@ -66,6 +66,17 @@ namespace MeshtasticDemodMsg
|
||||
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();
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
#include <QTime>
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
#include <stdio.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@ -33,6 +34,22 @@
|
||||
#include "meshtasticdemoddecoderlora.h"
|
||||
#include "meshtasticdemodsink.h"
|
||||
|
||||
// namespace { // For [LOOPBACK] debug only
|
||||
|
||||
// QString symbolPreview(const std::vector<unsigned short>& symbols, unsigned int maxCount)
|
||||
// {
|
||||
// QStringList parts;
|
||||
// const unsigned int count = std::min<unsigned int>(maxCount, static_cast<unsigned int>(symbols.size()));
|
||||
|
||||
// for (unsigned int i = 0; i < count; i++) {
|
||||
// parts.append(QString::number(symbols[i]));
|
||||
// }
|
||||
|
||||
// return parts.join(",");
|
||||
// }
|
||||
|
||||
// } // namespace
|
||||
|
||||
MeshtasticDemodSink::MeshtasticDemodSink() :
|
||||
m_decodeMsg(nullptr),
|
||||
m_decoderMsgQueue(nullptr),
|
||||
@ -316,6 +333,10 @@ unsigned int MeshtasticDemodSink::evalSymbol(unsigned int rawSymbol, bool header
|
||||
|
||||
void MeshtasticDemodSink::tryHeaderLock()
|
||||
{
|
||||
if (!m_decodeMsg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<unsigned short>& symbols = m_decodeMsg->getSymbols();
|
||||
|
||||
if (symbols.size() < 8) {
|
||||
@ -334,57 +355,84 @@ void MeshtasticDemodSink::tryHeaderLock()
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasCRC = true;
|
||||
unsigned int nbParityBits = 1U;
|
||||
unsigned int packetLength = 0U;
|
||||
int headerParityStatus = (int) MeshtasticDemodSettings::ParityUndefined;
|
||||
bool headerCRCStatus = false;
|
||||
const unsigned int maxOffset = std::min<unsigned int>(2U, static_cast<unsigned int>(symbols.size()) - 8U);
|
||||
const unsigned int headerSymbolMod = 1U << headerNbSymbolBits;
|
||||
|
||||
MeshtasticDemodDecoderLoRa::decodeHeader(
|
||||
symbols,
|
||||
headerNbSymbolBits,
|
||||
hasCRC,
|
||||
nbParityBits,
|
||||
packetLength,
|
||||
headerParityStatus,
|
||||
headerCRCStatus
|
||||
);
|
||||
|
||||
if (!headerCRCStatus || packetLength == 0U || nbParityBits < 1U || nbParityBits > 4U)
|
||||
for (unsigned int offset = 0U; offset <= maxOffset; offset++)
|
||||
{
|
||||
qDebug("MeshtasticDemodSink::tryHeaderLock: header invalid (CRC=%d len=%u CR=%u parity=%d)",
|
||||
headerCRCStatus ? 1 : 0,
|
||||
packetLength,
|
||||
nbParityBits,
|
||||
headerParityStatus);
|
||||
return;
|
||||
const std::vector<unsigned short> baseHeaderSlice(symbols.begin() + offset, symbols.begin() + offset + 8U);
|
||||
|
||||
for (int delta = -2; delta <= 2; delta++)
|
||||
{
|
||||
std::vector<unsigned short> headerSlice(baseHeaderSlice);
|
||||
|
||||
if (delta != 0)
|
||||
{
|
||||
for (auto& sym : headerSlice) {
|
||||
const int shifted = loRaMod(static_cast<int>(sym) + delta, static_cast<int>(headerSymbolMod));
|
||||
sym = static_cast<unsigned short>(shifted);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasCRC = true;
|
||||
unsigned int nbParityBits = 1U;
|
||||
unsigned int packetLength = 0U;
|
||||
int headerParityStatus = (int) MeshtasticDemodSettings::ParityUndefined;
|
||||
bool headerCRCStatus = false;
|
||||
|
||||
MeshtasticDemodDecoderLoRa::decodeHeader(
|
||||
headerSlice,
|
||||
headerNbSymbolBits,
|
||||
hasCRC,
|
||||
nbParityBits,
|
||||
packetLength,
|
||||
headerParityStatus,
|
||||
headerCRCStatus
|
||||
);
|
||||
|
||||
if (!headerCRCStatus || packetLength == 0U || nbParityBits < 1U || nbParityBits > 4U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const double symbolDurationMs = (double)(1U << sf) * 1000.0 / (double)m_bandwidth;
|
||||
const bool ldro = symbolDurationMs > 16.0;
|
||||
const unsigned int sfDenom = sf - (ldro ? 2U : 0U);
|
||||
|
||||
// gr-lora_sdr formula: symb_numb = 8 + ceil(max(0, 2*pay_len - sf + 2 + 5 + has_crc*4) / (sf - 2*ldro)) * (4 + cr)
|
||||
const int numerator = 2 * (int)packetLength - (int)sf + 2 + 5 + (hasCRC ? 4 : 0);
|
||||
unsigned int payloadBlocks = 0;
|
||||
|
||||
if (numerator > 0 && sfDenom > 0) {
|
||||
payloadBlocks = ((unsigned int)numerator + sfDenom - 1U) / sfDenom;
|
||||
}
|
||||
|
||||
const unsigned int expectedSymbols = 8U + payloadBlocks * (4U + nbParityBits);
|
||||
|
||||
if (expectedSymbols > m_settings.m_nbSymbolsMax) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offset > 0U)
|
||||
{
|
||||
m_decodeMsg->dropFront(offset);
|
||||
m_loRaFrameSymbolCount = m_loRaFrameSymbolCount > offset
|
||||
? (m_loRaFrameSymbolCount - offset)
|
||||
: 0U;
|
||||
}
|
||||
|
||||
m_expectedSymbols = expectedSymbols;
|
||||
m_headerLocked = true;
|
||||
|
||||
// qDebug("[LOOPBACK][RX] header_realign frameId=%u offset=%u delta=%d", m_loRaFrameId, offset, delta);
|
||||
qDebug("MeshtasticDemodSink::tryHeaderLock: LOCKED len=%u CR=%u CRC=%s LDRO=%s expected=%u symbols offset=%u delta=%d",
|
||||
packetLength, nbParityBits, hasCRC ? "on" : "off", ldro ? "on" : "off", m_expectedSymbols, offset, delta);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const double symbolDurationMs = (double)(1U << sf) * 1000.0 / (double)m_bandwidth;
|
||||
const bool ldro = symbolDurationMs > 16.0;
|
||||
const unsigned int sfDenom = sf - (ldro ? 2U : 0U);
|
||||
|
||||
// gr-lora_sdr formula: symb_numb = 8 + ceil(max(0, 2*pay_len - sf + 2 + 5 + has_crc*4) / (sf - 2*ldro)) * (4 + cr)
|
||||
const int numerator = 2 * (int)packetLength - (int)sf + 2 + 5 + (hasCRC ? 4 : 0);
|
||||
unsigned int payloadBlocks = 0;
|
||||
|
||||
if (numerator > 0 && sfDenom > 0) {
|
||||
payloadBlocks = ((unsigned int)numerator + sfDenom - 1U) / sfDenom;
|
||||
if (symbols.size() >= 10U) {
|
||||
qDebug("MeshtasticDemodSink::tryHeaderLock: header invalid after offsets 0..%u deltas -2..2", maxOffset);
|
||||
}
|
||||
|
||||
m_expectedSymbols = 8U + payloadBlocks * (4U + nbParityBits);
|
||||
|
||||
if (m_expectedSymbols > m_settings.m_nbSymbolsMax)
|
||||
{
|
||||
qDebug("MeshtasticDemodSink::tryHeaderLock: expected %u > max %u, falling back to EOM",
|
||||
m_expectedSymbols, m_settings.m_nbSymbolsMax);
|
||||
return;
|
||||
}
|
||||
|
||||
m_headerLocked = true;
|
||||
|
||||
qDebug("MeshtasticDemodSink::tryHeaderLock: LOCKED len=%u CR=%u CRC=%s LDRO=%s expected=%u symbols",
|
||||
packetLength, nbParityBits, hasCRC ? "on" : "off", ldro ? "on" : "off", m_expectedSymbols);
|
||||
}
|
||||
|
||||
bool MeshtasticDemodSink::sendLoRaHeaderProbe()
|
||||
@ -418,6 +466,17 @@ bool MeshtasticDemodSink::sendLoRaHeaderProbe()
|
||||
m_settings.m_hasCRC
|
||||
);
|
||||
m_decoderMsgQueue->push(probe);
|
||||
|
||||
// qDebug().noquote() << QString(
|
||||
// "[LOOPBACK][RX] header_probe token=%1 frameId=%2 sf=%3 de=%4 bw=%5 headerSymbols=[%6]"
|
||||
// )
|
||||
// .arg(m_loRaFrameId)
|
||||
// .arg(m_loRaFrameId)
|
||||
// .arg(m_settings.m_spreadFactor)
|
||||
// .arg(m_settings.m_deBits)
|
||||
// .arg(m_bandwidth)
|
||||
// .arg(symbolPreview(headerSymbols, 8U));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -466,6 +525,14 @@ void MeshtasticDemodSink::applyLoRaHeaderFeedback(
|
||||
m_expectedSymbols = expectedSymbols;
|
||||
m_headerLocked = true;
|
||||
m_loRaReceivedHeader = true;
|
||||
|
||||
// qDebug("[LOOPBACK][RX] header_lock token=%u frameId=%u len=%u cr=%u expected=%u frameSymbols=%u",
|
||||
// frameId,
|
||||
// frameId,
|
||||
// packetLength,
|
||||
// nbParityBits,
|
||||
// expectedSymbols,
|
||||
// m_loRaFrameSymbolCount);
|
||||
}
|
||||
|
||||
int MeshtasticDemodSink::loRaMod(int a, int b) const
|
||||
@ -732,6 +799,12 @@ void MeshtasticDemodSink::finalizeLoRaFrame()
|
||||
return;
|
||||
}
|
||||
|
||||
// const bool hitExpected = m_headerLocked && (m_loRaFrameSymbolCount >= m_expectedSymbols);
|
||||
// const bool hitMax = (!m_headerLocked) && (m_loRaFrameSymbolCount >= m_settings.m_nbSymbolsMax);
|
||||
// const char *endReason = hitExpected
|
||||
// ? "expected"
|
||||
// : (hitMax ? "max" : "other");
|
||||
|
||||
qDebug(
|
||||
"MeshtasticDemodSink::finalizeLoRaFrame: frameId=%u symbols=%u headerLocked=%d expected=%u",
|
||||
m_loRaFrameId,
|
||||
@ -740,6 +813,15 @@ void MeshtasticDemodSink::finalizeLoRaFrame()
|
||||
m_expectedSymbols
|
||||
);
|
||||
|
||||
// qDebug("[LOOPBACK][RX] frame_finalize token=%u frameId=%u reason=%s locked=%d expected=%u actual=%u waitHeader=%d",
|
||||
// m_loRaFrameId,
|
||||
// m_loRaFrameId,
|
||||
// endReason,
|
||||
// m_headerLocked ? 1 : 0,
|
||||
// m_expectedSymbols,
|
||||
// m_loRaFrameSymbolCount,
|
||||
// m_waitHeaderFeedback ? 1 : 0);
|
||||
|
||||
m_decodeMsg->setSignalDb(CalcDb::dbPower(m_magsqOnAvg.asDouble() / (1 << m_settings.m_spreadFactor)));
|
||||
m_decodeMsg->setNoiseDb(CalcDb::dbPower(m_magsqOffAvg.asDouble() / (1 << m_settings.m_spreadFactor)));
|
||||
|
||||
@ -1105,21 +1187,6 @@ int MeshtasticDemodSink::processLoRaFrameSyncStep()
|
||||
return static_cast<int>(m_loRaSymbolSpan);
|
||||
}
|
||||
|
||||
if (m_settings.m_hasHeader
|
||||
&& !m_headerLocked
|
||||
&& (m_loRaFrameSymbolCount >= 8U)
|
||||
&& m_waitHeaderFeedback)
|
||||
{
|
||||
const unsigned int maxWaitSteps = std::max(1U, m_headerFeedbackMaxWaitSteps);
|
||||
|
||||
if (++m_headerFeedbackWaitSteps > maxWaitSteps) {
|
||||
// Safety fallback when async feedback is delayed.
|
||||
m_waitHeaderFeedback = false;
|
||||
qDebug("MeshtasticDemodSink::processLoRaFrameSyncStep: header feedback timeout -> local fallback");
|
||||
tryHeaderLock();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<float> symbolMags;
|
||||
const unsigned int rawSymbol = getLoRaSymbolVal(m_loRaInDown.data(), m_loRaPayloadDownchirp.data(), &symbolMags, true);
|
||||
const bool headerSymbol = m_settings.m_hasHeader && (m_loRaFrameSymbolCount < 8U);
|
||||
@ -1155,14 +1222,9 @@ int MeshtasticDemodSink::processLoRaFrameSyncStep()
|
||||
|
||||
if (!m_headerLocked
|
||||
&& m_settings.m_hasHeader
|
||||
&& (m_loRaFrameSymbolCount == 8U))
|
||||
&& (m_loRaFrameSymbolCount >= 8U))
|
||||
{
|
||||
if (sendLoRaHeaderProbe()) {
|
||||
m_waitHeaderFeedback = true;
|
||||
m_headerFeedbackWaitSteps = 0U;
|
||||
} else {
|
||||
tryHeaderLock();
|
||||
}
|
||||
tryHeaderLock();
|
||||
}
|
||||
|
||||
if (m_headerLocked)
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
#include "maincore.h"
|
||||
#include "channel/channelwebapiutils.h"
|
||||
|
||||
#include "meshtasticpacket.h"
|
||||
#include "meshtasticmodbaseband.h"
|
||||
#include "meshtasticmod.h"
|
||||
|
||||
@ -204,8 +205,102 @@ void MeshtasticMod::setCenterFrequency(qint64 frequency)
|
||||
}
|
||||
}
|
||||
|
||||
void MeshtasticMod::applySettings(const MeshtasticModSettings& settings, bool force)
|
||||
int MeshtasticMod::findBandwidthIndex(int bandwidthHz) const
|
||||
{
|
||||
int bestIndex = -1;
|
||||
int bestDelta = 1 << 30;
|
||||
|
||||
for (int i = 0; i < MeshtasticModSettings::nbBandwidths; i++)
|
||||
{
|
||||
const int delta = std::abs(MeshtasticModSettings::bandwidths[i] - bandwidthHz);
|
||||
|
||||
if (delta < bestDelta)
|
||||
{
|
||||
bestDelta = delta;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
bool MeshtasticMod::applyMeshtasticRadioSettingsIfPresent(MeshtasticModSettings& settings) const
|
||||
{
|
||||
if (settings.m_codingScheme != MeshtasticModSettings::CodingLoRa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!modemmeshtastic::Packet::isCommand(settings.m_textMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
modemmeshtastic::TxRadioSettings meshRadio;
|
||||
QString error;
|
||||
if (!modemmeshtastic::Packet::deriveTxRadioSettings(settings.m_textMessage, meshRadio, error))
|
||||
{
|
||||
qWarning() << "MeshtasticMod::applyMeshtasticRadioSettingsIfPresent:" << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
const int bwIndex = findBandwidthIndex(meshRadio.bandwidthHz);
|
||||
|
||||
if ((bwIndex >= 0) && (bwIndex != settings.m_bandwidthIndex)) {
|
||||
settings.m_bandwidthIndex = bwIndex;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ((meshRadio.spreadFactor > 0) && (meshRadio.spreadFactor != settings.m_spreadFactor)) {
|
||||
settings.m_spreadFactor = meshRadio.spreadFactor;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ((meshRadio.parityBits > 0) && (meshRadio.parityBits != settings.m_nbParityBits)) {
|
||||
settings.m_nbParityBits = meshRadio.parityBits;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (meshRadio.deBits != settings.m_deBits) {
|
||||
settings.m_deBits = meshRadio.deBits;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (meshRadio.syncWord != settings.m_syncWord) {
|
||||
settings.m_syncWord = meshRadio.syncWord;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (meshRadio.hasCenterFrequency && (m_deviceAPI != nullptr))
|
||||
{
|
||||
const QList<quint64> centerFrequencies = m_deviceAPI->getCenterFrequency();
|
||||
const int streamIndex = std::max(0, settings.m_streamIndex);
|
||||
const int selectedIndex = (streamIndex < centerFrequencies.size()) ? streamIndex : 0;
|
||||
|
||||
if (!centerFrequencies.isEmpty())
|
||||
{
|
||||
const qint64 deviceCenterFrequency = static_cast<qint64>(centerFrequencies.at(selectedIndex));
|
||||
const qint64 wantedOffset = meshRadio.centerFrequencyHz - deviceCenterFrequency;
|
||||
|
||||
if (wantedOffset != settings.m_inputFrequencyOffset)
|
||||
{
|
||||
settings.m_inputFrequencyOffset = static_cast<int>(wantedOffset);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
qInfo() << "MeshtasticMod::applyMeshtasticRadioSettingsIfPresent:" << meshRadio.summary;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void MeshtasticMod::applySettings(const MeshtasticModSettings& incomingSettings, bool force)
|
||||
{
|
||||
MeshtasticModSettings settings(incomingSettings);
|
||||
applyMeshtasticRadioSettingsIfPresent(settings);
|
||||
|
||||
qDebug() << "MeshtasticMod::applySettings:"
|
||||
<< " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset
|
||||
<< " m_rfBandwidth: " << settings.m_bandwidthIndex
|
||||
@ -226,6 +321,7 @@ void MeshtasticMod::applySettings(const MeshtasticModSettings& settings, bool fo
|
||||
<< " 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
|
||||
@ -289,6 +385,9 @@ void MeshtasticMod::applySettings(const MeshtasticModSettings& settings, bool fo
|
||||
if ((settings.m_invertRamps != m_settings.m_invertRamps) || force) {
|
||||
reverseAPIKeys.append("invertRamps");
|
||||
}
|
||||
if ((settings.m_syncWord != m_settings.m_syncWord) || force) {
|
||||
reverseAPIKeys.append("syncWord");
|
||||
}
|
||||
|
||||
MeshtasticModBaseband::MsgConfigureMeshtasticModPayload *payloadMsg = nullptr;
|
||||
|
||||
|
||||
@ -213,6 +213,8 @@ private:
|
||||
void openUDP(const MeshtasticModSettings& settings);
|
||||
void closeUDP();
|
||||
void sendCurrentSettingsMessage();
|
||||
int findBandwidthIndex(int bandwidthHz) const;
|
||||
bool applyMeshtasticRadioSettingsIfPresent(MeshtasticModSettings& settings) const;
|
||||
|
||||
private slots:
|
||||
void networkManagerFinished(QNetworkReply *reply);
|
||||
|
||||
@ -117,5 +117,17 @@ void MeshtasticModEncoder::encodeBytesLoRa(const QByteArray& bytes, std::vector<
|
||||
MeshtasticModEncoderLoRa::addChecksum(payload);
|
||||
}
|
||||
|
||||
MeshtasticModEncoderLoRa::encodeBytes(payload, symbols, m_nbSymbolBits, m_hasHeader, m_hasCRC, m_nbParityBits);
|
||||
const unsigned int headerNbSymbolBits = (m_hasHeader && (m_spreadFactor > 2U))
|
||||
? (m_spreadFactor - 2U)
|
||||
: m_nbSymbolBits;
|
||||
|
||||
MeshtasticModEncoderLoRa::encodeBytes(
|
||||
payload,
|
||||
symbols,
|
||||
m_nbSymbolBits,
|
||||
headerNbSymbolBits,
|
||||
m_hasHeader,
|
||||
m_hasCRC,
|
||||
m_nbParityBits
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,17 +29,35 @@ void MeshtasticModEncoderLoRa::addChecksum(QByteArray& bytes)
|
||||
void MeshtasticModEncoderLoRa::encodeBytes(
|
||||
const QByteArray& bytes,
|
||||
std::vector<unsigned short>& symbols,
|
||||
unsigned int nbSymbolBits,
|
||||
unsigned int payloadNbSymbolBits,
|
||||
unsigned int headerNbSymbolBits,
|
||||
bool hasHeader,
|
||||
bool hasCRC,
|
||||
unsigned int nbParityBits
|
||||
)
|
||||
{
|
||||
if (nbSymbolBits < 5) {
|
||||
if (payloadNbSymbolBits < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned int numCodewords = roundUp(bytes.size()*2 + (hasHeader ? headerCodewords : 0), nbSymbolBits); // uses payload + CRC for encoding size
|
||||
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;
|
||||
|
||||
@ -61,30 +79,67 @@ void MeshtasticModEncoderLoRa::encodeBytes(
|
||||
codewords[cOfs++] = encodeHamming84sx(hdr[2] & 0xf);
|
||||
}
|
||||
|
||||
unsigned int headerSize = cOfs;
|
||||
|
||||
// fill nbSymbolBits codewords with 8 bit codewords using payload data (ecode and whiten)
|
||||
encodeFec(codewords, 4, cOfs, dOfs, reinterpret_cast<const uint8_t*>(bytes.data()), nbSymbolBits - headerSize);
|
||||
Sx1272ComputeWhitening(codewords.data() + headerSize, nbSymbolBits - headerSize, 0, headerParityBits);
|
||||
|
||||
// encode and whiten the rest of the payload with 4 + nbParityBits bits codewords
|
||||
if (numCodewords > nbSymbolBits)
|
||||
// Fill first interleaver block (explicit header + first payload codewords) with 4/8 FEC.
|
||||
if (firstBlockCodewords > headerSize)
|
||||
{
|
||||
unsigned int cOfs2 = cOfs;
|
||||
encodeFec(codewords, nbParityBits, cOfs, dOfs, reinterpret_cast<const uint8_t*>(bytes.data()), numCodewords - nbSymbolBits);
|
||||
Sx1272ComputeWhitening(codewords.data() + cOfs2, numCodewords - nbSymbolBits, nbSymbolBits - headerSize, nbParityBits);
|
||||
encodeFec(
|
||||
codewords,
|
||||
4,
|
||||
cOfs,
|
||||
dOfs,
|
||||
reinterpret_cast<const uint8_t*>(bytes.data()),
|
||||
bytes.size(),
|
||||
firstBlockCodewords - headerSize
|
||||
);
|
||||
Sx1272ComputeWhitening(codewords.data() + headerSize, firstBlockCodewords - headerSize, 0, headerParityBits);
|
||||
}
|
||||
|
||||
// header is always coded with 8 bits and yields exactly 8 symbols (headerSymbols)
|
||||
const unsigned int numSymbols = headerSymbols + (numCodewords / nbSymbolBits - 1) * (4 + nbParityBits);
|
||||
// Encode and whiten remaining payload blocks with payload coding rate.
|
||||
if (remainingCodewords > 0U)
|
||||
{
|
||||
unsigned int cOfs2 = cOfs;
|
||||
encodeFec(
|
||||
codewords,
|
||||
nbParityBits,
|
||||
cOfs,
|
||||
dOfs,
|
||||
reinterpret_cast<const uint8_t*>(bytes.data()),
|
||||
bytes.size(),
|
||||
remainingCodewords
|
||||
);
|
||||
Sx1272ComputeWhitening(
|
||||
codewords.data() + cOfs2,
|
||||
remainingCodewords,
|
||||
static_cast<int>(firstBlockCodewords - headerSize),
|
||||
nbParityBits
|
||||
);
|
||||
}
|
||||
|
||||
const unsigned int numSymbols = hasHeader
|
||||
? (headerSymbols + (remainingCodewords / payloadNbSymbolBits) * (4U + nbParityBits))
|
||||
: ((numCodewords / payloadNbSymbolBits) * (4U + nbParityBits));
|
||||
|
||||
// interleave the codewords into symbols
|
||||
symbols.clear();
|
||||
symbols.resize(numSymbols);
|
||||
diagonalInterleaveSx(codewords.data(), nbSymbolBits, symbols.data(), nbSymbolBits, headerParityBits);
|
||||
|
||||
if (numCodewords > nbSymbolBits) {
|
||||
diagonalInterleaveSx(codewords.data() + nbSymbolBits, numCodewords - nbSymbolBits, symbols.data() + headerSymbols, nbSymbolBits, nbParityBits);
|
||||
if (hasHeader)
|
||||
{
|
||||
diagonalInterleaveSx(codewords.data(), firstBlockCodewords, symbols.data(), headerNbSymbolBits, headerParityBits);
|
||||
|
||||
if (remainingCodewords > 0U) {
|
||||
diagonalInterleaveSx(
|
||||
codewords.data() + firstBlockCodewords,
|
||||
remainingCodewords,
|
||||
symbols.data() + headerSymbols,
|
||||
payloadNbSymbolBits,
|
||||
nbParityBits
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
diagonalInterleaveSx(codewords.data(), numCodewords, symbols.data(), payloadNbSymbolBits, nbParityBits);
|
||||
}
|
||||
|
||||
// gray decode
|
||||
@ -99,49 +154,53 @@ void MeshtasticModEncoderLoRa::encodeFec(
|
||||
unsigned int& cOfs,
|
||||
unsigned int& dOfs,
|
||||
const uint8_t *bytes,
|
||||
const unsigned int bytesLength,
|
||||
const unsigned int codewordCount
|
||||
)
|
||||
{
|
||||
for (unsigned int i = 0; i < codewordCount; i++, dOfs++)
|
||||
{
|
||||
const unsigned int byteIdx = dOfs / 2;
|
||||
const uint8_t byteVal = byteIdx < bytesLength ? bytes[byteIdx] : 0U;
|
||||
|
||||
if (nbParityBits == 1)
|
||||
{
|
||||
if (dOfs % 2 == 1) {
|
||||
codewords[cOfs++] = encodeParity54(bytes[dOfs/2] >> 4);
|
||||
codewords[cOfs++] = encodeParity54(byteVal >> 4);
|
||||
} else {
|
||||
codewords[cOfs++] = encodeParity54(bytes[dOfs/2] & 0xf);
|
||||
codewords[cOfs++] = encodeParity54(byteVal & 0xf);
|
||||
}
|
||||
}
|
||||
else if (nbParityBits == 2)
|
||||
{
|
||||
if (dOfs % 2 == 1) {
|
||||
codewords[cOfs++] = encodeParity64(bytes[dOfs/2] >> 4);
|
||||
codewords[cOfs++] = encodeParity64(byteVal >> 4);
|
||||
} else {
|
||||
codewords[cOfs++] = encodeParity64(bytes[dOfs/2] & 0xf);
|
||||
codewords[cOfs++] = encodeParity64(byteVal & 0xf);
|
||||
}
|
||||
}
|
||||
else if (nbParityBits == 3)
|
||||
{
|
||||
if (dOfs % 2 == 1) {
|
||||
codewords[cOfs++] = encodeHamming74sx(bytes[dOfs/2] >> 4);
|
||||
codewords[cOfs++] = encodeHamming74sx(byteVal >> 4);
|
||||
} else {
|
||||
codewords[cOfs++] = encodeHamming74sx(bytes[dOfs/2] & 0xf);
|
||||
codewords[cOfs++] = encodeHamming74sx(byteVal & 0xf);
|
||||
}
|
||||
}
|
||||
else if (nbParityBits == 4)
|
||||
{
|
||||
if (dOfs % 2 == 1) {
|
||||
codewords[cOfs++] = encodeHamming84sx(bytes[dOfs/2] >> 4);
|
||||
codewords[cOfs++] = encodeHamming84sx(byteVal >> 4);
|
||||
} else {
|
||||
codewords[cOfs++] = encodeHamming84sx(bytes[dOfs/2] & 0xf);
|
||||
codewords[cOfs++] = encodeHamming84sx(byteVal & 0xf);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dOfs % 2 == 1) {
|
||||
codewords[cOfs++] = bytes[dOfs/2] >> 4;
|
||||
codewords[cOfs++] = byteVal >> 4;
|
||||
} else {
|
||||
codewords[cOfs++] = bytes[dOfs/2] & 0xf;
|
||||
codewords[cOfs++] = byteVal & 0xf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,8 @@ public:
|
||||
static void encodeBytes(
|
||||
const QByteArray& bytes,
|
||||
std::vector<unsigned short>& symbols,
|
||||
unsigned int nbSymbolBits,
|
||||
unsigned int payloadNbSymbolBits,
|
||||
unsigned int headerNbSymbolBits,
|
||||
bool hasHeader,
|
||||
bool hasCRC,
|
||||
unsigned int nbParityBits
|
||||
@ -43,6 +44,7 @@ private:
|
||||
unsigned int& cOfs,
|
||||
unsigned int& dOfs,
|
||||
const uint8_t *bytes,
|
||||
const unsigned int bytesLength,
|
||||
const unsigned int codewordCount
|
||||
);
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ void MeshtasticModSettings::resetToDefaults()
|
||||
m_bandwidthIndex = 5;
|
||||
m_spreadFactor = 7;
|
||||
m_deBits = 0;
|
||||
m_preambleChirps = 8;
|
||||
m_preambleChirps = 17;
|
||||
m_quietMillis = 1000;
|
||||
m_nbParityBits = 1;
|
||||
m_textMessage = "Hello LoRa";
|
||||
@ -192,7 +192,7 @@ bool MeshtasticModSettings::deserialize(const QByteArray& data)
|
||||
d.readBool(8, &m_channelMute, false);
|
||||
d.readU32(9, &utmp, 0x34);
|
||||
m_syncWord = utmp > 255 ? 0 : utmp;
|
||||
d.readU32(10, &m_preambleChirps, 8);
|
||||
d.readU32(10, &m_preambleChirps, 17);
|
||||
d.readS32(11, &m_quietMillis, 1000);
|
||||
d.readBool(11, &m_useReverseAPI, false);
|
||||
d.readBool(12, &m_invertRamps, false);
|
||||
|
||||
@ -16,16 +16,35 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
|
||||
#include "meshtasticmodsource.h"
|
||||
|
||||
// namespace { // For [LOOPBACK] debug only
|
||||
|
||||
// QString symbolPreview(const std::vector<unsigned short>& symbols, unsigned int maxCount)
|
||||
// {
|
||||
// QStringList parts;
|
||||
// const unsigned int count = std::min<unsigned int>(maxCount, static_cast<unsigned int>(symbols.size()));
|
||||
|
||||
// for (unsigned int i = 0; i < count; i++) {
|
||||
// parts.append(QString::number(symbols[i]));
|
||||
// }
|
||||
|
||||
// return parts.join(",");
|
||||
// }
|
||||
|
||||
// } // namespace
|
||||
|
||||
const int MeshtasticModSource::m_levelNbSamples = 480; // every 10ms
|
||||
|
||||
MeshtasticModSource::MeshtasticModSource() :
|
||||
m_channelSampleRate(48000),
|
||||
m_channelFrequencyOffset(0),
|
||||
m_bandwidth(MeshtasticModSettings::bandwidths[5]),
|
||||
m_phaseIncrements(nullptr),
|
||||
m_repeatCount(0),
|
||||
m_txFrameToken(0U),
|
||||
m_active(false),
|
||||
m_modPhasor(0.0f),
|
||||
m_levelCalcCount(0),
|
||||
@ -38,7 +57,12 @@ MeshtasticModSource::MeshtasticModSource() :
|
||||
initTest(m_settings.m_spreadFactor, m_settings.m_deBits);
|
||||
reset();
|
||||
applySettings(m_settings, true);
|
||||
applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true);
|
||||
applyChannelSettings(
|
||||
m_channelSampleRate,
|
||||
MeshtasticModSettings::bandwidths[m_settings.m_bandwidthIndex],
|
||||
m_channelFrequencyOffset,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
MeshtasticModSource::~MeshtasticModSource()
|
||||
@ -235,11 +259,9 @@ void MeshtasticModSource::modulateSample()
|
||||
if (m_fftCounter == m_fftLength*MeshtasticModSettings::oversampling)
|
||||
{
|
||||
m_chirpCount++;
|
||||
m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8;
|
||||
m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1;
|
||||
m_fftCounter = 0;
|
||||
|
||||
if (m_chirpCount == 2)
|
||||
if (m_chirpCount >= 2)
|
||||
{
|
||||
m_sampleCounter = 0;
|
||||
m_chirpCount = 0;
|
||||
@ -247,6 +269,11 @@ void MeshtasticModSource::modulateSample()
|
||||
m_chirp = m_fftLength*MeshtasticModSettings::oversampling - 1;
|
||||
m_state = ChirpChatStateSFD;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_chirp0 = ((m_settings.m_syncWord >> ((1-m_chirpCount)*4)) & 0xf)*8;
|
||||
m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (m_state == ChirpChatStateSFD)
|
||||
@ -273,7 +300,29 @@ void MeshtasticModSource::modulateSample()
|
||||
{
|
||||
m_fftCounter = 0;
|
||||
m_chirpCount = 0;
|
||||
m_chirp0 = encodeSymbol(m_symbols[m_chirpCount]);
|
||||
m_chirp0 = encodeSymbol(m_symbols[m_chirpCount], m_settings.m_hasHeader && (m_chirpCount < 8U));
|
||||
m_txFrameToken++;
|
||||
|
||||
std::vector<unsigned short> mappedPreview;
|
||||
const unsigned int previewCount = std::min<unsigned int>(8U, static_cast<unsigned int>(m_symbols.size()));
|
||||
mappedPreview.reserve(previewCount);
|
||||
|
||||
for (unsigned int i = 0; i < previewCount; i++) {
|
||||
mappedPreview.push_back(encodeSymbol(m_symbols[i], m_settings.m_hasHeader && (i < 8U)));
|
||||
}
|
||||
|
||||
// qDebug().noquote() << QString(
|
||||
// "[LOOPBACK][TX] frame_start token=%1 sf=%2 de=%3 bw=%4 preamble=%5 symbols=%6 hdrRaw=[%7] hdrMapped=[%8]"
|
||||
// )
|
||||
// .arg(m_txFrameToken)
|
||||
// .arg(m_settings.m_spreadFactor)
|
||||
// .arg(m_settings.m_deBits)
|
||||
// .arg(m_bandwidth)
|
||||
// .arg(m_settings.m_preambleChirps)
|
||||
// .arg(m_symbols.size())
|
||||
// .arg(symbolPreview(m_symbols, previewCount))
|
||||
// .arg(symbolPreview(mappedPreview, previewCount));
|
||||
|
||||
m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1;
|
||||
m_state = ChirpChatStatePayload;
|
||||
}
|
||||
@ -295,7 +344,7 @@ void MeshtasticModSource::modulateSample()
|
||||
}
|
||||
else
|
||||
{
|
||||
m_chirp0 = encodeSymbol(m_symbols[m_chirpCount]);
|
||||
m_chirp0 = encodeSymbol(m_symbols[m_chirpCount], m_settings.m_hasHeader && (m_chirpCount < 8U));
|
||||
m_chirp = (m_chirp0 + m_fftLength)*MeshtasticModSettings::oversampling - 1;
|
||||
m_fftCounter = 0;
|
||||
}
|
||||
@ -314,16 +363,19 @@ void MeshtasticModSource::modulateSample()
|
||||
}
|
||||
}
|
||||
|
||||
unsigned short MeshtasticModSource::encodeSymbol(unsigned short symbol)
|
||||
unsigned short MeshtasticModSource::encodeSymbol(unsigned short symbol, bool headerSymbol)
|
||||
{
|
||||
if (m_settings.m_deBits == 0) {
|
||||
return symbol;
|
||||
unsigned int deBits = static_cast<unsigned int>(std::max(0, m_settings.m_deBits));
|
||||
|
||||
if (headerSymbol && deBits < 2U) {
|
||||
deBits = 2U;
|
||||
}
|
||||
|
||||
unsigned int deWidth = 1<<m_settings.m_deBits;
|
||||
unsigned int baseSymbol = symbol % (m_fftLength/deWidth); // symbols range control
|
||||
return deWidth*baseSymbol;
|
||||
// return deWidth*baseSymbol + (deWidth/2) - 1;
|
||||
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 MeshtasticModSource::calculateLevel(Real& sample)
|
||||
|
||||
@ -78,6 +78,7 @@ private:
|
||||
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;
|
||||
@ -107,7 +108,7 @@ private:
|
||||
void reset();
|
||||
void calculateLevel(Real& sample);
|
||||
void modulateSample();
|
||||
unsigned short encodeSymbol(unsigned short symbol); //!< Encodes symbol with possible DE bits spacing
|
||||
unsigned short encodeSymbol(unsigned short symbol, bool headerSymbol); //!< Encodes symbol with payload/header DE spacing
|
||||
};
|
||||
|
||||
#endif // INCLUDE_MESHTASTICMODSOURCE_H
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
|
||||
<h2>Introduction</h2>
|
||||
|
||||
This plugin can be used to code and modulate a transmission signal based on Chirp Spread Spectrum (CSS). The basic idea is to transform each symbol of a MFSK modulation to an ascending frequency ramp shifted in time. It could equally be a descending ramp but this one is reserved to detect a break in the preamble sequence (synchronization). This plugin has been designed to work in conjunction with the ChirpChat demodulator plugin that should be used ideally on the reception side.
|
||||
This plugin can be used to code and modulate a transmission signal based the LoRa Chirp Spread Spectrum (CSS) modulation scheme with a Meshtastic payload.
|
||||
|
||||
It has clearly been inspired by the LoRa technique but is designed for experimentation and extension to other protocols mostly inspired by amateur radio techniques using chirp modulation to transmit symbols. Thanks to the MFSK to chirp translation it is possible to adapt any MFSK based mode.
|
||||
The basic idea of the CSS modulation is to transform each symbol of a MFSK modulation to an ascending frequency ramp shifted in time. It could equally be a descending ramp but this one is reserved to detect a break in the preamble sequence (synchronization). This plugin has been designed to work in conjunction with the Modmeshtastic demodulator plugin that should be used ideally on the reception side.
|
||||
|
||||
LoRa is a property of Semtech and the details of the protocol are not made public. However a LoRa compatible protocol has been implemented based on the reverse engineering performed by the community. It is mainly based on the work done in https://github.com/myriadrf/LoRa-SDR. You can find more information about LoRa and chirp modulation here:
|
||||
|
||||
@ -13,7 +13,23 @@ LoRa is a property of Semtech and the details of the protocol are not made publi
|
||||
|
||||
This LoRa encoder is designed for experimentation. For production grade applications it is recommended to use dedicated hardware instead.
|
||||
|
||||
Modulation characteristics from LoRa have been augmented with more bandwidths and FFT bin collations (DE factor). Plain TTY and ASCII have also been added that match character value to symbols directly. The FT protocol used in FT8 and FT4 is introduced packing the 174 bits payload into (SF -DE) bits symbols. There are plans to add some more of these typically amateur radio MFSK based modes like JT65.
|
||||
Modulation characteristics from LoRa have been augmented with more bandwidths and FFT bin collations (DE factor) but this should be rendered ineffective with Meshtastic presets.
|
||||
|
||||
<h2>Fixes done by Copilot (GPT 5.3 Codex) from first PR</h2>
|
||||
|
||||
- TX header explicit-header sizing (SF-2 first block)
|
||||
- TX backend Meshtastic auto-radio-derive (sync word 0x2B, PHY params)
|
||||
- RX robust header lock (offset 0..2 + delta -2..+2 scan with realignment)
|
||||
- This is on Rx side (MeshtasticDemodSink)
|
||||
- Diagnostic loopback logs with token correlation (commented out)
|
||||
- Type mismatches resolved
|
||||
|
||||
Now a full end to end test with a Meshtastic text message works:
|
||||
|
||||
- ✅ TX emits Meshtastic-compatible sync word (0x2B)
|
||||
- ✅ RX header lock resumes with offset/delta scan recovery
|
||||
- ✅ Payload CRC validation passes end-to-end
|
||||
- ✅ Full decode chain: preamble → sync → header → payload → message text displayed
|
||||
|
||||
<h2>Meshtastic frame mode</h2>
|
||||
|
||||
@ -94,7 +110,7 @@ Thus available bandwidths are:
|
||||
- **488** (500000 / 1024) Hz not in LoRa standard
|
||||
- **375** (384000 / 1024) Hz not in LoRa standard
|
||||
|
||||
The ChirpChat signal is oversampled by four therefore it needs a baseband of at least four times the bandwidth. This drives the maximum value on the slider automatically.
|
||||
The ChirpChat signal is oversampled by four therefore it needs a baseband of at least four times the bandwidth. This drives the maximum value on the slider automatically. When using Meshtastic presets you have to make sure this condition is set yourself.
|
||||
|
||||
<h3>16: Invert chirp ramps</h3>
|
||||
|
||||
@ -118,7 +134,7 @@ In practice it is difficult on the Rx side to make correct decodes if only one F
|
||||
|
||||
<h3>8: Number of preamble chirps</h3>
|
||||
|
||||
This is the number of preamble chirps to transmit that are used for the Rx to synchronize. The LoRa standard specifies it can be between 2 and 65535. Here it is limited to the 4 to 20 range that corresponds to realistic values. The RN2483 uses 6 preamble chirps. You may use 12 preamble chirps or more to facilitate signal acquisition with poor SNR on the Rx side.
|
||||
This is the number of preamble chirps to transmit that are used for the Rx to synchronize. The LoRa standard specifies it can be between 2 and 65535. Here it is limited to the 4 to 20 range that corresponds to realistic values. The RN2483 uses 6 preamble chirps. You may use 12 preamble chirps or more to facilitate signal acquisition with poor SNR on the Rx side. Meshtastic imposes a number of 17 preamble chirps.
|
||||
|
||||
<h3>9: Idle time between transmissions</h3>
|
||||
|
||||
@ -128,20 +144,13 @@ When sending a message repeatedly this is the time between the end of one transm
|
||||
|
||||

|
||||
|
||||
ChirpChat is primarily designed to make QSOs in the amateur radio sense. To be efficient the messages have to be kept short and minimal therefore the standard exchange follows WSJT scheme and is reflected in the sequence of messages you can follow with the message selection combo (10.9): CQ, Reply to CQ, Report to callee, Report to caller (R-Report), RRR and 73.
|
||||
|
||||
To populate messages you can specify your callsign (10.5), the other party callsign (10.6), your QRA locator (10.7) and a signal report (10.8)
|
||||
|
||||
<h4>10.1: Modulation scheme</h4>
|
||||
|
||||
- **LoRa**: LoRa compatible
|
||||
- **ASCII**: 7 bit plain ASCII without FEC and CRC. Requires exactly 7 bit effective samples thus SF-DE = 7 where SF is the spreading factor (5) and DE the distance enhancement factor (6)
|
||||
- **TTY**: 5 bit Baudot (Teletype) without FEC and CRC. Requires exactly 5 bit effective samples thus SF-DE = 5 where SF is the spreading factor (5) and DE the distance enhancement factor (6)
|
||||
- **FT**: FT8/FT4 coding is applied using data in (10.5) to (10.8) to encode the 174 bit message payload with CRC and FEC as per FT8/FT4 protocol using a type 1 (standard) type of message. Note that the report (10.8) must comply with the FT rule (coded "-35" to "+99" with a leading 0 for the number) and would usually represent the integer part of the S/N ratio in the ChirpChat demodulator receiver. Calls should not be prefixed nor suffixed and the first 4 characters of the locator must represent a valid 4 character grid square. Plain text messages (13 characters) are also supported with the 0.0 type of message using the text entered in the message box (11). These 174 bits are packed into (SF - DE) bits symbols padded with zero bits if necessary. For the details of the FT protocol see: https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf For example for SF=9 and DE=3 we have 6 bits per symbols so the 174 bits are packed in exactly 29 symbols this should appear in the message length ML (13)
|
||||
- **LoRa**: LoRa compatible for Meshtastic
|
||||
|
||||
<h4>10.2: Number of FEC parity bits (LoRa)</h4>
|
||||
<h4>10.2: Number of FEC parity bits</h4>
|
||||
|
||||
This is a LoRa specific feature. Each byte of the payload is split into two four bit nibbles and Hamming code of various "strength" in number of parity bits can be applied to these nibbles. The number of parity bits can vary from 1 to 4. 0 (no FEC) has been added but is not part of the LoRa original standard:
|
||||
Each byte of the payload is split into two four bit nibbles and Hamming code of various "strength" in number of parity bits can be applied to these nibbles. The number of parity bits can vary from 1 to 4. 0 (no FEC) has been added but is not part of the LoRa original standard:
|
||||
|
||||
- **0**: no FEC
|
||||
- **1**: 1 bit parity thus Hamming H(4,5) applies
|
||||
@ -149,71 +158,11 @@ This is a LoRa specific feature. Each byte of the payload is split into two four
|
||||
- **3**: 3 bit parity thus Hamming H(4,7) applies
|
||||
- **4**: 4 bit parity thus Hamming H(4,8) applies
|
||||
|
||||
<h4>10.3: Append two byte CRC to payload (LoRa)</h4>
|
||||
|
||||
This is a LoRa specific feature. A 2 bytes CRC can be appended to the payload.
|
||||
|
||||
<h4>10.4: Send a header at the start of the payload (LoRa)</h4>
|
||||
|
||||
This is a LoRa specific feature and is also known as explicit (with header) or implicit (without header) modes. In explicit mode a header with net payload length in bytes, presence of a CRC and number of parity bits is prepended to the actual payload. This header has a 1 byte CRC and is coded with H(4,8) FEC.
|
||||
|
||||
<h4>10.5: My callsign (QSO mode)</h4>
|
||||
|
||||
Enter your callsign so it can populate message placeholders (See next)
|
||||
|
||||
<h4>10.6: Your callsign (QSO mode)</h4>
|
||||
|
||||
Enter the other party callsign so it can populate message placeholders (See next)
|
||||
|
||||
<h4>10.7: My locator (QSO mode)</h4>
|
||||
|
||||
Enter your Maidenhead QRA locator so it can populate message placeholders (See next)
|
||||
|
||||
<h4>10.8: My report (QSO mode)</h4>
|
||||
|
||||
Enter the signal report you will send to the other party so it can populate message placeholders (See next)
|
||||
|
||||
<h4>10.9: Message selector</h4>
|
||||
|
||||
This lets you choose which pre-formatted message to send:
|
||||
|
||||
- **None**: empty message. In fact this is used to make a transition to trigger sending of the same message again. It is used internally by the "play" button (11) and can be used with the REST API.
|
||||
- **Beacon**: a beacon message
|
||||
- **CQ**: (QSO mode) CQ general call message
|
||||
- **Reply**: (QSO mode) reply to a CQ call
|
||||
- **Report**: (QSO mode) signal report to the callee of a CQ call
|
||||
- **R-Report**: (QSO mode) signal report to the caller of a CQ call
|
||||
- **RRR**: (QSO mode) report received confirmation to the callee
|
||||
- **73**: (QSO mode) confirmation back to the caller and closing the QSO
|
||||
- **QSO text**: (QSO mode) free form message with callsigns
|
||||
- **Text**: plain text
|
||||
- **Bytes**: binary message in the form of a string of bytes. Use the hex window (12) to specify the message
|
||||
|
||||
In FT mode standard FT type messages are generated regardless of placeholders based on MyCall, YourCall, MyLoc, Report and Msg data (entered while in "Text" format). Locators are 4 character grids i.e. only the 4 first characters are taken. Reports must be valid FT reports from -35 to 99 coded as < sign>< zero padded value> (e.g -12, -04, +00, +04, +12) :
|
||||
|
||||
- **Beacon**: DE < MyCall > < MyLoc >
|
||||
- **CQ**: CQ < MyCall> < MyLoc >
|
||||
- **Reply**: < YourCall > < MyCall > < MyLoc >
|
||||
- **Report**: < YourCall > < MyCall > < Report >
|
||||
- **R-Report**: < YourCall > < MyCall > R< Report >
|
||||
- **RRR**: < YourCall > < MyCall > RRR
|
||||
- **73**: < YourCall > < MyCall > 73
|
||||
- **QSO text**: < Msg >
|
||||
- **Text**: < Msg >
|
||||
- **Bytes**: < Msg >
|
||||
|
||||
<h4>10.10: Revert to standard messages</h4>
|
||||
|
||||
Reformat all predefined messages in standard messages with placeholders. The Generate button (13) replaces the placeholders with the given QSO elements (10.5 to 10.8)
|
||||
|
||||
- **Beacon**: `VVV DE %1 %2`
|
||||
- **CQ**: `CQ DE %1 %2`
|
||||
- **Reply**: `%1 %2 %3`
|
||||
- **Report**: `%1 %2 %3`
|
||||
- **R-Reply**: `%1 %2 R%3`
|
||||
- **RRR**: `%1 %2 RRR`
|
||||
- **73**: `%1 %2 73`
|
||||
- **QSO text**: `%1 %2 %3`
|
||||
- **Text**: plain text, either just text or formatted text for Meshtastic transmission (see above)
|
||||
|
||||
<h4>10.11 Play current message immediately</h4>
|
||||
|
||||
@ -223,41 +172,15 @@ This starts playing the current selected message immediately. It may be necessar
|
||||
|
||||
The message is repeated this number of times (use 0 for infinite). The end of one message sequence and the start of the next is separated by the delay specified with the "Idle" slidebar (9)
|
||||
|
||||
<h4>10.13: Generate messages</h4>
|
||||
|
||||
This applies the QSO elements (10.5 to 10.8) to the placeholders in messages to generate the final messages:
|
||||
|
||||
- **Beacon**: `VVV DE %1 %2`: `%1` is my call (10.5) and `%2` is my locator (10.7)
|
||||
- **CQ**: `CQ DE %1 %2`: `%1` is my call (10.5) and `%2` is my locator (10.7)
|
||||
- **Reply**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my locator (10.7)
|
||||
- **Report**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my report (10.8)
|
||||
- **R-Reply**: `%1 %2 R%3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is my report (10.8)
|
||||
- **RRR**: `%1 %2 RRR`: `%1` is your call (10.6) and `%2` is my call (10.5)
|
||||
- **73**: `%1 %2 73`: `%1` is your call (10.6) and `%2` is my call (10.5)
|
||||
- **QSO text**: `%1 %2 %3`: `%1` is your call (10.6), `%2` is my call (10.5) and `%3` is the text specified as the free form text message
|
||||
|
||||
<h4>10.14: Sync word</h4>
|
||||
|
||||
This is a LoRa specific feature and is the sync word (byte) to transmit entered as a 2 nibble hexadecimal number.
|
||||
This is a LoRa specific feature and is the sync word (byte) to transmit entered as a 2 nibble hexadecimal number. For Meshtastic this is 0x2B.
|
||||
|
||||
<h3>11: Message text</h3>
|
||||
|
||||
This window lets you edit the message selected in (10.9). You can use `%n` placeholders that depend on the type of message selected.
|
||||
|
||||
- **Beacon**: `%1` is my callsign and `%2` is my locator
|
||||
- **CQ message**: `%1` is my callsign and `%2` is my locator
|
||||
- **Reply**: `%1` is the other callsign, `%2` is my callsign and `%3` is my locator
|
||||
- **Report**: `%1` is the other callsign, `%2` is my callsign and `%3` is my report
|
||||
- **R-Report**: `%1` is the other callsign, `%2` is my callsign and `%3` is my report
|
||||
- **RRR**: `%1` is the other callsign and `%2` is my callsign
|
||||
- **73**: `%1` is the other callsign and `%2` is my callsign
|
||||
- **QSO Text**: `%1` is the other callsign, `%2` is my callsign and `%3` is the free text message
|
||||
- **Text**: free text message no placeholders
|
||||
- **Bytes**: binary message no placeholders
|
||||
|
||||
<h3>12: Message bytes</h3>
|
||||
|
||||
Use this line editor to specify the hex string used as the bytes message.
|
||||
|
||||
<h3>13: Symbol time and message length</h3>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user