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}