Skip to main content

openlogi_device/
channel.rs

1//! HID++ transport and channel lifecycle.
2//!
3//! Resolving a [`route::DeviceRoute`] to an open channel, and the strategies
4//! that keep one open: [`ChannelPool`] for sessions that open on demand,
5//! [`ChannelRegistry`] for channels owned by the inventory enumerator, and
6//! [`SharedChannel`] handles lent out to this crate's read/write entry points.
7//!
8//! Opening itself belongs to a [`crate::backend::HidBackend`]; nothing here
9//! names a HID stack.
10
11use std::sync::{Arc, Weak};
12
13use hidpp::channel::HidppChannel;
14
15use route::DeviceRoute;
16
17pub(crate) mod pool;
18pub(crate) mod registry;
19pub(crate) mod route;
20#[cfg(test)]
21pub(crate) mod scripted;
22
23pub use pool::ChannelPool;
24pub use registry::ChannelRegistry;
25
26/// An open HID++ channel to a device, shared so route-addressed reads and writes
27/// can reuse an inventory- or capture-owned connection instead of
28/// re-enumerating and opening a fresh channel each time (which costs ~100ms+).
29///
30/// Cheap to clone (an `Arc` plus the [`DeviceRoute`] it points at). Built by
31/// the inventory registry or a standalone capture session.
32#[derive(Clone)]
33pub struct SharedChannel {
34    channel: Arc<HidppChannel>,
35    route: DeviceRoute,
36}
37
38impl SharedChannel {
39    /// Wrap an open channel that reaches `route`.
40    #[must_use]
41    pub(crate) fn new(channel: Arc<HidppChannel>, route: DeviceRoute) -> Self {
42        Self { channel, route }
43    }
44
45    /// Whether this channel reaches `route` — so the write path only reuses it
46    /// for the device it actually points at.
47    #[must_use]
48    pub fn matches(&self, route: &DeviceRoute) -> bool {
49        self.route == *route
50    }
51
52    pub(crate) fn channel(&self) -> &Arc<HidppChannel> {
53        &self.channel
54    }
55
56    pub(crate) fn device_index(&self) -> u8 {
57        self.route.device_index()
58    }
59
60    /// The physical route this channel addresses, used to serialize complete
61    /// native lighting transactions rather than just individual HID reports.
62    #[must_use]
63    pub fn route(&self) -> &DeviceRoute {
64        &self.route
65    }
66
67    /// A handle to this same connection that does not keep it open, for
68    /// state that must not outlive the connection it was set through.
69    #[must_use]
70    pub fn downgrade(&self) -> WeakSharedChannel {
71        WeakSharedChannel {
72            channel: Arc::downgrade(&self.channel),
73            route: self.route.clone(),
74        }
75    }
76}
77
78/// A [`SharedChannel`] that does not keep its connection open. Two handles are
79/// equal when they name the same connection to the same route, so a replug or
80/// another device answering on the route compares unequal.
81#[derive(Clone)]
82pub struct WeakSharedChannel {
83    channel: Weak<HidppChannel>,
84    route: DeviceRoute,
85}
86
87impl WeakSharedChannel {
88    /// The channel, while its connection is still open.
89    #[must_use]
90    pub fn upgrade(&self) -> Option<SharedChannel> {
91        self.channel
92            .upgrade()
93            .map(|channel| SharedChannel::new(channel, self.route.clone()))
94    }
95}
96
97impl PartialEq for WeakSharedChannel {
98    fn eq(&self, other: &Self) -> bool {
99        self.route == other.route && Weak::ptr_eq(&self.channel, &other.channel)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::scripted::{ScriptedRawHidChannel, scripted_channel};
106    use super::*;
107
108    fn route(slot: u8) -> DeviceRoute {
109        DeviceRoute::Unifying {
110            receiver_uid: "weak-channel".into(),
111            slot,
112        }
113    }
114
115    async fn connection() -> Arc<HidppChannel> {
116        let (raw, _handle) = ScriptedRawHidChannel::with_dynamic_responder(|_| None);
117        scripted_channel(raw).await
118    }
119
120    #[tokio::test]
121    async fn a_weak_channel_names_one_connection_without_keeping_it_open() {
122        let receiver = connection().await;
123        let shared = SharedChannel::new(Arc::clone(&receiver), route(1));
124        let weak = shared.downgrade();
125        assert!(weak == shared.downgrade());
126        assert!(
127            weak.upgrade()
128                .is_some_and(|channel| channel.matches(&route(1)))
129        );
130
131        let reconnected = SharedChannel::new(connection().await, route(1));
132        assert!(
133            weak != reconnected.downgrade(),
134            "a reconnect is another connection"
135        );
136        let other_slot = SharedChannel::new(Arc::clone(&receiver), route(2));
137        assert!(
138            weak != other_slot.downgrade(),
139            "the same receiver, another slot"
140        );
141
142        drop((receiver, shared, other_slot));
143        assert!(
144            weak.upgrade().is_none(),
145            "a closed connection is not reopened"
146        );
147    }
148}