Skip to main content

mx_remote_ffi/
subsystems.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! What a device reports about one of its subsystems.
5//!
6//! These are the values that do not fit in a device or bay snapshot: streams,
7//! statistics, the audio tree, the network ports. Each has an event that says
8//! it moved and a call here that says what it is now, which is why the events
9//! carry only an identifier - what they would carry instead is this, and a
10//! copy taken at event time could only be staler than a read.
11//!
12//! A call returns `MXR_ERR_NOT_REPORTED` when the device exists but has not
13//! sent that subsystem, which is a different answer from a device that has
14//! never been heard from at all.
15
16use std::ffi::c_char;
17use std::net::Ipv4Addr;
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 - as reason 10, or
274/// from an older sender as reason 1, which is the same reading a sink whose
275/// source has died produces. Nothing here answers whether a sink is enabled.
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 the exception and is read here: it outranks the
298    /// whole word, and testing it first is what stops a switched-off sink
299    /// being reported as broken. Reason 9 is the one most often hidden here —
300    /// see `flags`.
301    ///
302    /// A pending switch is a step in an operation someone asked for rather
303    /// than a fault, and PTP unlocked costs audio alone: audio cannot enable
304    /// and the picture is unaffected, so reporting it as a fault puts an
305    /// overlay over a good picture. Reason 10 says the sink is switched off,
306    /// and carries no implication about `width` and `height`: those are read
307    /// before any cause is decided, so a switched-off sink still detecting a
308    /// codestream reports a real geometry. An older sender reports reason 1
309    /// for the same sink, so an absent reason 10 is not evidence a sink is
310    /// enabled - nothing here answers that, which comes from
311    /// `mxr_v2ip_details()` or the device's HTTP status.
312    pub reason: u8,
313    /// The converter watchdog is holding the stream back.
314    pub blocking: bool,
315    /// The recovered picture width, and 0 when none was recovered.
316    pub width: u16,
317    /// The recovered picture height, and 0 when none was recovered.
318    pub height: u16,
319    /// The recovered colour space: 0 RGB, 1 YCbCr 4:4:4, 2 YCbCr 4:2:2,
320    /// 3 YCbCr 4:2:0, 255 the decoder cannot name it.
321    ///
322    /// No value here means "no signal": a decoder with nothing to decode
323    /// reports 0, which is indistinguishable from a real RGB reading. A zero
324    /// `width` or `height` is what says the decoder recovered nothing - which
325    /// is not the same as the sink being switched off, and does not imply it. The 255
326    /// is its own value rather than the 0xF a signal report uses for an
327    /// unknown colour space.
328    pub format: u16,
329    /// How many readings the sink has stored. Monotonic, wrapping at 65535
330    /// after some 36 hours, and never reset.
331    ///
332    /// A sink reads its video processor every two seconds and reports every
333    /// second, so roughly every other report repeats a reading already seen:
334    /// a frame arriving says nothing about how fresh the values in it are.
335    /// This counter moves only when a reading is stored, so a processor that
336    /// stopped answering leaves it still rather than implying a refresh.
337    ///
338    /// After pointing a sink at something else, wait for this to advance by
339    /// two before trusting the geometry. It ticks when a reply lands rather
340    /// than when a query is sent, so the first tick can carry an answer the
341    /// processor read fractionally before the switch; the second cannot,
342    /// because at most one query is outstanding at a time.
343    pub updates: u16,
344    /// Every cause that applies, as bit N for reason N, where `reason` carries
345    /// the primary one. Bit 0 is cleared by the sender, so an empty word means
346    /// nothing beyond the primary cause applies.
347    ///
348    /// This is what to classify on, once reason 10 has been ruled out. That
349    /// cause outranks the whole word and leaves the bits below it set - a sink
350    /// switched off while running keeps the bits the decoder genuinely saw on
351    /// the way down - so a fault mask over `flags` reports a deliberately
352    /// disabled sink as broken.
353    ///
354    /// A cause that is true can be missing from
355    /// `reason` and present here: reason 9, the pipeline rebuilding after the
356    /// transmitter bridge stayed unlocked, sits below every input-side cause,
357    /// so a pipeline restarting in a loop shows an input-side cause in
358    /// `reason` and bit 9 here alone - always, rather than briefly. Bit 9
359    /// needs a sustained five seconds to appear at all, and sustained across
360    /// reports it means a restart loop rather than one event, because the
361    /// sender's debounce restarts each time it elapses.
362    ///
363    /// Reasons 3 and 4 are the two arms of one decision and never appear
364    /// together.
365    pub flags: u32,
366    /// How many times the converter watchdog has triggered.
367    pub blocked_count: u32,
368}
369
370impl From<V2ipDecoderDetail> for mxr_v2ip_decoder_t {
371    fn from(detail: V2ipDecoderDetail) -> Self {
372        let empty = Self {
373            detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ABSENT,
374            reason: 0,
375            blocking: false,
376            width: 0,
377            height: 0,
378            format: 0,
379            updates: 0,
380            flags: 0,
381            blocked_count: 0,
382        };
383        match detail {
384            V2ipDecoderDetail::Absent => empty,
385            V2ipDecoderDetail::NeverAnswered => Self {
386                detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_NEVER_ANSWERED,
387                ..empty
388            },
389            V2ipDecoderDetail::Answered(r) => Self {
390                detail: mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ANSWERED,
391                reason: r.reason.to_wire(),
392                blocking: r.blocking,
393                width: r.width,
394                height: r.height,
395                format: r.format.to_wire(),
396                updates: r.updates,
397                flags: r.flags,
398                blocked_count: r.blocked_count,
399            },
400        }
401    }
402}
403
404/// A device's V2IP statistics, cumulative and over the last minute.
405#[repr(C)]
406#[derive(Clone, Copy)]
407pub struct mxr_v2ip_stats_t {
408    /// Transmit totals since boot.
409    pub tx: mxr_v2ip_tx_stats_t,
410    /// Transmit counts over the last minute.
411    pub tx_per_minute: mxr_v2ip_tx_stats_t,
412    /// Receive totals since boot.
413    pub rx: mxr_v2ip_rx_stats_t,
414    /// Receive counts over the last minute.
415    pub rx_per_minute: mxr_v2ip_rx_stats_t,
416    /// What the sink's decoder recovered from the codestream it is decoding.
417    pub decoder: mxr_v2ip_decoder_t,
418}
419
420/// The window a sink is currently told to show.
421///
422/// This is the pollable view of a sink's window, not the persisted video wall
423/// setting: on a sink running the wall module a write here is transient,
424/// because that module pushes its own target window back within about a
425/// second.
426#[repr(C)]
427#[derive(Clone, Copy)]
428pub struct mxr_tiling_config_t {
429    /// The sink this window belongs to.
430    pub target: mxr_uid_t,
431    /// Window origin, horizontal.
432    pub pos_x: u16,
433    /// Window origin, vertical.
434    pub pos_y: u16,
435    /// Window width.
436    pub width: u16,
437    /// Window height.
438    pub height: u16,
439}
440
441/// What a multiviewer reports about itself.
442#[repr(C)]
443#[derive(Clone, Copy)]
444pub struct mxr_multiviewer_status_t {
445    /// The multiviewer.
446    pub uid: mxr_uid_t,
447    /// The source device mapped to each input.
448    pub mappings: [mxr_uid_t; MXR_MULTIVIEWER_INPUTS],
449    /// The MCU firmware version.
450    pub mcu_version: [c_char; MXR_NAME_LEN],
451    /// The scaler firmware version.
452    pub scaler_version: [c_char; MXR_NAME_LEN],
453    /// The view mode the hardware reports, which is its own numbering rather
454    /// than `view_mode`'s.
455    pub hw_view_mode: u8,
456    /// The window layout.
457    pub view_mode: u8,
458    /// Which corner the picture-in-picture window sits in.
459    pub pip_position: u8,
460    /// The size of the picture-in-picture window.
461    pub pip_size: u8,
462    /// The output resolution.
463    pub output_mode: u8,
464    /// The HDCP mode.
465    pub hdcp_mode: u8,
466    /// The IT content flag.
467    pub output_itc: u8,
468    /// The EDID presented to sources.
469    pub edid_template: u8,
470    /// How a source is fitted into its window.
471    pub aspect_ratio: u8,
472    /// Whether automatic source switching is on.
473    pub auto_switch: u8,
474    /// Which window the audio is taken from.
475    pub audio_source: u8,
476    /// Whether a volume has been reported.
477    pub has_audio_volume: bool,
478    /// The output volume.
479    pub audio_volume: u8,
480    /// Whether the output is muted.
481    pub audio_muted: u8,
482    /// The source shown in each window.
483    pub video_sources: [u8; MXR_MULTIVIEWER_INPUTS],
484    /// Which window remote control is forwarded to.
485    pub remote_control: u8,
486}
487
488/// One node of a device's audio tree.
489#[repr(C)]
490#[derive(Clone, Copy)]
491pub struct mxr_audio_endpoint_t {
492    /// The endpoint's identifier on its device.
493    pub id: u8,
494    /// What the endpoint can do, as `MXR_AUDIO_*` bits.
495    pub features: u32,
496    /// Whether the endpoint carries a stream address.
497    pub has_address: bool,
498    /// The stream address, meaningful only when `has_address` is set.
499    pub address: mxr_stream_source_t,
500    /// The endpoint this one hangs off, or -1 at a root.
501    pub parent: i16,
502    /// How many children this endpoint has; read them with
503    /// `mxr_audio_endpoint_children()`.
504    pub child_count: usize,
505    /// Whether the device reported which inputs are selectable.
506    pub has_inputs_available: bool,
507    /// Bitmask of the endpoints this one may be switched to.
508    pub inputs_available: u32,
509    /// Whether the device reported which input is selected.
510    pub has_inputs_routed: bool,
511    /// Bitmask of the endpoint this one is listening to.
512    pub inputs_routed: u32,
513    /// The device at the other end of the link, zero when unlinked.
514    pub linked_device: mxr_uid_t,
515    /// The endpoint at the other end of the link, or -1 when unlinked.
516    pub linked_endpoint: i16,
517}
518
519impl From<&AudioEndpoint> for mxr_audio_endpoint_t {
520    fn from(e: &AudioEndpoint) -> Self {
521        Self {
522            id: e.id,
523            features: e.features.bits(),
524            has_address: e.address.is_some(),
525            address: e.address.unwrap_or_default().into(),
526            // An endpoint id is a byte on the wire, so -1 cannot collide.
527            parent: e.parent.map_or(-1, i16::from),
528            child_count: e.children.len(),
529            has_inputs_available: e.inputs_available.is_some(),
530            inputs_available: e.inputs_available.unwrap_or(0),
531            has_inputs_routed: e.inputs_routed.is_some(),
532            inputs_routed: e.inputs_routed.unwrap_or(0),
533            linked_device: e.linked_device.into(),
534            linked_endpoint: e.linked_endpoint.map_or(-1, i16::from),
535        }
536    }
537}
538
539/// The diagnostic result for one UTP cable pair.
540#[repr(C)]
541#[derive(Clone, Copy)]
542pub struct mxr_cable_status_t {
543    /// Whether the pair is wired with normal polarity.
544    pub polarity: bool,
545    /// Which pair this describes.
546    pub pair: u8,
547    /// Measured skew.
548    pub skew: u32,
549    /// Measured length.
550    pub length: u32,
551}
552
553impl From<UtpCableStatus> for mxr_cable_status_t {
554    fn from(c: UtpCableStatus) -> Self {
555        Self {
556            polarity: c.polarity,
557            pair: c.pair,
558            skew: c.skew,
559            length: c.length,
560        }
561    }
562}
563
564/// The link state and diagnostics of one network port.
565#[repr(C)]
566#[derive(Clone, Copy)]
567pub struct mxr_network_port_t {
568    /// Port number.
569    pub port: u16,
570    /// Port name.
571    pub name: [c_char; MXR_NAME_LEN],
572    /// Negotiated link speed.
573    pub link_speed: u8,
574    /// Whether the link negotiated full duplex.
575    pub link_full_duplex: bool,
576    /// The port's own address, empty when it has not reported one.
577    pub ip: [c_char; MXR_IP_STRING_LEN],
578    /// The IGMP querier the port sees, empty when it sees none.
579    pub querier: [c_char; MXR_IP_STRING_LEN],
580    /// Whether the port reported a hardware address.
581    pub has_mac_address: bool,
582    /// The hardware address, meaningful only when `has_mac_address` is set.
583    pub mac_address: [u8; 6],
584    /// Whether the port reported link errors.
585    pub has_errors: bool,
586    /// Input errors.
587    pub in_error: bool,
588    /// Input frame check errors.
589    pub in_fcs_error: bool,
590    /// Input collisions.
591    pub in_collision: bool,
592    /// Deferred transmissions.
593    pub out_deferred: bool,
594    /// Excessive transmissions.
595    pub out_excessive: bool,
596    /// Polarity errors.
597    pub polarity_error: bool,
598    /// Skew warning.
599    pub skew_warning: bool,
600    /// Length warning.
601    pub length_warning: bool,
602    /// Whether the port reported a virtual cable test.
603    pub has_vct_status: bool,
604    /// Whether each pair raised a warning, meaningful only when
605    /// `has_vct_status` is set.
606    pub vct_warning: [bool; MXR_UTP_PAIRS],
607    /// How many entries of `cable_status` the port filled in.
608    pub cable_status_count: usize,
609    /// Cable diagnostics per pair.
610    pub cable_status: [mxr_cable_status_t; MXR_UTP_PAIRS],
611}
612
613/// One device in a topology report.
614#[repr(C)]
615#[derive(Clone, Copy)]
616pub struct mxr_topology_entry_t {
617    /// The device this entry describes.
618    pub uid: mxr_uid_t,
619    /// Bitmask of the devices it is connected to.
620    pub mask: u32,
621}
622
623impl From<TopologyEntry> for mxr_topology_entry_t {
624    fn from(e: TopologyEntry) -> Self {
625        Self {
626            uid: e.uid.into(),
627            mask: e.mask,
628        }
629    }
630}
631
632/// One firmware component a device reports.
633#[repr(C)]
634#[derive(Clone, Copy)]
635pub struct mxr_firmware_version_t {
636    /// Which component this describes.
637    pub firmware_type: u8,
638    /// Build timestamp, in seconds since the Unix epoch.
639    pub timestamp: u32,
640    /// Source revision hash.
641    pub hash: u32,
642    /// Human-readable version string.
643    pub version: [c_char; MXR_VERSION_LEN],
644}
645
646/// A ProAmp8's Dolby settings.
647#[repr(C)]
648#[derive(Clone, Copy)]
649pub struct mxr_dolby_settings_t {
650    /// 0 = standard, 1 = 3-zone Dolby, 2 = 4-zone Dolby.
651    pub mode: u8,
652    /// Whether PCM is up-mixed to 5.1 rather than passed through.
653    pub pcm_upmix: bool,
654    /// Whether a Dolby stream was detected.
655    pub dolby_detected: bool,
656    /// Whether up-mixing is currently running.
657    pub pcm_upmix_active: bool,
658}
659
660impl From<AmpDolbySettings> for mxr_dolby_settings_t {
661    fn from(s: AmpDolbySettings) -> Self {
662        Self {
663            mode: s.mode,
664            pcm_upmix: s.pcm_upmix,
665            dolby_detected: s.dolby_detected,
666            pcm_upmix_active: s.pcm_upmix_active,
667        }
668    }
669}
670
671/// The remote-control configuration of a source bay.
672#[repr(C)]
673#[derive(Clone, Copy)]
674pub struct mxr_rc_settings_t {
675    /// The device this configuration belongs to.
676    pub target: mxr_uid_t,
677    /// The control method, as the wire value.
678    ///
679    /// Zero is infrared, a method a bay really uses, so it is not a stand-in
680    /// for "not reported". Check that `mxr_rc_settings()` returned `MXR_OK`
681    /// before reading this: a device that has not sent its settings yet
682    /// leaves the struct as the caller allocated it, and a zeroed one then
683    /// reads as a bay set to infrared. `mxr_bay_info_t` answers the same
684    /// question with a `has_rc_type` flag beside its `rc_type`.
685    pub rc_target: u8,
686    /// The control target's address, empty when unset.
687    pub ip: [c_char; MXR_IP_STRING_LEN],
688    /// Whether CEC is enabled.
689    pub cec_enabled: bool,
690    /// Whether CEC powers the sink on automatically.
691    pub cec_auto_on: bool,
692    /// Whether remote-control commands are forwarded.
693    pub forward_rc: bool,
694    /// Whether infrared is forwarded.
695    pub forward_ir: bool,
696    /// The driver state on the source, as the wire value. One above the last
697    /// this library knows is passed through as it arrived.
698    pub rc_status: u8,
699    /// The driver-reported status string, empty when unknown.
700    pub status_name: [c_char; MXR_NAME_LEN],
701}
702
703/// Writes an address into a fixed-width field, leaving it empty when there is
704/// none.
705fn put_ip(dst: &mut [c_char], ip: Option<Ipv4Addr>) {
706    put_str(dst, &ip.map(|ip| ip.to_string()).unwrap_or_default());
707}
708
709/// Declares a getter for one subsystem of a device.
710///
711/// Each has the same three answers - no such device, the device has not sent
712/// this, here it is - and writing them out once keeps a getter that answers
713/// differently visible as one.
714/// Writes a subsystem reading through `out`, or reports why there is none.
715///
716/// # Safety
717///
718/// `out` is null or points at a writable `T`.
719unsafe fn fill<T>(
720    r: &mxr_remote_t,
721    uid: mxr_uid_t,
722    out: *mut T,
723    what: &str,
724    value: Option<T>,
725) -> mxr_result_t {
726    if out.is_null() {
727        return null_out(what);
728    }
729    match value {
730        Some(value) => {
731            // SAFETY: the caller guarantees a writable T, and it is not null.
732            unsafe { *out = value };
733            mxr_result_t::MXR_OK
734        }
735        None => not_reported(r, uid, what),
736    }
737}
738
739/// Reports why a subsystem read found nothing: no such device, or a device
740/// that has not sent this.
741fn not_reported(r: &mxr_remote_t, uid: mxr_uid_t, what: &str) -> mxr_result_t {
742    if r.remote.device(uid.into()).is_none() {
743        return not_heard_from(uid);
744    }
745    fail(
746        mxr_result_t::MXR_ERR_NOT_REPORTED,
747        &format!("the device has reported no {what}"),
748    )
749}
750
751/// Fills `out` with a device's V2IP statistics.
752///
753/// A device sends these only while subscribed; see
754/// `mxr_subscribe_v2ip_stats()`.
755///
756/// # Safety
757///
758/// `remote` is null or a live handle, and `out` points at a writable
759/// [`mxr_v2ip_stats_t`].
760#[no_mangle]
761pub unsafe extern "C" fn mxr_v2ip_stats(
762    remote: *const mxr_remote_t,
763    uid: mxr_uid_t,
764    out: *mut mxr_v2ip_stats_t,
765) -> mxr_result_t {
766    // SAFETY: the caller guarantees a live handle or null.
767    let handle = unsafe { remote.as_ref() };
768    with(handle, |r| {
769        let value = r
770            .remote
771            .v2ip_stats(uid.into())
772            .map(|s: V2ipDeviceStats| mxr_v2ip_stats_t {
773                tx: s.tx.into(),
774                tx_per_minute: s.tx_per_minute.into(),
775                rx: s.rx.into(),
776                rx_per_minute: s.rx_per_minute.into(),
777                decoder: s.decoder.into(),
778            });
779        // SAFETY: the caller guarantees a writable mxr_v2ip_stats_t or null.
780        unsafe { fill(r, uid, out, "V2IP statistics", value) }
781    })
782}
783
784/// Fills `out` with a V2IP device's own encoder configuration.
785///
786/// # Safety
787///
788/// `remote` is null or a live handle, and `out` points at a writable
789/// [`mxr_v2ip_details_t`].
790#[no_mangle]
791pub unsafe extern "C" fn mxr_v2ip_details(
792    remote: *const mxr_remote_t,
793    uid: mxr_uid_t,
794    out: *mut mxr_v2ip_details_t,
795) -> mxr_result_t {
796    // SAFETY: the caller guarantees a live handle or null.
797    let handle = unsafe { remote.as_ref() };
798    with(handle, |r| {
799        let value = r
800            .remote
801            .v2ip_details(uid.into())
802            .map(|d: DeviceV2ipDetails| mxr_v2ip_details_t {
803                video: d.video.into(),
804                audio: d.audio.into(),
805                anc: d.anc.into(),
806                arc: d.arc.into(),
807                // A rate and a marking are both bytes on the wire, so -1 cannot
808                // collide with a value a device could report.
809                tx_rate: d.tx_rate.map_or(-1, i16::from),
810                dscp_video: d.dscp.video.map_or(-1, i16::from),
811                dscp_audio: d.dscp.audio.map_or(-1, i16::from),
812                dscp_anc: d.dscp.anc.map_or(-1, i16::from),
813                scaling_mode: d.scaling.mode.to_wire(),
814                scaling_refresh: d.scaling.refresh,
815                scaling_flags: d.scaling.flags,
816            });
817        // SAFETY: the caller guarantees a writable mxr_v2ip_details_t or null.
818        unsafe { fill(r, uid, out, "V2IP encoder configuration", value) }
819    })
820}
821
822/// Fills `out` with the streams a V2IP sink is subscribed to.
823///
824/// # Safety
825///
826/// `remote` is null or a live handle, and `out` points at a writable
827/// [`mxr_v2ip_sink_t`].
828#[no_mangle]
829pub unsafe extern "C" fn mxr_v2ip_sink(
830    remote: *const mxr_remote_t,
831    uid: mxr_uid_t,
832    out: *mut mxr_v2ip_sink_t,
833) -> mxr_result_t {
834    // SAFETY: the caller guarantees a live handle or null.
835    let handle = unsafe { remote.as_ref() };
836    with(handle, |r| {
837        let value = r
838            .remote
839            .v2ip_sink(uid.into())
840            .map(|s: DeviceV2ipSink| mxr_v2ip_sink_t {
841                addresses: s.addresses.into(),
842                has_audio_format: s.audio_fmt.is_some(),
843                audio_format: {
844                    let f = s.audio_fmt.unwrap_or_default();
845                    mxr_audio_format_t {
846                        sample_rate: f.sample_rate,
847                        channels: f.channels,
848                    }
849                },
850            });
851        // SAFETY: the caller guarantees a writable mxr_v2ip_sink_t or null.
852        unsafe { fill(r, uid, out, "V2IP sink route", value) }
853    })
854}
855
856/// Fills `out` with the window a sink is told to show.
857///
858/// # Safety
859///
860/// `remote` is null or a live handle, and `out` points at a writable
861/// [`mxr_tiling_config_t`].
862#[no_mangle]
863pub unsafe extern "C" fn mxr_v2ip_tiling(
864    remote: *const mxr_remote_t,
865    uid: mxr_uid_t,
866    out: *mut mxr_tiling_config_t,
867) -> mxr_result_t {
868    // SAFETY: the caller guarantees a live handle or null.
869    let handle = unsafe { remote.as_ref() };
870    with(handle, |r| {
871        let value =
872            r.remote
873                .v2ip_tiling(uid.into())
874                .map(|t: V2ipTilingConfig| mxr_tiling_config_t {
875                    target: t.target.into(),
876                    pos_x: t.pos_x,
877                    pos_y: t.pos_y,
878                    width: t.width,
879                    height: t.height,
880                });
881        // SAFETY: the caller guarantees a writable mxr_tiling_config_t or null.
882        unsafe { fill(r, uid, out, "window", value) }
883    })
884}
885
886/// Fills `out` with what a multiviewer reports about itself.
887///
888/// # Safety
889///
890/// `remote` is null or a live handle, and `out` points at a writable
891/// [`mxr_multiviewer_status_t`].
892#[no_mangle]
893pub unsafe extern "C" fn mxr_multiviewer_status(
894    remote: *const mxr_remote_t,
895    uid: mxr_uid_t,
896    out: *mut mxr_multiviewer_status_t,
897) -> mxr_result_t {
898    // SAFETY: the caller guarantees a live handle or null.
899    let handle = unsafe { remote.as_ref() };
900    with(handle, |r| {
901        let value = r.remote.multiviewer_status(uid.into()).map(multiviewer_of);
902        // SAFETY: the caller guarantees a writable mxr_multiviewer_status_t or null.
903        unsafe { fill(r, uid, out, "multiviewer status", value) }
904    })
905}
906
907/// Copies a multiviewer's report into the C shape.
908fn multiviewer_of(s: MultiviewerStatus) -> mxr_multiviewer_status_t {
909    let mut out = mxr_multiviewer_status_t {
910        uid: s.uid.into(),
911        mappings: [mxr_uid_t::default(); MXR_MULTIVIEWER_INPUTS],
912        mcu_version: [0; MXR_NAME_LEN],
913        scaler_version: [0; MXR_NAME_LEN],
914        hw_view_mode: s.hw_view_mode,
915        view_mode: s.view_mode.to_wire(),
916        pip_position: s.pip_position.to_wire(),
917        pip_size: s.pip_size.to_wire(),
918        output_mode: s.output_mode.to_wire(),
919        hdcp_mode: s.hdcp_mode.to_wire(),
920        output_itc: s.output_itc.to_wire(),
921        edid_template: s.edid_template.to_wire(),
922        aspect_ratio: s.aspect_ratio.to_wire(),
923        auto_switch: s.auto_switch.to_wire(),
924        audio_source: s.audio_source.to_wire(),
925        has_audio_volume: s.audio_volume.is_some(),
926        audio_volume: s.audio_volume.unwrap_or(0),
927        audio_muted: s.audio_muted.to_wire(),
928        video_sources: [0; MXR_MULTIVIEWER_INPUTS],
929        remote_control: s.remote_control.to_wire(),
930    };
931    for (slot, uid) in out.mappings.iter_mut().zip(s.mappings) {
932        *slot = uid.into();
933    }
934    for (slot, source) in out.video_sources.iter_mut().zip(s.video_sources) {
935        *slot = source.to_wire();
936    }
937    put_str(&mut out.mcu_version, &s.mcu_version);
938    put_str(&mut out.scaler_version, &s.scaler_version);
939    out
940}
941
942/// Fills `out` with a ProAmp8's Dolby settings.
943///
944/// # Safety
945///
946/// `remote` is null or a live handle, and `out` points at a writable
947/// [`mxr_dolby_settings_t`].
948#[no_mangle]
949pub unsafe extern "C" fn mxr_dolby_settings(
950    remote: *const mxr_remote_t,
951    uid: mxr_uid_t,
952    out: *mut mxr_dolby_settings_t,
953) -> mxr_result_t {
954    // SAFETY: the caller guarantees a live handle or null.
955    let handle = unsafe { remote.as_ref() };
956    with(handle, |r| {
957        let value = r
958            .remote
959            .dolby_settings(uid.into())
960            .map(mxr_dolby_settings_t::from);
961        // SAFETY: the caller guarantees a writable mxr_dolby_settings_t or null.
962        unsafe { fill(r, uid, out, "Dolby settings", value) }
963    })
964}
965
966/// Fills `out` with a source bay's remote-control configuration.
967///
968/// # Safety
969///
970/// `remote` is null or a live handle, and `out` points at a writable
971/// [`mxr_rc_settings_t`].
972#[no_mangle]
973pub unsafe extern "C" fn mxr_rc_settings(
974    remote: *const mxr_remote_t,
975    uid: mxr_uid_t,
976    out: *mut mxr_rc_settings_t,
977) -> mxr_result_t {
978    // SAFETY: the caller guarantees a live handle or null.
979    let handle = unsafe { remote.as_ref() };
980    with(handle, |r| {
981        let value = r.remote.rc_settings(uid.into()).map(rc_settings_of);
982        // SAFETY: the caller guarantees a writable mxr_rc_settings_t or null.
983        unsafe { fill(r, uid, out, "remote-control configuration", value) }
984    })
985}
986
987/// Copies a remote-control configuration into the C shape.
988fn rc_settings_of(s: RcSettings) -> mxr_rc_settings_t {
989    let mut out = mxr_rc_settings_t {
990        target: s.target.into(),
991        rc_target: s.rc_target,
992        ip: [0; MXR_IP_STRING_LEN],
993        cec_enabled: s.cec_enabled,
994        cec_auto_on: s.cec_auto_on,
995        forward_rc: s.forward_rc,
996        forward_ir: s.forward_ir,
997        rc_status: s.rc_status,
998        status_name: [0; MXR_NAME_LEN],
999    };
1000    put_ip(&mut out.ip, s.ip);
1001    put_str(&mut out.status_name, &s.status_name);
1002    out
1003}
1004
1005/// Writes the streams a device's source bays advertise, and returns how many
1006/// there are.
1007///
1008/// Returns the full count even when it exceeds `cap`, so calling with `cap`
1009/// zero sizes the buffer.
1010///
1011/// # Safety
1012///
1013/// `remote` is null or a live handle, and `out` is null or points at `cap`
1014/// writable [`mxr_stream_sources_t`].
1015#[no_mangle]
1016pub unsafe extern "C" fn mxr_v2ip_sources(
1017    remote: *const mxr_remote_t,
1018    uid: mxr_uid_t,
1019    out: *mut mxr_stream_sources_t,
1020    cap: usize,
1021) -> usize {
1022    guard(0, || {
1023        // SAFETY: the caller guarantees a live handle or null.
1024        let Some(r) = (unsafe { remote.as_ref() }) else {
1025            return no_handle();
1026        };
1027        let Some(sources) = r.remote.v2ip_sources(uid.into()) else {
1028            not_reported(r, uid, "V2IP stream sources");
1029            return 0;
1030        };
1031        // SAFETY: the caller guarantees cap writable elements at out.
1032        unsafe { copy_into(&sources, out, cap) }
1033    })
1034}
1035
1036/// Writes a device's network ports, and returns how many there are.
1037///
1038/// # Safety
1039///
1040/// `remote` is null or a live handle, and `out` is null or points at `cap`
1041/// writable [`mxr_network_port_t`].
1042#[no_mangle]
1043pub unsafe extern "C" fn mxr_network_status(
1044    remote: *const mxr_remote_t,
1045    uid: mxr_uid_t,
1046    out: *mut mxr_network_port_t,
1047    cap: usize,
1048) -> usize {
1049    guard(0, || {
1050        // SAFETY: the caller guarantees a live handle or null.
1051        let Some(r) = (unsafe { remote.as_ref() }) else {
1052            return no_handle();
1053        };
1054        let ports: Vec<mxr_network_port_t> = r
1055            .remote
1056            .network_status(uid.into())
1057            .iter()
1058            .map(port_of)
1059            .collect();
1060        // SAFETY: the caller guarantees cap writable elements at out.
1061        unsafe { copy_into(&ports, out, cap) }
1062    })
1063}
1064
1065/// Copies one port report into the C shape.
1066fn port_of(p: &NetworkPortStatus) -> mxr_network_port_t {
1067    let errors = p.errors.unwrap_or_default();
1068    let mut out = mxr_network_port_t {
1069        port: p.port,
1070        name: [0; MXR_NAME_LEN],
1071        link_speed: p.link_speed.to_wire(),
1072        link_full_duplex: p.link_full_duplex,
1073        ip: [0; MXR_IP_STRING_LEN],
1074        querier: [0; MXR_IP_STRING_LEN],
1075        has_mac_address: p.mac_address.is_some(),
1076        mac_address: p.mac_address.unwrap_or_default().0,
1077        has_errors: p.errors.is_some(),
1078        in_error: errors.in_error,
1079        in_fcs_error: errors.in_fcs_error,
1080        in_collision: errors.in_collision,
1081        out_deferred: errors.out_deferred,
1082        out_excessive: errors.out_excessive,
1083        polarity_error: errors.polarity_error,
1084        skew_warning: errors.skew_warning,
1085        length_warning: errors.length_warning,
1086        has_vct_status: p.vct_status.is_some(),
1087        vct_warning: [false; MXR_UTP_PAIRS],
1088        cable_status_count: p.cable_status.len().min(MXR_UTP_PAIRS),
1089        cable_status: [mxr_cable_status_t {
1090            polarity: false,
1091            pair: 0,
1092            skew: 0,
1093            length: 0,
1094        }; MXR_UTP_PAIRS],
1095    };
1096    put_str(&mut out.name, &p.name);
1097    put_ip(&mut out.ip, p.ip);
1098    put_ip(&mut out.querier, p.querier);
1099    if let Some(vct) = p.vct_status {
1100        for (slot, status) in out.vct_warning.iter_mut().zip(vct) {
1101            *slot = status == VctStatus::Warning;
1102        }
1103    }
1104    for (slot, cable) in out.cable_status.iter_mut().zip(&p.cable_status) {
1105        *slot = (*cable).into();
1106    }
1107    out
1108}
1109
1110/// Writes a device's view of the mesh topology, and returns how many entries
1111/// there are.
1112///
1113/// # Safety
1114///
1115/// `remote` is null or a live handle, and `out` is null or points at `cap`
1116/// writable [`mxr_topology_entry_t`].
1117#[no_mangle]
1118pub unsafe extern "C" fn mxr_topology(
1119    remote: *const mxr_remote_t,
1120    uid: mxr_uid_t,
1121    out: *mut mxr_topology_entry_t,
1122    cap: usize,
1123) -> usize {
1124    guard(0, || {
1125        // SAFETY: the caller guarantees a live handle or null.
1126        let Some(r) = (unsafe { remote.as_ref() }) else {
1127            return no_handle();
1128        };
1129        let topology = r.remote.topology(uid.into());
1130        // SAFETY: the caller guarantees cap writable elements at out.
1131        unsafe { copy_into(&topology, out, cap) }
1132    })
1133}
1134
1135/// Writes the firmware versions a device reports, and returns how many there
1136/// are.
1137///
1138/// # Safety
1139///
1140/// `remote` is null or a live handle, and `out` is null or points at `cap`
1141/// writable [`mxr_firmware_version_t`].
1142#[no_mangle]
1143pub unsafe extern "C" fn mxr_device_firmware(
1144    remote: *const mxr_remote_t,
1145    uid: mxr_uid_t,
1146    out: *mut mxr_firmware_version_t,
1147    cap: usize,
1148) -> usize {
1149    guard(0, || {
1150        // SAFETY: the caller guarantees a live handle or null.
1151        let Some(r) = (unsafe { remote.as_ref() }) else {
1152            return no_handle();
1153        };
1154        let versions: Vec<mxr_firmware_version_t> = r
1155            .remote
1156            .firmware(uid.into())
1157            .iter()
1158            .map(|(_, v)| firmware_of(v))
1159            .collect();
1160        // SAFETY: the caller guarantees cap writable elements at out.
1161        unsafe { copy_into(&versions, out, cap) }
1162    })
1163}
1164
1165/// Copies one firmware report into the C shape.
1166fn firmware_of(v: &FirmwareVersion) -> mxr_firmware_version_t {
1167    let mut out = mxr_firmware_version_t {
1168        firmware_type: v.firmware_type.to_wire(),
1169        timestamp: v.timestamp,
1170        hash: v.hash,
1171        version: [0; MXR_VERSION_LEN],
1172    };
1173    put_str(&mut out.version, &v.version);
1174    out
1175}
1176
1177/// Writes a device's audio endpoints, in the order it reported them, and
1178/// returns how many there are.
1179///
1180/// # Safety
1181///
1182/// `remote` is null or a live handle, and `out` is null or points at `cap`
1183/// writable [`mxr_audio_endpoint_t`].
1184#[no_mangle]
1185pub unsafe extern "C" fn mxr_audio_endpoints(
1186    remote: *const mxr_remote_t,
1187    uid: mxr_uid_t,
1188    out: *mut mxr_audio_endpoint_t,
1189    cap: usize,
1190) -> usize {
1191    guard(0, || {
1192        // SAFETY: the caller guarantees a live handle or null.
1193        let Some(r) = (unsafe { remote.as_ref() }) else {
1194            return no_handle();
1195        };
1196        let Some(endpoints) = r.remote.audio_endpoints(uid.into()) else {
1197            not_reported(r, uid, "audio endpoints");
1198            return 0;
1199        };
1200        let list: Vec<mxr_audio_endpoint_t> = endpoints.list().map(Into::into).collect();
1201        // SAFETY: the caller guarantees cap writable elements at out.
1202        unsafe { copy_into(&list, out, cap) }
1203    })
1204}
1205
1206/// Writes the endpoints hanging off one audio endpoint, and returns how many
1207/// there are.
1208///
1209/// # Safety
1210///
1211/// `remote` is null or a live handle, and `out` is null or points at `cap`
1212/// writable bytes.
1213#[no_mangle]
1214pub unsafe extern "C" fn mxr_audio_endpoint_children(
1215    remote: *const mxr_remote_t,
1216    uid: mxr_uid_t,
1217    endpoint: u8,
1218    out: *mut u8,
1219    cap: usize,
1220) -> usize {
1221    guard(0, || {
1222        // SAFETY: the caller guarantees a live handle or null.
1223        let Some(r) = (unsafe { remote.as_ref() }) else {
1224            return no_handle();
1225        };
1226        let children = match r.remote.audio_endpoints(uid.into()) {
1227            Some(endpoints) => match endpoints.get(endpoint) {
1228                Some(e) => e.children.clone(),
1229                None => {
1230                    fail(
1231                        mxr_result_t::MXR_ERR_NOT_FOUND,
1232                        &format!("the device has no audio endpoint {endpoint}"),
1233                    );
1234                    return 0;
1235                }
1236            },
1237            None => {
1238                not_reported(r, uid, "audio endpoints");
1239                return 0;
1240            }
1241        };
1242        // SAFETY: the caller guarantees cap writable bytes at out.
1243        unsafe { copy_into(&children, out, cap) }
1244    })
1245}
1246
1247/// Reports a null handle from a call whose answer is a count.
1248fn no_handle() -> usize {
1249    fail(
1250        mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1251        "the client handle is null",
1252    );
1253    0
1254}