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}