use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
device::Device,
feature::{
CreatableFeature as _,
haptic_feedback::{HapticFeedbackFeature, HapticIntensity, HapticWaveform},
},
};
use crate::channel_registry::ChannelRegistry;
use crate::route::DeviceRoute;
use super::{
HidppOperation, SharedChannel, WriteError, classify_hidpp_error, open_feature, with_route,
};
async fn feature_on_channel(
channel: &Arc<HidppChannel>,
device_index: u8,
) -> Result<Arc<HapticFeedbackFeature>, WriteError> {
let mut device = Device::new(Arc::clone(channel), device_index)
.await
.map_err(|_| WriteError::DeviceUnreachable {
index: device_index,
})?;
open_feature::<HapticFeedbackFeature>(&mut device).await
}
struct EpochGuarded<T> {
epoch: u64,
entry: Option<(usize, u8, T)>,
}
impl<T: Clone> EpochGuarded<T> {
const fn new() -> Self {
Self {
epoch: 0,
entry: None,
}
}
fn get(&self, ptr: usize, index: u8) -> Option<T> {
let (entry_ptr, entry_index, value) = self.entry.as_ref()?;
(*entry_ptr == ptr && *entry_index == index).then(|| value.clone())
}
fn store(
&mut self,
epoch: u64,
ptr: usize,
index: u8,
value: T,
still_current: impl FnOnce() -> bool,
) {
if self.epoch == epoch && still_current() {
self.entry = Some((ptr, index, value));
}
}
fn clear(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
self.entry = None;
}
fn clear_for(&mut self, ptr: usize) {
self.epoch = self.epoch.wrapping_add(1);
if self
.entry
.as_ref()
.is_some_and(|(entry_ptr, _, _)| *entry_ptr == ptr)
{
self.entry = None;
}
}
}
static CACHED_FEATURE: std::sync::Mutex<EpochGuarded<Arc<HapticFeedbackFeature>>> =
std::sync::Mutex::new(EpochGuarded::new());
fn cache_epoch() -> u64 {
CACHED_FEATURE.lock().map_or(0, |guard| guard.epoch)
}
fn cached_feature(channel: &Arc<HidppChannel>, index: u8) -> Option<Arc<HapticFeedbackFeature>> {
let guard = CACHED_FEATURE.lock().ok()?;
guard.get(Arc::as_ptr(channel) as usize, index)
}
fn store_cached_feature(
epoch: u64,
registry: &ChannelRegistry,
shared: &SharedChannel,
feature: &Arc<HapticFeedbackFeature>,
) {
if let Ok(mut guard) = CACHED_FEATURE.lock() {
guard.store(
epoch,
Arc::as_ptr(shared.channel()) as usize,
shared.device_index(),
Arc::clone(feature),
|| registry.is_current(shared),
);
}
}
fn clear_cached_feature() {
if let Ok(mut guard) = CACHED_FEATURE.lock() {
guard.clear();
}
}
pub fn clear_haptic_feature_cache() {
clear_cached_feature();
}
pub(crate) fn clear_haptic_feature_cache_for(channel: &Arc<HidppChannel>) {
if let Ok(mut guard) = CACHED_FEATURE.lock() {
guard.clear_for(Arc::as_ptr(channel) as usize);
}
}
pub async fn ensure_haptics_armed_on(
registry: &ChannelRegistry,
shared: &SharedChannel,
) -> Result<bool, WriteError> {
let channel = shared.channel();
let index = shared.device_index();
let feature = if let Some(feature) = cached_feature(channel, index) {
feature
} else {
let epoch = cache_epoch();
let feature = feature_on_channel(channel, index).await?;
store_cached_feature(epoch, registry, shared, &feature);
feature
};
let config = feature.get_configuration().await.map_err(|error| {
clear_cached_feature();
classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
})?;
let intensity = if config.intensity.get() == 0 {
HapticIntensity::new(25).unwrap_or(config.intensity)
} else {
config.intensity
};
if config.enabled && intensity == config.intensity {
return Ok(false);
}
feature
.set_configuration(true, intensity)
.await
.map_err(|error| {
clear_cached_feature();
classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
})?;
Ok(true)
}
pub async fn play_haptic_on(
registry: &ChannelRegistry,
shared: &SharedChannel,
waveform: HapticWaveform,
) -> Result<(), WriteError> {
let channel = shared.channel();
let index = shared.device_index();
if let Some(feature) = cached_feature(channel, index) {
if feature.play(waveform).await.is_ok() {
return Ok(());
}
clear_cached_feature();
}
let epoch = cache_epoch();
let feature = feature_on_channel(channel, index).await?;
let result = feature.play(waveform).await.map_err(|error| {
classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
});
if result.is_ok() {
store_cached_feature(epoch, registry, shared, &feature);
}
result
}
pub async fn play_haptic(route: &DeviceRoute, waveform: HapticWaveform) -> Result<(), WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
let feature = feature_on_channel(&channel, index).await?;
feature.play(waveform).await.map_err(|error| {
classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
})
})
.await
}
#[cfg(test)]
mod tests {
use super::EpochGuarded;
const CURRENT: fn() -> bool = || true;
const RETIRED: fn() -> bool = || false;
#[test]
fn a_store_started_before_a_clear_is_discarded() {
let mut cache = EpochGuarded::new();
let epoch = cache.epoch;
cache.clear_for(0xA);
cache.store(epoch, 0xA, 2, "stale", CURRENT);
assert_eq!(cache.get(0xA, 2), None);
}
#[test]
fn a_store_for_a_channel_retired_before_the_open_is_discarded() {
let mut cache = EpochGuarded::new();
cache.clear_for(0xA);
let epoch = cache.epoch;
cache.store(epoch, 0xA, 2, "retired", RETIRED);
assert_eq!(
cache.get(0xA, 2),
None,
"a channel the enumerator has retired must never be cached again"
);
}
#[test]
fn a_store_with_a_current_epoch_lands() {
let mut cache = EpochGuarded::new();
cache.store(cache.epoch, 0xA, 2, "fresh", CURRENT);
assert_eq!(cache.get(0xA, 2), Some("fresh"));
assert_eq!(cache.get(0xB, 2), None);
assert_eq!(cache.get(0xA, 3), None);
}
#[test]
fn retiring_one_channel_keeps_anothers_entry_but_blocks_stale_stores() {
let mut cache = EpochGuarded::new();
cache.store(cache.epoch, 0xA, 2, "kept", CURRENT);
let epoch = cache.epoch;
cache.clear_for(0xB);
assert_eq!(cache.get(0xA, 2), Some("kept"));
cache.store(epoch, 0xB, 1, "stale", CURRENT);
assert_eq!(cache.get(0xB, 1), None);
}
#[test]
fn a_full_clear_empties_the_entry_and_blocks_stale_stores() {
let mut cache = EpochGuarded::new();
let epoch = cache.epoch;
cache.store(epoch, 0xA, 2, "cached", CURRENT);
cache.clear();
assert_eq!(cache.get(0xA, 2), None);
cache.store(epoch, 0xA, 2, "stale", CURRENT);
assert_eq!(cache.get(0xA, 2), None);
}
}