prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::interfaces::{SignalQualityTenthsPercent, SnrQuarterDb};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SpreadingFactor {
    Sf5 = 5,
    Sf6 = 6,
    Sf7 = 7,
    Sf8 = 8,
    Sf9 = 9,
    Sf10 = 10,
    Sf11 = 11,
    Sf12 = 12,
}

impl SpreadingFactor {
    #[cfg(feature = "alloc")]
    pub(crate) const fn from_number(value: u8) -> Option<Self> {
        match value {
            5 => Some(Self::Sf5),
            6 => Some(Self::Sf6),
            7 => Some(Self::Sf7),
            8 => Some(Self::Sf8),
            9 => Some(Self::Sf9),
            10 => Some(Self::Sf10),
            11 => Some(Self::Sf11),
            12 => Some(Self::Sf12),
            _ => None,
        }
    }

    pub const fn next(self) -> Self {
        match self {
            Self::Sf5 => Self::Sf6,
            Self::Sf6 => Self::Sf7,
            Self::Sf7 => Self::Sf8,
            Self::Sf8 => Self::Sf9,
            Self::Sf9 => Self::Sf10,
            Self::Sf10 => Self::Sf11,
            Self::Sf11 => Self::Sf12,
            Self::Sf12 => Self::Sf5,
        }
    }

    pub fn signal_quality(self, snr: SnrQuarterDb) -> Option<SignalQualityTenthsPercent> {
        let spreading_factor = i32::from(self as u8);
        let minimum_quarters = (5 - 2 * spreading_factor) * 4;
        let span_db = 1 + 2 * spreading_factor;
        let numerator_quarters = i32::from(snr.quarters()) - minimum_quarters;
        let tenths_percent = if numerator_quarters <= 0 {
            0
        } else {
            ((numerator_quarters * 250 + span_db / 2) / span_db).min(1_000)
        };
        SignalQualityTenthsPercent::new(tenths_percent as u16)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoraBandwidth {
    Bw125kHz,
    Bw250kHz,
    Bw500kHz,
}

impl LoraBandwidth {
    pub const fn hz(self) -> u32 {
        match self {
            Self::Bw125kHz => 125_000,
            Self::Bw250kHz => 250_000,
            Self::Bw500kHz => 500_000,
        }
    }

    pub const fn next(self) -> Self {
        match self {
            Self::Bw125kHz => Self::Bw250kHz,
            Self::Bw250kHz => Self::Bw500kHz,
            Self::Bw500kHz => Self::Bw125kHz,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CodingRate {
    Cr45 = 5,
    Cr46 = 6,
    Cr47 = 7,
    Cr48 = 8,
}

impl CodingRate {
    pub const fn denominator(self) -> u8 {
        self as u8
    }

    pub const fn next(self) -> Self {
        match self {
            Self::Cr45 => Self::Cr46,
            Self::Cr46 => Self::Cr47,
            Self::Cr47 => Self::Cr48,
            Self::Cr48 => Self::Cr45,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Modulation {
    Lora {
        spreading_factor: SpreadingFactor,
        bandwidth: LoraBandwidth,
        coding_rate: CodingRate,
    },
}

#[must_use]
pub const fn nominal_lora_bitrate_bps(
    spreading_factor: u8,
    coding_rate: u8,
    bandwidth_hz: u32,
) -> u32 {
    let sf = spreading_factor as u64;
    let cr = coding_rate as u64;
    let bw = bandwidth_hz as u64;
    if sf == 0 || cr == 0 {
        return 0;
    }
    ((sf * bw * 4) / ((1u64 << sf) * cr)) as u32
}

impl Modulation {
    pub const fn spreading_factor(self) -> SpreadingFactor {
        let Self::Lora {
            spreading_factor, ..
        } = self;
        spreading_factor
    }

    pub const fn nominal_bitrate_bps(self) -> u32 {
        let Self::Lora {
            spreading_factor,
            bandwidth,
            coding_rate,
        } = self;
        nominal_lora_bitrate_bps(spreading_factor as u8, coding_rate as u8, bandwidth.hz())
    }

    pub const fn is_low_data_rate(&self) -> bool {
        let Self::Lora {
            spreading_factor,
            bandwidth,
            ..
        } = self;
        matches!(
            (spreading_factor, bandwidth),
            (
                SpreadingFactor::Sf11 | SpreadingFactor::Sf12,
                LoraBandwidth::Bw125kHz
            ) | (SpreadingFactor::Sf12, LoraBandwidth::Bw250kHz)
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lora_nominal_bitrate_matches_the_standard_formula() {
        let slow = Modulation::Lora {
            spreading_factor: SpreadingFactor::Sf8,
            bandwidth: LoraBandwidth::Bw125kHz,
            coding_rate: CodingRate::Cr45,
        };
        assert_eq!(slow.nominal_bitrate_bps(), 3125);
        let fast = Modulation::Lora {
            spreading_factor: SpreadingFactor::Sf7,
            bandwidth: LoraBandwidth::Bw500kHz,
            coding_rate: CodingRate::Cr45,
        };
        assert_eq!(fast.nominal_bitrate_bps(), 21875);
    }

    #[test]
    fn low_data_rate_covers_exactly_the_slow_combos() {
        let slow_combos = [
            (SpreadingFactor::Sf11, LoraBandwidth::Bw125kHz),
            (SpreadingFactor::Sf12, LoraBandwidth::Bw125kHz),
            (SpreadingFactor::Sf12, LoraBandwidth::Bw250kHz),
        ];
        for sf in [
            SpreadingFactor::Sf5,
            SpreadingFactor::Sf6,
            SpreadingFactor::Sf7,
            SpreadingFactor::Sf8,
            SpreadingFactor::Sf9,
            SpreadingFactor::Sf10,
            SpreadingFactor::Sf11,
            SpreadingFactor::Sf12,
        ] {
            for bandwidth in [
                LoraBandwidth::Bw125kHz,
                LoraBandwidth::Bw250kHz,
                LoraBandwidth::Bw500kHz,
            ] {
                let modulation = Modulation::Lora {
                    spreading_factor: sf,
                    bandwidth,
                    coding_rate: CodingRate::Cr45,
                };
                assert_eq!(
                    modulation.is_low_data_rate(),
                    slow_combos.contains(&(sf, bandwidth)),
                );
            }
        }
    }

    #[test]
    fn modulation_settings_cycle_through_all_values() {
        let mut sf = SpreadingFactor::Sf5;
        for _ in 0..8 {
            sf = sf.next();
        }
        assert_eq!(sf, SpreadingFactor::Sf5);
        assert_eq!(SpreadingFactor::Sf12.next(), SpreadingFactor::Sf5);

        let mut bw = LoraBandwidth::Bw125kHz;
        for _ in 0..3 {
            bw = bw.next();
        }
        assert_eq!(bw, LoraBandwidth::Bw125kHz);

        let mut cr = CodingRate::Cr45;
        for _ in 0..4 {
            cr = cr.next();
        }
        assert_eq!(cr, CodingRate::Cr45);
    }
}