use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
device::Device,
feature::CreatableFeature,
receiver::{self, Receiver},
};
use thiserror::Error;
use tracing::debug;
use crate::adjustable_dpi::AdjustableDpiFeatureV0;
use crate::smartshift::{SmartShiftFeatureV0, SmartShiftMode, SmartShiftStatus};
use crate::transport::{enumerate_hidpp_devices, open_hidpp_channel};
#[derive(Debug, Error)]
pub enum WriteError {
#[error("HID transport error")]
Hid(#[from] async_hid::HidError),
#[error("no matching Bolt receiver found")]
ReceiverNotFound,
#[error("device on slot {slot} did not respond to HID++")]
DeviceUnreachable { slot: u8 },
#[error("device does not expose HID++ feature {feature_hex:#06x}")]
FeatureUnsupported { feature_hex: u16 },
#[error("HID++ protocol error: {0}")]
Hidpp(String),
}
#[derive(Debug, Clone, Copy)]
pub struct FeatureEntry {
pub id: u16,
pub version: u8,
}
pub async fn dump_features(
receiver_uid: Option<&str>,
slot: u8,
) -> Result<Vec<FeatureEntry>, WriteError> {
use hidpp::feature::feature_set::v0::FeatureSetFeatureV0;
with_device(receiver_uid, slot, |channel| async move {
let mut device = Device::new(Arc::clone(&channel), slot)
.await
.map_err(|_| WriteError::DeviceUnreachable { slot })?;
let feature_set_info = device
.root()
.get_feature(FeatureSetFeatureV0::ID)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?
.ok_or(WriteError::FeatureUnsupported {
feature_hex: FeatureSetFeatureV0::ID,
})?;
let feature_set = device.add_feature::<FeatureSetFeatureV0>(feature_set_info.index);
let count = feature_set
.count()
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
let mut entries = Vec::with_capacity(usize::from(count));
for i in 0..=count {
let info = feature_set
.get_feature(i)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
entries.push(FeatureEntry {
id: info.id,
version: info.version,
});
}
Ok(entries)
})
.await
}
async fn open_feature<F: CreatableFeature + 'static>(
device: &mut Device,
_slot: u8,
) -> Result<Arc<F>, WriteError> {
let info = device
.root()
.get_feature(F::ID)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?
.ok_or(WriteError::FeatureUnsupported { feature_hex: F::ID })?;
Ok(device.add_feature::<F>(info.index))
}
pub async fn get_dpi(receiver_uid: Option<&str>, slot: u8) -> Result<u16, WriteError> {
with_device(receiver_uid, slot, |channel| async move {
let mut device = Device::new(Arc::clone(&channel), slot)
.await
.map_err(|_| WriteError::DeviceUnreachable { slot })?;
let feature = open_feature::<AdjustableDpiFeatureV0>(&mut device, slot).await?;
feature
.get_sensor_dpi(0)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))
})
.await
}
pub async fn get_smartshift_status(
receiver_uid: Option<&str>,
slot: u8,
) -> Result<SmartShiftStatus, WriteError> {
with_device(receiver_uid, slot, |channel| async move {
let mut device = Device::new(Arc::clone(&channel), slot)
.await
.map_err(|_| WriteError::DeviceUnreachable { slot })?;
let feature = open_feature::<SmartShiftFeatureV0>(&mut device, slot).await?;
feature
.get_status()
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))
})
.await
}
pub async fn set_dpi(receiver_uid: Option<&str>, slot: u8, dpi: u16) -> Result<(), WriteError> {
with_device(receiver_uid, slot, |channel| async move {
set_dpi_on_channel(&channel, slot, dpi).await
})
.await
}
async fn set_dpi_on_channel(
channel: &Arc<HidppChannel>,
slot: u8,
dpi: u16,
) -> Result<(), WriteError> {
let mut device = Device::new(Arc::clone(channel), slot)
.await
.map_err(|_| WriteError::DeviceUnreachable { slot })?;
let feature = open_feature::<AdjustableDpiFeatureV0>(&mut device, slot).await?;
feature
.set_sensor_dpi(0, dpi)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
if let Ok(actual) = feature.get_sensor_dpi(0).await {
if actual == dpi {
debug!(slot, dpi, "wrote DPI (verified)");
} else {
tracing::warn!(
slot,
requested = dpi,
actual,
"DPI write accepted but device reports a different value — \
likely out of the device's supported range"
);
}
} else {
debug!(slot, dpi, "wrote DPI (read-back skipped)");
}
Ok(())
}
pub async fn toggle_smartshift(
receiver_uid: Option<&str>,
slot: u8,
) -> Result<SmartShiftMode, WriteError> {
with_device(receiver_uid, slot, |channel| async move {
toggle_smartshift_on_channel(&channel, slot).await
})
.await
}
async fn toggle_smartshift_on_channel(
channel: &Arc<HidppChannel>,
slot: u8,
) -> Result<SmartShiftMode, WriteError> {
let mut device = Device::new(Arc::clone(channel), slot)
.await
.map_err(|_| WriteError::DeviceUnreachable { slot })?;
let feature = open_feature::<SmartShiftFeatureV0>(&mut device, slot).await?;
let SmartShiftStatus { mode, sensitivity } = feature
.get_status()
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
let next = mode.flipped();
feature
.set_status(next, sensitivity)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
debug!(slot, ?next, "wrote SmartShift mode");
Ok(next)
}
#[derive(Clone)]
pub struct SharedChannel {
channel: Arc<HidppChannel>,
receiver_uid: Option<String>,
slot: u8,
}
impl SharedChannel {
#[must_use]
pub(crate) fn new(channel: Arc<HidppChannel>, receiver_uid: Option<String>, slot: u8) -> Self {
Self {
channel,
receiver_uid,
slot,
}
}
#[must_use]
pub fn matches(&self, receiver_uid: Option<&str>, slot: u8) -> bool {
self.slot == slot
&& match (self.receiver_uid.as_deref(), receiver_uid) {
(Some(a), Some(b)) => a.eq_ignore_ascii_case(b),
(None, None) => true,
_ => false,
}
}
}
pub async fn set_dpi_on(shared: &SharedChannel, dpi: u16) -> Result<(), WriteError> {
set_dpi_on_channel(&shared.channel, shared.slot, dpi).await
}
pub async fn toggle_smartshift_on(shared: &SharedChannel) -> Result<SmartShiftMode, WriteError> {
toggle_smartshift_on_channel(&shared.channel, shared.slot).await
}
async fn with_device<F, Fut, T>(
receiver_uid: Option<&str>,
_slot: u8,
f: F,
) -> Result<T, WriteError>
where
F: FnOnce(Arc<HidppChannel>) -> Fut,
Fut: std::future::Future<Output = Result<T, WriteError>>,
{
let candidates = enumerate_hidpp_devices().await?;
for dev in candidates {
let Some((_, channel)) = open_hidpp_channel(dev).await? else {
continue;
};
let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(&channel)) else {
continue;
};
if let Some(want) = receiver_uid {
match bolt.get_unique_id().await {
Ok(uid) if uid.eq_ignore_ascii_case(want) => {}
_ => continue,
}
}
return f(channel).await;
}
Err(WriteError::ReceiverNotFound)
}