use yodel::SampleRate;
use yodel::aprs::{AprsPacket, Latitude, Longitude, Position, Symbol};
use yodel::ax25::Address;
use yodel::geo::{Ambiguity, UNITS_PER_HUNDREDTH_MINUTE};
use yodel::tnc::{DefaultTncReceiver, SpaceGainSweep, TncConfig, TncReceiver, TncTransmitter};
const SR: u32 = 48_000;
fn config() -> TncConfig {
TncConfig::bell_202(SampleRate::new(SR).unwrap()).unwrap()
}
fn packet() -> AprsPacket<'static> {
AprsPacket::Position(Position {
ambiguity: Ambiguity::EXACT,
latitude: Latitude::new((34 * 6000 + 1234) * UNITS_PER_HUNDREDTH_MINUTE).unwrap(),
longitude: Longitude::new(-(118 * 6000 + 4321) * UNITS_PER_HUNDREDTH_MINUTE).unwrap(),
symbol: Symbol::CAR,
messaging: false,
compressed: false,
extension: None,
comment: b"slicer bank test",
})
}
fn decode_count_tilted(mark_num: i32, space_num: i32) -> u32 {
let cfg = config();
let tx = TncTransmitter::new(cfg);
let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
let dest = Address::new(b"APRS", 0).unwrap();
let src = Address::new(b"N0CALL", 7).unwrap();
let mut info_buf = [0u8; 330];
let mut frame_buf = [0u8; 330];
let samples = tx
.transmit_i16(&packet(), dest, src, &[], &mut info_buf, &mut frame_buf)
.unwrap();
let (g0, g1) = solve_two_tap(mark_num as f64 / 32.0, space_num as f64 / 32.0);
let mut prev = 0.0f64;
let mut frames = 0u32;
for s in samples {
let x = s as f64;
let y = g0 * x + g1 * prev;
prev = x;
let tilted = y.clamp(-32768.0, 32767.0) as i16;
if rx.push_i16(tilted).is_some() {
frames += 1;
}
}
for _ in 0..(SR / 100) {
if rx.push_i16(0).is_some() {
frames += 1;
}
}
frames
}
fn solve_two_tap(a_mark: f64, a_space: f64) -> (f64, f64) {
let wm = 2.0 * core::f64::consts::PI * 1200.0 / SR as f64;
let ws = 2.0 * core::f64::consts::PI * 2200.0 / SR as f64;
let target = (a_space / a_mark).powi(2);
let ratio_at = |r: f64| {
let num = 1.0 + r * r + 2.0 * r * ws.cos();
let den = 1.0 + r * r + 2.0 * r * wm.cos();
num / den
};
let (mut lo, mut hi) = (-0.999f64, 0.999f64);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if ratio_at(mid) > target {
lo = mid;
} else {
hi = mid;
}
}
let r = 0.5 * (lo + hi);
let g0 = a_mark / (1.0 + r * r + 2.0 * r * wm.cos()).sqrt();
(g0, g0 * r)
}
#[test]
fn flat_channel_decodes_once() {
assert_eq!(decode_count_tilted(32, 32), 1);
}
#[test]
fn space_tone_attenuated_5db_decodes() {
assert_eq!(decode_count_tilted(32, 18), 1);
}
#[test]
fn mark_tone_attenuated_5db_decodes() {
assert_eq!(decode_count_tilted(18, 32), 1);
}
#[test]
fn duplicate_chains_emit_once() {
let sweep = SpaceGainSweep::new(&[256, 256]).unwrap();
let cfg = config().with_space_gain_sweep(sweep);
let tx = TncTransmitter::new(cfg);
let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
let dest = Address::new(b"APRS", 0).unwrap();
let src = Address::new(b"N0CALL", 7).unwrap();
let mut info_buf = [0u8; 330];
let mut frame_buf = [0u8; 330];
let samples = tx
.transmit_i16(&packet(), dest, src, &[], &mut info_buf, &mut frame_buf)
.unwrap();
let mut frames = 0u32;
for s in samples {
if rx.push_i16(s).is_some() {
frames += 1;
}
}
assert_eq!(frames, 1, "duplicate chain emissions not merged");
assert_eq!(rx.stats().frames_ok, 1);
}
#[test]
fn sweep_validation() {
assert!(SpaceGainSweep::new(&[]).is_err());
assert!(SpaceGainSweep::new(&[256; 10]).is_err());
assert!(SpaceGainSweep::new(&[256, 0, 128]).is_err());
let ok = SpaceGainSweep::new(&[128, 256, 512]).unwrap();
assert_eq!(ok.gains(), &[128, 256, 512]);
assert_eq!(ok.len(), 3);
assert!(!ok.is_empty());
}