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