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}