[fix](trx-ft8): add hard-error verification to FT2 OSD decoder

The OSD-lite decoder was the source of FT2 false positives. It tries
685 CRC-14 checks across 5 passes (1 + 16 + 120 per pass), giving a
~4% chance of accepting random noise as a valid decode.

The reference implementation (decode174_91) verifies OSD results
against the received signal; the trx-rs OSD-lite only checked CRC.

Add ft2_count_hard_errors_vs_llr() which counts how many of the 174
coded bits in an OSD candidate disagree with the received hard
decisions. A legitimate correction disagrees in very few positions;
a false CRC match on noise disagrees in ~40-50 parity positions.
Reject OSD results with more than 36 hard errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-17 21:44:21 +01:00
parent 8aa1884d2d
commit 46415fa307
+34 -5
View File
@@ -980,11 +980,40 @@ static void ft2_encode_codeword_from_a91(const uint8_t a91[FTX_LDPC_K_BYTES], ui
}
static bool ft2_try_crc_candidate(const uint8_t a91[FTX_LDPC_K_BYTES], ftx_message_t* message)
// Count how many of the 174 coded bits in a candidate codeword disagree
// with the received hard decisions (sign of LLRs). A legitimate OSD
// correction should disagree in very few positions; a false CRC match on
// noise will disagree in ~half of the 83 parity positions.
static int ft2_count_hard_errors_vs_llr(const float log174[FTX_LDPC_N], const uint8_t codeword[FTX_LDPC_N])
{
int errors = 0;
for (int i = 0; i < FTX_LDPC_N; ++i)
{
uint8_t received = (log174[i] >= 0.0f) ? 1 : 0;
if (received != codeword[i])
++errors;
}
return errors;
}
// Maximum hard-error count for accepting an OSD result.
// The (174,91) code has minimum distance ~20, so a legitimate near-threshold
// decode should disagree in far fewer than 36 positions. Random CRC matches
// typically disagree in ~40-50 parity positions alone.
#define FT2_OSD_MAX_HARD_ERRORS 36
static bool ft2_try_crc_candidate(const uint8_t a91[FTX_LDPC_K_BYTES],
const float log174[FTX_LDPC_N],
ftx_message_t* message)
{
uint8_t codeword[FTX_LDPC_N];
ft2_encode_codeword_from_a91(a91, codeword);
return ft2_unpack_message(codeword, message);
if (!ft2_unpack_message(codeword, message))
return false;
// Verify the codeword is consistent with what we actually received.
if (log174 && ft2_count_hard_errors_vs_llr(log174, codeword) > FT2_OSD_MAX_HARD_ERRORS)
return false;
return true;
}
static bool ft2_osd_lite_decode(const float log174[FTX_LDPC_N], ftx_message_t* message)
@@ -997,7 +1026,7 @@ static bool ft2_osd_lite_decode(const float log174[FTX_LDPC_N], ftx_message_t* m
base_a91[i / 8] |= (uint8_t)(0x80u >> (i % 8));
}
if (ft2_try_crc_candidate(base_a91, message))
if (ft2_try_crc_candidate(base_a91, log174, message))
return true;
ft2_reliability_t reliabilities[FTX_LDPC_K];
@@ -1018,7 +1047,7 @@ static bool ft2_osd_lite_decode(const float log174[FTX_LDPC_N], ftx_message_t* m
memcpy(trial_a91, base_a91, sizeof(trial_a91));
int b0 = reliabilities[i].index;
trial_a91[b0 / 8] ^= (uint8_t)(0x80u >> (b0 % 8));
if (ft2_try_crc_candidate(trial_a91, message))
if (ft2_try_crc_candidate(trial_a91, log174, message))
return true;
}
@@ -1032,7 +1061,7 @@ static bool ft2_osd_lite_decode(const float log174[FTX_LDPC_N], ftx_message_t* m
int b1 = reliabilities[j].index;
trial_a91[b0 / 8] ^= (uint8_t)(0x80u >> (b0 % 8));
trial_a91[b1 / 8] ^= (uint8_t)(0x80u >> (b1 % 8));
if (ft2_try_crc_candidate(trial_a91, message))
if (ft2_try_crc_candidate(trial_a91, log174, message))
return true;
}
}