Skip to main content

rs1090/decode/bds/
bds60.rs

1use deku::prelude::*;
2use serde::{Deserialize, Serialize};
3
4/**
5 * ## Heading and Speed Report (BDS 6,0)
6 *
7 * Comm-B message providing aircraft heading and speed data.
8 * Per ICAO Doc 9871 Table A-2-96: BDS code 6,0 — Heading and speed report
9 *
10 * Purpose: Provides heading and speed data to ground systems for improved
11 * trajectory prediction and conflict detection.
12 *
13 * Message Structure (56 bits):
14 * | HDG   | IAS  | MACH | BARO | INER |
15 * |-------|------|------|------|------|
16 * | 1+1+10| 1+10 | 1+10 | 1+1+9| 1+1+9|
17 *
18 * Field Encoding per ICAO Doc 9871:
19 *
20 * **Magnetic Heading** (bits 1-12):
21 *   - Bit 1: Status (0=invalid, 1=valid)
22 *   - Bit 2: Sign (0=east, 1=west)
23 *   - Bits 3-12: 10-bit heading magnitude
24 *     * MSB = 90 degrees
25 *     * LSB = 90/512 degrees (≈0.1758°)
26 *     * Range: [-180, +180] degrees (two's complement)
27 *     * Converted to [0, 360]° for display (e.g., 315° = -45°)
28 *
29 * **Indicated Airspeed (IAS)** (bits 13-23):
30 *   - Bit 13: Status (0=invalid, 1=valid)
31 *   - Bits 14-23: 10-bit IAS value
32 *     * MSB = 512 kt
33 *     * LSB = 1 kt
34 *     * Range: [0, 1023] kt
35 *     * Formula: IAS = value × 1 kt
36 *
37 * **Mach Number** (bits 24-34):
38 *   - Bit 24: Status (0=invalid, 1=valid)
39 *   - Bits 25-34: 10-bit Mach value
40 *     * MSB = 2.048 Mach
41 *     * LSB = 2.048/512 Mach (≈0.004 Mach)
42 *     * Range: [0, 4.092] Mach
43 *     * Formula: Mach = value × (2.048/512)
44 *
45 * **Barometric Altitude Rate** (bits 35-45):
46 *   - Bit 35: Status (0=invalid, 1=valid)
47 *   - Bit 36: Sign (0=climbing, 1=descending/"below")
48 *   - Bits 37-45: 9-bit vertical rate value
49 *     * MSB = 8,192 ft/min
50 *     * LSB = 32 ft/min (8192/256)
51 *     * Range: [-16,384, +16,352] ft/min
52 *     * Formula: rate = value × 32 ft/min (two's complement)
53 *     * Special values: 0 or 511 = no rate information (returns 0)
54 *
55 * **Inertial Vertical Velocity** (bits 46-56):
56 *   - Bit 46: Status (0=invalid, 1=valid)
57 *   - Bit 47: Sign (0=climbing, 1=descending/"below")
58 *   - Bits 48-56: 9-bit vertical velocity value
59 *     * MSB = 8,192 ft/min
60 *     * LSB = 32 ft/min (8192/256)
61 *     * Range: [-16,384, +16,352] ft/min
62 *     * Formula: velocity = value × 32 ft/min (two's complement)
63 *     * Special values: 0 or 511 = no velocity information (returns 0)
64 *
65 * Data Source Notes per ICAO Doc 9871:
66 * - **Barometric Altitude Rate**: Solely derived from barometric measurement
67 *   (Air Data System or IRS/FMS). Usually unsteady and may suffer from
68 *   barometric instrument inertia.
69 * - **Inertial Vertical Velocity**: Derived from inertial equipment (IRS, AHRS)
70 *   using different sources than barometric (FMC/GNSS integrated, FMC general,
71 *   or IRS/FMS). More filtered and smooth parameter. When barometric altitude
72 *   rate is integrated and smoothed with inertial data (baro-inertial), it
73 *   shall be transmitted in this field.
74 *
75 * Validation Rules per ICAO Doc 9871:
76 * - If parameter exceeds range, use maximum allowable value (requires GFM intervention)
77 * - Data should be from sources controlling the aircraft (when possible)
78 * - LSB values obtained by rounding
79 * - If parameter unavailable, all bits set to ZERO by GFM
80 *
81 * Implementation Validation:
82 * - IAS must be in range (0, 500] kt (operational validation)
83 * - Mach must be in range (0, 1] (subsonic aircraft assumption)
84 * - IAS and Mach cross-validation:
85 *   * If IAS > 250 kt, then Mach must be ≥ 0.4 (250 kt ≈ Mach 0.45 at 10,000 ft)
86 *   * If IAS < 150 kt, then Mach must be ≤ 0.5 (150 kt ≈ Mach 0.5 at FL400)
87 * - Vertical rate/velocity abs value ≤ 6,000 ft/min (typical operational limit)
88 *
89 * Note: Two's complement coding used for all signed fields (§A.2.2.2)
90 * Additional implementation guidelines in ICAO Doc 9871 §D.2.4.6
91 */
92#[derive(Debug, PartialEq, Serialize, Deserialize, DekuRead, Clone)]
93#[serde(tag = "bds", rename = "60")]
94pub struct HeadingAndSpeedReport {
95    /// Magnetic Heading (bits 1-12): Per ICAO Doc 9871 Table A-2-96  
96    /// Aircraft magnetic heading in degrees (magnetic north reference).  
97    /// Encoding details:
98    ///   - Bit 1: Status (0=invalid, 1=valid)
99    ///   - Bit 2: Sign (0=east, 1=west)
100    ///   - Bits 3-12: 10-bit heading magnitude
101    ///   - MSB = 90 degrees
102    ///   - LSB = 90/512 degrees (≈0.1758°)
103    ///   - Formula: heading = value × (90/512) degrees (two's complement)
104    ///   - Range: [-180, +180] degrees (internal), converted to [0, 360]° for output
105    ///   - Example: 315° encoded as -45°
106    ///
107    /// Returns None if status bit is 0.
108    #[deku(reader = "read_heading(deku::reader)")]
109    #[serde(rename = "heading", skip_serializing_if = "Option::is_none")]
110    pub magnetic_heading: Option<f64>,
111
112    /// Indicated Airspeed (bits 13-23): Per ICAO Doc 9871 Table A-2-96  
113    /// Aircraft indicated airspeed in knots.  
114    /// Encoding details:
115    ///   - Bit 13: Status (0=invalid, 1=valid)
116    ///   - Bits 14-23: 10-bit IAS value
117    ///   - MSB = 512 kt
118    ///   - LSB = 1 kt
119    ///   - Formula: IAS = value × 1 kt
120    ///   - Range: [0, 1023] kt
121    ///
122    /// Returns None if status bit is 0.
123    /// Implementation validates IAS ∈ (0, 500] kt (operational range).  
124    /// Note: TAS (True Airspeed) is available in BDS 5,0.
125    #[deku(reader = "read_ias(deku::reader)")]
126    #[serde(rename = "IAS", skip_serializing_if = "Option::is_none")]
127    pub indicated_airspeed: Option<u16>,
128
129    /// Mach Number (bits 24-34): Per ICAO Doc 9871 Table A-2-96  
130    /// Aircraft Mach number (ratio of aircraft speed to speed of sound).  
131    /// Encoding details:
132    ///   - Bit 24: Status (0=invalid, 1=valid)
133    ///   - Bits 25-34: 10-bit Mach value
134    ///   - MSB = 2.048 Mach
135    ///   - LSB = 2.048/512 Mach (≈0.004 Mach)
136    ///   - Formula: Mach = value × (2.048/512)
137    ///   - Range: [0, 4.092] Mach
138    ///
139    /// Returns None if status bit is 0.  
140    /// Implementation validates:
141    ///   - Mach ∈ (0, 1] (subsonic aircraft)
142    ///   - Cross-validation with IAS:
143    ///     * If IAS > 250 kt: Mach ≥ 0.4 (250 kt ≈ Mach 0.45 at 10,000 ft)
144    ///     * If IAS < 150 kt: Mach ≤ 0.5 (150 kt ≈ Mach 0.5 at FL400)
145    #[deku(reader = "read_mach(deku::reader, *indicated_airspeed)")]
146    #[serde(rename = "Mach", skip_serializing_if = "Option::is_none")]
147    pub mach_number: Option<f64>,
148
149    /// Barometric Altitude Rate (bits 35-45): Per ICAO Doc 9871 Table A-2-96  
150    /// Vertical rate derived solely from barometric measurement in ft/min.  
151    /// Encoding details:
152    ///   - Bit 35: Status (0=invalid, 1=valid)
153    ///   - Bit 36: Sign (0=climbing, 1=descending)
154    ///   - Bits 37-45: 9-bit vertical rate magnitude
155    ///   - MSB = 8,192 ft/min
156    ///   - LSB = 32 ft/min (8192/256)
157    ///   - Formula: rate = value × 32 ft/min (two's complement)
158    ///   - Range: [-16,384, +16,352] ft/min
159    ///   - Special values: 0 or 511 = no rate information (returns 0)
160    ///
161    /// Returns None if status bit is 0, returns Some(0) if value is 0 or 511.  
162    /// Implementation validates abs(rate) ≤ 6,000 ft/min.  
163    /// Source: Air Data System or Inertial Reference System/FMS.  
164    /// Note: Usually unsteady and may suffer from barometric instrument inertia.
165    #[deku(reader = "read_vertical(deku::reader)")]
166    #[serde(
167        rename = "vrate_barometric",
168        skip_serializing_if = "Option::is_none"
169    )]
170    pub barometric_altitude_rate: Option<i16>,
171
172    /// Inertial Vertical Velocity (bits 46-56): Per ICAO Doc 9871 Table A-2-96  
173    /// Vertical velocity from inertial/navigational equipment in ft/min.  
174    /// Encoding details:
175    ///   - Bit 46: Status (0=invalid, 1=valid)
176    ///   - Bit 47: Sign (0=climbing, 1=descending)
177    ///   - Bits 48-56: 9-bit vertical velocity magnitude
178    ///   - MSB = 8,192 ft/min
179    ///   - LSB = 32 ft/min (8192/256)
180    ///   - Formula: velocity = value × 32 ft/min (two's complement)
181    ///   - Range: [-16,384, +16,352] ft/min
182    ///   - Special values: 0 or 511 = no velocity information (returns 0)
183    ///
184    /// Returns None if status bit is 0, returns Some(0) if value is 0 or 511.  
185    /// Implementation validates abs(velocity) ≤ 6,000 ft/min.  
186    /// Sources: FMC/GNSS integrated, FMC (General), or IRS/FMS.  
187    /// Note: More filtered and smooth than barometric rate. When barometric
188    /// altitude rate is integrated and smoothed with inertial data
189    /// (baro-inertial), it shall be transmitted in this field.
190    #[deku(reader = "read_vertical(deku::reader)")]
191    #[serde(
192        rename = "vrate_inertial",
193        skip_serializing_if = "Option::is_none"
194    )]
195    pub inertial_vertical_velocity: Option<i16>,
196}
197
198fn read_heading<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
199    reader: &mut Reader<R>,
200) -> Result<Option<f64>, DekuError> {
201    let status = bool::from_reader_with_ctx(
202        reader,
203        (deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
204    )?;
205    let sign = u8::from_reader_with_ctx(
206        reader,
207        (deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
208    )?;
209    let value = u16::from_reader_with_ctx(
210        reader,
211        (deku::ctx::Endian::Big, deku::ctx::BitSize(10)),
212    )?;
213
214    if !status {
215        if (sign != 0) | (value != 0) {
216            return Err(DekuError::Assertion(
217                "Non-null value with invalid status: heading".into(),
218            ));
219        } else {
220            return Ok(None);
221        }
222    }
223
224    let value = if sign == 1 {
225        value as i16 - 1024
226    } else {
227        value as i16
228    };
229    let mut heading = value as f64 * 90. / 512.;
230    if heading < 0. {
231        heading += 360.
232    }
233
234    Ok(Some(heading))
235}
236
237fn read_ias<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
238    reader: &mut Reader<R>,
239) -> Result<Option<u16>, DekuError> {
240    let status = bool::from_reader_with_ctx(
241        reader,
242        (deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
243    )?;
244    let value = u16::from_reader_with_ctx(
245        reader,
246        (deku::ctx::Endian::Big, deku::ctx::BitSize(10)),
247    )?;
248
249    if !status {
250        if value != 0 {
251            return Err(DekuError::Assertion(
252                "Non-null value with invalid status: IAS".into(),
253            ));
254        } else {
255            return Ok(None);
256        }
257    }
258
259    if (value == 0) | (value > 500) {
260        return Err(DekuError::Assertion(
261            format!("IAS value {value} is equal to 0 or greater than 500")
262                .into(),
263        ));
264    }
265    Ok(Some(value))
266}
267
268fn read_mach<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
269    reader: &mut Reader<R>,
270    ias: Option<u16>,
271) -> Result<Option<f64>, DekuError> {
272    let status = bool::from_reader_with_ctx(
273        reader,
274        (deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
275    )?;
276    let value = u16::from_reader_with_ctx(
277        reader,
278        (deku::ctx::Endian::Big, deku::ctx::BitSize(10)),
279    )?;
280
281    if !status {
282        if value != 0 {
283            return Err(DekuError::Assertion(
284                "Non-null value with invalid status: Mach".into(),
285            ));
286        } else {
287            return Ok(None);
288        }
289    }
290
291    let mach = value as f64 * 2.048 / 512.;
292
293    if (mach == 0.) | (mach > 1.) {
294        return Err(DekuError::Assertion(
295            format!("Mach value {mach} equal to 0 or greater than 1 ").into(),
296        ));
297    }
298    if let Some(ias) = ias {
299        /*
300         * >>> pitot.aero.cas2mach(250, 10000)
301         * 0.45229071380275554
302         *
303         * Let's do this:
304         * 10000 ft has IAS max to 250, i.e. Mach 0.45
305         * forbid IAS > 250 and Mach < 0.5
306         */
307        if (ias > 250) & (mach < 0.4) {
308            return Err(DekuError::Assertion(
309                format!(
310                    "IAS: {ias} and Mach: {mach} (250kts is Mach 0.45 at 10,000 ft)"
311                )
312                .into(),
313            ));
314        }
315        // this one is easy IAS = 150 (close to take-off) at FL 400 is Mach 0.5
316        if (ias < 150) & (mach > 0.5) {
317            return Err(DekuError::Assertion(
318                format!(
319                    "IAS: {ias} and Mach: {mach} (150kts is Mach 0.5 at FL400)"
320                )
321                .into(),
322            ));
323        }
324    }
325    Ok(Some(mach))
326}
327
328fn read_vertical<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
329    reader: &mut Reader<R>,
330) -> Result<Option<i16>, DekuError> {
331    let status = bool::from_reader_with_ctx(
332        reader,
333        (deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
334    )?;
335    let sign = u8::from_reader_with_ctx(
336        reader,
337        (deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
338    )?;
339    let value = u16::from_reader_with_ctx(
340        reader,
341        (deku::ctx::Endian::Big, deku::ctx::BitSize(9)),
342    )?;
343
344    if !status {
345        if (sign != 0) | (value != 0) {
346            return Err(DekuError::Assertion(
347                "Non-null value with invalid status: vertical rate".into(),
348            ));
349        } else {
350            return Ok(None);
351        }
352    }
353
354    if (value == 0) | (value == 511) {
355        // all zeros or all ones
356        return Ok(Some(0));
357    }
358    let value = if sign == 1 {
359        (value as i16 - 512) * 32
360    } else {
361        value as i16 * 32
362    };
363
364    if value.abs() > 6000 {
365        Err(DekuError::Assertion(
366            format!("Vertical rate absolute value {} > 6000", value.abs())
367                .into(),
368        ))
369    } else {
370        Ok(Some(value))
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::prelude::*;
378    use approx::assert_relative_eq;
379    use hexlit::hex;
380
381    #[test]
382    fn test_valid_bds60() {
383        let bytes = hex!("a80004aaa74a072bfdefc1d5cb4f");
384        let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
385        if let CommBIdentityReply { bds, .. } = msg.df {
386            let HeadingAndSpeedReport {
387                magnetic_heading,
388                indicated_airspeed,
389                mach_number,
390                barometric_altitude_rate,
391                inertial_vertical_velocity,
392            } = bds.bds60.unwrap();
393            assert_relative_eq!(
394                magnetic_heading.unwrap(),
395                110.391,
396                max_relative = 1e-3
397            );
398            assert_eq!(indicated_airspeed.unwrap(), 259);
399            assert_relative_eq!(mach_number.unwrap(), 0.7, max_relative = 1e-3);
400            assert_eq!(barometric_altitude_rate.unwrap(), -2144);
401            assert_eq!(inertial_vertical_velocity.unwrap(), -2016);
402        } else {
403            unreachable!();
404        }
405    }
406    #[test]
407    fn test_invalid_bds60() {
408        let bytes = hex!("a0000638fa81c10000000081a92f");
409        let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
410        if let CommBAltitudeReply { bds, .. } = msg.df {
411            assert_eq!(bds.bds60, None);
412        } else {
413            unreachable!();
414        }
415    }
416}