Skip to main content

hidpp/feature/dual_platform/
mod.rs

1//! Implements the `DualPlatform` feature (ID `0x4530`) that selects which of two
2//! OS platforms a device sends HID codes for.
3//!
4//! This is the predecessor of [`MultiPlatform`](super::multi_platform)
5//! (`0x4531`); a device exposing `0x4531` should be driven through that feature
6//! instead.
7
8use std::sync::Arc;
9
10use num_enum::{IntoPrimitive, TryFromPrimitive};
11
12use crate::{
13    channel::{HidppChannel, MessageListenerGuard},
14    event::EventEmitter,
15    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
16    protocol::v20::Hidpp20Error,
17};
18
19/// The platform a [`DualPlatformFeature`] device is configured for.
20///
21/// The selection is persistent and chosen by the user during pairing or by short
22/// pressing an OS-selection button; there is no default.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25#[non_exhaustive]
26#[repr(u8)]
27pub enum DualPlatformSelection {
28    /// iOS or macOS.
29    IosOrMac = 0,
30    /// Android or Windows.
31    AndroidOrWindows = 1,
32}
33
34/// An event emitted by [`DualPlatformFeature`].
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37#[non_exhaustive]
38pub enum DualPlatformEvent {
39    /// The user changed the platform via an OS-selection button.
40    PlatformChanged(DualPlatformSelection),
41}
42
43/// Implements the `DualPlatform` / `0x4530` feature.
44pub struct DualPlatformFeature {
45    /// The endpoint this feature talks to.
46    endpoint: FeatureEndpoint,
47
48    /// The emitter used to publish decoded events.
49    emitter: Arc<EventEmitter<DualPlatformEvent>>,
50
51    /// Removes the message listener when the feature is dropped.
52    _msg_listener: MessageListenerGuard,
53}
54
55impl CreatableFeature for DualPlatformFeature {
56    const ID: u16 = 0x4530;
57    const STARTING_VERSION: u8 = 0;
58
59    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
60        let emitter = Arc::new(EventEmitter::new());
61
62        let listener = chan.add_msg_listener_guarded({
63            let emitter = Arc::clone(&emitter);
64
65            move |raw, matched| {
66                let Some((func, payload)) =
67                    event_payload(raw, matched, device_index, feature_index)
68                else {
69                    return;
70                };
71                // PlatformChange is the only event and carries sub-id 0.
72                if func.to_lo() != 0 {
73                    return;
74                }
75                if let Ok(platform) = DualPlatformSelection::try_from(payload[0]) {
76                    emitter.emit(DualPlatformEvent::PlatformChanged(platform));
77                }
78            }
79        });
80
81        Self {
82            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
83            emitter,
84            _msg_listener: listener,
85        }
86    }
87}
88
89impl Feature for DualPlatformFeature {}
90
91impl EmittingFeature<DualPlatformEvent> for DualPlatformFeature {
92    fn listen(&self) -> async_channel::Receiver<DualPlatformEvent> {
93        self.emitter.create_receiver()
94    }
95}
96
97impl DualPlatformFeature {
98    /// Retrieves the current platform setting.
99    pub async fn get_platform(&self) -> Result<DualPlatformSelection, Hidpp20Error> {
100        // `getPlatform` is function 1 in this feature, not the usual 0.
101        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
102        DualPlatformSelection::try_from(payload[0]).map_err(|_| Hidpp20Error::UnsupportedResponse)
103    }
104
105    /// Sets the platform and returns the device's echo of the new setting.
106    ///
107    /// This does not trigger a [`DualPlatformEvent::PlatformChanged`] event.
108    pub async fn set_platform(
109        &self,
110        platform: DualPlatformSelection,
111    ) -> Result<DualPlatformSelection, Hidpp20Error> {
112        let payload = self
113            .endpoint
114            .call(2, [platform.into(), 0, 0])
115            .await?
116            .extend_payload();
117        DualPlatformSelection::try_from(payload[0]).map_err(|_| Hidpp20Error::UnsupportedResponse)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::DualPlatformSelection;
124
125    #[test]
126    fn maps_platform_wire_values() {
127        assert_eq!(
128            DualPlatformSelection::try_from(0).unwrap(),
129            DualPlatformSelection::IosOrMac
130        );
131        assert_eq!(
132            DualPlatformSelection::try_from(1).unwrap(),
133            DualPlatformSelection::AndroidOrWindows
134        );
135        assert!(DualPlatformSelection::try_from(2).is_err());
136        assert_eq!(u8::from(DualPlatformSelection::AndroidOrWindows), 1);
137    }
138}