Skip to main content

pokeys_lib/
keyboard_matrix.rs

1//! Matrix keyboard support
2//!
3//! This module provides matrix keyboard functionality for PoKeys devices using the official
4//! protocol specification (command 0xCA). The implementation supports up to 16x8 matrix
5//! keyboards with proper key indexing according to the PoKeys protocol.
6//!
7//! ## Key Features
8//! - Supports matrix keyboards up to 16 rows x 8 columns
9//! - Protocol-compliant implementation using command 0xCA
10//! - Proper key indexing with 8-column internal layout
11//! - Real-time key state monitoring
12//! - Configurable pin assignments for rows and columns
13//!
14//! ## Key Indexing
15//! The PoKeys protocol uses a fixed 8-column internal layout regardless of configured width:
16//! - Row 0: keys 0-7 (only 0-width used)
17//! - Row 1: keys 8-15 (only 8-(8+width) used)
18//! - Row 2: keys 16-23, etc.
19//!
20//! ## Byte numbering convention
21//! The protocol specification uses **1-based** byte indices ("byte 2: 0xCA",
22//! "byte 9: new configuration", etc.); the Rust code uses **0-based** indices into
23//! the request/response buffers. Mapping: spec byte `N` → `request[N-1]` / `response[N-1]`.
24//! The data payload of a request lands at `request[8]` (= spec byte 9), so the `data` slice
25//! passed to `send_request_with_data` has `data[k]` ≡ spec byte `9+k`.
26//!
27//! ## Spec vs vendor reference library
28//! The matrix keyboard section of the protocol specification is internally inconsistent:
29//! the request layout shows "If option == 16" but footnote 6 says "Use option 1 to setup".
30//! The vendor reference library (PoLabsEE/PoKeysLib `PK_MatrixKBConfigurationSet`) uses
31//! **option 1** for the configuration write — that is the load-bearing answer. Sending
32//! option 16 is silently echoed but does not bind pins to row/column slots.
33//!
34//! The configuration write is also a **two-phase sequence** in the vendor library:
35//! deactivate (option 1, enable=0) → key mappings → activate (option 1, enable=1). The
36//! single-shot variant doesn't reliably enable scanning. [`crate::PoKeysDevice::configure_matrix_keyboard`]
37//! follows the vendor sequence.
38//!
39//! ## Required setup sequence
40//! [`crate::PoKeysDevice::configure_matrix_keyboard`] handles the underlying pin-function
41//! prerequisites internally — row pins are forced to `DigitalOutput`, column pins to
42//! `DigitalInput` — so callers do not need to set pin functions first.
43//!
44//! ## Example Usage
45//! ```rust,no_run
46//! use pokeys_lib::*;
47//!
48//! fn main() -> Result<()> {
49//!     let mut device = connect_to_device(0)?;
50//!
51//!     // Configure 4x4 matrix keyboard. Pin functions are set automatically;
52//!     // a successful return guarantees the device confirmed the configuration.
53//!     let column_pins = [21, 22, 23, 24];
54//!     let row_pins = [13, 14, 15, 16];
55//!     device.configure_matrix_keyboard(4, 4, &column_pins, &row_pins)?;
56//!
57//!     // Read keyboard state
58//!     device.read_matrix_keyboard()?;
59//!     let key_pressed = device.matrix_keyboard.get_key_state(0, 0);
60//!
61//!     Ok(())
62//! }
63//! ```
64
65use serde::{Deserialize, Serialize};
66
67/// A snapshot of the device-side matrix-keyboard configuration, returned by
68/// [`crate::PoKeysDevice::get_matrix_keyboard_configuration`].
69///
70/// Read-only view of what the firmware currently has stored — reflects the
71/// result of the most recent successful configuration write or pre-existing
72/// state from non-volatile storage. Useful for diff-against-device flows.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct MatrixKeyboardConfig {
75    /// `true` if the matrix keyboard is currently enabled (bit 0 of the
76    /// configuration byte at spec byte 9).
77    pub enabled: bool,
78    /// Number of columns (1..=8). Decoded from `(size_byte >> 4) + 1`.
79    pub width: u8,
80    /// Number of rows (1..=16). Decoded from `(size_byte & 0x0F) + 1`.
81    pub height: u8,
82    /// 1-based pin numbers for rows 0..16. Trailing entries beyond `height`
83    /// are 0. Stored 1-based for human readability; the protocol uses
84    /// 0-based pin codes on the wire.
85    pub row_pins: [u8; 16],
86    /// 1-based pin numbers for columns 0..8. Trailing entries beyond `width`
87    /// are 0.
88    pub column_pins: [u8; 8],
89    /// Direct/macro mapping bitmap, 128 bits. Bit `k` set means key `k`
90    /// uses macro mapping; clear means direct key mapping.
91    pub direct_macro_bitmap: [u8; 16],
92    /// Alternate-function pin (1-based, or 0 if disabled). When set, the
93    /// state of this digital input pin selects between the primary and
94    /// alternate keyboard mapping for each key.
95    pub alternate_function_pin: u8,
96    /// Scanning decimation factor (0..=50). Higher values reduce the
97    /// device-side scan rate.
98    pub scanning_decimation: u8,
99}
100
101impl MatrixKeyboardConfig {
102    /// Wire size byte: `(width-1) << 4 | (height-1)`.
103    pub fn size_byte(&self) -> u8 {
104        ((self.width.saturating_sub(1)) << 4) | (self.height.saturating_sub(1) & 0x0F)
105    }
106}
107
108/// Matrix keyboard configuration
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct MatrixKeyboard {
111    pub configuration: u8,
112    pub width: u8,
113    pub height: u8,
114    pub scanning_decimation: u8,
115    pub column_pins: [u8; 8],
116    pub row_pins: [u8; 16],
117    pub macro_mapping_options: Vec<u8>,
118    pub key_mapping_key_code: Vec<u8>,
119    pub key_mapping_key_modifier: Vec<u8>,
120    pub key_mapping_triggered_key: Vec<u8>,
121    pub key_mapping_key_code_up: Vec<u8>,
122    pub key_mapping_key_modifier_up: Vec<u8>,
123    pub key_values: Vec<u8>,
124}
125
126impl MatrixKeyboard {
127    pub fn new() -> Self {
128        Self {
129            configuration: 0,
130            width: 0,
131            height: 0,
132            scanning_decimation: 0,
133            column_pins: [0; 8],
134            row_pins: [0; 16],
135            macro_mapping_options: vec![0; 128],
136            key_mapping_key_code: vec![0; 128],
137            key_mapping_key_modifier: vec![0; 128],
138            key_mapping_triggered_key: vec![0; 128],
139            key_mapping_key_code_up: vec![0; 128],
140            key_mapping_key_modifier_up: vec![0; 128],
141            key_values: vec![0; 128],
142        }
143    }
144
145    pub fn is_enabled(&self) -> bool {
146        self.configuration != 0
147    }
148
149    pub fn get_key_state(&self, row: usize, col: usize) -> bool {
150        if row >= self.height as usize || col >= self.width as usize {
151            return false;
152        }
153        // Protocol uses 8-column layout internally: key_index = row * 8 + col
154        let key_index = row * 8 + col;
155        if key_index < self.key_values.len() {
156            self.key_values[key_index] != 0
157        } else {
158            false
159        }
160    }
161}
162
163impl Default for MatrixKeyboard {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_matrix_keyboard_config_size_byte() {
175        let cfg = MatrixKeyboardConfig {
176            enabled: true,
177            width: 4,
178            height: 12,
179            row_pins: [0u8; 16],
180            column_pins: [0u8; 8],
181            direct_macro_bitmap: [0u8; 16],
182            alternate_function_pin: 0,
183            scanning_decimation: 0,
184        };
185        // (4-1) << 4 | (12-1) = 0x3B
186        assert_eq!(cfg.size_byte(), 0x3B);
187    }
188
189    #[test]
190    fn test_matrix_keyboard_config_size_byte_min() {
191        let cfg = MatrixKeyboardConfig {
192            enabled: false,
193            width: 1,
194            height: 1,
195            row_pins: [0u8; 16],
196            column_pins: [0u8; 8],
197            direct_macro_bitmap: [0u8; 16],
198            alternate_function_pin: 0,
199            scanning_decimation: 0,
200        };
201        assert_eq!(cfg.size_byte(), 0);
202    }
203
204    #[test]
205    fn test_matrix_keyboard_creation() {
206        let kb = MatrixKeyboard::new();
207        assert!(!kb.is_enabled());
208        assert_eq!(kb.width, 0);
209        assert_eq!(kb.height, 0);
210    }
211
212    #[test]
213    fn test_matrix_keyboard_configuration() {
214        let mut kb = MatrixKeyboard::new();
215
216        // Test initial state
217        assert_eq!(kb.configuration, 0);
218        assert_eq!(kb.width, 0);
219        assert_eq!(kb.height, 0);
220        assert_eq!(kb.scanning_decimation, 0);
221        assert_eq!(kb.column_pins.len(), 8);
222        assert_eq!(kb.row_pins.len(), 16);
223
224        // Test enabling
225        kb.configuration = 1;
226        kb.width = 4;
227        kb.height = 4;
228
229        assert!(kb.is_enabled());
230        assert_eq!(kb.width, 4);
231        assert_eq!(kb.height, 4);
232    }
233
234    #[test]
235    fn test_get_key_state() {
236        let mut kb = MatrixKeyboard::new();
237        kb.width = 4;
238        kb.height = 4;
239
240        // Test bounds checking
241        assert!(!kb.get_key_state(0, 0)); // Should be false initially
242        assert!(!kb.get_key_state(4, 0)); // Out of bounds
243        assert!(!kb.get_key_state(0, 4)); // Out of bounds
244
245        // Test setting key state
246        kb.key_values[0] = 1; // Row 0, Col 0
247        assert!(kb.get_key_state(0, 0));
248
249        kb.key_values[8] = 1; // Row 1, Col 0 (1*8 + 0 = 8)
250        assert!(kb.get_key_state(1, 0));
251    }
252
253    #[test]
254    fn test_key_index_calculation() {
255        let mut kb = MatrixKeyboard::new();
256        kb.width = 3;
257        kb.height = 3;
258
259        // Test key index calculation: row * 8 + col (protocol uses 8-column layout)
260        kb.key_values[0] = 1; // (0,0) = 0*8 + 0 = 0
261        kb.key_values[1] = 1; // (0,1) = 0*8 + 1 = 1
262        kb.key_values[8] = 1; // (1,0) = 1*8 + 0 = 8
263        kb.key_values[9] = 1; // (1,1) = 1*8 + 1 = 9
264
265        assert!(kb.get_key_state(0, 0));
266        assert!(kb.get_key_state(0, 1));
267        assert!(!kb.get_key_state(0, 2));
268        assert!(kb.get_key_state(1, 0));
269        assert!(kb.get_key_state(1, 1));
270        assert!(!kb.get_key_state(1, 2));
271    }
272}