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 with the given named bit constants.
16///
17/// The representation defaults to `u32`; give it explicitly as `Name: u64` for
18/// a mask whose wire field is wider.
19macro_rules! bitmask {
20    (
21        $(#[$meta:meta])*
22        $name:ident { $( $(#[$cmeta:meta])* $cname:ident = $value:expr; )* }
23    ) => {
24        bitmask! {
25            $(#[$meta])*
26            $name: u32 { $( $(#[$cmeta])* $cname = $value; )* }
27        }
28    };
29    (
30        $(#[$meta:meta])*
31        $name:ident: $repr:ty { $( $(#[$cmeta:meta])* $cname:ident = $value:expr; )* }
32    ) => {
33        $(#[$meta])*
34        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
35        pub struct $name($repr);
36
37        impl $name {
38            /// No bits set.
39            pub const NONE: Self = Self(0);
40
41            $( $(#[$cmeta])* pub const $cname: Self = Self($value); )*
42
43            /// Wraps a raw wire value, including bits this library has no name for.
44            pub const fn from_bits(bits: $repr) -> Self {
45                Self(bits)
46            }
47
48            /// Returns the raw wire value.
49            pub const fn bits(self) -> $repr {
50                self.0
51            }
52
53            /// Reports whether every bit in `other` is set.
54            pub const fn has(self, other: Self) -> bool {
55                self.0 & other.0 == other.0
56            }
57
58            /// Reports whether no bit is set.
59            pub const fn is_empty(self) -> bool {
60                self.0 == 0
61            }
62        }
63
64        impl BitOr for $name {
65            type Output = Self;
66            fn bitor(self, rhs: Self) -> Self {
67                Self(self.0 | rhs.0)
68            }
69        }
70
71        impl BitOrAssign for $name {
72            fn bitor_assign(&mut self, rhs: Self) {
73                self.0 |= rhs.0;
74            }
75        }
76
77        impl BitAnd for $name {
78            type Output = Self;
79            fn bitand(self, rhs: Self) -> Self {
80                Self(self.0 & rhs.0)
81            }
82        }
83
84        impl fmt::Display for $name {
85            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86                // Two hex digits per byte of the wire field, plus "0x".
87                write!(
88                    f,
89                    "{:#0width$x}",
90                    self.0,
91                    width = core::mem::size_of::<$repr>() * 2 + 2
92                )
93            }
94        }
95    };
96}
97
98/// Declares an enumeration newtype over `$repr` with the given named constants.
99macro_rules! wire_enum {
100    (
101        $(#[$meta:meta])*
102        $name:ident: $repr:ty { $( $(#[$cmeta:meta])* $cname:ident = $value:expr; )* }
103    ) => {
104        $(#[$meta])*
105        #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
106        pub struct $name($repr);
107
108        impl $name {
109            $( $(#[$cmeta])* pub const $cname: Self = Self($value); )*
110
111            /// Wraps a raw wire value, including one this library has no name for.
112            pub const fn from_wire(value: $repr) -> Self {
113                Self(value)
114            }
115
116            /// Returns the raw wire value.
117            pub const fn to_wire(self) -> $repr {
118                self.0
119            }
120        }
121    };
122}
123
124bitmask! {
125    /// Capabilities a device reports in its hello frame.
126    DeviceFeature {
127        /// Receives infrared.
128        IR_RX = 1 << 0;
129        /// Transmits infrared.
130        IR_TX = 1 << 1;
131        /// Speaks CEC.
132        CEC = 1 << 2;
133        /// Acts as a V2IP stream source.
134        V2IP_SOURCE = 1 << 3;
135        /// Acts as a V2IP stream sink.
136        V2IP_SINK = 1 << 4;
137        /// Routes video.
138        VIDEO_ROUTING = 1 << 5;
139        /// Routes audio.
140        AUDIO_ROUTING = 1 << 6;
141        /// Controls volume.
142        VOLUME_CONTROL = 1 << 7;
143        /// Supports audio return.
144        AUDIO_RETURN = 1 << 8;
145        /// Passes remote-control commands through.
146        REMOTE_CONTROL = 1 << 9;
147        /// Installer setup has been completed.
148        SETUP_COMPLETED = 1 << 10;
149        /// Is the master of its mesh.
150        MESH_MASTER = 1 << 11;
151        /// Has a notification pending.
152        STATUS_NOTIFY = 1 << 12;
153        /// Has a warning pending.
154        STATUS_WARNING = 1 << 13;
155        /// Has an error pending.
156        STATUS_ERROR = 1 << 14;
157        /// Is about to reboot.
158        STATUS_REBOOT = 1 << 15;
159        /// Is a member of a mesh.
160        MESH_MEMBER = 1 << 16;
161        /// Is an audio amplifier.
162        AUDIO_AMPLIFIER = 1 << 17;
163        /// Is still booting.
164        BOOTING = 1 << 18;
165        /// Is a management client rather than a device.
166        MANAGER = 1 << 19;
167        /// Is in power-save mode.
168        STATUS_POWER_SAVE = 1 << 20;
169        /// Supports meshing.
170        MESH = 1 << 21;
171        /// Is a multiviewer.
172        MULTIVIEWER = 1 << 22;
173        /// Has crashed since it last booted.
174        STATUS_CRASHED = 1 << 23;
175        /// Supports video walls.
176        VIDEO_WALL = 1 << 24;
177        /// Initialises the configuration it broadcasts.
178        ///
179        /// Firmware without this bit sends a device configuration built over
180        /// uninitialised memory, so fields it did not mean to write carry junk.
181        CONFIG_INITIALISED = 1 << 25;
182        /// Set while the device is in its boot loader.
183        BOOT_BIT = 1 << 31;
184    }
185}
186
187bitmask! {
188    /// What a V2IP device's video processor supports, as the device reports it
189    /// in its configuration.
190    ///
191    /// Read-only, and a device's own: it fills the field in only on the frame
192    /// describing itself, and leaves it zero on one it sends to configure
193    /// another device. There is no write path.
194    ///
195    /// Bits are assigned by the video processor and only ever appended, so a
196    /// bit this library has no name for is a later capability rather than an
197    /// error. A device reports no features at all until its processor answers,
198    /// and an older processor answers with none of the optional commands, so an
199    /// empty mask is never reported as a capability set - see
200    /// [`crate::Remote::v2ip_features`], which reports it as unknown instead.
201    V2ipFpgaFeature: u64 {
202        /// Applies a DSCP marking to the streams it sources.
203        SOURCE_DSCP = 1 << 0;
204        /// Reports the audio format arriving at its sink.
205        SINK_AUDIO_FORMAT = 1 << 1;
206        /// Places a tiling window on its sink.
207        SINK_TILING_WINDOW = 1 << 2;
208        /// Reports the state of its sink's overlay.
209        SINK_OVERLAY_STATE = 1 << 3;
210        /// Reports its sink's state.
211        SINK_STATE = 1 << 4;
212        /// Reports information about the stream its sink receives.
213        SINK_STREAM_INFO = 1 << 5;
214    }
215}
216
217bitmask! {
218    /// The device settings a V2IP configuration can carry, each on its own bit.
219    ///
220    /// The same bits serve as the settings a frame carries and as the values
221    /// of the on/off ones among them. The last three carry a value elsewhere in
222    /// the block and have no on/off value of their own.
223    ///
224    /// A device reports every setting it has, so one it never reports is one
225    /// it does not have.
226    V2ipDeviceSetting {
227        /// Disables the decoder while the display is off.
228        SINK_CHECK_POWER = 1 << 0;
229        /// Disables the HDMI output while there is no signal.
230        SINK_OFF_NO_SIGNAL = 1 << 1;
231        /// Sends infrared modulated.
232        IR_TX_MODULATED = 1 << 2;
233        /// Lights the status LED.
234        STATUS_LED = 1 << 3;
235        /// Lights the network port LEDs.
236        NETWORK_LED = 1 << 4;
237        /// Runs the fan in quiet mode.
238        FAN_QUIET = 1 << 5;
239        /// Accepts CEC combo keys.
240        CEC_COMBO_KEYS = 1 << 6;
241        /// Accepts CEC combo keys for the device's own input.
242        CEC_COMBO_INPUT = 1 << 7;
243        /// The infrared profile of the device's global infrared port.
244        IR_PROFILE = 1 << 8;
245        /// The infrared profile of the output's infrared port.
246        IR_PROFILE_SINK = 1 << 9;
247        /// The infrared profiles stored on the device. Reported by the device
248        /// itself and never written.
249        IR_PROFILES = 1 << 10;
250    }
251}
252
253impl V2ipDeviceSetting {
254    /// The settings that are on or off, as opposed to carrying a value.
255    pub const SWITCHES: Self = Self(0xFF);
256
257    /// These bits with every bit of `other` cleared.
258    pub(crate) const fn without(self, other: Self) -> Self {
259        Self(self.0 & !other.0)
260    }
261}
262
263bitmask! {
264    /// Capabilities of a single bay.
265    BayFeatures {
266        /// HDMI output.
267        HDMI_OUT = 1 << 0;
268        /// HDMI input.
269        HDMI_IN = 1 << 1;
270        /// Digital audio output.
271        AUDIO_DIG_OUT = 1 << 2;
272        /// Digital audio input.
273        AUDIO_DIG_IN = 1 << 3;
274        /// Analogue audio output.
275        AUDIO_ANA_OUT = 1 << 4;
276        /// Analogue audio input.
277        AUDIO_ANA_IN = 1 << 5;
278        /// Infrared input.
279        IR_IN = 1 << 6;
280        /// Infrared output.
281        IR_OUT = 1 << 7;
282        /// Amplified audio output.
283        AUDIO_AMP_OUT = 1 << 8;
284        /// Remote-control output.
285        RC_OUT = 1 << 9;
286        /// Remote-control input.
287        RC_IN = 1 << 10;
288        /// Dolby decoding.
289        DOLBY = 1 << 11;
290        /// Switches itself off when idle.
291        AUTO_OFF = 1 << 12;
292        /// Is a remote V2IP source.
293        V2IP_SOURCE_REMOTE = 1 << 13;
294        /// Is a remote V2IP sink.
295        V2IP_SINK_REMOTE = 1 << 14;
296        /// Is a local V2IP source.
297        V2IP_SOURCE_LOCAL = 1 << 15;
298        /// Is a local V2IP sink.
299        V2IP_SINK_LOCAL = 1 << 16;
300    }
301}
302
303bitmask! {
304    /// Live status flags of a single bay.
305    ///
306    /// Bits 16-19 and 22-23 are bit-fields rather than flags; read them with
307    /// [`BayStatus::rc_type`] and [`BayStatus::hdcp`].
308    BayStatus {
309        /// The bay reports a fault.
310        FAULT = 1 << 0;
311        /// The bay is hidden from the user interface.
312        HIDDEN = 1 << 1;
313        /// The bay has power.
314        POWERED = 1 << 2;
315        /// A signal is present.
316        SIGNAL_DETECTED = 1 << 3;
317        /// Hot-plug detect is asserted.
318        HPD_DETECTED = 1 << 4;
319        /// The signal is scrambled.
320        SIGNAL_SCRAMBLE = 1 << 5;
321        /// An HDBaseT link is up.
322        HDBT_CONNECTED = 1 << 6;
323        /// A CEC device answered.
324        CEC_DETECTED = 1 << 7;
325        /// The attached device was powered on.
326        POWERED_ON = 1 << 8;
327        /// The attached device was powered off.
328        POWERED_OFF = 1 << 9;
329        /// Audio return over HDMI is active.
330        AUDIO_ARC_HDMI = 1 << 10;
331        /// Audio return over optical is active.
332        AUDIO_ARC_OPTIC = 1 << 11;
333        /// Audio return over analogue is active.
334        AUDIO_ARC_ANALOG = 1 << 12;
335        /// The bay is offline.
336        OFFLINE = 1 << 13;
337        /// The V2IP decoder is disabled.
338        DECODER_DISABLE = 1 << 14;
339        /// The V2IP encoder is disabled.
340        ENCODER_DISABLE = 1 << 15;
341        /// CEC is switched off for this bay.
342        CEC_DISABLED = 1 << 20;
343        /// The V2IP encoder reports an error.
344        ENCODER_ERROR = 1 << 21;
345    }
346}
347
348impl BayStatus {
349    const RC_TYPE_SHIFT: u32 = 16;
350    const RC_TYPE_MASK: u32 = 0xF << Self::RC_TYPE_SHIFT;
351    const HDCP_SHIFT: u32 = 22;
352    const HDCP_MASK: u32 = 0x3 << Self::HDCP_SHIFT;
353
354    /// Extracts the remote-control type carried in bits 16-19.
355    pub const fn rc_type(self) -> RcType {
356        RcType(((self.0 & Self::RC_TYPE_MASK) >> Self::RC_TYPE_SHIFT) as u8)
357    }
358
359    /// Extracts the HDCP version carried in bits 22-23.
360    pub const fn hdcp(self) -> u8 {
361        ((self.0 & Self::HDCP_MASK) >> Self::HDCP_SHIFT) as u8
362    }
363}
364
365bitmask! {
366    /// Media carried by a virtual link.
367    LinkFeature {
368        /// Video over HDMI.
369        VIDEO_HDMI = 1 << 0;
370        /// Audio over optical.
371        AUDIO_OPTICAL = 1 << 1;
372        /// Audio over analogue.
373        AUDIO_ANALOG = 1 << 2;
374        /// Infrared.
375        IR = 1 << 3;
376        /// Remote control.
377        RC = 1 << 4;
378    }
379}
380
381wire_enum! {
382    /// A remote-control action.
383    RcAction: u16 {
384        /// Toggle power.
385        POWER_TOGGLE = 0;
386        /// Power on.
387        POWER_ON = 1;
388        /// Power off.
389        POWER_OFF = 2;
390        /// Volume down.
391        VOLUME_DOWN = 3;
392        /// Volume up.
393        VOLUME_UP = 4;
394        /// Toggle mute.
395        VOLUME_MUTE = 5;
396    }
397}
398
399wire_enum! {
400    /// A remote-control key code (CEC or IR).
401    RcKey: u16 {
402        /// Digit 0.
403        NUM0 = 0;
404        /// Digit 1.
405        NUM1 = 1;
406        /// Digit 2.
407        NUM2 = 2;
408        /// Digit 3.
409        NUM3 = 3;
410        /// Digit 4.
411        NUM4 = 4;
412        /// Digit 5.
413        NUM5 = 5;
414        /// Digit 6.
415        NUM6 = 6;
416        /// Digit 7.
417        NUM7 = 7;
418        /// Digit 8.
419        NUM8 = 8;
420        /// Digit 9.
421        NUM9 = 9;
422        /// Confirm the highlighted item.
423        SELECT = 10;
424        /// Go back one step.
425        BACK = 11;
426        /// Navigate up.
427        UP = 12;
428        /// Navigate down.
429        DOWN = 13;
430        /// Navigate left.
431        LEFT = 14;
432        /// Navigate right.
433        RIGHT = 15;
434        /// Open the main menu.
435        MENU = 16;
436        /// Open the content menu.
437        CONTENT_MENU = 17;
438        /// Next channel.
439        CHANNEL_UP = 18;
440        /// Previous channel.
441        CHANNEL_DOWN = 19;
442        /// Start playback.
443        PLAY = 20;
444        /// Pause playback.
445        PAUSE = 21;
446        /// Stop playback.
447        STOP = 22;
448        /// Start recording.
449        RECORD = 23;
450        /// Fast forward.
451        FAST_FORWARD = 24;
452        /// Rewind.
453        REWIND = 25;
454        /// Red colour key.
455        RED = 26;
456        /// Green colour key.
457        GREEN = 27;
458        /// Yellow colour key.
459        YELLOW = 28;
460        /// Blue colour key.
461        BLUE = 29;
462        /// Open help.
463        HELP = 30;
464        /// Show information.
465        INFORMATION = 31;
466        /// Open teletext.
467        TEXT = 32;
468        /// Open the programme guide.
469        GUIDE = 33;
470        /// Open video on demand.
471        VIDEO_ON_DEMAND = 34;
472        /// Return to the previous channel.
473        PREVIOUS_CHANNEL = 80;
474        /// Toggle 3D mode.
475        MODE_3D = 81;
476        /// Toggle subtitles.
477        SUBTITLE = 82;
478        /// Select an audio track.
479        SOUND_SELECT = 83;
480        /// Select an input.
481        INPUT_SELECT = 84;
482        /// Eject the medium.
483        EJECT = 85;
484        /// Next chapter.
485        NEXT_CHAPTER = 86;
486        /// Previous chapter.
487        PREV_CHAPTER = 87;
488        /// Open interactive services.
489        INTERACTIVE = 128;
490        /// Open search.
491        SEARCH = 129;
492        /// Sky home key.
493        SKY = 130;
494        /// Base of the range carrying a raw CEC user-control code.
495        CUSTOM_CEC = 1280;
496        /// Base of the range carrying a raw Sky key code.
497        CUSTOM_SKY = 2048;
498    }
499}
500
501wire_enum! {
502    /// The remote-control protocol of a connected sink or source.
503    RcType: u8 {
504        /// Infrared.
505        IR = 0;
506        /// HDMI CEC.
507        CEC = 1;
508        /// Sky UK over IP.
509        SKY_UK = 2;
510        /// TiVo.
511        TIVO = 3;
512        /// Kodi.
513        KODI = 4;
514        /// Dish.
515        DISH = 5;
516        /// DirecTV.
517        DIRECTV = 6;
518        /// Another MX Remote device.
519        MX_REMOTE = 7;
520    }
521}
522
523impl fmt::Display for RcType {
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525        let name = match *self {
526            Self::IR => "IR",
527            Self::CEC => "CEC",
528            Self::SKY_UK => "Sky",
529            Self::TIVO => "TiVo",
530            Self::KODI => "Kodi",
531            Self::DISH => "Dish",
532            Self::DIRECTV => "DirecTV",
533            Self::MX_REMOTE => "MX-Remote",
534            _ => "Unknown",
535        };
536        f.write_str(name)
537    }
538}
539
540wire_enum! {
541    /// An EDID preset selectable on an HDMI input.
542    EdidProfile: u16 {
543        /// 1080p with stereo audio.
544        STEREO_1080P = 0;
545        /// A fixed EDID stored on the device.
546        FIXED = 1;
547        /// 4K.
548        UHD_4K = 2;
549        /// 1080p with 5.1 audio.
550        SURROUND51_1080P = 3;
551        /// 720p.
552        HD_720P = 4;
553        /// 1080p with 7.1 audio.
554        SURROUND71_1080P = 5;
555        /// 4K with 7.1 audio.
556        SURROUND71_4K = 6;
557        /// 4K HDR with stereo audio.
558        HDR_STEREO_4K = 7;
559        /// 4K HDR with 7.1 audio.
560        HDR_SURROUND71_4K = 8;
561        /// 4K HDR, audio to the AVR only.
562        HDR_AVR_ONLY_4K = 9;
563        /// The lowest common denominator of the connected sinks.
564        LOWEST_COMMON = 10;
565        /// The lowest common denominator of every sink, connected or not.
566        LOWEST_COMMON_ALL = 11;
567        /// 4K HDR with Dolby Atmos.
568        HDR_ATMOS_4K = 12;
569        /// Copy the EDID of sink 1; the range runs to [`EdidProfile::SINK_32`].
570        SINK_1 = 101;
571        /// Copy the EDID of sink 32; the range starts at [`EdidProfile::SINK_1`].
572        SINK_32 = 132;
573        /// Base of the range carrying a user-supplied EDID.
574        CUSTOM_0 = 500;
575        /// The device reports no profile.
576        UNKNOWN = 0xFFF;
577    }
578}
579
580impl fmt::Display for EdidProfile {
581    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582        let name = match *self {
583            Self::STEREO_1080P => "1080p stereo",
584            Self::FIXED => "fixed",
585            Self::UHD_4K => "4K",
586            Self::SURROUND51_1080P => "1080p 5.1",
587            Self::HD_720P => "720p",
588            Self::SURROUND71_1080P => "1080p 7.1",
589            Self::SURROUND71_4K => "4K 7.1",
590            Self::HDR_STEREO_4K => "4K HDR Stereo",
591            Self::HDR_SURROUND71_4K => "4K HDR 7.1",
592            Self::HDR_AVR_ONLY_4K => "4K HDR AVR",
593            Self::LOWEST_COMMON => "lowest common denominator",
594            Self::LOWEST_COMMON_ALL => "lowest common denominator (all sinks)",
595            Self::HDR_ATMOS_4K => "4K HDR Dolby Atmos",
596            _ => {
597                if self.0 >= Self::SINK_1.0 && self.0 <= Self::SINK_32.0 {
598                    return write!(f, "copy from sink #{}", self.0 - Self::SINK_1.0 + 1);
599                }
600                return write!(f, "custom #{}", self.0);
601            }
602        };
603        f.write_str(name)
604    }
605}
606
607wire_enum! {
608    /// A firmware component.
609    FirmwareType: u8 {
610        /// The component is not known.
611        UNKNOWN = 0;
612        /// The FPGA bitstream.
613        FPGA = 1;
614        /// The Linux system image.
615        LINUX = 2;
616        /// A loadable overlay.
617        LOADING_OVERLAY = 3;
618    }
619}
620
621impl fmt::Display for FirmwareType {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        let name = match *self {
624            Self::FPGA => "FPGA",
625            Self::LINUX => "Linux",
626            Self::LOADING_OVERLAY => "Loading Overlay",
627            _ => "Unknown",
628        };
629        f.write_str(name)
630    }
631}
632
633wire_enum! {
634    /// The negotiated speed of a network port.
635    UtpLinkSpeed: u8 {
636        /// The device reports no speed.
637        UNKNOWN = 0;
638        /// 10Mbit/s.
639        SPEED_10M = 1;
640        /// 100Mbit/s.
641        SPEED_100M = 2;
642        /// 200Mbit/s.
643        SPEED_200M = 3;
644        /// 1Gbit/s.
645        SPEED_1G = 4;
646    }
647}
648
649impl fmt::Display for UtpLinkSpeed {
650    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651        let name = match *self {
652            Self::SPEED_10M => "10Mbit/s",
653            Self::SPEED_100M => "100Mbit/s",
654            Self::SPEED_200M => "200Mbit/s",
655            Self::SPEED_1G => "1Gbit/s",
656            _ => "Unknown",
657        };
658        f.write_str(name)
659    }
660}
661
662wire_enum! {
663    /// The window layout of a multiviewer.
664    MultiviewerViewMode: u8 {
665        /// The device reports no layout.
666        UNKNOWN = 0;
667        /// One full-screen window.
668        SINGLE = 1;
669        /// Picture in picture.
670        PIP = 2;
671        /// Two windows, large.
672        TWO_SCREEN_LARGE = 3;
673        /// Two windows, small.
674        TWO_SCREEN_SMALL = 4;
675        /// Three windows, large.
676        THREE_SCREEN_LARGE = 5;
677        /// Three windows, small.
678        THREE_SCREEN_SMALL = 6;
679        /// Four windows, equal size.
680        FOUR_SCREEN_EQUAL = 7;
681        /// Four windows, small.
682        FOUR_SCREEN_SMALL = 8;
683    }
684}
685
686wire_enum! {
687    /// The corner a multiviewer places its picture-in-picture window in.
688    MultiviewerPipPosition: u8 {
689        /// The device reports no position.
690        UNKNOWN = 0;
691        /// Top left.
692        LEFT_TOP = 1;
693        /// Bottom left.
694        LEFT_BOTTOM = 2;
695        /// Top right.
696        RIGHT_TOP = 3;
697        /// Bottom right.
698        RIGHT_BOTTOM = 4;
699    }
700}
701
702wire_enum! {
703    /// The size of a multiviewer's picture-in-picture window.
704    MultiviewerPipSize: u8 {
705        /// The device reports no size.
706        UNKNOWN = 0;
707        /// Small.
708        SMALL = 1;
709        /// Medium.
710        MEDIUM = 2;
711        /// Large.
712        LARGE = 3;
713    }
714}
715
716wire_enum! {
717    /// The resolution and refresh rate a multiviewer drives its output at.
718    MultiviewerOutputMode: u8 {
719        /// The device reports no output mode.
720        UNKNOWN = 0;
721        /// 4096x2160p60.
722        DCI4K_P60 = 1;
723        /// 4096x2160p50.
724        DCI4K_P50 = 2;
725        /// 3840x2160p60.
726        UHD_P60 = 3;
727        /// 3840x2160p50.
728        UHD_P50 = 4;
729        /// 3840x2160p30.
730        UHD_P30 = 5;
731        /// 3840x2160p25.
732        UHD_P25 = 6;
733        /// 1920x1200p60, reduced blanking.
734        WUXGA_P60_RB = 7;
735        /// 1920x1080p60.
736        HD1080_P60 = 8;
737        /// 1920x1080p50.
738        HD1080_P50 = 9;
739        /// 1360x768p60.
740        WXGA_P60 = 10;
741        /// 1280x800p60.
742        WXGA800_P60 = 11;
743        /// 1280x720p60.
744        HD720_P60 = 12;
745        /// 1280x720p50.
746        HD720_P50 = 13;
747        /// 1024x768p60.
748        XGA_P60 = 14;
749    }
750}
751
752wire_enum! {
753    /// The HDCP version a multiviewer output negotiates.
754    MultiviewerHdcpMode: u8 {
755        /// The device reports no HDCP mode.
756        UNKNOWN = 0;
757        /// HDCP 1.4.
758        V14 = 1;
759        /// HDCP 2.2.
760        V22 = 2;
761        /// Content protection off.
762        OFF = 3;
763    }
764}
765
766wire_enum! {
767    /// The IT-content flag a multiviewer sets on its output.
768    MultiviewerItcMode: u8 {
769        /// The device reports no IT-content mode.
770        UNKNOWN = 0;
771        /// Video content.
772        VIDEO = 1;
773        /// PC content.
774        PC = 2;
775    }
776}
777
778wire_enum! {
779    /// The EDID template a multiviewer presents to its sources.
780    ///
781    /// A template's name is the largest resolution it advertises and the audio
782    /// format it declares support for.
783    MultiviewerEdidTemplate: u8 {
784        /// The device reports no template.
785        EDID_UNKNOWN = 0;
786        /// 4K2K60 4:4:4, stereo 2.0.
787        EDID_4K2K60_444_STEREO = 1;
788        /// 4K2K60 4:4:4, Dolby/DTS 5.1.
789        EDID_4K2K60_444_DOLBY_DTS_51 = 2;
790        /// 4K2K60 4:4:4, HD audio 7.1.
791        EDID_4K2K60_444_HD_AUDIO_71 = 3;
792        /// 4K2K30 4:4:4, stereo 2.0.
793        EDID_4K2K30_444_STEREO = 4;
794        /// 4K2K30 4:4:4, Dolby/DTS 5.1.
795        EDID_4K2K30_444_DOLBY_DTS_51 = 5;
796        /// 4K2K30 4:4:4, HD audio 7.1.
797        EDID_4K2K30_444_HD_AUDIO_71 = 6;
798        /// 1080p, stereo 2.0.
799        EDID_1080P_STEREO = 7;
800        /// 1080p, Dolby/DTS 5.1.
801        EDID_1080P_DOLBY_DTS_51 = 8;
802        /// 1080p, HD audio 7.1.
803        EDID_1080P_HD_AUDIO_71 = 9;
804        /// 1920x1200, stereo 2.0.
805        EDID_1920X1200_STEREO = 10;
806        /// 1680x1050, stereo 2.0.
807        EDID_1680X1050_STEREO = 11;
808        /// 1600x1200, stereo 2.0.
809        EDID_1600X1200_STEREO = 12;
810        /// 1440x900, stereo 2.0.
811        EDID_1440X900_STEREO = 13;
812        /// 1360x768, stereo 2.0.
813        EDID_1360X768_STEREO = 14;
814        /// 1280x1024, stereo 2.0.
815        EDID_1280X1024_STEREO = 15;
816        /// 1024x768, stereo 2.0.
817        EDID_1024X768_STEREO = 16;
818        /// 720p, stereo 2.0.
819        EDID_720P_STEREO = 17;
820        /// Whatever the display connected to the HDMI output presents. The
821        /// template a multiviewer leaves the factory with.
822        EDID_COPY_OUTPUT = 18;
823        /// The EDID loaded onto the device.
824        EDID_CUSTOM = 19;
825    }
826}
827
828wire_enum! {
829    /// The aspect ratio a multiviewer scales its windows to.
830    MultiviewerAspectRatio: u8 {
831        /// The device reports no aspect ratio.
832        UNKNOWN = 0;
833        /// Fill the window.
834        FULL = 1;
835        /// 16:9.
836        RATIO_16_9 = 2;
837    }
838}
839
840wire_enum! {
841    /// A multiviewer setting that is on, off, or not reported.
842    MultiviewerBool: u8 {
843        /// Off.
844        OFF = 0;
845        /// On.
846        ON = 1;
847        /// The device reports no value.
848        UNKNOWN = 0xFF;
849    }
850}
851
852wire_enum! {
853    /// One of a multiviewer's four inputs.
854    ///
855    /// The wire numbers the inputs from zero and this type from one, so that
856    /// zero can mean "not reported" the way it does for every other
857    /// multiviewer setting. So `to_wire` and `from_wire` carry this type's
858    /// numbering rather than the wire's, and neither is the conversion to
859    /// reach for when a raw multiviewer byte is what is in hand.
860    MultiviewerSource: u8 {
861        /// The device reports no source.
862        UNKNOWN = 0;
863        /// Input 1.
864        INPUT_1 = 1;
865        /// Input 2.
866        INPUT_2 = 2;
867        /// Input 3.
868        INPUT_3 = 3;
869        /// Input 4.
870        INPUT_4 = 4;
871    }
872}
873
874impl MultiviewerSource {
875    /// Reads a zero-based wire value, mapping anything past input 4 to
876    /// [`MultiviewerSource::UNKNOWN`].
877    ///
878    /// The firmware spells "not known" as 0xFF, which lands past input 4 and
879    /// so needs no case of its own.
880    pub(crate) const fn from_zero_based(value: u8) -> Self {
881        if value > 3 {
882            Self::UNKNOWN
883        } else {
884            Self(value + 1)
885        }
886    }
887
888    /// The zero-based value the wire carries, or `None` for a source naming no
889    /// input.
890    ///
891    /// A multiviewer reads zero as its first input, so there is no value that
892    /// says "leave this alone": a request that cannot name an input has to be
893    /// refused rather than sent.
894    pub(crate) const fn to_zero_based(self) -> Option<u8> {
895        match self.0 {
896            1..=4 => Some(self.0 - 1),
897            _ => None,
898        }
899    }
900}
901
902impl MultiviewerBool {
903    /// Reads a wire value, mapping anything but 0 and 1 to
904    /// [`MultiviewerBool::UNKNOWN`].
905    pub(crate) const fn from_wire_tristate(value: u8) -> Self {
906        if value > 1 {
907            Self::UNKNOWN
908        } else {
909            Self(value)
910        }
911    }
912}
913
914wire_enum! {
915    /// The colour space a V2IP output scales to.
916    ///
917    /// The field is four bits wide and only these four values are defined. A
918    /// receiver passes the whole nibble to its validator, so a fifth value is
919    /// dropped without a word rather than clamped to one of these.
920    V2ipColourSpace: u8 {
921        /// RGB.
922        RGB = 0;
923        /// YCbCr 4:4:4.
924        YCBCR444 = 1;
925        /// YCbCr 4:2:2.
926        YCBCR422 = 2;
927        /// YCbCr 4:2:0.
928        YCBCR420 = 3;
929    }
930}
931
932wire_enum! {
933    /// The 2-byte `mxr_signal_type` carried in scaling configs and bay signal
934    /// reports.
935    ///
936    /// Byte 0 is the CTA-861 short video descriptor, 0 when the signal is not
937    /// HDMI. Byte 1 packs `color:4` in the low nibble, then `non_int:1` and
938    /// `bpp:3` above it.
939    MxrSignalType: u16 {
940        /// No signal format was reported.
941        NONE = 0;
942    }
943}
944
945/// The bpp index a sender writes when it has no bit depth to report.
946///
947/// It sits outside the four indices that name a real depth, so an unset
948/// format reads differently from every genuine one.
949const SIG_BPP_UNSET: u16 = 5;
950
951impl MxrSignalType {
952    /// The CTA-861 short video descriptor, 0 when the signal is not HDMI.
953    pub const fn svd(self) -> u8 {
954        (self.0 & 0xFF) as u8
955    }
956
957    /// The colour space.
958    pub const fn colour_space(self) -> u8 {
959        ((self.0 >> 8) & 0xF) as u8
960    }
961
962    /// Whether the frame rate carries a 1000/1001 clock.
963    pub const fn is_non_integer(self) -> bool {
964        self.0 & (1 << 12) != 0
965    }
966
967    /// The raw bpp index as carried on the wire. The field is an index, not a
968    /// bit depth; [`MxrSignalType::bpp`] converts it.
969    pub const fn bpp_index(self) -> u8 {
970        ((self.0 >> 13) & 0x7) as u8
971    }
972
973    /// The bit depth the bpp index stands for, `None` when unknown or unset.
974    pub const fn bpp(self) -> Option<u8> {
975        match self.bpp_index() {
976            1 => Some(8),
977            2 => Some(10),
978            3 => Some(12),
979            4 => Some(16),
980            _ => None,
981        }
982    }
983
984    /// Reports whether the word carries a signal format at all.
985    ///
986    /// A bay with nothing configured says so two ways. A sender that zeroes
987    /// the word and stamps the unset bpp index leaves an index no real depth
988    /// uses, and one that writes a plain zero leaves nothing at all. Neither
989    /// is a format, and the svd and colour space beside them are not answers
990    /// either: both read as zero, which is what this word says for "not HDMI"
991    /// and "RGB" when it *is* set.
992    pub const fn is_set(self) -> bool {
993        self.0 != 0 && self.bpp_index() as u16 != SIG_BPP_UNSET
994    }
995
996    /// Builds the word from the fields a scaling write consumes.
997    ///
998    /// `non_int` is left clear: the receiving struct carries the bit, and the
999    /// apply path does not read it.
1000    ///
1001    /// Building rather than editing is the point. A sink with no mode
1002    /// configured reports the word with the unset bpp index in it, so a caller
1003    /// that read that word back and filled in an svd would send an index no
1004    /// depth uses - which the receiver decodes to zero and rejects without
1005    /// answering.
1006    pub(crate) const fn from_parts(svd: u8, colour: u8, bpp_index: u8) -> Self {
1007        Self((svd as u16) | (((colour & 0xF) as u16) << 8) | (((bpp_index & 0x7) as u16) << 13))
1008    }
1009
1010    /// The bpp index that stands for a bit depth, `None` for a depth no index
1011    /// names.
1012    ///
1013    /// Only the three depths a V2IP output stage accepts are here. Index 4
1014    /// names 16bpp, which [`MxrSignalType::bpp`] reads back from a device, but
1015    /// the output stage refuses it - so offering it as something to write would
1016    /// send a frame that is decoded cleanly and then dropped in silence.
1017    pub(crate) const fn bpp_index_for_depth(depth: u8) -> Option<u8> {
1018        match depth {
1019            8 => Some(1),
1020            10 => Some(2),
1021            12 => Some(3),
1022            _ => None,
1023        }
1024    }
1025}
1026
1027impl fmt::Display for MxrSignalType {
1028    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1029        if !self.is_set() {
1030            return f.write_str("unset");
1031        }
1032        match self.bpp() {
1033            Some(bpp) => write!(
1034                f,
1035                "svd {}, color {}, {}bpp",
1036                self.svd(),
1037                self.colour_space(),
1038                bpp
1039            ),
1040            None => write!(f, "svd {}, color {}", self.svd(), self.colour_space()),
1041        }
1042    }
1043}