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}