use std::assert_matches;
use std::sync::{Arc, Mutex};
use super::*;
use crate::channel::scripted::{ScriptedRawHidChannel, feature_error, scripted_channel};
fn hz(rates: &[u16]) -> Vec<ReportRate> {
rates.iter().copied().map(ReportRate::new).collect()
}
#[test]
fn legacy_intervals_round_to_the_nearest_hertz() {
let rates: Vec<u16> = legacy_intervals_ms()
.map(|ms| legacy_rate(ms).into())
.collect();
assert_eq!(rates, [1000, 500, 333, 250, 200, 167, 143, 125]);
for interval_ms in legacy_intervals_ms() {
assert_eq!(legacy_interval(legacy_rate(interval_ms)), Some(interval_ms));
}
assert_eq!(legacy_interval(ReportRate::new(2000)), None);
assert_eq!(legacy_interval(ReportRate::new(334)), None);
}
#[test]
fn extended_settings_double_from_125_hz() {
let rates: Vec<u16> = EXTENDED_RATES
.map(|setting| extended_rate(setting).into())
.to_vec();
assert_eq!(rates, [125, 250, 500, 1000, 2000, 4000, 8000]);
for setting in EXTENDED_RATES {
assert_eq!(extended_setting(extended_rate(setting)), Some(setting));
}
assert_eq!(extended_setting(ReportRate::new(333)), None);
}
#[test]
fn rate_lists_read_ascending() {
let legacy = ReportRateList::from_bits_retain(0b1000_1011);
assert_eq!(legacy_rates(legacy), hz(&[125, 250, 500, 1000]));
let extended = ExtendedReportRateList::from_bits_retain(0b1100_1001);
assert_eq!(extended_rates(extended), hz(&[125, 1000, 8000]));
}
#[test]
fn the_connection_follows_the_hidpp_index() {
assert_eq!(connection_for(DIRECT_DEVICE_INDEX), ConnectionType::Wired);
assert_eq!(connection_for(1), ConnectionType::GamingWireless);
assert_eq!(connection_for(6), ConnectionType::GamingWireless);
}
const EXTENDED_INDEX: u8 = 0x0f;
const LEGACY_INDEX: u8 = 0x0e;
const BUSY: u8 = 0x08;
#[derive(Clone, Copy, PartialEq, Eq)]
enum RateFeature {
Extended,
Legacy,
Neither,
}
struct Mouse {
feature: RateFeature,
mask: u8,
extended_rates: [u8; 2],
connection: usize,
interval_ms: u8,
takes_writes: bool,
fails_read_back: bool,
writes: Vec<u8>,
rate_reads: Vec<u8>,
}
impl Mouse {
fn extended() -> Self {
Self {
feature: RateFeature::Extended,
mask: 0b0111_1111,
extended_rates: [3, 3],
connection: 0,
interval_ms: 1,
takes_writes: true,
fails_read_back: false,
writes: Vec::new(),
rate_reads: Vec::new(),
}
}
fn legacy() -> Self {
Self {
feature: RateFeature::Legacy,
mask: 0b1000_1011,
interval_ms: 8,
..Self::extended()
}
}
fn respond(&mut self, request: &[u8]) -> Option<Vec<u8>> {
if request.len() < 7 || !matches!(request[0], 0x10 | 0x11) {
return None;
}
let function = request[3] >> 4;
let mut payload = [0u8; 16];
match (request[2], function) {
(0x00, 0x01) => payload[0] = 4,
(0x00, 0x00) => {
payload[0] = match u16::from_be_bytes([request[4], request[5]]) {
0x8061 if self.feature == RateFeature::Extended => EXTENDED_INDEX,
0x8060 if self.feature == RateFeature::Legacy => LEGACY_INDEX,
_ => 0,
};
}
(EXTENDED_INDEX, 0x00 | 0x01) => payload[1] = self.mask,
(EXTENDED_INDEX, 0x02) => {
self.rate_reads.push(request[4]);
if self.fails_read_back && !self.writes.is_empty() {
return Some(feature_error(request, BUSY));
}
payload[0] = *self.extended_rates.get(usize::from(request[4]))?;
}
(EXTENDED_INDEX, 0x03) => {
self.writes.push(request[4]);
if self.takes_writes {
self.extended_rates[self.connection] = request[4];
}
}
(LEGACY_INDEX, 0x00) => payload[0] = self.mask,
(LEGACY_INDEX, 0x01) => {
if self.fails_read_back && !self.writes.is_empty() {
return Some(feature_error(request, BUSY));
}
payload[0] = self.interval_ms;
}
(LEGACY_INDEX, 0x02) => {
self.writes.push(request[4]);
if self.takes_writes {
self.interval_ms = request[4];
}
}
_ => return None,
}
let mut response = vec![0u8; 20];
response[0] = 0x11;
response[1..4].copy_from_slice(&request[1..4]);
response[4..].copy_from_slice(&payload);
Some(response)
}
}
async fn mouse_channel(mouse: &Arc<Mutex<Mouse>>, route: DeviceRoute) -> SharedChannel {
let mouse = Arc::clone(mouse);
let (raw, _handle) = ScriptedRawHidChannel::with_dynamic_responder(move |request| {
mouse.lock().expect("mouse lock").respond(request)
});
SharedChannel::new(scripted_channel(raw).await, route)
}
fn direct() -> DeviceRoute {
DeviceRoute::Direct {
vendor_id: 0x046d,
product_id: 0xc547,
}
}
fn behind_receiver() -> DeviceRoute {
DeviceRoute::Unifying {
receiver_uid: "LIGHTSPEED".to_string(),
slot: 1,
}
}
#[tokio::test]
async fn reads_the_wireless_rate_of_a_mouse_behind_a_receiver() {
let mouse = Arc::new(Mutex::new(Mouse {
connection: 1,
extended_rates: [3, 5],
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, behind_receiver()).await;
let info = get_report_rate_info_on(&shared)
.await
.expect("the mouse answers");
assert_eq!(info.current, ReportRate::new(4000));
assert_eq!(info.supported, hz(&[125, 250, 500, 1000, 2000, 4000, 8000]));
assert_eq!(mouse.lock().expect("mouse lock").rate_reads, [1]);
}
#[tokio::test]
async fn reads_the_wired_rate_of_a_mouse_on_its_cable() {
let mouse = Arc::new(Mutex::new(Mouse {
extended_rates: [6, 3],
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, direct()).await;
let info = get_report_rate_info_on(&shared)
.await
.expect("the mouse answers");
assert_eq!(info.current, ReportRate::new(8000));
assert_eq!(mouse.lock().expect("mouse lock").rate_reads, [0]);
}
#[tokio::test]
async fn writes_a_listed_rate_and_reads_it_back() {
let mouse = Arc::new(Mutex::new(Mouse {
connection: 1,
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, behind_receiver()).await;
let info = set_report_rate_on(&shared, ReportRate::new(2000))
.await
.expect("the mouse takes the write");
assert_eq!(info.current, ReportRate::new(2000));
let mouse = mouse.lock().expect("mouse lock");
assert_eq!(mouse.writes, [4]);
assert_eq!(mouse.extended_rates, [3, 4]);
assert_eq!(mouse.rate_reads, [1, 1]);
}
#[tokio::test]
async fn refuses_a_rate_the_device_does_not_list() {
let mouse = Arc::new(Mutex::new(Mouse {
mask: 0b0000_1111,
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, direct()).await;
let error = set_report_rate_on(&shared, ReportRate::new(8000))
.await
.expect_err("8 kHz is not listed");
assert_eq!(
error,
WriteError::ReportRateUnsupported {
requested: ReportRate::new(8000)
}
);
assert!(mouse.lock().expect("mouse lock").writes.is_empty());
}
#[tokio::test]
async fn leaves_a_device_already_at_the_rate_alone() {
let mouse = Arc::new(Mutex::new(Mouse::extended()));
let shared = mouse_channel(&mouse, direct()).await;
let info = set_report_rate_on(&shared, ReportRate::new(1000))
.await
.expect("nothing to do");
assert_eq!(info.current, ReportRate::new(1000));
assert!(mouse.lock().expect("mouse lock").writes.is_empty());
}
#[tokio::test]
async fn a_rate_the_device_does_not_keep_is_not_applied() {
let mouse = Arc::new(Mutex::new(Mouse {
takes_writes: false,
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, direct()).await;
let error = set_report_rate_on(&shared, ReportRate::new(500))
.await
.expect_err("the mouse keeps 1 kHz");
assert_eq!(
error,
WriteError::ReportRateNotApplied {
requested: ReportRate::new(500),
actual: ReportRate::new(1000),
}
);
assert_eq!(mouse.lock().expect("mouse lock").writes, [2]);
}
#[tokio::test]
async fn an_acknowledged_write_whose_read_back_fails_counts_as_written() {
let mouse = Arc::new(Mutex::new(Mouse {
fails_read_back: true,
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, direct()).await;
let info = set_report_rate_on(&shared, ReportRate::new(250))
.await
.expect("the write was acknowledged");
assert_eq!(info.current, ReportRate::new(250));
assert_eq!(mouse.lock().expect("mouse lock").writes, [1]);
}
#[tokio::test]
async fn falls_back_to_the_legacy_feature_in_milliseconds() {
let mouse = Arc::new(Mutex::new(Mouse::legacy()));
let shared = mouse_channel(&mouse, direct()).await;
let info = get_report_rate_info_on(&shared)
.await
.expect("the mouse answers");
assert_eq!(info.current, ReportRate::new(125));
assert_eq!(info.supported, hz(&[125, 250, 500, 1000]));
let info = set_report_rate_on(&shared, ReportRate::new(500))
.await
.expect("the mouse takes the write");
assert_eq!(info.current, ReportRate::new(500));
let mouse = mouse.lock().expect("mouse lock");
assert_eq!(mouse.writes, [2]);
assert_eq!(mouse.interval_ms, 2);
assert!(mouse.rate_reads.is_empty(), "0x8061 was never asked");
}
#[tokio::test]
async fn writes_a_legacy_interval_whose_rate_is_rounded() {
let mouse = Arc::new(Mutex::new(Mouse {
mask: 0b1111_1111,
..Mouse::legacy()
}));
let shared = mouse_channel(&mouse, direct()).await;
let info = set_report_rate_on(&shared, ReportRate::new(333))
.await
.expect("3 ms is listed");
assert_eq!(info.current, ReportRate::new(333));
assert_eq!(mouse.lock().expect("mouse lock").writes, [3]);
}
#[tokio::test]
async fn a_device_without_either_feature_is_unsupported() {
let mouse = Arc::new(Mutex::new(Mouse {
feature: RateFeature::Neither,
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, direct()).await;
let error = get_report_rate_info_on(&shared)
.await
.expect_err("no report-rate feature");
assert_matches!(
error,
WriteError::FeatureUnsupported {
feature_hex: 0x8060
}
);
}
#[tokio::test]
async fn a_device_that_lists_no_rate_answered_nothing_usable() {
let mouse = Arc::new(Mutex::new(Mouse {
mask: 0,
..Mouse::extended()
}));
let shared = mouse_channel(&mouse, direct()).await;
let error = get_report_rate_info_on(&shared)
.await
.expect_err("an empty list");
assert_matches!(
error,
WriteError::UnsupportedResponse {
operation: HidppOperation::ReadReportRate,
feature_hex: 0x8061,
}
);
}