use num_complex::Complex32;
use crate::error::AcarsError;
use crate::frame::{AcarsMessage, FrameParser};
use crate::msk::{IF_RATE_HZ, MskDemod};
struct Channel {
oscillator: Vec<Complex32>,
osc_idx: usize,
accum: Complex32,
decim_count: u32,
decim_factor: u32,
if_buffer: Vec<f32>,
msk: MskDemod,
parser: FrameParser,
assembler: crate::reassembly::MessageAssembler,
}
pub const NO_SIGNAL_FLOOR_DB: f32 = -120.0;
#[derive(Clone, Copy, Debug)]
pub struct ChannelStats {
pub freq_hz: f64,
pub last_msg_at: Option<std::time::SystemTime>,
pub msg_count: u32,
pub level_db: f32,
pub lock_state: ChannelLockState,
}
impl Default for ChannelStats {
fn default() -> Self {
Self {
freq_hz: 0.0,
last_msg_at: None,
msg_count: 0,
level_db: NO_SIGNAL_FLOOR_DB,
lock_state: ChannelLockState::Idle,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ChannelLockState {
#[default]
Idle,
Signal,
Locked,
}
pub struct ChannelBank {
channels: Vec<Channel>,
stats: Vec<ChannelStats>,
}
impl ChannelBank {
pub fn new(source_rate_hz: f64, center_hz: f64, channels: &[f64]) -> Result<Self, AcarsError> {
if channels.is_empty() {
return Err(AcarsError::InvalidChannelConfig(
"channel list is empty".into(),
));
}
let if_rate = f64::from(IF_RATE_HZ);
if !source_rate_hz.is_finite() || source_rate_hz <= 0.0 {
return Err(AcarsError::InvalidChannelConfig(format!(
"source rate {source_rate_hz} Hz must be finite and positive"
)));
}
let decim_f = source_rate_hz / if_rate;
if decim_f.fract().abs() > 1e-6 {
return Err(AcarsError::NonIntegerDecimation {
source_rate_hz,
if_rate_hz: if_rate,
});
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let decim_factor = decim_f.round() as u32;
let mut built = Vec::with_capacity(channels.len());
let mut stats = Vec::with_capacity(channels.len());
for (idx, &freq_hz) in channels.iter().enumerate() {
let offset_hz = freq_hz - center_hz;
if offset_hz.abs() > source_rate_hz / 2.0 {
return Err(AcarsError::InvalidChannelConfig(format!(
"channel {freq_hz} Hz outside source bandwidth ({source_rate_hz} Hz centered on {center_hz} Hz)"
)));
}
let mut oscillator = Vec::with_capacity(decim_factor as usize);
for n in 0..decim_factor {
let phase =
-2.0 * core::f64::consts::PI * offset_hz * f64::from(n) / source_rate_hz;
#[allow(clippy::cast_possible_truncation)]
oscillator.push(Complex32::new(phase.cos() as f32, phase.sin() as f32));
}
#[allow(clippy::cast_possible_truncation)]
let idx_u8 = idx as u8;
built.push(Channel {
oscillator,
osc_idx: 0,
accum: Complex32::new(0.0, 0.0),
decim_count: 0,
decim_factor,
if_buffer: Vec::with_capacity(4096),
msk: MskDemod::new(),
parser: FrameParser::new(idx_u8, freq_hz),
assembler: crate::reassembly::MessageAssembler::new(),
});
stats.push(ChannelStats {
freq_hz,
last_msg_at: None,
msg_count: 0,
level_db: NO_SIGNAL_FLOOR_DB,
lock_state: ChannelLockState::Idle,
});
}
Ok(Self {
channels: built,
stats,
})
}
pub fn process<F: FnMut(AcarsMessage)>(&mut self, iq: &[Complex32], mut on_message: F) {
for (idx, ch) in self.channels.iter_mut().enumerate() {
ch.if_buffer.clear();
for &sample in iq {
let osc = ch.oscillator[ch.osc_idx];
ch.osc_idx = (ch.osc_idx + 1) % ch.oscillator.len();
ch.accum += sample * osc;
ch.decim_count += 1;
if ch.decim_count >= ch.decim_factor {
let am_sample = ch.accum.norm();
ch.if_buffer.push(am_sample);
ch.accum = Complex32::new(0.0, 0.0);
ch.decim_count = 0;
}
}
ch.msk.process(&ch.if_buffer, &mut ch.parser);
let stats = &mut self.stats[idx];
ch.parser.drain(|msg| {
let now = msg.timestamp;
for mut emitted in ch.assembler.observe(msg, now) {
stats.msg_count = stats.msg_count.saturating_add(1);
stats.last_msg_at = Some(emitted.timestamp);
stats.level_db = emitted.level_db;
stats.lock_state = ChannelLockState::Locked;
emitted.parsed =
crate::label_parsers::decode_label(emitted.label, &emitted.text);
on_message(emitted);
}
});
for mut emitted in ch.assembler.drain_timeouts(std::time::SystemTime::now()) {
stats.msg_count = stats.msg_count.saturating_add(1);
stats.last_msg_at = Some(emitted.timestamp);
stats.level_db = emitted.level_db;
stats.lock_state = ChannelLockState::Locked;
emitted.parsed = crate::label_parsers::decode_label(emitted.label, &emitted.text);
on_message(emitted);
}
if ch.parser.take_polarity_flip() {
ch.msk.toggle_polarity();
}
}
let _ = &self.stats;
}
#[must_use]
pub fn channels(&self) -> &[ChannelStats] {
&self.stats
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn rejects_empty_channel_list() {
match ChannelBank::new(2_400_000.0, 130_450_000.0, &[]) {
Err(AcarsError::InvalidChannelConfig(_)) => {}
Err(other) => panic!("expected InvalidChannelConfig, got {other:?}"),
Ok(_) => panic!("expected InvalidChannelConfig, got Ok"),
}
}
#[test]
fn rejects_zero_or_negative_source_rate() {
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
match ChannelBank::new(bad, 130_337_500.0, &[131_550_000.0]) {
Err(AcarsError::InvalidChannelConfig(_)) => {}
Err(other) => panic!("rate={bad}: expected InvalidChannelConfig, got {other:?}"),
Ok(_) => panic!("rate={bad}: expected error, got Ok"),
}
}
}
#[test]
fn rejects_non_integer_decimation() {
match ChannelBank::new(2_400_001.0, 130_450_000.0, &[131_550_000.0]) {
Err(AcarsError::NonIntegerDecimation { .. }) => {}
Err(other) => panic!("expected NonIntegerDecimation, got {other:?}"),
Ok(_) => panic!("expected NonIntegerDecimation, got Ok"),
}
}
#[test]
fn rejects_channel_outside_source_bandwidth() {
match ChannelBank::new(2_400_000.0, 130_450_000.0, &[200_000_000.0]) {
Err(AcarsError::InvalidChannelConfig(_)) => {}
Err(other) => panic!("expected InvalidChannelConfig, got {other:?}"),
Ok(_) => panic!("expected InvalidChannelConfig, got Ok"),
}
}
#[test]
fn accepts_valid_us_six_config() {
let bank = match ChannelBank::new(
2_500_000.0,
130_337_500.0,
&[
129_125_000.0,
130_025_000.0,
130_425_000.0,
130_450_000.0,
131_525_000.0,
131_550_000.0,
],
) {
Ok(b) => b,
Err(e) => panic!("expected Ok, got {e:?}"),
};
assert_eq!(bank.channels().len(), 6);
assert!((bank.channels()[0].freq_hz - 129_125_000.0).abs() < f64::EPSILON);
}
#[test]
fn process_silent_iq_doesnt_decode() {
let mut bank = match ChannelBank::new(2_500_000.0, 130_450_000.0, &[131_550_000.0]) {
Ok(b) => b,
Err(e) => panic!("expected Ok, got {e:?}"),
};
let silent = vec![Complex32::new(0.0, 0.0); 2500];
bank.process(&silent, |_msg| {
panic!("silence shouldn't produce messages");
});
}
}