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}