Skip to main content

hidpp/feature/
multi_platform.rs

1//! Implements `MultiPlatform` (feature `0x4531`).
2
3#[cfg(test)]
4mod tests;
5
6use num_enum::TryFromPrimitive;
7use openlogi_hidpp_derive::Feature;
8
9use crate::{
10    feature::{FeatureEndpoint, hosts_info::HostIndex},
11    protocol::v20::Hidpp20Error,
12};
13
14bitflags::bitflags! {
15    /// Capabilities reported by `MultiPlatform`.
16    ///
17    /// The values are the big-endian `u16` read from reply bytes 0-1, the way
18    /// Logi Options+ 2.8 reads them (`Feature4531MultiPlatform::GetFeatureInfos`).
19    /// Options+ lets software set a host platform only while `0x0200` is set
20    /// (`platforms_info::set_platform`), so both flags sit in byte 0: bit 1 is
21    /// [`Self::SET_HOST_PLATFORM`], and [`Self::OS_DETECTION`] is bit 0 beside
22    /// it. Options+ tests no other bit, so the position of `OS_DETECTION` is
23    /// inferred from that one.
24    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
26    pub struct MultiPlatformCapabilities: u16 {
27        /// The device can detect the host OS automatically.
28        const OS_DETECTION = 1 << 8;
29        /// Software can set the host platform.
30        const SET_HOST_PLATFORM = 1 << 9;
31    }
32}
33
34bitflags::bitflags! {
35    /// Operating systems covered by a platform descriptor.
36    ///
37    /// The values are the big-endian `u16` read from descriptor bytes 2-3.
38    /// Logitech's documentation lists the OSes from the high bit down as
39    /// webOS, iOS, macOS, Android, Chrome, Linux, Windows Embedded, Windows
40    /// and Tizen, so a real Windows descriptor is `0x0100` and Tizen is
41    /// `0x0001`. Solaar's x4531 table and the Linux HID++ 0x4531 patch use
42    /// the same values.
43    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
45    pub struct OsMask: u16 {
46        /// Tizen.
47        const TIZEN = 1 << 0;
48        /// Microsoft Windows.
49        const WINDOWS = 1 << 8;
50        /// Windows Embedded.
51        const WINDOWS_EMBEDDED = 1 << 9;
52        /// Linux.
53        const LINUX = 1 << 10;
54        /// ChromeOS.
55        const CHROME = 1 << 11;
56        /// Android.
57        const ANDROID = 1 << 12;
58        /// macOS.
59        const MACOS = 1 << 13;
60        /// iOS.
61        const IOS = 1 << 14;
62        /// webOS.
63        const WEBOS = 1 << 15;
64    }
65}
66
67/// Source of a host-platform selection.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize))]
70#[non_exhaustive]
71#[repr(u8)]
72pub enum PlatformSource {
73    /// Device default.
74    Default = 0,
75    /// Automatically detected by the device.
76    Auto = 1,
77    /// Manually selected on the device.
78    Manual = 2,
79    /// Set by host software.
80    Software = 3,
81}
82
83/// Static `MultiPlatform` feature information.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86#[non_exhaustive]
87pub struct MultiPlatformInfo {
88    /// Feature capabilities.
89    pub capabilities: MultiPlatformCapabilities,
90    /// Number of platform IDs.
91    pub platform_count: u8,
92    /// Number of platform descriptor rows.
93    pub descriptor_count: u8,
94    /// Number of host slots.
95    pub host_count: u8,
96    /// Current host slot.
97    pub current_host: HostIndex,
98    /// Platform index selected for the current host.
99    pub current_host_platform: Option<u8>,
100}
101
102impl MultiPlatformInfo {
103    fn from_payload(payload: &[u8; 16]) -> Self {
104        Self {
105            capabilities: MultiPlatformCapabilities::from_bits_retain(u16::from_be_bytes([
106                payload[0], payload[1],
107            ])),
108            platform_count: payload[2],
109            descriptor_count: payload[3],
110            host_count: payload[4],
111            current_host: HostIndex::from(payload[5]),
112            current_host_platform: optional_index(payload[6]),
113        }
114    }
115}
116
117/// A platform descriptor row.
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize))]
120#[non_exhaustive]
121pub struct PlatformDescriptor {
122    /// Platform index this descriptor belongs to.
123    pub platform_index: u8,
124    /// Descriptor row index.
125    pub descriptor_index: u8,
126    /// Covered operating systems.
127    pub os_mask: OsMask,
128    /// First supported OS major version.
129    pub from_version: u8,
130    /// First supported OS revision.
131    pub from_revision: u8,
132    /// Last supported OS major version.
133    pub to_version: u8,
134    /// Last supported OS revision.
135    pub to_revision: u8,
136}
137
138impl PlatformDescriptor {
139    fn from_payload(payload: &[u8; 16]) -> Self {
140        Self {
141            platform_index: payload[0],
142            descriptor_index: payload[1],
143            os_mask: OsMask::from_bits_retain(u16::from_be_bytes([payload[2], payload[3]])),
144            from_version: payload[4],
145            from_revision: payload[5],
146            to_version: payload[6],
147            to_revision: payload[7],
148        }
149    }
150}
151
152/// Platform selection for a host slot.
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize))]
155#[non_exhaustive]
156pub struct HostPlatform {
157    /// Host slot index returned by the device.
158    pub host_index: HostIndex,
159    /// Raw host status byte.
160    pub status: u8,
161    /// Selected platform, or `None` when undefined.
162    pub platform_index: Option<u8>,
163    /// Source of the platform selection.
164    pub source: PlatformSource,
165    /// Automatically detected platform, if available.
166    pub auto_platform_index: Option<u8>,
167    /// Automatically matched platform descriptor, if available.
168    pub auto_descriptor_index: Option<u8>,
169}
170
171/// A host-platform change the device reported on its own, such as after the
172/// user pressed a platform key combination.
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize))]
175#[non_exhaustive]
176pub struct PlatformChange {
177    /// Host slot whose platform changed.
178    pub host_index: HostIndex,
179    /// Platform now selected, or `None` when undefined.
180    pub platform_index: Option<u8>,
181    /// Source of the new selection, or `None` for a value this crate does not
182    /// know.
183    pub source: Option<PlatformSource>,
184}
185
186impl PlatformChange {
187    /// Decodes an unsolicited `MultiPlatform` report.
188    ///
189    /// Logi Options+ 2.8 (`Feature4531MultiPlatform::onFeatureReport`) treats
190    /// every unsolicited report on this feature as a platform change, whatever
191    /// its function, and reads the host, platform and source from bytes 0-2.
192    /// It refuses a report shorter than three bytes, and so does this.
193    #[must_use]
194    pub fn decode(payload: &[u8]) -> Option<Self> {
195        let [host, platform, source, ..] = *payload else {
196            return None;
197        };
198        Some(Self {
199            host_index: HostIndex::from(host),
200            platform_index: optional_index(platform),
201            source: PlatformSource::try_from(source).ok(),
202        })
203    }
204}
205
206/// Implements the `MultiPlatform` / `0x4531` feature.
207#[derive(Clone, Feature)]
208#[creatable(id = 0x4531, version = 1)]
209pub struct MultiPlatformFeature {
210    /// The endpoint this feature talks to.
211    endpoint: FeatureEndpoint,
212}
213
214impl MultiPlatformFeature {
215    /// Retrieves feature capabilities and platform counts.
216    pub async fn get_feature_infos(&self) -> Result<MultiPlatformInfo, Hidpp20Error> {
217        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
218        Ok(MultiPlatformInfo::from_payload(&payload))
219    }
220
221    /// Retrieves a platform descriptor row.
222    pub async fn get_platform_descriptor(
223        &self,
224        descriptor_index: u8,
225    ) -> Result<PlatformDescriptor, Hidpp20Error> {
226        let payload = self
227            .endpoint
228            .call(1, [descriptor_index, 0, 0])
229            .await?
230            .extend_payload();
231        Ok(PlatformDescriptor::from_payload(&payload))
232    }
233
234    /// Retrieves the platform selected for `host`.
235    pub async fn get_host_platform(&self, host: HostIndex) -> Result<HostPlatform, Hidpp20Error> {
236        let payload = self
237            .endpoint
238            .call(2, [u8::from(host), 0, 0])
239            .await?
240            .extend_payload();
241        Ok(HostPlatform {
242            host_index: HostIndex::from(payload[0]),
243            status: payload[1],
244            platform_index: optional_index(payload[2]),
245            source: PlatformSource::try_from(payload[3])
246                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
247            auto_platform_index: optional_index(payload[4]),
248            auto_descriptor_index: optional_index(payload[5]),
249        })
250    }
251
252    /// Selects `platform_index` for `host` (function 3).
253    ///
254    /// Logi Options+ 2.8 sends `[host, platform]` and does not read the reply
255    /// (`Feature4531MultiPlatform::setHostPlatform`), so this does not either:
256    /// read the slot back with [`Self::get_host_platform`] to confirm it.
257    pub async fn set_host_platform(
258        &self,
259        host: HostIndex,
260        platform_index: u8,
261    ) -> Result<(), Hidpp20Error> {
262        self.endpoint
263            .call(3, [u8::from(host), platform_index, 0])
264            .await?;
265        Ok(())
266    }
267}
268
269fn optional_index(value: u8) -> Option<u8> {
270    (value != 0xff).then_some(value)
271}