use core::fmt;
use crate::geo::{GeoError, MaidenheadGrid};
use crate::types::{SampleRate, sine_at};
#[cfg(feature = "std")]
mod rx;
#[cfg(feature = "std")]
pub use rx::{Ft8Decode, Ft8Decoder, Ft8DecoderConfig, Ft8RxError};
pub const SYMBOL_COUNT: usize = 79;
pub const PAYLOAD_BITS: usize = 77;
pub const PAYLOAD_LEN: usize = 10;
pub const MESSAGE_BITS: usize = 91;
pub const MESSAGE_LEN: usize = 12;
pub const CODEWORD_BITS: usize = 174;
pub const CODEWORD_LEN: usize = 22;
pub const PARITY_BITS: usize = 83;
pub const CRC_POLY: u16 = 0x2757;
pub const COSTAS: [u8; 7] = [3, 1, 4, 0, 6, 5, 2];
pub const GRAY_MAP: [u8; 8] = [0, 1, 3, 2, 5, 6, 4, 7];
pub const NTOKENS: u32 = 2_063_592;
pub const MAX22: u32 = 4_194_304;
const _C28_FULL: () =
assert!(NTOKENS as u64 + MAX22 as u64 + 37 * 36 * 10 * 27 * 27 * 27 == 1 << 28);
pub const MAXGRID4: u16 = 32_400;
pub const FREE_TEXT_ALPHABET: &[u8; 42] = b" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?";
pub const C28_SETS: [&[u8]; 4] = [
b" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
b"0123456789",
b" ABCDEFGHIJKLMNOPQRSTUVWXYZ",
];
pub const GFSK_BT: f64 = 2.0;
const GFSK_K: f64 = 5.336_446_256_636_997;
#[rustfmt::skip]
pub const GENERATOR_BITS: [u128; 83] = [
0x4194e708df98f57a84f93fe, 0x3b0e132712e12c99aa49899, 0x6e132c817d93be320850dee,
0x0d9fa0bc2c6696e99f63fb1, 0x04fed27f7020cafe81a3c1d, 0x03be66608dc439f6ae1ea45,
0x14db157f1e501b7a7f0d4ed, 0x302a7d7af9aecb69d86461f, 0x7103cc7218877693c425748,
0x3bae4e047407136ed72b18c, 0x585c08814615fccb909a43e, 0x0c5064918fe3056fae2f519,
0x3b238f418150390f00d895c, 0x7fde65c06541a0fd7da3d97, 0x3353950ac7c992d15fb38b8,
0x62121b44ff42d8e289b1d0c, 0x06ffb9ca0a68d0d9a58e138, 0x0ada441831b645ccc4a4b97,
0x14d44e069ef40eb32a44d87, 0x2789379bfd28e5f30deb5ca, 0x4ce2391ce86cbe9e42704a0,
0x0c8cdba88cbb2b10dda78f4, 0x04ed896b98fd7705c36fb5c, 0x2447e19efa1fdef752757da,
0x413a11f7205b3afbab75aff, 0x55f0cbe24265ba3ab8a254d, 0x15a80725e0762d3695edee8,
0x623a5529eb810c3b0b349b0, 0x475d0d09ed99c85eb38c676, 0x3a9c22339d13bc166210097,
0x037fc1d0a2e1b81ad2e0934, 0x1d9ba0bc2c6616e99f61fb1, 0x4d252d14770be54e1924216,
0x5e14fa32984e4bbf44b0852, 0x1331d736efc5ae715d94a44, 0x237918f7f22b81a60c0a20c,
0x1fd96742d5f4d86397037df, 0x6f43a40f94160a9cb8d0517, 0x7e6be6791e34fd4cddd0a09,
0x78130a23f4a48654723a676, 0x220808ac0c0cb7cae6eb809, 0x0447e18efa5fdef152757da,
0x5c7f78db183b94fd8503c60, 0x2d7f53d6665bbdde4eccd48, 0x24d380b56329fb2f66e483b,
0x0ca26842df273ed46b663e8, 0x128fb156e2019787738a001, 0x2b238fc38150390f005895c,
0x15c72491f96ea8f16a9bfd0, 0x35aa85205337a3aaef4ae13, 0x50c56946a713ff49527b642,
0x086172c31c465c151ec03ac, 0x779a520c0bf701099ed9758, 0x3f4e062a192d4e0ac1b7000,
0x1b49f2b968fef266f83cf43, 0x5fd96762d5f0d8639703fdf, 0x3f70c11862c1e6662bea584,
0x50336597f6d7e4fa9332093, 0x5d91b92d5e23e62fa662669, 0x6f6cedd1df72062cdab04da,
0x6cd380b56329f36f66e481b, 0x4d6a3576afb83f94055afe2, 0x72c90e3bc112c398b6be9e1,
0x278a6d4121545c36e5399a9, 0x45c5a83d6a33ea220efbb87, 0x11418e4e788b4a33d6825b4,
0x109dc1c7f1572a61c7738c0, 0x2ec935b6eb8f8428c0d2709, 0x3355bcea594f73734a84f2b,
0x4ac0a43416ba451c6eb45d5, 0x5c6701067834e195391d58a, 0x7a198eb6a30b03f4aba93a3,
0x36d11dd2125cacb099e7ce4, 0x531b5e5e3d9862fdf5733ff, 0x2e586c3503efb2a54844d10,
0x788f8834243c07e4f66ec05, 0x0fdda9b27dc6964eb986add, 0x7e5c35e3852864e8152e81a,
0x529a219814f560af99171a6, 0x64c4ece3e1e9dc62aeba898, 0x3dd9c59780c36a3321d74b1,
0x132275d6f5a25ca33e8fa16, 0x3046642baca5fddaaeb4b00,
];
static GENERATOR_ROWS: [[u8; MESSAGE_LEN]; PARITY_BITS] = parse_generator();
const fn parse_generator() -> [[u8; MESSAGE_LEN]; PARITY_BITS] {
let mut rows = [[0u8; MESSAGE_LEN]; PARITY_BITS];
let mut r = 0;
while r < PARITY_BITS {
let row = GENERATOR_BITS[r];
assert!(row >> MESSAGE_BITS == 0, "generator row exceeds 91 bits");
let mut j = 0;
while j < MESSAGE_BITS {
if (row >> (MESSAGE_BITS - 1 - j)) & 1 == 1 {
rows[r][j / 8] |= 1 << (7 - j % 8);
}
j += 1;
}
r += 1;
}
rows
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Ft8Error {
CallsignCompound,
CallsignLength {
len: usize,
},
CallsignChar {
ch: char,
index: usize,
},
CallsignShape,
TokenNotAllowedHere,
DirectedCqUnsupported,
GridLength {
len: usize,
},
ReportOutOfRange {
got: i8,
},
AckFlagInvalid,
FreeTextLength {
len: usize,
},
FreeTextChar {
ch: char,
index: usize,
},
UnsupportedMessageType,
LdpcNotConverged,
LlrNotFinite,
CrcMismatch,
SampleRateInexact {
got: u32,
},
ToneOutOfRange {
base_hz: u32,
sample_rate: u32,
},
}
impl fmt::Display for Ft8Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::CallsignCompound => write!(
f,
"callsign contains '/': compound calls need message types outside the supported subset"
),
Self::CallsignLength { len } => write!(
f,
"callsign length {len} is invalid: must be 1..=6 characters"
),
Self::CallsignChar { ch, index } => write!(
f,
"callsign character {ch:?} is invalid at aligned position {index}"
),
Self::CallsignShape => write!(
f,
"callsign cannot be aligned to the standard shape (third character must be a digit)"
),
Self::TokenNotAllowedHere => write!(
f,
"CQ/QRZ/DE cannot stand as the second callsign of a standard message"
),
Self::DirectedCqUnsupported => write!(
f,
"directed CQ is outside the supported subset: only plain \"CQ\" is encoded"
),
Self::GridLength { len } => write!(
f,
"grid locator length {len} is invalid: must be exactly 4 characters"
),
Self::ReportOutOfRange { got } => write!(
f,
"report {got} dB is out of range: must be within -30..=+49"
),
Self::AckFlagInvalid => write!(
f,
"the R flag can only accompany a grid or a report trailer"
),
Self::FreeTextLength { len } => write!(
f,
"free text length {len} is invalid: must be at most 13 characters"
),
Self::FreeTextChar { ch, index } => write!(
f,
"free-text character {ch:?} at position {index} is outside the 42-character alphabet"
),
Self::UnsupportedMessageType => write!(
f,
"message needs an FT8 payload type outside the supported subset (standard i3=1, free text i3.n3=0.0)"
),
Self::LdpcNotConverged => write!(
f,
"LDPC decode did not converge within the {LDPC_MAX_ITERS}-iteration cap"
),
Self::LlrNotFinite => write!(
f,
"a channel LLR was NaN or infinite, so the decode would have been fabricated"
),
Self::CrcMismatch => write!(
f,
"CRC-14 mismatch: the LDPC decode converged to a wrong codeword"
),
Self::SampleRateInexact { got } => write!(
f,
"sample rate {got} Hz cannot time FT8 symbols exactly: must be a multiple of 25 Hz"
),
Self::ToneOutOfRange {
base_hz,
sample_rate,
} => write!(
f,
"base frequency {base_hz} Hz is invalid at {sample_rate} Hz: tones must be nonzero and below Nyquist"
),
}
}
}
impl core::error::Error for Ft8Error {}
pub const LDPC_MAX_ITERS: u32 = 40;
const MIN_SUM_ALPHA: f32 = 0.8;
#[rustfmt::skip]
pub const CHECK_ROWS: [[u8; 7]; PARITY_BITS] = [
[16, 26, 88, 102, 115, 152, 255], [3, 28, 67, 119, 133, 172, 255],
[27, 28, 83, 87, 116, 142, 149], [13, 29, 82, 112, 124, 169, 255],
[2, 23, 29, 71, 103, 138, 255], [30, 68, 132, 149, 154, 168, 255],
[13, 30, 78, 97, 131, 163, 255], [27, 31, 71, 102, 131, 165, 255],
[0, 32, 71, 105, 106, 156, 255], [5, 32, 84, 107, 115, 155, 255],
[4, 33, 64, 77, 97, 106, 153], [22, 33, 70, 93, 126, 152, 255],
[28, 33, 86, 96, 146, 161, 255], [34, 81, 132, 141, 170, 173, 255],
[8, 34, 65, 98, 138, 145, 255], [9, 35, 66, 99, 106, 125, 255],
[17, 35, 75, 88, 112, 113, 142], [10, 36, 66, 86, 100, 138, 157],
[16, 36, 73, 80, 108, 130, 153], [20, 36, 72, 137, 151, 168, 255],
[11, 37, 67, 101, 104, 154, 255], [18, 37, 76, 103, 115, 162, 255],
[24, 37, 64, 98, 121, 159, 255], [4, 38, 74, 101, 135, 166, 255],
[12, 38, 68, 102, 148, 161, 255], [7, 39, 69, 81, 103, 113, 144],
[8, 39, 89, 105, 133, 150, 255], [13, 40, 70, 87, 101, 122, 155],
[25, 40, 76, 108, 140, 147, 255], [16, 41, 74, 128, 169, 171, 255],
[17, 41, 78, 143, 145, 151, 255], [11, 42, 65, 88, 96, 134, 158],
[15, 42, 72, 107, 140, 159, 255], [22, 42, 78, 119, 130, 144, 255],
[2, 43, 79, 123, 126, 168, 255], [9, 43, 81, 90, 110, 143, 148],
[10, 43, 74, 109, 120, 165, 255], [20, 44, 77, 82, 116, 120, 150],
[0, 25, 44, 79, 127, 146, 255], [7, 45, 70, 111, 118, 165, 255],
[18, 45, 80, 116, 134, 166, 255], [19, 45, 64, 79, 119, 139, 169],
[15, 46, 75, 129, 136, 153, 255], [19, 46, 69, 91, 137, 164, 255],
[1, 47, 73, 112, 127, 159, 255], [2, 12, 47, 77, 94, 122, 255],
[27, 47, 69, 84, 104, 128, 157], [10, 48, 87, 91, 141, 156, 255],
[6, 49, 80, 98, 131, 172, 255], [51, 83, 109, 114, 144, 167, 255],
[23, 51, 75, 128, 147, 148, 255], [9, 52, 65, 83, 111, 127, 164],
[21, 52, 67, 108, 120, 173, 255], [24, 52, 68, 89, 100, 129, 155],
[1, 53, 85, 100, 134, 163, 255], [20, 53, 76, 99, 139, 170, 255],
[22, 54, 66, 94, 171, 173, 255], [17, 48, 54, 123, 140, 166, 255],
[14, 55, 86, 107, 118, 170, 255], [26, 39, 55, 123, 124, 125, 255],
[25, 50, 55, 90, 121, 136, 167], [21, 56, 84, 92, 139, 158, 255],
[50, 56, 97, 162, 164, 171, 255], [0, 3, 51, 56, 85, 135, 151],
[21, 46, 57, 117, 126, 163, 255], [6, 48, 57, 89, 99, 104, 167],
[3, 30, 58, 90, 91, 95, 152], [18, 34, 58, 72, 109, 124, 160],
[14, 41, 58, 105, 122, 158, 255], [4, 31, 59, 92, 114, 145, 255],
[29, 49, 59, 85, 136, 141, 161], [14, 57, 59, 73, 110, 149, 162],
[5, 23, 60, 93, 121, 150, 255], [11, 49, 60, 117, 118, 143, 255],
[6, 32, 61, 94, 95, 142, 255], [15, 38, 61, 111, 133, 157, 255],
[1, 26, 40, 60, 61, 114, 132], [7, 24, 62, 82, 92, 95, 147],
[19, 35, 62, 93, 135, 160, 255], [8, 53, 62, 130, 146, 154, 255],
[5, 31, 63, 96, 125, 137, 255], [12, 50, 63, 113, 117, 156, 255],
[44, 54, 63, 110, 129, 160, 172],
];
#[inline]
fn fabs(x: f32) -> f32 {
f32::from_bits(x.to_bits() & 0x7FFF_FFFF)
}
#[must_use]
pub fn llrs_from_energies(energies: &[[f32; 8]; 58]) -> [f32; CODEWORD_BITS] {
let mut inv = [0u8; 8];
for (bits, &tone) in GRAY_MAP.iter().enumerate() {
inv[usize::from(tone)] = bits as u8;
}
let mut mean = 0.0f32;
for e in energies {
for &v in e {
mean += v;
}
}
mean = (mean / (58.0 * 8.0)).max(f32::MIN_POSITIVE);
let mut llr = [0.0f32; CODEWORD_BITS];
for (j, e) in energies.iter().enumerate() {
for b in 0..3 {
let mut max0 = f32::MIN;
let mut max1 = f32::MIN;
for (tone, &v) in e.iter().enumerate() {
if (inv[tone] >> (2 - b)) & 1 == 0 {
if v > max0 {
max0 = v;
}
} else if v > max1 {
max1 = v;
}
}
llr[3 * j + b] = (max0 - max1) / mean;
}
}
llr
}
pub fn ldpc_decode(llr: &[f32; CODEWORD_BITS]) -> Result<[u8; CODEWORD_LEN], Ft8Error> {
if llr.iter().any(|value| !value.is_finite()) {
return Err(Ft8Error::LlrNotFinite);
}
let mut c2v = [[0.0f32; 7]; PARITY_BITS];
let mut posterior = [0.0f32; CODEWORD_BITS];
for iter in 0..LDPC_MAX_ITERS {
posterior.copy_from_slice(llr);
for (row, msgs) in CHECK_ROWS.iter().zip(c2v.iter()) {
for (&v, &m) in row.iter().zip(msgs.iter()) {
if v != 255 {
posterior[usize::from(v)] += m;
}
}
}
if checks_satisfied(&posterior) {
let _ = iter;
return Ok(pack_hard_decision(&posterior));
}
for (row, msgs) in CHECK_ROWS.iter().zip(c2v.iter_mut()) {
let mut v2c = [0.0f32; 7];
let mut sign_all = 1.0f32;
let (mut min1, mut min2) = (f32::MAX, f32::MAX);
let mut min_at = 0usize;
for (e, &v) in row.iter().enumerate() {
if v == 255 {
continue;
}
let m = posterior[usize::from(v)] - msgs[e];
v2c[e] = m;
if m < 0.0 {
sign_all = -sign_all;
}
let a = fabs(m);
if a < min1 {
min2 = min1;
min1 = a;
min_at = e;
} else if a < min2 {
min2 = a;
}
}
for (e, &v) in row.iter().enumerate() {
if v == 255 {
continue;
}
let mag = if e == min_at { min2 } else { min1 };
let sign = if v2c[e] < 0.0 { -sign_all } else { sign_all };
msgs[e] = MIN_SUM_ALPHA * sign * mag;
}
}
}
Err(Ft8Error::LdpcNotConverged)
}
fn checks_satisfied(posterior: &[f32; CODEWORD_BITS]) -> bool {
CHECK_ROWS.iter().all(|row| {
let mut parity = 0u8;
for &v in row {
if v != 255 && posterior[usize::from(v)] < 0.0 {
parity ^= 1;
}
}
parity == 0
})
}
fn pack_hard_decision(posterior: &[f32; CODEWORD_BITS]) -> [u8; CODEWORD_LEN] {
let mut out = [0u8; CODEWORD_LEN];
for (pos, &p) in posterior.iter().enumerate() {
if p < 0.0 {
out[pos / 8] |= 1 << (7 - pos % 8);
}
}
out
}
#[must_use]
pub fn message_from_codeword(codeword: &[u8; CODEWORD_LEN]) -> [u8; MESSAGE_LEN] {
let mut out = [0u8; MESSAGE_LEN];
out.copy_from_slice(&codeword[..MESSAGE_LEN]);
out[MESSAGE_LEN - 1] &= 0xE0;
out
}
pub fn verify_crc(message: &[u8; MESSAGE_LEN]) -> Result<[u8; PAYLOAD_LEN], Ft8Error> {
let mut payload = [0u8; PAYLOAD_LEN];
payload.copy_from_slice(&message[..PAYLOAD_LEN]);
payload[PAYLOAD_LEN - 1] &= 0xF8;
let mut crc: u16 = 0;
for pos in PAYLOAD_BITS..MESSAGE_BITS {
crc = (crc << 1) | u16::from((message[pos / 8] >> (7 - pos % 8)) & 1);
}
if crc14(&payload) == crc {
Ok(payload)
} else {
Err(Ft8Error::CrcMismatch)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ft8Text {
buf: [u8; Self::CAPACITY],
len: usize,
}
impl Ft8Text {
pub const CAPACITY: usize = 24;
fn new() -> Self {
Self {
buf: [0; Self::CAPACITY],
len: 0,
}
}
fn push_bytes(&mut self, s: &[u8]) {
for &b in s {
if self.len < Self::CAPACITY {
self.buf[self.len] = b;
self.len += 1;
}
}
}
#[must_use]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
}
}
impl fmt::Display for Ft8Text {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub fn unpack_message(payload: &[u8; PAYLOAD_LEN]) -> Result<Ft8Text, Ft8Error> {
let i3 = (payload[9] >> 3) & 0x7;
match i3 {
0 => {
let chars = unpack_free_text(payload)?;
let start = chars.iter().position(|&c| c != b' ').unwrap_or(chars.len());
let end = chars
.iter()
.rposition(|&c| c != b' ')
.map_or(start, |p| p + 1);
let mut text = Ft8Text::new();
text.push_bytes(&chars[start..end]);
Ok(text)
}
1 => unpack_standard(payload),
_ => Err(Ft8Error::UnsupportedMessageType),
}
}
fn read_bits(payload: &[u8; PAYLOAD_LEN], pos: usize, count: usize) -> u32 {
let mut v = 0u32;
for i in pos..pos + count {
v = (v << 1) | u32::from((payload[i / 8] >> (7 - i % 8)) & 1);
}
v
}
fn unpack_standard(payload: &[u8; PAYLOAD_LEN]) -> Result<Ft8Text, Ft8Error> {
let c28a = read_bits(payload, 0, 28);
let r1a = read_bits(payload, 28, 1);
let c28b = read_bits(payload, 29, 28);
let r1b = read_bits(payload, 57, 1);
let r = read_bits(payload, 58, 1) == 1;
let g15 = read_bits(payload, 59, 15) as u16;
if r1a != 0 || r1b != 0 {
return Err(Ft8Error::UnsupportedMessageType);
}
let mut text = Ft8Text::new();
unpack_c28(c28a, &mut text)?;
text.push_bytes(b" ");
unpack_c28(c28b, &mut text)?;
match g15 {
v if v < MAXGRID4 => {
text.push_bytes(if r { b" R " } else { b" " });
let d2 = v % 10;
let d1 = (v / 10) % 10;
let f2 = (v / 100) % 18;
let f1 = v / 1800;
text.push_bytes(&[
b'A' + f1 as u8,
b'A' + f2 as u8,
b'0' + d1 as u8,
b'0' + d2 as u8,
]);
}
v if v == MAXGRID4 + 1 => {}
v if v == MAXGRID4 + 2 => text.push_bytes(b" RRR"),
v if v == MAXGRID4 + 3 => text.push_bytes(b" RR73"),
v if v == MAXGRID4 + 4 => text.push_bytes(b" 73"),
v if (MAXGRID4 + 5..=MAXGRID4 + 84).contains(&v) => {
let report = (v - MAXGRID4) as i16 - 35;
text.push_bytes(if r { b" R" } else { b" " });
text.push_bytes(if report < 0 { b"-" } else { b"+" });
let mag = report.unsigned_abs();
text.push_bytes(&[b'0' + (mag / 10) as u8, b'0' + (mag % 10) as u8]);
}
_ => return Err(Ft8Error::UnsupportedMessageType),
}
Ok(text)
}
fn unpack_c28(v: u32, text: &mut Ft8Text) -> Result<(), Ft8Error> {
match v {
0 => {
text.push_bytes(b"DE");
return Ok(());
}
1 => {
text.push_bytes(b"QRZ");
return Ok(());
}
2 => {
text.push_bytes(b"CQ");
return Ok(());
}
_ => {}
}
if v < NTOKENS {
return Err(Ft8Error::DirectedCqUnsupported);
}
if v < NTOKENS + MAX22 {
return Err(Ft8Error::UnsupportedMessageType);
}
let mut n = v - NTOKENS - MAX22;
let [s0, s1, s2, s3] = C28_SETS;
let c5 = s3[(n % 27) as usize];
n /= 27;
let c4 = s3[(n % 27) as usize];
n /= 27;
let c3 = s3[(n % 27) as usize];
n /= 27;
let c2 = s2[(n % 10) as usize];
n /= 10;
let c1 = s1[(n % 36) as usize];
n /= 36;
let c0 = s0[n as usize];
let aligned = [c0, c1, c2, c3, c4, c5];
let start = aligned.iter().position(|&c| c != b' ').unwrap_or(0);
let end = aligned
.iter()
.rposition(|&c| c != b' ')
.map_or(0, |p| p + 1);
text.push_bytes(&aligned[start..end.max(start)]);
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ft8Tail {
None,
Grid(MaidenheadGrid),
Report(i8),
Rrr,
Rr73,
Seventy3,
}
impl Ft8Tail {
pub const fn grid(text: &str) -> Result<Self, GeoError> {
match MaidenheadGrid::new(text) {
Ok(grid) => Ok(Self::Grid(grid)),
Err(e) => Err(e),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ft8Message {
payload: [u8; PAYLOAD_LEN],
}
impl Ft8Message {
pub fn standard(call_a: &str, call_b: &str, r: bool, tail: Ft8Tail) -> Result<Self, Ft8Error> {
let c28a = pack_c28(call_a)?;
let c28b = pack_c28(call_b)?;
if c28b < NTOKENS + MAX22 {
return Err(Ft8Error::TokenNotAllowedHere);
}
if r && !matches!(tail, Ft8Tail::Grid(_) | Ft8Tail::Report(_)) {
return Err(Ft8Error::AckFlagInvalid);
}
let g15 = pack_g15(tail)?;
let mut w = BitWriter::new();
w.push(u64::from(c28a), 28);
w.push(0, 1); w.push(u64::from(c28b), 28);
w.push(0, 1); w.push(u64::from(r), 1); w.push(u64::from(g15), 15);
w.push(1, 3); Ok(Self {
payload: w.finish(),
})
}
pub fn free_text(text: &str) -> Result<Self, Ft8Error> {
let len = text.chars().count();
if len > 13 {
return Err(Ft8Error::FreeTextLength { len });
}
let mut chars = [b' '; 13];
let offset = 13 - len;
for (index, ch) in text.chars().enumerate() {
let up = ch.to_ascii_uppercase();
if !up.is_ascii() || free_text_index(up).is_none() {
return Err(Ft8Error::FreeTextChar { ch: up, index });
}
chars[offset + index] = up as u8;
}
let mut value: u128 = 0;
for &c in &chars {
let idx = free_text_index(c as char).unwrap_or(0);
value = value * 42 + u128::from(idx);
}
let mut w = BitWriter::new();
w.push((value >> 64) as u64, 7);
w.push(value as u64, 64);
w.push(0, 3); w.push(0, 3); Ok(Self {
payload: w.finish(),
})
}
#[must_use]
pub fn payload(&self) -> [u8; PAYLOAD_LEN] {
self.payload
}
#[must_use]
pub fn channel_symbols(&self) -> [u8; SYMBOL_COUNT] {
let message = add_crc(&self.payload);
let codeword = ldpc_encode(&message);
symbols_from_codeword(&codeword)
}
}
struct BitWriter {
bytes: [u8; PAYLOAD_LEN],
used: usize,
}
impl BitWriter {
fn new() -> Self {
Self {
bytes: [0; PAYLOAD_LEN],
used: 0,
}
}
fn push(&mut self, value: u64, count: usize) {
for i in (0..count).rev() {
let bit = (value >> i) & 1;
let pos = self.used;
self.bytes[pos / 8] |= (bit as u8) << (7 - pos % 8);
self.used += 1;
}
}
fn finish(self) -> [u8; PAYLOAD_LEN] {
debug_assert_eq!(self.used, PAYLOAD_BITS);
self.bytes
}
}
fn free_text_index(ch: char) -> Option<u8> {
FREE_TEXT_ALPHABET
.iter()
.position(|&a| a == ch as u8 && ch.is_ascii())
.map(|i| i as u8)
}
pub fn unpack_free_text(payload: &[u8; PAYLOAD_LEN]) -> Result<[u8; 13], Ft8Error> {
let i3 = (payload[9] >> 3) & 0x7;
let n3 = ((payload[8] & 1) << 2) | ((payload[9] >> 6) & 0x3);
if i3 != 0 || n3 != 0 {
return Err(Ft8Error::UnsupportedMessageType);
}
let mut value: u128 = 0;
for pos in 0..71 {
let bit = (payload[pos / 8] >> (7 - pos % 8)) & 1;
value = (value << 1) | u128::from(bit);
}
if value >= 42u128.pow(13) {
return Err(Ft8Error::UnsupportedMessageType);
}
let mut chars = [b' '; 13];
for i in (0..13).rev() {
chars[i] = FREE_TEXT_ALPHABET[(value % 42) as usize];
value /= 42;
}
Ok(chars)
}
pub fn pack_c28(call: &str) -> Result<u32, Ft8Error> {
let mut up = [0u8; 8];
let mut len = 0usize;
for ch in call.chars() {
let c = ch.to_ascii_uppercase();
if c == '/' {
return Err(Ft8Error::CallsignCompound);
}
if c == ' ' {
if len >= 2 && &up[..2] == b"CQ" {
return Err(Ft8Error::DirectedCqUnsupported);
}
return Err(Ft8Error::CallsignChar { ch: c, index: len });
}
if !c.is_ascii() || len >= 7 {
return Err(Ft8Error::CallsignLength {
len: call.chars().count(),
});
}
up[len] = c as u8;
len += 1;
}
match &up[..len] {
b"DE" => return Ok(0),
b"QRZ" => return Ok(1),
b"CQ" => return Ok(2),
_ => {}
}
if len == 0 || len > 6 {
return Err(Ft8Error::CallsignLength { len });
}
let mut aligned = [b' '; 6];
if len >= 3 && up[2].is_ascii_digit() {
aligned[..len].copy_from_slice(&up[..len]);
} else if (2..=5).contains(&len) && up[1].is_ascii_digit() {
aligned[1..=len].copy_from_slice(&up[..len]);
} else {
return Err(Ft8Error::CallsignShape);
}
let i0 = c28_index(aligned[0], C28_SETS[0], 0)?;
let i1 = c28_index(aligned[1], C28_SETS[1], 1)?;
let i2 = c28_index(aligned[2], C28_SETS[2], 2)?;
let i3 = c28_index(aligned[3], C28_SETS[3], 3)?;
let i4 = c28_index(aligned[4], C28_SETS[3], 4)?;
let i5 = c28_index(aligned[5], C28_SETS[3], 5)?;
let n = ((((i0 * 36 + i1) * 10 + i2) * 27 + i3) * 27 + i4) * 27 + i5;
Ok(n + NTOKENS + MAX22)
}
fn c28_index(c: u8, set: &[u8], index: usize) -> Result<u32, Ft8Error> {
set.iter()
.position(|&s| s == c)
.map(|i| i as u32)
.ok_or(Ft8Error::CallsignChar {
ch: c as char,
index,
})
}
pub fn pack_g15(tail: Ft8Tail) -> Result<u16, Ft8Error> {
match tail {
Ft8Tail::None => Ok(MAXGRID4 + 1),
Ft8Tail::Rrr => Ok(MAXGRID4 + 2),
Ft8Tail::Rr73 => Ok(RR73_AS_GRID),
Ft8Tail::Seventy3 => Ok(MAXGRID4 + 4),
Ft8Tail::Report(r) => {
if !(-30..=49).contains(&r) {
return Err(Ft8Error::ReportOutOfRange { got: r });
}
Ok(MAXGRID4 + (i16::from(r) + 35) as u16)
}
Ft8Tail::Grid(grid) => {
let &[f1, f2, d1, d2] = grid.as_bytes() else {
return Err(Ft8Error::GridLength {
len: grid.precision().characters(),
});
};
let v = (u16::from(f1 - b'A') * 18 + u16::from(f2 - b'A')) * 10 + u16::from(d1 - b'0');
Ok(v * 10 + u16::from(d2 - b'0'))
}
}
}
const RR73_AS_GRID: u16 = 32_373;
#[must_use]
pub fn crc14(payload: &[u8; PAYLOAD_LEN]) -> u16 {
let mut reg: u16 = 0;
for pos in 0..82 {
let bit = if pos < PAYLOAD_BITS {
(payload[pos / 8] >> (7 - pos % 8)) & 1
} else {
0
};
let top = (reg >> 13) & 1;
reg = (reg << 1) & 0x3FFF;
if top ^ u16::from(bit) == 1 {
reg ^= CRC_POLY;
}
}
reg
}
#[must_use]
pub fn add_crc(payload: &[u8; PAYLOAD_LEN]) -> [u8; MESSAGE_LEN] {
let crc = crc14(payload);
let mut out = [0u8; MESSAGE_LEN];
out[..PAYLOAD_LEN].copy_from_slice(payload);
for i in 0..14 {
let bit = (crc >> (13 - i)) & 1;
let pos = PAYLOAD_BITS + i;
out[pos / 8] |= (bit as u8) << (7 - pos % 8);
}
out
}
#[must_use]
pub fn ldpc_encode(message: &[u8; MESSAGE_LEN]) -> [u8; CODEWORD_LEN] {
let mut out = [0u8; CODEWORD_LEN];
out[..MESSAGE_LEN].copy_from_slice(message);
out[MESSAGE_LEN - 1] &= 0xE0; for (i, row) in GENERATOR_ROWS.iter().enumerate() {
let mut parity = 0u8;
for (a, b) in row.iter().zip(message.iter()) {
parity ^= a & b;
}
let bit = (parity.count_ones() & 1) as u8;
let pos = MESSAGE_BITS + i;
out[pos / 8] |= bit << (7 - pos % 8);
}
out
}
#[must_use]
pub fn ldpc_check(codeword: &[u8; CODEWORD_LEN]) -> u32 {
let mut failed = 0u32;
for (i, row) in GENERATOR_ROWS.iter().enumerate() {
let mut parity = 0u8;
for (a, b) in row.iter().zip(codeword.iter()) {
parity ^= a & b;
}
let mut sum = parity.count_ones() & 1;
let pos = MESSAGE_BITS + i;
sum ^= u32::from((codeword[pos / 8] >> (7 - pos % 8)) & 1);
failed += sum;
}
failed
}
#[must_use]
pub fn symbols_from_codeword(codeword: &[u8; CODEWORD_LEN]) -> [u8; SYMBOL_COUNT] {
let mut symbols = [0u8; SYMBOL_COUNT];
symbols[0..7].copy_from_slice(&COSTAS);
symbols[36..43].copy_from_slice(&COSTAS);
symbols[72..79].copy_from_slice(&COSTAS);
for j in 0..58 {
let mut bits = 0u8;
for b in 0..3 {
let pos = 3 * j + b;
bits = (bits << 1) | ((codeword[pos / 8] >> (7 - pos % 8)) & 1);
}
let tone = GRAY_MAP[usize::from(bits)];
let position = if j < 29 { 7 + j } else { 43 + (j - 29) };
symbols[position] = tone;
}
symbols
}
#[must_use]
pub fn gfsk_pulse(t: f64) -> f64 {
let c = GFSK_K * GFSK_BT;
0.5 * (erf(c * (t + 0.5)) - erf(c * (t - 0.5)))
}
fn exp_neg(x: f64) -> f64 {
debug_assert!(x <= 0.0);
if x < -700.0 {
return 0.0;
}
const LN2: f64 = core::f64::consts::LN_2;
let k = -(((-x) / LN2 + 0.5) as i64);
let r = x - (k as f64) * LN2;
let mut term = 1.0;
let mut sum = 1.0;
for n in 1..=13 {
term *= r / f64::from(n);
sum += term;
}
let two_k = f64::from_bits(((k + 1023) as u64) << 52);
sum * two_k
}
fn erf(x: f64) -> f64 {
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = if x < 0.0 { -x } else { x };
let t = 1.0 / (1.0 + 0.327_591_1 * x);
let poly = t
* (0.254_829_592
+ t * (-0.284_496_736
+ t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
sign * (1.0 - poly * exp_neg(-x * x))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ft8Config {
base_hz: u32,
sample_rate: SampleRate,
}
impl Ft8Config {
pub const fn new(base_hz: u32, sample_rate: SampleRate) -> Result<Self, Ft8Error> {
let sr = sample_rate.hz();
if !sr.is_multiple_of(25) {
return Err(Ft8Error::SampleRateInexact { got: sr });
}
if base_hz == 0 || (base_hz as u64) * 4 + 175 >= (sr as u64) * 2 {
return Err(Ft8Error::ToneOutOfRange {
base_hz,
sample_rate: sr,
});
}
Ok(Self {
base_hz,
sample_rate,
})
}
#[must_use]
pub const fn base_hz(self) -> u32 {
self.base_hz
}
#[must_use]
pub const fn sample_rate(self) -> SampleRate {
self.sample_rate
}
#[must_use]
pub const fn samples_per_symbol(self) -> u32 {
self.sample_rate.hz() / 25 * 4
}
}
#[derive(Debug, Clone)]
pub struct Ft8Modulator {
phase: u32,
symbols: [u8; SYMBOL_COUNT],
symbol_idx: usize,
emitted_in_symbol: u32,
samples_per_symbol: u32,
base_hz: f64,
phase_per_hz: f64,
}
impl Ft8Modulator {
#[must_use]
pub fn new(config: Ft8Config, symbols: [u8; SYMBOL_COUNT]) -> Self {
Self {
phase: 0,
symbols,
symbol_idx: 0,
emitted_in_symbol: 0,
samples_per_symbol: config.samples_per_symbol(),
base_hz: f64::from(config.base_hz()),
phase_per_hz: 4_294_967_296.0 / f64::from(config.sample_rate().hz()),
}
}
#[must_use]
pub fn for_message(config: Ft8Config, message: &Ft8Message) -> Self {
Self::new(config, message.channel_symbols())
}
#[must_use]
pub fn total_samples(&self) -> u64 {
SYMBOL_COUNT as u64 * u64::from(self.samples_per_symbol)
}
fn current_hz(&self) -> f64 {
let i = self.symbol_idx;
let cur = f64::from(self.symbols[i]);
let prev = if i == 0 {
cur
} else {
f64::from(self.symbols[i - 1])
};
let next = if i + 1 == SYMBOL_COUNT {
cur
} else {
f64::from(self.symbols[i + 1])
};
let t =
(f64::from(self.emitted_in_symbol) + 0.5) / f64::from(self.samples_per_symbol) - 0.5;
let blend = prev * gfsk_pulse(t + 1.0) + cur * gfsk_pulse(t) + next * gfsk_pulse(t - 1.0);
self.base_hz + 6.25 * blend
}
fn advance(&mut self) {
let inc = (self.current_hz() * self.phase_per_hz + 0.5) as u32;
self.phase = self.phase.wrapping_add(inc);
self.emitted_in_symbol += 1;
if self.emitted_in_symbol == self.samples_per_symbol {
self.emitted_in_symbol = 0;
self.symbol_idx += 1;
}
}
pub fn next_i16(&mut self) -> Option<i16> {
if self.symbol_idx >= SYMBOL_COUNT {
return None;
}
let sample = sine_at(self.phase);
self.advance();
Some(sample)
}
pub fn next_f32(&mut self) -> Option<f32> {
if self.symbol_idx >= SYMBOL_COUNT {
return None;
}
let sample = sine_at_f32(self.phase);
self.advance();
Some(sample)
}
pub fn fill_i16(&mut self, buf: &mut [i16]) -> usize {
let mut written = 0;
for slot in buf.iter_mut() {
match self.next_i16() {
Some(s) => {
*slot = s;
written += 1;
}
None => break,
}
}
written
}
pub fn fill_f32(&mut self, buf: &mut [f32]) -> usize {
let mut written = 0;
for slot in buf.iter_mut() {
match self.next_f32() {
Some(s) => {
*slot = s;
written += 1;
}
None => break,
}
}
written
}
}
impl Iterator for Ft8Modulator {
type Item = i16;
fn next(&mut self) -> Option<i16> {
self.next_i16()
}
}
fn sine_at_f32(phase: u32) -> f32 {
use crate::types::{SINE_I16, TABLE_BITS, TABLE_MASK};
let idx = (phase >> (32 - TABLE_BITS)) as usize & TABLE_MASK;
let frac_bits = phase & ((1 << (32 - TABLE_BITS)) - 1);
let frac = frac_bits as f32 / (1u32 << (32 - TABLE_BITS)) as f32;
let a = SINE_I16.get(idx).copied().unwrap_or(0) as f32;
let b = SINE_I16.get((idx + 1) & TABLE_MASK).copied().unwrap_or(0) as f32;
(a + (b - a) * frac) / 32_767.0
}