use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
nibble::U4,
protocol::v20::{self, Hidpp20Error},
};
pub const FEATURE_ID: u16 = 0x1b04;
pub const GESTURE_BUTTON_CID: u16 = 0x00c3;
pub const DPI_MODE_SHIFT_CIDS: [u16; 3] = [0x00c4, 0x00ed, 0x00fd];
const FN_GET_COUNT: u8 = 0;
const FN_GET_CTRL_ID_INFO: u8 = 1;
const FN_SET_CID_REPORTING: u8 = 3;
const MAPPING_DIVERTED: u8 = 0x01;
const MAPPING_RAW_XY_DIVERTED: u8 = 0x10;
const KEYFLAG_DIVERTABLE: u16 = 0x20;
const KEYFLAG_RAW_XY: u16 = 0x100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CtrlIdInfo {
pub cid: u16,
pub task_id: u16,
pub flags: u16,
}
impl CtrlIdInfo {
#[must_use]
pub fn is_divertable(self) -> bool {
self.flags & KEYFLAG_DIVERTABLE != 0
}
#[must_use]
pub fn supports_raw_xy(self) -> bool {
self.flags & KEYFLAG_RAW_XY != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RawControlEvent {
DivertedButtons([u16; 4]),
RawXy {
dx: i16,
dy: i16,
},
}
impl RawControlEvent {
#[must_use]
pub fn is_pressed(&self, cid: u16) -> bool {
matches!(self, RawControlEvent::DivertedButtons(cids) if cids.contains(&cid))
}
}
#[must_use]
pub fn decode_event(
msg: &v20::Message,
device_index: u8,
feature_index: u8,
) -> Option<RawControlEvent> {
let header = msg.header();
if header.device_index != device_index
|| header.feature_index != feature_index
|| header.software_id.to_lo() != 0
{
return None;
}
let p = msg.extend_payload();
match header.function_id.to_lo() {
0 => Some(RawControlEvent::DivertedButtons([
u16::from_be_bytes([p[0], p[1]]),
u16::from_be_bytes([p[2], p[3]]),
u16::from_be_bytes([p[4], p[5]]),
u16::from_be_bytes([p[6], p[7]]),
])),
1 => Some(RawControlEvent::RawXy {
dx: i16::from_be_bytes([p[0], p[1]]),
dy: i16::from_be_bytes([p[2], p[3]]),
}),
_ => None,
}
}
#[must_use]
fn reporting_bfield(diverted: bool, raw_xy: bool) -> u8 {
let mut bfield = 0u8;
for (flag, on) in [
(MAPPING_DIVERTED, diverted),
(MAPPING_RAW_XY_DIVERTED, raw_xy),
] {
if on {
bfield |= flag;
}
bfield |= flag << 1;
}
bfield
}
#[derive(Clone)]
pub struct ReprogControlsV4 {
chan: Arc<HidppChannel>,
device_index: u8,
feature_index: u8,
}
impl ReprogControlsV4 {
#[must_use]
pub fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
Self {
chan,
device_index,
feature_index,
}
}
#[must_use]
pub fn feature_index(&self) -> u8 {
self.feature_index
}
#[must_use]
pub fn device_index(&self) -> u8 {
self.device_index
}
async fn call(&self, function_id: u8, params: [u8; 16]) -> Result<[u8; 16], Hidpp20Error> {
let response = self
.chan
.send_v20(v20::Message::Long(
v20::MessageHeader {
device_index: self.device_index,
feature_index: self.feature_index,
function_id: U4::from_lo(function_id),
software_id: self.chan.get_sw_id(),
},
params,
))
.await?;
Ok(response.extend_payload())
}
pub async fn get_count(&self) -> Result<u8, Hidpp20Error> {
Ok(self.call(FN_GET_COUNT, [0; 16]).await?[0])
}
pub async fn get_ctrl_id_info(&self, index: u8) -> Result<CtrlIdInfo, Hidpp20Error> {
let mut params = [0u8; 16];
params[0] = index;
let p = self.call(FN_GET_CTRL_ID_INFO, params).await?;
Ok(CtrlIdInfo {
cid: u16::from_be_bytes([p[0], p[1]]),
task_id: u16::from_be_bytes([p[2], p[3]]),
flags: u16::from(p[4]) | (u16::from(p[8]) << 8),
})
}
pub async fn find_control(&self, cid: u16) -> Result<Option<CtrlIdInfo>, Hidpp20Error> {
let count = self.get_count().await?;
for index in 0..count {
let info = self.get_ctrl_id_info(index).await?;
if info.cid == cid {
return Ok(Some(info));
}
}
Ok(None)
}
pub async fn set_cid_reporting(
&self,
cid: u16,
diverted: bool,
raw_xy: bool,
) -> Result<(), Hidpp20Error> {
let [cid_hi, cid_lo] = cid.to_be_bytes();
let mut params = [0u8; 16];
params[0] = cid_hi;
params[1] = cid_lo;
params[2] = reporting_bfield(diverted, raw_xy);
self.call(FN_SET_CID_REPORTING, params).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bfield_enable_and_clear() {
assert_eq!(reporting_bfield(true, true), 0x33);
assert_eq!(reporting_bfield(false, false), 0x22);
assert_eq!(reporting_bfield(true, false), 0x23);
assert_eq!(reporting_bfield(false, true), 0x32);
}
fn event(function_id: u8, software_id: u8, payload: [u8; 16]) -> v20::Message {
v20::Message::Long(
v20::MessageHeader {
device_index: 2,
feature_index: 7,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
payload,
)
}
#[test]
fn decodes_diverted_buttons() {
let mut p = [0u8; 16];
p[0..2].copy_from_slice(&GESTURE_BUTTON_CID.to_be_bytes());
let decoded = decode_event(&event(0, 0, p), 2, 7);
assert_eq!(
decoded,
Some(RawControlEvent::DivertedButtons([
GESTURE_BUTTON_CID,
0,
0,
0
]))
);
assert!(decoded.is_some_and(|e| e.is_pressed(GESTURE_BUTTON_CID)));
}
#[test]
fn decodes_signed_raw_xy() {
let mut p = [0u8; 16];
p[0..2].copy_from_slice(&(-5i16).to_be_bytes());
p[2..4].copy_from_slice(&12i16.to_be_bytes());
assert_eq!(
decode_event(&event(1, 0, p), 2, 7),
Some(RawControlEvent::RawXy { dx: -5, dy: 12 })
);
}
#[test]
fn ignores_responses_and_foreign_messages() {
let p = [0u8; 16];
assert_eq!(decode_event(&event(0, 5, p), 2, 7), None);
assert_eq!(decode_event(&event(2, 0, p), 2, 7), None);
assert_eq!(decode_event(&event(0, 0, p), 2, 9), None);
}
}