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}