use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
device::Device,
feature::hires_wheel::HiResWheelFeature,
feature::{
CreatableFeature, device_information::DeviceInformationFeature,
device_type_and_name::DeviceTypeAndNameFeature, unified_battery::UnifiedBatteryFeature,
},
};
use openlogi_core::device::{
BatteryInfo, Capabilities, DeviceKind, DeviceModelInfo, DeviceTransports,
};
use tracing::debug;
use crate::mappings::{
map_battery_level, map_battery_status, map_device_type, normalize_serial_number,
};
#[derive(Default, Clone)]
pub(super) struct ProbedFeatures {
pub(super) battery: Option<BatteryInfo>,
pub(super) model_info: Option<DeviceModelInfo>,
pub(super) kind: Option<DeviceKind>,
pub(super) capabilities: Option<Capabilities>,
pub(super) identity_incomplete: bool,
}
pub(super) async fn read_battery(
channel: &Arc<HidppChannel>,
slot: u8,
feature_index: u8,
) -> Option<BatteryInfo> {
let feature = UnifiedBatteryFeature::new(Arc::clone(channel), slot, feature_index);
feature
.get_battery_info()
.await
.ok()
.map(|info| BatteryInfo {
percentage: info.charging_percentage,
level: map_battery_level(info.level),
status: map_battery_status(info.status),
})
}
pub(super) fn battery_feature_index(ids: impl IntoIterator<Item = u16>) -> Option<u8> {
ids.into_iter()
.position(|id| id == UnifiedBatteryFeature::ID)
.and_then(|pos| u8::try_from(pos + 1).ok())
}
pub(super) async fn probe_features(
channel: &Arc<HidppChannel>,
slot: u8,
) -> (ProbedFeatures, Option<u8>) {
let mut device = match Device::new(Arc::clone(channel), slot).await {
Ok(d) => d,
Err(e) => {
debug!(slot, error = ?e, "Device::new failed");
return (ProbedFeatures::default(), None);
}
};
let mut battery_index = None;
let mut capabilities = match device.enumerate_features().await {
Ok(Some(features)) => {
let ids: Vec<u16> = features.iter().map(|f| f.id).collect();
battery_index = battery_feature_index(ids.iter().copied());
Some(Capabilities::from_feature_ids(&ids))
}
Ok(None) => None,
Err(e) => {
debug!(slot, error = ?e, "enumerate_features failed");
return (ProbedFeatures::default(), None);
}
};
if let Some(caps) = capabilities.as_mut()
&& let Some(feature) = device.get_feature::<HiResWheelFeature>()
{
caps.scroll_inversion = feature
.get_wheel_capabilities()
.await
.is_ok_and(|wheel| wheel.has_invert);
}
let battery = match battery_index {
Some(feature_index) => read_battery(channel, slot, feature_index).await,
None => None,
};
let mut identity_incomplete = false;
let model_info = match device.get_feature::<DeviceInformationFeature>() {
Some(feature) => match feature.get_device_info().await {
Ok(info) => {
let serial_number = if info.capabilities.serial_number {
match feature.get_serial_number().await {
Ok(serial) => normalize_serial_number(&serial),
Err(e) => {
debug!(slot, error = ?e, "DeviceInformation serial read failed");
identity_incomplete = true;
None
}
}
} else {
None
};
Some(DeviceModelInfo {
entity_count: info.entity_count,
serial_number,
unit_id: info.unit_id,
transports: DeviceTransports {
usb: info.transport.usb,
equad: info.transport.e_quad,
btle: info.transport.btle,
bluetooth: info.transport.bluetooth,
},
model_ids: info.model_id,
extended_model_id: info.extended_model_id,
})
}
Err(e) => {
debug!(slot, error = ?e, "DeviceInformation read failed");
identity_incomplete = true;
None
}
},
None => None,
};
let kind = match device.get_feature::<DeviceTypeAndNameFeature>() {
Some(feature) => match feature.get_device_type().await {
Ok(ty) => Some(map_device_type(ty)),
Err(e) => {
debug!(slot, error = ?e, "DeviceType read failed");
None
}
},
None => None,
};
(
ProbedFeatures {
battery,
model_info,
kind,
capabilities,
identity_incomplete,
},
battery_index,
)
}
#[cfg(test)]
mod tests {
use hidpp::feature::{CreatableFeature as _, unified_battery::UnifiedBatteryFeature};
use super::battery_feature_index;
#[test]
fn battery_index_is_one_based_in_the_enumerated_table() {
let table = [0x0001, UnifiedBatteryFeature::ID, 0x2201];
assert_eq!(battery_feature_index(table), Some(2));
assert_eq!(
battery_feature_index([UnifiedBatteryFeature::ID]),
Some(1),
"first entry maps to index 1, not 0"
);
}
#[test]
fn no_battery_feature_means_no_index() {
assert_eq!(battery_feature_index([0x0001, 0x2201, 0x1b04]), None);
assert_eq!(battery_feature_index([]), None);
}
}