use crate::communication::{CommunicationManager, NetworkInterface, UsbHidInterface};
use crate::encoders::EncoderData;
use crate::error::{PoKeysError, Result};
use crate::io::{PinData, PinFunction};
use crate::keyboard_matrix::{MatrixKeyboard, MatrixKeyboardConfig};
use crate::lcd::LcdData;
use crate::matrix::MatrixLed;
use crate::pulse_engine::PulseEngineV2;
use crate::pwm::PwmData;
use crate::sensors::EasySensor;
use crate::types::*;
use std::sync::{LazyLock, Mutex};
use std::time::Duration;
const MATRIX_KEYBOARD_ID: u8 = 0;
const MATRIX_KB_OPT_WRITE_CONFIG: u8 = 1;
const MATRIX_KB_OPT_READ_CONFIG: u8 = 10;
fn encode_matrix_kb_mapping_chunk(
codes: &[u8],
modifiers: &[u8],
triggered: Option<&[u8]>,
chunk_base: usize,
) -> [u8; 34] {
let mut data = [0u8; 34];
for i in 0..16 {
let idx = chunk_base + i;
if idx < codes.len() {
data[i] = codes[idx];
}
if idx < modifiers.len() {
data[16 + i] = modifiers[idx];
}
}
if let Some(triggered) = triggered {
let mut bitmap: u16 = 0;
for i in 0..16 {
let idx = chunk_base + i;
if idx < triggered.len() && triggered[idx] != 0 {
bitmap |= 1 << i;
}
}
data[32] = (bitmap & 0xFF) as u8;
data[33] = (bitmap >> 8) as u8;
}
data
}
fn decode_matrix_kb_mapping_chunk(
response: &[u8],
codes: &mut [u8],
modifiers: &mut [u8],
triggered: Option<&mut [u8]>,
chunk_base: usize,
) {
for i in 0..16 {
let idx = chunk_base + i;
if idx < codes.len() {
codes[idx] = response[8 + i];
}
if idx < modifiers.len() {
modifiers[idx] = response[24 + i];
}
}
if let Some(triggered) = triggered {
let bitmap = u16::from_le_bytes([response[40], response[41]]);
for i in 0..16 {
let idx = chunk_base + i;
if idx < triggered.len() {
triggered[idx] = if (bitmap & (1 << i)) != 0 { 1 } else { 0 };
}
}
}
}
fn encode_matrix_kb_config_payload(
enabled: bool,
width: u8,
height: u8,
column_pins: &[u8],
row_pins: &[u8],
) -> [u8; 55] {
let mut data = [0u8; 55];
data[0] = if enabled { 1 } else { 0 };
data[1] = ((width - 1) << 4) | (height - 1);
for (i, &pin) in row_pins.iter().enumerate().take(8) {
data[2 + i] = pin.saturating_sub(1);
}
for (i, &pin) in column_pins.iter().enumerate().take(8) {
data[10 + i] = pin.saturating_sub(1);
}
for (offset, &pin) in row_pins.iter().enumerate().skip(8).take(8) {
data[34 + (offset - 8)] = pin.saturating_sub(1);
}
data
}
fn validate_matrix_kb_mapping_response(response: &[u8], option: u8) -> Result<()> {
if response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"Invalid response command for key mapping read (option {option}): 0x{:02X}",
response[1]
)));
}
if response.len() < 42 {
return Err(PoKeysError::Protocol(format!(
"Key mapping response too short for option {option}: {} bytes (need 42)",
response.len()
)));
}
Ok(())
}
fn parse_matrix_kb_config_readback(response: &[u8]) -> Result<MatrixKeyboardConfig> {
if response.len() < 2 || response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"matrix keyboard config readback: echo mismatch: 0x{:02X}",
response.get(1).copied().unwrap_or(0)
)));
}
if response.len() < 52 {
return Err(PoKeysError::Protocol(format!(
"matrix keyboard config readback too short: {} bytes (need 52)",
response.len()
)));
}
let enabled = (response[8] & 0x01) != 0;
let size_byte = response[9];
let width = (size_byte >> 4) + 1;
let height = (size_byte & 0x0F) + 1;
let mut row_pins = [0u8; 16];
for (i, slot) in row_pins.iter_mut().enumerate().take(8) {
*slot = response[10 + i].wrapping_add(1);
}
for (offset, slot) in row_pins.iter_mut().enumerate().skip(8).take(8) {
*slot = response[42 + (offset - 8)].wrapping_add(1);
}
let mut column_pins = [0u8; 8];
for (i, slot) in column_pins.iter_mut().enumerate() {
*slot = response[18 + i].wrapping_add(1);
}
let mut direct_macro_bitmap = [0u8; 16];
direct_macro_bitmap.copy_from_slice(&response[26..42]);
Ok(MatrixKeyboardConfig {
enabled,
width,
height,
row_pins,
column_pins,
direct_macro_bitmap,
alternate_function_pin: response[50],
scanning_decimation: response[51],
})
}
fn verify_matrix_kb_config_matches(
actual: &MatrixKeyboardConfig,
expected_width: u8,
expected_height: u8,
expected_column_pins: &[u8],
expected_row_pins: &[u8],
expected_enabled: bool,
) -> Result<()> {
if actual.enabled != expected_enabled {
return Err(PoKeysError::Protocol(format!(
"configure_matrix_keyboard: device reports enabled={}, expected {expected_enabled} \
(configuration may be locked; raw config byte=0x{:02X})",
actual.enabled,
if actual.enabled { 0x01 } else { 0x00 }
)));
}
if !expected_enabled {
return Ok(());
}
if actual.width != expected_width || actual.height != expected_height {
return Err(PoKeysError::Protocol(format!(
"configure_matrix_keyboard: device reports width={} height={}, \
expected width={expected_width} height={expected_height} (raw size byte=0x{:02X})",
actual.width,
actual.height,
actual.size_byte()
)));
}
for (i, &pin) in expected_row_pins
.iter()
.enumerate()
.take(expected_height as usize)
.take(8)
{
if actual.row_pins[i] != pin {
return Err(PoKeysError::Protocol(format!(
"configure_matrix_keyboard: row pin {i} mismatch: device has pin {}, expected pin {pin}",
actual.row_pins[i]
)));
}
}
for (i, &pin) in expected_column_pins
.iter()
.enumerate()
.take(expected_width as usize)
.take(8)
{
if actual.column_pins[i] != pin {
return Err(PoKeysError::Protocol(format!(
"configure_matrix_keyboard: column pin {i} mismatch: device has pin {}, expected pin {pin}",
actual.column_pins[i]
)));
}
}
if expected_height > 8 {
for (offset, &pin) in expected_row_pins
.iter()
.enumerate()
.skip(8)
.take((expected_height as usize).saturating_sub(8))
.take(8)
{
if actual.row_pins[offset] != pin {
return Err(PoKeysError::Protocol(format!(
"configure_matrix_keyboard: extended row pin {offset} mismatch: device has pin {}, expected pin {pin}",
actual.row_pins[offset]
)));
}
}
}
Ok(())
}
pub struct PoKeysDevice {
connection_type: DeviceConnectionType,
connection_param: ConnectionParam,
pub info: DeviceInfo,
pub device_data: DeviceData,
pub network_device_data: Option<NetworkDeviceInfo>,
pub model: Option<crate::models::DeviceModel>,
pub pins: Vec<PinData>,
pub encoders: Vec<EncoderData>,
pub pwm: PwmData,
pub matrix_keyboard: MatrixKeyboard,
pub matrix_led: Vec<MatrixLed>,
pub lcd: LcdData,
pub pulse_engine_v2: PulseEngineV2,
pub easy_sensors: Vec<EasySensor>,
pub rtc: RealTimeClock,
pub(crate) communication: CommunicationManager,
usb_interface: Option<Box<dyn UsbHidInterface>>,
network_interface: Option<Box<dyn NetworkInterface>>,
pub fast_encoders_configuration: u8,
pub fast_encoders_options: u8,
pub ultra_fast_encoder_configuration: u8,
pub ultra_fast_encoder_options: u8,
pub ultra_fast_encoder_filter: u32,
pub po_ext_bus_data: Vec<u8>,
pub i2c_config: I2cConfig,
pub i2c_metrics: I2cMetrics,
pub validation_level: ValidationLevel,
}
impl PoKeysDevice {
fn new(connection_type: DeviceConnectionType) -> Self {
Self {
connection_type,
connection_param: ConnectionParam::Tcp,
info: DeviceInfo::default(),
device_data: DeviceData::default(),
network_device_data: None,
model: None,
pins: Vec::new(),
encoders: Vec::new(),
pwm: PwmData::new(),
matrix_keyboard: MatrixKeyboard::new(),
matrix_led: Vec::new(),
lcd: LcdData::new(),
pulse_engine_v2: PulseEngineV2::new(),
easy_sensors: Vec::new(),
rtc: RealTimeClock::default(),
communication: CommunicationManager::new(connection_type),
usb_interface: None,
network_interface: None,
fast_encoders_configuration: 0,
fast_encoders_options: 0,
ultra_fast_encoder_configuration: 0,
ultra_fast_encoder_options: 0,
ultra_fast_encoder_filter: 0,
po_ext_bus_data: Vec::new(),
i2c_config: I2cConfig::default(),
i2c_metrics: I2cMetrics::default(),
validation_level: ValidationLevel::None,
}
}
pub fn get_device_data(&mut self) -> Result<()> {
self.read_device_data()?;
self.initialize_device_structures()?;
self.load_device_model()?;
Ok(())
}
fn load_device_model(&mut self) -> Result<()> {
use crate::models::load_model;
let device_type_name = self.device_data.device_type_name();
let model_name = match device_type_name.as_str() {
"PoKeys56U" => "PoKeys56U",
"PoKeys56E" => "PoKeys56E",
"PoKeys57U" => "PoKeys57U",
"PoKeys57E" => "PoKeys57E",
"PoKeys57Ev1.1" => "PoKeys57E", "PoKeys57CNC" => "PoKeys57CNC",
_ => return Ok(()), };
match load_model(model_name, None) {
Ok(model) => {
log::info!("Loaded device model: {}", model_name);
self.model = Some(model);
Ok(())
}
Err(e) => {
log::warn!("Failed to load device model {}: {}", model_name, e);
Ok(()) }
}
}
pub fn is_pin_capability_supported(&self, pin: u32, capability: &str) -> bool {
if let Some(model) = &self.model {
model.is_pin_capability_supported(pin as u8, capability)
} else {
true
}
}
pub fn get_pin_capabilities(&self, pin: u32) -> Vec<String> {
if let Some(model) = &self.model {
model.get_pin_capabilities(pin as u8)
} else {
Vec::new()
}
}
pub fn validate_pin_capability(&self, pin: u32, capability: &str) -> Result<()> {
if let Some(model) = &self.model {
model.validate_pin_capability(pin as u8, capability)
} else {
Ok(())
}
}
pub fn get_related_capabilities(&self, pin: u32, capability: &str) -> Vec<(String, u8)> {
if let Some(model) = &self.model {
model.get_related_capabilities(pin as u8, capability)
} else {
Vec::new()
}
}
pub fn save_configuration(&mut self) -> Result<()> {
self.send_request(0x50, 0xAA, 0x55, 0, 0)?;
Ok(())
}
pub fn get_system_load(&mut self) -> Result<u8> {
let response = self.send_request(0x05, 0, 0, 0, 0)?;
Ok(parse_system_load_response(&response))
}
pub fn set_device_name(&mut self, name: &str) -> Result<()> {
let mut name_bytes = [0u8; 20];
let name_str = if name.len() > 20 { &name[..20] } else { name };
let name_bytes_slice = name_str.as_bytes();
name_bytes[..name_bytes_slice.len()].copy_from_slice(name_bytes_slice);
let mut request = [0u8; 64];
request[0] = 0xBB; request[1] = 0x06; request[2] = 0x01; request[3] = 0x01; request[4] = 0x00; request[5] = 0x00; request[6] = self.communication.get_next_request_id();
request[35..55].copy_from_slice(&name_bytes);
let mut checksum: u8 = 0;
for i in 0..7 {
checksum = checksum.wrapping_add(request[i]);
}
request[7] = checksum;
match self.connection_type {
DeviceConnectionType::UsbDevice | DeviceConnectionType::FastUsbDevice => {
if let Some(ref mut interface) = self.usb_interface {
let mut hid_packet = [0u8; 65];
hid_packet[0] = 0; hid_packet[1..65].copy_from_slice(&request);
interface.write(&hid_packet)?;
let mut response = [0u8; 65];
interface.read(&mut response)?;
} else {
return Err(PoKeysError::NotConnected);
}
}
DeviceConnectionType::NetworkDevice => {
if let Some(ref mut interface) = self.network_interface {
interface.send(&request)?;
let mut response = [0u8; 64];
let _ = interface
.receive_timeout(&mut response, std::time::Duration::from_millis(2000));
} else {
return Err(PoKeysError::NotConnected);
}
}
}
self.save_configuration()
}
pub fn clear_configuration(&mut self) -> Result<()> {
match self.send_request(0x52, 0xAA, 0x55, 0, 0) {
Ok(_) => Ok(()),
Err(PoKeysError::Transfer(_)) => Ok(()),
Err(e) => Err(e),
}
}
pub fn is_configuration_locked(&mut self) -> Result<bool> {
self.read_device_data()?;
Ok(self.device_data.device_lock_status != 0)
}
pub fn reboot_device(&mut self) -> Result<()> {
match self.send_request(0xF3, 0, 0, 0, 0) {
Ok(_) => Ok(()),
Err(PoKeysError::Transfer(_)) => Ok(()),
Err(e) => Err(e),
}
}
pub fn custom_request(
&mut self,
request_type: u8,
param1: u8,
param2: u8,
param3: u8,
param4: u8,
) -> Result<[u8; RESPONSE_BUFFER_SIZE]> {
self.send_request(request_type, param1, param2, param3, param4)
}
#[deprecated(
since = "1.0.5",
note = "Use `set_network_timeout` and `set_network_retries` separately. \
The `read_retries` parameter has never been honoured by the \
retry loops in this crate; passing any value here is a no-op."
)]
pub fn set_ethernet_retry_count_and_timeout(
&mut self,
send_retries: u32,
_read_retries: u32,
timeout_ms: u32,
) {
self.communication.set_retries_and_timeout(
send_retries,
0,
Duration::from_millis(timeout_ms as u64),
);
}
pub fn set_network_timeout(&mut self, timeout: Duration) {
let send_retries = self.communication.send_retries();
self.communication
.set_retries_and_timeout(send_retries, 0, timeout);
}
pub fn set_network_retries(&mut self, retries: u32) {
let timeout = self.communication.socket_timeout();
self.communication
.set_retries_and_timeout(retries, 0, timeout);
}
pub fn network_timeout(&self) -> Duration {
self.communication.socket_timeout()
}
pub fn send_retries(&self) -> u32 {
self.communication.send_retries()
}
pub fn tune_for_realtime_polling(&mut self) {
self.communication
.set_retries_and_timeout(1, 0, Duration::from_millis(100));
}
pub fn get_connection_type(&self) -> DeviceConnectionType {
self.connection_type
}
pub fn set_i2c_config(&mut self, config: I2cConfig) {
self.i2c_config = config;
}
pub fn get_i2c_config(&self) -> &I2cConfig {
&self.i2c_config
}
pub fn set_validation_level(&mut self, level: ValidationLevel) {
self.validation_level = level;
}
pub fn get_i2c_metrics(&self) -> &I2cMetrics {
&self.i2c_metrics
}
pub fn reset_i2c_metrics(&mut self) {
self.i2c_metrics = I2cMetrics::default();
}
pub fn health_check(&mut self) -> HealthStatus {
HealthStatus {
connectivity: self.test_connectivity(),
i2c_health: self.test_i2c_health(),
error_rate: self.calculate_error_rate(),
performance: self.get_performance_summary(),
}
}
fn test_connectivity(&mut self) -> ConnectivityStatus {
match self.get_device_data() {
Ok(_) => ConnectivityStatus::Healthy,
Err(e) => ConnectivityStatus::Degraded(e.to_string()),
}
}
fn test_i2c_health(&mut self) -> I2cHealthStatus {
match self.i2c_get_status() {
Ok(I2cStatus::Ok) => I2cHealthStatus::Healthy,
Ok(status) => I2cHealthStatus::Degraded(format!("I2C status: {:?}", status)),
Err(e) => I2cHealthStatus::Failed(e.to_string()),
}
}
fn calculate_error_rate(&self) -> f64 {
if self.i2c_metrics.total_commands == 0 {
0.0
} else {
self.i2c_metrics.failed_commands as f64 / self.i2c_metrics.total_commands as f64
}
}
fn get_performance_summary(&self) -> PerformanceSummary {
let success_rate = if self.i2c_metrics.total_commands == 0 {
1.0
} else {
self.i2c_metrics.successful_commands as f64 / self.i2c_metrics.total_commands as f64
};
PerformanceSummary {
avg_response_time_ms: self.i2c_metrics.average_response_time.as_millis() as f64,
success_rate,
throughput_commands_per_sec: 0.0, }
}
#[allow(dead_code)]
fn validate_packet(&self, data: &[u8]) -> Result<()> {
match &self.validation_level {
ValidationLevel::None => Ok(()),
ValidationLevel::Basic => self.validate_basic_structure(data),
ValidationLevel::Strict => self.validate_strict_protocol(data),
ValidationLevel::Custom(config) => self.validate_custom(data, config),
}
}
#[allow(dead_code)]
fn validate_basic_structure(&self, data: &[u8]) -> Result<()> {
if data.len() < 3 {
return Err(PoKeysError::InvalidPacketStructure(
"Minimum packet size is 3 bytes".to_string(),
));
}
Ok(())
}
#[allow(dead_code)]
fn validate_strict_protocol(&self, data: &[u8]) -> Result<()> {
if data.len() < 3 {
return Err(PoKeysError::InvalidPacketStructure(
"Minimum packet size is 3 bytes".to_string(),
));
}
let command = data[0];
let device_id = data[1];
let checksum = data[data.len() - 1];
if !matches!(command, 0x11..=0x42) {
return Err(PoKeysError::InvalidCommand(command));
}
if device_id >= 16 {
return Err(PoKeysError::InvalidDeviceId(device_id));
}
let calculated_checksum = self.calculate_checksum(&data[..data.len() - 1]);
if checksum != calculated_checksum {
return Err(PoKeysError::InvalidChecksumDetailed {
expected: calculated_checksum,
received: checksum,
});
}
Ok(())
}
#[allow(dead_code)]
fn validate_custom(&self, data: &[u8], config: &crate::types::ValidationConfig) -> Result<()> {
if config.validate_packet_structure && data.len() < 3 {
return Err(PoKeysError::InvalidPacketStructure(
"Minimum packet size is 3 bytes".to_string(),
));
}
if data.len() >= 3 {
let command = data[0];
let device_id = data[1];
let checksum = data[data.len() - 1];
if config.validate_command_ids
&& !config.valid_commands.is_empty()
&& !config.valid_commands.contains(&command)
{
return Err(PoKeysError::InvalidCommand(command));
}
if config.validate_device_ids && device_id > config.max_device_id {
return Err(PoKeysError::InvalidDeviceId(device_id));
}
if config.validate_checksums {
let calculated_checksum = self.calculate_checksum(&data[..data.len() - 1]);
if checksum != calculated_checksum {
return Err(PoKeysError::InvalidChecksumDetailed {
expected: calculated_checksum,
received: checksum,
});
}
}
}
Ok(())
}
#[allow(dead_code)]
fn calculate_checksum(&self, data: &[u8]) -> u8 {
data.iter().fold(0, |acc, &byte| acc ^ byte)
}
pub fn check_pin_capability(&self, pin: u32, capability: crate::io::PinCapability) -> bool {
if pin as usize >= self.pins.len() {
return false;
}
match self.device_data.device_type_id {
32 => check_pokeys57cnc_pin_capability(pin, capability), _ => false, }
}
pub fn get_network_configuration(
&mut self,
timeout_ms: u32,
) -> Result<(Option<NetworkDeviceSummary>, NetworkDeviceInfo)> {
let discovery_info = enumerate_network_devices(timeout_ms)?
.into_iter()
.find(|d| d.serial_number == self.device_data.serial_number);
let response = self.send_request(0xE0, 0x00, 0x00, 0, 0)?;
let config = NetworkDeviceInfo {
dhcp: response.get(8).copied().unwrap_or(0),
ip_address_setup: [
response.get(9).copied().unwrap_or(0),
response.get(10).copied().unwrap_or(0),
response.get(11).copied().unwrap_or(0),
response.get(12).copied().unwrap_or(0),
],
ip_address_current: [
response.get(13).copied().unwrap_or(0),
response.get(14).copied().unwrap_or(0),
response.get(15).copied().unwrap_or(0),
response.get(16).copied().unwrap_or(0),
],
tcp_timeout: u16::from_le_bytes([
response.get(17).copied().unwrap_or(0),
response.get(18).copied().unwrap_or(0),
])
.saturating_mul(100),
gateway_ip: [
response.get(19).copied().unwrap_or(0),
response.get(20).copied().unwrap_or(0),
response.get(21).copied().unwrap_or(0),
response.get(22).copied().unwrap_or(0),
],
subnet_mask: [
response.get(23).copied().unwrap_or(0),
response.get(24).copied().unwrap_or(0),
response.get(25).copied().unwrap_or(0),
response.get(26).copied().unwrap_or(0),
],
additional_network_options: response.get(27).copied().unwrap_or(0),
};
Ok((discovery_info, config))
}
pub fn set_network_configuration(&mut self, config: &NetworkDeviceInfo) -> Result<()> {
let timeout_units = (config.tcp_timeout / 100).max(1);
let timeout_bytes = timeout_units.to_le_bytes();
let gateway_subnet_set: u8 =
if config.gateway_ip != [0, 0, 0, 0] || config.subnet_mask != [0, 0, 0, 0] {
1
} else {
0
};
let options = (config.additional_network_options & 0x0F) | 0xA0;
let mut data = [0u8; 21];
data[0] = config.dhcp; data[1..5].copy_from_slice(&config.ip_address_setup); data[9] = timeout_bytes[0]; data[10] = timeout_bytes[1];
data[11..15].copy_from_slice(&config.gateway_ip); data[15..19].copy_from_slice(&config.subnet_mask); data[19] = gateway_subnet_set; data[20] = options;
self.send_request_with_data(0xE0, 10, 0, 0, 0, &data)?;
Ok(())
}
pub fn send_request(
&mut self,
request_type: u8,
param1: u8,
param2: u8,
param3: u8,
param4: u8,
) -> Result<[u8; RESPONSE_BUFFER_SIZE]> {
match self.connection_type {
DeviceConnectionType::UsbDevice | DeviceConnectionType::FastUsbDevice => {
if let Some(ref mut interface) = self.usb_interface {
self.communication.send_usb_request(
interface,
request_type,
param1,
param2,
param3,
param4,
)
} else {
Err(PoKeysError::NotConnected)
}
}
DeviceConnectionType::NetworkDevice => {
if let Some(ref mut interface) = self.network_interface {
self.communication.send_network_request(
interface,
request_type,
param1,
param2,
param3,
param4,
)
} else {
Err(PoKeysError::NotConnected)
}
}
}
}
pub fn send_request_with_data(
&mut self,
request_type: u8,
param1: u8,
param2: u8,
param3: u8,
param4: u8,
data: &[u8],
) -> Result<[u8; RESPONSE_BUFFER_SIZE]> {
let request = self.communication.prepare_request_with_data(
request_type,
param1,
param2,
param3,
param4,
Some(data),
);
match self.connection_type {
DeviceConnectionType::UsbDevice | DeviceConnectionType::FastUsbDevice => {
if let Some(ref mut interface) = self.usb_interface {
self.communication.send_usb_request_raw(interface, &request)
} else {
Err(PoKeysError::NotConnected)
}
}
DeviceConnectionType::NetworkDevice => {
if let Some(ref mut interface) = self.network_interface {
self.communication
.send_network_request_raw(interface, &request)
} else {
Err(PoKeysError::NotConnected)
}
}
}
}
pub fn read_device_data(&mut self) -> Result<()> {
let response = self.send_request(0x00, 0, 0, 0, 0)?;
if response.len() < 64 {
return Err(PoKeysError::Protocol(
"Read device data response too short".to_string(),
));
}
self.parse_device_data_response(&response)?;
Ok(())
}
fn parse_device_data_response(&mut self, response: &[u8]) -> Result<()> {
if response.len() >= 64 && (&response[8..12] == b"PK58" || &response[8..12] == b"PKEx") {
let software_version_encoded = response[4]; let revision_number = response[5];
let major_bits = (software_version_encoded >> 4) & 0x0F; let minor_bits = software_version_encoded & 0x0F; let decoded_major = 1 + major_bits;
let decoded_minor = minor_bits;
let serial_32bit =
u32::from_le_bytes([response[12], response[13], response[14], response[15]]);
let firmware_version = response[16];
let firmware_revision = response[17];
let hw_id = response[18];
let user_id = response[19];
let build_date_bytes = &response[20..31];
let device_name_bytes = &response[31..41];
let firmware_type = response[41];
let _app_firmware_major = response[42];
let _app_firmware_minor = response[43];
let product_id = response[57];
let configuration_lock_status = response[58];
self.device_data.serial_number = serial_32bit;
self.device_data.device_lock_status = configuration_lock_status;
self.device_data.firmware_version_major = decoded_major;
self.device_data.firmware_version_minor = decoded_minor;
self.device_data.firmware_revision = revision_number;
self.device_data.secondary_firmware_version_major = firmware_version;
self.device_data.secondary_firmware_version_minor = firmware_revision;
self.device_data.hw_type = hw_id;
self.device_data.user_id = user_id;
self.device_data.fw_type = firmware_type;
self.device_data.product_id = product_id;
self.device_data.device_name.fill(0);
let name_len =
std::cmp::min(device_name_bytes.len(), self.device_data.device_name.len());
self.device_data.device_name[..name_len]
.copy_from_slice(&device_name_bytes[..name_len]);
self.device_data.build_date.fill(0);
let date_len = std::cmp::min(build_date_bytes.len(), self.device_data.build_date.len());
self.device_data.build_date[..date_len].copy_from_slice(&build_date_bytes[..date_len]);
log::debug!("Extended device info parsed:");
log::debug!(" Serial: {}", serial_32bit);
log::debug!(
" Decoded firmware: {}.{}.{}",
decoded_major,
decoded_minor,
revision_number
);
log::debug!(
" Raw software version byte: 0x{:02X}",
software_version_encoded
);
log::debug!(
" Extended firmware: {}.{}",
firmware_version,
firmware_revision
);
log::debug!(" Hardware type: {}", hw_id);
} else {
let serial_16bit = ((response[2] as u32) << 8) | (response[3] as u32);
let software_version_encoded = response[4];
let revision_number = response[5];
let major_bits = (software_version_encoded >> 4) & 0x0F;
let minor_bits = software_version_encoded & 0x0F;
let decoded_major = 1 + major_bits;
let decoded_minor = minor_bits;
self.device_data.serial_number = serial_16bit;
self.device_data.firmware_version_major = decoded_major;
self.device_data.firmware_version_minor = decoded_minor;
self.device_data.firmware_revision = revision_number;
self.device_data.hw_type = 0; self.device_data.device_name.fill(0);
self.device_data.build_date.fill(0);
log::debug!("Legacy device info parsed:");
log::debug!(" Serial: {}", serial_16bit);
log::debug!(
" Decoded firmware: {}.{}.{}",
decoded_major,
decoded_minor,
revision_number
);
}
Ok(())
}
fn initialize_device_structures(&mut self) -> Result<()> {
match self.device_data.device_type_id {
32 => self.initialize_pokeys57cnc(), _ => self.initialize_generic_device(),
}
}
fn initialize_pokeys57cnc(&mut self) -> Result<()> {
self.info.pin_count = 55;
self.info.pwm_count = 6;
self.info.encoders_count = 25;
self.info.fast_encoders = 3;
self.info.ultra_fast_encoders = 1;
self.info.analog_inputs = 8;
self.info.pulse_engine_v2 = 1;
self.pins = vec![PinData::new(); self.info.pin_count as usize];
self.encoders = vec![EncoderData::new(); self.info.encoders_count as usize];
self.po_ext_bus_data = vec![0; 10];
Ok(())
}
fn initialize_generic_device(&mut self) -> Result<()> {
self.info.pin_count = 55; self.pins = vec![PinData::new(); self.info.pin_count as usize];
self.encoders = vec![EncoderData::new(); 25]; Ok(())
}
pub fn configure_led_matrix(
&mut self,
config: &crate::matrix::MatrixLedProtocolConfig,
) -> Result<()> {
let enabled_flags = self.encode_matrix_enabled(config);
let display1_size = self.encode_display_size(
config.display1_characters,
crate::matrix::SEVEN_SEGMENT_COLUMNS,
);
let display2_size = self.encode_display_size(
config.display2_characters,
crate::matrix::SEVEN_SEGMENT_COLUMNS,
);
let _response =
self.send_request(0xD5, 0x00, enabled_flags, display1_size, display2_size)?;
Ok(())
}
pub fn read_led_matrix_config(&mut self) -> Result<crate::matrix::MatrixLedProtocolConfig> {
let response = self.send_request(0xD5, 0x01, 0, 0, 0)?;
let enabled_flags = response[2];
let display1_size = response[3];
let display2_size = response[4];
let display1_enabled = (enabled_flags & 0x01) != 0;
let display2_enabled = (enabled_flags & 0x02) != 0;
let display1_characters = display2_size & 0x0F; let display2_characters = display1_size & 0x0F;
Ok(crate::matrix::MatrixLedProtocolConfig {
display1_enabled,
display2_enabled,
display1_characters,
display2_characters,
})
}
pub fn update_led_matrix(
&mut self,
matrix_id: u8,
action: crate::matrix::MatrixAction,
row: u8,
column: u8,
data: &[u8],
) -> Result<()> {
let action_code = match (matrix_id, action) {
(1, crate::matrix::MatrixAction::UpdateWhole) => 1,
(1, crate::matrix::MatrixAction::SetPixel) => 5,
(1, crate::matrix::MatrixAction::ClearPixel) => 6,
(2, crate::matrix::MatrixAction::UpdateWhole) => 11,
(2, crate::matrix::MatrixAction::SetPixel) => 15,
(2, crate::matrix::MatrixAction::ClearPixel) => 16,
_ => {
return Err(PoKeysError::Parameter(format!(
"Invalid matrix ID: {}",
matrix_id
)));
}
};
let request = self.communication.prepare_request_with_data(
0xD6,
action_code,
row,
column,
0,
Some(data),
);
match self.connection_type {
DeviceConnectionType::NetworkDevice => {
if let Some(ref mut interface) = self.network_interface {
let request_id = request[6];
interface.send(&request[..64])?;
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
interface.receive(&mut response)?;
self.communication
.validate_response(&response, request_id)?;
}
}
_ => {
return Err(PoKeysError::NotSupported);
}
}
Ok(())
}
pub fn configure_matrix_keyboard(
&mut self,
width: u8,
height: u8,
column_pins: &[u8],
row_pins: &[u8],
) -> Result<()> {
if width == 0 || width > 8 {
return Err(PoKeysError::Parameter(format!(
"Matrix width must be 1-8 (got {width})"
)));
}
if height == 0 || height > 16 {
return Err(PoKeysError::Parameter(format!(
"Matrix height must be 1-16 (got {height})"
)));
}
if column_pins.len() < width as usize {
return Err(PoKeysError::Parameter(format!(
"column_pins has {} entries, need at least {width}",
column_pins.len()
)));
}
if row_pins.len() < height as usize {
return Err(PoKeysError::Parameter(format!(
"row_pins has {} entries, need at least {height}",
row_pins.len()
)));
}
let used_rows = &row_pins[..height as usize];
let used_cols = &column_pins[..width as usize];
for &pin in used_rows.iter().chain(used_cols.iter()) {
if pin == 0 || pin > 55 {
return Err(PoKeysError::Parameter(format!(
"Matrix keyboard pin {pin} is out of range (must be 1-55)"
)));
}
}
for &row in used_rows {
if used_cols.contains(&row) {
return Err(PoKeysError::Parameter(format!(
"Pin {row} cannot be used as both a row and a column"
)));
}
}
let mut raw = self.read_all_pin_settings_raw()?;
for &pin in used_rows {
let idx = (pin - 1) as usize;
let invert = raw[idx] & 0x80;
raw[idx] = (PinFunction::DigitalOutput as u8) | invert;
}
for &pin in used_cols {
let idx = (pin - 1) as usize;
let invert = raw[idx] & 0x80;
raw[idx] = (PinFunction::DigitalInput as u8) | invert;
}
self.set_all_pin_settings_raw(&raw)?;
let deactivate_payload =
encode_matrix_kb_config_payload(false, width, height, column_pins, row_pins);
self.send_matrix_kb_config_write(&deactivate_payload, "deactivate phase")?;
let activate_payload =
encode_matrix_kb_config_payload(true, width, height, column_pins, row_pins);
self.send_matrix_kb_config_write(&activate_payload, "activate phase")?;
let actual = self.get_matrix_keyboard_configuration()?;
verify_matrix_kb_config_matches(&actual, width, height, column_pins, row_pins, true)?;
self.matrix_keyboard.configuration = 1;
self.matrix_keyboard.width = width;
self.matrix_keyboard.height = height;
self.matrix_keyboard.column_pins.fill(0);
self.matrix_keyboard.row_pins.fill(0);
for (i, &pin) in column_pins.iter().enumerate().take(8) {
self.matrix_keyboard.column_pins[i] = pin;
}
for (i, &pin) in row_pins.iter().enumerate().take(16) {
self.matrix_keyboard.row_pins[i] = pin;
}
Ok(())
}
fn send_matrix_kb_config_write(&mut self, data: &[u8; 55], phase: &str) -> Result<()> {
let response = self.send_request_with_data(
0xCA,
MATRIX_KB_OPT_WRITE_CONFIG,
MATRIX_KEYBOARD_ID,
0,
0,
data,
)?;
if response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"Invalid response command for matrix keyboard {phase}: 0x{:02X}",
response[1]
)));
}
Ok(())
}
pub fn disable_matrix_keyboard(&mut self) -> Result<()> {
let data = [0u8; 55]; self.send_matrix_kb_config_write(&data, "disable")?;
let actual = self.get_matrix_keyboard_configuration()?;
verify_matrix_kb_config_matches(&actual, 0, 0, &[], &[], false)?;
self.matrix_keyboard.configuration = 0;
Ok(())
}
pub fn get_matrix_keyboard_configuration(&mut self) -> Result<MatrixKeyboardConfig> {
let response =
self.send_request(0xCA, MATRIX_KB_OPT_READ_CONFIG, MATRIX_KEYBOARD_ID, 0, 0)?;
parse_matrix_kb_config_readback(&response)
}
pub fn read_matrix_keyboard(&mut self) -> Result<()> {
let response = self.send_request(0xCA, 20, MATRIX_KEYBOARD_ID, 0, 0)?;
if response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"Invalid response command for matrix keyboard read: 0x{:02X}",
response[1]
)));
}
if response.len() < 24 {
return Err(PoKeysError::Protocol(format!(
"Matrix keyboard status response too short: {} bytes (need 24)",
response.len()
)));
}
self.matrix_keyboard.key_values.fill(0);
for (row, &byte_val) in response[8..24].iter().enumerate() {
for col in 0..8 {
let key_index = row * 8 + col;
if key_index < self.matrix_keyboard.key_values.len() && (byte_val & (1 << col)) != 0
{
self.matrix_keyboard.key_values[key_index] = 1;
}
}
}
Ok(())
}
pub fn set_matrix_keyboard_scanning_decimation(&mut self, decimation: u8) -> Result<()> {
if decimation > 50 {
return Err(PoKeysError::Parameter(format!(
"scanning decimation must be 0-50 (got {decimation})"
)));
}
let mut data = [0u8; 1];
data[0] = decimation;
let response = self.send_request_with_data(0xCA, 50, MATRIX_KEYBOARD_ID, 0, 0, &data)?;
if response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"Invalid response command for scanning decimation: 0x{:02X}",
response[1]
)));
}
self.matrix_keyboard.scanning_decimation = decimation;
Ok(())
}
pub fn set_matrix_keyboard_key_mapping(&mut self) -> Result<()> {
for option in 2u8..=9u8 {
let chunk_base = (option as usize - 2) * 16;
let data = encode_matrix_kb_mapping_chunk(
&self.matrix_keyboard.key_mapping_key_code,
&self.matrix_keyboard.key_mapping_key_modifier,
Some(&self.matrix_keyboard.key_mapping_triggered_key),
chunk_base,
);
let response =
self.send_request_with_data(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0, &data)?;
if response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"Invalid response command for key mapping write (option {option}): 0x{:02X}",
response[1]
)));
}
}
Ok(())
}
pub fn set_matrix_keyboard_key_mapping_up(&mut self) -> Result<()> {
for option in 22u8..=29u8 {
let chunk_base = (option as usize - 22) * 16;
let data = encode_matrix_kb_mapping_chunk(
&self.matrix_keyboard.key_mapping_key_code_up,
&self.matrix_keyboard.key_mapping_key_modifier_up,
None,
chunk_base,
);
let response =
self.send_request_with_data(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0, &data)?;
if response[1] != 0xCA {
return Err(PoKeysError::Protocol(format!(
"Invalid response command for up-key mapping write (option {option}): 0x{:02X}",
response[1]
)));
}
}
Ok(())
}
pub fn get_matrix_keyboard_key_mapping(&mut self) -> Result<()> {
for option in 12u8..=19u8 {
let chunk_base = (option as usize - 12) * 16;
let response = self.send_request(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0)?;
validate_matrix_kb_mapping_response(&response, option)?;
decode_matrix_kb_mapping_chunk(
&response,
&mut self.matrix_keyboard.key_mapping_key_code,
&mut self.matrix_keyboard.key_mapping_key_modifier,
Some(&mut self.matrix_keyboard.key_mapping_triggered_key),
chunk_base,
);
}
Ok(())
}
pub fn get_matrix_keyboard_key_mapping_up(&mut self) -> Result<()> {
for option in 32u8..=39u8 {
let chunk_base = (option as usize - 32) * 16;
let response = self.send_request(0xCA, option, MATRIX_KEYBOARD_ID, 0, 0)?;
validate_matrix_kb_mapping_response(&response, option)?;
decode_matrix_kb_mapping_chunk(
&response,
&mut self.matrix_keyboard.key_mapping_key_code_up,
&mut self.matrix_keyboard.key_mapping_key_modifier_up,
None,
chunk_base,
);
}
Ok(())
}
fn encode_display_size(&self, characters: u8, columns: u8) -> u8 {
(characters & 0x0F) | ((columns & 0x0F) << 4)
}
fn encode_matrix_enabled(&self, config: &crate::matrix::MatrixLedProtocolConfig) -> u8 {
let mut enabled = 0u8;
if config.display1_enabled {
enabled |= 1 << 0;
}
if config.display2_enabled {
enabled |= 1 << 1;
}
enabled
}
pub fn configure_led_matrices(
&mut self,
configs: &[crate::matrix::LedMatrixConfig],
) -> Result<()> {
for config in configs {
if config.matrix_id < 1 || config.matrix_id > 2 {
return Err(PoKeysError::InvalidConfiguration(
"Matrix ID must be 1 or 2".to_string(),
));
}
if config.characters < 1 || config.characters > 8 {
return Err(PoKeysError::InvalidConfiguration(
"Character count must be between 1 and 8 for 7-segment displays".to_string(),
));
}
}
for config in configs {
if let Some(model) = &self.model {
model.validate_led_matrix_config(config)?;
}
}
let mut protocol_config = crate::matrix::MatrixLedProtocolConfig {
display1_enabled: false,
display2_enabled: false,
display1_characters: 1,
display2_characters: 1,
};
for config in configs {
match config.matrix_id {
1 => {
protocol_config.display1_enabled = config.enabled;
protocol_config.display1_characters = config.characters;
}
2 => {
protocol_config.display2_enabled = config.enabled;
protocol_config.display2_characters = config.characters;
}
_ => {
return Err(PoKeysError::InvalidConfiguration(
"Invalid matrix ID".to_string(),
));
}
}
}
self.configure_led_matrix(&protocol_config)?;
if let Some(model) = &mut self.model {
for config in configs {
if config.enabled {
model.reserve_led_matrix_pins(config.matrix_id)?;
}
}
}
Ok(())
}
pub fn configure_servo(&mut self, config: crate::pwm::ServoConfig) -> Result<()> {
if self.pwm.pwm_period == 0 {
self.set_pwm_period(500000)?; }
self.enable_pwm_for_pin(config.pin, true)?;
Ok(())
}
pub fn set_servo_angle(&mut self, config: &crate::pwm::ServoConfig, angle: f32) -> Result<()> {
config.set_angle(self, angle)
}
pub fn set_servo_speed(&mut self, config: &crate::pwm::ServoConfig, speed: f32) -> Result<()> {
config.set_speed(self, speed)
}
pub fn stop_servo(&mut self, config: &crate::pwm::ServoConfig) -> Result<()> {
config.stop(self)
}
}
impl Default for DeviceInfo {
fn default() -> Self {
Self {
pin_count: 0,
pwm_count: 0,
basic_encoder_count: 0,
encoders_count: 0,
fast_encoders: 0,
ultra_fast_encoders: 0,
pwm_internal_frequency: 0,
analog_inputs: 0,
key_mapping: 0,
triggered_key_mapping: 0,
key_repeat_delay: 0,
digital_counters: 0,
joystick_button_axis_mapping: 0,
joystick_analog_to_digital_mapping: 0,
macros: 0,
matrix_keyboard: 0,
matrix_keyboard_triggered_mapping: 0,
lcd: 0,
matrix_led: 0,
connection_signal: 0,
po_ext_bus: 0,
po_net: 0,
analog_filtering: 0,
init_outputs_start: 0,
prot_i2c: 0,
prot_1wire: 0,
additional_options: 0,
load_status: 0,
custom_device_name: 0,
po_tlog27_support: 0,
sensor_list: 0,
web_interface: 0,
fail_safe_settings: 0,
joystick_hat_switch: 0,
pulse_engine: 0,
pulse_engine_v2: 0,
easy_sensors: 0,
}
}
}
impl Default for RealTimeClock {
fn default() -> Self {
Self {
sec: 0,
min: 0,
hour: 0,
dow: 0,
dom: 0,
tmp: 0,
doy: 0,
month: 0,
year: 0,
}
}
}
fn check_pokeys57cnc_pin_capability(pin: u32, capability: crate::io::PinCapability) -> bool {
use crate::io::PinCapability;
match pin {
1..=2 => matches!(
capability,
PinCapability::DigitalInput
| PinCapability::DigitalOutput
| PinCapability::DigitalCounter
| PinCapability::FastEncoder1A
),
3..=6 => matches!(
capability,
PinCapability::DigitalInput
| PinCapability::DigitalCounter
| PinCapability::FastEncoder1A
),
8 | 12..=13 => matches!(
capability,
PinCapability::DigitalInput | PinCapability::DigitalOutput
),
9..=11 | 15..=16 => matches!(
capability,
PinCapability::DigitalInput | PinCapability::DigitalCounter
),
14 => matches!(capability, PinCapability::DigitalInput),
17..=18 | 20..=21 => matches!(
capability,
PinCapability::DigitalOutput | PinCapability::PwmOutput
),
19 => matches!(
capability,
PinCapability::DigitalInput | PinCapability::DigitalCounter
),
_ => false,
}
}
static USB_DEVICE_LIST: LazyLock<Mutex<Vec<UsbDeviceInfo>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
#[allow(dead_code)]
static NETWORK_DEVICE_LIST: LazyLock<Mutex<Vec<NetworkDeviceSummary>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct UsbDeviceInfo {
vendor_id: u16,
product_id: u16,
serial_number: Option<String>,
path: String,
}
pub fn enumerate_usb_devices() -> Result<i32> {
let mut device_list = USB_DEVICE_LIST
.lock()
.map_err(|_| PoKeysError::InternalError("Failed to lock USB device list".to_string()))?;
device_list.clear();
#[cfg(target_os = "macos")]
{
enumerate_usb_devices_macos(&mut device_list)
}
#[cfg(target_os = "linux")]
{
enumerate_usb_devices_linux(&mut device_list)
}
#[cfg(target_os = "windows")]
{
enumerate_usb_devices_windows(&mut device_list)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(PoKeysError::NotSupported)
}
}
pub fn enumerate_network_devices(timeout_ms: u32) -> Result<Vec<NetworkDeviceSummary>> {
use crate::network::discover_all_devices;
log::info!("Enumerating network devices with timeout {timeout_ms}ms");
discover_all_devices(timeout_ms)
}
pub fn connect_to_device(device_index: u32) -> Result<PoKeysDevice> {
let device_list = USB_DEVICE_LIST
.lock()
.map_err(|_| PoKeysError::InternalError("Failed to lock USB device list".to_string()))?;
if device_index as usize >= device_list.len() {
return Err(PoKeysError::Parameter("Invalid device index".to_string()));
}
let device_info = device_list[device_index as usize].clone();
drop(device_list);
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
#[cfg(target_os = "macos")]
{
device.usb_interface = Some(Box::new(connect_usb_device_macos(&device_info.path)?));
}
#[cfg(target_os = "linux")]
{
device.usb_interface = Some(Box::new(connect_usb_device_linux(&device_info.path)?));
}
#[cfg(target_os = "windows")]
{
device.usb_interface = Some(Box::new(connect_usb_device_windows(&device_info.path)?));
}
device.get_device_data()?;
Ok(device)
}
pub fn connect_to_device_with_serial(
serial_number: u32,
check_network: bool,
timeout_ms: u32,
) -> Result<PoKeysDevice> {
if check_network {
let network_devices = enumerate_network_devices(timeout_ms)?;
for network_device in network_devices {
if network_device.serial_number == serial_number {
return connect_to_network_device(&network_device);
}
}
}
let usb_count = enumerate_usb_devices()?;
for i in 0..usb_count {
if let Ok(device) = connect_to_device(i as u32) {
if device.device_data.serial_number == serial_number {
return Ok(device);
}
}
}
Err(PoKeysError::CannotConnect)
}
pub fn connect_to_network_device(device_summary: &NetworkDeviceSummary) -> Result<PoKeysDevice> {
let mut device = PoKeysDevice::new(DeviceConnectionType::NetworkDevice);
if device_summary.use_udp != 0 {
device.connection_param = ConnectionParam::Udp;
} else {
device.connection_param = ConnectionParam::Tcp;
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
{
device.network_interface = Some(connect_network_device(device_summary)?);
}
device.get_device_data()?;
Ok(device)
}
struct StubUsbInterface;
impl UsbHidInterface for StubUsbInterface {
fn write(&mut self, _data: &[u8]) -> Result<usize> {
Err(PoKeysError::NotSupported)
}
fn read(&mut self, _buffer: &mut [u8]) -> Result<usize> {
Err(PoKeysError::NotSupported)
}
fn read_timeout(&mut self, _buffer: &mut [u8], _timeout: Duration) -> Result<usize> {
Err(PoKeysError::NotSupported)
}
}
#[cfg(target_os = "macos")]
fn enumerate_usb_devices_macos(device_list: &mut Vec<UsbDeviceInfo>) -> Result<i32> {
use std::process::Command;
let output = Command::new("system_profiler")
.args(["SPUSBDataType", "-xml"])
.output()
.map_err(|e| PoKeysError::InternalError(format!("Failed to run system_profiler: {}", e)))?;
if !output.status.success() {
return Err(PoKeysError::InternalError(
"system_profiler command failed".to_string(),
));
}
let output_str = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = output_str.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.contains("<key>vendor_id</key>") && i + 1 < lines.len() {
let next_line = lines[i + 1].trim();
if next_line.contains("<integer>7619</integer>")
|| next_line.contains("<string>0x1dc3</string>")
{
let mut product_id = 0u16;
let mut serial_number = None;
let mut location_id = String::new();
for j in (i + 2)..(i + 50).min(lines.len()) {
let search_line = lines[j].trim();
if search_line.contains("<key>product_id</key>") && j + 1 < lines.len() {
let product_line = lines[j + 1].trim();
if let Some(start) = product_line.find("<integer>") {
if let Some(end) = product_line.find("</integer>") {
if let Ok(pid) = product_line[start + 9..end].parse::<u16>() {
product_id = pid;
}
}
}
}
if search_line.contains("<key>serial_num</key>") && j + 1 < lines.len() {
let serial_line = lines[j + 1].trim();
if let Some(start) = serial_line.find("<string>") {
if let Some(end) = serial_line.find("</string>") {
serial_number = Some(serial_line[start + 8..end].to_string());
}
}
}
if search_line.contains("<key>location_id</key>") && j + 1 < lines.len() {
let location_line = lines[j + 1].trim();
if let Some(start) = location_line.find("<string>") {
if let Some(end) = location_line.find("</string>") {
location_id = location_line[start + 8..end].to_string();
}
}
}
if search_line.contains("<key>vendor_id</key>") && j > i + 2 {
break;
}
}
device_list.push(UsbDeviceInfo {
vendor_id: 0x1DC3,
product_id,
serial_number,
path: format!("macos_usb_{}", location_id),
});
}
}
i += 1;
}
Ok(device_list.len() as i32)
}
#[cfg(target_os = "linux")]
fn enumerate_usb_devices_linux(device_list: &mut Vec<UsbDeviceInfo>) -> Result<i32> {
Ok(device_list.len() as i32)
}
#[cfg(target_os = "windows")]
fn enumerate_usb_devices_windows(device_list: &mut Vec<UsbDeviceInfo>) -> Result<i32> {
Ok(device_list.len() as i32)
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
fn connect_network_device(
device_summary: &NetworkDeviceSummary,
) -> Result<Box<dyn NetworkInterface>> {
use crate::network::{TcpNetworkInterface, UdpNetworkInterface};
if device_summary.use_udp != 0 {
let interface = UdpNetworkInterface::new(device_summary.ip_address, 20055)?;
Ok(Box::new(interface))
} else {
let interface = TcpNetworkInterface::new(device_summary.ip_address, 20055)?;
Ok(Box::new(interface))
}
}
#[cfg(target_os = "macos")]
fn connect_usb_device_macos(_path: &str) -> Result<StubUsbInterface> {
Err(PoKeysError::NotSupported)
}
#[cfg(target_os = "linux")]
fn connect_usb_device_linux(_path: &str) -> Result<StubUsbInterface> {
Err(PoKeysError::NotSupported)
}
#[cfg(target_os = "windows")]
fn connect_usb_device_windows(_path: &str) -> Result<StubUsbInterface> {
Err(PoKeysError::NotSupported)
}
fn parse_system_load_response(response: &[u8]) -> u8 {
response[2]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_device_creation() {
let device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
assert_eq!(device.connection_type, DeviceConnectionType::UsbDevice);
assert_eq!(device.pins.len(), 0); }
#[test]
fn test_parse_system_load_response() {
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
response[1] = 0x05; response[2] = 42; assert_eq!(parse_system_load_response(&response), 42);
response[2] = 0;
assert_eq!(parse_system_load_response(&response), 0);
response[2] = 100;
assert_eq!(parse_system_load_response(&response), 100);
}
#[test]
fn test_encode_matrix_kb_config_payload_layout_enabled() {
let row_pins: Vec<u8> = (1..=16).collect();
let col_pins = vec![21u8, 22, 23, 24];
let data = encode_matrix_kb_config_payload(true, 4, 16, &col_pins, &row_pins);
assert_eq!(data[0], 1);
assert_eq!(data[1], 0x3F);
for i in 0..8 {
assert_eq!(data[2 + i], i as u8, "row {} (low half)", i);
}
for i in 0..4 {
assert_eq!(data[10 + i], 20 + i as u8, "column {}", i);
}
assert!(
data[18..34].iter().all(|&b| b == 0),
"direct/macro bitmap must be zero by default"
);
for i in 0..8 {
assert_eq!(
data[34 + i],
(8 + i) as u8,
"row {} (extended; high half)",
8 + i
);
}
assert_eq!(data[42], 0);
}
#[test]
fn test_encode_matrix_kb_config_payload_layout_disabled() {
let row_pins: Vec<u8> = (1..=16).collect();
let col_pins = vec![21u8, 22, 23, 24];
let data = encode_matrix_kb_config_payload(false, 4, 16, &col_pins, &row_pins);
assert_eq!(data[0], 0, "enable bit must be cleared");
assert_eq!(data[1], 0x3F, "size byte unchanged from activated payload");
for i in 0..8 {
assert_eq!(data[2 + i], i as u8);
}
for i in 0..8 {
assert_eq!(data[34 + i], (8 + i) as u8);
}
}
#[test]
fn test_encode_matrix_kb_config_payload_extended_rows_unconditional() {
let row_pins = vec![1u8, 2, 3, 4]; let col_pins = vec![21u8, 22, 23, 24];
let data = encode_matrix_kb_config_payload(true, 4, 4, &col_pins, &row_pins);
assert!(data[34..42].iter().all(|&b| b == 0));
let long_rows: Vec<u8> = (1..=16).collect();
let data2 = encode_matrix_kb_config_payload(true, 4, 4, &col_pins, &long_rows);
for i in 0..8 {
assert_eq!(
data2[34 + i],
(8 + i) as u8,
"extended row pin {i} should be written verbatim"
);
}
}
#[test]
fn test_encode_matrix_kb_mapping_chunk_writes_codes_modifiers_and_triggered_bitmap() {
let codes: Vec<u8> = (0..128).map(|i| i as u8).collect();
let modifiers: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(2)).collect();
let mut triggered = vec![0u8; 128];
triggered[33] = 1;
triggered[47] = 99;
let chunk = encode_matrix_kb_mapping_chunk(&codes, &modifiers, Some(&triggered), 32);
for i in 0..16 {
assert_eq!(chunk[i], (32 + i) as u8, "code {i}");
assert_eq!(chunk[16 + i], (32 + i) as u8 * 2, "modifier {i}");
}
let bitmap = u16::from_le_bytes([chunk[32], chunk[33]]);
assert_eq!(bitmap, (1 << 1) | (1 << 15));
}
#[test]
fn test_encode_matrix_kb_mapping_chunk_omits_bitmap_when_no_triggered() {
let codes = vec![0u8; 128];
let modifiers = vec![0u8; 128];
let chunk = encode_matrix_kb_mapping_chunk(&codes, &modifiers, None, 0);
assert_eq!(chunk[32], 0);
assert_eq!(chunk[33], 0);
}
#[test]
fn test_decode_matrix_kb_mapping_chunk_roundtrip() {
let codes_in: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_add(7)).collect();
let modifiers_in: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(3)).collect();
let mut triggered_in = vec![0u8; 128];
triggered_in[5] = 1;
triggered_in[20] = 1;
let chunk =
encode_matrix_kb_mapping_chunk(&codes_in, &modifiers_in, Some(&triggered_in), 16);
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
response[1] = 0xCA;
response[8..8 + 16].copy_from_slice(&chunk[0..16]); response[24..24 + 16].copy_from_slice(&chunk[16..32]); response[40] = chunk[32];
response[41] = chunk[33];
let mut codes_out = vec![0u8; 128];
let mut modifiers_out = vec![0u8; 128];
let mut triggered_out = vec![0u8; 128];
decode_matrix_kb_mapping_chunk(
&response,
&mut codes_out,
&mut modifiers_out,
Some(&mut triggered_out),
16,
);
for i in 16..32 {
assert_eq!(codes_out[i], codes_in[i], "code {i}");
assert_eq!(modifiers_out[i], modifiers_in[i], "modifier {i}");
}
assert_eq!(triggered_out[20], 1);
assert_eq!(triggered_out[16], 0);
assert_eq!(codes_out[0], 0);
assert_eq!(codes_out[40], 0);
}
#[test]
fn test_validate_matrix_kb_mapping_response_rejects_wrong_command() {
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
response[1] = 0xC9; assert!(validate_matrix_kb_mapping_response(&response, 12).is_err());
}
#[test]
fn test_validate_matrix_kb_mapping_response_rejects_short_response() {
let mut response = [0u8; 30]; response[1] = 0xCA;
assert!(validate_matrix_kb_mapping_response(&response, 12).is_err());
}
#[test]
fn test_validate_matrix_kb_mapping_response_accepts_valid() {
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
response[1] = 0xCA;
assert!(validate_matrix_kb_mapping_response(&response, 12).is_ok());
}
#[test]
fn test_configure_matrix_keyboard_rejects_pin_in_both_row_and_col() {
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
let cols = [5u8, 6, 7, 8];
let rows = [1u8, 2, 3, 5]; let err = device
.configure_matrix_keyboard(4, 4, &cols, &rows)
.expect_err("must reject overlapping pin assignment");
match err {
PoKeysError::Parameter(msg) => {
assert!(
msg.contains("Pin 5"),
"error must name the offending pin, got: {msg}"
);
}
other => panic!("expected Parameter error, got {other:?}"),
}
}
#[test]
fn test_configure_matrix_keyboard_rejects_pin_zero() {
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
let cols = [5u8, 6, 7, 8];
let rows = [0u8, 2, 3, 4]; let err = device
.configure_matrix_keyboard(4, 4, &cols, &rows)
.expect_err("must reject pin 0");
assert!(matches!(err, PoKeysError::Parameter(_)));
}
#[test]
fn test_configure_matrix_keyboard_rejects_pin_above_55() {
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
let cols = [5u8, 6, 7, 8];
let rows = [1u8, 2, 3, 56]; let err = device
.configure_matrix_keyboard(4, 4, &cols, &rows)
.expect_err("must reject pin 56");
assert!(matches!(err, PoKeysError::Parameter(_)));
}
#[test]
fn test_configure_matrix_keyboard_only_validates_used_pins() {
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
let cols = [5u8, 6, 7, 8, 0, 99, 99, 99]; let rows = [1u8, 2, 3, 4, 0, 99, 99, 99];
let err = device
.configure_matrix_keyboard(4, 4, &cols, &rows)
.expect_err("expected NotConnected, not Parameter");
assert!(
!matches!(err, PoKeysError::Parameter(_)),
"validation should have accepted these pins; got Parameter error: {err:?}"
);
}
fn make_matrix_kb_readback(
enabled: bool,
width: u8,
height: u8,
rows: &[u8],
cols: &[u8],
) -> [u8; RESPONSE_BUFFER_SIZE] {
let mut r = [0u8; RESPONSE_BUFFER_SIZE];
r[1] = 0xCA;
r[8] = if enabled { 1 } else { 0 };
r[9] = ((width.saturating_sub(1)) << 4) | (height.saturating_sub(1));
for (i, &p) in rows.iter().enumerate().take(8) {
r[10 + i] = p.saturating_sub(1);
}
for (i, &p) in cols.iter().enumerate().take(8) {
r[18 + i] = p.saturating_sub(1);
}
for (offset, &p) in rows.iter().enumerate().skip(8).take(8) {
r[42 + (offset - 8)] = p.saturating_sub(1);
}
r
}
#[test]
fn test_parse_matrix_kb_config_readback_full() {
let rows: Vec<u8> = (1..=16).collect();
let cols = [17u8, 18, 19, 20, 21, 22, 23, 24];
let mut r = make_matrix_kb_readback(true, 8, 16, &rows, &cols);
r[26] = 0b0000_0101; r[50] = 7; r[51] = 25; let cfg = parse_matrix_kb_config_readback(&r).expect("parse");
assert!(cfg.enabled);
assert_eq!(cfg.width, 8);
assert_eq!(cfg.height, 16);
for i in 0..16 {
assert_eq!(cfg.row_pins[i], (i + 1) as u8);
}
for i in 0..8 {
assert_eq!(cfg.column_pins[i], (17 + i) as u8);
}
assert_eq!(cfg.direct_macro_bitmap[0], 0b0000_0101);
assert_eq!(cfg.alternate_function_pin, 7);
assert_eq!(cfg.scanning_decimation, 25);
assert_eq!(cfg.size_byte(), 0x7F);
}
#[test]
fn test_parse_matrix_kb_config_readback_rejects_short_response() {
let mut r = [0u8; 30];
r[1] = 0xCA;
let err = parse_matrix_kb_config_readback(&r).expect_err("must reject");
assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("readback too short")));
}
#[test]
fn test_parse_matrix_kb_config_readback_rejects_wrong_command_echo() {
let mut r = [0u8; RESPONSE_BUFFER_SIZE];
r[1] = 0xC9;
let err = parse_matrix_kb_config_readback(&r).expect_err("must reject");
assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("echo mismatch")));
}
#[test]
fn test_verify_matches_accepts_matching_4x4() {
let rows = [1u8, 2, 3, 4];
let cols = [5u8, 6, 7, 8];
let r = make_matrix_kb_readback(true, 4, 4, &rows, &cols);
let cfg = parse_matrix_kb_config_readback(&r).unwrap();
verify_matrix_kb_config_matches(&cfg, 4, 4, &cols, &rows, true).expect("must accept");
}
#[test]
fn test_verify_matches_rejects_disabled_when_expecting_enabled() {
let rows = [1u8, 2, 3, 4];
let cols = [5u8, 6, 7, 8];
let r = make_matrix_kb_readback(false, 4, 4, &rows, &cols);
let cfg = parse_matrix_kb_config_readback(&r).unwrap();
let err = verify_matrix_kb_config_matches(&cfg, 4, 4, &cols, &rows, true)
.expect_err("must reject silent rejection");
match err {
PoKeysError::Protocol(msg) => assert!(
msg.contains("enabled=false") && msg.contains("locked"),
"error must hint at locked state, got: {msg}"
),
other => panic!("expected Protocol error, got {other:?}"),
}
}
#[test]
fn test_verify_matches_disabled_path_skips_size_and_pin_checks() {
let mut r = [0u8; RESPONSE_BUFFER_SIZE];
r[1] = 0xCA;
r[8] = 0; r[9] = 0xFF; let cfg = parse_matrix_kb_config_readback(&r).unwrap();
verify_matrix_kb_config_matches(&cfg, 0, 0, &[], &[], false).expect("must accept");
}
#[test]
fn test_verify_matches_rejects_wrong_size() {
let rows = [1u8, 2, 3, 4];
let cols = [5u8, 6, 7, 8];
let r = make_matrix_kb_readback(true, 4, 4, &rows, &cols);
let cfg = parse_matrix_kb_config_readback(&r).unwrap();
let err = verify_matrix_kb_config_matches(&cfg, 8, 4, &cols, &rows, true)
.expect_err("must reject size mismatch");
assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("width=4")));
}
#[test]
fn test_verify_matches_rejects_wrong_row_pin() {
let rows = [1u8, 2, 3, 4];
let cols = [5u8, 6, 7, 8];
let mut r = make_matrix_kb_readback(true, 4, 4, &rows, &cols);
r[12] = 99; let cfg = parse_matrix_kb_config_readback(&r).unwrap();
let err = verify_matrix_kb_config_matches(&cfg, 4, 4, &cols, &rows, true)
.expect_err("must reject pin mismatch");
assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("row pin 2")));
}
#[test]
fn test_verify_matches_rejects_wrong_extended_row_pin() {
let rows: Vec<u8> = (1..=12).collect();
let cols = [13u8, 14, 15, 16];
let mut r = make_matrix_kb_readback(true, 4, 12, &rows, &cols);
r[44] = 99; let cfg = parse_matrix_kb_config_readback(&r).unwrap();
let err = verify_matrix_kb_config_matches(&cfg, 4, 12, &cols, &rows, true)
.expect_err("must reject extended row pin mismatch");
assert!(matches!(err, PoKeysError::Protocol(msg) if msg.contains("extended row pin 10")));
}
#[test]
fn test_parse_device_data_populates_lock_status() {
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
response[8..12].copy_from_slice(b"PK58");
response[4] = 0x12; response[5] = 3;
response[58] = 1; device
.parse_device_data_response(&response)
.expect("parse must succeed");
assert_eq!(device.device_data.device_lock_status, 1);
assert!(device.device_data.device_locked());
}
#[test]
fn test_parse_device_data_lock_status_zero_means_unlocked() {
let mut device = PoKeysDevice::new(DeviceConnectionType::UsbDevice);
let mut response = [0u8; RESPONSE_BUFFER_SIZE];
response[8..12].copy_from_slice(b"PKEx");
response[4] = 0x10;
response[5] = 1;
response[58] = 0;
device
.parse_device_data_response(&response)
.expect("parse must succeed");
assert!(!device.device_data.device_locked());
}
#[test]
fn test_pin_capability_checking() {
assert!(check_pokeys57cnc_pin_capability(
1,
crate::io::PinCapability::DigitalInput
));
assert!(check_pokeys57cnc_pin_capability(
1,
crate::io::PinCapability::DigitalOutput
));
assert!(!check_pokeys57cnc_pin_capability(
14,
crate::io::PinCapability::DigitalOutput
)); assert!(!check_pokeys57cnc_pin_capability(
100,
crate::io::PinCapability::DigitalInput
)); }
#[cfg(feature = "hardware-tests")]
mod hardware_tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_hardware_device_enumeration() {
println!("Testing hardware device enumeration...");
match enumerate_usb_devices() {
Ok(count) => {
println!("Found {} USB PoKeys devices", count);
if count == 0 {
println!(
"WARNING: No PoKeys devices found. Connect a device to run hardware tests."
);
}
}
Err(e) => {
panic!("Failed to enumerate USB devices: {}", e);
}
}
}
#[test]
fn test_hardware_device_connection() {
println!("Testing hardware device connection...");
let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
if device_count == 0 {
println!("SKIP: No PoKeys devices found for hardware test");
return;
}
match connect_to_device(0) {
Ok(mut device) => {
println!("Successfully connected to device");
match device.get_device_data() {
Ok(_) => {
println!("Device Serial: {}", device.device_data.serial_number);
println!(
"Firmware: {}.{}",
device.device_data.firmware_version_major,
device.device_data.firmware_version_minor
);
println!("Pin Count: {}", device.info.pin_count);
}
Err(e) => {
println!("WARNING: Could not get device data: {}", e);
}
}
}
Err(e) => {
panic!("Failed to connect to device: {}", e);
}
}
}
#[test]
fn test_hardware_digital_io() {
println!("Testing hardware digital I/O...");
let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
if device_count == 0 {
println!("SKIP: No PoKeys devices found for hardware test");
return;
}
let mut device = connect_to_device(0).expect("Failed to connect to device");
device.get_device_data().expect("Failed to get device data");
match device.set_pin_function(1, crate::io::PinFunction::DigitalOutput) {
Ok(_) => {
println!("Successfully configured pin 1 as digital output");
for state in [true, false, true, false] {
match device.set_digital_output(1, state) {
Ok(_) => {
println!("Set pin 1 to {}", if state { "HIGH" } else { "LOW" })
}
Err(e) => println!("WARNING: Failed to set digital output: {}", e),
}
thread::sleep(Duration::from_millis(100));
}
}
Err(e) => {
println!("WARNING: Could not configure pin function: {}", e);
}
}
}
#[test]
fn test_hardware_analog_input() {
println!("Testing hardware analog input...");
let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
if device_count == 0 {
println!("SKIP: No PoKeys devices found for hardware test");
return;
}
let mut device = connect_to_device(0).expect("Failed to connect to device");
device.get_device_data().expect("Failed to get device data");
match device.get_analog_input(1) {
Ok(value) => {
println!("Analog input 1 value: {}", value);
assert!(value <= 4095, "Analog value out of range for 12-bit ADC");
}
Err(e) => {
println!("WARNING: Could not read analog input: {}", e);
}
}
}
#[test]
fn test_hardware_pwm_output() {
println!("Testing hardware PWM output...");
let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
if device_count == 0 {
println!("SKIP: No PoKeys devices found for hardware test");
return;
}
let mut device = connect_to_device(0).expect("Failed to connect to device");
device.get_device_data().expect("Failed to get device data");
match device.set_pwm_period(25000) {
Ok(_) => {
println!("Set PWM period to 25000 cycles");
for duty in [25.0, 50.0, 75.0, 0.0] {
match device.set_pwm_duty_cycle_percent_for_pin(17, duty) {
Ok(_) => {
println!("Set PWM duty cycle to {}%", duty);
thread::sleep(Duration::from_millis(200));
}
Err(e) => {
println!("WARNING: Could not set PWM duty cycle: {}", e);
}
}
}
}
Err(e) => {
println!("WARNING: Could not configure PWM: {}", e);
}
}
}
#[test]
fn test_hardware_encoder_reading() {
println!("Testing hardware encoder reading...");
let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
if device_count == 0 {
println!("SKIP: No PoKeys devices found for hardware test");
return;
}
let mut device = connect_to_device(0).expect("Failed to connect to device");
device.get_device_data().expect("Failed to get device data");
let mut options = crate::encoders::EncoderOptions::new();
options.enabled = true;
options.sampling_4x = true;
match device.configure_encoder(0, 1, 2, options) {
Ok(_) => {
println!("Configured encoder 0 on pins 1 and 2");
for i in 0..5 {
match device.get_encoder_value(0) {
Ok(value) => {
println!("Encoder 0 reading {}: {}", i + 1, value);
}
Err(e) => {
println!("WARNING: Could not read encoder: {}", e);
}
}
thread::sleep(Duration::from_millis(100));
}
}
Err(e) => {
println!("WARNING: Could not configure encoder: {}", e);
}
}
}
#[test]
fn test_hardware_network_discovery() {
println!("Testing hardware network device discovery...");
match enumerate_network_devices(2000) {
Ok(devices) => {
println!("Found {} network PoKeys devices", devices.len());
for (i, device) in devices.iter().enumerate() {
println!(
"Network device {}: Serial {}, IP {}.{}.{}.{}",
i + 1,
device.serial_number,
device.ip_address[0],
device.ip_address[1],
device.ip_address[2],
device.ip_address[3]
);
}
if devices.is_empty() {
println!(
"No network devices found - this is normal if no network PoKeys devices are present"
);
}
}
Err(e) => {
println!("WARNING: Network discovery failed: {}", e);
}
}
}
#[test]
fn test_hardware_device_info_validation() {
println!("Testing hardware device info validation...");
let device_count = enumerate_usb_devices().expect("Failed to enumerate devices");
if device_count == 0 {
println!("SKIP: No PoKeys devices found for hardware test");
return;
}
let mut device = connect_to_device(0).expect("Failed to connect to device");
device.get_device_data().expect("Failed to get device data");
assert!(
device.device_data.serial_number > 0,
"Serial number should be non-zero"
);
assert!(device.info.pin_count > 0, "Pin count should be non-zero");
assert!(
device.info.pin_count <= 100,
"Pin count should be reasonable"
);
println!("Device validation passed:");
println!(" Serial: {}", device.device_data.serial_number);
println!(" Pins: {}", device.info.pin_count);
println!(" PWM Channels: {}", device.info.pwm_count);
println!(" Encoders: {}", device.info.encoders_count);
println!(" Analog Inputs: {}", device.info.analog_inputs);
}
}
}