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