use openlogi_hidpp_derive::Feature;
use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
#[derive(Clone, Feature)]
#[creatable(id = 0x2201, version = 0)]
pub struct AdjustableDpiFeature {
endpoint: FeatureEndpoint,
}
impl AdjustableDpiFeature {
pub async fn get_sensor_count(&self) -> Result<u8, Hidpp20Error> {
Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
}
pub async fn get_sensor_dpi_list(&self, sensor_index: u8) -> Result<Vec<u16>, Hidpp20Error> {
let payload = self
.endpoint
.call(1, [sensor_index, 0x00, 0x00])
.await?
.extend_payload();
if payload[0] != sensor_index {
return Err(Hidpp20Error::UnsupportedResponse);
}
parse_dpi_list_payload(&payload[1..])
}
pub async fn get_sensor_dpi(&self, sensor_index: u8) -> Result<u16, Hidpp20Error> {
let payload = self
.endpoint
.call(2, [sensor_index, 0x00, 0x00])
.await?
.extend_payload();
Ok(u16::from_be_bytes([payload[1], payload[2]]))
}
pub async fn set_sensor_dpi(&self, sensor_index: u8, dpi: u16) -> Result<(), Hidpp20Error> {
let [dpi_hi, dpi_lo] = dpi.to_be_bytes();
self.endpoint
.call(3, [sensor_index, dpi_hi, dpi_lo])
.await?;
Ok(())
}
}
fn parse_dpi_list_payload(bytes: &[u8]) -> Result<Vec<u16>, Hidpp20Error> {
let mut values = Vec::new();
let mut offset = 0;
while offset + 1 < bytes.len() {
let value = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]);
if value == 0 {
break;
}
if value >> 13 == 0b111 {
let step = value & 0x1fff;
if step == 0 || offset + 3 >= bytes.len() {
return Err(Hidpp20Error::UnsupportedResponse);
}
let start = u32::from(*values.last().ok_or(Hidpp20Error::UnsupportedResponse)?);
let last = u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]]);
if u32::from(last) < start {
return Err(Hidpp20Error::UnsupportedResponse);
}
let mut next = start + u32::from(step);
while next < u32::from(last) {
values.push(u16::try_from(next).map_err(|_| Hidpp20Error::UnsupportedResponse)?);
next += u32::from(step);
}
values.push(last);
offset += 4;
} else {
values.push(value);
offset += 2;
}
}
if values.is_empty() {
return Err(Hidpp20Error::UnsupportedResponse);
}
values.sort_unstable();
values.dedup();
Ok(values)
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::parse_dpi_list_payload;
use crate::protocol::v20::Hidpp20Error;
fn answer<T, F>(
function: u8,
call: impl FnOnce(super::AdjustableDpiFeature) -> F,
reply: [u8; 16],
) -> Result<T, Hidpp20Error>
where
F: Future<Output = Result<T, Hidpp20Error>>,
{
use std::sync::Arc;
use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
use crate::feature::CreatableFeature;
use crate::nibble::U4;
use crate::protocol::v20::{Message, MessageHeader};
futures::executor::block_on(async {
let (raw, handle) = MockRawHidChannel::new();
let channel = Arc::new(channel_with_reader(raw).await);
let mut pending = Box::pin(call(super::AdjustableDpiFeature::new(channel, 3, 8)));
assert!(futures::poll!(pending.as_mut()).is_pending());
let header = MessageHeader {
device_index: 3,
feature_index: 8,
function_id: U4::from_lo(function),
software_id: U4::from_lo(1),
};
handle
.send_incoming(Message::Long(header, reply).into())
.await;
pending.await
})
}
fn dpi_list_reply(sensor: u8) -> [u8; 16] {
let mut reply = [0; 16];
reply[0] = sensor;
reply[1..5].copy_from_slice(&[0x03, 0x20, 0x06, 0x40]);
reply
}
#[test]
fn dpi_list_takes_a_reply_for_its_sensor() {
let list = answer(
1,
|dpi| async move { dpi.get_sensor_dpi_list(1).await },
dpi_list_reply(1),
);
assert_matches!(list, Ok(values) if values == [800, 1600]);
}
#[test]
fn dpi_list_refuses_a_reply_for_another_sensor() {
let list = answer(
1,
|dpi| async move { dpi.get_sensor_dpi_list(1).await },
dpi_list_reply(0),
);
assert_matches!(list, Err(Hidpp20Error::UnsupportedResponse));
}
#[test]
fn current_dpi_read_does_not_check_the_echo() {
let mut reply = [0; 16];
reply[1..3].copy_from_slice(&1600u16.to_be_bytes());
let dpi = answer(2, |dpi| async move { dpi.get_sensor_dpi(1).await }, reply);
assert_matches!(dpi, Ok(1600));
}
#[test]
fn parses_explicit_dpi_list() {
let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40, 0x00, 0x00];
assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
}
#[test]
fn expands_range_encoded_dpi_list() {
let payload = [0x01, 0x90, 0xe1, 0x90, 0x06, 0x40, 0x00, 0x00];
assert_eq!(
parse_dpi_list_payload(&payload).unwrap(),
[400, 800, 1200, 1600]
);
}
#[test]
fn sorts_and_deduplicates_values() {
let payload = [0x06, 0x40, 0x03, 0x20, 0x03, 0x20, 0x00, 0x00];
assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [800, 1600]);
}
#[test]
fn rejects_range_marker_without_previous_value() {
let payload = [0xe0, 0x32, 0x1f, 0x40, 0x00, 0x00];
assert_matches!(
parse_dpi_list_payload(&payload),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_range_marker_without_end_value() {
let payload = [0x01, 0x90, 0xe0, 0x32];
assert_matches!(
parse_dpi_list_payload(&payload),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_zero_step_range_marker() {
let payload = [0x01, 0x90, 0xe0, 0x00, 0x06, 0x40, 0x00, 0x00];
assert_matches!(
parse_dpi_list_payload(&payload),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_descending_range_marker() {
let payload = [0x06, 0x40, 0xe0, 0x32, 0x01, 0x90, 0x00, 0x00];
assert_matches!(
parse_dpi_list_payload(&payload),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn range_keeps_off_grid_high_endpoint() {
let payload = [0x01, 0x90, 0xe1, 0x90, 0x05, 0xdc, 0x00, 0x00];
assert_eq!(
parse_dpi_list_payload(&payload).unwrap(),
[400, 800, 1200, 1500]
);
}
#[test]
fn parses_full_list_without_terminator() {
let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40];
assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
}
#[test]
fn rejects_payload_with_no_values() {
assert_matches!(
parse_dpi_list_payload(&[0x00, 0x00]),
Err(Hidpp20Error::UnsupportedResponse)
);
}
}