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