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 (offsets per the PoKeys protocol
366    /// specification v14.3.2025, "Block inputs reading"):
367    /// - `0x31` "Block inputs reading" — pins 1–32 packed into response bytes 3–6 (0-based 2–5)
368    /// - `0x32` "Block inputs reading – part 2" — pins 33–55 packed into response bytes 3–5 (0-based 2–4)
369    pub fn get_digital_inputs(&mut self) -> Result<()> {
370        let pin_count = self.pins.len();
371
372        let resp1 = self.send_request(0x31, 0, 0, 0, 0)?;
373        apply_block_inputs_response(&resp1, &mut self.pins, 0..pin_count.min(32), 0);
374
375        if pin_count > 32 {
376            let resp2 = self.send_request(0x32, 0, 0, 0, 0)?;
377            apply_block_inputs_response(&resp2, &mut self.pins, 32..pin_count.min(55), 32);
378        }
379
380        Ok(())
381    }
382
383    /// Write all digital outputs.
384    ///
385    /// Uses the spec-correct block-output commands:
386    /// - `0x42` "Block set output I" — pins 1–32 packed into request bytes 3–6
387    /// - `0x43` "Block set output II" — pins 33–55 packed into request bytes 3–5
388    ///
389    /// Earlier revisions of this function sent `0x11`/`0x12`, which are the
390    /// "Set encoder settings" / "Set encoder key mapping A" commands. That
391    /// collision could corrupt encoder configuration on every bulk write.
392    pub fn write_digital_outputs(&mut self) -> Result<()> {
393        let mut output_data = [0u8; 8];
394
395        for i in 0..self.pins.len().min(55) {
396            if self.pins[i].is_digital_output() && self.pins[i].digital_value_set != 0 {
397                let byte_index = i / 8;
398                let bit_index = i % 8;
399                output_data[byte_index] |= 1 << bit_index;
400            }
401        }
402
403        self.send_request(
404            0x42,
405            output_data[0],
406            output_data[1],
407            output_data[2],
408            output_data[3],
409        )?;
410
411        if self.pins.len() > 32 {
412            self.send_request(0x43, output_data[4], output_data[5], output_data[6], 0)?;
413        }
414
415        Ok(())
416    }
417
418    /// Read all analog inputs
419    pub fn read_analog_inputs(&mut self) -> Result<()> {
420        let response = self.send_request(0x20, 0, 0, 0, 0)?;
421
422        // Parse analog input data from response
423        let mut data_index = 8;
424        for i in 0..self.pins.len() {
425            if self.pins[i].is_analog_input() && data_index + 3 < response.len() {
426                self.pins[i].analog_value = u32::from_le_bytes([
427                    response[data_index],
428                    response[data_index + 1],
429                    response[data_index + 2],
430                    response[data_index + 3],
431                ]);
432                data_index += 4;
433            }
434        }
435
436        Ok(())
437    }
438
439    /// Write analog outputs for every pin currently configured as an analog output.
440    ///
441    /// Sends one "Analog outputs settings" request (`0x41`) per pin, carrying the pin's
442    /// `analog_value` as a 10-bit DAC value (0–1023). Values larger than 10 bits are
443    /// clamped by masking. The `pin ID` sent to the device is the 0-based pin code
444    /// per the protocol spec (pin 43 → pin code 42).
445    pub fn write_analog_outputs(&mut self) -> Result<()> {
446        let targets: Vec<(u8, u32)> = self
447            .pins
448            .iter()
449            .enumerate()
450            .filter_map(|(i, p)| {
451                if p.is_analog_output() {
452                    Some((i as u8, p.analog_value))
453                } else {
454                    None
455                }
456            })
457            .collect();
458
459        for (pin_code, value) in targets {
460            let (msb, lsb) = encode_analog_output_10bit(value);
461            let response = self.send_request(0x41, pin_code, msb, lsb, 0)?;
462            // Response byte 3 (0-based index 2): 0 = OK, non-zero = error ID
463            if response[2] != 0 {
464                return Err(PoKeysError::Protocol(format!(
465                    "Analog output write failed for pin code {}: error code {}",
466                    pin_code, response[2]
467                )));
468            }
469        }
470
471        Ok(())
472    }
473
474    /// Read digital counter values via protocol command `0xD8`.
475    ///
476    /// The request carries up to 13 pin IDs at spec bytes 9–21 (0-based 8–20)
477    /// identifying which counters to return. The response packs thirteen 32-bit
478    /// LE counter values at spec bytes 9–60 (0-based 8–59).
479    pub fn read_digital_counters(&mut self) -> Result<()> {
480        // Collect up to 13 counter-capable pin IDs (0-based pin codes per spec).
481        let mut pin_ids = [0u8; 13];
482        let mut selected: Vec<usize> = Vec::with_capacity(13);
483        for (i, pin) in self.pins.iter().enumerate() {
484            if selected.len() == 13 {
485                break;
486            }
487            if pin.digital_counter_available != 0 {
488                pin_ids[selected.len()] = i as u8;
489                selected.push(i);
490            }
491        }
492
493        let response = self.send_request_with_data(0xD8, 0, 0, 0, 0, &pin_ids)?;
494
495        // Response: 13 × 4-byte LE counter values starting at 0-based byte 8.
496        for (slot, pin_index) in selected.iter().enumerate() {
497            let start = 8 + slot * 4;
498            if start + 4 <= response.len() {
499                self.pins[*pin_index].digital_counter_value = u32::from_le_bytes([
500                    response[start],
501                    response[start + 1],
502                    response[start + 2],
503                    response[start + 3],
504                ]);
505            }
506        }
507
508        Ok(())
509    }
510
511    /// Read all pin functions at once using extended mode (0xC0)
512    /// This is much more efficient than reading pins individually
513    /// Performance improvement: 55x fewer commands
514    pub fn read_all_pin_functions(&mut self) -> Result<[PinFunction; 55]> {
515        use crate::io::private::Command;
516
517        // Send extended I/O command: Read all pin functions
518        // Command 0xC0, option1=0 (read), option2=0 (pin functions)
519        let response = self.send_request(
520            Command::InputOutputExtended as u8,
521            0, // option1: 0 = read all pin functions
522            0, // option2: 0 = pin functions (not additional settings)
523            0, // reserved
524            0, // request ID will be set by send_request
525        )?;
526
527        let raw = parse_bulk_pin_settings_response(&response)?;
528
529        // Decode to the public `[PinFunction; 55]` view (invert bit dropped).
530        let mut functions = [PinFunction::PinRestricted; 55];
531        for i in 0..55 {
532            functions[i] = PinFunction::from_u8(raw[i])?;
533        }
534
535        // Update local pin cache with the full wire byte so bit 7 (invert)
536        // is preserved; the returned `[PinFunction; 55]` still carries only
537        // the base function, matching the existing public contract.
538        for i in 0..55 {
539            if i < self.pins.len() {
540                self.pins[i].pin_function = raw[i];
541            }
542        }
543
544        Ok(functions)
545    }
546
547    /// Read all 55 pin-setting bytes verbatim, including the invert bit
548    /// (`0x80`) when set. Unlike [`Self::read_all_pin_functions`], this
549    /// preserves the full protocol byte so callers can reason about
550    /// hardware-level polarity inversion.
551    ///
552    /// Also refreshes the local pin cache with the returned bytes.
553    pub fn read_all_pin_settings_raw(&mut self) -> Result<[u8; 55]> {
554        use crate::io::private::Command;
555
556        let response = self.send_request(Command::InputOutputExtended as u8, 0, 0, 0, 0)?;
557        let raw = parse_bulk_pin_settings_response(&response)?;
558
559        for i in 0..55 {
560            if i < self.pins.len() {
561                self.pins[i].pin_function = raw[i];
562            }
563        }
564
565        Ok(raw)
566    }
567
568    /// Send all 55 pin-setting bytes verbatim using the bulk `0xC0` command.
569    /// Callers compose each byte themselves — e.g.
570    /// `(PinFunction::DigitalInput as u8) | 0x80` for an inverted digital
571    /// input.
572    ///
573    /// The local pin cache is updated to match the bytes sent.
574    pub fn set_all_pin_settings_raw(&mut self, raw: &[u8; 55]) -> Result<()> {
575        use crate::io::private::Command;
576
577        let response = self.send_request_with_data(
578            Command::InputOutputExtended as u8,
579            1, // option1: 1 = set all pin functions
580            0, // option2: 0 = pin functions (not additional settings)
581            0,
582            0,
583            raw,
584        )?;
585
586        if response.len() < 64 {
587            return Err(PoKeysError::Protocol(
588                "Response too short for bulk pin write".to_string(),
589            ));
590        }
591
592        if response[1] != Command::InputOutputExtended as u8 {
593            return Err(PoKeysError::Protocol(
594                "Invalid response command".to_string(),
595            ));
596        }
597
598        for i in 0..55 {
599            if i < self.pins.len() {
600                self.pins[i].pin_function = raw[i];
601            }
602        }
603
604        Ok(())
605    }
606
607    /// Set all pin functions at once using extended mode (0xC0)
608    /// This is much more efficient than setting pins individually
609    /// Performance improvement: 55x fewer commands — the 55-byte function
610    /// array is sent in a single request instead of one request per pin.
611    pub fn set_all_pin_functions(&mut self, functions: &[PinFunction; 55]) -> Result<()> {
612        use crate::io::private::Command;
613
614        // Payload: 55 pin-function bytes that land at protocol bytes 8..63
615        // (prepare_request_with_data copies payload starting at request[8]).
616        let payload: [u8; 55] = std::array::from_fn(|i| functions[i] as u8);
617
618        let response = self.send_request_with_data(
619            Command::InputOutputExtended as u8,
620            1, // option1: 1 = set all pin functions
621            0, // option2: 0 = pin functions (not additional settings)
622            0, // reserved
623            0, // request ID will be set by send_request_with_data
624            &payload,
625        )?;
626
627        if response.len() < 64 {
628            return Err(PoKeysError::Protocol(
629                "Response too short for bulk pin write".to_string(),
630            ));
631        }
632
633        if response[1] != Command::InputOutputExtended as u8 {
634            return Err(PoKeysError::Protocol(
635                "Invalid response command".to_string(),
636            ));
637        }
638
639        // Update local pin cache to reflect the values we just wrote.
640        for (i, &function) in functions.iter().enumerate() {
641            if i < self.pins.len() {
642                self.pins[i].pin_function = function as u8;
643            }
644        }
645
646        Ok(())
647    }
648
649    /// Read combined device status (digital inputs, analog inputs, and encoder values)
650    /// in a single round-trip using protocol command `0xCC`
651    /// ("Get device status (IO, analog, encoders)").
652    ///
653    /// Updates `self.pins[*].digital_value_get`, `self.pins[*].analog_value` (for up to 5
654    /// analog-input pins), and `self.encoders[*].encoder_value` (up to 25 channels).
655    ///
656    /// Matrix keyboard rows and ultra-fast encoder data from the response are not
657    /// applied here — use the dedicated methods for those when needed.
658    pub fn get_device_status(&mut self) -> Result<()> {
659        let response = self.send_request(0xCC, 0, 0, 0, 0)?;
660        apply_device_status_response(&response, &mut self.pins, &mut self.encoders);
661        Ok(())
662    }
663}
664
665/// Pack a 10-bit analog output value into the `(MSB, LSB)` pair expected by
666/// protocol command `0x41`. Byte 4 holds the top 8 bits; the upper 2 bits of
667/// byte 5 hold the low 2 bits of the value. Values wider than 10 bits are
668/// truncated.
669fn encode_analog_output_10bit(value: u32) -> (u8, u8) {
670    let v = value & 0x3FF;
671    let msb = ((v >> 2) & 0xFF) as u8;
672    let lsb = ((v & 0x03) << 6) as u8;
673    (msb, lsb)
674}
675
676/// Validate a bulk `0xC0` pin-settings response and extract the 55 setting
677/// bytes starting at offset 8. Shared by [`PoKeysDevice::read_all_pin_functions`]
678/// and [`PoKeysDevice::read_all_pin_settings_raw`] so the parsing can be
679/// unit-tested without a live device.
680///
681/// Emits a `log::warn!` if all 55 returned bytes are `0x00` — this is the
682/// silent-failure signature of a configuration-locked device, where the
683/// firmware echoes the command but refuses to populate bytes 9-63. It can
684/// also legitimately occur on a freshly-cleared device, so it's reported
685/// rather than rejected.
686pub(crate) fn parse_bulk_pin_settings_response(response: &[u8]) -> Result<[u8; 55]> {
687    use crate::io::private::Command;
688
689    if response.len() < 64 {
690        return Err(PoKeysError::Protocol(
691            "Response too short for bulk pin read".to_string(),
692        ));
693    }
694
695    if response[1] != Command::InputOutputExtended as u8 {
696        return Err(PoKeysError::Protocol(
697            "Invalid response command".to_string(),
698        ));
699    }
700
701    let mut raw = [0u8; 55];
702    raw.copy_from_slice(&response[8..8 + 55]);
703
704    if raw.iter().all(|&b| b == 0) {
705        log::warn!(
706            "Bulk pin-settings read returned all-zero bytes — every pin reports as \
707             PinRestricted. This often indicates the device's configuration is locked \
708             (the firmware echoes 0xC0 but refuses to populate the response). If you \
709             expect non-restricted pins, check is_configuration_locked() and unlock \
710             via clear_configuration() if appropriate."
711        );
712    }
713
714    Ok(raw)
715}
716
717/// Apply a `0x31`/`0x32` ("Block inputs reading") response to the caller-owned
718/// pin array. Split out from [`PoKeysDevice::get_digital_inputs`] so the
719/// parsing can be unit-tested without a live device.
720///
721/// Per the PoKeys protocol specification (v14.3.2025, "Block inputs reading"):
722/// - `0x31` returned packet: byte 3 = pins 1–8, byte 4 = 9–16, byte 5 = 17–24,
723///   byte 6 = 25–32 (1-based; 0-based bytes 2–5).
724/// - `0x32` returned packet: byte 3 = pins 33–40, byte 4 = 41–48, byte 5 =
725///   49–55 (1-based; 0-based bytes 2–4).
726///
727/// `bit_offset` is 0 for the `0x31` bank (pins 1–32) and 32 for the `0x32`
728/// bank (pins 33–55).
729///
730/// Note: this layout differs from the `0xCC` ("Get device status") response,
731/// which packs pin state into 0-based bytes 8–14. An earlier revision of this
732/// function read at byte 8 to mirror `0xCC`; that produced all-low reads on
733/// firmware 4.8.x (and presumably every spec-compliant firmware), because
734/// bytes 8+ on `0x31`/`0x32` either don't exist or carry unrelated trailer
735/// data. See pokeys-toolkit/core PR #44 / its revert for the trail.
736pub(crate) fn apply_block_inputs_response(
737    response: &[u8],
738    pins: &mut [PinData],
739    pin_range: std::ops::Range<usize>,
740    bit_offset: usize,
741) {
742    for i in pin_range {
743        let rel = i - bit_offset;
744        let byte_index = 2 + (rel / 8);
745        let bit_index = rel % 8;
746        if byte_index < response.len() {
747            pins[i].digital_value_get = if (response[byte_index] & (1 << bit_index)) != 0 {
748                1
749            } else {
750                0
751            };
752        }
753    }
754}
755
756/// Apply a `0xCC` ("Get device status") response to the caller-owned pin and
757/// encoder arrays. Split out from [`PoKeysDevice::get_device_status`] so the
758/// parsing can be unit-tested without a live device.
759fn apply_device_status_response(
760    response: &[u8],
761    pins: &mut [PinData],
762    encoders: &mut [crate::encoders::EncoderData],
763) {
764    // Digital inputs: doc bytes 9-15 (0-based 8-14), bit-mapped pin 1..=55.
765    for i in 0..pins.len().min(55) {
766        let byte_index = 8 + (i / 8);
767        let bit_index = i % 8;
768        if byte_index < response.len() {
769            pins[i].digital_value_get = if (response[byte_index] & (1 << bit_index)) != 0 {
770                1
771            } else {
772                0
773            };
774        }
775    }
776
777    // Analog inputs: doc bytes 16-25 (0-based 15-24), 5 channels × (MSB, LSB).
778    let mut data_index = 15;
779    let mut channels_consumed = 0;
780    for pin in pins.iter_mut() {
781        if channels_consumed >= 5 {
782            break;
783        }
784        if pin.is_analog_input() && data_index + 1 < response.len() {
785            let msb = response[data_index] as u32;
786            let lsb = response[data_index + 1] as u32;
787            pin.analog_value = (msb << 8) | lsb;
788            data_index += 2;
789            channels_consumed += 1;
790        }
791    }
792
793    // Encoders: doc bytes 26-50 (0-based 25-49), 25 × 8-bit signed RAW values.
794    for i in 0..encoders.len().min(25) {
795        let idx = 25 + i;
796        if idx < response.len() {
797            encoders[i].encoder_value = response[idx] as i8 as i32;
798        }
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805
806    #[test]
807    fn test_pin_data_creation() {
808        let pin_data = PinData::new();
809        assert_eq!(pin_data.pin_function, PinFunction::PinRestricted as u8);
810        assert_eq!(pin_data.digital_value_get, 0);
811        assert_eq!(pin_data.digital_value_set, 0);
812    }
813
814    #[test]
815    fn test_pin_function_checks() {
816        let mut pin_data = PinData::new();
817
818        pin_data.pin_function = PinFunction::DigitalInput as u8;
819        assert!(pin_data.is_digital_input());
820        assert!(!pin_data.is_digital_output());
821
822        pin_data.pin_function = PinFunction::DigitalOutput as u8;
823        assert!(!pin_data.is_digital_input());
824        assert!(pin_data.is_digital_output());
825
826        pin_data.pin_function =
827            (PinFunction::DigitalInput as u8) | (PinFunction::DigitalOutput as u8);
828        assert!(pin_data.is_digital_input());
829        assert!(pin_data.is_digital_output());
830    }
831
832    #[test]
833    fn test_encode_analog_output_10bit() {
834        assert_eq!(encode_analog_output_10bit(0), (0x00, 0x00));
835        assert_eq!(encode_analog_output_10bit(1023), (0xFF, 0xC0));
836        assert_eq!(encode_analog_output_10bit(512), (0x80, 0x00));
837        // low two bits land in LSB byte's upper two bits
838        assert_eq!(encode_analog_output_10bit(3), (0x00, 0xC0));
839        assert_eq!(encode_analog_output_10bit(1), (0x00, 0x40));
840        // values wider than 10 bits are truncated
841        assert_eq!(encode_analog_output_10bit(0xFFFF), (0xFF, 0xC0));
842    }
843
844    #[test]
845    fn test_apply_device_status_response() {
846        use crate::encoders::EncoderData;
847
848        let mut response = [0u8; 64];
849
850        // Digital inputs: set pins 1, 9, and 55 high.
851        response[8] = 0b0000_0001; // pin 1
852        response[9] = 0b0000_0001; // pin 9
853        response[14] = 0b0100_0000; // pin 55 = bit (55-1) % 8 = 6 of byte 14
854
855        // Analog channel 1 → 0x1234 (MSB=0x12, LSB=0x34) at doc bytes 16-17 (0-based 15-16)
856        response[15] = 0x12;
857        response[16] = 0x34;
858        // Analog channel 2 → 0xABCD (MSB=0xAB, LSB=0xCD) at doc bytes 18-19 (0-based 17-18)
859        response[17] = 0xAB;
860        response[18] = 0xCD;
861
862        // Encoder 1 raw = -1 (0xFF signed), encoder 2 raw = 5
863        response[25] = 0xFF;
864        response[26] = 0x05;
865
866        let mut pins = vec![PinData::new(); 55];
867        pins[0].pin_function = PinFunction::DigitalInput as u8; // pin 1 digital
868        pins[8].pin_function = PinFunction::DigitalInput as u8; // pin 9 digital
869        pins[40].pin_function = PinFunction::AnalogInput as u8; // first analog-capable pin
870        pins[41].pin_function = PinFunction::AnalogInput as u8; // second analog-capable pin
871
872        let mut encoders = vec![EncoderData::new(); 25];
873
874        apply_device_status_response(&response, &mut pins, &mut encoders);
875
876        assert_eq!(pins[0].digital_value_get, 1);
877        assert_eq!(pins[1].digital_value_get, 0);
878        assert_eq!(pins[8].digital_value_get, 1);
879        assert_eq!(pins[54].digital_value_get, 1);
880
881        assert_eq!(pins[40].analog_value, 0x1234);
882        assert_eq!(pins[41].analog_value, 0xABCD);
883
884        assert_eq!(encoders[0].encoder_value, -1);
885        assert_eq!(encoders[1].encoder_value, 5);
886        assert_eq!(encoders[2].encoder_value, 0);
887    }
888
889    // Block-input layout per the PoKeys protocol specification
890    // (v14.3.2025, "Block inputs reading"):
891    //   0x31 returned packet
892    //     byte 3 (0-based 2) = pins  1– 8
893    //     byte 4 (0-based 3) = pins  9–16
894    //     byte 5 (0-based 4) = pins 17–24
895    //     byte 6 (0-based 5) = pins 25–32
896    //   0x32 returned packet
897    //     byte 3 (0-based 2) = pins 33–40
898    //     byte 4 (0-based 3) = pins 41–48
899    //     byte 5 (0-based 4) = pins 49–55
900
901    #[test]
902    fn test_apply_block_inputs_pin_4() {
903        // 0x31: pin 4 high → 0-based byte 2, bit 3.
904        // Captured live: response[2] flipped C8 → CD when pin 4's switch was
905        // toggled on PoKeys57E firmware 4.8.2.
906        let mut response = [0u8; 64];
907        response[2] = 0b0000_1000;
908
909        let mut pins = vec![PinData::new(); 55];
910        apply_block_inputs_response(&response, &mut pins, 0..32, 0);
911
912        assert_eq!(pins[2].digital_value_get, 0, "pin 3 should stay low");
913        assert_eq!(pins[3].digital_value_get, 1, "pin 4 should read high");
914        assert_eq!(pins[4].digital_value_get, 0, "pin 5 should stay low");
915    }
916
917    #[test]
918    fn test_apply_block_inputs_pin_28() {
919        // 0x31: pin 28 high → 0-based byte 2 + 27/8 = 5, bit 27 % 8 = 3.
920        let mut response = [0u8; 64];
921        response[5] = 0b0000_1000;
922
923        let mut pins = vec![PinData::new(); 55];
924        apply_block_inputs_response(&response, &mut pins, 0..32, 0);
925
926        assert_eq!(pins[26].digital_value_get, 0, "pin 27 should stay low");
927        assert_eq!(pins[27].digital_value_get, 1, "pin 28 should read high");
928        assert_eq!(pins[28].digital_value_get, 0, "pin 29 should stay low");
929    }
930
931    #[test]
932    fn test_apply_block_inputs_pin_33() {
933        // 0x32: pin 33 high → rel 0 → 0-based byte 2, bit 0.
934        let mut response = [0u8; 64];
935        response[2] = 0b0000_0001;
936
937        let mut pins = vec![PinData::new(); 55];
938        apply_block_inputs_response(&response, &mut pins, 32..55, 32);
939
940        assert_eq!(pins[31].digital_value_get, 0, "pin 32 untouched by 0x32");
941        assert_eq!(pins[32].digital_value_get, 1, "pin 33 should read high");
942        assert_eq!(pins[33].digital_value_get, 0, "pin 34 should stay low");
943    }
944
945    #[test]
946    fn test_apply_block_inputs_pin_50() {
947        // 0x32: pin 50 high → rel = 50-33 = 17 → 0-based byte 2 + 17/8 = 4,
948        // bit 17 % 8 = 1.
949        let mut response = [0u8; 64];
950        response[4] = 0b0000_0010;
951
952        let mut pins = vec![PinData::new(); 55];
953        apply_block_inputs_response(&response, &mut pins, 32..55, 32);
954
955        assert_eq!(pins[48].digital_value_get, 0);
956        assert_eq!(pins[49].digital_value_get, 1, "pin 50 should read high");
957        assert_eq!(pins[50].digital_value_get, 0);
958    }
959
960    #[test]
961    fn test_apply_block_inputs_pin_55() {
962        // 0x32: pin 55 high → rel = 55-33 = 22 → 0-based byte 2 + 22/8 = 4,
963        // bit 22 % 8 = 6.
964        let mut response = [0u8; 64];
965        response[4] = 0b0100_0000;
966
967        let mut pins = vec![PinData::new(); 55];
968        apply_block_inputs_response(&response, &mut pins, 32..55, 32);
969
970        assert_eq!(pins[53].digital_value_get, 0);
971        assert_eq!(pins[54].digital_value_get, 1, "pin 55 should read high");
972    }
973
974    #[test]
975    fn test_apply_block_inputs_ignores_trailer_bytes() {
976        // Bytes after the spec-defined data zone (0-based 6+ for 0x31,
977        // 0-based 5+ for 0x32) carry the request ID and unused trailer —
978        // never pin state. A response with the entire trailer set high and
979        // bytes 2-5 zero must produce all-low pin states.
980        let mut response = [0u8; 64];
981        for b in &mut response[6..] {
982            *b = 0xFF;
983        }
984
985        let mut pins = vec![PinData::new(); 55];
986        apply_block_inputs_response(&response, &mut pins, 0..32, 0);
987        apply_block_inputs_response(&response, &mut pins, 32..55, 32);
988
989        for (i, p) in pins.iter().enumerate() {
990            assert_eq!(
991                p.digital_value_get,
992                0,
993                "pin {} must not read trailer zone",
994                i + 1
995            );
996        }
997    }
998
999    #[test]
1000    fn test_decode_read_digital_input_response_uses_byte_3() {
1001        use crate::io::private::decode_read_digital_input_response;
1002
1003        // Per spec ("Reading of inputs"): byte 3 (0-based 2) = status,
1004        // byte 4 (0-based 3) = input value.
1005
1006        // Status OK, value byte zero → pin reads low.
1007        let res = [0u8; 64];
1008        assert_eq!(decode_read_digital_input_response(&res).unwrap(), 0);
1009
1010        // Status OK, value byte set → pin reads high.
1011        let mut res = [0u8; 64];
1012        res[3] = 1;
1013        assert_eq!(decode_read_digital_input_response(&res).unwrap(), 1);
1014
1015        // Non-zero status byte still surfaces as InternalError.
1016        let mut res = [0u8; 64];
1017        res[2] = 1;
1018        res[3] = 1;
1019        assert!(decode_read_digital_input_response(&res).is_err());
1020    }
1021
1022    #[test]
1023    fn test_compose_pin_function_byte() {
1024        use crate::io::private::compose_pin_function_byte;
1025
1026        // Non-inverted: exactly the enum discriminant.
1027        assert_eq!(
1028            compose_pin_function_byte(PinFunction::DigitalInput, false),
1029            0x02
1030        );
1031        assert_eq!(
1032            compose_pin_function_byte(PinFunction::DigitalOutput, false),
1033            0x04
1034        );
1035        assert_eq!(
1036            compose_pin_function_byte(PinFunction::TriggeredInput, false),
1037            0x20
1038        );
1039
1040        // Inverted: bit 7 set, base function preserved.
1041        assert_eq!(
1042            compose_pin_function_byte(PinFunction::DigitalInput, true),
1043            0x82
1044        );
1045        assert_eq!(
1046            compose_pin_function_byte(PinFunction::DigitalOutput, true),
1047            0x84
1048        );
1049        assert_eq!(
1050            compose_pin_function_byte(PinFunction::TriggeredInput, true),
1051            0xA0
1052        );
1053    }
1054
1055    #[test]
1056    fn test_decode_pin_function_from_cache() {
1057        // Combined bytes decode to their base function; the invert bit is
1058        // ignored by the decoder (caller uses `is_inverted` / `get_pin_invert`
1059        // to recover it).
1060        assert_eq!(
1061            decode_pin_function_from_cache(0x82),
1062            PinFunction::DigitalInput
1063        );
1064        assert_eq!(
1065            decode_pin_function_from_cache(0x84),
1066            PinFunction::DigitalOutput
1067        );
1068        assert_eq!(
1069            decode_pin_function_from_cache(0xA0),
1070            PinFunction::TriggeredInput
1071        );
1072
1073        // Base-only values round-trip.
1074        assert_eq!(
1075            decode_pin_function_from_cache(0x02),
1076            PinFunction::DigitalInput
1077        );
1078        assert_eq!(
1079            decode_pin_function_from_cache(0x00),
1080            PinFunction::PinRestricted
1081        );
1082
1083        // `0x80` alone (invert flag with no base function) is not a valid
1084        // pin configuration — decode as PinRestricted.
1085        assert_eq!(
1086            decode_pin_function_from_cache(0x80),
1087            PinFunction::PinRestricted
1088        );
1089    }
1090
1091    #[test]
1092    fn test_pin_data_invert_accessors() {
1093        let mut pin_data = PinData::new();
1094
1095        pin_data.pin_function = 0x82; // DigitalInput | Invert
1096        assert!(pin_data.is_inverted());
1097        assert_eq!(pin_data.base_function(), PinFunction::DigitalInput);
1098        // Existing masked helpers still work on the OR'd byte.
1099        assert!(pin_data.is_digital_input());
1100        assert!(!pin_data.is_digital_output());
1101
1102        pin_data.pin_function = 0x02; // DigitalInput, no invert
1103        assert!(!pin_data.is_inverted());
1104        assert_eq!(pin_data.base_function(), PinFunction::DigitalInput);
1105    }
1106
1107    #[test]
1108    fn test_parse_bulk_pin_settings_response_preserves_invert_bit() {
1109        use crate::io::private::Command;
1110
1111        let mut response = [0u8; 64];
1112        response[1] = Command::InputOutputExtended as u8;
1113        response[8] = 0x82; // pin 1 = DigitalInput | Invert
1114        response[9] = 0x04; // pin 2 = DigitalOutput (no invert)
1115        response[10] = 0xA0; // pin 3 = TriggeredInput | Invert
1116
1117        let raw = parse_bulk_pin_settings_response(&response).unwrap();
1118        assert_eq!(raw[0], 0x82);
1119        assert_eq!(raw[1], 0x04);
1120        assert_eq!(raw[2], 0xA0);
1121
1122        // The typed-view decode in `read_all_pin_functions` drops the invert
1123        // bit; verify `from_u8` returns the base function for a combined byte.
1124        assert_eq!(
1125            PinFunction::from_u8(raw[0]).unwrap(),
1126            PinFunction::DigitalInput
1127        );
1128        assert_eq!(
1129            PinFunction::from_u8(raw[2]).unwrap(),
1130            PinFunction::TriggeredInput
1131        );
1132    }
1133
1134    #[test]
1135    fn test_parse_bulk_pin_settings_response_rejects_short() {
1136        let short = [0u8; 32];
1137        assert!(parse_bulk_pin_settings_response(&short).is_err());
1138    }
1139
1140    #[test]
1141    fn test_parse_bulk_pin_settings_response_rejects_wrong_command() {
1142        let mut response = [0u8; 64];
1143        response[1] = 0x12; // not InputOutputExtended
1144        assert!(parse_bulk_pin_settings_response(&response).is_err());
1145    }
1146
1147    #[test]
1148    fn test_parse_bulk_pin_settings_response_accepts_all_zero() {
1149        // All-zero pin bytes is the silent-failure signature of a locked
1150        // device. Parse logs a warning but does NOT fail —
1151        // fresh-after-clear is also legitimately all-zero.
1152        use crate::io::private::Command;
1153        let mut response = [0u8; 64];
1154        response[1] = Command::InputOutputExtended as u8;
1155        let raw = parse_bulk_pin_settings_response(&response)
1156            .expect("all-zero pin settings should still parse");
1157        assert!(raw.iter().all(|&b| b == 0));
1158    }
1159}