Skip to main content

mx_remote_ffi/
subsystems.rs

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