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;
18
19use mx_remote::{
20 DeviceUid, EdidProfile, MultiviewerAspectRatio, MultiviewerEdidTemplate, MultiviewerHdcpMode,
21 MultiviewerItcMode, MultiviewerOutputMode, MultiviewerPipPosition, MultiviewerPipSize,
22 MultiviewerSource, MultiviewerViewMode, RcAction, RcKey, V2ipAudioFormat, V2ipRoute,
23 V2ipRouteTarget, VideoWallWindow,
24};
25
26use crate::abi::{
27 fail, from_control, mxr_bay_uid_t, mxr_result_t, mxr_tribool_t, mxr_uid_t, opt_str, req_str,
28};
29use crate::info::mxr_amp_zone_settings_t;
30use crate::remote::{mxr_remote_t, with};
31
32/// A stream's sample rate and channel count.
33#[repr(C)]
34#[derive(Clone, Copy)]
35pub struct mxr_audio_format_t {
36 /// Sample rate in Hz.
37 pub sample_rate: u32,
38 /// Channel count.
39 pub channels: u8,
40}
41
42impl From<mxr_audio_format_t> for V2ipAudioFormat {
43 fn from(f: mxr_audio_format_t) -> Self {
44 Self {
45 sample_rate: f.sample_rate,
46 channels: f.channels,
47 }
48 }
49}
50
51// ---- routing ----
52
53/// Routes a V2IP sink's video to the stream a source port advertises.
54///
55/// # Safety
56///
57/// `remote` is null or a live handle from `mxr_remote_new()`.
58#[no_mangle]
59pub unsafe extern "C" fn mxr_select_video_source(
60 remote: *const mxr_remote_t,
61 sink: mxr_bay_uid_t,
62 source_port: u16,
63) -> mxr_result_t {
64 // SAFETY: the caller guarantees a live handle or null.
65 let handle = unsafe { remote.as_ref() };
66 with(handle, |r| {
67 from_control(r.remote.select_video_source(sink.into(), source_port))
68 })
69}
70
71/// Routes a V2IP sink's audio to the stream a source port advertises,
72/// leaving its video where it is.
73///
74/// # Safety
75///
76/// `remote` is null or a live handle from `mxr_remote_new()`.
77#[no_mangle]
78pub unsafe extern "C" fn mxr_select_audio_source(
79 remote: *const mxr_remote_t,
80 sink: mxr_bay_uid_t,
81 source_port: u16,
82) -> mxr_result_t {
83 // SAFETY: the caller guarantees a live handle or null.
84 let handle = unsafe { remote.as_ref() };
85 with(handle, |r| {
86 from_control(r.remote.select_audio_source(sink.into(), source_port))
87 })
88}
89
90/// Routes a V2IP sink's video to the source bay with this user-assigned name.
91///
92/// # Safety
93///
94/// `remote` is null or a live handle, and `name` is a NUL-terminated string.
95#[no_mangle]
96pub unsafe extern "C" fn mxr_select_video_source_by_name(
97 remote: *const mxr_remote_t,
98 sink: mxr_bay_uid_t,
99 name: *const c_char,
100) -> mxr_result_t {
101 // SAFETY: the caller guarantees a live handle or null.
102 let handle = unsafe { remote.as_ref() };
103 with(handle, |r| {
104 // SAFETY: the caller guarantees a NUL-terminated string.
105 match unsafe { req_str(name) } {
106 Ok(name) => from_control(r.remote.select_video_source_by_name(sink.into(), name)),
107 Err(code) => code,
108 }
109 })
110}
111
112/// Routes a V2IP sink's audio to the source bay with this user-assigned name.
113///
114/// `format` may be null to leave the sink's audio format alone.
115///
116/// # Safety
117///
118/// `remote` is null or a live handle, `name` is a NUL-terminated string, and
119/// `format` is null or points at an initialised [`mxr_audio_format_t`].
120#[no_mangle]
121pub unsafe extern "C" fn mxr_select_audio_source_by_name(
122 remote: *const mxr_remote_t,
123 sink: mxr_bay_uid_t,
124 name: *const c_char,
125 format: *const mxr_audio_format_t,
126) -> mxr_result_t {
127 // SAFETY: the caller guarantees a live handle or null.
128 let handle = unsafe { remote.as_ref() };
129 with(handle, |r| {
130 // SAFETY: the caller guarantees a NUL-terminated string.
131 let name = match unsafe { req_str(name) } {
132 Ok(name) => name,
133 Err(code) => return code,
134 };
135 // SAFETY: the caller guarantees an initialised struct or null.
136 let format = unsafe { format.as_ref() }.map(|f| (*f).into());
137 from_control(
138 r.remote
139 .select_audio_source_by_name(sink.into(), name, format),
140 )
141 })
142}
143
144/// Routes a V2IP sink's audio to a multicast group directly, for a source this
145/// client has not heard advertise it.
146///
147/// `audio_port` may be zero for the default, and `format` may be null to leave
148/// the sink's audio format alone.
149///
150/// # Safety
151///
152/// `remote` is null or a live handle, `audio_ip` is a NUL-terminated dotted
153/// quad, and `format` is null or points at an initialised
154/// [`mxr_audio_format_t`].
155#[no_mangle]
156pub unsafe extern "C" fn mxr_select_audio_source_addr(
157 remote: *const mxr_remote_t,
158 sink: mxr_bay_uid_t,
159 audio_ip: *const c_char,
160 audio_port: u16,
161 format: *const mxr_audio_format_t,
162) -> mxr_result_t {
163 // SAFETY: the caller guarantees a live handle or null.
164 let handle = unsafe { remote.as_ref() };
165 with(handle, |r| {
166 // SAFETY: the caller guarantees a NUL-terminated string.
167 let text = match unsafe { req_str(audio_ip) } {
168 Ok(text) => text,
169 Err(code) => return code,
170 };
171 let Ok(ip) = text.parse() else {
172 return fail(
173 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
174 &format!("audio_ip is not an IPv4 address: {text:?}"),
175 );
176 };
177 // SAFETY: the caller guarantees an initialised struct or null.
178 let format = unsafe { format.as_ref() }.map(|f| (*f).into());
179 from_control(r.remote.select_audio_source_addr(
180 sink.into(),
181 ip,
182 // Zero is not a port a stream can arrive on, so it is how the
183 // caller declines to name one.
184 (audio_port != 0).then_some(audio_port),
185 format,
186 ))
187 })
188}
189
190/// One stream of a route the caller assembles.
191#[repr(C)]
192#[derive(Clone, Copy)]
193pub struct mxr_stream_addr_t {
194 /// The multicast group, as a dotted quad. Null or empty sends the slot
195 /// zeroed, naming no group for that stream.
196 ///
197 /// It is not a way to leave one stream alone. The firmware decides
198 /// whether a sink has a manual route at all by reading the video and
199 /// ancillary slots, so an empty one of those disqualifies the whole
200 /// route rather than preserving anything - see
201 /// `mxr_select_source_addr()`.
202 pub ip: *const c_char,
203 /// The destination UDP port. Zero means the standard port for the stream
204 /// this slot names.
205 pub port: u16,
206}
207
208/// The three streams a manual route points a V2IP sink at.
209#[repr(C)]
210#[derive(Clone, Copy)]
211pub struct mxr_v2ip_route_t {
212 /// The video stream, at port 50020 unless the port says otherwise.
213 pub video: mxr_stream_addr_t,
214 /// The audio stream, at port 50022 unless the port says otherwise.
215 pub audio: mxr_stream_addr_t,
216 /// The ancillary-data stream, at port 50021 unless the port says
217 /// otherwise.
218 pub anc: mxr_stream_addr_t,
219}
220
221/// Reads one route slot, where a null or empty address means "not set".
222///
223/// # Safety
224///
225/// `slot.ip` is null or a NUL-terminated string.
226unsafe fn to_target(slot: mxr_stream_addr_t, what: &str) -> Result<V2ipRouteTarget, mxr_result_t> {
227 // SAFETY: the caller guarantees a NUL-terminated string or null.
228 let text = unsafe { opt_str(slot.ip) }?.unwrap_or_default();
229 if text.is_empty() {
230 return Ok(V2ipRouteTarget::default());
231 }
232 match text.parse() {
233 Ok(ip) => Ok(V2ipRouteTarget {
234 ip,
235 port: slot.port,
236 }),
237 Err(_) => Err(fail(
238 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
239 &format!("{what} is not an IPv4 address: {text:?}"),
240 )),
241 }
242}
243
244/// Routes a V2IP sink's video, audio and ancillary streams to multicast groups
245/// the caller names.
246///
247/// This is the only way to reach a stream no device on the mesh advertises,
248/// such as one the calling program is transmitting itself; the routes by
249/// source port and by name can only name a stream some bay has announced.
250///
251/// Set all three groups. The firmware decides whether a sink has a manual
252/// route by looking at the video and ancillary groups, so a route that leaves
253/// either unset does not register as one and the sink falls back to the audio
254/// source its mesh picks.
255///
256/// A null `format` sends 48kHz stereo rather than omitting the field. The
257/// firmware stores whatever the frame carries and hands it to the FPGA
258/// unexamined, so a frame without a format leaves a zero sample rate there,
259/// which the FPGA rejects and which takes the switch down with it.
260///
261/// # Safety
262///
263/// `remote` is null or a live handle, `route` points at an initialised
264/// [`mxr_v2ip_route_t`] whose addresses are null or NUL-terminated strings,
265/// and `format` is null or points at an initialised [`mxr_audio_format_t`].
266#[no_mangle]
267pub unsafe extern "C" fn mxr_select_source_addr(
268 remote: *const mxr_remote_t,
269 sink: mxr_bay_uid_t,
270 route: *const mxr_v2ip_route_t,
271 format: *const mxr_audio_format_t,
272) -> mxr_result_t {
273 // SAFETY: the caller guarantees a live handle or null.
274 let handle = unsafe { remote.as_ref() };
275 with(handle, |r| {
276 // SAFETY: the caller guarantees an initialised struct or null.
277 let Some(route) = (unsafe { route.as_ref() }) else {
278 return fail(
279 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
280 "the route pointer is null",
281 );
282 };
283 // SAFETY: the caller guarantees NUL-terminated strings or null.
284 let route = unsafe {
285 match (
286 to_target(route.video, "video ip"),
287 to_target(route.audio, "audio ip"),
288 to_target(route.anc, "anc ip"),
289 ) {
290 (Ok(video), Ok(audio), Ok(anc)) => V2ipRoute { video, audio, anc },
291 (Err(code), _, _) | (_, Err(code), _) | (_, _, Err(code)) => return code,
292 }
293 };
294 // SAFETY: the caller guarantees an initialised struct or null.
295 let format = unsafe { format.as_ref() }.map(|f| (*f).into());
296 from_control(r.remote.select_source_addr(sink.into(), route, format))
297 })
298}
299
300// ---- bays ----
301
302/// Renames a bay. The device stores the first 16 bytes.
303///
304/// # Safety
305///
306/// `remote` is null or a live handle, and `name` is a NUL-terminated string.
307#[no_mangle]
308pub unsafe extern "C" fn mxr_set_bay_name(
309 remote: *const mxr_remote_t,
310 bay: mxr_bay_uid_t,
311 name: *const c_char,
312) -> mxr_result_t {
313 // SAFETY: the caller guarantees a live handle or null.
314 let handle = unsafe { remote.as_ref() };
315 with(handle, |r| {
316 // SAFETY: the caller guarantees a NUL-terminated string.
317 match unsafe { req_str(name) } {
318 Ok(name) => from_control(r.remote.set_bay_name(bay.into(), name)),
319 Err(code) => code,
320 }
321 })
322}
323
324/// Hides a bay from the installation's user interface, or shows it again.
325///
326/// # Safety
327///
328/// `remote` is null or a live handle from `mxr_remote_new()`.
329#[no_mangle]
330pub unsafe extern "C" fn mxr_set_bay_hidden(
331 remote: *const mxr_remote_t,
332 bay: mxr_bay_uid_t,
333 hidden: bool,
334) -> mxr_result_t {
335 // SAFETY: the caller guarantees a live handle or null.
336 let handle = unsafe { remote.as_ref() };
337 with(handle, |r| {
338 from_control(r.remote.set_bay_hidden(bay.into(), hidden))
339 })
340}
341
342/// Switches an input bay's EDID profile.
343///
344/// # Safety
345///
346/// `remote` is null or a live handle from `mxr_remote_new()`.
347#[no_mangle]
348pub unsafe extern "C" fn mxr_select_edid_profile(
349 remote: *const mxr_remote_t,
350 bay: mxr_bay_uid_t,
351 profile: u16,
352) -> mxr_result_t {
353 // SAFETY: the caller guarantees a live handle or null.
354 let handle = unsafe { remote.as_ref() };
355 with(handle, |r| {
356 from_control(
357 r.remote
358 .select_edid_profile(bay.into(), EdidProfile::from_wire(profile)),
359 )
360 })
361}
362
363/// Sends a remote-control action to whatever is attached to a bay.
364///
365/// # Safety
366///
367/// `remote` is null or a live handle from `mxr_remote_new()`.
368#[no_mangle]
369pub unsafe extern "C" fn mxr_send_action(
370 remote: *const mxr_remote_t,
371 bay: mxr_bay_uid_t,
372 action: u16,
373) -> mxr_result_t {
374 // SAFETY: the caller guarantees a live handle or null.
375 let handle = unsafe { remote.as_ref() };
376 with(handle, |r| {
377 from_control(
378 r.remote
379 .send_action(bay.into(), RcAction::from_wire(action)),
380 )
381 })
382}
383
384/// Sends a remote-control key press to whatever is attached to a bay.
385///
386/// The device forwards it over CEC, infrared or IP, whichever that bay is
387/// configured for; the caller does not choose. `key` is one of the `MXR_KEY_*`
388/// values, or a raw code above `MXR_KEY_CUSTOM_CEC` or `MXR_KEY_CUSTOM_SKY`.
389/// A value this library has no name for is sent as it was given.
390///
391/// `mxr_send_action()` names an outcome instead, and lets the device decide
392/// which keys reach it.
393///
394/// # Safety
395///
396/// `remote` is null or a live handle from `mxr_remote_new()`.
397#[no_mangle]
398pub unsafe extern "C" fn mxr_send_key(
399 remote: *const mxr_remote_t,
400 bay: mxr_bay_uid_t,
401 key: u16,
402) -> mxr_result_t {
403 // SAFETY: the caller guarantees a live handle or null.
404 let handle = unsafe { remote.as_ref() };
405 with(handle, |r| {
406 from_control(r.remote.send_key(bay.into(), RcKey::from_wire(key)))
407 })
408}
409
410/// Powers on what is attached to a bay.
411///
412/// # Safety
413///
414/// `remote` is null or a live handle from `mxr_remote_new()`.
415#[no_mangle]
416pub unsafe extern "C" fn mxr_power_on(
417 remote: *const mxr_remote_t,
418 bay: mxr_bay_uid_t,
419) -> mxr_result_t {
420 // SAFETY: the caller guarantees a live handle or null.
421 let handle = unsafe { remote.as_ref() };
422 with(handle, |r| from_control(r.remote.power_on(bay.into())))
423}
424
425/// Powers off what is attached to a bay.
426///
427/// # Safety
428///
429/// `remote` is null or a live handle from `mxr_remote_new()`.
430#[no_mangle]
431pub unsafe extern "C" fn mxr_power_off(
432 remote: *const mxr_remote_t,
433 bay: mxr_bay_uid_t,
434) -> mxr_result_t {
435 // SAFETY: the caller guarantees a live handle or null.
436 let handle = unsafe { remote.as_ref() };
437 with(handle, |r| from_control(r.remote.power_off(bay.into())))
438}
439
440// ---- volume ----
441
442/// Sets a bay's volume percentage, and its mute state when `muted` is not
443/// `MXR_UNKNOWN`.
444///
445/// A bay with no volume control of its own is set through its `linked_bay`,
446/// so an output wired to an amplifier zone reaches that zone.
447///
448/// # Safety
449///
450/// `remote` is null or a live handle from `mxr_remote_new()`.
451#[no_mangle]
452pub unsafe extern "C" fn mxr_set_volume(
453 remote: *const mxr_remote_t,
454 bay: mxr_bay_uid_t,
455 volume: u8,
456 muted: mxr_tribool_t,
457) -> mxr_result_t {
458 // SAFETY: the caller guarantees a live handle or null.
459 let handle = unsafe { remote.as_ref() };
460 with(handle, |r| {
461 from_control(r.remote.set_volume(
462 bay.into(),
463 volume,
464 match muted {
465 mxr_tribool_t::MXR_UNKNOWN => None,
466 mxr_tribool_t::MXR_FALSE => Some(false),
467 mxr_tribool_t::MXR_TRUE => Some(true),
468 },
469 ))
470 })
471}
472
473/// Asks a bay to step its volume up.
474///
475/// # Safety
476///
477/// `remote` is null or a live handle from `mxr_remote_new()`.
478#[no_mangle]
479pub unsafe extern "C" fn mxr_volume_up(
480 remote: *const mxr_remote_t,
481 bay: mxr_bay_uid_t,
482) -> mxr_result_t {
483 // SAFETY: the caller guarantees a live handle or null.
484 let handle = unsafe { remote.as_ref() };
485 with(handle, |r| from_control(r.remote.volume_up(bay.into())))
486}
487
488/// Asks a bay to step its volume down.
489///
490/// # Safety
491///
492/// `remote` is null or a live handle from `mxr_remote_new()`.
493#[no_mangle]
494pub unsafe extern "C" fn mxr_volume_down(
495 remote: *const mxr_remote_t,
496 bay: mxr_bay_uid_t,
497) -> mxr_result_t {
498 // SAFETY: the caller guarantees a live handle or null.
499 let handle = unsafe { remote.as_ref() };
500 with(handle, |r| from_control(r.remote.volume_down(bay.into())))
501}
502
503/// Mutes or unmutes a bay, leaving its volume alone.
504///
505/// # Safety
506///
507/// `remote` is null or a live handle from `mxr_remote_new()`.
508#[no_mangle]
509pub unsafe extern "C" fn mxr_set_muted(
510 remote: *const mxr_remote_t,
511 bay: mxr_bay_uid_t,
512 muted: bool,
513) -> mxr_result_t {
514 // SAFETY: the caller guarantees a live handle or null.
515 let handle = unsafe { remote.as_ref() };
516 with(handle, |r| {
517 from_control(r.remote.set_muted(bay.into(), muted))
518 })
519}
520
521/// Writes an amplifier zone's gain, delay, tone and power settings.
522///
523/// This replaces every setting at once, so a caller changing one reads the
524/// current set with `mxr_bay_amp_settings()`
525/// first.
526///
527/// # Safety
528///
529/// `remote` is null or a live handle, and `settings` points at an initialised
530/// [`mxr_amp_zone_settings_t`].
531#[no_mangle]
532pub unsafe extern "C" fn mxr_set_amp_zone_settings(
533 remote: *const mxr_remote_t,
534 bay: mxr_bay_uid_t,
535 settings: *const mxr_amp_zone_settings_t,
536) -> mxr_result_t {
537 // SAFETY: the caller guarantees a live handle or null.
538 let handle = unsafe { remote.as_ref() };
539 with(handle, |r| {
540 // SAFETY: the caller guarantees an initialised struct or null.
541 let Some(settings) = (unsafe { settings.as_ref() }) else {
542 return fail(
543 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
544 "the amp zone settings pointer is null",
545 );
546 };
547 from_control(
548 r.remote
549 .set_amp_zone_settings(bay.into(), (*settings).into()),
550 )
551 })
552}
553
554// ---- audio endpoints ----
555
556/// Mutes or unmutes one of a device's audio endpoints.
557///
558/// A loadable module serves this, not the device firmware, and a model may
559/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
560/// sent and not that anything acted on it.
561///
562/// # Safety
563///
564/// `remote` is null or a live handle from `mxr_remote_new()`.
565#[no_mangle]
566pub unsafe extern "C" fn mxr_set_audio_endpoint_muted(
567 remote: *const mxr_remote_t,
568 device: mxr_uid_t,
569 endpoint: u16,
570 muted: bool,
571) -> mxr_result_t {
572 // SAFETY: the caller guarantees a live handle or null.
573 let handle = unsafe { remote.as_ref() };
574 with(handle, |r| {
575 from_control(
576 r.remote
577 .set_audio_endpoint_muted(device.into(), endpoint, muted),
578 )
579 })
580}
581
582/// Activates or clears an audio endpoint's trigger.
583///
584/// A loadable module serves this, not the device firmware, and a model may
585/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
586/// sent and not that anything acted on it.
587///
588/// # Safety
589///
590/// `remote` is null or a live handle from `mxr_remote_new()`.
591#[no_mangle]
592pub unsafe extern "C" fn mxr_set_audio_endpoint_trigger(
593 remote: *const mxr_remote_t,
594 device: mxr_uid_t,
595 endpoint: u16,
596 active: bool,
597) -> mxr_result_t {
598 // SAFETY: the caller guarantees a live handle or null.
599 let handle = unsafe { remote.as_ref() };
600 with(handle, |r| {
601 from_control(
602 r.remote
603 .set_audio_endpoint_trigger(device.into(), endpoint, active),
604 )
605 })
606}
607
608/// Sets an audio endpoint's volume.
609///
610/// A loadable module serves this, not the device firmware, and a model may
611/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
612/// sent and not that anything acted on it.
613///
614/// # Safety
615///
616/// `remote` is null or a live handle from `mxr_remote_new()`.
617#[no_mangle]
618pub unsafe extern "C" fn mxr_set_audio_endpoint_volume(
619 remote: *const mxr_remote_t,
620 device: mxr_uid_t,
621 endpoint: u16,
622 volume: u32,
623) -> mxr_result_t {
624 // SAFETY: the caller guarantees a live handle or null.
625 let handle = unsafe { remote.as_ref() };
626 with(handle, |r| {
627 from_control(
628 r.remote
629 .set_audio_endpoint_volume(device.into(), endpoint, volume),
630 )
631 })
632}
633
634/// Points one device's audio endpoint at another device's.
635///
636/// `sink` is the end doing the listening and `source` the end being
637/// listened to.
638///
639/// A loadable module serves this, not the device firmware, and a model may
640/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
641/// sent and not that anything acted on it.
642///
643/// # Safety
644///
645/// `remote` is null or a live handle from `mxr_remote_new()`.
646#[no_mangle]
647pub unsafe extern "C" fn mxr_select_audio_endpoint_input(
648 remote: *const mxr_remote_t,
649 sink: mxr_uid_t,
650 sink_endpoint: u16,
651 source: mxr_uid_t,
652 source_endpoint: u16,
653) -> mxr_result_t {
654 // SAFETY: the caller guarantees a live handle or null.
655 let handle = unsafe { remote.as_ref() };
656 with(handle, |r| {
657 from_control(r.remote.select_audio_endpoint_input(
658 sink.into(),
659 sink_endpoint,
660 source.into(),
661 source_endpoint,
662 ))
663 })
664}
665
666// ---- devices ----
667
668/// Asks a device for an EDID: the one the display on its output publishes, or
669/// the one the device presents to the source on its input.
670///
671/// The device answers a moment later. The bytes reach `on_edid_received` and
672/// stay readable through `mxr_device_edid()`.
673///
674/// Only V2IP hardware handles this opcode. A matrix or an amplifier accepts
675/// the frame and answers nothing, at any protocol version, so the silence that
676/// follows is permanent rather than a reply still to come. `MXR_OK` here means
677/// the frame was sent, and nothing more; a caller polling for an EDID should
678/// ask a device that can answer rather than wait on one that cannot.
679///
680/// # Safety
681///
682/// `remote` is null or a live handle from `mxr_remote_new()`.
683#[no_mangle]
684pub unsafe extern "C" fn mxr_request_edid(
685 remote: *const mxr_remote_t,
686 device: mxr_uid_t,
687 output: bool,
688) -> mxr_result_t {
689 // SAFETY: the caller guarantees a live handle or null.
690 let handle = unsafe { remote.as_ref() };
691 with(handle, |r| {
692 from_control(r.remote.request_edid(device.into(), output))
693 })
694}
695
696/// Asks for a detailed signal report from every bay of one device, or - with
697/// the zero uid - from every bay on the network.
698///
699/// Devices report on their own when a signal changes, so this is what a client
700/// that has just started needs: without it, a bay that has been showing the
701/// same picture for an hour says nothing until it changes.
702///
703/// # Safety
704///
705/// `remote` is null or a live handle from `mxr_remote_new()`.
706#[no_mangle]
707pub unsafe extern "C" fn mxr_request_signal_status(
708 remote: *const mxr_remote_t,
709 device: mxr_uid_t,
710) -> mxr_result_t {
711 // SAFETY: the caller guarantees a live handle or null.
712 let handle = unsafe { remote.as_ref() };
713 with(handle, |r| {
714 let uid = DeviceUid::from(device);
715 let target = (uid != DeviceUid::ZERO).then_some(uid);
716 from_control(r.remote.request_signal_status(target))
717 })
718}
719
720/// Subscribes to, or unsubscribes from, a device's V2IP statistics.
721///
722/// # Safety
723///
724/// `remote` is null or a live handle from `mxr_remote_new()`.
725#[no_mangle]
726pub unsafe extern "C" fn mxr_subscribe_v2ip_stats(
727 remote: *const mxr_remote_t,
728 device: mxr_uid_t,
729 subscribe: bool,
730) -> mxr_result_t {
731 // SAFETY: the caller guarantees a live handle or null.
732 let handle = unsafe { remote.as_ref() };
733 with(handle, |r| {
734 from_control(r.remote.subscribe_v2ip_stats(device.into(), subscribe))
735 })
736}
737
738/// Reboots a device.
739///
740/// # Safety
741///
742/// `remote` is null or a live handle from `mxr_remote_new()`.
743#[no_mangle]
744pub unsafe extern "C" fn mxr_reboot(
745 remote: *const mxr_remote_t,
746 device: mxr_uid_t,
747) -> mxr_result_t {
748 // SAFETY: the caller guarantees a live handle or null.
749 let handle = unsafe { remote.as_ref() };
750 with(handle, |r| from_control(r.remote.reboot(device.into())))
751}
752
753/// Sends the monitoring pulse that tells devices this client is watching.
754///
755/// # Safety
756///
757/// `remote` is null or a live handle from `mxr_remote_new()`.
758#[no_mangle]
759pub unsafe extern "C" fn mxr_send_monitoring_pulse(remote: *const mxr_remote_t) -> mxr_result_t {
760 // SAFETY: the caller guarantees a live handle or null.
761 let handle = unsafe { remote.as_ref() };
762 with(handle, |r| from_control(r.remote.send_monitoring_pulse()))
763}
764
765// ---- video wall ----
766
767/// Where a video-wall sink's window sits, and the picture it was measured
768/// against.
769///
770/// `pos_x` must be a multiple of `MXR_VIDEO_WALL_POS_ALIGN`, `width` a
771/// multiple of `MXR_VIDEO_WALL_WIDTH_ALIGN`, both sides at least
772/// `MXR_VIDEO_WALL_MIN_SIZE`, and the window must fit inside the raster it
773/// names. `pos_y` and `height` have no alignment rule. A zero `width` or
774/// `height` clears the wall and is checked against none of this.
775#[repr(C)]
776#[derive(Clone, Copy)]
777pub struct mxr_video_wall_window_t {
778 /// Window origin, horizontal.
779 pub pos_x: u16,
780 /// Window origin, vertical.
781 pub pos_y: u16,
782 /// Window width, or zero to clear the wall.
783 pub width: u16,
784 /// Window height, or zero to clear the wall.
785 pub height: u16,
786 /// Active picture width the window was measured against.
787 pub raster_w: u16,
788 /// Active picture height the window was measured against.
789 pub raster_h: u16,
790}
791
792/// A window's horizontal origin must be a multiple of this.
793pub const MXR_VIDEO_WALL_POS_ALIGN: u16 = 64;
794
795/// A window's width must be a multiple of this.
796pub const MXR_VIDEO_WALL_WIDTH_ALIGN: u16 = 4;
797
798/// Neither side of a window may be smaller than this.
799pub const MXR_VIDEO_WALL_MIN_SIZE: u16 = 64;
800
801impl From<mxr_video_wall_window_t> for VideoWallWindow {
802 fn from(w: mxr_video_wall_window_t) -> Self {
803 Self {
804 pos_x: w.pos_x,
805 pos_y: w.pos_y,
806 width: w.width,
807 height: w.height,
808 raster_w: w.raster_w,
809 raster_h: w.raster_h,
810 }
811 }
812}
813
814/// Reads a window argument, refusing a null pointer.
815unsafe fn wall_window(
816 window: *const mxr_video_wall_window_t,
817) -> Result<VideoWallWindow, mxr_result_t> {
818 // SAFETY: the caller guarantees an initialised struct or null.
819 match unsafe { window.as_ref() } {
820 Some(w) => Ok((*w).into()),
821 None => Err(fail(
822 mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
823 "the window pointer is null",
824 )),
825 }
826}
827
828/// Shows a window on a sink's video wall without storing it.
829///
830/// The window lasts until the sink is told otherwise or restarts;
831/// `mxr_revert_video_wall()` puts back whatever it has stored. A zero width or
832/// height shows the whole frame again.
833///
834/// The geometry is checked here and `MXR_ERR_INVALID_ARGUMENT` returned
835/// without sending anything, because the sink is not guaranteed to check it
836/// itself.
837///
838/// A loadable module serves this, not the device firmware, and a model may
839/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
840/// sent and not that anything acted on it.
841///
842/// # Safety
843///
844/// `remote` is null or a live handle, and `window` points at an initialised
845/// [`mxr_video_wall_window_t`].
846#[no_mangle]
847pub unsafe extern "C" fn mxr_preview_video_wall(
848 remote: *const mxr_remote_t,
849 sink: mxr_uid_t,
850 window: *const mxr_video_wall_window_t,
851) -> mxr_result_t {
852 // SAFETY: the caller guarantees a live handle or null.
853 let handle = unsafe { remote.as_ref() };
854 with(handle, |r| {
855 // SAFETY: the caller guarantees an initialised struct or null.
856 match unsafe { wall_window(window) } {
857 Ok(w) => from_control(r.remote.preview_video_wall(sink.into(), w)),
858 Err(code) => code,
859 }
860 })
861}
862
863/// Stores a window as a sink's video wall.
864///
865/// The geometry is checked here and `MXR_ERR_INVALID_ARGUMENT` returned
866/// without sending anything. That matters more than a refused frame would: a
867/// sink running a video-wall module older than 2026083100 writes the window to
868/// its configuration before asking its video processor to apply it, and the
869/// processor's refusal does not undo the write, so an out-of-spec window
870/// survives a reboot and is re-offered on every stream restart until something
871/// else replaces it. A power cycle does not clear it.
872///
873/// A zero width or height stores "show the whole frame".
874///
875/// A loadable module serves this, not the device firmware, and a model may
876/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
877/// sent and not that anything acted on it.
878///
879/// # Safety
880///
881/// `remote` is null or a live handle, and `window` points at an initialised
882/// [`mxr_video_wall_window_t`].
883#[no_mangle]
884pub unsafe extern "C" fn mxr_store_video_wall(
885 remote: *const mxr_remote_t,
886 sink: mxr_uid_t,
887 window: *const mxr_video_wall_window_t,
888) -> mxr_result_t {
889 // SAFETY: the caller guarantees a live handle or null.
890 let handle = unsafe { remote.as_ref() };
891 with(handle, |r| {
892 // SAFETY: the caller guarantees an initialised struct or null.
893 match unsafe { wall_window(window) } {
894 Ok(w) => from_control(r.remote.store_video_wall(sink.into(), w)),
895 Err(code) => code,
896 }
897 })
898}
899
900/// Restores the window a sink has stored, discarding a preview.
901///
902/// Carries no window: the sink already holds the one this puts back.
903///
904/// A loadable module serves this, not the device firmware, and a model may
905/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
906/// sent and not that anything acted on it.
907///
908/// # Safety
909///
910/// `remote` is null or a live handle from `mxr_remote_new()`.
911#[no_mangle]
912pub unsafe extern "C" fn mxr_revert_video_wall(
913 remote: *const mxr_remote_t,
914 sink: mxr_uid_t,
915) -> mxr_result_t {
916 // SAFETY: the caller guarantees a live handle or null.
917 let handle = unsafe { remote.as_ref() };
918 with(handle, |r| {
919 from_control(r.remote.revert_video_wall(sink.into()))
920 })
921}
922
923// ---- multiviewer ----
924
925/// Switches a multiviewer's window layout.
926///
927/// A loadable module serves this, not the device firmware, and a model may
928/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
929/// sent and not that anything acted on it.
930///
931/// # Safety
932///
933/// `remote` is null or a live handle from `mxr_remote_new()`.
934#[no_mangle]
935pub unsafe extern "C" fn mxr_set_multiviewer_view_mode(
936 remote: *const mxr_remote_t,
937 device: mxr_uid_t,
938 mode: u8,
939) -> mxr_result_t {
940 // SAFETY: the caller guarantees a live handle or null.
941 let handle = unsafe { remote.as_ref() };
942 with(handle, |r| {
943 from_control(
944 r.remote
945 .set_multiviewer_view_mode(device.into(), MultiviewerViewMode::from_wire(mode)),
946 )
947 })
948}
949
950/// Puts a source in one of a multiviewer's windows.
951///
952/// A loadable module serves this, not the device firmware, and a model may
953/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
954/// sent and not that anything acted on it.
955///
956/// # Safety
957///
958/// `remote` is null or a live handle from `mxr_remote_new()`.
959#[no_mangle]
960pub unsafe extern "C" fn mxr_set_multiviewer_video_source(
961 remote: *const mxr_remote_t,
962 device: mxr_uid_t,
963 screen: u8,
964 source: u8,
965) -> mxr_result_t {
966 // SAFETY: the caller guarantees a live handle or null.
967 let handle = unsafe { remote.as_ref() };
968 with(handle, |r| {
969 from_control(r.remote.set_multiviewer_video_source(
970 device.into(),
971 screen,
972 MultiviewerSource::from_wire(source),
973 ))
974 })
975}
976
977/// Chooses which window a multiviewer takes its audio from.
978///
979/// A loadable module serves this, not the device firmware, and a model may
980/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
981/// sent and not that anything acted on it.
982///
983/// # Safety
984///
985/// `remote` is null or a live handle from `mxr_remote_new()`.
986#[no_mangle]
987pub unsafe extern "C" fn mxr_set_multiviewer_audio_source(
988 remote: *const mxr_remote_t,
989 device: mxr_uid_t,
990 source: u8,
991) -> mxr_result_t {
992 // SAFETY: the caller guarantees a live handle or null.
993 let handle = unsafe { remote.as_ref() };
994 with(handle, |r| {
995 from_control(
996 r.remote
997 .set_multiviewer_audio_source(device.into(), MultiviewerSource::from_wire(source)),
998 )
999 })
1000}
1001
1002/// Sets a multiviewer's output volume and mute state.
1003///
1004/// A loadable module serves this, not the device firmware, and a model may
1005/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1006/// sent and not that anything acted on it.
1007///
1008/// # Safety
1009///
1010/// `remote` is null or a live handle from `mxr_remote_new()`.
1011#[no_mangle]
1012pub unsafe extern "C" fn mxr_set_multiviewer_audio_volume(
1013 remote: *const mxr_remote_t,
1014 device: mxr_uid_t,
1015 volume: u8,
1016 muted: bool,
1017) -> mxr_result_t {
1018 // SAFETY: the caller guarantees a live handle or null.
1019 let handle = unsafe { remote.as_ref() };
1020 with(handle, |r| {
1021 from_control(
1022 r.remote
1023 .set_multiviewer_audio_volume(device.into(), volume, muted),
1024 )
1025 })
1026}
1027
1028/// Switches the EDID a multiviewer presents to its sources.
1029///
1030/// A loadable module serves this, not the device firmware, and a model may
1031/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1032/// sent and not that anything acted on it.
1033///
1034/// # Safety
1035///
1036/// `remote` is null or a live handle from `mxr_remote_new()`.
1037#[no_mangle]
1038pub unsafe extern "C" fn mxr_set_multiviewer_edid_template(
1039 remote: *const mxr_remote_t,
1040 device: mxr_uid_t,
1041 template: u8,
1042) -> mxr_result_t {
1043 // SAFETY: the caller guarantees a live handle or null.
1044 let handle = unsafe { remote.as_ref() };
1045 with(handle, |r| {
1046 from_control(r.remote.set_multiviewer_edid_template(
1047 device.into(),
1048 MultiviewerEdidTemplate::from_wire(template),
1049 ))
1050 })
1051}
1052
1053/// Chooses which window a multiviewer forwards remote control to.
1054///
1055/// A loadable module serves this, not the device firmware, and a model may
1056/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1057/// sent and not that anything acted on it.
1058///
1059/// # Safety
1060///
1061/// `remote` is null or a live handle from `mxr_remote_new()`.
1062#[no_mangle]
1063pub unsafe extern "C" fn mxr_set_multiviewer_remote_control(
1064 remote: *const mxr_remote_t,
1065 device: mxr_uid_t,
1066 source: u8,
1067) -> mxr_result_t {
1068 // SAFETY: the caller guarantees a live handle or null.
1069 let handle = unsafe { remote.as_ref() };
1070 with(handle, |r| {
1071 from_control(
1072 r.remote.set_multiviewer_remote_control(
1073 device.into(),
1074 MultiviewerSource::from_wire(source),
1075 ),
1076 )
1077 })
1078}
1079
1080/// Sets the size of a multiviewer's picture-in-picture window.
1081///
1082/// A loadable module serves this, not the device firmware, and a model may
1083/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1084/// sent and not that anything acted on it.
1085///
1086/// # Safety
1087///
1088/// `remote` is null or a live handle from `mxr_remote_new()`.
1089#[no_mangle]
1090pub unsafe extern "C" fn mxr_set_multiviewer_pip_size(
1091 remote: *const mxr_remote_t,
1092 device: mxr_uid_t,
1093 size: u8,
1094) -> mxr_result_t {
1095 // SAFETY: the caller guarantees a live handle or null.
1096 let handle = unsafe { remote.as_ref() };
1097 with(handle, |r| {
1098 from_control(
1099 r.remote
1100 .set_multiviewer_pip_size(device.into(), MultiviewerPipSize::from_wire(size)),
1101 )
1102 })
1103}
1104
1105/// Sets which corner a multiviewer's picture-in-picture window sits in.
1106///
1107/// A loadable module serves this, not the device firmware, and a model may
1108/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1109/// sent and not that anything acted on it.
1110///
1111/// # Safety
1112///
1113/// `remote` is null or a live handle from `mxr_remote_new()`.
1114#[no_mangle]
1115pub unsafe extern "C" fn mxr_set_multiviewer_pip_position(
1116 remote: *const mxr_remote_t,
1117 device: mxr_uid_t,
1118 position: u8,
1119) -> mxr_result_t {
1120 // SAFETY: the caller guarantees a live handle or null.
1121 let handle = unsafe { remote.as_ref() };
1122 with(handle, |r| {
1123 from_control(r.remote.set_multiviewer_pip_position(
1124 device.into(),
1125 MultiviewerPipPosition::from_wire(position),
1126 ))
1127 })
1128}
1129
1130/// Sets how a multiviewer fits a source into its window.
1131///
1132/// A loadable module serves this, not the device firmware, and a model may
1133/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1134/// sent and not that anything acted on it.
1135///
1136/// # Safety
1137///
1138/// `remote` is null or a live handle from `mxr_remote_new()`.
1139#[no_mangle]
1140pub unsafe extern "C" fn mxr_set_multiviewer_aspect_ratio(
1141 remote: *const mxr_remote_t,
1142 device: mxr_uid_t,
1143 aspect: u8,
1144) -> mxr_result_t {
1145 // SAFETY: the caller guarantees a live handle or null.
1146 let handle = unsafe { remote.as_ref() };
1147 with(handle, |r| {
1148 from_control(
1149 r.remote.set_multiviewer_aspect_ratio(
1150 device.into(),
1151 MultiviewerAspectRatio::from_wire(aspect),
1152 ),
1153 )
1154 })
1155}
1156
1157/// Turns a multiviewer's automatic source switching on or off.
1158///
1159/// A loadable module serves this, not the device firmware, and a model may
1160/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1161/// sent and not that anything acted on it.
1162///
1163/// # Safety
1164///
1165/// `remote` is null or a live handle from `mxr_remote_new()`.
1166#[no_mangle]
1167pub unsafe extern "C" fn mxr_set_multiviewer_auto_switch(
1168 remote: *const mxr_remote_t,
1169 device: mxr_uid_t,
1170 enable: bool,
1171) -> mxr_result_t {
1172 // SAFETY: the caller guarantees a live handle or null.
1173 let handle = unsafe { remote.as_ref() };
1174 with(handle, |r| {
1175 from_control(r.remote.set_multiviewer_auto_switch(device.into(), enable))
1176 })
1177}
1178
1179/// Switches a multiviewer's output resolution.
1180///
1181/// A loadable module serves this, not the device firmware, and a model may
1182/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1183/// sent and not that anything acted on it.
1184///
1185/// # Safety
1186///
1187/// `remote` is null or a live handle from `mxr_remote_new()`.
1188#[no_mangle]
1189pub unsafe extern "C" fn mxr_set_multiviewer_output_mode(
1190 remote: *const mxr_remote_t,
1191 device: mxr_uid_t,
1192 mode: u8,
1193) -> mxr_result_t {
1194 // SAFETY: the caller guarantees a live handle or null.
1195 let handle = unsafe { remote.as_ref() };
1196 with(handle, |r| {
1197 from_control(
1198 r.remote
1199 .set_multiviewer_output_mode(device.into(), MultiviewerOutputMode::from_wire(mode)),
1200 )
1201 })
1202}
1203
1204/// Sets a multiviewer's IT content flag.
1205///
1206/// A loadable module serves this, not the device firmware, and a model may
1207/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1208/// sent and not that anything acted on it.
1209///
1210/// # Safety
1211///
1212/// `remote` is null or a live handle from `mxr_remote_new()`.
1213#[no_mangle]
1214pub unsafe extern "C" fn mxr_set_multiviewer_output_itc(
1215 remote: *const mxr_remote_t,
1216 device: mxr_uid_t,
1217 mode: u8,
1218) -> mxr_result_t {
1219 // SAFETY: the caller guarantees a live handle or null.
1220 let handle = unsafe { remote.as_ref() };
1221 with(handle, |r| {
1222 from_control(
1223 r.remote
1224 .set_multiviewer_output_itc(device.into(), MultiviewerItcMode::from_wire(mode)),
1225 )
1226 })
1227}
1228
1229/// Switches a multiviewer's HDCP mode.
1230///
1231/// A loadable module serves this, not the device firmware, and a model may
1232/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1233/// sent and not that anything acted on it.
1234///
1235/// # Safety
1236///
1237/// `remote` is null or a live handle from `mxr_remote_new()`.
1238#[no_mangle]
1239pub unsafe extern "C" fn mxr_set_multiviewer_hdcp_mode(
1240 remote: *const mxr_remote_t,
1241 device: mxr_uid_t,
1242 mode: u8,
1243) -> mxr_result_t {
1244 // SAFETY: the caller guarantees a live handle or null.
1245 let handle = unsafe { remote.as_ref() };
1246 with(handle, |r| {
1247 from_control(
1248 r.remote
1249 .set_multiviewer_hdcp_mode(device.into(), MultiviewerHdcpMode::from_wire(mode)),
1250 )
1251 })
1252}
1253
1254/// Maps one of a multiviewer's inputs to a source device.
1255///
1256/// A loadable module serves this, not the device firmware, and a model may
1257/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1258/// sent and not that anything acted on it.
1259///
1260/// # Safety
1261///
1262/// `remote` is null or a live handle from `mxr_remote_new()`.
1263#[no_mangle]
1264pub unsafe extern "C" fn mxr_set_multiviewer_input_source(
1265 remote: *const mxr_remote_t,
1266 device: mxr_uid_t,
1267 input: u8,
1268 source: mxr_uid_t,
1269) -> mxr_result_t {
1270 // SAFETY: the caller guarantees a live handle or null.
1271 let handle = unsafe { remote.as_ref() };
1272 with(handle, |r| {
1273 from_control(
1274 r.remote
1275 .set_multiviewer_input_source(device.into(), input, source.into()),
1276 )
1277 })
1278}
1279
1280/// Asks a multiviewer to map its inputs to the sources it can see.
1281///
1282/// A loadable module serves this, not the device firmware, and a model may
1283/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1284/// sent and not that anything acted on it.
1285///
1286/// # Safety
1287///
1288/// `remote` is null or a live handle from `mxr_remote_new()`.
1289#[no_mangle]
1290pub unsafe extern "C" fn mxr_multiviewer_auto_route(
1291 remote: *const mxr_remote_t,
1292 device: mxr_uid_t,
1293) -> mxr_result_t {
1294 // SAFETY: the caller guarantees a live handle or null.
1295 let handle = unsafe { remote.as_ref() };
1296 with(handle, |r| {
1297 from_control(r.remote.multiviewer_auto_route(device.into()))
1298 })
1299}