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) and the
218/// bay's signal description all reading the format from before the change.
219/// Re-routing the bay does resample them, unless the newly selected source's
220/// record has not arrived yet - which leaves the previous values standing
221/// rather than clearing them.
222///
223/// [`audio`](Self::audio) is the exception: it describes the audio the output
224/// receives, from the stream's own description while its sender gives one, and
225/// the output reports a change in it with the picture unchanged.
226///
227/// [`status`](Self::status) and [`scaling`](Self::scaling) are re-stamped on
228/// every scaling change too, so they can be newer than the description beside
229/// them. [`clock_rate`](Self::clock_rate) is computed from the snapshot and
230/// carries its age despite sitting with them.
231///
232/// **For a decoder's current input format, read the input bay it is routed to.**
233/// That record is refreshed by the source's own reports, and it is the reason a
234/// decoder's own output bay can disagree with the picture on the cable.
235#[derive(Clone, Copy, Debug, Default, PartialEq)]
236pub struct BaySignalDetails {
237 /// Frame rate in Hz, already corrected for a 1000/1001 clock.
238 pub frame_rate: f64,
239 /// TMDS clock rate in Hz.
240 pub tmds_clock: u32,
241 /// The bay status word from the report's bay block.
242 pub status: BayStatus,
243 /// The format the bay is scaling to, not [`MxrSignalType::is_set`] while it
244 /// is not scaling.
245 pub scaling: MxrSignalType,
246 /// Video clock rate in Hz, computed rather than measured: the pixel clock of
247 /// [`scaling`](Self::scaling) while the bay scales, and otherwise the
248 /// signal description's own, halved for 4:2:0 and scaled by colour depth.
249 pub clock_rate: u32,
250 /// The audio alongside the video, absent when the report carried no audio
251 /// block.
252 pub audio: Option<BayAudioDetails>,
253}
254
255/// The audio a bay signal report describes.
256#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
257pub struct BayAudioDetails {
258 /// How the stream is encoded: 0 unknown, 1 L-PCM, 2 high bit rate, 3
259 /// IEC 61937 (compressed audio in two-channel frames).
260 pub format: u8,
261 /// Channel count: the stream's, or the infoframe's where the stream
262 /// gives none, and 0 where neither does.
263 pub channels: u8,
264 /// Sample rate in Hz.
265 pub sample_rate: u32,
266 /// The coding type the source claims in its CTA-861 audio infoframe, and
267 /// `None` where it sent no infoframe at all.
268 ///
269 /// The two are worth keeping apart: a source sending no infoframe leaves
270 /// this field zero, and zero is also what a source claiming "refer to the
271 /// stream header" writes into it.
272 pub coding: Option<u8>,
273}
274
275/// A firmware component reported by a device.
276#[derive(Clone, Debug, Default, PartialEq, Eq)]
277pub struct FirmwareVersion {
278 /// Which component this describes.
279 pub firmware_type: FirmwareType,
280 /// When the image was built or installed, in seconds since the Unix epoch,
281 /// or 0 when it carries no time.
282 pub timestamp: u32,
283 /// Human-readable version string.
284 pub version: String,
285 /// Checksum of the image.
286 pub hash: u32,
287}
288
289impl fmt::Display for FirmwareVersion {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 write!(
292 f,
293 "firmware {} version {} hash {}",
294 self.firmware_type, self.version, self.hash
295 )
296 }
297}
298
299/// The most bytes in a [`BayContentField::key`].
300pub const BAY_CONTENT_KEY_MAX: usize = 31;
301
302/// The most bytes in a [`BayContentField::value`].
303pub const BAY_CONTENT_VALUE_MAX: usize = 255;
304
305/// One thing an input's source is showing, such as its channel name.
306///
307/// A source reports these as named values in the order they are meant to be
308/// shown. The names are open-ended: a driver for a new kind of source adds
309/// its own without any other unit changing, so a caller passes on names it
310/// does not know.
311#[derive(Clone, Debug, PartialEq, Eq)]
312pub struct BayContentField {
313 /// What the value is, such as `name` or `number`.
314 pub key: String,
315 /// The value, as text.
316 pub value: String,
317}
318
319/// Whether an output bay mirrors another device's output, and which one.
320///
321/// The default is "not mirroring".
322#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
323pub struct BayMirrorStatus {
324 /// The bay being mirrored, or `None` when this bay mirrors nothing.
325 pub target: Option<BayUid>,
326}
327
328impl BayMirrorStatus {
329 /// Reports whether this bay mirrors another.
330 pub const fn is_mirroring(&self) -> bool {
331 self.target.is_some()
332 }
333}
334
335/// One device a unit found behind its switch ports, in its topology report.
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
337pub struct TopologyEntry {
338 /// The device this entry describes.
339 pub uid: crate::wire::DeviceUid,
340 /// The sender's switch ports this device's MAC address was learnt on, one
341 /// bit per port.
342 pub mask: u32,
343}
344
345/// The audio return channel a bay is carrying.
346#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
347pub enum ArcStatus {
348 /// No audio is being returned.
349 #[default]
350 Inactive,
351 /// Returned over HDMI.
352 Hdmi,
353 /// Returned over optical.
354 Optical,
355 /// Returned over analogue.
356 Analog,
357}
358
359impl fmt::Display for ArcStatus {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 f.write_str(match self {
362 Self::Inactive => "Inactive",
363 Self::Hdmi => "HDMI",
364 Self::Optical => "optical",
365 Self::Analog => "analog",
366 })
367 }
368}
369
370/// The time zone a device announces for its mesh.
371///
372/// The mesh controller announces it with every periodic broadcast, and each
373/// device of the mesh keeps its clock and its power save windows by it. An
374/// empty name and rule mean the controller has none, and the devices keep
375/// UTC.
376#[derive(Clone, Debug, Default, PartialEq, Eq)]
377pub struct TimeZone {
378 /// The IANA name, such as `Europe/Amsterdam`.
379 pub zone: String,
380 /// The POSIX TZ rule the devices keep time by, such as
381 /// `CET-1CEST,M3.5.0,M10.5.0/3`.
382 pub rule: String,
383}