Skip to main content

mx_remote/
event.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Events and the handler that receives them.
5
6use crate::types::*;
7use crate::wire::{BayUid, DeviceUid, EdidProfile, LinkFeature, RcAction, RcKey, RcType};
8
9/// Declares the event set.
10///
11/// One declaration produces the [`Event`] enum, the [`EventHandler`] trait and
12/// the dispatch that connects them, so a new event cannot reach the enum
13/// without reaching the trait, and cannot be dispatched without fanning in to
14/// the generic update. The fan-in is written once here rather than repeated at
15/// each call site.
16///
17/// The section an event is declared in decides what it fans in to: `device`
18/// events reach `on_device_update`, `bay` events reach `on_bay_update`, and
19/// `bay_and_device` events reach both. A link change concerns the bay and the
20/// device that owns it, which is why it is neither of the first two.
21macro_rules! events {
22    (
23        device {
24            $( $(#[$dmeta:meta])* $dvariant:ident => $dmethod:ident ( $($darg:ident : $dty:ty),* ); )*
25        }
26        bay {
27            $( $(#[$bmeta:meta])* $bvariant:ident => $bmethod:ident ( $($barg:ident : $bty:ty),* ); )*
28        }
29        bay_and_device {
30            $( $(#[$lmeta:meta])* $lvariant:ident => $lmethod:ident ( $($larg:ident : $lty:ty),* ); )*
31        }
32    ) => {
33        /// Something that changed, or a request that arrived.
34        ///
35        /// Events are collected while the state lock is held and dispatched
36        /// after it is released, so a handler may call back into the library.
37        #[derive(Clone, Debug, PartialEq)]
38        #[non_exhaustive]
39        pub enum Event {
40            $(
41                $(#[$dmeta])*
42                $dvariant {
43                    /// The device the event concerns.
44                    device: DeviceUid,
45                    $( #[allow(missing_docs)] $darg: $dty, )*
46                },
47            )*
48            $(
49                $(#[$bmeta])*
50                $bvariant {
51                    /// The bay the event concerns.
52                    bay: BayUid,
53                    $( #[allow(missing_docs)] $barg: $bty, )*
54                },
55            )*
56            $(
57                $(#[$lmeta])*
58                $lvariant {
59                    /// The bay the event concerns.
60                    bay: BayUid,
61                    $( #[allow(missing_docs)] $larg: $lty, )*
62                },
63            )*
64        }
65
66        /// Receives events.
67        ///
68        /// Every method has a no-op default, so an implementation names only
69        /// the events it cares about. Handlers run one at a time from the
70        /// thread that produced the event, with no lock held: calling back into
71        /// the library from one is safe, but blocking for long stalls the
72        /// receive path.
73        #[allow(unused_variables)]
74        pub trait EventHandler: Send + Sync {
75            $(
76                $(#[$dmeta])*
77                fn $dmethod(&self, device: DeviceUid $(, $darg: $dty)*) {}
78            )*
79            $(
80                $(#[$bmeta])*
81                fn $bmethod(&self, bay: BayUid $(, $barg: $bty)*) {}
82            )*
83            $(
84                $(#[$lmeta])*
85                fn $lmethod(&self, bay: BayUid $(, $larg: $lty)*) {}
86            )*
87
88            /// Fired after every device-level event above.
89            fn on_device_update(&self, device: DeviceUid) {}
90
91            /// Fired after every bay-level event above.
92            fn on_bay_update(&self, bay: BayUid) {}
93        }
94
95        impl Event {
96            /// Delivers this event to `handler`, then the generic update it
97            /// fans in to.
98            pub(crate) fn dispatch(self, handler: &dyn EventHandler) {
99                match self {
100                    $(
101                        Self::$dvariant { device $(, $darg)* } => {
102                            handler.$dmethod(device $(, $darg)*);
103                            handler.on_device_update(device);
104                        }
105                    )*
106                    $(
107                        Self::$bvariant { bay $(, $barg)* } => {
108                            handler.$bmethod(bay $(, $barg)*);
109                            handler.on_bay_update(bay);
110                        }
111                    )*
112                    $(
113                        Self::$lvariant { bay $(, $larg)* } => {
114                            handler.$lmethod(bay $(, $larg)*);
115                            handler.on_bay_update(bay);
116                            handler.on_device_update(bay.device);
117                        }
118                    )*
119                }
120            }
121        }
122    };
123}
124
125/// Ignores every event.
126///
127/// The handler a client that only reads state through [`Remote`] needs, since
128/// the trait is not optional and its methods all default to nothing.
129///
130/// [`Remote`]: crate::Remote
131impl EventHandler for () {}
132
133events! {
134    device {
135        /// The device's configuration changed.
136        DeviceConfigChanged => on_device_config_changed();
137        /// The device has reported every part of its configuration.
138        DeviceConfigComplete => on_device_config_complete();
139        /// The device started or stopped answering.
140        DeviceOnlineChanged => on_device_online_changed(online: bool);
141        /// The device reported new temperatures.
142        DeviceTemperatureChanged => on_device_temperature_changed(temperatures: Vec<u8>);
143        /// A firmware component reported its version.
144        FirmwareVersionChanged => on_firmware_version_changed(version: FirmwareVersion);
145        /// The device reported a system status.
146        SystemStatusChanged => on_system_status_changed(status: u16, message: String);
147        /// A network port reported its link state.
148        NetworkStatusChanged => on_network_status_changed(status: NetworkPortStatus);
149        /// The device reported V2IP statistics.
150        V2ipStatsChanged => on_v2ip_stats_changed(stats: V2ipDeviceStats);
151        /// The streams the device's source bays advertise changed.
152        V2ipSourcesChanged => on_v2ip_sources_changed(sources: Vec<V2ipStreamSources>);
153        /// The device's V2IP encoder configuration changed.
154        V2ipDetailsChanged => on_v2ip_details_changed(details: DeviceV2ipDetails);
155        /// The streams the device's sink is subscribed to changed.
156        V2ipSinkChanged => on_v2ip_sink_changed(sink: DeviceV2ipSink);
157        /// A multiviewer reported its state.
158        MultiviewerStatusChanged => on_multiviewer_status_changed(status: MultiviewerStatus);
159        /// The device reported its audio endpoint tree.
160        AudioEndpointsChanged => on_audio_endpoints_changed(endpoints: AudioEndpoints);
161        /// The device reported its mesh master.
162        MeshMasterChanged => on_mesh_master_changed(master: DeviceUid);
163        /// The device reported its view of the mesh topology.
164        TopologyChanged => on_topology_changed(topology: Vec<TopologyEntry>);
165        /// A ProAmp8 reported its Dolby settings.
166        AmpDolbySettingsChanged => on_amp_dolby_settings_changed(settings: AmpDolbySettings);
167        /// A PDU reported its electrical state.
168        PduStateChanged => on_pdu_state_changed(state: PduState);
169        /// Installer setup was completed or cleared.
170        SetupStatusChanged => on_setup_status_changed(completed: bool);
171        /// The installer id changed.
172        InstallerIdChanged => on_installer_id_changed(installer_id: u16);
173        /// The sink was told to show a window.
174        TilingChanged => on_tiling_changed(tiling: V2ipTilingConfig);
175        /// A source bay's remote-control configuration changed.
176        RcSettingsChanged => on_rc_settings_changed(settings: RcSettings);
177        /// A V2IP device was linked to a remote peer.
178        V2ipLinkChanged => on_v2ip_link_changed(target: DeviceUid);
179        /// A multiviewer command arrived.
180        MultiviewerCommand => on_multiviewer_command(command: MultiviewerCommand);
181        /// An audio endpoint was switched to a new source.
182        AudioSelectInput => on_audio_select_input(change: AudioChangeSource);
183        /// An audio endpoint was muted or unmuted.
184        AudioEndpointMute => on_audio_endpoint_mute(endpoint: u16, muted: bool);
185        /// An audio endpoint's trigger changed.
186        AudioEndpointTrigger => on_audio_endpoint_trigger(endpoint: u16, active: bool);
187        /// An audio endpoint's volume changed.
188        AudioEndpointVolume => on_audio_endpoint_volume(endpoint: u16, volume: u32);
189        /// A peer asked every device to announce itself.
190        DiscoverRequest => on_discover_request();
191        /// A peer asked a device to switch a sink.
192        SetRouteRequested => on_set_route_requested(request: SetRouteRequest);
193        /// A peer asked a device for its EDID.
194        EdidRequested => on_edid_requested(request: EdidRequest);
195        /// A device answered with its EDID.
196        EdidReceived => on_edid_received(edid: EdidRecord);
197        /// A peer asked a device to rename a bay.
198        BayNameChangeRequested => on_bay_name_change_requested(change: BayNameChange);
199        /// A peer asked a device to switch its EDID profile.
200        EdidProfileChangeRequested => on_edid_profile_change_requested(change: EdidProfileChange);
201        /// A peer asked a device to reboot.
202        RebootRequested => on_reboot_requested(request: RebootRequest);
203        /// A peer asked devices to factory-reset.
204        FactoryResetRequested => on_factory_reset_requested(request: FactoryResetRequest);
205        /// A device sent its monitoring pulse.
206        MonitoringPulse => on_monitoring_pulse();
207        /// A peer asked a device to upgrade its FPGA.
208        UpgradeFpgaRequested => on_upgrade_fpga_requested();
209        /// A peer asked a device to re-detect its bays.
210        DetectBaysRequested => on_detect_bays_requested();
211        /// A peer asked a sink to enter or leave power save.
212        PowerSaveRequested => on_power_save_requested(request: V2ipPowerSaveRequest);
213        /// A peer asked a device to send a remote-control key.
214        KeyTransmitRequested => on_key_transmit_requested(request: KeyTransmitRequest);
215        /// A peer asked a device to perform a remote-control action.
216        ActionTransmitRequested => on_action_transmit_requested(request: ActionTransmitRequest);
217        /// A peer asked a device to blast raw infrared.
218        IrTransmitRequested => on_ir_transmit_requested(request: IrTransmitRequest);
219        /// A device was added to or removed from the source blacklist.
220        BlacklistChanged => on_blacklist_changed(change: V2ipBlacklistChange);
221        /// A video wall command arrived.
222        VideoWallCommand => on_video_wall_command(command: VideoWallCommand);
223    }
224    bay {
225        /// A bay was seen for the first time.
226        BayRegistered => on_bay_registered();
227        /// The bay's routed video source changed.
228        VideoSourceChanged => on_video_source_changed(source: Option<BayUid>);
229        /// The bay's routed audio source changed.
230        AudioSourceChanged => on_audio_source_changed(source: Option<BayUid>);
231        /// The bay's volume or mute state changed.
232        VolumeChanged => on_volume_changed(volume: VolumeMuteStatus);
233        /// The attached device's power state changed.
234        PowerChanged => on_power_changed(power: PowerStatus);
235        /// The bay was renamed.
236        NameChanged => on_name_changed(name: String);
237        /// A signal appeared or disappeared.
238        SignalDetectedChanged => on_signal_detected_changed(detected: bool);
239        /// The bay started or stopped reporting a fault.
240        FaultyChanged => on_faulty_changed(faulty: bool);
241        /// The bay was hidden or shown.
242        HiddenChanged => on_hidden_changed(hidden: bool);
243        /// Power over Ethernet started or stopped supplying the bay.
244        PoePoweredChanged => on_poe_powered_changed(powered: bool);
245        /// The HDBaseT link came up or went down.
246        HdbtConnectedChanged => on_hdbt_connected_changed(connected: bool);
247        /// The signal format description changed.
248        SignalTypeChanged => on_signal_type_changed(signal_type: String);
249        /// Hot-plug detect was asserted or released.
250        HpdDetectedChanged => on_hpd_detected_changed(detected: bool);
251        /// A CEC device answered or stopped answering.
252        CecDetectedChanged => on_cec_detected_changed(detected: bool);
253        /// The audio return channel changed.
254        ArcChanged => on_arc_changed(arc: ArcStatus);
255        /// The input's EDID profile changed.
256        EdidProfileChanged => on_edid_profile_changed(profile: EdidProfile);
257        /// The input's remote-control type changed.
258        RcTypeChanged => on_rc_type_changed(rc_type: RcType);
259        /// A remote-control key was pressed on the bay.
260        KeyPressed => on_key_pressed(key: RcKey);
261        /// A remote-control action was received on the bay.
262        ActionReceived => on_action_received(action: RcAction);
263        /// The bay started or stopped mirroring another output.
264        MirrorStatusChanged => on_mirror_status_changed(mirror: BayMirrorStatus);
265        /// A ProAmp8 zone's settings changed.
266        AmpZoneSettingsChanged => on_amp_zone_settings_changed(settings: AmpZoneSettings);
267        /// A volume step was requested on the bay.
268        VolumeStep => on_volume_step(up: bool);
269        /// The bay detected audio clipping.
270        AudioClipped => on_audio_clip(clip: AudioClip);
271        /// Raw infrared was captured on the bay.
272        IrCaptured => on_ir_captured(capture: IrCapture);
273        /// The devices filtered out of this sink's picker changed.
274        FilteredDevicesChanged => on_filtered_devices_changed(filtered: Vec<DeviceUid>);
275        /// The audio endpoint the bay carries changed.
276        AudioEndpointChanged => on_audio_endpoint_changed(endpoint: u8);
277        /// The bay's V2IP encoder was enabled or disabled.
278        EncoderDisabledChanged => on_encoder_disabled_changed(disabled: bool);
279        /// The bay's V2IP decoder was enabled or disabled.
280        DecoderDisabledChanged => on_decoder_disabled_changed(disabled: bool);
281    }
282    bay_and_device {
283        /// The bay was linked to a bay on another device.
284        ///
285        /// `linked_serial` is the serial of the device at the other end of the
286        /// link, and `bay_name` the name of the bay whose link record changed:
287        /// this bay on the device that reported the change, and the far bay on
288        /// its peer. Both ends are told, so both fire.
289        BayLinked => on_bay_linked(linked_serial: String, bay_name: String, features: LinkFeature);
290        /// The bay's link to another device was removed.
291        ///
292        /// The arguments describe the link that was removed, and mean what
293        /// they do on [`Event::BayLinked`].
294        BayUnlinked => on_bay_unlinked(linked_serial: String, bay_name: String);
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use std::sync::Mutex;
302
303    #[derive(Default)]
304    struct Recorder {
305        calls: Mutex<Vec<String>>,
306    }
307
308    impl Recorder {
309        fn record(&self, what: &str) {
310            if let Ok(mut calls) = self.calls.lock() {
311                calls.push(what.to_owned());
312            }
313        }
314
315        fn calls(&self) -> Vec<String> {
316            self.calls.lock().map(|c| c.clone()).unwrap_or_default()
317        }
318    }
319
320    impl EventHandler for Recorder {
321        fn on_power_changed(&self, _bay: BayUid, power: PowerStatus) {
322            self.record(&format!("power={power}"));
323        }
324
325        fn on_setup_status_changed(&self, _device: DeviceUid, completed: bool) {
326            self.record(&format!("setup={completed}"));
327        }
328
329        fn on_bay_update(&self, _bay: BayUid) {
330            self.record("bay_update");
331        }
332
333        fn on_device_update(&self, _device: DeviceUid) {
334            self.record("device_update");
335        }
336    }
337
338    const DEVICE: DeviceUid = DeviceUid::from_array([9; 16]);
339
340    #[test]
341    fn a_bay_event_fires_its_own_method_then_the_generic_bay_update() {
342        let recorder = Recorder::default();
343        Event::PowerChanged {
344            bay: BayUid::new(DEVICE, 3),
345            power: PowerStatus::On,
346        }
347        .dispatch(&recorder);
348        assert_eq!(recorder.calls(), ["power=on", "bay_update"]);
349    }
350
351    #[test]
352    fn a_device_event_fires_its_own_method_then_the_generic_device_update() {
353        let recorder = Recorder::default();
354        Event::SetupStatusChanged {
355            device: DEVICE,
356            completed: true,
357        }
358        .dispatch(&recorder);
359        assert_eq!(recorder.calls(), ["setup=true", "device_update"]);
360    }
361
362    #[test]
363    fn a_link_event_fires_both_generic_updates() {
364        let recorder = Recorder::default();
365        Event::BayUnlinked {
366            bay: BayUid::new(DEVICE, 3),
367            linked_serial: "AB1234".to_owned(),
368            bay_name: "Output 1".to_owned(),
369        }
370        .dispatch(&recorder);
371        assert_eq!(recorder.calls(), ["bay_update", "device_update"]);
372    }
373
374    #[test]
375    fn an_event_a_handler_does_not_name_still_fires_the_generic_update() {
376        let recorder = Recorder::default();
377        Event::MonitoringPulse { device: DEVICE }.dispatch(&recorder);
378        assert_eq!(
379            recorder.calls(),
380            ["device_update"],
381            "the default no-op must not swallow the fan-in"
382        );
383    }
384}