ados-crsf-codec 0.1.1

no_std CRSF (Crossfire / ExpressLRS) codec: frames, packed RC channels, telemetry and parameter frames, axis scaling and link liveness.
Documentation
//! A validated bank of 16 RC channel values.
//!
//! The bank is the value container both channel sources fill: writes validate
//! against the usable endpoint range (172..=1811) at this boundary, so an
//! out-of-range value is rejected before it ever reaches the wire; the codec
//! below stays faithful to any 11-bit value. Choosing which bank feeds the transmitter
//! is the caller's job; this module owns only values and validation.

use crate::channels::{CHANNEL_COUNT, CHANNEL_MAX, CHANNEL_MID, CHANNEL_MIN};

/// Why a channel write was rejected.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BankError {
    /// Channel index outside 0..16.
    BadIndex(usize),
    /// Value outside the usable 172..=1811 endpoint range.
    BadValue(u16),
}

/// A set of 16 validated channel values.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelBank {
    values: [u16; CHANNEL_COUNT],
}

impl Default for ChannelBank {
    /// The base a fresh bank starts from before a source fills it: roll,
    /// pitch and yaw (channels 1/2/4 in AETR order) centered, throttle
    /// (channel 3) full low, every auxiliary channel full low. A single-channel
    /// injection builds on it, and so do HID axes the device does not report.
    /// It is never transmitted on its own: with no live source the lane sends
    /// no RC frames.
    fn default() -> Self {
        let mut values = [CHANNEL_MIN; CHANNEL_COUNT];
        values[0] = CHANNEL_MID; // roll
        values[1] = CHANNEL_MID; // pitch
        values[3] = CHANNEL_MID; // yaw
        Self { values }
    }
}

impl ChannelBank {
    /// The current values.
    pub fn values(&self) -> [u16; CHANNEL_COUNT] {
        self.values
    }

    fn check(value: u16) -> Result<(), BankError> {
        if (CHANNEL_MIN..=CHANNEL_MAX).contains(&value) {
            Ok(())
        } else {
            Err(BankError::BadValue(value))
        }
    }

    /// Replace all 16 channels. Rejects the whole set if any value is out of
    /// range, a partial apply would transmit a mixed frame nobody asked for.
    pub fn set_all(&mut self, values: [u16; CHANNEL_COUNT]) -> Result<(), BankError> {
        for &v in &values {
            Self::check(v)?;
        }
        self.values = values;
        Ok(())
    }

    /// Set one channel by zero-based index.
    pub fn set_one(&mut self, index: usize, value: u16) -> Result<(), BankError> {
        if index >= CHANNEL_COUNT {
            return Err(BankError::BadIndex(index));
        }
        Self::check(value)?;
        self.values[index] = value;
        Ok(())
    }
}

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

    #[test]
    fn default_bank_is_stick_neutral_throttle_low() {
        let bank = ChannelBank::default();
        let v = bank.values();
        assert_eq!(v[0], CHANNEL_MID);
        assert_eq!(v[1], CHANNEL_MID);
        assert_eq!(v[2], CHANNEL_MIN, "throttle low");
        assert_eq!(v[3], CHANNEL_MID);
        assert!(v[4..].iter().all(|&x| x == CHANNEL_MIN));
    }

    #[test]
    fn set_all_applies() {
        let mut bank = ChannelBank::default();
        let values = [CHANNEL_MID; CHANNEL_COUNT];
        bank.set_all(values).unwrap();
        assert_eq!(bank.values(), values);
    }

    #[test]
    fn set_all_rejects_any_out_of_range_value_atomically() {
        let mut bank = ChannelBank::default();
        let before = bank.values();
        let mut values = [CHANNEL_MID; CHANNEL_COUNT];
        values[9] = CHANNEL_MAX + 1;
        assert_eq!(
            bank.set_all(values),
            Err(BankError::BadValue(CHANNEL_MAX + 1))
        );
        assert_eq!(bank.values(), before, "no partial apply");
    }

    #[test]
    fn set_one_bounds_index_and_value() {
        let mut bank = ChannelBank::default();
        bank.set_one(4, 1500).unwrap();
        assert_eq!(bank.values()[4], 1500);
        assert_eq!(bank.set_one(16, CHANNEL_MID), Err(BankError::BadIndex(16)));
        assert_eq!(
            bank.set_one(0, CHANNEL_MIN - 1),
            Err(BankError::BadValue(CHANNEL_MIN - 1))
        );
    }
}