Skip to main content

mx_remote_ffi/
subsystems.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! What a device reports about one of its subsystems.
5//!
6//! These are the values that do not fit in a device or bay snapshot: streams,
7//! statistics, the audio tree, the network ports. Each has an event that says
8//! it moved and a call here that says what it is now, which is why the events
9//! carry only an identifier - what they would carry instead is this, and a
10//! copy taken at event time could only be staler than a read.
11//!
12//! A call returns `MXR_ERR_NOT_REPORTED` when the device exists but has not
13//! sent that subsystem, which is a different answer from a device that has
14//! never been heard from at all.
15
16use std::ffi::c_char;
17use std::net::Ipv4Addr;
18
19use mx_remote::{
20    AmpDolbySettings, AudioEndpoint, DeviceV2ipDetails, DeviceV2ipSink, FirmwareVersion,
21    MultiviewerStatus, NetworkPortStatus, PduState, RcSettings, StreamKind, TopologyEntry,
22    UtpCableStatus, V2ipDeviceStats, V2ipRxStats, V2ipStreamSource, V2ipStreamSources,
23    V2ipTilingConfig, V2ipTxStats, VctStatus, MULTIVIEWER_INPUTS,
24};
25
26use crate::abi::{fail, guard, mxr_result_t, mxr_uid_t, put_str};
27use crate::control::mxr_audio_format_t;
28use crate::info::{copy_into, not_heard_from, null_out, MXR_NAME_LEN, MXR_VERSION_LEN};
29use crate::remote::{mxr_remote_t, with, MXR_IP_STRING_LEN};
30
31/// How many inputs a multiviewer has.
32///
33/// Written as a literal because the generated header needs one, and checked
34/// against the core crate's value below so the two cannot drift apart.
35pub const MXR_MULTIVIEWER_INPUTS: usize = 4;
36
37const _: () = assert!(MXR_MULTIVIEWER_INPUTS == MULTIVIEWER_INPUTS);
38
39/// How many pairs a UTP cable diagnostic covers.
40pub const MXR_UTP_PAIRS: usize = 4;
41
42/// How many outlets a PDU reports.
43pub const MXR_PDU_OUTLETS: usize = 8;
44
45/// Which of a V2IP device's streams an address describes.
46#[repr(i32)]
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum mxr_stream_kind_t {
49    /// The video stream.
50    MXR_STREAM_VIDEO = 0,
51    /// The audio stream.
52    MXR_STREAM_AUDIO = 1,
53    /// The ancillary-data stream.
54    MXR_STREAM_ANC = 2,
55    /// The audio-return stream.
56    MXR_STREAM_ARC = 3,
57}
58
59impl From<StreamKind> for mxr_stream_kind_t {
60    fn from(kind: StreamKind) -> Self {
61        match kind {
62            StreamKind::Video => Self::MXR_STREAM_VIDEO,
63            StreamKind::Audio => Self::MXR_STREAM_AUDIO,
64            StreamKind::Anc => Self::MXR_STREAM_ANC,
65            StreamKind::Arc => Self::MXR_STREAM_ARC,
66        }
67    }
68}
69
70/// One multicast stream address.
71#[repr(C)]
72#[derive(Clone, Copy)]
73pub struct mxr_stream_source_t {
74    /// Which stream this address is for.
75    pub kind: mxr_stream_kind_t,
76    /// The multicast group, as a dotted quad.
77    pub ip: [c_char; MXR_IP_STRING_LEN],
78    /// The destination UDP port.
79    pub port: u16,
80    /// Whether this carries a usable address: a multicast group and a non-zero
81    /// port, both. A slot a device has not filled in is not an error, so this
82    /// is what separates an address from an empty slot.
83    pub valid: bool,
84}
85
86impl From<V2ipStreamSource> for mxr_stream_source_t {
87    fn from(s: V2ipStreamSource) -> Self {
88        let mut out = Self {
89            kind: s.kind.into(),
90            ip: [0; MXR_IP_STRING_LEN],
91            port: s.port,
92            valid: s.is_valid(),
93        };
94        put_str(&mut out.ip, &s.ip.to_string());
95        out
96    }
97}
98
99/// The streams one V2IP source advertises.
100#[repr(C)]
101#[derive(Clone, Copy)]
102pub struct mxr_stream_sources_t {
103    /// The originating device, zero when it is not known.
104    pub uid: mxr_uid_t,
105    /// The video stream.
106    pub video: mxr_stream_source_t,
107    /// The audio stream.
108    pub audio: mxr_stream_source_t,
109    /// The ancillary-data stream.
110    pub anc: mxr_stream_source_t,
111    /// Whether an audio-return stream is advertised.
112    pub has_arc: bool,
113    /// The audio-return stream, meaningful only when `has_arc` is set.
114    pub arc: mxr_stream_source_t,
115}
116
117impl From<V2ipStreamSources> for mxr_stream_sources_t {
118    fn from(s: V2ipStreamSources) -> Self {
119        Self {
120            uid: s.uid.into(),
121            video: s.video.into(),
122            audio: s.audio.into(),
123            anc: s.anc.into(),
124            has_arc: s.arc.is_some(),
125            arc: s.arc.unwrap_or_default().into(),
126        }
127    }
128}
129
130/// A V2IP device's own encoder configuration.
131#[repr(C)]
132#[derive(Clone, Copy)]
133pub struct mxr_v2ip_details_t {
134    /// The video stream this device sources.
135    pub video: mxr_stream_source_t,
136    /// The audio stream this device sources.
137    pub audio: mxr_stream_source_t,
138    /// The ancillary-data stream this device sources.
139    pub anc: mxr_stream_source_t,
140    /// The audio-return stream this device sources.
141    pub arc: mxr_stream_source_t,
142    /// Encoder rate in units of 10Mb/s, or -1 when no rate has been reported.
143    pub tx_rate: i16,
144    /// DSCP marking for the video stream, or -1 when unmarked.
145    pub dscp_video: i16,
146    /// DSCP marking for the audio stream, or -1 when unmarked.
147    pub dscp_audio: i16,
148    /// DSCP marking for the ancillary-data stream, or -1 when unmarked.
149    pub dscp_anc: i16,
150    /// The signal type the output scales to.
151    pub scaling_mode: u16,
152    /// Refresh rate in Hz.
153    pub scaling_refresh: u16,
154    /// `MXR_SCALING_FLAG_*` bits. Bits outside those are undefined and are not
155    /// reliably zero: firmware predating the fix builds this frame over an
156    /// uninitialised stack local.
157    pub scaling_flags: u8,
158}
159
160/// Set when the frame carries a scaling mode and refresh rate.
161pub const MXR_SCALING_FLAG_MODE_VALID: u8 = 1 << 0;
162/// Set when the frame carries the scaling options.
163pub const MXR_SCALING_FLAG_OPTIONS_VALID: u8 = 1 << 1;
164/// Set when the output scales automatically.
165pub const MXR_SCALING_FLAG_AUTO_SCALING: u8 = 1 << 7;
166
167/// The streams a V2IP sink is subscribed to.
168#[repr(C)]
169#[derive(Clone, Copy)]
170pub struct mxr_v2ip_sink_t {
171    /// The streams the sink subscribes to.
172    pub addresses: mxr_stream_sources_t,
173    /// Whether the sender reported a resolved audio format.
174    pub has_audio_format: bool,
175    /// The audio format, meaningful only when `has_audio_format` is set.
176    pub audio_format: mxr_audio_format_t,
177}
178
179/// Transmitter stream statistics.
180#[repr(C)]
181#[derive(Clone, Copy)]
182pub struct mxr_v2ip_tx_stats_t {
183    /// Video packets sent.
184    pub video: u32,
185    /// Audio packets sent.
186    pub audio: u32,
187    /// Ancillary-data packets sent.
188    pub anc: u32,
189    /// Times the stream went down.
190    pub stream_down: u32,
191    /// Transmit overflows.
192    pub overflow: u32,
193}
194
195impl From<V2ipTxStats> for mxr_v2ip_tx_stats_t {
196    fn from(s: V2ipTxStats) -> Self {
197        Self {
198            video: s.video,
199            audio: s.audio,
200            anc: s.anc,
201            stream_down: s.stream_down,
202            overflow: s.overflow,
203        }
204    }
205}
206
207/// Receiver stream statistics.
208#[repr(C)]
209#[derive(Clone, Copy)]
210pub struct mxr_v2ip_rx_stats_t {
211    /// Video packets received.
212    pub video_total: u32,
213    /// Video packets dropped.
214    pub video_dropped: u32,
215    /// Video sequence errors.
216    pub video_seq_errors: u32,
217    /// Watchdog timeouts.
218    pub wdt_timeout: u32,
219    /// Audio packets received.
220    pub audio_total: u32,
221    /// Audio packets dropped.
222    pub audio_dropped: u32,
223    /// Audio sequence errors.
224    pub audio_seq_errors: u32,
225    /// Ancillary-data packets received.
226    pub anc_total: u32,
227    /// Ancillary-data packets dropped.
228    pub anc_dropped: u32,
229    /// Ancillary-data sequence errors.
230    pub anc_seq_errors: u32,
231    /// The decoder's health state: 0 unknown, 1 healthy, 2 bad, 3 starting.
232    ///
233    /// Only healthy and bad are verdicts. Reading failure as "not healthy"
234    /// counts a decoder that is merely coming up as one that failed, which is
235    /// what every sink reports for a moment after a route change.
236    pub decoder_state: u8,
237}
238
239impl From<V2ipRxStats> for mxr_v2ip_rx_stats_t {
240    fn from(s: V2ipRxStats) -> Self {
241        Self {
242            video_total: s.video_total,
243            video_dropped: s.video_dropped,
244            video_seq_errors: s.video_seq_errors,
245            wdt_timeout: s.wdt_timeout,
246            audio_total: s.audio_total,
247            audio_dropped: s.audio_dropped,
248            audio_seq_errors: s.audio_seq_errors,
249            anc_total: s.anc_total,
250            anc_dropped: s.anc_dropped,
251            anc_seq_errors: s.anc_seq_errors,
252            decoder_state: s.decoder_state.to_wire(),
253        }
254    }
255}
256
257/// A device's V2IP statistics, cumulative and over the last minute.
258#[repr(C)]
259#[derive(Clone, Copy)]
260pub struct mxr_v2ip_stats_t {
261    /// Transmit totals since boot.
262    pub tx: mxr_v2ip_tx_stats_t,
263    /// Transmit counts over the last minute.
264    pub tx_per_minute: mxr_v2ip_tx_stats_t,
265    /// Receive totals since boot.
266    pub rx: mxr_v2ip_rx_stats_t,
267    /// Receive counts over the last minute.
268    pub rx_per_minute: mxr_v2ip_rx_stats_t,
269}
270
271/// The window a sink is currently told to show.
272///
273/// This is the pollable view of a sink's window, not the persisted video wall
274/// setting: on a sink running the wall module a write here is transient,
275/// because that module pushes its own target window back within about a
276/// second.
277#[repr(C)]
278#[derive(Clone, Copy)]
279pub struct mxr_tiling_config_t {
280    /// The sink this window belongs to.
281    pub target: mxr_uid_t,
282    /// Window origin, horizontal.
283    pub pos_x: u16,
284    /// Window origin, vertical.
285    pub pos_y: u16,
286    /// Window width.
287    pub width: u16,
288    /// Window height.
289    pub height: u16,
290}
291
292/// What a multiviewer reports about itself.
293#[repr(C)]
294#[derive(Clone, Copy)]
295pub struct mxr_multiviewer_status_t {
296    /// The multiviewer.
297    pub uid: mxr_uid_t,
298    /// The source device mapped to each input.
299    pub mappings: [mxr_uid_t; MXR_MULTIVIEWER_INPUTS],
300    /// The MCU firmware version.
301    pub mcu_version: [c_char; MXR_NAME_LEN],
302    /// The scaler firmware version.
303    pub scaler_version: [c_char; MXR_NAME_LEN],
304    /// The view mode the hardware reports, which is its own numbering rather
305    /// than `view_mode`'s.
306    pub hw_view_mode: u8,
307    /// The window layout.
308    pub view_mode: u8,
309    /// Which corner the picture-in-picture window sits in.
310    pub pip_position: u8,
311    /// The size of the picture-in-picture window.
312    pub pip_size: u8,
313    /// The output resolution.
314    pub output_mode: u8,
315    /// The HDCP mode.
316    pub hdcp_mode: u8,
317    /// The IT content flag.
318    pub output_itc: u8,
319    /// The EDID presented to sources.
320    pub edid_template: u8,
321    /// How a source is fitted into its window.
322    pub aspect_ratio: u8,
323    /// Whether automatic source switching is on.
324    pub auto_switch: u8,
325    /// Which window the audio is taken from.
326    pub audio_source: u8,
327    /// Whether a volume has been reported.
328    pub has_audio_volume: bool,
329    /// The output volume.
330    pub audio_volume: u8,
331    /// Whether the output is muted.
332    pub audio_muted: u8,
333    /// The source shown in each window.
334    pub video_sources: [u8; MXR_MULTIVIEWER_INPUTS],
335    /// Which window remote control is forwarded to.
336    pub remote_control: u8,
337}
338
339/// One node of a device's audio tree.
340#[repr(C)]
341#[derive(Clone, Copy)]
342pub struct mxr_audio_endpoint_t {
343    /// The endpoint's identifier on its device.
344    pub id: u8,
345    /// What the endpoint can do, as `MXR_AUDIO_*` bits.
346    pub features: u32,
347    /// Whether the endpoint carries a stream address.
348    pub has_address: bool,
349    /// The stream address, meaningful only when `has_address` is set.
350    pub address: mxr_stream_source_t,
351    /// The endpoint this one hangs off, or -1 at a root.
352    pub parent: i16,
353    /// How many children this endpoint has; read them with
354    /// `mxr_audio_endpoint_children()`.
355    pub child_count: usize,
356    /// Whether the device reported which inputs are selectable.
357    pub has_inputs_available: bool,
358    /// Bitmask of the endpoints this one may be switched to.
359    pub inputs_available: u32,
360    /// Whether the device reported which input is selected.
361    pub has_inputs_routed: bool,
362    /// Bitmask of the endpoint this one is listening to.
363    pub inputs_routed: u32,
364    /// The device at the other end of the link, zero when unlinked.
365    pub linked_device: mxr_uid_t,
366    /// The endpoint at the other end of the link, or -1 when unlinked.
367    pub linked_endpoint: i16,
368}
369
370impl From<&AudioEndpoint> for mxr_audio_endpoint_t {
371    fn from(e: &AudioEndpoint) -> Self {
372        Self {
373            id: e.id,
374            features: e.features.bits(),
375            has_address: e.address.is_some(),
376            address: e.address.unwrap_or_default().into(),
377            // An endpoint id is a byte on the wire, so -1 cannot collide.
378            parent: e.parent.map_or(-1, i16::from),
379            child_count: e.children.len(),
380            has_inputs_available: e.inputs_available.is_some(),
381            inputs_available: e.inputs_available.unwrap_or(0),
382            has_inputs_routed: e.inputs_routed.is_some(),
383            inputs_routed: e.inputs_routed.unwrap_or(0),
384            linked_device: e.linked_device.into(),
385            linked_endpoint: e.linked_endpoint.map_or(-1, i16::from),
386        }
387    }
388}
389
390/// The diagnostic result for one UTP cable pair.
391#[repr(C)]
392#[derive(Clone, Copy)]
393pub struct mxr_cable_status_t {
394    /// Whether the pair is wired with normal polarity.
395    pub polarity: bool,
396    /// Which pair this describes.
397    pub pair: u8,
398    /// Measured skew.
399    pub skew: u32,
400    /// Measured length.
401    pub length: u32,
402}
403
404impl From<UtpCableStatus> for mxr_cable_status_t {
405    fn from(c: UtpCableStatus) -> Self {
406        Self {
407            polarity: c.polarity,
408            pair: c.pair,
409            skew: c.skew,
410            length: c.length,
411        }
412    }
413}
414
415/// The link state and diagnostics of one network port.
416#[repr(C)]
417#[derive(Clone, Copy)]
418pub struct mxr_network_port_t {
419    /// Port number.
420    pub port: u16,
421    /// Port name.
422    pub name: [c_char; MXR_NAME_LEN],
423    /// Negotiated link speed.
424    pub link_speed: u8,
425    /// Whether the link negotiated full duplex.
426    pub link_full_duplex: bool,
427    /// The port's own address, empty when it has not reported one.
428    pub ip: [c_char; MXR_IP_STRING_LEN],
429    /// The IGMP querier the port sees, empty when it sees none.
430    pub querier: [c_char; MXR_IP_STRING_LEN],
431    /// Whether the port reported a hardware address.
432    pub has_mac_address: bool,
433    /// The hardware address, meaningful only when `has_mac_address` is set.
434    pub mac_address: [u8; 6],
435    /// Whether the port reported link errors.
436    pub has_errors: bool,
437    /// Input errors.
438    pub in_error: bool,
439    /// Input frame check errors.
440    pub in_fcs_error: bool,
441    /// Input collisions.
442    pub in_collision: bool,
443    /// Deferred transmissions.
444    pub out_deferred: bool,
445    /// Excessive transmissions.
446    pub out_excessive: bool,
447    /// Polarity errors.
448    pub polarity_error: bool,
449    /// Skew warning.
450    pub skew_warning: bool,
451    /// Length warning.
452    pub length_warning: bool,
453    /// Whether the port reported a virtual cable test.
454    pub has_vct_status: bool,
455    /// Whether each pair raised a warning, meaningful only when
456    /// `has_vct_status` is set.
457    pub vct_warning: [bool; MXR_UTP_PAIRS],
458    /// How many entries of `cable_status` the port filled in.
459    pub cable_status_count: usize,
460    /// Cable diagnostics per pair.
461    pub cable_status: [mxr_cable_status_t; MXR_UTP_PAIRS],
462}
463
464/// One device in a topology report.
465#[repr(C)]
466#[derive(Clone, Copy)]
467pub struct mxr_topology_entry_t {
468    /// The device this entry describes.
469    pub uid: mxr_uid_t,
470    /// Bitmask of the devices it is connected to.
471    pub mask: u32,
472}
473
474impl From<TopologyEntry> for mxr_topology_entry_t {
475    fn from(e: TopologyEntry) -> Self {
476        Self {
477            uid: e.uid.into(),
478            mask: e.mask,
479        }
480    }
481}
482
483/// One firmware component a device reports.
484#[repr(C)]
485#[derive(Clone, Copy)]
486pub struct mxr_firmware_version_t {
487    /// Which component this describes.
488    pub firmware_type: u8,
489    /// Build timestamp, in seconds since the Unix epoch.
490    pub timestamp: u32,
491    /// Source revision hash.
492    pub hash: u32,
493    /// Human-readable version string.
494    pub version: [c_char; MXR_VERSION_LEN],
495}
496
497/// A ProAmp8's Dolby settings.
498#[repr(C)]
499#[derive(Clone, Copy)]
500pub struct mxr_dolby_settings_t {
501    /// 0 = standard, 1 = 3-zone Dolby, 2 = 4-zone Dolby.
502    pub mode: u8,
503    /// Whether PCM is up-mixed to 5.1 rather than passed through.
504    pub pcm_upmix: bool,
505    /// Whether a Dolby stream was detected.
506    pub dolby_detected: bool,
507    /// Whether up-mixing is currently running.
508    pub pcm_upmix_active: bool,
509}
510
511impl From<AmpDolbySettings> for mxr_dolby_settings_t {
512    fn from(s: AmpDolbySettings) -> Self {
513        Self {
514            mode: s.mode,
515            pcm_upmix: s.pcm_upmix,
516            dolby_detected: s.dolby_detected,
517            pcm_upmix_active: s.pcm_upmix_active,
518        }
519    }
520}
521
522/// The electrical state a PDU reports.
523#[repr(C)]
524#[derive(Clone, Copy)]
525pub struct mxr_pdu_state_t {
526    /// Current in amperes.
527    pub current: f64,
528    /// Voltage in volts.
529    pub voltage: f64,
530    /// Real power in watts.
531    pub power: f64,
532    /// Dissipation in watts.
533    pub dissipation: f64,
534    /// Mains frequency in Hz.
535    pub frequency: f64,
536    /// Per-outlet state.
537    pub outlets: [u8; MXR_PDU_OUTLETS],
538}
539
540impl From<PduState> for mxr_pdu_state_t {
541    fn from(s: PduState) -> Self {
542        Self {
543            current: s.current,
544            voltage: s.voltage,
545            power: s.power,
546            dissipation: s.dissipation,
547            frequency: s.frequency,
548            outlets: s.outlets,
549        }
550    }
551}
552
553/// The remote-control configuration of a source bay.
554#[repr(C)]
555#[derive(Clone, Copy)]
556pub struct mxr_rc_settings_t {
557    /// The device this configuration belongs to.
558    pub target: mxr_uid_t,
559    /// The control method, as the wire value.
560    ///
561    /// Zero is infrared, a method a bay really uses, so it is not a stand-in
562    /// for "not reported". Check that `mxr_rc_settings()` returned `MXR_OK`
563    /// before reading this: a device that has not sent its settings yet
564    /// leaves the struct as the caller allocated it, and a zeroed one then
565    /// reads as a bay set to infrared. `mxr_bay_info_t` answers the same
566    /// question with a `has_rc_type` flag beside its `rc_type`.
567    pub rc_target: u8,
568    /// The control target's address, empty when unset.
569    pub ip: [c_char; MXR_IP_STRING_LEN],
570    /// Whether CEC is enabled.
571    pub cec_enabled: bool,
572    /// Whether CEC powers the sink on automatically.
573    pub cec_auto_on: bool,
574    /// Whether remote-control commands are forwarded.
575    pub forward_rc: bool,
576    /// Whether infrared is forwarded.
577    pub forward_ir: bool,
578    /// The driver state on the source, as the wire value. One above the last
579    /// this library knows is passed through as it arrived.
580    pub rc_status: u8,
581    /// The driver-reported status string, empty when unknown.
582    pub status_name: [c_char; MXR_NAME_LEN],
583}
584
585/// Writes an address into a fixed-width field, leaving it empty when there is
586/// none.
587fn put_ip(dst: &mut [c_char], ip: Option<Ipv4Addr>) {
588    put_str(dst, &ip.map(|ip| ip.to_string()).unwrap_or_default());
589}
590
591/// Declares a getter for one subsystem of a device.
592///
593/// Each has the same three answers - no such device, the device has not sent
594/// this, here it is - and writing them out once keeps a getter that answers
595/// differently visible as one.
596/// Writes a subsystem reading through `out`, or reports why there is none.
597///
598/// # Safety
599///
600/// `out` is null or points at a writable `T`.
601unsafe fn fill<T>(
602    r: &mxr_remote_t,
603    uid: mxr_uid_t,
604    out: *mut T,
605    what: &str,
606    value: Option<T>,
607) -> mxr_result_t {
608    if out.is_null() {
609        return null_out(what);
610    }
611    match value {
612        Some(value) => {
613            // SAFETY: the caller guarantees a writable T, and it is not null.
614            unsafe { *out = value };
615            mxr_result_t::MXR_OK
616        }
617        None => not_reported(r, uid, what),
618    }
619}
620
621/// Reports why a subsystem read found nothing: no such device, or a device
622/// that has not sent this.
623fn not_reported(r: &mxr_remote_t, uid: mxr_uid_t, what: &str) -> mxr_result_t {
624    if r.remote.device(uid.into()).is_none() {
625        return not_heard_from(uid);
626    }
627    fail(
628        mxr_result_t::MXR_ERR_NOT_REPORTED,
629        &format!("the device has reported no {what}"),
630    )
631}
632
633/// Fills `out` with a device's V2IP statistics.
634///
635/// A device sends these only while subscribed; see
636/// `mxr_subscribe_v2ip_stats()`.
637///
638/// # Safety
639///
640/// `remote` is null or a live handle, and `out` points at a writable
641/// [`mxr_v2ip_stats_t`].
642#[no_mangle]
643pub unsafe extern "C" fn mxr_v2ip_stats(
644    remote: *const mxr_remote_t,
645    uid: mxr_uid_t,
646    out: *mut mxr_v2ip_stats_t,
647) -> mxr_result_t {
648    // SAFETY: the caller guarantees a live handle or null.
649    let handle = unsafe { remote.as_ref() };
650    with(handle, |r| {
651        let value = r
652            .remote
653            .v2ip_stats(uid.into())
654            .map(|s: V2ipDeviceStats| mxr_v2ip_stats_t {
655                tx: s.tx.into(),
656                tx_per_minute: s.tx_per_minute.into(),
657                rx: s.rx.into(),
658                rx_per_minute: s.rx_per_minute.into(),
659            });
660        // SAFETY: the caller guarantees a writable mxr_v2ip_stats_t or null.
661        unsafe { fill(r, uid, out, "V2IP statistics", value) }
662    })
663}
664
665/// Fills `out` with a V2IP device's own encoder configuration.
666///
667/// # Safety
668///
669/// `remote` is null or a live handle, and `out` points at a writable
670/// [`mxr_v2ip_details_t`].
671#[no_mangle]
672pub unsafe extern "C" fn mxr_v2ip_details(
673    remote: *const mxr_remote_t,
674    uid: mxr_uid_t,
675    out: *mut mxr_v2ip_details_t,
676) -> mxr_result_t {
677    // SAFETY: the caller guarantees a live handle or null.
678    let handle = unsafe { remote.as_ref() };
679    with(handle, |r| {
680        let value = r
681            .remote
682            .v2ip_details(uid.into())
683            .map(|d: DeviceV2ipDetails| mxr_v2ip_details_t {
684                video: d.video.into(),
685                audio: d.audio.into(),
686                anc: d.anc.into(),
687                arc: d.arc.into(),
688                // A rate and a marking are both bytes on the wire, so -1 cannot
689                // collide with a value a device could report.
690                tx_rate: d.tx_rate.map_or(-1, i16::from),
691                dscp_video: d.dscp.video.map_or(-1, i16::from),
692                dscp_audio: d.dscp.audio.map_or(-1, i16::from),
693                dscp_anc: d.dscp.anc.map_or(-1, i16::from),
694                scaling_mode: d.scaling.mode.to_wire(),
695                scaling_refresh: d.scaling.refresh,
696                scaling_flags: d.scaling.flags,
697            });
698        // SAFETY: the caller guarantees a writable mxr_v2ip_details_t or null.
699        unsafe { fill(r, uid, out, "V2IP encoder configuration", value) }
700    })
701}
702
703/// Fills `out` with the streams a V2IP sink is subscribed to.
704///
705/// # Safety
706///
707/// `remote` is null or a live handle, and `out` points at a writable
708/// [`mxr_v2ip_sink_t`].
709#[no_mangle]
710pub unsafe extern "C" fn mxr_v2ip_sink(
711    remote: *const mxr_remote_t,
712    uid: mxr_uid_t,
713    out: *mut mxr_v2ip_sink_t,
714) -> mxr_result_t {
715    // SAFETY: the caller guarantees a live handle or null.
716    let handle = unsafe { remote.as_ref() };
717    with(handle, |r| {
718        let value = r
719            .remote
720            .v2ip_sink(uid.into())
721            .map(|s: DeviceV2ipSink| mxr_v2ip_sink_t {
722                addresses: s.addresses.into(),
723                has_audio_format: s.audio_fmt.is_some(),
724                audio_format: {
725                    let f = s.audio_fmt.unwrap_or_default();
726                    mxr_audio_format_t {
727                        sample_rate: f.sample_rate,
728                        channels: f.channels,
729                    }
730                },
731            });
732        // SAFETY: the caller guarantees a writable mxr_v2ip_sink_t or null.
733        unsafe { fill(r, uid, out, "V2IP sink route", value) }
734    })
735}
736
737/// Fills `out` with the window a sink is told to show.
738///
739/// # Safety
740///
741/// `remote` is null or a live handle, and `out` points at a writable
742/// [`mxr_tiling_config_t`].
743#[no_mangle]
744pub unsafe extern "C" fn mxr_v2ip_tiling(
745    remote: *const mxr_remote_t,
746    uid: mxr_uid_t,
747    out: *mut mxr_tiling_config_t,
748) -> mxr_result_t {
749    // SAFETY: the caller guarantees a live handle or null.
750    let handle = unsafe { remote.as_ref() };
751    with(handle, |r| {
752        let value =
753            r.remote
754                .v2ip_tiling(uid.into())
755                .map(|t: V2ipTilingConfig| mxr_tiling_config_t {
756                    target: t.target.into(),
757                    pos_x: t.pos_x,
758                    pos_y: t.pos_y,
759                    width: t.width,
760                    height: t.height,
761                });
762        // SAFETY: the caller guarantees a writable mxr_tiling_config_t or null.
763        unsafe { fill(r, uid, out, "window", value) }
764    })
765}
766
767/// Fills `out` with what a multiviewer reports about itself.
768///
769/// # Safety
770///
771/// `remote` is null or a live handle, and `out` points at a writable
772/// [`mxr_multiviewer_status_t`].
773#[no_mangle]
774pub unsafe extern "C" fn mxr_multiviewer_status(
775    remote: *const mxr_remote_t,
776    uid: mxr_uid_t,
777    out: *mut mxr_multiviewer_status_t,
778) -> mxr_result_t {
779    // SAFETY: the caller guarantees a live handle or null.
780    let handle = unsafe { remote.as_ref() };
781    with(handle, |r| {
782        let value = r.remote.multiviewer_status(uid.into()).map(multiviewer_of);
783        // SAFETY: the caller guarantees a writable mxr_multiviewer_status_t or null.
784        unsafe { fill(r, uid, out, "multiviewer status", value) }
785    })
786}
787
788/// Copies a multiviewer's report into the C shape.
789fn multiviewer_of(s: MultiviewerStatus) -> mxr_multiviewer_status_t {
790    let mut out = mxr_multiviewer_status_t {
791        uid: s.uid.into(),
792        mappings: [mxr_uid_t::default(); MXR_MULTIVIEWER_INPUTS],
793        mcu_version: [0; MXR_NAME_LEN],
794        scaler_version: [0; MXR_NAME_LEN],
795        hw_view_mode: s.hw_view_mode,
796        view_mode: s.view_mode.to_wire(),
797        pip_position: s.pip_position.to_wire(),
798        pip_size: s.pip_size.to_wire(),
799        output_mode: s.output_mode.to_wire(),
800        hdcp_mode: s.hdcp_mode.to_wire(),
801        output_itc: s.output_itc.to_wire(),
802        edid_template: s.edid_template.to_wire(),
803        aspect_ratio: s.aspect_ratio.to_wire(),
804        auto_switch: s.auto_switch.to_wire(),
805        audio_source: s.audio_source.to_wire(),
806        has_audio_volume: s.audio_volume.is_some(),
807        audio_volume: s.audio_volume.unwrap_or(0),
808        audio_muted: s.audio_muted.to_wire(),
809        video_sources: [0; MXR_MULTIVIEWER_INPUTS],
810        remote_control: s.remote_control.to_wire(),
811    };
812    for (slot, uid) in out.mappings.iter_mut().zip(s.mappings) {
813        *slot = uid.into();
814    }
815    for (slot, source) in out.video_sources.iter_mut().zip(s.video_sources) {
816        *slot = source.to_wire();
817    }
818    put_str(&mut out.mcu_version, &s.mcu_version);
819    put_str(&mut out.scaler_version, &s.scaler_version);
820    out
821}
822
823/// Fills `out` with a ProAmp8's Dolby settings.
824///
825/// # Safety
826///
827/// `remote` is null or a live handle, and `out` points at a writable
828/// [`mxr_dolby_settings_t`].
829#[no_mangle]
830pub unsafe extern "C" fn mxr_dolby_settings(
831    remote: *const mxr_remote_t,
832    uid: mxr_uid_t,
833    out: *mut mxr_dolby_settings_t,
834) -> mxr_result_t {
835    // SAFETY: the caller guarantees a live handle or null.
836    let handle = unsafe { remote.as_ref() };
837    with(handle, |r| {
838        let value = r
839            .remote
840            .dolby_settings(uid.into())
841            .map(mxr_dolby_settings_t::from);
842        // SAFETY: the caller guarantees a writable mxr_dolby_settings_t or null.
843        unsafe { fill(r, uid, out, "Dolby settings", value) }
844    })
845}
846
847/// Fills `out` with the electrical state a PDU reports.
848///
849/// # Safety
850///
851/// `remote` is null or a live handle, and `out` points at a writable
852/// [`mxr_pdu_state_t`].
853#[no_mangle]
854pub unsafe extern "C" fn mxr_pdu_state(
855    remote: *const mxr_remote_t,
856    uid: mxr_uid_t,
857    out: *mut mxr_pdu_state_t,
858) -> mxr_result_t {
859    // SAFETY: the caller guarantees a live handle or null.
860    let handle = unsafe { remote.as_ref() };
861    with(handle, |r| {
862        let value = r.remote.pdu_state(uid.into()).map(mxr_pdu_state_t::from);
863        // SAFETY: the caller guarantees a writable mxr_pdu_state_t or null.
864        unsafe { fill(r, uid, out, "PDU state", value) }
865    })
866}
867
868/// Fills `out` with a source bay's remote-control configuration.
869///
870/// # Safety
871///
872/// `remote` is null or a live handle, and `out` points at a writable
873/// [`mxr_rc_settings_t`].
874#[no_mangle]
875pub unsafe extern "C" fn mxr_rc_settings(
876    remote: *const mxr_remote_t,
877    uid: mxr_uid_t,
878    out: *mut mxr_rc_settings_t,
879) -> mxr_result_t {
880    // SAFETY: the caller guarantees a live handle or null.
881    let handle = unsafe { remote.as_ref() };
882    with(handle, |r| {
883        let value = r.remote.rc_settings(uid.into()).map(rc_settings_of);
884        // SAFETY: the caller guarantees a writable mxr_rc_settings_t or null.
885        unsafe { fill(r, uid, out, "remote-control configuration", value) }
886    })
887}
888
889/// Copies a remote-control configuration into the C shape.
890fn rc_settings_of(s: RcSettings) -> mxr_rc_settings_t {
891    let mut out = mxr_rc_settings_t {
892        target: s.target.into(),
893        rc_target: s.rc_target,
894        ip: [0; MXR_IP_STRING_LEN],
895        cec_enabled: s.cec_enabled,
896        cec_auto_on: s.cec_auto_on,
897        forward_rc: s.forward_rc,
898        forward_ir: s.forward_ir,
899        rc_status: s.rc_status,
900        status_name: [0; MXR_NAME_LEN],
901    };
902    put_ip(&mut out.ip, s.ip);
903    put_str(&mut out.status_name, &s.status_name);
904    out
905}
906
907/// Writes the streams a device's source bays advertise, and returns how many
908/// there are.
909///
910/// Returns the full count even when it exceeds `cap`, so calling with `cap`
911/// zero sizes the buffer.
912///
913/// # Safety
914///
915/// `remote` is null or a live handle, and `out` is null or points at `cap`
916/// writable [`mxr_stream_sources_t`].
917#[no_mangle]
918pub unsafe extern "C" fn mxr_v2ip_sources(
919    remote: *const mxr_remote_t,
920    uid: mxr_uid_t,
921    out: *mut mxr_stream_sources_t,
922    cap: usize,
923) -> usize {
924    guard(0, || {
925        // SAFETY: the caller guarantees a live handle or null.
926        let Some(r) = (unsafe { remote.as_ref() }) else {
927            return no_handle();
928        };
929        let Some(sources) = r.remote.v2ip_sources(uid.into()) else {
930            not_reported(r, uid, "V2IP stream sources");
931            return 0;
932        };
933        // SAFETY: the caller guarantees cap writable elements at out.
934        unsafe { copy_into(&sources, out, cap) }
935    })
936}
937
938/// Writes a device's network ports, and returns how many there are.
939///
940/// # Safety
941///
942/// `remote` is null or a live handle, and `out` is null or points at `cap`
943/// writable [`mxr_network_port_t`].
944#[no_mangle]
945pub unsafe extern "C" fn mxr_network_status(
946    remote: *const mxr_remote_t,
947    uid: mxr_uid_t,
948    out: *mut mxr_network_port_t,
949    cap: usize,
950) -> usize {
951    guard(0, || {
952        // SAFETY: the caller guarantees a live handle or null.
953        let Some(r) = (unsafe { remote.as_ref() }) else {
954            return no_handle();
955        };
956        let ports: Vec<mxr_network_port_t> = r
957            .remote
958            .network_status(uid.into())
959            .iter()
960            .map(port_of)
961            .collect();
962        // SAFETY: the caller guarantees cap writable elements at out.
963        unsafe { copy_into(&ports, out, cap) }
964    })
965}
966
967/// Copies one port report into the C shape.
968fn port_of(p: &NetworkPortStatus) -> mxr_network_port_t {
969    let errors = p.errors.unwrap_or_default();
970    let mut out = mxr_network_port_t {
971        port: p.port,
972        name: [0; MXR_NAME_LEN],
973        link_speed: p.link_speed.to_wire(),
974        link_full_duplex: p.link_full_duplex,
975        ip: [0; MXR_IP_STRING_LEN],
976        querier: [0; MXR_IP_STRING_LEN],
977        has_mac_address: p.mac_address.is_some(),
978        mac_address: p.mac_address.unwrap_or_default().0,
979        has_errors: p.errors.is_some(),
980        in_error: errors.in_error,
981        in_fcs_error: errors.in_fcs_error,
982        in_collision: errors.in_collision,
983        out_deferred: errors.out_deferred,
984        out_excessive: errors.out_excessive,
985        polarity_error: errors.polarity_error,
986        skew_warning: errors.skew_warning,
987        length_warning: errors.length_warning,
988        has_vct_status: p.vct_status.is_some(),
989        vct_warning: [false; MXR_UTP_PAIRS],
990        cable_status_count: p.cable_status.len().min(MXR_UTP_PAIRS),
991        cable_status: [mxr_cable_status_t {
992            polarity: false,
993            pair: 0,
994            skew: 0,
995            length: 0,
996        }; MXR_UTP_PAIRS],
997    };
998    put_str(&mut out.name, &p.name);
999    put_ip(&mut out.ip, p.ip);
1000    put_ip(&mut out.querier, p.querier);
1001    if let Some(vct) = p.vct_status {
1002        for (slot, status) in out.vct_warning.iter_mut().zip(vct) {
1003            *slot = status == VctStatus::Warning;
1004        }
1005    }
1006    for (slot, cable) in out.cable_status.iter_mut().zip(&p.cable_status) {
1007        *slot = (*cable).into();
1008    }
1009    out
1010}
1011
1012/// Writes a device's view of the mesh topology, and returns how many entries
1013/// there are.
1014///
1015/// # Safety
1016///
1017/// `remote` is null or a live handle, and `out` is null or points at `cap`
1018/// writable [`mxr_topology_entry_t`].
1019#[no_mangle]
1020pub unsafe extern "C" fn mxr_topology(
1021    remote: *const mxr_remote_t,
1022    uid: mxr_uid_t,
1023    out: *mut mxr_topology_entry_t,
1024    cap: usize,
1025) -> usize {
1026    guard(0, || {
1027        // SAFETY: the caller guarantees a live handle or null.
1028        let Some(r) = (unsafe { remote.as_ref() }) else {
1029            return no_handle();
1030        };
1031        let topology = r.remote.topology(uid.into());
1032        // SAFETY: the caller guarantees cap writable elements at out.
1033        unsafe { copy_into(&topology, out, cap) }
1034    })
1035}
1036
1037/// Writes the firmware versions a device reports, and returns how many there
1038/// are.
1039///
1040/// # Safety
1041///
1042/// `remote` is null or a live handle, and `out` is null or points at `cap`
1043/// writable [`mxr_firmware_version_t`].
1044#[no_mangle]
1045pub unsafe extern "C" fn mxr_device_firmware(
1046    remote: *const mxr_remote_t,
1047    uid: mxr_uid_t,
1048    out: *mut mxr_firmware_version_t,
1049    cap: usize,
1050) -> usize {
1051    guard(0, || {
1052        // SAFETY: the caller guarantees a live handle or null.
1053        let Some(r) = (unsafe { remote.as_ref() }) else {
1054            return no_handle();
1055        };
1056        let versions: Vec<mxr_firmware_version_t> = r
1057            .remote
1058            .firmware(uid.into())
1059            .iter()
1060            .map(|(_, v)| firmware_of(v))
1061            .collect();
1062        // SAFETY: the caller guarantees cap writable elements at out.
1063        unsafe { copy_into(&versions, out, cap) }
1064    })
1065}
1066
1067/// Copies one firmware report into the C shape.
1068fn firmware_of(v: &FirmwareVersion) -> mxr_firmware_version_t {
1069    let mut out = mxr_firmware_version_t {
1070        firmware_type: v.firmware_type.to_wire(),
1071        timestamp: v.timestamp,
1072        hash: v.hash,
1073        version: [0; MXR_VERSION_LEN],
1074    };
1075    put_str(&mut out.version, &v.version);
1076    out
1077}
1078
1079/// Writes a device's audio endpoints, in the order it reported them, and
1080/// returns how many there are.
1081///
1082/// # Safety
1083///
1084/// `remote` is null or a live handle, and `out` is null or points at `cap`
1085/// writable [`mxr_audio_endpoint_t`].
1086#[no_mangle]
1087pub unsafe extern "C" fn mxr_audio_endpoints(
1088    remote: *const mxr_remote_t,
1089    uid: mxr_uid_t,
1090    out: *mut mxr_audio_endpoint_t,
1091    cap: usize,
1092) -> usize {
1093    guard(0, || {
1094        // SAFETY: the caller guarantees a live handle or null.
1095        let Some(r) = (unsafe { remote.as_ref() }) else {
1096            return no_handle();
1097        };
1098        let Some(endpoints) = r.remote.audio_endpoints(uid.into()) else {
1099            not_reported(r, uid, "audio endpoints");
1100            return 0;
1101        };
1102        let list: Vec<mxr_audio_endpoint_t> = endpoints.list().map(Into::into).collect();
1103        // SAFETY: the caller guarantees cap writable elements at out.
1104        unsafe { copy_into(&list, out, cap) }
1105    })
1106}
1107
1108/// Writes the endpoints hanging off one audio endpoint, and returns how many
1109/// there are.
1110///
1111/// # Safety
1112///
1113/// `remote` is null or a live handle, and `out` is null or points at `cap`
1114/// writable bytes.
1115#[no_mangle]
1116pub unsafe extern "C" fn mxr_audio_endpoint_children(
1117    remote: *const mxr_remote_t,
1118    uid: mxr_uid_t,
1119    endpoint: u8,
1120    out: *mut u8,
1121    cap: usize,
1122) -> usize {
1123    guard(0, || {
1124        // SAFETY: the caller guarantees a live handle or null.
1125        let Some(r) = (unsafe { remote.as_ref() }) else {
1126            return no_handle();
1127        };
1128        let children = match r.remote.audio_endpoints(uid.into()) {
1129            Some(endpoints) => match endpoints.get(endpoint) {
1130                Some(e) => e.children.clone(),
1131                None => {
1132                    fail(
1133                        mxr_result_t::MXR_ERR_NOT_FOUND,
1134                        &format!("the device has no audio endpoint {endpoint}"),
1135                    );
1136                    return 0;
1137                }
1138            },
1139            None => {
1140                not_reported(r, uid, "audio endpoints");
1141                return 0;
1142            }
1143        };
1144        // SAFETY: the caller guarantees cap writable bytes at out.
1145        unsafe { copy_into(&children, out, cap) }
1146    })
1147}
1148
1149/// Reports a null handle from a call whose answer is a count.
1150fn no_handle() -> usize {
1151    fail(
1152        mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1153        "the client handle is null",
1154    );
1155    0
1156}