Skip to main content

dvb_bbframe/
issy.rs

1//! ISSY (Input Stream SYnchronizer) field decoding per EN 302 755 §5.1.7 /
2//! Annex C Table C.1 (DVB-T2) and EN 302 307-1 Annex D Table D.1 (DVB-S2 BUFSTAT).
3//!
4//! ISSY carries the Input Stream Clock Reference (ISCR) and, in its long form,
5//! buffer-status / time-to-output signalling, used for jitter-free transport
6//! reconstruction at the receiver. The first bit selects the form:
7//!
8//! ```text
9//!   bit7 = 0          -> ISCR short: 15-bit ISCR    (2-byte ISSY)
10//!   bit7 = 1, bit6 = 0 -> ISCR long: 22-bit ISCR    (3-byte ISSY)
11//!   bit7 = 1, bit6 = 1 -> BUFS / TTO signalling      (3-byte ISSY)
12//! ```
13
14use crate::Error;
15
16const ISSY_LONG_FORM_BIT: u8 = 0x80;
17const ISCR_SHORT_PAYLOAD_MASK: u8 = 0x7F;
18const ISCR_LONG_PAYLOAD_MASK: u8 = 0x3F;
19const ISSY_SIGNALLING_BIT: u8 = 0x40;
20
21const SIGNALLING_KIND_SHIFT: u32 = 20;
22const SIGNALLING_KIND_MASK: u32 = 0x03;
23const BUFS_UNIT_SHIFT: u32 = 18;
24const BUFS_UNIT_MASK: u32 = 0x03;
25const BUFS_VALUE_SHIFT: u32 = 8;
26const BUFS_VALUE_MASK: u32 = 0x03FF;
27const TTO_E_MSB_SHIFT: u32 = 16;
28const TTO_E_MSB_MASK: u32 = 0x0F;
29const TTO_E_LSB_SHIFT: u32 = 15;
30const TTO_E_LSB_MASK: u32 = 0x01;
31const TTO_M_SHIFT: u32 = 8;
32const TTO_M_MASK: u32 = 0x7F;
33const RESERVED_PAYLOAD_MASK: u32 = 0x0F_FFFF;
34
35/// BUFS unit selector — EN 302 755 Annex C, Table C.1 (2-bit field).
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize))]
38#[non_exhaustive]
39pub enum BufsUnit {
40    /// 0b00 — bits.
41    Bits,
42    /// 0b01 — Kbits.
43    Kbits,
44    /// 0b10 — Mbits.
45    Mbits,
46    /// 0b11 — 8 Kbits.
47    Kbits8,
48}
49
50impl BufsUnit {
51    #[must_use]
52    /// Construct from a raw `u8` (only the low 2 bits are used).
53    pub fn from_u8(v: u8) -> Self {
54        match v & BUFS_UNIT_MASK as u8 {
55            0 => Self::Bits,
56            1 => Self::Kbits,
57            2 => Self::Mbits,
58            3 => Self::Kbits8,
59            _ => unreachable!(),
60        }
61    }
62
63    #[must_use]
64    /// Return the wire byte for this unit.
65    pub const fn to_u8(self) -> u8 {
66        match self {
67            Self::Bits => 0,
68            Self::Kbits => 1,
69            Self::Mbits => 2,
70            Self::Kbits8 => 3,
71        }
72    }
73
74    #[must_use]
75    /// Human-readable unit name.
76    pub fn name(self) -> &'static str {
77        match self {
78            Self::Bits => "bits",
79            Self::Kbits => "Kbits",
80            Self::Mbits => "Mbits",
81            Self::Kbits8 => "8 Kbits",
82        }
83    }
84
85    #[must_use]
86    /// Number of bits per BUFS unit.
87    ///
88    /// Per EN 302 755 Annex C Table C.1, the 2-bit unit selector names the
89    /// scale (bits / Kbits / Mbits / 8Kbits). The standard does not numerically
90    /// define K/M; the decimal convention (K = 1 000, M = 1 000 000, 8K = 8 000)
91    /// is used, consistent with the standard's use of decimal `Mbit/s`
92    /// elsewhere (see `dvb-bbframe/docs/enums/en_302_755/bufs_unit.md` §BUFS/TTO semantics).
93    pub fn multiplier_bits(self) -> u64 {
94        match self {
95            Self::Bits => 1,
96            Self::Kbits => 1_000,
97            Self::Mbits => 1_000_000,
98            Self::Kbits8 => 8_000,
99        }
100    }
101}
102
103broadcast_common::impl_spec_display!(BufsUnit);
104
105/// Decoded BUFS/TTO signalling — EN 302 755 Annex C, Table C.1.
106///
107/// The `11` prefix in the first ISSY byte selects one of two alternatives:
108/// BUFS (buffer status) or TTO (time-to-output), indicated by bits `[5:4]`.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize))]
111#[non_exhaustive]
112pub enum SignallingKind {
113    /// BUFS — maximum size of the requested receiver buffer.
114    ///
115    /// Fields: `(bufs, units)` where `bufs` is the 10-bit buffer status
116    /// and `units` is the 2-bit unit selector.
117    Bufs {
118        /// 10-bit buffer status value.
119        bufs: u16,
120        /// 2-bit unit selector (Table C.1).
121        units: BufsUnit,
122    },
123    /// TTO — time-to-output (mantissa + exponent form).
124    ///
125    /// The output time is `TTO = (tto_m + tto_l / 256) * 2^tto_e`
126    /// where `tto_l` is zero when ISCRshort is in use.
127    Tto {
128        /// 5-bit exponent `TTO_E`.
129        tto_e: u8,
130        /// 7-bit mantissa `TTO_M`.
131        tto_m: u8,
132        /// 8-bit low-fraction `TTO_L` (zero when ISCRshort is in use).
133        tto_l: u8,
134    },
135    /// BUFSTAT — actual receiver-buffer fill status (DVB-S2 only).
136    ///
137    /// EN 302 307-1 Annex D Table D.1: selected by bits `[5:4] = 0b10` (the
138    /// ISSY code range `0xE_XXXX`). Same field layout as [`Self::Bufs`] — a
139    /// 2-bit `units` selector and a 10-bit value (the number of filled bits,
140    /// scaled by `units`). **Not used in DVB-T2**, where EN 302 755 Annex C
141    /// reserves this code range ("shall not be transmitted in DVB-T2") and
142    /// replaces it with [`Self::Tto`]; decoding it as BUFSTAT here is correct
143    /// for DVB-S2 streams and harmless for T2 (the range is not transmitted).
144    BufStat {
145        /// 10-bit buffer-status value (filled bits, scaled by `units`).
146        bufstat: u16,
147        /// 2-bit unit selector (EN 302 307-1 Annex D Table D.1; note S2 marks
148        /// `0b11` reserved whereas T2's [`BufsUnit::Kbits8`] uses it).
149        units: BufsUnit,
150    },
151    /// Reserved signalling type (bits `[5:4]` = `0b11`).
152    ///
153    /// Holds the low 20 bits of the signalling payload; the 2-bit kind
154    /// selector is not retained. This is a decode-only view — the wire bytes
155    /// live in `Bbheader::issy_in_header` and are serialized verbatim, so the
156    /// dropped selector does not affect round-trip fidelity.
157    Reserved(u32),
158}
159
160impl SignallingKind {
161    #[must_use]
162    /// Decoded BUFS buffer size in bits, or `None` if this is not a BUFS variant.
163    ///
164    /// `bufs_bits = bufs × units.multiplier_bits()`
165    ///
166    /// See EN 302 755 Annex C Table C.1 + §BUFS/TTO semantics
167    /// (`dvb-bbframe/docs/enums/en_302_755/bufs_unit.md`).
168    ///
169    /// # Note on encoders
170    ///
171    /// Only decode accessors are provided. No `set_*` or `from_*` encoders are
172    /// added because the physical-value → mantissa/exponent TTO encoding is
173    /// lossy and the wire round-trip is already guaranteed by the existing
174    /// raw-field serialization in `Bbheader`. Use the raw-field constructors
175    /// (`SignallingKind::Bufs { … }` / `SignallingKind::Tto { … }`) for
176    /// encoding.
177    pub fn bufs_bits(&self) -> Option<u64> {
178        match self {
179            Self::Bufs { bufs, units } => Some(*bufs as u64 * units.multiplier_bits()),
180            _ => None,
181        }
182    }
183
184    #[must_use]
185    /// Decoded BUFS buffer size in bytes (integer floor), or `None`.
186    ///
187    /// `bufs_bytes = bufs_bits() / 8`. Integer division is used: BUFS is a
188    /// maximum-size bound per the standard, so a floor is appropriate.
189    ///
190    /// See EN 302 755 Annex C Table C.1 + §BUFS/TTO semantics
191    /// (`dvb-bbframe/docs/enums/en_302_755/bufs_unit.md`).
192    pub fn bufs_bytes(&self) -> Option<u64> {
193        self.bufs_bits().map(|b| b / 8)
194    }
195
196    #[must_use]
197    /// Decoded BUFSTAT fill status in bits, or `None` if this is not a BUFSTAT
198    /// variant (DVB-S2 only — EN 302 307-1 Annex D Table D.1).
199    ///
200    /// `bufstat_bits = bufstat × units.multiplier_bits()`
201    pub fn bufstat_bits(&self) -> Option<u64> {
202        match self {
203            Self::BufStat { bufstat, units } => Some(*bufstat as u64 * units.multiplier_bits()),
204            _ => None,
205        }
206    }
207
208    #[must_use]
209    /// Decoded BUFSTAT fill status in bytes (integer floor), or `None`.
210    pub fn bufstat_bytes(&self) -> Option<u64> {
211        self.bufstat_bits().map(|b| b / 8)
212    }
213
214    #[must_use]
215    /// Decoded time-to-output in units of T/256, or `None` if this is not a
216    /// TTO variant.
217    ///
218    /// `tto_t_over_256 = ((TTO_M × 256) + TTO_L) × 2^TTO_E`
219    ///
220    /// This is `TTO × 256` in units of the elementary period **T** (see EN 302
221    /// 755 §9.5 / Table 65). The `TTO_L / 256` fractional term is preserved
222    /// exactly without floating point; consumers divide by 256.0 to obtain
223    /// `TTO` in units of T.
224    ///
225    /// Per EN 302 755 Annex C Table C.1 + §8.3.3
226    /// (`dvb-bbframe/docs/enums/en_302_755/bufs_unit.md`).
227    pub fn tto_t_over_256(&self) -> Option<u64> {
228        match self {
229            Self::Tto {
230                tto_e,
231                tto_m,
232                tto_l,
233            } => Some((u64::from(*tto_m) * 256 + u64::from(*tto_l)) << tto_e),
234            _ => None,
235        }
236    }
237
238    #[must_use]
239    /// Human-readable spec display name for the signalling kind.
240    pub fn name(&self) -> &'static str {
241        match self {
242            Self::Bufs { .. } => "BUFS",
243            Self::Tto { .. } => "TTO",
244            Self::BufStat { .. } => "BUFSTAT",
245            Self::Reserved(_) => "reserved",
246        }
247    }
248}
249
250broadcast_common::impl_spec_display!(SignallingKind);
251
252/// Decoded ISSY value (EN 302 755 §5.1.7, Annex C).
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize))]
255#[non_exhaustive]
256pub enum Issy {
257    /// ISCR short form — 15-bit Input Stream Clock Reference (2-byte ISSY).
258    IscrShort(u16),
259    /// ISCR long form — 22-bit Input Stream Clock Reference (3-byte ISSY).
260    IscrLong(u32),
261    /// Long-form BUFS/TTO signalling (3-byte ISSY, `11` prefix).
262    ///
263    /// The 22-bit payload is decoded into [`SignallingKind`]; see
264    /// Annex C for the sub-coding.
265    Signalling(SignallingKind),
266}
267
268impl Issy {
269    #[must_use]
270    /// Human-readable spec display name for the ISSY form.
271    pub fn name(&self) -> &'static str {
272        match self {
273            Self::IscrShort(_) => "ISCR short",
274            Self::IscrLong(_) => "ISCR long",
275            Self::Signalling(_) => "signalling",
276        }
277    }
278}
279
280broadcast_common::impl_spec_display!(Issy);
281
282/// Decode a 2-byte (short) ISSY field.
283///
284/// Returns `Ok(Issy::IscrShort)` when the short-form bit (`[7]` of byte 0) is
285/// `0`; `Err` otherwise (a `1` prefix means a long-form field, which is 3 bytes
286/// and must be decoded with [`decode_issy_long`]).
287pub fn decode_issy_short(bytes: [u8; 2]) -> crate::Result<Issy> {
288    if bytes[0] & ISSY_LONG_FORM_BIT != 0 {
289        return Err(Error::InvalidIssyForm {
290            reason: "bit [7] is 1 (long form); use decode_issy_long for 3-byte ISSY",
291        });
292    }
293    let iscr = ((bytes[0] as u16 & ISCR_SHORT_PAYLOAD_MASK as u16) << 8) | bytes[1] as u16;
294    Ok(Issy::IscrShort(iscr))
295}
296
297/// Decode a 3-byte (long) ISSY field.
298///
299/// Byte 0 bit `[7]` must be `1` (long form). Byte 0 bit `[6]` then selects: `0` → 22-bit
300/// ISCR long; `1` → BUFS/TTO signalling. Returns `Err` if bit `[7]` is `0` (that is
301/// a short-form field — use [`decode_issy_short`]).
302pub fn decode_issy_long(bytes: [u8; 3]) -> crate::Result<Issy> {
303    if bytes[0] & ISSY_LONG_FORM_BIT == 0 {
304        return Err(Error::InvalidIssyForm {
305            reason: "bit [7] is 0 (short form); use decode_issy_short for 2-byte ISSY",
306        });
307    }
308    let payload = ((bytes[0] as u32 & ISCR_LONG_PAYLOAD_MASK as u32) << 16)
309        | (bytes[1] as u32) << 8
310        | bytes[2] as u32;
311    if bytes[0] & ISSY_SIGNALLING_BIT == 0 {
312        Ok(Issy::IscrLong(payload))
313    } else {
314        Ok(Issy::Signalling(decode_signalling(payload)))
315    }
316}
317
318/// Decode the 22-bit `11`-prefix payload per EN 302 755 Annex C Table C.1
319/// (DVB-T2) and EN 302 307-1 Annex D Table D.1 (DVB-S2).
320///
321/// Bits `[21:20]` select the signalling type:
322/// - `0b00` → BUFS: bits `[19:18]` = unit, bits `[17:8]` = 10-bit BUFS, `[7:0]` reserved
323/// - `0b01` → TTO (DVB-T2): bits `[19:16]` = 4 MSBs of TTO_E, byte 1 bit `[7]` = LSB
324///   of TTO_E, byte 1 bits `[6:0]` = TTO_M, byte 2 = TTO_L (or reserved for ISCRshort)
325/// - `0b10` → BUFSTAT (DVB-S2): bits `[19:18]` = unit, bits `[17:8]` = 10-bit BUFSTAT
326///   (reserved / not transmitted in DVB-T2 — replaced by TTO)
327/// - `0b11` → reserved
328fn decode_signalling(payload: u32) -> SignallingKind {
329    let kind = (payload >> SIGNALLING_KIND_SHIFT) & SIGNALLING_KIND_MASK;
330    match kind {
331        0 => {
332            let units = BufsUnit::from_u8(((payload >> BUFS_UNIT_SHIFT) & BUFS_UNIT_MASK) as u8);
333            let bufs = ((payload >> BUFS_VALUE_SHIFT) & BUFS_VALUE_MASK) as u16;
334            SignallingKind::Bufs { bufs, units }
335        }
336        1 => {
337            let tto_e = (((payload >> TTO_E_MSB_SHIFT) & TTO_E_MSB_MASK) << 1
338                | ((payload >> TTO_E_LSB_SHIFT) & TTO_E_LSB_MASK)) as u8;
339            let tto_m = ((payload >> TTO_M_SHIFT) & TTO_M_MASK) as u8;
340            let tto_l = (payload & 0xFF) as u8;
341            SignallingKind::Tto {
342                tto_e,
343                tto_m,
344                tto_l,
345            }
346        }
347        2 => {
348            // BUFSTAT (DVB-S2, EN 302 307-1 Annex D Table D.1) — same layout as BUFS.
349            let units = BufsUnit::from_u8(((payload >> BUFS_UNIT_SHIFT) & BUFS_UNIT_MASK) as u8);
350            let bufstat = ((payload >> BUFS_VALUE_SHIFT) & BUFS_VALUE_MASK) as u16;
351            SignallingKind::BufStat { bufstat, units }
352        }
353        _ => {
354            let remainder = payload & RESERVED_PAYLOAD_MASK;
355            SignallingKind::Reserved(remainder)
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn iscr_short_decodes_15_bits() {
366        assert_eq!(decode_issy_short([0x7A, 0xBC]), Ok(Issy::IscrShort(0x7ABC)));
367        assert_eq!(decode_issy_short([0x00, 0x01]), Ok(Issy::IscrShort(1)));
368    }
369
370    #[test]
371    fn short_rejects_long_prefix() {
372        assert!(decode_issy_short([0x80, 0x00]).is_err());
373    }
374
375    #[test]
376    fn iscr_long_decodes_22_bits() {
377        assert_eq!(
378            decode_issy_long([0xBF, 0xFF, 0xFF]),
379            Ok(Issy::IscrLong(0x3FFFFF))
380        );
381        assert_eq!(
382            decode_issy_long([0x80, 0x12, 0x34]),
383            Ok(Issy::IscrLong(0x1234))
384        );
385    }
386
387    #[test]
388    fn signalling_decodes_with_11_prefix() {
389        assert_eq!(
390            decode_issy_long([0xC0, 0x12, 0x34]),
391            Ok(Issy::Signalling(decode_signalling(0x1234)))
392        );
393    }
394
395    #[test]
396    fn long_rejects_short_prefix() {
397        assert!(decode_issy_long([0x00, 0x00, 0x00]).is_err());
398    }
399
400    #[test]
401    fn signalling_bufs_decode() {
402        // bytes [0xCB, 0xFF, 0x00]: byte0 has the '11' ISSY prefix in bits[7:6];
403        // the 22-bit payload = ((0xCB & 0x3F) << 16) | (0xFF << 8) | 0x00 = 0x0B_FF_00
404        // = 0000_1011_1111_1111_0000_0000, so:
405        //   bits[21:20] = 00 => BUFS form
406        //   bits[19:18] = 10 => unit (Mbit)
407        //   bits[17:8]  = 11_1111_1111 = 0x3FF => BUFS = 1023
408        //   bits[7:0]   = reserved
409        let result = decode_issy_long([0xCB, 0xFF, 0x00]).unwrap();
410        match result {
411            Issy::Signalling(SignallingKind::Bufs { bufs, units }) => {
412                assert_eq!(bufs, 0x3FF);
413                assert_eq!(units, BufsUnit::Mbits);
414            }
415            other => panic!("expected BUFS, got {other:?}"),
416        }
417    }
418
419    #[test]
420    fn signalling_tto_decode() {
421        // '11' prefix, bits[21:20]=0b01 (TTO)
422        // bits[19:16]=0b0101 (4 MSBs of TTO_E = 5)
423        // byte1 bit7 = LSB of TTO_E (1 => TTO_E = 0b1011 = 11)
424        // byte1 bits[6:0] = TTO_M = 0x7F = 127
425        // byte2 = TTO_L = 0x80
426        // byte0: 0b11_01_0101 = 0xD5
427        // byte1: 0b1_1111111 = 0xFF
428        // byte2: 0x80
429        // payload = ((0xD5 & 0x3F) << 16) | (0xFF << 8) | 0x80
430        // = (0x15 << 16) | 0xFF00 | 0x80
431        // = 0x15_FF_80
432        // bits[21:20] = 01 => TTO
433        // bits[19:16] = 0101 => TTO_E MSBs = 5
434        // bit 15 = 1 => TTO_E LSB = 1 => TTO_E = 0b1011 = 11
435        // bits[14:8] = 1111111 => TTO_M = 127
436        // bits[7:0] = 10000000 => TTO_L = 128
437        let result = decode_issy_long([0xD5, 0xFF, 0x80]).unwrap();
438        match result {
439            Issy::Signalling(SignallingKind::Tto {
440                tto_e,
441                tto_m,
442                tto_l,
443            }) => {
444                assert_eq!(tto_e, 11);
445                assert_eq!(tto_m, 127);
446                assert_eq!(tto_l, 128);
447            }
448            other => panic!("expected TTO, got {other:?}"),
449        }
450    }
451
452    #[test]
453    fn signalling_bufstat_decode() {
454        // BUFSTAT (DVB-S2, EN 302 307-1 Annex D Table D.1): '11' prefix,
455        // bits[21:20]=0b10 (BUFSTAT), bits[19:18]=0b10 (Mbits unit),
456        // bits[17:8]=11_1111_1111=0x3FF (BUFSTAT=1023).
457        // byte0: 0b11_10_10_11 = 0xEB; payload = (0xEB & 0x3F)<<16 | 0xFF<<8 = 0x2BFF00.
458        let result = decode_issy_long([0xEB, 0xFF, 0x00]).unwrap();
459        match result {
460            Issy::Signalling(SignallingKind::BufStat { bufstat, units }) => {
461                assert_eq!(bufstat, 0x3FF);
462                assert_eq!(units, BufsUnit::Mbits);
463            }
464            other => panic!("expected BufStat, got {other:?}"),
465        }
466        // BUFSTAT accessors
467        let bs = SignallingKind::BufStat {
468            bufstat: 2,
469            units: BufsUnit::Mbits,
470        };
471        assert_eq!(bs.bufstat_bits(), Some(2_000_000));
472        assert_eq!(bs.bufstat_bytes(), Some(250_000));
473        // non-BUFSTAT variants return None
474        assert_eq!(SignallingKind::Reserved(0).bufstat_bits(), None);
475    }
476
477    #[test]
478    fn signalling_reserved_decode() {
479        // '11' prefix, bits[21:20]=0b11 (reserved — the only remaining reserved kind
480        // now that 0b10 is decoded as BUFSTAT).
481        // byte0: 0b11_11_0000 = 0xF0; payload = ((0xF0 & 0x3F) << 16) = 0x300000
482        // kind = (0x300000 >> 20) & 0x03 = 3 => reserved; remainder = 0x300000 & 0x0FFFFF = 0
483        let result = decode_issy_long([0xF0, 0x00, 0x00]).unwrap();
484        match result {
485            Issy::Signalling(SignallingKind::Reserved(remainder)) => {
486                assert_eq!(remainder, 0x00000);
487            }
488            other => panic!("expected Reserved, got {other:?}"),
489        }
490    }
491
492    #[test]
493    fn bufs_unit_round_trip() {
494        for b in 0..=3u8 {
495            assert_eq!(BufsUnit::from_u8(b).to_u8(), b);
496        }
497    }
498
499    #[test]
500    fn bufs_unit_name() {
501        assert_eq!(BufsUnit::Bits.name(), "bits");
502        assert_eq!(BufsUnit::Kbits.name(), "Kbits");
503        assert_eq!(BufsUnit::Mbits.name(), "Mbits");
504        assert_eq!(BufsUnit::Kbits8.name(), "8 Kbits");
505    }
506
507    #[test]
508    fn multiplier_bits() {
509        assert_eq!(BufsUnit::Bits.multiplier_bits(), 1);
510        assert_eq!(BufsUnit::Kbits.multiplier_bits(), 1_000);
511        assert_eq!(BufsUnit::Mbits.multiplier_bits(), 1_000_000);
512        assert_eq!(BufsUnit::Kbits8.multiplier_bits(), 8_000);
513    }
514
515    #[test]
516    fn bufs_bits_and_bytes() {
517        // BUFS = 2 Mbits → 2 * 1_000_000 = 2_000_000 bits → 250_000 bytes
518        let b = SignallingKind::Bufs {
519            bufs: 2,
520            units: BufsUnit::Mbits,
521        };
522        assert_eq!(b.bufs_bits(), Some(2_000_000));
523        assert_eq!(b.bufs_bytes(), Some(250_000));
524
525        // BUFS = 1 bit
526        let b = SignallingKind::Bufs {
527            bufs: 1,
528            units: BufsUnit::Bits,
529        };
530        assert_eq!(b.bufs_bits(), Some(1));
531        assert_eq!(b.bufs_bytes(), Some(0)); // 1/8 = 0 (integer floor)
532
533        // BUFS = 3 × 8Kbits → 3 * 8000 = 24_000 bits → 3_000 bytes
534        let b = SignallingKind::Bufs {
535            bufs: 3,
536            units: BufsUnit::Kbits8,
537        };
538        assert_eq!(b.bufs_bits(), Some(24_000));
539        assert_eq!(b.bufs_bytes(), Some(3_000));
540
541        // TTO variant returns None
542        let t = SignallingKind::Tto {
543            tto_e: 0,
544            tto_m: 0,
545            tto_l: 0,
546        };
547        assert_eq!(t.bufs_bits(), None);
548        assert_eq!(t.bufs_bytes(), None);
549
550        // Reserved variant returns None
551        assert_eq!(SignallingKind::Reserved(0).bufs_bits(), None);
552    }
553
554    #[test]
555    fn tto_t_over_256() {
556        // TTO_E=0, TTO_M=1, TTO_L=0 → ((1*256 + 0) << 0) = 256 (= 1·T in T/256 units)
557        let t = SignallingKind::Tto {
558            tto_e: 0,
559            tto_m: 1,
560            tto_l: 0,
561        };
562        assert_eq!(t.tto_t_over_256(), Some(256));
563
564        // TTO_E=1, TTO_M=0, TTO_L=128 → ((0*256 + 128) << 1) = 256 (= (128/256)*T*2 = 1·T → 256 in T/256 units)
565        let t = SignallingKind::Tto {
566            tto_e: 1,
567            tto_m: 0,
568            tto_l: 128,
569        };
570        assert_eq!(t.tto_t_over_256(), Some(256));
571
572        // TTO_E=5, TTO_M=3, TTO_L=64 → ((3*256 + 64) << 5) = (768 + 64) * 32 = 832 * 32 = 26_624
573        let t = SignallingKind::Tto {
574            tto_e: 5,
575            tto_m: 3,
576            tto_l: 64,
577        };
578        assert_eq!(t.tto_t_over_256(), Some(26_624));
579
580        // Bufs variant returns None
581        let b = SignallingKind::Bufs {
582            bufs: 1,
583            units: BufsUnit::Bits,
584        };
585        assert_eq!(b.tto_t_over_256(), None);
586
587        // Reserved variant returns None
588        assert_eq!(SignallingKind::Reserved(0).tto_t_over_256(), None);
589    }
590}