Skip to main content

openlogi_device/inventory/
events.rs

1//! Coalesced lifecycle events from inventory-owned HID++ channels.
2//!
3//! The persistent [`super::Enumerator`] opens each OS HID node once. This
4//! module attaches one message listener to that existing channel and keeps
5//! only the feature indexes needed to recognize lifecycle notifications. The
6//! events never mutate inventory: they request another authoritative
7//! reconciliation from the enumerator.
8
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::sync::{Arc, PoisonError, RwLock};
11
12use hidpp::channel::{HidppChannel, HidppMessage, MessageListenerGuard};
13use hidpp::feature::adc_measurement::AdcMeasurementFeature;
14use hidpp::feature::unified_battery::BatteryEvent;
15use hidpp::feature::wireless_device_status::WirelessDeviceStatusEvent;
16use hidpp::protocol::{v10, v20};
17use hidpp::receiver::{bolt, unifying};
18use serde::{Deserialize, Serialize};
19#[cfg(test)]
20use tokio::sync::Notify;
21use tokio::sync::mpsc;
22
23use crate::ReceiverProtocol;
24
25/// A HID++ lifecycle source that requested authoritative inventory
26/// reconciliation.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum HidppEventSource {
29    /// A receiver reported a paired slot connecting or disconnecting.
30    ReceiverConnection,
31    /// A device's `WirelessDeviceStatus` feature reported reconnection.
32    WirelessDeviceStatus,
33    /// A device's event-capable `UnifiedBattery` feature reported a change.
34    UnifiedBattery,
35    /// A device's `AdcMeasurement` feature broadcast a reading or a link
36    /// change — a headset dongle's headset switching on or off.
37    AdcMeasurement,
38}
39
40/// The sending half of the bounded HID++ reconciliation-request channel.
41///
42/// Clones are installed only on inventory-owned channels. Capacity is one:
43/// every source asks for the same full reconciliation, so a burst has no
44/// additional meaning and must not grow memory while a slow probe is running.
45#[derive(Clone)]
46pub struct EventNotifier {
47    sender: mpsc::Sender<HidppEventSource>,
48    #[cfg(test)]
49    observation: Option<Arc<EventObservation>>,
50}
51
52impl EventNotifier {
53    fn notify(&self, source: HidppEventSource) {
54        let _ = self.sender.try_send(source);
55        #[cfg(test)]
56        if let Some(observation) = &self.observation {
57            observation.count.fetch_add(1, Ordering::Release);
58            observation.changed.notify_waiters();
59        }
60    }
61}
62
63/// The receiving half of the coalesced HID++ reconciliation-request channel.
64pub type EventReceiver = mpsc::Receiver<HidppEventSource>;
65
66/// Build the bounded channel used by an inventory watcher and its enumerator.
67#[must_use]
68pub fn event_channel() -> (EventNotifier, EventReceiver) {
69    let (sender, receiver) = mpsc::channel(1);
70    (
71        EventNotifier {
72            sender,
73            #[cfg(test)]
74            observation: None,
75        },
76        receiver,
77    )
78}
79
80#[cfg(test)]
81struct EventObservation {
82    count: AtomicUsize,
83    changed: Notify,
84}
85
86/// Barrier observing decoded lifecycle events, including coalesced requests.
87#[cfg(test)]
88pub(crate) struct EventObserver {
89    observation: Arc<EventObservation>,
90}
91
92#[cfg(test)]
93impl EventObserver {
94    pub(crate) async fn wait_for(&self, expected: usize) {
95        loop {
96            let changed = self.observation.changed.notified();
97            if self.observation.count.load(Ordering::Acquire) >= expected {
98                return;
99            }
100            changed.await;
101        }
102    }
103}
104
105/// Build the production capacity-one event channel plus a decode observation barrier.
106#[cfg(test)]
107pub(crate) fn observed_event_channel() -> (EventNotifier, EventReceiver, EventObserver) {
108    let (sender, receiver) = mpsc::channel(1);
109    let observation = Arc::new(EventObservation {
110        count: AtomicUsize::new(0),
111        changed: Notify::new(),
112    });
113    (
114        EventNotifier {
115            sender,
116            observation: Some(Arc::clone(&observation)),
117        },
118        receiver,
119        EventObserver { observation },
120    )
121}
122
123/// Runtime feature indexes whose unsolicited events affect inventory.
124///
125/// Stored with the immutable feature-table cache because these indexes are
126/// discovered by the same walk. Unified battery and `0x1F20` ADC measurement
127/// have battery events; legacy `0x1000` and voltage `0x1001` remain
128/// recovery-scan reads.
129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
130pub(super) struct EventFeatureIndices {
131    pub(super) wireless_status: Option<u8>,
132    pub(super) unified_battery: Option<u8>,
133    pub(super) adc_measurement: Option<u8>,
134}
135
136impl EventFeatureIndices {
137    pub(super) fn from_feature_ids(ids: &[u16]) -> Self {
138        let mut indices = Self::default();
139        for (position, id) in ids.iter().copied().enumerate() {
140            let Ok(index) = u8::try_from(position + 1) else {
141                break;
142            };
143            match id {
144                0x1d4b => indices.wireless_status = Some(index),
145                0x1004 => indices.unified_battery = Some(index),
146                0x1f20 => indices.adc_measurement = Some(index),
147                _ => {}
148            }
149        }
150        indices
151    }
152
153    fn recognizes(self, message: &v20::Message) -> Option<HidppEventSource> {
154        let header = message.header();
155        if header.software_id.to_lo() != 0 {
156            return None;
157        }
158        let function_id = header.function_id.to_lo();
159        let payload = message.extend_payload();
160        if self.wireless_status == Some(header.feature_index)
161            && WirelessDeviceStatusEvent::decode(function_id, &payload).is_some()
162        {
163            return Some(HidppEventSource::WirelessDeviceStatus);
164        }
165        if self.unified_battery == Some(header.feature_index)
166            && BatteryEvent::decode(function_id, &payload).is_some()
167        {
168            return Some(HidppEventSource::UnifiedBattery);
169        }
170        // Unlike the two above, not gated on decoding the payload: an
171        // unknown flags value still announces a link change.
172        if self.adc_measurement == Some(header.feature_index)
173            && AdcMeasurementFeature::is_status_broadcast(function_id)
174        {
175            return Some(HidppEventSource::AdcMeasurement);
176        }
177        None
178    }
179}
180
181#[derive(Clone, Copy)]
182struct DeviceEvents {
183    device_index: u8,
184    features: EventFeatureIndices,
185}
186
187struct SubscriptionState {
188    protocol: Option<ReceiverProtocol>,
189    devices: RwLock<Vec<DeviceEvents>>,
190    receiver_snapshot_depth: AtomicUsize,
191    notifier: EventNotifier,
192}
193
194impl SubscriptionState {
195    fn decode(&self, raw: HidppMessage, matched: bool) -> Option<HidppEventSource> {
196        if matched {
197            return None;
198        }
199
200        let receiver_connection = match self.protocol {
201            Some(ReceiverProtocol::Bolt) => matches!(
202                bolt::decode_notification(&v10::Message::from(raw)),
203                Some(bolt::Event::DeviceConnection(_))
204            ),
205            Some(ReceiverProtocol::Unifying) => matches!(
206                unifying::decode_notification(&v10::Message::from(raw)),
207                Some(unifying::Event::DeviceConnection(_))
208            ),
209            None => false,
210        };
211        if receiver_connection {
212            return (self.receiver_snapshot_depth.load(Ordering::Acquire) == 0)
213                .then_some(HidppEventSource::ReceiverConnection);
214        }
215
216        let message = v20::Message::from(raw);
217        let device_index = message.header().device_index;
218        self.devices
219            .read()
220            .unwrap_or_else(PoisonError::into_inner)
221            .iter()
222            .find(|device| device.device_index == device_index)
223            .and_then(|device| device.features.recognizes(&message))
224    }
225}
226
227/// Cloneable registration handle passed through a node's probe.
228#[derive(Clone)]
229pub(super) struct EventSubscriptionHandle {
230    state: Arc<SubscriptionState>,
231}
232
233impl EventSubscriptionHandle {
234    /// Replace the decoder metadata for `device_index` as soon as a valid
235    /// feature table (or its cached equivalent) is available. The channel
236    /// listener was installed before the probe began, closing the wakeup race
237    /// before the resulting snapshot is published.
238    pub(super) fn register_device(&self, device_index: u8, features: EventFeatureIndices) {
239        let mut devices = self
240            .state
241            .devices
242            .write()
243            .unwrap_or_else(PoisonError::into_inner);
244        if let Some(device) = devices
245            .iter_mut()
246            .find(|device| device.device_index == device_index)
247        {
248            device.features = features;
249        } else {
250            devices.push(DeviceEvents {
251                device_index,
252                features,
253            });
254        }
255    }
256
257    /// Suppress receiver connection events fabricated by the enumerator's own
258    /// arrival trigger. A real event in this interval is covered by the probe
259    /// already in progress; events immediately before or after remain queued.
260    pub(super) fn begin_receiver_snapshot(&self) -> ReceiverSnapshotGuard {
261        self.state
262            .receiver_snapshot_depth
263            .fetch_add(1, Ordering::AcqRel);
264        ReceiverSnapshotGuard {
265            state: Arc::clone(&self.state),
266        }
267    }
268}
269
270/// Persistent listener owned alongside one cached HID++ channel.
271pub(super) struct ChannelEventSubscriptions {
272    handle: EventSubscriptionHandle,
273    _listener: MessageListenerGuard,
274}
275
276impl ChannelEventSubscriptions {
277    pub(super) fn attach(
278        channel: &Arc<HidppChannel>,
279        protocol: Option<ReceiverProtocol>,
280        notifier: EventNotifier,
281    ) -> Self {
282        let state = Arc::new(SubscriptionState {
283            protocol,
284            devices: RwLock::new(Vec::new()),
285            receiver_snapshot_depth: AtomicUsize::new(0),
286            notifier,
287        });
288        let listener = channel.add_msg_listener_guarded({
289            let state = Arc::clone(&state);
290            move |raw, matched| {
291                if let Some(source) = state.decode(raw, matched) {
292                    state.notifier.notify(source);
293                }
294            }
295        });
296        Self {
297            handle: EventSubscriptionHandle { state },
298            _listener: listener,
299        }
300    }
301
302    pub(super) fn handle(&self) -> EventSubscriptionHandle {
303        self.handle.clone()
304    }
305}
306
307/// Receiver-arrival suppression scoped to one authoritative snapshot.
308pub(super) struct ReceiverSnapshotGuard {
309    state: Arc<SubscriptionState>,
310}
311
312impl Drop for ReceiverSnapshotGuard {
313    fn drop(&mut self) {
314        self.state
315            .receiver_snapshot_depth
316            .fetch_sub(1, Ordering::AcqRel);
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use hidpp::nibble::U4;
323    use hidpp::protocol::{v10, v20};
324
325    use super::*;
326
327    fn state(protocol: Option<ReceiverProtocol>) -> Arc<SubscriptionState> {
328        let (notifier, _receiver) = event_channel();
329        Arc::new(SubscriptionState {
330            protocol,
331            devices: RwLock::new(Vec::new()),
332            receiver_snapshot_depth: AtomicUsize::new(0),
333            notifier,
334        })
335    }
336
337    #[test]
338    fn feature_indices_are_runtime_table_positions() {
339        assert_eq!(
340            EventFeatureIndices::from_feature_ids(&[0x0001, 0x1d4b, 0x1004]),
341            EventFeatureIndices {
342                wireless_status: Some(2),
343                unified_battery: Some(3),
344                adc_measurement: None,
345            }
346        );
347    }
348
349    #[test]
350    fn receiver_snapshot_suppresses_only_triggered_connection_wakeups() {
351        let state = state(Some(ReceiverProtocol::Unifying));
352        let raw = v10::Message::Short(
353            v10::MessageHeader {
354                device_index: 2,
355                sub_id: 0x41,
356            },
357            [0, 0x02, 0x34, 0x12],
358        )
359        .into();
360
361        assert_eq!(
362            state.decode(raw, false),
363            Some(HidppEventSource::ReceiverConnection)
364        );
365        state.receiver_snapshot_depth.store(1, Ordering::Release);
366        assert_eq!(state.decode(raw, false), None);
367    }
368
369    #[test]
370    fn receiver_snapshot_does_not_suppress_device_feature_events() {
371        let state = state(Some(ReceiverProtocol::Bolt));
372        state.devices.write().unwrap().push(DeviceEvents {
373            device_index: 2,
374            features: EventFeatureIndices {
375                wireless_status: Some(5),
376                unified_battery: None,
377                adc_measurement: None,
378            },
379        });
380        state.receiver_snapshot_depth.store(1, Ordering::Release);
381        let wireless = v20::Message::Long(
382            v20::MessageHeader {
383                device_index: 2,
384                feature_index: 5,
385                function_id: U4::from_lo(0),
386                software_id: U4::from_lo(0),
387            },
388            [0; 16],
389        )
390        .into();
391
392        assert_eq!(
393            state.decode(wireless, false),
394            Some(HidppEventSource::WirelessDeviceStatus)
395        );
396    }
397
398    #[test]
399    fn registered_device_events_decode_to_typed_sources() {
400        let state = state(None);
401        state.devices.write().unwrap().push(DeviceEvents {
402            device_index: 3,
403            features: EventFeatureIndices {
404                wireless_status: Some(5),
405                unified_battery: Some(7),
406                adc_measurement: None,
407            },
408        });
409
410        let wireless = v20::Message::Long(
411            v20::MessageHeader {
412                device_index: 3,
413                feature_index: 5,
414                function_id: U4::from_lo(0),
415                software_id: U4::from_lo(0),
416            },
417            [0; 16],
418        )
419        .into();
420        assert_eq!(
421            state.decode(wireless, false),
422            Some(HidppEventSource::WirelessDeviceStatus)
423        );
424
425        let mut payload = [0; 16];
426        payload[0] = 80;
427        payload[1] = 4;
428        let battery = v20::Message::Long(
429            v20::MessageHeader {
430                device_index: 3,
431                feature_index: 7,
432                function_id: U4::from_lo(0),
433                software_id: U4::from_lo(0),
434            },
435            payload,
436        )
437        .into();
438        assert_eq!(
439            state.decode(battery, false),
440            Some(HidppEventSource::UnifiedBattery)
441        );
442    }
443
444    #[test]
445    fn adc_measurement_index_is_its_table_position() {
446        // The G733 feature table, 0x1F20 at runtime index 8.
447        let table = [
448            0x0001, 0x0003, 0x0005, 0x8070, 0x8010, 0x8310, 0x8300, 0x1f20,
449        ];
450        assert_eq!(
451            EventFeatureIndices::from_feature_ids(&table).adc_measurement,
452            Some(8)
453        );
454    }
455
456    #[test]
457    fn adc_measurement_broadcasts_request_reconciliation() {
458        let state = state(None);
459        state.devices.write().unwrap().push(DeviceEvents {
460            device_index: 0xff,
461            features: EventFeatureIndices {
462                wireless_status: None,
463                unified_battery: None,
464                adc_measurement: Some(8),
465            },
466        });
467        let event = |function_id: u8, software_id: u8, bytes: [u8; 3]| {
468            let mut payload = [0; 16];
469            payload[..3].copy_from_slice(&bytes);
470            v20::Message::Long(
471                v20::MessageHeader {
472                    device_index: 0xff,
473                    feature_index: 8,
474                    function_id: U4::from_lo(function_id),
475                    software_id: U4::from_lo(software_id),
476                },
477                payload,
478            )
479            .into()
480        };
481
482        // Both broadcasts captured from a G733 dongle: `11 ff 08 00 00 00 00`
483        // as the headset switched off, `11 ff 08 00 10 4a 03` as it switched
484        // back on while charging.
485        for bytes in [[0x00, 0x00, 0x00], [0x10, 0x4a, 0x03]] {
486            assert_eq!(
487                state.decode(event(0, 0, bytes), false),
488                Some(HidppEventSource::AdcMeasurement),
489                "{bytes:02x?}"
490            );
491        }
492        // A broadcast whose flags value is unknown (the kernel's `0x0F`) still
493        // announces a change; the reconciliation's read decides what it means.
494        assert_eq!(
495            state.decode(event(0, 0, [0x10, 0x4a, 0x0f]), false),
496            Some(HidppEventSource::AdcMeasurement)
497        );
498        // A reply to our own request carries a software id; not an event.
499        assert_eq!(state.decode(event(0, 0xd, [0x10, 0x4a, 0x01]), false), None);
500        // Only event `0` is the status broadcast.
501        assert_eq!(state.decode(event(1, 0, [0x10, 0x4a, 0x01]), false), None);
502    }
503
504    #[test]
505    fn event_requests_are_bounded_and_coalesced() {
506        let (notifier, mut receiver) = event_channel();
507        notifier.notify(HidppEventSource::ReceiverConnection);
508        notifier.notify(HidppEventSource::UnifiedBattery);
509
510        assert_eq!(
511            receiver.try_recv(),
512            Ok(HidppEventSource::ReceiverConnection)
513        );
514        assert_eq!(receiver.try_recv(), Err(mpsc::error::TryRecvError::Empty));
515    }
516}