Skip to main content

hidpp/feature/
device_information.rs

1//! Implements the `DeviceInformation` feature (ID `0x0003`) that provides some
2//! general information about the device.
3
4use num_enum::{IntoPrimitive, TryFromPrimitive};
5use openlogi_hidpp_derive::Feature;
6
7use crate::{feature::FeatureEndpoint, nibble::U4, protocol::v20::Hidpp20Error};
8
9/// Decodes a packed binary-coded decimal byte — two decimal digits, high
10/// nibble first. `None` when either nibble is outside `0..=9`, which means the
11/// device sent something that is not BCD at all.
12fn packed_bcd_u8(bcd: u8) -> Option<u8> {
13    let digit_0 = U4::from_hi(bcd).to_lo();
14    let digit_1 = U4::from_lo(bcd).to_lo();
15    (digit_0 <= 9 && digit_1 <= 9).then_some(digit_0 * 10 + digit_1)
16}
17
18/// Decodes a packed binary-coded decimal `u16` as four decimal digits, most
19/// significant byte first.
20fn packed_bcd_u16(bcd: u16) -> Option<u16> {
21    let high = u16::from(packed_bcd_u8((bcd >> 8) as u8)?);
22    let low = u16::from(packed_bcd_u8((bcd & 0xff) as u8)?);
23    Some(high * 100 + low)
24}
25
26/// Implements the `DeviceInformation` / `0x0003` feature.
27#[derive(Clone, Feature)]
28#[creatable(id = 0x0003, version = 0)]
29pub struct DeviceInformationFeature {
30    /// The endpoint this feature talks to.
31    endpoint: FeatureEndpoint,
32}
33
34impl DeviceInformationFeature {
35    /// Retrieves general information about the device and its capabilities.
36    pub async fn get_device_info(&self) -> Result<DeviceInformation, Hidpp20Error> {
37        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
38
39        Ok(DeviceInformation {
40            entity_count: payload[0],
41            unit_id: [payload[1], payload[2], payload[3], payload[4]],
42            transport: DeviceTransport::from_bits_retain(payload[6]),
43            model_id: [
44                u16::from_be_bytes([payload[7], payload[8]]),
45                u16::from_be_bytes([payload[9], payload[10]]),
46                u16::from_be_bytes([payload[11], payload[12]]),
47            ],
48            extended_model_id: payload[13],
49            capabilities: DeviceInformationCapabilities::from(payload[14]),
50        })
51    }
52
53    /// Retrieves information about the firmware of a specific entity,
54    /// identified by its index bound by the value in
55    /// [`DeviceInformation::entity_count`].
56    pub async fn get_fw_info(
57        &self,
58        entity_index: u8,
59    ) -> Result<DeviceEntityFirmwareInfo, Hidpp20Error> {
60        let payload = self
61            .endpoint
62            .call(1, [entity_index, 0x00, 0x00])
63            .await?
64            .extend_payload();
65
66        Ok(DeviceEntityFirmwareInfo {
67            entity_type: DeviceEntityType::try_from(payload[0])
68                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
69            firmware_prefix: String::from_utf8(payload[1..=3].to_vec())
70                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
71            firmware_number: packed_bcd_u8(payload[4]).ok_or(Hidpp20Error::UnsupportedResponse)?,
72            revision: packed_bcd_u8(payload[5]).ok_or(Hidpp20Error::UnsupportedResponse)?,
73            build: packed_bcd_u16(u16::from_be_bytes([payload[6], payload[7]]))
74                .ok_or(Hidpp20Error::UnsupportedResponse)?,
75            active: payload[8] & 1 != 0,
76            transport_pid: u16::from_be_bytes([payload[9], payload[10]]),
77            extra_version: [
78                payload[11],
79                payload[12],
80                payload[13],
81                payload[14],
82                payload[15],
83            ],
84        })
85    }
86
87    /// Retrieves the serial number of the device.
88    ///
89    /// This function was added in feature version 4 and will likely result in
90    /// an [`v20::ErrorType::InvalidFunctionId`](crate::protocol::v20::ErrorType::InvalidFunctionId)
91    /// error for older versions, so
92    /// [`DeviceInformationCapabilities::serial_number`] should be verified
93    /// before calling.
94    pub async fn get_serial_number(&self) -> Result<String, Hidpp20Error> {
95        let payload = self.endpoint.call(2, [0; 3]).await?.extend_payload();
96
97        String::from_utf8(payload[..12].to_vec()).map_err(|_| Hidpp20Error::UnsupportedResponse)
98    }
99}
100
101/// Represents information about the device as reported by
102/// [`DeviceInformationFeature::get_device_info`].
103#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
104#[non_exhaustive]
105pub struct DeviceInformation {
106    /// The amount of entities in the device from which version information can
107    /// be retrieved using [`DeviceInformationFeature::get_fw_info`].
108    pub entity_count: u8,
109
110    /// A 4-byte random value serving as a unique identifier (among all devices
111    /// with the same [`Self::model_id`]) for the unit.
112    ///
113    /// This field was added in feature version 1 and will always be `0` for
114    /// older versions.
115    pub unit_id: [u8; 4],
116
117    /// A bitfield about which transport protocols the device supports.
118    ///
119    /// This field was added in feature version 1 and will always be `0` for
120    /// older versions.
121    pub transport: DeviceTransport,
122
123    /// A 6-byte array serving as the identifier for the device model.
124    ///
125    /// This array will consist of the application PIDs of the different
126    /// transport protocols supported by the device, as stated in
127    /// [`Self::transport`].
128    /// The 16-bit PID for every supported transport protocol will be appended
129    /// into this array, limiting the total amount of supported transport
130    /// protocols to three.
131    ///
132    /// This field was added in feature version 1 and will always be `0` for
133    /// older versions.
134    pub model_id: [u16; 3],
135
136    /// An 8-bit value representing an additional configurable attribute for a
137    /// given [`Self::model_id`], set on the production line. This could be the
138    /// color of the device.
139    ///
140    /// This field was added in feature version 2 and will always be `0` for
141    /// older versions.
142    pub extended_model_id: u8,
143
144    /// Additional capability flags of this feature.
145    ///
146    /// This field was added in feature version 4 together with the serial
147    /// number retrieval function. All capabilities will be flagged as
148    /// unsupported for older versions.
149    pub capabilities: DeviceInformationCapabilities,
150}
151
152bitflags::bitflags! {
153    /// Represents the bitfield stating which transport protocols a device
154    /// supports.
155    ///
156    /// One given device can only support up to three transport protocols at a
157    /// time.
158    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
159    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
160    pub struct DeviceTransport: u8 {
161        /// The device supports USB.
162        const USB = 1 << 3;
163
164        /// The device supports eQuad, the protocol used by the Unifying
165        /// Receiver.
166        const E_QUAD = 1 << 2;
167
168        /// The device supports Bluetooth Low Energy as used by the Bolt
169        /// Receiver.
170        const BTLE = 1 << 1;
171
172        /// The device supports Bluetooth.
173        const BLUETOOTH = 1 << 0;
174    }
175}
176
177/// Represents the bitfield stating which additional capabilities this feature
178/// supports.
179#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize))]
181#[non_exhaustive]
182pub struct DeviceInformationCapabilities {
183    /// Whether serial number retrieval is supported.
184    ///
185    /// This field was added in feature version 4 and will always be `false` for
186    /// older versions.
187    pub serial_number: bool,
188}
189
190impl From<u8> for DeviceInformationCapabilities {
191    fn from(value: u8) -> Self {
192        Self {
193            serial_number: value & 1 != 0,
194        }
195    }
196}
197
198/// Represents information about the firmware of a specific device entity as
199/// obtained via [`DeviceInformationFeature::get_fw_info`].
200#[derive(Clone, PartialEq, Eq, Hash, Debug)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize))]
202#[non_exhaustive]
203pub struct DeviceEntityFirmwareInfo {
204    /// The type of the described entity.
205    pub entity_type: DeviceEntityType,
206
207    /// A 3-letter prefix for the firmware name.
208    pub firmware_prefix: String,
209
210    /// The firmware number.
211    ///
212    /// This is represented in packed BCD format in the protocol itself, but
213    /// decoding is handled by this implementation automatically.
214    pub firmware_number: u8,
215
216    /// The firmware revision.
217    ///
218    /// This is represented in packed BCD format in the protocol itself, but
219    /// decoding is handled by this implementation automatically.
220    pub revision: u8,
221
222    /// The firmware build.
223    ///
224    /// This is represented in packed BCD format in the protocol itself, but
225    /// decoding is handled by this implementation automatically.
226    pub build: u16,
227
228    /// Whether the entity is the responding and active one.
229    ///
230    /// Exactly one entity will be active at any given time.
231    pub active: bool,
232
233    /// The transport protocol PID.
234    ///
235    /// If this entity is the active one (see [`Self::active`]), this will be
236    /// set to the actual PID. If it is not, this field COULD be all-zero.
237    pub transport_pid: u16,
238
239    /// Optional extra versioning information.
240    pub extra_version: [u8; 5],
241}
242
243/// Represents the type of a device entity.
244#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
245#[cfg_attr(feature = "serde", derive(serde::Serialize))]
246#[non_exhaustive]
247#[repr(u8)]
248pub enum DeviceEntityType {
249    /// Main application firmware entity.
250    MainApplication = 0,
251    /// Bootloader firmware entity.
252    Bootloader = 1,
253    /// Hardware entity.
254    Hardware = 2,
255    /// Touchpad firmware/entity.
256    Touchpad = 3,
257    /// Optical sensor entity.
258    OpticalSensor = 4,
259    /// Bluetooth SoftDevice entity.
260    Softdevice = 5,
261    /// RF companion MCU entity.
262    RfCompanionMcu = 6,
263    /// Factory application firmware entity.
264    FactoryApplication = 7,
265    /// RGB custom effect entity.
266    RgbCustomEffect = 8,
267    /// Motor drive entity.
268    MotorDrive = 9,
269}