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