Skip to main content

mx_remote_ffi/
events.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The event bridge: a C table of function pointers, and the handler that
5//! calls into it.
6//!
7//! Every member may be null, and a null member drops its event. Two of them
8//! are worth naming first, because a program that sets only those two is
9//! already complete: `on_device_update` fires after every device-level event
10//! and `on_bay_update` after every bay-level one, so a caller that redraws
11//! from `mxr_device()` and `mxr_bay()`
12//! needs nothing else.
13//!
14//! The rest divide by what they can tell a caller that a snapshot cannot. An
15//! event whose payload is state carries only the identifier, because the
16//! snapshot is where that value lives and a copy in the callback could only be
17//! staler. An event that is a request or a one-off - a key press, a reboot
18//! demand, an IR blast - carries a struct, because nothing stores it.
19//!
20//! Callbacks run on the receive thread with no lock held, one at a time.
21//! Calling back into the library from one is safe; blocking in one stalls
22//! every device. Pointers handed to a callback are borrowed for the length of
23//! that call, so anything needed afterwards must be copied.
24
25use std::ffi::{c_char, c_void, CString};
26
27use mx_remote::{
28    ActionTransmitRequest, ArcStatus, AudioChangeSource, AudioClip, BayNameChange, BayUid,
29    DeviceUid, EdidProfileChange, EdidRecord, EdidRequest, EventHandler, FactoryResetRequest,
30    IrCapture, IrMeta, IrTransmitRequest, KeyTransmitRequest, LinkFeature, MultiviewerCommand,
31    PowerStatus, RcAction, RcKey, RebootRequest, SetRouteRequest, V2ipBlacklistChange,
32    V2ipPowerSaveRequest, VideoWallCommand, VolumeMuteStatus,
33};
34
35use crate::abi::{bay_or_zero, guard, mxr_bay_uid_t, mxr_tribool_t, mxr_uid_t};
36use crate::info::{mxr_arc_status_t, mxr_power_status_t};
37
38// ---- callback signatures ----
39
40/// Names only the device the event concerns.
41pub type mxr_device_cb = Option<extern "C" fn(userdata: *mut c_void, device: mxr_uid_t)>;
42/// Names a device and a flag.
43pub type mxr_device_bool_cb =
44    Option<extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, value: bool)>;
45/// Names a device and a second device.
46pub type mxr_device_uid_cb =
47    Option<extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, other: mxr_uid_t)>;
48/// Names a device and a 16-bit value.
49pub type mxr_device_u16_cb =
50    Option<extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, value: u16)>;
51/// Names a device, one of its audio endpoints, and a flag.
52pub type mxr_endpoint_bool_cb =
53    Option<extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, endpoint: u16, value: bool)>;
54/// Names a device, one of its audio endpoints, and a 32-bit value.
55pub type mxr_endpoint_u32_cb =
56    Option<extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, endpoint: u16, value: u32)>;
57/// Names a device, a status code and a message.
58pub type mxr_system_status_cb = Option<
59    extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, status: u16, message: *const c_char),
60>;
61
62/// Names only the bay the event concerns.
63pub type mxr_bay_cb = Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t)>;
64/// Names a bay and a flag.
65pub type mxr_bay_bool_cb =
66    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, value: bool)>;
67/// Names a bay and a string, borrowed for the call.
68pub type mxr_bay_str_cb =
69    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, value: *const c_char)>;
70/// Names a bay and another bay, the zero device standing for none.
71pub type mxr_bay_bay_cb =
72    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, other: mxr_bay_uid_t)>;
73/// Names a bay and an 8-bit value.
74pub type mxr_bay_u8_cb =
75    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, value: u8)>;
76/// Names a bay and a 16-bit value.
77pub type mxr_bay_u16_cb =
78    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, value: u16)>;
79/// Names a bay, its combined volume percentage and its mute state.
80pub type mxr_volume_cb = Option<
81    extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, volume: u8, muted: mxr_tribool_t),
82>;
83/// Names a bay and the power state of what is connected to it.
84pub type mxr_power_cb =
85    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, power: mxr_power_status_t)>;
86/// Names a bay and its audio return channel.
87pub type mxr_arc_cb =
88    Option<extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, arc: mxr_arc_status_t)>;
89/// Names a bay and the link that was made to it.
90pub type mxr_bay_linked_cb = Option<
91    extern "C" fn(
92        userdata: *mut c_void,
93        bay: mxr_bay_uid_t,
94        linked_serial: *const c_char,
95        bay_name: *const c_char,
96        features: u32,
97    ),
98>;
99/// Names a bay and the link that was removed from it.
100pub type mxr_bay_unlinked_cb = Option<
101    extern "C" fn(
102        userdata: *mut c_void,
103        bay: mxr_bay_uid_t,
104        linked_serial: *const c_char,
105        bay_name: *const c_char,
106    ),
107>;
108
109// ---- notification payloads ----
110
111/// Asks a device, addressed by serial, to switch a sink.
112#[repr(C)]
113#[derive(Clone, Copy)]
114pub struct mxr_set_route_request_t {
115    /// Serial of the device to act on, borrowed for the call.
116    pub serial: *const c_char,
117    /// Output bay to switch.
118    pub sink_bay: u16,
119    /// Source bay to switch it to.
120    pub source_bay: u16,
121    /// Whether to skip the power-on commands that normally accompany a switch.
122    pub no_power_on: bool,
123    /// Set when the request routes audio only.
124    pub audio_only: bool,
125}
126
127/// Asks one device for its EDID.
128#[repr(C)]
129#[derive(Clone, Copy)]
130pub struct mxr_edid_request_t {
131    /// The device being asked.
132    pub target: mxr_uid_t,
133    /// Whether the sink's EDID is wanted rather than the source's.
134    pub output: bool,
135}
136
137/// One EDID block from a device's reply.
138#[repr(C)]
139#[derive(Clone, Copy)]
140pub struct mxr_edid_record_t {
141    /// True for a sink's EDID, false for a source's.
142    pub output: bool,
143    /// A base block plus one extension block, borrowed for the call.
144    pub data: *const u8,
145    /// Length of `data`, normally 256.
146    pub data_len: usize,
147}
148
149/// Asks a device to rename one of its bays.
150#[repr(C)]
151#[derive(Clone, Copy)]
152pub struct mxr_bay_name_change_t {
153    /// The device to act on.
154    pub target: mxr_uid_t,
155    /// The bay to rename.
156    pub port: u16,
157    /// The new name, borrowed for the call.
158    pub name: *const c_char,
159}
160
161/// Asks a device to switch its input EDID profile.
162#[repr(C)]
163#[derive(Clone, Copy)]
164pub struct mxr_edid_profile_change_t {
165    /// The device to act on.
166    pub target: mxr_uid_t,
167    /// The profile to switch to.
168    pub profile: u16,
169}
170
171/// Asks peers to factory-reset.
172#[repr(C)]
173#[derive(Clone, Copy)]
174pub struct mxr_factory_reset_request_t {
175    /// Set by the broadcast form, which targets every peer.
176    pub all: bool,
177    /// The one device addressed, zero when `all` is set.
178    pub target: mxr_uid_t,
179}
180
181/// Asks a sink to enter or leave power save.
182#[repr(C)]
183#[derive(Clone, Copy)]
184pub struct mxr_power_save_request_t {
185    /// The sink to act on, zero on the broadcast form.
186    pub target: mxr_uid_t,
187    /// Whether power save is being entered.
188    pub enabled: bool,
189}
190
191/// Asks one device to send a remote-control key on a bay.
192#[repr(C)]
193#[derive(Clone, Copy)]
194pub struct mxr_key_transmit_request_t {
195    /// The device to act on.
196    pub target: mxr_uid_t,
197    /// Bay in the target's own numbering, which is not a port number.
198    pub local_bay: u16,
199    /// The key to send.
200    pub key: u16,
201}
202
203/// Asks one device to perform a remote-control action.
204#[repr(C)]
205#[derive(Clone, Copy)]
206pub struct mxr_action_transmit_request_t {
207    /// The device to act on.
208    pub target: mxr_uid_t,
209    /// Bay in the target's own numbering, which is not a port number.
210    pub local_bay: u16,
211    /// The action to perform.
212    pub action: u16,
213}
214
215/// The metadata shared by the raw-IR capture and transmit frames.
216#[repr(C)]
217#[derive(Clone, Copy)]
218pub struct mxr_ir_meta_t {
219    /// Tick length of the timing values.
220    pub timer_resolution: u16,
221    /// Carrier frequency in Hz.
222    pub frequency: u16,
223    /// Number of timing values that follow.
224    pub nb_timings: u16,
225    /// Index at which the repeat section starts.
226    pub repeat_offset: u16,
227    /// Capture status.
228    pub status: u8,
229}
230
231impl From<IrMeta> for mxr_ir_meta_t {
232    fn from(m: IrMeta) -> Self {
233        Self {
234            timer_resolution: m.timer_resolution,
235            frequency: m.frequency,
236            nb_timings: m.nb_timings,
237            repeat_offset: m.repeat_offset,
238            status: m.status,
239        }
240    }
241}
242
243/// Raw IR captured on a bay of the sending device.
244#[repr(C)]
245#[derive(Clone, Copy)]
246pub struct mxr_ir_capture_t {
247    /// Sender clock at capture time.
248    pub timestamp: u32,
249    /// Sender clock at the last signal change.
250    pub last_change: u32,
251    /// Metadata for the timings.
252    pub meta: mxr_ir_meta_t,
253    /// The raw on/off timing blob, borrowed for the call.
254    pub timings: *const u8,
255    /// Length of `timings`.
256    pub timings_len: usize,
257}
258
259/// Asks one device to blast raw IR on one of its local bays.
260#[repr(C)]
261#[derive(Clone, Copy)]
262pub struct mxr_ir_transmit_request_t {
263    /// The device to act on.
264    pub target: mxr_uid_t,
265    /// Bay mode in the target's own numbering, which is not a port number.
266    pub local_mode: u8,
267    /// Bay number in the target's own numbering, which is not a port number.
268    pub local_bay: u8,
269    /// Sender clock at send time.
270    pub timestamp: u32,
271    /// Metadata for the timings.
272    pub meta: mxr_ir_meta_t,
273    /// The raw on/off timing blob, borrowed for the call.
274    pub timings: *const u8,
275    /// Length of `timings`.
276    pub timings_len: usize,
277}
278
279/// Adds multicast groups to, or removes them from, the mesh-wide list that
280/// automatic address assignment avoids.
281#[repr(C)]
282#[derive(Clone, Copy)]
283pub struct mxr_blacklist_change_t {
284    /// The groups, four bytes each in network order, borrowed for the call.
285    pub groups: *const u8,
286    /// How many groups `groups` holds.
287    pub groups_len: usize,
288    /// Whether they are being added rather than removed.
289    pub registered: bool,
290}
291
292/// Asks one sink to crop its source to a wall window.
293///
294/// The window replaces the sink's outright: a zero width or height is the wire
295/// spelling of "clear the wall and show the full frame", not of "unset". A
296/// revert carries no window, and the geometry in it means nothing.
297#[repr(C)]
298#[derive(Clone, Copy)]
299pub struct mxr_video_wall_command_t {
300    /// The sink to act on.
301    pub target: mxr_uid_t,
302    /// Window origin, horizontal.
303    pub pos_x: u16,
304    /// Window origin, vertical.
305    pub pos_y: u16,
306    /// Window width.
307    pub width: u16,
308    /// Window height.
309    pub height: u16,
310    /// Active picture width the window was authored against.
311    pub raster_w: u16,
312    /// Active picture height the window was authored against.
313    pub raster_h: u16,
314    /// 0 preview, 1 store, 2 revert.
315    pub op: u8,
316}
317
318/// A command addressed to a multiviewer.
319///
320/// The parameters are raw: the opcode belongs to the multiviewer module rather
321/// than to MatrixOS, so there is no firmware source here to pin per-sub-command
322/// field semantics against.
323#[repr(C)]
324#[derive(Clone, Copy)]
325pub struct mxr_multiviewer_command_t {
326    /// The multiviewer being addressed.
327    pub target: mxr_uid_t,
328    /// The sub-opcode. A value this library has no name for still arrives.
329    pub op: u8,
330    /// Everything after the envelope, borrowed for the call.
331    pub params: *const u8,
332    /// Length of `params`.
333    pub params_len: usize,
334}
335
336/// Which source endpoint an audio sink endpoint was switched to.
337#[repr(C)]
338#[derive(Clone, Copy)]
339pub struct mxr_audio_change_source_t {
340    /// The device whose endpoint is being listened to.
341    pub source_uid: mxr_uid_t,
342    /// The endpoint being listened to.
343    pub source_id: u16,
344    /// The device doing the listening.
345    pub target_uid: mxr_uid_t,
346    /// The endpoint doing the listening.
347    pub target_id: u16,
348}
349
350/// Carries a request addressed to a device.
351pub type mxr_set_route_cb = Option<
352    extern "C" fn(
353        userdata: *mut c_void,
354        device: mxr_uid_t,
355        request: *const mxr_set_route_request_t,
356    ),
357>;
358/// Carries a request for a device's EDID.
359pub type mxr_edid_request_cb = Option<
360    extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, request: *const mxr_edid_request_t),
361>;
362/// Carries one EDID block a device replied with.
363pub type mxr_edid_record_cb = Option<
364    extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, record: *const mxr_edid_record_t),
365>;
366/// Carries a request to rename a bay.
367pub type mxr_bay_name_change_cb = Option<
368    extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, change: *const mxr_bay_name_change_t),
369>;
370/// Carries a request to switch an EDID profile.
371pub type mxr_edid_profile_change_cb = Option<
372    extern "C" fn(
373        userdata: *mut c_void,
374        device: mxr_uid_t,
375        change: *const mxr_edid_profile_change_t,
376    ),
377>;
378/// Carries a factory-reset request.
379pub type mxr_factory_reset_cb = Option<
380    extern "C" fn(
381        userdata: *mut c_void,
382        device: mxr_uid_t,
383        request: *const mxr_factory_reset_request_t,
384    ),
385>;
386/// Carries a power-save request.
387pub type mxr_power_save_cb = Option<
388    extern "C" fn(
389        userdata: *mut c_void,
390        device: mxr_uid_t,
391        request: *const mxr_power_save_request_t,
392    ),
393>;
394/// Carries a request to send a remote-control key.
395pub type mxr_key_transmit_cb = Option<
396    extern "C" fn(
397        userdata: *mut c_void,
398        device: mxr_uid_t,
399        request: *const mxr_key_transmit_request_t,
400    ),
401>;
402/// Carries a request to perform a remote-control action.
403pub type mxr_action_transmit_cb = Option<
404    extern "C" fn(
405        userdata: *mut c_void,
406        device: mxr_uid_t,
407        request: *const mxr_action_transmit_request_t,
408    ),
409>;
410/// Carries a request to blast raw infrared.
411pub type mxr_ir_transmit_cb = Option<
412    extern "C" fn(
413        userdata: *mut c_void,
414        device: mxr_uid_t,
415        request: *const mxr_ir_transmit_request_t,
416    ),
417>;
418/// Carries a blacklist change.
419pub type mxr_blacklist_cb = Option<
420    extern "C" fn(userdata: *mut c_void, device: mxr_uid_t, change: *const mxr_blacklist_change_t),
421>;
422/// Carries a video wall command.
423pub type mxr_video_wall_cb = Option<
424    extern "C" fn(
425        userdata: *mut c_void,
426        device: mxr_uid_t,
427        command: *const mxr_video_wall_command_t,
428    ),
429>;
430/// Carries a multiviewer command.
431pub type mxr_multiviewer_command_cb = Option<
432    extern "C" fn(
433        userdata: *mut c_void,
434        device: mxr_uid_t,
435        command: *const mxr_multiviewer_command_t,
436    ),
437>;
438/// Carries an audio input selection.
439pub type mxr_audio_select_cb = Option<
440    extern "C" fn(
441        userdata: *mut c_void,
442        device: mxr_uid_t,
443        change: *const mxr_audio_change_source_t,
444    ),
445>;
446/// Carries raw infrared captured on a bay.
447pub type mxr_ir_capture_cb = Option<
448    extern "C" fn(userdata: *mut c_void, bay: mxr_bay_uid_t, capture: *const mxr_ir_capture_t),
449>;
450
451/// What to call when something happens.
452///
453/// Zero the whole struct and fill in only what is wanted: a null member drops
454/// its event. `userdata` is whatever was passed to
455/// `mxr_remote_new()` and is never examined here.
456#[repr(C)]
457#[derive(Clone, Copy)]
458pub struct mxr_callbacks_t {
459    /// Fires after every device-level event below.
460    pub on_device_update: mxr_device_cb,
461    /// Fires after every bay-level event below.
462    pub on_bay_update: mxr_bay_cb,
463
464    /// The device's configuration changed.
465    pub on_device_config_changed: mxr_device_cb,
466    /// The device has reported every part of its configuration.
467    pub on_device_config_complete: mxr_device_cb,
468    /// The device started or stopped answering.
469    pub on_device_online_changed: mxr_device_bool_cb,
470    /// The device reported new temperatures; read them with
471    /// `mxr_device_temperatures()`.
472    pub on_device_temperature_changed: mxr_device_cb,
473    /// A firmware component reported its version; read it with
474    /// `mxr_device_firmware()`.
475    pub on_firmware_version_changed: mxr_device_cb,
476    /// The device reported a status about itself.
477    pub on_system_status_changed: mxr_system_status_cb,
478    /// A network port reported its link state; read it with
479    /// `mxr_network_status()`.
480    pub on_network_status_changed: mxr_device_cb,
481    /// The device reported V2IP statistics; read them with
482    /// `mxr_v2ip_stats()`.
483    pub on_v2ip_stats_changed: mxr_device_cb,
484    /// The streams the device's source bays advertise changed; read them with
485    /// `mxr_v2ip_sources()`.
486    pub on_v2ip_sources_changed: mxr_device_cb,
487    /// The device's V2IP encoder configuration changed; read it with
488    /// `mxr_v2ip_details()`.
489    pub on_v2ip_details_changed: mxr_device_cb,
490    /// The streams the device's sink is subscribed to changed; read them with
491    /// `mxr_v2ip_sink()`.
492    ///
493    /// A route request addressed to the device fires this as soon as it is
494    /// seen, so this reports what the mesh now believes rather than what the
495    /// device confirmed - it acknowledges nothing, and only its own
496    /// configuration report, sent on its own schedule, settles a route.
497    pub on_v2ip_sink_changed: mxr_device_cb,
498    /// A multiviewer reported its state; read it with
499    /// `mxr_multiviewer_status()`.
500    pub on_multiviewer_status_changed: mxr_device_cb,
501    /// The device reported its audio endpoint tree; read it with
502    /// `mxr_audio_endpoints()`.
503    pub on_audio_endpoints_changed: mxr_device_cb,
504    /// The device reported its mesh master.
505    pub on_mesh_master_changed: mxr_device_uid_cb,
506    /// The device reported its view of the mesh topology; read it with
507    /// `mxr_topology()`.
508    pub on_topology_changed: mxr_device_cb,
509    /// A ProAmp8 reported its Dolby settings; read them with
510    /// `mxr_dolby_settings()`.
511    pub on_amp_dolby_settings_changed: mxr_device_cb,
512    /// Installer setup was completed or cleared.
513    pub on_setup_status_changed: mxr_device_bool_cb,
514    /// The installer identifier changed.
515    pub on_installer_id_changed: mxr_device_u16_cb,
516    /// The sink was told to show a window; read it with
517    /// `mxr_v2ip_tiling()`.
518    pub on_tiling_changed: mxr_device_cb,
519    /// The device reported what its video processor supports; read it with
520    /// `mxr_v2ip_features()`.
521    pub on_v2ip_features_changed: mxr_device_cb,
522    /// A source bay's remote-control configuration changed; read it with
523    /// `mxr_rc_settings()`.
524    pub on_rc_settings_changed: mxr_device_cb,
525    /// A V2IP device was linked to a remote peer.
526    pub on_v2ip_link_changed: mxr_device_uid_cb,
527    /// A multiviewer command arrived.
528    pub on_multiviewer_command: mxr_multiviewer_command_cb,
529    /// Management asked the device to route a source endpoint to one of its
530    /// sink endpoints.
531    pub on_audio_select_input: mxr_audio_select_cb,
532    /// Management asked the device to mute or unmute one of its endpoints.
533    pub on_audio_endpoint_mute: mxr_endpoint_bool_cb,
534    /// Management asked the device to set one of its endpoints' trigger.
535    pub on_audio_endpoint_trigger: mxr_endpoint_bool_cb,
536    /// Management asked the device to set one of its endpoints' volume, which
537    /// no unit acts on.
538    pub on_audio_endpoint_volume: mxr_endpoint_u32_cb,
539    /// A peer asked this client, or every device, to announce itself.
540    pub on_discover_request: mxr_device_cb,
541    /// A peer asked a device to switch a sink.
542    pub on_set_route_requested: mxr_set_route_cb,
543    /// A peer asked a device for its EDID.
544    pub on_edid_requested: mxr_edid_request_cb,
545    /// A device answered with its EDID.
546    pub on_edid_received: mxr_edid_record_cb,
547    /// A peer asked a device to rename a bay.
548    pub on_bay_name_change_requested: mxr_bay_name_change_cb,
549    /// A peer asked a device to switch its EDID profile.
550    pub on_edid_profile_change_requested: mxr_edid_profile_change_cb,
551    /// A peer asked a device to reboot. The second identifier is the device
552    /// being asked, which is not always the sender, or zero when every unit
553    /// is asked.
554    pub on_reboot_requested: mxr_device_uid_cb,
555    /// A peer asked devices to factory-reset.
556    pub on_factory_reset_requested: mxr_factory_reset_cb,
557    /// A device told every unit to check for new firmware now.
558    pub on_monitoring_pulse: mxr_device_cb,
559    /// A peer asked a device to upgrade its FPGA.
560    pub on_upgrade_fpga_requested: mxr_device_cb,
561    /// A peer asked a device to re-detect its bays.
562    pub on_detect_bays_requested: mxr_device_cb,
563    /// A peer asked a sink to enter or leave power save.
564    pub on_power_save_requested: mxr_power_save_cb,
565    /// A peer asked a device to send a remote-control key.
566    pub on_key_transmit_requested: mxr_key_transmit_cb,
567    /// A peer asked a device to perform a remote-control action.
568    pub on_action_transmit_requested: mxr_action_transmit_cb,
569    /// A peer asked a device to blast raw infrared.
570    pub on_ir_transmit_requested: mxr_ir_transmit_cb,
571    /// Multicast groups were added to or removed from the list automatic
572    /// address assignment avoids.
573    pub on_blacklist_changed: mxr_blacklist_cb,
574    /// A video wall command arrived.
575    pub on_video_wall_command: mxr_video_wall_cb,
576
577    /// A bay was seen for the first time.
578    pub on_bay_registered: mxr_bay_cb,
579    /// The bay's routed video source changed, zero when it was unrouted.
580    pub on_video_source_changed: mxr_bay_bay_cb,
581    /// The bay's routed audio source changed, zero when it was unrouted.
582    pub on_audio_source_changed: mxr_bay_bay_cb,
583    /// The bay's volume or mute state changed.
584    pub on_volume_changed: mxr_volume_cb,
585    /// The attached device's power state changed.
586    pub on_power_changed: mxr_power_cb,
587    /// The bay was renamed.
588    pub on_name_changed: mxr_bay_str_cb,
589    /// A signal appeared or disappeared.
590    pub on_signal_detected_changed: mxr_bay_bool_cb,
591    /// The bay started or stopped reporting a fault.
592    pub on_faulty_changed: mxr_bay_bool_cb,
593    /// The bay was hidden or shown.
594    pub on_hidden_changed: mxr_bay_bool_cb,
595    /// The amplifier zone the bay drives was powered on or off.
596    pub on_zone_powered_changed: mxr_bay_bool_cb,
597    /// The HDBaseT link came up or went down.
598    pub on_hdbt_connected_changed: mxr_bay_bool_cb,
599    /// The signal format description changed.
600    pub on_signal_type_changed: mxr_bay_str_cb,
601    /// Hot-plug detect was asserted or released.
602    pub on_hpd_detected_changed: mxr_bay_bool_cb,
603    /// A CEC device answered or stopped answering.
604    pub on_cec_detected_changed: mxr_bay_bool_cb,
605    /// The audio return channel changed.
606    pub on_arc_changed: mxr_arc_cb,
607    /// The input's EDID profile changed.
608    pub on_edid_profile_changed: mxr_bay_u16_cb,
609    /// The input's remote-control type changed.
610    pub on_rc_type_changed: mxr_bay_u8_cb,
611    /// A remote-control key was pressed on the bay.
612    pub on_key_pressed: mxr_bay_u16_cb,
613    /// A remote-control action was received on the bay.
614    pub on_action_received: mxr_bay_u16_cb,
615    /// The bay started or stopped mirroring another output, zero when it
616    /// stopped.
617    pub on_mirror_status_changed: mxr_bay_bay_cb,
618    /// A ProAmp8 zone's settings changed; read them with
619    /// `mxr_bay_amp_settings()`.
620    pub on_amp_zone_settings_changed: mxr_bay_cb,
621    /// A volume step was requested on the bay.
622    pub on_volume_step: mxr_bay_bool_cb,
623    /// The bay detected audio clipping, at the reported level.
624    pub on_audio_clip: mxr_bay_u8_cb,
625    /// Raw infrared was captured on the bay.
626    pub on_ir_captured: mxr_ir_capture_cb,
627    /// The devices filtered out of this sink's picker changed; read them with
628    /// `mxr_bay_filtered()`.
629    pub on_filtered_devices_changed: mxr_bay_cb,
630    /// The audio endpoint the bay carries changed.
631    pub on_audio_endpoint_changed: mxr_bay_u8_cb,
632    /// The bay's V2IP encoder was enabled or disabled.
633    pub on_encoder_disabled_changed: mxr_bay_bool_cb,
634    /// The bay's V2IP decoder was enabled or disabled.
635    pub on_decoder_disabled_changed: mxr_bay_bool_cb,
636
637    /// The bay was linked to a bay on another device. Both ends are told, so
638    /// both fire: `bay_name` names the bay whose link record changed, which is
639    /// this bay on the device that reported the change and the far bay on its
640    /// peer.
641    pub on_bay_linked: mxr_bay_linked_cb,
642    /// The bay's link to another device was removed. The arguments describe
643    /// the link that went, and mean what they do on `on_bay_linked`.
644    pub on_bay_unlinked: mxr_bay_unlinked_cb,
645}
646
647/// The caller's cookie, which this library carries and never reads.
648///
649/// # Safety
650///
651/// The contract on `mxr_remote_new()` is what makes
652/// these impls sound: the caller keeps the pointer valid, and safe to use from
653/// the library's threads, until the client is freed. Nothing here dereferences
654/// it.
655struct UserData(*mut c_void);
656
657// SAFETY: an opaque pointer this library only ever copies. See the type's own
658// docs for the contract the caller holds up.
659unsafe impl Send for UserData {}
660// SAFETY: as above.
661unsafe impl Sync for UserData {}
662
663/// Turns library events into calls through a C table.
664pub(crate) struct Bridge {
665    cb: mxr_callbacks_t,
666    userdata: UserData,
667}
668
669impl Bridge {
670    /// Copies the caller's table, so a caller may free or reuse theirs.
671    pub(crate) fn new(table: &mxr_callbacks_t, userdata: *mut c_void) -> Self {
672        Self {
673            cb: *table,
674            userdata: UserData(userdata),
675        }
676    }
677
678    fn ud(&self) -> *mut c_void {
679        self.userdata.0
680    }
681}
682
683/// Calls one member of the table, if the caller set it.
684///
685/// The guard is here rather than at the caller because this is an exit point,
686/// not an entry one: a panic while marshalling would otherwise unwind through
687/// the receive thread and take the client down with it.
688macro_rules! forward {
689    ($self:ident.$field:ident( $($arg:expr),* $(,)? )) => {
690        if let Some(f) = $self.cb.$field {
691            guard((), || f($self.ud() $(, $arg)*));
692        }
693    };
694}
695
696/// Calls `body` with `text` as a C string that lives for the call.
697fn with_cstr<R>(text: &str, body: impl FnOnce(*const c_char) -> R) -> R {
698    // A NUL inside would cut the string short in C. Protocol strings are read
699    // up to their first NUL, so this is a fallback, not a case that happens.
700    let owned = CString::new(text).unwrap_or_else(|_| c"".to_owned());
701    body(owned.as_ptr())
702}
703
704/// The identifier a request names, where the protocol's zero stands for
705/// "unaddressed": a broadcast, or a request that addresses only its sender.
706fn uid_or_zero(uid: Option<DeviceUid>) -> mxr_uid_t {
707    uid.unwrap_or(DeviceUid::ZERO).into()
708}
709
710impl EventHandler for Bridge {
711    fn on_device_update(&self, device: DeviceUid) {
712        forward!(self.on_device_update(device.into()));
713    }
714
715    fn on_bay_update(&self, bay: BayUid) {
716        forward!(self.on_bay_update(bay.into()));
717    }
718
719    // ---- device ----
720
721    fn on_device_config_changed(&self, device: DeviceUid) {
722        forward!(self.on_device_config_changed(device.into()));
723    }
724
725    fn on_device_config_complete(&self, device: DeviceUid) {
726        forward!(self.on_device_config_complete(device.into()));
727    }
728
729    fn on_device_online_changed(&self, device: DeviceUid, online: bool) {
730        forward!(self.on_device_online_changed(device.into(), online));
731    }
732
733    fn on_device_temperature_changed(&self, device: DeviceUid, _temperatures: Vec<i8>) {
734        forward!(self.on_device_temperature_changed(device.into()));
735    }
736
737    fn on_firmware_version_changed(&self, device: DeviceUid, _version: mx_remote::FirmwareVersion) {
738        forward!(self.on_firmware_version_changed(device.into()));
739    }
740
741    fn on_system_status_changed(&self, device: DeviceUid, status: u16, message: String) {
742        with_cstr(&message, |m| {
743            forward!(self.on_system_status_changed(device.into(), status, m));
744        });
745    }
746
747    fn on_network_status_changed(&self, device: DeviceUid, _status: mx_remote::NetworkPortStatus) {
748        forward!(self.on_network_status_changed(device.into()));
749    }
750
751    fn on_v2ip_stats_changed(&self, device: DeviceUid, _stats: mx_remote::V2ipDeviceStats) {
752        forward!(self.on_v2ip_stats_changed(device.into()));
753    }
754
755    fn on_v2ip_sources_changed(
756        &self,
757        device: DeviceUid,
758        _sources: Vec<mx_remote::V2ipStreamSources>,
759    ) {
760        forward!(self.on_v2ip_sources_changed(device.into()));
761    }
762
763    fn on_v2ip_details_changed(&self, device: DeviceUid, _details: mx_remote::DeviceV2ipDetails) {
764        forward!(self.on_v2ip_details_changed(device.into()));
765    }
766
767    fn on_v2ip_sink_changed(&self, device: DeviceUid, _sink: mx_remote::DeviceV2ipSink) {
768        forward!(self.on_v2ip_sink_changed(device.into()));
769    }
770
771    fn on_multiviewer_status_changed(
772        &self,
773        device: DeviceUid,
774        _status: mx_remote::MultiviewerStatus,
775    ) {
776        forward!(self.on_multiviewer_status_changed(device.into()));
777    }
778
779    fn on_audio_endpoints_changed(&self, device: DeviceUid, _endpoints: mx_remote::AudioEndpoints) {
780        forward!(self.on_audio_endpoints_changed(device.into()));
781    }
782
783    fn on_mesh_master_changed(&self, device: DeviceUid, master: DeviceUid) {
784        forward!(self.on_mesh_master_changed(device.into(), master.into()));
785    }
786
787    fn on_topology_changed(&self, device: DeviceUid, _topology: Vec<mx_remote::TopologyEntry>) {
788        forward!(self.on_topology_changed(device.into()));
789    }
790
791    fn on_amp_dolby_settings_changed(
792        &self,
793        device: DeviceUid,
794        _settings: mx_remote::AmpDolbySettings,
795    ) {
796        forward!(self.on_amp_dolby_settings_changed(device.into()));
797    }
798
799    fn on_setup_status_changed(&self, device: DeviceUid, completed: bool) {
800        forward!(self.on_setup_status_changed(device.into(), completed));
801    }
802
803    fn on_installer_id_changed(&self, device: DeviceUid, installer_id: u16) {
804        forward!(self.on_installer_id_changed(device.into(), installer_id));
805    }
806
807    fn on_tiling_changed(&self, device: DeviceUid, _tiling: mx_remote::V2ipTilingConfig) {
808        forward!(self.on_tiling_changed(device.into()));
809    }
810
811    fn on_v2ip_features_changed(&self, device: DeviceUid, _features: mx_remote::V2ipFpgaFeature) {
812        forward!(self.on_v2ip_features_changed(device.into()));
813    }
814
815    fn on_rc_settings_changed(&self, device: DeviceUid, _settings: mx_remote::RcSettings) {
816        forward!(self.on_rc_settings_changed(device.into()));
817    }
818
819    fn on_v2ip_link_changed(&self, device: DeviceUid, target: DeviceUid) {
820        forward!(self.on_v2ip_link_changed(device.into(), target.into()));
821    }
822
823    fn on_multiviewer_command(&self, device: DeviceUid, command: MultiviewerCommand) {
824        let payload = mxr_multiviewer_command_t {
825            target: command.target.into(),
826            op: command.op,
827            params: command.params.as_ptr(),
828            params_len: command.params.len(),
829        };
830        forward!(self.on_multiviewer_command(device.into(), &payload));
831    }
832
833    fn on_audio_select_input(&self, device: DeviceUid, change: AudioChangeSource) {
834        let payload = mxr_audio_change_source_t {
835            source_uid: change.source_uid.into(),
836            source_id: change.source_id,
837            target_uid: change.target_uid.into(),
838            target_id: change.target_id,
839        };
840        forward!(self.on_audio_select_input(device.into(), &payload));
841    }
842
843    fn on_audio_endpoint_mute(&self, device: DeviceUid, endpoint: u16, muted: bool) {
844        forward!(self.on_audio_endpoint_mute(device.into(), endpoint, muted));
845    }
846
847    fn on_audio_endpoint_trigger(&self, device: DeviceUid, endpoint: u16, active: bool) {
848        forward!(self.on_audio_endpoint_trigger(device.into(), endpoint, active));
849    }
850
851    fn on_audio_endpoint_volume(&self, device: DeviceUid, endpoint: u16, volume: u32) {
852        forward!(self.on_audio_endpoint_volume(device.into(), endpoint, volume));
853    }
854
855    fn on_discover_request(&self, device: DeviceUid) {
856        forward!(self.on_discover_request(device.into()));
857    }
858
859    fn on_set_route_requested(&self, device: DeviceUid, request: SetRouteRequest) {
860        with_cstr(&request.serial, |serial| {
861            let payload = mxr_set_route_request_t {
862                serial,
863                sink_bay: request.sink_bay,
864                source_bay: request.source_bay,
865                no_power_on: request.no_power_on,
866                audio_only: request.audio_only,
867            };
868            forward!(self.on_set_route_requested(device.into(), &payload));
869        });
870    }
871
872    fn on_edid_requested(&self, device: DeviceUid, request: EdidRequest) {
873        let payload = mxr_edid_request_t {
874            target: request.target.into(),
875            output: request.output,
876        };
877        forward!(self.on_edid_requested(device.into(), &payload));
878    }
879
880    fn on_edid_received(&self, device: DeviceUid, edid: EdidRecord) {
881        let payload = mxr_edid_record_t {
882            output: edid.output,
883            data: edid.data.as_ptr(),
884            data_len: edid.data.len(),
885        };
886        forward!(self.on_edid_received(device.into(), &payload));
887    }
888
889    fn on_bay_name_change_requested(&self, device: DeviceUid, change: BayNameChange) {
890        with_cstr(&change.name, |name| {
891            let payload = mxr_bay_name_change_t {
892                target: change.target.into(),
893                port: change.port,
894                name,
895            };
896            forward!(self.on_bay_name_change_requested(device.into(), &payload));
897        });
898    }
899
900    fn on_edid_profile_change_requested(&self, device: DeviceUid, change: EdidProfileChange) {
901        let payload = mxr_edid_profile_change_t {
902            target: change.target.into(),
903            profile: change.profile.to_wire(),
904        };
905        forward!(self.on_edid_profile_change_requested(device.into(), &payload));
906    }
907
908    fn on_reboot_requested(&self, device: DeviceUid, request: RebootRequest) {
909        forward!(self.on_reboot_requested(device.into(), uid_or_zero(request.target)));
910    }
911
912    fn on_factory_reset_requested(&self, device: DeviceUid, request: FactoryResetRequest) {
913        let payload = mxr_factory_reset_request_t {
914            all: request.all,
915            target: uid_or_zero(request.target),
916        };
917        forward!(self.on_factory_reset_requested(device.into(), &payload));
918    }
919
920    fn on_monitoring_pulse(&self, device: DeviceUid) {
921        forward!(self.on_monitoring_pulse(device.into()));
922    }
923
924    fn on_upgrade_fpga_requested(&self, device: DeviceUid) {
925        forward!(self.on_upgrade_fpga_requested(device.into()));
926    }
927
928    fn on_detect_bays_requested(&self, device: DeviceUid) {
929        forward!(self.on_detect_bays_requested(device.into()));
930    }
931
932    fn on_power_save_requested(&self, device: DeviceUid, request: V2ipPowerSaveRequest) {
933        let payload = mxr_power_save_request_t {
934            target: uid_or_zero(request.target),
935            enabled: request.enabled,
936        };
937        forward!(self.on_power_save_requested(device.into(), &payload));
938    }
939
940    fn on_key_transmit_requested(&self, device: DeviceUid, request: KeyTransmitRequest) {
941        let payload = mxr_key_transmit_request_t {
942            target: request.target.into(),
943            local_bay: request.local_bay,
944            key: request.key.to_wire(),
945        };
946        forward!(self.on_key_transmit_requested(device.into(), &payload));
947    }
948
949    fn on_action_transmit_requested(&self, device: DeviceUid, request: ActionTransmitRequest) {
950        let payload = mxr_action_transmit_request_t {
951            target: request.target.into(),
952            local_bay: request.local_bay,
953            action: request.action.to_wire(),
954        };
955        forward!(self.on_action_transmit_requested(device.into(), &payload));
956    }
957
958    fn on_ir_transmit_requested(&self, device: DeviceUid, request: IrTransmitRequest) {
959        let payload = mxr_ir_transmit_request_t {
960            target: request.target.into(),
961            local_mode: request.local_mode,
962            local_bay: request.local_bay,
963            timestamp: request.timestamp,
964            meta: request.meta.into(),
965            timings: request.timings.as_ptr(),
966            timings_len: request.timings.len(),
967        };
968        forward!(self.on_ir_transmit_requested(device.into(), &payload));
969    }
970
971    fn on_blacklist_changed(&self, device: DeviceUid, change: V2ipBlacklistChange) {
972        let groups: Vec<u8> = change.groups.iter().flat_map(|g| g.octets()).collect();
973        let payload = mxr_blacklist_change_t {
974            groups: groups.as_ptr(),
975            groups_len: change.groups.len(),
976            registered: change.registered,
977        };
978        forward!(self.on_blacklist_changed(device.into(), &payload));
979    }
980
981    fn on_video_wall_command(&self, device: DeviceUid, command: VideoWallCommand) {
982        let payload = mxr_video_wall_command_t {
983            target: command.target.into(),
984            pos_x: command.pos_x,
985            pos_y: command.pos_y,
986            width: command.width,
987            height: command.height,
988            raster_w: command.raster_w,
989            raster_h: command.raster_h,
990            op: command.op.to_wire(),
991        };
992        forward!(self.on_video_wall_command(device.into(), &payload));
993    }
994
995    // ---- bay ----
996
997    fn on_bay_registered(&self, bay: BayUid) {
998        forward!(self.on_bay_registered(bay.into()));
999    }
1000
1001    fn on_video_source_changed(&self, bay: BayUid, source: Option<BayUid>) {
1002        forward!(self.on_video_source_changed(bay.into(), bay_or_zero(source)));
1003    }
1004
1005    fn on_audio_source_changed(&self, bay: BayUid, source: Option<BayUid>) {
1006        forward!(self.on_audio_source_changed(bay.into(), bay_or_zero(source)));
1007    }
1008
1009    fn on_volume_changed(&self, bay: BayUid, volume: VolumeMuteStatus) {
1010        forward!(self.on_volume_changed(bay.into(), volume.volume(), volume.muted().into()));
1011    }
1012
1013    fn on_power_changed(&self, bay: BayUid, power: PowerStatus) {
1014        forward!(self.on_power_changed(bay.into(), Some(power).into()));
1015    }
1016
1017    fn on_name_changed(&self, bay: BayUid, name: String) {
1018        with_cstr(&name, |n| forward!(self.on_name_changed(bay.into(), n)));
1019    }
1020
1021    fn on_signal_detected_changed(&self, bay: BayUid, detected: bool) {
1022        forward!(self.on_signal_detected_changed(bay.into(), detected));
1023    }
1024
1025    fn on_faulty_changed(&self, bay: BayUid, faulty: bool) {
1026        forward!(self.on_faulty_changed(bay.into(), faulty));
1027    }
1028
1029    fn on_hidden_changed(&self, bay: BayUid, hidden: bool) {
1030        forward!(self.on_hidden_changed(bay.into(), hidden));
1031    }
1032
1033    fn on_zone_powered_changed(&self, bay: BayUid, powered: bool) {
1034        forward!(self.on_zone_powered_changed(bay.into(), powered));
1035    }
1036
1037    fn on_hdbt_connected_changed(&self, bay: BayUid, connected: bool) {
1038        forward!(self.on_hdbt_connected_changed(bay.into(), connected));
1039    }
1040
1041    fn on_signal_type_changed(&self, bay: BayUid, signal_type: String) {
1042        with_cstr(&signal_type, |s| {
1043            forward!(self.on_signal_type_changed(bay.into(), s))
1044        });
1045    }
1046
1047    fn on_hpd_detected_changed(&self, bay: BayUid, detected: bool) {
1048        forward!(self.on_hpd_detected_changed(bay.into(), detected));
1049    }
1050
1051    fn on_cec_detected_changed(&self, bay: BayUid, detected: bool) {
1052        forward!(self.on_cec_detected_changed(bay.into(), detected));
1053    }
1054
1055    fn on_arc_changed(&self, bay: BayUid, arc: ArcStatus) {
1056        forward!(self.on_arc_changed(bay.into(), arc.into()));
1057    }
1058
1059    fn on_edid_profile_changed(&self, bay: BayUid, profile: mx_remote::EdidProfile) {
1060        forward!(self.on_edid_profile_changed(bay.into(), profile.to_wire()));
1061    }
1062
1063    fn on_rc_type_changed(&self, bay: BayUid, rc_type: mx_remote::RcType) {
1064        forward!(self.on_rc_type_changed(bay.into(), rc_type.to_wire()));
1065    }
1066
1067    fn on_key_pressed(&self, bay: BayUid, key: RcKey) {
1068        forward!(self.on_key_pressed(bay.into(), key.to_wire()));
1069    }
1070
1071    fn on_action_received(&self, bay: BayUid, action: RcAction) {
1072        forward!(self.on_action_received(bay.into(), action.to_wire()));
1073    }
1074
1075    fn on_mirror_status_changed(&self, bay: BayUid, mirror: mx_remote::BayMirrorStatus) {
1076        forward!(self.on_mirror_status_changed(bay.into(), bay_or_zero(mirror.target)));
1077    }
1078
1079    fn on_amp_zone_settings_changed(&self, bay: BayUid, _settings: mx_remote::AmpZoneSettings) {
1080        forward!(self.on_amp_zone_settings_changed(bay.into()));
1081    }
1082
1083    fn on_volume_step(&self, bay: BayUid, up: bool) {
1084        forward!(self.on_volume_step(bay.into(), up));
1085    }
1086
1087    fn on_audio_clip(&self, bay: BayUid, clip: AudioClip) {
1088        forward!(self.on_audio_clip(bay.into(), clip.clip));
1089    }
1090
1091    fn on_ir_captured(&self, bay: BayUid, capture: IrCapture) {
1092        let payload = mxr_ir_capture_t {
1093            timestamp: capture.timestamp,
1094            last_change: capture.last_change,
1095            meta: capture.meta.into(),
1096            timings: capture.timings.as_ptr(),
1097            timings_len: capture.timings.len(),
1098        };
1099        forward!(self.on_ir_captured(bay.into(), &payload));
1100    }
1101
1102    fn on_filtered_devices_changed(&self, bay: BayUid, _filtered: Vec<DeviceUid>) {
1103        forward!(self.on_filtered_devices_changed(bay.into()));
1104    }
1105
1106    fn on_audio_endpoint_changed(&self, bay: BayUid, endpoint: u8) {
1107        forward!(self.on_audio_endpoint_changed(bay.into(), endpoint));
1108    }
1109
1110    fn on_encoder_disabled_changed(&self, bay: BayUid, disabled: bool) {
1111        forward!(self.on_encoder_disabled_changed(bay.into(), disabled));
1112    }
1113
1114    fn on_decoder_disabled_changed(&self, bay: BayUid, disabled: bool) {
1115        forward!(self.on_decoder_disabled_changed(bay.into(), disabled));
1116    }
1117
1118    // ---- both ----
1119
1120    fn on_bay_linked(
1121        &self,
1122        bay: BayUid,
1123        linked_serial: String,
1124        bay_name: String,
1125        features: LinkFeature,
1126    ) {
1127        with_cstr(&linked_serial, |serial| {
1128            with_cstr(&bay_name, |name| {
1129                forward!(self.on_bay_linked(bay.into(), serial, name, features.bits()));
1130            });
1131        });
1132    }
1133
1134    fn on_bay_unlinked(&self, bay: BayUid, linked_serial: String, bay_name: String) {
1135        with_cstr(&linked_serial, |serial| {
1136            with_cstr(&bay_name, |name| {
1137                forward!(self.on_bay_unlinked(bay.into(), serial, name));
1138            });
1139        });
1140    }
1141}