1use thiserror::Error;
36
37
38#[derive(Error, Debug, PartialEq, Eq)]
40pub enum ArincError {
41 #[error("Data exceeds 19 bits: {0}")]
43 DataOverflow(u32),
44
45 #[error("SDI must be 0-3: {0}")]
47 InvalidSdi(u8),
48
49 #[error("SSM must be 0-3: {0}")]
51 InvalidSsm(u8),
52
53 #[error("Parity check failed")]
55 ParityMismatch,
56
57 #[error("Invalid octal label string")]
59 InvalidOctalLabel,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Ssm {
67 FailureWarning,
69 NoComputedData,
71 FunctionalTest,
73 NormalOperation,
75}
76
77impl Ssm {
78 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, }
87 }
88
89 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Label {
106 GroundSpeed,
108 UtcTime,
110 PressureAltitude,
112 BaroCorrectedAlt,
114 Mach,
116 TrueAirspeed,
118 Tat,
120 Date,
122 RollAngle,
124 Unknown(u8),
126}
127
128impl Label {
129 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 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 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 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 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 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#[derive(Debug, PartialEq)]
217pub struct ArincWord {
218 pub label: Label,
220 pub sdi: u8,
222 pub data: u32,
224 pub ssm: Ssm,
226}
227
228impl ArincWord {
229 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 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 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
328pub 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
364pub 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}