Skip to main content

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}