use core::fmt;
use crate::aprs::{AprsError, AprsUiError};
use crate::ax25::{Ax25Error, RecoveryPolicy, hdlc};
#[cfg(feature = "g3ruh")]
use crate::baseband::BasebandModulator;
use crate::demodulator::DemodulatorConfig;
use crate::error::ConfigError;
use crate::modulator::ModulatorConfig;
use crate::types::{BaudRate, DevicePreset, ModemProfile, ModulationScheme, SampleRate, TonePair};
pub const MAX_SWEEP: usize = 9;
pub(super) const MAX_CHAINS: usize = MAX_SWEEP + 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpaceGainSweep {
gains: [u16; MAX_SWEEP],
len: usize,
}
impl SpaceGainSweep {
pub const DEFAULT: Self = Self {
gains: [156, 202, 262, 340, 441, 572, 741, 961, 1246],
len: 9,
};
pub const UNITY: Self = Self {
gains: [256, 0, 0, 0, 0, 0, 0, 0, 0],
len: 1,
};
pub const fn new(gains: &[u16]) -> Result<Self, ConfigError> {
if gains.is_empty() || gains.len() > MAX_SWEEP {
return Err(ConfigError::SweepLenInvalid {
got: gains.len(),
max: MAX_SWEEP,
});
}
let mut packed = [0u16; MAX_SWEEP];
let mut i = 0;
while i < gains.len() {
if gains[i] == 0 {
return Err(ConfigError::SweepGainZero { index: i });
}
packed[i] = gains[i];
i += 1;
}
Ok(Self {
gains: packed,
len: gains.len(),
})
}
#[must_use]
pub fn gains(&self) -> &[u16] {
self.gains.get(..self.len).unwrap_or(&[])
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub(super) fn primary_index(&self) -> usize {
let mut best = 0;
let mut best_dist = u16::MAX;
for (i, &g) in self.gains().iter().enumerate() {
let dist = g.abs_diff(256);
if dist < best_dist {
best_dist = dist;
best = i;
}
}
best
}
}
impl Default for SpaceGainSweep {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TncError {
Aprs(AprsError),
Ax25(Ax25Error),
Config(ConfigError),
}
impl fmt::Display for TncError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
TncError::Aprs(ref e) => write!(f, "APRS layer: {e}"),
TncError::Ax25(ref e) => write!(f, "AX.25 layer: {e}"),
TncError::Config(ref e) => write!(f, "configuration: {e}"),
}
}
}
impl core::error::Error for TncError {}
impl From<AprsError> for TncError {
fn from(e: AprsError) -> Self {
TncError::Aprs(e)
}
}
impl From<Ax25Error> for TncError {
fn from(e: Ax25Error) -> Self {
TncError::Ax25(e)
}
}
impl From<ConfigError> for TncError {
fn from(e: ConfigError) -> Self {
TncError::Config(e)
}
}
impl From<AprsUiError> for TncError {
fn from(e: AprsUiError) -> Self {
match e {
AprsUiError::Aprs(inner) => TncError::Aprs(inner),
AprsUiError::Ax25(inner) => TncError::Ax25(inner),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InputBandPass {
#[default]
Off,
On,
}
impl InputBandPass {
#[must_use]
pub const fn is_on(self) -> bool {
matches!(self, Self::On)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ChainVoting {
Off,
#[default]
On,
}
impl ChainVoting {
#[must_use]
pub const fn is_on(self) -> bool {
matches!(self, Self::On)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TncConfig {
pub(super) modulator: ModulatorConfig,
pub(super) demodulator: DemodulatorConfig,
pub(super) preamble_flags: usize,
pub(super) tail_flags: usize,
pub(super) sweep: SpaceGainSweep,
pub(super) recovery: RecoveryPolicy,
pub(super) band_pass: InputBandPass,
pub(super) voting: ChainVoting,
#[cfg(feature = "g3ruh")]
pub(super) baseband: Option<BasebandModulator>,
}
impl TncConfig {
pub const fn new(
sample_rate: SampleRate,
baud: BaudRate,
tones: TonePair,
) -> Result<Self, ConfigError> {
let modulator = match ModulatorConfig::new(sample_rate, baud, tones) {
Ok(m) => m,
Err(e) => return Err(e),
};
let demodulator = match DemodulatorConfig::new(sample_rate, baud, tones) {
Ok(d) => d,
Err(e) => return Err(e),
};
let is_bell_202 = baud.bps() == 1_200
&& tones.mark_hz() == TonePair::BELL_202.mark_hz()
&& tones.space_hz() == TonePair::BELL_202.space_hz();
let sweep = if is_bell_202 {
SpaceGainSweep::DEFAULT
} else {
SpaceGainSweep::UNITY
};
Ok(Self {
modulator,
demodulator,
preamble_flags: hdlc::DEFAULT_PREAMBLE_FLAGS,
tail_flags: hdlc::DEFAULT_TAIL_FLAGS,
sweep,
recovery: RecoveryPolicy::PreDestuffFlip,
band_pass: InputBandPass::Off,
voting: ChainVoting::On,
#[cfg(feature = "g3ruh")]
baseband: None,
})
}
pub const fn bell_202(sample_rate: SampleRate) -> Result<Self, ConfigError> {
Self::from_profile(sample_rate, ModemProfile::BELL_202)
}
pub const fn from_profile(
sample_rate: SampleRate,
profile: ModemProfile,
) -> Result<Self, ConfigError> {
let tones = match TonePair::new(
profile.tones().mark_hz(),
profile.tones().space_hz(),
sample_rate,
) {
Ok(t) => t,
Err(e) => return Err(e),
};
let base = match Self::new(sample_rate, profile.baud(), tones) {
Ok(c) => c,
Err(e) => return Err(e),
};
match profile.scheme() {
ModulationScheme::ToneAfsk => Ok(base),
ModulationScheme::ScrambledBaseband => Self::into_baseband(base, sample_rate, profile),
}
}
#[cfg(feature = "g3ruh")]
const fn into_baseband(
mut base: Self,
sample_rate: SampleRate,
profile: ModemProfile,
) -> Result<Self, ConfigError> {
base.baseband = match BasebandModulator::new(sample_rate, profile.baud()) {
Ok(m) => Some(m),
Err(e) => return Err(e),
};
Ok(base)
}
#[cfg(not(feature = "g3ruh"))]
const fn into_baseband(
_base: Self,
sample_rate: SampleRate,
profile: ModemProfile,
) -> Result<Self, ConfigError> {
Err(ConfigError::BaudExceedsSampleRate {
baud: profile.baud().bps(),
sample_rate: sample_rate.hz(),
})
}
#[must_use]
pub const fn scheme(self) -> ModulationScheme {
#[cfg(feature = "g3ruh")]
if self.baseband.is_some() {
return ModulationScheme::ScrambledBaseband;
}
ModulationScheme::ToneAfsk
}
#[must_use]
pub const fn with_flags(mut self, preamble_flags: usize, tail_flags: usize) -> Self {
self.preamble_flags = preamble_flags;
self.tail_flags = tail_flags;
self
}
#[must_use]
pub const fn with_space_gain_sweep(mut self, sweep: SpaceGainSweep) -> Self {
self.sweep = sweep;
self
}
#[must_use]
pub const fn space_gain_sweep(self) -> SpaceGainSweep {
self.sweep
}
#[must_use]
pub const fn with_recovery(mut self, recovery: RecoveryPolicy) -> Self {
self.recovery = recovery;
self
}
#[must_use]
pub const fn recovery(self) -> RecoveryPolicy {
self.recovery
}
#[must_use]
pub const fn bounded_latency(mut self) -> Self {
self.recovery = RecoveryPolicy::None;
self.voting = ChainVoting::Off;
self
}
#[must_use]
pub const fn with_band_pass(mut self, band_pass: InputBandPass) -> Self {
self.band_pass = band_pass;
self
}
#[must_use]
pub const fn band_pass(self) -> InputBandPass {
self.band_pass
}
#[must_use]
pub const fn with_voting(mut self, voting: ChainVoting) -> Self {
self.voting = voting;
self
}
#[must_use]
pub const fn voting(self) -> ChainVoting {
self.voting
}
#[must_use]
pub const fn sample_rate(self) -> SampleRate {
self.modulator.sample_rate()
}
#[must_use]
pub const fn baud(self) -> BaudRate {
self.modulator.baud()
}
#[must_use]
pub const fn tones(self) -> TonePair {
self.modulator.tones()
}
#[must_use]
pub const fn preamble_flags(self) -> usize {
self.preamble_flags
}
#[must_use]
pub const fn tail_flags(self) -> usize {
self.tail_flags
}
}
impl DevicePreset {
pub const fn tnc_config(self) -> Result<TncConfig, ConfigError> {
let base = match TncConfig::from_profile(self.sample_rate(), self.profile()) {
Ok(c) => c,
Err(e) => return Err(e),
};
let sweep = if self.full_chain_bank() {
SpaceGainSweep::DEFAULT
} else {
SpaceGainSweep::UNITY
};
Ok(base.with_space_gain_sweep(sweep))
}
}