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;
18use std::time::UNIX_EPOCH;
19
20use mx_remote::{
21    AmpDolbySettings, AudioEndpoint, DeviceV2ipDetails, DeviceV2ipSink, FirmwareVersion,
22    MultiviewerStatus, NetworkPortStatus, RcSettings, StreamKind, TopologyEntry, UtpCableStatus,
23    V2ipDecoderDetail, V2ipDeviceSettings, V2ipDeviceStats, V2ipFpgaFeature, V2ipRxStats,
24    V2ipStreamSource, V2ipStreamSources, V2ipTestSync, V2ipTestTone, V2ipTestcard,
25    V2ipTilingConfig, V2ipToneMode, V2ipTxStats, V2ipVlan, VctStatus, MULTIVIEWER_INPUTS,
26};
27
28use crate::abi::{fail, guard, mxr_result_t, mxr_uid_t, put_str};
29use crate::control::mxr_audio_format_t;
30use crate::info::{copy_into, not_heard_from, null_out, MXR_NAME_LEN, MXR_VERSION_LEN};
31use crate::remote::{mxr_remote_t, with, MXR_IP_STRING_LEN};
32
33/// How many inputs a multiviewer has.
34///
35/// Written as a literal because the generated header needs one, and checked
36/// against the core crate's value below so the two cannot drift apart.
37pub const MXR_MULTIVIEWER_INPUTS: usize = 4;
38
39const _: () = assert!(MXR_MULTIVIEWER_INPUTS == MULTIVIEWER_INPUTS);
40
41/// How many pairs a UTP cable diagnostic covers.
42pub const MXR_UTP_PAIRS: usize = 4;
43
44/// Which of a V2IP device's streams an address describes.
45#[repr(i32)]
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum mxr_stream_kind_t {
48    /// The video stream.
49    MXR_STREAM_VIDEO = 0,
50    /// The audio stream.
51    MXR_STREAM_AUDIO = 1,
52    /// The ancillary-data stream.
53    MXR_STREAM_ANC = 2,
54    /// The audio-return stream.
55    MXR_STREAM_ARC = 3,
56}
57
58impl From<StreamKind> for mxr_stream_kind_t {
59    fn from(kind: StreamKind) -> Self {
60        match kind {
61            StreamKind::Video => Self::MXR_STREAM_VIDEO,
62            StreamKind::Audio => Self::MXR_STREAM_AUDIO,
63            StreamKind::Anc => Self::MXR_STREAM_ANC,
64            StreamKind::Arc => Self::MXR_STREAM_ARC,
65        }
66    }
67}
68
69/// One multicast stream address.
70#[repr(C)]
71#[derive(Clone, Copy)]
72pub struct mxr_stream_source_t {
73    /// Which stream this address is for.
74    pub kind: mxr_stream_kind_t,
75    /// The multicast group, as a dotted quad.
76    pub ip: [c_char; MXR_IP_STRING_LEN],
77    /// The destination UDP port.
78    pub port: u16,
79    /// Whether this carries a usable address: a multicast group and a non-zero
80    /// port, both. A slot a device has not filled in is not an error, so this
81    /// is what separates an address from an empty slot.
82    pub valid: bool,
83}
84
85impl From<V2ipStreamSource> for mxr_stream_source_t {
86    fn from(s: V2ipStreamSource) -> Self {
87        let mut out = Self {
88            kind: s.kind.into(),
89            ip: [0; MXR_IP_STRING_LEN],
90            port: s.port,
91            valid: s.is_valid(),
92        };
93        put_str(&mut out.ip, &s.ip.to_string());
94        out
95    }
96}
97
98/// The streams one V2IP source advertises.
99#[repr(C)]
100#[derive(Clone, Copy)]
101pub struct mxr_stream_sources_t {
102    /// The originating device, zero when it is not known.
103    pub uid: mxr_uid_t,
104    /// The video stream.
105    pub video: mxr_stream_source_t,
106    /// The audio stream.
107    pub audio: mxr_stream_source_t,
108    /// The ancillary-data stream.
109    pub anc: mxr_stream_source_t,
110    /// Whether an audio-return stream is advertised.
111    pub has_arc: bool,
112    /// The audio-return stream, meaningful only when `has_arc` is set.
113    pub arc: mxr_stream_source_t,
114}
115
116impl From<V2ipStreamSources> for mxr_stream_sources_t {
117    fn from(s: V2ipStreamSources) -> Self {
118        Self {
119            uid: s.uid.into(),
120            video: s.video.into(),
121            audio: s.audio.into(),
122            anc: s.anc.into(),
123            has_arc: s.arc.is_some(),
124            arc: s.arc.unwrap_or_default().into(),
125        }
126    }
127}
128
129/// A V2IP device's own encoder configuration.
130#[repr(C)]
131#[derive(Clone, Copy)]
132pub struct mxr_v2ip_details_t {
133    /// The video stream this device sources.
134    pub video: mxr_stream_source_t,
135    /// The audio stream this device sources.
136    pub audio: mxr_stream_source_t,
137    /// The ancillary-data stream this device sources.
138    pub anc: mxr_stream_source_t,
139    /// The audio-return stream this device sources.
140    pub arc: mxr_stream_source_t,
141    /// Encoder rate in units of 10Mb/s, or -1 when no rate has been reported.
142    pub tx_rate: i16,
143    /// DSCP marking for the video stream, or -1 when unmarked.
144    pub dscp_video: i16,
145    /// DSCP marking for the audio stream, or -1 when unmarked.
146    pub dscp_audio: i16,
147    /// DSCP marking for the ancillary-data stream, or -1 when unmarked.
148    pub dscp_anc: i16,
149    /// The signal type the output scales to.
150    pub scaling_mode: u16,
151    /// Refresh rate in Hz.
152    pub scaling_refresh: u16,
153    /// `MXR_SCALING_FLAG_*` bits. Bits outside those are undefined and are not
154    /// reliably zero: firmware predating the fix builds this frame over an
155    /// uninitialised stack local.
156    pub scaling_flags: u8,
157}
158
159/// Set when the frame carries a scaling mode and refresh rate.
160pub const MXR_SCALING_FLAG_MODE_VALID: u8 = 1 << 0;
161/// Set when the frame carries the scaling options.
162pub const MXR_SCALING_FLAG_OPTIONS_VALID: u8 = 1 << 1;
163/// Set when the frame carries the second group of scaling options.
164///
165/// Firmware with those options sets this on every configuration it sends about
166/// itself, so it doubles as the report that the device has them at all. It is
167/// read only from a sender that says it initialises its configuration: on one
168/// that does not, this bit is uninitialised stack and the settings behind it
169/// would be invented rather than misread.
170pub const MXR_SCALING_FLAG_OPTIONS2_VALID: u8 = 1 << 4;
171/// Set when the output follows its source's format instead of a fixed one.
172pub const MXR_SCALING_FLAG_MATCH_SOURCE: u8 = 1 << 5;
173/// Set when the output declines 4:2:0 rather than scaling it.
174pub const MXR_SCALING_FLAG_SKIP_420: u8 = 1 << 6;
175/// Set when the output scales automatically.
176pub const MXR_SCALING_FLAG_AUTO_SCALING: u8 = 1 << 7;
177
178/// The streams a V2IP sink is subscribed to.
179///
180/// **Addresses that read as unset mean "no route, or the sink could not work
181/// one out" - never "definitely not subscribed".** This is the one part of a
182/// device configuration with no validity marker of its own, so a sender with
183/// nothing to say sends zeros and every receiver stores them. A sender leaves
184/// it empty whenever its own stream configuration does not resolve, which
185/// covers more than having no route: a selected source whose record has not
186/// arrived yet, the state after a restart at either end, missing audio bay
187/// configuration, or a stream failing its validity check.
188///
189/// Expect it rather than guard against it. Any scaling change makes the device
190/// rebuild and rebroadcast this block - so the empty reading turns up most
191/// often during exactly the no-signal troubleshooting that prompted the change.
192/// A device's periodic report puts a real route back within a minute of it
193/// having one, so a reader that needs certainty should wait one out rather than
194/// treat the first empty reading as an answer.
195///
196/// Only the device's own report sets this. A controller writing another
197/// device's configuration sends the block zeroed, and that frame is ignored.
198#[repr(C)]
199#[derive(Clone, Copy)]
200pub struct mxr_v2ip_sink_t {
201    /// The streams the sink subscribes to.
202    pub addresses: mxr_stream_sources_t,
203    /// Whether the sender reported a resolved audio format.
204    pub has_audio_format: bool,
205    /// The audio format, meaningful only when `has_audio_format` is set.
206    pub audio_format: mxr_audio_format_t,
207}
208
209/// Transmitter stream statistics.
210#[repr(C)]
211#[derive(Clone, Copy)]
212pub struct mxr_v2ip_tx_stats_t {
213    /// Video packets sent.
214    pub video: u32,
215    /// Audio packets sent.
216    pub audio: u32,
217    /// Ancillary-data packets sent.
218    pub anc: u32,
219    /// Times the stream went down.
220    pub stream_down: u32,
221    /// Transmit overflows.
222    pub overflow: u32,
223}
224
225impl From<V2ipTxStats> for mxr_v2ip_tx_stats_t {
226    fn from(s: V2ipTxStats) -> Self {
227        Self {
228            video: s.video,
229            audio: s.audio,
230            anc: s.anc,
231            stream_down: s.stream_down,
232            overflow: s.overflow,
233        }
234    }
235}
236
237/// Receiver stream statistics.
238#[repr(C)]
239#[derive(Clone, Copy)]
240pub struct mxr_v2ip_rx_stats_t {
241    /// Video packets received.
242    pub video_total: u32,
243    /// Video packets dropped.
244    pub video_dropped: u32,
245    /// Video sequence errors.
246    pub video_seq_errors: u32,
247    /// Watchdog timeouts.
248    pub wdt_timeout: u32,
249    /// Audio packets received.
250    pub audio_total: u32,
251    /// Audio packets dropped.
252    pub audio_dropped: u32,
253    /// Audio sequence errors.
254    pub audio_seq_errors: u32,
255    /// Ancillary-data packets received.
256    pub anc_total: u32,
257    /// Ancillary-data packets dropped.
258    pub anc_dropped: u32,
259    /// Ancillary-data sequence errors.
260    pub anc_seq_errors: u32,
261    /// The decoder's health state: 0 unknown, 1 healthy, 2 bad, 3 starting.
262    ///
263    /// Only healthy and bad are verdicts. Reading failure as "not healthy"
264    /// counts a decoder that is merely coming up as one that failed, which is
265    /// what every sink reports for a moment after a route change.
266    pub decoder_state: u8,
267}
268
269impl From<V2ipRxStats> for mxr_v2ip_rx_stats_t {
270    fn from(s: V2ipRxStats) -> Self {
271        Self {
272            video_total: s.video_total,
273            video_dropped: s.video_dropped,
274            video_seq_errors: s.video_seq_errors,
275            wdt_timeout: s.wdt_timeout,
276            audio_total: s.audio_total,
277            audio_dropped: s.audio_dropped,
278            audio_seq_errors: s.audio_seq_errors,
279            anc_total: s.anc_total,
280            anc_dropped: s.anc_dropped,
281            anc_seq_errors: s.anc_seq_errors,
282            decoder_state: s.decoder_state.to_wire(),
283        }
284    }
285}
286
287/// What a statistics report says about a sink's decoder.
288#[repr(i32)]
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub enum mxr_v2ip_decoder_detail_t {
291    /// The report carried no decoder block: the sender's firmware predates it.
292    MXR_V2IP_DECODER_ABSENT = 0,
293    /// The block is there and the decoder has never answered.
294    MXR_V2IP_DECODER_NEVER_ANSWERED = 1,
295    /// The block carries a reading.
296    MXR_V2IP_DECODER_ANSWERED = 2,
297}
298
299/// What a sink's decoder recovered from the codestream it is being given.
300///
301/// This is what the decoder understood, read ahead of the scaler: the geometry
302/// is unrounded and is not what the display is being sent. Every field but
303/// `detail` is zero unless `detail` is `MXR_V2IP_DECODER_ANSWERED`.
304///
305/// `detail` follows the sink being configured rather than the sink being
306/// enabled, so a sink that is switched off still reports - as reason 10, or
307/// from an older sender as reason 1, which is the same reading a sink whose
308/// source has died produces. Nothing here answers whether a sink is enabled.
309///
310/// Colour depth is absent on purpose and will stay absent: the video processor
311/// answers that from a driver constant rather than from the codestream, so
312/// there is no reading to carry. Assert depth at the encoder's input bay
313/// instead.
314#[repr(C)]
315#[derive(Clone, Copy)]
316pub struct mxr_v2ip_decoder_t {
317    /// Which of the three states this report is in.
318    pub detail: mxr_v2ip_decoder_detail_t,
319    /// The primary cause of the state the decoder is in, by the sender's own
320    /// names: 0 OK, 1 NO_PACKETS, 2 PACKETS_DEGRADED, 3 NO_FORMAT, 4
321    /// FORMAT_MISMATCH, 5 FORMAT_REJECTED, 6 DECODER_BLOCKED, 7
322    /// SWITCH_PENDING, 8 PTP_UNLOCKED, 9 TX_BRIDGE_UNLOCKED, 10 IDLE.
323    /// Firmware adds causes, so an unrecognised value is passed through as it
324    /// arrived.
325    ///
326    /// The primary cause only, and the numbers are identities rather than
327    /// ranks: several causes can be true at once and a fixed priority order in
328    /// the firmware decides which lands here. Classify on `flags`, which
329    /// carries all of them; a test against this field asks which cause won
330    /// instead. Reason 10 is the exception and is read here: it outranks the
331    /// whole word, and testing it first is what stops a switched-off sink
332    /// being reported as broken. Reason 9 is the one most often hidden here —
333    /// see `flags`.
334    ///
335    /// A pending switch is a step in an operation someone asked for rather
336    /// than a fault, and PTP unlocked costs audio alone: audio cannot enable
337    /// and the picture is unaffected, so reporting it as a fault puts an
338    /// overlay over a good picture. Reason 10 says the sink is switched off,
339    /// and carries no implication about `width` and `height`: those are read
340    /// before any cause is decided, so a switched-off sink still detecting a
341    /// codestream reports a real geometry. An older sender reports reason 1
342    /// for the same sink, so an absent reason 10 is not evidence a sink is
343    /// enabled - nothing here answers that, which comes from
344    /// `mxr_v2ip_details()` or the device's HTTP status.
345    pub reason: u8,
346    /// The converter watchdog is holding the stream back.
347    pub blocking: bool,
348    /// The recovered picture width, and 0 when none was recovered.
349    pub width: u16,
350    /// The recovered picture height, and 0 when none was recovered.
351    pub height: u16,
352    /// The recovered colour space: 0 RGB, 1 YCbCr 4:4:4, 2 YCbCr 4:2:2,
353    /// 3 YCbCr 4:2:0, 255 the decoder cannot name it.
354    ///
355    /// No value here means "no signal": a decoder with nothing to decode
356    /// reports 0, which is indistinguishable from a real RGB reading. A zero
357    /// `width` or `height` is what says the decoder recovered nothing - which
358    /// is not the same as the sink being switched off, and does not imply it. The 255
359    /// is its own value rather than the 0xF a signal report uses for an
360    /// unknown colour space.
361    pub format: u16,
362    /// How many readings the sink has stored. Monotonic, wrapping at 65535
363    /// after some 36 hours, and never reset.
364    ///
365    /// A sink reads its video processor every two seconds and reports every
366    /// second, so roughly every other report repeats a reading already seen:
367    /// a frame arriving says nothing about how fresh the values in it are.
368    /// This counter moves only when a reading is stored, so a processor that
369    /// stopped answering leaves it still rather than implying a refresh.
370    ///
371    /// After pointing a sink at something else, wait for this to advance by
372    /// two before trusting the geometry. It ticks when a reply lands rather
373    /// than when a query is sent, so the first tick can carry an answer the
374    /// processor read fractionally before the switch; the second cannot,
375    /// because at most one query is outstanding at a time.
376    pub updates: u16,
377    /// Every cause that applies, as bit N for reason N, where `reason` carries
378    /// the primary one. Bit 0 is cleared by the sender, so an empty word means
379    /// nothing beyond the primary cause applies.
380    ///
381    /// This is what to classify on, once reason 10 has been ruled out. That
382    /// cause outranks the whole word and leaves the bits below it set - a sink
383    /// switched off while running keeps the bits the decoder genuinely saw on
384    /// the way down - so a fault mask over `flags` reports a deliberately
385    /// disabled sink as broken.
386    ///
387    /// A cause that is true can be missing from
388    /// `reason` and present here: reason 9, the pipeline rebuilding after the
389    /// transmitter bridge stayed unlocked, sits below every input-side cause,
390    /// so a pipeline restarting in a loop shows an input-side cause in
391    /// `reason` and bit 9 here alone - always, rather than briefly. Bit 9
392    /// needs a sustained five seconds to appear at all, and sustained across
393    /// reports it means a restart loop rather than one event, because the
394    /// sender's debounce restarts each time it elapses.
395    ///
396    /// Reasons 3 and 4 are the two arms of one decision and never appear
397    /// together.
398    pub flags: u32,
399    /// How many times the converter watchdog has triggered.
400    pub blocked_count: u32,
401}
402
403impl From<V2ipDecoderDetail> for mxr_v2ip_decoder_t {
404    fn from(detail: V2ipDecoderDetail) -> Self {
405        let empty = Self {
406            detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ABSENT,
407            reason: 0,
408            blocking: false,
409            width: 0,
410            height: 0,
411            format: 0,
412            updates: 0,
413            flags: 0,
414            blocked_count: 0,
415        };
416        match detail {
417            V2ipDecoderDetail::Absent => empty,
418            V2ipDecoderDetail::NeverAnswered => Self {
419                detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_NEVER_ANSWERED,
420                ..empty
421            },
422            V2ipDecoderDetail::Answered(r) => Self {
423                detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ANSWERED,
424                reason: r.reason.to_wire(),
425                blocking: r.blocking,
426                width: r.width,
427                height: r.height,
428                format: r.format.to_wire(),
429                updates: r.updates,
430                flags: r.flags,
431                blocked_count: r.blocked_count,
432            },
433        }
434    }
435}
436
437/// A device's V2IP statistics, cumulative and over the last minute.
438#[repr(C)]
439#[derive(Clone, Copy)]
440pub struct mxr_v2ip_stats_t {
441    /// Transmit totals since boot.
442    pub tx: mxr_v2ip_tx_stats_t,
443    /// Transmit counts over the last minute.
444    pub tx_per_minute: mxr_v2ip_tx_stats_t,
445    /// Receive totals since boot.
446    pub rx: mxr_v2ip_rx_stats_t,
447    /// Receive counts over the last minute.
448    pub rx_per_minute: mxr_v2ip_rx_stats_t,
449    /// What the sink's decoder recovered from the codestream it is decoding.
450    pub decoder: mxr_v2ip_decoder_t,
451}
452
453/// The window a sink is currently told to show.
454///
455/// This is the pollable view of a sink's window, not the persisted video wall
456/// setting: on a sink running the wall module a write here is transient,
457/// because that module pushes its own target window back within about a
458/// second.
459#[repr(C)]
460#[derive(Clone, Copy)]
461pub struct mxr_tiling_config_t {
462    /// The sink this window belongs to.
463    pub target: mxr_uid_t,
464    /// Window origin, horizontal.
465    pub pos_x: u16,
466    /// Window origin, vertical.
467    pub pos_y: u16,
468    /// Window width.
469    pub width: u16,
470    /// Window height.
471    pub height: u16,
472}
473
474/// What a multiviewer reports about itself.
475#[repr(C)]
476#[derive(Clone, Copy)]
477pub struct mxr_multiviewer_status_t {
478    /// The multiviewer.
479    pub uid: mxr_uid_t,
480    /// The source device mapped to each input.
481    pub mappings: [mxr_uid_t; MXR_MULTIVIEWER_INPUTS],
482    /// The MCU firmware version.
483    pub mcu_version: [c_char; MXR_NAME_LEN],
484    /// The scaler firmware version.
485    pub scaler_version: [c_char; MXR_NAME_LEN],
486    /// The view mode the hardware reports, which is its own numbering rather
487    /// than `view_mode`'s.
488    pub hw_view_mode: u8,
489    /// The window layout.
490    pub view_mode: u8,
491    /// Which corner the picture-in-picture window sits in.
492    pub pip_position: u8,
493    /// The size of the picture-in-picture window.
494    pub pip_size: u8,
495    /// The output resolution.
496    pub output_mode: u8,
497    /// The HDCP mode.
498    pub hdcp_mode: u8,
499    /// The IT content flag.
500    pub output_itc: u8,
501    /// The EDID presented to sources.
502    pub edid_template: u8,
503    /// How a source is fitted into its window.
504    pub aspect_ratio: u8,
505    /// Whether automatic source switching is on.
506    pub auto_switch: u8,
507    /// Which window the audio is taken from.
508    pub audio_source: u8,
509    /// Whether a volume has been reported.
510    pub has_audio_volume: bool,
511    /// The output volume.
512    pub audio_volume: u8,
513    /// Whether the output is muted.
514    pub audio_muted: u8,
515    /// The source shown in each window.
516    pub video_sources: [u8; MXR_MULTIVIEWER_INPUTS],
517    /// Which window remote control is forwarded to.
518    pub remote_control: u8,
519}
520
521/// One node of a device's audio tree.
522#[repr(C)]
523#[derive(Clone, Copy)]
524pub struct mxr_audio_endpoint_t {
525    /// The endpoint's identifier on its device.
526    pub id: u8,
527    /// What the endpoint can do, as `MXR_AUDIO_*` bits.
528    pub features: u32,
529    /// Whether the endpoint carries a stream address.
530    pub has_address: bool,
531    /// The stream address, meaningful only when `has_address` is set.
532    pub address: mxr_stream_source_t,
533    /// The endpoint this one hangs off, or -1 at a root.
534    pub parent: i16,
535    /// How many children this endpoint has; read them with
536    /// `mxr_audio_endpoint_children()`.
537    pub child_count: usize,
538    /// Whether the device reported which inputs are selectable.
539    pub has_inputs_available: bool,
540    /// Bitmask of the endpoints this one may be switched to.
541    pub inputs_available: u32,
542    /// Whether the device reported which input is selected.
543    pub has_inputs_routed: bool,
544    /// Bitmask of the endpoint this one is listening to.
545    pub inputs_routed: u32,
546    /// The device at the other end of the link, zero when unlinked.
547    pub linked_device: mxr_uid_t,
548    /// The endpoint at the other end of the link, or -1 when unlinked.
549    pub linked_endpoint: i16,
550}
551
552impl From<&AudioEndpoint> for mxr_audio_endpoint_t {
553    fn from(e: &AudioEndpoint) -> Self {
554        Self {
555            id: e.id,
556            features: e.features.bits(),
557            has_address: e.address.is_some(),
558            address: e.address.unwrap_or_default().into(),
559            // An endpoint id is a byte on the wire, so -1 cannot collide.
560            parent: e.parent.map_or(-1, i16::from),
561            child_count: e.children.len(),
562            has_inputs_available: e.inputs_available.is_some(),
563            inputs_available: e.inputs_available.unwrap_or(0),
564            has_inputs_routed: e.inputs_routed.is_some(),
565            inputs_routed: e.inputs_routed.unwrap_or(0),
566            linked_device: e.linked_device.into(),
567            linked_endpoint: e.linked_endpoint.map_or(-1, i16::from),
568        }
569    }
570}
571
572/// The diagnostic result for one UTP cable pair.
573#[repr(C)]
574#[derive(Clone, Copy)]
575pub struct mxr_cable_status_t {
576    /// Whether the pair is wired with normal polarity.
577    pub polarity: bool,
578    /// Which pair this describes.
579    pub pair: u8,
580    /// Measured skew.
581    pub skew: u32,
582    /// Measured length.
583    pub length: u32,
584}
585
586impl From<UtpCableStatus> for mxr_cable_status_t {
587    fn from(c: UtpCableStatus) -> Self {
588        Self {
589            polarity: c.polarity,
590            pair: c.pair,
591            skew: c.skew,
592            length: c.length,
593        }
594    }
595}
596
597/// The link state and diagnostics of one network port.
598#[repr(C)]
599#[derive(Clone, Copy)]
600pub struct mxr_network_port_t {
601    /// Port number.
602    pub port: u16,
603    /// Port name.
604    pub name: [c_char; MXR_NAME_LEN],
605    /// Negotiated link speed.
606    pub link_speed: u8,
607    /// Whether the link negotiated full duplex.
608    pub link_full_duplex: bool,
609    /// The port's own address, empty when it has not reported one.
610    pub ip: [c_char; MXR_IP_STRING_LEN],
611    /// The IGMP querier the port sees, empty when it sees none.
612    pub querier: [c_char; MXR_IP_STRING_LEN],
613    /// Whether the port reported a hardware address.
614    pub has_mac_address: bool,
615    /// The hardware address, meaningful only when `has_mac_address` is set.
616    pub mac_address: [u8; 6],
617    /// Whether the port reported link errors.
618    pub has_errors: bool,
619    /// Input errors.
620    pub in_error: bool,
621    /// Input frame check errors.
622    pub in_fcs_error: bool,
623    /// Input collisions.
624    pub in_collision: bool,
625    /// Deferred transmissions.
626    pub out_deferred: bool,
627    /// Excessive transmissions.
628    pub out_excessive: bool,
629    /// Polarity errors.
630    pub polarity_error: bool,
631    /// Skew warning.
632    pub skew_warning: bool,
633    /// Length warning.
634    pub length_warning: bool,
635    /// Whether the port reported a virtual cable test.
636    pub has_vct_status: bool,
637    /// Whether each pair raised a warning, meaningful only when
638    /// `has_vct_status` is set.
639    pub vct_warning: [bool; MXR_UTP_PAIRS],
640    /// How many entries of `cable_status` the port filled in.
641    pub cable_status_count: usize,
642    /// Cable diagnostics per pair.
643    pub cable_status: [mxr_cable_status_t; MXR_UTP_PAIRS],
644}
645
646/// One device in a topology report.
647#[repr(C)]
648#[derive(Clone, Copy)]
649pub struct mxr_topology_entry_t {
650    /// The device this entry describes.
651    pub uid: mxr_uid_t,
652    /// Bitmask of the devices it is connected to.
653    pub mask: u32,
654}
655
656impl From<TopologyEntry> for mxr_topology_entry_t {
657    fn from(e: TopologyEntry) -> Self {
658        Self {
659            uid: e.uid.into(),
660            mask: e.mask,
661        }
662    }
663}
664
665/// One firmware component a device reports.
666#[repr(C)]
667#[derive(Clone, Copy)]
668pub struct mxr_firmware_version_t {
669    /// Which component this describes.
670    pub firmware_type: u8,
671    /// Build timestamp, in seconds since the Unix epoch.
672    pub timestamp: u32,
673    /// Source revision hash.
674    pub hash: u32,
675    /// Human-readable version string.
676    pub version: [c_char; MXR_VERSION_LEN],
677}
678
679/// A ProAmp8's Dolby settings.
680#[repr(C)]
681#[derive(Clone, Copy)]
682pub struct mxr_dolby_settings_t {
683    /// 0 = standard, 1 = 3-zone Dolby, 2 = 4-zone Dolby.
684    pub mode: u8,
685    /// Whether PCM is up-mixed to 5.1 rather than passed through.
686    pub pcm_upmix: bool,
687    /// Whether a Dolby stream was detected.
688    pub dolby_detected: bool,
689    /// Whether up-mixing is currently running.
690    pub pcm_upmix_active: bool,
691}
692
693impl From<AmpDolbySettings> for mxr_dolby_settings_t {
694    fn from(s: AmpDolbySettings) -> Self {
695        Self {
696            mode: s.mode,
697            pcm_upmix: s.pcm_upmix,
698            dolby_detected: s.dolby_detected,
699            pcm_upmix_active: s.pcm_upmix_active,
700        }
701    }
702}
703
704/// The remote-control configuration of a source bay.
705#[repr(C)]
706#[derive(Clone, Copy)]
707pub struct mxr_rc_settings_t {
708    /// The device this configuration belongs to.
709    pub target: mxr_uid_t,
710    /// The control method, as the wire value.
711    ///
712    /// Zero is infrared, a method a bay really uses, so it is not a stand-in
713    /// for "not reported". Check that `mxr_rc_settings()` returned `MXR_OK`
714    /// before reading this: a device that has not sent its settings yet
715    /// leaves the struct as the caller allocated it, and a zeroed one then
716    /// reads as a bay set to infrared. `mxr_bay_info_t` answers the same
717    /// question with a `has_rc_type` flag beside its `rc_type`.
718    pub rc_target: u8,
719    /// The control target's address, empty when unset.
720    pub ip: [c_char; MXR_IP_STRING_LEN],
721    /// Whether CEC is enabled.
722    pub cec_enabled: bool,
723    /// Whether CEC powers the sink on automatically.
724    pub cec_auto_on: bool,
725    /// Whether remote-control commands are forwarded.
726    pub forward_rc: bool,
727    /// Whether infrared is forwarded.
728    pub forward_ir: bool,
729    /// The driver state on the source, as the wire value. One above the last
730    /// this library knows is passed through as it arrived.
731    pub rc_status: u8,
732    /// The driver-reported status string, empty when unknown.
733    pub status_name: [c_char; MXR_NAME_LEN],
734}
735
736/// Writes an address into a fixed-width field, leaving it empty when there is
737/// none.
738fn put_ip(dst: &mut [c_char], ip: Option<Ipv4Addr>) {
739    put_str(dst, &ip.map(|ip| ip.to_string()).unwrap_or_default());
740}
741
742/// Declares a getter for one subsystem of a device.
743///
744/// Each has the same three answers - no such device, the device has not sent
745/// this, here it is - and writing them out once keeps a getter that answers
746/// differently visible as one.
747/// Writes a subsystem reading through `out`, or reports why there is none.
748///
749/// # Safety
750///
751/// `out` is null or points at a writable `T`.
752unsafe fn fill<T>(
753    r: &mxr_remote_t,
754    uid: mxr_uid_t,
755    out: *mut T,
756    what: &str,
757    value: Option<T>,
758) -> mxr_result_t {
759    if out.is_null() {
760        return null_out(what);
761    }
762    match value {
763        Some(value) => {
764            // SAFETY: the caller guarantees a writable T, and it is not null.
765            unsafe { *out = value };
766            mxr_result_t::MXR_OK
767        }
768        None => not_reported(r, uid, what),
769    }
770}
771
772/// Reports why a subsystem read found nothing: no such device, or a device
773/// that has not sent this.
774fn not_reported(r: &mxr_remote_t, uid: mxr_uid_t, what: &str) -> mxr_result_t {
775    if r.remote.device(uid.into()).is_none() {
776        return not_heard_from(uid);
777    }
778    fail(
779        mxr_result_t::MXR_ERR_NOT_REPORTED,
780        &format!("the device has reported no {what}"),
781    )
782}
783
784/// Fills `out` with a device's V2IP statistics.
785///
786/// A device sends these only while subscribed; see
787/// `mxr_subscribe_v2ip_stats()`.
788///
789/// # Safety
790///
791/// `remote` is null or a live handle, and `out` points at a writable
792/// [`mxr_v2ip_stats_t`].
793#[no_mangle]
794pub unsafe extern "C" fn mxr_v2ip_stats(
795    remote: *const mxr_remote_t,
796    uid: mxr_uid_t,
797    out: *mut mxr_v2ip_stats_t,
798) -> mxr_result_t {
799    // SAFETY: the caller guarantees a live handle or null.
800    let handle = unsafe { remote.as_ref() };
801    with(handle, |r| {
802        let value = r
803            .remote
804            .v2ip_stats(uid.into())
805            .map(|s: V2ipDeviceStats| mxr_v2ip_stats_t {
806                tx: s.tx.into(),
807                tx_per_minute: s.tx_per_minute.into(),
808                rx: s.rx.into(),
809                rx_per_minute: s.rx_per_minute.into(),
810                decoder: s.decoder.into(),
811            });
812        // SAFETY: the caller guarantees a writable mxr_v2ip_stats_t or null.
813        unsafe { fill(r, uid, out, "V2IP statistics", value) }
814    })
815}
816
817/// Fills `out` with a V2IP device's own encoder configuration.
818///
819/// # Safety
820///
821/// `remote` is null or a live handle, and `out` points at a writable
822/// [`mxr_v2ip_details_t`].
823#[no_mangle]
824pub unsafe extern "C" fn mxr_v2ip_details(
825    remote: *const mxr_remote_t,
826    uid: mxr_uid_t,
827    out: *mut mxr_v2ip_details_t,
828) -> mxr_result_t {
829    // SAFETY: the caller guarantees a live handle or null.
830    let handle = unsafe { remote.as_ref() };
831    with(handle, |r| {
832        let value = r
833            .remote
834            .v2ip_details(uid.into())
835            .map(|d: DeviceV2ipDetails| mxr_v2ip_details_t {
836                video: d.video.into(),
837                audio: d.audio.into(),
838                anc: d.anc.into(),
839                arc: d.arc.into(),
840                // A rate and a marking are both bytes on the wire, so -1 cannot
841                // collide with a value a device could report.
842                tx_rate: d.tx_rate.map_or(-1, i16::from),
843                dscp_video: d.dscp.video.map_or(-1, i16::from),
844                dscp_audio: d.dscp.audio.map_or(-1, i16::from),
845                dscp_anc: d.dscp.anc.map_or(-1, i16::from),
846                scaling_mode: d.scaling.mode.to_wire(),
847                scaling_refresh: d.scaling.refresh,
848                scaling_flags: d.scaling.flags,
849            });
850        // SAFETY: the caller guarantees a writable mxr_v2ip_details_t or null.
851        unsafe { fill(r, uid, out, "V2IP encoder configuration", value) }
852    })
853}
854
855/// Fills `out` with the streams a V2IP sink is subscribed to.
856///
857/// # Safety
858///
859/// `remote` is null or a live handle, and `out` points at a writable
860/// [`mxr_v2ip_sink_t`].
861#[no_mangle]
862pub unsafe extern "C" fn mxr_v2ip_sink(
863    remote: *const mxr_remote_t,
864    uid: mxr_uid_t,
865    out: *mut mxr_v2ip_sink_t,
866) -> mxr_result_t {
867    // SAFETY: the caller guarantees a live handle or null.
868    let handle = unsafe { remote.as_ref() };
869    with(handle, |r| {
870        let value = r
871            .remote
872            .v2ip_sink(uid.into())
873            .map(|s: DeviceV2ipSink| mxr_v2ip_sink_t {
874                addresses: s.addresses.into(),
875                has_audio_format: s.audio_fmt.is_some(),
876                audio_format: {
877                    let f = s.audio_fmt.unwrap_or_default();
878                    mxr_audio_format_t {
879                        sample_rate: f.sample_rate,
880                        channels: f.channels,
881                    }
882                },
883            });
884        // SAFETY: the caller guarantees a writable mxr_v2ip_sink_t or null.
885        unsafe { fill(r, uid, out, "V2IP sink route", value) }
886    })
887}
888
889/// Fills `out` with what a V2IP device's video processor supports.
890///
891/// Reports `MXR_ERR_NOT_REPORTED` while the device has not said: a processor
892/// that has yet to answer and one with none of the optional commands send the
893/// same empty mask, so neither is reported as a capability set.
894///
895/// # Safety
896///
897/// `remote` is null or a live handle, and `out` points at a writable
898/// `uint64_t`.
899#[no_mangle]
900pub unsafe extern "C" fn mxr_v2ip_features(
901    remote: *const mxr_remote_t,
902    uid: mxr_uid_t,
903    out: *mut u64,
904) -> mxr_result_t {
905    // SAFETY: the caller guarantees a live handle or null.
906    let handle = unsafe { remote.as_ref() };
907    with(handle, |r| {
908        let value = r
909            .remote
910            .v2ip_features(uid.into())
911            .map(V2ipFpgaFeature::bits);
912        // SAFETY: the caller guarantees a writable uint64_t or null.
913        unsafe { fill(r, uid, out, "processor features", value) }
914    })
915}
916
917/// A V2IP device's settings.
918///
919/// A setting is reported only when its `MXR_V2IP_SETTING_*` bit is set in
920/// `valid`, and a device reports every setting it has.
921#[repr(C)]
922#[derive(Clone, Copy)]
923pub struct mxr_v2ip_device_settings_t {
924    /// `MXR_V2IP_SETTING_*` bits of the settings reported so far.
925    pub valid: u32,
926    /// `MXR_V2IP_SETTING_*` values of the on/off settings among `valid`.
927    pub flags: u32,
928    /// The infrared profiles stored on the device, bit n for profile n, when
929    /// `valid` has `MXR_V2IP_SETTING_IR_PROFILES`.
930    pub ir_profiles: u32,
931    /// The global infrared port's profile, when `valid` has
932    /// `MXR_V2IP_SETTING_IR_PROFILE`.
933    pub ir_profile: i8,
934    /// The output infrared port's profile, when `valid` has
935    /// `MXR_V2IP_SETTING_IR_PROFILE_SINK`. `MXR_V2IP_IR_PROFILE_NOT_SET` means
936    /// it follows the global one.
937    pub ir_profile_sink: i8,
938}
939
940/// The output infrared port follows the global one.
941pub const MXR_V2IP_IR_PROFILE_NOT_SET: i8 = -1;
942/// One past the highest infrared profile.
943pub const MXR_V2IP_IR_PROFILE_MAX: i8 = 18;
944
945// The header carries these as literals, so they are held to the core's here.
946const _: () = assert!(MXR_V2IP_IR_PROFILE_NOT_SET == mx_remote::V2IP_IR_PROFILE_NOT_SET);
947const _: () = assert!(MXR_V2IP_IR_PROFILE_MAX == mx_remote::V2IP_IR_PROFILE_MAX);
948
949/// Fills `out` with a V2IP device's settings.
950///
951/// Reports `MXR_ERR_NOT_REPORTED` until the device has reported any. A change
952/// is announced through `on_device_update`.
953///
954/// # Safety
955///
956/// `remote` is null or a live handle, and `out` points at a writable
957/// `mxr_v2ip_device_settings_t`.
958#[no_mangle]
959pub unsafe extern "C" fn mxr_v2ip_device_settings(
960    remote: *const mxr_remote_t,
961    uid: mxr_uid_t,
962    out: *mut mxr_v2ip_device_settings_t,
963) -> mxr_result_t {
964    // SAFETY: the caller guarantees a live handle or null.
965    let handle = unsafe { remote.as_ref() };
966    with(handle, |r| {
967        let value = r
968            .remote
969            .v2ip_device_settings(uid.into())
970            .map(|s: V2ipDeviceSettings| mxr_v2ip_device_settings_t {
971                valid: s.valid.bits(),
972                flags: s.flags.bits(),
973                ir_profiles: s.ir_profiles,
974                ir_profile: s.ir_profile,
975                ir_profile_sink: s.ir_profile_sink,
976            });
977        // SAFETY: the caller guarantees a writable struct or null.
978        unsafe { fill(r, uid, out, "device settings", value) }
979    })
980}
981
982/// A V2IP device's power save settings, beside `mxr_v2ip_device_settings_t`
983/// rather than in it so that struct keeps its size.
984///
985/// Which of them the device has reported is in that struct's `valid`.
986#[repr(C)]
987#[derive(Clone, Copy)]
988pub struct mxr_v2ip_power_save_t {
989    /// The idle minutes before the device powers down by itself, 0 for never,
990    /// when `valid` has `MXR_V2IP_SETTING_AUTO_POWER_SAVE`.
991    pub auto_minutes: u16,
992    /// When each day's power save window starts, Monday first, in minutes
993    /// after midnight in the device's time zone, when `valid` has
994    /// `MXR_V2IP_SETTING_POWER_SAVE_SCHEDULE`.
995    pub start: [u16; 7],
996    /// When each day's window ends. A window ending before it starts runs
997    /// past midnight, and one ending where it starts means none that day.
998    pub end: [u16; 7],
999}
1000
1001/// Minutes in a day: every power save time is below this.
1002pub const MXR_V2IP_MINUTES_PER_DAY: u16 = 1440;
1003
1004const _: () = assert!(MXR_V2IP_MINUTES_PER_DAY == mx_remote::V2IP_MINUTES_PER_DAY);
1005
1006/// Fills `out` with a V2IP device's power save settings.
1007///
1008/// Reports `MXR_ERR_NOT_REPORTED` until the device has reported any settings;
1009/// `mxr_v2ip_device_settings()` says which of these it has.
1010///
1011/// # Safety
1012///
1013/// `remote` is null or a live handle, and `out` points at a writable
1014/// `mxr_v2ip_power_save_t`.
1015#[no_mangle]
1016pub unsafe extern "C" fn mxr_v2ip_power_save(
1017    remote: *const mxr_remote_t,
1018    uid: mxr_uid_t,
1019    out: *mut mxr_v2ip_power_save_t,
1020) -> mxr_result_t {
1021    // SAFETY: the caller guarantees a live handle or null.
1022    let handle = unsafe { remote.as_ref() };
1023    with(handle, |r| {
1024        let value = r
1025            .remote
1026            .v2ip_device_settings(uid.into())
1027            .map(|s: V2ipDeviceSettings| mxr_v2ip_power_save_t {
1028                auto_minutes: s.auto_power_save,
1029                start: s.power_save.start,
1030                end: s.power_save.end,
1031            });
1032        // SAFETY: the caller guarantees a writable struct or null.
1033        unsafe { fill(r, uid, out, "device settings", value) }
1034    })
1035}
1036
1037/// Bytes in `mxr_time_zone_t.zone`, its NUL included.
1038pub const MXR_TIME_ZONE_NAME_LEN: usize = 48;
1039/// Bytes in `mxr_time_zone_t.rule`, its NUL included.
1040pub const MXR_TIME_ZONE_RULE_LEN: usize = 64;
1041
1042const _: () = assert!(MXR_TIME_ZONE_NAME_LEN == mx_remote::TIME_ZONE_NAME_LEN);
1043const _: () = assert!(MXR_TIME_ZONE_RULE_LEN == mx_remote::TIME_ZONE_RULE_LEN);
1044
1045/// The time zone a device announces for its mesh. An empty name and rule mean
1046/// the controller has none, and the devices keep UTC.
1047#[repr(C)]
1048#[derive(Clone, Copy)]
1049pub struct mxr_time_zone_t {
1050    /// The IANA name, such as `Europe/Amsterdam`.
1051    pub zone: [c_char; MXR_TIME_ZONE_NAME_LEN],
1052    /// The POSIX TZ rule the devices keep time by.
1053    pub rule: [c_char; MXR_TIME_ZONE_RULE_LEN],
1054}
1055
1056/// Fills `out` with the time zone a device announced for its mesh.
1057///
1058/// Reports `MXR_ERR_NOT_REPORTED` until it has announced one; the mesh
1059/// controller does with every periodic broadcast. A change is announced
1060/// through `on_device_update`.
1061///
1062/// # Safety
1063///
1064/// `remote` is null or a live handle, and `out` points at a writable
1065/// `mxr_time_zone_t`.
1066#[no_mangle]
1067pub unsafe extern "C" fn mxr_time_zone(
1068    remote: *const mxr_remote_t,
1069    uid: mxr_uid_t,
1070    out: *mut mxr_time_zone_t,
1071) -> mxr_result_t {
1072    // SAFETY: the caller guarantees a live handle or null.
1073    let handle = unsafe { remote.as_ref() };
1074    with(handle, |r| {
1075        let value = r.remote.time_zone(uid.into()).map(|tz| {
1076            let mut out = mxr_time_zone_t {
1077                zone: [0; MXR_TIME_ZONE_NAME_LEN],
1078                rule: [0; MXR_TIME_ZONE_RULE_LEN],
1079            };
1080            put_str(&mut out.zone, &tz.zone);
1081            put_str(&mut out.rule, &tz.rule);
1082            out
1083        });
1084        // SAFETY: the caller guarantees a writable struct or null.
1085        unsafe { fill(r, uid, out, "time zone", value) }
1086    })
1087}
1088
1089/// Fills `out` with a device's clock as of now, in seconds since 1970 UTC:
1090/// the time it last announced, advanced by how long ago that arrived.
1091///
1092/// Reports `MXR_ERR_NOT_REPORTED` until it has announced one; the mesh
1093/// controller does with every periodic broadcast once its clock is set.
1094///
1095/// # Safety
1096///
1097/// `remote` is null or a live handle, and `out` points at a writable
1098/// `uint64_t`.
1099#[no_mangle]
1100pub unsafe extern "C" fn mxr_device_clock(
1101    remote: *const mxr_remote_t,
1102    uid: mxr_uid_t,
1103    out: *mut u64,
1104) -> mxr_result_t {
1105    // SAFETY: the caller guarantees a live handle or null.
1106    let handle = unsafe { remote.as_ref() };
1107    with(handle, |r| {
1108        let value = r
1109            .remote
1110            .device_clock(uid.into())
1111            .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
1112            .map(|d| d.as_secs());
1113        // SAFETY: the caller guarantees a writable uint64_t or null.
1114        unsafe { fill(r, uid, out, "clock", value) }
1115    })
1116}
1117
1118/// VLAN tagging on a V2IP device's uplink.
1119///
1120/// Only the device knows what it runs, so this is what it reported about
1121/// itself. A change written to it is applied at once and carries
1122/// `MXR_V2IP_VLAN_PENDING` until the mesh controller confirms it; unconfirmed,
1123/// the device reverts it.
1124#[repr(C)]
1125#[derive(Clone, Copy)]
1126pub struct mxr_v2ip_vlan_t {
1127    /// `MXR_V2IP_VLAN_*` bits.
1128    pub flags: u16,
1129    /// The VLAN id of the device's own traffic, 0 for untagged.
1130    pub device: u16,
1131    /// The VLAN id each downlink port carries, 0 to share the device's
1132    /// untagged traffic: the SFP port, the UTP port with PoE, then the UTP
1133    /// port.
1134    pub port: [u16; MXR_V2IP_VLAN_PORTS],
1135    /// The port pinned as the uplink as 1 + its index in `port`, 0 to detect
1136    /// it.
1137    pub uplink: u8,
1138    /// Reported by the device: the port in use as the uplink, as 1 + its
1139    /// index in `port`.
1140    pub active_uplink: u8,
1141    /// Reported by the device: the seconds before a pending configuration is
1142    /// reverted.
1143    pub revert_s: u8,
1144}
1145
1146/// The ports in `mxr_v2ip_vlan_t::port`.
1147pub const MXR_V2IP_VLAN_PORTS: usize = 3;
1148/// The index of the SFP port in `mxr_v2ip_vlan_t::port`.
1149pub const MXR_V2IP_VLAN_PORT_SFP: usize = 0;
1150/// The highest VLAN id: 0 means untagged, and 4095 is reserved by IEEE 802.1Q.
1151pub const MXR_V2IP_VLAN_ID_MAX: u16 = 4094;
1152
1153const _: () = assert!(MXR_V2IP_VLAN_PORTS == mx_remote::V2IP_VLAN_PORTS);
1154const _: () = assert!(MXR_V2IP_VLAN_PORT_SFP == mx_remote::V2IP_VLAN_PORT_SFP);
1155const _: () = assert!(MXR_V2IP_VLAN_ID_MAX == mx_remote::V2IP_VLAN_ID_MAX);
1156
1157/// Fills `out` with the VLAN configuration a V2IP device last reported.
1158///
1159/// Reports `MXR_ERR_NOT_REPORTED` until the device has reported one. A change
1160/// is announced through `on_device_update`.
1161///
1162/// # Safety
1163///
1164/// `remote` is null or a live handle, and `out` points at a writable
1165/// `mxr_v2ip_vlan_t`.
1166#[no_mangle]
1167pub unsafe extern "C" fn mxr_v2ip_vlan(
1168    remote: *const mxr_remote_t,
1169    uid: mxr_uid_t,
1170    out: *mut mxr_v2ip_vlan_t,
1171) -> mxr_result_t {
1172    // SAFETY: the caller guarantees a live handle or null.
1173    let handle = unsafe { remote.as_ref() };
1174    with(handle, |r| {
1175        let value = r
1176            .remote
1177            .v2ip_vlan(uid.into())
1178            .map(|v: V2ipVlan| mxr_v2ip_vlan_t {
1179                flags: v.flags.bits(),
1180                device: v.device,
1181                port: v.port,
1182                uplink: v.uplink,
1183                active_uplink: v.active_uplink,
1184                revert_s: v.revert_s,
1185            });
1186        // SAFETY: the caller guarantees a writable struct or null.
1187        unsafe { fill(r, uid, out, "VLAN configuration", value) }
1188    })
1189}
1190
1191/// A V2IP sink's test tone.
1192#[repr(C)]
1193#[derive(Clone, Copy)]
1194pub struct mxr_v2ip_test_tone_t {
1195    /// `MXR_V2IP_TONE_MODE_*`.
1196    pub mode: u8,
1197    /// The level, in dBFS, `MXR_V2IP_TONE_LEVEL_MIN` to 0.
1198    pub level: i8,
1199    /// The channels it plays on, 1 to `MXR_V2IP_TONE_CHANNELS_MAX`; a line-up
1200    /// tone needs 2 or more.
1201    pub channels: u8,
1202    /// The frequency, `MXR_V2IP_TONE_FREQ_MIN` to `MXR_V2IP_TONE_FREQ_MAX` Hz.
1203    pub freq: u16,
1204    /// The sample rate: 44100, 48000 or 96000 Hz.
1205    pub rate: u32,
1206}
1207
1208/// A V2IP sink's lip-sync flash: a pattern frame is marked every `period`,
1209/// the frame `lead` after a mark flashes white, and a beep tone starts
1210/// `offset` sample periods after it.
1211#[repr(C)]
1212#[derive(Clone, Copy)]
1213pub struct mxr_v2ip_test_sync_t {
1214    /// Every this many pattern frames is marked, 0 for none.
1215    pub period: u16,
1216    /// The frame this many after a mark flashes; below `period`, or 0 without
1217    /// one.
1218    pub lead: u16,
1219    /// Sample periods between a mark and the beep, at most
1220    /// `MXR_V2IP_SYNC_OFFSET_MAX`.
1221    pub offset: u32,
1222    /// How long the beep lasts, `MXR_V2IP_SYNC_BEEP_MS_MIN` to
1223    /// `MXR_V2IP_SYNC_BEEP_MS_MAX` ms.
1224    pub beep_ms: u16,
1225}
1226
1227/// The test card a V2IP sink last reported. The counters run free and wrap.
1228#[repr(C)]
1229#[derive(Clone, Copy)]
1230pub struct mxr_v2ip_testcard_t {
1231    /// `MXR_V2IP_TESTCARD_*` bits.
1232    pub flags: u8,
1233    /// `MXR_V2IP_TEST_PATTERN_*`.
1234    pub pattern: u8,
1235    /// The colour of a flat pattern, `0xRRGGBB`.
1236    pub colour: u32,
1237    /// The tone.
1238    pub tone: mxr_v2ip_test_tone_t,
1239    /// The lip-sync flash.
1240    pub sync: mxr_v2ip_test_sync_t,
1241    /// Pattern frames sent.
1242    pub frames: u32,
1243    /// Sample periods since the tone started.
1244    pub periods: u32,
1245    /// Lip-sync marks the tone has seen.
1246    pub marks: u32,
1247}
1248
1249/// The lowest test tone frequency, in Hz.
1250pub const MXR_V2IP_TONE_FREQ_MIN: u16 = 20;
1251/// The highest test tone frequency, in Hz.
1252pub const MXR_V2IP_TONE_FREQ_MAX: u16 = 20000;
1253/// The quietest test tone level, in dBFS.
1254pub const MXR_V2IP_TONE_LEVEL_MIN: i8 = -60;
1255/// The most channels a test tone plays on.
1256pub const MXR_V2IP_TONE_CHANNELS_MAX: u8 = 8;
1257/// The largest lip-sync beep offset, in sample periods.
1258pub const MXR_V2IP_SYNC_OFFSET_MAX: u32 = 0xFFFFFF;
1259/// The shortest lip-sync beep, in milliseconds.
1260pub const MXR_V2IP_SYNC_BEEP_MS_MIN: u16 = 1;
1261/// The longest lip-sync beep, in milliseconds.
1262pub const MXR_V2IP_SYNC_BEEP_MS_MAX: u16 = 10000;
1263
1264const _: () = assert!(MXR_V2IP_TONE_FREQ_MIN == mx_remote::V2IP_TONE_FREQ_MIN);
1265const _: () = assert!(MXR_V2IP_TONE_FREQ_MAX == mx_remote::V2IP_TONE_FREQ_MAX);
1266const _: () = assert!(MXR_V2IP_TONE_LEVEL_MIN == mx_remote::V2IP_TONE_LEVEL_MIN);
1267const _: () = assert!(MXR_V2IP_TONE_CHANNELS_MAX == mx_remote::V2IP_TONE_CHANNELS_MAX);
1268const _: () = assert!(MXR_V2IP_SYNC_OFFSET_MAX == mx_remote::V2IP_SYNC_OFFSET_MAX);
1269const _: () = assert!(MXR_V2IP_SYNC_BEEP_MS_MIN == mx_remote::V2IP_SYNC_BEEP_MS_MIN);
1270const _: () = assert!(MXR_V2IP_SYNC_BEEP_MS_MAX == mx_remote::V2IP_SYNC_BEEP_MS_MAX);
1271
1272impl From<V2ipTestTone> for mxr_v2ip_test_tone_t {
1273    fn from(t: V2ipTestTone) -> Self {
1274        Self {
1275            mode: t.mode.to_wire(),
1276            level: t.level,
1277            channels: t.channels,
1278            freq: t.freq,
1279            rate: t.rate,
1280        }
1281    }
1282}
1283
1284impl From<mxr_v2ip_test_tone_t> for V2ipTestTone {
1285    fn from(t: mxr_v2ip_test_tone_t) -> Self {
1286        Self {
1287            mode: V2ipToneMode::from_wire(t.mode),
1288            freq: t.freq,
1289            level: t.level,
1290            channels: t.channels,
1291            rate: t.rate,
1292        }
1293    }
1294}
1295
1296impl From<V2ipTestSync> for mxr_v2ip_test_sync_t {
1297    fn from(s: V2ipTestSync) -> Self {
1298        Self {
1299            period: s.period,
1300            lead: s.lead,
1301            offset: s.offset,
1302            beep_ms: s.beep_ms,
1303        }
1304    }
1305}
1306
1307impl From<mxr_v2ip_test_sync_t> for V2ipTestSync {
1308    fn from(s: mxr_v2ip_test_sync_t) -> Self {
1309        Self {
1310            period: s.period,
1311            lead: s.lead,
1312            offset: s.offset,
1313            beep_ms: s.beep_ms,
1314        }
1315    }
1316}
1317
1318/// Fills `out` with the test card a V2IP sink last reported.
1319///
1320/// Reports `MXR_ERR_NOT_REPORTED` until the sink has answered
1321/// `mxr_request_v2ip_testcard()` or a change. A change is announced through
1322/// `on_device_update`.
1323///
1324/// # Safety
1325///
1326/// `remote` is null or a live handle, and `out` points at a writable
1327/// `mxr_v2ip_testcard_t`.
1328#[no_mangle]
1329pub unsafe extern "C" fn mxr_v2ip_testcard(
1330    remote: *const mxr_remote_t,
1331    uid: mxr_uid_t,
1332    out: *mut mxr_v2ip_testcard_t,
1333) -> mxr_result_t {
1334    // SAFETY: the caller guarantees a live handle or null.
1335    let handle = unsafe { remote.as_ref() };
1336    with(handle, |r| {
1337        let value = r
1338            .remote
1339            .v2ip_testcard(uid.into())
1340            .map(|t: V2ipTestcard| mxr_v2ip_testcard_t {
1341                flags: t.flags.bits(),
1342                pattern: t.pattern.to_wire(),
1343                colour: t.colour,
1344                tone: t.tone.into(),
1345                sync: t.sync.into(),
1346                frames: t.frames,
1347                periods: t.periods,
1348                marks: t.marks,
1349            });
1350        // SAFETY: the caller guarantees a writable struct or null.
1351        unsafe { fill(r, uid, out, "test card", value) }
1352    })
1353}
1354
1355/// Fills `out` with the window a sink is told to show.
1356///
1357/// # Safety
1358///
1359/// `remote` is null or a live handle, and `out` points at a writable
1360/// [`mxr_tiling_config_t`].
1361#[no_mangle]
1362pub unsafe extern "C" fn mxr_v2ip_tiling(
1363    remote: *const mxr_remote_t,
1364    uid: mxr_uid_t,
1365    out: *mut mxr_tiling_config_t,
1366) -> mxr_result_t {
1367    // SAFETY: the caller guarantees a live handle or null.
1368    let handle = unsafe { remote.as_ref() };
1369    with(handle, |r| {
1370        let value =
1371            r.remote
1372                .v2ip_tiling(uid.into())
1373                .map(|t: V2ipTilingConfig| mxr_tiling_config_t {
1374                    target: t.target.into(),
1375                    pos_x: t.pos_x,
1376                    pos_y: t.pos_y,
1377                    width: t.width,
1378                    height: t.height,
1379                });
1380        // SAFETY: the caller guarantees a writable mxr_tiling_config_t or null.
1381        unsafe { fill(r, uid, out, "window", value) }
1382    })
1383}
1384
1385/// Fills `out` with what a multiviewer reports about itself.
1386///
1387/// # Safety
1388///
1389/// `remote` is null or a live handle, and `out` points at a writable
1390/// [`mxr_multiviewer_status_t`].
1391#[no_mangle]
1392pub unsafe extern "C" fn mxr_multiviewer_status(
1393    remote: *const mxr_remote_t,
1394    uid: mxr_uid_t,
1395    out: *mut mxr_multiviewer_status_t,
1396) -> mxr_result_t {
1397    // SAFETY: the caller guarantees a live handle or null.
1398    let handle = unsafe { remote.as_ref() };
1399    with(handle, |r| {
1400        let value = r.remote.multiviewer_status(uid.into()).map(multiviewer_of);
1401        // SAFETY: the caller guarantees a writable mxr_multiviewer_status_t or null.
1402        unsafe { fill(r, uid, out, "multiviewer status", value) }
1403    })
1404}
1405
1406/// Copies a multiviewer's report into the C shape.
1407fn multiviewer_of(s: MultiviewerStatus) -> mxr_multiviewer_status_t {
1408    let mut out = mxr_multiviewer_status_t {
1409        uid: s.uid.into(),
1410        mappings: [mxr_uid_t::default(); MXR_MULTIVIEWER_INPUTS],
1411        mcu_version: [0; MXR_NAME_LEN],
1412        scaler_version: [0; MXR_NAME_LEN],
1413        hw_view_mode: s.hw_view_mode,
1414        view_mode: s.view_mode.to_wire(),
1415        pip_position: s.pip_position.to_wire(),
1416        pip_size: s.pip_size.to_wire(),
1417        output_mode: s.output_mode.to_wire(),
1418        hdcp_mode: s.hdcp_mode.to_wire(),
1419        output_itc: s.output_itc.to_wire(),
1420        edid_template: s.edid_template.to_wire(),
1421        aspect_ratio: s.aspect_ratio.to_wire(),
1422        auto_switch: s.auto_switch.to_wire(),
1423        audio_source: s.audio_source.to_wire(),
1424        has_audio_volume: s.audio_volume.is_some(),
1425        audio_volume: s.audio_volume.unwrap_or(0),
1426        audio_muted: s.audio_muted.to_wire(),
1427        video_sources: [0; MXR_MULTIVIEWER_INPUTS],
1428        remote_control: s.remote_control.to_wire(),
1429    };
1430    for (slot, uid) in out.mappings.iter_mut().zip(s.mappings) {
1431        *slot = uid.into();
1432    }
1433    for (slot, source) in out.video_sources.iter_mut().zip(s.video_sources) {
1434        *slot = source.to_wire();
1435    }
1436    put_str(&mut out.mcu_version, &s.mcu_version);
1437    put_str(&mut out.scaler_version, &s.scaler_version);
1438    out
1439}
1440
1441/// Fills `out` with a ProAmp8's Dolby settings.
1442///
1443/// # Safety
1444///
1445/// `remote` is null or a live handle, and `out` points at a writable
1446/// [`mxr_dolby_settings_t`].
1447#[no_mangle]
1448pub unsafe extern "C" fn mxr_dolby_settings(
1449    remote: *const mxr_remote_t,
1450    uid: mxr_uid_t,
1451    out: *mut mxr_dolby_settings_t,
1452) -> mxr_result_t {
1453    // SAFETY: the caller guarantees a live handle or null.
1454    let handle = unsafe { remote.as_ref() };
1455    with(handle, |r| {
1456        let value = r
1457            .remote
1458            .dolby_settings(uid.into())
1459            .map(mxr_dolby_settings_t::from);
1460        // SAFETY: the caller guarantees a writable mxr_dolby_settings_t or null.
1461        unsafe { fill(r, uid, out, "Dolby settings", value) }
1462    })
1463}
1464
1465/// Fills `out` with a source bay's remote-control configuration.
1466///
1467/// # Safety
1468///
1469/// `remote` is null or a live handle, and `out` points at a writable
1470/// [`mxr_rc_settings_t`].
1471#[no_mangle]
1472pub unsafe extern "C" fn mxr_rc_settings(
1473    remote: *const mxr_remote_t,
1474    uid: mxr_uid_t,
1475    out: *mut mxr_rc_settings_t,
1476) -> mxr_result_t {
1477    // SAFETY: the caller guarantees a live handle or null.
1478    let handle = unsafe { remote.as_ref() };
1479    with(handle, |r| {
1480        let value = r.remote.rc_settings(uid.into()).map(rc_settings_of);
1481        // SAFETY: the caller guarantees a writable mxr_rc_settings_t or null.
1482        unsafe { fill(r, uid, out, "remote-control configuration", value) }
1483    })
1484}
1485
1486/// Copies a remote-control configuration into the C shape.
1487fn rc_settings_of(s: RcSettings) -> mxr_rc_settings_t {
1488    let mut out = mxr_rc_settings_t {
1489        target: s.target.into(),
1490        rc_target: s.rc_target,
1491        ip: [0; MXR_IP_STRING_LEN],
1492        cec_enabled: s.cec_enabled,
1493        cec_auto_on: s.cec_auto_on,
1494        forward_rc: s.forward_rc,
1495        forward_ir: s.forward_ir,
1496        rc_status: s.rc_status,
1497        status_name: [0; MXR_NAME_LEN],
1498    };
1499    put_ip(&mut out.ip, s.ip);
1500    put_str(&mut out.status_name, &s.status_name);
1501    out
1502}
1503
1504/// Writes the streams a device's source bays advertise, and returns how many
1505/// there are.
1506///
1507/// Returns the full count even when it exceeds `cap`, so calling with `cap`
1508/// zero sizes the buffer.
1509///
1510/// # Safety
1511///
1512/// `remote` is null or a live handle, and `out` is null or points at `cap`
1513/// writable [`mxr_stream_sources_t`].
1514#[no_mangle]
1515pub unsafe extern "C" fn mxr_v2ip_sources(
1516    remote: *const mxr_remote_t,
1517    uid: mxr_uid_t,
1518    out: *mut mxr_stream_sources_t,
1519    cap: usize,
1520) -> usize {
1521    guard(0, || {
1522        // SAFETY: the caller guarantees a live handle or null.
1523        let Some(r) = (unsafe { remote.as_ref() }) else {
1524            return no_handle();
1525        };
1526        let Some(sources) = r.remote.v2ip_sources(uid.into()) else {
1527            not_reported(r, uid, "V2IP stream sources");
1528            return 0;
1529        };
1530        // SAFETY: the caller guarantees cap writable elements at out.
1531        unsafe { copy_into(&sources, out, cap) }
1532    })
1533}
1534
1535/// Writes a device's network ports, and returns how many there are.
1536///
1537/// # Safety
1538///
1539/// `remote` is null or a live handle, and `out` is null or points at `cap`
1540/// writable [`mxr_network_port_t`].
1541#[no_mangle]
1542pub unsafe extern "C" fn mxr_network_status(
1543    remote: *const mxr_remote_t,
1544    uid: mxr_uid_t,
1545    out: *mut mxr_network_port_t,
1546    cap: usize,
1547) -> usize {
1548    guard(0, || {
1549        // SAFETY: the caller guarantees a live handle or null.
1550        let Some(r) = (unsafe { remote.as_ref() }) else {
1551            return no_handle();
1552        };
1553        let ports: Vec<mxr_network_port_t> = r
1554            .remote
1555            .network_status(uid.into())
1556            .iter()
1557            .map(port_of)
1558            .collect();
1559        // SAFETY: the caller guarantees cap writable elements at out.
1560        unsafe { copy_into(&ports, out, cap) }
1561    })
1562}
1563
1564/// Copies one port report into the C shape.
1565fn port_of(p: &NetworkPortStatus) -> mxr_network_port_t {
1566    let errors = p.errors.unwrap_or_default();
1567    let mut out = mxr_network_port_t {
1568        port: p.port,
1569        name: [0; MXR_NAME_LEN],
1570        link_speed: p.link_speed.to_wire(),
1571        link_full_duplex: p.link_full_duplex,
1572        ip: [0; MXR_IP_STRING_LEN],
1573        querier: [0; MXR_IP_STRING_LEN],
1574        has_mac_address: p.mac_address.is_some(),
1575        mac_address: p.mac_address.unwrap_or_default().0,
1576        has_errors: p.errors.is_some(),
1577        in_error: errors.in_error,
1578        in_fcs_error: errors.in_fcs_error,
1579        in_collision: errors.in_collision,
1580        out_deferred: errors.out_deferred,
1581        out_excessive: errors.out_excessive,
1582        polarity_error: errors.polarity_error,
1583        skew_warning: errors.skew_warning,
1584        length_warning: errors.length_warning,
1585        has_vct_status: p.vct_status.is_some(),
1586        vct_warning: [false; MXR_UTP_PAIRS],
1587        cable_status_count: p.cable_status.len().min(MXR_UTP_PAIRS),
1588        cable_status: [mxr_cable_status_t {
1589            polarity: false,
1590            pair: 0,
1591            skew: 0,
1592            length: 0,
1593        }; MXR_UTP_PAIRS],
1594    };
1595    put_str(&mut out.name, &p.name);
1596    put_ip(&mut out.ip, p.ip);
1597    put_ip(&mut out.querier, p.querier);
1598    if let Some(vct) = p.vct_status {
1599        for (slot, status) in out.vct_warning.iter_mut().zip(vct) {
1600            *slot = status == VctStatus::Warning;
1601        }
1602    }
1603    for (slot, cable) in out.cable_status.iter_mut().zip(&p.cable_status) {
1604        *slot = (*cable).into();
1605    }
1606    out
1607}
1608
1609/// Writes a device's view of the mesh topology, and returns how many entries
1610/// there are.
1611///
1612/// # Safety
1613///
1614/// `remote` is null or a live handle, and `out` is null or points at `cap`
1615/// writable [`mxr_topology_entry_t`].
1616#[no_mangle]
1617pub unsafe extern "C" fn mxr_topology(
1618    remote: *const mxr_remote_t,
1619    uid: mxr_uid_t,
1620    out: *mut mxr_topology_entry_t,
1621    cap: usize,
1622) -> usize {
1623    guard(0, || {
1624        // SAFETY: the caller guarantees a live handle or null.
1625        let Some(r) = (unsafe { remote.as_ref() }) else {
1626            return no_handle();
1627        };
1628        let topology = r.remote.topology(uid.into());
1629        // SAFETY: the caller guarantees cap writable elements at out.
1630        unsafe { copy_into(&topology, out, cap) }
1631    })
1632}
1633
1634/// Writes the firmware versions a device reports, and returns how many there
1635/// are.
1636///
1637/// # Safety
1638///
1639/// `remote` is null or a live handle, and `out` is null or points at `cap`
1640/// writable [`mxr_firmware_version_t`].
1641#[no_mangle]
1642pub unsafe extern "C" fn mxr_device_firmware(
1643    remote: *const mxr_remote_t,
1644    uid: mxr_uid_t,
1645    out: *mut mxr_firmware_version_t,
1646    cap: usize,
1647) -> usize {
1648    guard(0, || {
1649        // SAFETY: the caller guarantees a live handle or null.
1650        let Some(r) = (unsafe { remote.as_ref() }) else {
1651            return no_handle();
1652        };
1653        let versions: Vec<mxr_firmware_version_t> = r
1654            .remote
1655            .firmware(uid.into())
1656            .iter()
1657            .map(|(_, v)| firmware_of(v))
1658            .collect();
1659        // SAFETY: the caller guarantees cap writable elements at out.
1660        unsafe { copy_into(&versions, out, cap) }
1661    })
1662}
1663
1664/// Copies one firmware report into the C shape.
1665fn firmware_of(v: &FirmwareVersion) -> mxr_firmware_version_t {
1666    let mut out = mxr_firmware_version_t {
1667        firmware_type: v.firmware_type.to_wire(),
1668        timestamp: v.timestamp,
1669        hash: v.hash,
1670        version: [0; MXR_VERSION_LEN],
1671    };
1672    put_str(&mut out.version, &v.version);
1673    out
1674}
1675
1676/// Writes a device's audio endpoints, in the order it reported them, and
1677/// returns how many there are.
1678///
1679/// # Safety
1680///
1681/// `remote` is null or a live handle, and `out` is null or points at `cap`
1682/// writable [`mxr_audio_endpoint_t`].
1683#[no_mangle]
1684pub unsafe extern "C" fn mxr_audio_endpoints(
1685    remote: *const mxr_remote_t,
1686    uid: mxr_uid_t,
1687    out: *mut mxr_audio_endpoint_t,
1688    cap: usize,
1689) -> usize {
1690    guard(0, || {
1691        // SAFETY: the caller guarantees a live handle or null.
1692        let Some(r) = (unsafe { remote.as_ref() }) else {
1693            return no_handle();
1694        };
1695        let Some(endpoints) = r.remote.audio_endpoints(uid.into()) else {
1696            not_reported(r, uid, "audio endpoints");
1697            return 0;
1698        };
1699        let list: Vec<mxr_audio_endpoint_t> = endpoints.list().map(Into::into).collect();
1700        // SAFETY: the caller guarantees cap writable elements at out.
1701        unsafe { copy_into(&list, out, cap) }
1702    })
1703}
1704
1705/// Fills `out` with the state an audio endpoint reports, on the bits of its
1706/// features: `MXR_AUDIO_MUTE` while it is muted, `MXR_AUDIO_TRIGGER` while
1707/// its trigger is active, and `MXR_AUDIO_AUDIO_LOCK` while its source is
1708/// locked. Beside `mxr_audio_endpoint_t` rather than in it, so that struct
1709/// keeps its size.
1710///
1711/// Reports `MXR_ERR_NOT_REPORTED` until the device has reported its audio
1712/// endpoints, and `MXR_ERR_NOT_FOUND` for an endpoint it did not report.
1713///
1714/// # Safety
1715///
1716/// `remote` is null or a live handle, and `out` points at a writable
1717/// `uint32_t`.
1718#[no_mangle]
1719pub unsafe extern "C" fn mxr_audio_endpoint_status(
1720    remote: *const mxr_remote_t,
1721    uid: mxr_uid_t,
1722    endpoint: u8,
1723    out: *mut u32,
1724) -> mxr_result_t {
1725    // SAFETY: the caller guarantees a live handle or null.
1726    let handle = unsafe { remote.as_ref() };
1727    with(handle, |r| {
1728        if out.is_null() {
1729            return null_out("audio endpoint status");
1730        }
1731        let Some(endpoints) = r.remote.audio_endpoints(uid.into()) else {
1732            return not_reported(r, uid, "audio endpoints");
1733        };
1734        let Some(status) = endpoints.status(endpoint) else {
1735            return fail(
1736                mxr_result_t::MXR_ERR_NOT_FOUND,
1737                "the device reported no such audio endpoint",
1738            );
1739        };
1740        // SAFETY: the caller guarantees a writable uint32_t or null.
1741        unsafe { fill(r, uid, out, "audio endpoint status", Some(status.bits())) }
1742    })
1743}
1744
1745/// Writes the endpoints hanging off one audio endpoint, and returns how many
1746/// there are.
1747///
1748/// # Safety
1749///
1750/// `remote` is null or a live handle, and `out` is null or points at `cap`
1751/// writable bytes.
1752#[no_mangle]
1753pub unsafe extern "C" fn mxr_audio_endpoint_children(
1754    remote: *const mxr_remote_t,
1755    uid: mxr_uid_t,
1756    endpoint: u8,
1757    out: *mut u8,
1758    cap: usize,
1759) -> usize {
1760    guard(0, || {
1761        // SAFETY: the caller guarantees a live handle or null.
1762        let Some(r) = (unsafe { remote.as_ref() }) else {
1763            return no_handle();
1764        };
1765        let children = match r.remote.audio_endpoints(uid.into()) {
1766            Some(endpoints) => match endpoints.get(endpoint) {
1767                Some(e) => e.children.clone(),
1768                None => {
1769                    fail(
1770                        mxr_result_t::MXR_ERR_NOT_FOUND,
1771                        &format!("the device has no audio endpoint {endpoint}"),
1772                    );
1773                    return 0;
1774                }
1775            },
1776            None => {
1777                not_reported(r, uid, "audio endpoints");
1778                return 0;
1779            }
1780        };
1781        // SAFETY: the caller guarantees cap writable bytes at out.
1782        unsafe { copy_into(&children, out, cap) }
1783    })
1784}
1785
1786/// Reports a null handle from a call whose answer is a count.
1787fn no_handle() -> usize {
1788    fail(
1789        mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1790        "the client handle is null",
1791    );
1792    0
1793}