use crate::aprs::{AprsError, AprsPacket, Decoded};
#[cfg(feature = "micE")]
use crate::aprs::{MicE, MicEError, mic_e};
use crate::ax25::frame::MAX_DIGIPEATERS;
use crate::ax25::{Address, Ax25Error, HdlcDeframer, PathHop, RecoveryPolicy, UiFrame, hdlc};
#[cfg(feature = "g3ruh")]
use crate::baseband::BasebandFilter;
use crate::discriminator::QuadratureCorrelator;
use crate::error::ConfigError;
use crate::nrzi::NrziDecoder;
#[cfg(feature = "g3ruh")]
use crate::scrambler::Descrambler;
use crate::slicer::Slicer;
use crate::types::Bit;
#[cfg(any(feature = "g3ruh", test))]
use crate::types::ModulationScheme;
use crate::types::TonePair;
mod config;
mod tx;
pub use config::*;
pub use tx::*;
use config::MAX_CHAINS;
pub const MAX_FRAME_BYTES: usize = 330;
pub type DefaultTncReceiver = TncReceiver<MAX_FRAME_BYTES>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RxFrame<'a> {
frame: UiFrame<'a>,
}
impl<'a> RxFrame<'a> {
#[must_use]
pub const fn dest(&self) -> Address {
self.frame.dest
}
#[must_use]
pub const fn src(&self) -> Address {
self.frame.src
}
#[must_use]
pub fn path(&self) -> &[Address] {
self.frame.path()
}
#[must_use]
pub const fn info(&self) -> &'a [u8] {
self.frame.info
}
#[must_use]
pub const fn ui_frame(&self) -> &UiFrame<'a> {
&self.frame
}
pub fn aprs(&self) -> Result<AprsPacket<'a>, AprsError> {
AprsPacket::parse(self.frame.info)
}
#[cfg(feature = "micE")]
pub fn mic_e(&self) -> Result<MicE<'a>, MicEError> {
mic_e::decode_address(self.frame.dest, self.frame.info)
}
#[must_use]
pub fn decoded(&self) -> Decoded<'a> {
Decoded::decode_frame(self.dest(), self.info())
}
}
#[derive(Debug, Clone)]
pub struct OwnedFrame {
dest: Address,
src: Address,
hops: [PathHop; MAX_DIGIPEATERS],
hop_count: usize,
info: [u8; MAX_FRAME_BYTES],
info_len: usize,
}
impl OwnedFrame {
pub fn new(frame: &RxFrame<'_>) -> Result<Self, Ax25Error> {
let ui = frame.ui_frame();
if ui.info.len() > MAX_FRAME_BYTES {
return Err(Ax25Error::FrameTooLarge {
len: ui.info.len(),
max: MAX_FRAME_BYTES,
});
}
let mut hops = [PathHop::unused(ui.src); MAX_DIGIPEATERS];
let mut hop_count = 0;
for (slot, hop) in hops.iter_mut().zip(ui.hops()) {
*slot = hop;
hop_count += 1;
}
let mut info = [0u8; MAX_FRAME_BYTES];
for (dst, &b) in info.iter_mut().zip(ui.info.iter()) {
*dst = b;
}
Ok(Self {
dest: ui.dest,
src: ui.src,
hops,
hop_count,
info,
info_len: ui.info.len(),
})
}
#[must_use]
pub const fn dest(&self) -> Address {
self.dest
}
#[must_use]
pub const fn src(&self) -> Address {
self.src
}
#[must_use]
pub fn hops(&self) -> &[PathHop] {
self.hops.get(..self.hop_count).unwrap_or(&[])
}
#[must_use]
pub fn info(&self) -> &[u8] {
self.info.get(..self.info_len).unwrap_or(&[])
}
pub fn ui_frame(&self) -> Result<UiFrame<'_>, Ax25Error> {
UiFrame::with_hops(self.dest, self.src, self.hops(), self.info())
}
pub fn aprs(&self) -> Result<AprsPacket<'_>, AprsError> {
AprsPacket::parse(self.info())
}
#[cfg(feature = "micE")]
pub fn mic_e(&self) -> Result<MicE<'_>, MicEError> {
mic_e::decode_address(self.dest, self.info())
}
#[must_use]
pub fn decoded(&self) -> Decoded<'_> {
Decoded::decode_frame(self.dest(), self.info())
}
}
impl TryFrom<&RxFrame<'_>> for OwnedFrame {
type Error = Ax25Error;
fn try_from(frame: &RxFrame<'_>) -> Result<Self, Ax25Error> {
Self::new(frame)
}
}
impl PartialEq for OwnedFrame {
fn eq(&self, other: &Self) -> bool {
self.dest == other.dest
&& self.src == other.src
&& self.hops() == other.hops()
&& self.info() == other.info()
}
}
impl Eq for OwnedFrame {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TncStats {
pub frames_ok: u32,
pub fcs_errors: u32,
pub oversize: u32,
pub malformed: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ChainInput {
Raw,
BandPassed,
Emphasized,
}
#[derive(Debug, Clone)]
struct Chain<const N: usize> {
gain: i64,
input: ChainInput,
slicer: Slicer,
nrzi: NrziDecoder,
deframer: HdlcDeframer<N>,
hist: BitHistory,
}
const HIST_BITS: usize = hdlc::RAW_BITS + 16;
const HIST_BYTES: usize = HIST_BITS / 8;
const VOTE_SLIDE: usize = 8;
const VOTE_AGREE_NUM: usize = 8;
#[derive(Debug, Clone)]
struct BitHistory {
bits: [u8; HIST_BYTES],
count: u64,
}
impl BitHistory {
const fn new() -> Self {
Self {
bits: [0; HIST_BYTES],
count: 0,
}
}
const fn push(&mut self, bit: Bit) {
let idx = (self.count % HIST_BITS as u64) as usize;
let mask = 1u8 << (idx % 8);
match bit {
Bit::One => self.bits[idx / 8] |= mask,
Bit::Zero => self.bits[idx / 8] &= !mask,
}
self.count = self.count.wrapping_add(1);
}
fn get_back(&self, back: usize) -> Option<bool> {
if back as u64 >= self.count || back >= HIST_BITS {
return None;
}
let abs = self.count - 1 - back as u64;
let idx = (abs % HIST_BITS as u64) as usize;
Some((self.bits[idx / 8] >> (idx % 8)) & 1 != 0)
}
}
#[derive(Debug, Clone, Copy)]
struct BandPass {
hp_state: i32,
lp_state: i32,
hp_shift: u32,
lp_shift: u32,
}
impl BandPass {
fn shift_for(sample_rate: u32, cutoff_hz: u32) -> u32 {
let target = ((cutoff_hz as u64) * 710 / 113) as u32;
let mut best = 0;
let mut best_err = u32::MAX;
for s in 0..12u32 {
let fc = sample_rate >> s;
let err = fc.abs_diff(target);
if err < best_err {
best_err = err;
best = s;
}
}
best
}
fn new(sample_rate: u32, baud: u32, tones: TonePair) -> Self {
let low = if tones.mark_hz() < tones.space_hz() {
tones.mark_hz()
} else {
tones.space_hz()
};
let high = if tones.mark_hz() > tones.space_hz() {
tones.mark_hz()
} else {
tones.space_hz()
};
Self {
hp_state: 0,
lp_state: 0,
hp_shift: Self::shift_for(sample_rate, low * 3 / 4),
lp_shift: Self::shift_for(sample_rate, high + baud + 100),
}
}
fn push(&mut self, sample: i32) -> i32 {
let x = sample << 8;
self.hp_state += (x - self.hp_state) >> self.hp_shift;
let high_passed = x - self.hp_state;
self.lp_state += (high_passed - self.lp_state) >> self.lp_shift;
self.lp_state >> 8
}
}
#[derive(Debug, Clone, Copy)]
struct PreEmphasis {
prev: i32,
a_q8: i32,
}
impl PreEmphasis {
fn new(a_q8: i32) -> Self {
Self { prev: 0, a_q8 }
}
fn push(&mut self, sample: i32) -> i32 {
let y = sample - ((self.prev * self.a_q8) >> 8);
self.prev = sample;
y
}
}
#[derive(Debug, Clone, Copy, Default)]
struct SeenFrame {
crc: u16,
len: u16,
seen_at: u64,
valid: bool,
}
#[cfg(feature = "g3ruh")]
#[derive(Debug, Clone)]
struct BasebandRx<const N: usize> {
filter: BasebandFilter,
slicer: Slicer,
descrambler: Descrambler,
nrzi: NrziDecoder,
deframer: HdlcDeframer<N>,
}
#[derive(Debug, Clone)]
pub struct TncReceiver<const N: usize> {
discriminator: QuadratureCorrelator,
disc_filtered: QuadratureCorrelator,
disc_emphasized: QuadratureCorrelator,
band_pass: BandPass,
pre_emphasis: PreEmphasis,
band_pass_emph: BandPass,
chains: [Chain<N>; MAX_CHAINS],
needs_raw: bool,
needs_filtered: bool,
needs_emphasized: bool,
active: usize,
primary: usize,
seen: [SeenFrame; MAX_CHAINS],
seen_next: usize,
samples: u64,
window: u64,
out_buf: [u8; N],
voting: ChainVoting,
#[cfg(feature = "g3ruh")]
baseband: Option<BasebandRx<N>>,
stats: TncStats,
}
impl<const N: usize> TncReceiver<N> {
pub fn new(config: TncConfig) -> Result<Self, ConfigError> {
let demod = config.demodulator;
let discriminator =
QuadratureCorrelator::new(demod.sample_rate(), demod.baud(), demod.tones())?;
let slicer = Slicer::new(demod.sample_rate(), demod.baud())?;
let sweep = config.sweep;
let gains = sweep.gains();
let extras: usize = if !config.band_pass.is_on() && sweep.len() == MAX_SWEEP {
2
} else {
0
};
let chains = core::array::from_fn(|i| {
let mut slicer = slicer.clone();
let stagger = (i as u32).wrapping_mul(u32::MAX / MAX_SWEEP as u32);
slicer.set_initial_phase(if i >= MAX_SWEEP {
stagger.wrapping_add(u32::MAX / (2 * MAX_SWEEP as u32))
} else {
stagger
});
if i >= sweep.len() {
return Chain {
gain: if i == sweep.len() { 215 } else { 345 },
input: ChainInput::Emphasized,
slicer,
nrzi: NrziDecoder::default(),
deframer: HdlcDeframer::with_recovery(config.recovery),
hist: BitHistory::new(),
};
}
let emphasized = !config.band_pass.is_on()
&& sweep.len() >= 3
&& i % 2 == 0
&& i + 5 >= sweep.len()
&& i > 0;
let input = if emphasized {
ChainInput::Emphasized
} else if config.band_pass.is_on() || i % 2 == 1 {
ChainInput::BandPassed
} else {
ChainInput::Raw
};
let gain = if emphasized {
match sweep.len() - i {
1 => 441,
3 => 256,
_ => 194,
}
} else {
gains.get(i).copied().unwrap_or(256) as i64
};
Chain {
gain,
input,
slicer,
nrzi: NrziDecoder::default(),
deframer: HdlcDeframer::with_recovery(config.recovery),
hist: BitHistory::new(),
}
});
let spb = (demod.sample_rate().hz() / demod.baud().bps()) as u64;
let active = (sweep.len() + extras).clamp(1, MAX_CHAINS);
let live = &chains[..active];
let needs_raw = live.iter().any(|c| matches!(c.input, ChainInput::Raw));
let needs_filtered = live
.iter()
.any(|c| matches!(c.input, ChainInput::BandPassed));
let needs_emphasized = live
.iter()
.any(|c| matches!(c.input, ChainInput::Emphasized));
Ok(Self {
disc_filtered: discriminator.clone(),
disc_emphasized: discriminator.clone(),
discriminator,
band_pass: BandPass::new(demod.sample_rate().hz(), demod.baud().bps(), demod.tones()),
pre_emphasis: PreEmphasis::new(256),
band_pass_emph: BandPass::new(
demod.sample_rate().hz(),
demod.baud().bps(),
demod.tones(),
),
chains,
needs_raw,
needs_filtered,
needs_emphasized,
active,
primary: sweep.primary_index(),
seen: [SeenFrame::default(); MAX_CHAINS],
seen_next: 0,
samples: 0,
window: spb.saturating_mul(32),
out_buf: [0; N],
voting: match config.recovery {
RecoveryPolicy::None => ChainVoting::Off,
RecoveryPolicy::SingleBitFlip | RecoveryPolicy::PreDestuffFlip => config.voting,
},
#[cfg(feature = "g3ruh")]
baseband: match config.scheme() {
ModulationScheme::ToneAfsk => None,
ModulationScheme::ScrambledBaseband => Some(BasebandRx {
filter: BasebandFilter::new(demod.sample_rate(), demod.baud()),
slicer: Slicer::new(demod.sample_rate(), demod.baud())?,
descrambler: Descrambler::default(),
nrzi: NrziDecoder::default(),
deframer: HdlcDeframer::with_recovery(config.recovery),
}),
},
stats: TncStats::default(),
})
}
#[must_use]
pub const fn stats(&self) -> TncStats {
self.stats
}
pub fn push_i16(&mut self, sample: i16) -> Option<RxFrame<'_>> {
self.push_sample(sample as i32)
}
pub fn push_f32(&mut self, sample: f32) -> Option<RxFrame<'_>> {
let scaled = (sample * 32_767.0).clamp(-32_768.0, 32_767.0) as i32;
self.push_sample(scaled)
}
fn push_sample(&mut self, sample: i32) -> Option<RxFrame<'_>> {
#[cfg(feature = "g3ruh")]
if self.baseband.is_some() {
return self.push_baseband(sample);
}
let raw = if self.needs_raw {
self.discriminator.push_envelopes(sample)
} else {
(0, 0)
};
let filtered = if self.needs_filtered {
let filtered_sample = self.band_pass.push(sample);
self.disc_filtered.push_envelopes(filtered_sample)
} else {
(0, 0)
};
let emphasized = if self.needs_emphasized {
let emphasized_sample = self.band_pass_emph.push(self.pre_emphasis.push(sample));
self.disc_emphasized.push_envelopes(emphasized_sample)
} else {
(0, 0)
};
self.push_envelopes(raw, filtered, emphasized)
}
#[cfg(feature = "g3ruh")]
fn push_baseband(&mut self, sample: i32) -> Option<RxFrame<'_>> {
self.samples = self.samples.wrapping_add(1);
let rx = self.baseband.as_mut()?;
let metric = rx.filter.push(sample);
let line = rx.slicer.push(metric)?;
let data = rx.nrzi.decode(rx.descrambler.descramble(line));
let event = rx.deframer.push(data)?;
let len = match event {
Ok(frame) => {
let len = frame.len().min(N);
for (dst, src) in self.out_buf.iter_mut().zip(frame.iter()) {
*dst = *src;
}
len
}
Err(Ax25Error::FcsMismatch { .. }) => {
self.stats.fcs_errors = self.stats.fcs_errors.saturating_add(1);
return None;
}
Err(Ax25Error::FrameTooLarge { .. }) => {
self.stats.oversize = self.stats.oversize.saturating_add(1);
return None;
}
Err(_) => {
self.stats.malformed = self.stats.malformed.saturating_add(1);
return None;
}
};
match UiFrame::parse(self.out_buf.get(..len).unwrap_or(&[])) {
Ok(frame) => {
self.stats.frames_ok = self.stats.frames_ok.saturating_add(1);
Some(RxFrame { frame })
}
Err(_) => {
self.stats.malformed = self.stats.malformed.saturating_add(1);
None
}
}
}
fn push_envelopes(
&mut self,
raw: (i64, i64),
filtered: (i64, i64),
emphasized: (i64, i64),
) -> Option<RxFrame<'_>> {
self.samples = self.samples.wrapping_add(1);
let mut out_len: Option<usize> = None;
let mut fcs_failed: Option<usize> = None;
for i in 0..self.active {
let Some(chain) = self.chains.get_mut(i) else {
break;
};
let (mark, space) = match chain.input {
ChainInput::Raw => raw,
ChainInput::BandPassed => filtered,
ChainInput::Emphasized => emphasized,
};
let scaled = (space.saturating_mul(chain.gain)) >> 8;
let metric = (mark - scaled).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let Some(line) = chain.slicer.push(metric) else {
continue;
};
let data = chain.nrzi.decode(line);
chain.hist.push(data);
let Some(event) = chain.deframer.push(data) else {
continue;
};
match event {
Ok(frame) => {
let key = (crate::ax25::crc16_x25(frame), frame.len() as u16);
let fresh = Self::register(
&mut self.seen,
&mut self.seen_next,
self.samples,
self.window,
key,
);
if fresh && out_len.is_none() {
let len = frame.len().min(N);
for (dst, src) in self.out_buf.iter_mut().zip(frame.iter()) {
*dst = *src;
}
out_len = Some(len);
}
}
Err(Ax25Error::FcsMismatch { .. }) => {
if fcs_failed.is_none() {
fcs_failed = Some(i);
}
if i == self.primary {
self.stats.fcs_errors = self.stats.fcs_errors.saturating_add(1);
}
}
Err(Ax25Error::FrameTooLarge { .. }) => {
if i == self.primary {
self.stats.oversize = self.stats.oversize.saturating_add(1);
}
}
Err(_) => {
if i == self.primary {
self.stats.malformed = self.stats.malformed.saturating_add(1);
}
}
}
}
if out_len.is_none()
&& self.voting.is_on()
&& let Some(failed) = fcs_failed
{
out_len = self.try_vote(failed);
}
let len = out_len?;
match UiFrame::parse(self.out_buf.get(..len).unwrap_or(&[])) {
Ok(frame) => {
self.stats.frames_ok = self.stats.frames_ok.saturating_add(1);
Some(RxFrame { frame })
}
Err(_) => {
self.stats.malformed = self.stats.malformed.saturating_add(1);
None
}
}
}
fn try_vote(&mut self, failed: usize) -> Option<usize> {
let (window, total) = {
let chain = self.chains.get(failed)?;
let (w, t) = chain.deframer.failed_window()?;
(*w, t)
};
let mut votes = [0i16; hdlc::RAW_BITS];
for (i, vote) in votes.iter_mut().enumerate().take(total) {
let bit = (window[i / 8] >> (i % 8)) & 1 != 0;
*vote += if bit { 1 } else { -1 };
}
let mut voters = 1usize;
for c in 0..self.active {
if c == failed {
continue;
}
let Some(chain) = self.chains.get(c) else {
continue;
};
let mut best_slide = None;
let mut best_agree = 0usize;
for slide in 0..=VOTE_SLIDE {
let mut agree = 0usize;
let mut have = 0usize;
for i in 0..total {
let Some(h) = chain.hist.get_back(total - 1 - i + slide) else {
continue;
};
have += 1;
let w = (window[i / 8] >> (i % 8)) & 1 != 0;
if h == w {
agree += 1;
}
}
if have == total && agree > best_agree {
best_agree = agree;
best_slide = Some(slide);
}
}
let Some(slide) = best_slide else {
continue;
};
if best_agree * 10 < total * VOTE_AGREE_NUM {
continue;
}
for (i, vote) in votes.iter_mut().enumerate().take(total) {
if let Some(h) = chain.hist.get_back(total - 1 - i + slide) {
*vote += if h { 2 } else { -2 };
}
}
voters += 1;
}
if voters < 2 {
return None;
}
let mut voted = [0u8; hdlc::RAW_BYTES];
for (i, &v) in votes.iter().enumerate().take(total) {
let bit = if v > 0 {
true
} else if v < 0 {
false
} else {
(window[i / 8] >> (i % 8)) & 1 != 0
};
if bit {
voted[i / 8] |= 1 << (i % 8);
}
}
let content_len = {
let chain = self.chains.get_mut(failed)?;
chain.deframer.try_voted_window(&voted, total)?
};
let (key, len) = {
let chain = self.chains.get(failed)?;
let frame = chain.deframer.frame_bytes(content_len);
((crate::ax25::crc16_x25(frame), frame.len() as u16), {
let len = frame.len().min(N);
for (dst, src) in self.out_buf.iter_mut().zip(frame.iter()) {
*dst = *src;
}
len
})
};
let fresh = Self::register(
&mut self.seen,
&mut self.seen_next,
self.samples,
self.window,
key,
);
if fresh { Some(len) } else { None }
}
fn register(
seen: &mut [SeenFrame; MAX_CHAINS],
next: &mut usize,
now: u64,
window: u64,
(crc, len): (u16, u16),
) -> bool {
for entry in seen.iter_mut() {
if entry.valid
&& entry.crc == crc
&& entry.len == len
&& now.wrapping_sub(entry.seen_at) <= window
{
entry.seen_at = now;
return false;
}
}
let slot = *next % MAX_CHAINS;
if let Some(entry) = seen.get_mut(slot) {
*entry = SeenFrame {
crc,
len,
seen_at: now,
valid: true,
};
}
*next = (*next + 1) % MAX_CHAINS;
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ax25::Address;
use crate::types::DevicePreset;
fn addr(callsign: &[u8], ssid: u8) -> Address {
Address::new(callsign, ssid).unwrap()
}
#[test]
fn every_device_preset_resolves_to_a_valid_config() {
for &preset in DevicePreset::ALL {
let config = preset
.tnc_config()
.unwrap_or_else(|e| panic!("{preset:?} failed to resolve: {e}"));
assert_eq!(config.sample_rate().hz(), 48_000, "{preset:?}");
assert_eq!(
config.baud().bps(),
preset.profile().baud().bps(),
"{preset:?}"
);
assert_eq!(config.scheme(), preset.profile().scheme(), "{preset:?}");
let expected_sweep = if preset.full_chain_bank() {
SpaceGainSweep::DEFAULT
} else {
SpaceGainSweep::UNITY
};
assert_eq!(config.space_gain_sweep(), expected_sweep, "{preset:?}");
assert!(DefaultTncReceiver::new(config).is_ok(), "{preset:?}");
}
}
#[test]
fn device_preset_resolution_is_const() {
const CONFIG: TncConfig = match DevicePreset::Esp32C3.tnc_config() {
Ok(c) => c,
Err(_) => panic!("Esp32C3 must resolve"),
};
assert_eq!(CONFIG.baud().bps(), 1_200);
}
#[test]
fn every_device_preset_decodes_a_synthesized_frame() {
for &preset in DevicePreset::ALL {
let config = preset.tnc_config().unwrap();
let tx = TncTransmitter::new(config);
let mut rx = DefaultTncReceiver::new(config).unwrap();
let text = b"preset round trip";
let mut frame_buf = [0u8; MAX_FRAME_BYTES];
let len = tx
.build_frame_raw(
addr(b"APRS", 0),
addr(b"N0CALL", 7),
&[],
text,
&mut frame_buf,
)
.unwrap();
let mut decoded = 0;
for s in tx.frame_samples_i16(&frame_buf[..len]) {
if let Some(frame) = rx.push_i16(s) {
assert_eq!(frame.src().callsign.as_bytes(), b"N0CALL", "{preset:?}");
assert_eq!(frame.info(), text, "{preset:?}");
decoded += 1;
}
}
assert_eq!(decoded, 1, "{preset:?} decoded {decoded} frames");
}
}
#[cfg(feature = "g3ruh")]
#[test]
fn p4_g3ruh_preset_selects_baseband() {
let config = DevicePreset::Esp32P4G3ruh.tnc_config().unwrap();
assert_eq!(config.scheme(), ModulationScheme::ScrambledBaseband);
assert_eq!(config.baud().bps(), 9_600);
}
}