Skip to main content

pokeys_lib/io/
mod.rs

1//! Digital and analog I/O operations
2
3use crate::device::PoKeysDevice;
4use crate::error::{PoKeysError, Result};
5use log::info;
6use serde::{Deserialize, Serialize};
7
8mod private;
9
10use private::INVERT_PIN_BIT;
11
12// PoKeys pin-function identifier (defined in a tiny private module so the
13// `#[allow(deprecated)]` scope covers the derive-macro expansions, which
14// otherwise flag every variant reference inside the generated Debug /
15// PartialEq / Serialize / Deserialize code).
16#[allow(deprecated)]
17mod pin_function {
18    use serde::{Deserialize, Serialize};
19
20    /// PoKeys pin-function identifier.
21    ///
22    /// The wire-level "pin settings" byte (byte 4 of protocol command
23    /// `0x10`) is a bitfield: the low 7 bits carry the base function and
24    /// bit 7 (`0x80`, [`PinFunction::InvertPin`]) composes with any
25    /// digital function to request firmware-level polarity inversion.
26    /// This enum models the base function only. To apply the invert flag,
27    /// use [`crate::PoKeysDevice::set_pin_function_with_invert`] — passing
28    /// `PinFunction::InvertPin` directly to
29    /// [`crate::PoKeysDevice::set_pin_function`] is not meaningful and is
30    /// kept only for backward compatibility.
31    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32    pub enum PinFunction {
33        PinRestricted = 0,
34        Reserved = 1,
35        DigitalInput = 2,
36        DigitalOutput = 4,
37        AnalogInput = 8,
38        AnalogOutput = 16,
39        TriggeredInput = 32,
40        DigitalCounter = 64,
41        #[deprecated(
42            since = "0.23.0",
43            note = "Use PoKeysDevice::set_pin_function_with_invert to apply the invert flag; the variant is a protocol bit, not a standalone pin function."
44        )]
45        InvertPin = 128,
46    }
47}
48
49pub use pin_function::PinFunction;
50
51/// Decode a cached pin-settings byte into a `PinFunction`, ignoring bit 7
52/// (the invert flag). Unknown bit patterns decode to `PinRestricted`.
53///
54/// Split out from [`PoKeysDevice::get_pin_function`] so the mapping can be
55/// unit-tested and reused by [`PinData::base_function`].
56pub(crate) fn decode_pin_function_from_cache(byte: u8) -> PinFunction {
57    match byte & 0x7F {
58        0 => PinFunction::PinRestricted,
59        1 => PinFunction::Reserved,
60        2 => PinFunction::DigitalInput,
61        4 => PinFunction::DigitalOutput,
62        8 => PinFunction::AnalogInput,
63        16 => PinFunction::AnalogOutput,
64        32 => PinFunction::TriggeredInput,
65        64 => PinFunction::DigitalCounter,
66        _ => PinFunction::PinRestricted,
67    }
68}
69
70impl PinFunction {
71    /// Convert u8 value to PinFunction enum
72    /// Note: PoKeys uses bit flags for pin functions
73    pub fn from_u8(value: u8) -> Result<Self> {
74        #[allow(deprecated)]
75        match value {
76            0 => Ok(PinFunction::PinRestricted),
77            1 => Ok(PinFunction::Reserved),
78            2 => Ok(PinFunction::DigitalInput),
79            4 => Ok(PinFunction::DigitalOutput),
80            8 => Ok(PinFunction::AnalogInput),
81            16 => Ok(PinFunction::AnalogOutput),
82            32 => Ok(PinFunction::TriggeredInput),
83            64 => Ok(PinFunction::DigitalCounter),
84            128 => Ok(PinFunction::InvertPin),
85            // Handle combined flags by returning the primary function
86            v if (v & PinFunction::DigitalOutput as u8) != 0 => Ok(PinFunction::DigitalOutput),
87            v if (v & PinFunction::DigitalInput as u8) != 0 => Ok(PinFunction::DigitalInput),
88            v if (v & PinFunction::AnalogInput as u8) != 0 => Ok(PinFunction::AnalogInput),
89            v if (v & PinFunction::AnalogOutput as u8) != 0 => Ok(PinFunction::AnalogOutput),
90            v if (v & PinFunction::DigitalCounter as u8) != 0 => Ok(PinFunction::DigitalCounter),
91            v if (v & PinFunction::TriggeredInput as u8) != 0 => Ok(PinFunction::TriggeredInput),
92            _ => Ok(PinFunction::PinRestricted), // Default to restricted for unknown values
93        }
94    }
95}
96
97/// Pin capabilities for checking device support
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PinCapability {
100    DigitalInput = 1,
101    DigitalOutput,
102    AnalogInput,
103    MfAnalogInput,
104    AnalogOutput,
105    KeyboardMapping,
106    TriggeredInput,
107    DigitalCounter,
108    PwmOutput,
109    FastEncoder1A,
110    FastEncoder1B,
111    FastEncoder1I,
112    FastEncoder2A,
113    FastEncoder2B,
114    FastEncoder2I,
115    FastEncoder3A,
116    FastEncoder3B,
117    FastEncoder3I,
118    UltraFastEncoderA,
119    UltraFastEncoderB,
120    UltraFastEncoderI,
121    LcdE,
122    LcdRw,
123    LcdRs,
124    LcdD4,
125    LcdD5,
126    LcdD6,
127    LcdD7,
128}
129
130/// Pin-specific data structure
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct PinData {
133    pub digital_counter_value: u32,
134    pub analog_value: u32,
135    pub pin_function: u8,
136    pub counter_options: u8,
137    pub digital_value_get: u8,
138    pub digital_value_set: u8,
139    pub digital_counter_available: u8,
140    pub mapping_type: u8,
141    pub key_code_macro_id: u8,
142    pub key_modifier: u8,
143    pub down_key_code_macro_id: u8,
144    pub down_key_modifier: u8,
145    pub up_key_code_macro_id: u8,
146    pub up_key_modifier: u8,
147    pub prevent_update: u8,
148}
149
150impl PinData {
151    pub fn new() -> Self {
152        Self {
153            digital_counter_value: 0,
154            analog_value: 0,
155            pin_function: PinFunction::PinRestricted as u8,
156            counter_options: 0,
157            digital_value_get: 0,
158            digital_value_set: 0,
159            digital_counter_available: 0,
160            mapping_type: 0,
161            key_code_macro_id: 0,
162            key_modifier: 0,
163            down_key_code_macro_id: 0,
164            down_key_modifier: 0,
165            up_key_code_macro_id: 0,
166            up_key_modifier: 0,
167            prevent_update: 0,
168        }
169    }
170
171    pub fn is_digital_input(&self) -> bool {
172        (self.pin_function & PinFunction::DigitalInput as u8) != 0
173    }
174
175    pub fn is_digital_output(&self) -> bool {
176        (self.pin_function & PinFunction::DigitalOutput as u8) != 0
177    }
178
179    pub fn is_analog_input(&self) -> bool {
180        (self.pin_function & PinFunction::AnalogInput as u8) != 0
181    }
182
183    pub fn is_analog_output(&self) -> bool {
184        (self.pin_function & PinFunction::AnalogOutput as u8) != 0
185    }
186
187    pub fn is_digital_counter(&self) -> bool {
188        (self.pin_function & PinFunction::DigitalCounter as u8) != 0
189    }
190
191    /// True if bit 7 (the hardware invert flag) is set on this pin's
192    /// cached function byte.
193    pub fn is_inverted(&self) -> bool {
194        (self.pin_function & INVERT_PIN_BIT) != 0
195    }
196
197    /// Decode the base pin function from the cached byte, ignoring the
198    /// invert bit. Pair with [`Self::is_inverted`] for the full picture.
199    pub fn base_function(&self) -> PinFunction {
200        decode_pin_function_from_cache(self.pin_function)
201    }
202}
203
204impl Default for PinData {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210impl PoKeysDevice {
211    /// Set a pin's function (non-inverted polarity).
212    ///
213    /// Thin wrapper over [`Self::set_pin_function_with_invert`] with
214    /// `inverted = false`; byte-identical on the wire to the pre-invert
215    /// behaviour for every existing caller.
216    pub fn set_pin_function(
217        &mut self,
218        pin: u32,
219        pin_function: PinFunction,
220    ) -> Result<(u32, PinFunction)> {
221        self.set_pin_function_with_invert(pin, pin_function, false)
222    }
223
224    /// Set a pin's function with an optional hardware invert flag.
225    ///
226    /// When `inverted == true`, bit 7 (`0x80`) of protocol byte 4 is set,
227    /// producing a combined wire byte such as `0x82` for inverted
228    /// `DigitalInput`. The firmware then reports/drives the logical
229    /// complement of the electrical state at no CPU cost to the caller.
230    ///
231    /// Pin functions that honor the invert bit:
232    /// - [`PinFunction::DigitalInput`]
233    /// - [`PinFunction::DigitalOutput`]
234    /// - [`PinFunction::TriggeredInput`]
235    ///
236    /// Pin functions that ignore the invert bit (firmware silently drops it):
237    /// - [`PinFunction::AnalogInput`], [`PinFunction::AnalogOutput`]
238    /// - [`PinFunction::DigitalCounter`]
239    /// - [`PinFunction::PinRestricted`], [`PinFunction::Reserved`]
240    ///
241    /// For functions that ignore the flag, this method logs a warning via the
242    /// `log` crate and still sends the byte as requested. Use
243    /// [`Self::get_pin_invert`] to read the invert state back from the cache.
244    pub fn set_pin_function_with_invert(
245        &mut self,
246        pin: u32,
247        pin_function: PinFunction,
248        inverted: bool,
249    ) -> Result<(u32, PinFunction)> {
250        self.write_pin_function(pin, pin_function, inverted)
251    }
252
253    /// Get pin function (base function only; the invert bit is ignored).
254    ///
255    /// To read the invert flag, use [`Self::get_pin_invert`].
256    pub fn get_pin_function(&self, pin: u32) -> Result<PinFunction> {
257        let pin_index = self.check_pin_range(pin)?;
258        Ok(decode_pin_function_from_cache(
259            self.pins[pin_index].pin_function,
260        ))
261    }
262
263    /// Return whether bit 7 (the hardware invert flag) is set on the pin's
264    /// cached configuration byte.
265    ///
266    /// Reflects the most recently written or read-back wire byte; call
267    /// [`Self::read_all_pin_functions`] first if you want the device's
268    /// current state rather than the local cache.
269    pub fn get_pin_invert(&self, pin: u32) -> Result<bool> {
270        let pin_index = self.check_pin_range(pin)?;
271        Ok((self.pins[pin_index].pin_function & INVERT_PIN_BIT) != 0)
272    }
273
274    /// Read digital input
275    pub fn get_digital_input(&mut self, pin: u32) -> Result<bool> {
276        if pin == 0 || pin as usize > self.pins.len() {
277            return Err(PoKeysError::Parameter("Invalid pin number".to_string()));
278        }
279
280        // Read all digital inputs from device
281        let res = self.read_digital_input(pin)?;
282
283        let pin_index = (pin - 1) as usize;
284        self.pins[pin_index].digital_value_get = res;
285        Ok(res != 0)
286    }
287
288    /// Set digital output
289    pub fn set_digital_output(&mut self, pin: u32, value: bool) -> Result<bool> {
290        if pin == 0 || pin as usize > self.pins.len() {
291            return Err(PoKeysError::Parameter("Invalid pin number".to_string()));
292        }
293
294        // Send digital output to device
295        match self.write_digital_output(pin, !value) {
296            Ok(_) => {
297                let pin_index = (pin - 1) as usize;
298                self.pins[pin_index].digital_value_set = if value { 1 } else { 0 };
299                info!(
300                    "Pin {} set to {:?}",
301                    pin,
302                    if value { "High" } else { "Low" }
303                );
304                Ok(true)
305            }
306            Err(e) => Err(e),
307        }
308    }
309
310    /// Read analog input
311    pub fn get_analog_input(&mut self, pin: u32) -> Result<u32> {
312        if pin == 0 || pin as usize > self.pins.len() {
313            return Err(PoKeysError::Parameter("Invalid pin number".to_string()));
314        }
315
316        // Read all analog inputs from device
317        self.read_analog_inputs()?;
318
319        let pin_index = (pin - 1) as usize;
320        Ok(self.pins[pin_index].analog_value)
321    }
322
323    /// Set analog output
324    pub fn set_analog_output(&mut self, pin: u32, value: u32) -> Result<()> {
325        if pin == 0 || pin as usize > self.pins.len() {
326            return Err(PoKeysError::Parameter("Invalid pin number".to_string()));
327        }
328
329        let pin_index = (pin - 1) as usize;
330        self.pins[pin_index].analog_value = value;
331
332        // Send analog output to device
333        self.write_analog_outputs()?;
334        Ok(())
335    }
336
337    /// Read digital counter value
338    pub fn get_digital_counter(&mut self, pin: u32) -> Result<u32> {
339        if pin == 0 || pin as usize > self.pins.len() {
340            return Err(PoKeysError::Parameter("Invalid pin number".to_string()));
341        }
342
343        let pin_index = (pin - 1) as usize;
344        if self.pins[pin_index].digital_counter_available == 0 {
345            return Err(PoKeysError::NotSupported);
346        }
347
348        // Read digital counters from device
349        self.read_digital_counters()?;
350
351        Ok(self.pins[pin_index].digital_counter_value)
352    }
353
354    /// Reset all digital counter values (protocol command `0x1D`).
355    ///
356    /// The PoKeys protocol does not support per-pin counter resets — `0x1D`
357    /// clears all digital counters at once.
358    pub fn reset_all_digital_counters(&mut self) -> Result<()> {
359        self.send_request(0x1D, 0, 0, 0, 0)?;
360        Ok(())
361    }
362
363    /// Read all digital inputs.
364    ///
365    /// Uses two protocol commands:
366    /// - `0x31` "Block inputs reading" — pins 1–32 packed into response bytes 3–6 (0-based 2–5)
367    /// - `0x32` "Block inputs reading – part 2" — pins 33–55 packed into response bytes 3–5 (0-based 2–4)
368    pub fn get_digital_inputs(&mut self) -> Result<()> {
369        let resp1 = self.send_request(0x31, 0, 0, 0, 0)?;
370        for i in 0..self.pins.len().min(32) {
371            let byte_index = 2 + (i / 8);
372            let bit_index = i % 8;
373            self.pins[i].digital_value_get = if (resp1[byte_index] & (1 << bit_index)) != 0 {
374                1
375            } else {
376                0
377            };
378        }
379
380        if self.pins.len() > 32 {
381            let resp2 = self.send_request(0x32, 0, 0, 0, 0)?;
382            for i in 32..self.pins.len().min(55) {
383                let rel = i - 32;
384                let byte_index = 2 + (rel / 8);
385                let bit_index = rel % 8;
386                self.pins[i].digital_value_get = if (resp2[byte_index] & (1 << bit_index)) != 0 {
387                    1
388                } else {
389                    0
390                };
391            }
392        }
393
394        Ok(())
395    }
396
397    /// Write all digital outputs
398    pub fn write_digital_outputs(&mut self) -> Result<()> {
399        // Prepare output data
400        let mut output_data = [0u8; 8];
401
402        for i in 0..self.pins.len().min(55) {
403            if self.pins[i].is_digital_output() && self.pins[i].digital_value_set != 0 {
404                let byte_index = i / 8;
405                let bit_index = i % 8;
406                output_data[byte_index] |= 1 << bit_index;
407            }
408        }
409
410        // Send digital outputs to device
411        self.send_request(
412            0x11,
413            output_data[0],
414            output_data[1],
415            output_data[2],
416            output_data[3],
417        )?;
418
419        // Send remaining bytes if needed
420        if self.pins.len() > 32 {
421            self.send_request(
422                0x12,
423                output_data[4],
424                output_data[5],
425                output_data[6],
426                output_data[7],
427            )?;
428        }
429
430        Ok(())
431    }
432
433    /// Read all analog inputs
434    pub fn read_analog_inputs(&mut self) -> Result<()> {
435        let response = self.send_request(0x20, 0, 0, 0, 0)?;
436
437        // Parse analog input data from response
438        let mut data_index = 8;
439        for i in 0..self.pins.len() {
440            if self.pins[i].is_analog_input() && data_index + 3 < response.len() {
441                self.pins[i].analog_value = u32::from_le_bytes([
442                    response[data_index],
443                    response[data_index + 1],
444                    response[data_index + 2],
445                    response[data_index + 3],
446                ]);
447                data_index += 4;
448            }
449        }
450
451        Ok(())
452    }
453
454    /// Write analog outputs for every pin currently configured as an analog output.
455    ///
456    /// Sends one "Analog outputs settings" request (`0x41`) per pin, carrying the pin's
457    /// `analog_value` as a 10-bit DAC value (0–1023). Values larger than 10 bits are
458    /// clamped by masking. The `pin ID` sent to the device is the 0-based pin code
459    /// per the protocol spec (pin 43 → pin code 42).
460    pub fn write_analog_outputs(&mut self) -> Result<()> {
461        let targets: Vec<(u8, u32)> = self
462            .pins
463            .iter()
464            .enumerate()
465            .filter_map(|(i, p)| {
466                if p.is_analog_output() {
467                    Some((i as u8, p.analog_value))
468                } else {
469                    None
470                }
471            })
472            .collect();
473
474        for (pin_code, value) in targets {
475            let (msb, lsb) = encode_analog_output_10bit(value);
476            let response = self.send_request(0x41, pin_code, msb, lsb, 0)?;
477            // Response byte 3 (0-based index 2): 0 = OK, non-zero = error ID
478            if response[2] != 0 {
479                return Err(PoKeysError::Protocol(format!(
480                    "Analog output write failed for pin code {}: error code {}",
481                    pin_code, response[2]
482                )));
483            }
484        }
485
486        Ok(())
487    }
488
489    /// Read digital counter values via protocol command `0xD8`.
490    ///
491    /// The request carries up to 13 pin IDs at spec bytes 9–21 (0-based 8–20)
492    /// identifying which counters to return. The response packs thirteen 32-bit
493    /// LE counter values at spec bytes 9–60 (0-based 8–59).
494    pub fn read_digital_counters(&mut self) -> Result<()> {
495        // Collect up to 13 counter-capable pin IDs (0-based pin codes per spec).
496        let mut pin_ids = [0u8; 13];
497        let mut selected: Vec<usize> = Vec::with_capacity(13);
498        for (i, pin) in self.pins.iter().enumerate() {
499            if selected.len() == 13 {
500                break;
501            }
502            if pin.digital_counter_available != 0 {
503                pin_ids[selected.len()] = i as u8;
504                selected.push(i);
505            }
506        }
507
508        let response = self.send_request_with_data(0xD8, 0, 0, 0, 0, &pin_ids)?;
509
510        // Response: 13 × 4-byte LE counter values starting at 0-based byte 8.
511        for (slot, pin_index) in selected.iter().enumerate() {
512            let start = 8 + slot * 4;
513            if start + 4 <= response.len() {
514                self.pins[*pin_index].digital_counter_value = u32::from_le_bytes([
515                    response[start],
516                    response[start + 1],
517                    response[start + 2],
518                    response[start + 3],
519                ]);
520            }
521        }
522
523        Ok(())
524    }
525
526    /// Read all pin functions at once using extended mode (0xC0)
527    /// This is much more efficient than reading pins individually
528    /// Performance improvement: 55x fewer commands
529    pub fn read_all_pin_functions(&mut self) -> Result<[PinFunction; 55]> {
530        use crate::io::private::Command;
531
532        // Send extended I/O command: Read all pin functions
533        // Command 0xC0, option1=0 (read), option2=0 (pin functions)
534        let response = self.send_request(
535            Command::InputOutputExtended as u8,
536            0, // option1: 0 = read all pin functions
537            0, // option2: 0 = pin functions (not additional settings)
538            0, // reserved
539            0, // request ID will be set by send_request
540        )?;
541
542        let raw = parse_bulk_pin_settings_response(&response)?;
543
544        // Decode to the public `[PinFunction; 55]` view (invert bit dropped).
545        let mut functions = [PinFunction::PinRestricted; 55];
546        for i in 0..55 {
547            functions[i] = PinFunction::from_u8(raw[i])?;
548        }
549
550        // Update local pin cache with the full wire byte so bit 7 (invert)
551        // is preserved; the returned `[PinFunction; 55]` still carries only
552        // the base function, matching the existing public contract.
553        for i in 0..55 {
554            if i < self.pins.len() {
555                self.pins[i].pin_function = raw[i];
556            }
557        }
558
559        Ok(functions)
560    }
561
562    /// Read all 55 pin-setting bytes verbatim, including the invert bit
563    /// (`0x80`) when set. Unlike [`Self::read_all_pin_functions`], this
564    /// preserves the full protocol byte so callers can reason about
565    /// hardware-level polarity inversion.
566    ///
567    /// Also refreshes the local pin cache with the returned bytes.
568    pub fn read_all_pin_settings_raw(&mut self) -> Result<[u8; 55]> {
569        use crate::io::private::Command;
570
571        let response = self.send_request(Command::InputOutputExtended as u8, 0, 0, 0, 0)?;
572        let raw = parse_bulk_pin_settings_response(&response)?;
573
574        for i in 0..55 {
575            if i < self.pins.len() {
576                self.pins[i].pin_function = raw[i];
577            }
578        }
579
580        Ok(raw)
581    }
582
583    /// Send all 55 pin-setting bytes verbatim using the bulk `0xC0` command.
584    /// Callers compose each byte themselves — e.g.
585    /// `(PinFunction::DigitalInput as u8) | 0x80` for an inverted digital
586    /// input.
587    ///
588    /// The local pin cache is updated to match the bytes sent.
589    pub fn set_all_pin_settings_raw(&mut self, raw: &[u8; 55]) -> Result<()> {
590        use crate::io::private::Command;
591
592        let response = self.send_request_with_data(
593            Command::InputOutputExtended as u8,
594            1, // option1: 1 = set all pin functions
595            0, // option2: 0 = pin functions (not additional settings)
596            0,
597            0,
598            raw,
599        )?;
600
601        if response.len() < 64 {
602            return Err(PoKeysError::Protocol(
603                "Response too short for bulk pin write".to_string(),
604            ));
605        }
606
607        if response[1] != Command::InputOutputExtended as u8 {
608            return Err(PoKeysError::Protocol(
609                "Invalid response command".to_string(),
610            ));
611        }
612
613        for i in 0..55 {
614            if i < self.pins.len() {
615                self.pins[i].pin_function = raw[i];
616            }
617        }
618
619        Ok(())
620    }
621
622    /// Set all pin functions at once using extended mode (0xC0)
623    /// This is much more efficient than setting pins individually
624    /// Performance improvement: 55x fewer commands — the 55-byte function
625    /// array is sent in a single request instead of one request per pin.
626    pub fn set_all_pin_functions(&mut self, functions: &[PinFunction; 55]) -> Result<()> {
627        use crate::io::private::Command;
628
629        // Payload: 55 pin-function bytes that land at protocol bytes 8..63
630        // (prepare_request_with_data copies payload starting at request[8]).
631        let payload: [u8; 55] = std::array::from_fn(|i| functions[i] as u8);
632
633        let response = self.send_request_with_data(
634            Command::InputOutputExtended as u8,
635            1, // option1: 1 = set all pin functions
636            0, // option2: 0 = pin functions (not additional settings)
637            0, // reserved
638            0, // request ID will be set by send_request_with_data
639            &payload,
640        )?;
641
642        if response.len() < 64 {
643            return Err(PoKeysError::Protocol(
644                "Response too short for bulk pin write".to_string(),
645            ));
646        }
647
648        if response[1] != Command::InputOutputExtended as u8 {
649            return Err(PoKeysError::Protocol(
650                "Invalid response command".to_string(),
651            ));
652        }
653
654        // Update local pin cache to reflect the values we just wrote.
655        for (i, &function) in functions.iter().enumerate() {
656            if i < self.pins.len() {
657                self.pins[i].pin_function = function as u8;
658            }
659        }
660
661        Ok(())
662    }
663
664    /// Read combined device status (digital inputs, analog inputs, and encoder values)
665    /// in a single round-trip using protocol command `0xCC`
666    /// ("Get device status (IO, analog, encoders)").
667    ///
668    /// Updates `self.pins[*].digital_value_get`, `self.pins[*].analog_value` (for up to 5
669    /// analog-input pins), and `self.encoders[*].encoder_value` (up to 25 channels).
670    ///
671    /// Matrix keyboard rows and ultra-fast encoder data from the response are not
672    /// applied here — use the dedicated methods for those when needed.
673    pub fn get_device_status(&mut self) -> Result<()> {
674        let response = self.send_request(0xCC, 0, 0, 0, 0)?;
675        apply_device_status_response(&response, &mut self.pins, &mut self.encoders);
676        Ok(())
677    }
678}
679
680/// Pack a 10-bit analog output value into the `(MSB, LSB)` pair expected by
681/// protocol command `0x41`. Byte 4 holds the top 8 bits; the upper 2 bits of
682/// byte 5 hold the low 2 bits of the value. Values wider than 10 bits are
683/// truncated.
684fn encode_analog_output_10bit(value: u32) -> (u8, u8) {
685    let v = value & 0x3FF;
686    let msb = ((v >> 2) & 0xFF) as u8;
687    let lsb = ((v & 0x03) << 6) as u8;
688    (msb, lsb)
689}
690
691/// Validate a bulk `0xC0` pin-settings response and extract the 55 setting
692/// bytes starting at offset 8. Shared by [`PoKeysDevice::read_all_pin_functions`]
693/// and [`PoKeysDevice::read_all_pin_settings_raw`] so the parsing can be
694/// unit-tested without a live device.
695pub(crate) fn parse_bulk_pin_settings_response(response: &[u8]) -> Result<[u8; 55]> {
696    use crate::io::private::Command;
697
698    if response.len() < 64 {
699        return Err(PoKeysError::Protocol(
700            "Response too short for bulk pin read".to_string(),
701        ));
702    }
703
704    if response[1] != Command::InputOutputExtended as u8 {
705        return Err(PoKeysError::Protocol(
706            "Invalid response command".to_string(),
707        ));
708    }
709
710    let mut raw = [0u8; 55];
711    raw.copy_from_slice(&response[8..8 + 55]);
712    Ok(raw)
713}
714
715/// Apply a `0xCC` ("Get device status") response to the caller-owned pin and
716/// encoder arrays. Split out from [`PoKeysDevice::get_device_status`] so the
717/// parsing can be unit-tested without a live device.
718fn apply_device_status_response(
719    response: &[u8],
720    pins: &mut [PinData],
721    encoders: &mut [crate::encoders::EncoderData],
722) {
723    // Digital inputs: doc bytes 9-15 (0-based 8-14), bit-mapped pin 1..=55.
724    for i in 0..pins.len().min(55) {
725        let byte_index = 8 + (i / 8);
726        let bit_index = i % 8;
727        if byte_index < response.len() {
728            pins[i].digital_value_get = if (response[byte_index] & (1 << bit_index)) != 0 {
729                1
730            } else {
731                0
732            };
733        }
734    }
735
736    // Analog inputs: doc bytes 16-25 (0-based 15-24), 5 channels × (MSB, LSB).
737    let mut data_index = 15;
738    let mut channels_consumed = 0;
739    for pin in pins.iter_mut() {
740        if channels_consumed >= 5 {
741            break;
742        }
743        if pin.is_analog_input() && data_index + 1 < response.len() {
744            let msb = response[data_index] as u32;
745            let lsb = response[data_index + 1] as u32;
746            pin.analog_value = (msb << 8) | lsb;
747            data_index += 2;
748            channels_consumed += 1;
749        }
750    }
751
752    // Encoders: doc bytes 26-50 (0-based 25-49), 25 × 8-bit signed RAW values.
753    for i in 0..encoders.len().min(25) {
754        let idx = 25 + i;
755        if idx < response.len() {
756            encoders[i].encoder_value = response[idx] as i8 as i32;
757        }
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn test_pin_data_creation() {
767        let pin_data = PinData::new();
768        assert_eq!(pin_data.pin_function, PinFunction::PinRestricted as u8);
769        assert_eq!(pin_data.digital_value_get, 0);
770        assert_eq!(pin_data.digital_value_set, 0);
771    }
772
773    #[test]
774    fn test_pin_function_checks() {
775        let mut pin_data = PinData::new();
776
777        pin_data.pin_function = PinFunction::DigitalInput as u8;
778        assert!(pin_data.is_digital_input());
779        assert!(!pin_data.is_digital_output());
780
781        pin_data.pin_function = PinFunction::DigitalOutput as u8;
782        assert!(!pin_data.is_digital_input());
783        assert!(pin_data.is_digital_output());
784
785        pin_data.pin_function =
786            (PinFunction::DigitalInput as u8) | (PinFunction::DigitalOutput as u8);
787        assert!(pin_data.is_digital_input());
788        assert!(pin_data.is_digital_output());
789    }
790
791    #[test]
792    fn test_encode_analog_output_10bit() {
793        assert_eq!(encode_analog_output_10bit(0), (0x00, 0x00));
794        assert_eq!(encode_analog_output_10bit(1023), (0xFF, 0xC0));
795        assert_eq!(encode_analog_output_10bit(512), (0x80, 0x00));
796        // low two bits land in LSB byte's upper two bits
797        assert_eq!(encode_analog_output_10bit(3), (0x00, 0xC0));
798        assert_eq!(encode_analog_output_10bit(1), (0x00, 0x40));
799        // values wider than 10 bits are truncated
800        assert_eq!(encode_analog_output_10bit(0xFFFF), (0xFF, 0xC0));
801    }
802
803    #[test]
804    fn test_apply_device_status_response() {
805        use crate::encoders::EncoderData;
806
807        let mut response = [0u8; 64];
808
809        // Digital inputs: set pins 1, 9, and 55 high.
810        response[8] = 0b0000_0001; // pin 1
811        response[9] = 0b0000_0001; // pin 9
812        response[14] = 0b0100_0000; // pin 55 = bit (55-1) % 8 = 6 of byte 14
813
814        // Analog channel 1 → 0x1234 (MSB=0x12, LSB=0x34) at doc bytes 16-17 (0-based 15-16)
815        response[15] = 0x12;
816        response[16] = 0x34;
817        // Analog channel 2 → 0xABCD (MSB=0xAB, LSB=0xCD) at doc bytes 18-19 (0-based 17-18)
818        response[17] = 0xAB;
819        response[18] = 0xCD;
820
821        // Encoder 1 raw = -1 (0xFF signed), encoder 2 raw = 5
822        response[25] = 0xFF;
823        response[26] = 0x05;
824
825        let mut pins = vec![PinData::new(); 55];
826        pins[0].pin_function = PinFunction::DigitalInput as u8; // pin 1 digital
827        pins[8].pin_function = PinFunction::DigitalInput as u8; // pin 9 digital
828        pins[40].pin_function = PinFunction::AnalogInput as u8; // first analog-capable pin
829        pins[41].pin_function = PinFunction::AnalogInput as u8; // second analog-capable pin
830
831        let mut encoders = vec![EncoderData::new(); 25];
832
833        apply_device_status_response(&response, &mut pins, &mut encoders);
834
835        assert_eq!(pins[0].digital_value_get, 1);
836        assert_eq!(pins[1].digital_value_get, 0);
837        assert_eq!(pins[8].digital_value_get, 1);
838        assert_eq!(pins[54].digital_value_get, 1);
839
840        assert_eq!(pins[40].analog_value, 0x1234);
841        assert_eq!(pins[41].analog_value, 0xABCD);
842
843        assert_eq!(encoders[0].encoder_value, -1);
844        assert_eq!(encoders[1].encoder_value, 5);
845        assert_eq!(encoders[2].encoder_value, 0);
846    }
847
848    #[test]
849    fn test_compose_pin_function_byte() {
850        use crate::io::private::compose_pin_function_byte;
851
852        // Non-inverted: exactly the enum discriminant.
853        assert_eq!(
854            compose_pin_function_byte(PinFunction::DigitalInput, false),
855            0x02
856        );
857        assert_eq!(
858            compose_pin_function_byte(PinFunction::DigitalOutput, false),
859            0x04
860        );
861        assert_eq!(
862            compose_pin_function_byte(PinFunction::TriggeredInput, false),
863            0x20
864        );
865
866        // Inverted: bit 7 set, base function preserved.
867        assert_eq!(
868            compose_pin_function_byte(PinFunction::DigitalInput, true),
869            0x82
870        );
871        assert_eq!(
872            compose_pin_function_byte(PinFunction::DigitalOutput, true),
873            0x84
874        );
875        assert_eq!(
876            compose_pin_function_byte(PinFunction::TriggeredInput, true),
877            0xA0
878        );
879    }
880
881    #[test]
882    fn test_decode_pin_function_from_cache() {
883        // Combined bytes decode to their base function; the invert bit is
884        // ignored by the decoder (caller uses `is_inverted` / `get_pin_invert`
885        // to recover it).
886        assert_eq!(
887            decode_pin_function_from_cache(0x82),
888            PinFunction::DigitalInput
889        );
890        assert_eq!(
891            decode_pin_function_from_cache(0x84),
892            PinFunction::DigitalOutput
893        );
894        assert_eq!(
895            decode_pin_function_from_cache(0xA0),
896            PinFunction::TriggeredInput
897        );
898
899        // Base-only values round-trip.
900        assert_eq!(
901            decode_pin_function_from_cache(0x02),
902            PinFunction::DigitalInput
903        );
904        assert_eq!(
905            decode_pin_function_from_cache(0x00),
906            PinFunction::PinRestricted
907        );
908
909        // `0x80` alone (invert flag with no base function) is not a valid
910        // pin configuration — decode as PinRestricted.
911        assert_eq!(
912            decode_pin_function_from_cache(0x80),
913            PinFunction::PinRestricted
914        );
915    }
916
917    #[test]
918    fn test_pin_data_invert_accessors() {
919        let mut pin_data = PinData::new();
920
921        pin_data.pin_function = 0x82; // DigitalInput | Invert
922        assert!(pin_data.is_inverted());
923        assert_eq!(pin_data.base_function(), PinFunction::DigitalInput);
924        // Existing masked helpers still work on the OR'd byte.
925        assert!(pin_data.is_digital_input());
926        assert!(!pin_data.is_digital_output());
927
928        pin_data.pin_function = 0x02; // DigitalInput, no invert
929        assert!(!pin_data.is_inverted());
930        assert_eq!(pin_data.base_function(), PinFunction::DigitalInput);
931    }
932
933    #[test]
934    fn test_parse_bulk_pin_settings_response_preserves_invert_bit() {
935        use crate::io::private::Command;
936
937        let mut response = [0u8; 64];
938        response[1] = Command::InputOutputExtended as u8;
939        response[8] = 0x82; // pin 1 = DigitalInput | Invert
940        response[9] = 0x04; // pin 2 = DigitalOutput (no invert)
941        response[10] = 0xA0; // pin 3 = TriggeredInput | Invert
942
943        let raw = parse_bulk_pin_settings_response(&response).unwrap();
944        assert_eq!(raw[0], 0x82);
945        assert_eq!(raw[1], 0x04);
946        assert_eq!(raw[2], 0xA0);
947
948        // The typed-view decode in `read_all_pin_functions` drops the invert
949        // bit; verify `from_u8` returns the base function for a combined byte.
950        assert_eq!(
951            PinFunction::from_u8(raw[0]).unwrap(),
952            PinFunction::DigitalInput
953        );
954        assert_eq!(
955            PinFunction::from_u8(raw[2]).unwrap(),
956            PinFunction::TriggeredInput
957        );
958    }
959
960    #[test]
961    fn test_parse_bulk_pin_settings_response_rejects_short() {
962        let short = [0u8; 32];
963        assert!(parse_bulk_pin_settings_response(&short).is_err());
964    }
965
966    #[test]
967    fn test_parse_bulk_pin_settings_response_rejects_wrong_command() {
968        let mut response = [0u8; 64];
969        response[1] = 0x12; // not InputOutputExtended
970        assert!(parse_bulk_pin_settings_response(&response).is_err());
971    }
972}