Skip to main content

pokeys_lib/
device.rs

1//! Device enumeration, connection, and management
2
3use crate::communication::{CommunicationManager, NetworkInterface, UsbHidInterface};
4use crate::encoders::EncoderData;
5use crate::error::{PoKeysError, Result};
6use crate::io::{PinData, PinFunction};
7use crate::keyboard_matrix::{MatrixKeyboard, MatrixKeyboardConfig};
8use crate::lcd::LcdData;
9use crate::matrix::MatrixLed;
10use crate::pulse_engine::PulseEngineV2;
11use crate::pwm::PwmData;
12use crate::sensors::EasySensor;
13use crate::types::*;
14use std::sync::{LazyLock, Mutex};
15use std::time::Duration;
16
17/// Matrix keyboard ID byte (spec § Matrix keyboard, byte 4).
18///
19/// Hard-coded to 0: every current PoKeys firmware variant exposes exactly
20/// one matrix keyboard, addressed as keyboard 0. The field exists in the
21/// spec for forward compatibility (multi-keyboard or PoNET/I²C-mapped
22/// keyboard variants), and may need to be promoted to a parameter on the
23/// public matrix-keyboard methods if such variants ship.
24const MATRIX_KEYBOARD_ID: u8 = 0;
25
26/// Matrix-keyboard `0xCA` write subcommand (spec § Matrix keyboard).
27///
28/// **The spec is ambiguous on which option byte writes the configuration.**
29/// The "If option == 16" header in the request layout disagrees with
30/// footnote 6 ("Use option 1 to setup of the matrix keyboard"). The vendor
31/// reference library (PoLabsEE/PoKeysLib `PK_MatrixKBConfigurationSet`)
32/// uses **option 1** — that is the authoritative answer. Sending option 16
33/// is silently accepted by the firmware (echo only) but does not bind pins
34/// to row/column slots, leaving the matrix dead.
35const MATRIX_KB_OPT_WRITE_CONFIG: u8 = 1;
36
37/// Matrix-keyboard `0xCA` read subcommand (spec § Matrix keyboard).
38///
39/// Per spec footnote 11 ("any option < 12 not previously defined for setup
40/// of matrix keyboard, settings are only read"), and the vendor reference
41/// library `PK_MatrixKBConfigurationGet`. Note that the *write* options
42/// (1, 2..9, 16, 22..29, 50) all live in this same range — this constant
43/// must NOT collide with any of them. Option 10 is the standard read.
44const MATRIX_KB_OPT_READ_CONFIG: u8 = 10;
45
46/// Encode 16 keys (codes + modifiers, optionally a triggered-mode bitmap) into the
47/// `data` payload for a `0xCA` key-mapping write (options 2-9 / 22-29).
48///
49/// Layout matches spec § Matrix keyboard:
50/// - data[0..16]  → bytes 9-24:  16 key codes
51/// - data[16..32] → bytes 25-40: 16 key modifiers
52/// - data[32..34] → bytes 41-42: triggered-mode bitmap (16 bits, LSB-first; only for down-event options 2-9)
53fn encode_matrix_kb_mapping_chunk(
54    codes: &[u8],
55    modifiers: &[u8],
56    triggered: Option<&[u8]>,
57    chunk_base: usize,
58) -> [u8; 34] {
59    let mut data = [0u8; 34];
60    for i in 0..16 {
61        let idx = chunk_base + i;
62        if idx < codes.len() {
63            data[i] = codes[idx];
64        }
65        if idx < modifiers.len() {
66            data[16 + i] = modifiers[idx];
67        }
68    }
69    if let Some(triggered) = triggered {
70        let mut bitmap: u16 = 0;
71        for i in 0..16 {
72            let idx = chunk_base + i;
73            if idx < triggered.len() && triggered[idx] != 0 {
74                bitmap |= 1 << i;
75            }
76        }
77        data[32] = (bitmap & 0xFF) as u8;
78        data[33] = (bitmap >> 8) as u8;
79    }
80    data
81}
82
83/// Decode a 16-key key-mapping read response into the supplied buffers.
84/// Inverse of [`encode_matrix_kb_mapping_chunk`].
85fn decode_matrix_kb_mapping_chunk(
86    response: &[u8],
87    codes: &mut [u8],
88    modifiers: &mut [u8],
89    triggered: Option<&mut [u8]>,
90    chunk_base: usize,
91) {
92    // response[8..24] = codes, response[24..40] = modifiers, response[40..42] = triggered bitmap
93    for i in 0..16 {
94        let idx = chunk_base + i;
95        if idx < codes.len() {
96            codes[idx] = response[8 + i];
97        }
98        if idx < modifiers.len() {
99            modifiers[idx] = response[24 + i];
100        }
101    }
102    if let Some(triggered) = triggered {
103        let bitmap = u16::from_le_bytes([response[40], response[41]]);
104        for i in 0..16 {
105            let idx = chunk_base + i;
106            if idx < triggered.len() {
107                triggered[idx] = if (bitmap & (1 << i)) != 0 { 1 } else { 0 };
108            }
109        }
110    }
111}
112
113/// Build the configuration payload for `0xCA` option 1.
114///
115/// Caller is expected to validate `width` (1-8) and `height` (1-16) and that the
116/// pin slices contain at least `width` / `height` entries.
117///
118/// Layout matches spec § Matrix keyboard / vendor reference impl:
119/// - `data[0]`      = byte 9:  enable bit (`enabled`)
120/// - `data[1]`      = byte 10: (width-1) << 4 | (height-1)
121/// - `data[2..10]`  = bytes 11-18: row pins 0-7 (0-based pin codes)
122/// - `data[10..18]` = bytes 19-26: column pins 0-7
123/// - `data[18..34]` = bytes 27-42: direct/macro bitmap (zero = all direct)
124/// - `data[34..42]` = bytes 43-50: row pins 8-15 (always sent — vendor
125///   reference impl writes the full extended-row block unconditionally;
126///   it's harmless when height ≤ 8 because the firmware ignores it)
127/// - `data[42]`     = byte 51:  alternate-function pin (0 = disabled)
128/// - `data[43]`     = byte 52:  (reserved on option 1; scanning decimation
129///   is sent via separate option-50 write per the vendor sequence)
130fn encode_matrix_kb_config_payload(
131    enabled: bool,
132    width: u8,
133    height: u8,
134    column_pins: &[u8],
135    row_pins: &[u8],
136) -> [u8; 55] {
137    let mut data = [0u8; 55];
138    data[0] = if enabled { 1 } else { 0 };
139    data[1] = ((width - 1) << 4) | (height - 1);
140
141    for (i, &pin) in row_pins.iter().enumerate().take(8) {
142        data[2 + i] = pin.saturating_sub(1);
143    }
144    for (i, &pin) in column_pins.iter().enumerate().take(8) {
145        data[10 + i] = pin.saturating_sub(1);
146    }
147    // Vendor reference impl writes the extended-row block unconditionally;
148    // mirror that even when height ≤ 8 (the firmware treats those bytes as
149    // unused based on the size field in data[1]).
150    for (offset, &pin) in row_pins.iter().enumerate().skip(8).take(8) {
151        data[34 + (offset - 8)] = pin.saturating_sub(1);
152    }
153
154    data
155}
156
157fn validate_matrix_kb_mapping_response(response: &[u8], option: u8) -> Result<()> {
158    if response[1] != 0xCA {
159        return Err(PoKeysError::Protocol(format!(
160            "Invalid response command for key mapping read (option {option}): 0x{:02X}",
161            response[1]
162        )));
163    }
164    if response.len() < 42 {
165        return Err(PoKeysError::Protocol(format!(
166            "Key mapping response too short for option {option}: {} bytes (need 42)",
167            response.len()
168        )));
169    }
170    Ok(())
171}
172
173/// Parse a `0xCA` option-10 (`MATRIX_KB_OPT_READ_CONFIG`) readback response
174/// into a [`MatrixKeyboardConfig`].
175///
176/// Layout (vendor reference: `PK_MatrixKBConfigurationGet`):
177/// - `response[8]`      = byte 9:  config (bit 0 = enable)
178/// - `response[9]`      = byte 10: size — bit 0-3 = height-1, bit 4-7 = width-1
179/// - `response[10..18]` = bytes 11-18: row pins 0-7 (0-based pin codes)
180/// - `response[18..26]` = bytes 19-26: column pins 0-7
181/// - `response[26..42]` = bytes 27-42: direct/macro bitmap (16 bytes, 128 bits)
182/// - `response[42..50]` = bytes 43-50: row pins 8-15 (only when height > 8)
183/// - `response[50]`     = byte 51:  alternate-function pin (pinID+1, 0 = disabled)
184/// - `response[51]`     = byte 52:  scanning decimation (0..=50)
185fn parse_matrix_kb_config_readback(response: &[u8]) -> Result<MatrixKeyboardConfig> {
186    if response.len() < 2 || response[1] != 0xCA {
187        return Err(PoKeysError::Protocol(format!(
188            "matrix keyboard config readback: echo mismatch: 0x{:02X}",
189            response.get(1).copied().unwrap_or(0)
190        )));
191    }
192    if response.len() < 52 {
193        return Err(PoKeysError::Protocol(format!(
194            "matrix keyboard config readback too short: {} bytes (need 52)",
195            response.len()
196        )));
197    }
198
199    let enabled = (response[8] & 0x01) != 0;
200    let size_byte = response[9];
201    let width = (size_byte >> 4) + 1;
202    let height = (size_byte & 0x0F) + 1;
203
204    let mut row_pins = [0u8; 16];
205    for (i, slot) in row_pins.iter_mut().enumerate().take(8) {
206        // Wire stores 0-based pin codes; expose 1-based, with 0 meaning
207        // "no pin assigned".
208        *slot = response[10 + i].wrapping_add(1);
209    }
210    for (offset, slot) in row_pins.iter_mut().enumerate().skip(8).take(8) {
211        *slot = response[42 + (offset - 8)].wrapping_add(1);
212    }
213
214    let mut column_pins = [0u8; 8];
215    for (i, slot) in column_pins.iter_mut().enumerate() {
216        *slot = response[18 + i].wrapping_add(1);
217    }
218
219    let mut direct_macro_bitmap = [0u8; 16];
220    direct_macro_bitmap.copy_from_slice(&response[26..42]);
221
222    Ok(MatrixKeyboardConfig {
223        enabled,
224        width,
225        height,
226        row_pins,
227        column_pins,
228        direct_macro_bitmap,
229        alternate_function_pin: response[50],
230        scanning_decimation: response[51],
231    })
232}
233
234/// Compare a parsed `MatrixKeyboardConfig` against an expected configuration
235/// and return a `Protocol` error describing the first mismatch (if any).
236///
237/// Used to catch firmware silent rejections (e.g. configuration locked, an
238/// out-of-range pin code, or a pin the firmware refuses to use as
239/// row/col).
240fn verify_matrix_kb_config_matches(
241    actual: &MatrixKeyboardConfig,
242    expected_width: u8,
243    expected_height: u8,
244    expected_column_pins: &[u8],
245    expected_row_pins: &[u8],
246    expected_enabled: bool,
247) -> Result<()> {
248    if actual.enabled != expected_enabled {
249        return Err(PoKeysError::Protocol(format!(
250            "configure_matrix_keyboard: device reports enabled={}, expected {expected_enabled} \
251             (configuration may be locked; raw config byte=0x{:02X})",
252            actual.enabled,
253            if actual.enabled { 0x01 } else { 0x00 }
254        )));
255    }
256    // When verifying after a deactivate, dimensions / pins may legitimately
257    // be stale; only the enable bit is interesting.
258    if !expected_enabled {
259        return Ok(());
260    }
261
262    if actual.width != expected_width || actual.height != expected_height {
263        return Err(PoKeysError::Protocol(format!(
264            "configure_matrix_keyboard: device reports width={} height={}, \
265             expected width={expected_width} height={expected_height} (raw size byte=0x{:02X})",
266            actual.width,
267            actual.height,
268            actual.size_byte()
269        )));
270    }
271    for (i, &pin) in expected_row_pins
272        .iter()
273        .enumerate()
274        .take(expected_height as usize)
275        .take(8)
276    {
277        if actual.row_pins[i] != pin {
278            return Err(PoKeysError::Protocol(format!(
279                "configure_matrix_keyboard: row pin {i} mismatch: device has pin {}, expected pin {pin}",
280                actual.row_pins[i]
281            )));
282        }
283    }
284    for (i, &pin) in expected_column_pins
285        .iter()
286        .enumerate()
287        .take(expected_width as usize)
288        .take(8)
289    {
290        if actual.column_pins[i] != pin {
291            return Err(PoKeysError::Protocol(format!(
292                "configure_matrix_keyboard: column pin {i} mismatch: device has pin {}, expected pin {pin}",
293                actual.column_pins[i]
294            )));
295        }
296    }
297    if expected_height > 8 {
298        for (offset, &pin) in expected_row_pins
299            .iter()
300            .enumerate()
301            .skip(8)
302            .take((expected_height as usize).saturating_sub(8))
303            .take(8)
304        {
305            if actual.row_pins[offset] != pin {
306                return Err(PoKeysError::Protocol(format!(
307                    "configure_matrix_keyboard: extended row pin {offset} mismatch: device has pin {}, expected pin {pin}",
308                    actual.row_pins[offset]
309                )));
310            }
311        }
312    }
313    Ok(())
314}
315
316/// Main PoKeys device structure
317pub struct PoKeysDevice {
318    // Connection information
319    connection_type: DeviceConnectionType,
320    connection_param: ConnectionParam,
321
322    // Device information
323    pub info: DeviceInfo,
324    pub device_data: DeviceData,
325    pub network_device_data: Option<NetworkDeviceInfo>,
326
327    // Device model
328    pub model: Option<crate::models::DeviceModel>,
329
330    // Pin and I/O data
331    pub pins: Vec<PinData>,
332    pub encoders: Vec<EncoderData>,
333    pub pwm: PwmData,
334
335    // Peripheral data
336    pub matrix_keyboard: MatrixKeyboard,
337    pub matrix_led: Vec<MatrixLed>,
338    pub lcd: LcdData,
339    pub pulse_engine_v2: PulseEngineV2,
340    pub easy_sensors: Vec<EasySensor>,
341    pub rtc: RealTimeClock,
342
343    // Communication
344    pub(crate) communication: CommunicationManager,
345    usb_interface: Option<Box<dyn UsbHidInterface>>,
346    network_interface: Option<Box<dyn NetworkInterface>>,
347
348    // Configuration
349    pub fast_encoders_configuration: u8,
350    pub fast_encoders_options: u8,
351    pub ultra_fast_encoder_configuration: u8,
352    pub ultra_fast_encoder_options: u8,
353    pub ultra_fast_encoder_filter: u32,
354    pub po_ext_bus_data: Vec<u8>,
355
356    // I2C configuration and metrics
357    pub i2c_config: I2cConfig,
358    pub i2c_metrics: I2cMetrics,
359    pub validation_level: ValidationLevel,
360}
361
362impl PoKeysDevice {
363    /// Create a new device instance
364    fn new(connection_type: DeviceConnectionType) -> Self {
365        Self {
366            connection_type,
367            connection_param: ConnectionParam::Tcp,
368            info: DeviceInfo::default(),
369            device_data: DeviceData::default(),
370            network_device_data: None,
371            model: None,
372            pins: Vec::new(),
373            encoders: Vec::new(),
374            pwm: PwmData::new(),
375            matrix_keyboard: MatrixKeyboard::new(),
376            matrix_led: Vec::new(),
377            lcd: LcdData::new(),
378            pulse_engine_v2: PulseEngineV2::new(),
379            easy_sensors: Vec::new(),
380            rtc: RealTimeClock::default(),
381            communication: CommunicationManager::new(connection_type),
382            usb_interface: None,
383            network_interface: None,
384            fast_encoders_configuration: 0,
385            fast_encoders_options: 0,
386            ultra_fast_encoder_configuration: 0,
387            ultra_fast_encoder_options: 0,
388            ultra_fast_encoder_filter: 0,
389            po_ext_bus_data: Vec::new(),
390            i2c_config: I2cConfig::default(),
391            i2c_metrics: I2cMetrics::default(),
392            validation_level: ValidationLevel::None,
393        }
394    }
395
396    /// Get device information from the connected device
397    pub fn get_device_data(&mut self) -> Result<()> {
398        // Use the comprehensive read device data command instead of the old method
399        self.read_device_data()?;
400        self.initialize_device_structures()?;
401
402        // Try to load the device model
403        self.load_device_model()?;
404
405        Ok(())
406    }
407
408    /// Load the device model based on the device type
409    fn load_device_model(&mut self) -> Result<()> {
410        use crate::models::load_model;
411
412        // Determine the model name based on the device type
413        let device_type_name = self.device_data.device_type_name();
414        let model_name = match device_type_name.as_str() {
415            "PoKeys56U" => "PoKeys56U",
416            "PoKeys56E" => "PoKeys56E",
417            "PoKeys57U" => "PoKeys57U",
418            "PoKeys57E" => "PoKeys57E",
419            "PoKeys57Ev1.1" => "PoKeys57E", // Map v1.1 to base model
420            "PoKeys57CNC" => "PoKeys57CNC",
421            _ => return Ok(()), // No model for other device types
422        };
423
424        // Try to load the model
425        match load_model(model_name, None) {
426            Ok(model) => {
427                log::info!("Loaded device model: {}", model_name);
428                self.model = Some(model);
429                Ok(())
430            }
431            Err(e) => {
432                log::warn!("Failed to load device model {}: {}", model_name, e);
433                Ok(()) // Continue without a model
434            }
435        }
436    }
437
438    /// Check if a pin supports a specific capability
439    ///
440    /// # Arguments
441    ///
442    /// * `pin` - The pin number to check
443    /// * `capability` - The capability to check for
444    ///
445    /// # Returns
446    ///
447    /// * `bool` - True if the pin supports the capability, false otherwise
448    pub fn is_pin_capability_supported(&self, pin: u32, capability: &str) -> bool {
449        if let Some(model) = &self.model {
450            model.is_pin_capability_supported(pin as u8, capability)
451        } else {
452            // If no model is loaded, assume all capabilities are supported
453            true
454        }
455    }
456
457    /// Get all capabilities for a pin
458    ///
459    /// # Arguments
460    ///
461    /// * `pin` - The pin number to get capabilities for
462    ///
463    /// # Returns
464    ///
465    /// * `Vec<String>` - List of capabilities supported by the pin
466    pub fn get_pin_capabilities(&self, pin: u32) -> Vec<String> {
467        if let Some(model) = &self.model {
468            model.get_pin_capabilities(pin as u8)
469        } else {
470            // If no model is loaded, return an empty list
471            Vec::new()
472        }
473    }
474
475    /// Validate that a pin can be configured with a specific capability
476    ///
477    /// # Arguments
478    ///
479    /// * `pin` - The pin number to check
480    /// * `capability` - The capability to check for
481    ///
482    /// # Returns
483    ///
484    /// * `Result<()>` - Ok if the capability is valid, an error otherwise
485    pub fn validate_pin_capability(&self, pin: u32, capability: &str) -> Result<()> {
486        if let Some(model) = &self.model {
487            model.validate_pin_capability(pin as u8, capability)
488        } else {
489            // If no model is loaded, assume all capabilities are valid
490            Ok(())
491        }
492    }
493
494    /// Get related capabilities for a specific capability
495    ///
496    /// # Arguments
497    ///
498    /// * `pin` - The pin number with the capability
499    /// * `capability` - The capability to find related capabilities for
500    ///
501    /// # Returns
502    ///
503    /// * `Vec<(String, u8)>` - List of related capabilities and their pin numbers
504    pub fn get_related_capabilities(&self, pin: u32, capability: &str) -> Vec<(String, u8)> {
505        if let Some(model) = &self.model {
506            model.get_related_capabilities(pin as u8, capability)
507        } else {
508            // If no model is loaded, return an empty list
509            Vec::new()
510        }
511    }
512
513    /// Save current configuration to device
514    pub fn save_configuration(&mut self) -> Result<()> {
515        // Based on official PoKeysLib source code:
516        // CreateRequest(device->request, 0x50, 0xAA, 0x55, 0, 0);
517        // Command 0x50 with parameters 0xAA, 0x55, 0, 0
518        self.send_request(0x50, 0xAA, 0x55, 0, 0)?;
519        Ok(())
520    }
521
522    /// Get the device's current system load as a percentage (0–100).
523    ///
524    /// Sends the "Get system load status" command (`0x05`) defined in the
525    /// PoKeys protocol specification. The device replies with the current
526    /// CPU/system load in byte 3 of the response.
527    pub fn get_system_load(&mut self) -> Result<u8> {
528        let response = self.send_request(0x05, 0, 0, 0, 0)?;
529        Ok(parse_system_load_response(&response))
530    }
531
532    /// Set the device name (up to 20 bytes for the long device name).
533    ///
534    /// Sends protocol command `0x06` with "write long device name" flags set.
535    /// After the write succeeds, [`save_configuration`](PoKeysDevice::save_configuration)
536    /// is called to persist the value to non-volatile storage — `0x06` alone
537    /// does not auto-save.
538    ///
539    /// # Side effect
540    ///
541    /// The request packet includes fields for the joystick device name
542    /// (spec bytes 19–34) and the product-ID offset (byte 35) that this
543    /// implementation always sends as zero. If the target device was using
544    /// those fields, they will be cleared as a side effect of this call.
545    pub fn set_device_name(&mut self, name: &str) -> Result<()> {
546        // Based on official documentation:
547        // - byte 2: 0x06
548        // - byte 3: Bit 0 for writing device name (0x01)
549        // - byte 4: use long device name (1)
550        // - byte 5-6: 0
551        // - byte 7: request ID
552        // - bytes 36-55: long device name string (20 bytes)
553
554        // Prepare long device name (20 bytes for long name)
555        let mut name_bytes = [0u8; 20];
556        let name_str = if name.len() > 20 { &name[..20] } else { name };
557        let name_bytes_slice = name_str.as_bytes();
558        name_bytes[..name_bytes_slice.len()].copy_from_slice(name_bytes_slice);
559
560        // Create request manually according to documentation
561        let mut request = [0u8; 64];
562        request[0] = 0xBB; // header
563        request[1] = 0x06; // command (byte 2 in doc)
564        request[2] = 0x01; // bit 0 set for writing device name (byte 3 in doc)
565        request[3] = 0x01; // use long device name (byte 4 in doc)
566        request[4] = 0x00; // byte 5 in doc
567        request[5] = 0x00; // byte 6 in doc
568        request[6] = self.communication.get_next_request_id();
569
570        // Long device name at bytes 36-55 in doc = bytes 35-54 in 0-based array
571        request[35..55].copy_from_slice(&name_bytes);
572
573        // Calculate checksum exactly like official PoKeysLib getChecksum function
574        // Sum bytes 0-6 (not including byte 7 where checksum goes)
575        let mut checksum: u8 = 0;
576        for i in 0..7 {
577            checksum = checksum.wrapping_add(request[i]);
578        }
579        request[7] = checksum;
580
581        // Send the request using the appropriate interface
582        match self.connection_type {
583            DeviceConnectionType::UsbDevice | DeviceConnectionType::FastUsbDevice => {
584                if let Some(ref mut interface) = self.usb_interface {
585                    let mut hid_packet = [0u8; 65];
586                    hid_packet[0] = 0; // Report ID
587                    hid_packet[1..65].copy_from_slice(&request);
588
589                    interface.write(&hid_packet)?;
590
591                    let mut response = [0u8; 65];
592                    interface.read(&mut response)?;
593                } else {
594                    return Err(PoKeysError::NotConnected);
595                }
596            }
597            DeviceConnectionType::NetworkDevice => {
598                if let Some(ref mut interface) = self.network_interface {
599                    interface.send(&request)?;
600
601                    // Use timeout for network response to avoid hanging
602                    let mut response = [0u8; 64];
603                    let _ = interface
604                        .receive_timeout(&mut response, std::time::Duration::from_millis(2000));
605                } else {
606                    return Err(PoKeysError::NotConnected);
607                }
608            }
609        }
610
611        self.save_configuration()
612    }
613
614    /// Reset device configuration to defaults (protocol command `0x52`
615    /// "Disable lock and reset configuration"). Requires the magic bytes
616    /// `0xAA, 0x55` to match the spec and the upstream PoKeysLib behaviour.
617    ///
618    /// On observed network firmware, this command interrupts the connection
619    /// mid-call (similar to [`Self::reboot_device`]). Transfer-level errors
620    /// after the request was sent are therefore treated as success — the
621    /// device received the command and is resetting its state, just
622    /// couldn't reply.
623    ///
624    /// After this call the active connection may be invalid; callers
625    /// observing further command failures should reconnect.
626    pub fn clear_configuration(&mut self) -> Result<()> {
627        match self.send_request(0x52, 0xAA, 0x55, 0, 0) {
628            Ok(_) => Ok(()),
629            Err(PoKeysError::Transfer(_)) => Ok(()),
630            Err(e) => Err(e),
631        }
632    }
633
634    /// Returns `true` if the device's configuration is currently locked.
635    ///
636    /// Reads byte 59 of the `0x00` "Read device data" response (spec
637    /// "configuration lock status"). When locked, the firmware echoes
638    /// configuration writes (matrix keyboard, pin functions, etc.) but
639    /// does not apply them — the silent-failure mode that prompted the
640    /// readback verification on [`Self::configure_matrix_keyboard`].
641    ///
642    /// Use this to preflight a configuration session, either failing fast
643    /// with a clear message or calling [`Self::clear_configuration`] to
644    /// disable the lock first.
645    pub fn is_configuration_locked(&mut self) -> Result<bool> {
646        self.read_device_data()?;
647        Ok(self.device_data.device_lock_status != 0)
648    }
649
650    /// Reboot the device (command `0xF3`).
651    ///
652    /// Sends the "Reboot system" command defined in the PoKeys protocol
653    /// specification. The device may reboot before a response reaches the
654    /// host; transfer-level failures caused by the interrupted response are
655    /// treated as success, as the request was already delivered.
656    ///
657    /// After a successful reboot the active connection is effectively
658    /// invalidated — callers should re-enumerate and reconnect before issuing
659    /// further commands.
660    pub fn reboot_device(&mut self) -> Result<()> {
661        match self.send_request(0xF3, 0, 0, 0, 0) {
662            Ok(_) => Ok(()),
663            // The device typically reboots before responding, so a transfer
664            // error after the request was sent is expected and not fatal.
665            Err(PoKeysError::Transfer(_)) => Ok(()),
666            Err(e) => Err(e),
667        }
668    }
669
670    /// Send custom request to device
671    pub fn custom_request(
672        &mut self,
673        request_type: u8,
674        param1: u8,
675        param2: u8,
676        param3: u8,
677        param4: u8,
678    ) -> Result<[u8; RESPONSE_BUFFER_SIZE]> {
679        self.send_request(request_type, param1, param2, param3, param4)
680    }
681
682    /// Set ethernet retry count and timeout.
683    #[deprecated(
684        since = "1.0.5",
685        note = "Use `set_network_timeout` and `set_network_retries` separately. \
686                The `read_retries` parameter has never been honoured by the \
687                retry loops in this crate; passing any value here is a no-op."
688    )]
689    pub fn set_ethernet_retry_count_and_timeout(
690        &mut self,
691        send_retries: u32,
692        _read_retries: u32,
693        timeout_ms: u32,
694    ) {
695        self.communication.set_retries_and_timeout(
696            send_retries,
697            0,
698            Duration::from_millis(timeout_ms as u64),
699        );
700    }
701
702    /// Set the timeout applied to UDP/TCP receive calls during
703    /// `send_request`.
704    ///
705    /// Each network `send_request` blocks up to this duration waiting for a
706    /// reply before retrying. Default: 1000 ms.
707    ///
708    /// **Tuning guide:**
709    /// - On a healthy LAN, PoKeys replies in 1–5 ms. A timeout of
710    ///   50–200 ms is reasonable for a latency-sensitive UI.
711    /// - Setting this too low (below ~20 ms) can cause spurious retries
712    ///   on a healthy device under OS scheduler jitter.
713    /// - **Network-only.** USB uses a fixed internal 50 × 20 ms polling
714    ///   loop and is not affected by this setter.
715    ///
716    /// Use [`Self::tune_for_realtime_polling`] for a sensible preset.
717    pub fn set_network_timeout(&mut self, timeout: Duration) {
718        let send_retries = self.communication.send_retries();
719        self.communication
720            .set_retries_and_timeout(send_retries, 0, timeout);
721    }
722
723    /// Set how many times `send_request` retries on a network timeout
724    /// before returning [`crate::PoKeysError::Transfer`].
725    ///
726    /// Each retry costs up to one full [`Self::network_timeout`] of
727    /// blocking plus one WARN log line (subject to rate-limiting).
728    /// Default: 3.
729    ///
730    /// For latency-sensitive pollers, consider setting this to 1: a
731    /// dropped UDP reply then costs one timeout period instead of three,
732    /// and the caller can drive higher-level recovery policy itself.
733    pub fn set_network_retries(&mut self, retries: u32) {
734        let timeout = self.communication.socket_timeout();
735        self.communication
736            .set_retries_and_timeout(retries, 0, timeout);
737    }
738
739    /// Current network receive timeout. See [`Self::set_network_timeout`].
740    pub fn network_timeout(&self) -> Duration {
741        self.communication.socket_timeout()
742    }
743
744    /// Current per-`send_request` retry count on network timeouts.
745    /// See [`Self::set_network_retries`].
746    pub fn send_retries(&self) -> u32 {
747        self.communication.send_retries()
748    }
749
750    /// Tune the network retry loop for a latency-sensitive polling use
751    /// case (e.g. a live UI or dashboard reading device state at tens
752    /// of Hz).
753    ///
754    /// Equivalent to:
755    ///
756    /// ```ignore
757    /// device.set_network_timeout(Duration::from_millis(100));
758    /// device.set_network_retries(1);
759    /// ```
760    ///
761    /// A dropped reply then costs one 100 ms blocking call instead of
762    /// three 1-second calls. Your application's tick budget is preserved
763    /// and application-level recovery policy (skip / backoff / reconnect)
764    /// takes precedence over library-internal retries.
765    pub fn tune_for_realtime_polling(&mut self) {
766        self.communication
767            .set_retries_and_timeout(1, 0, Duration::from_millis(100));
768    }
769
770    /// Get connection type
771    pub fn get_connection_type(&self) -> DeviceConnectionType {
772        self.connection_type
773    }
774
775    /// Set I2C configuration
776    pub fn set_i2c_config(&mut self, config: I2cConfig) {
777        self.i2c_config = config;
778    }
779
780    /// Get I2C configuration
781    pub fn get_i2c_config(&self) -> &I2cConfig {
782        &self.i2c_config
783    }
784
785    /// Set validation level
786    pub fn set_validation_level(&mut self, level: ValidationLevel) {
787        self.validation_level = level;
788    }
789
790    /// Get I2C metrics
791    pub fn get_i2c_metrics(&self) -> &I2cMetrics {
792        &self.i2c_metrics
793    }
794
795    /// Reset I2C metrics
796    pub fn reset_i2c_metrics(&mut self) {
797        self.i2c_metrics = I2cMetrics::default();
798    }
799
800    /// Perform device health check
801    pub fn health_check(&mut self) -> HealthStatus {
802        HealthStatus {
803            connectivity: self.test_connectivity(),
804            i2c_health: self.test_i2c_health(),
805            error_rate: self.calculate_error_rate(),
806            performance: self.get_performance_summary(),
807        }
808    }
809
810    /// Test basic connectivity
811    fn test_connectivity(&mut self) -> ConnectivityStatus {
812        match self.get_device_data() {
813            Ok(_) => ConnectivityStatus::Healthy,
814            Err(e) => ConnectivityStatus::Degraded(e.to_string()),
815        }
816    }
817
818    /// Test I2C bus health
819    fn test_i2c_health(&mut self) -> I2cHealthStatus {
820        match self.i2c_get_status() {
821            Ok(I2cStatus::Ok) => I2cHealthStatus::Healthy,
822            Ok(status) => I2cHealthStatus::Degraded(format!("I2C status: {:?}", status)),
823            Err(e) => I2cHealthStatus::Failed(e.to_string()),
824        }
825    }
826
827    /// Calculate current error rate
828    fn calculate_error_rate(&self) -> f64 {
829        if self.i2c_metrics.total_commands == 0 {
830            0.0
831        } else {
832            self.i2c_metrics.failed_commands as f64 / self.i2c_metrics.total_commands as f64
833        }
834    }
835
836    /// Get performance summary
837    fn get_performance_summary(&self) -> PerformanceSummary {
838        let success_rate = if self.i2c_metrics.total_commands == 0 {
839            1.0
840        } else {
841            self.i2c_metrics.successful_commands as f64 / self.i2c_metrics.total_commands as f64
842        };
843
844        PerformanceSummary {
845            avg_response_time_ms: self.i2c_metrics.average_response_time.as_millis() as f64,
846            success_rate,
847            throughput_commands_per_sec: 0.0, // Would need timing data to calculate
848        }
849    }
850
851    /// Validate packet according to current validation level
852    #[allow(dead_code)]
853    fn validate_packet(&self, data: &[u8]) -> Result<()> {
854        match &self.validation_level {
855            ValidationLevel::None => Ok(()),
856            ValidationLevel::Basic => self.validate_basic_structure(data),
857            ValidationLevel::Strict => self.validate_strict_protocol(data),
858            ValidationLevel::Custom(config) => self.validate_custom(data, config),
859        }
860    }
861
862    /// Validate basic packet structure
863    #[allow(dead_code)]
864    fn validate_basic_structure(&self, data: &[u8]) -> Result<()> {
865        if data.len() < 3 {
866            return Err(PoKeysError::InvalidPacketStructure(
867                "Minimum packet size is 3 bytes".to_string(),
868            ));
869        }
870        Ok(())
871    }
872
873    /// Validate strict protocol compliance
874    #[allow(dead_code)]
875    fn validate_strict_protocol(&self, data: &[u8]) -> Result<()> {
876        if data.len() < 3 {
877            return Err(PoKeysError::InvalidPacketStructure(
878                "Minimum packet size is 3 bytes".to_string(),
879            ));
880        }
881
882        let command = data[0];
883        let device_id = data[1];
884        let checksum = data[data.len() - 1];
885
886        // Validate command ID (uSPIBridge command range)
887        if !matches!(command, 0x11..=0x42) {
888            return Err(PoKeysError::InvalidCommand(command));
889        }
890
891        // Validate device ID
892        if device_id >= 16 {
893            // Reasonable max device count
894            return Err(PoKeysError::InvalidDeviceId(device_id));
895        }
896
897        // Validate checksum
898        let calculated_checksum = self.calculate_checksum(&data[..data.len() - 1]);
899        if checksum != calculated_checksum {
900            return Err(PoKeysError::InvalidChecksumDetailed {
901                expected: calculated_checksum,
902                received: checksum,
903            });
904        }
905
906        Ok(())
907    }
908
909    /// Validate with custom configuration
910    #[allow(dead_code)]
911    fn validate_custom(&self, data: &[u8], config: &crate::types::ValidationConfig) -> Result<()> {
912        if config.validate_packet_structure && data.len() < 3 {
913            return Err(PoKeysError::InvalidPacketStructure(
914                "Minimum packet size is 3 bytes".to_string(),
915            ));
916        }
917
918        if data.len() >= 3 {
919            let command = data[0];
920            let device_id = data[1];
921            let checksum = data[data.len() - 1];
922
923            if config.validate_command_ids
924                && !config.valid_commands.is_empty()
925                && !config.valid_commands.contains(&command)
926            {
927                return Err(PoKeysError::InvalidCommand(command));
928            }
929
930            if config.validate_device_ids && device_id > config.max_device_id {
931                return Err(PoKeysError::InvalidDeviceId(device_id));
932            }
933
934            if config.validate_checksums {
935                let calculated_checksum = self.calculate_checksum(&data[..data.len() - 1]);
936                if checksum != calculated_checksum {
937                    return Err(PoKeysError::InvalidChecksumDetailed {
938                        expected: calculated_checksum,
939                        received: checksum,
940                    });
941                }
942            }
943        }
944
945        Ok(())
946    }
947
948    /// Calculate XOR checksum
949    #[allow(dead_code)]
950    fn calculate_checksum(&self, data: &[u8]) -> u8 {
951        data.iter().fold(0, |acc, &byte| acc ^ byte)
952    }
953
954    /// Check if device supports a specific capability
955    pub fn check_pin_capability(&self, pin: u32, capability: crate::io::PinCapability) -> bool {
956        if pin as usize >= self.pins.len() {
957            return false;
958        }
959
960        // Implementation would check device-specific pin capabilities
961        // This is a simplified version
962        match self.device_data.device_type_id {
963            32 => check_pokeys57cnc_pin_capability(pin, capability), // PoKeys57CNC
964            _ => false, // Add other device types as needed
965        }
966    }
967
968    /// Get complete network configuration including discovery info
969    pub fn get_network_configuration(
970        &mut self,
971        timeout_ms: u32,
972    ) -> Result<(Option<NetworkDeviceSummary>, NetworkDeviceInfo)> {
973        // Get discovery info
974        let discovery_info = enumerate_network_devices(timeout_ms)?
975            .into_iter()
976            .find(|d| d.serial_number == self.device_data.serial_number);
977
978        // Get detailed config using 0xBB request
979        let response = self.send_request(0xE0, 0x00, 0x00, 0, 0)?;
980
981        // Parse network configuration from response
982        let config = NetworkDeviceInfo {
983            dhcp: response.get(8).copied().unwrap_or(0),
984            ip_address_setup: [
985                response.get(9).copied().unwrap_or(0),
986                response.get(10).copied().unwrap_or(0),
987                response.get(11).copied().unwrap_or(0),
988                response.get(12).copied().unwrap_or(0),
989            ],
990            ip_address_current: [
991                response.get(13).copied().unwrap_or(0),
992                response.get(14).copied().unwrap_or(0),
993                response.get(15).copied().unwrap_or(0),
994                response.get(16).copied().unwrap_or(0),
995            ],
996            tcp_timeout: u16::from_le_bytes([
997                response.get(17).copied().unwrap_or(0),
998                response.get(18).copied().unwrap_or(0),
999            ])
1000            .saturating_mul(100),
1001            gateway_ip: [
1002                response.get(19).copied().unwrap_or(0),
1003                response.get(20).copied().unwrap_or(0),
1004                response.get(21).copied().unwrap_or(0),
1005                response.get(22).copied().unwrap_or(0),
1006            ],
1007            subnet_mask: [
1008                response.get(23).copied().unwrap_or(0),
1009                response.get(24).copied().unwrap_or(0),
1010                response.get(25).copied().unwrap_or(0),
1011                response.get(26).copied().unwrap_or(0),
1012            ],
1013            additional_network_options: response.get(27).copied().unwrap_or(0),
1014        };
1015
1016        Ok((discovery_info, config))
1017    }
1018
1019    /// Set network configuration on the device.
1020    ///
1021    /// Sends command `0xE0` with option `10`, which writes **and saves** the
1022    /// configuration to non-volatile storage in one operation — no separate
1023    /// [`save_configuration`](PoKeysDevice::save_configuration) call is needed.
1024    ///
1025    /// # Field mapping
1026    ///
1027    /// | `NetworkDeviceInfo` field      | Protocol byte (doc) | Notes |
1028    /// |-------------------------------|---------------------|-------|
1029    /// | `dhcp`                        | 9                   | 0 = fixed IP, 1 = DHCP |
1030    /// | `ip_address_setup`            | 10–13               | Applied when `dhcp == 0` |
1031    /// | `tcp_timeout` (ms)            | 18–19               | Stored in units of 100 ms |
1032    /// | `gateway_ip`                  | 20–23               | Applied when non-zero |
1033    /// | `subnet_mask`                 | 24–27               | Applied when non-zero |
1034    /// | `additional_network_options`  | 29                  | Upper nibble forced to `0xA` |
1035    ///
1036    /// `ip_address_current` is read-only (assigned by DHCP) and is ignored here.
1037    pub fn set_network_configuration(&mut self, config: &NetworkDeviceInfo) -> Result<()> {
1038        // TCP timeout is stored in units of 100 ms; NetworkDeviceInfo holds ms.
1039        let timeout_units = (config.tcp_timeout / 100).max(1);
1040        let timeout_bytes = timeout_units.to_le_bytes();
1041
1042        // Set gateway/subnet flag: tell the device to apply those fields too.
1043        let gateway_subnet_set: u8 =
1044            if config.gateway_ip != [0, 0, 0, 0] || config.subnet_mask != [0, 0, 0, 0] {
1045                1
1046            } else {
1047                0
1048            };
1049
1050        // Upper nibble of the options byte must always be 0xA (protocol requirement).
1051        let options = (config.additional_network_options & 0x0F) | 0xA0;
1052
1053        // Build the data payload (doc bytes 9–29, 0-based bytes 8–28).
1054        let mut data = [0u8; 21];
1055        data[0] = config.dhcp; // doc byte  9: IP setup
1056        data[1..5].copy_from_slice(&config.ip_address_setup); // doc bytes 10-13: fixed IP
1057        // data[5..9] = reserved (zeros)               // doc bytes 14-17
1058        data[9] = timeout_bytes[0]; // doc bytes 18-19: TCP timeout (LE)
1059        data[10] = timeout_bytes[1];
1060        data[11..15].copy_from_slice(&config.gateway_ip); // doc bytes 20-23
1061        data[15..19].copy_from_slice(&config.subnet_mask); // doc bytes 24-27
1062        data[19] = gateway_subnet_set; // doc byte  28: apply gateway/subnet
1063        data[20] = options; // doc byte  29: additional options
1064
1065        // option = 10 triggers write + save (per spec footnote 17 and 18).
1066        self.send_request_with_data(0xE0, 10, 0, 0, 0, &data)?;
1067        Ok(())
1068    }
1069
1070    // Internal methods
1071
1072    pub fn send_request(
1073        &mut self,
1074        request_type: u8,
1075        param1: u8,
1076        param2: u8,
1077        param3: u8,
1078        param4: u8,
1079    ) -> Result<[u8; RESPONSE_BUFFER_SIZE]> {
1080        match self.connection_type {
1081            DeviceConnectionType::UsbDevice | DeviceConnectionType::FastUsbDevice => {
1082                if let Some(ref mut interface) = self.usb_interface {
1083                    self.communication.send_usb_request(
1084                        interface,
1085                        request_type,
1086                        param1,
1087                        param2,
1088                        param3,
1089                        param4,
1090                    )
1091                } else {
1092                    Err(PoKeysError::NotConnected)
1093                }
1094            }
1095            DeviceConnectionType::NetworkDevice => {
1096                if let Some(ref mut interface) = self.network_interface {
1097                    self.communication.send_network_request(
1098                        interface,
1099                        request_type,
1100                        param1,
1101                        param2,
1102                        param3,
1103                        param4,
1104                    )
1105                } else {
1106                    Err(PoKeysError::NotConnected)
1107                }
1108            }
1109        }
1110    }
1111
1112    /// Send request with data payload
1113    pub fn send_request_with_data(
1114        &mut self,
1115        request_type: u8,
1116        param1: u8,
1117        param2: u8,
1118        param3: u8,
1119        param4: u8,
1120        data: &[u8],
1121    ) -> Result<[u8; RESPONSE_BUFFER_SIZE]> {
1122        let request = self.communication.prepare_request_with_data(
1123            request_type,
1124            param1,
1125            param2,
1126            param3,
1127            param4,
1128            Some(data),
1129        );
1130
1131        match self.connection_type {
1132            DeviceConnectionType::UsbDevice | DeviceConnectionType::FastUsbDevice => {
1133                if let Some(ref mut interface) = self.usb_interface {
1134                    self.communication.send_usb_request_raw(interface, &request)
1135                } else {
1136                    Err(PoKeysError::NotConnected)
1137                }
1138            }
1139            DeviceConnectionType::NetworkDevice => {
1140                if let Some(ref mut interface) = self.network_interface {
1141                    self.communication
1142                        .send_network_request_raw(interface, &request)
1143                } else {
1144                    Err(PoKeysError::NotConnected)
1145                }
1146            }
1147        }
1148    }
1149
1150    /// Read comprehensive device data using command 0x00
1151    /// This provides accurate device information including proper firmware version and device type
1152    pub fn read_device_data(&mut self) -> Result<()> {
1153        // Send read device data command (byte 2: 0x00, bytes 3-6: 0)
1154        let response = self.send_request(0x00, 0, 0, 0, 0)?;
1155
1156        if response.len() < 64 {
1157            return Err(PoKeysError::Protocol(
1158                "Read device data response too short".to_string(),
1159            ));
1160        }
1161
1162        self.parse_device_data_response(&response)?;
1163        Ok(())
1164    }
1165
1166    /// Parse the comprehensive device data response according to PoKeys protocol specification
1167    fn parse_device_data_response(&mut self, response: &[u8]) -> Result<()> {
1168        // Check for extended device signature (PK58/PKEx) at bytes 9-12 (doc says 9-12, 0-based = 8-11)
1169        if response.len() >= 64 && (&response[8..12] == b"PK58" || &response[8..12] == b"PKEx") {
1170            // Extended device parsing (based on official documentation)
1171
1172            // Basic firmware info (bytes 5-6)
1173            let software_version_encoded = response[4]; // Byte 5 in doc = byte 4 in 0-based
1174            let revision_number = response[5]; // Byte 6 in doc = byte 5 in 0-based
1175
1176            // Decode software version: v(1+[bits 4-7]).(bits [0-3])
1177            let major_bits = (software_version_encoded >> 4) & 0x0F; // Extract bits 4-7
1178            let minor_bits = software_version_encoded & 0x0F; // Extract bits 0-3
1179            let decoded_major = 1 + major_bits;
1180            let decoded_minor = minor_bits;
1181
1182            // 32-bit serial number (bytes 13-16 in doc = bytes 12-15 in 0-based)
1183            let serial_32bit =
1184                u32::from_le_bytes([response[12], response[13], response[14], response[15]]);
1185
1186            // Extended firmware info (bytes 17-18 in doc = bytes 16-17 in 0-based)
1187            let firmware_version = response[16];
1188            let firmware_revision = response[17];
1189
1190            // Hardware ID (byte 19 in doc = byte 18 in 0-based)
1191            let hw_id = response[18];
1192
1193            // User ID (byte 20 in doc = byte 19 in 0-based)
1194            let user_id = response[19];
1195
1196            // Build date (bytes 21-31 in doc = bytes 20-30 in 0-based)
1197            let build_date_bytes = &response[20..31];
1198
1199            // Device name (bytes 32-41 in doc = bytes 31-40 in 0-based)
1200            let device_name_bytes = &response[31..41];
1201
1202            // Firmware type (byte 42 in doc = byte 41 in 0-based)
1203            let firmware_type = response[41];
1204
1205            // Application firmware version (bytes 43-44 in doc = bytes 42-43 in 0-based)
1206            let _app_firmware_major = response[42];
1207            let _app_firmware_minor = response[43];
1208
1209            // Product ID offset (byte 58 in doc = byte 57 in 0-based)
1210            let product_id = response[57];
1211
1212            // Configuration lock status (byte 59 in doc = byte 58 in 0-based).
1213            // Non-zero = configuration is locked; configuration writes will be
1214            // silently rejected until cleared via 0x52.
1215            let configuration_lock_status = response[58];
1216
1217            // Update device data structure
1218            self.device_data.serial_number = serial_32bit;
1219            self.device_data.device_lock_status = configuration_lock_status;
1220
1221            // Use the decoded firmware version for display
1222            self.device_data.firmware_version_major = decoded_major;
1223            self.device_data.firmware_version_minor = decoded_minor;
1224            self.device_data.firmware_revision = revision_number;
1225
1226            // Store the extended firmware info in secondary fields
1227            self.device_data.secondary_firmware_version_major = firmware_version;
1228            self.device_data.secondary_firmware_version_minor = firmware_revision;
1229
1230            self.device_data.hw_type = hw_id;
1231            self.device_data.user_id = user_id;
1232            self.device_data.fw_type = firmware_type;
1233            self.device_data.product_id = product_id;
1234
1235            // Store device name (copy raw bytes)
1236            self.device_data.device_name.fill(0);
1237            let name_len =
1238                std::cmp::min(device_name_bytes.len(), self.device_data.device_name.len());
1239            self.device_data.device_name[..name_len]
1240                .copy_from_slice(&device_name_bytes[..name_len]);
1241
1242            // Store build date (copy raw bytes)
1243            self.device_data.build_date.fill(0);
1244            let date_len = std::cmp::min(build_date_bytes.len(), self.device_data.build_date.len());
1245            self.device_data.build_date[..date_len].copy_from_slice(&build_date_bytes[..date_len]);
1246
1247            log::debug!("Extended device info parsed:");
1248            log::debug!("  Serial: {}", serial_32bit);
1249            log::debug!(
1250                "  Decoded firmware: {}.{}.{}",
1251                decoded_major,
1252                decoded_minor,
1253                revision_number
1254            );
1255            log::debug!(
1256                "  Raw software version byte: 0x{:02X}",
1257                software_version_encoded
1258            );
1259            log::debug!(
1260                "  Extended firmware: {}.{}",
1261                firmware_version,
1262                firmware_revision
1263            );
1264            log::debug!("  Hardware type: {}", hw_id);
1265        } else {
1266            // Legacy device parsing
1267
1268            // Serial number (bytes 3-4 in doc = bytes 2-3 in 0-based)
1269            let serial_16bit = ((response[2] as u32) << 8) | (response[3] as u32);
1270
1271            // Software version (byte 5 in doc = byte 4 in 0-based)
1272            let software_version_encoded = response[4];
1273            let revision_number = response[5];
1274
1275            // Decode software version: v(1+[bits 4-7]).(bits [0-3])
1276            let major_bits = (software_version_encoded >> 4) & 0x0F;
1277            let minor_bits = software_version_encoded & 0x0F;
1278            let decoded_major = 1 + major_bits;
1279            let decoded_minor = minor_bits;
1280
1281            self.device_data.serial_number = serial_16bit;
1282            self.device_data.firmware_version_major = decoded_major;
1283            self.device_data.firmware_version_minor = decoded_minor;
1284            self.device_data.firmware_revision = revision_number;
1285            self.device_data.hw_type = 0; // Unknown for legacy
1286            self.device_data.device_name.fill(0);
1287            self.device_data.build_date.fill(0);
1288
1289            log::debug!("Legacy device info parsed:");
1290            log::debug!("  Serial: {}", serial_16bit);
1291            log::debug!(
1292                "  Decoded firmware: {}.{}.{}",
1293                decoded_major,
1294                decoded_minor,
1295                revision_number
1296            );
1297        }
1298
1299        Ok(())
1300    }
1301
1302    fn initialize_device_structures(&mut self) -> Result<()> {
1303        // Initialize device-specific structures based on device type
1304        match self.device_data.device_type_id {
1305            32 => self.initialize_pokeys57cnc(), // PoKeys57CNC
1306            _ => self.initialize_generic_device(),
1307        }
1308    }
1309
1310    fn initialize_pokeys57cnc(&mut self) -> Result<()> {
1311        // PoKeys57CNC specific initialization
1312        self.info.pin_count = 55;
1313        self.info.pwm_count = 6;
1314        self.info.encoders_count = 25;
1315        self.info.fast_encoders = 3;
1316        self.info.ultra_fast_encoders = 1;
1317        self.info.analog_inputs = 8;
1318        self.info.pulse_engine_v2 = 1;
1319
1320        // Initialize pin array
1321        self.pins = vec![PinData::new(); self.info.pin_count as usize];
1322        self.encoders = vec![EncoderData::new(); self.info.encoders_count as usize];
1323        // PWM data is already initialized in new()
1324        self.po_ext_bus_data = vec![0; 10]; // PoKeys57CNC has 10 PoExtBus outputs
1325
1326        Ok(())
1327    }
1328
1329    fn initialize_generic_device(&mut self) -> Result<()> {
1330        // Generic device initialization
1331        self.info.pin_count = 55; // Default
1332        self.pins = vec![PinData::new(); self.info.pin_count as usize];
1333        self.encoders = vec![EncoderData::new(); 25]; // Default encoder count
1334        Ok(())
1335    }
1336
1337    // LED Matrix Protocol Implementation
1338
1339    /// Command 0xD5: Get/Set Matrix LED Configuration
1340    pub fn configure_led_matrix(
1341        &mut self,
1342        config: &crate::matrix::MatrixLedProtocolConfig,
1343    ) -> Result<()> {
1344        let enabled_flags = self.encode_matrix_enabled(config);
1345        let display1_size = self.encode_display_size(
1346            config.display1_characters,
1347            crate::matrix::SEVEN_SEGMENT_COLUMNS,
1348        );
1349        let display2_size = self.encode_display_size(
1350            config.display2_characters,
1351            crate::matrix::SEVEN_SEGMENT_COLUMNS,
1352        );
1353
1354        let _response =
1355            self.send_request(0xD5, 0x00, enabled_flags, display1_size, display2_size)?;
1356        Ok(())
1357    }
1358
1359    /// Read Matrix LED Configuration
1360    pub fn read_led_matrix_config(&mut self) -> Result<crate::matrix::MatrixLedProtocolConfig> {
1361        // Read the configuration using command 0xD5 with parameter 0x01
1362        let response = self.send_request(0xD5, 0x01, 0, 0, 0)?;
1363
1364        // Parse the response
1365        let enabled_flags = response[2];
1366        let display1_size = response[3];
1367        let display2_size = response[4];
1368
1369        // Decode enabled flags
1370        let display1_enabled = (enabled_flags & 0x01) != 0;
1371        let display2_enabled = (enabled_flags & 0x02) != 0;
1372
1373        // Decode character counts (lower 4 bits)
1374        // Note: The protocol response has display parameters swapped
1375        let display1_characters = display2_size & 0x0F; // Use display2_size for display1
1376        let display2_characters = display1_size & 0x0F; // Use display1_size for display2
1377
1378        Ok(crate::matrix::MatrixLedProtocolConfig {
1379            display1_enabled,
1380            display2_enabled,
1381            display1_characters,
1382            display2_characters,
1383        })
1384    }
1385
1386    /// Command 0xD6: Update Matrix Display
1387    pub fn update_led_matrix(
1388        &mut self,
1389        matrix_id: u8,
1390        action: crate::matrix::MatrixAction,
1391        row: u8,
1392        column: u8,
1393        data: &[u8],
1394    ) -> Result<()> {
1395        let action_code = match (matrix_id, action) {
1396            (1, crate::matrix::MatrixAction::UpdateWhole) => 1,
1397            (1, crate::matrix::MatrixAction::SetPixel) => 5,
1398            (1, crate::matrix::MatrixAction::ClearPixel) => 6,
1399            (2, crate::matrix::MatrixAction::UpdateWhole) => 11,
1400            (2, crate::matrix::MatrixAction::SetPixel) => 15,
1401            (2, crate::matrix::MatrixAction::ClearPixel) => 16,
1402            _ => {
1403                return Err(PoKeysError::Parameter(format!(
1404                    "Invalid matrix ID: {}",
1405                    matrix_id
1406                )));
1407            }
1408        };
1409
1410        // Prepare the request with data payload
1411        let request = self.communication.prepare_request_with_data(
1412            0xD6,
1413            action_code,
1414            row,
1415            column,
1416            0,
1417            Some(data),
1418        );
1419
1420        // Send the full request
1421        match self.connection_type {
1422            DeviceConnectionType::NetworkDevice => {
1423                if let Some(ref mut interface) = self.network_interface {
1424                    let request_id = request[6];
1425
1426                    // Send request
1427                    interface.send(&request[..64])?;
1428
1429                    // Receive response
1430                    let mut response = [0u8; RESPONSE_BUFFER_SIZE];
1431                    interface.receive(&mut response)?;
1432
1433                    // Validate response
1434                    self.communication
1435                        .validate_response(&response, request_id)?;
1436                }
1437            }
1438            _ => {
1439                return Err(PoKeysError::NotSupported);
1440            }
1441        }
1442
1443        Ok(())
1444    }
1445
1446    /// Command 0xCA: Configure Matrix Keyboard
1447    ///
1448    /// Configures a matrix keyboard using the official PoKeys protocol.
1449    /// Uses command 0xCA with option 16 for configuration.
1450    ///
1451    /// # Pin function prerequisite (user manual § 8.5)
1452    /// # Pin function prerequisite (user manual § 8.5)
1453    /// The PoKeys user manual states: "Make sure the selected pin is configured
1454    /// as digital input [for columns] / digital output [for rows], then check
1455    /// the 'Matrix keyboard' option for the pin..." Without this, the firmware
1456    /// acknowledges the configure but does not scan — it cannot drive a row
1457    /// that isn't configured as `DigitalOutput`, nor read a column that isn't
1458    /// configured as `DigitalInput` with its internal pull-up active.
1459    ///
1460    /// This method enforces the prerequisite by issuing a single bulk pin-
1461    /// function write (`0xC0`) before the configure: it reads the current
1462    /// 55-byte pin-settings array (preserving the invert bit on untouched
1463    /// pins), forces row pins to `DigitalOutput` and column pins to
1464    /// `DigitalInput`, and writes the array back.
1465    ///
1466    /// # Two-phase activation (vendor reference impl)
1467    /// The vendor reference library `PK_MatrixKBConfigurationSet` writes the
1468    /// configuration in two phases:
1469    ///
1470    /// 1. **Deactivate**: `0xCA` option 1 with `enable=0`, full pin / size /
1471    ///    bitmap payload. The firmware loads the configuration but does not
1472    ///    yet bind pins to row/column slots.
1473    /// 2. **Activate**: `0xCA` option 1 with `enable=1`, identical payload.
1474    ///    The firmware binds pins, turns on scanning, and the matrix becomes
1475    ///    live.
1476    ///
1477    /// A single-shot write with `enable=1` does **not** reliably activate
1478    /// scanning on observed firmware. This method follows the vendor
1479    /// sequence.
1480    ///
1481    /// # Readback verification
1482    /// After activation, the configuration is read back via `0xCA` option 10
1483    /// and verified to match. A "configuration locked" or otherwise rejected
1484    /// write will be caught here as a `Protocol` error rather than silently
1485    /// returning Ok.
1486    ///
1487    /// # Arguments
1488    /// * `width` - Number of columns (1-8)
1489    /// * `height` - Number of rows (1-16)
1490    /// * `column_pins` - Pin numbers for columns (1-based, 1..=55); must be at least `width` long
1491    /// * `row_pins` - Pin numbers for rows (1-based, 1..=55); must be at least `height` long
1492    ///
1493    /// # Errors
1494    /// Returns [`PoKeysError::Parameter`] if `width`/`height` are out of
1495    /// range, slices are too short, a pin number is outside 1..=55, or the
1496    /// same pin appears in both rows and columns. Returns
1497    /// [`PoKeysError::Protocol`] if the device's stored configuration after
1498    /// activation doesn't match what was sent.
1499    pub fn configure_matrix_keyboard(
1500        &mut self,
1501        width: u8,
1502        height: u8,
1503        column_pins: &[u8],
1504        row_pins: &[u8],
1505    ) -> Result<()> {
1506        if width == 0 || width > 8 {
1507            return Err(PoKeysError::Parameter(format!(
1508                "Matrix width must be 1-8 (got {width})"
1509            )));
1510        }
1511        if height == 0 || height > 16 {
1512            return Err(PoKeysError::Parameter(format!(
1513                "Matrix height must be 1-16 (got {height})"
1514            )));
1515        }
1516        if column_pins.len() < width as usize {
1517            return Err(PoKeysError::Parameter(format!(
1518                "column_pins has {} entries, need at least {width}",
1519                column_pins.len()
1520            )));
1521        }
1522        if row_pins.len() < height as usize {
1523            return Err(PoKeysError::Parameter(format!(
1524                "row_pins has {} entries, need at least {height}",
1525                row_pins.len()
1526            )));
1527        }
1528
1529        let used_rows = &row_pins[..height as usize];
1530        let used_cols = &column_pins[..width as usize];
1531        for &pin in used_rows.iter().chain(used_cols.iter()) {
1532            if pin == 0 || pin > 55 {
1533                return Err(PoKeysError::Parameter(format!(
1534                    "Matrix keyboard pin {pin} is out of range (must be 1-55)"
1535                )));
1536            }
1537        }
1538        for &row in used_rows {
1539            if used_cols.contains(&row) {
1540                return Err(PoKeysError::Parameter(format!(
1541                    "Pin {row} cannot be used as both a row and a column"
1542                )));
1543            }
1544        }
1545
1546        // Pin function prerequisite — see method-level docs.
1547        //
1548        // Use raw read/write (not the PinFunction view) so we preserve the
1549        // invert flag (bit 7 / 0x80) on every pin we don't touch.
1550        let mut raw = self.read_all_pin_settings_raw()?;
1551        for &pin in used_rows {
1552            let idx = (pin - 1) as usize;
1553            let invert = raw[idx] & 0x80;
1554            raw[idx] = (PinFunction::DigitalOutput as u8) | invert;
1555        }
1556        for &pin in used_cols {
1557            let idx = (pin - 1) as usize;
1558            let invert = raw[idx] & 0x80;
1559            raw[idx] = (PinFunction::DigitalInput as u8) | invert;
1560        }
1561        self.set_all_pin_settings_raw(&raw)?;
1562
1563        // Phase 1: deactivate write (option 1 with enable=0). Loads pins,
1564        // dimensions, and direct/macro bitmap into the firmware.
1565        let deactivate_payload =
1566            encode_matrix_kb_config_payload(false, width, height, column_pins, row_pins);
1567        self.send_matrix_kb_config_write(&deactivate_payload, "deactivate phase")?;
1568
1569        // Phase 2: activate write (option 1 with enable=1). Same payload,
1570        // enable bit set — firmware binds pins to row/column slots and
1571        // turns on scanning.
1572        let activate_payload =
1573            encode_matrix_kb_config_payload(true, width, height, column_pins, row_pins);
1574        self.send_matrix_kb_config_write(&activate_payload, "activate phase")?;
1575
1576        // Verify the device actually applied the configuration. Catches
1577        // "configuration locked" silent rejections where the firmware
1578        // echoes 0xCA but doesn't update its stored config.
1579        let actual = self.get_matrix_keyboard_configuration()?;
1580        verify_matrix_kb_config_matches(&actual, width, height, column_pins, row_pins, true)?;
1581
1582        // Update local state
1583        self.matrix_keyboard.configuration = 1;
1584        self.matrix_keyboard.width = width;
1585        self.matrix_keyboard.height = height;
1586
1587        // Copy pin assignments verbatim (1-based, as the user supplied them)
1588        self.matrix_keyboard.column_pins.fill(0);
1589        self.matrix_keyboard.row_pins.fill(0);
1590        for (i, &pin) in column_pins.iter().enumerate().take(8) {
1591            self.matrix_keyboard.column_pins[i] = pin;
1592        }
1593        for (i, &pin) in row_pins.iter().enumerate().take(16) {
1594            self.matrix_keyboard.row_pins[i] = pin;
1595        }
1596
1597        Ok(())
1598    }
1599
1600    /// Issue a `0xCA` option-1 write with the given 55-byte payload and
1601    /// validate the response echo. Used by the two-phase activate/deactivate
1602    /// sequence in [`Self::configure_matrix_keyboard`] and by
1603    /// [`Self::disable_matrix_keyboard`].
1604    fn send_matrix_kb_config_write(&mut self, data: &[u8; 55], phase: &str) -> Result<()> {
1605        let response = self.send_request_with_data(
1606            0xCA,
1607            MATRIX_KB_OPT_WRITE_CONFIG,
1608            MATRIX_KEYBOARD_ID,
1609            0,
1610            0,
1611            data,
1612        )?;
1613        if response[1] != 0xCA {
1614            return Err(PoKeysError::Protocol(format!(
1615                "Invalid response command for matrix keyboard {phase}: 0x{:02X}",
1616                response[1]
1617            )));
1618        }
1619        Ok(())
1620    }
1621
1622    /// Disable the matrix keyboard (`0xCA` option 1, enable bit cleared).
1623    ///
1624    /// Sends a single option-1 write with `enable=0`, then verifies via
1625    /// option-10 readback that the device reports the keyboard as
1626    /// disabled. Catches "configuration locked" rejections.
1627    pub fn disable_matrix_keyboard(&mut self) -> Result<()> {
1628        let data = [0u8; 55]; // data[0] = 0 → enable bit cleared
1629        self.send_matrix_kb_config_write(&data, "disable")?;
1630
1631        let actual = self.get_matrix_keyboard_configuration()?;
1632        verify_matrix_kb_config_matches(&actual, 0, 0, &[], &[], false)?;
1633
1634        self.matrix_keyboard.configuration = 0;
1635        Ok(())
1636    }
1637
1638    /// Read the device's current matrix-keyboard configuration via `0xCA`
1639    /// option 10.
1640    ///
1641    /// Returns a [`MatrixKeyboardConfig`] snapshot of what the firmware has
1642    /// stored. Useful for diff-against-device flows; also the building
1643    /// block [`Self::configure_matrix_keyboard`] uses internally to verify
1644    /// that its writes actually took effect.
1645    ///
1646    /// **Do not** issue option 1 with an empty payload thinking it's a
1647    /// read — option 1 is the configuration **write** subcommand, and
1648    /// would deactivate the matrix.
1649    pub fn get_matrix_keyboard_configuration(&mut self) -> Result<MatrixKeyboardConfig> {
1650        let response =
1651            self.send_request(0xCA, MATRIX_KB_OPT_READ_CONFIG, MATRIX_KEYBOARD_ID, 0, 0)?;
1652        parse_matrix_kb_config_readback(&response)
1653    }
1654
1655    /// Command 0xCA: Read Matrix Keyboard State
1656    ///
1657    /// Reads the current state of all keys in the matrix keyboard.
1658    /// Uses command 0xCA with option 20 to read the 16x8 matrix status.
1659    ///
1660    /// # Protocol Details (spec § Matrix keyboard, option 20)
1661    /// - Response bytes 9-24 (response[8..24]): 16-byte bitmap, one byte per row
1662    /// - Bit `c` of row `r` = 1 if the key at (row=r, col=c) is pressed
1663    /// - Key indexing: `row * 8 + col` (8-column internal layout regardless of configured width)
1664    pub fn read_matrix_keyboard(&mut self) -> Result<()> {
1665        let response = self.send_request(0xCA, 20, MATRIX_KEYBOARD_ID, 0, 0)?;
1666
1667        if response[1] != 0xCA {
1668            return Err(PoKeysError::Protocol(format!(
1669                "Invalid response command for matrix keyboard read: 0x{:02X}",
1670                response[1]
1671            )));
1672        }
1673        if response.len() < 24 {
1674            return Err(PoKeysError::Protocol(format!(
1675                "Matrix keyboard status response too short: {} bytes (need 24)",
1676                response.len()
1677            )));
1678        }
1679
1680        self.matrix_keyboard.key_values.fill(0);
1681        for (row, &byte_val) in response[8..24].iter().enumerate() {
1682            for col in 0..8 {
1683                let key_index = row * 8 + col;
1684                if key_index < self.matrix_keyboard.key_values.len() && (byte_val & (1 << col)) != 0
1685                {
1686                    self.matrix_keyboard.key_values[key_index] = 1;
1687                }
1688            }
1689        }
1690
1691        Ok(())
1692    }
1693
1694    /// Command 0xCA, option 50: set the matrix-keyboard scanning decimation (0-50).
1695    ///
1696    /// Higher values reduce the device's scan rate, useful for noisy keypads.
1697    pub fn set_matrix_keyboard_scanning_decimation(&mut self, decimation: u8) -> Result<()> {
1698        if decimation > 50 {
1699            return Err(PoKeysError::Parameter(format!(
1700                "scanning decimation must be 0-50 (got {decimation})"
1701            )));
1702        }
1703
1704        let mut data = [0u8; 1];
1705        data[0] = decimation;
1706        let response = self.send_request_with_data(0xCA, 50, MATRIX_KEYBOARD_ID, 0, 0, &data)?;
1707        if response[1] != 0xCA {
1708            return Err(PoKeysError::Protocol(format!(
1709                "Invalid response command for scanning decimation: 0x{:02X}",
1710                response[1]
1711            )));
1712        }
1713
1714        self.matrix_keyboard.scanning_decimation = decimation;
1715        Ok(())
1716    }
1717
1718    /// Command 0xCA, options 2-9: write the down-event key mapping for all 128 keys.
1719    ///
1720    /// Sends the key codes, modifiers, and triggered-mode flags currently held in
1721    /// `self.matrix_keyboard.key_mapping_key_code`,
1722    /// `self.matrix_keyboard.key_mapping_key_modifier`, and
1723    /// `self.matrix_keyboard.key_mapping_triggered_key` (one byte per key, non-zero = triggered).
1724    ///
1725    /// # Protocol Details
1726    /// Each option writes 16 keys: option 2 → keys 0-15, option 3 → 16-31, ..., option 9 → 112-127.
1727    /// Per the spec footnote, even if `width < 8`, the unused columns of each row still consume
1728    /// indices 0-7 in the mapping (rows are always 8 keys wide internally).
1729    pub fn set_matrix_keyboard_key_mapping(&mut self) -> Result<()> {
1730        for option in 2u8..=9u8 {
1731            let chunk_base = (option as usize - 2) * 16;
1732            let data = encode_matrix_kb_mapping_chunk(
1733                &self.matrix_keyboard.key_mapping_key_code,
1734                &self.matrix_keyboard.key_mapping_key_modifier,
1735                Some(&self.matrix_keyboard.key_mapping_triggered_key),
1736                chunk_base,
1737            );
1738            let response =
1739                self.send_request_with_data(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0, &data)?;
1740            if response[1] != 0xCA {
1741                return Err(PoKeysError::Protocol(format!(
1742                    "Invalid response command for key mapping write (option {option}): 0x{:02X}",
1743                    response[1]
1744                )));
1745            }
1746        }
1747        Ok(())
1748    }
1749
1750    /// Command 0xCA, options 22-29: write the up-event key mapping for all 128 keys.
1751    ///
1752    /// Sends `self.matrix_keyboard.key_mapping_key_code_up` and
1753    /// `self.matrix_keyboard.key_mapping_key_modifier_up`. Up-event mapping only takes effect
1754    /// for keys whose triggered flag is set in the down mapping (see
1755    /// [`Self::set_matrix_keyboard_key_mapping`]).
1756    pub fn set_matrix_keyboard_key_mapping_up(&mut self) -> Result<()> {
1757        for option in 22u8..=29u8 {
1758            let chunk_base = (option as usize - 22) * 16;
1759            let data = encode_matrix_kb_mapping_chunk(
1760                &self.matrix_keyboard.key_mapping_key_code_up,
1761                &self.matrix_keyboard.key_mapping_key_modifier_up,
1762                None,
1763                chunk_base,
1764            );
1765            let response =
1766                self.send_request_with_data(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0, &data)?;
1767            if response[1] != 0xCA {
1768                return Err(PoKeysError::Protocol(format!(
1769                    "Invalid response command for up-key mapping write (option {option}): 0x{:02X}",
1770                    response[1]
1771                )));
1772            }
1773        }
1774        Ok(())
1775    }
1776
1777    /// Command 0xCA, options 12-19: read the down-event key mapping for all 128 keys.
1778    ///
1779    /// Populates `self.matrix_keyboard.key_mapping_key_code`,
1780    /// `self.matrix_keyboard.key_mapping_key_modifier`, and
1781    /// `self.matrix_keyboard.key_mapping_triggered_key` from the device.
1782    pub fn get_matrix_keyboard_key_mapping(&mut self) -> Result<()> {
1783        for option in 12u8..=19u8 {
1784            let chunk_base = (option as usize - 12) * 16;
1785            let response = self.send_request(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0)?;
1786            validate_matrix_kb_mapping_response(&response, option)?;
1787            decode_matrix_kb_mapping_chunk(
1788                &response,
1789                &mut self.matrix_keyboard.key_mapping_key_code,
1790                &mut self.matrix_keyboard.key_mapping_key_modifier,
1791                Some(&mut self.matrix_keyboard.key_mapping_triggered_key),
1792                chunk_base,
1793            );
1794        }
1795        Ok(())
1796    }
1797
1798    /// Command 0xCA, options 32-39: read the up-event key mapping for all 128 keys.
1799    ///
1800    /// Populates `self.matrix_keyboard.key_mapping_key_code_up` and
1801    /// `self.matrix_keyboard.key_mapping_key_modifier_up` from the device.
1802    pub fn get_matrix_keyboard_key_mapping_up(&mut self) -> Result<()> {
1803        for option in 32u8..=39u8 {
1804            let chunk_base = (option as usize - 32) * 16;
1805            let response = self.send_request(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0)?;
1806            validate_matrix_kb_mapping_response(&response, option)?;
1807            decode_matrix_kb_mapping_chunk(
1808                &response,
1809                &mut self.matrix_keyboard.key_mapping_key_code_up,
1810                &mut self.matrix_keyboard.key_mapping_key_modifier_up,
1811                None,
1812                chunk_base,
1813            );
1814        }
1815        Ok(())
1816    }
1817
1818    /// Helper to encode display size (characters as rows, always 8 columns for 7-segment)
1819    fn encode_display_size(&self, characters: u8, columns: u8) -> u8 {
1820        // bits 0-3: number of rows (characters)
1821        // bits 4-7: number of columns (always 8 for 7-segment)
1822        (characters & 0x0F) | ((columns & 0x0F) << 4)
1823    }
1824
1825    /// Helper to encode matrix enabled flags
1826    fn encode_matrix_enabled(&self, config: &crate::matrix::MatrixLedProtocolConfig) -> u8 {
1827        let mut enabled = 0u8;
1828        if config.display1_enabled {
1829            enabled |= 1 << 0;
1830        }
1831        if config.display2_enabled {
1832            enabled |= 1 << 1;
1833        }
1834        enabled
1835    }
1836
1837    /// Configure multiple LED matrices with device model validation
1838    ///
1839    /// This method validates all configurations against the device model first,
1840    /// then applies the configurations and reserves the necessary pins.
1841    ///
1842    /// # Arguments
1843    ///
1844    /// * `configs` - Array of LED matrix configurations
1845    ///
1846    /// # Returns
1847    ///
1848    /// * `Result<()>` - Ok if all configurations were applied successfully
1849    pub fn configure_led_matrices(
1850        &mut self,
1851        configs: &[crate::matrix::LedMatrixConfig],
1852    ) -> Result<()> {
1853        // Basic validation (always performed)
1854        for config in configs {
1855            // Validate matrix ID
1856            if config.matrix_id < 1 || config.matrix_id > 2 {
1857                return Err(PoKeysError::InvalidConfiguration(
1858                    "Matrix ID must be 1 or 2".to_string(),
1859                ));
1860            }
1861
1862            // Validate character count (1-8 characters for 7-segment displays)
1863            if config.characters < 1 || config.characters > 8 {
1864                return Err(PoKeysError::InvalidConfiguration(
1865                    "Character count must be between 1 and 8 for 7-segment displays".to_string(),
1866                ));
1867            }
1868        }
1869
1870        // Device model validation (if available)
1871        for config in configs {
1872            if let Some(model) = &self.model {
1873                model.validate_led_matrix_config(config)?;
1874            }
1875        }
1876
1877        // Apply configurations
1878        let mut protocol_config = crate::matrix::MatrixLedProtocolConfig {
1879            display1_enabled: false,
1880            display2_enabled: false,
1881            display1_characters: 1,
1882            display2_characters: 1,
1883        };
1884
1885        for config in configs {
1886            match config.matrix_id {
1887                1 => {
1888                    protocol_config.display1_enabled = config.enabled;
1889                    protocol_config.display1_characters = config.characters;
1890                }
1891                2 => {
1892                    protocol_config.display2_enabled = config.enabled;
1893                    protocol_config.display2_characters = config.characters;
1894                }
1895                _ => {
1896                    return Err(PoKeysError::InvalidConfiguration(
1897                        "Invalid matrix ID".to_string(),
1898                    ));
1899                }
1900            }
1901        }
1902
1903        self.configure_led_matrix(&protocol_config)?;
1904
1905        // Reserve pins in model
1906        if let Some(model) = &mut self.model {
1907            for config in configs {
1908                if config.enabled {
1909                    model.reserve_led_matrix_pins(config.matrix_id)?;
1910                }
1911            }
1912        }
1913
1914        Ok(())
1915    }
1916
1917    /// Configure and control a servo motor
1918    pub fn configure_servo(&mut self, config: crate::pwm::ServoConfig) -> Result<()> {
1919        // Ensure PWM period is set for servo control (20ms = 500,000 cycles)
1920        if self.pwm.pwm_period == 0 {
1921            self.set_pwm_period(500000)?; // 20ms period for servo control
1922        }
1923
1924        // Enable PWM for the servo pin
1925        self.enable_pwm_for_pin(config.pin, true)?;
1926
1927        Ok(())
1928    }
1929
1930    /// Set servo angle (for position servos)
1931    pub fn set_servo_angle(&mut self, config: &crate::pwm::ServoConfig, angle: f32) -> Result<()> {
1932        config.set_angle(self, angle)
1933    }
1934
1935    /// Set servo speed (for speed servos)
1936    pub fn set_servo_speed(&mut self, config: &crate::pwm::ServoConfig, speed: f32) -> Result<()> {
1937        config.set_speed(self, speed)
1938    }
1939
1940    /// Stop servo (for speed servos)
1941    pub fn stop_servo(&mut self, config: &crate::pwm::ServoConfig) -> Result<()> {
1942        config.stop(self)
1943    }
1944}
1945
1946// Default implementations for device structures
1947impl Default for DeviceInfo {
1948    fn default() -> Self {
1949        Self {
1950            pin_count: 0,
1951            pwm_count: 0,
1952            basic_encoder_count: 0,
1953            encoders_count: 0,
1954            fast_encoders: 0,
1955            ultra_fast_encoders: 0,
1956            pwm_internal_frequency: 0,
1957            analog_inputs: 0,
1958            key_mapping: 0,
1959            triggered_key_mapping: 0,
1960            key_repeat_delay: 0,
1961            digital_counters: 0,
1962            joystick_button_axis_mapping: 0,
1963            joystick_analog_to_digital_mapping: 0,
1964            macros: 0,
1965            matrix_keyboard: 0,
1966            matrix_keyboard_triggered_mapping: 0,
1967            lcd: 0,
1968            matrix_led: 0,
1969            connection_signal: 0,
1970            po_ext_bus: 0,
1971            po_net: 0,
1972            analog_filtering: 0,
1973            init_outputs_start: 0,
1974            prot_i2c: 0,
1975            prot_1wire: 0,
1976            additional_options: 0,
1977            load_status: 0,
1978            custom_device_name: 0,
1979            po_tlog27_support: 0,
1980            sensor_list: 0,
1981            web_interface: 0,
1982            fail_safe_settings: 0,
1983            joystick_hat_switch: 0,
1984            pulse_engine: 0,
1985            pulse_engine_v2: 0,
1986            easy_sensors: 0,
1987        }
1988    }
1989}
1990
1991impl Default for RealTimeClock {
1992    fn default() -> Self {
1993        Self {
1994            sec: 0,
1995            min: 0,
1996            hour: 0,
1997            dow: 0,
1998            dom: 0,
1999            tmp: 0,
2000            doy: 0,
2001            month: 0,
2002            year: 0,
2003        }
2004    }
2005}
2006
2007// Device capability checking functions
2008fn check_pokeys57cnc_pin_capability(pin: u32, capability: crate::io::PinCapability) -> bool {
2009    use crate::io::PinCapability;
2010
2011    match pin {
2012        1..=2 => matches!(
2013            capability,
2014            PinCapability::DigitalInput
2015                | PinCapability::DigitalOutput
2016                | PinCapability::DigitalCounter
2017                | PinCapability::FastEncoder1A
2018        ),
2019        3..=6 => matches!(
2020            capability,
2021            PinCapability::DigitalInput
2022                | PinCapability::DigitalCounter
2023                | PinCapability::FastEncoder1A
2024        ),
2025        8 | 12..=13 => matches!(
2026            capability,
2027            PinCapability::DigitalInput | PinCapability::DigitalOutput
2028        ),
2029        9..=11 | 15..=16 => matches!(
2030            capability,
2031            PinCapability::DigitalInput | PinCapability::DigitalCounter
2032        ),
2033        14 => matches!(capability, PinCapability::DigitalInput),
2034        17..=18 | 20..=21 => matches!(
2035            capability,
2036            PinCapability::DigitalOutput | PinCapability::PwmOutput
2037        ),
2038        19 => matches!(
2039            capability,
2040            PinCapability::DigitalInput | PinCapability::DigitalCounter
2041        ),
2042        _ => false,
2043    }
2044}
2045
2046// Global device enumeration and connection functions
2047
2048static USB_DEVICE_LIST: LazyLock<Mutex<Vec<UsbDeviceInfo>>> =
2049    LazyLock::new(|| Mutex::new(Vec::new()));
2050#[allow(dead_code)]
2051static NETWORK_DEVICE_LIST: LazyLock<Mutex<Vec<NetworkDeviceSummary>>> =
2052    LazyLock::new(|| Mutex::new(Vec::new()));
2053
2054#[derive(Debug, Clone)]
2055#[allow(dead_code)]
2056struct UsbDeviceInfo {
2057    vendor_id: u16,
2058    product_id: u16,
2059    serial_number: Option<String>,
2060    path: String,
2061}
2062
2063/// Enumerate USB PoKeys devices
2064pub fn enumerate_usb_devices() -> Result<i32> {
2065    let mut device_list = USB_DEVICE_LIST
2066        .lock()
2067        .map_err(|_| PoKeysError::InternalError("Failed to lock USB device list".to_string()))?;
2068    device_list.clear();
2069
2070    // Platform-specific USB enumeration
2071    #[cfg(target_os = "macos")]
2072    {
2073        enumerate_usb_devices_macos(&mut device_list)
2074    }
2075    #[cfg(target_os = "linux")]
2076    {
2077        enumerate_usb_devices_linux(&mut device_list)
2078    }
2079    #[cfg(target_os = "windows")]
2080    {
2081        enumerate_usb_devices_windows(&mut device_list)
2082    }
2083    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
2084    {
2085        Err(PoKeysError::NotSupported)
2086    }
2087}
2088
2089/// Enumerate network PoKeys devices
2090pub fn enumerate_network_devices(timeout_ms: u32) -> Result<Vec<NetworkDeviceSummary>> {
2091    use crate::network::discover_all_devices;
2092
2093    log::info!("Enumerating network devices with timeout {timeout_ms}ms");
2094
2095    // Use the network discovery implementation
2096    discover_all_devices(timeout_ms)
2097}
2098
2099/// Connect to USB device by index
2100pub fn connect_to_device(device_index: u32) -> Result<PoKeysDevice> {
2101    let device_list = USB_DEVICE_LIST
2102        .lock()
2103        .map_err(|_| PoKeysError::InternalError("Failed to lock USB device list".to_string()))?;
2104
2105    if device_index as usize >= device_list.len() {
2106        return Err(PoKeysError::Parameter("Invalid device index".to_string()));
2107    }
2108
2109    let device_info = device_list[device_index as usize].clone();
2110    drop(device_list); // Release the lock early
2111
2112    // Create device instance
2113    let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2114
2115    // Platform-specific USB connection
2116    #[cfg(target_os = "macos")]
2117    {
2118        device.usb_interface = Some(Box::new(connect_usb_device_macos(&device_info.path)?));
2119    }
2120    #[cfg(target_os = "linux")]
2121    {
2122        device.usb_interface = Some(Box::new(connect_usb_device_linux(&device_info.path)?));
2123    }
2124    #[cfg(target_os = "windows")]
2125    {
2126        device.usb_interface = Some(Box::new(connect_usb_device_windows(&device_info.path)?));
2127    }
2128
2129    // Get device data
2130    device.get_device_data()?;
2131
2132    Ok(device)
2133}
2134
2135/// Connect to device by serial number
2136pub fn connect_to_device_with_serial(
2137    serial_number: u32,
2138    check_network: bool,
2139    timeout_ms: u32,
2140) -> Result<PoKeysDevice> {
2141    if check_network {
2142        let network_devices = enumerate_network_devices(timeout_ms)?;
2143        for network_device in network_devices {
2144            if network_device.serial_number == serial_number {
2145                return connect_to_network_device(&network_device);
2146            }
2147        }
2148    }
2149
2150    // First try USB devices
2151    let usb_count = enumerate_usb_devices()?;
2152
2153    for i in 0..usb_count {
2154        if let Ok(device) = connect_to_device(i as u32) {
2155            if device.device_data.serial_number == serial_number {
2156                return Ok(device);
2157            }
2158        }
2159    }
2160
2161    Err(PoKeysError::CannotConnect)
2162}
2163
2164/// Connect to network device
2165pub fn connect_to_network_device(device_summary: &NetworkDeviceSummary) -> Result<PoKeysDevice> {
2166    let mut device = PoKeysDevice::new(DeviceConnectionType::NetworkDevice);
2167
2168    // Create network interface based on device settings
2169    if device_summary.use_udp != 0 {
2170        device.connection_param = ConnectionParam::Udp;
2171    } else {
2172        device.connection_param = ConnectionParam::Tcp;
2173    }
2174
2175    // Platform-specific network connection
2176    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
2177    {
2178        device.network_interface = Some(connect_network_device(device_summary)?);
2179    }
2180
2181    // Get device data
2182    device.get_device_data()?;
2183
2184    Ok(device)
2185}
2186
2187// Stub implementations for compilation
2188struct StubUsbInterface;
2189
2190impl UsbHidInterface for StubUsbInterface {
2191    fn write(&mut self, _data: &[u8]) -> Result<usize> {
2192        Err(PoKeysError::NotSupported)
2193    }
2194
2195    fn read(&mut self, _buffer: &mut [u8]) -> Result<usize> {
2196        Err(PoKeysError::NotSupported)
2197    }
2198
2199    fn read_timeout(&mut self, _buffer: &mut [u8], _timeout: Duration) -> Result<usize> {
2200        Err(PoKeysError::NotSupported)
2201    }
2202}
2203
2204#[cfg(target_os = "macos")]
2205fn enumerate_usb_devices_macos(device_list: &mut Vec<UsbDeviceInfo>) -> Result<i32> {
2206    use std::process::Command;
2207
2208    // Use system_profiler to get USB device information
2209    let output = Command::new("system_profiler")
2210        .args(["SPUSBDataType", "-xml"])
2211        .output()
2212        .map_err(|e| PoKeysError::InternalError(format!("Failed to run system_profiler: {}", e)))?;
2213
2214    if !output.status.success() {
2215        return Err(PoKeysError::InternalError(
2216            "system_profiler command failed".to_string(),
2217        ));
2218    }
2219
2220    let output_str = String::from_utf8_lossy(&output.stdout);
2221
2222    // Look for PoKeys devices (vendor ID 0x1DC3)
2223    // This is a simple text-based search since we don't want to add XML parsing dependencies
2224    let lines: Vec<&str> = output_str.lines().collect();
2225    let mut i = 0;
2226
2227    while i < lines.len() {
2228        let line = lines[i].trim();
2229
2230        // Look for vendor_id entries
2231        if line.contains("<key>vendor_id</key>") && i + 1 < lines.len() {
2232            let next_line = lines[i + 1].trim();
2233
2234            // Check if it's a PoKeys device (vendor ID 0x1DC3 = 7619 decimal)
2235            if next_line.contains("<integer>7619</integer>")
2236                || next_line.contains("<string>0x1dc3</string>")
2237            {
2238                // Found a PoKeys device, now look for product_id and serial
2239                let mut product_id = 0u16;
2240                let mut serial_number = None;
2241                let mut location_id = String::new();
2242
2243                // Search forward for product_id and serial_number
2244                for j in (i + 2)..(i + 50).min(lines.len()) {
2245                    let search_line = lines[j].trim();
2246
2247                    if search_line.contains("<key>product_id</key>") && j + 1 < lines.len() {
2248                        let product_line = lines[j + 1].trim();
2249                        if let Some(start) = product_line.find("<integer>") {
2250                            if let Some(end) = product_line.find("</integer>") {
2251                                if let Ok(pid) = product_line[start + 9..end].parse::<u16>() {
2252                                    product_id = pid;
2253                                }
2254                            }
2255                        }
2256                    }
2257
2258                    if search_line.contains("<key>serial_num</key>") && j + 1 < lines.len() {
2259                        let serial_line = lines[j + 1].trim();
2260                        if let Some(start) = serial_line.find("<string>") {
2261                            if let Some(end) = serial_line.find("</string>") {
2262                                serial_number = Some(serial_line[start + 8..end].to_string());
2263                            }
2264                        }
2265                    }
2266
2267                    if search_line.contains("<key>location_id</key>") && j + 1 < lines.len() {
2268                        let location_line = lines[j + 1].trim();
2269                        if let Some(start) = location_line.find("<string>") {
2270                            if let Some(end) = location_line.find("</string>") {
2271                                location_id = location_line[start + 8..end].to_string();
2272                            }
2273                        }
2274                    }
2275
2276                    // Stop searching when we hit the next device or end of this device
2277                    if search_line.contains("<key>vendor_id</key>") && j > i + 2 {
2278                        break;
2279                    }
2280                }
2281
2282                // Add the device to our list
2283                device_list.push(UsbDeviceInfo {
2284                    vendor_id: 0x1DC3,
2285                    product_id,
2286                    serial_number,
2287                    path: format!("macos_usb_{}", location_id),
2288                });
2289            }
2290        }
2291        i += 1;
2292    }
2293
2294    Ok(device_list.len() as i32)
2295}
2296
2297#[cfg(target_os = "linux")]
2298fn enumerate_usb_devices_linux(device_list: &mut Vec<UsbDeviceInfo>) -> Result<i32> {
2299    // Linux-specific USB enumeration using libudev
2300    // This is a placeholder implementation
2301    // When implemented, add devices to device_list
2302    Ok(device_list.len() as i32)
2303}
2304
2305#[cfg(target_os = "windows")]
2306fn enumerate_usb_devices_windows(device_list: &mut Vec<UsbDeviceInfo>) -> Result<i32> {
2307    // Windows-specific USB enumeration using WinAPI
2308    // This is a placeholder implementation
2309    // When implemented, add devices to device_list
2310    Ok(device_list.len() as i32)
2311}
2312
2313// Network connection function
2314#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
2315fn connect_network_device(
2316    device_summary: &NetworkDeviceSummary,
2317) -> Result<Box<dyn NetworkInterface>> {
2318    use crate::network::{TcpNetworkInterface, UdpNetworkInterface};
2319
2320    if device_summary.use_udp != 0 {
2321        // Use UDP connection
2322        let interface = UdpNetworkInterface::new(device_summary.ip_address, 20055)?;
2323        Ok(Box::new(interface))
2324    } else {
2325        // Use TCP connection
2326        let interface = TcpNetworkInterface::new(device_summary.ip_address, 20055)?;
2327        Ok(Box::new(interface))
2328    }
2329}
2330
2331// Placeholder USB connection functions
2332#[cfg(target_os = "macos")]
2333fn connect_usb_device_macos(_path: &str) -> Result<StubUsbInterface> {
2334    Err(PoKeysError::NotSupported)
2335}
2336
2337#[cfg(target_os = "linux")]
2338fn connect_usb_device_linux(_path: &str) -> Result<StubUsbInterface> {
2339    Err(PoKeysError::NotSupported)
2340}
2341
2342#[cfg(target_os = "windows")]
2343fn connect_usb_device_windows(_path: &str) -> Result<StubUsbInterface> {
2344    Err(PoKeysError::NotSupported)
2345}
2346
2347/// Parse the "Get system load status" (0x05) response payload.
2348///
2349/// Protocol byte 3 (0-based index 2) holds the system load percentage.
2350fn parse_system_load_response(response: &[u8]) -> u8 {
2351    response[2]
2352}
2353
2354#[cfg(test)]
2355mod tests {
2356    use super::*;
2357
2358    #[test]
2359    fn test_device_creation() {
2360        let device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2361        assert_eq!(device.connection_type, DeviceConnectionType::UsbDevice);
2362        assert_eq!(device.pins.len(), 0); // Not initialized yet
2363    }
2364
2365    #[test]
2366    fn test_parse_system_load_response() {
2367        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
2368        response[1] = 0x05; // echo of command ID in byte 2
2369        response[2] = 42; // system load % in byte 3
2370        assert_eq!(parse_system_load_response(&response), 42);
2371
2372        response[2] = 0;
2373        assert_eq!(parse_system_load_response(&response), 0);
2374
2375        response[2] = 100;
2376        assert_eq!(parse_system_load_response(&response), 100);
2377    }
2378
2379    #[test]
2380    fn test_encode_matrix_kb_config_payload_layout_enabled() {
2381        // 16-row keyboard: rows 1..=16, columns 21..=24, activated.
2382        let row_pins: Vec<u8> = (1..=16).collect();
2383        let col_pins = vec![21u8, 22, 23, 24];
2384        let data = encode_matrix_kb_config_payload(true, 4, 16, &col_pins, &row_pins);
2385
2386        // byte 9 (data[0]): enable bit set
2387        assert_eq!(data[0], 1);
2388        // byte 10 (data[1]): (width-1) << 4 | (height-1) = (3 << 4) | 15 = 0x3F
2389        assert_eq!(data[1], 0x3F);
2390
2391        // bytes 11-18 (data[2..10]): rows 1..8 → pin codes 0..7
2392        for i in 0..8 {
2393            assert_eq!(data[2 + i], i as u8, "row {} (low half)", i);
2394        }
2395        // bytes 19-26 (data[10..18]): columns 21..24 → pin codes 20..23
2396        for i in 0..4 {
2397            assert_eq!(data[10 + i], 20 + i as u8, "column {}", i);
2398        }
2399
2400        // bytes 27-42 (data[18..34]): direct/macro bitmap, all zero (direct)
2401        assert!(
2402            data[18..34].iter().all(|&b| b == 0),
2403            "direct/macro bitmap must be zero by default"
2404        );
2405
2406        // bytes 43-50 (data[34..42]): rows 9..16 → pin codes 8..15
2407        for i in 0..8 {
2408            assert_eq!(
2409                data[34 + i],
2410                (8 + i) as u8,
2411                "row {} (extended; high half)",
2412                8 + i
2413            );
2414        }
2415
2416        // byte 51 (data[42]): alt-function pin must be zero (disabled)
2417        assert_eq!(data[42], 0);
2418    }
2419
2420    #[test]
2421    fn test_encode_matrix_kb_config_payload_layout_disabled() {
2422        // Same payload as enabled, but data[0] = 0. This is the deactivate
2423        // phase of the vendor sequence — pins / dimensions are still loaded
2424        // into the firmware, scanning just isn't activated yet.
2425        let row_pins: Vec<u8> = (1..=16).collect();
2426        let col_pins = vec![21u8, 22, 23, 24];
2427        let data = encode_matrix_kb_config_payload(false, 4, 16, &col_pins, &row_pins);
2428
2429        assert_eq!(data[0], 0, "enable bit must be cleared");
2430        assert_eq!(data[1], 0x3F, "size byte unchanged from activated payload");
2431        // Pins still populated even though not enabled
2432        for i in 0..8 {
2433            assert_eq!(data[2 + i], i as u8);
2434        }
2435        for i in 0..8 {
2436            assert_eq!(data[34 + i], (8 + i) as u8);
2437        }
2438    }
2439
2440    #[test]
2441    fn test_encode_matrix_kb_config_payload_extended_rows_unconditional() {
2442        // The vendor reference impl writes the extended-row block (data[34..42])
2443        // unconditionally — even when height ≤ 8. The firmware ignores those
2444        // bytes based on the size field in data[1], so it's harmless.
2445        let row_pins = vec![1u8, 2, 3, 4]; // only 4 rows used
2446        let col_pins = vec![21u8, 22, 23, 24];
2447        let data = encode_matrix_kb_config_payload(true, 4, 4, &col_pins, &row_pins);
2448
2449        // bytes 43-50 / data[34..42] are zero ONLY because row_pins doesn't
2450        // have entries 8-15. If the caller passes a longer row_pins slice
2451        // with non-zero values, those values are written through unchanged.
2452        assert!(data[34..42].iter().all(|&b| b == 0));
2453
2454        // Verify the unconditional-write property by passing row_pins with
2455        // entries beyond `height`.
2456        let long_rows: Vec<u8> = (1..=16).collect();
2457        let data2 = encode_matrix_kb_config_payload(true, 4, 4, &col_pins, &long_rows);
2458        // height=4 but row_pins[8..16] are populated → those bytes are written
2459        for i in 0..8 {
2460            assert_eq!(
2461                data2[34 + i],
2462                (8 + i) as u8,
2463                "extended row pin {i} should be written verbatim"
2464            );
2465        }
2466    }
2467
2468    #[test]
2469    fn test_encode_matrix_kb_mapping_chunk_writes_codes_modifiers_and_triggered_bitmap() {
2470        let codes: Vec<u8> = (0..128).map(|i| i as u8).collect();
2471        let modifiers: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(2)).collect();
2472        let mut triggered = vec![0u8; 128];
2473        // Mark keys 33 and 47 as triggered (chunk_base = 32 → bits 1 and 15 of bitmap).
2474        triggered[33] = 1;
2475        triggered[47] = 99;
2476
2477        let chunk = encode_matrix_kb_mapping_chunk(&codes, &modifiers, Some(&triggered), 32);
2478
2479        // codes 32..48 → data[0..16]
2480        for i in 0..16 {
2481            assert_eq!(chunk[i], (32 + i) as u8, "code {i}");
2482            assert_eq!(chunk[16 + i], (32 + i) as u8 * 2, "modifier {i}");
2483        }
2484        // bitmap: bit 1 (key 33) and bit 15 (key 47)
2485        let bitmap = u16::from_le_bytes([chunk[32], chunk[33]]);
2486        assert_eq!(bitmap, (1 << 1) | (1 << 15));
2487    }
2488
2489    #[test]
2490    fn test_encode_matrix_kb_mapping_chunk_omits_bitmap_when_no_triggered() {
2491        let codes = vec![0u8; 128];
2492        let modifiers = vec![0u8; 128];
2493        let chunk = encode_matrix_kb_mapping_chunk(&codes, &modifiers, None, 0);
2494        assert_eq!(chunk[32], 0);
2495        assert_eq!(chunk[33], 0);
2496    }
2497
2498    #[test]
2499    fn test_decode_matrix_kb_mapping_chunk_roundtrip() {
2500        let codes_in: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_add(7)).collect();
2501        let modifiers_in: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(3)).collect();
2502        let mut triggered_in = vec![0u8; 128];
2503        triggered_in[5] = 1;
2504        triggered_in[20] = 1;
2505
2506        // Build response payload from the encoded chunk for chunk_base=16
2507        let chunk =
2508            encode_matrix_kb_mapping_chunk(&codes_in, &modifiers_in, Some(&triggered_in), 16);
2509        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
2510        response[1] = 0xCA;
2511        response[8..8 + 16].copy_from_slice(&chunk[0..16]); // codes
2512        response[24..24 + 16].copy_from_slice(&chunk[16..32]); // modifiers
2513        response[40] = chunk[32];
2514        response[41] = chunk[33];
2515
2516        let mut codes_out = vec![0u8; 128];
2517        let mut modifiers_out = vec![0u8; 128];
2518        let mut triggered_out = vec![0u8; 128];
2519        decode_matrix_kb_mapping_chunk(
2520            &response,
2521            &mut codes_out,
2522            &mut modifiers_out,
2523            Some(&mut triggered_out),
2524            16,
2525        );
2526
2527        // Only keys 16..32 should be populated
2528        for i in 16..32 {
2529            assert_eq!(codes_out[i], codes_in[i], "code {i}");
2530            assert_eq!(modifiers_out[i], modifiers_in[i], "modifier {i}");
2531        }
2532        assert_eq!(triggered_out[20], 1);
2533        assert_eq!(triggered_out[16], 0);
2534        // Untouched buckets remain zero
2535        assert_eq!(codes_out[0], 0);
2536        assert_eq!(codes_out[40], 0);
2537    }
2538
2539    #[test]
2540    fn test_validate_matrix_kb_mapping_response_rejects_wrong_command() {
2541        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
2542        response[1] = 0xC9; // not 0xCA
2543        assert!(validate_matrix_kb_mapping_response(&response, 12).is_err());
2544    }
2545
2546    #[test]
2547    fn test_validate_matrix_kb_mapping_response_rejects_short_response() {
2548        let mut response = [0u8; 30]; // < 42 required
2549        response[1] = 0xCA;
2550        assert!(validate_matrix_kb_mapping_response(&response, 12).is_err());
2551    }
2552
2553    #[test]
2554    fn test_validate_matrix_kb_mapping_response_accepts_valid() {
2555        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
2556        response[1] = 0xCA;
2557        assert!(validate_matrix_kb_mapping_response(&response, 12).is_ok());
2558    }
2559
2560    #[test]
2561    fn test_configure_matrix_keyboard_rejects_pin_in_both_row_and_col() {
2562        let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2563        let cols = [5u8, 6, 7, 8];
2564        let rows = [1u8, 2, 3, 5]; // pin 5 is also in cols
2565        let err = device
2566            .configure_matrix_keyboard(4, 4, &cols, &rows)
2567            .expect_err("must reject overlapping pin assignment");
2568        match err {
2569            PoKeysError::Parameter(msg) => {
2570                assert!(
2571                    msg.contains("Pin 5"),
2572                    "error must name the offending pin, got: {msg}"
2573                );
2574            }
2575            other => panic!("expected Parameter error, got {other:?}"),
2576        }
2577    }
2578
2579    #[test]
2580    fn test_configure_matrix_keyboard_rejects_pin_zero() {
2581        let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2582        let cols = [5u8, 6, 7, 8];
2583        let rows = [0u8, 2, 3, 4]; // pin 0 is invalid
2584        let err = device
2585            .configure_matrix_keyboard(4, 4, &cols, &rows)
2586            .expect_err("must reject pin 0");
2587        assert!(matches!(err, PoKeysError::Parameter(_)));
2588    }
2589
2590    #[test]
2591    fn test_configure_matrix_keyboard_rejects_pin_above_55() {
2592        let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2593        let cols = [5u8, 6, 7, 8];
2594        let rows = [1u8, 2, 3, 56]; // pin 56 is out of range
2595        let err = device
2596            .configure_matrix_keyboard(4, 4, &cols, &rows)
2597            .expect_err("must reject pin 56");
2598        assert!(matches!(err, PoKeysError::Parameter(_)));
2599    }
2600
2601    #[test]
2602    fn test_configure_matrix_keyboard_only_validates_used_pins() {
2603        // Pins beyond `width`/`height` should be ignored entirely so callers
2604        // can pass the full row_pins/column_pins arrays without padding.
2605        let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2606        let cols = [5u8, 6, 7, 8, 0, 99, 99, 99]; // unused entries are bogus
2607        let rows = [1u8, 2, 3, 4, 0, 99, 99, 99];
2608        // Validation must pass; the call will then fail on read_all_pin_settings_raw
2609        // because there's no connection — that's not what we're testing here, just
2610        // that we got past the parameter checks.
2611        let err = device
2612            .configure_matrix_keyboard(4, 4, &cols, &rows)
2613            .expect_err("expected NotConnected, not Parameter");
2614        assert!(
2615            !matches!(err, PoKeysError::Parameter(_)),
2616            "validation should have accepted these pins; got Parameter error: {err:?}"
2617        );
2618    }
2619
2620    /// Build a synthetic `0xCA` option-10 readback response. Uses 1-based
2621    /// row/col pin arguments, encoding them as 0-based on the wire (matching
2622    /// what the firmware stores).
2623    fn make_matrix_kb_readback(
2624        enabled: bool,
2625        width: u8,
2626        height: u8,
2627        rows: &[u8],
2628        cols: &[u8],
2629    ) -> [u8; RESPONSE_BUFFER_SIZE] {
2630        let mut r = [0u8; RESPONSE_BUFFER_SIZE];
2631        r[1] = 0xCA;
2632        r[8] = if enabled { 1 } else { 0 };
2633        r[9] = ((width.saturating_sub(1)) << 4) | (height.saturating_sub(1));
2634        for (i, &p) in rows.iter().enumerate().take(8) {
2635            r[10 + i] = p.saturating_sub(1);
2636        }
2637        for (i, &p) in cols.iter().enumerate().take(8) {
2638            r[18 + i] = p.saturating_sub(1);
2639        }
2640        for (offset, &p) in rows.iter().enumerate().skip(8).take(8) {
2641            r[42 + (offset - 8)] = p.saturating_sub(1);
2642        }
2643        r
2644    }
2645
2646    #[test]
2647    fn test_parse_matrix_kb_config_readback_full() {
2648        let rows: Vec<u8> = (1..=16).collect();
2649        let cols = [17u8, 18, 19, 20, 21, 22, 23, 24];
2650        let mut r = make_matrix_kb_readback(true, 8, 16, &rows, &cols);
2651        // Populate fields the helper doesn't touch.
2652        r[26] = 0b0000_0101; // keys 0 and 2 set to macro
2653        r[50] = 7; // alt function pin = 7
2654        r[51] = 25; // scanning decimation
2655        let cfg = parse_matrix_kb_config_readback(&r).expect("parse");
2656        assert!(cfg.enabled);
2657        assert_eq!(cfg.width, 8);
2658        assert_eq!(cfg.height, 16);
2659        for i in 0..16 {
2660            assert_eq!(cfg.row_pins[i], (i + 1) as u8);
2661        }
2662        for i in 0..8 {
2663            assert_eq!(cfg.column_pins[i], (17 + i) as u8);
2664        }
2665        assert_eq!(cfg.direct_macro_bitmap[0], 0b0000_0101);
2666        assert_eq!(cfg.alternate_function_pin, 7);
2667        assert_eq!(cfg.scanning_decimation, 25);
2668        assert_eq!(cfg.size_byte(), 0x7F);
2669    }
2670
2671    #[test]
2672    fn test_parse_matrix_kb_config_readback_rejects_short_response() {
2673        let mut r = [0u8; 30];
2674        r[1] = 0xCA;
2675        let err = parse_matrix_kb_config_readback(&r).expect_err("must reject");
2676        assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("readback too short")));
2677    }
2678
2679    #[test]
2680    fn test_parse_matrix_kb_config_readback_rejects_wrong_command_echo() {
2681        let mut r = [0u8; RESPONSE_BUFFER_SIZE];
2682        r[1] = 0xC9;
2683        let err = parse_matrix_kb_config_readback(&r).expect_err("must reject");
2684        assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("echo mismatch")));
2685    }
2686
2687    #[test]
2688    fn test_verify_matches_accepts_matching_4x4() {
2689        let rows = [1u8, 2, 3, 4];
2690        let cols = [5u8, 6, 7, 8];
2691        let r = make_matrix_kb_readback(true, 4, 4, &rows, &cols);
2692        let cfg = parse_matrix_kb_config_readback(&r).unwrap();
2693        verify_matrix_kb_config_matches(&cfg, 4, 4, &cols, &rows, true).expect("must accept");
2694    }
2695
2696    #[test]
2697    fn test_verify_matches_rejects_disabled_when_expecting_enabled() {
2698        // Simulates "configuration locked" — write echoed but device kept
2699        // the keyboard disabled.
2700        let rows = [1u8, 2, 3, 4];
2701        let cols = [5u8, 6, 7, 8];
2702        let r = make_matrix_kb_readback(false, 4, 4, &rows, &cols);
2703        let cfg = parse_matrix_kb_config_readback(&r).unwrap();
2704        let err = verify_matrix_kb_config_matches(&cfg, 4, 4, &cols, &rows, true)
2705            .expect_err("must reject silent rejection");
2706        match err {
2707            PoKeysError::Protocol(msg) => assert!(
2708                msg.contains("enabled=false") && msg.contains("locked"),
2709                "error must hint at locked state, got: {msg}"
2710            ),
2711            other => panic!("expected Protocol error, got {other:?}"),
2712        }
2713    }
2714
2715    #[test]
2716    fn test_verify_matches_disabled_path_skips_size_and_pin_checks() {
2717        let mut r = [0u8; RESPONSE_BUFFER_SIZE];
2718        r[1] = 0xCA;
2719        r[8] = 0; // disabled
2720        r[9] = 0xFF; // garbage size
2721        let cfg = parse_matrix_kb_config_readback(&r).unwrap();
2722        verify_matrix_kb_config_matches(&cfg, 0, 0, &[], &[], false).expect("must accept");
2723    }
2724
2725    #[test]
2726    fn test_verify_matches_rejects_wrong_size() {
2727        let rows = [1u8, 2, 3, 4];
2728        let cols = [5u8, 6, 7, 8];
2729        let r = make_matrix_kb_readback(true, 4, 4, &rows, &cols);
2730        let cfg = parse_matrix_kb_config_readback(&r).unwrap();
2731        let err = verify_matrix_kb_config_matches(&cfg, 8, 4, &cols, &rows, true)
2732            .expect_err("must reject size mismatch");
2733        assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("width=4")));
2734    }
2735
2736    #[test]
2737    fn test_verify_matches_rejects_wrong_row_pin() {
2738        let rows = [1u8, 2, 3, 4];
2739        let cols = [5u8, 6, 7, 8];
2740        let mut r = make_matrix_kb_readback(true, 4, 4, &rows, &cols);
2741        r[12] = 99; // corrupt row pin 2
2742        let cfg = parse_matrix_kb_config_readback(&r).unwrap();
2743        let err = verify_matrix_kb_config_matches(&cfg, 4, 4, &cols, &rows, true)
2744            .expect_err("must reject pin mismatch");
2745        assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("row pin 2")));
2746    }
2747
2748    #[test]
2749    fn test_verify_matches_rejects_wrong_extended_row_pin() {
2750        let rows: Vec<u8> = (1..=12).collect();
2751        let cols = [13u8, 14, 15, 16];
2752        let mut r = make_matrix_kb_readback(true, 4, 12, &rows, &cols);
2753        r[44] = 99; // corrupt extended row pin (offset 10)
2754        let cfg = parse_matrix_kb_config_readback(&r).unwrap();
2755        let err = verify_matrix_kb_config_matches(&cfg, 4, 12, &cols, &rows, true)
2756            .expect_err("must reject extended row pin mismatch");
2757        assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("extended row pin 10")));
2758    }
2759
2760    #[test]
2761    fn test_parse_device_data_populates_lock_status() {
2762        let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2763        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
2764        response[8..12].copy_from_slice(b"PK58");
2765        response[4] = 0x12; // software version (just non-zero)
2766        response[5] = 3;
2767        response[58] = 1; // configuration lock status (byte 59 1-based)
2768        device
2769            .parse_device_data_response(&response)
2770            .expect("parse must succeed");
2771        assert_eq!(device.device_data.device_lock_status, 1);
2772        assert!(device.device_data.device_locked());
2773    }
2774
2775    #[test]
2776    fn test_parse_device_data_lock_status_zero_means_unlocked() {
2777        let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
2778        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
2779        response[8..12].copy_from_slice(b"PKEx");
2780        response[4] = 0x10;
2781        response[5] = 1;
2782        response[58] = 0;
2783        device
2784            .parse_device_data_response(&response)
2785            .expect("parse must succeed");
2786        assert!(!device.device_data.device_locked());
2787    }
2788
2789    #[test]
2790    fn test_pin_capability_checking() {
2791        // Test PoKeys57CNC pin capabilities
2792        assert!(check_pokeys57cnc_pin_capability(
2793            1,
2794            crate::io::PinCapability::DigitalInput
2795        ));
2796        assert!(check_pokeys57cnc_pin_capability(
2797            1,
2798            crate::io::PinCapability::DigitalOutput
2799        ));
2800        assert!(!check_pokeys57cnc_pin_capability(
2801            14,
2802            crate::io::PinCapability::DigitalOutput
2803        )); // Input only
2804        assert!(!check_pokeys57cnc_pin_capability(
2805            100,
2806            crate::io::PinCapability::DigitalInput
2807        )); // Invalid pin
2808    }
2809
2810    // Hardware tests - only run when hardware-tests feature is enabled
2811    #[cfg(feature = "hardware-tests")]
2812    mod hardware_tests {
2813        use super::*;
2814        use std::thread;
2815        use std::time::Duration;
2816
2817        #[test]
2818        fn test_hardware_device_enumeration() {
2819            println!("Testing hardware device enumeration...");
2820
2821            match enumerate_usb_devices() {
2822                Ok(count) => {
2823                    println!("Found {} USB PoKeys devices", count);
2824                    if count == 0 {
2825                        println!(
2826                            "WARNING: No PoKeys devices found. Connect a device to run hardware tests."
2827                        );
2828                    }
2829                }
2830                Err(e) => {
2831                    panic!("Failed to enumerate USB devices: {}", e);
2832                }
2833            }
2834        }
2835
2836        #[test]
2837        fn test_hardware_device_connection() {
2838            println!("Testing hardware device connection...");
2839
2840            let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
2841
2842            if device_count == 0 {
2843                println!("SKIP: No PoKeys devices found for hardware test");
2844                return;
2845            }
2846
2847            match connect_to_device(0) {
2848                Ok(mut device) => {
2849                    println!("Successfully connected to device");
2850
2851                    // Test getting device data
2852                    match device.get_device_data() {
2853                        Ok(_) => {
2854                            println!("Device Serial: {}", device.device_data.serial_number);
2855                            println!(
2856                                "Firmware: {}.{}",
2857                                device.device_data.firmware_version_major,
2858                                device.device_data.firmware_version_minor
2859                            );
2860                            println!("Pin Count: {}", device.info.pin_count);
2861                        }
2862                        Err(e) => {
2863                            println!("WARNING: Could not get device data: {}", e);
2864                        }
2865                    }
2866                }
2867                Err(e) => {
2868                    panic!("Failed to connect to device: {}", e);
2869                }
2870            }
2871        }
2872
2873        #[test]
2874        fn test_hardware_digital_io() {
2875            println!("Testing hardware digital I/O...");
2876
2877            let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
2878            if device_count == 0 {
2879                println!("SKIP: No PoKeys devices found for hardware test");
2880                return;
2881            }
2882
2883            let mut device = connect_to_device(0).expect("Failed to connect to device");
2884            device.get_device_data().expect("Failed to get device data");
2885
2886            // Test setting pin as digital output
2887            match device.set_pin_function(1, crate::io::PinFunction::DigitalOutput) {
2888                Ok(_) => {
2889                    println!("Successfully configured pin 1 as digital output");
2890
2891                    // Test setting output high and low
2892                    for state in [true, false, true, false] {
2893                        match device.set_digital_output(1, state) {
2894                            Ok(_) => {
2895                                println!("Set pin 1 to {}", if state { "HIGH" } else { "LOW" })
2896                            }
2897                            Err(e) => println!("WARNING: Failed to set digital output: {}", e),
2898                        }
2899                        thread::sleep(Duration::from_millis(100));
2900                    }
2901                }
2902                Err(e) => {
2903                    println!("WARNING: Could not configure pin function: {}", e);
2904                }
2905            }
2906        }
2907
2908        #[test]
2909        fn test_hardware_analog_input() {
2910            println!("Testing hardware analog input...");
2911
2912            let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
2913            if device_count == 0 {
2914                println!("SKIP: No PoKeys devices found for hardware test");
2915                return;
2916            }
2917
2918            let mut device = connect_to_device(0).expect("Failed to connect to device");
2919            device.get_device_data().expect("Failed to get device data");
2920
2921            // Test reading analog input
2922            match device.get_analog_input(1) {
2923                Ok(value) => {
2924                    println!("Analog input 1 value: {}", value);
2925                    // Basic sanity check - value should be within ADC range
2926                    assert!(value <= 4095, "Analog value out of range for 12-bit ADC");
2927                }
2928                Err(e) => {
2929                    println!("WARNING: Could not read analog input: {}", e);
2930                }
2931            }
2932        }
2933
2934        #[test]
2935        fn test_hardware_pwm_output() {
2936            println!("Testing hardware PWM output...");
2937
2938            let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
2939            if device_count == 0 {
2940                println!("SKIP: No PoKeys devices found for hardware test");
2941                return;
2942            }
2943
2944            let mut device = connect_to_device(0).expect("Failed to connect to device");
2945            device.get_device_data().expect("Failed to get device data");
2946
2947            // Test PWM configuration
2948            match device.set_pwm_period(25000) {
2949                Ok(_) => {
2950                    println!("Set PWM period to 25000 cycles");
2951
2952                    // Test different duty cycles
2953                    for duty in [25.0, 50.0, 75.0, 0.0] {
2954                        match device.set_pwm_duty_cycle_percent_for_pin(17, duty) {
2955                            Ok(_) => {
2956                                println!("Set PWM duty cycle to {}%", duty);
2957                                thread::sleep(Duration::from_millis(200));
2958                            }
2959                            Err(e) => {
2960                                println!("WARNING: Could not set PWM duty cycle: {}", e);
2961                            }
2962                        }
2963                    }
2964                }
2965                Err(e) => {
2966                    println!("WARNING: Could not configure PWM: {}", e);
2967                }
2968            }
2969        }
2970
2971        #[test]
2972        fn test_hardware_encoder_reading() {
2973            println!("Testing hardware encoder reading...");
2974
2975            let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
2976            if device_count == 0 {
2977                println!("SKIP: No PoKeys devices found for hardware test");
2978                return;
2979            }
2980
2981            let mut device = connect_to_device(0).expect("Failed to connect to device");
2982            device.get_device_data().expect("Failed to get device data");
2983
2984            // Test encoder configuration
2985            let mut options = crate::encoders::EncoderOptions::new();
2986            options.enabled = true;
2987            options.sampling_4x = true;
2988
2989            match device.configure_encoder(0, 1, 2, options) {
2990                Ok(_) => {
2991                    println!("Configured encoder 0 on pins 1 and 2");
2992
2993                    // Read encoder value multiple times
2994                    for i in 0..5 {
2995                        match device.get_encoder_value(0) {
2996                            Ok(value) => {
2997                                println!("Encoder 0 reading {}: {}", i + 1, value);
2998                            }
2999                            Err(e) => {
3000                                println!("WARNING: Could not read encoder: {}", e);
3001                            }
3002                        }
3003                        thread::sleep(Duration::from_millis(100));
3004                    }
3005                }
3006                Err(e) => {
3007                    println!("WARNING: Could not configure encoder: {}", e);
3008                }
3009            }
3010        }
3011
3012        #[test]
3013        fn test_hardware_network_discovery() {
3014            println!("Testing hardware network device discovery...");
3015
3016            match enumerate_network_devices(2000) {
3017                Ok(devices) => {
3018                    println!("Found {} network PoKeys devices", devices.len());
3019
3020                    for (i, device) in devices.iter().enumerate() {
3021                        println!(
3022                            "Network device {}: Serial {}, IP {}.{}.{}.{}",
3023                            i + 1,
3024                            device.serial_number,
3025                            device.ip_address[0],
3026                            device.ip_address[1],
3027                            device.ip_address[2],
3028                            device.ip_address[3]
3029                        );
3030                    }
3031
3032                    if devices.is_empty() {
3033                        println!(
3034                            "No network devices found - this is normal if no network PoKeys devices are present"
3035                        );
3036                    }
3037                }
3038                Err(e) => {
3039                    println!("WARNING: Network discovery failed: {}", e);
3040                }
3041            }
3042        }
3043
3044        #[test]
3045        fn test_hardware_device_info_validation() {
3046            println!("Testing hardware device info validation...");
3047
3048            let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
3049            if device_count == 0 {
3050                println!("SKIP: No PoKeys devices found for hardware test");
3051                return;
3052            }
3053
3054            let mut device = connect_to_device(0).expect("Failed to connect to device");
3055            device.get_device_data().expect("Failed to get device data");
3056
3057            // Validate device information makes sense
3058            assert!(
3059                device.device_data.serial_number > 0,
3060                "Serial number should be non-zero"
3061            );
3062            assert!(device.info.pin_count > 0, "Pin count should be non-zero");
3063            assert!(
3064                device.info.pin_count <= 100,
3065                "Pin count should be reasonable"
3066            );
3067
3068            println!("Device validation passed:");
3069            println!("  Serial: {}", device.device_data.serial_number);
3070            println!("  Pins: {}", device.info.pin_count);
3071            println!("  PWM Channels: {}", device.info.pwm_count);
3072            println!("  Encoders: {}", device.info.encoders_count);
3073            println!("  Analog Inputs: {}", device.info.analog_inputs);
3074        }
3075    }
3076}