From bd9f94c244672cd87851b20dd3ef8675acf7d9ad Mon Sep 17 00:00:00 2001 From: raven <7156279+RavenX8@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:26:52 -0400 Subject: [PATCH] Added the ability to support different firmware report versions --- Cargo.lock | 64 ++++++- Cargo.toml | 1 + src/device.rs | 2 +- src/hid_worker.rs | 454 ++++++++++++++++++++++++++++++---------------- src/main.rs | 18 +- src/util.rs | 235 +++++++++++++++++++++++- 6 files changed, 598 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f55f798..875b7ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ "paste", "static_assertions", "windows", - "windows-core", + "windows-core 0.58.0", ] [[package]] @@ -165,6 +165,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -650,6 +656,20 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.27" @@ -1523,6 +1543,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2712,6 +2756,7 @@ dependencies = [ name = "shift_tool" version = "0.4.0" dependencies = [ + "chrono", "clap", "dirs", "eframe", @@ -3481,7 +3526,16 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ "windows-targets 0.52.6", ] @@ -3520,6 +3574,12 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-result" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index a81782e..8956bd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ hidapi = "2.6.1" log = "0.4.21" serde = { version = "1.0.197", features = ["derive"] } dirs = { version = "6.0.0", features = [] } +chrono = "0.4.40" [features] diff --git a/src/device.rs b/src/device.rs index 8dad19c..a47017a 100644 --- a/src/device.rs +++ b/src/device.rs @@ -192,7 +192,7 @@ impl crate::ShiftTool { .position(|d| { d.vendor_id == saved_device.vendor_id && d.product_id == saved_device.product_id - && d.serial_number == saved_device.serial_number + // && d.serial_number == saved_device.serial_number }) .unwrap_or(0) // Default to index 0 ("No Connection") if not found } diff --git a/src/hid_worker.rs b/src/hid_worker.rs index 417f8f6..319f143 100644 --- a/src/hid_worker.rs +++ b/src/hid_worker.rs @@ -1,7 +1,8 @@ use crate::config::{ModifiersArray}; use crate::device::SavedDevice; use crate::{SharedDeviceState, SharedStateFlag}; // Import shared types -use crate::util::{merge_u8_into_u16, read_bit, set_bit}; +use crate::util::{self, merge_u8_into_u16, read_bit, set_bit, ReportFormat, MAX_REPORT_SIZE}; +use log::{debug, error, info, trace, warn}; use hidapi::{HidApi, HidDevice, HidError}; use std::{ sync::{Arc, Condvar, Mutex}, @@ -10,17 +11,22 @@ use std::{ }; // Constants for HID communication -const FEATURE_REPORT_ID: u8 = 4; -const REPORT_BUFFER_SIZE: usize = 19; // 1 byte ID + 64 bytes data pub const VENDOR_ID_FILTER: u16 = 0x3344; // Assuming Virpil VID const WORKER_SLEEP_MS: u64 = 100; // Reduced sleep time for better responsiveness + +#[derive(Clone)] +struct DeviceWorkerInfo { + config: SavedDevice, + format: ReportFormat, +} + // Structure to hold data passed to the worker thread // Clone Arcs for shared state, clone config data needed struct WorkerData { run_state: SharedStateFlag, - sources_config: Vec, - receivers_config: Vec, + sources_info: Vec, + receivers_info: Vec, shift_modifiers: ModifiersArray, source_states_shared: Vec, receiver_states_shared: Vec, @@ -31,13 +37,97 @@ struct WorkerData { // Now part of ShiftTool impl block impl crate::ShiftTool { pub(crate) fn spawn_worker(&mut self) -> bool { - log::info!("Attempting to spawn HID worker thread..."); + info!("Attempting to spawn HID worker thread..."); + + let mut sources_info: Vec = Vec::new(); + for (i, source_config) in self.config.data.sources.iter().enumerate() { + + // 1. Find the corresponding VpcDevice in the current device_list + // This is needed to get the firmware string. + let device_idx = crate::device::find_device_index_for_saved( + &self.device_list, // The list of currently detected devices + source_config, // The config for the i-th source slot + ); + + // 2. Get the firmware string from the found VpcDevice + let firmware_str = if device_idx != 0 && device_idx < self.device_list.len() { + // Successfully found the device in the current list + self.device_list[device_idx].firmware.to_string() // Access the firmware field + } else { + // Device not found (index 0 is default/placeholder) or list issue + warn!("Source device {} not found in current list for format determination.", i); + "".to_string() // Use empty string if not found + }; + + let name_str = if device_idx != 0 && device_idx < self.device_list.len() { + // Successfully found the device in the current list + self.device_list[device_idx].name.to_string() // Access the firmware field + } else { + // Device not found (index 0 is default/placeholder) or list issue + warn!("Source device {} not found in current list for format determination.", i); + "".to_string() // Use empty string if not found + }; + + // 3. Call determine_report_format with the firmware string + // This function (from src/util.rs) contains the logic + // to check dates or patterns and return the correct format. + let determined_format: ReportFormat = util::determine_report_format(&name_str, &firmware_str); + + // 4. Log the result for debugging + info!( + "Determined report format {:?} for source {} (Firmware: '{}')", + determined_format, // Log the whole struct (uses Debug derive) + i, + firmware_str + ); + + // 5. Store the result along with the config in DeviceWorkerInfo + sources_info.push(DeviceWorkerInfo { + config: source_config.clone(), // Clone the config part + format: determined_format, // Store the determined format + }); + } + + let mut receivers_info: Vec = Vec::new(); + for (i, receiver_config) in self.config.data.receivers.iter().enumerate() { + let device_idx = crate::device::find_device_index_for_saved( + &self.device_list, + receiver_config, + ); + let firmware_str = if device_idx != 0 && device_idx < self.device_list.len() { + self.device_list[device_idx].firmware.to_string() + } else { + warn!("Receiver device {} not found in current list for format determination.", i); + "".to_string() + }; + let name_str = if device_idx != 0 && device_idx < self.device_list.len() { + self.device_list[device_idx].name.to_string() + } else { + warn!("Receiver device {} not found in current list for format determination.", i); + "".to_string() + }; + + let determined_format: ReportFormat = util::determine_report_format(&name_str, &firmware_str); + + info!( + "Determined report format {:?} for receiver {} (Firmware: '{}')", + determined_format, + i, + firmware_str + ); + + receivers_info.push(DeviceWorkerInfo { + config: receiver_config.clone(), + format: determined_format, + }); + } + // Clone data needed by the thread let worker_data = WorkerData { run_state: self.thread_state.clone(), - sources_config: self.config.data.sources.clone(), - receivers_config: self.config.data.receivers.clone(), + sources_info, + receivers_info, shift_modifiers: self.config.data.shift_modifiers, // Copy (it's Copy) source_states_shared: self.source_states.clone(), receiver_states_shared: self.receiver_states.clone(), @@ -49,27 +139,27 @@ impl crate::ShiftTool { // Create HidApi instance *within* the thread match HidApi::new() { // Use new() which enumerates internally Ok(hidapi) => { - log::info!("HidApi created successfully in worker thread."); + info!("HidApi created successfully in worker thread."); // Filter devices *within* the thread if needed, though opening by VID/PID/SN is primary // hidapi.add_devices(VENDOR_ID_FILTER, 0).ok(); // Optional filtering run_hid_worker_loop(hidapi, worker_data); } Err(e) => { - log::error!("Failed to create HidApi in worker thread: {}", e); + error!("Failed to create HidApi in worker thread: {}", e); // How to signal failure back? Could use another shared state. // For now, thread just exits. } } }); - log::info!("HID worker thread spawn initiated."); + info!("HID worker thread spawn initiated."); true // Indicate spawn attempt was made } // Cleanup actions when the worker is stopped from the UI pub(crate) fn stop_worker_cleanup(&mut self) { - log::info!("Performing worker stop cleanup..."); + info!("Performing worker stop cleanup..."); // Reset shared states displayed in the UI let reset_state = |state_arc: &SharedDeviceState| { if let Ok(mut state) = state_arc.lock() { @@ -86,45 +176,67 @@ impl crate::ShiftTool { for device in self.device_list.iter_mut() { device.active = false; } - log::info!("Worker stop cleanup finished."); + info!("Worker stop cleanup finished."); } } -// Helper to open devices, returns Result for better error handling +/// Opens HID devices based on the provided configuration and format info. +/// +/// Iterates through the `device_infos`, attempts to open each device using +/// VID, PID, and Serial Number from the `config` field. Sets non-blocking mode. +/// +/// Returns a Vec where each element corresponds to an input `DeviceWorkerInfo`. +/// Contains `Some(HidDevice)` on success, or `None` if the device couldn't be +/// opened, wasn't configured (VID/PID=0), or failed to set non-blocking mode. fn open_hid_devices( hidapi: &HidApi, - device_configs: &[SavedDevice], -) -> Vec> { // Return Option to represent open failures - let mut devices = Vec::with_capacity(device_configs.len()); - for config in device_configs { + device_infos: &[DeviceWorkerInfo], // Accepts a slice of the new struct +) -> Vec> { + let mut devices = Vec::with_capacity(device_infos.len()); + + // Iterate through the DeviceWorkerInfo structs + for (i, info) in device_infos.iter().enumerate() { + // Use info.config to get the device identifiers + let config = &info.config; + + // Skip if device is not configured (VID/PID are zero) if config.vendor_id == 0 || config.product_id == 0 { - log::warn!("Skipping device with zero VID/PID in config."); - devices.push(None); // Placeholder for unconfigured/invalid device + log::trace!("Skipping opening device slot {} (unconfigured).", i); + devices.push(None); // Placeholder for unconfigured slot continue; } + + // Attempt to open the device match hidapi.open( config.vendor_id, - config.product_id + config.product_id, ) { Ok(device) => { + // Log success with format info for context log::info!( - "Successfully opened device: VID={:04x}, PID={:04x}, SN={}", - config.vendor_id, config.product_id, config.serial_number + "Successfully opened device slot {}: VID={:04X}, PID={:04X}, SN='{}', Format='{}'", + i, config.vendor_id, config.product_id, config.serial_number, info.format.name // Log format name ); - // Set non-blocking mode + + // Attempt to set non-blocking mode if let Err(e) = device.set_blocking_mode(false) { - log::error!("Failed to set non-blocking mode: {}", e); - // Decide if this is fatal for this device - devices.push(None); // Treat as failure if non-blocking fails + log::error!( + "Failed to set non-blocking mode for device slot {}: {:?}. Treating as open failure.", + i, e + ); + // Decide if this is fatal: Yes, treat as failure if non-blocking fails + devices.push(None); } else { + // Successfully opened and set non-blocking devices.push(Some(device)); } } Err(e) => { + // Log failure to open log::warn!( - "Failed to open device VID={:04x}, PID={:04x}, SN={}: {}", - config.vendor_id, config.product_id, config.serial_number, e + "Failed to open device slot {}: VID={:04X}, PID={:04X}, SN='{}': {:?}", + i, config.vendor_id, config.product_id, config.serial_number, e ); devices.push(None); // Push None on failure } @@ -140,12 +252,12 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { // --- Device Opening --- // Open sources and receivers, keeping track of which ones succeeded - let mut source_devices = open_hid_devices(&hidapi, &data.sources_config); - let mut receiver_devices = open_hid_devices(&hidapi, &data.receivers_config); + let mut source_devices = open_hid_devices(&hidapi, &data.sources_info); + let mut receiver_devices = open_hid_devices(&hidapi, &data.receivers_info); // Buffers for HID reports - let mut read_buffer = [0u8; REPORT_BUFFER_SIZE]; - let mut write_buffer = [0u8; REPORT_BUFFER_SIZE]; // Buffer for calculated output + let mut read_buffer = [0u8; MAX_REPORT_SIZE]; + let mut write_buffer = [0u8; MAX_REPORT_SIZE]; // Buffer for calculated output let &(ref run_lock, ref run_cvar) = &*data.run_state; @@ -155,60 +267,61 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { match run_lock.lock() { Ok(guard) => *guard, Err(poisoned) => { - log::error!("Run state mutex poisoned in worker loop!"); + error!("Run state mutex poisoned in worker loop!"); false } } }; if !should_run { - log::info!("Stop signal received, exiting worker loop."); + info!("Stop signal received, exiting worker loop."); break; // Exit the loop } // --- Read from Source Devices --- let mut current_source_states: Vec> = vec![None; source_devices.len()]; - read_buffer[0] = FEATURE_REPORT_ID; // Set report ID for reading for (i, device_opt) in source_devices.iter_mut().enumerate() { if let Some(device) = device_opt { + let source_info = &data.sources_info[i]; + let source_format = source_info.format; + read_buffer[0] = source_format.report_id; + // Attempt to read feature report match device.get_feature_report(&mut read_buffer) { - Ok(bytes_read) if bytes_read >= 3 => { // Need at least ID + 2 bytes data - let state_val = merge_u8_into_u16(read_buffer[1], read_buffer[2]); - current_source_states[i] = Some(state_val); - // Update shared state for UI - if let Some(shared_state) = data.source_states_shared.get(i) { - if let Ok(mut guard) = shared_state.lock() { - log::debug!("Worker: Updating source_states_shared[{}] from {} to {}", i, *guard, state_val); - *guard = state_val; + Ok(bytes_read) => { + if let Some(state_val) = source_format.unpack_state(&read_buffer[0..bytes_read]) { + trace!("Worker: Unpacked state {} from source {}", state_val, i); + current_source_states[i] = Some(state_val); + // Update shared state for UI + if let Some(shared_state) = data.source_states_shared.get(i) { + if let Ok(mut guard) = shared_state.lock() { *guard = state_val; } + else { log::error!("Worker: Mutex poisoned for source_states_shared[{}]!", i); } + } + } else { + // unpack_state returned None (e.g., wrong ID, too short) + log::warn!("Worker: Failed to unpack state from source {} (bytes read: {}) using format '{}'", i, bytes_read, source_format.name); + current_source_states[i] = None; + if let Some(shared_state) = data.source_states_shared.get(i) { + if let Ok(mut guard) = shared_state.lock() { *guard = 0; } // Reset UI } } } - Ok(bytes_read) => { // Read ok, but not enough data? - log::warn!("Source {} read only {} bytes for report {}.", i, bytes_read, FEATURE_REPORT_ID); - current_source_states[i] = None; // Treat as no data - if let Some(shared_state) = data.source_states_shared.get(i) { - if let Ok(mut guard) = shared_state.lock() { *guard = 0; } // Reset UI state - } - } Err(e) => { - log::warn!("Error reading from source {}: {}. Attempting reopen.", i, e); - current_source_states[i] = None; // Clear state on error + log::warn!("Worker: Error reading from source {}: {:?}. Attempting reopen.", i, e); + current_source_states[i] = None; if let Some(shared_state) = data.source_states_shared.get(i) { - if let Ok(mut guard) = shared_state.lock() { *guard = 0; } // Reset UI state - } - // Attempt to reopen the device - *device_opt = hidapi.open( - data.sources_config[i].vendor_id, - data.sources_config[i].product_id - ).ok().and_then(|d| { d.set_blocking_mode(false).ok()?; Some(d) }); // Re-open and set non-blocking - - if device_opt.is_none() { - log::warn!("Reopen failed for source {}.", i); - } else { - log::info!("Reopen successful for source {}.", i); + if let Ok(mut guard) = shared_state.lock() { *guard = 0; } } + // Reopen logic using source_info.config + log::debug!("Worker: Attempting to reopen source[{}]...", i); + *device_opt = hidapi.open_serial( + source_info.config.vendor_id, + source_info.config.product_id, + &source_info.config.serial_number, + ).ok().and_then(|d| d.set_blocking_mode(false).ok().map(|_| d)); // Simplified reopen + if device_opt.is_some() { log::info!("Worker: Reopen successful for source[{}].", i); } + else { log::warn!("Worker: Reopen failed for source[{}].", i); } } } } else { @@ -220,26 +333,15 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { } } - // --- Calculate Final State based on Rules --- + // --- 3. Calculate Final State based on Rules --- let mut final_state: u16 = 0; - write_buffer.fill(0); // Clear write buffer - write_buffer[0] = FEATURE_REPORT_ID; - for bit_pos in 0..8u8 { let mut relevant_values: Vec = Vec::new(); for (source_idx, state_opt) in current_source_states.iter().enumerate() { - // Check if this source is enabled for this bit - if data.sources_config[source_idx].state_enabled[bit_pos as usize] { - if let Some(state_val) = state_opt { - relevant_values.push(read_bit(*state_val, bit_pos)); - } else { - // How to handle missing data? Assume false? Or skip? - // Assuming false if device errored or didn't report - relevant_values.push(false); - } + if data.sources_info[source_idx].config.state_enabled[bit_pos as usize] { + relevant_values.push(state_opt.map_or(false, |s| util::read_bit(s, bit_pos))); } } - if !relevant_values.is_empty() { let modifier = data.shift_modifiers[bit_pos as usize]; let result_bit = match modifier { @@ -247,100 +349,130 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { crate::config::ShiftModifiers::AND => relevant_values.iter().all(|&v| v), crate::config::ShiftModifiers::XOR => relevant_values.iter().fold(false, |acc, &v| acc ^ v), }; - - // Set the corresponding bit in the final state and write buffer - if result_bit { - final_state |= 1 << bit_pos; - // Assuming the state maps directly to bytes 1 and 2 - write_buffer[1] = final_state as u8; // Low byte - write_buffer[2] = (final_state >> 8) as u8; // High byte - } + if result_bit { final_state |= 1 << bit_pos; } } } - // Update shared final state for UI if let Ok(mut guard) = data.final_shift_state_shared.lock() { *guard = final_state; } + // --- End Calculate Final State --- - // --- Write to Receiver Devices --- - let zero_buffer: [u8; REPORT_BUFFER_SIZE] = { - let mut buf = [0u8; REPORT_BUFFER_SIZE]; - buf[0] = FEATURE_REPORT_ID; // Set Report ID 4 - // All other bytes (1-18) remain 0 for the zero state - buf - }; - + // --- 4. Write to Receiver Devices --- for (i, device_opt) in receiver_devices.iter_mut().enumerate() { if let Some(device) = device_opt { - match device.send_feature_report(&zero_buffer) { + let receiver_info = &data.receivers_info[i]; + let receiver_format = receiver_info.format; + + // --- 4a. Send Zero State Report First --- + let zero_buffer_slice = receiver_format.pack_state(&mut write_buffer, 0); + if zero_buffer_slice.is_empty() { /* handle error */ continue; } + + log::trace!("Worker: Sending zero state reset ({} bytes) to receiver[{}] using format '{}'", receiver_format.total_size, i, receiver_format.name); + match device.send_feature_report(zero_buffer_slice) { Ok(_) => { - // Create a temporary buffer potentially filtered by receiver's enabled bits - let mut filtered_write_buffer = write_buffer; // Copy base calculated state - let mut filtered_final_state = final_state; + log::trace!("Worker: Zero state sent successfully to receiver[{}].", i); + + // --- 4b. If Zero Send OK, Prepare and Send Actual State --- + let mut state_to_send = final_state; // Start with the globally calculated state // Apply receiver's enabled mask for bit_pos in 0..8u8 { - if !data.receivers_config[i].state_enabled[bit_pos as usize] { - // If this bit is disabled for this receiver, force it to 0 - filtered_final_state &= !(1 << bit_pos); + if !receiver_info.config.state_enabled[bit_pos as usize] { + state_to_send &= !(1 << bit_pos); } } - // Update buffer bytes based on filtered state - filtered_write_buffer[1] = (filtered_final_state >> 8) as u8; - filtered_write_buffer[2] = filtered_final_state as u8; - filtered_write_buffer[3..19].fill(0); + // --- Start: Read receiver's current state and merge --- + let mut receiver_current_state: u16 = 0; // Default to 0 if read fails + read_buffer[0] = receiver_format.report_id; // Set ID for reading receiver - // --- Optional: Read receiver's current state and merge --- - // This part makes it more complex. If you want the output to *combine* - // with the receiver's own state, you'd read it first. - // For simplicity, let's just *set* the state based on calculation. - // If merging is needed, uncomment and adapt: - read_buffer[0] = FEATURE_REPORT_ID; - if let Ok(bytes) = device.get_feature_report(&mut read_buffer) { - if bytes >= 3 { - let receiver_current_low = read_buffer[1]; - let receiver_current_high = read_buffer[2]; - // Merge logic here, e.g., ORing the states - filtered_write_buffer[1] |= receiver_current_low; - filtered_write_buffer[2] |= receiver_current_high; + log::trace!("Worker: Reading current state from receiver[{}] before merge.", i); + match device.get_feature_report(&mut read_buffer) { + Ok(bytes_read) => { + if let Some(current_state) = receiver_format.unpack_state(&read_buffer[0..bytes_read]) { + log::trace!("Worker: Receiver[{}] current unpacked state: {}", i, current_state); + receiver_current_state = current_state; + } else { + log::warn!("Worker: Failed to unpack current state from receiver {} (bytes read: {}) using format '{}'. Merge will use 0.", i, bytes_read, receiver_format.name); + } + } + Err(e_read) => { + // Log error reading current state, but proceed with merge using 0 + log::warn!("Worker: Error reading current state from receiver[{}]: {:?}. Merge will use 0.", i, e_read); + // Note: Don't attempt reopen here, as we are about to send anyway. + // If send fails later, reopen will be attempted then. } } - // --- End Optional Merge --- - log::debug!( - "Worker: Attempting send to receiver[{}], state: {}, buffer ({} bytes): {:02X?}", - i, - filtered_final_state, - 19, // Log the length being sent - &filtered_write_buffer[0..19] // Log the full slice + // --- Read current state (Optional Merge) --- + read_buffer[0] = receiver_format.report_id; // Use receiver format ID + let mut receiver_current_state: u16 = 0; + log::trace!("Worker: Reading current state from receiver[{}] before merge using format '{}'.", i, receiver_format.name); + match device.get_feature_report(&mut read_buffer) { + Ok(bytes_read) => { + // --- Use correct unpack_state function name --- + if let Some(current_state) = receiver_format.unpack_state(&read_buffer[0..bytes_read]) { + // --- End change --- + receiver_current_state = current_state; + } else { /* warn unpack failed */ } + } + Err(e_read) => { /* warn read failed */ } + } + state_to_send |= receiver_current_state; // Merge + // --- End Read current state --- + + // Use pack_state to prepare the buffer slice with the potentially merged state + let actual_buffer_slice = receiver_format.pack_state( + &mut write_buffer, + state_to_send, // Use the final (potentially merged) state ); + if actual_buffer_slice.is_empty() { /* handle pack error */ continue; } - // Send the potentially filtered feature report - match device.send_feature_report(&filtered_write_buffer[0..REPORT_BUFFER_SIZE]) { + log::debug!( + "Worker: Attempting send final state to receiver[{}], state: {}, buffer ({} bytes): {:02X?}", + i, state_to_send, receiver_format.total_size, actual_buffer_slice + ); + + // Send the actual calculated/merged state + match device.send_feature_report(actual_buffer_slice) { Ok(_) => { - log::debug!("Worker: Send to receiver[{}] successful.", i); - // Successfully sent. Update UI state for this receiver. + log::debug!("Worker: Final state send to receiver[{}] successful.", i); + // Update shared state for UI with the state we just sent if let Some(shared_state) = data.receiver_states_shared.get(i) { if let Ok(mut guard) = shared_state.lock() { - // Update with the state *we sent* - let state_val = merge_u8_into_u16(filtered_write_buffer[1], filtered_write_buffer[2]); - log::debug!("Worker: Updating receiver_states_shared[{}] from {} to {}", i, *guard, state_val); - *guard = state_val; + *guard = state_to_send; // Update with the sent state + } else { + if let Some(shared_state) = data.receiver_states_shared.get(i) { + match shared_state.lock() { + Ok(mut guard) => *guard = 0, + Err(poisoned) => { + log::error!("Mutex for receiver_states_shared[{}] poisoned! Recovering and resetting.", i); + *poisoned.into_inner() = 0; + } + } + } } } } - Err(e) => { - log::warn!("Error writing to receiver {}: {}. Attempting reopen.", i, e); + Err(e_actual) => { + // ... (error handling, reopen logic for send failure) ... + log::warn!("Worker: Error sending final state to receiver[{}]: {:?}", i, e_actual); if let Some(shared_state) = data.receiver_states_shared.get(i) { - if let Ok(mut guard) = shared_state.lock() { *guard = 0; } // Reset UI state + match shared_state.lock() { + Ok(mut guard) => *guard = 0, + Err(poisoned) => { + log::error!("Mutex for receiver_states_shared[{}] poisoned! Recovering and resetting.", i); + *poisoned.into_inner() = 0; + } + } } - // Attempt to reopen + + log::debug!("Worker: Attempting to reopen receiver[{}] after final-send failure...", i); *device_opt = hidapi.open( - data.receivers_config[i].vendor_id, - data.receivers_config[i].product_id, + data.receivers_info[i].config.vendor_id, + data.receivers_info[i].config.product_id, ).ok().and_then(|d| { d.set_blocking_mode(false).ok()?; Some(d) @@ -352,8 +484,8 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { log::info!("Reopen successful for receiver {}.", i); } } - } - } + } // End match send actual state + } // End Ok for zero send Err(e_zero) => { // Handle error sending the zero state reset log::warn!("Worker: Error sending zero state reset to receiver[{}]: {:?}", i, e_zero); @@ -362,8 +494,8 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { if let Ok(mut guard) = shared_state.lock() { *guard = 0; } } log::debug!("Worker: Attempting to reopen receiver[{}] after zero-send failure...", i); - *device_opt = hidapi.open( data.receivers_config[i].vendor_id, - data.receivers_config[i].product_id + *device_opt = hidapi.open( data.receivers_info[i].config.vendor_id, + data.receivers_info[i].config.product_id ).ok().and_then(|d| { d.set_blocking_mode(false).ok()?; Some(d) @@ -389,20 +521,30 @@ fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) { // --- Cleanup before thread exit --- log::info!("Worker loop finished. Performing cleanup..."); - // Send a 'zero' report to all devices on exit - let cleanup_buffer: [u8; REPORT_BUFFER_SIZE] = { - let mut buf = [0u8; REPORT_BUFFER_SIZE]; - buf[0] = FEATURE_REPORT_ID; // Set Report ID 4 - // All other bytes (1-18) remain 0 for the zero state - buf - }; - for device_opt in source_devices.iter_mut().chain(receiver_devices.iter_mut()) { + for (i, device_opt) in receiver_devices.iter_mut().enumerate() { if let Some(device) = device_opt { - if let Err(e) = device.send_feature_report(&cleanup_buffer) { - log::warn!("Error sending cleanup report: {}", e); + let receiver_info = &data.receivers_info[i]; + let receiver_format = receiver_info.format; + + // --- 4a. Send Zero State Report First --- + let zero_buffer_slice = receiver_format.pack_state(&mut write_buffer, 0); + if zero_buffer_slice.is_empty() { /* handle error */ continue; } + + log::trace!("Worker: Sending zero state reset ({} bytes) to receiver[{}] using format '{}'", receiver_format.total_size, i, receiver_format.name); + match device.send_feature_report(zero_buffer_slice) { + Ok(_) => { + log::trace!("Worker: Zero state sent successfully to receiver[{}].", i); + if let Some(shared_state) = data.receiver_states_shared.get(i) { + if let Ok(mut guard) = shared_state.lock() { *guard = 0; } + } + } + Err(e_actual) => { + if let Some(shared_state) = data.receiver_states_shared.get(i) { + if let Ok(mut guard) = shared_state.lock() { *guard = 0; } + } + } } } } log::info!("Worker thread cleanup complete. Exiting."); - // HidApi and HidDevices are dropped automatically here, closing handles. } diff --git a/src/main.rs b/src/main.rs index 059499b..db23c5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use eframe::{egui, glow}; use fast_config::Config; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; +use clap::Parser; // Internal Module Imports use config::{ConfigData}; // Import specific items @@ -30,6 +31,13 @@ const INITIAL_HEIGHT: f32 = 260.0; pub type SharedStateFlag = Arc<(Mutex, Condvar)>; pub type SharedDeviceState = Arc>; // Assuming Condvar isn't strictly needed here +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long, default_value_t = false)] + skip_firmware: bool, +} + // The main application struct pub struct ShiftTool { // State @@ -205,15 +213,7 @@ fn main() -> eframe::Result<()> { env_logger::init(); // --- Command Line Argument Parsing --- - // If you need args, keep this, otherwise remove clap dependency. - use clap::Parser; - #[derive(Parser, Debug)] - #[command(version, about, long_about = None)] - struct Args { - #[arg(short, long, default_value_t = false)] - skip_firmware: bool, - } - let _args = Args::parse(); + // let _args = Args::parse(); // --- End Argument Parsing --- log::info!("Starting {}", PROGRAM_TITLE); diff --git a/src/util.rs b/src/util.rs index cd540e7..01141fb 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,224 @@ -use log::warn; // Use log crate +use clap::Parser; +use chrono::NaiveDate; +use log::{error, info, trace, warn}; + +pub(crate) const FEATURE_REPORT_ID_SHIFT: u8 = 4; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ReportFormat { + pub name: &'static str, + pub report_id: u8, + pub total_size: usize, + high_byte_idx: usize, + low_byte_idx: usize, +} + +impl ReportFormat { + /// Packs the u16 state into the provided buffer according to this format's rules. + /// + /// It sets the report ID, places the high and low bytes of the state at the + /// correct indices, and zeros out any remaining padding bytes up to `total_size`. + /// Assumes the provided `buffer` is large enough to hold `total_size` bytes. + /// + /// # Arguments + /// * `buffer`: A mutable byte slice, assumed to be large enough (e.g., MAX_REPORT_SIZE). + /// The relevant part (`0..total_size`) will be modified. + /// * `state`: The `u16` state value to pack. + /// + /// # Returns + /// A slice `&'buf [u8]` representing the packed report (`&buffer[0..self.total_size]`). + /// Returns an empty slice if the buffer is too small. + pub fn pack_state<'buf>( + &self, + buffer: &'buf mut [u8], + state: u16, + ) -> &'buf [u8] { + // 1. Safety Check: Ensure buffer is large enough + if buffer.len() < self.total_size { + error!( + "Buffer too small (len={}) for packing report format '{}' (size={})", + buffer.len(), + self.name, + self.total_size + ); + // Return empty slice to indicate error, calling code should handle this + return &[]; + } + + // 2. Clear the portion of the buffer we will use (safer than assuming zeros) + // This handles the zero-padding requirement automatically. + buffer[0..self.total_size].fill(0); + + // 3. Set the Report ID (Byte 0) + buffer[0] = self.report_id; + + // 4. Pack state bytes into their defined indices + // Check indices against buffer length again just in case format is invalid + if self.high_byte_idx != usize::MAX { + if self.high_byte_idx < self.total_size { // Check index within format size + buffer[self.high_byte_idx] = (state >> 8) as u8; + } else { error!("High byte index {} out of bounds for format '{}' (size={})", self.high_byte_idx, self.name, self.total_size); } + } else if (state >> 8) != 0 { + warn!("pack_state ({}): State {} has high byte, but format doesn't support it.", self.name, state); + } + + if self.low_byte_idx < self.total_size { + buffer[self.low_byte_idx] = state as u8; // Low byte + } else { + error!("Low byte index {} out of bounds for format '{}' (size={})", self.low_byte_idx, self.name, self.total_size); + } + + // 5. Return the slice representing the fully packed report + &buffer[0..self.total_size] + } + + /// Unpacks the u16 state from a received buffer slice based on this format's rules. + /// + /// Checks the report ID and minimum length required by the format. + /// Extracts the high and low bytes from the specified indices and merges them. + /// + /// # Arguments + /// * `received_data`: A byte slice containing the data read from the HID device + /// (should include the report ID at index 0). + /// + /// # Returns + /// `Some(u16)` containing the unpacked state if successful, `None` otherwise + /// (e.g., wrong report ID, buffer too short). + pub fn unpack_state(&self, received_data: &[u8]) -> Option { + // 1. Basic Checks: Empty buffer or incorrect Report ID + if received_data.is_empty() || received_data[0] != self.report_id { + trace!( + "unpack_state ({}): Invalid ID (expected {}, got {}) or empty buffer.", + self.name, self.report_id, if received_data.is_empty() { "N/A".to_string() } else { received_data[0].to_string() } + ); + return None; + } + + // 2. Determine minimum length required based on defined indices + // We absolutely need the bytes up to the highest index used. + let low_byte = if received_data.len() > self.low_byte_idx { + received_data[self.low_byte_idx] + } else { + warn!("unpack_state ({}): Received data length {} too short for low byte index {}.", self.name, received_data.len(), self.low_byte_idx); + return None; + }; + + let high_byte = if self.high_byte_idx != usize::MAX { // Does format expect a high byte? + if received_data.len() > self.high_byte_idx { // Did we receive enough data for it? + received_data[self.high_byte_idx] + } else { // Expected high byte, but didn't receive it + trace!("unpack_state ({}): Received data length {} too short for high byte index {}. Assuming 0.", self.name, received_data.len(), self.high_byte_idx); + 0 + } + } else { // Format doesn't define a high byte + 0 + }; + // --- End Graceful Handling --- + + + // 4. Merge bytes + let state = (high_byte as u16) << 8 | (low_byte as u16); + + trace!("unpack_state ({}): Extracted state {}", self.name, state); + Some(state) + } +} + +const FORMAT_ORIGINAL: ReportFormat = ReportFormat { + name: "Original (Size 3)", // Add name + report_id: FEATURE_REPORT_ID_SHIFT, + total_size: 3, + high_byte_idx: 1, + low_byte_idx: 2, +}; + +const FORMAT_THROTTLE: ReportFormat = ReportFormat { + name: "Original Throttle (Size 2)", // Add name + report_id: FEATURE_REPORT_ID_SHIFT, + total_size: 2, + high_byte_idx: usize::MAX, + low_byte_idx: 1, +}; + +const FORMAT_NEW: ReportFormat = ReportFormat { + name: "NEW (Size 19)", // Add name + report_id: FEATURE_REPORT_ID_SHIFT, + total_size: 19, + high_byte_idx: 1, + low_byte_idx: 2, +}; + +struct FormatRule { + // Criteria: Function that takes firmware string and returns true if it matches + matches: fn(&str, &str) -> bool, + // Result: The format to use if criteria matches + format: ReportFormat, +} + +const FORMAT_RULES: &[FormatRule] = &[ + // Rule 1: Check for Original format based on date for Throttles + FormatRule { + matches: |name, fw| { + if name.contains("Throttle") == false { + return false + } + const THRESHOLD: &str = "2024-12-26"; + let date_str = fw.split_whitespace().last().unwrap_or(""); + if date_str.len() == 8 { + if let Ok(fw_date) = NaiveDate::parse_from_str(date_str, "%Y%m%d") { + if let Ok(t_date) = NaiveDate::parse_from_str(THRESHOLD, "%Y-%m-%d") { + return fw_date < t_date; // Return true if older + } + } + } + false // Don't match if parsing fails or format wrong + }, + format: FORMAT_THROTTLE, + }, + // Rule 2: Check for Original format based on date + FormatRule { + matches: |name, fw| { + const THRESHOLD: &str = "2024-12-26"; + let date_str = fw.split_whitespace().last().unwrap_or(""); + if date_str.len() == 8 { + if let Ok(fw_date) = NaiveDate::parse_from_str(date_str, "%Y%m%d") { + if let Ok(t_date) = NaiveDate::parse_from_str(THRESHOLD, "%Y-%m-%d") { + return fw_date < t_date; // Return true if older + } + } + } + false // Don't match if parsing fails or format wrong + }, + format: FORMAT_ORIGINAL, + }, + // Rule 2: Add more rules here if needed (e.g., for FORMAT_MIDDLE) + // FormatRule { matches: |fw| fw.contains("SPECIAL"), format: FORMAT_MIDDLE }, + + // Rule N: Default rule (matches anything if previous rules didn't) + // This isn't strictly needed if we have a default below, but can be explicit. + // FormatRule { matches: |_| true, format: FORMAT_NEW }, +]; + +// --- The main function to determine the format --- +pub(crate) fn determine_report_format(name: &str, firmware: &str) -> ReportFormat { + // Iterate through the rules + for rule in FORMAT_RULES { + if (rule.matches)(name, firmware) { + trace!("Device '{}' Firmware '{}' matched rule for format '{}'", name, firmware, rule.format.name); + return rule.format; + } + } + + // If no rules matched, return a default (e.g., the newest format) + let default_format = FORMAT_NEW; // Define the default + warn!( + "Firmware '{}' did not match any specific rules. Defaulting to format '{}'", + firmware, default_format.name + ); + default_format +} + +pub(crate) const MAX_REPORT_SIZE: usize = FORMAT_NEW.total_size; /// Reads a specific bit from a u16 value. /// `position` is 0-indexed (0-15). @@ -34,17 +254,16 @@ pub(crate) fn merge_u8_into_u16(high_byte: u8, low_byte: u8) -> u16 { /// TODO: Implement actual firmware checking logic if needed. pub(crate) fn is_supported(firmware_string: String) -> bool { // Currently allows all devices. - // If you re-enable firmware checking, use the `args` or a config setting. - // let args = crate::main::Args::parse(); // Need to handle args properly - // if args.skip_firmware { return true; } + let args = crate::Args::parse(); // Need to handle args properly + if args.skip_firmware { return true; } // Example fixed list check: // let supported_firmware = [ - // "VIRPIL Controls 20220720", - // "VIRPIL Controls 20230328", - // "VIRPIL Controls 20240323", + // // "VIRPIL Controls 20220720", + // // "VIRPIL Controls 20230328", + // // "VIRPIL Controls 20240323", + // "VIRPIL Controls 20241226", // ]; - // supported_firmware.contains(&firmware_string.as_str()) if firmware_string.is_empty() || firmware_string == "Unknown Firmware" { warn!("Device has missing or unknown firmware string.");