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}