Skip to main content

mx_remote/wire/
enums.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Wire enumerations and bitmasks.
5//!
6//! Each type is a newtype over the integer that travels on the wire, with
7//! named constants rather than a closed set of variants. A value this library
8//! has no name for reaches the caller as it arrived: zero is a valid value for
9//! most of these, so a confidently wrong reading is worse than an unrecognised
10//! one.
11
12use core::fmt;
13use core::ops::{BitAnd, BitOr, BitOrAssign};
14
15/// Declares a bitmask newtype over `u32` with the given named bit constants.
16macro_rules! bitmask {
17    (
18        $(#[$meta:meta])*
19        $name:ident { $( $(#[$cmeta:meta])* $cname:ident = $value:expr; )* }
20    ) => {
21        $(#[$meta])*
22        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
23        pub struct $name(u32);
24
25        impl $name {
26            /// No bits set.
27            pub const NONE: Self = Self(0);
28
29            $( $(#[$cmeta])* pub const $cname: Self = Self($value); )*
30
31            /// Wraps a raw wire value, including bits this library has no name for.
32            pub const fn from_bits(bits: u32) -> Self {
33                Self(bits)
34            }
35
36            /// Returns the raw wire value.
37            pub const fn bits(self) -> u32 {
38                self.0
39            }
40
41            /// Reports whether every bit in `other` is set.
42            pub const fn has(self, other: Self) -> bool {
43                self.0 & other.0 == other.0
44            }
45
46            /// Reports whether no bit is set.
47            pub const fn is_empty(self) -> bool {
48                self.0 == 0
49            }
50        }
51
52        impl BitOr for $name {
53            type Output = Self;
54            fn bitor(self, rhs: Self) -> Self {
55                Self(self.0 | rhs.0)
56            }
57        }
58
59        impl BitOrAssign for $name {
60            fn bitor_assign(&mut self, rhs: Self) {
61                self.0 |= rhs.0;
62            }
63        }
64
65        impl BitAnd for $name {
66            type Output = Self;
67            fn bitand(self, rhs: Self) -> Self {
68                Self(self.0 & rhs.0)
69            }
70        }
71
72        impl fmt::Display for $name {
73            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74                write!(f, "{:#010x}", self.0)
75            }
76        }
77    };
78}
79
80/// Declares an enumeration newtype over `$repr` with the given named constants.
81macro_rules! wire_enum {
82    (
83        $(#[$meta:meta])*
84        $name:ident: $repr:ty { $( $(#[$cmeta:meta])* $cname:ident = $value:expr; )* }
85    ) => {
86        $(#[$meta])*
87        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
88        pub struct $name($repr);
89
90        impl $name {
91            $( $(#[$cmeta])* pub const $cname: Self = Self($value); )*
92
93            /// Wraps a raw wire value, including one this library has no name for.
94            pub const fn from_wire(value: $repr) -> Self {
95                Self(value)
96            }
97
98            /// Returns the raw wire value.
99            pub const fn to_wire(self) -> $repr {
100                self.0
101            }
102        }
103    };
104}
105
106bitmask! {
107    /// Capabilities a device reports in its hello frame.
108    DeviceFeature {
109        /// Receives infrared.
110        IR_RX = 1 << 0;
111        /// Transmits infrared.
112        IR_TX = 1 << 1;
113        /// Speaks CEC.
114        CEC = 1 << 2;
115        /// Acts as a V2IP stream source.
116        V2IP_SOURCE = 1 << 3;
117        /// Acts as a V2IP stream sink.
118        V2IP_SINK = 1 << 4;
119        /// Routes video.
120        VIDEO_ROUTING = 1 << 5;
121        /// Routes audio.
122        AUDIO_ROUTING = 1 << 6;
123        /// Controls volume.
124        VOLUME_CONTROL = 1 << 7;
125        /// Supports audio return.
126        AUDIO_RETURN = 1 << 8;
127        /// Passes remote-control commands through.
128        REMOTE_CONTROL = 1 << 9;
129        /// Installer setup has been completed.
130        SETUP_COMPLETED = 1 << 10;
131        /// Is the master of its mesh.
132        MESH_MASTER = 1 << 11;
133        /// Has a notification pending.
134        STATUS_NOTIFY = 1 << 12;
135        /// Has a warning pending.
136        STATUS_WARNING = 1 << 13;
137        /// Has an error pending.
138        STATUS_ERROR = 1 << 14;
139        /// Is about to reboot.
140        STATUS_REBOOT = 1 << 15;
141        /// Is a member of a mesh.
142        MESH_MEMBER = 1 << 16;
143        /// Is an audio amplifier.
144        AUDIO_AMPLIFIER = 1 << 17;
145        /// Is still booting.
146        BOOTING = 1 << 18;
147        /// Is a management client rather than a device.
148        MANAGER = 1 << 19;
149        /// Is in power-save mode.
150        STATUS_POWER_SAVE = 1 << 20;
151        /// Supports meshing.
152        MESH = 1 << 21;
153        /// Is a multiviewer.
154        MULTIVIEWER = 1 << 22;
155        /// Has crashed since it last booted.
156        STATUS_CRASHED = 1 << 23;
157        /// Supports video walls.
158        VIDEO_WALL = 1 << 24;
159        /// Initialises the configuration it broadcasts.
160        ///
161        /// Firmware without this bit sends a device configuration built over
162        /// uninitialised memory, so fields it did not mean to write carry junk.
163        CONFIG_INITIALISED = 1 << 25;
164        /// Set while the device is in its boot loader.
165        BOOT_BIT = 1 << 31;
166    }
167}
168
169bitmask! {
170    /// Capabilities of a single bay.
171    BayFeatures {
172        /// HDMI output.
173        HDMI_OUT = 1 << 0;
174        /// HDMI input.
175        HDMI_IN = 1 << 1;
176        /// Digital audio output.
177        AUDIO_DIG_OUT = 1 << 2;
178        /// Digital audio input.
179        AUDIO_DIG_IN = 1 << 3;
180        /// Analogue audio output.
181        AUDIO_ANA_OUT = 1 << 4;
182        /// Analogue audio input.
183        AUDIO_ANA_IN = 1 << 5;
184        /// Infrared input.
185        IR_IN = 1 << 6;
186        /// Infrared output.
187        IR_OUT = 1 << 7;
188        /// Amplified audio output.
189        AUDIO_AMP_OUT = 1 << 8;
190        /// Remote-control output.
191        RC_OUT = 1 << 9;
192        /// Remote-control input.
193        RC_IN = 1 << 10;
194        /// Dolby decoding.
195        DOLBY = 1 << 11;
196        /// Switches itself off when idle.
197        AUTO_OFF = 1 << 12;
198        /// Is a remote V2IP source.
199        V2IP_SOURCE_REMOTE = 1 << 13;
200        /// Is a remote V2IP sink.
201        V2IP_SINK_REMOTE = 1 << 14;
202        /// Is a local V2IP source.
203        V2IP_SOURCE_LOCAL = 1 << 15;
204        /// Is a local V2IP sink.
205        V2IP_SINK_LOCAL = 1 << 16;
206    }
207}
208
209bitmask! {
210    /// Live status flags of a single bay.
211    ///
212    /// Bits 16-19 and 22-23 are bit-fields rather than flags; read them with
213    /// [`BayStatus::rc_type`] and [`BayStatus::hdcp`].
214    BayStatus {
215        /// The bay reports a fault.
216        FAULT = 1 << 0;
217        /// The bay is hidden from the user interface.
218        HIDDEN = 1 << 1;
219        /// The bay has power.
220        POWERED = 1 << 2;
221        /// A signal is present.
222        SIGNAL_DETECTED = 1 << 3;
223        /// Hot-plug detect is asserted.
224        HPD_DETECTED = 1 << 4;
225        /// The signal is scrambled.
226        SIGNAL_SCRAMBLE = 1 << 5;
227        /// An HDBaseT link is up.
228        HDBT_CONNECTED = 1 << 6;
229        /// A CEC device answered.
230        CEC_DETECTED = 1 << 7;
231        /// The attached device was powered on.
232        POWERED_ON = 1 << 8;
233        /// The attached device was powered off.
234        POWERED_OFF = 1 << 9;
235        /// Audio return over HDMI is active.
236        AUDIO_ARC_HDMI = 1 << 10;
237        /// Audio return over optical is active.
238        AUDIO_ARC_OPTIC = 1 << 11;
239        /// Audio return over analogue is active.
240        AUDIO_ARC_ANALOG = 1 << 12;
241        /// The bay is offline.
242        OFFLINE = 1 << 13;
243        /// The V2IP decoder is disabled.
244        DECODER_DISABLE = 1 << 14;
245        /// The V2IP encoder is disabled.
246        ENCODER_DISABLE = 1 << 15;
247        /// CEC is switched off for this bay.
248        CEC_DISABLED = 1 << 20;
249        /// The V2IP encoder reports an error.
250        ENCODER_ERROR = 1 << 21;
251    }
252}
253
254impl BayStatus {
255    const RC_TYPE_SHIFT: u32 = 16;
256    const RC_TYPE_MASK: u32 = 0xF << Self::RC_TYPE_SHIFT;
257    const HDCP_SHIFT: u32 = 22;
258    const HDCP_MASK: u32 = 0x3 << Self::HDCP_SHIFT;
259
260    /// Extracts the remote-control type carried in bits 16-19.
261    pub const fn rc_type(self) -> RcType {
262        RcType(((self.0 & Self::RC_TYPE_MASK) >> Self::RC_TYPE_SHIFT) as u8)
263    }
264
265    /// Extracts the HDCP version carried in bits 22-23.
266    pub const fn hdcp(self) -> u8 {
267        ((self.0 & Self::HDCP_MASK) >> Self::HDCP_SHIFT) as u8
268    }
269}
270
271bitmask! {
272    /// Media carried by a virtual link.
273    LinkFeature {
274        /// Video over HDMI.
275        VIDEO_HDMI = 1 << 0;
276        /// Audio over optical.
277        AUDIO_OPTICAL = 1 << 1;
278        /// Audio over analogue.
279        AUDIO_ANALOG = 1 << 2;
280        /// Infrared.
281        IR = 1 << 3;
282        /// Remote control.
283        RC = 1 << 4;
284    }
285}
286
287wire_enum! {
288    /// A remote-control action.
289    RcAction: u16 {
290        /// Toggle power.
291        POWER_TOGGLE = 0;
292        /// Power on.
293        POWER_ON = 1;
294        /// Power off.
295        POWER_OFF = 2;
296        /// Volume down.
297        VOLUME_DOWN = 3;
298        /// Volume up.
299        VOLUME_UP = 4;
300        /// Toggle mute.
301        VOLUME_MUTE = 5;
302    }
303}
304
305wire_enum! {
306    /// A remote-control key code (CEC or IR).
307    RcKey: u16 {
308        /// Digit 0.
309        NUM0 = 0;
310        /// Digit 1.
311        NUM1 = 1;
312        /// Digit 2.
313        NUM2 = 2;
314        /// Digit 3.
315        NUM3 = 3;
316        /// Digit 4.
317        NUM4 = 4;
318        /// Digit 5.
319        NUM5 = 5;
320        /// Digit 6.
321        NUM6 = 6;
322        /// Digit 7.
323        NUM7 = 7;
324        /// Digit 8.
325        NUM8 = 8;
326        /// Digit 9.
327        NUM9 = 9;
328        /// Confirm the highlighted item.
329        SELECT = 10;
330        /// Go back one step.
331        BACK = 11;
332        /// Navigate up.
333        UP = 12;
334        /// Navigate down.
335        DOWN = 13;
336        /// Navigate left.
337        LEFT = 14;
338        /// Navigate right.
339        RIGHT = 15;
340        /// Open the main menu.
341        MENU = 16;
342        /// Open the content menu.
343        CONTENT_MENU = 17;
344        /// Next channel.
345        CHANNEL_UP = 18;
346        /// Previous channel.
347        CHANNEL_DOWN = 19;
348        /// Start playback.
349        PLAY = 20;
350        /// Pause playback.
351        PAUSE = 21;
352        /// Stop playback.
353        STOP = 22;
354        /// Start recording.
355        RECORD = 23;
356        /// Fast forward.
357        FAST_FORWARD = 24;
358        /// Rewind.
359        REWIND = 25;
360        /// Red colour key.
361        RED = 26;
362        /// Green colour key.
363        GREEN = 27;
364        /// Yellow colour key.
365        YELLOW = 28;
366        /// Blue colour key.
367        BLUE = 29;
368        /// Open help.
369        HELP = 30;
370        /// Show information.
371        INFORMATION = 31;
372        /// Open teletext.
373        TEXT = 32;
374        /// Open the programme guide.
375        GUIDE = 33;
376        /// Open video on demand.
377        VIDEO_ON_DEMAND = 34;
378        /// Return to the previous channel.
379        PREVIOUS_CHANNEL = 80;
380        /// Toggle 3D mode.
381        MODE_3D = 81;
382        /// Toggle subtitles.
383        SUBTITLE = 82;
384        /// Select an audio track.
385        SOUND_SELECT = 83;
386        /// Select an input.
387        INPUT_SELECT = 84;
388        /// Eject the medium.
389        EJECT = 85;
390        /// Next chapter.
391        NEXT_CHAPTER = 86;
392        /// Previous chapter.
393        PREV_CHAPTER = 87;
394        /// Open interactive services.
395        INTERACTIVE = 128;
396        /// Open search.
397        SEARCH = 129;
398        /// Sky home key.
399        SKY = 130;
400        /// Base of the range carrying a raw CEC user-control code.
401        CUSTOM_CEC = 1280;
402        /// Base of the range carrying a raw Sky key code.
403        CUSTOM_SKY = 2048;
404    }
405}
406
407wire_enum! {
408    /// The remote-control protocol of a connected sink or source.
409    RcType: u8 {
410        /// Infrared.
411        IR = 0;
412        /// HDMI CEC.
413        CEC = 1;
414        /// Sky UK over IP.
415        SKY_UK = 2;
416        /// TiVo.
417        TIVO = 3;
418        /// Kodi.
419        KODI = 4;
420        /// Dish.
421        DISH = 5;
422        /// DirecTV.
423        DIRECTV = 6;
424        /// Another MX Remote device.
425        MX_REMOTE = 7;
426    }
427}
428
429impl fmt::Display for RcType {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        let name = match *self {
432            Self::IR => "IR",
433            Self::CEC => "CEC",
434            Self::SKY_UK => "Sky",
435            Self::TIVO => "TiVo",
436            Self::KODI => "Kodi",
437            Self::DISH => "Dish",
438            Self::DIRECTV => "DirecTV",
439            Self::MX_REMOTE => "MX-Remote",
440            _ => "Unknown",
441        };
442        f.write_str(name)
443    }
444}
445
446wire_enum! {
447    /// An EDID preset selectable on an HDMI input.
448    EdidProfile: u16 {
449        /// 1080p with stereo audio.
450        STEREO_1080P = 0;
451        /// A fixed EDID stored on the device.
452        FIXED = 1;
453        /// 4K.
454        UHD_4K = 2;
455        /// 1080p with 5.1 audio.
456        SURROUND51_1080P = 3;
457        /// 720p.
458        HD_720P = 4;
459        /// 1080p with 7.1 audio.
460        SURROUND71_1080P = 5;
461        /// 4K with 7.1 audio.
462        SURROUND71_4K = 6;
463        /// 4K HDR with stereo audio.
464        HDR_STEREO_4K = 7;
465        /// 4K HDR with 7.1 audio.
466        HDR_SURROUND71_4K = 8;
467        /// 4K HDR, audio to the AVR only.
468        HDR_AVR_ONLY_4K = 9;
469        /// The lowest common denominator of the connected sinks.
470        LOWEST_COMMON = 10;
471        /// The lowest common denominator of every sink, connected or not.
472        LOWEST_COMMON_ALL = 11;
473        /// 4K HDR with Dolby Atmos.
474        HDR_ATMOS_4K = 12;
475        /// Copy the EDID of sink 1; the range runs to [`EdidProfile::SINK_32`].
476        SINK_1 = 101;
477        /// Copy the EDID of sink 32; the range starts at [`EdidProfile::SINK_1`].
478        SINK_32 = 132;
479        /// Base of the range carrying a user-supplied EDID.
480        CUSTOM_0 = 500;
481        /// The device reports no profile.
482        UNKNOWN = 0xFFF;
483    }
484}
485
486impl fmt::Display for EdidProfile {
487    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488        let name = match *self {
489            Self::STEREO_1080P => "1080p stereo",
490            Self::FIXED => "fixed",
491            Self::UHD_4K => "4K",
492            Self::SURROUND51_1080P => "1080p 5.1",
493            Self::HD_720P => "720p",
494            Self::SURROUND71_1080P => "1080p 7.1",
495            Self::SURROUND71_4K => "4K 7.1",
496            Self::HDR_STEREO_4K => "4K HDR Stereo",
497            Self::HDR_SURROUND71_4K => "4K HDR 7.1",
498            Self::HDR_AVR_ONLY_4K => "4K HDR AVR",
499            Self::LOWEST_COMMON => "lowest common denominator",
500            Self::LOWEST_COMMON_ALL => "lowest common denominator (all sinks)",
501            Self::HDR_ATMOS_4K => "4K HDR Dolby Atmos",
502            _ => {
503                if self.0 >= Self::SINK_1.0 && self.0 <= Self::SINK_32.0 {
504                    return write!(f, "copy from sink #{}", self.0 - Self::SINK_1.0 + 1);
505                }
506                return write!(f, "custom #{}", self.0);
507            }
508        };
509        f.write_str(name)
510    }
511}
512
513wire_enum! {
514    /// A firmware component.
515    FirmwareType: u8 {
516        /// The component is not known.
517        UNKNOWN = 0;
518        /// The FPGA bitstream.
519        FPGA = 1;
520        /// The Linux system image.
521        LINUX = 2;
522        /// A loadable overlay.
523        LOADING_OVERLAY = 3;
524    }
525}
526
527impl fmt::Display for FirmwareType {
528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529        let name = match *self {
530            Self::FPGA => "FPGA",
531            Self::LINUX => "Linux",
532            Self::LOADING_OVERLAY => "Loading Overlay",
533            _ => "Unknown",
534        };
535        f.write_str(name)
536    }
537}
538
539wire_enum! {
540    /// The negotiated speed of a network port.
541    UtpLinkSpeed: u8 {
542        /// The device reports no speed.
543        UNKNOWN = 0;
544        /// 10Mbit/s.
545        SPEED_10M = 1;
546        /// 100Mbit/s.
547        SPEED_100M = 2;
548        /// 200Mbit/s.
549        SPEED_200M = 3;
550        /// 1Gbit/s.
551        SPEED_1G = 4;
552    }
553}
554
555impl fmt::Display for UtpLinkSpeed {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        let name = match *self {
558            Self::SPEED_10M => "10Mbit/s",
559            Self::SPEED_100M => "100Mbit/s",
560            Self::SPEED_200M => "200Mbit/s",
561            Self::SPEED_1G => "1Gbit/s",
562            _ => "Unknown",
563        };
564        f.write_str(name)
565    }
566}
567
568wire_enum! {
569    /// The window layout of a multiviewer.
570    MultiviewerViewMode: u8 {
571        /// The device reports no layout.
572        UNKNOWN = 0;
573        /// One full-screen window.
574        SINGLE = 1;
575        /// Picture in picture.
576        PIP = 2;
577        /// Two windows, large.
578        TWO_SCREEN_LARGE = 3;
579        /// Two windows, small.
580        TWO_SCREEN_SMALL = 4;
581        /// Three windows, large.
582        THREE_SCREEN_LARGE = 5;
583        /// Three windows, small.
584        THREE_SCREEN_SMALL = 6;
585        /// Four windows, large.
586        FOUR_SCREEN_LARGE = 7;
587        /// Four windows, small.
588        FOUR_SCREEN_SMALL = 8;
589    }
590}
591
592wire_enum! {
593    /// The corner a multiviewer places its picture-in-picture window in.
594    MultiviewerPipPosition: u8 {
595        /// The device reports no position.
596        UNKNOWN = 0;
597        /// Top left.
598        LEFT_TOP = 1;
599        /// Bottom left.
600        LEFT_BOTTOM = 2;
601        /// Top right.
602        RIGHT_TOP = 3;
603        /// Bottom right.
604        RIGHT_BOTTOM = 4;
605    }
606}
607
608wire_enum! {
609    /// The size of a multiviewer's picture-in-picture window.
610    MultiviewerPipSize: u8 {
611        /// The device reports no size.
612        UNKNOWN = 0;
613        /// Small.
614        SMALL = 1;
615        /// Medium.
616        MEDIUM = 2;
617        /// Large.
618        LARGE = 3;
619    }
620}
621
622wire_enum! {
623    /// The resolution and refresh rate a multiviewer drives its output at.
624    MultiviewerOutputMode: u8 {
625        /// The device reports no output mode.
626        UNKNOWN = 0;
627        /// 4096x2160p60.
628        DCI4K_P60 = 1;
629        /// 4096x2160p50.
630        DCI4K_P50 = 2;
631        /// 3840x2160p60.
632        UHD_P60 = 3;
633        /// 3840x2160p50.
634        UHD_P50 = 4;
635        /// 3840x2160p30.
636        UHD_P30 = 5;
637        /// 3840x2160p25.
638        UHD_P25 = 6;
639        /// 1920x1200p60, reduced blanking.
640        WUXGA_P60_RB = 7;
641        /// 1920x1080p60.
642        HD1080_P60 = 8;
643        /// 1920x1080p50.
644        HD1080_P50 = 9;
645        /// 1360x768p60.
646        WXGA_P60 = 10;
647        /// 1280x800p60.
648        WXGA800_P60 = 11;
649        /// 1280x720p60.
650        HD720_P60 = 12;
651        /// 1280x720p50.
652        HD720_P50 = 13;
653        /// 1024x768p60.
654        XGA_P60 = 14;
655    }
656}
657
658wire_enum! {
659    /// The HDCP version a multiviewer output negotiates.
660    MultiviewerHdcpMode: u8 {
661        /// The device reports no HDCP mode.
662        UNKNOWN = 0;
663        /// HDCP 1.4.
664        V14 = 1;
665        /// HDCP 2.2.
666        V22 = 2;
667    }
668}
669
670wire_enum! {
671    /// The IT-content flag a multiviewer sets on its output.
672    MultiviewerItcMode: u8 {
673        /// The device reports no IT-content mode.
674        UNKNOWN = 0;
675        /// Video content.
676        VIDEO = 1;
677        /// PC content.
678        PC = 2;
679    }
680}
681
682wire_enum! {
683    /// The EDID template a multiviewer presents to its sources.
684    MultiviewerEdidTemplate: u8 {
685        /// The device reports no template.
686        UNKNOWN = 0;
687    }
688}
689
690wire_enum! {
691    /// The aspect ratio a multiviewer scales its windows to.
692    MultiviewerAspectRatio: u8 {
693        /// The device reports no aspect ratio.
694        UNKNOWN = 0;
695        /// Fill the window.
696        FULL = 1;
697        /// 16:9.
698        RATIO_16_9 = 2;
699    }
700}
701
702wire_enum! {
703    /// A multiviewer setting that is on, off, or not reported.
704    MultiviewerBool: u8 {
705        /// Off.
706        OFF = 0;
707        /// On.
708        ON = 1;
709        /// The device reports no value.
710        UNKNOWN = 0xFF;
711    }
712}
713
714wire_enum! {
715    /// One of a multiviewer's four inputs.
716    ///
717    /// The wire numbers the inputs from zero and this type from one, so that
718    /// zero can mean "not reported" the way it does for every other
719    /// multiviewer setting.
720    MultiviewerSource: u8 {
721        /// The device reports no source.
722        UNKNOWN = 0;
723        /// Input 1.
724        INPUT_1 = 1;
725        /// Input 2.
726        INPUT_2 = 2;
727        /// Input 3.
728        INPUT_3 = 3;
729        /// Input 4.
730        INPUT_4 = 4;
731    }
732}
733
734impl MultiviewerSource {
735    /// Reads a zero-based wire value, mapping anything past input 4 to
736    /// [`MultiviewerSource::UNKNOWN`].
737    pub(crate) const fn from_zero_based(value: u8) -> Self {
738        if value > 3 {
739            Self::UNKNOWN
740        } else {
741            Self(value + 1)
742        }
743    }
744}
745
746impl MultiviewerBool {
747    /// Reads a wire value, mapping anything but 0 and 1 to
748    /// [`MultiviewerBool::UNKNOWN`].
749    pub(crate) const fn from_wire_tristate(value: u8) -> Self {
750        if value > 1 {
751            Self::UNKNOWN
752        } else {
753            Self(value)
754        }
755    }
756}
757
758wire_enum! {
759    /// The 2-byte `mxr_signal_type` carried in scaling configs and bay signal
760    /// reports.
761    ///
762    /// Byte 0 is the CTA-861 short video descriptor, 0 when the signal is not
763    /// HDMI. Byte 1 packs `color:4` in the low nibble, then `non_int:1` and
764    /// `bpp:3` above it.
765    MxrSignalType: u16 {
766        /// No signal format was reported.
767        NONE = 0;
768    }
769}
770
771/// The bpp index a sender writes when it has no bit depth to report.
772const SIG_BPP_UNSET: u16 = 5;
773
774impl MxrSignalType {
775    /// The CTA-861 short video descriptor, 0 when the signal is not HDMI.
776    pub const fn svd(self) -> u8 {
777        (self.0 & 0xFF) as u8
778    }
779
780    /// The colour space.
781    pub const fn colour_space(self) -> u8 {
782        ((self.0 >> 8) & 0xF) as u8
783    }
784
785    /// Whether the frame rate carries a 1000/1001 clock.
786    pub const fn is_non_integer(self) -> bool {
787        self.0 & (1 << 12) != 0
788    }
789
790    /// The raw bpp index as carried on the wire. The field is an index, not a
791    /// bit depth; [`MxrSignalType::bpp`] converts it.
792    pub const fn bpp_index(self) -> u8 {
793        ((self.0 >> 13) & 0x7) as u8
794    }
795
796    /// The bit depth the bpp index stands for, `None` when unknown or unset.
797    pub const fn bpp(self) -> Option<u8> {
798        match self.bpp_index() {
799            1 => Some(8),
800            2 => Some(10),
801            3 => Some(12),
802            4 => Some(16),
803            _ => None,
804        }
805    }
806
807    /// Reports whether the signal type carries anything but the unset sentinel.
808    pub const fn is_set(self) -> bool {
809        self.bpp_index() as u16 != SIG_BPP_UNSET
810    }
811}
812
813impl fmt::Display for MxrSignalType {
814    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815        if !self.is_set() {
816            return f.write_str("unset");
817        }
818        match self.bpp() {
819            Some(bpp) => write!(
820                f,
821                "svd {}, color {}, {}bpp",
822                self.svd(),
823                self.colour_space(),
824                bpp
825            ),
826            None => write!(f, "svd {}, color {}", self.svd(), self.colour_space()),
827        }
828    }
829}