use num_enum::{IntoPrimitive, TryFromPrimitive};
use thiserror::Error;
use crate::{
channel::{
AbandonedReply, ChannelError, HidppChannel, HidppMessage, LONG_REPORT_LENGTH,
SEND_RESPONSE_TIMEOUT, SHORT_REPORT_LENGTH,
},
nibble::{self, U4},
};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct MessageHeader {
pub device_index: u8,
pub feature_index: u8,
pub function_id: U4,
pub software_id: U4,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Message {
Short(MessageHeader, [u8; SHORT_REPORT_LENGTH - 4]),
Long(MessageHeader, [u8; LONG_REPORT_LENGTH - 4]),
}
impl Message {
#[must_use]
pub fn header(&self) -> MessageHeader {
match *self {
Message::Short(header, _) | Message::Long(header, _) => header,
}
}
#[must_use]
pub fn extend_payload(&self) -> [u8; LONG_REPORT_LENGTH - 4] {
match *self {
Message::Short(_, payload) => {
let mut data = [0; LONG_REPORT_LENGTH - 4];
data[..SHORT_REPORT_LENGTH - 4].copy_from_slice(&payload);
data
}
Message::Long(_, payload) => payload,
}
}
#[must_use]
pub fn feature_error_for(&self, request: MessageHeader) -> Option<Hidpp20Error> {
if !self.is_error_for(request) {
return None;
}
Some(
ErrorType::try_from(self.extend_payload()[1])
.map_or(Hidpp20Error::UnsupportedResponse, Hidpp20Error::Feature),
)
}
fn is_error_for(&self, request: MessageHeader) -> bool {
let header = self.header();
header.device_index == request.device_index
&& header.feature_index == 0xff
&& nibble::combine(header.function_id, header.software_id) == request.feature_index
&& self.extend_payload()[0] == nibble::combine(request.function_id, request.software_id)
}
}
impl From<HidppMessage> for Message {
fn from(msg: HidppMessage) -> Self {
match msg {
HidppMessage::Short(payload) => {
let [_, _, _, rest @ ..] = payload;
Message::Short(
MessageHeader {
device_index: payload[0],
feature_index: payload[1],
function_id: U4::from_hi(payload[2]),
software_id: U4::from_lo(payload[2]),
},
rest,
)
}
HidppMessage::Long(payload) => {
let [_, _, _, rest @ ..] = payload;
Message::Long(
MessageHeader {
device_index: payload[0],
feature_index: payload[1],
function_id: U4::from_hi(payload[2]),
software_id: U4::from_lo(payload[2]),
},
rest,
)
}
}
}
}
impl From<Message> for HidppMessage {
fn from(msg: Message) -> Self {
match msg {
Message::Short(header, payload) => {
let mut data = [0u8; SHORT_REPORT_LENGTH - 1];
data[0] = header.device_index;
data[1] = header.feature_index;
data[2] = nibble::combine(header.function_id, header.software_id);
data[3..].copy_from_slice(&payload);
HidppMessage::Short(data)
}
Message::Long(header, payload) => {
let mut data = [0u8; LONG_REPORT_LENGTH - 1];
data[0] = header.device_index;
data[1] = header.feature_index;
data[2] = nibble::combine(header.function_id, header.software_id);
data[3..].copy_from_slice(&payload);
HidppMessage::Long(data)
}
}
}
}
impl HidppChannel {
pub async fn send_v20(&self, msg: Message) -> Result<Message, Hidpp20Error> {
self.send_v20_with(msg, AbandonedReply::Quarantine).await
}
pub async fn send_v20_with(
&self,
msg: Message,
abandoned: AbandonedReply,
) -> Result<Message, Hidpp20Error> {
let header = msg.header();
let response = self
.send_with(
msg.into(),
v20_response_predicate(header),
SEND_RESPONSE_TIMEOUT,
abandoned,
)
.await?;
decode_v20_response(response)
}
pub async fn send_v20_write_through(
&self,
msg: Message,
matches_payload: impl Fn(&Message) -> bool + Send + 'static,
) -> Result<Message, Hidpp20Error> {
let matches_header = v20_response_predicate(msg.header());
let response = self
.send_write_through(
msg.into(),
move |raw| {
let response = Message::from(*raw);
matches_header(raw)
&& (response.header().feature_index == 0xff || matches_payload(&response))
},
SEND_RESPONSE_TIMEOUT,
)
.await?;
decode_v20_response(response)
}
}
fn v20_response_predicate(
header: MessageHeader,
) -> impl Fn(&HidppMessage) -> bool + Send + 'static {
move |&response| {
let response = Message::from(response);
response.is_error_for(header) || response.header() == header
}
}
fn decode_v20_response(response: HidppMessage) -> Result<Message, Hidpp20Error> {
let response = Message::from(response);
if response.header().feature_index == 0xff {
let err = ErrorType::try_from(response.extend_payload()[1])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?;
return Err(Hidpp20Error::Feature(err));
}
Ok(response)
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum ErrorType {
NoError = 0,
Unknown = 1,
InvalidArgument = 2,
OutOfRange = 3,
HwError = 4,
LogitechInternal = 5,
InvalidFeatureIndex = 6,
InvalidFunctionId = 7,
Busy = 8,
Unsupported = 9,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Hidpp20Error {
#[error("the HID++ channel returned an error")]
Channel(#[from] ChannelError),
#[error("a HID++2.0 feature returned an error")]
Feature(ErrorType),
#[error("the received response from the device is (partly) unsupported")]
UnsupportedResponse,
}
#[cfg(test)]
mod tests {
use super::*;
fn request() -> MessageHeader {
MessageHeader {
device_index: 0x01,
feature_index: 0x07,
function_id: U4::from_lo(0x3),
software_id: U4::from_lo(0xa),
}
}
fn short(bytes: [u8; SHORT_REPORT_LENGTH - 1]) -> Message {
Message::from(HidppMessage::Short(bytes))
}
#[test]
fn an_error_naming_the_request_is_attributed_to_it() {
let error = short([0x01, 0xff, 0x07, 0x3a, 0x02, 0x00]);
assert!(matches!(
error.feature_error_for(request()),
Some(Hidpp20Error::Feature(ErrorType::InvalidArgument))
));
}
#[test]
fn an_error_for_another_request_is_not() {
for other in [
[0x01, 0xff, 0x07, 0x5a, 0x02, 0x00],
[0x01, 0xff, 0x08, 0x3a, 0x02, 0x00],
[0x02, 0xff, 0x07, 0x3a, 0x02, 0x00],
[0x01, 0x07, 0x3a, 0x00, 0x00, 0x00],
] {
assert!(
short(other).feature_error_for(request()).is_none(),
"{other:02x?}"
);
}
}
#[test]
fn an_unknown_error_code_is_an_unsupported_response() {
let error = short([0x01, 0xff, 0x07, 0x3a, 0x7f, 0x00]);
assert!(matches!(
error.feature_error_for(request()),
Some(Hidpp20Error::UnsupportedResponse)
));
}
}