use std::{any::Any, sync::Arc};
use crate::{
channel::{HidppChannel, HidppMessage, LONG_REPORT_LENGTH},
nibble::U4,
protocol::v20::{self, Hidpp20Error},
};
pub mod adjustable_dpi;
pub mod device_friendly_name;
pub mod device_information;
pub mod device_type_and_name;
pub mod feature_set;
pub mod hires_wheel;
pub mod registry;
pub mod root;
pub mod smartshift;
pub mod thumbwheel;
pub mod unified_battery;
pub mod wireless_device_status;
pub trait Feature: Any + Send + Sync {}
pub trait CreatableFeature: Feature {
const ID: u16;
const STARTING_VERSION: u8;
fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self;
}
pub trait EmittingFeature<T>: Feature {
fn listen(&self) -> async_channel::Receiver<T>;
}
#[derive(Clone)]
pub(crate) struct FeatureEndpoint {
chan: Arc<HidppChannel>,
device_index: u8,
feature_index: u8,
}
impl FeatureEndpoint {
pub(crate) fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
Self {
chan,
device_index,
feature_index,
}
}
pub(crate) fn chan(&self) -> &HidppChannel {
&self.chan
}
fn header(&self, function: u8) -> v20::MessageHeader {
debug_assert!(
function < 16,
"HID++2.0 function id {function} exceeds 4 bits"
);
v20::MessageHeader {
device_index: self.device_index,
feature_index: self.feature_index,
function_id: U4::from_lo(function),
software_id: self.chan.get_sw_id(),
}
}
pub(crate) async fn call(
&self,
function: u8,
args: [u8; 3],
) -> Result<v20::Message, Hidpp20Error> {
self.chan
.send_v20(v20::Message::Short(self.header(function), args))
.await
}
pub(crate) async fn call_long(
&self,
function: u8,
args: [u8; 16],
) -> Result<v20::Message, Hidpp20Error> {
self.chan
.send_v20(v20::Message::Long(self.header(function), args))
.await
}
}
pub(crate) fn event_payload(
raw: HidppMessage,
matched: bool,
device_index: u8,
feature_index: u8,
) -> Option<(U4, [u8; LONG_REPORT_LENGTH - 4])> {
if matched {
return None;
}
let msg = v20::Message::from(raw);
let header = msg.header();
if header.device_index != device_index
|| header.feature_index != feature_index
|| header.software_id.to_lo() != 0
{
return None;
}
Some((header.function_id, msg.extend_payload()))
}
#[derive(Clone, Copy, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct FeatureType {
pub obsolete: bool,
pub hidden: bool,
pub engineering: bool,
pub manufacturing_deactivatable: bool,
pub compliance_deactivatable: bool,
}
impl From<u8> for FeatureType {
fn from(value: u8) -> Self {
Self {
obsolete: value & (1 << 7) != 0,
hidden: value & (1 << 6) != 0,
engineering: value & (1 << 5) != 0,
manufacturing_deactivatable: value & (1 << 4) != 0,
compliance_deactivatable: value & (1 << 3) != 0,
}
}
}
impl From<FeatureType> for u8 {
fn from(value: FeatureType) -> Self {
let mut raw = 0;
if value.obsolete {
raw |= 1 << 7
}
if value.hidden {
raw |= 1 << 6
}
if value.engineering {
raw |= 1 << 5
}
if value.manufacturing_deactivatable {
raw |= 1 << 4
}
if value.compliance_deactivatable {
raw |= 1 << 3
}
raw
}
}
#[cfg(test)]
mod tests {
use super::event_payload;
use crate::{
channel::HidppMessage,
nibble::U4,
protocol::v20::{Message, MessageHeader},
};
fn broadcast(device_index: u8, feature_index: u8, function: u8, software: u8) -> HidppMessage {
Message::Long(
MessageHeader {
device_index,
feature_index,
function_id: U4::from_lo(function),
software_id: U4::from_lo(software),
},
[0xab; 16],
)
.into()
}
#[test]
fn accepts_matching_broadcast_and_returns_sub_id() {
let (func, payload) =
event_payload(broadcast(2, 5, 1, 0), false, 2, 5).expect("broadcast should pass");
assert_eq!(func.to_lo(), 1);
assert_eq!(payload, [0xab; 16]);
}
#[test]
fn rejects_request_matched_report() {
assert!(event_payload(broadcast(2, 5, 0, 0), true, 2, 5).is_none());
}
#[test]
fn rejects_other_device_or_feature() {
assert!(event_payload(broadcast(9, 5, 0, 0), false, 2, 5).is_none());
assert!(event_payload(broadcast(2, 9, 0, 0), false, 2, 5).is_none());
}
#[test]
fn gates_on_software_id_only_not_sub_id() {
assert!(event_payload(broadcast(2, 5, 0, 1), false, 2, 5).is_none());
assert!(event_payload(broadcast(2, 5, 7, 0), false, 2, 5).is_some());
}
}