dvb_t2mi/payload/any.rs
1//! Unified payload dispatch: [`AnyPayload`].
2//!
3//! [`AnyPayload`] is generated from a single declarative list
4//! (`declare_payloads!`) — one line per T2-MI payload type.
5//! The list is the single source of truth: it produces the enum, the
6//! `From<T>` conversions, the `packet_type` → parser dispatcher, and a drift
7//! test that pins each literal to the type's
8//! [`crate::traits::PayloadDef::PACKET_TYPE`].
9//!
10//! # Dispatch contract
11//!
12//! [`AnyPayload::dispatch`] takes the **payload bytes only** (the bytes after
13//! the 6-byte T2-MI header, up to but not including the 4-byte CRC trailer).
14//! Each payload parser expects exactly those bytes — the header and CRC are NOT
15//! passed in. To recover the payload slice from a raw packet buffer use
16//! [`crate::packet::Header::payload_bytes`].
17//!
18//! # Adding a payload
19//!
20//! 1. Create the module with the wire layout and the symmetric
21//! [`dvb_common::Parse`] / [`dvb_common::Serialize`] impls + round-trip
22//! tests (copy an existing module).
23//! 2. `impl PayloadDef` for the type (`PACKET_TYPE` from the spec / the
24//! [`crate::packet::PacketType`] enum value, `NAME` in SCREAMING_SNAKE
25//! without the `_payload` suffix).
26//! 3. Add one line to the `declare_payloads!` invocation below — the enum
27//! variant, dispatcher arm, and drift test are generated from it.
28//! 4. The integration completeness test walks the generated
29//! [`AnyPayload::DISPATCHED_TYPES`] automatically — no test edits needed.
30
31/// Declares [`AnyPayload`] + its dispatcher from one packet-type list.
32///
33/// Each line is `Variant = 0xTYPE => module::Type[<'a>]`.
34macro_rules! declare_payloads {
35 (
36 $lt:lifetime;
37 $( $variant:ident = $ptype:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
38 ) => {
39 /// Every crate-implemented T2-MI payload, plus an `Unknown` fallthrough.
40 ///
41 /// serde uses external tagging with camelCase variant keys.
42 /// Variant names map 1:1 to the payload modules; see each module
43 /// for the wire layout.
44 ///
45 /// # Dispatch contract
46 ///
47 /// Use [`AnyPayload::dispatch`] with the payload bytes (post-header,
48 /// pre-CRC). See the module-level documentation for details.
49 #[derive(Debug)]
50 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
51 #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
52 // Covariant in `$lt`: every variant holds only lifetime-parametrised
53 // payload views or `&$lt [u8]` (`Unknown`), so the derive is sound.
54 #[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
55 #[non_exhaustive]
56 pub enum AnyPayload<$lt> {
57 $(
58 #[allow(missing_docs)]
59 $variant($($path)::+ $(<$plt>)?),
60 )+
61 /// Runtime-registered custom payload (see [`PayloadRegistry`]).
62 ///
63 /// [`PayloadRegistry`]: crate::payload::registry::PayloadRegistry
64 Other {
65 /// The raw `packet_type` byte.
66 packet_type: u8,
67 /// The parsed, type-erased payload value. Call `downcast_ref` on
68 /// it (see [`PayloadObject`][crate::payload::registry::PayloadObject]) to recover the concrete type.
69 #[cfg_attr(
70 feature = "serde",
71 serde(serialize_with = "crate::payload::registry::serialize_erased")
72 )]
73 value: Box<dyn crate::payload::registry::PayloadObject>,
74 },
75 /// Packet type with no typed implementation; `body` contains the
76 /// raw payload bytes (post-header, pre-CRC).
77 Unknown {
78 /// The raw `packet_type` byte.
79 packet_type: u8,
80 /// The raw payload bytes.
81 body: &$lt [u8],
82 },
83 }
84
85 $(
86 impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyPayload<$lt> {
87 fn from(p: $($path)::+ $(<$plt>)?) -> Self {
88 Self::$variant(p)
89 }
90 }
91 )+
92
93 impl<$lt> AnyPayload<$lt> {
94 /// Every `packet_type` the generated dispatcher routes (excludes
95 /// [`AnyPayload::Unknown`]).
96 pub const DISPATCHED_TYPES: &'static [u8] = &[$($ptype),+];
97
98 /// Diagnostic name of the contained payload — the type's
99 /// [`PayloadDef::NAME`](crate::traits::PayloadDef::NAME)
100 /// (`"BBFRAME"`, `"L1_CURRENT"`, …); `"CUSTOM"` for
101 /// [`AnyPayload::Other`] (runtime-registered) and `"UNKNOWN"`
102 /// for [`AnyPayload::Unknown`].
103 #[must_use]
104 pub fn name(&self) -> &'static str {
105 match self {
106 $(
107 Self::$variant(_) =>
108 <$($path)::+ as crate::traits::PayloadDef>::NAME,
109 )+
110 Self::Other { .. } => "CUSTOM",
111 Self::Unknown { .. } => "UNKNOWN",
112 }
113 }
114
115 /// Parse one payload by its `packet_type`.
116 ///
117 /// `payload_bytes` must be the **payload-only slice** (bytes after
118 /// the 6-byte T2-MI header, before the 4-byte CRC trailer).
119 ///
120 /// Returns `None` when `packet_type` has no typed implementation
121 /// (the caller turns that into [`AnyPayload::Unknown`]).
122 /// Returns `Some(Err)` on a typed parse failure for a recognised type.
123 ///
124 /// See the [module-level documentation][self] for the dispatch
125 /// contract (payload-only bytes, header and CRC excluded).
126 pub fn dispatch(
127 packet_type: u8,
128 payload_bytes: &$lt [u8],
129 ) -> Option<crate::Result<Self>> {
130 use dvb_common::Parse;
131 match packet_type {
132 $(
133 $ptype => Some(
134 <$($path)::+>::parse(payload_bytes).map(Self::$variant),
135 ),
136 )+
137 _ => None,
138 }
139 }
140 }
141
142 #[cfg(test)]
143 mod macro_drift {
144 #[test]
145 fn packet_type_literals_match_payload_def() {
146 use crate::traits::PayloadDef;
147 $(
148 assert_eq!(
149 $ptype,
150 <$($path)::+ as PayloadDef>::PACKET_TYPE,
151 concat!("PACKET_TYPE literal drift for ", stringify!($variant)),
152 );
153 assert!(
154 !<$($path)::+ as PayloadDef>::NAME.is_empty(),
155 concat!("empty NAME for ", stringify!($variant)),
156 );
157 )+
158 }
159 }
160 };
161}
162
163declare_payloads! {'a;
164 // TS 102 773 Table 1 — all 12 defined packet types in numerical order.
165 Bbframe = 0x00 => crate::payload::bbframe::BbframePayload<'a>,
166 AuxIq = 0x01 => crate::payload::aux_iq::AuxIqPayload<'a>,
167 ArbitraryCells = 0x02 => crate::payload::arbitrary_cells::ArbitraryCellsPayload<'a>,
168 L1Current = 0x10 => crate::payload::l1_current::L1CurrentPayload<'a>,
169 L1Future = 0x11 => crate::payload::l1_future::L1FuturePayload<'a>,
170 P2Bias = 0x12 => crate::payload::p2_bias::P2BiasPayload,
171 Timestamp = 0x20 => crate::payload::timestamp::T2TimestampPayload,
172 IndividualAddressing = 0x21 => crate::payload::individual_addressing::IndividualAddressingPayload<'a>,
173 FefNull = 0x30 => crate::payload::fef_null::FefNullPayload,
174 FefIq = 0x31 => crate::payload::fef_iq::FefIqPayload<'a>,
175 FefComposite = 0x32 => crate::payload::fef_composite::FefCompositePayload,
176 FefSubpart = 0x33 => crate::payload::fef_subpart::FefSubPartPayload<'a>,
177}
178
179impl<'a> AnyPayload<'a> {
180 /// Parse one payload by its `packet_type`, preferring the registry's custom
181 /// parsers over the built-in dispatch.
182 ///
183 /// `payload_bytes` must be the **payload-only slice** (bytes after the
184 /// 6-byte T2-MI header, before the 4-byte CRC trailer).
185 ///
186 /// # Precedence
187 ///
188 /// 1. If `registry` holds a custom parser for `packet_type`, it is called;
189 /// the result becomes [`AnyPayload::Other`] (or an error on parse failure).
190 /// 2. Otherwise, falls back to the built-in [`AnyPayload::dispatch`].
191 /// 3. If neither route handles `packet_type`, returns `None` — the caller
192 /// turns that into [`AnyPayload::Unknown`].
193 ///
194 /// See the [module-level documentation][self] for the dispatch contract
195 /// (payload-only bytes, header and CRC excluded).
196 pub fn dispatch_with(
197 registry: &crate::payload::registry::PayloadRegistry,
198 packet_type: u8,
199 payload_bytes: &'a [u8],
200 ) -> Option<crate::Result<Self>> {
201 if let Some(parse_fn) = registry.lookup(packet_type) {
202 return Some(match parse_fn(payload_bytes) {
203 Ok(value) => Ok(Self::Other { packet_type, value }),
204 Err(e) => Err(e),
205 });
206 }
207 // Fall back to built-in dispatch
208 Self::dispatch(packet_type, payload_bytes)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 // ── Completeness ─────────────────────────────────────────────────────────
217
218 /// `AnyPayload::name()` reflects `PayloadDef::NAME`; `UNKNOWN` for unknowns.
219 #[test]
220 fn name_maps_variant_to_payloaddef_name() {
221 let bb = AnyPayload::dispatch(0x00, &[0x00, 0x00, 0x00])
222 .expect("dispatched")
223 .expect("valid bbframe payload");
224 assert_eq!(bb.name(), "BBFRAME");
225 let unknown = AnyPayload::Unknown {
226 packet_type: 0x7F,
227 body: &[],
228 };
229 assert_eq!(unknown.name(), "UNKNOWN");
230
231 // A runtime-registered custom payload reports "CUSTOM".
232 use crate::payload::registry::PayloadRegistry;
233
234 // An unregistered packet_type yields no custom dispatch.
235 let empty = PayloadRegistry::new();
236 assert!(AnyPayload::dispatch_with(&empty, 0x40, &[]).is_none());
237
238 #[derive(Debug)]
239 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
240 struct NameTestPayload {
241 _x: u8,
242 }
243
244 impl<'a> dvb_common::Parse<'a> for NameTestPayload {
245 type Error = crate::Error;
246 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
247 if bytes.is_empty() {
248 return Err(crate::Error::BufferTooShort {
249 need: 1,
250 have: 0,
251 what: "NameTest",
252 });
253 }
254 Ok(Self { _x: bytes[0] })
255 }
256 }
257
258 impl<'a> crate::traits::PayloadDef<'a> for NameTestPayload {
259 const PACKET_TYPE: u8 = 0x41;
260 const NAME: &'static str = "NAME_TEST";
261 }
262
263 let mut reg = PayloadRegistry::new();
264 reg.register::<NameTestPayload>();
265 let parsed = AnyPayload::dispatch_with(®, 0x41, &[0xAA])
266 .unwrap()
267 .unwrap();
268 assert_eq!(parsed.name(), "CUSTOM");
269 }
270
271 /// Every entry in DISPATCHED_TYPES must dispatch to a non-Unknown variant.
272 #[test]
273 fn every_dispatched_type_routes_non_unknown() {
274 // Minimal valid payload bytes for each packet type (all RFU = 0 — the
275 // parsers reject non-zero reserved bits). See each payload module's
276 // own tests for full boundary coverage.
277
278 // 0x00 BBFrame: frame_idx(1) + plp_id(1) + intl_frame_start+rfu(1) = 3 bytes.
279 let bbframe_bytes: &[u8] = &[0x00, 0x00, 0x00];
280 // 0x01 AuxIq: frame_idx(1) + aux_id(4bits, must be 1..=15)+rfu(4bits)(1) + rfu(1) = 3 bytes.
281 // aux_id=1: byte1 = (1<<4) = 0x10.
282 let aux_iq_bytes: &[u8] = &[0x00, 0x10, 0x00];
283 // 0x02 ArbitraryCells: 8-byte header (rfu bytes 3,4 = 0, byte5 top 2 = 0).
284 let arb_cells_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
285 // 0x10 L1Current: frame_idx(1) + freq_source(2bits)+rfu(6bits)(1) = 2 bytes.
286 let l1_current_bytes: &[u8] = &[0x00, 0x00];
287 // 0x11 L1Future: frame_idx(1) + rfu(1) = 2 bytes.
288 let l1_future_bytes: &[u8] = &[0x00, 0x00];
289 // 0x12 P2Bias: 5 bytes, all rfu = 0.
290 let p2_bias_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00];
291 // 0x20 Timestamp: 11 bytes, rfu top 4 bits of byte0 = 0, bw=0 (1.7 MHz).
292 let timestamp_bytes: &[u8] = &[
293 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
294 ];
295 // 0x21 IndividualAddressing: rfu(1) + length(1, value 0) = 2 bytes.
296 let indiv_addr_bytes: &[u8] = &[0x00, 0x00];
297 // 0x30 FefNull: fef_idx(1) + rfu(1, must be 0) + s1_field+s2_field(1) = 3 bytes.
298 let fef_null_bytes: &[u8] = &[0x00, 0x00, 0x00];
299 // 0x31 FefIq: fef_idx(1) + rfu(1, must be 0) + s1+s2(1) = 3 bytes.
300 let fef_iq_bytes: &[u8] = &[0x00, 0x00, 0x00];
301 // 0x32 FefComposite: 8 bytes. byte1 [7]=rfu1=0, bytes2-5=rfu2=0.
302 let fef_composite_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
303 // 0x33 FefSubpart: 15 bytes.
304 // bytes 3-6 = rfu1 = 0, byte 11 = rfu2 = 0, byte 12 top 2 = 0.
305 // subpart_variety bytes 9-10 = 0x0000 = Null.
306 let fef_subpart_bytes: &[u8] = &[
307 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
308 0x00,
309 ];
310
311 let fixtures: &[(u8, &[u8])] = &[
312 (0x00, bbframe_bytes),
313 (0x01, aux_iq_bytes),
314 (0x02, arb_cells_bytes),
315 (0x10, l1_current_bytes),
316 (0x11, l1_future_bytes),
317 (0x12, p2_bias_bytes),
318 (0x20, timestamp_bytes),
319 (0x21, indiv_addr_bytes),
320 (0x30, fef_null_bytes),
321 (0x31, fef_iq_bytes),
322 (0x32, fef_composite_bytes),
323 (0x33, fef_subpart_bytes),
324 ];
325
326 for &(pt, bytes) in fixtures {
327 let result = AnyPayload::dispatch(pt, bytes);
328 assert!(result.is_some(), "0x{pt:02x} returned None from dispatch");
329 let parsed = result.unwrap();
330 assert!(
331 parsed.is_ok(),
332 "0x{pt:02x} dispatch parse failed: {:?}",
333 parsed.unwrap_err()
334 );
335 assert!(
336 !matches!(parsed.unwrap(), AnyPayload::Unknown { .. }),
337 "0x{pt:02x} was dispatched to Unknown"
338 );
339 }
340 }
341
342 /// DISPATCHED_TYPES has exactly 12 entries (one per TS 102 773 Table 1 type).
343 #[test]
344 fn dispatched_types_count_is_twelve() {
345 assert_eq!(AnyPayload::DISPATCHED_TYPES.len(), 12);
346 }
347
348 /// DISPATCHED_TYPES contains all 12 defined packet_type values.
349 #[test]
350 fn dispatched_types_contains_all_defined_packet_types() {
351 let expected = [
352 0x00u8, 0x01, 0x02, 0x10, 0x11, 0x12, 0x20, 0x21, 0x30, 0x31, 0x32, 0x33,
353 ];
354 for pt in expected {
355 assert!(
356 AnyPayload::DISPATCHED_TYPES.contains(&pt),
357 "0x{pt:02x} missing from DISPATCHED_TYPES"
358 );
359 }
360 }
361
362 // ── Unknown fallthrough ───────────────────────────────────────────────────
363
364 /// An undispatched packet_type returns None from dispatch (caller makes Unknown).
365 #[test]
366 fn undispatched_packet_type_returns_none() {
367 // 0x22..=0x2F are RFU, never defined.
368 assert!(AnyPayload::dispatch(0x22, &[]).is_none());
369 assert!(AnyPayload::dispatch(0xFF, &[]).is_none());
370 }
371
372 // ── From impls ────────────────────────────────────────────────────────────
373
374 #[test]
375 fn from_bbframe_payload_into_any_payload() {
376 use crate::payload::bbframe::BbframePayload;
377 let p = BbframePayload {
378 frame_idx: 1,
379 plp_id: 2,
380 intl_frame_start: false,
381 bbframe: &[],
382 };
383 let any = AnyPayload::from(p);
384 assert!(matches!(any, AnyPayload::Bbframe(_)));
385 }
386
387 #[test]
388 fn from_fef_null_payload_into_any_payload() {
389 use crate::payload::fef_null::{FefNullPayload, S1Field};
390 let p = FefNullPayload {
391 fef_idx: 0,
392 s1_field: S1Field::V0,
393 s2_field: 0,
394 };
395 let any = AnyPayload::from(p);
396 assert!(matches!(any, AnyPayload::FefNull(_)));
397 }
398
399 // ── serde ─────────────────────────────────────────────────────────────────
400
401 #[cfg(feature = "serde")]
402 #[test]
403 fn bbframe_serializes_as_camel_case_external_tag() {
404 use crate::payload::bbframe::BbframePayload;
405 let p = BbframePayload {
406 frame_idx: 0x42,
407 plp_id: 0x05,
408 intl_frame_start: true,
409 bbframe: &[],
410 };
411 let any = AnyPayload::Bbframe(p);
412 let json = serde_json::to_value(&any).unwrap();
413 assert!(
414 json.get("bbframe").is_some(),
415 "expected camelCase 'bbframe' key, got: {json}"
416 );
417 assert_eq!(json["bbframe"]["frame_idx"], 0x42);
418 }
419
420 #[cfg(feature = "serde")]
421 #[test]
422 fn unknown_serializes_with_packet_type_and_body() {
423 let any = AnyPayload::Unknown {
424 packet_type: 0x22,
425 body: &[0xDE, 0xAD],
426 };
427 let json = serde_json::to_value(&any).unwrap();
428 assert!(
429 json.get("unknown").is_some(),
430 "expected 'unknown' key, got: {json}"
431 );
432 assert_eq!(json["unknown"]["packet_type"], 0x22);
433 }
434}