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, BayContentField, BayMirrorStatus, BaySignalDetails, DeviceStatus,
16 PowerStatus, 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 ///
51 /// A device that withholds its link configuration is reported complete
52 /// fifteen seconds after it was discovered, and is asked for the rest up to
53 /// three times, a minute apart: an unreported link reads as no link, which
54 /// is what a link coming up later looks like anyway. Its bays, and a OneIP device's source list,
55 /// are waited for without limit - a caller names things after a bay, and a
56 /// name assigned to a placeholder outlives the frame that would have
57 /// corrected it. A management client has none of these to send and is
58 /// complete from its hello.
59 pub configuration_complete: bool,
60 /// Whether the device's firmware initialises the configuration it
61 /// broadcasts.
62 ///
63 /// Firmware without it builds some frames over uninitialised stack, so
64 /// those fields carry noise rather than values: the scaling flags and,
65 /// behind a spuriously set valid bit, the scaling mode and refresh; bay 0's
66 /// addresses in the OneIP sources frame; and the padding beside the
67 /// remote-control target.
68 pub config_initialised: bool,
69 /// Temperatures the device reports, in degrees Celsius, in its own order.
70 /// A sensor the device failed to read reports -55.
71 pub temperatures: Vec<i8>,
72 /// The mesh master this device follows, or the zero uid when it is in no
73 /// mesh.
74 pub mesh_master: DeviceUid,
75 /// How many HDBaseT outputs this model has.
76 pub hdbt_outputs: u8,
77 /// Whether installation was marked complete, `None` before the device
78 /// has said.
79 pub setup_done: Option<bool>,
80 /// The installer identifier the device carries, if it has reported one.
81 pub installer_id: Option<u16>,
82 /// A status code and message the device reports about itself.
83 pub system_status: Option<(u16, String)>,
84 /// Every bay on the device, in port order.
85 pub bays: Vec<BayUid>,
86}
87
88impl DeviceInfo {
89 pub(crate) fn of(device: &Device, now: Instant) -> Self {
90 Self {
91 uid: device.uid,
92 name: device.name().to_owned(),
93 serial: device.serial().to_owned(),
94 model: device.model_name().to_owned(),
95 version: device.hello.version.clone(),
96 supported_protocol: device.hello.supported_protocol,
97 features: device.hello.features,
98 address: device.hello.address,
99 status: device.status(now),
100 online: device.is_online(now),
101 configuration_complete: device.configuration_complete(now),
102 config_initialised: device.config_initialised(),
103 temperatures: device.temperatures.clone(),
104 mesh_master: device.mesh_master,
105 hdbt_outputs: device.hdbt_outputs(),
106 setup_done: device.setup_done,
107 installer_id: device.installer_id,
108 system_status: device.sys_status.clone(),
109 bays: device.bays.values().map(Bay::uid).collect(),
110 }
111 }
112}
113
114/// What a bay is, and what is connected to it.
115///
116/// A status this bay has never reported is `None`, which is not the same as
117/// the status being off: a device reports only what it has.
118///
119/// A mesh bay, a OneIP bay on one unit standing for another unit's input or
120/// output, reports that unit's own bay 0 in the same direction. Only its
121/// identity is its own: `uid`, `port_name`, `bay_num`, `features`, its
122/// direction, `is_local` and `v2ip_uid`, which names that unit. Every change
123/// to that bay is reported for each mesh bay standing for it as well. Until
124/// the unit has reported the bay, the rest reads as never reported.
125///
126/// A command about a mesh bay goes to that unit about that bay, and is
127/// refused until the unit has reported it. A route keeps naming its source
128/// by a port of the device listing the mesh bay.
129#[derive(Clone, Debug, PartialEq)]
130#[non_exhaustive]
131pub struct BayInfo {
132 /// How the bay is addressed: its device and its port.
133 pub uid: BayUid,
134 /// The name the device gives the port, such as `Output 1`.
135 pub port_name: String,
136 /// The name the installer gave the bay, falling back to the port name.
137 pub user_name: String,
138 /// The bay number the device's own API and topology use.
139 pub bay_num: u8,
140 /// What the bay is wired for.
141 pub features: BayFeatures,
142 /// Whether the bay takes a signal in.
143 pub is_input: bool,
144 /// Whether the bay puts a signal out.
145 pub is_output: bool,
146 /// Whether the bay carries audio and no video.
147 pub is_audio: bool,
148 /// Whether the bay can decode Dolby.
149 pub has_dolby: bool,
150 /// Whether the bay is on this device rather than reached through the mesh.
151 pub is_local: bool,
152 /// The bay routed to this one for video.
153 pub video_source: Option<BayUid>,
154 /// The bay routed to this one for audio, which follows the video source
155 /// until the bay is told otherwise.
156 pub audio_source: Option<BayUid>,
157 /// Power state of what is connected.
158 pub power_status: Option<PowerStatus>,
159 /// Whether the bay is hidden from the installation's user interface.
160 pub hidden: Option<bool>,
161 /// Whether the device reports the bay as faulty.
162 pub faulty: Option<bool>,
163 /// Whether the amplifier zone the bay drives is powered on.
164 pub zone_powered: Option<bool>,
165 /// Whether an HDBaseT link is up.
166 pub hdbt_connected: Option<bool>,
167 /// Whether a signal is present.
168 pub signal_detected: Option<bool>,
169 /// Whether hot-plug detect is asserted.
170 pub hpd_detected: Option<bool>,
171 /// Whether a CEC device answered.
172 pub cec_detected: Option<bool>,
173 /// Whether the bay's encoder is switched off.
174 pub encoder_disabled: Option<bool>,
175 /// Whether the bay's decoder is switched off.
176 pub decoder_disabled: Option<bool>,
177 /// The signal as the device describes it, such as `1080p60 444 8`.
178 pub signal_type: Option<String>,
179 /// The signal as the device measures it.
180 pub signal_details: Option<BaySignalDetails>,
181 /// The signal format the device reports for the bay.
182 pub signal_mode: MxrSignalType,
183 /// Whether audio return is active, and over which connector.
184 pub arc: ArcStatus,
185 /// Volume and mute, for a bay that has them.
186 ///
187 /// A bay with no volume control of its own reads its
188 /// [`linked_bay`](BayInfo::linked_bay)'s, because a link to an amplifier
189 /// zone is how the mesh says that zone is this bay's volume.
190 pub volume: Option<VolumeMuteStatus>,
191 /// The kind of remote control attached.
192 pub rc_type: Option<RcType>,
193 /// The EDID profile the bay presents.
194 pub edid_profile: Option<EdidProfile>,
195 /// Which bay this one mirrors, if any.
196 pub mirror: BayMirrorStatus,
197 /// The audio endpoint this bay feeds, on a device that has them.
198 pub audio_endpoint: Option<u8>,
199 /// Amplifier settings, on an amplifier zone.
200 pub amp_settings: Option<AmpZoneSettings>,
201 /// Devices whose signals this bay refuses.
202 pub filtered: Vec<DeviceUid>,
203 /// The bay on another device this one is linked to, once that bay has been
204 /// discovered.
205 ///
206 /// A link is mesh configuration rather than a route: it names the bay
207 /// elsewhere that belongs to this one, such as the amplifier zone carrying
208 /// a OneIP output's volume. [`BayInfo::volume`] is already read through it.
209 pub linked_bay: Option<BayUid>,
210 /// The unit a OneIP bay stands for, or the zero uid.
211 pub v2ip_uid: DeviceUid,
212 /// What the source on an input is showing, as its unit last reported.
213 ///
214 /// A unit reports this whenever it changes, and keeps it while the source
215 /// has no signal; check [`BayInfo::signal_detected`] before showing it.
216 pub content: Vec<BayContentField>,
217}
218
219impl BayInfo {
220 pub(crate) fn of(state: &State, bay: &Bay) -> Self {
221 let d = state.described(bay);
222 Self {
223 uid: bay.uid(),
224 port_name: bay.port_name.clone(),
225 user_name: d.user_name().to_owned(),
226 bay_num: bay.bay_num(),
227 features: bay.features,
228 is_input: bay.is_input(),
229 is_output: bay.is_output(),
230 is_audio: bay.is_audio(),
231 has_dolby: bay.has_dolby(),
232 is_local: bay.is_local(),
233 video_source: d.video_source,
234 audio_source: d.effective_audio_source(),
235 power_status: d.power_status,
236 hidden: d.hidden,
237 faulty: d.faulty,
238 zone_powered: d.zone_powered,
239 hdbt_connected: d.hdbt_connected,
240 signal_detected: d.signal_detected,
241 hpd_detected: d.hpd_detected,
242 cec_detected: d.cec_detected,
243 encoder_disabled: d.encoder_disabled,
244 decoder_disabled: d.decoder_disabled,
245 signal_type: d.signal_type.clone(),
246 signal_details: d.signal_details,
247 signal_mode: d.signal_mode,
248 arc: d.arc,
249 volume: state
250 .bay(state.volume_bay(d.uid()))
251 .and_then(|b| b.audio_volume),
252 rc_type: d.rc_type,
253 edid_profile: d.edid_profile,
254 mirror: d.mirror,
255 audio_endpoint: d.audio_endpoint,
256 amp_settings: d.amp_settings,
257 filtered: d.filtered.clone(),
258 linked_bay: state.linked_bay(d.uid()),
259 v2ip_uid: bay.v2ip_uid,
260 content: d.content.clone(),
261 }
262 }
263}