arinc429/
lib.rs

1//! # arinc429
2//!
3//! A Rust library for the **ARINC 429** avionics data bus protocol.
4//!
5//! Provides full support for:
6//! - Encoding and decoding 32-bit ARINC 429 words
7//! - Label bit reversal and odd parity
8//! - Octal label parsing (e.g., `"012"`, `"203"`)
9//! - BNR (Binary) physical value interpretation with signed/unsigned handling
10//! - BCD date (label 260) and UTC time (label 150) decoding
11//! - SSM (Sign/Status Matrix) interpretation
12//! - Common flight parameters (ground speed, altitude, Mach, TAT, roll angle, etc.)
13//!
14//! ## Features
15//! - Pure Rust, no_std compatible (with minor changes)
16//! - Comprehensive error handling via [`thiserror`]
17//! - Well-tested with unit tests and cross-validation
18//! - Ready for integration with flight simulators (JSBSim, FlightGear) or real hardware
19//!
20//! ## Example
21//!
22//! ```rust
23//! use arinc429::{encode, decode, Label};
24//!
25//! // Encode Ground Speed = 250 knots (label 012)
26//! let word = encode(Label::GroundSpeed.raw(), 0, 2000, 3).unwrap(); // 2000 * 0.125 = 250
27//! assert_eq!(format!("{:08X}", word), "E01F4050");
28//!
29//! // Decode it back
30//! let decoded = decode(word).unwrap();
31//! assert_eq!(decoded.label, Label::GroundSpeed);
32//! assert_eq!(decoded.to_physical(), Some(250.0));
33//! ```
34
35use thiserror::Error;
36
37
38/// Errors that can occur during ARINC 429 operations.
39#[derive(Error, Debug, PartialEq, Eq)]
40pub enum ArincError {
41    /// Data field exceeds 19 bits (max allowed value: 524287)
42    #[error("Data exceeds 19 bits: {0}")]
43    DataOverflow(u32),
44
45    /// Source/Destination Identifier must be 0–3
46    #[error("SDI must be 0-3: {0}")]
47    InvalidSdi(u8),
48
49    /// Sign/Status Matrix must be 0–3
50    #[error("SSM must be 0-3: {0}")]
51    InvalidSsm(u8),
52
53    /// Odd parity check failed
54    #[error("Parity check failed")]
55    ParityMismatch,
56
57    /// Invalid octal label string (e.g., contains non-octal digits or out of range)
58    #[error("Invalid octal label string")]
59    InvalidOctalLabel,
60}
61
62/// Sign/Status Matrix (SSM) values as defined in ARINC 429.
63///
64/// These indicate data validity and are common to both BNR and BCD data types.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Ssm {
67    /// Failure Warning – equipment failure detected
68    FailureWarning,
69    /// No Computed Data – data not available or invalid
70    NoComputedData,
71    /// Functional Test – self-test in progress
72    FunctionalTest,
73    /// Normal Operation – data is valid
74    NormalOperation,
75}
76
77impl Ssm {
78    /// Convert raw SSM bits (0–3) to the corresponding enum variant.
79    pub fn from_u8(value: u8) -> Self {
80        match value {
81            0 => Self::FailureWarning,
82            1 => Self::NoComputedData,
83            2 => Self::FunctionalTest,
84            3 => Self::NormalOperation,
85            _ => Self::NoComputedData, // Invalid values treated as NCD
86        }
87    }
88
89    /// Human-readable description of the SSM state.
90    pub fn name(&self) -> &'static str {
91        match self {
92            Self::FailureWarning => "Failure Warning",
93            Self::NoComputedData => "No Computed Data",
94            Self::FunctionalTest => "Functional Test",
95            Self::NormalOperation => "Normal Operation",
96        }
97    }
98}
99
100/// Known ARINC 429 parameter labels supported by this crate.
101///
102/// Each variant includes its standard octal and decimal code, data type (BNR/BCD),
103/// and physical interpretation.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Label {
106    /// Ground Speed – label 012 (decimal 10), BNR, resolution 0.125 knots
107    GroundSpeed,
108    /// UTC Time – label 150 (decimal 104), BCD, format hh:mm:ss
109    UtcTime,
110    /// Pressure Altitude – label 203 (decimal 131), BNR signed, feet
111    PressureAltitude,
112    /// Baro-Corrected Altitude – label 204 (decimal 132), BNR signed, feet
113    BaroCorrectedAlt,
114    /// Mach – label 205 (decimal 133), BNR positive, resolution 0.001
115    Mach,
116    /// True Airspeed – label 210 (decimal 136), BNR, knots
117    TrueAirspeed,
118    /// Total Air Temperature (TAT) – label 211 (decimal 137), BNR signed, resolution 0.25 °C
119    Tat,
120    /// Date – label 260 (decimal 176), BCD, format dd-mm-yy
121    Date,
122    /// Roll Angle – label 324 (decimal 212), BNR signed, resolution 0.01°
123    RollAngle,
124    /// Unknown or unsupported label
125    Unknown(u8),
126}
127
128impl Label {
129    /// Convert a raw decimal label code (after bit reversal) to the enum variant.
130    pub fn from_u8(raw: u8) -> Self {
131        match raw {
132            10 => Label::GroundSpeed,
133            104 => Label::UtcTime,
134            131 => Label::PressureAltitude,
135            132 => Label::BaroCorrectedAlt,
136            133 => Label::Mach,
137            136 => Label::TrueAirspeed,
138            137 => Label::Tat,
139            176 => Label::Date,
140            212 => Label::RollAngle,
141            _ => Label::Unknown(raw),
142        }
143    }
144
145    /// Parse an octal label string (e.g., `"012"`, `"203"`) into the corresponding variant.
146    ///
147    /// Returns an error if the string is not valid octal or maps to an unknown label.
148    pub fn from_octal_str(s: &str) -> Result<Self, ArincError> {
149        let decimal = u8::from_str_radix(s, 8).map_err(|_| ArincError::InvalidOctalLabel)?;
150        Ok(Self::from_u8(decimal))
151    }
152
153    /// Raw decimal label code for use with [`encode`].
154    pub fn raw(&self) -> u8 {
155        match self {
156            Label::GroundSpeed => 10,
157            Label::UtcTime => 104,
158            Label::PressureAltitude => 131,
159            Label::BaroCorrectedAlt => 132,
160            Label::Mach => 133,
161            Label::TrueAirspeed => 136,
162            Label::Tat => 137,
163            Label::Date => 176,
164            Label::RollAngle => 212,
165            Label::Unknown(n) => *n,
166        }
167    }
168
169    /// Standard octal representation (3 digits, zero-padded).
170    pub fn octal(&self) -> String {
171        match self {
172            Label::GroundSpeed => "012".to_string(),
173            Label::UtcTime => "150".to_string(),
174            Label::PressureAltitude => "203".to_string(),
175            Label::BaroCorrectedAlt => "204".to_string(),
176            Label::Mach => "205".to_string(),
177            Label::Tat => "211".to_string(),
178            Label::TrueAirspeed => "210".to_string(),
179            Label::Date => "260".to_string(),
180            Label::RollAngle => "324".to_string(),
181            Label::Unknown(n) => format!("{:03o}", n),
182        }
183    }
184
185    /// Human-readable parameter name.
186    pub fn name(&self) -> &'static str {
187        match self {
188            Label::GroundSpeed => "Ground Speed",
189            Label::UtcTime => "UTC Time",
190            Label::PressureAltitude => "Pressure Altitude (1013.25 mb)",
191            Label::BaroCorrectedAlt => "Baro-Corrected Altitude",
192            Label::Mach => "Mach",
193            Label::Tat => "Total Air Temperature (TAT)",
194            Label::TrueAirspeed => "True Airspeed",
195            Label::Date => "Date",
196            Label::RollAngle => "Roll Angle",
197            Label::Unknown(_) => "Unknown Label",
198        }
199    }
200
201    /// Physical units (empty string if none).
202    pub fn units(&self) -> &'static str {
203        match self {
204            Label::GroundSpeed | Label::TrueAirspeed => "knots",
205            Label::PressureAltitude | Label::BaroCorrectedAlt => "feet",
206            Label::Mach => "",
207            Label::Tat => "°C",
208            Label::RollAngle => "°",
209            Label::Date | Label::UtcTime => "",
210            Label::Unknown(_) => "",
211        }
212    }
213}
214
215/// A fully decoded ARINC 429 word.
216#[derive(Debug, PartialEq)]
217pub struct ArincWord {
218    /// The parameter label
219    pub label: Label,
220    /// Source/Destination Identifier (0–3)
221    pub sdi: u8,
222    /// Raw 19-bit data field
223    pub data: u32,
224    /// Sign/Status Matrix
225    pub ssm: Ssm,
226}
227
228impl ArincWord {
229    /// Convert the raw data to a physical value (e.g., knots, feet, °C) for supported BNR labels.
230    ///
231    /// Returns `None` if:
232    /// - SSM is not Normal Operation
233    /// - Label is not supported or is BCD (use `to_bcd_date`/`to_bcd_time` instead)
234    pub fn to_physical(&self) -> Option<f64> {
235        if !matches!(self.ssm, Ssm::NormalOperation) {
236            return None;
237        }
238
239        let raw = self.data as i32;
240        let signed = if (raw & 0x40000) != 0 {
241            raw.wrapping_sub(0x80000)
242        } else {
243            raw
244        };
245
246        match self.label {
247            Label::GroundSpeed => Some(self.data as f64 * 0.125),
248            Label::PressureAltitude | Label::BaroCorrectedAlt => Some(signed as f64),
249            Label::Mach => Some(self.data as f64 * 0.001),
250            Label::Tat => Some(signed as f64 * 0.25),
251            Label::TrueAirspeed => Some(self.data as f64),
252            Label::RollAngle => Some(signed as f64 * 0.01),
253            _ => None,
254        }
255    }
256
257    /// Decode BCD Date (label 260) → `"dd-mm-yy"` string.
258    ///
259    /// Returns `None` if label mismatch, invalid BCD digits, or SSM not Normal.
260    pub fn to_bcd_date(&self) -> Option<String> {
261        if self.label != Label::Date || !matches!(self.ssm, Ssm::NormalOperation) {
262            return None;
263        }
264
265        let d = self.data;
266        let year_units = (d & 0xF) as u8;
267        let year_tens = ((d >> 4) & 0xF) as u8;
268        let month_units = ((d >> 8) & 0xF) as u8;
269        let month_tens = ((d >> 12) & 0x1) as u8;
270        let day_units = ((d >> 13) & 0xF) as u8;
271        let day_tens = ((d >> 17) & 0x3) as u8;
272
273        if year_tens > 9
274            || year_units > 9
275            || month_tens > 1
276            || month_units > 9
277            || day_tens > 3
278            || day_units > 9
279            || (month_tens * 10 + month_units) == 0
280            || (day_tens * 10 + day_units) == 0
281        {
282            return None;
283        }
284
285        Some(format!(
286            "{:02}-{:02}-{:02}",
287            day_tens * 10 + day_units,
288            month_tens * 10 + month_units,
289            year_tens * 10 + year_units
290        ))
291    }
292
293    /// Decode BCD UTC Time (label 150) → `"hh:mm:ss"` string.
294    ///
295    /// Returns `None` if label mismatch, invalid BCD digits, or SSM not Normal.
296    pub fn to_bcd_time(&self) -> Option<String> {
297        if self.label != Label::UtcTime || !matches!(self.ssm, Ssm::NormalOperation) {
298            return None;
299        }
300
301        let d = self.data;
302        let sec_units = (d & 0xF) as u8;
303        let sec_tens = ((d >> 4) & 0x7) as u8;
304        let min_units = ((d >> 7) & 0xF) as u8;
305        let min_tens = ((d >> 11) & 0x7) as u8;
306        let hour_units = ((d >> 14) & 0xF) as u8;
307        let hour_tens = ((d >> 18) & 0x3) as u8;
308
309        if hour_tens > 2
310            || hour_units > 9
311            || min_tens > 5
312            || min_units > 9
313            || sec_tens > 5
314            || sec_units > 9
315        {
316            return None;
317        }
318
319        Some(format!(
320            "{:02}:{:02}:{:02}",
321            hour_tens * 10 + hour_units,
322            min_tens * 10 + min_units,
323            sec_tens * 10 + sec_units
324        ))
325    }
326}
327
328/// Encode an ARINC 429 word.
329///
330/// Performs label bit reversal, packs fields, and adds odd parity.
331///
332/// # Arguments
333/// - `label`: Raw decimal label code (before reversal)
334/// - `sdi`: Source/Destination Identifier (0–3)
335/// - `data`: 19-bit data field (0–524287)
336/// - `ssm`: Sign/Status Matrix (0–3)
337///
338/// # Returns
339/// 32-bit ARINC 429 word on success
340pub fn encode(label: u8, sdi: u8, data: u32, ssm: u8) -> Result<u32, ArincError> {
341    if sdi > 3 {
342        return Err(ArincError::InvalidSdi(sdi));
343    }
344    if ssm > 3 {
345        return Err(ArincError::InvalidSsm(ssm));
346    }
347    if data > 0x7FFFF {
348        return Err(ArincError::DataOverflow(data));
349    }
350
351    let label_bits = label.reverse_bits();
352    let mut word = (label_bits as u32)
353        | ((sdi as u32) << 8)
354        | (data << 10)
355        | ((ssm as u32) << 29);
356
357    let ones = (word & 0x7FFFFFFF).count_ones();
358    let parity = if ones % 2 == 0 { 1 << 31 } else { 0 };
359    word |= parity;
360
361    Ok(word)
362}
363
364/// Decode a 32-bit ARINC 429 word.
365///
366/// Validates odd parity, reverses label bits, extracts fields, and maps SSM/label.
367///
368/// # Returns
369/// [`ArincWord`] struct on success
370pub fn decode(word: u32) -> Result<ArincWord, ArincError> {
371    if word.count_ones() % 2 == 0 {
372        return Err(ArincError::ParityMismatch);
373    }
374
375    let label_bits = (word & 0xFF) as u8;
376    let label = label_bits.reverse_bits();
377    let sdi = ((word >> 8) & 0x3) as u8;
378    let data = (word >> 10) & 0x7FFFF;
379    let ssm_raw = ((word >> 29) & 0x3) as u8;
380
381    Ok(ArincWord {
382        label: Label::from_u8(label),
383        sdi,
384        data,
385        ssm: Ssm::from_u8(ssm_raw),
386    })
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_all_labels_parse() {
395        assert_eq!(Label::from_octal_str("012").unwrap(), Label::GroundSpeed);
396        assert_eq!(Label::from_octal_str("150").unwrap(), Label::UtcTime);
397        assert_eq!(Label::from_octal_str("260").unwrap(), Label::Date);
398    }
399
400    #[test]
401    fn test_bcd_time() {
402        let data =
403            (0b01 << 18) | (0b0010 << 14) | (0b011 << 11) | (0b0100 << 7) | (0b101 << 4) | 0b0110;
404        let word = encode(104, 0, data, 3).unwrap();
405        let decoded = decode(word).unwrap();
406        assert_eq!(decoded.to_bcd_time(), Some("12:34:56".to_string()));
407    }
408
409    #[test]
410    fn test_bcd_date() {
411        let data =
412            (0b00 << 17) | (0b0110 << 13) | (0b0 << 12) | (0b0001 << 8) | (0b0010 << 4) | 0b0110;
413        let word = encode(176, 0, data, 3).unwrap();
414        let decoded = decode(word).unwrap();
415        assert_eq!(decoded.to_bcd_date(), Some("06-01-26".to_string()));
416    }
417
418    #[test]
419    fn test_cross_py_ground_speed() {
420        let word: u32 = 0xE01F4050;
421        let decoded = decode(word).unwrap();
422        assert_eq!(decoded.label, Label::GroundSpeed);
423        assert_eq!(decoded.ssm, Ssm::NormalOperation);
424        assert_eq!(decoded.to_physical(), Some(250.0));
425    }
426}