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