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