kiibohd_keyscanning/
lib.rs

1// Copyright 2021 Zion Koyl
2// Copyright 2021-2023 Jacob Alexander
3//
4// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
5// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
6// http://opensource.org/licenses/MIT>, at your option. This file may not be
7// copied, modified, or distributed except according to those terms.
8
9#![no_std]
10
11pub mod state;
12
13pub use self::state::{KeyState, State};
14use embedded_hal::digital::v2::{InputPin, IoPin, OutputPin, PinState};
15
16#[cfg(feature = "kll-core")]
17pub trait KeyScanning<const MAX_EVENTS: usize> {
18    fn generate_events(&self, index: usize) -> kll_core::layout::TriggerEventIterator<MAX_EVENTS>;
19}
20
21/// Records momentary push button events
22///
23/// Cycles can be converted to time by multiplying by the scan period (Matrix::period())
24#[derive(Copy, Clone, Debug, PartialEq, Eq)]
25#[cfg_attr(feature = "defmt", derive(defmt::Format))]
26pub enum KeyEvent {
27    On {
28        /// Cycles since the last state change
29        cycles_since_state_change: u32,
30    },
31    Off {
32        /// Key is idle (a key can only be idle in the off state)
33        idle: bool,
34        /// Cycles since the last state change
35        cycles_since_state_change: u32,
36    },
37}
38
39/// This struct handles scanning and strobing of the key matrix.
40///
41/// It also handles the debouncing of key input to ensure acurate keypresses are being read.
42/// OutputPin's are passed as columns (cols) which are strobed.
43/// IoPins are functionally InputPins (rows) which are read. Rows are IoPins in order to drain the
44/// row/sense between strobes to prevent stray capacitance.
45///
46/// ```rust,ignore
47/// const CSIZE: usize = 18; // Number of columns
48/// const RSIZE: usize = 6; // Number of rows
49/// const MSIZE: usize = RSIZE * CSIZE; // Total matrix size
50/// // Period of time it takes to re-scan a column (everything must be constant time!)
51/// const SCAN_PERIOD_US = 40;
52/// // Debounce timer in us. Can only be as precise as a multiple of SCAN_PERIOD_US.
53/// // Per-key timer is reset if the raw gpio reading changes for any reason.
54/// const DEBOUNCE_US = 5000; // 5 ms
55/// // Idle timer in ms. Only valid if the switch is in the off state.
56/// const IDLE_MS = 600_0000; // 600 seconds or 10 minutes
57///
58/// let cols = [
59///     pins.strobe1.downgrade(),
60///     pins.strobe2.downgrade(),
61///     pins.strobe3.downgrade(),
62///     pins.strobe4.downgrade(),
63///     pins.strobe5.downgrade(),
64///     pins.strobe6.downgrade(),
65///     pins.strobe7.downgrade(),
66///     pins.strobe8.downgrade(),
67///     pins.strobe9.downgrade(),
68///     pins.strobe10.downgrade(),
69///     pins.strobe11.downgrade(),
70///     pins.strobe12.downgrade(),
71///     pins.strobe13.downgrade(),
72///     pins.strobe14.downgrade(),
73///     pins.strobe15.downgrade(),
74///     pins.strobe16.downgrade(),
75///     pins.strobe17.downgrade(),
76///     pins.strobe18.downgrade(),
77/// ];
78///
79/// let rows = [
80///     pins.sense1.downgrade(),
81///     pins.sense2.downgrade(),
82///     pins.sense3.downgrade(),
83///     pins.sense4.downgrade(),
84///     pins.sense5.downgrade(),
85///     pins.sense6.downgrade(),
86/// ];
87///
88/// let mut matrix = Matrix::<OutputPin, InputPin, CSIZE, RSIZE, MSIZE, SCAN_PERIOD_US, DEBOUNCE_US,
89/// IDLE_MS>::new(cols, rows);
90///
91/// // Prepare first strobe
92/// matrix.next_strobe().unwrap();
93///
94/// // --> This next part must be done in constant time (SCAN_PERIOD_US) <--
95/// let state = matrix.sense().unwrap();
96/// matrix.next_strobe().unwrap();
97/// ```
98pub struct Matrix<
99    C: OutputPin,
100    R: InputPin,
101    const CSIZE: usize,
102    const RSIZE: usize,
103    const MSIZE: usize,
104    const SCAN_PERIOD_US: u32,
105    const DEBOUNCE_US: u32,
106    const IDLE_MS: u32,
107> {
108    /// Strobe GPIOs (columns)
109    cols: [C; CSIZE],
110    /// Sense GPIOs (rows)
111    rows: [R; RSIZE],
112    /// Current GPIO column being strobed
113    cur_strobe: usize,
114    /// Recorded state of the entire matrix
115    state_matrix: [KeyState<CSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>; MSIZE],
116}
117
118impl<
119        C: OutputPin,
120        R: InputPin,
121        const CSIZE: usize,
122        const RSIZE: usize,
123        const MSIZE: usize,
124        const SCAN_PERIOD_US: u32,
125        const DEBOUNCE_US: u32,
126        const IDLE_MS: u32,
127    > Matrix<C, R, CSIZE, RSIZE, MSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>
128{
129    pub fn new<'a, E: 'a>(cols: [C; CSIZE], rows: [R; RSIZE]) -> Result<Self, E>
130    where
131        C: OutputPin<Error = E>,
132        E: core::convert::From<<C as OutputPin>::Error>,
133    {
134        let state_matrix = [KeyState::<CSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>::new(); MSIZE];
135        let mut res = Self {
136            cols,
137            rows,
138            cur_strobe: CSIZE - 1,
139            state_matrix,
140        };
141
142        // Reset strobe position and make sure all strobes are off
143        res.clear()?;
144        Ok(res)
145    }
146
147    /// Clears strobes
148    /// Resets strobe counter to the last element (so next_strobe starts at 0)
149    pub fn clear<'a, E: 'a>(&'a mut self) -> Result<(), E>
150    where
151        C: OutputPin<Error = E>,
152    {
153        // Clear all strobes
154        for c in self.cols.iter_mut() {
155            c.set_low()?;
156        }
157
158        // Reset strobe position
159        self.cur_strobe = CSIZE - 1;
160        Ok(())
161    }
162
163    /// Next strobe
164    pub fn next_strobe<'a, E: 'a>(&'a mut self) -> Result<usize, E>
165    where
166        C: OutputPin<Error = E> + IoPin<R, C>,
167        R: InputPin<Error = E> + IoPin<R, C>,
168        E: core::convert::From<<R as IoPin<R, C>>::Error>
169            + core::convert::From<<C as IoPin<R, C>>::Error>,
170    {
171        // Unset current strobe
172        self.cols[self.cur_strobe].set_low()?;
173
174        // Drain stray potential from sense lines
175        // NOTE: This is unsafe because the gpio are stored in an array and (likely) do not implement
176        //       copy or clone. Since they are in an array, we can't move them either.
177        //       Since we're just temporarily sinking the pin and putting it back, this is safe to
178        //       do.
179        for s in self.rows.iter_mut() {
180            let ptr = s as *const R;
181            unsafe {
182                let row = core::ptr::read(ptr);
183                // Temporarily sink sense gpios and reset to sense/read gpio
184                row.into_output_pin(PinState::Low)?.into_input_pin()?;
185            }
186        }
187
188        // Check for roll-over condition
189        if self.cur_strobe >= CSIZE - 1 {
190            self.cur_strobe = 0;
191        } else {
192            self.cur_strobe += 1;
193        }
194
195        // Set new strobe
196        self.cols[self.cur_strobe].set_high()?;
197
198        Ok(self.cur_strobe)
199    }
200
201    /// Current strobe
202    pub fn strobe(&self) -> usize {
203        self.cur_strobe
204    }
205
206    /// Sense a column of switches
207    ///
208    /// Returns the results of each row for the currently strobed column and the measured strobe
209    pub fn sense<'a, E: 'a>(&'a mut self) -> Result<([KeyEvent; RSIZE], usize), E>
210    where
211        E: core::convert::From<<R as InputPin>::Error>,
212    {
213        let mut res = [KeyEvent::Off {
214            idle: false,
215            cycles_since_state_change: 0,
216        }; RSIZE];
217
218        for (i, r) in self.rows.iter().enumerate() {
219            // Read GPIO
220            let on = r.is_high()?;
221            // Determine matrix index
222            let index = self.cur_strobe * RSIZE + i;
223            // Record GPIO event and determine current status after debouncing algorithm
224            let (keystate, idle, cycles_since_state_change) = self.state_matrix[index].record(on);
225
226            // Assign KeyEvent using the output keystate
227            res[i] = if keystate == State::On {
228                KeyEvent::On {
229                    cycles_since_state_change,
230                }
231            } else {
232                KeyEvent::Off {
233                    idle,
234                    cycles_since_state_change,
235                }
236            };
237        }
238
239        Ok((res, self.cur_strobe))
240    }
241
242    /// Return the KeyState for a given index
243    pub fn state(
244        &self,
245        index: usize,
246    ) -> Option<KeyState<CSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>> {
247        if index >= self.state_matrix.len() {
248            None
249        } else {
250            Some(self.state_matrix[index])
251        }
252    }
253
254    /// Generate event from KeyState
255    /// Useful when trying to determine if a key has not been pressed
256    pub fn generate_key_event(&self, index: usize) -> Option<KeyEvent> {
257        let state = self.state(index);
258
259        state.map(|state| match state.state().0 {
260            State::On => KeyEvent::On {
261                cycles_since_state_change: state.cycles_since_state_change(),
262            },
263            State::Off => KeyEvent::Off {
264                idle: state.idle(),
265                cycles_since_state_change: state.cycles_since_state_change(),
266            },
267        })
268    }
269}
270
271#[cfg(feature = "kll-core")]
272mod converters {
273    #[cfg(feature = "defmt")]
274    use defmt::*;
275    #[cfg(not(feature = "defmt"))]
276    use log::*;
277
278    use crate::*;
279    use heapless::Vec;
280    use kll_core::layout::TriggerEventIterator;
281
282    impl<
283            C: OutputPin,
284            R: InputPin,
285            const CSIZE: usize,
286            const RSIZE: usize,
287            const MSIZE: usize,
288            const SCAN_PERIOD_US: u32,
289            const DEBOUNCE_US: u32,
290            const IDLE_MS: u32,
291            const MAX_EVENTS: usize,
292        > KeyScanning<MAX_EVENTS> for Matrix<C, R, CSIZE, RSIZE, MSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>
293    {
294        /// Convert matrix state into a TriggerEvent
295        fn generate_events(&self, index: usize) -> TriggerEventIterator<MAX_EVENTS> {
296            self.generate_key_event(index)
297                .unwrap()
298                .trigger_events(index, false)
299        }
300    }
301
302    impl KeyEvent {
303        pub fn trigger_events<const MAX_EVENTS: usize>(
304            &self,
305            index: usize,
306            ignore_off: bool,
307        ) -> TriggerEventIterator<MAX_EVENTS> {
308            let mut events = Vec::new();
309
310            // Handle on/off events
311            match self {
312                KeyEvent::On {
313                    cycles_since_state_change,
314                } => {
315                    if *cycles_since_state_change == 0 {
316                        trace!("Reading: {} {}", index, self);
317                        events
318                            .push(kll_core::TriggerEvent::Switch {
319                                state: kll_core::trigger::Phro::Press,
320                                index: index as u16,
321                                last_state: 0,
322                            })
323                            .unwrap();
324                    } else {
325                        events
326                            .push(kll_core::TriggerEvent::Switch {
327                                state: kll_core::trigger::Phro::Hold,
328                                index: index as u16,
329                                last_state: *cycles_since_state_change,
330                            })
331                            .unwrap();
332                    }
333                }
334                KeyEvent::Off {
335                    cycles_since_state_change,
336                    ..
337                } => {
338                    if *cycles_since_state_change == 0 {
339                        trace!("Reading: {} {}", index, self);
340                        events
341                            .push(kll_core::TriggerEvent::Switch {
342                                state: kll_core::trigger::Phro::Release,
343                                index: index as u16,
344                                last_state: 0,
345                            })
346                            .unwrap();
347                    // Ignore off events unless ignore_off is set
348                    } else if !ignore_off {
349                        events
350                            .push(kll_core::TriggerEvent::Switch {
351                                state: kll_core::trigger::Phro::Off,
352                                index: index as u16,
353                                last_state: *cycles_since_state_change,
354                            })
355                            .unwrap();
356                    }
357                }
358            }
359            TriggerEventIterator::new(events)
360        }
361    }
362}