1use num_enum::{IntoPrimitive, TryFromPrimitive};
4use thiserror::Error;
5
6use crate::{
7 channel::{
8 AbandonedReply, ChannelError, HidppChannel, HidppMessage, LONG_REPORT_LENGTH,
9 SEND_RESPONSE_TIMEOUT, SHORT_REPORT_LENGTH,
10 },
11 nibble::{self, U4},
12};
13
14#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17pub struct MessageHeader {
18 pub device_index: u8,
20
21 pub feature_index: u8,
26
27 pub function_id: U4,
29
30 pub software_id: U4,
32}
33
34#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37pub enum Message {
38 Short(MessageHeader, [u8; SHORT_REPORT_LENGTH - 4]),
40
41 Long(MessageHeader, [u8; LONG_REPORT_LENGTH - 4]),
43}
44
45impl Message {
46 #[must_use]
48 pub fn header(&self) -> MessageHeader {
49 match *self {
50 Message::Short(header, _) | Message::Long(header, _) => header,
51 }
52 }
53
54 #[must_use]
58 pub fn extend_payload(&self) -> [u8; LONG_REPORT_LENGTH - 4] {
59 match *self {
60 Message::Short(_, payload) => {
61 let mut data = [0; LONG_REPORT_LENGTH - 4];
62 data[..SHORT_REPORT_LENGTH - 4].copy_from_slice(&payload);
63 data
64 }
65 Message::Long(_, payload) => payload,
66 }
67 }
68}
69
70impl From<HidppMessage> for Message {
71 fn from(msg: HidppMessage) -> Self {
72 match msg {
73 HidppMessage::Short(payload) => {
74 let [_, _, _, rest @ ..] = payload;
75 Message::Short(
76 MessageHeader {
77 device_index: payload[0],
78 feature_index: payload[1],
79 function_id: U4::from_hi(payload[2]),
80 software_id: U4::from_lo(payload[2]),
81 },
82 rest,
83 )
84 }
85 HidppMessage::Long(payload) => {
86 let [_, _, _, rest @ ..] = payload;
87 Message::Long(
88 MessageHeader {
89 device_index: payload[0],
90 feature_index: payload[1],
91 function_id: U4::from_hi(payload[2]),
92 software_id: U4::from_lo(payload[2]),
93 },
94 rest,
95 )
96 }
97 }
98 }
99}
100
101impl From<Message> for HidppMessage {
102 fn from(msg: Message) -> Self {
103 match msg {
104 Message::Short(header, payload) => {
105 let mut data = [0u8; SHORT_REPORT_LENGTH - 1];
106 data[0] = header.device_index;
107 data[1] = header.feature_index;
108 data[2] = nibble::combine(header.function_id, header.software_id);
109 data[3..].copy_from_slice(&payload);
110
111 HidppMessage::Short(data)
112 }
113 Message::Long(header, payload) => {
114 let mut data = [0u8; LONG_REPORT_LENGTH - 1];
115 data[0] = header.device_index;
116 data[1] = header.feature_index;
117 data[2] = nibble::combine(header.function_id, header.software_id);
118 data[3..].copy_from_slice(&payload);
119
120 HidppMessage::Long(data)
121 }
122 }
123 }
124}
125
126impl HidppChannel {
127 pub async fn send_v20(&self, msg: Message) -> Result<Message, Hidpp20Error> {
133 self.send_v20_with(msg, AbandonedReply::Quarantine).await
134 }
135
136 pub async fn send_v20_with(
139 &self,
140 msg: Message,
141 abandoned: AbandonedReply,
142 ) -> Result<Message, Hidpp20Error> {
143 let header = msg.header();
144 let response = self
145 .send_with(
146 msg.into(),
147 v20_response_predicate(header),
148 SEND_RESPONSE_TIMEOUT,
149 abandoned,
150 )
151 .await?;
152 decode_v20_response(response)
153 }
154
155 pub async fn send_v20_write_through(
165 &self,
166 msg: Message,
167 matches_payload: impl Fn(&Message) -> bool + Send + 'static,
168 ) -> Result<Message, Hidpp20Error> {
169 let matches_header = v20_response_predicate(msg.header());
170 let response = self
171 .send_write_through(
172 msg.into(),
173 move |raw| {
174 let response = Message::from(*raw);
175 matches_header(raw)
176 && (response.header().feature_index == 0xff || matches_payload(&response))
177 },
178 SEND_RESPONSE_TIMEOUT,
179 )
180 .await?;
181 decode_v20_response(response)
182 }
183}
184
185fn v20_response_predicate(
186 header: MessageHeader,
187) -> impl Fn(&HidppMessage) -> bool + Send + 'static {
188 move |&response| {
189 let response = Message::from(response);
190 let response_header = response.header();
191
192 let is_error = response_header.device_index == header.device_index
195 && response_header.feature_index == 0xff
196 && nibble::combine(response_header.function_id, response_header.software_id)
197 == header.feature_index
198 && response.extend_payload()[0]
199 == nibble::combine(header.function_id, header.software_id);
200
201 is_error || response_header == header
202 }
203}
204
205fn decode_v20_response(response: HidppMessage) -> Result<Message, Hidpp20Error> {
206 let response = Message::from(response);
207 if response.header().feature_index == 0xff {
208 let err = ErrorType::try_from(response.extend_payload()[1])
209 .map_err(|_| Hidpp20Error::UnsupportedResponse)?;
210
211 return Err(Hidpp20Error::Feature(err));
212 }
213
214 Ok(response)
215}
216
217#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize))]
221#[non_exhaustive]
222#[repr(u8)]
223pub enum ErrorType {
224 NoError = 0,
226 Unknown = 1,
228 InvalidArgument = 2,
230 OutOfRange = 3,
232 HwError = 4,
234 LogitechInternal = 5,
236 InvalidFeatureIndex = 6,
238 InvalidFunctionId = 7,
240 Busy = 8,
242 Unsupported = 9,
244}
245
246#[derive(Debug, Error)]
248#[non_exhaustive]
249pub enum Hidpp20Error {
250 #[error("the HID++ channel returned an error")]
253 Channel(#[from] ChannelError),
254
255 #[error("a HID++2.0 feature returned an error")]
258 Feature(ErrorType),
259
260 #[error("the received response from the device is (partly) unsupported")]
262 UnsupportedResponse,
263}