use crate::codec::crc::{ft8_add_crc, ft8_crc14, ft8_extract_crc};
use crate::codec::gray::{gray8_decode, gray8_encode};
use crate::codec::ldpc::{self, ldpc_decode_soft, ldpc_encode};
use crate::message::{CallsignHashTable, Ft8Message, unpack77};
use crate::modulate::Ft8Frame;
use crate::modulate::ft4::{FT4_FRAME_LEN, FT4_TONE_SPACING_HZ};
use crate::modulate::ft8::{FT8_DATA_SYMS, FT8_FRAME_LEN, FT8_TONE_SPACING_HZ};
use crate::sync::{ft4_sync, ft8_sync};
use num_complex::Complex32 as C32;
pub type Ft8Bits = [u8; 10];
pub struct Ft8Codec;
impl Ft8Codec {
pub fn encode(payload: &Ft8Bits) -> Ft8Frame {
let mut a91 = [0u8; ldpc::K_BYTES];
ft8_add_crc(payload, &mut a91);
let mut codeword = [0u8; ldpc::N_BYTES];
ldpc_encode(&a91, &mut codeword);
let mut tones = [0u8; FT8_DATA_SYMS];
let mut mask: u8 = 0x80;
let mut byte_idx = 0usize;
for tone in tones.iter_mut() {
let mut bits3: u8 = 0;
for bit_pos in (0u8..3).rev() {
if codeword[byte_idx] & mask != 0 {
bits3 |= 1 << bit_pos;
}
mask >>= 1;
if mask == 0 {
mask = 0x80;
byte_idx += 1;
}
}
*tone = gray8_encode(bits3);
}
Ft8Frame::new(tones)
}
pub fn decode_hard(frame: &Ft8Frame) -> Option<Ft8Bits> {
let llr = Self::frame_to_llr_hard(frame);
Self::decode_llr(&llr)
}
pub fn decode_soft(llr: &[f32; ldpc::N]) -> Option<Ft8Bits> {
Self::decode_llr(llr)
}
pub fn frame_to_llr_hard(frame: &Ft8Frame) -> [f32; ldpc::N] {
let mut llr = [0.0f32; ldpc::N];
for (sym_idx, &tone) in frame.0.iter().enumerate() {
let bin = gray8_decode(tone);
for bit_pos in 0..3usize {
let bit = (bin >> (2 - bit_pos)) & 1;
llr[sym_idx * 3 + bit_pos] = if bit == 0 { 10.0 } else { -10.0 };
}
}
llr
}
fn decode_llr(llr: &[f32; ldpc::N]) -> Option<Ft8Bits> {
let mut plain = [0u8; ldpc::N];
let errors = ldpc_decode_soft(llr, 20, &mut plain);
if errors != 0 {
return None;
}
let mut a91 = [0u8; ldpc::K_BYTES];
for i in 0..ldpc::K {
if plain[i] == 1 {
a91[i / 8] |= 0x80 >> (i % 8);
}
}
let extracted = ft8_extract_crc(&a91);
let mut buf = a91;
buf[9] &= 0xF8; buf[10] = 0; buf[11] = 0; let computed = ft8_crc14(&buf, 82);
if extracted != computed {
return None;
}
let mut payload = [0u8; 10];
payload.copy_from_slice(&a91[..10]);
payload[9] &= 0xF8;
Some(payload)
}
}
pub struct Ft8DecodeResult {
pub message: Ft8Message,
pub carrier_hz: f32,
pub snr_db: f32,
}
pub struct Ft8StreamDecoder {
buf: Vec<C32>,
fs: f32,
base_hz: f32,
max_hz: f32,
frame_len: usize,
is_ft8: bool,
max_cand: usize,
hash_table: CallsignHashTable,
}
impl Ft8StreamDecoder {
pub fn new_ft8(fs: f32, base_hz: f32, max_hz: f32, max_cand: usize) -> Self {
Self {
buf: Vec::new(),
fs,
base_hz,
max_hz,
frame_len: FT8_FRAME_LEN,
is_ft8: true,
max_cand: max_cand.max(1),
hash_table: CallsignHashTable::new(),
}
}
pub fn new_ft4(fs: f32, base_hz: f32, max_hz: f32, max_cand: usize) -> Self {
Self {
buf: Vec::new(),
fs,
base_hz,
max_hz,
frame_len: FT4_FRAME_LEN,
is_ft8: false,
max_cand: max_cand.max(1),
hash_table: CallsignHashTable::new(),
}
}
pub fn feed(&mut self, iq: &[C32]) -> Vec<Ft8DecodeResult> {
self.buf.extend_from_slice(iq);
if self.buf.len() >= self.frame_len {
self.decode_buf()
} else {
Vec::new()
}
}
pub fn flush(&mut self) -> Vec<Ft8DecodeResult> {
if self.buf.is_empty() {
return Vec::new();
}
self.decode_buf()
}
pub fn clear(&mut self) {
self.buf.clear();
}
pub fn len(&self) -> usize {
self.buf.len()
}
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
pub fn view_buf(&self) -> &[C32] {
&self.buf
}
fn decode_buf(&mut self) -> Vec<Ft8DecodeResult> {
let mut results = Vec::new();
let tone_spacing = if self.is_ft8 {
FT8_TONE_SPACING_HZ
} else {
FT4_TONE_SPACING_HZ
};
let search_min = self.base_hz;
let search_max = (self.max_hz + tone_spacing).max(search_min + tone_spacing);
let candidates = if self.is_ft8 {
let raw = ft8_sync(
&self.buf,
self.fs,
search_min,
search_max,
0,
0,
self.max_cand,
);
raw.into_iter()
.map(|r| SyncCandidate {
freq_bin: r.freq_bin,
score: r.score,
llr: r.llr,
})
.collect::<Vec<_>>()
} else {
let raw = ft4_sync(
&self.buf,
self.fs,
search_min,
search_max,
0,
0,
self.max_cand,
);
raw.into_iter()
.map(|r| SyncCandidate {
freq_bin: r.freq_bin,
score: r.score,
llr: r.llr,
})
.collect::<Vec<_>>()
};
for cand in candidates {
let payload = if self.is_ft8 {
Ft8Codec::decode_soft(&cand.llr)
} else {
crate::codec::ft4::Ft4Codec::decode_soft(&cand.llr)
};
if let Some(bits) = payload {
let message = unpack77(&bits, &self.hash_table);
let carrier_hz = self.base_hz + cand.freq_bin as f32 * tone_spacing;
results.push(Ft8DecodeResult {
message,
carrier_hz,
snr_db: cand.score,
});
break;
}
}
results
}
}
struct SyncCandidate {
freq_bin: usize,
score: f32,
llr: [f32; ldpc::N],
}