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