Skip to main content

dvb_vbi/
data_unit_id.rs

1//! `data_unit_id` interpretation — ETSI EN 301 775 §4.4.2, Table 3.
2//!
3//! The 8-bit `data_unit_id` field identifies the kind of each data unit in the
4//! PES data field, for `data_identifier` in `0x10`–`0x1F` or `0x99`–`0x9B`.
5//! Coded per Table 3 (the authoritative value table).
6//!
7//! ⚠ Table 1's parse branch routes `0x02`, `0x03`, `0xC0`, **and `0xC1`** to
8//! `txt_data_field()`, but Table 3 marks `0xC1` as *reserved → discard*. The
9//! spec is internally inconsistent; this crate treats Table 3 as authoritative,
10//! so `0xC1` is [`DataUnitId::Reserved`] (see `docs/vbi.md`).
11
12/// `data_unit_id` value: EBU Teletext non-subtitle data (`0x02`, Table 3).
13pub const ID_EBU_TELETEXT_NON_SUBTITLE: u8 = 0x02;
14/// `data_unit_id` value: EBU Teletext subtitle data (`0x03`, Table 3).
15pub const ID_EBU_TELETEXT_SUBTITLE: u8 = 0x03;
16/// `data_unit_id` value: Inverted Teletext (`0xC0`, Table 3).
17pub const ID_INVERTED_TELETEXT: u8 = 0xC0;
18/// `data_unit_id` value: VPS (`0xC3`, Table 3).
19pub const ID_VPS: u8 = 0xC3;
20/// `data_unit_id` value: WSS (`0xC4`, Table 3).
21pub const ID_WSS: u8 = 0xC4;
22/// `data_unit_id` value: Closed Captioning (`0xC5`, Table 3).
23pub const ID_CLOSED_CAPTIONING: u8 = 0xC5;
24/// `data_unit_id` value: monochrome 4:2:2 samples (`0xC6`, Table 3).
25pub const ID_MONOCHROME_422_SAMPLES: u8 = 0xC6;
26/// `data_unit_id` value: stuffing (`0xFF`, Table 3).
27pub const ID_STUFFING: u8 = 0xFF;
28
29/// A decoded `data_unit_id` (ETSI EN 301 775 §4.4.2, Table 3).
30///
31/// The named variants carry the typed payloads this crate decodes; everything
32/// else falls into [`DataUnitId::Reserved`] or [`DataUnitId::UserDefined`],
33/// preserving the raw byte. Per Table 3 the spec action for the latter two is
34/// "discard".
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37#[non_exhaustive]
38pub enum DataUnitId {
39    /// `0x02` — EBU Teletext non-subtitle data.
40    EbuTeletextNonSubtitle,
41    /// `0x03` — EBU Teletext subtitle data.
42    EbuTeletextSubtitle,
43    /// `0xC0` — Inverted Teletext.
44    InvertedTeletext,
45    /// `0xC3` — VPS (Video Programme System).
46    Vps,
47    /// `0xC4` — WSS (Wide Screen Signalling).
48    Wss,
49    /// `0xC5` — Closed Captioning (line 21, EIA-608 Rev A).
50    ClosedCaptioning,
51    /// `0xC6` — monochrome 4:2:2 luminance samples.
52    Monochrome422Samples,
53    /// `0xFF` — stuffing (no data field).
54    Stuffing,
55    /// `0x00`–`0x01`, `0x04`–`0x7F`, `0xC1`, `0xC2`, `0xC7`–`0xFE` — reserved
56    /// for future use (Table 3: discard). Carries the raw `data_unit_id`.
57    Reserved(u8),
58    /// `0x80`–`0xBF` — user defined (Table 3: discard). Carries the raw
59    /// `data_unit_id`.
60    UserDefined(u8),
61}
62
63impl DataUnitId {
64    /// Decode a raw 8-bit `data_unit_id` per Table 3.
65    pub fn from_u8(raw: u8) -> Self {
66        match raw {
67            ID_EBU_TELETEXT_NON_SUBTITLE => DataUnitId::EbuTeletextNonSubtitle,
68            ID_EBU_TELETEXT_SUBTITLE => DataUnitId::EbuTeletextSubtitle,
69            ID_INVERTED_TELETEXT => DataUnitId::InvertedTeletext,
70            ID_VPS => DataUnitId::Vps,
71            ID_WSS => DataUnitId::Wss,
72            ID_CLOSED_CAPTIONING => DataUnitId::ClosedCaptioning,
73            ID_MONOCHROME_422_SAMPLES => DataUnitId::Monochrome422Samples,
74            ID_STUFFING => DataUnitId::Stuffing,
75            // 0x80–0xBF is the user-defined range; everything else not named
76            // above is reserved (incl. 0xC1, 0xC2, 0xC7–0xFE).
77            0x80..=0xBF => DataUnitId::UserDefined(raw),
78            other => DataUnitId::Reserved(other),
79        }
80    }
81
82    /// Encode back to the raw 8-bit wire value.
83    pub fn to_u8(self) -> u8 {
84        match self {
85            DataUnitId::EbuTeletextNonSubtitle => ID_EBU_TELETEXT_NON_SUBTITLE,
86            DataUnitId::EbuTeletextSubtitle => ID_EBU_TELETEXT_SUBTITLE,
87            DataUnitId::InvertedTeletext => ID_INVERTED_TELETEXT,
88            DataUnitId::Vps => ID_VPS,
89            DataUnitId::Wss => ID_WSS,
90            DataUnitId::ClosedCaptioning => ID_CLOSED_CAPTIONING,
91            DataUnitId::Monochrome422Samples => ID_MONOCHROME_422_SAMPLES,
92            DataUnitId::Stuffing => ID_STUFFING,
93            DataUnitId::Reserved(v) => v,
94            DataUnitId::UserDefined(v) => v,
95        }
96    }
97
98    /// Spec label for this `data_unit_id` (Table 3).
99    pub fn name(&self) -> &'static str {
100        match self {
101            DataUnitId::EbuTeletextNonSubtitle => "EBU Teletext non-subtitle data",
102            DataUnitId::EbuTeletextSubtitle => "EBU Teletext subtitle data",
103            DataUnitId::InvertedTeletext => "Inverted Teletext",
104            DataUnitId::Vps => "VPS",
105            DataUnitId::Wss => "WSS",
106            DataUnitId::ClosedCaptioning => "Closed Captioning",
107            DataUnitId::Monochrome422Samples => "monochrome 4:2:2 samples",
108            DataUnitId::Stuffing => "stuffing",
109            DataUnitId::Reserved(_) => "reserved",
110            DataUnitId::UserDefined(_) => "user defined",
111        }
112    }
113}
114
115broadcast_common::impl_spec_display!(DataUnitId, Reserved, UserDefined);
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use alloc::string::ToString;
121
122    #[test]
123    fn all_u8_round_trip() {
124        for raw in 0u16..=0xFF {
125            let raw = raw as u8;
126            assert_eq!(DataUnitId::from_u8(raw).to_u8(), raw, "raw={raw:#04X}");
127        }
128    }
129
130    #[test]
131    fn c1_is_reserved_not_teletext() {
132        // ⚠ Table 1 vs Table 3 conflict: Table 3 wins — 0xC1 is reserved.
133        assert_eq!(DataUnitId::from_u8(0xC1), DataUnitId::Reserved(0xC1));
134        assert_eq!(DataUnitId::from_u8(0xC2), DataUnitId::Reserved(0xC2));
135    }
136
137    #[test]
138    fn ranges() {
139        assert_eq!(DataUnitId::from_u8(0x00), DataUnitId::Reserved(0x00));
140        assert_eq!(DataUnitId::from_u8(0x7F), DataUnitId::Reserved(0x7F));
141        assert_eq!(DataUnitId::from_u8(0x80), DataUnitId::UserDefined(0x80));
142        assert_eq!(DataUnitId::from_u8(0xBF), DataUnitId::UserDefined(0xBF));
143        assert_eq!(DataUnitId::from_u8(0xC7), DataUnitId::Reserved(0xC7));
144        assert_eq!(DataUnitId::from_u8(0xFE), DataUnitId::Reserved(0xFE));
145    }
146
147    #[test]
148    fn display_is_lossless_for_byte_bearing() {
149        assert_eq!(DataUnitId::Reserved(0xC1).to_string(), "reserved(0xC1)");
150        assert_eq!(
151            DataUnitId::UserDefined(0x90).to_string(),
152            "user defined(0x90)"
153        );
154        assert_eq!(DataUnitId::Vps.to_string(), "VPS");
155    }
156}