diff --git a/src/decoders/trx-ftx/src/decode.rs b/src/decoders/trx-ftx/src/decode.rs index 6041c20..e1c0f49 100644 --- a/src/decoders/trx-ftx/src/decode.rs +++ b/src/decoders/trx-ftx/src/decode.rs @@ -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) { + 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 { + 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. fn ftx_normalize_logl(log174: &mut [f32; FTX_LDPC_N]) { let mut sum = 0.0f32; @@ -583,32 +650,7 @@ pub fn ftx_decode_candidate( return None; } - let mut a91 = [0u8; FTX_LDPC_K_BYTES]; - 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) + verify_crc_and_build_message(&plain174, wf.protocol.uses_ft4_layout()) } fn max4(a: f32, b: f32, c: f32, d: f32) -> f32 { diff --git a/src/decoders/trx-ftx/src/encode.rs b/src/decoders/trx-ftx/src/encode.rs index 0a28455..d9f6d8d 100644 --- a/src/decoders/trx-ftx/src/encode.rs +++ b/src/decoders/trx-ftx/src/encode.rs @@ -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}; /// 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 >> 2); let x = x ^ (x >> 1); - x % 2 + x & 1 } /// 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. /// `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 for j in 0..FTX_LDPC_N_BYTES { 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. /// /// `payload` is a 10-byte array containing 77 bits of payload data. diff --git a/src/decoders/trx-ftx/src/ft2/mod.rs b/src/decoders/trx-ftx/src/ft2/mod.rs index e87f509..57ddbe9 100644 --- a/src/decoders/trx-ftx/src/ft2/mod.rs +++ b/src/decoders/trx-ftx/src/ft2/mod.rs @@ -16,10 +16,7 @@ pub mod sync; use num_complex::Complex32; use realfft::RealFftPlanner; -use crate::constants::FT4_XOR_SEQUENCE; -use crate::crc::{ftx_compute_crc, ftx_extract_crc}; -use crate::decode::{pack_bits, FtxMessage}; -use crate::ldpc; +use crate::decode::{normalize_llr, verify_crc_and_build_message, FtxMessage}; use crate::protocol::*; 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_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. pub fn ft2_frequency_offset_hz() -> f32 { -1.5 / FT2_SYMBOL_PERIOD_F @@ -603,9 +597,9 @@ impl Ft2Pipeline { // Scale and derive combined passes for i in 0..FTX_LDPC_N { - llr_passes[0][i] *= 3.2; - llr_passes[1][i] *= 3.2; - llr_passes[2][i] *= 3.2; + llr_passes[0][i] *= 2.83; + llr_passes[1][i] *= 2.83; + llr_passes[2][i] *= 2.83; let a = llr_passes[0][i]; let b = llr_passes[1][i]; @@ -624,66 +618,39 @@ impl Ft2Pipeline { 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 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 { if ok { break; } 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 - let mut bp_plain = [0u8; FTX_LDPC_N]; - let bp_errors = ldpc::bp_decode(&log174, 50, &mut bp_plain); - if bp_errors < nharderror { - nharderror = bp_errors; - } - if bp_errors == 0 { - if let Some(msg) = unpack_message(&bp_plain) { - message = msg; - ok = true; - nharderror = 0; - } - } + osd::ft2_decode174_91_osd( + &mut log174, + FTX_LDPC_K, + 3, + 3, + &mut apmask, + &mut message91, + &mut cw, + &mut ntype, + &mut nharderror, + &mut dmin, + ); - // Sum-product decode (fallback) - if !ok { - 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) { + if ntype > 0 && nharderror >= 0 { + if let Some(msg) = verify_crc_and_build_message(&cw, true) { message = msg; 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)]); } -/// 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 { - 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 { - 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 { - // 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 = (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)] mod tests { use super::*; @@ -996,17 +792,9 @@ mod tests { } #[test] - fn parity8_basic() { - 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() { + fn encode174_to_bits_all_zeros() { 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 { assert_eq!(b, 0); } diff --git a/src/decoders/trx-ftx/src/ft2/osd.rs b/src/decoders/trx-ftx/src/ft2/osd.rs index f469063..a9880be 100644 --- a/src/decoders/trx-ftx/src/ft2/osd.rs +++ b/src/decoders/trx-ftx/src/ft2/osd.rs @@ -18,6 +18,8 @@ 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::decode::pack_bits; +use crate::encode::parity8; 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. @@ -67,30 +69,10 @@ fn platanh(x: f32) -> f32 { 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). fn check_crc91(plain91: &[u8]) -> bool { 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); a91[9] &= 0xF8; a91[10] = 0x00; @@ -98,18 +80,10 @@ fn check_crc91(plain91: &[u8]) -> bool { 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. fn encode174_91_nocrc_bits(message91: &[u8], codeword: &mut [u8; FTX_LDPC_N]) { 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 for i in 0..FTX_LDPC_K { @@ -904,12 +878,12 @@ mod tests { } #[test] - fn pack_bits91_basic() { + fn shared_pack_bits_basic() { let mut bits = [0u8; FTX_LDPC_K]; bits[0] = 1; bits[7] = 1; 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); } @@ -923,7 +897,7 @@ mod tests { } #[test] - fn parity8_basic() { + fn shared_parity8_basic() { assert_eq!(parity8(0x00), 0); assert_eq!(parity8(0x01), 1); assert_eq!(parity8(0x03), 0);