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