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        /// Receives one or more peers only by broadcast, because its multicast
183        /// receive path is faulty.
184        STATUS_MCAST_FAULT = 1 << 26;
185        /// Powers its video processor down in power save. A device without it
186        /// refuses to enter power save.
187        POWER_SAVE = 1 << 27;
188        /// Tags its uplink by the VLAN configuration it reports, and takes one
189        /// written to it.
190        VLAN = 1 << 28;
191        /// Set while the device is in its boot loader.
192        BOOT_BIT = 1 << 31;
193    }
194}
195
196bitmask! {
197    /// What a V2IP device's video processor supports, as the device reports it
198    /// in its configuration.
199    ///
200    /// Read-only, and a device's own: it fills the field in only on the frame
201    /// describing itself, and leaves it zero on one it sends to configure
202    /// another device. There is no write path.
203    ///
204    /// Bits are assigned by the video processor and only ever appended, so a
205    /// bit this library has no name for is a later capability rather than an
206    /// error. A device reports no features at all until its processor answers,
207    /// and an older processor answers with none of the optional commands, so an
208    /// empty mask is never reported as a capability set - see
209    /// [`crate::Remote::v2ip_features`], which reports it as unknown instead.
210    V2ipFpgaFeature: u64 {
211        /// Applies a DSCP marking to the streams it sources.
212        SOURCE_DSCP = 1 << 0;
213        /// Reports the audio format arriving at its sink.
214        SINK_AUDIO_FORMAT = 1 << 1;
215        /// Places a tiling window on its sink.
216        SINK_TILING_WINDOW = 1 << 2;
217        /// Reports the state of its sink's overlay.
218        SINK_OVERLAY_STATE = 1 << 3;
219        /// Reports its sink's state.
220        SINK_STATE = 1 << 4;
221        /// Reports information about the stream its sink receives.
222        SINK_STREAM_INFO = 1 << 5;
223        /// Draws a test pattern, tone and lip-sync flash on its sink's output.
224        SINK_TEST_PATTERN = 1 << 8;
225    }
226}
227
228bitmask! {
229    /// The device settings a V2IP configuration can carry, each on its own bit.
230    ///
231    /// The same bits serve as the settings a frame carries and as the values
232    /// of the on/off ones among them. `IR_PROFILE` to `POWER_SAVE_SCHEDULE`
233    /// carry a value elsewhere in the block and have no on/off value of their
234    /// own.
235    ///
236    /// A device reports every setting it has, so one it never reports is one
237    /// it does not have.
238    V2ipDeviceSetting {
239        /// Disables the decoder while the display is off.
240        SINK_CHECK_POWER = 1 << 0;
241        /// Disables the HDMI output while there is no signal.
242        SINK_OFF_NO_SIGNAL = 1 << 1;
243        /// Sends infrared modulated.
244        IR_TX_MODULATED = 1 << 2;
245        /// Lights the status LED.
246        STATUS_LED = 1 << 3;
247        /// Lights the network port LEDs.
248        NETWORK_LED = 1 << 4;
249        /// Runs the fan in quiet mode.
250        FAN_QUIET = 1 << 5;
251        /// Accepts CEC combo keys.
252        CEC_COMBO_KEYS = 1 << 6;
253        /// Accepts CEC combo keys for the device's own input.
254        CEC_COMBO_INPUT = 1 << 7;
255        /// The infrared profile of the device's global infrared port.
256        IR_PROFILE = 1 << 8;
257        /// The infrared profile of the output's infrared port.
258        IR_PROFILE_SINK = 1 << 9;
259        /// The infrared profiles stored on the device. Reported by the device
260        /// itself and never written.
261        IR_PROFILES = 1 << 10;
262        /// The minutes a device stays idle before it powers down by itself.
263        AUTO_POWER_SAVE = 1 << 11;
264        /// The daily windows in which the device powers down.
265        POWER_SAVE_SCHEDULE = 1 << 12;
266        /// The device's clock has been set, on/off. Reported by the device
267        /// itself and never written.
268        CLOCK_SET = 1 << 13;
269    }
270}
271
272wire_enum! {
273    /// The test pattern a V2IP sink draws on its output.
274    V2ipTestPattern: u8 {
275        /// No pattern.
276        OFF = 0;
277        /// Colour bars.
278        BARS = 1;
279        /// One flat colour, the one the test card carries.
280        FLAT = 2;
281        /// A ramp.
282        RAMP = 3;
283        /// A grid.
284        GRID = 4;
285        /// A strip.
286        STRIP = 5;
287        /// A test card.
288        CARD = 6;
289    }
290}
291
292wire_enum! {
293    /// The test tone a V2IP sink plays on its output.
294    V2ipToneMode: u8 {
295        /// No tone.
296        OFF = 0;
297        /// A continuous tone.
298        CONTINUOUS = 1;
299        /// A channel ident.
300        IDENT = 2;
301        /// A line-up tone, which needs two channels or more.
302        LINEUP = 3;
303        /// A beep on each lip-sync mark.
304        BEEP = 4;
305    }
306}
307
308bitmask! {
309    /// What a V2IP sink reports about its test card.
310    V2ipTestcardFlag: u8 {
311        /// The sink can draw the test card.
312        SUPPORTED = 1 << 0;
313        /// The output shows the pattern.
314        SHOWING = 1 << 1;
315        /// The tone plays on the output.
316        PLAYING = 1 << 2;
317        /// A pattern change has yet to reach the video processor.
318        PATTERN_PENDING = 1 << 3;
319        /// A tone change has yet to reach the video processor.
320        TONE_PENDING = 1 << 4;
321        /// A lip-sync change has yet to reach the video processor.
322        SYNC_PENDING = 1 << 5;
323    }
324}
325
326bitmask! {
327    /// The flags of a V2IP device's VLAN configuration.
328    V2ipVlanFlag: u16 {
329        /// The block carries a configuration. A block without it carries
330        /// nothing, whatever its other bytes hold.
331        VALID = 1 << 0;
332        /// Untagged frames arriving on the uplink are dropped instead of
333        /// reaching the device.
334        TRUNK = 1 << 1;
335        /// Reported by the device: the configuration is applied, and reverted
336        /// unless the mesh controller confirms it. Never written.
337        PENDING = 1 << 2;
338        /// Sent by the mesh controller: a pending configuration it heard, which
339        /// the device then keeps.
340        CONFIRM = 1 << 3;
341        /// Reported by the device: it has an SFP port. Never written.
342        HAS_SFP = 1 << 4;
343    }
344}
345
346impl V2ipDeviceSetting {
347    /// The settings that are on or off, as opposed to carrying a value, and
348    /// that can be written.
349    pub const SWITCHES: Self = Self(0xFF);
350
351    /// The settings only a device reports about itself, which no write
352    /// carries.
353    pub const REPORTED_ONLY: Self = Self(Self::IR_PROFILES.0 | Self::CLOCK_SET.0);
354
355    /// These bits with every bit of `other` cleared.
356    pub(crate) const fn without(self, other: Self) -> Self {
357        Self(self.0 & !other.0)
358    }
359}
360
361bitmask! {
362    /// Capabilities of a single bay.
363    BayFeatures {
364        /// HDMI output.
365        HDMI_OUT = 1 << 0;
366        /// HDMI input.
367        HDMI_IN = 1 << 1;
368        /// Digital audio output.
369        AUDIO_DIG_OUT = 1 << 2;
370        /// Digital audio input.
371        AUDIO_DIG_IN = 1 << 3;
372        /// Analogue audio output.
373        AUDIO_ANA_OUT = 1 << 4;
374        /// Analogue audio input.
375        AUDIO_ANA_IN = 1 << 5;
376        /// Infrared input.
377        IR_IN = 1 << 6;
378        /// Infrared output.
379        IR_OUT = 1 << 7;
380        /// Amplified audio output.
381        AUDIO_AMP_OUT = 1 << 8;
382        /// Remote-control output.
383        RC_OUT = 1 << 9;
384        /// Remote-control input.
385        RC_IN = 1 << 10;
386        /// Dolby decoding.
387        DOLBY = 1 << 11;
388        /// Switches itself off when idle.
389        AUTO_OFF = 1 << 12;
390        /// Is a remote V2IP source.
391        V2IP_SOURCE_REMOTE = 1 << 13;
392        /// Is a remote V2IP sink.
393        V2IP_SINK_REMOTE = 1 << 14;
394        /// Is a local V2IP source.
395        V2IP_SOURCE_LOCAL = 1 << 15;
396        /// Is a local V2IP sink.
397        V2IP_SINK_LOCAL = 1 << 16;
398    }
399}
400
401bitmask! {
402    /// Live status flags of a single bay.
403    ///
404    /// Bits 16-19 and 22-23 are bit-fields rather than flags; read them with
405    /// [`BayStatus::rc_type`] and [`BayStatus::hdcp`].
406    BayStatus {
407        /// The bay reports a fault.
408        FAULT = 1 << 0;
409        /// The bay is hidden from the user interface.
410        HIDDEN = 1 << 1;
411        /// The bay has power.
412        POWERED = 1 << 2;
413        /// A signal is present.
414        SIGNAL_DETECTED = 1 << 3;
415        /// Hot-plug detect is asserted.
416        HPD_DETECTED = 1 << 4;
417        /// The signal is scrambled.
418        SIGNAL_SCRAMBLE = 1 << 5;
419        /// An HDBaseT link is up.
420        HDBT_CONNECTED = 1 << 6;
421        /// A CEC device answered.
422        CEC_DETECTED = 1 << 7;
423        /// The attached device was powered on.
424        POWERED_ON = 1 << 8;
425        /// The attached device was powered off.
426        POWERED_OFF = 1 << 9;
427        /// Audio return over HDMI is active.
428        AUDIO_ARC_HDMI = 1 << 10;
429        /// Audio return over optical is active.
430        AUDIO_ARC_OPTIC = 1 << 11;
431        /// Audio return over analogue is active.
432        AUDIO_ARC_ANALOG = 1 << 12;
433        /// The bay is offline.
434        OFFLINE = 1 << 13;
435        /// The V2IP decoder is disabled.
436        DECODER_DISABLE = 1 << 14;
437        /// The V2IP encoder is disabled.
438        ENCODER_DISABLE = 1 << 15;
439        /// CEC is switched off for this bay.
440        CEC_DISABLED = 1 << 20;
441        /// The V2IP encoder reports an error.
442        ENCODER_ERROR = 1 << 21;
443        /// The bay's name was generated - a default, or taken from CEC or the
444        /// EDID - rather than set by a user. A device that predates this bit
445        /// never sets it, so a clear bit does not prove a user set the name.
446        AUTO_NAME = 1 << 24;
447    }
448}
449
450impl BayStatus {
451    const RC_TYPE_SHIFT: u32 = 16;
452    const RC_TYPE_MASK: u32 = 0xF << Self::RC_TYPE_SHIFT;
453    const HDCP_SHIFT: u32 = 22;
454    const HDCP_MASK: u32 = 0x3 << Self::HDCP_SHIFT;
455
456    /// Extracts the remote-control type carried in bits 16-19.
457    pub const fn rc_type(self) -> RcType {
458        RcType(((self.0 & Self::RC_TYPE_MASK) >> Self::RC_TYPE_SHIFT) as u8)
459    }
460
461    /// Extracts the HDCP version carried in bits 22-23.
462    pub const fn hdcp(self) -> u8 {
463        ((self.0 & Self::HDCP_MASK) >> Self::HDCP_SHIFT) as u8
464    }
465}
466
467bitmask! {
468    /// Media carried by a virtual link.
469    LinkFeature {
470        /// Video over HDMI.
471        VIDEO_HDMI = 1 << 0;
472        /// Audio over optical.
473        AUDIO_OPTICAL = 1 << 1;
474        /// Audio over analogue.
475        AUDIO_ANALOG = 1 << 2;
476        /// Infrared.
477        IR = 1 << 3;
478        /// Remote control.
479        RC = 1 << 4;
480    }
481}
482
483wire_enum! {
484    /// A remote-control action.
485    RcAction: u16 {
486        /// Toggle power.
487        POWER_TOGGLE = 0;
488        /// Power on.
489        POWER_ON = 1;
490        /// Power off.
491        POWER_OFF = 2;
492        /// Volume down.
493        VOLUME_DOWN = 3;
494        /// Volume up.
495        VOLUME_UP = 4;
496        /// Toggle mute.
497        VOLUME_MUTE = 5;
498    }
499}
500
501wire_enum! {
502    /// A remote-control key code (CEC or IR).
503    RcKey: u16 {
504        /// Digit 0.
505        NUM0 = 0;
506        /// Digit 1.
507        NUM1 = 1;
508        /// Digit 2.
509        NUM2 = 2;
510        /// Digit 3.
511        NUM3 = 3;
512        /// Digit 4.
513        NUM4 = 4;
514        /// Digit 5.
515        NUM5 = 5;
516        /// Digit 6.
517        NUM6 = 6;
518        /// Digit 7.
519        NUM7 = 7;
520        /// Digit 8.
521        NUM8 = 8;
522        /// Digit 9.
523        NUM9 = 9;
524        /// Confirm the highlighted item.
525        SELECT = 10;
526        /// Go back one step.
527        BACK = 11;
528        /// Navigate up.
529        UP = 12;
530        /// Navigate down.
531        DOWN = 13;
532        /// Navigate left.
533        LEFT = 14;
534        /// Navigate right.
535        RIGHT = 15;
536        /// Open the main menu.
537        MENU = 16;
538        /// Open the content menu.
539        CONTENT_MENU = 17;
540        /// Next channel.
541        CHANNEL_UP = 18;
542        /// Previous channel.
543        CHANNEL_DOWN = 19;
544        /// Start playback.
545        PLAY = 20;
546        /// Pause playback.
547        PAUSE = 21;
548        /// Stop playback.
549        STOP = 22;
550        /// Start recording.
551        RECORD = 23;
552        /// Fast forward.
553        FAST_FORWARD = 24;
554        /// Rewind.
555        REWIND = 25;
556        /// Red colour key.
557        RED = 26;
558        /// Green colour key.
559        GREEN = 27;
560        /// Yellow colour key.
561        YELLOW = 28;
562        /// Blue colour key.
563        BLUE = 29;
564        /// Open help.
565        HELP = 30;
566        /// Show information.
567        INFORMATION = 31;
568        /// Open teletext.
569        TEXT = 32;
570        /// Open the programme guide.
571        GUIDE = 33;
572        /// Open video on demand.
573        VIDEO_ON_DEMAND = 34;
574        /// Return to the previous channel.
575        PREVIOUS_CHANNEL = 80;
576        /// Toggle 3D mode.
577        MODE_3D = 81;
578        /// Toggle subtitles.
579        SUBTITLE = 82;
580        /// Select an audio track.
581        SOUND_SELECT = 83;
582        /// Select an input.
583        INPUT_SELECT = 84;
584        /// Eject the medium.
585        EJECT = 85;
586        /// Next chapter.
587        NEXT_CHAPTER = 86;
588        /// Previous chapter.
589        PREV_CHAPTER = 87;
590        /// Open interactive services.
591        INTERACTIVE = 128;
592        /// Open search.
593        SEARCH = 129;
594        /// Sky home key.
595        SKY = 130;
596        /// Base of the range carrying a raw CEC user-control code.
597        CUSTOM_CEC = 1280;
598        /// Base of the range carrying a raw Sky key code.
599        CUSTOM_SKY = 2048;
600    }
601}
602
603wire_enum! {
604    /// The remote-control protocol of a connected sink or source.
605    RcType: u8 {
606        /// Infrared.
607        IR = 0;
608        /// HDMI CEC.
609        CEC = 1;
610        /// Sky UK over IP.
611        SKY_UK = 2;
612        /// TiVo.
613        TIVO = 3;
614        /// Kodi.
615        KODI = 4;
616        /// Dish.
617        DISH = 5;
618        /// DirecTV.
619        DIRECTV = 6;
620        /// Another MX Remote device.
621        MX_REMOTE = 7;
622    }
623}
624
625impl fmt::Display for RcType {
626    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627        let name = match *self {
628            Self::IR => "IR",
629            Self::CEC => "CEC",
630            Self::SKY_UK => "Sky",
631            Self::TIVO => "TiVo",
632            Self::KODI => "Kodi",
633            Self::DISH => "Dish",
634            Self::DIRECTV => "DirecTV",
635            Self::MX_REMOTE => "MX-Remote",
636            _ => "Unknown",
637        };
638        f.write_str(name)
639    }
640}
641
642wire_enum! {
643    /// An EDID preset selectable on an HDMI input.
644    EdidProfile: u16 {
645        /// 1080p with stereo audio.
646        STEREO_1080P = 0;
647        /// A fixed EDID stored on the device.
648        FIXED = 1;
649        /// 4K.
650        UHD_4K = 2;
651        /// 1080p with 5.1 audio.
652        SURROUND51_1080P = 3;
653        /// 720p.
654        HD_720P = 4;
655        /// 1080p with 7.1 audio.
656        SURROUND71_1080P = 5;
657        /// 4K with 7.1 audio.
658        SURROUND71_4K = 6;
659        /// 4K HDR with stereo audio.
660        HDR_STEREO_4K = 7;
661        /// 4K HDR with 7.1 audio.
662        HDR_SURROUND71_4K = 8;
663        /// 4K HDR, audio to the AVR only.
664        HDR_AVR_ONLY_4K = 9;
665        /// The lowest common denominator of the connected sinks.
666        LOWEST_COMMON = 10;
667        /// The lowest common denominator of every sink, connected or not.
668        LOWEST_COMMON_ALL = 11;
669        /// 4K HDR with Dolby Atmos.
670        HDR_ATMOS_4K = 12;
671        /// Copy the EDID of sink 1; the range runs to [`EdidProfile::SINK_32`].
672        SINK_1 = 101;
673        /// Copy the EDID of sink 32; the range starts at [`EdidProfile::SINK_1`].
674        SINK_32 = 132;
675        /// Base of the range carrying a user-supplied EDID.
676        CUSTOM_0 = 500;
677        /// The device reports no profile.
678        UNKNOWN = 0xFFF;
679    }
680}
681
682impl fmt::Display for EdidProfile {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        let name = match *self {
685            Self::STEREO_1080P => "1080p stereo",
686            Self::FIXED => "fixed",
687            Self::UHD_4K => "4K",
688            Self::SURROUND51_1080P => "1080p 5.1",
689            Self::HD_720P => "720p",
690            Self::SURROUND71_1080P => "1080p 7.1",
691            Self::SURROUND71_4K => "4K 7.1",
692            Self::HDR_STEREO_4K => "4K HDR Stereo",
693            Self::HDR_SURROUND71_4K => "4K HDR 7.1",
694            Self::HDR_AVR_ONLY_4K => "4K HDR AVR",
695            Self::LOWEST_COMMON => "lowest common denominator",
696            Self::LOWEST_COMMON_ALL => "lowest common denominator (all sinks)",
697            Self::HDR_ATMOS_4K => "4K HDR Dolby Atmos",
698            _ => {
699                if self.0 >= Self::SINK_1.0 && self.0 <= Self::SINK_32.0 {
700                    return write!(f, "copy from sink #{}", self.0 - Self::SINK_1.0 + 1);
701                }
702                return write!(f, "custom #{}", self.0);
703            }
704        };
705        f.write_str(name)
706    }
707}
708
709wire_enum! {
710    /// A firmware component.
711    FirmwareType: u8 {
712        /// The component is not known.
713        UNKNOWN = 0;
714        /// The FPGA bitstream.
715        FPGA = 1;
716        /// The Linux system image.
717        LINUX = 2;
718        /// A loadable overlay.
719        LOADING_OVERLAY = 3;
720    }
721}
722
723impl fmt::Display for FirmwareType {
724    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
725        let name = match *self {
726            Self::FPGA => "FPGA",
727            Self::LINUX => "Linux",
728            Self::LOADING_OVERLAY => "Loading Overlay",
729            _ => "Unknown",
730        };
731        f.write_str(name)
732    }
733}
734
735wire_enum! {
736    /// The negotiated speed of a network port.
737    UtpLinkSpeed: u8 {
738        /// The device reports no speed.
739        UNKNOWN = 0;
740        /// 10Mbit/s.
741        SPEED_10M = 1;
742        /// 100Mbit/s.
743        SPEED_100M = 2;
744        /// 200Mbit/s.
745        SPEED_200M = 3;
746        /// 1Gbit/s.
747        SPEED_1G = 4;
748    }
749}
750
751impl fmt::Display for UtpLinkSpeed {
752    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753        let name = match *self {
754            Self::SPEED_10M => "10Mbit/s",
755            Self::SPEED_100M => "100Mbit/s",
756            Self::SPEED_200M => "200Mbit/s",
757            Self::SPEED_1G => "1Gbit/s",
758            _ => "Unknown",
759        };
760        f.write_str(name)
761    }
762}
763
764wire_enum! {
765    /// The window layout of a multiviewer.
766    MultiviewerViewMode: u8 {
767        /// The device reports no layout.
768        UNKNOWN = 0;
769        /// One full-screen window.
770        SINGLE = 1;
771        /// Picture in picture.
772        PIP = 2;
773        /// Two windows, large.
774        TWO_SCREEN_LARGE = 3;
775        /// Two windows, small.
776        TWO_SCREEN_SMALL = 4;
777        /// Three windows, large.
778        THREE_SCREEN_LARGE = 5;
779        /// Three windows, small.
780        THREE_SCREEN_SMALL = 6;
781        /// Four windows, equal size.
782        FOUR_SCREEN_EQUAL = 7;
783        /// Four windows, small.
784        FOUR_SCREEN_SMALL = 8;
785    }
786}
787
788wire_enum! {
789    /// The corner a multiviewer places its picture-in-picture window in.
790    MultiviewerPipPosition: u8 {
791        /// The device reports no position.
792        UNKNOWN = 0;
793        /// Top left.
794        LEFT_TOP = 1;
795        /// Bottom left.
796        LEFT_BOTTOM = 2;
797        /// Top right.
798        RIGHT_TOP = 3;
799        /// Bottom right.
800        RIGHT_BOTTOM = 4;
801    }
802}
803
804wire_enum! {
805    /// The size of a multiviewer's picture-in-picture window.
806    MultiviewerPipSize: u8 {
807        /// The device reports no size.
808        UNKNOWN = 0;
809        /// Small.
810        SMALL = 1;
811        /// Medium.
812        MEDIUM = 2;
813        /// Large.
814        LARGE = 3;
815    }
816}
817
818wire_enum! {
819    /// The resolution and refresh rate a multiviewer drives its output at.
820    MultiviewerOutputMode: u8 {
821        /// The device reports no output mode.
822        UNKNOWN = 0;
823        /// 4096x2160p60.
824        DCI4K_P60 = 1;
825        /// 4096x2160p50.
826        DCI4K_P50 = 2;
827        /// 3840x2160p60.
828        UHD_P60 = 3;
829        /// 3840x2160p50.
830        UHD_P50 = 4;
831        /// 3840x2160p30.
832        UHD_P30 = 5;
833        /// 3840x2160p25.
834        UHD_P25 = 6;
835        /// 1920x1200p60, reduced blanking.
836        WUXGA_P60_RB = 7;
837        /// 1920x1080p60.
838        HD1080_P60 = 8;
839        /// 1920x1080p50.
840        HD1080_P50 = 9;
841        /// 1360x768p60.
842        WXGA_P60 = 10;
843        /// 1280x800p60.
844        WXGA800_P60 = 11;
845        /// 1280x720p60.
846        HD720_P60 = 12;
847        /// 1280x720p50.
848        HD720_P50 = 13;
849        /// 1024x768p60.
850        XGA_P60 = 14;
851    }
852}
853
854wire_enum! {
855    /// The HDCP version a multiviewer output negotiates.
856    MultiviewerHdcpMode: u8 {
857        /// The device reports no HDCP mode.
858        UNKNOWN = 0;
859        /// HDCP 1.4.
860        V14 = 1;
861        /// HDCP 2.2.
862        V22 = 2;
863        /// Content protection off.
864        OFF = 3;
865    }
866}
867
868wire_enum! {
869    /// The IT-content flag a multiviewer sets on its output.
870    MultiviewerItcMode: u8 {
871        /// The device reports no IT-content mode.
872        UNKNOWN = 0;
873        /// Video content.
874        VIDEO = 1;
875        /// PC content.
876        PC = 2;
877    }
878}
879
880wire_enum! {
881    /// The EDID template a multiviewer presents to its sources.
882    ///
883    /// A template's name is the largest resolution it advertises and the audio
884    /// format it declares support for.
885    MultiviewerEdidTemplate: u8 {
886        /// The device reports no template.
887        EDID_UNKNOWN = 0;
888        /// 4K2K60 4:4:4, stereo 2.0.
889        EDID_4K2K60_444_STEREO = 1;
890        /// 4K2K60 4:4:4, Dolby/DTS 5.1.
891        EDID_4K2K60_444_DOLBY_DTS_51 = 2;
892        /// 4K2K60 4:4:4, HD audio 7.1.
893        EDID_4K2K60_444_HD_AUDIO_71 = 3;
894        /// 4K2K30 4:4:4, stereo 2.0.
895        EDID_4K2K30_444_STEREO = 4;
896        /// 4K2K30 4:4:4, Dolby/DTS 5.1.
897        EDID_4K2K30_444_DOLBY_DTS_51 = 5;
898        /// 4K2K30 4:4:4, HD audio 7.1.
899        EDID_4K2K30_444_HD_AUDIO_71 = 6;
900        /// 1080p, stereo 2.0.
901        EDID_1080P_STEREO = 7;
902        /// 1080p, Dolby/DTS 5.1.
903        EDID_1080P_DOLBY_DTS_51 = 8;
904        /// 1080p, HD audio 7.1.
905        EDID_1080P_HD_AUDIO_71 = 9;
906        /// 1920x1200, stereo 2.0.
907        EDID_1920X1200_STEREO = 10;
908        /// 1680x1050, stereo 2.0.
909        EDID_1680X1050_STEREO = 11;
910        /// 1600x1200, stereo 2.0.
911        EDID_1600X1200_STEREO = 12;
912        /// 1440x900, stereo 2.0.
913        EDID_1440X900_STEREO = 13;
914        /// 1360x768, stereo 2.0.
915        EDID_1360X768_STEREO = 14;
916        /// 1280x1024, stereo 2.0.
917        EDID_1280X1024_STEREO = 15;
918        /// 1024x768, stereo 2.0.
919        EDID_1024X768_STEREO = 16;
920        /// 720p, stereo 2.0.
921        EDID_720P_STEREO = 17;
922        /// Whatever the display connected to the HDMI output presents. The
923        /// template a multiviewer leaves the factory with.
924        EDID_COPY_OUTPUT = 18;
925        /// The EDID loaded onto the device.
926        EDID_CUSTOM = 19;
927    }
928}
929
930wire_enum! {
931    /// The aspect ratio a multiviewer scales its windows to.
932    MultiviewerAspectRatio: u8 {
933        /// The device reports no aspect ratio.
934        UNKNOWN = 0;
935        /// Fill the window.
936        FULL = 1;
937        /// 16:9.
938        RATIO_16_9 = 2;
939    }
940}
941
942wire_enum! {
943    /// A multiviewer setting that is on, off, or not reported.
944    MultiviewerBool: u8 {
945        /// Off.
946        OFF = 0;
947        /// On.
948        ON = 1;
949        /// The device reports no value.
950        UNKNOWN = 0xFF;
951    }
952}
953
954wire_enum! {
955    /// One of a multiviewer's four inputs.
956    ///
957    /// The wire numbers the inputs from zero and this type from one, so that
958    /// zero can mean "not reported" the way it does for every other
959    /// multiviewer setting. So `to_wire` and `from_wire` carry this type's
960    /// numbering rather than the wire's, and neither is the conversion to
961    /// reach for when a raw multiviewer byte is what is in hand.
962    MultiviewerSource: u8 {
963        /// The device reports no source.
964        UNKNOWN = 0;
965        /// Input 1.
966        INPUT_1 = 1;
967        /// Input 2.
968        INPUT_2 = 2;
969        /// Input 3.
970        INPUT_3 = 3;
971        /// Input 4.
972        INPUT_4 = 4;
973    }
974}
975
976impl MultiviewerSource {
977    /// Reads a zero-based wire value, mapping anything past input 4 to
978    /// [`MultiviewerSource::UNKNOWN`].
979    ///
980    /// The firmware spells "not known" as 0xFF, which lands past input 4 and
981    /// so needs no case of its own.
982    pub(crate) const fn from_zero_based(value: u8) -> Self {
983        if value > 3 {
984            Self::UNKNOWN
985        } else {
986            Self(value + 1)
987        }
988    }
989
990    /// The zero-based value the wire carries, or `None` for a source naming no
991    /// input.
992    ///
993    /// A multiviewer reads zero as its first input, so there is no value that
994    /// says "leave this alone": a request that cannot name an input has to be
995    /// refused rather than sent.
996    pub(crate) const fn to_zero_based(self) -> Option<u8> {
997        match self.0 {
998            1..=4 => Some(self.0 - 1),
999            _ => None,
1000        }
1001    }
1002}
1003
1004impl MultiviewerBool {
1005    /// Reads a wire value, mapping anything but 0 and 1 to
1006    /// [`MultiviewerBool::UNKNOWN`].
1007    pub(crate) const fn from_wire_tristate(value: u8) -> Self {
1008        if value > 1 {
1009            Self::UNKNOWN
1010        } else {
1011            Self(value)
1012        }
1013    }
1014}
1015
1016wire_enum! {
1017    /// The colour space a V2IP output scales to.
1018    ///
1019    /// The field is four bits wide and only these four values are defined. A
1020    /// receiver passes the whole nibble to its validator, so a fifth value is
1021    /// dropped without a word rather than clamped to one of these.
1022    V2ipColourSpace: u8 {
1023        /// RGB.
1024        RGB = 0;
1025        /// YCbCr 4:4:4.
1026        YCBCR444 = 1;
1027        /// YCbCr 4:2:2.
1028        YCBCR422 = 2;
1029        /// YCbCr 4:2:0.
1030        YCBCR420 = 3;
1031    }
1032}
1033
1034wire_enum! {
1035    /// The 2-byte `mxr_signal_type` carried in scaling configs and bay signal
1036    /// reports.
1037    ///
1038    /// Byte 0 is the CTA-861 short video descriptor, 0 when the signal is not
1039    /// HDMI. Byte 1 packs `color:4` in the low nibble, then `non_int:1` and
1040    /// `bpp:3` above it.
1041    MxrSignalType: u16 {
1042        /// No signal format was reported.
1043        NONE = 0;
1044    }
1045}
1046
1047/// The bpp index a sender writes when it has no bit depth to report.
1048///
1049/// It sits outside the four indices that name a real depth, so an unset
1050/// format reads differently from every genuine one.
1051const SIG_BPP_UNSET: u16 = 5;
1052
1053impl MxrSignalType {
1054    /// The CTA-861 short video descriptor, 0 when the signal is not HDMI.
1055    pub const fn svd(self) -> u8 {
1056        (self.0 & 0xFF) as u8
1057    }
1058
1059    /// The colour space.
1060    pub const fn colour_space(self) -> u8 {
1061        ((self.0 >> 8) & 0xF) as u8
1062    }
1063
1064    /// Whether the frame rate carries a 1000/1001 clock.
1065    pub const fn is_non_integer(self) -> bool {
1066        self.0 & (1 << 12) != 0
1067    }
1068
1069    /// The raw bpp index as carried on the wire. The field is an index, not a
1070    /// bit depth; [`MxrSignalType::bpp`] converts it.
1071    pub const fn bpp_index(self) -> u8 {
1072        ((self.0 >> 13) & 0x7) as u8
1073    }
1074
1075    /// The bit depth the bpp index stands for, `None` when unknown or unset.
1076    pub const fn bpp(self) -> Option<u8> {
1077        match self.bpp_index() {
1078            1 => Some(8),
1079            2 => Some(10),
1080            3 => Some(12),
1081            4 => Some(16),
1082            _ => None,
1083        }
1084    }
1085
1086    /// Reports whether the word carries a signal format at all.
1087    ///
1088    /// A bay with nothing configured says so two ways. A sender that zeroes
1089    /// the word and stamps the unset bpp index leaves an index no real depth
1090    /// uses, and one that writes a plain zero leaves nothing at all. Neither
1091    /// is a format, and the svd and colour space beside them are not answers
1092    /// either: both read as zero, which is what this word says for "not HDMI"
1093    /// and "RGB" when it *is* set.
1094    pub const fn is_set(self) -> bool {
1095        self.0 != 0 && self.bpp_index() as u16 != SIG_BPP_UNSET
1096    }
1097
1098    /// Builds the word from the fields a scaling write consumes.
1099    ///
1100    /// `non_int` is left clear: the receiving struct carries the bit, and the
1101    /// apply path does not read it.
1102    ///
1103    /// Building rather than editing is the point. A sink with no mode
1104    /// configured reports the word with the unset bpp index in it, so a caller
1105    /// that read that word back and filled in an svd would send an index no
1106    /// depth uses - which the receiver decodes to zero and rejects without
1107    /// answering.
1108    pub(crate) const fn from_parts(svd: u8, colour: u8, bpp_index: u8) -> Self {
1109        Self((svd as u16) | (((colour & 0xF) as u16) << 8) | (((bpp_index & 0x7) as u16) << 13))
1110    }
1111
1112    /// The bpp index that stands for a bit depth, `None` for a depth no index
1113    /// names.
1114    ///
1115    /// Only the three depths a V2IP output stage accepts are here. Index 4
1116    /// names 16bpp, which [`MxrSignalType::bpp`] reads back from a device, but
1117    /// the output stage refuses it - so offering it as something to write would
1118    /// send a frame that is decoded cleanly and then dropped in silence.
1119    pub(crate) const fn bpp_index_for_depth(depth: u8) -> Option<u8> {
1120        match depth {
1121            8 => Some(1),
1122            10 => Some(2),
1123            12 => Some(3),
1124            _ => None,
1125        }
1126    }
1127}
1128
1129impl fmt::Display for MxrSignalType {
1130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1131        if !self.is_set() {
1132            return f.write_str("unset");
1133        }
1134        match self.bpp() {
1135            Some(bpp) => write!(
1136                f,
1137                "svd {}, color {}, {}bpp",
1138                self.svd(),
1139                self.colour_space(),
1140                bpp
1141            ),
1142            None => write!(f, "svd {}, color {}", self.svd(), self.colour_space()),
1143        }
1144    }
1145}