Skip to main content

mx_remote_ffi/
subsystems.rs

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