Skip to main content

mx_remote/types/
v2ip.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! OneIP stream configuration, statistics and the sink-side route.
5
6use core::fmt;
7use std::net::Ipv4Addr;
8
9use crate::wire::{
10    DeviceUid, MxrSignalType, V2ipColourSpace, V2ipDeviceSetting, V2ipDownmixLevel,
11    V2ipDownmixMode, V2ipPowerSaveLevel, V2ipPtpFlag, V2ipPtpLock, V2ipPtpMode, V2ipSdpClockMode,
12    V2ipSdpClockState, V2ipSdpEncoding, V2ipSdpFlag, V2ipSdpMediaClock, V2ipSdpRefClock,
13    V2ipTestPattern, V2ipTestcardFlag, V2ipToneMode, V2ipVlanFlag, V2IP_AUDIO_DEFAULT_CHANNELS,
14    V2IP_AUDIO_DEFAULT_SAMPLE_RATE, V2IP_DSCP_MAX, V2IP_DSCP_SET, V2IP_IR_PROFILE_MAX,
15    V2IP_IR_PROFILE_NOT_SET,
16};
17
18/// Which of a OneIP device's streams an address describes.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum StreamKind {
21    /// The video stream.
22    #[default]
23    Video,
24    /// The audio stream.
25    Audio,
26    /// The ancillary-data stream.
27    Anc,
28    /// The audio-return stream.
29    Arc,
30}
31
32impl fmt::Display for StreamKind {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.write_str(match self {
35            Self::Video => "video",
36            Self::Audio => "audio",
37            Self::Anc => "anc",
38            Self::Arc => "arc",
39        })
40    }
41}
42
43/// A single multicast stream address.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct V2ipStreamSource {
46    /// Which stream this address is for.
47    pub kind: StreamKind,
48    /// The multicast group.
49    pub ip: Ipv4Addr,
50    /// The destination UDP port.
51    pub port: u16,
52}
53
54impl Default for V2ipStreamSource {
55    fn default() -> Self {
56        Self {
57            kind: StreamKind::default(),
58            ip: Ipv4Addr::UNSPECIFIED,
59            port: 0,
60        }
61    }
62}
63
64impl V2ipStreamSource {
65    /// Reports whether this carries a usable address: a multicast group and a
66    /// non-zero port, both, matching firmware `mxr_v2ip_stream_valid`.
67    pub const fn is_valid(&self) -> bool {
68        self.ip.is_multicast() && self.port != 0
69    }
70}
71
72impl fmt::Display for V2ipStreamSource {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "{}={}:{}", self.kind, self.ip, self.port)
75    }
76}
77
78/// The streams advertised by a single OneIP source.
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
80pub struct V2ipStreamSources {
81    /// The originating device, or the zero UID when it is not known.
82    pub uid: DeviceUid,
83    /// The video stream.
84    pub video: V2ipStreamSource,
85    /// The audio stream.
86    pub audio: V2ipStreamSource,
87    /// The ancillary-data stream.
88    pub anc: V2ipStreamSource,
89    /// The audio-return stream, when one is advertised.
90    pub arc: Option<V2ipStreamSource>,
91}
92
93impl fmt::Display for V2ipStreamSources {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(
96            f,
97            "video:{} audio:{} anc:{}",
98            self.video, self.audio, self.anc
99        )
100    }
101}
102
103/// One multicast destination in a route the caller assembles.
104///
105/// The unspecified address sends the slot zeroed, naming no group for that
106/// stream. It is not a way to leave one stream alone: the firmware decides
107/// whether a sink has a manual route at all by reading the video and
108/// ancillary slots, so an empty one of those disqualifies the whole route
109/// rather than preserving anything - see
110/// [`crate::Remote::select_source_addr`].
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub struct V2ipRouteTarget {
113    /// The multicast group.
114    pub ip: Ipv4Addr,
115    /// The destination UDP port. Zero means the standard port for the stream
116    /// this target is given as.
117    pub port: u16,
118}
119
120impl Default for V2ipRouteTarget {
121    fn default() -> Self {
122        Self {
123            ip: Ipv4Addr::UNSPECIFIED,
124            port: 0,
125        }
126    }
127}
128
129impl V2ipRouteTarget {
130    /// A target at the standard port for its stream.
131    pub const fn new(ip: Ipv4Addr) -> Self {
132        Self { ip, port: 0 }
133    }
134
135    /// The port to send, substituting `standard` for an unset one.
136    pub(crate) const fn port_or(self, standard: u16) -> u16 {
137        if self.port == 0 {
138            standard
139        } else {
140            self.port
141        }
142    }
143}
144
145impl fmt::Display for V2ipRouteTarget {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}:{}", self.ip, self.port)
148    }
149}
150
151/// The three streams a manual route points a OneIP sink at.
152///
153/// Fill in all three. The firmware decides whether a sink has a manual route
154/// at all by looking at the video and ancillary groups, so a route carrying
155/// only audio does not register as one and the sink falls back to the audio
156/// source its mesh picks.
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
158pub struct V2ipRoute {
159    /// The video stream, at [`crate::V2IP_PORT_VIDEO`] unless the port says otherwise.
160    pub video: V2ipRouteTarget,
161    /// The audio stream, at [`crate::V2IP_PORT_AUDIO`] unless the port says otherwise.
162    pub audio: V2ipRouteTarget,
163    /// The ancillary-data stream, at [`crate::V2IP_PORT_ANC`] unless the port says
164    /// otherwise.
165    pub anc: V2ipRouteTarget,
166}
167
168impl V2ipRoute {
169    /// The three streams of one source, at the ports it advertises them on.
170    pub fn of(sources: &V2ipStreamSources) -> Self {
171        let target = |s: &V2ipStreamSource| V2ipRouteTarget {
172            ip: s.ip,
173            port: s.port,
174        };
175        Self {
176            video: target(&sources.video),
177            audio: target(&sources.audio),
178            anc: target(&sources.anc),
179        }
180    }
181}
182
183impl fmt::Display for V2ipRoute {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(
186            f,
187            "video:{} audio:{} anc:{}",
188            self.video, self.audio, self.anc
189        )
190    }
191}
192
193/// The sample rate and channel count a OneIP audio stream is decoded at.
194///
195/// Fill both in. The firmware header calls zero "use the default", but the
196/// path that applies a manual route substitutes nothing: it hands the pair to
197/// the FPGA as it arrived, and the FPGA rejects a zero rate and takes the
198/// whole switch down with it. [`V2ipAudioFormat::STANDARD`] is the pair the
199/// header documents as the default.
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
201pub struct V2ipAudioFormat {
202    /// Sample rate in Hz.
203    pub sample_rate: u32,
204    /// Channel count.
205    pub channels: u8,
206}
207
208impl V2ipAudioFormat {
209    /// 48kHz stereo: the rate and channel count the firmware header names as
210    /// its default, which a caller has to send because firmware does not
211    /// substitute it.
212    pub const STANDARD: Self = Self {
213        sample_rate: V2IP_AUDIO_DEFAULT_SAMPLE_RATE,
214        channels: V2IP_AUDIO_DEFAULT_CHANNELS,
215    };
216
217    /// Encodes `v2ip_audio_format`: a `u32` rate, a channel byte and three
218    /// reserved bytes, padded to the struct's 8-byte alignment.
219    pub(crate) fn wire(&self) -> [u8; 8] {
220        let r = self.sample_rate.to_le_bytes();
221        [r[0], r[1], r[2], r[3], self.channels, 0, 0, 0]
222    }
223}
224
225impl fmt::Display for V2ipAudioFormat {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        write!(f, "{}Hz/{}ch", self.sample_rate, self.channels)
228    }
229}
230
231/// A OneIP output's scaling mode, refresh rate and flags.
232#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
233pub struct V2ipScalingSettings {
234    /// The signal type the output scales to.
235    pub mode: MxrSignalType,
236    /// Refresh rate in Hz.
237    pub refresh: u16,
238    /// The flag bits below.
239    pub flags: u8,
240}
241
242/// Set when the frame carries a scaling mode and refresh rate.
243pub const SCALING_FLAG_MODE_VALID: u8 = 1 << 0;
244
245/// Set when the frame carries the scaling options.
246pub const SCALING_FLAG_OPTIONS_VALID: u8 = 1 << 1;
247
248/// Set when the frame carries the second group of scaling options.
249///
250/// Firmware that has those options sets this on every configuration it sends
251/// about itself, so it doubles as the report that the device has them at all.
252pub const SCALING_FLAG_OPTIONS2_VALID: u8 = 1 << 4;
253
254/// Set when the output follows its source's format instead of a fixed one.
255pub const SCALING_FLAG_MATCH_SOURCE: u8 = 1 << 5;
256
257/// Set when the output declines 4:2:0 rather than scaling it.
258pub const SCALING_FLAG_SKIP_420: u8 = 1 << 6;
259
260/// Set when the output scales automatically.
261pub const SCALING_FLAG_AUTO_SCALING: u8 = 1 << 7;
262
263/// The flag bits that carry meaning.
264///
265/// Bits 2 and 3 have no meaning and are excluded: they are not reliably zero
266/// on the wire, because firmware that does not initialise the configuration it
267/// broadcasts builds this frame from an uninitialised stack local and ORs its
268/// flags onto whatever was there. The same is true of every bit here on such a
269/// sender, which is why each reading below says what it rests on.
270pub const SCALING_FLAGS_DEFINED: u8 = SCALING_FLAG_MODE_VALID
271    | SCALING_FLAG_OPTIONS_VALID
272    | SCALING_FLAG_OPTIONS2_VALID
273    | SCALING_FLAG_MATCH_SOURCE
274    | SCALING_FLAG_SKIP_420
275    | SCALING_FLAG_AUTO_SCALING;
276
277/// Lowest refresh rate a OneIP output stage accepts, in Hz.
278///
279/// A receiver replaces anything outside
280/// [`V2IP_SCALING_REFRESH_MIN`]..=[`V2IP_SCALING_REFRESH_MAX`] with 50 rather
281/// than refusing the write, so 0 asks for 50Hz here instead of asking for
282/// nothing.
283pub const V2IP_SCALING_REFRESH_MIN: u16 = 24;
284
285/// Highest refresh rate a OneIP output stage accepts, in Hz. See
286/// [`V2IP_SCALING_REFRESH_MIN`].
287pub const V2IP_SCALING_REFRESH_MAX: u16 = 120;
288
289/// The output format to scale a OneIP sink to.
290///
291/// Built from a depth and a colour space rather than from a packed signal-type
292/// word, so the word a caller sends cannot carry the unset bpp index a sink
293/// reports while it has no mode configured.
294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
295pub struct V2ipOutputMode {
296    /// The CTA-861 short video descriptor to output.
297    pub svd: u8,
298    /// Bit depth: 8, 10 or 12.
299    pub depth: u8,
300    /// The colour space to output.
301    pub colour: V2ipColourSpace,
302    /// Refresh rate in Hz, [`V2IP_SCALING_REFRESH_MIN`] to
303    /// [`V2IP_SCALING_REFRESH_MAX`].
304    pub refresh: u16,
305}
306
307impl V2ipOutputMode {
308    /// Reports whether a sink will take this mode, or why it will not.
309    ///
310    /// Checked here because a sink checks it and then says nothing: every value
311    /// this rejects is one the receiver decodes cleanly and drops, leaving a
312    /// caller with a send that succeeded and a setting that did not move.
313    ///
314    /// Passing is not a guarantee. A sink also weighs the format against the
315    /// EDID of the display attached to it and against what its own clock and
316    /// output stage can produce, and none of that is knowable from here.
317    pub fn validate(&self) -> Result<(), &'static str> {
318        if self.svd == 0 {
319            return Err("svd 0 is how a mode is cleared, not a mode to set");
320        }
321        if crate::lookup_svd(u16::from(self.svd)).is_none() {
322            return Err("the svd names no known video descriptor");
323        }
324        if MxrSignalType::bpp_index_for_depth(self.depth).is_none() {
325            return Err("a OneIP output stage takes 8, 10 or 12 bits per pixel");
326        }
327        if self.colour > V2ipColourSpace::YCBCR420 {
328            return Err("the colour space names none of RGB, 4:4:4, 4:2:2 or 4:2:0");
329        }
330        if !(V2IP_SCALING_REFRESH_MIN..=V2IP_SCALING_REFRESH_MAX).contains(&self.refresh) {
331            return Err("the refresh rate is outside 24..=120Hz");
332        }
333        Ok(())
334    }
335
336    /// The packed signal type a scaling write carries for this mode.
337    ///
338    /// Call [`V2ipOutputMode::validate`] first: an unvalidated depth packs as
339    /// the index for "no depth", which a receiver drops.
340    pub(crate) fn to_signal_type(self) -> MxrSignalType {
341        MxrSignalType::from_parts(
342            self.svd,
343            self.colour.to_wire(),
344            MxrSignalType::bpp_index_for_depth(self.depth).unwrap_or(0),
345        )
346    }
347}
348
349impl fmt::Display for V2ipOutputMode {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        write!(
352            f,
353            "svd {}, colour {}, {}bpp, {}Hz",
354            self.svd,
355            self.colour.to_wire(),
356            self.depth,
357            self.refresh
358        )
359    }
360}
361
362impl V2ipScalingSettings {
363    /// The mode this sink is configured to scale to, `None` when it has none.
364    ///
365    /// The two are distinct on the wire: a sink with no mode configured leaves
366    /// [`SCALING_FLAG_MODE_VALID`] clear, and never sets it over a zero mode.
367    ///
368    /// Trust it only where the sender reports
369    /// [`crate::DeviceInfo::config_initialised`]. Firmware without that builds
370    /// this block over uninitialised stack, where the valid bit itself is
371    /// noise.
372    pub const fn configured_mode(&self) -> Option<(MxrSignalType, u16)> {
373        if self.flags & SCALING_FLAG_MODE_VALID == 0 {
374            return None;
375        }
376        Some((self.mode, self.refresh))
377    }
378
379    /// Whether the output scales automatically, `None` when the sender did not
380    /// say.
381    pub const fn auto_scaling(&self) -> Option<bool> {
382        if self.flags & SCALING_FLAG_OPTIONS_VALID == 0 {
383            return None;
384        }
385        Some(self.flags & SCALING_FLAG_AUTO_SCALING != 0)
386    }
387
388    /// Whether the output follows its source's format, `None` when the device
389    /// has never said.
390    ///
391    /// Firmware with this option announces it on every configuration it sends
392    /// about itself, so a device that has reported it once is known to have
393    /// it. The cached block accumulates its validity bits, so a later write
394    /// carrying only the first options group does not take that back.
395    ///
396    /// Reported only from a sender announcing
397    /// [`crate::DeviceInfo::config_initialised`], so this needs no caveat of
398    /// its own: no firmware has these options without that announcement, and
399    /// one that lacks it would be reporting uninitialised stack. That is the
400    /// difference from [`Self::configured_mode`], which is reported from any
401    /// sender because a mode can be genuine on one of those.
402    pub const fn match_source(&self) -> Option<bool> {
403        if self.flags & SCALING_FLAG_OPTIONS2_VALID == 0 {
404            return None;
405        }
406        Some(self.flags & SCALING_FLAG_MATCH_SOURCE != 0)
407    }
408
409    /// Whether the output declines 4:2:0 rather than scaling it, `None` when
410    /// the device has never said. Reported on the same terms as
411    /// [`Self::match_source`], which shares its validity bit.
412    pub const fn skip_420(&self) -> Option<bool> {
413        if self.flags & SCALING_FLAG_OPTIONS2_VALID == 0 {
414            return None;
415        }
416        Some(self.flags & SCALING_FLAG_SKIP_420 != 0)
417    }
418
419    /// Folds a received scaling config onto the cached one, field by field.
420    ///
421    /// A write carries the mode or the options alone, so taking the block
422    /// wholesale would drop whichever half was not being written. The options
423    /// branch replaces the option bit rather than adding to it, which is what
424    /// lets an options-only write clear [`SCALING_FLAG_AUTO_SCALING`].
425    #[must_use]
426    pub fn merge(self, previous: Self) -> Self {
427        let mut out = previous;
428        if self.flags & SCALING_FLAG_MODE_VALID != 0 {
429            out.mode = self.mode;
430            out.refresh = self.refresh;
431            out.flags |= SCALING_FLAG_MODE_VALID;
432        }
433        if self.flags & SCALING_FLAG_OPTIONS_VALID != 0 {
434            out.flags &= !SCALING_FLAG_AUTO_SCALING;
435            out.flags |= SCALING_FLAG_OPTIONS_VALID;
436            out.flags |= self.flags & SCALING_FLAG_AUTO_SCALING;
437        }
438        // One validity bit covers both options in this group, so a frame
439        // carrying it replaces both and a frame without it leaves both alone -
440        // which is also what keeps the bit itself, and so the knowledge that
441        // the device has these options, from being taken back by a later write
442        // that carries only the first group.
443        if self.flags & SCALING_FLAG_OPTIONS2_VALID != 0 {
444            out.flags &= !(SCALING_FLAG_MATCH_SOURCE | SCALING_FLAG_SKIP_420);
445            out.flags |= SCALING_FLAG_OPTIONS2_VALID;
446            out.flags |= self.flags & (SCALING_FLAG_MATCH_SOURCE | SCALING_FLAG_SKIP_420);
447        }
448        out
449    }
450}
451
452/// The per-stream DSCP marking in a OneIP device configuration.
453///
454/// A stream whose wire byte carries no [`V2IP_DSCP_SET`] bit reads back as
455/// `None`. Firmware treats the marking as all-or-nothing: it applies one only
456/// when all three streams carry a value and otherwise falls back to the
457/// default, so [`V2ipDscpConfig::is_complete`] reports which case a frame is in.
458#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
459pub struct V2ipDscpConfig {
460    /// Marking for the video stream.
461    pub video: Option<u8>,
462    /// Marking for the audio stream.
463    pub audio: Option<u8>,
464    /// Marking for the ancillary-data stream.
465    pub anc: Option<u8>,
466}
467
468impl V2ipDscpConfig {
469    /// Reports whether all three streams carry a marking, which is what
470    /// firmware requires before it applies one.
471    pub const fn is_complete(&self) -> bool {
472        self.video.is_some() && self.audio.is_some() && self.anc.is_some()
473    }
474}
475
476impl fmt::Display for V2ipDscpConfig {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        match (self.video, self.audio, self.anc) {
479            (Some(v), Some(a), Some(n)) => write!(f, "video:{v} audio:{a} anc:{n}"),
480            _ => f.write_str("no marking"),
481        }
482    }
483}
484
485/// Decodes one `dscp` byte, or `None` when the byte carries no marking.
486pub(crate) fn parse_dscp(raw: u8) -> Option<u8> {
487    (raw & V2IP_DSCP_SET != 0).then_some(raw & V2IP_DSCP_MAX)
488}
489
490/// The local encoder/decoder configuration of a OneIP device.
491#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
492pub struct DeviceV2ipDetails {
493    /// The video stream this device sources.
494    pub video: V2ipStreamSource,
495    /// The audio stream this device sources.
496    pub audio: V2ipStreamSource,
497    /// The ancillary-data stream this device sources.
498    pub anc: V2ipStreamSource,
499    /// The audio-return stream this device sources.
500    pub arc: V2ipStreamSource,
501
502    /// Encoder rate in units of 10Mb/s, or `None` when the sender offered no
503    /// rate.
504    ///
505    /// A rate-only write carries the rate on its own; every other controller
506    /// write puts a value outside the valid range here, which firmware drops as
507    /// invalid so that address-only and scaling writes leave the peer's rate
508    /// alone.
509    pub tx_rate: Option<u8>,
510
511    /// Per-stream DSCP marking.
512    pub dscp: V2ipDscpConfig,
513    /// Scaling mode, refresh rate and flags.
514    pub scaling: V2ipScalingSettings,
515}
516
517impl DeviceV2ipDetails {
518    /// Reports whether the source block carries usable addresses.
519    ///
520    /// Firmware requires video and anc; audio is optional and is carried with
521    /// them.
522    pub const fn source_is_valid(&self) -> bool {
523        self.video.is_valid() && self.anc.is_valid()
524    }
525
526    /// Folds a received device configuration onto the cached one.
527    ///
528    /// Every field is optional behind its own validity marker: the payload is
529    /// zeroed before a sender fills in the one field it is writing, so a
530    /// controller writing a TX rate sends zeroed addresses and a controller
531    /// writing addresses sends an out-of-range rate. Firmware applies each
532    /// field only behind its own test, so replacing the whole cached config on
533    /// every frame would make the peer read back with its addresses, rate or
534    /// marking gone.
535    #[must_use]
536    pub fn merge(mut self, previous: Option<Self>) -> Self {
537        let Some(previous) = previous else {
538            return self;
539        };
540        if !self.source_is_valid() {
541            self.video = previous.video;
542            self.audio = previous.audio;
543            self.anc = previous.anc;
544        }
545        if !self.arc.is_valid() {
546            self.arc = previous.arc;
547        }
548        if self.tx_rate.is_none() {
549            self.tx_rate = previous.tx_rate;
550        }
551        // A unit keeps the markings only when all three carry their set bit.
552        if !self.dscp.is_complete() {
553            self.dscp = previous.dscp;
554        }
555        self.scaling = self.scaling.merge(previous.scaling);
556        self
557    }
558}
559
560/// The sink-side route a OneIP device is subscribed to, as the mesh believes it.
561///
562/// A route request addressed to the device sets this the moment it is seen,
563/// which is what every device on the mesh does with one. So a request the
564/// device refused, or that reached it while it was offline, reads back here as
565/// though it had taken effect. Only the device's own configuration report
566/// confirms a route, and it sends that on its own schedule rather than in reply.
567///
568/// **Addresses that read as unset mean "no route, or the sink could not work
569/// one out" - never "definitely not subscribed".** This block is the one part
570/// of a device configuration with no validity marker of its own, so a sender
571/// with nothing to say sends zeros and every receiver stores them. A sender
572/// leaves it empty when its own stream configuration does not resolve, and that
573/// covers more than having no route: a selected source whose record has not
574/// arrived yet, which is the state after a restart at either end, missing audio
575/// bay configuration, or any of the three streams failing its validity check.
576/// The audio format has a second gate of its own, so it can be absent while the
577/// addresses are not.
578///
579/// This is worth expecting rather than guarding against. Any scaling change
580/// makes the device rebuild and rebroadcast this block - so the empty reading
581/// arrives most often during exactly the no-signal troubleshooting that
582/// prompted the change. A device's periodic report puts a real route back
583/// within a minute of it having one.
584///
585/// An empty reading is applied rather than ignored on purpose. A sink that has
586/// genuinely dropped its route sends the same zeros, and so does every report
587/// after it, so refusing them would cache a route that nothing later could ever
588/// clear.
589///
590/// A configuration frame sets this only when the device sent it about itself.
591/// A controller writing another device's configuration sends the block zeroed
592/// because it has nothing to say about that sink, so its frame is ignored here.
593#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
594pub struct DeviceV2ipSink {
595    /// The streams the sink subscribes to.
596    pub addresses: V2ipStreamSources,
597    /// The resolved audio format, when the sender reported one.
598    pub audio_fmt: Option<V2ipAudioFormat>,
599}
600
601/// The highest PTP domain a OneIP device runs in.
602pub const V2IP_PTP_DOMAIN_MAX: u8 = 127;
603
604/// Minutes in a day: every time in a [`V2ipPowerSaveSchedule`] is below this.
605pub const V2IP_MINUTES_PER_DAY: u16 = 24 * 60;
606
607/// A OneIP device's daily power save windows, Monday first, in the device's
608/// time zone.
609///
610/// Day `d` powers down at `start[d]` and up again at `end[d]`, both in minutes
611/// after midnight. A window belongs to the day it starts on and runs past
612/// midnight into the next when it ends before it starts; one that ends where it
613/// starts means no window that day.
614#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
615pub struct V2ipPowerSaveSchedule {
616    /// When each day's window starts.
617    pub start: [u16; 7],
618    /// When each day's window ends.
619    pub end: [u16; 7],
620}
621
622impl V2ipPowerSaveSchedule {
623    /// The window of day `day`, Monday being 0, or `None` for a day without
624    /// one or past Sunday.
625    pub fn window(&self, day: usize) -> Option<(u16, u16)> {
626        let (start, end) = (*self.start.get(day)?, *self.end.get(day)?);
627        (start != end).then_some((start, end))
628    }
629
630    /// Whether every time is a time of day.
631    pub fn is_valid(&self) -> bool {
632        self.start
633            .iter()
634            .chain(&self.end)
635            .all(|m| *m < V2IP_MINUTES_PER_DAY)
636    }
637}
638
639/// The device settings of a OneIP unit, as it reports them and as its
640/// controller changes them.
641///
642/// Each setting is carried only behind its bit in [`valid`](Self::valid), so a
643/// frame changes one setting without restating the others, and a device
644/// reports only the settings it has. Read a setting through the accessors,
645/// which answer `None` for one the device has not reported.
646#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
647pub struct V2ipDeviceSettings {
648    /// The settings reported so far.
649    pub valid: V2ipDeviceSetting,
650    /// The values of the on/off settings among [`valid`](Self::valid).
651    pub flags: V2ipDeviceSetting,
652    /// The infrared profiles stored on the device, bit n for profile n.
653    pub ir_profiles: u32,
654    /// The infrared profile of the global infrared port.
655    pub ir_profile: i8,
656    /// The infrared profile of the output's infrared port, or
657    /// [`V2IP_IR_PROFILE_NOT_SET`] when it follows the global one.
658    pub ir_profile_sink: i8,
659    /// The minutes the device stays idle before it powers down by itself, 0
660    /// for never.
661    pub auto_power_save: u16,
662    /// The daily windows in which the device powers down.
663    pub power_save: V2ipPowerSaveSchedule,
664    /// The PTP mode the device runs.
665    pub ptp_mode: V2ipPtpMode,
666    /// The PTP domain the device runs in, up to [`V2IP_PTP_DOMAIN_MAX`].
667    pub ptp_domain: u8,
668    /// The priority1 the device announces as a PTP grandmaster, lower winning.
669    pub ptp_priority1: u8,
670    /// The level the device powers down to in power save. While
671    /// [`POWER_SAVE_DISABLED`](V2ipDeviceSetting::POWER_SAVE_DISABLED) is on,
672    /// the level a power save request powers it down to.
673    pub power_save_level: V2ipPowerSaveLevel,
674    /// The device's PTP message settings.
675    pub ptp_messages: V2ipPtpMessages,
676}
677
678/// How a OneIP output mixes 5.1 and 7.1 PCM down to stereo, and whether it
679/// does now.
680///
681/// A device reports it once its video processor has answered for it, so
682/// never one whose output cannot mix down.
683#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
684pub struct V2ipDownmix {
685    /// When the output mixes down.
686    pub mode: V2ipDownmixMode,
687    /// How loud the mix is.
688    pub level: V2ipDownmixLevel,
689    /// The percent the LFE channel is mixed in at, at most
690    /// [`V2IP_DOWNMIX_LFE_MAX`]; 0 leaves it out.
691    pub lfe: u8,
692    /// Why the stream is or is not mixed down, as the device's own code. Only
693    /// the device reports it.
694    pub state: u8,
695    /// The stream's channels while it is mixed down, else 0. Only the device
696    /// reports it.
697    pub channels: u8,
698}
699
700/// The largest percent a OneIP output mixes its LFE channel in at.
701pub const V2IP_DOWNMIX_LFE_MAX: u8 = 100;
702
703impl V2ipDownmix {
704    /// Whether the settings a write carries are in range.
705    pub(crate) fn is_valid(&self) -> bool {
706        self.mode <= V2ipDownmixMode::ALWAYS
707            && self.level <= V2ipDownmixLevel::UNSCALED
708            && self.lfe <= V2IP_DOWNMIX_LFE_MAX
709    }
710
711    /// The settings of `write` applied to this report, which keeps what only
712    /// the device reports.
713    #[must_use]
714    pub(crate) fn written(self, write: Self) -> Self {
715        Self {
716            mode: write.mode,
717            level: write.level,
718            lfe: write.lfe,
719            ..self
720        }
721    }
722}
723
724/// How a OneIP device paces its PTP messages, its SAP timings, its streams'
725/// TTL and its output's rotation, which a frame carries in the same block. Each value is behind its own bit of
726/// [`V2ipDeviceSettings::valid`], one of
727/// [`SETTINGS_BLOCK`](V2ipDeviceSetting::SETTINGS_BLOCK).
728///
729/// Intervals are base-2 logarithms of seconds, so -3 is 125 ms and 1 is 2 s.
730#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
731pub struct V2ipPtpMessages {
732    /// Between announce messages, behind
733    /// [`PTP_ANNOUNCE`](V2ipDeviceSetting::PTP_ANNOUNCE).
734    pub log_announce_interval: i8,
735    /// Between sync messages, behind [`PTP_SYNC`](V2ipDeviceSetting::PTP_SYNC).
736    pub log_sync_interval: i8,
737    /// Between delay requests, behind
738    /// [`PTP_DELAY_REQ`](V2ipDeviceSetting::PTP_DELAY_REQ).
739    pub log_delay_req_interval: i8,
740    /// The announce intervals before a silent master is dropped, behind
741    /// [`PTP_ANNOUNCE_TIMEOUT`](V2ipDeviceSetting::PTP_ANNOUNCE_TIMEOUT).
742    pub announce_timeout: u8,
743    /// The DSCP of the messages, at most [`V2IP_DSCP_MAX`], behind
744    /// [`PTP_DSCP`](V2ipDeviceSetting::PTP_DSCP).
745    pub dscp: u8,
746    /// The minutes after which a SAP announcement the device heard is
747    /// forgotten, behind [`SAP_EXPIRE`](V2ipDeviceSetting::SAP_EXPIRE).
748    pub sap_expire_min: u8,
749    /// The seconds between the device's SAP announcements, behind
750    /// [`SAP_INTERVAL`](V2ipDeviceSetting::SAP_INTERVAL).
751    pub sap_interval_s: u16,
752    /// The TTL of the device's streams, 1 to 255, behind
753    /// [`STREAM_TTL`](V2ipDeviceSetting::STREAM_TTL). A device whose video
754    /// processor cannot change it keeps the setting and sends
755    /// [`V2IP_STREAM_TTL_DEFAULT`].
756    pub stream_ttl: u8,
757    /// The degrees the device's HDMI output is turned by, clockwise, at most
758    /// [`V2IP_SINK_ROTATION_MAX`], behind
759    /// [`SINK_ROTATION`](V2ipDeviceSetting::SINK_ROTATION).
760    pub sink_rotation: u16,
761    /// The step the output turns by: the device takes only an angle that is
762    /// a multiple of it, and 0 alone when it is 0. Only the device reports
763    /// it, behind the same bit.
764    pub sink_rotation_step: u16,
765}
766
767/// The TTL a OneIP device's streams carry until it is set.
768pub const V2IP_STREAM_TTL_DEFAULT: u8 = 64;
769
770/// The largest angle a OneIP device's HDMI output is turned by, in degrees.
771pub const V2IP_SINK_ROTATION_MAX: u16 = 359;
772
773impl V2ipPtpMessages {
774    /// The values behind the bits of `which` copied from `from`, the rest
775    /// kept.
776    #[must_use]
777    pub(crate) fn merge(self, from: Self, which: V2ipDeviceSetting) -> Self {
778        let mut out = self;
779        if which.has(V2ipDeviceSetting::PTP_ANNOUNCE) {
780            out.log_announce_interval = from.log_announce_interval;
781        }
782        if which.has(V2ipDeviceSetting::PTP_SYNC) {
783            out.log_sync_interval = from.log_sync_interval;
784        }
785        if which.has(V2ipDeviceSetting::PTP_DELAY_REQ) {
786            out.log_delay_req_interval = from.log_delay_req_interval;
787        }
788        if which.has(V2ipDeviceSetting::PTP_ANNOUNCE_TIMEOUT) {
789            out.announce_timeout = from.announce_timeout;
790        }
791        if which.has(V2ipDeviceSetting::PTP_DSCP) {
792            out.dscp = from.dscp;
793        }
794        if which.has(V2ipDeviceSetting::SAP_EXPIRE) {
795            out.sap_expire_min = from.sap_expire_min;
796        }
797        if which.has(V2ipDeviceSetting::SAP_INTERVAL) {
798            out.sap_interval_s = from.sap_interval_s;
799        }
800        if which.has(V2ipDeviceSetting::STREAM_TTL) {
801            out.stream_ttl = from.stream_ttl;
802        }
803        if which.has(V2ipDeviceSetting::SINK_ROTATION) {
804            out.sink_rotation = from.sink_rotation;
805            out.sink_rotation_step = from.sink_rotation_step;
806        }
807        out
808    }
809
810    /// Whether a device reporting this step takes `degrees` as its output's
811    /// rotation.
812    pub(crate) const fn takes_sink_rotation(&self, degrees: u16) -> bool {
813        degrees == 0
814            || (degrees <= V2IP_SINK_ROTATION_MAX
815                && self.sink_rotation_step != 0
816                && degrees % self.sink_rotation_step == 0)
817    }
818}
819
820impl V2ipDeviceSettings {
821    /// Whether an on/off setting is on, `None` while the device has not
822    /// reported it.
823    pub const fn get(&self, setting: V2ipDeviceSetting) -> Option<bool> {
824        if !self.valid.has(setting) {
825            return None;
826        }
827        Some(self.flags.has(setting))
828    }
829
830    /// The global infrared port's profile, `None` while it is not reported.
831    pub const fn ir_profile(&self) -> Option<i8> {
832        if !self.valid.has(V2ipDeviceSetting::IR_PROFILE) {
833            return None;
834        }
835        Some(self.ir_profile)
836    }
837
838    /// The output infrared port's profile, `None` while it is not reported.
839    /// [`V2IP_IR_PROFILE_NOT_SET`] means the port follows the global one.
840    pub const fn ir_profile_sink(&self) -> Option<i8> {
841        if !self.valid.has(V2ipDeviceSetting::IR_PROFILE_SINK) {
842            return None;
843        }
844        Some(self.ir_profile_sink)
845    }
846
847    /// The infrared profiles stored on the device, bit n for profile n, `None`
848    /// while it is not reported.
849    pub const fn stored_ir_profiles(&self) -> Option<u32> {
850        if !self.valid.has(V2ipDeviceSetting::IR_PROFILES) {
851            return None;
852        }
853        Some(self.ir_profiles)
854    }
855
856    /// The idle minutes before the device powers down by itself, 0 for never,
857    /// `None` while it is not reported.
858    pub const fn auto_power_save(&self) -> Option<u16> {
859        if !self.valid.has(V2ipDeviceSetting::AUTO_POWER_SAVE) {
860            return None;
861        }
862        Some(self.auto_power_save)
863    }
864
865    /// The daily power save windows, `None` while they are not reported.
866    pub const fn power_save_schedule(&self) -> Option<V2ipPowerSaveSchedule> {
867        if !self.valid.has(V2ipDeviceSetting::POWER_SAVE_SCHEDULE) {
868            return None;
869        }
870        Some(self.power_save)
871    }
872
873    /// The PTP mode, `None` while it is not reported.
874    pub const fn ptp_mode(&self) -> Option<V2ipPtpMode> {
875        if !self.valid.has(V2ipDeviceSetting::PTP_MODE) {
876            return None;
877        }
878        Some(self.ptp_mode)
879    }
880
881    /// The PTP domain, `None` while it is not reported.
882    pub const fn ptp_domain(&self) -> Option<u8> {
883        if !self.valid.has(V2ipDeviceSetting::PTP_DOMAIN) {
884            return None;
885        }
886        Some(self.ptp_domain)
887    }
888
889    /// The PTP priority1, `None` while it is not reported.
890    pub const fn ptp_priority1(&self) -> Option<u8> {
891        if !self.valid.has(V2ipDeviceSetting::PTP_PRIORITY1) {
892            return None;
893        }
894        Some(self.ptp_priority1)
895    }
896
897    /// The level the device powers down to in power save, which it picks
898    /// itself, `None` while it is not reported.
899    pub const fn power_save_level(&self) -> Option<V2ipPowerSaveLevel> {
900        if !self.valid.has(V2ipDeviceSetting::POWER_SAVE_LEVEL) {
901            return None;
902        }
903        Some(self.power_save_level)
904    }
905
906    /// The PTP message and SAP settings the device has reported, `None` while
907    /// it has reported none of them; each value is meaningful only behind its
908    /// own bit of [`valid`](Self::valid).
909    pub fn ptp_messages(&self) -> Option<V2ipPtpMessages> {
910        (!(self.valid & V2ipDeviceSetting::SETTINGS_BLOCK).is_empty()).then_some(self.ptp_messages)
911    }
912
913    /// Folds a received settings block onto the cached one.
914    ///
915    /// Each bit in the frame's [`valid`](Self::valid) replaces its own setting
916    /// and leaves the others alone, so a write about one setting does not
917    /// clear what was known about the rest.
918    #[must_use]
919    pub(crate) fn merge(self, previous: Self) -> Self {
920        let valid = self.valid;
921        let mut out = previous;
922        out.valid |= valid;
923        out.flags = previous.flags.without(valid) | (self.flags & valid);
924        // A device takes a power save level as leaving power save enabled.
925        if valid.has(V2ipDeviceSetting::POWER_SAVE_LEVEL)
926            && !valid.has(V2ipDeviceSetting::POWER_SAVE_DISABLED)
927        {
928            out.flags = out.flags.without(V2ipDeviceSetting::POWER_SAVE_DISABLED);
929        }
930        if valid.has(V2ipDeviceSetting::IR_PROFILE) {
931            out.ir_profile = self.ir_profile;
932        }
933        if valid.has(V2ipDeviceSetting::IR_PROFILE_SINK) {
934            out.ir_profile_sink = self.ir_profile_sink;
935        }
936        if valid.has(V2ipDeviceSetting::IR_PROFILES) {
937            out.ir_profiles = self.ir_profiles;
938        }
939        if valid.has(V2ipDeviceSetting::AUTO_POWER_SAVE) {
940            out.auto_power_save = self.auto_power_save;
941        }
942        if valid.has(V2ipDeviceSetting::POWER_SAVE_SCHEDULE) {
943            out.power_save = self.power_save;
944        }
945        if valid.has(V2ipDeviceSetting::PTP_MODE) {
946            out.ptp_mode = self.ptp_mode;
947        }
948        if valid.has(V2ipDeviceSetting::PTP_DOMAIN) {
949            out.ptp_domain = self.ptp_domain;
950        }
951        if valid.has(V2ipDeviceSetting::PTP_PRIORITY1) {
952            out.ptp_priority1 = self.ptp_priority1;
953        }
954        if valid.has(V2ipDeviceSetting::POWER_SAVE_LEVEL) {
955            out.power_save_level = self.power_save_level;
956        }
957        out.ptp_messages = previous.ptp_messages.merge(self.ptp_messages, valid);
958        out
959    }
960
961    /// This block limited to what a device takes from a write about it.
962    ///
963    /// A device applies a setting only if it has it, a profile only within
964    /// its range, a rotation only by its own step, and never what only it can
965    /// report about itself. It picks its power save level itself, and takes
966    /// a level it is written, without
967    /// [`POWER_SAVE_DISABLED`](V2ipDeviceSetting::POWER_SAVE_DISABLED), as
968    /// switching power save on.
969    #[must_use]
970    pub(crate) fn as_applied_to(self, reported: &Self) -> Self {
971        let mut valid = (self.valid & reported.valid).without(V2ipDeviceSetting::REPORTED_ONLY);
972        if !(0..V2IP_IR_PROFILE_MAX).contains(&self.ir_profile) {
973            valid = valid.without(V2ipDeviceSetting::IR_PROFILE);
974        }
975        if !(V2IP_IR_PROFILE_NOT_SET..V2IP_IR_PROFILE_MAX).contains(&self.ir_profile_sink) {
976            valid = valid.without(V2ipDeviceSetting::IR_PROFILE_SINK);
977        }
978        if self.ptp_mode > V2ipPtpMode::AUTO {
979            valid = valid.without(V2ipDeviceSetting::PTP_MODE);
980        }
981        if self.ptp_domain > V2IP_PTP_DOMAIN_MAX {
982            valid = valid.without(V2ipDeviceSetting::PTP_DOMAIN);
983        }
984        if self.ptp_messages.dscp > V2IP_DSCP_MAX {
985            valid = valid.without(V2ipDeviceSetting::PTP_DSCP);
986        }
987        if self.ptp_messages.stream_ttl == 0 {
988            valid = valid.without(V2ipDeviceSetting::STREAM_TTL);
989        }
990        if !reported
991            .ptp_messages
992            .takes_sink_rotation(self.ptp_messages.sink_rotation)
993        {
994            valid = valid.without(V2ipDeviceSetting::SINK_ROTATION);
995        }
996        let mut flags = self.flags;
997        if valid.has(V2ipDeviceSetting::POWER_SAVE_LEVEL) {
998            valid = valid.without(V2ipDeviceSetting::POWER_SAVE_LEVEL);
999            if !valid.has(V2ipDeviceSetting::POWER_SAVE_DISABLED)
1000                && reported.valid.has(V2ipDeviceSetting::POWER_SAVE_DISABLED)
1001            {
1002                valid |= V2ipDeviceSetting::POWER_SAVE_DISABLED;
1003                flags = flags.without(V2ipDeviceSetting::POWER_SAVE_DISABLED);
1004            }
1005        }
1006        let ptp_messages = V2ipPtpMessages {
1007            sink_rotation_step: reported.ptp_messages.sink_rotation_step,
1008            ..self.ptp_messages
1009        };
1010        Self {
1011            valid,
1012            flags,
1013            ptp_messages,
1014            ..self
1015        }
1016    }
1017}
1018
1019/// The longest session description a OneIP output takes, in bytes.
1020pub const V2IP_SDP_MAX: usize = 2048;
1021
1022/// What a OneIP output plays from a session description, as it last reported
1023/// it. The stream's fields are zero while it plays none.
1024#[derive(Clone, Debug, PartialEq, Eq)]
1025pub struct V2ipSinkSdp {
1026    /// The report's flags.
1027    pub flags: V2ipSdpFlag,
1028    /// What the output shows while it plays audio alone, as the wire value.
1029    pub screen: u8,
1030    /// The sample encoding.
1031    pub encoding: V2ipSdpEncoding,
1032    /// The channel count.
1033    pub channels: u8,
1034    /// The RTP payload type.
1035    pub payload_type: u8,
1036    /// Where the media clock comes from.
1037    pub media_clock: V2ipSdpMediaClock,
1038    /// The reference clock.
1039    pub ref_clock: V2ipSdpRefClock,
1040    /// What the video processor times the stream by, when `flags` has
1041    /// [`CLOCK`](V2ipSdpFlag::CLOCK).
1042    pub clock_mode: V2ipSdpClockMode,
1043    /// How far the video processor has got with the timing, when `flags` has
1044    /// [`CLOCK`](V2ipSdpFlag::CLOCK).
1045    pub clock_state: V2ipSdpClockState,
1046    /// The destination UDP port.
1047    pub port: u16,
1048    /// The sample rate, in Hz.
1049    pub rate_hz: u32,
1050    /// The media clock offset.
1051    pub media_clock_offset: u32,
1052    /// The multicast group.
1053    pub group: Ipv4Addr,
1054    /// The session name (`s=`).
1055    pub name: String,
1056    /// The channel grouping it plays, empty without one.
1057    pub layout: String,
1058}
1059
1060impl V2ipSinkSdp {
1061    /// Whether the output plays a stream from a description.
1062    pub const fn is_active(&self) -> bool {
1063        self.flags.has(V2ipSdpFlag::ACTIVE)
1064    }
1065}
1066
1067/// How one of a OneIP source unit's streams is encrypted, as the unit reports
1068/// it: the values of the stream's `a=privacy` and `a=extmap` session
1069/// description attributes.
1070///
1071/// A receiver needs these to decrypt the stream. The key itself never
1072/// travels: every unit of a mesh holds the same one. This library holds no
1073/// key and decrypts nothing.
1074#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1075pub struct V2ipPrivacyStream {
1076    /// The stream's destination address.
1077    pub group: Ipv4Addr,
1078    /// The stream's UDP port.
1079    pub port: u16,
1080    /// Steps whenever the parameters change.
1081    pub version: u8,
1082    /// Whether the stream is encrypted. Without it the stream goes out in the
1083    /// clear, and the fields below mean nothing.
1084    pub on: bool,
1085    /// The RTP header extension id of the full IV counter.
1086    pub full_id: u8,
1087    /// The RTP header extension id of the short IV counter.
1088    pub short_id: u8,
1089    /// The initialisation vector.
1090    pub iv: [u8; 8],
1091    /// The key generator.
1092    pub key_generator: [u8; 16],
1093    /// Names the mesh's key. A receiver holding another drops the stream.
1094    pub key_id: [u8; 8],
1095    /// The key version.
1096    pub key_version: u32,
1097}
1098
1099/// How a OneIP source unit's streams are encrypted, as it last reported it.
1100///
1101/// A unit reports this whenever it changes, at least once a minute, and when
1102/// asked. A unit that is in no mesh sends in the clear.
1103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1104pub struct V2ipPrivacy {
1105    /// The video stream.
1106    pub video: V2ipPrivacyStream,
1107    /// The audio stream.
1108    pub audio: V2ipPrivacyStream,
1109    /// The ancillary data stream.
1110    pub anc: V2ipPrivacyStream,
1111}
1112
1113/// A OneIP device's PTP state, as it reports it about itself.
1114///
1115/// Only the device can read its own video processor, so this is never written
1116/// to a device, and a controller's frame about one carries none.
1117#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1118pub struct V2ipPtpState {
1119    /// The block's flags.
1120    pub flags: V2ipPtpFlag,
1121    /// The mode the video processor runs.
1122    pub mode: V2ipPtpMode,
1123    /// The domain the video processor runs in.
1124    pub domain: u8,
1125    /// The IEEE 1588 portState.
1126    pub port_state: u8,
1127    /// How far the clock has locked.
1128    pub lock: V2ipPtpLock,
1129    /// The device's own clock identity.
1130    pub clock_identity: [u8; 8],
1131    /// The clock identity of the grandmaster the device runs on, all zero
1132    /// while it runs on none.
1133    pub grandmaster_identity: [u8; 8],
1134}
1135
1136impl V2ipPtpState {
1137    /// Whether the device locked to a master since its PTP started, so its
1138    /// mesh holds a time.
1139    pub const fn followed(&self) -> bool {
1140        self.flags.has(V2ipPtpFlag::FOLLOWED)
1141    }
1142
1143    /// The grandmaster the device runs on, `None` while it runs on none.
1144    pub fn grandmaster(&self) -> Option<[u8; 8]> {
1145        (self.grandmaster_identity != [0; 8]).then_some(self.grandmaster_identity)
1146    }
1147}
1148
1149/// The external network ports a [`V2ipVlan`] covers, in the order of
1150/// [`V2ipVlan::port`]: the SFP port, the UTP port with PoE, then the UTP port.
1151pub const V2IP_VLAN_PORTS: usize = 3;
1152
1153/// The index of the SFP port in [`V2ipVlan::port`].
1154pub const V2IP_VLAN_PORT_SFP: usize = 0;
1155
1156/// The highest VLAN id: 0 means untagged, and 4095 is reserved by IEEE 802.1Q.
1157pub const V2IP_VLAN_ID_MAX: u16 = 4094;
1158
1159/// VLAN tagging on a OneIP device's uplink.
1160///
1161/// Only the device knows what it runs, so this is what it reported about
1162/// itself. A change written to it is applied at once and stays
1163/// [`PENDING`](V2ipVlanFlag::PENDING) until the mesh controller confirms it,
1164/// which proves the device still reaches the mesh and the mesh still reaches
1165/// it; unconfirmed, the device reverts it.
1166#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1167pub struct V2ipVlan {
1168    /// The block's flags.
1169    pub flags: V2ipVlanFlag,
1170    /// The VLAN id of the device's own traffic, 0 for untagged.
1171    pub device: u16,
1172    /// The VLAN id each downlink port carries, 0 to share the device's
1173    /// untagged traffic, indexed as [`V2IP_VLAN_PORTS`] orders them.
1174    pub port: [u16; V2IP_VLAN_PORTS],
1175    /// The port pinned as the uplink as 1 + its index, 0 to detect it.
1176    pub uplink: u8,
1177    /// Reported by the device: the port in use as the uplink, as 1 + its
1178    /// index.
1179    pub active_uplink: u8,
1180    /// Reported by the device: the seconds before a pending configuration is
1181    /// reverted.
1182    pub revert_s: u8,
1183}
1184
1185impl V2ipVlan {
1186    /// Whether untagged frames arriving on the uplink are dropped.
1187    pub const fn trunk(&self) -> bool {
1188        self.flags.has(V2ipVlanFlag::TRUNK)
1189    }
1190
1191    /// Whether the device reverts this configuration unless the mesh
1192    /// controller confirms it.
1193    pub const fn is_pending(&self) -> bool {
1194        self.flags.has(V2ipVlanFlag::PENDING)
1195    }
1196
1197    /// Whether the device has an SFP port.
1198    pub const fn has_sfp(&self) -> bool {
1199        self.flags.has(V2ipVlanFlag::HAS_SFP)
1200    }
1201
1202    /// The index of the port pinned as the uplink, `None` when the device
1203    /// detects it.
1204    pub const fn pinned_uplink(&self) -> Option<usize> {
1205        port_index(self.uplink)
1206    }
1207
1208    /// The index of the port the device uses as its uplink, `None` for a
1209    /// value that names no port.
1210    pub const fn active_uplink(&self) -> Option<usize> {
1211        port_index(self.active_uplink)
1212    }
1213
1214    /// Whether every VLAN id is at most [`V2IP_VLAN_ID_MAX`] and the uplink is
1215    /// detected or names a port.
1216    pub fn is_valid(&self) -> bool {
1217        let ids_ok = core::iter::once(&self.device)
1218            .chain(&self.port)
1219            .all(|id| *id <= V2IP_VLAN_ID_MAX);
1220        ids_ok && (self.uplink == 0 || self.pinned_uplink().is_some())
1221    }
1222}
1223
1224/// A port index from its 1-based wire form.
1225const fn port_index(wire: u8) -> Option<usize> {
1226    match wire {
1227        1..=3 => Some(wire as usize - 1),
1228        _ => None,
1229    }
1230}
1231
1232/// The lowest test tone frequency, in Hz.
1233pub const V2IP_TONE_FREQ_MIN: u16 = 20;
1234/// The highest test tone frequency, in Hz.
1235pub const V2IP_TONE_FREQ_MAX: u16 = 20_000;
1236/// The quietest test tone level, in dBFS; the loudest is 0.
1237pub const V2IP_TONE_LEVEL_MIN: i8 = -60;
1238/// The most channels a test tone plays on.
1239pub const V2IP_TONE_CHANNELS_MAX: u8 = 8;
1240/// The sample rates a test tone plays at, in Hz.
1241pub const V2IP_TONE_RATES: [u32; 3] = [44_100, 48_000, 96_000];
1242/// The largest lip-sync beep offset, in sample periods.
1243pub const V2IP_SYNC_OFFSET_MAX: u32 = 0x00FF_FFFF;
1244/// The shortest lip-sync beep, in milliseconds.
1245pub const V2IP_SYNC_BEEP_MS_MIN: u16 = 1;
1246/// The longest lip-sync beep, in milliseconds.
1247pub const V2IP_SYNC_BEEP_MS_MAX: u16 = 10_000;
1248
1249/// A OneIP sink's test tone.
1250#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1251pub struct V2ipTestTone {
1252    /// What plays.
1253    pub mode: V2ipToneMode,
1254    /// The frequency, in Hz.
1255    pub freq: u16,
1256    /// The level, in dBFS.
1257    pub level: i8,
1258    /// The channels it plays on.
1259    pub channels: u8,
1260    /// The sample rate, in Hz.
1261    pub rate: u32,
1262}
1263
1264impl V2ipTestTone {
1265    /// Whether a sink plays this tone: a mode it names, and every value in
1266    /// range. A line-up tone needs two channels or more.
1267    pub fn is_valid(&self) -> bool {
1268        let min_channels = if self.mode == V2ipToneMode::LINEUP {
1269            2
1270        } else {
1271            1
1272        };
1273        self.mode.to_wire() <= V2ipToneMode::BEEP.to_wire()
1274            && (V2IP_TONE_FREQ_MIN..=V2IP_TONE_FREQ_MAX).contains(&self.freq)
1275            && (V2IP_TONE_LEVEL_MIN..=0).contains(&self.level)
1276            && (min_channels..=V2IP_TONE_CHANNELS_MAX).contains(&self.channels)
1277            && V2IP_TONE_RATES.contains(&self.rate)
1278    }
1279}
1280
1281/// A OneIP sink's lip-sync flash: a pattern frame is marked every `period`,
1282/// the frame `lead` after a mark flashes white, and a
1283/// [`BEEP`](V2ipToneMode::BEEP) tone starts `offset` sample periods after it.
1284///
1285/// The offset is a calibration: nothing measures the sink's own delay between
1286/// a picture and a sample reaching the HDMI link, and a display adds its own.
1287#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1288pub struct V2ipTestSync {
1289    /// Every this many pattern frames is marked, 0 for none.
1290    pub period: u16,
1291    /// The frame this many after a mark flashes.
1292    pub lead: u16,
1293    /// The sample periods between a mark and the beep.
1294    pub offset: u32,
1295    /// How long the beep lasts, in milliseconds.
1296    pub beep_ms: u16,
1297}
1298
1299impl V2ipTestSync {
1300    /// Whether a sink takes these settings: the lead is below the period, or
1301    /// 0 without one, and the offset and beep are in range.
1302    pub fn is_valid(&self) -> bool {
1303        let lead_ok = if self.period == 0 {
1304            self.lead == 0
1305        } else {
1306            self.lead < self.period
1307        };
1308        lead_ok
1309            && self.offset <= V2IP_SYNC_OFFSET_MAX
1310            && (V2IP_SYNC_BEEP_MS_MIN..=V2IP_SYNC_BEEP_MS_MAX).contains(&self.beep_ms)
1311    }
1312}
1313
1314/// The test card a OneIP sink draws on its output, as it last reported it.
1315///
1316/// A sink reports it only when asked, with
1317/// [`Remote::request_v2ip_testcard`](crate::Remote::request_v2ip_testcard) or
1318/// in answer to a change. The counters run free and wrap.
1319#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1320pub struct V2ipTestcard {
1321    /// What the sink reports about its test card.
1322    pub flags: V2ipTestcardFlag,
1323    /// The pattern.
1324    pub pattern: V2ipTestPattern,
1325    /// The colour of a [`FLAT`](V2ipTestPattern::FLAT) pattern, `0xRRGGBB`.
1326    pub colour: u32,
1327    /// The tone.
1328    pub tone: V2ipTestTone,
1329    /// The lip-sync flash.
1330    pub sync: V2ipTestSync,
1331    /// Pattern frames sent.
1332    pub frames: u32,
1333    /// Sample periods since the tone started.
1334    pub periods: u32,
1335    /// Lip-sync marks the tone has seen.
1336    pub marks: u32,
1337}
1338
1339impl V2ipTestcard {
1340    /// Whether the sink can draw the test card.
1341    pub const fn supported(&self) -> bool {
1342        self.flags.has(V2ipTestcardFlag::SUPPORTED)
1343    }
1344}
1345
1346/// Transmitter stream statistics.
1347#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1348pub struct V2ipTxStats {
1349    /// Video packets sent.
1350    pub video: u32,
1351    /// Audio packets sent.
1352    pub audio: u32,
1353    /// Ancillary-data packets sent.
1354    pub anc: u32,
1355    /// Times the stream went down.
1356    pub stream_down: u32,
1357    /// Transmit overflows.
1358    pub overflow: u32,
1359}
1360
1361/// The health state of a OneIP decoder.
1362#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1363pub struct V2ipDecoderState(u8);
1364
1365impl V2ipDecoderState {
1366    /// The sink has not reported a state.
1367    pub const UNKNOWN: Self = Self(0);
1368    /// Decoding normally.
1369    pub const HEALTHY: Self = Self(1);
1370    /// Failed to decode.
1371    pub const BAD: Self = Self(2);
1372    /// Still coming up, which any sink subscribed to during a route change
1373    /// reports.
1374    pub const STARTING: Self = Self(3);
1375
1376    /// Wraps a raw wire value, including one this library has no name for.
1377    pub const fn from_wire(value: u8) -> Self {
1378        Self(value)
1379    }
1380
1381    /// Returns the raw wire value.
1382    pub const fn to_wire(self) -> u8 {
1383        self.0
1384    }
1385
1386    /// Reports whether the decoder has reached a verdict.
1387    ///
1388    /// Only healthy and bad are verdicts. Testing for failure as "not healthy"
1389    /// reads a receiver that is merely coming up as one that failed to decode,
1390    /// which is what a sink reports for a moment after every route change.
1391    pub const fn is_settled(self) -> bool {
1392        matches!(self, Self::HEALTHY | Self::BAD)
1393    }
1394}
1395
1396impl fmt::Display for V2ipDecoderState {
1397    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1398        match *self {
1399            Self::UNKNOWN => f.write_str("Unknown"),
1400            Self::HEALTHY => f.write_str("Healthy"),
1401            Self::BAD => f.write_str("Bad"),
1402            Self::STARTING => f.write_str("Starting"),
1403            Self(v) => write!(f, "state {v}"),
1404        }
1405    }
1406}
1407
1408/// Receiver stream statistics.
1409#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1410pub struct V2ipRxStats {
1411    /// Video packets received.
1412    pub video_total: u32,
1413    /// Video packets dropped.
1414    pub video_dropped: u32,
1415    /// Video sequence errors.
1416    pub video_seq_errors: u32,
1417    /// Watchdog timeouts.
1418    pub wdt_timeout: u32,
1419    /// Audio packets received.
1420    pub audio_total: u32,
1421    /// Audio packets dropped.
1422    pub audio_dropped: u32,
1423    /// Audio sequence errors.
1424    pub audio_seq_errors: u32,
1425    /// Ancillary-data packets received.
1426    pub anc_total: u32,
1427    /// Ancillary-data packets dropped.
1428    pub anc_dropped: u32,
1429    /// Ancillary-data sequence errors.
1430    pub anc_seq_errors: u32,
1431    /// The decoder's health state.
1432    pub decoder_state: V2ipDecoderState,
1433}
1434
1435/// Why a decoder reports the state it does.
1436///
1437/// The primary cause only. Several causes can be true at once, and which of
1438/// them lands here is a fixed priority order in the firmware that the numbering
1439/// does not express: these values are identities, not ranks, and comparing or
1440/// ordering them says nothing. Ask [`V2ipDecoderReport::has_cause`] whether a
1441/// particular cause applies - a test against this field answers "is this the
1442/// one that won" instead, which is a different question.
1443///
1444/// Firmware adds causes, so the wire value is carried as it arrived: folding an
1445/// unrecognised one onto a named cause would report a fault this library
1446/// invented. Appending one cannot reorder the existing priorities.
1447#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1448pub struct V2ipDecoderReason(u8);
1449
1450impl V2ipDecoderReason {
1451    /// Decoding normally.
1452    pub const OK: Self = Self(0);
1453    /// No packets are arriving.
1454    pub const NO_PACKETS: Self = Self(1);
1455    /// Packets are arriving, degraded.
1456    pub const PACKETS_DEGRADED: Self = Self(2);
1457    /// No format could be recovered from the codestream.
1458    pub const NO_FORMAT: Self = Self(3);
1459    /// The recovered format is not the one the sink is configured for.
1460    pub const FORMAT_MISMATCH: Self = Self(4);
1461    /// The configured output format was refused.
1462    pub const FORMAT_REJECTED: Self = Self(5);
1463    /// The converter watchdog is holding the stream back.
1464    pub const DECODER_BLOCKED: Self = Self(6);
1465    /// A source switch is in progress: a step in an operation someone asked
1466    /// for, rather than a fault.
1467    pub const SWITCH_PENDING: Self = Self(7);
1468    /// PTP is unlocked. That costs audio alone; the picture is unaffected.
1469    pub const PTP_UNLOCKED: Self = Self(8);
1470    /// The pipeline is rebuilding after the HDMI transmitter stayed unlocked.
1471    ///
1472    /// The picture is down, and has been for five seconds before this can
1473    /// appear: the sender debounces the unlocked reading for that long, so
1474    /// this never reports a transient. Unlike [`Self::SWITCH_PENDING`] nobody
1475    /// asked for it.
1476    ///
1477    /// The debounce restarts each time it elapses, so this holding across
1478    /// reports is a restart loop rather than one event, and that is what to
1479    /// escalate on.
1480    ///
1481    /// It sits near the bottom of the priority order, below every input-side
1482    /// cause, so a rebuilding pipeline names one of those in
1483    /// [`V2ipDecoderReport::reason`] and carries this in
1484    /// [`V2ipDecoderReport::flags`] alone - always, rather than briefly.
1485    ///
1486    /// It is evaluated only while no format change is in progress. Across a
1487    /// switch it holds its previous value and clears on the first reading
1488    /// after the change settles, which [`V2ipDecoderReport::updates`] cannot
1489    /// distinguish: a value carried forward is still a stored reading.
1490    pub const TX_BRIDGE_UNLOCKED: Self = Self(9);
1491    /// The sink is configured but switched off, so no stream is expected.
1492    ///
1493    /// This outranks every other cause: whenever it applies it is what
1494    /// [`V2ipDecoderReport::reason`] carries.
1495    ///
1496    /// **The causes beneath it stay set in [`V2ipDecoderReport::flags`].** A
1497    /// sink switched off while it was running keeps the bits the decoder
1498    /// genuinely observed on the way down - no packets, no format - so a
1499    /// classifier that tests a fault mask over the whole word calls a
1500    /// deliberately disabled sink broken. Ask for this cause first and stop
1501    /// there; the bits below it describe what was seen, not a fault to report.
1502    ///
1503    /// This says nothing about geometry, in either direction. The decoder
1504    /// reports what it currently detects whatever the cause, so a switched-off
1505    /// sink still detecting a codestream carries a real geometry, and a zero
1506    /// one means the decoder has nothing rather than that the sink is off.
1507    ///
1508    /// Older senders never report this and give [`Self::NO_PACKETS`] for a
1509    /// disabled sink instead, indistinguishable from one whose source has
1510    /// died. So an absent [`Self::IDLE`] is not evidence a sink is enabled,
1511    /// and **nothing in this block answers enablement**: it carries no such
1512    /// field, and the answer comes from `V2IP_DEVICE_CFG` or the device's HTTP
1513    /// status.
1514    pub const IDLE: Self = Self(10);
1515
1516    /// Wraps a raw wire value, including one this library has no name for.
1517    pub const fn from_wire(value: u8) -> Self {
1518        Self(value)
1519    }
1520
1521    /// Returns the raw wire value.
1522    pub const fn to_wire(self) -> u8 {
1523        self.0
1524    }
1525}
1526
1527impl fmt::Display for V2ipDecoderReason {
1528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1529        match *self {
1530            Self::OK => f.write_str("ok"),
1531            Self::NO_PACKETS => f.write_str("no packets"),
1532            Self::PACKETS_DEGRADED => f.write_str("packets degraded"),
1533            Self::NO_FORMAT => f.write_str("no format recovered"),
1534            Self::FORMAT_MISMATCH => f.write_str("format mismatch"),
1535            Self::FORMAT_REJECTED => f.write_str("format rejected"),
1536            Self::DECODER_BLOCKED => f.write_str("decoder blocked"),
1537            Self::SWITCH_PENDING => f.write_str("switch pending"),
1538            Self::PTP_UNLOCKED => f.write_str("PTP unlocked"),
1539            Self::TX_BRIDGE_UNLOCKED => f.write_str("TX bridge unlocked"),
1540            Self::IDLE => f.write_str("idle"),
1541            Self(v) => write!(f, "reason {v}"),
1542        }
1543    }
1544}
1545
1546/// The colour space a decoder recovered from a codestream.
1547///
1548/// Zero is RGB and is also what a decoder with nothing to decode reports, so no
1549/// value here means "no signal" - [`V2ipDecoderReport::has_geometry`] is what
1550/// answers that.
1551#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1552pub struct V2ipDecoderFormat(u16);
1553
1554impl V2ipDecoderFormat {
1555    /// RGB.
1556    pub const RGB: Self = Self(0);
1557    /// YCbCr 4:4:4.
1558    pub const YCBCR_444: Self = Self(1);
1559    /// YCbCr 4:2:2.
1560    pub const YCBCR_422: Self = Self(2);
1561    /// YCbCr 4:2:0.
1562    pub const YCBCR_420: Self = Self(3);
1563    /// The decoder cannot name the format.
1564    ///
1565    /// 255, which is a value of its own rather than the 0xF a signal report
1566    /// uses for an unknown colour space. Mapping one onto the other yields a
1567    /// colour space the decoder never reported.
1568    pub const UNNAMED: Self = Self(255);
1569
1570    /// Wraps a raw wire value, including one this library has no name for.
1571    pub const fn from_wire(value: u16) -> Self {
1572        Self(value)
1573    }
1574
1575    /// Returns the raw wire value.
1576    pub const fn to_wire(self) -> u16 {
1577        self.0
1578    }
1579}
1580
1581impl fmt::Display for V2ipDecoderFormat {
1582    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1583        match *self {
1584            Self::RGB => f.write_str("RGB"),
1585            Self::YCBCR_444 => f.write_str("YCbCr 4:4:4"),
1586            Self::YCBCR_422 => f.write_str("YCbCr 4:2:2"),
1587            Self::YCBCR_420 => f.write_str("YCbCr 4:2:0"),
1588            Self::UNNAMED => f.write_str("unnamed"),
1589            Self(v) => write!(f, "format {v}"),
1590        }
1591    }
1592}
1593
1594/// What a sink's decoder recovered from the codestream it is being given.
1595///
1596/// This is what the decoder understood, read ahead of the scaler: the geometry
1597/// is unrounded and is not what the display is being sent. It separates "the
1598/// decoder understood the codestream" from "a picture came out the other end".
1599///
1600/// Colour depth is absent on purpose and will stay absent. The video processor
1601/// answers that one from a driver constant rather than from the codestream, so
1602/// there is no reading to carry; assert depth at the encoder's input bay
1603/// instead.
1604#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1605pub struct V2ipDecoderReport {
1606    /// The primary cause of the state the decoder is in.
1607    pub reason: V2ipDecoderReason,
1608    /// The converter watchdog is holding the stream back.
1609    pub blocking: bool,
1610    /// The recovered picture width, and 0 when none was recovered.
1611    pub width: u16,
1612    /// The recovered picture height, and 0 when none was recovered.
1613    pub height: u16,
1614    /// The recovered colour space.
1615    pub format: V2ipDecoderFormat,
1616    /// How many readings the sink has stored. Monotonic, wrapping at 65535
1617    /// after some 36 hours, and never reset.
1618    ///
1619    /// A sink reads its video processor every two seconds and reports every
1620    /// second, so roughly every other report repeats a reading already seen:
1621    /// a frame arriving says nothing about how fresh the values in it are.
1622    /// This counter moves only when a reading is stored, so a processor that
1623    /// stopped answering leaves it still rather than implying a refresh.
1624    ///
1625    /// After pointing a sink at something else, wait for this to advance by
1626    /// two before trusting the geometry. It ticks when a reply lands rather
1627    /// than when a query is sent, so the first tick can carry an answer the
1628    /// processor read fractionally before the switch; the second cannot,
1629    /// because at most one query is outstanding at a time.
1630    pub updates: u16,
1631    /// Every cause that applies, as bit N for reason N. See
1632    /// [`Self::has_cause`].
1633    ///
1634    /// This is what to classify on, once [`V2ipDecoderReason::IDLE`] has been
1635    /// ruled out: that cause outranks the whole word and leaves the bits below
1636    /// it set, so a fault mask over `flags` reports a switched-off sink as
1637    /// broken. [`Self::reason`] carries whichever cause won a fixed priority
1638    /// contest, so a cause that is true can be absent from it while present
1639    /// here. Bit 0 is cleared by the sender, so an empty word means nothing
1640    /// beyond the primary cause applies.
1641    ///
1642    /// [`V2ipDecoderReason::NO_FORMAT`] and
1643    /// [`V2ipDecoderReason::FORMAT_MISMATCH`] are the two arms of one decision
1644    /// and never appear together.
1645    pub flags: u32,
1646    /// How many times the converter watchdog has triggered.
1647    pub blocked_count: u32,
1648}
1649
1650impl V2ipDecoderReport {
1651    /// Reports whether the decoder recovered a geometry.
1652    ///
1653    /// This is what says whether the decoder is being given a codestream it
1654    /// understands. [`Self::format`] cannot: it reads
1655    /// [`V2ipDecoderFormat::RGB`] when nothing is arriving, which is
1656    /// indistinguishable from a real RGB reading.
1657    ///
1658    /// It answers that and nothing else. The reading is taken before any cause
1659    /// is decided, so it does not say whether the sink is switched on: a sink
1660    /// that is off can still detect a codestream, and one that is on can
1661    /// detect nothing.
1662    pub const fn has_geometry(&self) -> bool {
1663        self.width != 0 && self.height != 0
1664    }
1665
1666    /// Reports whether `reason` is among the causes that apply.
1667    ///
1668    /// [`Self::reason`] carries the primary cause and `flags` carries all of
1669    /// them at once. Bit 0 is unused, so [`V2ipDecoderReason::OK`] is never
1670    /// among them and an empty word means nothing beyond the primary cause
1671    /// applies.
1672    pub const fn has_cause(&self, reason: V2ipDecoderReason) -> bool {
1673        let bit = reason.to_wire();
1674        bit > 0 && bit < u32::BITS as u8 && self.flags & (1 << bit) != 0
1675    }
1676}
1677
1678/// What a statistics report says about the sink's decoder.
1679///
1680/// The three states are distinct answers and only [`Self::Answered`] carries a
1681/// reading. `valid` follows the sink being configured rather than the sink
1682/// being enabled, so a sink that is switched off still reports: as
1683/// [`V2ipDecoderReason::IDLE`], or from an older sender as
1684/// [`V2ipDecoderReason::NO_PACKETS`], which is the same reading a sink whose
1685/// source has died produces.
1686#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1687pub enum V2ipDecoderDetail {
1688    /// The report carried no decoder block: the sender's firmware predates it.
1689    #[default]
1690    Absent,
1691    /// The block is there and the decoder has never answered. Every field it
1692    /// would carry is meaningless, so none is offered.
1693    NeverAnswered,
1694    /// A reading.
1695    Answered(V2ipDecoderReport),
1696}
1697
1698impl V2ipDecoderDetail {
1699    /// The reading, for a caller that treats both of the other states as
1700    /// "nothing to show".
1701    pub const fn reading(self) -> Option<V2ipDecoderReport> {
1702        match self {
1703            Self::Answered(report) => Some(report),
1704            _ => None,
1705        }
1706    }
1707}
1708
1709/// The cumulative and per-minute transmit and receive statistics.
1710#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1711pub struct V2ipDeviceStats {
1712    /// Transmit totals since boot.
1713    pub tx: V2ipTxStats,
1714    /// Transmit counts over the last minute.
1715    pub tx_per_minute: V2ipTxStats,
1716    /// Receive totals since boot.
1717    pub rx: V2ipRxStats,
1718    /// Receive counts over the last minute.
1719    pub rx_per_minute: V2ipRxStats,
1720    /// What the sink's decoder recovered from the codestream it is decoding.
1721    pub decoder: V2ipDecoderDetail,
1722}