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