Skip to main content

openlogi_device/write/
fn_lock.rs

1//! HID++ keyboard Fn-lock writes — fn inversion `0x40a3` (multi-host), with
2//! the single-host `0x40a2` as fallback.
3//!
4//! "Fn-lock on" means the F-row sends plain F1–F12 without holding Fn
5//! ([`FnInversionState::On`]); off restores the printed media/shortcut
6//! functions, with Fn+key producing the F-keys. Multi-host keyboards store the
7//! state per Easy-Switch slot, so the `0x40a3` path addresses
8//! [`HostIndex::Current`] — the slot the keyboard is talking to right now.
9
10use std::sync::Arc;
11
12use hidpp::{
13    channel::HidppChannel,
14    device::Device,
15    feature::{
16        fn_inversion::{
17            FnInversionMultiHostFeature, FnInversionState, FnInversionWithDefaultStateFeature,
18        },
19        hosts_info::HostIndex,
20    },
21};
22use tracing::debug;
23
24use crate::SharedChannel;
25use crate::backend::HidBackend;
26use crate::channel::route::DeviceRoute;
27
28use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
29
30/// Whether a failure to open the `0x40a3` multi-host feature should trigger
31/// the `0x40a2` single-host fallback. Only a missing-`0x40a3` feature
32/// qualifies; transport and protocol errors propagate unchanged.
33fn is_missing_multi_host(err: &WriteError) -> bool {
34    matches!(
35        err,
36        WriteError::FeatureUnsupported { feature_hex } if *feature_hex == 0x40a3
37    )
38}
39
40/// Whichever fn-inversion feature the keyboard exposes, normalised onto one
41/// setter. Multi-host boards (Easy-Switch) carry `0x40a3`; single-host boards
42/// carry `0x40a2`.
43enum FnInversion {
44    /// `0x40a3 FnInversionForMultiHostDevices`.
45    MultiHost(Arc<FnInversionMultiHostFeature>),
46    /// `0x40a2 FnInversionWithDefaultState`.
47    SingleHost(Arc<FnInversionWithDefaultStateFeature>),
48}
49
50impl FnInversion {
51    /// Open whichever fn-inversion feature the device exposes. Tries `0x40a3`
52    /// first; on a missing-`0x40a3` error (and only that), retries with
53    /// `0x40a2`.
54    async fn open(device: &mut Device) -> Result<Self, WriteError> {
55        match open_feature::<FnInversionMultiHostFeature>(device).await {
56            Ok(feature) => Ok(Self::MultiHost(feature)),
57            Err(err) if is_missing_multi_host(&err) => {
58                let feature = open_feature::<FnInversionWithDefaultStateFeature>(device).await?;
59                Ok(Self::SingleHost(feature))
60            }
61            Err(err) => Err(err),
62        }
63    }
64
65    /// Write the inversion state (for the current host on `0x40a3`).
66    async fn set(&self, state: FnInversionState) -> Result<(), WriteError> {
67        match self {
68            Self::MultiHost(feature) => {
69                feature
70                    .set_global_fn_inversion(HostIndex::Current, state)
71                    .await
72                    .map_err(|e| classify_hidpp_error(e, HidppOperation::WriteFnLock, 0x40a3))?;
73            }
74            Self::SingleHost(feature) => {
75                feature
76                    .set_global_fn_inversion(state)
77                    .await
78                    .map_err(|e| classify_hidpp_error(e, HidppOperation::WriteFnLock, 0x40a2))?;
79            }
80        }
81        Ok(())
82    }
83}
84
85/// Write the keyboard's Fn-lock state: `true` = F-row sends F1–F12 directly.
86pub async fn set_fn_lock(
87    backend: &dyn HidBackend,
88    route: &DeviceRoute,
89    on: bool,
90) -> Result<(), WriteError> {
91    let index = route.device_index();
92    with_route(backend, route, move |channel| async move {
93        set_fn_lock_on_channel(&channel, index, on).await
94    })
95    .await
96}
97
98/// The Fn-lock write itself, on an already-open channel at HID++ `index`.
99pub(super) async fn set_fn_lock_on_channel(
100    channel: &Arc<HidppChannel>,
101    index: u8,
102    on: bool,
103) -> Result<(), WriteError> {
104    let mut device = Device::new(Arc::clone(channel), index)
105        .await
106        .map_err(|_| WriteError::DeviceUnreachable { index })?;
107    let fn_inversion = FnInversion::open(&mut device).await?;
108    fn_inversion.set(FnInversionState::from(on)).await?;
109    debug!(index, on, "fn-lock written");
110    Ok(())
111}
112
113/// Write keyboard Fn-lock on an already-open [`SharedChannel`] — the fast
114/// path that skips enumeration and channel setup.
115pub async fn set_fn_lock_on(shared: &SharedChannel, on: bool) -> Result<(), WriteError> {
116    set_fn_lock_on_channel(shared.channel(), shared.device_index(), on).await
117}