g11_macro_keys/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::mem;
4use derive_more::{Display, Error};
5
6pub mod usb_id {
7    //! Constants for locating the right USB device
8
9    /// USB VID for Logitech
10    pub const VENDOR_LOGITECH: u16 = 0x46d;
11
12    /// USB PID for the macro key interface on a G11 Keyboard
13    pub const PRODUCT_G11_MACRO: u16 = 0xc225;
14
15    #[deprecated = "renamed to `PRODUCT_G11_MACRO`"]
16    pub const PRODUCT_G11: u16 = PRODUCT_G11_MACRO;
17
18    /// USB PID for the regular (104-key) interface on a G11 Keyboard
19    pub const PRODUCT_G11_STANDARD: u16 = 0xc221;
20}
21
22mod multikey;
23mod led;
24
25/// A specific key on the G11
26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27pub enum Key {
28    /// `G` keys, numbered `1 ..= 18`
29    ///
30    /// (the macro keys themselves)
31    G(u8),
32    /// `M` key, numbered `1 ..= 3`
33    ///
34    /// (for switching between macro sets)
35    M(u8),
36    /// Macro Record key
37    MR,
38    /// Main keyboard backlight key
39    /// (unrelated to macro keys but runs on the same interface)
40    Backlight,
41}
42
43/// Whether the user has been observed to have [`Pressed`] or [`Released`] a [`Key`]
44///
45/// [`Pressed`]: Action::Pressed
46/// [`Released`]: Action::Released
47#[derive(Debug, Copy, Clone, PartialEq, Eq)]
48pub enum Action {
49    Pressed,
50    Released,
51}
52
53/// Signal from the G11 that the user has performed an [`Action`] on a [`Key`]
54#[derive(Debug, Copy, Clone, PartialEq, Eq)]
55pub struct Event {
56    pub key: Key,
57    pub action: Action,
58}
59
60/// Keeps track of the known device state,
61/// so that an individual [`Event`] may be isolated from each set of new bytes received over USB.
62///
63/// You must keep this object up-to-date by feeding all of the bytes read from the G11's HID interface through [`State::try_consume_event`].
64#[derive(Default, Debug, Clone)]
65pub struct State(multikey::MultiKey, Option<led::Led>);
66impl State {
67    #[must_use] pub fn new() -> Self { Self::default() }
68
69    /// Returns `true` if the given [`Key`] is known to be currently pressed, `false` otherwise
70    #[must_use]
71    pub fn is_pressed(&self, key: Key) -> bool {
72        match multikey::MultiKey::try_from(key) {
73            Ok(multi) => self.0.contains(multi),
74            _ => false,
75        }
76    }
77
78    /// Returns every [`Key`] for which [`Self::is_pressed`] would return `true`
79    /// (there may be up to five pressed simultaneously on the G11)
80    pub fn iter_pressed(&self) -> impl Iterator<Item = Key> {
81        self.0.iter()
82            .filter_map(|key| Key::try_from(key).ok())
83    }
84
85    /// Updates the [`State`] by inspecting the given bytes (which should have been acquired from the G11's HID interface).
86    /// This, combined with the previously known state, will allow an [`Event`] to be inferred as the signal's meaning.
87    ///
88    /// Note: The G11 macro interface emits HID packets of 9 bytes. Anything less will produce an [`EventError`]
89    /// The provided buffer may be larger than that, but only the first 9 bytes will be inspected.
90    pub fn try_consume_event(&mut self, usb_bytes: &[u8]) -> Result<Event, EventError> {
91        let new_state = multikey::MultiKey::try_from(usb_bytes)
92            .map_err(|_| EventError::InvalidBytes)?;
93
94        let old_state = mem::replace(&mut self.0, new_state);
95
96        //Handle the most likely states first (going from nothing to pressed, or from pressed to nothing)
97        if old_state.is_empty() {
98            Key::try_from(new_state)
99                .map_err(|_| EventError::UnreconcilableState)
100                .map(|key| Event { key, action: Action::Pressed })
101        }
102        else if new_state.is_empty() {
103            Key::try_from(old_state)
104                .map_err(|_| EventError::UnreconcilableState)
105                .map(|key| Event { key, action: Action::Released })
106        }
107        else { //Multiple keys are/were pressed, so a more thorough diff
108            let changed_key = new_state.symmetric_difference(old_state);
109            Key::try_from(changed_key)
110                .map_err(|_| EventError::UnreconcilableState)
111                .map(|key| {
112                    let action = if old_state.contains(changed_key) { Action::Released } else { Action::Pressed };
113                    Event { key, action }
114                })
115        }
116    }
117
118    /// Produces an HID Feature Report (which you may then submit to the G11's HID interface)
119    /// that will cause only the given [`Key`] LEDs to be lit (and all others unlit).
120    ///
121    /// Will return `None` if the request would be fruitless (if these exact LEDs are already lit)
122    #[must_use]
123    pub fn set_exact_lit_leds(&mut self, lit_keys: &[Key]) -> Option<[u8; 4]> {
124        self.set_exact_lit_leds_if_changed(
125            lit_keys.iter()
126                .filter_map(|key| led::Led::try_from(*key).ok())
127                .reduce(|a, b| a | b)
128                .unwrap_or_default()
129        )
130    }
131
132    /// Produces an HID Feature Report (which you may then submit to the G11's HID interface)
133    /// that will cause the given [`Key`] LED to transition from unlit to lit, leaving all other LEDs alone.
134    ///
135    /// Will return `None` if the request would be fruitless (if the LED is already lit or a key with no LED is passed)
136    #[must_use]
137    pub fn light_led(&mut self, key: Key) -> Option<[u8; 4]> {
138        led::Led::try_from(key).ok()
139            .map(|new| self.1.map_or(new, |current | current | new))
140            .and_then(|desired| self.set_exact_lit_leds_if_changed(desired))
141    }
142
143    /// Produces an HID Feature Report (which you may then submit to the G11's HID interface)
144    /// that will cause the given [`Key`] LED to transition from lit to unlit, leaving all other LEDs alone.
145    ///
146    /// Will return `None` if the request would be fruitless (if the LED is already unlit or a key with no LED is passed)
147    #[must_use]
148    pub fn extinguish_led(&mut self, key: Key) -> Option<[u8; 4]> {
149        led::Led::try_from(key).ok()
150            .and_then(|old| self.1.map(|current| current & !old))
151            .and_then(|desired| self.set_exact_lit_leds_if_changed(desired))
152    }
153
154    fn set_exact_lit_leds_if_changed(&mut self, desired: led::Led) -> Option<[u8; 4]> {
155        if self.1.is_some_and(|current| current == desired) {
156            None
157        } else {
158            self.1 = Some(desired);
159            Some(desired.into())
160        }
161    }
162}
163
164/// Errors that may arise during [`State::try_consume_event`]
165#[derive(Debug, Display, Error, Clone)]
166pub enum EventError {
167    /// The given bytes did not represent a valid G11 macro USB event.
168    /// Internal state was not updated.
169    #[display("invalid bytes")]
170    InvalidBytes,
171    /// The given bytes were valid, and the internal state was updated.
172    /// However, an individual [`Action`] could not be determined as the cause
173    /// (for example, if the first event observed is a 'release')
174    #[display("unreconcilable state")]
175    UnreconcilableState,
176}
177
178#[derive(Debug, Display, Error, Default, Clone, Copy, PartialEq, Eq)]
179#[display("unrecognized key")]
180#[doc(hidden = true)]
181pub struct UnrecognizedKey;
182
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    /// <https://rust-lang.github.io/api-guidelines/interoperability.html#types-are-send-and-sync-where-possible-c-send-sync>
189    mod auto_trait_regression {
190        use super::*;
191
192        #[test]
193        fn test_send() {
194            fn assert_send<T: Send>() {}
195            assert_send::<Event>();
196            assert_send::<EventError>();
197            assert_send::<State>();
198        }
199
200        #[test]
201        fn test_sync() {
202            fn assert_sync<T: Sync>() {}
203            assert_sync::<Event>();
204            assert_sync::<EventError>();
205            assert_sync::<State>();
206        }
207    }
208}