Added the ability to support different firmware report versions
This commit is contained in:
64
Cargo.lock
generated
64
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<SavedDevice>,
|
||||
receivers_config: Vec<SavedDevice>,
|
||||
sources_info: Vec<DeviceWorkerInfo>,
|
||||
receivers_info: Vec<DeviceWorkerInfo>,
|
||||
shift_modifiers: ModifiersArray,
|
||||
source_states_shared: Vec<SharedDeviceState>,
|
||||
receiver_states_shared: Vec<SharedDeviceState>,
|
||||
@@ -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<DeviceWorkerInfo> = 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<DeviceWorkerInfo> = 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<Option<HidDevice>> { // Return Option<HidDevice> 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<Option<HidDevice>> {
|
||||
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<Option<u16>> = 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]);
|
||||
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() {
|
||||
log::debug!("Worker: Updating source_states_shared[{}] from {} to {}", i, *guard, state_val);
|
||||
*guard = state_val;
|
||||
if let Ok(mut guard) = shared_state.lock() { *guard = state_val; }
|
||||
else { log::error!("Worker: Mutex poisoned for source_states_shared[{}]!", i); }
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
} 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 state
|
||||
if let Ok(mut guard) = shared_state.lock() { *guard = 0; } // Reset UI
|
||||
}
|
||||
}
|
||||
}
|
||||
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<bool> = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Error writing to receiver {}: {}. Attempting reopen.", i, e);
|
||||
*guard = state_to_send; // Update with the sent state
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
18
src/main.rs
18
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<bool>, Condvar)>;
|
||||
pub type SharedDeviceState = Arc<Mutex<u16>>; // 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);
|
||||
|
||||
235
src/util.rs
235
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<u16> {
|
||||
// 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.");
|
||||
|
||||
Reference in New Issue
Block a user