use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
device::Device,
feature::{
CreatableFeature as _,
fn_inversion::{
FnInversionMultiHostFeature, FnInversionState, FnInversionWithDefaultStateFeature,
},
hosts_info::{HostIndex, HostsInfoFeature},
},
};
use tracing::debug;
use crate::SharedChannel;
use crate::backend::HidBackend;
use crate::channel::route::DeviceRoute;
use openlogi_core::hid::FnLockState;
use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
fn is_missing_multi_host(err: &WriteError) -> bool {
matches!(
err,
WriteError::FeatureUnsupported { feature_hex } if *feature_hex == 0x40a3
)
}
fn inversion_for(fn_lock: bool) -> FnInversionState {
if fn_lock {
FnInversionState::Off
} else {
FnInversionState::On
}
}
fn fn_lock_for(inversion: FnInversionState) -> bool {
matches!(inversion, FnInversionState::Off)
}
enum FnInversion {
MultiHost {
feature: Arc<FnInversionMultiHostFeature>,
host: HostIndex,
},
SingleHost(Arc<FnInversionWithDefaultStateFeature>),
}
impl FnInversion {
async fn open(device: &mut Device, addressing: HostAddressing) -> Result<Self, WriteError> {
match open_feature::<FnInversionMultiHostFeature>(device).await {
Ok(feature) => {
let host = match addressing {
HostAddressing::Current => HostIndex::Current,
HostAddressing::ExplicitSlot { operation } => {
current_host(device, operation).await?
}
};
Ok(Self::MultiHost { feature, host })
}
Err(err) if is_missing_multi_host(&err) => {
let feature = open_feature::<FnInversionWithDefaultStateFeature>(device).await?;
Ok(Self::SingleHost(feature))
}
Err(err) => Err(err),
}
}
fn feature_hex(&self) -> u16 {
match self {
Self::MultiHost { .. } => 0x40a3,
Self::SingleHost(_) => 0x40a2,
}
}
async fn get(&self) -> Result<FnLockState, WriteError> {
let (state, default_state) = match self {
Self::MultiHost { feature, host } => {
let info = feature.get_global_fn_inversion(*host).await.map_err(|e| {
classify_hidpp_error(e, HidppOperation::ReadFnLock, self.feature_hex())
})?;
(info.state, info.default_state)
}
Self::SingleHost(feature) => {
let global = feature.get_global_fn_inversion().await.map_err(|e| {
classify_hidpp_error(e, HidppOperation::ReadFnLock, self.feature_hex())
})?;
(global.state, global.default_state)
}
};
Ok(FnLockState {
fn_lock: fn_lock_for(state),
default_fn_lock: fn_lock_for(default_state),
})
}
async fn set(&self, state: FnInversionState) -> Result<FnLockState, WriteError> {
let (state, default_state) = match self {
Self::MultiHost { feature, host } => {
let info = feature
.set_global_fn_inversion(*host, state)
.await
.map_err(|e| {
classify_hidpp_error(e, HidppOperation::WriteFnLock, self.feature_hex())
})?;
(info.state, info.default_state)
}
Self::SingleHost(feature) => {
let global = feature.set_global_fn_inversion(state).await.map_err(|e| {
classify_hidpp_error(e, HidppOperation::WriteFnLock, self.feature_hex())
})?;
(global.state, global.default_state)
}
};
Ok(FnLockState {
fn_lock: fn_lock_for(state),
default_fn_lock: fn_lock_for(default_state),
})
}
}
#[derive(Debug, Clone, Copy)]
enum HostAddressing {
Current,
ExplicitSlot { operation: HidppOperation },
}
async fn current_host(
device: &mut Device,
operation: HidppOperation,
) -> Result<HostIndex, WriteError> {
let hosts_info = match open_feature::<HostsInfoFeature>(device).await {
Ok(hosts_info) => hosts_info,
Err(WriteError::FeatureUnsupported { feature_hex })
if feature_hex == HostsInfoFeature::ID =>
{
return Ok(HostIndex::Current);
}
Err(error) => return Err(error),
};
let info = hosts_info
.get_feature_info()
.await
.map_err(|e| classify_hidpp_error(e, operation, HostsInfoFeature::ID))?;
Ok(info.current_host)
}
pub async fn get_fn_lock(
backend: &dyn HidBackend,
route: &DeviceRoute,
) -> Result<FnLockState, WriteError> {
let index = route.device_index();
with_route(backend, route, move |channel| async move {
get_fn_lock_on_channel(&channel, index).await
})
.await
}
pub async fn get_fn_lock_on(shared: &SharedChannel) -> Result<FnLockState, WriteError> {
get_fn_lock_on_channel(shared.channel(), shared.device_index()).await
}
async fn get_fn_lock_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
) -> Result<FnLockState, WriteError> {
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
FnInversion::open(&mut device, HostAddressing::Current)
.await?
.get()
.await
}
pub async fn set_fn_lock(
backend: &dyn HidBackend,
route: &DeviceRoute,
on: bool,
) -> Result<FnLockState, WriteError> {
let index = route.device_index();
with_route(backend, route, move |channel| async move {
set_fn_lock_on_channel(&channel, index, on).await
})
.await
}
pub(super) async fn set_fn_lock_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
on: bool,
) -> Result<FnLockState, WriteError> {
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let fn_inversion = FnInversion::open(
&mut device,
HostAddressing::ExplicitSlot {
operation: HidppOperation::WriteFnLock,
},
)
.await?;
let echoed = fn_inversion.set(inversion_for(on)).await?;
if echoed.fn_lock != on {
return Err(WriteError::UnsupportedResponse {
operation: HidppOperation::WriteFnLock,
feature_hex: fn_inversion.feature_hex(),
});
}
debug!(index, on, "fn-lock written");
Ok(echoed)
}
pub async fn set_fn_lock_on(shared: &SharedChannel, on: bool) -> Result<FnLockState, WriteError> {
set_fn_lock_on_channel(shared.channel(), shared.device_index(), on).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fn_lock_is_the_inverse_of_firmware_inversion() {
assert_eq!(inversion_for(true), FnInversionState::Off);
assert_eq!(inversion_for(false), FnInversionState::On);
for on in [true, false] {
assert_eq!(fn_lock_for(inversion_for(on)), on);
}
}
}