rs1090 0.5.2

Rust library to decode Mode S and ADS-B signals
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
#![allow(clippy::suspicious_else_formatting)]

use deku::prelude::*;
use serde::{Deserialize, Serialize};
use std::fmt;

/**
 * ## Target State and Status Information (BDS 6,2 / TYPE=29)
 *
 * Extended Squitter ADS-B message providing aircraft target state and status.  
 * Per DO-260B §2.2.3.2.7.1: Target State and Status Messages (TYPE=29, Subtype=1)
 *
 * Purpose: Provides the selected altitude, heading, barometric setting, and
 * autopilot/flight mode status for trajectory prediction and conflict detection.
 *
 * Message Structure (56 bits):
 * | TYPE | SUB | SIL | SRC | ALT  | QNH | HDGS | HDG | NACP | NICB | SIL | STAT | AP | VN | AH | IMF | APR | TCAS | LN | RES |
 * |------|-----|-----|-----|------|-----|------|-----|------|------|-----|------|----|----|----|----|-----|------|----|----|
 * | 5    | 2   | 1   | 1   | 11   | 9   | 1    | 9   | 4    | 1    | 2   | 1    | 1  | 1  | 1  | 1  | 1   | 1    | 1  | 2  |
 *
 * Field Encoding per DO-260B §2.2.3.2.7.1.3:
 *
 * **Subtype** (bits 6-7): Must be 01 (1) for DO-260B compliance
 *
 * **SIL Supplement** (bit 8): Source Integrity Level probability basis
 *   - 0 = per hour basis (GNSS sources)
 *   - 1 = per sample basis (IRU, DME/DME sources)
 *
 * **Selected Altitude Source** (bit 9):
 *   - 0 = MCP/FCU (Mode Control Panel / Flight Control Unit)
 *   - 1 = FMS (Flight Management System)
 *   - Set to 0 if no valid altitude data available
 *
 * **Selected Altitude** (bits 10-20):
 *   - 11-bit field containing MCP/FCU or FMS selected altitude
 *   - LSB = 32 ft
 *   - Formula: altitude = value × 32 ft (if value > 1)
 *   - Range: [0, 65,472] ft
 *   - value=0: No data or invalid data
 *   - value=1: 0 ft
 *   - value=2047: 65,472 ft
 *   - Implementation rounds to nearest 100 ft: ((value - 1) × 32 + 16) / 100 × 100
 *   - Note: May not reflect true intention during VNAV/approach modes
 *
 * **Barometric Pressure Setting** (bits 21-29):
 *   - 9-bit field encoding QNH/QFE minus 800 millibars
 *   - LSB = 0.8 mbar
 *   - Formula: pressure = 800 + value × 0.8 mbar (if value > 0)
 *   - Range: [800, 1208.4] mbar
 *   - value=0: No data or invalid data
 *   - value=1: 800.0 mbar
 *   - value=511: 1208.0 mbar
 *   - Values outside [800, 1208.4] mbar encoded as 0
 *   - Note: Can represent QFE or QNH depending on local procedures
 *
 * **Selected Heading Status** (bit 30):
 *   - 0 = heading data not available or invalid
 *   - 1 = heading data available and valid
 *
 * **Selected Heading Sign** (bit 31):
 *   - 0 = Positive (0° to 179.9°)
 *   - 1 = Negative (180° to 359.9°, encoded as -180° to -0.7°)
 *
 * **Selected Heading** (bits 32-39):
 *   - 8-bit field encoding selected heading (magnetic)
 *   - LSB = 180/256 degrees (≈0.703125°)
 *   - Formula: heading = value × (180/256) degrees
 *   - Range: [0, 359.9] degrees
 *   - Angular system: [-180, +180] degrees internally, converted to [0, 360]°
 *   - Returns None if status bit = 0
 *
 * **NAC_P** (bits 40-43): Navigation Accuracy Category - Position (4 bits)
 *   - Indicates horizontal position accuracy
 *   - Values 0-15, higher values = better accuracy
 *
 * **NIC_BARO** (bit 44): Navigation Integrity Category - Barometric
 *   - 0 = barometric altitude not cross-checked
 *   - 1 = barometric altitude cross-checked with another pressure source
 *
 * **SIL** (bits 45-46): Source Integrity Level (2 bits)
 *   - Per sample or per hour probability (see SIL Supplement)
 *   - Values 0-3, higher values = better integrity
 *
 * **Mode Status** (bit 47): Status bit for following mode flags
 *   - 0 = mode flags invalid
 *   - 1 = mode flags valid
 *
 * - **Autopilot Engaged** (bit 48): 1 = autopilot engaged (if mode_status=1)
 * - **VNAV Mode Engaged** (bit 49): 1 = VNAV active (if mode_status=1)
 * - **Altitude Hold Mode** (bit 50): 1 = altitude hold active (if mode_status=1)
 * - **IMF** (bit 51): Reserved for ADS-R flag
 * - **Approach Mode** (bit 52): 1 = approach mode active (if mode_status=1)
 * - **TCAS Operational** (bit 53): 1 = TCAS/ACAS operational (ALWAYS valid)
 * - **LNAV Mode Engaged** (bit 54): 1 = LNAV active (if mode_status=1)
 * - **Reserved** (bits 55-56): Reserved bits
 *
 * Important Notes per DO-260B:
 * - Selected altitude may not reflect true intention during certain flight modes
 * - Many aircraft only transmit MCP/FCU selected altitude, not FMS altitude
 * - Target altitude (next level-off altitude) may differ from selected altitude
 * - Barometric setting represents value currently being used to fly the aircraft
 * - Mode flags are only valid if mode_status bit is set
 * - TCAS operational flag is always valid regardless of mode_status
 *
 * Reference: DO-260B §2.2.3.2.7.1, Figure 2-10
 */
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, DekuRead)]
pub struct TargetStateAndStatusInformation {
    /// Subtype (bits 6-7): Per DO-260B §2.2.3.2.7.1.2  
    /// Identifies format of Target State and Status Message.  
    /// Encoding:
    ///   - 0 = Reserved (DO-260A format)
    ///   - 1 = DO-260B format (required for MOPS compliance)
    ///   - 2-3 = Reserved
    ///
    /// DO-260B compliant systems must transmit subtype=1.
    #[deku(bits = "2")]
    #[serde(skip)]
    pub subtype: u8,

    /// Selected Altitude Source (bit 9): Per DO-260B §2.2.3.2.7.1.3.2  
    /// Indicates source of selected altitude data.  
    /// Encoding:
    ///   - 0 = MCP/FCU (Mode Control Panel / Flight Control Unit)
    ///   - 1 = FMS (Flight Management System)
    ///
    /// Set to 0 if no valid altitude data available.  
    /// Note: Many aircraft only provide MCP/FCU altitude, not FMS altitude.
    #[deku(pad_bits_before = "1")]
    #[serde(rename = "source")]
    pub alt_source: AltSource,

    /// Selected Altitude (bits 10-20): Per DO-260B §2.2.3.2.7.1.3.3
    /// MCP/FCU or FMS selected altitude in feet.
    /// Encoding details:
    ///   - LSB = 32 ft
    ///   - Formula: altitude = value × 32 ft (if value > 1)
    ///   - Range: [0, 65,472] ft
    ///   - value=0: No data or invalid data
    ///   - value=1: 0 ft
    ///   - value=2047: 65,472 ft
    ///
    /// Implementation rounds to nearest 100 ft: ((value - 1) × 32 + 16) / 100 × 100  
    /// Returns None if value ≤ 1.  
    /// Note: May not reflect true intention during VNAV or approach modes.  
    /// This is the selected altitude, not necessarily the target altitude.
    #[deku(
        bits = "11",
        endian = "big",
        map = "|altitude: u16| -> Result<_, DekuError> {
            Ok(
                if altitude > 1 {Some(((altitude - 1) * 32 + 16) / 100 * 100)}
                else {None}
            )
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub selected_altitude: Option<u16>,

    /// Barometric Pressure Setting (bits 21-29): Per DO-260B §2.2.3.2.7.1.3.4  
    /// QNH or QFE setting in millibars.  
    /// Encoding details:
    ///   - LSB = 0.8 mbar
    ///   - Formula: pressure = 800 + value × 0.8 mbar (if value > 0)
    ///   - Range: [800, 1208.4] mbar
    ///   - value=0: No data or invalid data
    ///   - value=1: 800.0 mbar
    ///   - value=511: 1208.0 mbar
    ///
    /// Values outside [800, 1208.4] mbar are encoded as 0.  
    /// Returns None if value = 0.  
    /// Note: Can represent QFE or QNH depending on local procedures.  
    /// This is the value currently being used to fly the aircraft.
    #[deku(
        bits = "9",
        endian = "big",
        map = "|qnh: u32| -> Result<_, DekuError> {
            if qnh == 0 { Ok(None) }
            else { Ok(Some(800.0 + ((qnh - 1) as f32) * 0.8)) }
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub barometric_setting: Option<f32>,

    /// Selected Heading Status (bit 30): Per DO-260B §2.2.3.2.7.1.3.5  
    /// Status bit for selected heading validity.  
    /// Encoding:
    ///   - 0 = heading data not available or invalid
    ///   - 1 = heading data available and valid
    #[deku(bits = "1")]
    #[serde(skip)]
    pub heading_status: bool,

    /// Selected Heading (bits 31-39): Per DO-260B §2.2.3.2.7.1.3.6/3.7  
    /// Selected heading in degrees (magnetic north reference).  
    /// Encoding details:
    ///   - Bit 31: Sign (0=positive [0-179.9°], 1=negative [180-359.9°])
    ///   - Bits 32-39: 8-bit heading magnitude
    ///   - LSB = 180/256 degrees (≈0.703125°)
    ///   - Formula: heading = value × (180/256) degrees
    ///   - Range: [0, 359.9] degrees
    ///   - Angular system: [-180, +180]° internally, converted to [0, 360]°
    ///
    /// Returns None if heading_status = 0.
    #[deku(
        bits = "9",
        endian = "big",
        map = "|heading: u16| -> Result<_, DekuError> {
            if *heading_status {Ok(Some(heading as f32 * 180.0 / 256.0))} 
            else {Ok(None)}
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub selected_heading: Option<f32>,

    /// NAC_P (bits 40-43): Per DO-260B §2.2.3.2.7.1.3.8  
    /// Navigation Accuracy Category - Position.  
    /// 4-bit value indicating horizontal position accuracy.  
    /// Values 0-15, higher values indicate better accuracy.
    #[deku(bits = "4")]
    #[serde(rename = "NACp")]
    pub nac_p: u8,

    /// NIC_BARO (bit 44): Per DO-260B §2.2.3.2.7.1.3.9  
    /// Navigation Integrity Category - Barometric.  
    /// Encoding:
    ///   - 0 = barometric altitude not cross-checked with another source
    ///   - 1 = barometric altitude cross-checked with another pressure source
    #[deku(bits = "1")]
    #[serde(skip)]
    pub nic_baro: bool,

    /// SIL (bits 45-46): Per DO-260B §2.2.3.2.7.1.3.10  
    /// Source Integrity Level (2 bits).  
    /// Probability basis determined by SIL Supplement (bit 8).  
    /// Values 0-3, higher values indicate better integrity.
    #[deku(bits = "2")]
    #[serde(skip)]
    pub sil: u8,

    /// Mode Status (bit 47): Per DO-260B §2.2.3.2.7.1.3.11  
    /// Status bit for MCP/FCU mode flags.  
    /// Encoding:
    ///   - 0 = mode flags (autopilot, VNAV, LNAV, altitude hold, approach) invalid
    ///   - 1 = mode flags valid
    ///
    /// Note: TCAS operational flag is always valid regardless of this bit.
    #[deku(bits = "1")]
    #[serde(skip)]
    pub mode_status: bool,

    /// Autopilot Engaged (bit 48): Per DO-260B §2.2.3.2.7.1.3.12  
    /// Autopilot engagement status.  
    /// Encoding:
    ///   - 0 = autopilot not engaged
    ///   - 1 = autopilot engaged
    ///
    /// Returns None if mode_status = 0.
    #[deku(
        bits = "1",
        map = "|val: bool| -> Result<_, DekuError> {
            if *mode_status {Ok(Some(val))} else {Ok(None)}
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub autopilot: Option<bool>,

    /// VNAV Mode Engaged (bit 49): Per DO-260B §2.2.3.2.7.1.3.13  
    /// Vertical Navigation mode status.  
    /// Encoding:
    ///   - 0 = VNAV mode not engaged
    ///   - 1 = VNAV mode engaged
    ///
    /// Returns None if mode_status = 0.
    #[deku(
        bits = "1",
        map = "|val: bool| -> Result<_, DekuError> {
            if *mode_status {Ok(Some(val))} else {Ok(None)}
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vnav_mode: Option<bool>,

    /// Altitude Hold Mode (bit 50): Per DO-260B §2.2.3.2.7.1.3.14  
    /// Altitude hold mode status.  
    /// Encoding:
    ///   - 0 = altitude hold mode not active
    ///   - 1 = altitude hold mode active
    ///
    /// Returns None if mode_status = 0.
    #[deku(
        bits = "1",
        map = "|val: bool| -> Result<_, DekuError> {
            if *mode_status {Ok(Some(val))} else {Ok(None)}
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alt_hold: Option<bool>,

    /// IMF (bit 51): Per DO-260B §2.2.3.2.7.1.3.15  
    /// Reserved for ADS-R (Automatic Dependent Surveillance-Rebroadcast) flag.
    #[deku(bits = "1")]
    #[serde(skip)]
    pub imf: bool,

    /// Approach Mode (bit 52): Per DO-260B §2.2.3.2.7.1.3.16  
    /// Approach mode status.  
    /// Encoding:
    ///   - 0 = approach mode not active
    ///   - 1 = approach mode active
    ///
    /// Returns None if mode_status = 0.
    #[deku(
        bits = "1",
        map = "|val: bool| -> Result<_, DekuError> {
            if *mode_status {Ok(Some(val))} else {Ok(None)}
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub approach_mode: Option<bool>,

    /// TCAS Operational (bit 53): Per DO-260B §2.2.3.2.7.1.3.17  
    /// TCAS/ACAS operational status.  
    /// Encoding:
    ///   - 0 = TCAS/ACAS not operational
    ///   - 1 = TCAS/ACAS operational
    ///
    /// Note: This flag is ALWAYS valid, regardless of mode_status bit.
    #[deku(bits = "1")]
    pub tcas_operational: bool,

    /// LNAV Mode Engaged (bit 54): Per DO-260B §2.2.3.2.7.1.3.18  
    /// Lateral Navigation mode status.  
    /// Encoding:
    ///   - 0 = LNAV mode not engaged
    ///   - 1 = LNAV mode engaged
    ///
    /// Returns None if mode_status = 0.
    #[deku(
        bits = "1",
        map = "|val: bool| -> Result<_, DekuError> {
            if *mode_status {Ok(Some(val))} else {Ok(None)}
        }"
    )]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[deku(pad_bits_after = "2")]
    pub lnav_mode: Option<bool>,
}

impl fmt::Display for TargetStateAndStatusInformation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "  Target state and status (BDS 6,2)")?;
        if let Some(sel_alt) = &self.selected_altitude {
            writeln!(
                f,
                "  Selected alt:  {} ft {}",
                sel_alt, &self.alt_source
            )?;
        }
        if let Some(sel_hdg) = &self.selected_heading {
            writeln!(f, "  Selected hdg:  {sel_hdg:.1}°")?;
        }
        if let Some(qnh) = &self.barometric_setting {
            writeln!(f, "  QNH:           {qnh:.1} mbar")?;
        }
        if self.mode_status {
            write!(f, "  Mode:         ")?;
            if let Some(value) = self.autopilot {
                if value {
                    write!(f, " autopilot")?;
                }
            }
            if let Some(value) = self.vnav_mode {
                if value {
                    write!(f, " VNAV")?;
                }
            }
            if let Some(value) = self.lnav_mode {
                if value {
                    write!(f, " LNAV")?;
                }
            }
            if let Some(value) = self.alt_hold {
                if value {
                    write!(f, " alt_hold")?;
                }
            }
            if let Some(value) = self.approach_mode {
                if value {
                    write!(f, " approach")?;
                }
            }
            writeln!(f)?;
        }
        writeln!(
            f,
            "  TCAS:          {}",
            if self.tcas_operational { "on" } else { "off" }
        )
    }
}

/// Selected Altitude Source: Per DO-260B §2.2.3.2.7.1.3.2
/// Indicates the source of the selected altitude data.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, DekuRead)]
#[deku(id_type = "u8", bits = "1")]
pub enum AltSource {
    /// Mode Control Panel / Flight Control Unit
    /// Manual altitude selection by pilot via MCP/FCU panel.
    #[deku(id = "0")]
    #[serde(rename = "MCP/FCU")]
    MCP,

    /// Flight Management System
    /// Altitude from FMS flight plan.
    /// Note: Many aircraft only provide MCP/FCU altitude, not FMS.
    #[deku(id = "1")]
    FMS,
}
impl fmt::Display for AltSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self {
            Self::MCP => write!(f, "MCP/FCU"),
            Self::FMS => write!(f, "FMS"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::prelude::*;
    use approx::assert_relative_eq;
    use hexlit::hex;

    #[test]
    fn test_surface_position() {
        let bytes = hex!("8DA05629EA21485CBF3F8CADAEEB");
        let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
        if let ExtendedSquitterADSB(adsb_msg) = msg.df {
            if let ME::BDS62(TargetStateAndStatusInformation {
                selected_altitude,
                alt_source,
                barometric_setting,
                selected_heading,
                mode_status,
                autopilot,
                vnav_mode,
                lnav_mode,
                alt_hold,
                approach_mode,
                tcas_operational,
                ..
            }) = adsb_msg.message
            {
                assert_eq!(selected_altitude, Some(17000));
                assert_eq!(alt_source, AltSource::MCP);
                assert_eq!(barometric_setting, Some(1012.8));
                assert_relative_eq!(
                    selected_heading.unwrap(),
                    66.8,
                    max_relative = 1e-2
                );
                assert!(mode_status);
                assert_eq!(autopilot, Some(true));
                assert_eq!(vnav_mode, Some(true));
                assert_eq!(lnav_mode, Some(true));
                assert_eq!(alt_hold, Some(false));
                assert_eq!(approach_mode, Some(false));
                assert!(tcas_operational);
            }
            return;
        }
        unreachable!();
    }

    #[test]
    fn test_format_groundspeed() {
        let bytes = hex!("8DA05629EA21485CBF3F8CADAEEB");
        let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
        assert_eq!(
            format!("{msg}"),
            r#" DF17. Extended Squitter
  Address:       a05629
  Air/Ground:    airborne
  Target state and status (BDS 6,2)
  Selected alt:  17000 ft MCP/FCU
  Selected hdg:  66.8°
  QNH:           1012.8 mbar
  Mode:          autopilot VNAV LNAV
  TCAS:          on
"#
        )
    }
}