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}