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}