[refactor](trx-ftx): consolidate FT2 decoder with shared FT8 code
Eliminate duplicated code between FT2 and FT8/shared modules: - Share parity8() from encode.rs, remove copies in ft2/mod.rs and osd.rs - Share pack_bits() from decode.rs, remove pack_bits91() from osd.rs - Add verify_crc_and_build_message() to decode.rs, used by both FT8 and FT2 - Add normalize_llr() to decode.rs, replacing per-module normalization - Make encode174() pub(crate), add encode174_to_bits() for bit-array output - Wire FT2 decode_hit to use full BP+OSD decoder from osd.rs instead of separate BP + sum-product + OSD-lite flow - Align LLR scale factor to 2.83 matching reference implementation Net -178 lines removed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -521,6 +521,73 @@ fn ft2_extract_logl_seq(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize LLR array by dividing by standard deviation, then optionally
|
||||||
|
/// scaling to a target variance.
|
||||||
|
///
|
||||||
|
/// If `target_variance` is `Some(v)`, the output has variance ≈ v.
|
||||||
|
/// If `None`, the output has unit variance (σ = 1).
|
||||||
|
pub(crate) fn normalize_llr(log174: &mut [f32; FTX_LDPC_N], target_variance: Option<f32>) {
|
||||||
|
let mut sum = 0.0f32;
|
||||||
|
let mut sum2 = 0.0f32;
|
||||||
|
for &v in log174.iter() {
|
||||||
|
sum += v;
|
||||||
|
sum2 += v * v;
|
||||||
|
}
|
||||||
|
let inv_n = 1.0 / FTX_LDPC_N as f32;
|
||||||
|
let variance = (sum2 - sum * sum * inv_n) * inv_n;
|
||||||
|
if variance <= 1e-12 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let scale = match target_variance {
|
||||||
|
Some(tv) => (tv / variance).sqrt(),
|
||||||
|
None => 1.0 / variance.sqrt(),
|
||||||
|
};
|
||||||
|
for v in log174.iter_mut() {
|
||||||
|
*v *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify CRC of a 174-bit plaintext and build an FtxMessage.
|
||||||
|
///
|
||||||
|
/// `plain174`: decoded LDPC codeword (174 bits, each 0 or 1).
|
||||||
|
/// `uses_xor`: true for FT4/FT2 (apply XOR sequence), false for FT8.
|
||||||
|
///
|
||||||
|
/// Returns `None` if CRC check fails.
|
||||||
|
pub(crate) fn verify_crc_and_build_message(
|
||||||
|
plain174: &[u8; FTX_LDPC_N],
|
||||||
|
uses_xor: bool,
|
||||||
|
) -> Option<FtxMessage> {
|
||||||
|
let mut a91 = [0u8; crate::protocol::FTX_LDPC_K_BYTES];
|
||||||
|
pack_bits(plain174, crate::protocol::FTX_LDPC_K, &mut a91);
|
||||||
|
|
||||||
|
let crc_extracted = crate::crc::ftx_extract_crc(&a91);
|
||||||
|
a91[9] &= 0xF8;
|
||||||
|
a91[10] = 0x00;
|
||||||
|
let crc_calculated = crate::crc::ftx_compute_crc(&a91, 96 - 14);
|
||||||
|
|
||||||
|
if crc_extracted != crc_calculated {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read a91 since we modified it for CRC check
|
||||||
|
pack_bits(plain174, crate::protocol::FTX_LDPC_K, &mut a91);
|
||||||
|
|
||||||
|
let mut message = FtxMessage {
|
||||||
|
hash: crc_calculated,
|
||||||
|
payload: [0; FTX_PAYLOAD_LENGTH_BYTES],
|
||||||
|
};
|
||||||
|
|
||||||
|
if uses_xor {
|
||||||
|
for i in 0..10 {
|
||||||
|
message.payload[i] = a91[i] ^ FT4_XOR_SEQUENCE[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.payload[..10].copy_from_slice(&a91[..10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(message)
|
||||||
|
}
|
||||||
|
|
||||||
/// Normalize log-likelihoods.
|
/// Normalize log-likelihoods.
|
||||||
fn ftx_normalize_logl(log174: &mut [f32; FTX_LDPC_N]) {
|
fn ftx_normalize_logl(log174: &mut [f32; FTX_LDPC_N]) {
|
||||||
let mut sum = 0.0f32;
|
let mut sum = 0.0f32;
|
||||||
@@ -583,32 +650,7 @@ pub fn ftx_decode_candidate(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut a91 = [0u8; FTX_LDPC_K_BYTES];
|
verify_crc_and_build_message(&plain174, wf.protocol.uses_ft4_layout())
|
||||||
pack_bits(&plain174, FTX_LDPC_K, &mut a91);
|
|
||||||
|
|
||||||
let crc_extracted = crate::crc::ftx_extract_crc(&a91);
|
|
||||||
a91[9] &= 0xF8;
|
|
||||||
a91[10] = 0x00;
|
|
||||||
let crc_calculated = crate::crc::ftx_compute_crc(&a91, 96 - 14);
|
|
||||||
|
|
||||||
if crc_extracted != crc_calculated {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut message = FtxMessage {
|
|
||||||
hash: crc_calculated,
|
|
||||||
payload: [0; FTX_PAYLOAD_LENGTH_BYTES],
|
|
||||||
};
|
|
||||||
|
|
||||||
if wf.protocol.uses_ft4_layout() {
|
|
||||||
for i in 0..10 {
|
|
||||||
message.payload[i] = a91[i] ^ FT4_XOR_SEQUENCE[i];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.payload[..10].copy_from_slice(&a91[..10]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max4(a: f32, b: f32, c: f32, d: f32) -> f32 {
|
fn max4(a: f32, b: f32, c: f32, d: f32) -> f32 {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ use crate::crc::ftx_add_crc;
|
|||||||
use crate::protocol::{FT4_NN, FT8_NN, FTX_LDPC_K, FTX_LDPC_K_BYTES, FTX_LDPC_M, FTX_LDPC_N_BYTES};
|
use crate::protocol::{FT4_NN, FT8_NN, FTX_LDPC_K, FTX_LDPC_K_BYTES, FTX_LDPC_M, FTX_LDPC_N_BYTES};
|
||||||
|
|
||||||
/// Returns 1 if an odd number of bits are set in `x`, zero otherwise.
|
/// Returns 1 if an odd number of bits are set in `x`, zero otherwise.
|
||||||
fn parity8(x: u8) -> u8 {
|
pub(crate) fn parity8(x: u8) -> u8 {
|
||||||
let x = x ^ (x >> 4);
|
let x = x ^ (x >> 4);
|
||||||
let x = x ^ (x >> 2);
|
let x = x ^ (x >> 2);
|
||||||
let x = x ^ (x >> 1);
|
let x = x ^ (x >> 1);
|
||||||
x % 2
|
x & 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode via LDPC a 91-bit message and return a 174-bit codeword.
|
/// Encode via LDPC a 91-bit message and return a 174-bit codeword.
|
||||||
@@ -24,7 +24,7 @@ fn parity8(x: u8) -> u8 {
|
|||||||
///
|
///
|
||||||
/// `message` must be at least `FTX_LDPC_K_BYTES` (12) bytes.
|
/// `message` must be at least `FTX_LDPC_K_BYTES` (12) bytes.
|
||||||
/// `codeword` must be at least `FTX_LDPC_N_BYTES` (22) bytes.
|
/// `codeword` must be at least `FTX_LDPC_N_BYTES` (22) bytes.
|
||||||
fn encode174(message: &[u8], codeword: &mut [u8]) {
|
pub(crate) fn encode174(message: &[u8], codeword: &mut [u8]) {
|
||||||
// Fill the codeword with message and zeros
|
// Fill the codeword with message and zeros
|
||||||
for j in 0..FTX_LDPC_N_BYTES {
|
for j in 0..FTX_LDPC_N_BYTES {
|
||||||
codeword[j] = if j < FTX_LDPC_K_BYTES { message[j] } else { 0 };
|
codeword[j] = if j < FTX_LDPC_K_BYTES { message[j] } else { 0 };
|
||||||
@@ -53,6 +53,23 @@ fn encode174(message: &[u8], codeword: &mut [u8]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a packed 91-bit message into a 174-bit codeword (bit array).
|
||||||
|
///
|
||||||
|
/// Each element of the returned array is 0 or 1.
|
||||||
|
/// Uses the same (174, 91) LDPC generator as `encode174`.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn encode174_to_bits(a91: &[u8; FTX_LDPC_K_BYTES]) -> [u8; crate::protocol::FTX_LDPC_N] {
|
||||||
|
use crate::protocol::FTX_LDPC_N;
|
||||||
|
let mut codeword_packed = [0u8; FTX_LDPC_N_BYTES];
|
||||||
|
encode174(a91, &mut codeword_packed);
|
||||||
|
|
||||||
|
let mut bits = [0u8; FTX_LDPC_N];
|
||||||
|
for i in 0..FTX_LDPC_N {
|
||||||
|
bits[i] = (codeword_packed[i / 8] >> (7 - (i % 8))) & 0x01;
|
||||||
|
}
|
||||||
|
bits
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate FT8 tone sequence from payload data.
|
/// Generate FT8 tone sequence from payload data.
|
||||||
///
|
///
|
||||||
/// `payload` is a 10-byte array containing 77 bits of payload data.
|
/// `payload` is a 10-byte array containing 77 bits of payload data.
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ pub mod sync;
|
|||||||
use num_complex::Complex32;
|
use num_complex::Complex32;
|
||||||
use realfft::RealFftPlanner;
|
use realfft::RealFftPlanner;
|
||||||
|
|
||||||
use crate::constants::FT4_XOR_SEQUENCE;
|
use crate::decode::{normalize_llr, verify_crc_and_build_message, FtxMessage};
|
||||||
use crate::crc::{ftx_compute_crc, ftx_extract_crc};
|
|
||||||
use crate::decode::{pack_bits, FtxMessage};
|
|
||||||
use crate::ldpc;
|
|
||||||
use crate::protocol::*;
|
use crate::protocol::*;
|
||||||
|
|
||||||
use bitmetrics::BitMetricsWorkspace;
|
use bitmetrics::BitMetricsWorkspace;
|
||||||
@@ -41,9 +38,6 @@ pub const FT2_FRAME_SYMBOLS: usize = FT2_NN - FT2_NR;
|
|||||||
pub const FT2_FRAME_SAMPLES: usize = FT2_FRAME_SYMBOLS * FT2_NSS;
|
pub const FT2_FRAME_SAMPLES: usize = FT2_FRAME_SYMBOLS * FT2_NSS;
|
||||||
pub const FT2_SYMBOL_PERIOD_F: f32 = FT2_SYMBOL_PERIOD;
|
pub const FT2_SYMBOL_PERIOD_F: f32 = FT2_SYMBOL_PERIOD;
|
||||||
|
|
||||||
/// Maximum hard-error count for accepting an OSD result.
|
|
||||||
const FT2_OSD_MAX_HARD_ERRORS: usize = 36;
|
|
||||||
|
|
||||||
/// Frequency offset applied to FT2 candidates.
|
/// Frequency offset applied to FT2 candidates.
|
||||||
pub fn ft2_frequency_offset_hz() -> f32 {
|
pub fn ft2_frequency_offset_hz() -> f32 {
|
||||||
-1.5 / FT2_SYMBOL_PERIOD_F
|
-1.5 / FT2_SYMBOL_PERIOD_F
|
||||||
@@ -603,9 +597,9 @@ impl Ft2Pipeline {
|
|||||||
|
|
||||||
// Scale and derive combined passes
|
// Scale and derive combined passes
|
||||||
for i in 0..FTX_LDPC_N {
|
for i in 0..FTX_LDPC_N {
|
||||||
llr_passes[0][i] *= 3.2;
|
llr_passes[0][i] *= 2.83;
|
||||||
llr_passes[1][i] *= 3.2;
|
llr_passes[1][i] *= 2.83;
|
||||||
llr_passes[2][i] *= 3.2;
|
llr_passes[2][i] *= 2.83;
|
||||||
|
|
||||||
let a = llr_passes[0][i];
|
let a = llr_passes[0][i];
|
||||||
let b = llr_passes[1][i];
|
let b = llr_passes[1][i];
|
||||||
@@ -624,66 +618,39 @@ impl Ft2Pipeline {
|
|||||||
llr_passes[4][i] = (a + b + c) / 3.0;
|
llr_passes[4][i] = (a + b + c) / 3.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-pass LDPC decode
|
// Multi-pass LDPC decode using full BP+OSD decoder
|
||||||
let mut ok = false;
|
let mut ok = false;
|
||||||
let mut message = FtxMessage::default();
|
let mut message = FtxMessage::default();
|
||||||
let mut global_best_errors = FTX_LDPC_M as i32;
|
let mut apmask = [0u8; FTX_LDPC_N];
|
||||||
|
|
||||||
for pass in 0..5 {
|
for pass in 0..5 {
|
||||||
if ok {
|
if ok {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let mut log174 = llr_passes[pass];
|
let mut log174 = llr_passes[pass];
|
||||||
normalize_log174(&mut log174);
|
normalize_llr(&mut log174, None);
|
||||||
|
|
||||||
let mut nharderror = FTX_LDPC_M as i32;
|
let mut message91 = [0u8; FTX_LDPC_K];
|
||||||
|
let mut cw = [0u8; FTX_LDPC_N];
|
||||||
|
let mut ntype = 0i32;
|
||||||
|
let mut nharderror = -1i32;
|
||||||
|
let mut dmin = 0.0f32;
|
||||||
|
|
||||||
// BP decode
|
osd::ft2_decode174_91_osd(
|
||||||
let mut bp_plain = [0u8; FTX_LDPC_N];
|
&mut log174,
|
||||||
let bp_errors = ldpc::bp_decode(&log174, 50, &mut bp_plain);
|
FTX_LDPC_K,
|
||||||
if bp_errors < nharderror {
|
3,
|
||||||
nharderror = bp_errors;
|
3,
|
||||||
}
|
&mut apmask,
|
||||||
if bp_errors == 0 {
|
&mut message91,
|
||||||
if let Some(msg) = unpack_message(&bp_plain) {
|
&mut cw,
|
||||||
message = msg;
|
&mut ntype,
|
||||||
ok = true;
|
&mut nharderror,
|
||||||
nharderror = 0;
|
&mut dmin,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Sum-product decode (fallback)
|
if ntype > 0 && nharderror >= 0 {
|
||||||
if !ok {
|
if let Some(msg) = verify_crc_and_build_message(&cw, true) {
|
||||||
let mut sp_log174 = llr_passes[pass];
|
|
||||||
normalize_log174(&mut sp_log174);
|
|
||||||
let mut sp_plain = [0u8; FTX_LDPC_N];
|
|
||||||
let sp_errors = ldpc::ldpc_decode(&mut sp_log174, 50, &mut sp_plain);
|
|
||||||
if sp_errors < nharderror {
|
|
||||||
nharderror = sp_errors;
|
|
||||||
}
|
|
||||||
if sp_errors == 0 {
|
|
||||||
if let Some(msg) = unpack_message(&sp_plain) {
|
|
||||||
message = msg;
|
|
||||||
ok = true;
|
|
||||||
nharderror = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if nharderror < global_best_errors {
|
|
||||||
global_best_errors = nharderror;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRC-based OSD-1/OSD-2 fallback when LDPC was close to converging
|
|
||||||
if !ok && global_best_errors <= 6 {
|
|
||||||
for pass in 0..5 {
|
|
||||||
if ok {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let mut osd_log174 = llr_passes[pass];
|
|
||||||
normalize_log174(&mut osd_log174);
|
|
||||||
if let Some(msg) = osd_lite_decode(&osd_log174) {
|
|
||||||
message = msg;
|
message = msg;
|
||||||
ok = true;
|
ok = true;
|
||||||
}
|
}
|
||||||
@@ -775,177 +742,6 @@ fn extract_signal_region(input: &[Complex32], start: i32, out_signal: &mut [Comp
|
|||||||
.copy_from_slice(&input[src_start..(src_start + copy_len)]);
|
.copy_from_slice(&input[src_start..(src_start + copy_len)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalize LLR array (divide by standard deviation).
|
|
||||||
fn normalize_log174(log174: &mut [f32; FTX_LDPC_N]) {
|
|
||||||
let mut sum = 0.0f32;
|
|
||||||
let mut sum2 = 0.0f32;
|
|
||||||
for &v in log174.iter() {
|
|
||||||
sum += v;
|
|
||||||
sum2 += v * v;
|
|
||||||
}
|
|
||||||
let inv_n = 1.0 / FTX_LDPC_N as f32;
|
|
||||||
let variance = (sum2 - sum * sum * inv_n) * inv_n;
|
|
||||||
if variance <= 1e-12 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let sigma = variance.sqrt();
|
|
||||||
for v in log174.iter_mut() {
|
|
||||||
*v /= sigma;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unpack a 174-bit plaintext into an FtxMessage, verifying CRC and applying XOR sequence.
|
|
||||||
fn unpack_message(plain174: &[u8; FTX_LDPC_N]) -> Option<FtxMessage> {
|
|
||||||
let mut a91 = [0u8; FTX_LDPC_K_BYTES];
|
|
||||||
pack_bits(plain174, FTX_LDPC_K, &mut a91);
|
|
||||||
|
|
||||||
let crc_extracted = ftx_extract_crc(&a91);
|
|
||||||
a91[9] &= 0xF8;
|
|
||||||
a91[10] = 0x00;
|
|
||||||
let crc_calculated = ftx_compute_crc(&a91, 96 - 14);
|
|
||||||
|
|
||||||
if crc_extracted != crc_calculated {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-read a91 since we modified it for CRC check
|
|
||||||
pack_bits(plain174, FTX_LDPC_K, &mut a91);
|
|
||||||
|
|
||||||
let mut msg = FtxMessage {
|
|
||||||
hash: crc_calculated,
|
|
||||||
payload: [0; FTX_PAYLOAD_LENGTH_BYTES],
|
|
||||||
};
|
|
||||||
for i in 0..10 {
|
|
||||||
msg.payload[i] = a91[i] ^ FT4_XOR_SEQUENCE[i];
|
|
||||||
}
|
|
||||||
Some(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode a packed 91-bit message into a 174-bit codeword (bit array).
|
|
||||||
fn encode_codeword_from_a91(a91: &[u8; FTX_LDPC_K_BYTES]) -> [u8; FTX_LDPC_N] {
|
|
||||||
let mut codeword = [0u8; FTX_LDPC_N];
|
|
||||||
// Systematic part
|
|
||||||
for i in 0..FTX_LDPC_K {
|
|
||||||
codeword[i] = (a91[i / 8] >> (7 - (i % 8))) & 0x01;
|
|
||||||
}
|
|
||||||
// Parity part using generator matrix
|
|
||||||
for i in 0..FTX_LDPC_M {
|
|
||||||
let mut nsum: u8 = 0;
|
|
||||||
for j in 0..FTX_LDPC_K_BYTES {
|
|
||||||
let x = a91[j] & crate::constants::FTX_LDPC_GENERATOR[i][j];
|
|
||||||
nsum ^= parity8(x);
|
|
||||||
}
|
|
||||||
codeword[FTX_LDPC_K + i] = nsum & 0x01;
|
|
||||||
}
|
|
||||||
codeword
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count parity of a byte.
|
|
||||||
fn parity8(x: u8) -> u8 {
|
|
||||||
let x = x ^ (x >> 4);
|
|
||||||
let x = x ^ (x >> 2);
|
|
||||||
let x = x ^ (x >> 1);
|
|
||||||
x & 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count hard errors between LLR signs and a candidate codeword.
|
|
||||||
fn count_hard_errors_vs_llr(log174: &[f32; FTX_LDPC_N], codeword: &[u8; FTX_LDPC_N]) -> usize {
|
|
||||||
let mut errors = 0;
|
|
||||||
for i in 0..FTX_LDPC_N {
|
|
||||||
let received = if log174[i] >= 0.0 { 1u8 } else { 0u8 };
|
|
||||||
if received != codeword[i] {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errors
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try a CRC candidate: encode the packed message, verify CRC and hard-error count.
|
|
||||||
fn try_crc_candidate(
|
|
||||||
a91: &[u8; FTX_LDPC_K_BYTES],
|
|
||||||
log174: &[f32; FTX_LDPC_N],
|
|
||||||
) -> Option<FtxMessage> {
|
|
||||||
let codeword = encode_codeword_from_a91(a91);
|
|
||||||
|
|
||||||
// Check CRC via unpack
|
|
||||||
let mut plain174 = [0u8; FTX_LDPC_N];
|
|
||||||
plain174.copy_from_slice(&codeword);
|
|
||||||
let msg = unpack_message(&plain174)?;
|
|
||||||
|
|
||||||
// Verify consistency with received LLRs
|
|
||||||
if count_hard_errors_vs_llr(log174, &codeword) > FT2_OSD_MAX_HARD_ERRORS {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reliability entry for OSD-lite sorting.
|
|
||||||
struct ReliabilityEntry {
|
|
||||||
index: usize,
|
|
||||||
reliability: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CRC-guided OSD-1/OSD-2 lite decoder.
|
|
||||||
///
|
|
||||||
/// Tries flipping each of the 16 least-reliable systematic bits (OSD-1),
|
|
||||||
/// then all pairs (OSD-2). Returns decoded message on CRC match.
|
|
||||||
fn osd_lite_decode(log174: &[f32; FTX_LDPC_N]) -> Option<FtxMessage> {
|
|
||||||
// Build base hard decision from systematic bits
|
|
||||||
let mut base_a91 = [0u8; FTX_LDPC_K_BYTES];
|
|
||||||
for i in 0..FTX_LDPC_K {
|
|
||||||
if log174[i] >= 0.0 {
|
|
||||||
base_a91[i / 8] |= 0x80u8 >> (i % 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try base (zero flips)
|
|
||||||
if let Some(msg) = try_crc_candidate(&base_a91, log174) {
|
|
||||||
return Some(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort systematic bits by reliability (ascending = least reliable first)
|
|
||||||
let mut rel: Vec<ReliabilityEntry> = (0..FTX_LDPC_K)
|
|
||||||
.map(|i| ReliabilityEntry {
|
|
||||||
index: i,
|
|
||||||
reliability: log174[i].abs(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
rel.sort_by(|a, b| {
|
|
||||||
a.reliability
|
|
||||||
.partial_cmp(&b.reliability)
|
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
});
|
|
||||||
|
|
||||||
let max_candidates = 16.min(FTX_LDPC_K);
|
|
||||||
|
|
||||||
// OSD-1: single bit flips
|
|
||||||
for i in 0..max_candidates {
|
|
||||||
let mut trial = base_a91;
|
|
||||||
let b0 = rel[i].index;
|
|
||||||
trial[b0 / 8] ^= 0x80u8 >> (b0 % 8);
|
|
||||||
if let Some(msg) = try_crc_candidate(&trial, log174) {
|
|
||||||
return Some(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OSD-2: all pairs
|
|
||||||
for i in 0..max_candidates {
|
|
||||||
for j in (i + 1)..max_candidates {
|
|
||||||
let mut trial = base_a91;
|
|
||||||
let b0 = rel[i].index;
|
|
||||||
let b1 = rel[j].index;
|
|
||||||
trial[b0 / 8] ^= 0x80u8 >> (b0 % 8);
|
|
||||||
trial[b1 / 8] ^= 0x80u8 >> (b1 % 8);
|
|
||||||
if let Some(msg) = try_crc_candidate(&trial, log174) {
|
|
||||||
return Some(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -996,17 +792,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parity8_basic() {
|
fn encode174_to_bits_all_zeros() {
|
||||||
assert_eq!(parity8(0x00), 0);
|
|
||||||
assert_eq!(parity8(0x01), 1);
|
|
||||||
assert_eq!(parity8(0x03), 0);
|
|
||||||
assert_eq!(parity8(0xFF), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn encode_codeword_all_zeros() {
|
|
||||||
let a91 = [0u8; FTX_LDPC_K_BYTES];
|
let a91 = [0u8; FTX_LDPC_K_BYTES];
|
||||||
let cw = encode_codeword_from_a91(&a91);
|
let cw = crate::encode::encode174_to_bits(&a91);
|
||||||
for &b in &cw {
|
for &b in &cw {
|
||||||
assert_eq!(b, 0);
|
assert_eq!(b, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
use crate::constants::{FTX_LDPC_GENERATOR, FTX_LDPC_MN, FTX_LDPC_NM, FTX_LDPC_NUM_ROWS};
|
use crate::constants::{FTX_LDPC_GENERATOR, FTX_LDPC_MN, FTX_LDPC_NM, FTX_LDPC_NUM_ROWS};
|
||||||
use crate::crc::{ftx_compute_crc, ftx_extract_crc};
|
use crate::crc::{ftx_compute_crc, ftx_extract_crc};
|
||||||
|
use crate::decode::pack_bits;
|
||||||
|
use crate::encode::parity8;
|
||||||
use crate::protocol::{FTX_LDPC_K, FTX_LDPC_K_BYTES, FTX_LDPC_M, FTX_LDPC_N};
|
use crate::protocol::{FTX_LDPC_K, FTX_LDPC_K_BYTES, FTX_LDPC_M, FTX_LDPC_N};
|
||||||
|
|
||||||
/// Check LDPC parity of a 174-bit codeword. Returns number of parity errors.
|
/// Check LDPC parity of a 174-bit codeword. Returns number of parity errors.
|
||||||
@@ -67,30 +69,10 @@ fn platanh(x: f32) -> f32 {
|
|||||||
isign * 7.0
|
isign * 7.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pack bit array into bytes (MSB first).
|
|
||||||
fn pack_bits91(bit_array: &[u8], num_bits: usize, packed: &mut [u8]) {
|
|
||||||
let num_bytes = num_bits.div_ceil(8);
|
|
||||||
for b in packed[..num_bytes].iter_mut() {
|
|
||||||
*b = 0;
|
|
||||||
}
|
|
||||||
let mut mask: u8 = 0x80;
|
|
||||||
let mut byte_idx = 0;
|
|
||||||
for i in 0..num_bits {
|
|
||||||
if bit_array[i] != 0 {
|
|
||||||
packed[byte_idx] |= mask;
|
|
||||||
}
|
|
||||||
mask >>= 1;
|
|
||||||
if mask == 0 {
|
|
||||||
mask = 0x80;
|
|
||||||
byte_idx += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check CRC of a 91-bit message (in bit array form).
|
/// Check CRC of a 91-bit message (in bit array form).
|
||||||
fn check_crc91(plain91: &[u8]) -> bool {
|
fn check_crc91(plain91: &[u8]) -> bool {
|
||||||
let mut a91 = [0u8; FTX_LDPC_K_BYTES];
|
let mut a91 = [0u8; FTX_LDPC_K_BYTES];
|
||||||
pack_bits91(plain91, FTX_LDPC_K, &mut a91);
|
pack_bits(plain91, FTX_LDPC_K, &mut a91);
|
||||||
let crc_extracted = ftx_extract_crc(&a91);
|
let crc_extracted = ftx_extract_crc(&a91);
|
||||||
a91[9] &= 0xF8;
|
a91[9] &= 0xF8;
|
||||||
a91[10] = 0x00;
|
a91[10] = 0x00;
|
||||||
@@ -98,18 +80,10 @@ fn check_crc91(plain91: &[u8]) -> bool {
|
|||||||
crc_extracted == crc_calculated
|
crc_extracted == crc_calculated
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute parity of a byte.
|
|
||||||
fn parity8(x: u8) -> u8 {
|
|
||||||
let x = x ^ (x >> 4);
|
|
||||||
let x = x ^ (x >> 2);
|
|
||||||
let x = x ^ (x >> 1);
|
|
||||||
x & 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode a 91-bit message (bit array) into a 174-bit codeword without CRC computation.
|
/// Encode a 91-bit message (bit array) into a 174-bit codeword without CRC computation.
|
||||||
fn encode174_91_nocrc_bits(message91: &[u8], codeword: &mut [u8; FTX_LDPC_N]) {
|
fn encode174_91_nocrc_bits(message91: &[u8], codeword: &mut [u8; FTX_LDPC_N]) {
|
||||||
let mut packed = [0u8; FTX_LDPC_K_BYTES];
|
let mut packed = [0u8; FTX_LDPC_K_BYTES];
|
||||||
pack_bits91(message91, FTX_LDPC_K, &mut packed);
|
pack_bits(message91, FTX_LDPC_K, &mut packed);
|
||||||
|
|
||||||
// Systematic bits
|
// Systematic bits
|
||||||
for i in 0..FTX_LDPC_K {
|
for i in 0..FTX_LDPC_K {
|
||||||
@@ -904,12 +878,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pack_bits91_basic() {
|
fn shared_pack_bits_basic() {
|
||||||
let mut bits = [0u8; FTX_LDPC_K];
|
let mut bits = [0u8; FTX_LDPC_K];
|
||||||
bits[0] = 1;
|
bits[0] = 1;
|
||||||
bits[7] = 1;
|
bits[7] = 1;
|
||||||
let mut packed = [0u8; FTX_LDPC_K_BYTES];
|
let mut packed = [0u8; FTX_LDPC_K_BYTES];
|
||||||
pack_bits91(&bits, FTX_LDPC_K, &mut packed);
|
pack_bits(&bits, FTX_LDPC_K, &mut packed);
|
||||||
assert_eq!(packed[0], 0x81);
|
assert_eq!(packed[0], 0x81);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,7 +897,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parity8_basic() {
|
fn shared_parity8_basic() {
|
||||||
assert_eq!(parity8(0x00), 0);
|
assert_eq!(parity8(0x00), 0);
|
||||||
assert_eq!(parity8(0x01), 1);
|
assert_eq!(parity8(0x01), 1);
|
||||||
assert_eq!(parity8(0x03), 0);
|
assert_eq!(parity8(0x03), 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user