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    V2ipDecoderDetail, 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/// 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///
166/// **Addresses that read as unset mean "no route, or the sink could not work
167/// one out" - never "definitely not subscribed".** This is the one part of a
168/// device configuration with no validity marker of its own, so a sender with
169/// nothing to say sends zeros and every receiver stores them. A sender leaves
170/// it empty whenever its own stream configuration does not resolve, which
171/// covers more than having no route: a selected source whose record has not
172/// arrived yet, the state after a restart at either end, missing audio bay
173/// configuration, or a stream failing its validity check.
174///
175/// Expect it rather than guard against it. Any scaling change makes the device
176/// rebuild and rebroadcast this block, and a write aimed at a remote bay sends
177/// it zeroed however it was requested - so the empty reading turns up most
178/// often during exactly the no-signal troubleshooting that prompted the change.
179/// A device's periodic report puts a real route back within a minute of it
180/// having one, so a reader that needs certainty should wait one out rather than
181/// treat the first empty reading as an answer.
182#[repr(C)]
183#[derive(Clone, Copy)]
184pub struct mxr_v2ip_sink_t {
185    /// The streams the sink subscribes to.
186    pub addresses: mxr_stream_sources_t,
187    /// Whether the sender reported a resolved audio format.
188    pub has_audio_format: bool,
189    /// The audio format, meaningful only when `has_audio_format` is set.
190    pub audio_format: mxr_audio_format_t,
191}
192
193/// Transmitter stream statistics.
194#[repr(C)]
195#[derive(Clone, Copy)]
196pub struct mxr_v2ip_tx_stats_t {
197    /// Video packets sent.
198    pub video: u32,
199    /// Audio packets sent.
200    pub audio: u32,
201    /// Ancillary-data packets sent.
202    pub anc: u32,
203    /// Times the stream went down.
204    pub stream_down: u32,
205    /// Transmit overflows.
206    pub overflow: u32,
207}
208
209impl From<V2ipTxStats> for mxr_v2ip_tx_stats_t {
210    fn from(s: V2ipTxStats) -> Self {
211        Self {
212            video: s.video,
213            audio: s.audio,
214            anc: s.anc,
215            stream_down: s.stream_down,
216            overflow: s.overflow,
217        }
218    }
219}
220
221/// Receiver stream statistics.
222#[repr(C)]
223#[derive(Clone, Copy)]
224pub struct mxr_v2ip_rx_stats_t {
225    /// Video packets received.
226    pub video_total: u32,
227    /// Video packets dropped.
228    pub video_dropped: u32,
229    /// Video sequence errors.
230    pub video_seq_errors: u32,
231    /// Watchdog timeouts.
232    pub wdt_timeout: u32,
233    /// Audio packets received.
234    pub audio_total: u32,
235    /// Audio packets dropped.
236    pub audio_dropped: u32,
237    /// Audio sequence errors.
238    pub audio_seq_errors: u32,
239    /// Ancillary-data packets received.
240    pub anc_total: u32,
241    /// Ancillary-data packets dropped.
242    pub anc_dropped: u32,
243    /// Ancillary-data sequence errors.
244    pub anc_seq_errors: u32,
245    /// The decoder's health state: 0 unknown, 1 healthy, 2 bad, 3 starting.
246    ///
247    /// Only healthy and bad are verdicts. Reading failure as "not healthy"
248    /// counts a decoder that is merely coming up as one that failed, which is
249    /// what every sink reports for a moment after a route change.
250    pub decoder_state: u8,
251}
252
253impl From<V2ipRxStats> for mxr_v2ip_rx_stats_t {
254    fn from(s: V2ipRxStats) -> Self {
255        Self {
256            video_total: s.video_total,
257            video_dropped: s.video_dropped,
258            video_seq_errors: s.video_seq_errors,
259            wdt_timeout: s.wdt_timeout,
260            audio_total: s.audio_total,
261            audio_dropped: s.audio_dropped,
262            audio_seq_errors: s.audio_seq_errors,
263            anc_total: s.anc_total,
264            anc_dropped: s.anc_dropped,
265            anc_seq_errors: s.anc_seq_errors,
266            decoder_state: s.decoder_state.to_wire(),
267        }
268    }
269}
270
271/// What a statistics report says about a sink's decoder.
272#[repr(i32)]
273#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum mxr_v2ip_decoder_detail_t {
275    /// The report carried no decoder block: the sender's firmware predates it.
276    MXR_V2IP_DECODER_ABSENT = 0,
277    /// The block is there and the decoder has never answered.
278    MXR_V2IP_DECODER_NEVER_ANSWERED = 1,
279    /// The block carries a reading.
280    MXR_V2IP_DECODER_ANSWERED = 2,
281}
282
283/// What a sink's decoder recovered from the codestream it is being given.
284///
285/// This is what the decoder understood, read ahead of the scaler: the geometry
286/// is unrounded and is not what the display is being sent. Every field but
287/// `detail` is zero unless `detail` is `MXR_V2IP_DECODER_ANSWERED`.
288///
289/// `detail` follows the sink being configured rather than the sink being
290/// enabled, so a sink that is switched off still reports - as reason 10, or
291/// from an older sender as reason 1, which is the same reading a sink whose
292/// source has died produces. Nothing here answers whether a sink is enabled.
293///
294/// Colour depth is absent on purpose and will stay absent: the video processor
295/// answers that from a driver constant rather than from the codestream, so
296/// there is no reading to carry. Assert depth at the encoder's input bay
297/// instead.
298#[repr(C)]
299#[derive(Clone, Copy)]
300pub struct mxr_v2ip_decoder_t {
301    /// Which of the three states this report is in.
302    pub detail: mxr_v2ip_decoder_detail_t,
303    /// The primary cause of the state the decoder is in, by the sender's own
304    /// names: 0 OK, 1 NO_PACKETS, 2 PACKETS_DEGRADED, 3 NO_FORMAT, 4
305    /// FORMAT_MISMATCH, 5 FORMAT_REJECTED, 6 DECODER_BLOCKED, 7
306    /// SWITCH_PENDING, 8 PTP_UNLOCKED, 9 TX_BRIDGE_UNLOCKED, 10 IDLE.
307    /// Firmware adds causes, so an unrecognised value is passed through as it
308    /// arrived.
309    ///
310    /// The primary cause only, and the numbers are identities rather than
311    /// ranks: several causes can be true at once and a fixed priority order in
312    /// the firmware decides which lands here. Classify on `flags`, which
313    /// carries all of them; a test against this field asks which cause won
314    /// instead. Reason 10 is the exception and is read here: it outranks the
315    /// whole word, and testing it first is what stops a switched-off sink
316    /// being reported as broken. Reason 9 is the one most often hidden here —
317    /// see `flags`.
318    ///
319    /// A pending switch is a step in an operation someone asked for rather
320    /// than a fault, and PTP unlocked costs audio alone: audio cannot enable
321    /// and the picture is unaffected, so reporting it as a fault puts an
322    /// overlay over a good picture. Reason 10 says the sink is switched off,
323    /// and carries no implication about `width` and `height`: those are read
324    /// before any cause is decided, so a switched-off sink still detecting a
325    /// codestream reports a real geometry. An older sender reports reason 1
326    /// for the same sink, so an absent reason 10 is not evidence a sink is
327    /// enabled - nothing here answers that, which comes from
328    /// `mxr_v2ip_details()` or the device's HTTP status.
329    pub reason: u8,
330    /// The converter watchdog is holding the stream back.
331    pub blocking: bool,
332    /// The recovered picture width, and 0 when none was recovered.
333    pub width: u16,
334    /// The recovered picture height, and 0 when none was recovered.
335    pub height: u16,
336    /// The recovered colour space: 0 RGB, 1 YCbCr 4:4:4, 2 YCbCr 4:2:2,
337    /// 3 YCbCr 4:2:0, 255 the decoder cannot name it.
338    ///
339    /// No value here means "no signal": a decoder with nothing to decode
340    /// reports 0, which is indistinguishable from a real RGB reading. A zero
341    /// `width` or `height` is what says the decoder recovered nothing - which
342    /// is not the same as the sink being switched off, and does not imply it. The 255
343    /// is its own value rather than the 0xF a signal report uses for an
344    /// unknown colour space.
345    pub format: u16,
346    /// How many readings the sink has stored. Monotonic, wrapping at 65535
347    /// after some 36 hours, and never reset.
348    ///
349    /// A sink reads its video processor every two seconds and reports every
350    /// second, so roughly every other report repeats a reading already seen:
351    /// a frame arriving says nothing about how fresh the values in it are.
352    /// This counter moves only when a reading is stored, so a processor that
353    /// stopped answering leaves it still rather than implying a refresh.
354    ///
355    /// After pointing a sink at something else, wait for this to advance by
356    /// two before trusting the geometry. It ticks when a reply lands rather
357    /// than when a query is sent, so the first tick can carry an answer the
358    /// processor read fractionally before the switch; the second cannot,
359    /// because at most one query is outstanding at a time.
360    pub updates: u16,
361    /// Every cause that applies, as bit N for reason N, where `reason` carries
362    /// the primary one. Bit 0 is cleared by the sender, so an empty word means
363    /// nothing beyond the primary cause applies.
364    ///
365    /// This is what to classify on, once reason 10 has been ruled out. That
366    /// cause outranks the whole word and leaves the bits below it set - a sink
367    /// switched off while running keeps the bits the decoder genuinely saw on
368    /// the way down - so a fault mask over `flags` reports a deliberately
369    /// disabled sink as broken.
370    ///
371    /// A cause that is true can be missing from
372    /// `reason` and present here: reason 9, the pipeline rebuilding after the
373    /// transmitter bridge stayed unlocked, sits below every input-side cause,
374    /// so a pipeline restarting in a loop shows an input-side cause in
375    /// `reason` and bit 9 here alone - always, rather than briefly. Bit 9
376    /// needs a sustained five seconds to appear at all, and sustained across
377    /// reports it means a restart loop rather than one event, because the
378    /// sender's debounce restarts each time it elapses.
379    ///
380    /// Reasons 3 and 4 are the two arms of one decision and never appear
381    /// together.
382    pub flags: u32,
383    /// How many times the converter watchdog has triggered.
384    pub blocked_count: u32,
385}
386
387impl From<V2ipDecoderDetail> for mxr_v2ip_decoder_t {
388    fn from(detail: V2ipDecoderDetail) -> Self {
389        let empty = Self {
390            detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ABSENT,
391            reason: 0,
392            blocking: false,
393            width: 0,
394            height: 0,
395            format: 0,
396            updates: 0,
397            flags: 0,
398            blocked_count: 0,
399        };
400        match detail {
401            V2ipDecoderDetail::Absent => empty,
402            V2ipDecoderDetail::NeverAnswered => Self {
403                detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_NEVER_ANSWERED,
404                ..empty
405            },
406            V2ipDecoderDetail::Answered(r) => Self {
407                detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ANSWERED,
408                reason: r.reason.to_wire(),
409                blocking: r.blocking,
410                width: r.width,
411                height: r.height,
412                format: r.format.to_wire(),
413                updates: r.updates,
414                flags: r.flags,
415                blocked_count: r.blocked_count,
416            },
417        }
418    }
419}
420
421/// A device's V2IP statistics, cumulative and over the last minute.
422#[repr(C)]
423#[derive(Clone, Copy)]
424pub struct mxr_v2ip_stats_t {
425    /// Transmit totals since boot.
426    pub tx: mxr_v2ip_tx_stats_t,
427    /// Transmit counts over the last minute.
428    pub tx_per_minute: mxr_v2ip_tx_stats_t,
429    /// Receive totals since boot.
430    pub rx: mxr_v2ip_rx_stats_t,
431    /// Receive counts over the last minute.
432    pub rx_per_minute: mxr_v2ip_rx_stats_t,
433    /// What the sink's decoder recovered from the codestream it is decoding.
434    pub decoder: mxr_v2ip_decoder_t,
435}
436
437/// The window a sink is currently told to show.
438///
439/// This is the pollable view of a sink's window, not the persisted video wall
440/// setting: on a sink running the wall module a write here is transient,
441/// because that module pushes its own target window back within about a
442/// second.
443#[repr(C)]
444#[derive(Clone, Copy)]
445pub struct mxr_tiling_config_t {
446    /// The sink this window belongs to.
447    pub target: mxr_uid_t,
448    /// Window origin, horizontal.
449    pub pos_x: u16,
450    /// Window origin, vertical.
451    pub pos_y: u16,
452    /// Window width.
453    pub width: u16,
454    /// Window height.
455    pub height: u16,
456}
457
458/// What a multiviewer reports about itself.
459#[repr(C)]
460#[derive(Clone, Copy)]
461pub struct mxr_multiviewer_status_t {
462    /// The multiviewer.
463    pub uid: mxr_uid_t,
464    /// The source device mapped to each input.
465    pub mappings: [mxr_uid_t; MXR_MULTIVIEWER_INPUTS],
466    /// The MCU firmware version.
467    pub mcu_version: [c_char; MXR_NAME_LEN],
468    /// The scaler firmware version.
469    pub scaler_version: [c_char; MXR_NAME_LEN],
470    /// The view mode the hardware reports, which is its own numbering rather
471    /// than `view_mode`'s.
472    pub hw_view_mode: u8,
473    /// The window layout.
474    pub view_mode: u8,
475    /// Which corner the picture-in-picture window sits in.
476    pub pip_position: u8,
477    /// The size of the picture-in-picture window.
478    pub pip_size: u8,
479    /// The output resolution.
480    pub output_mode: u8,
481    /// The HDCP mode.
482    pub hdcp_mode: u8,
483    /// The IT content flag.
484    pub output_itc: u8,
485    /// The EDID presented to sources.
486    pub edid_template: u8,
487    /// How a source is fitted into its window.
488    pub aspect_ratio: u8,
489    /// Whether automatic source switching is on.
490    pub auto_switch: u8,
491    /// Which window the audio is taken from.
492    pub audio_source: u8,
493    /// Whether a volume has been reported.
494    pub has_audio_volume: bool,
495    /// The output volume.
496    pub audio_volume: u8,
497    /// Whether the output is muted.
498    pub audio_muted: u8,
499    /// The source shown in each window.
500    pub video_sources: [u8; MXR_MULTIVIEWER_INPUTS],
501    /// Which window remote control is forwarded to.
502    pub remote_control: u8,
503}
504
505/// One node of a device's audio tree.
506#[repr(C)]
507#[derive(Clone, Copy)]
508pub struct mxr_audio_endpoint_t {
509    /// The endpoint's identifier on its device.
510    pub id: u8,
511    /// What the endpoint can do, as `MXR_AUDIO_*` bits.
512    pub features: u32,
513    /// Whether the endpoint carries a stream address.
514    pub has_address: bool,
515    /// The stream address, meaningful only when `has_address` is set.
516    pub address: mxr_stream_source_t,
517    /// The endpoint this one hangs off, or -1 at a root.
518    pub parent: i16,
519    /// How many children this endpoint has; read them with
520    /// `mxr_audio_endpoint_children()`.
521    pub child_count: usize,
522    /// Whether the device reported which inputs are selectable.
523    pub has_inputs_available: bool,
524    /// Bitmask of the endpoints this one may be switched to.
525    pub inputs_available: u32,
526    /// Whether the device reported which input is selected.
527    pub has_inputs_routed: bool,
528    /// Bitmask of the endpoint this one is listening to.
529    pub inputs_routed: u32,
530    /// The device at the other end of the link, zero when unlinked.
531    pub linked_device: mxr_uid_t,
532    /// The endpoint at the other end of the link, or -1 when unlinked.
533    pub linked_endpoint: i16,
534}
535
536impl From<&AudioEndpoint> for mxr_audio_endpoint_t {
537    fn from(e: &AudioEndpoint) -> Self {
538        Self {
539            id: e.id,
540            features: e.features.bits(),
541            has_address: e.address.is_some(),
542            address: e.address.unwrap_or_default().into(),
543            // An endpoint id is a byte on the wire, so -1 cannot collide.
544            parent: e.parent.map_or(-1, i16::from),
545            child_count: e.children.len(),
546            has_inputs_available: e.inputs_available.is_some(),
547            inputs_available: e.inputs_available.unwrap_or(0),
548            has_inputs_routed: e.inputs_routed.is_some(),
549            inputs_routed: e.inputs_routed.unwrap_or(0),
550            linked_device: e.linked_device.into(),
551            linked_endpoint: e.linked_endpoint.map_or(-1, i16::from),
552        }
553    }
554}
555
556/// The diagnostic result for one UTP cable pair.
557#[repr(C)]
558#[derive(Clone, Copy)]
559pub struct mxr_cable_status_t {
560    /// Whether the pair is wired with normal polarity.
561    pub polarity: bool,
562    /// Which pair this describes.
563    pub pair: u8,
564    /// Measured skew.
565    pub skew: u32,
566    /// Measured length.
567    pub length: u32,
568}
569
570impl From<UtpCableStatus> for mxr_cable_status_t {
571    fn from(c: UtpCableStatus) -> Self {
572        Self {
573            polarity: c.polarity,
574            pair: c.pair,
575            skew: c.skew,
576            length: c.length,
577        }
578    }
579}
580
581/// The link state and diagnostics of one network port.
582#[repr(C)]
583#[derive(Clone, Copy)]
584pub struct mxr_network_port_t {
585    /// Port number.
586    pub port: u16,
587    /// Port name.
588    pub name: [c_char; MXR_NAME_LEN],
589    /// Negotiated link speed.
590    pub link_speed: u8,
591    /// Whether the link negotiated full duplex.
592    pub link_full_duplex: bool,
593    /// The port's own address, empty when it has not reported one.
594    pub ip: [c_char; MXR_IP_STRING_LEN],
595    /// The IGMP querier the port sees, empty when it sees none.
596    pub querier: [c_char; MXR_IP_STRING_LEN],
597    /// Whether the port reported a hardware address.
598    pub has_mac_address: bool,
599    /// The hardware address, meaningful only when `has_mac_address` is set.
600    pub mac_address: [u8; 6],
601    /// Whether the port reported link errors.
602    pub has_errors: bool,
603    /// Input errors.
604    pub in_error: bool,
605    /// Input frame check errors.
606    pub in_fcs_error: bool,
607    /// Input collisions.
608    pub in_collision: bool,
609    /// Deferred transmissions.
610    pub out_deferred: bool,
611    /// Excessive transmissions.
612    pub out_excessive: bool,
613    /// Polarity errors.
614    pub polarity_error: bool,
615    /// Skew warning.
616    pub skew_warning: bool,
617    /// Length warning.
618    pub length_warning: bool,
619    /// Whether the port reported a virtual cable test.
620    pub has_vct_status: bool,
621    /// Whether each pair raised a warning, meaningful only when
622    /// `has_vct_status` is set.
623    pub vct_warning: [bool; MXR_UTP_PAIRS],
624    /// How many entries of `cable_status` the port filled in.
625    pub cable_status_count: usize,
626    /// Cable diagnostics per pair.
627    pub cable_status: [mxr_cable_status_t; MXR_UTP_PAIRS],
628}
629
630/// One device in a topology report.
631#[repr(C)]
632#[derive(Clone, Copy)]
633pub struct mxr_topology_entry_t {
634    /// The device this entry describes.
635    pub uid: mxr_uid_t,
636    /// Bitmask of the devices it is connected to.
637    pub mask: u32,
638}
639
640impl From<TopologyEntry> for mxr_topology_entry_t {
641    fn from(e: TopologyEntry) -> Self {
642        Self {
643            uid: e.uid.into(),
644            mask: e.mask,
645        }
646    }
647}
648
649/// One firmware component a device reports.
650#[repr(C)]
651#[derive(Clone, Copy)]
652pub struct mxr_firmware_version_t {
653    /// Which component this describes.
654    pub firmware_type: u8,
655    /// Build timestamp, in seconds since the Unix epoch.
656    pub timestamp: u32,
657    /// Source revision hash.
658    pub hash: u32,
659    /// Human-readable version string.
660    pub version: [c_char; MXR_VERSION_LEN],
661}
662
663/// A ProAmp8's Dolby settings.
664#[repr(C)]
665#[derive(Clone, Copy)]
666pub struct mxr_dolby_settings_t {
667    /// 0 = standard, 1 = 3-zone Dolby, 2 = 4-zone Dolby.
668    pub mode: u8,
669    /// Whether PCM is up-mixed to 5.1 rather than passed through.
670    pub pcm_upmix: bool,
671    /// Whether a Dolby stream was detected.
672    pub dolby_detected: bool,
673    /// Whether up-mixing is currently running.
674    pub pcm_upmix_active: bool,
675}
676
677impl From<AmpDolbySettings> for mxr_dolby_settings_t {
678    fn from(s: AmpDolbySettings) -> Self {
679        Self {
680            mode: s.mode,
681            pcm_upmix: s.pcm_upmix,
682            dolby_detected: s.dolby_detected,
683            pcm_upmix_active: s.pcm_upmix_active,
684        }
685    }
686}
687
688/// The remote-control configuration of a source bay.
689#[repr(C)]
690#[derive(Clone, Copy)]
691pub struct mxr_rc_settings_t {
692    /// The device this configuration belongs to.
693    pub target: mxr_uid_t,
694    /// The control method, as the wire value.
695    ///
696    /// Zero is infrared, a method a bay really uses, so it is not a stand-in
697    /// for "not reported". Check that `mxr_rc_settings()` returned `MXR_OK`
698    /// before reading this: a device that has not sent its settings yet
699    /// leaves the struct as the caller allocated it, and a zeroed one then
700    /// reads as a bay set to infrared. `mxr_bay_info_t` answers the same
701    /// question with a `has_rc_type` flag beside its `rc_type`.
702    pub rc_target: u8,
703    /// The control target's address, empty when unset.
704    pub ip: [c_char; MXR_IP_STRING_LEN],
705    /// Whether CEC is enabled.
706    pub cec_enabled: bool,
707    /// Whether CEC powers the sink on automatically.
708    pub cec_auto_on: bool,
709    /// Whether remote-control commands are forwarded.
710    pub forward_rc: bool,
711    /// Whether infrared is forwarded.
712    pub forward_ir: bool,
713    /// The driver state on the source, as the wire value. One above the last
714    /// this library knows is passed through as it arrived.
715    pub rc_status: u8,
716    /// The driver-reported status string, empty when unknown.
717    pub status_name: [c_char; MXR_NAME_LEN],
718}
719
720/// Writes an address into a fixed-width field, leaving it empty when there is
721/// none.
722fn put_ip(dst: &mut [c_char], ip: Option<Ipv4Addr>) {
723    put_str(dst, &ip.map(|ip| ip.to_string()).unwrap_or_default());
724}
725
726/// Declares a getter for one subsystem of a device.
727///
728/// Each has the same three answers - no such device, the device has not sent
729/// this, here it is - and writing them out once keeps a getter that answers
730/// differently visible as one.
731/// Writes a subsystem reading through `out`, or reports why there is none.
732///
733/// # Safety
734///
735/// `out` is null or points at a writable `T`.
736unsafe fn fill<T>(
737    r: &mxr_remote_t,
738    uid: mxr_uid_t,
739    out: *mut T,
740    what: &str,
741    value: Option<T>,
742) -> mxr_result_t {
743    if out.is_null() {
744        return null_out(what);
745    }
746    match value {
747        Some(value) => {
748            // SAFETY: the caller guarantees a writable T, and it is not null.
749            unsafe { *out = value };
750            mxr_result_t::MXR_OK
751        }
752        None => not_reported(r, uid, what),
753    }
754}
755
756/// Reports why a subsystem read found nothing: no such device, or a device
757/// that has not sent this.
758fn not_reported(r: &mxr_remote_t, uid: mxr_uid_t, what: &str) -> mxr_result_t {
759    if r.remote.device(uid.into()).is_none() {
760        return not_heard_from(uid);
761    }
762    fail(
763        mxr_result_t::MXR_ERR_NOT_REPORTED,
764        &format!("the device has reported no {what}"),
765    )
766}
767
768/// Fills `out` with a device's V2IP statistics.
769///
770/// A device sends these only while subscribed; see
771/// `mxr_subscribe_v2ip_stats()`.
772///
773/// # Safety
774///
775/// `remote` is null or a live handle, and `out` points at a writable
776/// [`mxr_v2ip_stats_t`].
777#[no_mangle]
778pub unsafe extern "C" fn mxr_v2ip_stats(
779    remote: *const mxr_remote_t,
780    uid: mxr_uid_t,
781    out: *mut mxr_v2ip_stats_t,
782) -> mxr_result_t {
783    // SAFETY: the caller guarantees a live handle or null.
784    let handle = unsafe { remote.as_ref() };
785    with(handle, |r| {
786        let value = r
787            .remote
788            .v2ip_stats(uid.into())
789            .map(|s: V2ipDeviceStats| mxr_v2ip_stats_t {
790                tx: s.tx.into(),
791                tx_per_minute: s.tx_per_minute.into(),
792                rx: s.rx.into(),
793                rx_per_minute: s.rx_per_minute.into(),
794                decoder: s.decoder.into(),
795            });
796        // SAFETY: the caller guarantees a writable mxr_v2ip_stats_t or null.
797        unsafe { fill(r, uid, out, "V2IP statistics", value) }
798    })
799}
800
801/// Fills `out` with a V2IP device's own encoder configuration.
802///
803/// # Safety
804///
805/// `remote` is null or a live handle, and `out` points at a writable
806/// [`mxr_v2ip_details_t`].
807#[no_mangle]
808pub unsafe extern "C" fn mxr_v2ip_details(
809    remote: *const mxr_remote_t,
810    uid: mxr_uid_t,
811    out: *mut mxr_v2ip_details_t,
812) -> mxr_result_t {
813    // SAFETY: the caller guarantees a live handle or null.
814    let handle = unsafe { remote.as_ref() };
815    with(handle, |r| {
816        let value = r
817            .remote
818            .v2ip_details(uid.into())
819            .map(|d: DeviceV2ipDetails| mxr_v2ip_details_t {
820                video: d.video.into(),
821                audio: d.audio.into(),
822                anc: d.anc.into(),
823                arc: d.arc.into(),
824                // A rate and a marking are both bytes on the wire, so -1 cannot
825                // collide with a value a device could report.
826                tx_rate: d.tx_rate.map_or(-1, i16::from),
827                dscp_video: d.dscp.video.map_or(-1, i16::from),
828                dscp_audio: d.dscp.audio.map_or(-1, i16::from),
829                dscp_anc: d.dscp.anc.map_or(-1, i16::from),
830                scaling_mode: d.scaling.mode.to_wire(),
831                scaling_refresh: d.scaling.refresh,
832                scaling_flags: d.scaling.flags,
833            });
834        // SAFETY: the caller guarantees a writable mxr_v2ip_details_t or null.
835        unsafe { fill(r, uid, out, "V2IP encoder configuration", value) }
836    })
837}
838
839/// Fills `out` with the streams a V2IP sink is subscribed to.
840///
841/// # Safety
842///
843/// `remote` is null or a live handle, and `out` points at a writable
844/// [`mxr_v2ip_sink_t`].
845#[no_mangle]
846pub unsafe extern "C" fn mxr_v2ip_sink(
847    remote: *const mxr_remote_t,
848    uid: mxr_uid_t,
849    out: *mut mxr_v2ip_sink_t,
850) -> mxr_result_t {
851    // SAFETY: the caller guarantees a live handle or null.
852    let handle = unsafe { remote.as_ref() };
853    with(handle, |r| {
854        let value = r
855            .remote
856            .v2ip_sink(uid.into())
857            .map(|s: DeviceV2ipSink| mxr_v2ip_sink_t {
858                addresses: s.addresses.into(),
859                has_audio_format: s.audio_fmt.is_some(),
860                audio_format: {
861                    let f = s.audio_fmt.unwrap_or_default();
862                    mxr_audio_format_t {
863                        sample_rate: f.sample_rate,
864                        channels: f.channels,
865                    }
866                },
867            });
868        // SAFETY: the caller guarantees a writable mxr_v2ip_sink_t or null.
869        unsafe { fill(r, uid, out, "V2IP sink route", value) }
870    })
871}
872
873/// Fills `out` with the window a sink is told to show.
874///
875/// # Safety
876///
877/// `remote` is null or a live handle, and `out` points at a writable
878/// [`mxr_tiling_config_t`].
879#[no_mangle]
880pub unsafe extern "C" fn mxr_v2ip_tiling(
881    remote: *const mxr_remote_t,
882    uid: mxr_uid_t,
883    out: *mut mxr_tiling_config_t,
884) -> mxr_result_t {
885    // SAFETY: the caller guarantees a live handle or null.
886    let handle = unsafe { remote.as_ref() };
887    with(handle, |r| {
888        let value =
889            r.remote
890                .v2ip_tiling(uid.into())
891                .map(|t: V2ipTilingConfig| mxr_tiling_config_t {
892                    target: t.target.into(),
893                    pos_x: t.pos_x,
894                    pos_y: t.pos_y,
895                    width: t.width,
896                    height: t.height,
897                });
898        // SAFETY: the caller guarantees a writable mxr_tiling_config_t or null.
899        unsafe { fill(r, uid, out, "window", value) }
900    })
901}
902
903/// Fills `out` with what a multiviewer reports about itself.
904///
905/// # Safety
906///
907/// `remote` is null or a live handle, and `out` points at a writable
908/// [`mxr_multiviewer_status_t`].
909#[no_mangle]
910pub unsafe extern "C" fn mxr_multiviewer_status(
911    remote: *const mxr_remote_t,
912    uid: mxr_uid_t,
913    out: *mut mxr_multiviewer_status_t,
914) -> mxr_result_t {
915    // SAFETY: the caller guarantees a live handle or null.
916    let handle = unsafe { remote.as_ref() };
917    with(handle, |r| {
918        let value = r.remote.multiviewer_status(uid.into()).map(multiviewer_of);
919        // SAFETY: the caller guarantees a writable mxr_multiviewer_status_t or null.
920        unsafe { fill(r, uid, out, "multiviewer status", value) }
921    })
922}
923
924/// Copies a multiviewer's report into the C shape.
925fn multiviewer_of(s: MultiviewerStatus) -> mxr_multiviewer_status_t {
926    let mut out = mxr_multiviewer_status_t {
927        uid: s.uid.into(),
928        mappings: [mxr_uid_t::default(); MXR_MULTIVIEWER_INPUTS],
929        mcu_version: [0; MXR_NAME_LEN],
930        scaler_version: [0; MXR_NAME_LEN],
931        hw_view_mode: s.hw_view_mode,
932        view_mode: s.view_mode.to_wire(),
933        pip_position: s.pip_position.to_wire(),
934        pip_size: s.pip_size.to_wire(),
935        output_mode: s.output_mode.to_wire(),
936        hdcp_mode: s.hdcp_mode.to_wire(),
937        output_itc: s.output_itc.to_wire(),
938        edid_template: s.edid_template.to_wire(),
939        aspect_ratio: s.aspect_ratio.to_wire(),
940        auto_switch: s.auto_switch.to_wire(),
941        audio_source: s.audio_source.to_wire(),
942        has_audio_volume: s.audio_volume.is_some(),
943        audio_volume: s.audio_volume.unwrap_or(0),
944        audio_muted: s.audio_muted.to_wire(),
945        video_sources: [0; MXR_MULTIVIEWER_INPUTS],
946        remote_control: s.remote_control.to_wire(),
947    };
948    for (slot, uid) in out.mappings.iter_mut().zip(s.mappings) {
949        *slot = uid.into();
950    }
951    for (slot, source) in out.video_sources.iter_mut().zip(s.video_sources) {
952        *slot = source.to_wire();
953    }
954    put_str(&mut out.mcu_version, &s.mcu_version);
955    put_str(&mut out.scaler_version, &s.scaler_version);
956    out
957}
958
959/// Fills `out` with a ProAmp8's Dolby settings.
960///
961/// # Safety
962///
963/// `remote` is null or a live handle, and `out` points at a writable
964/// [`mxr_dolby_settings_t`].
965#[no_mangle]
966pub unsafe extern "C" fn mxr_dolby_settings(
967    remote: *const mxr_remote_t,
968    uid: mxr_uid_t,
969    out: *mut mxr_dolby_settings_t,
970) -> mxr_result_t {
971    // SAFETY: the caller guarantees a live handle or null.
972    let handle = unsafe { remote.as_ref() };
973    with(handle, |r| {
974        let value = r
975            .remote
976            .dolby_settings(uid.into())
977            .map(mxr_dolby_settings_t::from);
978        // SAFETY: the caller guarantees a writable mxr_dolby_settings_t or null.
979        unsafe { fill(r, uid, out, "Dolby settings", value) }
980    })
981}
982
983/// Fills `out` with a source bay's remote-control configuration.
984///
985/// # Safety
986///
987/// `remote` is null or a live handle, and `out` points at a writable
988/// [`mxr_rc_settings_t`].
989#[no_mangle]
990pub unsafe extern "C" fn mxr_rc_settings(
991    remote: *const mxr_remote_t,
992    uid: mxr_uid_t,
993    out: *mut mxr_rc_settings_t,
994) -> mxr_result_t {
995    // SAFETY: the caller guarantees a live handle or null.
996    let handle = unsafe { remote.as_ref() };
997    with(handle, |r| {
998        let value = r.remote.rc_settings(uid.into()).map(rc_settings_of);
999        // SAFETY: the caller guarantees a writable mxr_rc_settings_t or null.
1000        unsafe { fill(r, uid, out, "remote-control configuration", value) }
1001    })
1002}
1003
1004/// Copies a remote-control configuration into the C shape.
1005fn rc_settings_of(s: RcSettings) -> mxr_rc_settings_t {
1006    let mut out = mxr_rc_settings_t {
1007        target: s.target.into(),
1008        rc_target: s.rc_target,
1009        ip: [0; MXR_IP_STRING_LEN],
1010        cec_enabled: s.cec_enabled,
1011        cec_auto_on: s.cec_auto_on,
1012        forward_rc: s.forward_rc,
1013        forward_ir: s.forward_ir,
1014        rc_status: s.rc_status,
1015        status_name: [0; MXR_NAME_LEN],
1016    };
1017    put_ip(&mut out.ip, s.ip);
1018    put_str(&mut out.status_name, &s.status_name);
1019    out
1020}
1021
1022/// Writes the streams a device's source bays advertise, and returns how many
1023/// there are.
1024///
1025/// Returns the full count even when it exceeds `cap`, so calling with `cap`
1026/// zero sizes the buffer.
1027///
1028/// # Safety
1029///
1030/// `remote` is null or a live handle, and `out` is null or points at `cap`
1031/// writable [`mxr_stream_sources_t`].
1032#[no_mangle]
1033pub unsafe extern "C" fn mxr_v2ip_sources(
1034    remote: *const mxr_remote_t,
1035    uid: mxr_uid_t,
1036    out: *mut mxr_stream_sources_t,
1037    cap: usize,
1038) -> usize {
1039    guard(0, || {
1040        // SAFETY: the caller guarantees a live handle or null.
1041        let Some(r) = (unsafe { remote.as_ref() }) else {
1042            return no_handle();
1043        };
1044        let Some(sources) = r.remote.v2ip_sources(uid.into()) else {
1045            not_reported(r, uid, "V2IP stream sources");
1046            return 0;
1047        };
1048        // SAFETY: the caller guarantees cap writable elements at out.
1049        unsafe { copy_into(&sources, out, cap) }
1050    })
1051}
1052
1053/// Writes a device's network ports, and returns how many there are.
1054///
1055/// # Safety
1056///
1057/// `remote` is null or a live handle, and `out` is null or points at `cap`
1058/// writable [`mxr_network_port_t`].
1059#[no_mangle]
1060pub unsafe extern "C" fn mxr_network_status(
1061    remote: *const mxr_remote_t,
1062    uid: mxr_uid_t,
1063    out: *mut mxr_network_port_t,
1064    cap: usize,
1065) -> usize {
1066    guard(0, || {
1067        // SAFETY: the caller guarantees a live handle or null.
1068        let Some(r) = (unsafe { remote.as_ref() }) else {
1069            return no_handle();
1070        };
1071        let ports: Vec<mxr_network_port_t> = r
1072            .remote
1073            .network_status(uid.into())
1074            .iter()
1075            .map(port_of)
1076            .collect();
1077        // SAFETY: the caller guarantees cap writable elements at out.
1078        unsafe { copy_into(&ports, out, cap) }
1079    })
1080}
1081
1082/// Copies one port report into the C shape.
1083fn port_of(p: &NetworkPortStatus) -> mxr_network_port_t {
1084    let errors = p.errors.unwrap_or_default();
1085    let mut out = mxr_network_port_t {
1086        port: p.port,
1087        name: [0; MXR_NAME_LEN],
1088        link_speed: p.link_speed.to_wire(),
1089        link_full_duplex: p.link_full_duplex,
1090        ip: [0; MXR_IP_STRING_LEN],
1091        querier: [0; MXR_IP_STRING_LEN],
1092        has_mac_address: p.mac_address.is_some(),
1093        mac_address: p.mac_address.unwrap_or_default().0,
1094        has_errors: p.errors.is_some(),
1095        in_error: errors.in_error,
1096        in_fcs_error: errors.in_fcs_error,
1097        in_collision: errors.in_collision,
1098        out_deferred: errors.out_deferred,
1099        out_excessive: errors.out_excessive,
1100        polarity_error: errors.polarity_error,
1101        skew_warning: errors.skew_warning,
1102        length_warning: errors.length_warning,
1103        has_vct_status: p.vct_status.is_some(),
1104        vct_warning: [false; MXR_UTP_PAIRS],
1105        cable_status_count: p.cable_status.len().min(MXR_UTP_PAIRS),
1106        cable_status: [mxr_cable_status_t {
1107            polarity: false,
1108            pair: 0,
1109            skew: 0,
1110            length: 0,
1111        }; MXR_UTP_PAIRS],
1112    };
1113    put_str(&mut out.name, &p.name);
1114    put_ip(&mut out.ip, p.ip);
1115    put_ip(&mut out.querier, p.querier);
1116    if let Some(vct) = p.vct_status {
1117        for (slot, status) in out.vct_warning.iter_mut().zip(vct) {
1118            *slot = status == VctStatus::Warning;
1119        }
1120    }
1121    for (slot, cable) in out.cable_status.iter_mut().zip(&p.cable_status) {
1122        *slot = (*cable).into();
1123    }
1124    out
1125}
1126
1127/// Writes a device's view of the mesh topology, and returns how many entries
1128/// there are.
1129///
1130/// # Safety
1131///
1132/// `remote` is null or a live handle, and `out` is null or points at `cap`
1133/// writable [`mxr_topology_entry_t`].
1134#[no_mangle]
1135pub unsafe extern "C" fn mxr_topology(
1136    remote: *const mxr_remote_t,
1137    uid: mxr_uid_t,
1138    out: *mut mxr_topology_entry_t,
1139    cap: usize,
1140) -> usize {
1141    guard(0, || {
1142        // SAFETY: the caller guarantees a live handle or null.
1143        let Some(r) = (unsafe { remote.as_ref() }) else {
1144            return no_handle();
1145        };
1146        let topology = r.remote.topology(uid.into());
1147        // SAFETY: the caller guarantees cap writable elements at out.
1148        unsafe { copy_into(&topology, out, cap) }
1149    })
1150}
1151
1152/// Writes the firmware versions a device reports, and returns how many there
1153/// are.
1154///
1155/// # Safety
1156///
1157/// `remote` is null or a live handle, and `out` is null or points at `cap`
1158/// writable [`mxr_firmware_version_t`].
1159#[no_mangle]
1160pub unsafe extern "C" fn mxr_device_firmware(
1161    remote: *const mxr_remote_t,
1162    uid: mxr_uid_t,
1163    out: *mut mxr_firmware_version_t,
1164    cap: usize,
1165) -> usize {
1166    guard(0, || {
1167        // SAFETY: the caller guarantees a live handle or null.
1168        let Some(r) = (unsafe { remote.as_ref() }) else {
1169            return no_handle();
1170        };
1171        let versions: Vec<mxr_firmware_version_t> = r
1172            .remote
1173            .firmware(uid.into())
1174            .iter()
1175            .map(|(_, v)| firmware_of(v))
1176            .collect();
1177        // SAFETY: the caller guarantees cap writable elements at out.
1178        unsafe { copy_into(&versions, out, cap) }
1179    })
1180}
1181
1182/// Copies one firmware report into the C shape.
1183fn firmware_of(v: &FirmwareVersion) -> mxr_firmware_version_t {
1184    let mut out = mxr_firmware_version_t {
1185        firmware_type: v.firmware_type.to_wire(),
1186        timestamp: v.timestamp,
1187        hash: v.hash,
1188        version: [0; MXR_VERSION_LEN],
1189    };
1190    put_str(&mut out.version, &v.version);
1191    out
1192}
1193
1194/// Writes a device's audio endpoints, in the order it reported them, and
1195/// returns how many there are.
1196///
1197/// # Safety
1198///
1199/// `remote` is null or a live handle, and `out` is null or points at `cap`
1200/// writable [`mxr_audio_endpoint_t`].
1201#[no_mangle]
1202pub unsafe extern "C" fn mxr_audio_endpoints(
1203    remote: *const mxr_remote_t,
1204    uid: mxr_uid_t,
1205    out: *mut mxr_audio_endpoint_t,
1206    cap: usize,
1207) -> usize {
1208    guard(0, || {
1209        // SAFETY: the caller guarantees a live handle or null.
1210        let Some(r) = (unsafe { remote.as_ref() }) else {
1211            return no_handle();
1212        };
1213        let Some(endpoints) = r.remote.audio_endpoints(uid.into()) else {
1214            not_reported(r, uid, "audio endpoints");
1215            return 0;
1216        };
1217        let list: Vec<mxr_audio_endpoint_t> = endpoints.list().map(Into::into).collect();
1218        // SAFETY: the caller guarantees cap writable elements at out.
1219        unsafe { copy_into(&list, out, cap) }
1220    })
1221}
1222
1223/// Writes the endpoints hanging off one audio endpoint, and returns how many
1224/// there are.
1225///
1226/// # Safety
1227///
1228/// `remote` is null or a live handle, and `out` is null or points at `cap`
1229/// writable bytes.
1230#[no_mangle]
1231pub unsafe extern "C" fn mxr_audio_endpoint_children(
1232    remote: *const mxr_remote_t,
1233    uid: mxr_uid_t,
1234    endpoint: u8,
1235    out: *mut u8,
1236    cap: usize,
1237) -> usize {
1238    guard(0, || {
1239        // SAFETY: the caller guarantees a live handle or null.
1240        let Some(r) = (unsafe { remote.as_ref() }) else {
1241            return no_handle();
1242        };
1243        let children = match r.remote.audio_endpoints(uid.into()) {
1244            Some(endpoints) => match endpoints.get(endpoint) {
1245                Some(e) => e.children.clone(),
1246                None => {
1247                    fail(
1248                        mxr_result_t::MXR_ERR_NOT_FOUND,
1249                        &format!("the device has no audio endpoint {endpoint}"),
1250                    );
1251                    return 0;
1252                }
1253            },
1254            None => {
1255                not_reported(r, uid, "audio endpoints");
1256                return 0;
1257            }
1258        };
1259        // SAFETY: the caller guarantees cap writable bytes at out.
1260        unsafe { copy_into(&children, out, cap) }
1261    })
1262}
1263
1264/// Reports a null handle from a call whose answer is a count.
1265fn no_handle() -> usize {
1266    fail(
1267        mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1268        "the client handle is null",
1269    );
1270    0
1271}