Skip to main content

rs1090/decode/bds/
bds08.rs

1use deku::prelude::*;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use tracing::{debug, trace};
5
6/**
7 * ## Aircraft Identification and Category (BDS 0,8)
8 *
9 * Extended squitter message providing aircraft callsign and wake vortex category.  
10 * Per ICAO Doc 9871 Table A-2-8: BDS code 0,8 — Extended squitter aircraft
11 * identification and category
12 *
13 * Message Structure (56 bits):
14 * | TC  | CA  | C1  | C2  | C3  | C4  | C5  | C6  | C7  | C8  |
15 * | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
16 * | 5   | 3   | 6   | 6   | 6   | 6   | 6   | 6   | 6   | 6   |
17 *
18 * Field Encoding per ICAO Doc 9871 §A.2.3.4:
19 * - TC (bits 1-5): Format Type Code, determines category set
20 *   * TC=1 (D): Reserved
21 *   * TC=2 (C): Surface vehicles and obstructions (category set C)
22 *   * TC=3 (B): Aircraft without engines - gliders, ultralights, etc. (set B)
23 *   * TC=4 (A): Normal aircraft (category set A)
24 * - CA (bits 6-8): Aircraft/Vehicle Category (3 bits)
25 *   * Combined with TC to determine wake vortex category
26 * - C1-C8 (bits 9-56): Eight 6-bit characters encoding callsign
27 *
28 * Character Encoding per Annex 10 Vol IV Table 3-8:
29 * - 6-bit subset of IA-5 (International Alphabet #5)
30 * - Supports: A-Z (letters), 0-9 (digits), space (0x20)
31 * - Special character '#' at position 0
32 * - Trailing spaces typically omitted from callsign string
33 * - Callsign should be flight plan identification or aircraft registration
34 *
35 * Wake Vortex Categories (TC=4, Category Set A):
36 * - CA=0: No category information
37 * - CA=1: Light (< 7,031 kg / 15,500 lbs) → ICAO WTC L
38 * - CA=2: Medium 1 (7,031 to 34,019 kg) → ICAO WTC M
39 * - CA=3: Medium 2 (34,019 to 136,078 kg) → ICAO WTC M
40 * - CA=4: High vortex aircraft
41 * - CA=5: Heavy (> 136,078 kg / 300,000 lbs) → ICAO WTC H/J
42 * - CA=6: High performance (>5g accel, >400 kt)
43 * - CA=7: Rotorcraft
44 *
45 * Note: ADS-B wake vortex categories differ from ICAO WTC definitions.
46 */
47
48#[derive(Debug, PartialEq, DekuRead, Serialize, Deserialize, Clone)]
49#[deku(ctx = "id: u8")]
50pub struct AircraftIdentification {
51    /// Type Code Category (bits 1-5): Per ICAO Doc 9871 Table A-2-8
52    #[serde(skip)]
53    #[deku(skip, default = "Typecode::try_from(id)?")]
54    pub tc: Typecode,
55
56    /// Aircraft/Vehicle Category (bits 6-8): Per ICAO Doc 9871 Table A-2-8  
57    /// 3-bit field combined with TC to determine wake vortex category.  
58    /// See wake_vortex() function for complete category mapping.
59    #[deku(bits = "3")]
60    #[serde(skip)]
61    pub ca: u8,
62
63    /// Wake Vortex Category: Derived from TC and CA fields  
64    /// Per ICAO Doc 9871 Table A-2-8 category sets A, B, C, D.  
65    /// Note: ADS-B categories differ from ICAO Wake Turbulence Category (WTC).
66    #[deku(reader = "wake_vortex(*tc, *ca)")]
67    pub wake_vortex: WakeVortex,
68
69    /// Callsign (bits 9-56): Aircraft identification per ICAO Doc 9871 Table A-2-32  
70    /// Eight 6-bit characters using IA-5 subset (Annex 10 Vol IV Table 3-8).
71    ///
72    /// Character set: A-Z, 0-9, space (0x20), and '#' (position 0).  
73    /// Should contain flight plan identification or aircraft registration.  
74    /// Trailing spaces are typically omitted from the decoded string.
75    #[deku(reader = "callsign_read(deku::reader)")]
76    pub callsign: String,
77}
78
79#[derive(Debug, PartialEq, Copy, Clone, Default)]
80/// Type Code Category (bits 1-5): Per ICAO Doc 9871 Table A-2-8  
81/// Determines the category set:
82///   - D (TC=1): Reserved
83///   - C (TC=2): Surface vehicles and obstructions
84///   - B (TC=3): Aircraft without engines (gliders, ultralights, etc.)
85///   - A (TC=4): Normal aircraft (most common)
86pub enum Typecode {
87    /// Reserved
88    D = 1,
89    /// Ground vehicles
90    C = 2,
91    /// Without an engine (glider, hangglider, etc.)
92    B = 3,
93    /// Aircraft
94    #[default]
95    A = 4,
96}
97
98impl fmt::Display for Typecode {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(
101            f,
102            "{}",
103            match self {
104                Self::D => "D: Reserved",
105                Self::C => "C: Surface vehicles",
106                Self::B => "B: Without an engine",
107                Self::A => "A: Aircraft",
108            }
109        )
110    }
111}
112
113use std::convert::TryFrom;
114
115impl TryFrom<u8> for Typecode {
116    type Error = DekuError;
117
118    fn try_from(value: u8) -> Result<Self, Self::Error> {
119        match value {
120            1 => Ok(Self::D),
121            2 => Ok(Self::C),
122            3 => Ok(Self::B),
123            4 => Ok(Self::A),
124            _ => Err(DekuError::InvalidParam(
125                "Invalid value for Typecode".into(),
126            )),
127        }
128    }
129}
130
131/**
132 * Decode wake vortex category from Type Code (TC) and Category (CA)
133 *
134 * Per ICAO Doc 9871 Table A-2-8: Aircraft/vehicle category coding
135 *
136 * The CA value (3 bits) combined with TC value determines wake vortex category.
137 * Four category sets are defined:
138 *
139 * **Set A (TC=4)**: Normal aircraft
140 * - CA=0: No information
141 * - CA=1: Light (< 7,031 kg / 15,500 lbs)
142 * - CA=2: Medium 1 (7,031 to 34,019 kg / 15,500 to 75,000 lbs)
143 * - CA=3: Medium 2 (34,019 to 136,078 kg / 75,000 to 300,000 lbs)
144 * - CA=4: High vortex aircraft
145 * - CA=5: Heavy (> 136,078 kg / 300,000 lbs)
146 * - CA=6: High performance (>5g acceleration and >400 kt speed)
147 * - CA=7: Rotorcraft
148 *
149 * **Set B (TC=3)**: Aircraft without engines
150 * - CA=0: No information
151 * - CA=1: Glider/sailplane
152 * - CA=2: Lighter-than-air
153 * - CA=3: Parachutist/skydiver
154 * - CA=4: Ultralight/hang-glider/paraglider
155 * - CA=5: Reserved
156 * - CA=6: Unmanned aerial vehicle
157 * - CA=7: Space/transatmospheric vehicle
158 *
159 * **Set C (TC=2)**: Surface vehicles
160 * - CA=0: No information
161 * - CA=1: Surface emergency vehicle
162 * - CA=2: (Reserved/Obstruction)
163 * - CA=3: Surface service vehicle
164 * - CA=4-7: Fixed ground or tethered obstruction
165 *
166 * **Set D (TC=1)**: Reserved
167 *
168 * Note: ADS-B wake vortex categories differ from ICAO Wake Turbulence
169 * Category (WTC) definitions:
170 * - ICAO WTC L ≈ ADS-B (TC=4, CA=1)
171 * - ICAO WTC M ≈ ADS-B (TC=4, CA=2 or CA=3)
172 * - ICAO WTC H/J ≈ ADS-B (TC=4, CA=5)
173 */
174#[derive(Debug, PartialEq, Serialize, Deserialize, Copy, Clone)]
175pub enum WakeVortex {
176    Reserved,
177
178    // Category C
179    #[serde(rename = "n/a")]
180    /// No category information
181    NoInformation,
182    #[serde(rename = "Surface emergency vehicle")]
183    /// Surface emergency vehicle
184    EmergencyVehicle,
185    #[serde(rename = "Surface service vehicle")]
186    /// Surface service vehicle
187    ServiceVehicle,
188    /// Ground obstruction
189    Obstruction,
190
191    // Category B
192    /// Glider, sailplane
193    Glider,
194    #[serde(rename = "Lighter than air")]
195    /// Lighter than air
196    Lighter,
197    /// Parachutist, Skydiver
198    Parachutist,
199    /// Ultralight, hang-glider, paraglider
200    Ultralight,
201    #[serde(rename = "UAV")]
202    /// Unmanned Air Vehicle
203    Unmanned,
204    /// Space or transatmospheric vehicle
205    Space,
206
207    // Category A
208    #[serde(rename = "<7000kg")]
209    /// Light (< 7,000 kg)
210    Light,
211    #[serde(rename = "<34,000kg")]
212    /// Medium1 (7,000kg to 34,000kg)
213    Medium1,
214    #[serde(rename = "<136,000kg")]
215    /// Medium2 (34,000kg to 136,000kg)
216    Medium2,
217    #[serde(rename = "High vortex")]
218    /// High vortex aircraft
219    HighVortex,
220    /// Heavy (> 136,000 kg)
221    Heavy,
222    #[serde(rename = "High performance")]
223    /// High performance (>5 g acceleration and >400 kt)
224    HighPerformance,
225    /// Rotorcraft
226    Rotorcraft,
227}
228
229impl fmt::Display for WakeVortex {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        let string = match &self {
232            Self::Reserved => "Reserved",
233            Self::NoInformation => "No category information",
234            Self::EmergencyVehicle => "Surface Emergency Vehicle",
235            Self::ServiceVehicle => "Surface Service Vehicle",
236            Self::Obstruction => "Ground Obstruction",
237            Self::Glider => "Glider, sailplane",
238            Self::Lighter => "Lighter than air",
239            Self::Parachutist => "Parachutist, Skydiver",
240            Self::Ultralight => "Ultralight, hang-glider, paraglider",
241            Self::Unmanned => "Unmanned Air Vehicle",
242            Self::Space => "Space or transatmospheric vehicle",
243            Self::Light => "Light (less than 7000 kg)",
244            Self::Medium1 => "Medium 1 (between 7000 kg and 34000 kg)",
245            Self::Medium2 => "Medium 2 (between 34000 kg to 136000 kg)",
246            Self::HighVortex => "High vortex aircraft",
247            Self::Heavy => "Heavy (larger than 136000 kg)",
248            Self::HighPerformance => {
249                "High performance (>5 g acceleration) and high speed (>400 kt)"
250            }
251            Self::Rotorcraft => "Rotorcraft",
252        };
253        write!(f, "{string}")?;
254        Ok(())
255    }
256}
257
258/// Decode wake vortex category from Type Code (TC) and Category (CA)
259pub fn wake_vortex(tc: Typecode, ca: u8) -> Result<WakeVortex, DekuError> {
260    let wake_vortex = match (tc, ca) {
261        (Typecode::D, _) => WakeVortex::Reserved,
262        (_, 0) => WakeVortex::NoInformation,
263        (Typecode::C, 1) => WakeVortex::EmergencyVehicle,
264        (Typecode::C, 3) => WakeVortex::ServiceVehicle,
265        (Typecode::C, _) => WakeVortex::Obstruction,
266        (Typecode::B, 1) => WakeVortex::Glider,
267        (Typecode::B, 2) => WakeVortex::Lighter,
268        (Typecode::B, 3) => WakeVortex::Parachutist,
269        (Typecode::B, 4) => WakeVortex::Ultralight,
270        (Typecode::B, 5) => WakeVortex::Reserved,
271        (Typecode::B, 6) => WakeVortex::Unmanned,
272        (Typecode::B, 7) => WakeVortex::Space,
273        (Typecode::A, 1) => WakeVortex::Light,
274        (Typecode::A, 2) => WakeVortex::Medium1,
275        (Typecode::A, 3) => WakeVortex::Medium2,
276        (Typecode::A, 4) => WakeVortex::HighVortex,
277        (Typecode::A, 5) => WakeVortex::Heavy,
278        (Typecode::A, 6) => WakeVortex::HighPerformance,
279        (Typecode::A, 7) => WakeVortex::Rotorcraft,
280        _ => WakeVortex::Reserved, // only 3 bits anyway
281    };
282    Ok(wake_vortex)
283}
284
285/// Character lookup table for 6-bit IA-5 subset encoding
286///
287/// Per Annex 10 Volume IV Table 3-8: Character coding for aircraft identification
288///
289/// 6-bit encoding (64 possible values):
290/// - 0x00 (000000): '#'  
291/// - 0x01-0x1A: 'A'-'Z' (letters)
292/// - 0x20 (100000): ' ' (space, used for padding)
293/// - 0x30-0x39: '0'-'9' (digits)
294/// - Other positions: '#' (invalid/reserved)
295///
296/// This is a subset of the IA-5 (International Alphabet #5) character set,
297/// optimized for aircraft callsign transmission.
298const CHAR_LOOKUP: &[u8; 64] =
299    b"#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######";
300
301/// Decode 8-character callsign from 48 bits (8 × 6-bit characters)
302///
303/// Per ICAO Doc 9871 Table A-2-32 and Annex 10 Vol IV Table 3-8
304///
305/// Each character is encoded in 6 bits using IA-5 subset:
306/// - Bits 9-14: Character 1 (MSB)
307/// - Bits 15-20: Character 2
308/// - Bits 21-26: Character 3
309/// - Bits 27-32: Character 4
310/// - Bits 33-38: Character 5
311/// - Bits 39-44: Character 6
312/// - Bits 45-50: Character 7
313/// - Bits 51-56: Character 8 (LSB)
314///
315/// Trailing spaces (0x20) are omitted from the returned string.
316///
317/// Callsign content per ICAO Doc 9871:
318/// - Should be aircraft identification from flight plan
319/// - If no flight plan, use aircraft registration marking
320///
321/// Returns: Decoded callsign string (1-8 characters, trailing spaces removed)
322pub fn callsign_read<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
323    reader: &mut Reader<R>,
324) -> Result<String, DekuError> {
325    let mut chars = vec![];
326    for _ in 1..=8 {
327        let c = u8::from_reader_with_ctx(reader, deku::ctx::BitSize(6))?;
328        trace!("Reading letter {}", CHAR_LOOKUP[c as usize] as char);
329        if c != 32 {
330            chars.push(c);
331        }
332    }
333    let encoded = chars
334        .into_iter()
335        .map(|b| CHAR_LOOKUP[b as usize] as char)
336        .collect::<String>();
337
338    debug!("Reading callsign {}", encoded);
339    Ok(encoded)
340}
341
342impl fmt::Display for AircraftIdentification {
343    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        writeln!(f, "  Aircraft identification and category (BDS 0,8)")?;
345        writeln!(f, "  Callsign:      {}", &self.callsign)?;
346        writeln!(f, "  Category:      {}", &self.wake_vortex)?;
347        Ok(())
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::Typecode;
354    use crate::prelude::*;
355    use hexlit::hex;
356
357    #[test]
358    fn test_callsign() {
359        let bytes = hex!("8d406b902015a678d4d220aa4bda");
360        let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
361        if let ExtendedSquitterADSB(adsb_msg) = msg.df {
362            if let ME::BDS08 {
363                inner:
364                    AircraftIdentification {
365                        tc,
366                        ca,
367                        callsign,
368                        wake_vortex,
369                    },
370                ..
371            } = adsb_msg.message
372            {
373                // Check type code and category directly
374                assert_eq!(tc, Typecode::A);
375                assert_eq!(ca, 0);
376                assert_eq!(format!("{wake_vortex}"), "No category information");
377                assert_eq!(callsign, "EZY85MH");
378                return;
379            }
380        }
381        unreachable!();
382    }
383
384    #[test]
385    fn test_format() {
386        let bytes = hex!("8d406b902015a678d4d220aa4bda");
387        let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
388        assert_eq!(
389            format!("{msg}"),
390            r#" DF17. Extended Squitter
391  Address:       406b90
392  Air/Ground:    airborne
393  Aircraft identification and category (BDS 0,8)
394  Callsign:      EZY85MH
395  Category:      No category information
396"#
397        )
398    }
399}