use alloc::string::String;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use crate::core::dsp::msk::bipolar_sync_word;
use crate::core::{DecodeContext, FecCodec, FecOpts};
use crate::fec::Ldpc128_90;
use crate::fec::ldpc_128_90::check_crc13;
use crate::msg::CallsignHashTable;
use crate::msg::wsjt77;
#[derive(Clone, Debug)]
pub struct FrameDecodeResult {
pub message: String,
pub info77: [u8; 77],
pub hard_errors: u32,
}
fn count_bad_sync(softbits: &[f32; 144], sync_bipolar: &[i8; 8], offset: usize) -> i32 {
let mut sum = 0i32;
for k in 0..8 {
let hard_bipolar = if softbits[offset + k] >= 0.0 { 1 } else { -1 };
sum += hard_bipolar * sync_bipolar[k] as i32;
}
(8 - sum) / 2
}
fn extract_n3_i3(info77: &[u8; 77]) -> (u8, u8) {
let n3 = (info77[71] << 2) | (info77[72] << 1) | info77[73];
let i3 = (info77[74] << 2) | (info77[75] << 1) | info77[76];
(n3, i3)
}
fn n3_i3_plausible(n3: u8, i3: u8) -> bool {
!((i3 == 0 && (n3 == 1 || n3 == 3 || n3 == 4 || n3 > 5)) || i3 == 3 || i3 > 5)
}
pub fn decode_frame(softbits: &[f32; 144], ctx: &DecodeContext) -> Option<FrameDecodeResult> {
let sync_bipolar = bipolar_sync_word();
let nbadsync =
count_bad_sync(softbits, &sync_bipolar, 0) + count_bad_sync(softbits, &sync_bipolar, 56);
if nbadsync > 4 {
return None;
}
let sav: f32 = softbits.iter().sum::<f32>() / 144.0;
let s2av: f32 = softbits.iter().map(|&s| s * s).sum::<f32>() / 144.0;
let ssig = (s2av - sav * sav).sqrt();
let normalized: [f32; 144] = {
let mut n = [0.0f32; 144];
for i in 0..144 {
n[i] = softbits[i] / ssig;
}
n
};
const SIGMA: f32 = 0.60;
let mut llr = [0.0f32; 128];
llr[0..48].copy_from_slice(&normalized[8..56]);
llr[48..128].copy_from_slice(&normalized[64..144]);
for v in llr.iter_mut() {
*v = 2.0 * *v / (SIGMA * SIGMA);
}
let opts = FecOpts {
bp_max_iter: 10, verify_info: Some(check_crc13),
..Default::default()
};
let result = Ldpc128_90.decode_soft(&llr, &opts)?;
if result.hard_errors >= 18 {
return None;
}
let mut info77 = [0u8; 77];
info77.copy_from_slice(&result.info[..77]);
let (n3, i3) = extract_n3_i3(&info77);
if !n3_i3_plausible(n3, i3) {
return None;
}
let message = if let Some(any) = ctx.callsign_hash_table.as_ref()
&& let Some(ht) = any.downcast_ref::<CallsignHashTable>()
{
wsjt77::unpack77_with_hash(&info77, ht)
} else {
wsjt77::unpack77(&info77)
}?;
Some(FrameDecodeResult {
message,
info77,
hard_errors: result.hard_errors,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::dsp::msk::{build_bitseq, matched_filter_softbits, synth_frame};
fn build_info_with_crc(pattern: impl Fn(usize) -> u8) -> [u8; 90] {
let mut info = [0u8; 90];
for i in 0..77 {
info[i] = pattern(i);
}
let mut bytes = [0u8; 12];
for (i, &b) in info[..77].iter().enumerate() {
let byte_idx = i / 8;
let bit_pos = 7 - (i % 8);
bytes[byte_idx] |= (b & 1) << bit_pos;
}
let crc = crate::fec::ldpc_128_90::crc13(&bytes);
for i in 0..13 {
info[77 + i] = ((crc >> (12 - i)) & 1) as u8;
}
info
}
#[test]
fn decodes_standard_message_end_to_end() {
let msg77 = wsjt77::pack77("K1ABC", "W9XYZ", "EN37").expect("valid standard message");
let mut info = [0u8; 90];
info[..77].copy_from_slice(&msg77);
let mut bytes = [0u8; 12];
for (i, &b) in info[..77].iter().enumerate() {
let byte_idx = i / 8;
let bit_pos = 7 - (i % 8);
bytes[byte_idx] |= (b & 1) << bit_pos;
}
let crc = crate::fec::ldpc_128_90::crc13(&bytes);
for i in 0..13 {
info[77 + i] = ((crc >> (12 - i)) & 1) as u8;
}
let mut codeword = [0u8; 128];
Ldpc128_90.encode(&info, &mut codeword);
let bitseq = build_bitseq(&codeword);
let frame = synth_frame(&bitseq);
let softbits = matched_filter_softbits(&frame);
let ctx = DecodeContext::default();
let result = decode_frame(&softbits, &ctx).expect("clean frame decodes");
assert!(result.message.contains("K1ABC"));
assert!(result.message.contains("W9XYZ"));
assert!(result.message.contains("EN37"));
assert!(result.hard_errors < 18);
}
#[test]
fn rejects_frame_with_bad_sync() {
let info = build_info_with_crc(|i| ((i * 7 + 3) & 1) as u8);
let mut codeword = [0u8; 128];
Ldpc128_90.encode(&info, &mut codeword);
let bitseq = build_bitseq(&codeword);
let frame = synth_frame(&bitseq);
let mut softbits = matched_filter_softbits(&frame);
for s in softbits.iter_mut().take(8) {
*s = -*s;
}
let ctx = DecodeContext::default();
assert!(decode_frame(&softbits, &ctx).is_none());
}
#[test]
fn n3_i3_filter_matches_truth_table() {
for n3 in 0..8u8 {
for i3 in 0..8u8 {
let expected =
!((i3 == 0 && (n3 == 1 || n3 == 3 || n3 == 4 || n3 > 5)) || i3 == 3 || i3 > 5);
assert_eq!(n3_i3_plausible(n3, i3), expected, "n3={n3} i3={i3}");
}
}
}
}