Skip to main content

dvb_bbframe/
pump.rs

1//! [`BbframePump`] — per-PLP BBFrame→inner-TS pump.
2//!
3//! BBFrame user-packet framing per EN 302 755 §5.1.7 (BBHEADER) / §5.1.8
4//! (user-packet carriage, SYNCD); composes [`crate::header::Bbheader`] and the
5//! [`crate::packet::CarryOverExtractor`].
6//!
7//! Packages the BBFrame→inner-TS extraction chain that consumers otherwise
8//! hand-wire: parse the BBHEADER, detect the mode, run carry-over extraction
9//! keyed by PLP id, and return the completed 188-byte TS packets per frame.
10//!
11//! Zero dependencies on `dvb-t2mi` — the pump takes already-unwrapped BBFrame
12//! data-field bytes (`df_bytes` = BBHEADER + data field, as
13//! `dvb_t2mi::AnyPayload::Bbframe`'s `bbframe` field yields).
14
15use crate::header::{Bbheader, Mode, TsGs, BBHEADER_LEN};
16use crate::packet::{CarryOverExtractor, CarryOverStats, NM_UP_SIZE};
17
18const MAX_PLPS: usize = 256;
19
20/// Per-feed diagnostic counters for a [`BbframePump`].
21///
22/// The pump stays resilient (it never errors or panics on malformed input —
23/// bad frames are skipped so a stream keeps flowing). These counters make the
24/// otherwise-silent skips observable.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct BbframePumpStats {
28    /// Frames whose BBHEADER could not be parsed or was too short.
29    pub header_parse_failures: u64,
30    /// Frames with a non-TS MATYPE (GSE, GFPS, GCS) — the pump only extracts
31    /// TS user packets.
32    pub non_ts_payloads: u64,
33    /// Aggregated carry-over stats from all PLPs.
34    pub carry_over: CarryOverStats,
35}
36
37/// Per-PLP BBFrame→inner-TS pump.
38///
39/// Feed one PLP's BBFrame data-field bytes at a time with [`feed`](Self::feed);
40/// the pump parses the BBHEADER, detects NM vs HEM, and runs the per-PLP
41/// [`CarryOverExtractor`] — interleaved PLPs keep independent carry-over state.
42///
43/// The returned slice borrows an internal buffer that is cleared on every call;
44/// copy out anything you need to keep.
45///
46/// ```no_run
47/// # fn main() {
48/// use dvb_bbframe::pump::BbframePump;
49///
50/// let mut pump = BbframePump::new();
51/// // Receive a BBFrame data-field byte slice (BBHEADER + data field)
52/// // for PLP 5:
53/// let df_bytes = vec![0u8; 200];
54/// let inner_ts_packets = pump.feed(5, &df_bytes);
55/// for pkt in inner_ts_packets {
56///     println!("inner TS packet recovered");
57/// }
58/// # }
59/// ```
60pub struct BbframePump {
61    /// Per-PLP extractors, indexed by `plp_id`.  A `None` entry means that PLP
62    /// has not been seen yet (lazily created on first `feed`).
63    extractors: [Option<CarryOverExtractor>; MAX_PLPS],
64    /// Output buffer — cleared per `feed` call.
65    out: Vec<[u8; NM_UP_SIZE]>,
66    /// Per-PLP temporary buffer reused across frames.
67    up_buf: Vec<[u8; NM_UP_SIZE]>,
68    /// Diagnostic counters.
69    stats: BbframePumpStats,
70}
71
72impl BbframePump {
73    /// Create a fresh pump with no PLP state.
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            extractors: std::array::from_fn(|_| None),
78            out: Vec::new(),
79            up_buf: Vec::new(),
80            stats: BbframePumpStats::default(),
81        }
82    }
83
84    /// Feed one PLP's BBFrame data-field bytes (`df_bytes` = 10-byte BBHEADER
85    /// + data field).
86    ///
87    /// Returns the inner 188-byte TS packets completed by this frame.
88    ///
89    /// Per-PLP carry-over state is keyed by `plp_id` so interleaved PLPs don't
90    /// corrupt each other's carry-over.  A new `plp_id` lazily creates a
91    /// fresh extractor.
92    ///
93    /// This method is **infallible**: malformed input, bad BBHEADERs, and
94    /// non-TS payloads emit no packets and bump a stat counter — never panics,
95    /// never errors out.
96    pub fn feed(&mut self, plp_id: u8, df_bytes: &[u8]) -> &[[u8; NM_UP_SIZE]] {
97        self.out.clear();
98
99        // ── Validate BBHEADER length ──────────────────────────────────────
100        if df_bytes.len() < BBHEADER_LEN {
101            self.stats.header_parse_failures += 1;
102            return &self.out;
103        }
104
105        // ── Parse BBHEADER ────────────────────────────────────────────────
106        let hdr = match Bbheader::parse(df_bytes) {
107            Ok(h) => h,
108            Err(_) => {
109                self.stats.header_parse_failures += 1;
110                return &self.out;
111            }
112        };
113
114        // ── Non-TS payload → skip ─────────────────────────────────────────
115        if hdr.matype.ts_gs != TsGs::Ts {
116            self.stats.non_ts_payloads += 1;
117            return &self.out;
118        }
119
120        let header_bytes: [u8; BBHEADER_LEN] = match df_bytes[..BBHEADER_LEN].try_into() {
121            Ok(b) => b,
122            Err(_) => {
123                self.stats.header_parse_failures += 1;
124                return &self.out;
125            }
126        };
127        let data_field = &df_bytes[BBHEADER_LEN..];
128
129        // ── Get or create per-PLP extractor ───────────────────────────────
130        let idx = plp_id as usize;
131        let extractor = self.extractors[idx].get_or_insert_with(CarryOverExtractor::new);
132
133        // ── Dispatch by mode ──────────────────────────────────────────────
134        match hdr.mode {
135            Mode::Normal => {
136                extractor.feed_nm_into(&header_bytes, data_field, &mut self.up_buf);
137            }
138            Mode::HighEfficiency => {
139                // HEM with NPD is handled by CarryOverExtractor (npd_unsupported
140                // stat bump); we pass npd through and the extractor skips.
141                extractor.feed_hem_into(
142                    &header_bytes,
143                    data_field,
144                    hdr.matype.npd,
145                    &mut self.up_buf,
146                );
147            }
148        }
149
150        self.out.append(&mut self.up_buf);
151        &self.out
152    }
153
154    /// Diagnostic counters accumulated across all `feed` calls.
155    ///
156    /// The per-PLP `carry_over` field aggregates the stats from all
157    /// PLP extractors; check `carry_over.npd_unsupported` in particular —
158    /// a non-zero value means valid HEM frames were dropped (NPD reinsertion
159    /// unsupported).
160    #[must_use]
161    pub fn stats(&self) -> BbframePumpStats {
162        let mut carry_over = CarryOverStats::default();
163        for ext in self.extractors.iter().flatten() {
164            let s = ext.stats();
165            carry_over.npd_unsupported += s.npd_unsupported;
166            carry_over.header_parse_failures += s.header_parse_failures;
167            carry_over.mode_mismatches += s.mode_mismatches;
168            carry_over.partial_discards += s.partial_discards;
169        }
170        BbframePumpStats {
171            header_parse_failures: self.stats.header_parse_failures,
172            non_ts_payloads: self.stats.non_ts_payloads,
173            carry_over,
174        }
175    }
176}
177
178impl Default for BbframePump {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::crc::crc8;
188    use crate::header::{Matype, TsGs};
189
190    const TS_SYNC: u8 = 0x47;
191    const TS_LEN: usize = NM_UP_SIZE;
192
193    /// One inner TS packet: PID 0x0100, PUSI, all-0xAA payload (distinguishable).
194    fn inner_packet() -> [u8; TS_LEN] {
195        let mut p = [0xAAu8; TS_LEN];
196        p[0] = TS_SYNC;
197        p[1] = 0x41; // PUSI | PID hi = 0x0100
198        p[2] = 0x00;
199        p[3] = 0x10; // payload only
200        p
201    }
202
203    /// Build a Normal-Mode BBFrame (BBHEADER + data field) containing one TS packet.
204    fn nm_bbframe(inner: &[u8; TS_LEN]) -> Vec<u8> {
205        let hdr = Bbheader {
206            matype: Matype {
207                ts_gs: TsGs::Ts,
208                sis: true,
209                ccm: true,
210                issyi: false,
211                npd: false,
212                ext: 0,
213                isi: 0,
214            },
215            upl: 1504,
216            sync: TS_SYNC,
217            dfl: 1504,
218            syncd: 0,
219            mode: Mode::Normal,
220            issy_in_header: None,
221        };
222        let mut frame = hdr.serialize().to_vec();
223        let mut data = [0u8; TS_LEN];
224        data[0] = crc8(&[0u8; TS_LEN]);
225        data[1..].copy_from_slice(&inner[1..]);
226        frame.extend_from_slice(&data);
227        frame
228    }
229
230    /// Build a HEM BBFrame (no NPD) containing one TS packet (187 bytes + prepend sync).
231    fn hem_bbframe(inner: &[u8; TS_LEN]) -> Vec<u8> {
232        let hdr = Bbheader {
233            matype: Matype {
234                ts_gs: TsGs::Ts,
235                sis: true,
236                ccm: true,
237                issyi: false,
238                npd: false,
239                ext: 0,
240                isi: 0,
241            },
242            upl: 0,
243            sync: 0,
244            dfl: (crate::packet::HEM_UP_SIZE * 8) as u16,
245            syncd: 0,
246            mode: Mode::HighEfficiency,
247            issy_in_header: None,
248        };
249        let mut frame = hdr.serialize().to_vec();
250        // HEM data: 187 bytes = inner[1..188] (no sync byte)
251        frame.extend_from_slice(&inner[1..]);
252        frame
253    }
254
255    // ══════════════════════════════════════════════════════════════════════
256    // NM frame tests
257    // ══════════════════════════════════════════════════════════════════════
258
259    #[test]
260    fn nm_frame_yields_inner_ts_packet() {
261        let inner = inner_packet();
262        let frame = nm_bbframe(&inner);
263
264        let mut pump = BbframePump::new();
265        let pkts = pump.feed(0, &frame);
266        assert_eq!(pkts.len(), 1, "exactly one inner TS packet expected");
267        assert_eq!(pkts[0][0], TS_SYNC, "sync byte restored");
268        assert_eq!(&pkts[0][1..], &inner[1..]);
269    }
270
271    #[test]
272    fn two_nm_frames_yield_two_packets() {
273        let inner = inner_packet();
274        let frame = nm_bbframe(&inner);
275
276        let mut pump = BbframePump::new();
277        let pkts1 = pump.feed(0, &frame).to_vec();
278        let pkts2 = pump.feed(0, &frame).to_vec();
279        assert_eq!(pkts1.len(), 1);
280        assert_eq!(pkts2.len(), 1);
281    }
282
283    // ══════════════════════════════════════════════════════════════════════
284    // HEM frame tests
285    // ══════════════════════════════════════════════════════════════════════
286
287    #[test]
288    fn hem_frame_yields_inner_ts_packet() {
289        let inner = inner_packet();
290        let frame = hem_bbframe(&inner);
291
292        let mut pump = BbframePump::new();
293        let pkts = pump.feed(0, &frame);
294        assert_eq!(pkts.len(), 1, "exactly one inner TS packet expected");
295        assert_eq!(pkts[0][0], TS_SYNC, "sync byte prepended");
296        // Bytes 1..188 match the original (minus the sync byte which is prepended).
297        assert_eq!(&pkts[0][1..], &inner[1..]);
298    }
299
300    // ══════════════════════════════════════════════════════════════════════
301    // Per-PLP interleaving
302    // ══════════════════════════════════════════════════════════════════════
303
304    #[test]
305    fn interleaved_plps_keep_independent_carry_over() {
306        let inner = inner_packet();
307        let nm = nm_bbframe(&inner);
308
309        let mut pump = BbframePump::new();
310
311        // Feed PLP 0 twice, PLP 5 once.
312        let pkts_0a = pump.feed(0, &nm).to_vec();
313        let pkts_5 = pump.feed(5, &nm).to_vec();
314        let pkts_0b = pump.feed(0, &nm).to_vec();
315
316        assert_eq!(pkts_0a.len(), 1);
317        assert_eq!(pkts_5.len(), 1);
318        assert_eq!(pkts_0b.len(), 1);
319
320        // Both PLPs produced a packet → independent extractors.
321        assert_eq!(pkts_0a[0], pkts_0b[0]);
322        assert_eq!(pkts_5[0], inner);
323    }
324
325    // ══════════════════════════════════════════════════════════════════════
326    // Malformed input → stat bump, no panic
327    // ══════════════════════════════════════════════════════════════════════
328
329    #[test]
330    fn short_df_bytes_bumps_header_parse_failures() {
331        let mut pump = BbframePump::new();
332        let pkts = pump.feed(0, &[0u8; 5]); // shorter than BBHEADER_LEN
333        assert!(pkts.is_empty());
334        assert_eq!(pump.stats().header_parse_failures, 1);
335    }
336
337    #[test]
338    fn bad_bbheader_bumps_header_parse_failures() {
339        let mut pump = BbframePump::new();
340        let bad = [0xFFu8; BBHEADER_LEN + 10]; // CRC will fail → Bbheader::parse returns Err
341        let pkts = pump.feed(0, &bad);
342        assert!(pkts.is_empty());
343        assert_eq!(pump.stats().header_parse_failures, 1);
344    }
345
346    #[test]
347    fn non_ts_matype_bumps_non_ts_payloads() {
348        // Build a NM BBFrame with GSE (non-TS) MATYPE.
349        let hdr = Bbheader {
350            matype: Matype {
351                ts_gs: TsGs::Gse,
352                sis: true,
353                ccm: true,
354                issyi: false,
355                npd: false,
356                ext: 0,
357                isi: 0,
358            },
359            upl: 0,
360            sync: 0,
361            dfl: 100,
362            syncd: 0,
363            mode: Mode::Normal,
364            issy_in_header: None,
365        };
366        let mut frame = hdr.serialize().to_vec();
367        frame.extend_from_slice(&[0u8; 50]);
368
369        let mut pump = BbframePump::new();
370        let pkts = pump.feed(0, &frame);
371        assert!(pkts.is_empty());
372        assert_eq!(pump.stats().non_ts_payloads, 1);
373        assert_eq!(pump.stats().header_parse_failures, 0); // header itself parsed fine
374    }
375
376    #[test]
377    fn garbage_no_panic_no_output() {
378        let mut pump = BbframePump::new();
379        // Byte 9 is the CRC-8 XOR MODE byte; crc8([0u8; 9]) = 0, so 0xFF
380        // gives MODE = 0xFF which is neither 0 (NM) nor 1 (HEM) →
381        // Bbheader::parse fails → header_parse_failures bumped.
382        let mut junk = [0u8; 200];
383        junk[9] = 0xFF;
384        assert!(pump.feed(0, &junk).is_empty());
385        assert!(pump.stats().header_parse_failures > 0);
386    }
387
388    // ══════════════════════════════════════════════════════════════════════
389    // Stats aggregation
390    // ══════════════════════════════════════════════════════════════════════
391
392    #[test]
393    fn stats_aggregates_across_plps() {
394        let inner = inner_packet();
395        let nm = nm_bbframe(&inner);
396
397        let mut pump = BbframePump::new();
398
399        // Feed PLP 0 and PLP 1
400        pump.feed(0, &nm);
401        pump.feed(1, &nm);
402
403        let s = pump.stats();
404        // Each CarryOverExtractor fed one NM frame with no issues.
405        assert_eq!(s.carry_over.header_parse_failures, 0);
406        assert_eq!(s.carry_over.mode_mismatches, 0);
407    }
408}