use std::fmt;
use std::future::Future;
use std::sync::{Arc, RwLock, Weak};
use hidpp::channel::HidppChannel;
use thiserror::Error;
use crate::backend::BackendError;
use crate::reprog_controls::{self, ReprogControlsV4};
use crate::thumbwheel::Thumbwheel;
use crate::{ChannelRegistry, DeviceRoute, SharedChannel};
pub type CaptureChannel = Arc<RwLock<Option<SharedChannel>>>;
#[derive(Debug, Error)]
pub enum GestureError {
#[error("HID transport error")]
Hid(#[from] BackendError),
#[error("no connected device matched the capture route")]
DeviceNotFound,
#[error("device at index {0:#04x} did not respond to HID++")]
DeviceUnreachable(u8),
#[error("HID++ protocol error: {0}")]
Hidpp(String),
}
#[derive(Clone, Copy)]
pub(crate) struct ArmedReporting {
pub(crate) cid: u16,
pub(crate) original: reprog_controls::CidReporting,
}
pub(crate) struct ReprogRestore {
feature_index: u8,
controls: Vec<ArmedReporting>,
}
impl ReprogRestore {
pub(crate) fn new(feature_index: u8, controls: Vec<ArmedReporting>) -> Option<Self> {
(!controls.is_empty()).then_some(Self {
feature_index,
controls,
})
}
}
#[derive(Clone, Copy)]
pub(crate) enum CaptureStop {
Shutdown,
ChannelChanged,
}
#[derive(Clone, Copy)]
enum RetiredChannelPolicy {
ReplacementOnly,
CurrentAllowed,
}
#[must_use = "a pending restore must be retained until firmware ownership is released"]
pub enum CaptureSessionOutcome {
Restored,
RestorePending(PendingCaptureRestore),
}
#[derive(Debug, Error)]
#[error("{error}")]
pub struct CaptureSessionFailure {
#[source]
error: GestureError,
pending_restore: Option<PendingCaptureRestore>,
}
impl CaptureSessionFailure {
pub(crate) fn clean(error: GestureError) -> Self {
Self {
error,
pending_restore: None,
}
}
pub(crate) fn with_pending(error: GestureError, pending: PendingCaptureRestore) -> Self {
Self {
error,
pending_restore: Some(pending),
}
}
#[must_use]
pub fn into_parts(self) -> (GestureError, Option<PendingCaptureRestore>) {
(self.error, self.pending_restore)
}
}
impl From<GestureError> for CaptureSessionFailure {
fn from(error: GestureError) -> Self {
Self::clean(error)
}
}
pub struct PendingCaptureRestore {
route: DeviceRoute,
retired_channel: Weak<HidppChannel>,
retired_policy: RetiredChannelPolicy,
reprog: Option<ReprogRestore>,
thumb_index: Option<u8>,
}
impl fmt::Debug for PendingCaptureRestore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PendingCaptureRestore")
.field("route", &self.route)
.field(
"reporting_count",
&self
.reprog
.as_ref()
.map_or(0, |reprog| reprog.controls.len()),
)
.field("has_thumbwheel", &self.thumb_index.is_some())
.finish_non_exhaustive()
}
}
impl PendingCaptureRestore {
pub(crate) fn new(
retired: &SharedChannel,
reprog: Option<ReprogRestore>,
thumb_index: Option<u8>,
) -> Option<Self> {
if reprog.is_none() && thumb_index.is_none() {
return None;
}
Some(Self {
route: retired.route().clone(),
retired_channel: Arc::downgrade(retired.channel()),
retired_policy: RetiredChannelPolicy::ReplacementOnly,
reprog,
thumb_index,
})
}
pub async fn retry(self, registry: &ChannelRegistry) -> CaptureSessionOutcome {
let Some(current) = registry.lookup(&self.route) else {
return CaptureSessionOutcome::RestorePending(self);
};
if matches!(self.retired_policy, RetiredChannelPolicy::ReplacementOnly)
&& self
.retired_channel
.upgrade()
.is_some_and(|retired| Arc::ptr_eq(current.channel(), &retired))
{
return CaptureSessionOutcome::RestorePending(self);
}
let restored = self.restore_on(¤t).await;
if restored && registry.is_current(¤t) {
CaptureSessionOutcome::Restored
} else {
CaptureSessionOutcome::RestorePending(self)
}
}
pub(crate) async fn restore_standalone(self, retired: &SharedChannel) -> CaptureSessionOutcome {
if self.restore_on(retired).await {
CaptureSessionOutcome::Restored
} else {
CaptureSessionOutcome::RestorePending(self)
}
}
pub(crate) fn allow_current_channel(mut self) -> Self {
self.retired_policy = RetiredChannelPolicy::CurrentAllowed;
self
}
async fn restore_on(&self, current: &SharedChannel) -> bool {
let channel = Arc::clone(current.channel());
let device_index = current.device_index();
let mut restored = true;
if let Some(reprog) = &self.reprog {
let controls =
ReprogControlsV4::new(channel.clone(), device_index, reprog.feature_index);
for &reporting in &reprog.controls {
restored &= restore_reporting(&controls, reporting, "captured control").await;
}
}
if let Some(feature_index) = self.thumb_index {
let thumbwheel = Thumbwheel::new(channel, device_index, feature_index);
restored &= restore_result(thumbwheel.undivert().await, "thumb wheel");
}
restored
}
}
pub(crate) async fn rollback_capture_start(
error: GestureError,
pending: Option<PendingCaptureRestore>,
retired: &SharedChannel,
registry: Option<&ChannelRegistry>,
) -> CaptureSessionFailure {
let Some(pending) = pending else {
return CaptureSessionFailure::clean(error);
};
let pending = pending.allow_current_channel();
let outcome = match registry {
Some(registry) => pending.retry(registry).await,
None => pending.restore_standalone(retired).await,
};
match outcome {
CaptureSessionOutcome::Restored => CaptureSessionFailure::clean(error),
CaptureSessionOutcome::RestorePending(pending) => {
CaptureSessionFailure::with_pending(error, pending)
}
}
}
pub(crate) async fn restore_after_stop(
stop: CaptureStop,
pending: Option<PendingCaptureRestore>,
retired: &SharedChannel,
registry: Option<&ChannelRegistry>,
) -> CaptureSessionOutcome {
let Some(pending) = pending else {
return CaptureSessionOutcome::Restored;
};
match stop {
CaptureStop::Shutdown => {
let pending = pending.allow_current_channel();
match registry {
Some(registry) => pending.retry(registry).await,
None => pending.restore_standalone(retired).await,
}
}
CaptureStop::ChannelChanged => match registry {
Some(registry) => pending.retry(registry).await,
None => CaptureSessionOutcome::RestorePending(pending),
},
}
}
pub(crate) fn stop_for_current_publication(
registry: Option<&ChannelRegistry>,
retired: &SharedChannel,
) -> CaptureStop {
if registry.is_some_and(|registry| !registry.is_current(retired)) {
CaptureStop::ChannelChanged
} else {
CaptureStop::Shutdown
}
}
pub(crate) async fn wait_for_channel_change(
registry: Option<&ChannelRegistry>,
retired: &SharedChannel,
) -> CaptureStop {
let Some(registry) = registry else {
return std::future::pending().await;
};
let mut changes = registry.subscribe();
loop {
if !registry.is_current(retired) {
return CaptureStop::ChannelChanged;
}
if changes.changed().await.is_err() {
return std::future::pending().await;
}
}
}
pub(crate) async fn drop_listener_after<T, R>(listener: T, teardown: impl Future<Output = R>) -> R {
let result = teardown.await;
drop(listener);
result
}
pub(crate) fn divert_change(
reporting: reprog_controls::CidReporting,
raw_xy: bool,
) -> reprog_controls::CidReportingChange {
reprog_controls::CidReportingChange {
diverted: Some(true),
raw_xy: Some(raw_xy),
remap: reporting.remap,
..Default::default()
}
}
pub(crate) async fn restore_reporting(
controls: &ReprogControlsV4,
reporting: ArmedReporting,
what: &str,
) -> bool {
let result = controls
.set_cid_reporting_full(reporting.cid, undivert_change(reporting.original))
.await
.map(|_| ());
restore_result(result, what)
}
pub(crate) fn restore_result<E: fmt::Display>(result: Result<(), E>, what: &str) -> bool {
if let Err(error) = result {
tracing::warn!(%error, control = what, "failed to restore control mapping");
false
} else {
true
}
}
pub(crate) fn undivert_change(
reporting: reprog_controls::CidReporting,
) -> reprog_controls::CidReportingChange {
reprog_controls::CidReportingChange {
diverted: Some(false),
raw_xy: Some(false),
remap: reporting.remap,
..Default::default()
}
}