Skip to main content

mx_remote/types/
status.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Device and bay state.
5
6use core::fmt;
7
8use crate::wire::{BayStatus, BayUid, FirmwareType, MxrSignalType};
9
10/// The high-level state of a device or bay on the network.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum DeviceStatus {
13    /// Reachable and reporting.
14    #[default]
15    Online,
16    /// Has stopped answering.
17    Offline,
18    /// Announced a reboot.
19    Rebooting,
20    /// Still coming up.
21    Booting,
22    /// Present but not participating.
23    Inactive,
24}
25
26impl fmt::Display for DeviceStatus {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.write_str(match self {
29            Self::Online => "Online",
30            Self::Offline => "Offline",
31            Self::Rebooting => "Rebooting",
32            Self::Booting => "Booting",
33            Self::Inactive => "Inactive",
34        })
35    }
36}
37
38/// The CEC power state of a device connected to a bay.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum PowerStatus {
41    /// The device has not reported a power state.
42    #[default]
43    Unknown,
44    /// Powered on.
45    On,
46    /// Powered off.
47    Off,
48}
49
50impl fmt::Display for PowerStatus {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(match self {
53            Self::On => "on",
54            Self::Off => "off",
55            Self::Unknown => "unknown",
56        })
57    }
58}
59
60/// The connect / signal-detect state reported for a bay.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub enum ConnectStatus {
63    /// The device has not reported a connect state.
64    #[default]
65    Unknown,
66    /// Something is attached.
67    Connected,
68    /// Nothing is attached.
69    Disconnected,
70}
71
72impl fmt::Display for ConnectStatus {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(match self {
75            Self::Connected => "connected",
76            Self::Disconnected => "disconnected",
77            Self::Unknown => "unknown",
78        })
79    }
80}
81
82/// The visibility state of a bay.
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub enum HiddenStatus {
85    /// The device has not reported a visibility.
86    #[default]
87    Unknown,
88    /// Hidden from the user interface.
89    Hidden,
90    /// Shown in the user interface.
91    Visible,
92}
93
94impl fmt::Display for HiddenStatus {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(match self {
97            Self::Hidden => "hidden",
98            Self::Visible => "visible",
99            Self::Unknown => "unknown",
100        })
101    }
102}
103
104/// The value that means "leave this one alone" in a volume or mute field.
105///
106/// It is not a level and not a mute state. A device reads it as "do not
107/// change", and the right channel additionally as "no such channel", so a
108/// decoder that surfaces it as a volume of 255 or as a muted pair reports a
109/// setting the sender was declining to touch.
110pub const VOLUME_UNCHANGED: u8 = 0xFF;
111
112/// The per-channel mute bitfield: bit 0 is left, bit 1 is right.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
114pub struct MuteStatus(u8);
115
116impl MuteStatus {
117    /// Wraps the raw wire byte.
118    pub const fn from_wire(value: u8) -> Self {
119        Self(value)
120    }
121
122    /// Returns the raw wire byte.
123    pub const fn to_wire(self) -> u8 {
124        self.0
125    }
126
127    /// Whether the left channel is muted.
128    pub const fn left(self) -> bool {
129        self.0 & (1 << 0) != 0
130    }
131
132    /// Whether the right channel is muted.
133    pub const fn right(self) -> bool {
134        self.0 & (1 << 1) != 0
135    }
136
137    /// Whether either channel is muted.
138    pub const fn muted(self) -> bool {
139        self.0 != 0
140    }
141}
142
143/// The volume and mute state of a bay.
144///
145/// A `None` field is one the device did not report, which is distinct from a
146/// reported zero.
147#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
148pub struct VolumeMuteStatus {
149    /// Left channel volume, as a percentage.
150    pub volume_left: Option<u8>,
151    /// Right channel volume, as a percentage.
152    pub volume_right: Option<u8>,
153    /// Whether the left channel is muted.
154    pub muted_left: Option<bool>,
155    /// Whether the right channel is muted.
156    pub muted_right: Option<bool>,
157}
158
159impl VolumeMuteStatus {
160    /// The combined left/right volume percentage, or zero when neither channel
161    /// reported one.
162    pub fn volume(&self) -> u8 {
163        match (self.volume_left, self.volume_right) {
164            (Some(l), Some(r)) => ((u16::from(l) + u16::from(r)) / 2) as u8,
165            (Some(l), None) => l,
166            (None, Some(r)) => r,
167            (None, None) => 0,
168        }
169    }
170
171    /// The combined mute state, or `None` when neither channel reported one.
172    pub fn muted(&self) -> Option<bool> {
173        match (self.muted_left, self.muted_right) {
174            (None, None) => None,
175            (l, r) => Some(l.unwrap_or(false) || r.unwrap_or(false)),
176        }
177    }
178
179    /// Encodes the 3-byte `[volume_left, volume_right, muted]` field. A channel
180    /// that reported no volume is sent the combined one.
181    pub(crate) fn wire(&self) -> [u8; 3] {
182        [
183            self.volume_left.unwrap_or_else(|| self.volume()),
184            self.volume_right.unwrap_or_else(|| self.volume()),
185            self.muted_value(),
186        ]
187    }
188
189    /// Encodes the mute field: a per-channel bitmask, or
190    /// [`VOLUME_UNCHANGED`] where neither channel says.
191    ///
192    /// Not knowing is not the same as knowing it is unmuted. Zero is what a
193    /// device is told to unmute by, so sending it for a caller who named no
194    /// mute state would unmute a bay they only meant to set the volume of.
195    fn muted_value(&self) -> u8 {
196        match self.muted() {
197            None => VOLUME_UNCHANGED,
198            Some(false) => 0,
199            Some(true) => match (
200                self.muted_left.unwrap_or(false),
201                self.muted_right.unwrap_or(false),
202            ) {
203                (true, true) => 3,
204                (true, false) => 1,
205                _ => 2,
206            },
207        }
208    }
209}
210
211/// What a bay signal status report carries beyond the signal-detected flag and
212/// the human-readable signal type.
213///
214/// **On a decoder's output bay the signal description is a snapshot of the
215/// routed source, taken when that stream last started.** Nothing refreshes it
216/// while the route holds, so a source that changes format in place leaves
217/// [`frame_rate`](Self::frame_rate), [`tmds_clock`](Self::tmds_clock),
218/// [`audio`](Self::audio) and the bay's signal description all reading the
219/// format from before the change. Re-routing the bay does resample them, unless
220/// the newly selected source's record has not arrived yet - which leaves the
221/// previous values standing rather than clearing them.
222///
223/// [`status`](Self::status) and [`scaling`](Self::scaling) are re-stamped on
224/// every scaling change too, so they can be newer than the description beside
225/// them. [`clock_rate`](Self::clock_rate) is computed from the snapshot and
226/// carries its age despite sitting with them.
227///
228/// **For a decoder's current input format, read the input bay it is routed to.**
229/// That record is refreshed by the source's own reports, and it is the reason a
230/// decoder's own output bay can disagree with the picture on the cable.
231#[derive(Clone, Copy, Debug, Default, PartialEq)]
232pub struct BaySignalDetails {
233    /// Frame rate in Hz, already corrected for a 1000/1001 clock.
234    pub frame_rate: f64,
235    /// TMDS clock rate in Hz.
236    pub tmds_clock: u32,
237    /// The bay status word from the report's bay block.
238    pub status: BayStatus,
239    /// The format the bay is scaling to, not [`MxrSignalType::is_set`] while it
240    /// is not scaling.
241    pub scaling: MxrSignalType,
242    /// Video clock rate in Hz, computed rather than measured: the pixel clock of
243    /// [`scaling`](Self::scaling) while the bay scales, and otherwise the
244    /// signal description's own, halved for 4:2:0 and scaled by colour depth.
245    pub clock_rate: u32,
246    /// The audio alongside the video, absent when the report carried no audio
247    /// block.
248    pub audio: Option<BayAudioDetails>,
249}
250
251/// The audio a bay signal report describes.
252#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
253pub struct BayAudioDetails {
254    /// How the stream is encoded: 0 unknown, 1 L-PCM, 2 high bit rate, 3
255    /// IEC 61937 (compressed audio in two-channel frames).
256    pub format: u8,
257    /// Channel count: the stream's, or the infoframe's where the stream
258    /// gives none, and 0 where neither does.
259    pub channels: u8,
260    /// Sample rate in Hz.
261    pub sample_rate: u32,
262    /// The coding type the source claims in its CTA-861 audio infoframe, and
263    /// `None` where it sent no infoframe at all.
264    ///
265    /// The two are worth keeping apart: a source sending no infoframe leaves
266    /// this field zero, and zero is also what a source claiming "refer to the
267    /// stream header" writes into it.
268    pub coding: Option<u8>,
269}
270
271/// A firmware component reported by a device.
272#[derive(Clone, Debug, Default, PartialEq, Eq)]
273pub struct FirmwareVersion {
274    /// Which component this describes.
275    pub firmware_type: FirmwareType,
276    /// When the image was built or installed, in seconds since the Unix epoch,
277    /// or 0 when it carries no time.
278    pub timestamp: u32,
279    /// Human-readable version string.
280    pub version: String,
281    /// Checksum of the image.
282    pub hash: u32,
283}
284
285impl fmt::Display for FirmwareVersion {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        write!(
288            f,
289            "firmware {} version {} hash {}",
290            self.firmware_type, self.version, self.hash
291        )
292    }
293}
294
295/// The most bytes in a [`BayContentField::key`].
296pub const BAY_CONTENT_KEY_MAX: usize = 31;
297
298/// The most bytes in a [`BayContentField::value`].
299pub const BAY_CONTENT_VALUE_MAX: usize = 255;
300
301/// One thing an input's source is showing, such as its channel name.
302///
303/// A source reports these as named values in the order they are meant to be
304/// shown. The names are open-ended: a driver for a new kind of source adds
305/// its own without any other unit changing, so a caller passes on names it
306/// does not know.
307#[derive(Clone, Debug, PartialEq, Eq)]
308pub struct BayContentField {
309    /// What the value is, such as `name` or `number`.
310    pub key: String,
311    /// The value, as text.
312    pub value: String,
313}
314
315/// Whether an output bay mirrors another device's output, and which one.
316///
317/// The default is "not mirroring".
318#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
319pub struct BayMirrorStatus {
320    /// The bay being mirrored, or `None` when this bay mirrors nothing.
321    pub target: Option<BayUid>,
322}
323
324impl BayMirrorStatus {
325    /// Reports whether this bay mirrors another.
326    pub const fn is_mirroring(&self) -> bool {
327        self.target.is_some()
328    }
329}
330
331/// One device a unit found behind its switch ports, in its topology report.
332#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
333pub struct TopologyEntry {
334    /// The device this entry describes.
335    pub uid: crate::wire::DeviceUid,
336    /// The sender's switch ports this device's MAC address was learnt on, one
337    /// bit per port.
338    pub mask: u32,
339}
340
341/// The audio return channel a bay is carrying.
342#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
343pub enum ArcStatus {
344    /// No audio is being returned.
345    #[default]
346    Inactive,
347    /// Returned over HDMI.
348    Hdmi,
349    /// Returned over optical.
350    Optical,
351    /// Returned over analogue.
352    Analog,
353}
354
355impl fmt::Display for ArcStatus {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        f.write_str(match self {
358            Self::Inactive => "Inactive",
359            Self::Hdmi => "HDMI",
360            Self::Optical => "optical",
361            Self::Analog => "analog",
362        })
363    }
364}
365
366/// The time zone a device announces for its mesh.
367///
368/// The mesh controller announces it with every periodic broadcast, and each
369/// device of the mesh keeps its clock and its power save windows by it. An
370/// empty name and rule mean the controller has none, and the devices keep
371/// UTC.
372#[derive(Clone, Debug, Default, PartialEq, Eq)]
373pub struct TimeZone {
374    /// The IANA name, such as `Europe/Amsterdam`.
375    pub zone: String,
376    /// The POSIX TZ rule the devices keep time by, such as
377    /// `CET-1CEST,M3.5.0,M10.5.0/3`.
378    pub rule: String,
379}