From e63a3b792bbcba800c7d0c593d087d44c2c93b0e Mon Sep 17 00:00:00 2001 From: jvn314 Date: Thu, 27 Aug 2026 22:37:03 -0500 Subject: [PATCH] Fix Meshtastic soft FEC payload status --- .../meshtasticdemoddecoderlora.cpp | 62 +++++++++++++++++-- .../meshtasticdemoddecoderlora.h | 32 +++++++++- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp index 474c6c34c..97377a53f 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.cpp @@ -432,6 +432,8 @@ void MeshtasticDemodDecoderLoRa::decodeBytesSoft( { payloadCRCStatus = false; payloadParityStatus = (int) MeshtasticDemodSettings::ParityUndefined; + bool error = false; // set if any payload codeword requires soft FEC correction + bool bad = false; // set if any payload codeword has a structural decode failure if (inSymbols.size() < headerSymbols) { @@ -542,7 +544,18 @@ void MeshtasticDemodDecoderLoRa::decodeBytesSoft( } } - auto decodeSoftBlock = [&llrs, spreadFactor](unsigned int symOfs, unsigned int cwLen, unsigned int sfApp, unsigned int crApp, std::vector& nibbles) { + auto decodeSoftBlock = [ + &llrs, + spreadFactor, + &error, &bad // accumulated payload FEC status + ]( + unsigned int symOfs, + unsigned int cwLen, + unsigned int sfApp, + unsigned int crApp, + unsigned int payloadRowStart, // first row that contributes to payload FEC status + std::vector& nibbles) { + if (sfApp == 0U) { return false; } @@ -569,8 +582,22 @@ void MeshtasticDemodDecoderLoRa::decodeBytesSoft( } } - for (unsigned int row = 0; row < sfApp; row++) { - nibbles.push_back(decodeCodewordSoft(deinterBin[row], crApp)); + for (unsigned int row = 0; row < sfApp; row++) + { + if (row < payloadRowStart) + { + // Still in header, don't accumulate payload FEC + bool ignoredError = false; + bool ignoredBad = false; + nibbles.push_back( + decodeCodewordSoft(deinterBin[row], crApp, ignoredError, ignoredBad)); + } + else + { + // Payload row contributes to accumulated payload FEC status + nibbles.push_back( + decodeCodewordSoft(deinterBin[row], crApp, error, bad)); + } } return true; @@ -587,7 +614,15 @@ void MeshtasticDemodDecoderLoRa::decodeBytesSoft( return; } - if (!decodeSoftBlock(symOfs, 8U, headerNbSymbolBits, 4U, nibbles)) { + const unsigned int payloadRowStart = headerCodewords; // payload starts after the 5 header codewords + + if (!decodeSoftBlock( + symOfs, + headerSymbols, + headerNbSymbolBits, + headerParityBits, + payloadRowStart, + nibbles)) { earlyEOM = true; return; } @@ -595,9 +630,17 @@ void MeshtasticDemodDecoderLoRa::decodeBytesSoft( symOfs += headerSymbols; } + const unsigned int payloadOnlyRowStart = 0U; // all rows in these blocks are payload + while (symOfs + payloadBlockSymbols <= numSymbols) { - if (!decodeSoftBlock(symOfs, payloadBlockSymbols, payloadNbSymbolBits, nbParityBits, nibbles)) { + if (!decodeSoftBlock( + symOfs, + payloadBlockSymbols, + payloadNbSymbolBits, + nbParityBits, + payloadOnlyRowStart, + nibbles)) { earlyEOM = true; return; } @@ -628,6 +671,15 @@ void MeshtasticDemodDecoderLoRa::decodeBytesSoft( dewhitenPayloadBytes(bytes.data(), packetLength); } + // Report accumulated payload FEC result independently of CRC + if (bad) { + payloadParityStatus = (int) MeshtasticDemodSettings::ParityError; + } else if (error) { + payloadParityStatus = (int) MeshtasticDemodSettings::ParityCorrected; + } else { + payloadParityStatus = (int) MeshtasticDemodSettings::ParityOK; + } + if (hasCRC) { if ((packetLength >= 2U) && (dataByteLen >= packetLength + 2U)) diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h index 00e32c896..e3088e3e8 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemoddecoderlora.h @@ -274,7 +274,11 @@ private: return static_cast((nibbleBits[0] << 3) | (nibbleBits[1] << 2) | (nibbleBits[2] << 1) | nibbleBits[3]); } - static inline unsigned char decodeCodewordSoft(const std::vector& codewordLLR, unsigned int crApp) + static inline unsigned char decodeCodewordSoft( + const std::vector& codewordLLR, + unsigned int crApp, + bool& error, // set if soft FEC changes a hard decision; caller may accumulate across codewords + bool& bad) // set on structural decode failure; caller may accumulate across codewords { static const unsigned char cwLUT[16] = { 0, 23, 45, 58, 78, 89, 99, 116, @@ -286,10 +290,17 @@ private: }; if ((crApp < 1U) || (crApp > 4U)) { + bad = true; // invalid coding rate: structural decode failure return 0; } const unsigned int cwLen = 4U + crApp; + + if (codewordLLR.size() < cwLen) { + bad = true; // insufficient LLR data: structural decode failure (later mapped to ParityError) + return 0; + } + const unsigned char *lut = (crApp == 1U) ? cwLUTCr5 : cwLUT; float bestScore = std::numeric_limits::lowest(); unsigned int bestIdx = 0U; @@ -312,6 +323,25 @@ private: } } + const unsigned char selectedCW = + static_cast(lut[bestIdx] >> (8U - cwLen)); + + for (unsigned int j = 0; j < cwLen; j++) + { + if (codewordLLR[j] == 0.0f) { + continue; // no hard-decision preference for an exactly zero LLR + } + + const bool hardBit = codewordLLR[j] > 0.0f; + const bool selectedBit = + ((selectedCW >> (cwLen - 1U - j)) & 0x1U) != 0U; + + if (hardBit != selectedBit) { + error = true; // soft FEC changed one or more raw hard-decision bits + break; + } + } + const unsigned char dataNibbleSoft = static_cast(cwLUT[bestIdx] >> 4); return static_cast( (((dataNibbleSoft & 0x1U) != 0U) << 3) |