use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
device::Device,
feature::CreatableFeature,
feature::hires_wheel::{
HiResWheelFeature, WheelEventTarget, WheelMode as HidppWheelMode,
WheelResolution as HidppWheelResolution,
},
};
pub use openlogi_core::config::ScrollResolution;
pub use openlogi_core::hid::scroll::{ScrollReportingTarget, ScrollWheelMode};
use tracing::debug;
use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
use crate::SharedChannel;
use crate::backend::HidBackend;
use crate::channel::route::DeviceRoute;
pub async fn get_scroll_wheel_mode(
backend: &dyn HidBackend,
route: &DeviceRoute,
) -> Result<ScrollWheelMode, WriteError> {
let index = route.device_index();
with_route(backend, route, move |channel| async move {
get_scroll_wheel_mode_on_channel(&channel, index).await
})
.await
}
pub async fn get_scroll_wheel_mode_on(
shared: &SharedChannel,
) -> Result<ScrollWheelMode, WriteError> {
get_scroll_wheel_mode_on_channel(shared.channel(), shared.device_index()).await
}
async fn get_scroll_wheel_mode_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
) -> Result<ScrollWheelMode, WriteError> {
let mut device = open_device(channel, index).await?;
let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
read_mode(&feature).await
}
pub async fn set_scroll_resolution(
backend: &dyn HidBackend,
route: &DeviceRoute,
resolution: ScrollResolution,
) -> Result<ScrollWheelMode, WriteError> {
let index = route.device_index();
with_route(backend, route, move |channel| async move {
change_wheel_mode_on_channel(&channel, index, Some(resolution), None, false).await
})
.await
}
pub async fn set_scroll_resolution_on(
shared: &SharedChannel,
resolution: ScrollResolution,
) -> Result<ScrollWheelMode, WriteError> {
change_wheel_mode_on_channel(
shared.channel(),
shared.device_index(),
Some(resolution),
None,
false,
)
.await
}
pub async fn set_scroll_wheel_mode(
backend: &dyn HidBackend,
route: &DeviceRoute,
resolution: ScrollResolution,
inverted: bool,
) -> Result<ScrollWheelMode, WriteError> {
let index = route.device_index();
with_route(backend, route, move |channel| async move {
change_wheel_mode_on_channel(&channel, index, Some(resolution), Some(inverted), true).await
})
.await
}
pub async fn set_scroll_wheel_mode_on(
shared: &SharedChannel,
resolution: ScrollResolution,
inverted: bool,
) -> Result<ScrollWheelMode, WriteError> {
change_wheel_mode_on_channel(
shared.channel(),
shared.device_index(),
Some(resolution),
Some(inverted),
true,
)
.await
}
pub async fn set_scroll_inversion(
backend: &dyn HidBackend,
route: &DeviceRoute,
inverted: bool,
) -> Result<(), WriteError> {
let index = route.device_index();
with_route(backend, route, move |channel| async move {
change_wheel_mode_on_channel(&channel, index, None, Some(inverted), true)
.await
.map(|_| ())
})
.await
}
pub async fn set_scroll_inversion_on(
shared: &SharedChannel,
inverted: bool,
) -> Result<(), WriteError> {
change_wheel_mode_on_channel(
shared.channel(),
shared.device_index(),
None,
Some(inverted),
true,
)
.await
.map(|_| ())
}
async fn change_wheel_mode_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
resolution: Option<ScrollResolution>,
inverted: Option<bool>,
require_invert_support: bool,
) -> Result<ScrollWheelMode, WriteError> {
let mut device = open_device(channel, index).await?;
let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
if require_invert_support {
let capabilities = feature.get_wheel_capabilities().await.map_err(|error| {
classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
})?;
if !capabilities.has_invert {
return Err(WriteError::FeatureUnsupported {
feature_hex: HiResWheelFeature::ID,
});
}
}
let device_mode = read_device_mode(&feature).await?;
let current = mode_from_hidpp(device_mode)?;
let desired = desired_mode(current, resolution, inverted);
if current == desired {
debug!(index, ?desired, "wheel mode already set; skipping");
return Ok(current);
}
let mut request = device_mode;
request.target = target_to_hidpp(desired.target);
request.resolution = resolution_to_hidpp(desired.resolution);
request.inverted = desired.inverted;
let written = feature.set_wheel_mode(request).await.map_err(|error| {
classify_hidpp_error(error, HidppOperation::WriteWheelMode, HiResWheelFeature::ID)
})?;
validate_applied(mode_from_hidpp(written)?, desired)?;
let read_back = read_mode(&feature).await?;
validate_applied(read_back, desired)?;
debug!(index, ?read_back, "wheel mode written and verified");
Ok(read_back)
}
async fn open_device(channel: &Arc<HidppChannel>, index: u8) -> Result<Device, WriteError> {
Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })
}
async fn read_mode(feature: &HiResWheelFeature) -> Result<ScrollWheelMode, WriteError> {
mode_from_hidpp(read_device_mode(feature).await?)
}
async fn read_device_mode(feature: &HiResWheelFeature) -> Result<HidppWheelMode, WriteError> {
feature.get_wheel_mode().await.map_err(|error| {
classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
})
}
fn desired_mode(
current: ScrollWheelMode,
resolution: Option<ScrollResolution>,
inverted: Option<bool>,
) -> ScrollWheelMode {
ScrollWheelMode {
resolution: resolution.unwrap_or(current.resolution),
inverted: inverted.unwrap_or(current.inverted),
target: if resolution.is_some() || inverted == Some(true) {
ScrollReportingTarget::Native
} else {
current.target
},
}
}
fn validate_applied(actual: ScrollWheelMode, desired: ScrollWheelMode) -> Result<(), WriteError> {
if actual == desired {
Ok(())
} else {
Err(WriteError::UnsupportedResponse {
operation: HidppOperation::WriteWheelMode,
feature_hex: HiResWheelFeature::ID,
})
}
}
fn resolution_from_hidpp(resolution: HidppWheelResolution) -> Result<ScrollResolution, WriteError> {
Ok(match resolution {
HidppWheelResolution::Low => ScrollResolution::Low,
HidppWheelResolution::High => ScrollResolution::High,
_ => return Err(unsupported_read_response()),
})
}
fn resolution_to_hidpp(resolution: ScrollResolution) -> HidppWheelResolution {
match resolution {
ScrollResolution::Low => HidppWheelResolution::Low,
ScrollResolution::High => HidppWheelResolution::High,
}
}
fn target_from_hidpp(target: WheelEventTarget) -> Result<ScrollReportingTarget, WriteError> {
match target {
WheelEventTarget::Native => Ok(ScrollReportingTarget::Native),
WheelEventTarget::Diverted => Ok(ScrollReportingTarget::Diverted),
_ => Err(unsupported_read_response()),
}
}
fn target_to_hidpp(target: ScrollReportingTarget) -> WheelEventTarget {
match target {
ScrollReportingTarget::Native => WheelEventTarget::Native,
ScrollReportingTarget::Diverted => WheelEventTarget::Diverted,
}
}
fn mode_from_hidpp(mode: HidppWheelMode) -> Result<ScrollWheelMode, WriteError> {
Ok(ScrollWheelMode {
resolution: resolution_from_hidpp(mode.resolution)?,
inverted: mode.inverted,
target: target_from_hidpp(mode.target)?,
})
}
fn unsupported_read_response() -> WriteError {
WriteError::UnsupportedResponse {
operation: HidppOperation::ReadWheelMode,
feature_hex: HiResWheelFeature::ID,
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use crate::channel::scripted::{ScriptedRawHidChannel, scripted_channel};
const HIRES_WHEEL_INDEX: u8 = 0x05;
fn stateful_wheel(
initial: [u8; 3],
) -> (ScriptedRawHidChannel, crate::replay::ReplayChannelHandle) {
let state = Mutex::new(initial);
ScriptedRawHidChannel::with_dynamic_responder(move |request| {
if request.len() < 7 || request[0] != 0x10 {
return None;
}
let mut payload = [0u8; 3];
match (request[2], request[3] >> 4) {
(0x00, 0x01) => payload[0] = 4,
(0x00, 0x00) => {
if u16::from_be_bytes([request[4], request[5]]) == HiResWheelFeature::ID {
payload[0] = HIRES_WHEEL_INDEX;
}
}
(HIRES_WHEEL_INDEX, 0x00) => payload = [8, 0b1100, 24],
(HIRES_WHEEL_INDEX, 0x01) => payload = *state.lock().expect("wheel state"),
(HIRES_WHEEL_INDEX, 0x02) => {
let mut current = state.lock().expect("wheel state");
current.copy_from_slice(&request[4..7]);
payload = *current;
}
_ => return None,
}
let mut response = request[..4].to_vec();
response.extend_from_slice(&payload);
Some(response)
})
}
fn mode_writes(handle: &crate::replay::ReplayChannelHandle) -> Vec<[u8; 3]> {
handle
.written_reports()
.into_iter()
.filter(|report| {
report.len() >= 7 && report[2] == HIRES_WHEEL_INDEX && report[3] >> 4 == 0x02
})
.map(|report| [report[4], report[5], report[6]])
.collect()
}
#[tokio::test]
async fn resolution_write_preserves_firmware_mode_extension_fields() -> Result<(), WriteError> {
let (raw, handle) = stateful_wheel([0b1000, 0x05, 0x07]);
let shared = SharedChannel::new(
scripted_channel(raw).await,
DeviceRoute::Direct {
vendor_id: 0x046d,
product_id: 0xb035,
},
);
let applied = set_scroll_resolution_on(&shared, ScrollResolution::High).await?;
assert_eq!(applied, native(ScrollResolution::High, false));
assert_eq!(mode_writes(&handle), vec![[0b1010, 0x05, 0x07]]);
Ok(())
}
#[tokio::test]
async fn inversion_write_preserves_firmware_mode_extension_fields() -> Result<(), WriteError> {
let (raw, handle) = stateful_wheel([0b1010, 0x02, 0x09]);
let shared = SharedChannel::new(
scripted_channel(raw).await,
DeviceRoute::Direct {
vendor_id: 0x046d,
product_id: 0xb035,
},
);
set_scroll_inversion_on(&shared, true).await?;
assert_eq!(mode_writes(&handle), vec![[0b1110, 0x02, 0x09]]);
Ok(())
}
fn native(resolution: ScrollResolution, inverted: bool) -> ScrollWheelMode {
ScrollWheelMode {
resolution,
inverted,
target: ScrollReportingTarget::Native,
}
}
#[test]
fn mode_value_conversions_preserve_known_wire_values() -> Result<(), WriteError> {
assert_eq!(
resolution_from_hidpp(HidppWheelResolution::Low)?,
ScrollResolution::Low
);
assert_eq!(
resolution_from_hidpp(HidppWheelResolution::High)?,
ScrollResolution::High
);
assert_eq!(
target_from_hidpp(WheelEventTarget::Native)?,
ScrollReportingTarget::Native
);
assert_eq!(
target_from_hidpp(WheelEventTarget::Diverted)?,
ScrollReportingTarget::Diverted
);
assert_eq!(
target_to_hidpp(ScrollReportingTarget::Native),
WheelEventTarget::Native
);
assert_eq!(
target_to_hidpp(ScrollReportingTarget::Diverted),
WheelEventTarget::Diverted
);
Ok(())
}
#[test]
fn resolution_only_preserves_inversion_and_targets_native() {
let current = ScrollWheelMode {
resolution: ScrollResolution::High,
inverted: true,
target: ScrollReportingTarget::Diverted,
};
assert_eq!(
desired_mode(current, Some(ScrollResolution::Low), None),
native(ScrollResolution::Low, true)
);
}
#[test]
fn inversion_only_preserves_resolution_and_targets_native() {
let current = ScrollWheelMode {
resolution: ScrollResolution::Low,
inverted: false,
target: ScrollReportingTarget::Diverted,
};
assert_eq!(
desired_mode(current, None, Some(true)),
native(ScrollResolution::Low, true)
);
}
#[test]
fn default_non_inverted_setting_preserves_diverted_reporting() {
let current = ScrollWheelMode {
resolution: ScrollResolution::High,
inverted: false,
target: ScrollReportingTarget::Diverted,
};
assert_eq!(desired_mode(current, None, Some(false)), current);
}
#[test]
fn mismatched_set_or_read_back_is_rejected() {
let desired = native(ScrollResolution::Low, false);
let actual = native(ScrollResolution::High, false);
assert!(matches!(
validate_applied(actual, desired),
Err(WriteError::UnsupportedResponse {
operation: HidppOperation::WriteWheelMode,
feature_hex: 0x2121,
})
));
}
}