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