Skip to main content

mx_remote_ffi/
control.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The control surface: what a caller can ask a device to do.
5//!
6//! Every call here returns `MXR_OK` only when a frame left the socket. There
7//! is nothing further to wait for and nothing to acknowledge: a device answers
8//! a command by reporting its new state a moment later, through the callbacks,
9//! so a caller that needs confirmation waits for the event rather than for the
10//! return.
11//!
12//! A device that speaks a protocol older than a command requires is refused
13//! with `MXR_ERR_PROTOCOL_TOO_OLD` and nothing is sent, because such a device
14//! discards the frame without answering and a send would report a success that
15//! changed nothing.
16
17use std::ffi::c_char;
18use std::time::{Duration, UNIX_EPOCH};
19
20use mx_remote::{
21    DeviceUid, EdidProfile, MultiviewerAspectRatio, MultiviewerEdidTemplate, MultiviewerHdcpMode,
22    MultiviewerItcMode, MultiviewerOutputMode, MultiviewerPipPosition, MultiviewerPipSize,
23    MultiviewerSource, MultiviewerViewMode, RcAction, RcKey, V2ipAudioFormat, V2ipColourSpace,
24    V2ipDeviceSetting, V2ipDeviceSettings, V2ipOutputMode, V2ipPowerSaveSchedule, V2ipRoute,
25    V2ipRouteTarget, V2ipTestPattern, V2ipVlan, V2ipVlanFlag, VideoWallWindow,
26};
27
28use crate::abi::{
29    fail, from_control, mxr_bay_uid_t, mxr_result_t, mxr_tribool_t, mxr_uid_t, opt_str, req_str,
30};
31use crate::info::mxr_amp_zone_settings_t;
32use crate::remote::{mxr_remote_t, with};
33use crate::subsystems::{
34    mxr_v2ip_device_settings_t, mxr_v2ip_power_save_t, mxr_v2ip_test_sync_t, mxr_v2ip_test_tone_t,
35    mxr_v2ip_vlan_t,
36};
37
38/// A stream's sample rate and channel count.
39#[repr(C)]
40#[derive(Clone, Copy)]
41pub struct mxr_audio_format_t {
42    /// Sample rate in Hz.
43    pub sample_rate: u32,
44    /// Channel count.
45    pub channels: u8,
46}
47
48impl From<mxr_audio_format_t> for V2ipAudioFormat {
49    fn from(f: mxr_audio_format_t) -> Self {
50        Self {
51            sample_rate: f.sample_rate,
52            channels: f.channels,
53        }
54    }
55}
56
57// ---- routing ----
58
59/// Routes a V2IP sink's video to the stream a source port advertises.
60///
61/// # Safety
62///
63/// `remote` is null or a live handle from `mxr_remote_new()`.
64#[no_mangle]
65pub unsafe extern "C" fn mxr_select_video_source(
66    remote: *const mxr_remote_t,
67    sink: mxr_bay_uid_t,
68    source_port: u16,
69) -> mxr_result_t {
70    // SAFETY: the caller guarantees a live handle or null.
71    let handle = unsafe { remote.as_ref() };
72    with(handle, |r| {
73        from_control(r.remote.select_video_source(sink.into(), source_port))
74    })
75}
76
77/// Routes a V2IP sink's audio to the stream a source port advertises,
78/// leaving its video where it is.
79///
80/// # Safety
81///
82/// `remote` is null or a live handle from `mxr_remote_new()`.
83#[no_mangle]
84pub unsafe extern "C" fn mxr_select_audio_source(
85    remote: *const mxr_remote_t,
86    sink: mxr_bay_uid_t,
87    source_port: u16,
88) -> mxr_result_t {
89    // SAFETY: the caller guarantees a live handle or null.
90    let handle = unsafe { remote.as_ref() };
91    with(handle, |r| {
92        from_control(r.remote.select_audio_source(sink.into(), source_port))
93    })
94}
95
96/// Routes a V2IP sink's video to the source bay with this user-assigned name.
97///
98/// # Safety
99///
100/// `remote` is null or a live handle, and `name` is a NUL-terminated string.
101#[no_mangle]
102pub unsafe extern "C" fn mxr_select_video_source_by_name(
103    remote: *const mxr_remote_t,
104    sink: mxr_bay_uid_t,
105    name: *const c_char,
106) -> mxr_result_t {
107    // SAFETY: the caller guarantees a live handle or null.
108    let handle = unsafe { remote.as_ref() };
109    with(handle, |r| {
110        // SAFETY: the caller guarantees a NUL-terminated string.
111        match unsafe { req_str(name) } {
112            Ok(name) => from_control(r.remote.select_video_source_by_name(sink.into(), name)),
113            Err(code) => code,
114        }
115    })
116}
117
118/// Routes a V2IP sink's audio to the source bay with this user-assigned name.
119///
120/// `format` may be null to leave the sink's audio format alone.
121///
122/// # Safety
123///
124/// `remote` is null or a live handle, `name` is a NUL-terminated string, and
125/// `format` is null or points at an initialised [`mxr_audio_format_t`].
126#[no_mangle]
127pub unsafe extern "C" fn mxr_select_audio_source_by_name(
128    remote: *const mxr_remote_t,
129    sink: mxr_bay_uid_t,
130    name: *const c_char,
131    format: *const mxr_audio_format_t,
132) -> mxr_result_t {
133    // SAFETY: the caller guarantees a live handle or null.
134    let handle = unsafe { remote.as_ref() };
135    with(handle, |r| {
136        // SAFETY: the caller guarantees a NUL-terminated string.
137        let name = match unsafe { req_str(name) } {
138            Ok(name) => name,
139            Err(code) => return code,
140        };
141        // SAFETY: the caller guarantees an initialised struct or null.
142        let format = unsafe { format.as_ref() }.map(|f| (*f).into());
143        from_control(
144            r.remote
145                .select_audio_source_by_name(sink.into(), name, format),
146        )
147    })
148}
149
150/// Routes a V2IP sink's audio to a multicast group directly, for a source this
151/// client has not heard advertise it.
152///
153/// `audio_port` may be zero for the default, and `format` may be null to leave
154/// the sink's audio format alone.
155///
156/// # Safety
157///
158/// `remote` is null or a live handle, `audio_ip` is a NUL-terminated dotted
159/// quad, and `format` is null or points at an initialised
160/// [`mxr_audio_format_t`].
161#[no_mangle]
162pub unsafe extern "C" fn mxr_select_audio_source_addr(
163    remote: *const mxr_remote_t,
164    sink: mxr_bay_uid_t,
165    audio_ip: *const c_char,
166    audio_port: u16,
167    format: *const mxr_audio_format_t,
168) -> mxr_result_t {
169    // SAFETY: the caller guarantees a live handle or null.
170    let handle = unsafe { remote.as_ref() };
171    with(handle, |r| {
172        // SAFETY: the caller guarantees a NUL-terminated string.
173        let text = match unsafe { req_str(audio_ip) } {
174            Ok(text) => text,
175            Err(code) => return code,
176        };
177        let Ok(ip) = text.parse() else {
178            return fail(
179                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
180                &format!("audio_ip is not an IPv4 address: {text:?}"),
181            );
182        };
183        // SAFETY: the caller guarantees an initialised struct or null.
184        let format = unsafe { format.as_ref() }.map(|f| (*f).into());
185        from_control(r.remote.select_audio_source_addr(
186            sink.into(),
187            ip,
188            // Zero is not a port a stream can arrive on, so it is how the
189            // caller declines to name one.
190            (audio_port != 0).then_some(audio_port),
191            format,
192        ))
193    })
194}
195
196/// One stream of a route the caller assembles.
197#[repr(C)]
198#[derive(Clone, Copy)]
199pub struct mxr_stream_addr_t {
200    /// The multicast group, as a dotted quad. Null or empty sends the slot
201    /// zeroed, naming no group for that stream.
202    ///
203    /// It is not a way to leave one stream alone. The firmware decides
204    /// whether a sink has a manual route at all by reading the video and
205    /// ancillary slots, so an empty one of those disqualifies the whole
206    /// route rather than preserving anything - see
207    /// `mxr_select_source_addr()`.
208    pub ip: *const c_char,
209    /// The destination UDP port. Zero means the standard port for the stream
210    /// this slot names.
211    pub port: u16,
212}
213
214/// The three streams a manual route points a V2IP sink at.
215#[repr(C)]
216#[derive(Clone, Copy)]
217pub struct mxr_v2ip_route_t {
218    /// The video stream, at port 50020 unless the port says otherwise.
219    pub video: mxr_stream_addr_t,
220    /// The audio stream, at port 50022 unless the port says otherwise.
221    pub audio: mxr_stream_addr_t,
222    /// The ancillary-data stream, at port 50021 unless the port says
223    /// otherwise.
224    pub anc: mxr_stream_addr_t,
225}
226
227/// Reads one route slot, where a null or empty address means "not set".
228///
229/// # Safety
230///
231/// `slot.ip` is null or a NUL-terminated string.
232unsafe fn to_target(slot: mxr_stream_addr_t, what: &str) -> Result<V2ipRouteTarget, mxr_result_t> {
233    // SAFETY: the caller guarantees a NUL-terminated string or null.
234    let text = unsafe { opt_str(slot.ip) }?.unwrap_or_default();
235    if text.is_empty() {
236        return Ok(V2ipRouteTarget::default());
237    }
238    match text.parse() {
239        Ok(ip) => Ok(V2ipRouteTarget {
240            ip,
241            port: slot.port,
242        }),
243        Err(_) => Err(fail(
244            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
245            &format!("{what} is not an IPv4 address: {text:?}"),
246        )),
247    }
248}
249
250/// Routes a V2IP sink's video, audio and ancillary streams to multicast groups
251/// the caller names.
252///
253/// This is the only way to reach a stream no device on the mesh advertises,
254/// such as one the calling program is transmitting itself; the routes by
255/// source port and by name can only name a stream some bay has announced.
256///
257/// Set all three groups. The firmware decides whether a sink has a manual
258/// route by looking at the video and ancillary groups, so a route that leaves
259/// either unset does not register as one and the sink falls back to the audio
260/// source its mesh picks.
261///
262/// A null `format` sends 48kHz stereo rather than omitting the field. The
263/// firmware stores whatever the frame carries and hands it to the FPGA
264/// unexamined, so a frame without a format leaves a zero sample rate there,
265/// which the FPGA rejects and which takes the switch down with it.
266///
267/// # Safety
268///
269/// `remote` is null or a live handle, `route` points at an initialised
270/// [`mxr_v2ip_route_t`] whose addresses are null or NUL-terminated strings,
271/// and `format` is null or points at an initialised [`mxr_audio_format_t`].
272#[no_mangle]
273pub unsafe extern "C" fn mxr_select_source_addr(
274    remote: *const mxr_remote_t,
275    sink: mxr_bay_uid_t,
276    route: *const mxr_v2ip_route_t,
277    format: *const mxr_audio_format_t,
278) -> mxr_result_t {
279    // SAFETY: the caller guarantees a live handle or null.
280    let handle = unsafe { remote.as_ref() };
281    with(handle, |r| {
282        // SAFETY: the caller guarantees an initialised struct or null.
283        let Some(route) = (unsafe { route.as_ref() }) else {
284            return fail(
285                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
286                "the route pointer is null",
287            );
288        };
289        // SAFETY: the caller guarantees NUL-terminated strings or null.
290        let route = unsafe {
291            match (
292                to_target(route.video, "video ip"),
293                to_target(route.audio, "audio ip"),
294                to_target(route.anc, "anc ip"),
295            ) {
296                (Ok(video), Ok(audio), Ok(anc)) => V2ipRoute { video, audio, anc },
297                (Err(code), _, _) | (_, Err(code), _) | (_, _, Err(code)) => return code,
298            }
299        };
300        // SAFETY: the caller guarantees an initialised struct or null.
301        let format = unsafe { format.as_ref() }.map(|f| (*f).into());
302        from_control(r.remote.select_source_addr(sink.into(), route, format))
303    })
304}
305
306// ---- bays ----
307
308/// Renames a bay. The device stores the first 16 bytes.
309///
310/// # Safety
311///
312/// `remote` is null or a live handle, and `name` is a NUL-terminated string.
313#[no_mangle]
314pub unsafe extern "C" fn mxr_set_bay_name(
315    remote: *const mxr_remote_t,
316    bay: mxr_bay_uid_t,
317    name: *const c_char,
318) -> mxr_result_t {
319    // SAFETY: the caller guarantees a live handle or null.
320    let handle = unsafe { remote.as_ref() };
321    with(handle, |r| {
322        // SAFETY: the caller guarantees a NUL-terminated string.
323        match unsafe { req_str(name) } {
324            Ok(name) => from_control(r.remote.set_bay_name(bay.into(), name)),
325            Err(code) => code,
326        }
327    })
328}
329
330/// Hides a bay from the installation's user interface, or shows it again.
331///
332/// # Safety
333///
334/// `remote` is null or a live handle from `mxr_remote_new()`.
335#[no_mangle]
336pub unsafe extern "C" fn mxr_set_bay_hidden(
337    remote: *const mxr_remote_t,
338    bay: mxr_bay_uid_t,
339    hidden: bool,
340) -> mxr_result_t {
341    // SAFETY: the caller guarantees a live handle or null.
342    let handle = unsafe { remote.as_ref() };
343    with(handle, |r| {
344        from_control(r.remote.set_bay_hidden(bay.into(), hidden))
345    })
346}
347
348/// Switches an input bay's EDID profile.
349///
350/// # Safety
351///
352/// `remote` is null or a live handle from `mxr_remote_new()`.
353#[no_mangle]
354pub unsafe extern "C" fn mxr_select_edid_profile(
355    remote: *const mxr_remote_t,
356    bay: mxr_bay_uid_t,
357    profile: u16,
358) -> mxr_result_t {
359    // SAFETY: the caller guarantees a live handle or null.
360    let handle = unsafe { remote.as_ref() };
361    with(handle, |r| {
362        from_control(
363            r.remote
364                .select_edid_profile(bay.into(), EdidProfile::from_wire(profile)),
365        )
366    })
367}
368
369/// Sends a remote-control action to whatever is attached to a bay.
370///
371/// # Safety
372///
373/// `remote` is null or a live handle from `mxr_remote_new()`.
374#[no_mangle]
375pub unsafe extern "C" fn mxr_send_action(
376    remote: *const mxr_remote_t,
377    bay: mxr_bay_uid_t,
378    action: u16,
379) -> mxr_result_t {
380    // SAFETY: the caller guarantees a live handle or null.
381    let handle = unsafe { remote.as_ref() };
382    with(handle, |r| {
383        from_control(
384            r.remote
385                .send_action(bay.into(), RcAction::from_wire(action)),
386        )
387    })
388}
389
390/// Sends a remote-control key press to whatever is attached to a bay.
391///
392/// The device forwards it over CEC, infrared or IP, whichever that bay is
393/// configured for; the caller does not choose. `key` is one of the `MXR_KEY_*`
394/// values, or a raw code above `MXR_KEY_CUSTOM_CEC` or `MXR_KEY_CUSTOM_SKY`.
395/// A value this library has no name for is sent as it was given.
396///
397/// `mxr_send_action()` names an outcome instead, and lets the device decide
398/// which keys reach it.
399///
400/// # Safety
401///
402/// `remote` is null or a live handle from `mxr_remote_new()`.
403#[no_mangle]
404pub unsafe extern "C" fn mxr_send_key(
405    remote: *const mxr_remote_t,
406    bay: mxr_bay_uid_t,
407    key: u16,
408) -> mxr_result_t {
409    // SAFETY: the caller guarantees a live handle or null.
410    let handle = unsafe { remote.as_ref() };
411    with(handle, |r| {
412        from_control(r.remote.send_key(bay.into(), RcKey::from_wire(key)))
413    })
414}
415
416/// Powers on what is attached to a bay.
417///
418/// # Safety
419///
420/// `remote` is null or a live handle from `mxr_remote_new()`.
421#[no_mangle]
422pub unsafe extern "C" fn mxr_power_on(
423    remote: *const mxr_remote_t,
424    bay: mxr_bay_uid_t,
425) -> mxr_result_t {
426    // SAFETY: the caller guarantees a live handle or null.
427    let handle = unsafe { remote.as_ref() };
428    with(handle, |r| from_control(r.remote.power_on(bay.into())))
429}
430
431/// Powers off what is attached to a bay.
432///
433/// # Safety
434///
435/// `remote` is null or a live handle from `mxr_remote_new()`.
436#[no_mangle]
437pub unsafe extern "C" fn mxr_power_off(
438    remote: *const mxr_remote_t,
439    bay: mxr_bay_uid_t,
440) -> mxr_result_t {
441    // SAFETY: the caller guarantees a live handle or null.
442    let handle = unsafe { remote.as_ref() };
443    with(handle, |r| from_control(r.remote.power_off(bay.into())))
444}
445
446// ---- volume ----
447
448/// Sets a bay's volume percentage, and its mute state when `muted` is not
449/// `MXR_UNKNOWN`.
450///
451/// A bay with no volume control of its own is set through its `linked_bay`,
452/// so an output wired to an amplifier zone reaches that zone.
453///
454/// # Safety
455///
456/// `remote` is null or a live handle from `mxr_remote_new()`.
457#[no_mangle]
458pub unsafe extern "C" fn mxr_set_volume(
459    remote: *const mxr_remote_t,
460    bay: mxr_bay_uid_t,
461    volume: u8,
462    muted: mxr_tribool_t,
463) -> mxr_result_t {
464    // SAFETY: the caller guarantees a live handle or null.
465    let handle = unsafe { remote.as_ref() };
466    with(handle, |r| {
467        from_control(r.remote.set_volume(
468            bay.into(),
469            volume,
470            match muted {
471                mxr_tribool_t::MXR_UNKNOWN => None,
472                mxr_tribool_t::MXR_FALSE => Some(false),
473                mxr_tribool_t::MXR_TRUE => Some(true),
474            },
475        ))
476    })
477}
478
479/// Asks a bay to step its volume up.
480///
481/// # Safety
482///
483/// `remote` is null or a live handle from `mxr_remote_new()`.
484#[no_mangle]
485pub unsafe extern "C" fn mxr_volume_up(
486    remote: *const mxr_remote_t,
487    bay: mxr_bay_uid_t,
488) -> mxr_result_t {
489    // SAFETY: the caller guarantees a live handle or null.
490    let handle = unsafe { remote.as_ref() };
491    with(handle, |r| from_control(r.remote.volume_up(bay.into())))
492}
493
494/// Asks a bay to step its volume down.
495///
496/// # Safety
497///
498/// `remote` is null or a live handle from `mxr_remote_new()`.
499#[no_mangle]
500pub unsafe extern "C" fn mxr_volume_down(
501    remote: *const mxr_remote_t,
502    bay: mxr_bay_uid_t,
503) -> mxr_result_t {
504    // SAFETY: the caller guarantees a live handle or null.
505    let handle = unsafe { remote.as_ref() };
506    with(handle, |r| from_control(r.remote.volume_down(bay.into())))
507}
508
509/// Mutes or unmutes a bay, leaving its volume alone.
510///
511/// # Safety
512///
513/// `remote` is null or a live handle from `mxr_remote_new()`.
514#[no_mangle]
515pub unsafe extern "C" fn mxr_set_muted(
516    remote: *const mxr_remote_t,
517    bay: mxr_bay_uid_t,
518    muted: bool,
519) -> mxr_result_t {
520    // SAFETY: the caller guarantees a live handle or null.
521    let handle = unsafe { remote.as_ref() };
522    with(handle, |r| {
523        from_control(r.remote.set_muted(bay.into(), muted))
524    })
525}
526
527/// Writes an amplifier zone's gain, delay, tone and power settings.
528///
529/// This replaces every setting at once, so a caller changing one reads the
530/// current set with `mxr_bay_amp_settings()`
531/// first.
532///
533/// # Safety
534///
535/// `remote` is null or a live handle, and `settings` points at an initialised
536/// [`mxr_amp_zone_settings_t`].
537#[no_mangle]
538pub unsafe extern "C" fn mxr_set_amp_zone_settings(
539    remote: *const mxr_remote_t,
540    bay: mxr_bay_uid_t,
541    settings: *const mxr_amp_zone_settings_t,
542) -> mxr_result_t {
543    // SAFETY: the caller guarantees a live handle or null.
544    let handle = unsafe { remote.as_ref() };
545    with(handle, |r| {
546        // SAFETY: the caller guarantees an initialised struct or null.
547        let Some(settings) = (unsafe { settings.as_ref() }) else {
548            return fail(
549                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
550                "the amp zone settings pointer is null",
551            );
552        };
553        from_control(
554            r.remote
555                .set_amp_zone_settings(bay.into(), (*settings).into()),
556        )
557    })
558}
559
560// ---- audio endpoints ----
561
562/// Mutes or unmutes one of a device's audio endpoints.
563///
564/// A loadable module serves this, not the device firmware, and a model may
565/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
566/// sent and not that anything acted on it.
567///
568/// # Safety
569///
570/// `remote` is null or a live handle from `mxr_remote_new()`.
571#[no_mangle]
572pub unsafe extern "C" fn mxr_set_audio_endpoint_muted(
573    remote: *const mxr_remote_t,
574    device: mxr_uid_t,
575    endpoint: u16,
576    muted: bool,
577) -> mxr_result_t {
578    // SAFETY: the caller guarantees a live handle or null.
579    let handle = unsafe { remote.as_ref() };
580    with(handle, |r| {
581        from_control(
582            r.remote
583                .set_audio_endpoint_muted(device.into(), endpoint, muted),
584        )
585    })
586}
587
588/// Locks or unlocks an audio endpoint's source: while it is locked, a video
589/// route change leaves the endpoint's audio source alone.
590///
591/// `MXR_ERR_NOT_REPORTED` before the device has reported its audio endpoints,
592/// and `MXR_ERR_UNSUPPORTED` for an endpoint without `MXR_AUDIO_AUDIO_LOCK`,
593/// sending nothing in either case. The device reports its endpoints again
594/// once the lock has changed; `mxr_audio_endpoint_status()` reads it.
595///
596/// # Safety
597///
598/// `remote` is null or a live handle from `mxr_remote_new()`.
599#[no_mangle]
600pub unsafe extern "C" fn mxr_set_audio_endpoint_locked(
601    remote: *const mxr_remote_t,
602    device: mxr_uid_t,
603    endpoint: u8,
604    locked: bool,
605) -> mxr_result_t {
606    // SAFETY: the caller guarantees a live handle or null.
607    let handle = unsafe { remote.as_ref() };
608    with(handle, |r| {
609        from_control(
610            r.remote
611                .set_audio_endpoint_locked(device.into(), endpoint, locked),
612        )
613    })
614}
615
616/// Activates or clears an audio endpoint's trigger.
617///
618/// A loadable module serves this, not the device firmware, and a model may
619/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
620/// sent and not that anything acted on it.
621///
622/// # Safety
623///
624/// `remote` is null or a live handle from `mxr_remote_new()`.
625#[no_mangle]
626pub unsafe extern "C" fn mxr_set_audio_endpoint_trigger(
627    remote: *const mxr_remote_t,
628    device: mxr_uid_t,
629    endpoint: u16,
630    active: bool,
631) -> mxr_result_t {
632    // SAFETY: the caller guarantees a live handle or null.
633    let handle = unsafe { remote.as_ref() };
634    with(handle, |r| {
635        from_control(
636            r.remote
637                .set_audio_endpoint_trigger(device.into(), endpoint, active),
638        )
639    })
640}
641
642/// Sets an audio endpoint's volume.
643///
644/// A loadable module serves this, not the device firmware, and a model may
645/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
646/// sent and not that anything acted on it.
647///
648/// # Safety
649///
650/// `remote` is null or a live handle from `mxr_remote_new()`.
651#[no_mangle]
652pub unsafe extern "C" fn mxr_set_audio_endpoint_volume(
653    remote: *const mxr_remote_t,
654    device: mxr_uid_t,
655    endpoint: u16,
656    volume: u32,
657) -> mxr_result_t {
658    // SAFETY: the caller guarantees a live handle or null.
659    let handle = unsafe { remote.as_ref() };
660    with(handle, |r| {
661        from_control(
662            r.remote
663                .set_audio_endpoint_volume(device.into(), endpoint, volume),
664        )
665    })
666}
667
668/// Points one device's audio endpoint at another device's.
669///
670/// `sink` is the end doing the listening and `source` the end being
671/// listened to.
672///
673/// A loadable module serves this, not the device firmware, and a model may
674/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
675/// sent and not that anything acted on it.
676///
677/// # Safety
678///
679/// `remote` is null or a live handle from `mxr_remote_new()`.
680#[no_mangle]
681pub unsafe extern "C" fn mxr_select_audio_endpoint_input(
682    remote: *const mxr_remote_t,
683    sink: mxr_uid_t,
684    sink_endpoint: u16,
685    source: mxr_uid_t,
686    source_endpoint: u16,
687) -> mxr_result_t {
688    // SAFETY: the caller guarantees a live handle or null.
689    let handle = unsafe { remote.as_ref() };
690    with(handle, |r| {
691        from_control(r.remote.select_audio_endpoint_input(
692            sink.into(),
693            sink_endpoint,
694            source.into(),
695            source_endpoint,
696        ))
697    })
698}
699
700// ---- devices ----
701
702/// Asks a device for an EDID: the one the display on its output publishes, or
703/// the one the device presents to the source on its input.
704///
705/// The device answers a moment later. The bytes reach `on_edid_received` and
706/// stay readable through `mxr_device_edid()`.
707///
708/// Only V2IP hardware handles this opcode. A matrix or an amplifier accepts
709/// the frame and answers nothing, at any protocol version, so the silence that
710/// follows is permanent rather than a reply still to come. `MXR_OK` here means
711/// the frame was sent, and nothing more; a caller polling for an EDID should
712/// ask a device that can answer rather than wait on one that cannot.
713///
714/// # Safety
715///
716/// `remote` is null or a live handle from `mxr_remote_new()`.
717#[no_mangle]
718pub unsafe extern "C" fn mxr_request_edid(
719    remote: *const mxr_remote_t,
720    device: mxr_uid_t,
721    output: bool,
722) -> mxr_result_t {
723    // SAFETY: the caller guarantees a live handle or null.
724    let handle = unsafe { remote.as_ref() };
725    with(handle, |r| {
726        from_control(r.remote.request_edid(device.into(), output))
727    })
728}
729
730/// Asks for a detailed signal report from every bay of one device, or - with
731/// the zero uid - from every bay on the network.
732///
733/// Devices report on their own when a signal changes, so this is what a client
734/// that has just started needs: without it, a bay that has been showing the
735/// same picture for an hour says nothing until it changes.
736///
737/// # Safety
738///
739/// `remote` is null or a live handle from `mxr_remote_new()`.
740#[no_mangle]
741pub unsafe extern "C" fn mxr_request_signal_status(
742    remote: *const mxr_remote_t,
743    device: mxr_uid_t,
744) -> mxr_result_t {
745    // SAFETY: the caller guarantees a live handle or null.
746    let handle = unsafe { remote.as_ref() };
747    with(handle, |r| {
748        let uid = DeviceUid::from(device);
749        let target = (uid != DeviceUid::ZERO).then_some(uid);
750        from_control(r.remote.request_signal_status(target))
751    })
752}
753
754/// Subscribes to, or unsubscribes from, a device's V2IP statistics.
755///
756/// # Safety
757///
758/// `remote` is null or a live handle from `mxr_remote_new()`.
759#[no_mangle]
760pub unsafe extern "C" fn mxr_subscribe_v2ip_stats(
761    remote: *const mxr_remote_t,
762    device: mxr_uid_t,
763    subscribe: bool,
764) -> mxr_result_t {
765    // SAFETY: the caller guarantees a live handle or null.
766    let handle = unsafe { remote.as_ref() };
767    with(handle, |r| {
768        from_control(r.remote.subscribe_v2ip_stats(device.into(), subscribe))
769    })
770}
771
772/// Reboots a device.
773///
774/// # Safety
775///
776/// `remote` is null or a live handle from `mxr_remote_new()`.
777#[no_mangle]
778pub unsafe extern "C" fn mxr_reboot(
779    remote: *const mxr_remote_t,
780    device: mxr_uid_t,
781) -> mxr_result_t {
782    // SAFETY: the caller guarantees a live handle or null.
783    let handle = unsafe { remote.as_ref() };
784    with(handle, |r| from_control(r.remote.reboot(device.into())))
785}
786
787/// Asks a device to announce itself now, so a device suspected to be gone is
788/// confirmed or ruled out within a second or two.
789///
790/// `MXR_ERR_PROTOCOL_TOO_OLD` for a device below protocol 0x2A, which does not
791/// answer one.
792///
793/// # Safety
794///
795/// `remote` is null or a live handle from `mxr_remote_new()`.
796#[no_mangle]
797pub unsafe extern "C" fn mxr_ping(remote: *const mxr_remote_t, device: mxr_uid_t) -> mxr_result_t {
798    // SAFETY: the caller guarantees a live handle or null.
799    let handle = unsafe { remote.as_ref() };
800    with(handle, |r| from_control(r.remote.ping(device.into())))
801}
802
803/// Hands a V2IP source's stream addresses back to automatic assignment,
804/// undoing addresses that were set on it by hand. The device's next
805/// configuration report carries the addresses it ends up with.
806///
807/// `MXR_ERR_UNSUPPORTED` for a device that is not a V2IP source, and
808/// `MXR_ERR_PROTOCOL_TOO_OLD` for one below protocol 0x2B, which ignores it,
809/// sending nothing in either case.
810///
811/// # Safety
812///
813/// `remote` is null or a live handle from `mxr_remote_new()`.
814#[no_mangle]
815pub unsafe extern "C" fn mxr_auto_assign_v2ip_source_addresses(
816    remote: *const mxr_remote_t,
817    device: mxr_uid_t,
818) -> mxr_result_t {
819    // SAFETY: the caller guarantees a live handle or null.
820    let handle = unsafe { remote.as_ref() };
821    with(handle, |r| {
822        from_control(r.remote.auto_assign_v2ip_source_addresses(device.into()))
823    })
824}
825
826/// Sets the time zone of every device that hears it: `zone` an IANA name such
827/// as `Europe/Amsterdam`, `rule` the POSIX TZ rule the devices keep time by.
828///
829/// `MXR_ERR_INVALID_ARGUMENT` for an empty string, or one of
830/// `MXR_TIME_ZONE_NAME_LEN` or `MXR_TIME_ZONE_RULE_LEN` bytes or more. The
831/// mesh controller takes it too and announces it from then on.
832///
833/// # Safety
834///
835/// `remote` is null or a live handle, and `zone` and `rule` are
836/// NUL-terminated strings.
837#[no_mangle]
838pub unsafe extern "C" fn mxr_set_mesh_time_zone(
839    remote: *const mxr_remote_t,
840    zone: *const c_char,
841    rule: *const c_char,
842) -> mxr_result_t {
843    // SAFETY: the caller guarantees a live handle or null.
844    let handle = unsafe { remote.as_ref() };
845    with(handle, |r| {
846        // SAFETY: the caller guarantees NUL-terminated strings.
847        match unsafe { (req_str(zone), req_str(rule)) } {
848            (Ok(zone), Ok(rule)) => from_control(r.remote.set_mesh_time_zone(zone, rule)),
849            (Err(code), _) | (_, Err(code)) => code,
850        }
851    })
852}
853
854/// Clears the time zone of every device that hears it, which then keeps UTC:
855/// `mxr_set_mesh_time_zone()` with an empty name and rule, which that call
856/// refuses.
857///
858/// # Safety
859///
860/// `remote` is null or a live handle.
861#[no_mangle]
862pub unsafe extern "C" fn mxr_clear_mesh_time_zone(remote: *const mxr_remote_t) -> mxr_result_t {
863    // SAFETY: the caller guarantees a live handle or null.
864    let handle = unsafe { remote.as_ref() };
865    with(handle, |r| from_control(r.remote.clear_mesh_time_zone()))
866}
867
868/// Sets the clock of every device that hears it to `utc`, in seconds since
869/// 1970 UTC. A device keeps its own clock where that is within 2s.
870///
871/// `MXR_ERR_INVALID_ARGUMENT` for a time past what 32 bits of seconds hold.
872///
873/// # Safety
874///
875/// `remote` is null or a live handle from `mxr_remote_new()`.
876#[no_mangle]
877pub unsafe extern "C" fn mxr_set_mesh_time(remote: *const mxr_remote_t, utc: u64) -> mxr_result_t {
878    // SAFETY: the caller guarantees a live handle or null.
879    let handle = unsafe { remote.as_ref() };
880    with(handle, |r| {
881        let time = UNIX_EPOCH.checked_add(Duration::from_secs(utc));
882        match time {
883            Some(time) => from_control(r.remote.set_mesh_time(time)),
884            None => fail(
885                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
886                "the time does not fit the frame",
887            ),
888        }
889    })
890}
891
892/// Sends the monitoring pulse that tells devices this client is watching.
893///
894/// # Safety
895///
896/// `remote` is null or a live handle from `mxr_remote_new()`.
897#[no_mangle]
898pub unsafe extern "C" fn mxr_send_monitoring_pulse(remote: *const mxr_remote_t) -> mxr_result_t {
899    // SAFETY: the caller guarantees a live handle or null.
900    let handle = unsafe { remote.as_ref() };
901    with(handle, |r| from_control(r.remote.send_monitoring_pulse()))
902}
903
904// ---- V2IP scaling ----
905
906/// The colour space a V2IP sink scales its output to.
907pub const MXR_V2IP_COLOUR_RGB: u8 = 0;
908/// YCbCr 4:4:4. See `MXR_V2IP_COLOUR_RGB`.
909pub const MXR_V2IP_COLOUR_YCBCR444: u8 = 1;
910/// YCbCr 4:2:2. See `MXR_V2IP_COLOUR_RGB`.
911pub const MXR_V2IP_COLOUR_YCBCR422: u8 = 2;
912/// YCbCr 4:2:0. See `MXR_V2IP_COLOUR_RGB`.
913pub const MXR_V2IP_COLOUR_YCBCR420: u8 = 3;
914
915/// Lowest refresh rate a V2IP output stage accepts, in Hz.
916pub const MXR_V2IP_SCALING_REFRESH_MIN: u16 = 24;
917
918/// Highest refresh rate a V2IP output stage accepts, in Hz.
919pub const MXR_V2IP_SCALING_REFRESH_MAX: u16 = 120;
920
921/// The output format to scale a V2IP sink to.
922///
923/// Given as a depth and a colour space rather than as a packed signal-type
924/// word, so a caller cannot send the "no depth" index a sink reports while it
925/// has none configured - a value a sink decodes cleanly and then drops.
926///
927/// `svd` must name a known video descriptor and may not be zero, `depth` must
928/// be 8, 10 or 12, `colour` one of the `MXR_V2IP_COLOUR_*` values, and
929/// `refresh` between `MXR_V2IP_SCALING_REFRESH_MIN` and
930/// `MXR_V2IP_SCALING_REFRESH_MAX`. Each is checked before anything is sent.
931#[repr(C)]
932#[derive(Clone, Copy)]
933pub struct mxr_v2ip_output_mode_t {
934    /// The CTA-861 short video descriptor to output.
935    pub svd: u8,
936    /// Bit depth: 8, 10 or 12.
937    pub depth: u8,
938    /// One of the `MXR_V2IP_COLOUR_*` values.
939    pub colour: u8,
940    /// Refresh rate in Hz.
941    pub refresh: u16,
942}
943
944impl From<mxr_v2ip_output_mode_t> for V2ipOutputMode {
945    fn from(m: mxr_v2ip_output_mode_t) -> Self {
946        Self {
947            svd: m.svd,
948            depth: m.depth,
949            colour: V2ipColourSpace::from_wire(m.colour),
950            refresh: m.refresh,
951        }
952    }
953}
954
955/// Reads a mode argument, refusing a null pointer.
956unsafe fn output_mode(mode: *const mxr_v2ip_output_mode_t) -> Result<V2ipOutputMode, mxr_result_t> {
957    // SAFETY: the caller guarantees an initialised struct or null.
958    match unsafe { mode.as_ref() } {
959        Some(m) => Ok((*m).into()),
960        None => Err(fail(
961            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
962            "the mode pointer is null",
963        )),
964    }
965}
966
967/// Turns a V2IP sink's automatic scaling on or off.
968///
969/// Automatic scaling and a configured output mode are separate reasons for a
970/// sink to scale, and this moves only the first: a sink with a mode configured
971/// goes on scaling to it with automatic scaling off. Turning both off is this
972/// call plus `mxr_clear_v2ip_output_mode()`.
973///
974/// Nothing acknowledges the frame, so `MXR_OK` means it was sent. Read the sink
975/// back with `mxr_v2ip_details()`, and trust the scaling fields only where the
976/// device reports `MXR_FEATURE_CONFIG_INITIALISED`.
977///
978/// **Read any route you still need before writing.** The sink rebuilds and
979/// rebroadcasts its subscription in response, and the addresses in
980/// `mxr_v2ip_details()` can read as zero for up to a minute afterwards.
981///
982/// # Safety
983///
984/// `remote` is null or a live handle from `mxr_remote_new()`.
985#[no_mangle]
986pub unsafe extern "C" fn mxr_set_v2ip_auto_scaling(
987    remote: *const mxr_remote_t,
988    device: mxr_uid_t,
989    enabled: bool,
990) -> mxr_result_t {
991    // SAFETY: the caller guarantees a live handle or null.
992    let handle = unsafe { remote.as_ref() };
993    with(handle, |r| {
994        from_control(r.remote.set_v2ip_auto_scaling(device.into(), enabled))
995    })
996}
997
998/// Switches on/off settings of a V2IP device, all to the same value.
999///
1000/// `setting` is one or more `MXR_V2IP_SETTING_*` on/off bits, each one the
1001/// device has reported. Returns `MXR_ERR_NOT_REPORTED` before the device has
1002/// reported its settings, `MXR_ERR_UNSUPPORTED` for a setting it does not have
1003/// and `MXR_ERR_INVALID_ARGUMENT` for a bit that is not an on/off setting,
1004/// sending nothing in each case: the device ignores such a write. Until the
1005/// device reports back, `mxr_v2ip_device_settings()` reads what was written.
1006///
1007/// # Safety
1008///
1009/// `remote` is null or a live handle from `mxr_remote_new()`.
1010#[no_mangle]
1011pub unsafe extern "C" fn mxr_set_v2ip_device_setting(
1012    remote: *const mxr_remote_t,
1013    device: mxr_uid_t,
1014    setting: u32,
1015    enabled: bool,
1016) -> mxr_result_t {
1017    // SAFETY: the caller guarantees a live handle or null.
1018    let handle = unsafe { remote.as_ref() };
1019    with(handle, |r| {
1020        from_control(r.remote.set_v2ip_device_setting(
1021            device.into(),
1022            V2ipDeviceSetting::from_bits(setting),
1023            enabled,
1024        ))
1025    })
1026}
1027
1028/// Sets the infrared profile of a V2IP device's global infrared port.
1029///
1030/// `profile` is from 0 up to, not including, `MXR_V2IP_IR_PROFILE_MAX`.
1031/// Otherwise as `mxr_set_v2ip_device_setting()`.
1032///
1033/// # Safety
1034///
1035/// `remote` is null or a live handle from `mxr_remote_new()`.
1036#[no_mangle]
1037pub unsafe extern "C" fn mxr_set_v2ip_ir_profile(
1038    remote: *const mxr_remote_t,
1039    device: mxr_uid_t,
1040    profile: i8,
1041) -> mxr_result_t {
1042    // SAFETY: the caller guarantees a live handle or null.
1043    let handle = unsafe { remote.as_ref() };
1044    with(handle, |r| {
1045        from_control(r.remote.set_v2ip_ir_profile(device.into(), profile))
1046    })
1047}
1048
1049/// Sets how many idle minutes a V2IP device waits before it powers down by
1050/// itself, 0 for never. Otherwise as `mxr_set_v2ip_device_setting()`.
1051///
1052/// # Safety
1053///
1054/// `remote` is null or a live handle from `mxr_remote_new()`.
1055#[no_mangle]
1056pub unsafe extern "C" fn mxr_set_v2ip_auto_power_save(
1057    remote: *const mxr_remote_t,
1058    device: mxr_uid_t,
1059    minutes: u16,
1060) -> mxr_result_t {
1061    // SAFETY: the caller guarantees a live handle or null.
1062    let handle = unsafe { remote.as_ref() };
1063    with(handle, |r| {
1064        from_control(r.remote.set_v2ip_auto_power_save(device.into(), minutes))
1065    })
1066}
1067
1068/// Sets a V2IP device's daily power save windows from `schedule`'s `start`
1069/// and `end`; its `auto_minutes` is not sent.
1070///
1071/// `MXR_ERR_INVALID_ARGUMENT` for a time not below
1072/// `MXR_V2IP_MINUTES_PER_DAY`. Otherwise as `mxr_set_v2ip_device_setting()`.
1073///
1074/// # Safety
1075///
1076/// `remote` is null or a live handle from `mxr_remote_new()`, and `schedule`
1077/// is null or points at an initialised `mxr_v2ip_power_save_t`.
1078#[no_mangle]
1079pub unsafe extern "C" fn mxr_set_v2ip_power_save_schedule(
1080    remote: *const mxr_remote_t,
1081    device: mxr_uid_t,
1082    schedule: *const mxr_v2ip_power_save_t,
1083) -> mxr_result_t {
1084    // SAFETY: the caller guarantees a live handle or null.
1085    let handle = unsafe { remote.as_ref() };
1086    with(handle, |r| {
1087        // SAFETY: the caller guarantees an initialised struct or null.
1088        let Some(schedule) = (unsafe { schedule.as_ref() }) else {
1089            return fail(
1090                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1091                "the schedule pointer is null",
1092            );
1093        };
1094        from_control(r.remote.set_v2ip_power_save_schedule(
1095            device.into(),
1096            V2ipPowerSaveSchedule {
1097                start: schedule.start,
1098                end: schedule.end,
1099            },
1100        ))
1101    })
1102}
1103
1104/// Changes settings on every V2IP device of the mesh with one frame.
1105///
1106/// `settings` carries the settings behind their `MXR_V2IP_SETTING_*` bits in
1107/// `valid`, as `mxr_v2ip_device_settings()` reports them; its `ir_profiles`
1108/// is not sent. `power_save` carries the idle minutes and the schedule, and
1109/// may be null when `valid` has neither. Each device applies the settings it
1110/// has and ignores the rest.
1111///
1112/// `MXR_ERR_INVALID_ARGUMENT`, sending nothing, when no setting is carried,
1113/// when one only a device reports is, for a profile out of range or a
1114/// schedule time not below `MXR_V2IP_MINUTES_PER_DAY`. Nothing is cached: a
1115/// device that applies a change reports it.
1116///
1117/// # Safety
1118///
1119/// `remote` is null or a live handle from `mxr_remote_new()`, `settings` is
1120/// null or points at an initialised `mxr_v2ip_device_settings_t`, and
1121/// `power_save` is null or points at an initialised `mxr_v2ip_power_save_t`.
1122#[no_mangle]
1123pub unsafe extern "C" fn mxr_set_all_v2ip_device_settings(
1124    remote: *const mxr_remote_t,
1125    settings: *const mxr_v2ip_device_settings_t,
1126    power_save: *const mxr_v2ip_power_save_t,
1127) -> mxr_result_t {
1128    // SAFETY: the caller guarantees a live handle or null.
1129    let handle = unsafe { remote.as_ref() };
1130    with(handle, |r| {
1131        // SAFETY: the caller guarantees an initialised struct or null.
1132        let Some(settings) = (unsafe { settings.as_ref() }) else {
1133            return fail(
1134                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1135                "the settings pointer is null",
1136            );
1137        };
1138        let valid = V2ipDeviceSetting::from_bits(settings.valid);
1139        let needs_power_save = valid.has(V2ipDeviceSetting::AUTO_POWER_SAVE)
1140            || valid.has(V2ipDeviceSetting::POWER_SAVE_SCHEDULE);
1141        // SAFETY: the caller guarantees an initialised struct or null.
1142        let power_save = match unsafe { power_save.as_ref() } {
1143            Some(p) => *p,
1144            None if needs_power_save => {
1145                return fail(
1146                    mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1147                    "the power save pointer is null",
1148                )
1149            }
1150            None => mxr_v2ip_power_save_t {
1151                auto_minutes: 0,
1152                start: [0; 7],
1153                end: [0; 7],
1154            },
1155        };
1156        from_control(r.remote.set_all_v2ip_device_settings(V2ipDeviceSettings {
1157            valid,
1158            flags: V2ipDeviceSetting::from_bits(settings.flags),
1159            ir_profiles: 0,
1160            ir_profile: settings.ir_profile,
1161            ir_profile_sink: settings.ir_profile_sink,
1162            auto_power_save: power_save.auto_minutes,
1163            power_save: V2ipPowerSaveSchedule {
1164                start: power_save.start,
1165                end: power_save.end,
1166            },
1167        }))
1168    })
1169}
1170
1171/// Changes a V2IP device's VLAN configuration.
1172///
1173/// Sends `vlan`'s ids, its `uplink` and its `MXR_V2IP_VLAN_TRUNK` bit; its
1174/// other flags, `active_uplink` and `revert_s` are the device's to report and
1175/// are not sent. Returns `MXR_ERR_INVALID_ARGUMENT` for an id above
1176/// `MXR_V2IP_VLAN_ID_MAX` or an uplink above `MXR_V2IP_VLAN_PORTS`,
1177/// `MXR_ERR_UNSUPPORTED` for a device without `MXR_FEATURE_VLAN` or an SFP
1178/// uplink on a device without `MXR_V2IP_VLAN_HAS_SFP`, and
1179/// `MXR_ERR_NOT_REPORTED` before the device has reported its configuration,
1180/// sending nothing in each case.
1181///
1182/// The device reverts the change unless the mesh controller confirms it.
1183/// Nothing is cached: `mxr_v2ip_vlan()` reads what the device reports.
1184///
1185/// # Safety
1186///
1187/// `remote` is null or a live handle from `mxr_remote_new()`, and `vlan` is
1188/// null or points at an initialised `mxr_v2ip_vlan_t`.
1189#[no_mangle]
1190pub unsafe extern "C" fn mxr_set_v2ip_vlan(
1191    remote: *const mxr_remote_t,
1192    device: mxr_uid_t,
1193    vlan: *const mxr_v2ip_vlan_t,
1194) -> mxr_result_t {
1195    // SAFETY: the caller guarantees a live handle or null.
1196    let handle = unsafe { remote.as_ref() };
1197    with(handle, |r| {
1198        // SAFETY: the caller guarantees an initialised struct or null.
1199        let Some(vlan) = (unsafe { vlan.as_ref() }) else {
1200            return fail(
1201                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1202                "the VLAN pointer is null",
1203            );
1204        };
1205        from_control(r.remote.set_v2ip_vlan(
1206            device.into(),
1207            V2ipVlan {
1208                flags: V2ipVlanFlag::from_bits(vlan.flags),
1209                device: vlan.device,
1210                port: vlan.port,
1211                uplink: vlan.uplink,
1212                active_uplink: vlan.active_uplink,
1213                revert_s: vlan.revert_s,
1214            },
1215        ))
1216    })
1217}
1218
1219/// Asks a V2IP sink for its test card, which it reports straight back;
1220/// `mxr_v2ip_testcard()` reads it.
1221///
1222/// `MXR_ERR_NOT_REPORTED` before the sink has reported its video processor
1223/// features, `MXR_ERR_UNSUPPORTED` when they lack bit 8 of
1224/// `mxr_v2ip_features()` (the test pattern), and `MXR_ERR_PROTOCOL_TOO_OLD`
1225/// for a sink below protocol 0x2B, sending nothing in each case. A sink with
1226/// the feature but without the module that draws the test card does not
1227/// answer.
1228///
1229/// # Safety
1230///
1231/// `remote` is null or a live handle from `mxr_remote_new()`.
1232#[no_mangle]
1233pub unsafe extern "C" fn mxr_request_v2ip_testcard(
1234    remote: *const mxr_remote_t,
1235    device: mxr_uid_t,
1236) -> mxr_result_t {
1237    // SAFETY: the caller guarantees a live handle or null.
1238    let handle = unsafe { remote.as_ref() };
1239    with(handle, |r| {
1240        from_control(r.remote.request_v2ip_testcard(device.into()))
1241    })
1242}
1243
1244/// Shows `MXR_V2IP_TEST_PATTERN_*` on a V2IP sink's output; `colour` is
1245/// `0xRRGGBB`, used by a flat pattern. The pattern runs until it is turned
1246/// off, and holds the output on while it does.
1247///
1248/// `MXR_ERR_INVALID_ARGUMENT` for a pattern above
1249/// `MXR_V2IP_TEST_PATTERN_CARD` or a colour above `0xFFFFFF`. Otherwise as
1250/// `mxr_request_v2ip_testcard()`.
1251///
1252/// # Safety
1253///
1254/// `remote` is null or a live handle from `mxr_remote_new()`.
1255#[no_mangle]
1256pub unsafe extern "C" fn mxr_set_v2ip_test_pattern(
1257    remote: *const mxr_remote_t,
1258    device: mxr_uid_t,
1259    pattern: u8,
1260    colour: u32,
1261) -> mxr_result_t {
1262    // SAFETY: the caller guarantees a live handle or null.
1263    let handle = unsafe { remote.as_ref() };
1264    with(handle, |r| {
1265        from_control(r.remote.set_v2ip_test_pattern(
1266            device.into(),
1267            V2ipTestPattern::from_wire(pattern),
1268            colour,
1269        ))
1270    })
1271}
1272
1273/// Plays a test tone on a V2IP sink's output.
1274///
1275/// `MXR_ERR_INVALID_ARGUMENT` for a value out of the ranges
1276/// `mxr_v2ip_test_tone_t` gives, unless its mode is `MXR_V2IP_TONE_MODE_OFF`,
1277/// which stops the tone whatever the rest holds. Otherwise as
1278/// `mxr_request_v2ip_testcard()`.
1279///
1280/// # Safety
1281///
1282/// `remote` is null or a live handle from `mxr_remote_new()`, and `tone` is
1283/// null or points at an initialised `mxr_v2ip_test_tone_t`.
1284#[no_mangle]
1285pub unsafe extern "C" fn mxr_set_v2ip_test_tone(
1286    remote: *const mxr_remote_t,
1287    device: mxr_uid_t,
1288    tone: *const mxr_v2ip_test_tone_t,
1289) -> mxr_result_t {
1290    // SAFETY: the caller guarantees a live handle or null.
1291    let handle = unsafe { remote.as_ref() };
1292    with(handle, |r| {
1293        // SAFETY: the caller guarantees an initialised struct or null.
1294        let Some(tone) = (unsafe { tone.as_ref() }) else {
1295            return fail(
1296                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1297                "the tone pointer is null",
1298            );
1299        };
1300        from_control(r.remote.set_v2ip_test_tone(device.into(), (*tone).into()))
1301    })
1302}
1303
1304/// Sets a V2IP sink's lip-sync flash.
1305///
1306/// `MXR_ERR_INVALID_ARGUMENT` for a value out of the ranges
1307/// `mxr_v2ip_test_sync_t` gives. Otherwise as `mxr_request_v2ip_testcard()`.
1308///
1309/// # Safety
1310///
1311/// `remote` is null or a live handle from `mxr_remote_new()`, and `sync` is
1312/// null or points at an initialised `mxr_v2ip_test_sync_t`.
1313#[no_mangle]
1314pub unsafe extern "C" fn mxr_set_v2ip_test_sync(
1315    remote: *const mxr_remote_t,
1316    device: mxr_uid_t,
1317    sync: *const mxr_v2ip_test_sync_t,
1318) -> mxr_result_t {
1319    // SAFETY: the caller guarantees a live handle or null.
1320    let handle = unsafe { remote.as_ref() };
1321    with(handle, |r| {
1322        // SAFETY: the caller guarantees an initialised struct or null.
1323        let Some(sync) = (unsafe { sync.as_ref() }) else {
1324            return fail(
1325                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1326                "the lip-sync pointer is null",
1327            );
1328        };
1329        from_control(r.remote.set_v2ip_test_sync(device.into(), (*sync).into()))
1330    })
1331}
1332
1333/// Sets the infrared profile of a V2IP device's output infrared port.
1334///
1335/// `MXR_V2IP_IR_PROFILE_NOT_SET` makes the port follow the global one.
1336/// Otherwise as `mxr_set_v2ip_ir_profile()`.
1337///
1338/// # Safety
1339///
1340/// `remote` is null or a live handle from `mxr_remote_new()`.
1341#[no_mangle]
1342pub unsafe extern "C" fn mxr_set_v2ip_sink_ir_profile(
1343    remote: *const mxr_remote_t,
1344    device: mxr_uid_t,
1345    profile: i8,
1346) -> mxr_result_t {
1347    // SAFETY: the caller guarantees a live handle or null.
1348    let handle = unsafe { remote.as_ref() };
1349    with(handle, |r| {
1350        from_control(r.remote.set_v2ip_sink_ir_profile(device.into(), profile))
1351    })
1352}
1353
1354/// Sets the output format a V2IP sink scales to.
1355///
1356/// The mode is checked here and `MXR_ERR_INVALID_ARGUMENT` returned without
1357/// sending anything, because a sink refuses a bad one in silence. Passing is
1358/// not a guarantee: the sink also weighs the format against the attached
1359/// display's EDID and against what its own output stage can produce.
1360///
1361/// **Turn automatic scaling off first if it is on.** A sink silently refuses a
1362/// mode the display does not list while it is scaling automatically. Set the
1363/// mode, then turn automatic scaling back on if it was on.
1364///
1365/// **Pass an `svd` and a `refresh` that agree.** A sink stores both halves and,
1366/// with its match-source setting on as it ships, reports back the SVD matching
1367/// the refresh it holds: a 60Hz SVD written with a refresh of 50 reads back as
1368/// that SVD's 50Hz sibling, once, and stays there. A sink with match-source off
1369/// reports the SVD it was given. Either way a pair that agrees reads back
1370/// unchanged and the format driven is the same, and the substitution appears on
1371/// the sink's next report rather than in the next `mxr_v2ip_details()`.
1372///
1373/// A mode read from the sink's own web interface is not interchangeable with
1374/// this pair. That interface reports the SVD's 60Hz sibling and carries the
1375/// refresh in a field of its own, so writing back what it shows as the mode, on
1376/// its own, changes the setting rather than restoring it.
1377///
1378/// **Read any route you still need before writing.** The sink rebuilds and
1379/// rebroadcasts its subscription in response, and the addresses in
1380/// `mxr_v2ip_details()` can read as zero for up to a minute afterwards.
1381///
1382/// # Safety
1383///
1384/// `remote` is null or a live handle, and `mode` points at an initialised
1385/// [`mxr_v2ip_output_mode_t`].
1386#[no_mangle]
1387pub unsafe extern "C" fn mxr_set_v2ip_output_mode(
1388    remote: *const mxr_remote_t,
1389    device: mxr_uid_t,
1390    mode: *const mxr_v2ip_output_mode_t,
1391) -> mxr_result_t {
1392    // SAFETY: the caller guarantees a live handle or null.
1393    let handle = unsafe { remote.as_ref() };
1394    with(handle, |r| {
1395        // SAFETY: the caller guarantees an initialised struct or null.
1396        match unsafe { output_mode(mode) } {
1397            Ok(m) => from_control(r.remote.set_v2ip_output_mode(device.into(), m)),
1398            Err(code) => code,
1399        }
1400    })
1401}
1402
1403/// Clears the output format a V2IP sink is configured to scale to.
1404///
1405/// The sink stops scaling for that reason and keeps its automatic scaling
1406/// setting. This is the only way to express "no mode configured", and it is
1407/// what restoring a sink that had none requires: a sink reports no mode by
1408/// leaving `MXR_SCALING_FLAG_MODE_VALID` clear, which a write cannot say.
1409///
1410/// **Read any route you still need before writing.** The sink rebuilds and
1411/// rebroadcasts its subscription in response, and the addresses in
1412/// `mxr_v2ip_details()` can read as zero for up to a minute afterwards.
1413///
1414/// # Safety
1415///
1416/// `remote` is null or a live handle from `mxr_remote_new()`.
1417#[no_mangle]
1418pub unsafe extern "C" fn mxr_clear_v2ip_output_mode(
1419    remote: *const mxr_remote_t,
1420    device: mxr_uid_t,
1421) -> mxr_result_t {
1422    // SAFETY: the caller guarantees a live handle or null.
1423    let handle = unsafe { remote.as_ref() };
1424    with(handle, |r| {
1425        from_control(r.remote.clear_v2ip_output_mode(device.into()))
1426    })
1427}
1428
1429// ---- video wall ----
1430
1431/// Where a video-wall sink's window sits, and the picture it was measured
1432/// against.
1433///
1434/// `pos_x` must be a multiple of `MXR_VIDEO_WALL_POS_ALIGN`, `width` a
1435/// multiple of `MXR_VIDEO_WALL_WIDTH_ALIGN`, both sides at least
1436/// `MXR_VIDEO_WALL_MIN_SIZE`, and the window must fit inside the raster it
1437/// names. `pos_y` and `height` have no alignment rule. A zero `width` or
1438/// `height` clears the wall and is checked against none of this.
1439#[repr(C)]
1440#[derive(Clone, Copy)]
1441pub struct mxr_video_wall_window_t {
1442    /// Window origin, horizontal.
1443    pub pos_x: u16,
1444    /// Window origin, vertical.
1445    pub pos_y: u16,
1446    /// Window width, or zero to clear the wall.
1447    pub width: u16,
1448    /// Window height, or zero to clear the wall.
1449    pub height: u16,
1450    /// Active picture width the window was measured against.
1451    pub raster_w: u16,
1452    /// Active picture height the window was measured against.
1453    pub raster_h: u16,
1454}
1455
1456/// A window's horizontal origin must be a multiple of this.
1457pub const MXR_VIDEO_WALL_POS_ALIGN: u16 = 64;
1458
1459/// A window's width must be a multiple of this.
1460pub const MXR_VIDEO_WALL_WIDTH_ALIGN: u16 = 4;
1461
1462/// Neither side of a window may be smaller than this.
1463pub const MXR_VIDEO_WALL_MIN_SIZE: u16 = 64;
1464
1465impl From<mxr_video_wall_window_t> for VideoWallWindow {
1466    fn from(w: mxr_video_wall_window_t) -> Self {
1467        Self {
1468            pos_x: w.pos_x,
1469            pos_y: w.pos_y,
1470            width: w.width,
1471            height: w.height,
1472            raster_w: w.raster_w,
1473            raster_h: w.raster_h,
1474        }
1475    }
1476}
1477
1478/// Reads a window argument, refusing a null pointer.
1479unsafe fn wall_window(
1480    window: *const mxr_video_wall_window_t,
1481) -> Result<VideoWallWindow, mxr_result_t> {
1482    // SAFETY: the caller guarantees an initialised struct or null.
1483    match unsafe { window.as_ref() } {
1484        Some(w) => Ok((*w).into()),
1485        None => Err(fail(
1486            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1487            "the window pointer is null",
1488        )),
1489    }
1490}
1491
1492/// Shows a window on a sink's video wall without storing it.
1493///
1494/// The window lasts until the sink is told otherwise or restarts;
1495/// `mxr_revert_video_wall()` puts back whatever it has stored. A zero width or
1496/// height shows the whole frame again.
1497///
1498/// The geometry is checked here and `MXR_ERR_INVALID_ARGUMENT` returned
1499/// without sending anything, because the sink is not guaranteed to check it
1500/// itself.
1501///
1502/// A loadable module serves this, not the device firmware, and a model may
1503/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1504/// sent and not that anything acted on it.
1505///
1506/// # Safety
1507///
1508/// `remote` is null or a live handle, and `window` points at an initialised
1509/// [`mxr_video_wall_window_t`].
1510#[no_mangle]
1511pub unsafe extern "C" fn mxr_preview_video_wall(
1512    remote: *const mxr_remote_t,
1513    sink: mxr_uid_t,
1514    window: *const mxr_video_wall_window_t,
1515) -> mxr_result_t {
1516    // SAFETY: the caller guarantees a live handle or null.
1517    let handle = unsafe { remote.as_ref() };
1518    with(handle, |r| {
1519        // SAFETY: the caller guarantees an initialised struct or null.
1520        match unsafe { wall_window(window) } {
1521            Ok(w) => from_control(r.remote.preview_video_wall(sink.into(), w)),
1522            Err(code) => code,
1523        }
1524    })
1525}
1526
1527/// Stores a window as a sink's video wall.
1528///
1529/// The geometry is checked here and `MXR_ERR_INVALID_ARGUMENT` returned
1530/// without sending anything. That matters more than a refused frame would: a
1531/// sink running a video-wall module older than 2026083100 writes the window to
1532/// its configuration before asking its video processor to apply it, and the
1533/// processor's refusal does not undo the write, so an out-of-spec window
1534/// survives a reboot and is re-offered on every stream restart until something
1535/// else replaces it. A power cycle does not clear it.
1536///
1537/// A zero width or height stores "show the whole frame".
1538///
1539/// A loadable module serves this, not the device firmware, and a model may
1540/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1541/// sent and not that anything acted on it.
1542///
1543/// # Safety
1544///
1545/// `remote` is null or a live handle, and `window` points at an initialised
1546/// [`mxr_video_wall_window_t`].
1547#[no_mangle]
1548pub unsafe extern "C" fn mxr_store_video_wall(
1549    remote: *const mxr_remote_t,
1550    sink: mxr_uid_t,
1551    window: *const mxr_video_wall_window_t,
1552) -> mxr_result_t {
1553    // SAFETY: the caller guarantees a live handle or null.
1554    let handle = unsafe { remote.as_ref() };
1555    with(handle, |r| {
1556        // SAFETY: the caller guarantees an initialised struct or null.
1557        match unsafe { wall_window(window) } {
1558            Ok(w) => from_control(r.remote.store_video_wall(sink.into(), w)),
1559            Err(code) => code,
1560        }
1561    })
1562}
1563
1564/// Restores the window a sink has stored, discarding a preview.
1565///
1566/// Carries no window: the sink already holds the one this puts back.
1567///
1568/// A loadable module serves this, not the device firmware, and a model may
1569/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1570/// sent and not that anything acted on it.
1571///
1572/// # Safety
1573///
1574/// `remote` is null or a live handle from `mxr_remote_new()`.
1575#[no_mangle]
1576pub unsafe extern "C" fn mxr_revert_video_wall(
1577    remote: *const mxr_remote_t,
1578    sink: mxr_uid_t,
1579) -> mxr_result_t {
1580    // SAFETY: the caller guarantees a live handle or null.
1581    let handle = unsafe { remote.as_ref() };
1582    with(handle, |r| {
1583        from_control(r.remote.revert_video_wall(sink.into()))
1584    })
1585}
1586
1587// ---- multiviewer ----
1588
1589/// Switches a multiviewer's window layout.
1590///
1591/// A loadable module serves this, not the device firmware, and a model may
1592/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1593/// sent and not that anything acted on it.
1594///
1595/// # Safety
1596///
1597/// `remote` is null or a live handle from `mxr_remote_new()`.
1598#[no_mangle]
1599pub unsafe extern "C" fn mxr_set_multiviewer_view_mode(
1600    remote: *const mxr_remote_t,
1601    device: mxr_uid_t,
1602    mode: u8,
1603) -> mxr_result_t {
1604    // SAFETY: the caller guarantees a live handle or null.
1605    let handle = unsafe { remote.as_ref() };
1606    with(handle, |r| {
1607        from_control(
1608            r.remote
1609                .set_multiviewer_view_mode(device.into(), MultiviewerViewMode::from_wire(mode)),
1610        )
1611    })
1612}
1613
1614/// Puts a source in one of a multiviewer's windows.
1615///
1616/// A loadable module serves this, not the device firmware, and a model may
1617/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1618/// sent and not that anything acted on it.
1619///
1620/// # Safety
1621///
1622/// `remote` is null or a live handle from `mxr_remote_new()`.
1623#[no_mangle]
1624pub unsafe extern "C" fn mxr_set_multiviewer_video_source(
1625    remote: *const mxr_remote_t,
1626    device: mxr_uid_t,
1627    screen: u8,
1628    source: u8,
1629) -> mxr_result_t {
1630    // SAFETY: the caller guarantees a live handle or null.
1631    let handle = unsafe { remote.as_ref() };
1632    with(handle, |r| {
1633        from_control(r.remote.set_multiviewer_video_source(
1634            device.into(),
1635            screen,
1636            MultiviewerSource::from_wire(source),
1637        ))
1638    })
1639}
1640
1641/// Chooses which window a multiviewer takes its audio from.
1642///
1643/// A loadable module serves this, not the device firmware, and a model may
1644/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1645/// sent and not that anything acted on it.
1646///
1647/// # Safety
1648///
1649/// `remote` is null or a live handle from `mxr_remote_new()`.
1650#[no_mangle]
1651pub unsafe extern "C" fn mxr_set_multiviewer_audio_source(
1652    remote: *const mxr_remote_t,
1653    device: mxr_uid_t,
1654    source: u8,
1655) -> mxr_result_t {
1656    // SAFETY: the caller guarantees a live handle or null.
1657    let handle = unsafe { remote.as_ref() };
1658    with(handle, |r| {
1659        from_control(
1660            r.remote
1661                .set_multiviewer_audio_source(device.into(), MultiviewerSource::from_wire(source)),
1662        )
1663    })
1664}
1665
1666/// Sets a multiviewer's output volume and mute state.
1667///
1668/// A loadable module serves this, not the device firmware, and a model may
1669/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1670/// sent and not that anything acted on it.
1671///
1672/// # Safety
1673///
1674/// `remote` is null or a live handle from `mxr_remote_new()`.
1675#[no_mangle]
1676pub unsafe extern "C" fn mxr_set_multiviewer_audio_volume(
1677    remote: *const mxr_remote_t,
1678    device: mxr_uid_t,
1679    volume: u8,
1680    muted: bool,
1681) -> mxr_result_t {
1682    // SAFETY: the caller guarantees a live handle or null.
1683    let handle = unsafe { remote.as_ref() };
1684    with(handle, |r| {
1685        from_control(
1686            r.remote
1687                .set_multiviewer_audio_volume(device.into(), volume, muted),
1688        )
1689    })
1690}
1691
1692/// Switches the EDID a multiviewer presents to its sources.
1693///
1694/// A loadable module serves this, not the device firmware, and a model may
1695/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1696/// sent and not that anything acted on it.
1697///
1698/// # Safety
1699///
1700/// `remote` is null or a live handle from `mxr_remote_new()`.
1701#[no_mangle]
1702pub unsafe extern "C" fn mxr_set_multiviewer_edid_template(
1703    remote: *const mxr_remote_t,
1704    device: mxr_uid_t,
1705    template: u8,
1706) -> mxr_result_t {
1707    // SAFETY: the caller guarantees a live handle or null.
1708    let handle = unsafe { remote.as_ref() };
1709    with(handle, |r| {
1710        from_control(r.remote.set_multiviewer_edid_template(
1711            device.into(),
1712            MultiviewerEdidTemplate::from_wire(template),
1713        ))
1714    })
1715}
1716
1717/// Chooses which window a multiviewer forwards remote control to.
1718///
1719/// A loadable module serves this, not the device firmware, and a model may
1720/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1721/// sent and not that anything acted on it.
1722///
1723/// # Safety
1724///
1725/// `remote` is null or a live handle from `mxr_remote_new()`.
1726#[no_mangle]
1727pub unsafe extern "C" fn mxr_set_multiviewer_remote_control(
1728    remote: *const mxr_remote_t,
1729    device: mxr_uid_t,
1730    source: u8,
1731) -> mxr_result_t {
1732    // SAFETY: the caller guarantees a live handle or null.
1733    let handle = unsafe { remote.as_ref() };
1734    with(handle, |r| {
1735        from_control(
1736            r.remote.set_multiviewer_remote_control(
1737                device.into(),
1738                MultiviewerSource::from_wire(source),
1739            ),
1740        )
1741    })
1742}
1743
1744/// Sets the size of a multiviewer's picture-in-picture window.
1745///
1746/// A loadable module serves this, not the device firmware, and a model may
1747/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1748/// sent and not that anything acted on it.
1749///
1750/// # Safety
1751///
1752/// `remote` is null or a live handle from `mxr_remote_new()`.
1753#[no_mangle]
1754pub unsafe extern "C" fn mxr_set_multiviewer_pip_size(
1755    remote: *const mxr_remote_t,
1756    device: mxr_uid_t,
1757    size: u8,
1758) -> mxr_result_t {
1759    // SAFETY: the caller guarantees a live handle or null.
1760    let handle = unsafe { remote.as_ref() };
1761    with(handle, |r| {
1762        from_control(
1763            r.remote
1764                .set_multiviewer_pip_size(device.into(), MultiviewerPipSize::from_wire(size)),
1765        )
1766    })
1767}
1768
1769/// Sets which corner a multiviewer's picture-in-picture window sits in.
1770///
1771/// A loadable module serves this, not the device firmware, and a model may
1772/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1773/// sent and not that anything acted on it.
1774///
1775/// # Safety
1776///
1777/// `remote` is null or a live handle from `mxr_remote_new()`.
1778#[no_mangle]
1779pub unsafe extern "C" fn mxr_set_multiviewer_pip_position(
1780    remote: *const mxr_remote_t,
1781    device: mxr_uid_t,
1782    position: u8,
1783) -> mxr_result_t {
1784    // SAFETY: the caller guarantees a live handle or null.
1785    let handle = unsafe { remote.as_ref() };
1786    with(handle, |r| {
1787        from_control(r.remote.set_multiviewer_pip_position(
1788            device.into(),
1789            MultiviewerPipPosition::from_wire(position),
1790        ))
1791    })
1792}
1793
1794/// Sets how a multiviewer fits a source into its window.
1795///
1796/// A loadable module serves this, not the device firmware, and a model may
1797/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1798/// sent and not that anything acted on it.
1799///
1800/// # Safety
1801///
1802/// `remote` is null or a live handle from `mxr_remote_new()`.
1803#[no_mangle]
1804pub unsafe extern "C" fn mxr_set_multiviewer_aspect_ratio(
1805    remote: *const mxr_remote_t,
1806    device: mxr_uid_t,
1807    aspect: u8,
1808) -> mxr_result_t {
1809    // SAFETY: the caller guarantees a live handle or null.
1810    let handle = unsafe { remote.as_ref() };
1811    with(handle, |r| {
1812        from_control(
1813            r.remote.set_multiviewer_aspect_ratio(
1814                device.into(),
1815                MultiviewerAspectRatio::from_wire(aspect),
1816            ),
1817        )
1818    })
1819}
1820
1821/// Turns a multiviewer's automatic source switching on or off.
1822///
1823/// A loadable module serves this, not the device firmware, and a model may
1824/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1825/// sent and not that anything acted on it.
1826///
1827/// # Safety
1828///
1829/// `remote` is null or a live handle from `mxr_remote_new()`.
1830#[no_mangle]
1831pub unsafe extern "C" fn mxr_set_multiviewer_auto_switch(
1832    remote: *const mxr_remote_t,
1833    device: mxr_uid_t,
1834    enable: bool,
1835) -> mxr_result_t {
1836    // SAFETY: the caller guarantees a live handle or null.
1837    let handle = unsafe { remote.as_ref() };
1838    with(handle, |r| {
1839        from_control(r.remote.set_multiviewer_auto_switch(device.into(), enable))
1840    })
1841}
1842
1843/// Switches a multiviewer's output resolution.
1844///
1845/// A loadable module serves this, not the device firmware, and a model may
1846/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1847/// sent and not that anything acted on it.
1848///
1849/// # Safety
1850///
1851/// `remote` is null or a live handle from `mxr_remote_new()`.
1852#[no_mangle]
1853pub unsafe extern "C" fn mxr_set_multiviewer_output_mode(
1854    remote: *const mxr_remote_t,
1855    device: mxr_uid_t,
1856    mode: u8,
1857) -> mxr_result_t {
1858    // SAFETY: the caller guarantees a live handle or null.
1859    let handle = unsafe { remote.as_ref() };
1860    with(handle, |r| {
1861        from_control(
1862            r.remote
1863                .set_multiviewer_output_mode(device.into(), MultiviewerOutputMode::from_wire(mode)),
1864        )
1865    })
1866}
1867
1868/// Sets a multiviewer's IT content flag.
1869///
1870/// A loadable module serves this, not the device firmware, and a model may
1871/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1872/// sent and not that anything acted on it.
1873///
1874/// # Safety
1875///
1876/// `remote` is null or a live handle from `mxr_remote_new()`.
1877#[no_mangle]
1878pub unsafe extern "C" fn mxr_set_multiviewer_output_itc(
1879    remote: *const mxr_remote_t,
1880    device: mxr_uid_t,
1881    mode: u8,
1882) -> mxr_result_t {
1883    // SAFETY: the caller guarantees a live handle or null.
1884    let handle = unsafe { remote.as_ref() };
1885    with(handle, |r| {
1886        from_control(
1887            r.remote
1888                .set_multiviewer_output_itc(device.into(), MultiviewerItcMode::from_wire(mode)),
1889        )
1890    })
1891}
1892
1893/// Switches a multiviewer's HDCP mode.
1894///
1895/// A loadable module serves this, not the device firmware, and a model may
1896/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1897/// sent and not that anything acted on it.
1898///
1899/// # Safety
1900///
1901/// `remote` is null or a live handle from `mxr_remote_new()`.
1902#[no_mangle]
1903pub unsafe extern "C" fn mxr_set_multiviewer_hdcp_mode(
1904    remote: *const mxr_remote_t,
1905    device: mxr_uid_t,
1906    mode: u8,
1907) -> mxr_result_t {
1908    // SAFETY: the caller guarantees a live handle or null.
1909    let handle = unsafe { remote.as_ref() };
1910    with(handle, |r| {
1911        from_control(
1912            r.remote
1913                .set_multiviewer_hdcp_mode(device.into(), MultiviewerHdcpMode::from_wire(mode)),
1914        )
1915    })
1916}
1917
1918/// Maps one of a multiviewer's inputs to a source device.
1919///
1920/// A loadable module serves this, not the device firmware, and a model may
1921/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1922/// sent and not that anything acted on it.
1923///
1924/// # Safety
1925///
1926/// `remote` is null or a live handle from `mxr_remote_new()`.
1927#[no_mangle]
1928pub unsafe extern "C" fn mxr_set_multiviewer_input_source(
1929    remote: *const mxr_remote_t,
1930    device: mxr_uid_t,
1931    input: u8,
1932    source: mxr_uid_t,
1933) -> mxr_result_t {
1934    // SAFETY: the caller guarantees a live handle or null.
1935    let handle = unsafe { remote.as_ref() };
1936    with(handle, |r| {
1937        from_control(
1938            r.remote
1939                .set_multiviewer_input_source(device.into(), input, source.into()),
1940        )
1941    })
1942}
1943
1944/// Asks a multiviewer to map its inputs to the sources it can see.
1945///
1946/// A loadable module serves this, not the device firmware, and a model may
1947/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1948/// sent and not that anything acted on it.
1949///
1950/// # Safety
1951///
1952/// `remote` is null or a live handle from `mxr_remote_new()`.
1953#[no_mangle]
1954pub unsafe extern "C" fn mxr_multiviewer_auto_route(
1955    remote: *const mxr_remote_t,
1956    device: mxr_uid_t,
1957) -> mxr_result_t {
1958    // SAFETY: the caller guarantees a live handle or null.
1959    let handle = unsafe { remote.as_ref() };
1960    with(handle, |r| {
1961        from_control(r.remote.multiviewer_auto_route(device.into()))
1962    })
1963}