mx_remote/runtime/info.rs
1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Snapshots of a device and of a bay.
5//!
6//! State lives behind a lock and is read by the receive thread, so a caller is
7//! handed a copy rather than a borrow of it. The copy also matches how the C
8//! ABI reads state: a uid in, a struct filled out.
9
10use std::net::Ipv4Addr;
11use std::time::Instant;
12
13use crate::state::{Bay, Device, State};
14use crate::types::{
15 AmpZoneSettings, ArcStatus, BayMirrorStatus, BaySignalDetails, DeviceStatus, PowerStatus,
16 VolumeMuteStatus,
17};
18use crate::wire::{
19 BayFeatures, BayUid, DeviceFeature, DeviceUid, EdidProfile, MxrSignalType, RcType,
20};
21
22/// What a device is, and what it is doing.
23#[derive(Clone, Debug, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct DeviceInfo {
26 /// The device's unique identifier.
27 pub uid: DeviceUid,
28 /// The name the device advertises, or `Unknown` before it has said.
29 pub name: String,
30 /// Serial number, or `Unknown` before the device has said.
31 pub serial: String,
32 /// A friendly model name derived from the advertised name and the bays the
33 /// device reports.
34 pub model: String,
35 /// Firmware version string from the hello frame.
36 pub version: String,
37 /// The highest protocol version the device can decode, or zero before it
38 /// has said.
39 pub supported_protocol: u16,
40 /// What the device says it can do.
41 pub features: DeviceFeature,
42 /// The address the device was last heard from.
43 pub address: Option<Ipv4Addr>,
44 /// Online, offline, booting or rebooting.
45 pub status: DeviceStatus,
46 /// Whether the device has been heard from recently enough to count as
47 /// present.
48 pub online: bool,
49 /// Whether every part of the device's configuration has arrived.
50 pub configuration_complete: bool,
51 /// Whether the device's firmware initialises the configuration it
52 /// broadcasts.
53 ///
54 /// Firmware without it builds some frames over uninitialised stack, so
55 /// those fields carry noise rather than values: the scaling flags and,
56 /// behind a spuriously set valid bit, the scaling mode and refresh; bay 0's
57 /// addresses in the V2IP sources frame; and the padding beside the
58 /// remote-control target.
59 pub config_initialised: bool,
60 /// Temperatures the device reports, in degrees Celsius, in its own order.
61 pub temperatures: Vec<u8>,
62 /// The mesh master this device follows, or the zero uid when it is in no
63 /// mesh.
64 pub mesh_master: DeviceUid,
65 /// How many HDBaseT outputs this model has.
66 pub hdbt_outputs: u8,
67 /// Whether installation was marked complete, `None` before the device
68 /// has said.
69 pub setup_done: Option<bool>,
70 /// The installer identifier the device carries, if it has reported one.
71 pub installer_id: Option<u16>,
72 /// A status code and message the device reports about itself.
73 pub system_status: Option<(u16, String)>,
74 /// Every bay on the device, in port order.
75 pub bays: Vec<BayUid>,
76}
77
78impl DeviceInfo {
79 pub(crate) fn of(device: &Device, now: Instant) -> Self {
80 Self {
81 uid: device.uid,
82 name: device.name().to_owned(),
83 serial: device.serial().to_owned(),
84 model: device.model_name().to_owned(),
85 version: device.hello.version.clone(),
86 supported_protocol: device.hello.supported_protocol,
87 features: device.hello.features,
88 address: device.hello.address,
89 status: device.status(now),
90 online: device.is_online(now),
91 configuration_complete: device.configuration_complete(),
92 config_initialised: device.config_initialised(),
93 temperatures: device.temperatures.clone(),
94 mesh_master: device.mesh_master,
95 hdbt_outputs: device.hdbt_outputs(),
96 setup_done: device.setup_done,
97 installer_id: device.installer_id,
98 system_status: device.sys_status.clone(),
99 bays: device.bays.values().map(Bay::uid).collect(),
100 }
101 }
102}
103
104/// What a bay is, and what is connected to it.
105///
106/// A status this bay has never reported is `None`, which is not the same as
107/// the status being off: a device reports only what it has.
108#[derive(Clone, Debug, PartialEq)]
109#[non_exhaustive]
110pub struct BayInfo {
111 /// How the bay is addressed: its device and its port.
112 pub uid: BayUid,
113 /// The name the device gives the port, such as `Output 1`.
114 pub port_name: String,
115 /// The name the installer gave the bay, falling back to the port name.
116 pub user_name: String,
117 /// The bay number the device's own API and topology use.
118 pub bay_num: u8,
119 /// What the bay is wired for.
120 pub features: BayFeatures,
121 /// Whether the bay takes a signal in.
122 pub is_input: bool,
123 /// Whether the bay puts a signal out.
124 pub is_output: bool,
125 /// Whether the bay carries audio and no video.
126 pub is_audio: bool,
127 /// Whether the bay can decode Dolby.
128 pub has_dolby: bool,
129 /// Whether the bay is on this device rather than reached through the mesh.
130 pub is_local: bool,
131 /// The bay routed to this one for video.
132 pub video_source: Option<BayUid>,
133 /// The bay routed to this one for audio, which follows the video source
134 /// until the bay is told otherwise.
135 pub audio_source: Option<BayUid>,
136 /// Power state of what is connected.
137 pub power_status: Option<PowerStatus>,
138 /// Whether the bay is hidden from the installation's user interface.
139 pub hidden: Option<bool>,
140 /// Whether the device reports the bay as faulty.
141 pub faulty: Option<bool>,
142 /// Whether the bay is delivering power over the link.
143 pub poe_powered: Option<bool>,
144 /// Whether an HDBaseT link is up.
145 pub hdbt_connected: Option<bool>,
146 /// Whether a signal is present.
147 pub signal_detected: Option<bool>,
148 /// Whether hot-plug detect is asserted.
149 pub hpd_detected: Option<bool>,
150 /// Whether a CEC device answered.
151 pub cec_detected: Option<bool>,
152 /// Whether the bay's encoder is switched off.
153 pub encoder_disabled: Option<bool>,
154 /// Whether the bay's decoder is switched off.
155 pub decoder_disabled: Option<bool>,
156 /// The signal as the device describes it, such as `1080p60 444 8`.
157 pub signal_type: Option<String>,
158 /// The signal as the device measures it.
159 pub signal_details: Option<BaySignalDetails>,
160 /// The signal format the device reports for the bay.
161 pub signal_mode: MxrSignalType,
162 /// Whether audio return is active, and over which connector.
163 pub arc: ArcStatus,
164 /// Volume and mute, for a bay that has them.
165 ///
166 /// A bay with no volume control of its own reads its
167 /// [`linked_bay`](BayInfo::linked_bay)'s, because a link to an amplifier
168 /// zone is how the mesh says that zone is this bay's volume.
169 pub volume: Option<VolumeMuteStatus>,
170 /// The kind of remote control attached.
171 pub rc_type: Option<RcType>,
172 /// The EDID profile the bay presents.
173 pub edid_profile: Option<EdidProfile>,
174 /// Which bay this one mirrors, if any.
175 pub mirror: BayMirrorStatus,
176 /// The audio endpoint this bay feeds, on a device that has them.
177 pub audio_endpoint: Option<u8>,
178 /// Amplifier settings, on an amplifier zone.
179 pub amp_settings: Option<AmpZoneSettings>,
180 /// Devices whose signals this bay refuses.
181 pub filtered: Vec<DeviceUid>,
182 /// The bay on another device this one is linked to, once that bay has been
183 /// discovered.
184 ///
185 /// A link is mesh configuration rather than a route: it names the bay
186 /// elsewhere that belongs to this one, such as the amplifier zone carrying
187 /// a OneIP output's volume. [`BayInfo::volume`] is already read through it.
188 pub linked_bay: Option<BayUid>,
189 /// The source device a V2IP bay maps to, or the zero uid.
190 pub v2ip_uid: DeviceUid,
191}
192
193impl BayInfo {
194 pub(crate) fn of(state: &State, bay: &Bay) -> Self {
195 Self {
196 uid: bay.uid(),
197 port_name: bay.port_name.clone(),
198 user_name: bay.user_name().to_owned(),
199 bay_num: bay.bay_num(),
200 features: bay.features,
201 is_input: bay.is_input(),
202 is_output: bay.is_output(),
203 is_audio: bay.is_audio(),
204 has_dolby: bay.has_dolby(),
205 is_local: bay.is_local(),
206 video_source: bay.video_source,
207 audio_source: bay.effective_audio_source(),
208 power_status: bay.power_status,
209 hidden: bay.hidden,
210 faulty: bay.faulty,
211 poe_powered: bay.poe_powered,
212 hdbt_connected: bay.hdbt_connected,
213 signal_detected: bay.signal_detected,
214 hpd_detected: bay.hpd_detected,
215 cec_detected: bay.cec_detected,
216 encoder_disabled: bay.encoder_disabled,
217 decoder_disabled: bay.decoder_disabled,
218 signal_type: bay.signal_type.clone(),
219 signal_details: bay.signal_details,
220 signal_mode: bay.signal_mode,
221 arc: bay.arc,
222 volume: state
223 .bay(state.volume_bay(bay.uid()))
224 .and_then(|b| b.audio_volume),
225 rc_type: bay.rc_type,
226 edid_profile: bay.edid_profile,
227 mirror: bay.mirror,
228 audio_endpoint: bay.audio_endpoint,
229 amp_settings: bay.amp_settings,
230 filtered: bay.filtered.clone(),
231 linked_bay: state.linked_bay(bay.uid()),
232 v2ip_uid: bay.v2ip_uid,
233 }
234 }
235}