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