mx_remote_ffi/info.rs
1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Reading state: the snapshot structs, and the calls that fill them.
5//!
6//! State lives behind a lock that the receive thread also takes, so nothing
7//! here hands back a pointer into it. A caller passes an identifier and a
8//! struct it owns, and gets a copy that stays valid however the network
9//! behaves afterwards.
10//!
11//! Two conventions run through the structs. A flag the device has not reported
12//! is a [`mxr_tribool_t`] rather than a `bool`, because firmware sends only
13//! what it has and "off" is a different answer from "never said". A route that
14//! names no bay carries the zero device identifier, which is the spelling the
15//! protocol itself uses for absence.
16
17use std::ffi::c_char;
18
19use mx_remote::{
20 AmpZoneSettings, ArcStatus, BayAudioDetails, BayInfo, BaySignalDetails, DeviceInfo,
21 DeviceStatus, PowerStatus, AMP_EQ_BANDS,
22};
23
24use crate::abi::{
25 bay_or_zero, fail, guard, mxr_bay_uid_t, mxr_result_t, mxr_tribool_t, mxr_uid_t, put_str,
26};
27use crate::bits::mxr_signal_type_t;
28use crate::remote::{mxr_remote_t, with, MXR_IP_STRING_LEN};
29
30/// Bytes a device, bay or port name needs, the terminator included.
31///
32/// The wire field is 16 bytes wide. The rest is headroom for the names this
33/// library derives rather than reads, which that width does not bound.
34pub const MXR_NAME_LEN: usize = 32;
35
36/// Bytes a serial number needs, the terminator included.
37pub const MXR_SERIAL_LEN: usize = 32;
38
39/// Bytes a model name needs, the terminator included.
40pub const MXR_MODEL_LEN: usize = 48;
41
42/// Bytes a firmware version string needs, the terminator included.
43///
44/// The wire field is 128 bytes and is not NUL-terminated when full.
45pub const MXR_VERSION_LEN: usize = 129;
46
47/// Bytes a signal description such as `1080p60 444 8` needs, the terminator
48/// included.
49pub const MXR_SIGNAL_TYPE_LEN: usize = 48;
50
51/// Bytes a device's system-status message needs, the terminator included.
52pub const MXR_MESSAGE_LEN: usize = 128;
53
54/// Number of EQ bands an amplifier zone carries.
55///
56/// Written as a literal because the generated header needs one, and checked
57/// against the core crate's value below so the two cannot drift apart.
58pub const MXR_AMP_EQ_BANDS: usize = 5;
59
60const _: () = assert!(MXR_AMP_EQ_BANDS == AMP_EQ_BANDS);
61
62/// The high-level state of a device on the network.
63#[repr(i32)]
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum mxr_device_status_t {
66 /// Reachable and reporting.
67 MXR_DEVICE_ONLINE = 0,
68 /// Has stopped answering.
69 MXR_DEVICE_OFFLINE = 1,
70 /// Announced a reboot.
71 MXR_DEVICE_REBOOTING = 2,
72 /// Still coming up.
73 MXR_DEVICE_BOOTING = 3,
74 /// Present but not participating.
75 MXR_DEVICE_INACTIVE = 4,
76}
77
78impl From<DeviceStatus> for mxr_device_status_t {
79 fn from(status: DeviceStatus) -> Self {
80 match status {
81 DeviceStatus::Online => Self::MXR_DEVICE_ONLINE,
82 DeviceStatus::Offline => Self::MXR_DEVICE_OFFLINE,
83 DeviceStatus::Rebooting => Self::MXR_DEVICE_REBOOTING,
84 DeviceStatus::Booting => Self::MXR_DEVICE_BOOTING,
85 DeviceStatus::Inactive => Self::MXR_DEVICE_INACTIVE,
86 }
87 }
88}
89
90/// The CEC power state of whatever is connected to a bay.
91#[repr(i32)]
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum mxr_power_status_t {
94 /// The bay has not reported a power state.
95 MXR_POWER_UNKNOWN = 0,
96 /// Powered on.
97 MXR_POWER_ON = 1,
98 /// Powered off.
99 MXR_POWER_OFF = 2,
100}
101
102impl From<Option<PowerStatus>> for mxr_power_status_t {
103 fn from(status: Option<PowerStatus>) -> Self {
104 match status {
105 Some(PowerStatus::On) => Self::MXR_POWER_ON,
106 Some(PowerStatus::Off) => Self::MXR_POWER_OFF,
107 // A bay that reported PowerStatus::Unknown and one that reported
108 // nothing at all are the same answer to the only question a caller
109 // can ask of this field.
110 Some(PowerStatus::Unknown) | None => Self::MXR_POWER_UNKNOWN,
111 }
112 }
113}
114
115/// The audio return channel a bay is carrying.
116#[repr(i32)]
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum mxr_arc_status_t {
119 /// No audio is being returned.
120 MXR_ARC_INACTIVE = 0,
121 /// Returned over HDMI.
122 MXR_ARC_HDMI = 1,
123 /// Returned over optical.
124 MXR_ARC_OPTICAL = 2,
125 /// Returned over analogue.
126 MXR_ARC_ANALOG = 3,
127}
128
129impl From<ArcStatus> for mxr_arc_status_t {
130 fn from(arc: ArcStatus) -> Self {
131 match arc {
132 ArcStatus::Inactive => Self::MXR_ARC_INACTIVE,
133 ArcStatus::Hdmi => Self::MXR_ARC_HDMI,
134 ArcStatus::Optical => Self::MXR_ARC_OPTICAL,
135 ArcStatus::Analog => Self::MXR_ARC_ANALOG,
136 }
137 }
138}
139
140/// What a device is, and what it is doing.
141#[repr(C)]
142#[derive(Clone, Copy)]
143pub struct mxr_device_info_t {
144 /// The device's identifier.
145 pub uid: mxr_uid_t,
146 /// The name the device advertises.
147 pub name: [c_char; MXR_NAME_LEN],
148 /// Serial number.
149 pub serial: [c_char; MXR_SERIAL_LEN],
150 /// A friendly model name, derived from the advertised name and the bays
151 /// the device reports.
152 pub model: [c_char; MXR_MODEL_LEN],
153 /// Firmware version string from the hello frame.
154 pub version: [c_char; MXR_VERSION_LEN],
155 /// The highest protocol version the device can decode, or zero before it
156 /// has said. A command above it is refused rather than sent.
157 pub supported_protocol: u16,
158 /// What the device says it can do, as `MXR_FEATURE_*` bits.
159 pub features: u32,
160 /// The address the device was last heard from, empty when never.
161 pub address: [c_char; MXR_IP_STRING_LEN],
162 /// Online, offline, booting or rebooting.
163 pub status: mxr_device_status_t,
164 /// Whether the device has been heard from recently enough to count as
165 /// present.
166 pub online: bool,
167 /// Whether every part of the device's configuration has arrived.
168 pub configuration_complete: bool,
169 /// Whether the device's firmware initialises the configuration it
170 /// broadcasts.
171 ///
172 /// Firmware without it builds some frames over uninitialised stack, so
173 /// those fields carry noise rather than values: the scaling flags and,
174 /// behind a spuriously set valid bit, the scaling mode and refresh; bay
175 /// zero's addresses in the V2IP sources frame; and the padding beside the
176 /// remote-control target.
177 pub config_initialised: bool,
178 /// The mesh master this device follows, zero when it is in no mesh.
179 pub mesh_master: mxr_uid_t,
180 /// How many HDBaseT outputs this model has.
181 pub hdbt_outputs: u8,
182 /// Whether installation was marked complete.
183 pub setup_done: mxr_tribool_t,
184 /// The installer identifier, or -1 when the device has not reported one.
185 pub installer_id: i32,
186 /// Whether the device has reported a status about itself.
187 pub has_system_status: bool,
188 /// The status code, meaningful only when `has_system_status` is set.
189 pub system_status: u16,
190 /// The status message, empty when there is none.
191 pub system_message: [c_char; MXR_MESSAGE_LEN],
192 /// How many temperatures `mxr_device_temperatures()` would return.
193 pub temperature_count: usize,
194 /// How many bays `mxr_device_bays()` would return.
195 pub bay_count: usize,
196}
197
198/// What a bay is, and what is connected to it.
199#[repr(C)]
200#[derive(Clone, Copy)]
201pub struct mxr_bay_info_t {
202 /// How the bay is addressed.
203 pub uid: mxr_bay_uid_t,
204 /// The name the device gives the port, such as `Output 1`.
205 pub port_name: [c_char; MXR_NAME_LEN],
206 /// The name the installer gave the bay, falling back to the port name.
207 pub user_name: [c_char; MXR_NAME_LEN],
208 /// The bay number the device's own API and topology use, which is not the
209 /// port number this library addresses it by.
210 pub bay_num: u8,
211 /// What the bay is wired for, as `MXR_BAY_*` bits.
212 pub features: u32,
213 /// Whether the bay takes a signal in.
214 pub is_input: bool,
215 /// Whether the bay puts a signal out.
216 pub is_output: bool,
217 /// Whether the bay carries audio and no video.
218 pub is_audio: bool,
219 /// Whether the bay can decode Dolby.
220 pub has_dolby: bool,
221 /// Whether the bay is on this device rather than reached through the mesh.
222 pub is_local: bool,
223 /// The bay routed to this one for video, zero when unrouted.
224 pub video_source: mxr_bay_uid_t,
225 /// The bay routed to this one for audio, which follows the video source
226 /// until the bay is told otherwise. Zero when unrouted.
227 pub audio_source: mxr_bay_uid_t,
228 /// Power state of what is connected.
229 pub power_status: mxr_power_status_t,
230 /// Whether the bay is hidden from the installation's user interface.
231 pub hidden: mxr_tribool_t,
232 /// Whether the device reports the bay as faulty.
233 pub faulty: mxr_tribool_t,
234 /// Whether the bay is delivering power over the link.
235 pub poe_powered: mxr_tribool_t,
236 /// Whether an HDBaseT link is up.
237 pub hdbt_connected: mxr_tribool_t,
238 /// Whether a signal is present.
239 pub signal_detected: mxr_tribool_t,
240 /// Whether hot-plug detect is asserted.
241 pub hpd_detected: mxr_tribool_t,
242 /// Whether a CEC device answered.
243 pub cec_detected: mxr_tribool_t,
244 /// Whether the bay's encoder is switched off.
245 pub encoder_disabled: mxr_tribool_t,
246 /// Whether the bay's decoder is switched off.
247 pub decoder_disabled: mxr_tribool_t,
248 /// The signal as the device describes it, empty when it has not.
249 pub signal_type: [c_char; MXR_SIGNAL_TYPE_LEN],
250 /// The signal format the device reports, packed. Read it with the
251 /// `mxr_signal_type_*` functions.
252 pub signal_mode: mxr_signal_type_t,
253 /// Whether audio return is active, and over which connector.
254 pub arc: mxr_arc_status_t,
255 /// Whether the bay has reported a volume.
256 pub has_volume: bool,
257 /// The combined left/right volume percentage.
258 pub volume: u8,
259 /// Whether either channel is muted.
260 pub muted: mxr_tribool_t,
261 /// Whether the bay has reported a remote-control type.
262 pub has_rc_type: bool,
263 /// The kind of remote control attached, as the wire value.
264 pub rc_type: u8,
265 /// Whether the bay has reported an EDID profile.
266 pub has_edid_profile: bool,
267 /// The EDID profile the bay presents.
268 pub edid_profile: u16,
269 /// The bay this one mirrors, zero when it mirrors nothing.
270 pub mirror: mxr_bay_uid_t,
271 /// The audio endpoint this bay feeds, or -1 on a device without them.
272 pub audio_endpoint: i16,
273 /// The bay on another device this one is linked to, zero when it is
274 /// linked to none or that bay is not yet known.
275 ///
276 /// The link is mesh configuration, not a route: it names the bay elsewhere
277 /// that belongs to this one, such as the amplifier zone carrying a OneIP
278 /// output's volume. `volume` is already read through it, and
279 /// `mxr_set_volume()` already writes through it.
280 pub linked_bay: mxr_bay_uid_t,
281 /// The source device a V2IP bay maps to, zero when it maps to none.
282 pub v2ip_uid: mxr_uid_t,
283 /// How many devices `mxr_bay_filtered()` would return.
284 pub filtered_count: usize,
285}
286
287/// Bytes in one EDID: a base block and exactly one extension block.
288pub const MXR_EDID_LEN: usize = 256;
289
290/// The audio a bay signal report describes.
291#[repr(C)]
292#[derive(Clone, Copy)]
293pub struct mxr_audio_details_t {
294 /// How the stream is encoded: 0 unknown, 1 L-PCM, 2 high bit rate.
295 pub format: u8,
296 /// Channel count.
297 pub channels: u8,
298 /// Sample rate in Hz.
299 pub sample_rate: u32,
300 /// Whether the source sent a CTA-861 audio infoframe at all.
301 ///
302 /// Zero is a coding type a source can claim, so without this flag a source
303 /// that said nothing could not be told from one that claimed zero.
304 pub has_coding: bool,
305 /// The coding type the source claims, meaningful only when `has_coding`
306 /// is set.
307 pub coding: u8,
308}
309
310impl From<BayAudioDetails> for mxr_audio_details_t {
311 fn from(d: BayAudioDetails) -> Self {
312 Self {
313 format: d.format,
314 channels: d.channels,
315 sample_rate: d.sample_rate,
316 has_coding: d.coding.is_some(),
317 coding: d.coding.unwrap_or(0),
318 }
319 }
320}
321
322/// The signal a bay measures, beyond the description in
323/// [`mxr_bay_info_t::signal_type`].
324#[repr(C)]
325#[derive(Clone, Copy)]
326pub struct mxr_signal_details_t {
327 /// Frame rate in Hz, already corrected for a 1000/1001 clock.
328 pub frame_rate: f64,
329 /// TMDS clock rate in Hz.
330 pub tmds_clock: u32,
331 /// Video clock rate in Hz.
332 pub clock_rate: u32,
333 /// The bay status word from the report's bay block.
334 pub status: u32,
335 /// The signal type the bay is scaling to.
336 pub scaling: mxr_signal_type_t,
337}
338
339/// A ProAmp8 zone's gain, delay, tone and power settings.
340///
341/// Gains and volume limits run 0-248 in 0.5dB steps, with 200 as 0dB. Tone and
342/// EQ values are neutral at 128.
343#[repr(C)]
344#[derive(Clone, Copy)]
345pub struct mxr_amp_zone_settings_t {
346 /// Left channel gain.
347 pub gain_left: u8,
348 /// Right channel gain.
349 pub gain_right: u8,
350 /// Lowest volume the zone may be set to.
351 pub volume_min: u8,
352 /// Highest volume the zone may be set to.
353 pub volume_max: u8,
354 /// Bass tone control.
355 pub bass: u8,
356 /// Treble tone control.
357 pub treble: u8,
358 /// 0 = normal, 1 = bridged.
359 pub bridged: u8,
360 /// Power-on mode.
361 pub power_mode: u8,
362 /// Signal level that switches the zone on automatically.
363 pub power_level: u8,
364 /// Left channel delay, in 1/48000 second increments.
365 pub delay_left: u32,
366 /// Right channel delay, in 1/48000 second increments.
367 pub delay_right: u32,
368 /// Idle time before the zone powers down, in seconds.
369 pub power_timeout: u32,
370 /// Left channel EQ, from 100Hz to 10KHz.
371 pub eq_left: [u8; MXR_AMP_EQ_BANDS],
372 /// Right channel EQ, from 100Hz to 10KHz.
373 pub eq_right: [u8; MXR_AMP_EQ_BANDS],
374}
375
376impl From<AmpZoneSettings> for mxr_amp_zone_settings_t {
377 fn from(s: AmpZoneSettings) -> Self {
378 Self {
379 gain_left: s.gain_left,
380 gain_right: s.gain_right,
381 volume_min: s.volume_min,
382 volume_max: s.volume_max,
383 bass: s.bass,
384 treble: s.treble,
385 bridged: s.bridged,
386 power_mode: s.power_mode,
387 power_level: s.power_level,
388 delay_left: s.delay_left,
389 delay_right: s.delay_right,
390 power_timeout: s.power_timeout,
391 eq_left: s.eq_left,
392 eq_right: s.eq_right,
393 }
394 }
395}
396
397impl From<mxr_amp_zone_settings_t> for AmpZoneSettings {
398 fn from(s: mxr_amp_zone_settings_t) -> Self {
399 Self {
400 gain_left: s.gain_left,
401 gain_right: s.gain_right,
402 volume_min: s.volume_min,
403 volume_max: s.volume_max,
404 delay_left: s.delay_left,
405 delay_right: s.delay_right,
406 bass: s.bass,
407 treble: s.treble,
408 bridged: s.bridged,
409 power_mode: s.power_mode,
410 power_level: s.power_level,
411 power_timeout: s.power_timeout,
412 eq_left: s.eq_left,
413 eq_right: s.eq_right,
414 }
415 }
416}
417
418impl From<BaySignalDetails> for mxr_signal_details_t {
419 fn from(d: BaySignalDetails) -> Self {
420 Self {
421 frame_rate: d.frame_rate,
422 tmds_clock: d.tmds_clock,
423 clock_rate: d.clock_rate,
424 status: d.status.bits(),
425 scaling: d.scaling.to_wire(),
426 }
427 }
428}
429
430impl mxr_device_info_t {
431 /// Copies a snapshot into the C shape.
432 fn of(info: &DeviceInfo) -> Self {
433 let mut out = Self {
434 uid: info.uid.into(),
435 name: [0; MXR_NAME_LEN],
436 serial: [0; MXR_SERIAL_LEN],
437 model: [0; MXR_MODEL_LEN],
438 version: [0; MXR_VERSION_LEN],
439 supported_protocol: info.supported_protocol,
440 features: info.features.bits(),
441 address: [0; MXR_IP_STRING_LEN],
442 status: info.status.into(),
443 online: info.online,
444 configuration_complete: info.configuration_complete,
445 config_initialised: info.config_initialised,
446 mesh_master: info.mesh_master.into(),
447 hdbt_outputs: info.hdbt_outputs,
448 setup_done: info.setup_done.into(),
449 // The wire field is 16 bits, so -1 cannot collide with a value a
450 // device could report.
451 installer_id: info.installer_id.map_or(-1, i32::from),
452 has_system_status: info.system_status.is_some(),
453 system_status: info.system_status.as_ref().map_or(0, |(code, _)| *code),
454 system_message: [0; MXR_MESSAGE_LEN],
455 temperature_count: info.temperatures.len(),
456 bay_count: info.bays.len(),
457 };
458 put_str(&mut out.name, &info.name);
459 put_str(&mut out.serial, &info.serial);
460 put_str(&mut out.model, &info.model);
461 put_str(&mut out.version, &info.version);
462 if let Some(address) = info.address {
463 put_str(&mut out.address, &address.to_string());
464 }
465 if let Some((_, message)) = &info.system_status {
466 put_str(&mut out.system_message, message);
467 }
468 out
469 }
470}
471
472impl mxr_bay_info_t {
473 /// Copies a snapshot into the C shape.
474 fn of(info: &BayInfo) -> Self {
475 let mut out = Self {
476 uid: info.uid.into(),
477 port_name: [0; MXR_NAME_LEN],
478 user_name: [0; MXR_NAME_LEN],
479 bay_num: info.bay_num,
480 features: info.features.bits(),
481 is_input: info.is_input,
482 is_output: info.is_output,
483 is_audio: info.is_audio,
484 has_dolby: info.has_dolby,
485 is_local: info.is_local,
486 video_source: bay_or_zero(info.video_source),
487 audio_source: bay_or_zero(info.audio_source),
488 power_status: info.power_status.into(),
489 hidden: info.hidden.into(),
490 faulty: info.faulty.into(),
491 poe_powered: info.poe_powered.into(),
492 hdbt_connected: info.hdbt_connected.into(),
493 signal_detected: info.signal_detected.into(),
494 hpd_detected: info.hpd_detected.into(),
495 cec_detected: info.cec_detected.into(),
496 encoder_disabled: info.encoder_disabled.into(),
497 decoder_disabled: info.decoder_disabled.into(),
498 signal_type: [0; MXR_SIGNAL_TYPE_LEN],
499 signal_mode: info.signal_mode.to_wire(),
500 arc: info.arc.into(),
501 has_volume: info.volume.is_some(),
502 volume: info.volume.map_or(0, |v| v.volume()),
503 muted: info.volume.and_then(|v| v.muted()).into(),
504 has_rc_type: info.rc_type.is_some(),
505 rc_type: info.rc_type.map_or(0, |t| t.to_wire()),
506 has_edid_profile: info.edid_profile.is_some(),
507 edid_profile: info.edid_profile.map_or(0, |p| p.to_wire()),
508 mirror: bay_or_zero(info.mirror.target),
509 // An endpoint is a byte on the wire, so -1 cannot collide with one.
510 audio_endpoint: info.audio_endpoint.map_or(-1, i16::from),
511 linked_bay: bay_or_zero(info.linked_bay),
512 v2ip_uid: info.v2ip_uid.into(),
513 filtered_count: info.filtered.len(),
514 };
515 put_str(&mut out.port_name, &info.port_name);
516 put_str(&mut out.user_name, &info.user_name);
517 if let Some(signal_type) = &info.signal_type {
518 put_str(&mut out.signal_type, signal_type);
519 }
520 out
521 }
522}
523
524/// Fills `out` with what is known about a device.
525///
526/// # Safety
527///
528/// `remote` is null or a live handle, and `out` points at a writable
529/// [`mxr_device_info_t`].
530#[no_mangle]
531pub unsafe extern "C" fn mxr_device(
532 remote: *const mxr_remote_t,
533 uid: mxr_uid_t,
534 out: *mut mxr_device_info_t,
535) -> mxr_result_t {
536 // SAFETY: the caller guarantees a live handle or null.
537 let handle = unsafe { remote.as_ref() };
538 with(handle, |r| {
539 if out.is_null() {
540 return null_out("device info");
541 }
542 match r.remote.device(uid.into()) {
543 Some(info) => {
544 // SAFETY: checked non-null just above.
545 unsafe { *out = mxr_device_info_t::of(&info) };
546 mxr_result_t::MXR_OK
547 }
548 None => not_heard_from(uid),
549 }
550 })
551}
552
553/// Fills `out` with what is known about a bay.
554///
555/// # Safety
556///
557/// `remote` is null or a live handle, and `out` points at a writable
558/// [`mxr_bay_info_t`].
559#[no_mangle]
560pub unsafe extern "C" fn mxr_bay(
561 remote: *const mxr_remote_t,
562 bay: mxr_bay_uid_t,
563 out: *mut mxr_bay_info_t,
564) -> mxr_result_t {
565 // SAFETY: the caller guarantees a live handle or null.
566 let handle = unsafe { remote.as_ref() };
567 with(handle, |r| {
568 if out.is_null() {
569 return null_out("bay info");
570 }
571 match r.remote.bay(bay.into()) {
572 Some(info) => {
573 // SAFETY: checked non-null just above.
574 unsafe { *out = mxr_bay_info_t::of(&info) };
575 mxr_result_t::MXR_OK
576 }
577 None => fail(
578 mxr_result_t::MXR_ERR_NOT_FOUND,
579 &format!("no bay {}", mx_remote::BayUid::from(bay)),
580 ),
581 }
582 })
583}
584
585/// Fills `out` with the signal a bay measures.
586///
587/// Fails with `MXR_ERR_NOT_REPORTED` on a bay that has sent no signal report.
588///
589/// # Safety
590///
591/// `remote` is null or a live handle, and `out` points at a writable
592/// [`mxr_signal_details_t`].
593#[no_mangle]
594pub unsafe extern "C" fn mxr_bay_signal_details(
595 remote: *const mxr_remote_t,
596 bay: mxr_bay_uid_t,
597 out: *mut mxr_signal_details_t,
598) -> mxr_result_t {
599 // SAFETY: the caller guarantees a live handle or null.
600 let handle = unsafe { remote.as_ref() };
601 with(handle, |r| {
602 if out.is_null() {
603 return null_out("signal details");
604 }
605 let Some(info) = r.remote.bay(bay.into()) else {
606 return fail(
607 mxr_result_t::MXR_ERR_NOT_FOUND,
608 &format!("no bay {}", mx_remote::BayUid::from(bay)),
609 );
610 };
611 match info.signal_details {
612 Some(details) => {
613 // SAFETY: checked non-null just above.
614 unsafe { *out = details.into() };
615 mxr_result_t::MXR_OK
616 }
617 None => fail(
618 mxr_result_t::MXR_ERR_NOT_REPORTED,
619 "the bay has reported no signal details",
620 ),
621 }
622 })
623}
624
625/// Fills `out` with the audio a bay's signal report describes.
626///
627/// Separate from `mxr_bay_signal_details()` because a report can carry video
628/// and no audio: the video block is filled in whenever there is a signal,
629/// while the audio block appears only once the source claims one. Fails with
630/// `MXR_ERR_NOT_REPORTED` where the report carried none.
631///
632/// # Safety
633///
634/// `remote` is null or a live handle, and `out` points at a writable
635/// [`mxr_audio_details_t`].
636#[no_mangle]
637pub unsafe extern "C" fn mxr_bay_audio_details(
638 remote: *const mxr_remote_t,
639 bay: mxr_bay_uid_t,
640 out: *mut mxr_audio_details_t,
641) -> mxr_result_t {
642 // SAFETY: the caller guarantees a live handle or null.
643 let handle = unsafe { remote.as_ref() };
644 with(handle, |r| {
645 if out.is_null() {
646 return null_out("audio details");
647 }
648 let Some(info) = r.remote.bay(bay.into()) else {
649 return fail(
650 mxr_result_t::MXR_ERR_NOT_FOUND,
651 &format!("no bay {}", mx_remote::BayUid::from(bay)),
652 );
653 };
654 match info.signal_details.and_then(|d| d.audio) {
655 Some(audio) => {
656 // SAFETY: checked non-null just above.
657 unsafe { *out = audio.into() };
658 mxr_result_t::MXR_OK
659 }
660 None => fail(
661 mxr_result_t::MXR_ERR_NOT_REPORTED,
662 "the bay has reported no audio alongside its signal",
663 ),
664 }
665 })
666}
667
668/// Copies the EDID a device last reported into `out`.
669///
670/// `output` picks the EDID of the display on the device's output over the one
671/// the device presents to the source on its input. Ask for one with
672/// `mxr_request_edid()`; until a device has answered, or been overheard
673/// answering a peer, this fails with `MXR_ERR_NOT_REPORTED`.
674///
675/// `cap` must be at least `MXR_EDID_LEN`.
676///
677/// # Safety
678///
679/// `remote` is null or a live handle, and `out` points at `cap` writable
680/// bytes.
681#[no_mangle]
682pub unsafe extern "C" fn mxr_device_edid(
683 remote: *const mxr_remote_t,
684 device: mxr_uid_t,
685 output: bool,
686 out: *mut u8,
687 cap: usize,
688) -> mxr_result_t {
689 // SAFETY: the caller guarantees a live handle or null.
690 let handle = unsafe { remote.as_ref() };
691 with(handle, |r| {
692 if out.is_null() {
693 return null_out("edid");
694 }
695 if cap < MXR_EDID_LEN {
696 return fail(
697 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
698 &format!("an EDID needs {MXR_EDID_LEN} bytes, and {cap} were offered"),
699 );
700 }
701 let Some(edid) = r.remote.edid(device.into(), output) else {
702 return fail(
703 mxr_result_t::MXR_ERR_NOT_REPORTED,
704 "the device has reported no EDID",
705 );
706 };
707 // A record is one block and one extension, and the receive path drops
708 // a reply of any other length, so this is the whole of what arrived.
709 let n = edid.len().min(MXR_EDID_LEN);
710 // SAFETY: out is non-null with at least MXR_EDID_LEN writable bytes,
711 // checked above, and n is no larger.
712 unsafe { std::ptr::copy_nonoverlapping(edid.as_ptr(), out, n) };
713 mxr_result_t::MXR_OK
714 })
715}
716
717/// Fills `out` with an amplifier zone's settings.
718///
719/// Fails with `MXR_ERR_NOT_REPORTED` on a bay that is not an amplifier zone or
720/// has not reported its settings.
721///
722/// # Safety
723///
724/// `remote` is null or a live handle, and `out` points at a writable
725/// [`mxr_amp_zone_settings_t`].
726#[no_mangle]
727pub unsafe extern "C" fn mxr_bay_amp_settings(
728 remote: *const mxr_remote_t,
729 bay: mxr_bay_uid_t,
730 out: *mut mxr_amp_zone_settings_t,
731) -> mxr_result_t {
732 // SAFETY: the caller guarantees a live handle or null.
733 let handle = unsafe { remote.as_ref() };
734 with(handle, |r| {
735 if out.is_null() {
736 return null_out("amp zone settings");
737 }
738 let Some(info) = r.remote.bay(bay.into()) else {
739 return fail(
740 mxr_result_t::MXR_ERR_NOT_FOUND,
741 &format!("no bay {}", mx_remote::BayUid::from(bay)),
742 );
743 };
744 match info.amp_settings {
745 Some(settings) => {
746 // SAFETY: checked non-null just above.
747 unsafe { *out = settings.into() };
748 mxr_result_t::MXR_OK
749 }
750 None => fail(
751 mxr_result_t::MXR_ERR_NOT_REPORTED,
752 "the bay has reported no amplifier settings",
753 ),
754 }
755 })
756}
757
758/// Writes a device's bays in port order, and returns how many there are.
759///
760/// Returns the full count even when it exceeds `cap`, so calling with `cap`
761/// zero sizes the buffer. Returns zero for a device never heard from.
762///
763/// # Safety
764///
765/// `remote` is null or a live handle, and `out` is null or points at `cap`
766/// writable [`mxr_bay_uid_t`].
767#[no_mangle]
768pub unsafe extern "C" fn mxr_device_bays(
769 remote: *const mxr_remote_t,
770 uid: mxr_uid_t,
771 out: *mut mxr_bay_uid_t,
772 cap: usize,
773) -> usize {
774 guard(0, || {
775 // SAFETY: the caller guarantees a live handle or null.
776 let Some(info) = (unsafe { device_info(remote, uid) }) else {
777 return 0;
778 };
779 // SAFETY: the caller guarantees cap writable elements at out.
780 unsafe { copy_into(&info.bays, out, cap) }
781 })
782}
783
784/// Writes the temperatures a device reports, in its own order, in degrees
785/// Celsius, and returns how many there are.
786///
787/// # Safety
788///
789/// `remote` is null or a live handle, and `out` is null or points at `cap`
790/// writable bytes.
791#[no_mangle]
792pub unsafe extern "C" fn mxr_device_temperatures(
793 remote: *const mxr_remote_t,
794 uid: mxr_uid_t,
795 out: *mut u8,
796 cap: usize,
797) -> usize {
798 guard(0, || {
799 // SAFETY: the caller guarantees a live handle or null.
800 let Some(info) = (unsafe { device_info(remote, uid) }) else {
801 return 0;
802 };
803 // SAFETY: the caller guarantees cap writable bytes at out.
804 unsafe { copy_into(&info.temperatures, out, cap) }
805 })
806}
807
808/// Writes the devices whose signals a bay refuses, and returns how many there
809/// are.
810///
811/// # Safety
812///
813/// `remote` is null or a live handle, and `out` is null or points at `cap`
814/// writable [`mxr_uid_t`].
815#[no_mangle]
816pub unsafe extern "C" fn mxr_bay_filtered(
817 remote: *const mxr_remote_t,
818 bay: mxr_bay_uid_t,
819 out: *mut mxr_uid_t,
820 cap: usize,
821) -> usize {
822 guard(0, || {
823 // SAFETY: the caller guarantees a live handle or null.
824 let Some(r) = (unsafe { remote.as_ref() }) else {
825 fail(
826 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
827 "the client handle is null",
828 );
829 return 0;
830 };
831 let Some(info) = r.remote.bay(bay.into()) else {
832 fail(
833 mxr_result_t::MXR_ERR_NOT_FOUND,
834 &format!("no bay {}", mx_remote::BayUid::from(bay)),
835 );
836 return 0;
837 };
838 // SAFETY: the caller guarantees cap writable elements at out.
839 unsafe { copy_into(&info.filtered, out, cap) }
840 })
841}
842
843/// The snapshot of a device, having reported why there is none.
844///
845/// # Safety
846///
847/// `remote` is null or a live handle.
848unsafe fn device_info(remote: *const mxr_remote_t, uid: mxr_uid_t) -> Option<DeviceInfo> {
849 // SAFETY: the caller guarantees a live handle or null.
850 let Some(r) = (unsafe { remote.as_ref() }) else {
851 fail(
852 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
853 "the client handle is null",
854 );
855 return None;
856 };
857 let info = r.remote.device(uid.into());
858 if info.is_none() {
859 not_heard_from(uid);
860 }
861 info
862}
863
864/// Copies a list into a caller's array and reports how long the list is.
865///
866/// # Safety
867///
868/// `out` is null or points at `cap` writable elements.
869pub(crate) unsafe fn copy_into<T: Copy, U: Copy + Into<T>>(
870 items: &[U],
871 out: *mut T,
872 cap: usize,
873) -> usize {
874 if !out.is_null() {
875 // SAFETY: the caller guarantees cap writable elements at out.
876 let dst = unsafe { std::slice::from_raw_parts_mut(out, cap) };
877 for (slot, item) in dst.iter_mut().zip(items) {
878 *slot = (*item).into();
879 }
880 }
881 items.len()
882}
883
884pub(crate) fn null_out(what: &str) -> mxr_result_t {
885 fail(
886 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
887 &format!("the {what} output pointer is null"),
888 )
889}
890
891pub(crate) fn not_heard_from(uid: mxr_uid_t) -> mxr_result_t {
892 fail(
893 mxr_result_t::MXR_ERR_NOT_FOUND,
894 &format!("no device {}", mx_remote::DeviceUid::from(uid)),
895 )
896}