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