code clean-up #1
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2710,7 +2710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shift_tool"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"dirs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shift_tool"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
18
src/about.rs
18
src/about.rs
@@ -1,14 +1,14 @@
|
||||
pub fn about() -> [&'static str; 7] {
|
||||
[
|
||||
pub fn about() -> Vec<String> {
|
||||
vec![
|
||||
"This program was designed to replicate the VPC Shift Tool functions \
|
||||
bundled with the VirPil control software package.",
|
||||
"\n",
|
||||
"Shift Tool Copyright (C) 2024-2025 RavenX8",
|
||||
bundled with the VirPil control software package.".to_string(),
|
||||
"\n".to_string(),
|
||||
"Shift Tool Copyright (C) 2024-2025 RavenX8".to_string(),
|
||||
"This program comes with ABSOLUTELY NO WARRANTY.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions.",
|
||||
"License: GNU General Public License v3.0",
|
||||
"Author: RavenX8",
|
||||
"https://github.com/RavenX8/vpc-shift-tool",
|
||||
under certain conditions.".to_string(),
|
||||
"License: GNU General Public License v3.0".to_string(),
|
||||
"Author: RavenX8".to_string(),
|
||||
"https://github.com/RavenX8/vpc-shift-tool".to_string(),
|
||||
]
|
||||
}
|
||||
73
src/config.rs
Normal file
73
src/config.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
// Configuration data saved to JSON
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigData {
|
||||
#[serde(default)] // Ensure field exists even if missing in JSON
|
||||
pub sources: Vec<crate::device::SavedDevice>,
|
||||
#[serde(default)]
|
||||
pub receivers: Vec<crate::device::SavedDevice>,
|
||||
#[serde(default)] // Use default if missing
|
||||
pub shift_modifiers: ModifiersArray,
|
||||
}
|
||||
|
||||
// Default values for a new configuration
|
||||
impl Default for ConfigData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sources: vec![], // Start with no sources configured
|
||||
receivers: vec![],
|
||||
shift_modifiers: ModifiersArray::default(), // Defaults to all OR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enum for shift modifier logic
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ShiftModifiers {
|
||||
OR = 0,
|
||||
AND = 1,
|
||||
XOR = 2,
|
||||
}
|
||||
|
||||
// How the modifier is displayed in the UI
|
||||
impl std::fmt::Display for ShiftModifiers {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
ShiftModifiers::OR => write!(f, "OR"),
|
||||
ShiftModifiers::AND => write!(f, "AND"),
|
||||
ShiftModifiers::XOR => write!(f, "XOR"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for the array of modifiers to implement Default and Indexing
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct ModifiersArray {
|
||||
data: [ShiftModifiers; 8],
|
||||
}
|
||||
|
||||
impl Default for ModifiersArray {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: [ShiftModifiers::OR; 8], // Default to OR for all 8 bits
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow indexing like `modifiers_array[i]`
|
||||
impl Index<usize> for ModifiersArray {
|
||||
type Output = ShiftModifiers;
|
||||
|
||||
fn index(&self, index: usize) -> &ShiftModifiers {
|
||||
&self.data[index]
|
||||
}
|
||||
}
|
||||
|
||||
// Allow mutable indexing like `modifiers_array[i] = ...`
|
||||
impl IndexMut<usize> for ModifiersArray {
|
||||
fn index_mut(&mut self, index: usize) -> &mut ShiftModifiers {
|
||||
&mut self.data[index]
|
||||
}
|
||||
}
|
||||
267
src/device.rs
Normal file
267
src/device.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use hidapi::{DeviceInfo, HidApi, HidError};
|
||||
use log::{error, info, warn}; // Use log crate
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::rc::Rc; // Keep Rc for potential sharing within UI if needed
|
||||
|
||||
// Represents a discovered VPC device
|
||||
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone)]
|
||||
pub struct VpcDevice {
|
||||
pub full_name: String, // Combined identifier
|
||||
pub name: Rc<String>, // Product String
|
||||
pub firmware: Rc<String>, // Manufacturer String (often firmware version)
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub serial_number: String,
|
||||
pub usage: u16, // HID usage page/id (less commonly needed for opening)
|
||||
pub active: bool, // Is the worker thread currently connected?
|
||||
}
|
||||
|
||||
impl Default for VpcDevice {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
full_name: String::from(""),
|
||||
name: String::from("-NO CONNECTION (Select device from list)-").into(),
|
||||
firmware: String::from("").into(),
|
||||
vendor_id: 0,
|
||||
product_id: 0,
|
||||
serial_number: String::from(""),
|
||||
usage: 0,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// How the device is displayed in dropdowns
|
||||
impl std::fmt::Display for VpcDevice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if self.vendor_id == 0 && self.product_id == 0 {
|
||||
// Default/placeholder entry
|
||||
write!(f, "{}", self.name)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"VID:{:04X} PID:{:04X} {} (SN:{} FW:{})", // More info
|
||||
self.vendor_id,
|
||||
self.product_id,
|
||||
self.name,
|
||||
if self.serial_number.is_empty() { "N/A" } else { &self.serial_number },
|
||||
if self.firmware.is_empty() { "N/A" } else { &self.firmware }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data structure for saving selected devices in config
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SavedDevice {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub serial_number: String,
|
||||
pub state_enabled: [bool; 8], // Which shift bits are active for this device
|
||||
}
|
||||
|
||||
impl Default for SavedDevice {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vendor_id: 0,
|
||||
product_id: 0,
|
||||
serial_number: String::from(""),
|
||||
state_enabled: [true; 8], // Default to all enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the index in the `device_list` corresponding to the saved device data.
|
||||
/// Returns 0 (default "No Connection") if not found or if saved_device is invalid.
|
||||
// Make this function standalone or static, not requiring &self
|
||||
pub(crate) fn find_device_index_for_saved(
|
||||
device_list: &[VpcDevice], // Pass device list explicitly
|
||||
saved_device: &SavedDevice,
|
||||
) -> usize {
|
||||
if saved_device.vendor_id == 0 && saved_device.product_id == 0 {
|
||||
return 0; // Point to the default "No Connection" entry
|
||||
}
|
||||
device_list
|
||||
.iter()
|
||||
.position(|d| {
|
||||
d.vendor_id == saved_device.vendor_id
|
||||
&& d.product_id == saved_device.product_id
|
||||
&& d.serial_number == saved_device.serial_number
|
||||
})
|
||||
.unwrap_or(0) // Default to index 0 ("No Connection") if not found
|
||||
}
|
||||
|
||||
|
||||
// --- Device Management Functions ---
|
||||
|
||||
// Now part of ShiftTool impl block
|
||||
impl crate::ShiftTool {
|
||||
/// Refreshes the internal list of available HID devices.
|
||||
pub(crate) fn refresh_devices(&mut self) {
|
||||
info!("Refreshing device list...");
|
||||
match HidApi::new() {
|
||||
Ok(hidapi) => {
|
||||
let mut current_devices: Vec<VpcDevice> = Vec::new();
|
||||
// Keep track of seen devices to avoid duplicates
|
||||
// Use a HashSet for efficient checking
|
||||
use std::collections::HashSet;
|
||||
let mut seen_devices = HashSet::new();
|
||||
|
||||
for device_info in hidapi.device_list() {
|
||||
// Filter for specific vendor if desired
|
||||
if device_info.vendor_id() == crate::hid_worker::VENDOR_ID_FILTER {
|
||||
if let Some(vpc_device) =
|
||||
create_vpc_device_from_info(device_info)
|
||||
{
|
||||
// Create a unique key for the device
|
||||
let device_key = (
|
||||
vpc_device.vendor_id,
|
||||
vpc_device.product_id,
|
||||
vpc_device.serial_number.clone(),
|
||||
);
|
||||
|
||||
// Check if we've already added this unique device
|
||||
if seen_devices.insert(device_key) {
|
||||
// If insert returns true, it's a new device
|
||||
if crate::util::is_supported(
|
||||
vpc_device.firmware.to_string(),
|
||||
) {
|
||||
info!("Found supported device: {}", vpc_device);
|
||||
current_devices.push(vpc_device);
|
||||
} else {
|
||||
warn!(
|
||||
"Found unsupported device (firmware?): {}",
|
||||
vpc_device
|
||||
);
|
||||
// Optionally add unsupported devices too, just filter later?
|
||||
// current_devices.push(vpc_device);
|
||||
}
|
||||
} else {
|
||||
// Device already seen (duplicate entry from hidapi)
|
||||
log::trace!("Skipping duplicate device entry: {}", vpc_device);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort devices (e.g., by name)
|
||||
current_devices.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
// Add the default "no connection" entry *after* sorting real devices
|
||||
current_devices.insert(0, VpcDevice::default());
|
||||
|
||||
|
||||
// Update the app's device list
|
||||
self.device_list = current_devices;
|
||||
info!(
|
||||
"Device list refresh complete. Found {} unique devices.",
|
||||
self.device_list.len() - 1 // Exclude default entry
|
||||
);
|
||||
|
||||
// Validate selected devices against the new, deduplicated list
|
||||
self.validate_selected_devices();
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to create HidApi for device refresh: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the index in the `device_list` corresponding to the saved receiver config.
|
||||
pub(crate) fn find_receiver_device_index(&self, receiver_config_index: usize) -> usize {
|
||||
self.find_device_index_for_saved(
|
||||
&self.config.data.receivers[receiver_config_index]
|
||||
)
|
||||
}
|
||||
|
||||
/// Finds the index in the `device_list` corresponding to the saved source config.
|
||||
pub(crate) fn find_source_device_index(&self, source_config_index: usize) -> usize {
|
||||
self.find_device_index_for_saved(
|
||||
&self.config.data.sources[source_config_index]
|
||||
)
|
||||
}
|
||||
|
||||
/// Generic helper to find a device index based on SavedDevice data.
|
||||
fn find_device_index_for_saved(&self, saved_device: &SavedDevice) -> usize {
|
||||
if saved_device.vendor_id == 0 && saved_device.product_id == 0 {
|
||||
return 0; // Point to the default "No Connection" entry
|
||||
}
|
||||
self.device_list
|
||||
.iter()
|
||||
.position(|d| {
|
||||
d.vendor_id == saved_device.vendor_id
|
||||
&& d.product_id == saved_device.product_id
|
||||
&& d.serial_number == saved_device.serial_number
|
||||
})
|
||||
.unwrap_or(0) // Default to index 0 ("No Connection") if not found
|
||||
}
|
||||
|
||||
/// Checks if saved source/receiver devices still exist in the refreshed list.
|
||||
/// Resets the config entry to default if the device is gone.
|
||||
fn validate_selected_devices(&mut self) {
|
||||
let mut changed = false;
|
||||
for i in 0..self.config.data.sources.len() {
|
||||
let idx = self.find_source_device_index(i);
|
||||
if idx == 0 && (self.config.data.sources[i].vendor_id != 0 || self.config.data.sources[i].product_id != 0) {
|
||||
warn!("Previously selected source device {} not found after refresh. Resetting.", i + 1);
|
||||
self.config.data.sources[i] = SavedDevice::default();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for i in 0..self.config.data.receivers.len() {
|
||||
let idx = self.find_receiver_device_index(i);
|
||||
if idx == 0 && (self.config.data.receivers[i].vendor_id != 0 || self.config.data.receivers[i].product_id != 0) {
|
||||
warn!("Previously selected receiver device {} not found after refresh. Resetting.", i + 1);
|
||||
self.config.data.receivers[i] = SavedDevice::default();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
// Optionally save the config immediately after validation changes
|
||||
// if let Err(e) = self.config.save() {
|
||||
// error!("Failed to save config after device validation: {}", e);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Creates a VpcDevice from HidApi's DeviceInfo.
|
||||
fn create_vpc_device_from_info(device_info: &DeviceInfo) -> Option<VpcDevice> {
|
||||
// ... (same as before)
|
||||
let vendor_id = device_info.vendor_id();
|
||||
let product_id = device_info.product_id();
|
||||
let name = device_info
|
||||
.product_string()
|
||||
.unwrap_or("Unknown Product")
|
||||
.to_string();
|
||||
let firmware = device_info
|
||||
.manufacturer_string()
|
||||
.unwrap_or("Unknown Firmware")
|
||||
.to_string();
|
||||
let serial_number = device_info.serial_number().unwrap_or("").to_string();
|
||||
let usage = device_info.usage();
|
||||
|
||||
if vendor_id == 0 || product_id == 0 || name == "Unknown Product" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let full_name = format!(
|
||||
"{:04X}:{:04X}:{}",
|
||||
vendor_id,
|
||||
product_id,
|
||||
if serial_number.is_empty() { "no_sn" } else { &serial_number }
|
||||
);
|
||||
|
||||
Some(VpcDevice {
|
||||
full_name,
|
||||
name: name.into(),
|
||||
firmware: firmware.into(),
|
||||
vendor_id,
|
||||
product_id,
|
||||
serial_number,
|
||||
usage,
|
||||
active: false,
|
||||
})
|
||||
}
|
||||
406
src/hid_worker.rs
Normal file
406
src/hid_worker.rs
Normal file
@@ -0,0 +1,406 @@
|
||||
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 hidapi::{HidApi, HidDevice, HidError};
|
||||
use std::{
|
||||
sync::{Arc, Condvar, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
// 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>,
|
||||
shift_modifiers: ModifiersArray,
|
||||
source_states_shared: Vec<SharedDeviceState>,
|
||||
receiver_states_shared: Vec<SharedDeviceState>,
|
||||
final_shift_state_shared: SharedDeviceState,
|
||||
}
|
||||
|
||||
// Main function to spawn the worker thread
|
||||
// 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...");
|
||||
|
||||
// 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(),
|
||||
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(),
|
||||
final_shift_state_shared: self.shift_state.clone(),
|
||||
};
|
||||
|
||||
// Spawn the thread
|
||||
thread::spawn(move || {
|
||||
// Create HidApi instance *within* the thread
|
||||
match HidApi::new() { // Use new() which enumerates internally
|
||||
Ok(hidapi) => {
|
||||
log::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);
|
||||
// How to signal failure back? Could use another shared state.
|
||||
// For now, thread just exits.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log::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...");
|
||||
// Reset shared states displayed in the UI
|
||||
let reset_state = |state_arc: &SharedDeviceState| {
|
||||
if let Ok(mut state) = state_arc.lock() {
|
||||
*state = 0;
|
||||
}
|
||||
// No need to notify condvar if only UI reads it
|
||||
};
|
||||
|
||||
self.source_states.iter().for_each(reset_state);
|
||||
self.receiver_states.iter().for_each(reset_state);
|
||||
reset_state(&self.shift_state);
|
||||
|
||||
// Mark all devices as inactive in the UI list
|
||||
for device in self.device_list.iter_mut() {
|
||||
device.active = false;
|
||||
}
|
||||
log::info!("Worker stop cleanup finished.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper to open devices, returns Result for better error handling
|
||||
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 {
|
||||
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
|
||||
continue;
|
||||
}
|
||||
match hidapi.open(
|
||||
config.vendor_id,
|
||||
config.product_id
|
||||
) {
|
||||
Ok(device) => {
|
||||
log::info!(
|
||||
"Successfully opened device: VID={:04x}, PID={:04x}, SN={}",
|
||||
config.vendor_id, config.product_id, config.serial_number
|
||||
);
|
||||
// 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
|
||||
} else {
|
||||
devices.push(Some(device));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to open device VID={:04x}, PID={:04x}, SN={}: {}",
|
||||
config.vendor_id, config.product_id, config.serial_number, e
|
||||
);
|
||||
devices.push(None); // Push None on failure
|
||||
}
|
||||
}
|
||||
}
|
||||
devices
|
||||
}
|
||||
|
||||
|
||||
// The core worker loop logic
|
||||
fn run_hid_worker_loop(hidapi: HidApi, data: WorkerData) {
|
||||
log::info!("HID worker loop starting.");
|
||||
|
||||
// --- 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);
|
||||
|
||||
// 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 &(ref run_lock, ref run_cvar) = &*data.run_state;
|
||||
|
||||
loop {
|
||||
// --- Check Run State ---
|
||||
let should_run = { // Scope for mutex guard
|
||||
match run_lock.lock() {
|
||||
Ok(guard) => *guard,
|
||||
Err(poisoned) => {
|
||||
log::error!("Run state mutex poisoned in worker loop!");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !should_run {
|
||||
log::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 {
|
||||
// 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) => { // 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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Device was not opened initially or failed reopen
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 !relevant_values.is_empty() {
|
||||
let modifier = data.shift_modifiers[bit_pos as usize];
|
||||
let result_bit = match modifier {
|
||||
crate::config::ShiftModifiers::OR => relevant_values.iter().any(|&v| v),
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update shared final state for UI
|
||||
if let Ok(mut guard) = data.final_shift_state_shared.lock() {
|
||||
*guard = final_state;
|
||||
}
|
||||
|
||||
// --- Write to Receiver Devices ---
|
||||
// We need to determine the correct length to send. Assuming report 4 is always ID + 2 bytes.
|
||||
const bytes_to_send: usize = 19; // Report ID (1) + Data (2)
|
||||
|
||||
let zero_buffer: [u8; bytes_to_send] = {
|
||||
let mut buf = [0u8; bytes_to_send];
|
||||
buf[0] = FEATURE_REPORT_ID; // Set Report ID 4
|
||||
// All other bytes (1-18) remain 0 for the zero state
|
||||
buf
|
||||
};
|
||||
|
||||
for (i, device_opt) in receiver_devices.iter_mut().enumerate() {
|
||||
if let Some(device) = device_opt {
|
||||
match device.send_feature_report(&zero_buffer) {
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
}
|
||||
// --- 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
|
||||
);
|
||||
|
||||
|
||||
// Send the potentially filtered feature report
|
||||
match device.send_feature_report(&filtered_write_buffer[0..bytes_to_send]) {
|
||||
Ok(_) => {
|
||||
log::debug!("Worker: Send to receiver[{}] successful.", i);
|
||||
// Successfully sent. Update UI state for this receiver.
|
||||
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);
|
||||
if let Some(shared_state) = data.receiver_states_shared.get(i) {
|
||||
if let Ok(mut guard) = shared_state.lock() { *guard = 0; } // Reset UI state
|
||||
}
|
||||
// Attempt to reopen
|
||||
*device_opt = hidapi.open(
|
||||
data.receivers_config[i].vendor_id,
|
||||
data.receivers_config[i].product_id,
|
||||
).ok().and_then(|d| {
|
||||
d.set_blocking_mode(false).ok()?;
|
||||
Some(d)
|
||||
});
|
||||
|
||||
if device_opt.is_none() {
|
||||
log::warn!("Reopen failed for receiver {}.", i);
|
||||
} else {
|
||||
log::info!("Reopen successful for receiver {}.", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e_zero) => {
|
||||
// Handle error sending the zero state reset
|
||||
log::warn!("Worker: Error sending zero state reset to receiver[{}]: {:?}", i, e_zero);
|
||||
// Reset UI state, attempt reopen
|
||||
if let Some(shared_state) = data.receiver_states_shared.get(i) {
|
||||
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
|
||||
).ok().and_then(|d| {
|
||||
d.set_blocking_mode(false).ok()?;
|
||||
Some(d)
|
||||
});
|
||||
if device_opt.is_none() {
|
||||
log::warn!("Reopen failed for receiver {}.", i);
|
||||
} else {
|
||||
log::info!("Reopen successful for receiver {}.", i);
|
||||
}
|
||||
} // End Err for zero send
|
||||
}
|
||||
} else {
|
||||
// Device not open, reset UI state
|
||||
if let Some(shared_state) = data.receiver_states_shared.get(i) {
|
||||
if let Ok(mut guard) = shared_state.lock() { *guard = 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sleep ---
|
||||
thread::sleep(Duration::from_millis(WORKER_SLEEP_MS));
|
||||
} // End loop
|
||||
|
||||
// --- Cleanup before thread exit ---
|
||||
log::info!("Worker loop finished. Performing cleanup...");
|
||||
// Optionally send a 'zero' report to all devices on exit
|
||||
let cleanup_buffer: [u8; 3] = [FEATURE_REPORT_ID, 0, 0];
|
||||
for device_opt in source_devices.iter_mut().chain(receiver_devices.iter_mut()) {
|
||||
if let Some(device) = device_opt {
|
||||
if let Err(e) = device.send_feature_report(&cleanup_buffer) {
|
||||
log::warn!("Error sending cleanup report: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("Worker thread cleanup complete. Exiting.");
|
||||
// HidApi and HidDevices are dropped automatically here, closing handles.
|
||||
}
|
||||
1112
src/main.rs
1112
src/main.rs
File diff suppressed because it is too large
Load Diff
7
src/state.rs
Normal file
7
src/state.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Represents the current high-level state of the application UI
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum State {
|
||||
Initialising, // App is starting, loading config, doing initial scan
|
||||
Running, // Main operational state, showing devices and controls
|
||||
About, // Showing the about screen
|
||||
}
|
||||
561
src/ui.rs
Normal file
561
src/ui.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
use crate::about;
|
||||
use crate::config::{ShiftModifiers};
|
||||
use crate::device::VpcDevice; // Assuming VpcDevice has Display impl
|
||||
use crate::{ShiftTool, INITIAL_HEIGHT, INITIAL_WIDTH, PROGRAM_TITLE}; // Import main struct
|
||||
use crate::state::State;
|
||||
use crate::util::read_bit; // Import utility
|
||||
use eframe::egui::{self, Color32, Context, ScrollArea, Ui};
|
||||
|
||||
const DISABLED_COLOR: Color32 = Color32::from_rgb(255, 0, 0); // Red for disabled
|
||||
|
||||
// Keep UI drawing functions associated with ShiftTool
|
||||
impl ShiftTool {
|
||||
// --- Button/Action Handlers (called from draw_running_state) ---
|
||||
|
||||
fn handle_start_stop_toggle(&mut self) {
|
||||
if self.config.data.sources.is_empty()
|
||||
|| self.config.data.receivers.is_empty()
|
||||
{
|
||||
log::warn!("Start/Stop ignored: No source or receiver selected.");
|
||||
return; // Don't toggle if no devices configured
|
||||
}
|
||||
|
||||
let was_started;
|
||||
{
|
||||
let &(ref lock, ref cvar) = &*self.thread_state;
|
||||
let mut started_guard = lock.lock().expect("Thread state mutex poisoned");
|
||||
was_started = *started_guard;
|
||||
*started_guard = !was_started; // Toggle the state
|
||||
log::info!("Toggled worker thread state to: {}", *started_guard);
|
||||
cvar.notify_all(); // Notify thread if it was waiting
|
||||
} // Mutex guard dropped here
|
||||
|
||||
if !was_started {
|
||||
// If we just started it
|
||||
if !self.spawn_worker() {
|
||||
// If spawning failed, revert the state
|
||||
log::error!("Worker thread failed to spawn, reverting state.");
|
||||
let &(ref lock, ref cvar) = &*self.thread_state;
|
||||
let mut started_guard = lock.lock().expect("Thread state mutex poisoned");
|
||||
*started_guard = false;
|
||||
cvar.notify_all();
|
||||
} else {
|
||||
log::info!("Worker thread started.");
|
||||
// Save config on start
|
||||
if let Err(e) = self.config.save() {
|
||||
log::error!("Failed to save config on start: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we just stopped it
|
||||
log::info!("Worker thread stopped.");
|
||||
self.stop_worker_cleanup(); // Perform cleanup actions
|
||||
// Save config on stop
|
||||
if let Err(e) = self.config.save() {
|
||||
log::error!("Failed to save config on stop: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_add_source(&mut self) {
|
||||
self.add_source_state(); // Add state tracking
|
||||
self.config.data.sources.push(Default::default()); // Add config entry
|
||||
log::debug!("Added source device slot.");
|
||||
}
|
||||
|
||||
fn handle_remove_source(&mut self) {
|
||||
if self.config.data.sources.len() > 1 {
|
||||
self.source_states.pop();
|
||||
self.config.data.sources.pop();
|
||||
log::debug!("Removed last source device slot.");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_add_receiver(&mut self) {
|
||||
self.add_receiver_state(); // Add state tracking
|
||||
self.config.data.receivers.push(Default::default()); // Add config entry
|
||||
log::debug!("Added receiver device slot.");
|
||||
}
|
||||
|
||||
fn handle_remove_receiver(&mut self) {
|
||||
if !self.config.data.receivers.is_empty() {
|
||||
self.receiver_states.pop();
|
||||
self.config.data.receivers.pop();
|
||||
log::debug!("Removed last receiver device slot.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI Drawing Functions ---
|
||||
|
||||
pub(crate) fn draw_about_screen(app: &mut ShiftTool, ui: &mut Ui) {
|
||||
ui.set_width(INITIAL_WIDTH);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading(format!("About {}", PROGRAM_TITLE));
|
||||
ui.separator();
|
||||
for line in about::about() {
|
||||
ui.label(line);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("OK").clicked() {
|
||||
app.state = State::Running;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn draw_running_state(
|
||||
app: &mut ShiftTool,
|
||||
ui: &mut Ui,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let thread_running = app.get_thread_status();
|
||||
app.refresh_devices(); // Need to be careful about frequent HID API calls
|
||||
|
||||
if app.config.data.sources.is_empty() {
|
||||
// Ensure at least one source slot exists initially
|
||||
app.handle_add_source();
|
||||
}
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(&mut columns[0], |ui| {
|
||||
ui.vertical(|ui| {
|
||||
draw_sources_section(app, ui, thread_running);
|
||||
ui.separator();
|
||||
draw_rules_section(app, ui, thread_running);
|
||||
ui.separator();
|
||||
draw_receivers_section(app, ui, thread_running);
|
||||
ui.add_space(10.0);
|
||||
});
|
||||
});
|
||||
|
||||
columns[1].vertical(|ui| {
|
||||
draw_control_buttons(app, ui, ctx, thread_running);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_sources_section(
|
||||
app: &mut ShiftTool,
|
||||
ui: &mut Ui,
|
||||
thread_running: bool,
|
||||
) {
|
||||
ui.heading("Sources");
|
||||
for i in 0..app.config.data.sources.len() {
|
||||
// --- Immutable Operations First ---
|
||||
let saved_config_for_find = app.config.data.sources[i].clone();
|
||||
let selected_device_idx = crate::device::find_device_index_for_saved(
|
||||
&app.device_list, // Pass immutable borrow of device_list
|
||||
&saved_config_for_find,
|
||||
);
|
||||
|
||||
// --- Now get mutable borrow for UI elements that might change config ---
|
||||
let source_config = &mut app.config.data.sources[i];
|
||||
let device_list = &app.device_list; // Re-borrow immutably (allowed alongside mutable borrow of a *different* field)
|
||||
let source_states = &app.source_states;
|
||||
|
||||
let vid = source_config.vendor_id;
|
||||
let pid = source_config.product_id;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Source {}:", i + 1));
|
||||
// Device Selector Combo Box
|
||||
device_selector_combo(
|
||||
ui,
|
||||
format!("source_combo_{}", i),
|
||||
device_list, // Pass immutable borrow
|
||||
selected_device_idx,
|
||||
|selected_idx| {
|
||||
if selected_idx < device_list.len() { // Bounds check
|
||||
source_config.vendor_id = device_list[selected_idx].vendor_id;
|
||||
source_config.product_id = device_list[selected_idx].product_id;
|
||||
source_config.serial_number =
|
||||
device_list[selected_idx].serial_number.clone();
|
||||
}
|
||||
},
|
||||
thread_running,
|
||||
);
|
||||
}); // Mutable borrow of source_config might end here or after status bits
|
||||
|
||||
// Draw status bits for this source
|
||||
if let Some(state_arc) = source_states.get(i) {
|
||||
let state_val = match state_arc.lock() { // Use match
|
||||
Ok(guard) => {
|
||||
log::debug!("UI: Reading source_states[{}] = {}", i, *guard);
|
||||
*guard // Dereference the guard to get the value
|
||||
}
|
||||
Err(poisoned) => {
|
||||
log::error!("UI: Mutex poisoned for source_states[{}]!", i);
|
||||
**poisoned.get_ref() // Try to get value anyway
|
||||
}
|
||||
};
|
||||
|
||||
// Pass mutable borrow of state_enabled part of source_config
|
||||
draw_status_bits(
|
||||
ui,
|
||||
"Shift:",
|
||||
state_val,
|
||||
&mut source_config.state_enabled,
|
||||
vid,
|
||||
pid,
|
||||
thread_running,
|
||||
thread_running,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(Color32::RED, "Error: State mismatch");
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
} // Mutable borrow of source_config definitely ends here
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
|
||||
fn draw_rules_section(
|
||||
app: &mut ShiftTool,
|
||||
ui: &mut Ui,
|
||||
thread_running: bool,
|
||||
) {
|
||||
ui.heading("Rules & Result");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Rules:");
|
||||
ui.add_enabled_ui(!thread_running, |ui| {
|
||||
for j in 0..8 {
|
||||
let current_modifier = app.config.data.shift_modifiers[j];
|
||||
if ui
|
||||
.selectable_label(false, format!("{}", current_modifier))
|
||||
.clicked()
|
||||
{
|
||||
// Cycle through modifiers on click
|
||||
app.config.data.shift_modifiers[j] = match current_modifier {
|
||||
ShiftModifiers::OR => ShiftModifiers::AND,
|
||||
ShiftModifiers::AND => ShiftModifiers::XOR,
|
||||
ShiftModifiers::XOR => ShiftModifiers::OR,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Display combined result state
|
||||
let final_state_val = *app.shift_state.lock().unwrap();
|
||||
draw_status_bits(
|
||||
ui,
|
||||
"Result:",
|
||||
final_state_val,
|
||||
&mut [true; 8], // Pass dummy array
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
ui.add_space(10.0); // Space after the section
|
||||
}
|
||||
|
||||
fn draw_receivers_section(
|
||||
app: &mut ShiftTool,
|
||||
ui: &mut Ui,
|
||||
thread_running: bool,
|
||||
) {
|
||||
ui.heading("Receivers");
|
||||
if app.config.data.receivers.is_empty() {
|
||||
ui.label("(Add a receiver using the controls on the right)");
|
||||
}
|
||||
// Iterate by index
|
||||
for i in 0..app.config.data.receivers.len() {
|
||||
// --- Immutable Operations First ---
|
||||
let saved_config_for_find = app.config.data.receivers[i].clone();
|
||||
let selected_device_idx = crate::device::find_device_index_for_saved(
|
||||
&app.device_list,
|
||||
&saved_config_for_find,
|
||||
);
|
||||
|
||||
// --- Mutable Borrow Scope ---
|
||||
let receiver_config = &mut app.config.data.receivers[i];
|
||||
let device_list = &app.device_list;
|
||||
let receiver_states = &app.receiver_states;
|
||||
|
||||
let vid = receiver_config.vendor_id;
|
||||
let pid = receiver_config.product_id;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Receiver {}:", i + 1));
|
||||
device_selector_combo(
|
||||
ui,
|
||||
format!("receiver_combo_{}", i),
|
||||
device_list,
|
||||
selected_device_idx,
|
||||
|selected_idx| {
|
||||
if selected_idx < device_list.len() { // Bounds check
|
||||
receiver_config.vendor_id = device_list[selected_idx].vendor_id;
|
||||
receiver_config.product_id = device_list[selected_idx].product_id;
|
||||
receiver_config.serial_number =
|
||||
device_list[selected_idx].serial_number.clone();
|
||||
}
|
||||
},
|
||||
thread_running,
|
||||
);
|
||||
}); // Mut borrow might end here
|
||||
|
||||
if let Some(state_arc) = receiver_states.get(i) {
|
||||
let state_val = match state_arc.lock() { // Use match
|
||||
Ok(guard) => {
|
||||
log::debug!("UI: Reading receiver_states[{}] = {}", i, *guard);
|
||||
*guard // Dereference the guard to get the value
|
||||
}
|
||||
Err(poisoned) => {
|
||||
log::error!("UI: Mutex poisoned for receiver_states[{}]!", i);
|
||||
**poisoned.get_ref() // Try to get value anyway
|
||||
}
|
||||
};
|
||||
draw_status_bits(
|
||||
ui,
|
||||
"Shift:",
|
||||
state_val,
|
||||
&mut receiver_config.state_enabled, // Pass mut borrow
|
||||
vid,
|
||||
pid,
|
||||
thread_running,
|
||||
thread_running,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(Color32::RED, "Error: State mismatch");
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
} // Mut borrow ends here
|
||||
}
|
||||
|
||||
// --- UI Helper Widgets ---
|
||||
|
||||
/// Creates a ComboBox for selecting a device.
|
||||
fn device_selector_combo(
|
||||
ui: &mut Ui,
|
||||
id_source: impl std::hash::Hash,
|
||||
device_list: &[VpcDevice],
|
||||
selected_device_idx: usize,
|
||||
mut on_select: impl FnMut(usize), // Closure called when selection changes
|
||||
disabled: bool,
|
||||
) {
|
||||
let selected_text = if selected_device_idx < device_list.len() {
|
||||
format!("{}", device_list[selected_device_idx])
|
||||
} else {
|
||||
// Handle case where index might be out of bounds after a refresh
|
||||
"-SELECT DEVICE-".to_string()
|
||||
};
|
||||
|
||||
ui.add_enabled_ui(!disabled, |ui| {
|
||||
egui::ComboBox::from_id_source(id_source)
|
||||
.width(300.0) // Adjust width as needed
|
||||
.selected_text(selected_text)
|
||||
.show_ui(ui, |ui| {
|
||||
for (j, device) in device_list.iter().enumerate() {
|
||||
// Use selectable_value to handle selection logic
|
||||
if ui
|
||||
.selectable_label(
|
||||
j == selected_device_idx,
|
||||
format!("{}", device),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if j != selected_device_idx {
|
||||
on_select(j); // Call the provided closure
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draws the row of shift status bits (1-5, DTNT, ZOOM, TRIM).
|
||||
fn draw_status_bits(
|
||||
ui: &mut Ui,
|
||||
label: &str,
|
||||
state_value: u16,
|
||||
enabled_mask: &mut [bool; 8],
|
||||
vendor_id: u16,
|
||||
product_id: u16,
|
||||
thread_running: bool,
|
||||
bits_disabled: bool, // If the whole row should be unclickable
|
||||
show_online_status: bool,
|
||||
) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(label);
|
||||
log::debug!("draw_status_bits received state_value: {}", state_value);
|
||||
|
||||
ui.add_enabled_ui(!bits_disabled, |ui| {
|
||||
// Bits 0-4 (Shift 1-5)
|
||||
for j in 0..5u8 {
|
||||
let bit_is_set = read_bit(state_value, j);
|
||||
let is_enabled = enabled_mask[j as usize];
|
||||
let color = if !is_enabled {
|
||||
DISABLED_COLOR
|
||||
} else {
|
||||
Color32::TRANSPARENT // Default background
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
" Bit {}: state={}, enabled={}, calculated_selected={}",
|
||||
j, state_value, is_enabled, bit_is_set
|
||||
);
|
||||
|
||||
// Use selectable_value for clickable behavior
|
||||
if ui
|
||||
.selectable_label(
|
||||
bit_is_set,
|
||||
egui::RichText::new(format!("{}", j + 1))
|
||||
.background_color(color),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
// Toggle the enabled state if clicked
|
||||
enabled_mask[j as usize] = !is_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Bits (DTNT, ZOOM, TRIM) - Assuming order 5, 6, 7
|
||||
let special_bits = [("DTNT", 5u8), ("ZOOM", 6u8), ("TRIM", 7u8)];
|
||||
for (name, bit_pos) in special_bits {
|
||||
let bit_is_set = read_bit(state_value, bit_pos);
|
||||
let is_enabled = enabled_mask[bit_pos as usize];
|
||||
let color = if !is_enabled {
|
||||
DISABLED_COLOR
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
log::debug!(
|
||||
" Bit {}: name={}, state={}, enabled={}, calculated_selected={}",
|
||||
bit_pos, name, state_value, is_enabled, bit_is_set
|
||||
);
|
||||
|
||||
if ui
|
||||
.selectable_label(
|
||||
bit_is_set,
|
||||
egui::RichText::new(name).background_color(color),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
enabled_mask[bit_pos as usize] = !is_enabled;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Draw the Online/Offline Status ---
|
||||
// Add some spacing before the status
|
||||
if show_online_status {
|
||||
ui.add_space(15.0); // Adjust as needed
|
||||
|
||||
let is_configured = vendor_id != 0 && product_id != 0;
|
||||
let (text, color) = if thread_running && is_configured {
|
||||
("ONLINE", Color32::GREEN)
|
||||
} else if !is_configured {
|
||||
("UNCONFIGURED", Color32::YELLOW)
|
||||
} else {
|
||||
("OFFLINE", Color32::GRAY)
|
||||
};
|
||||
// Add the status label directly here
|
||||
ui.label(egui::RichText::new(text).color(color));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draws the ONLINE/OFFLINE status indicator.
|
||||
fn draw_online_status(
|
||||
ui: &mut Ui,
|
||||
saved_device_config: &crate::device::SavedDevice, // Pass the config for this slot
|
||||
thread_running: bool,
|
||||
) {
|
||||
// Infer status: Online if thread is running AND device is configured (VID/PID != 0)
|
||||
let is_configured = saved_device_config.vendor_id != 0 && saved_device_config.product_id != 0;
|
||||
|
||||
// Determine status text and color
|
||||
let (text, color) = if thread_running && is_configured {
|
||||
// We assume the worker *tries* to talk to configured devices.
|
||||
// A more advanced check could involve reading another shared state
|
||||
// updated by the worker indicating recent success/failure for this device.
|
||||
("ONLINE", Color32::GREEN)
|
||||
} else if !is_configured {
|
||||
("UNCONFIGURED", Color32::YELLOW) // Show if slot is empty
|
||||
} else { // Thread not running or device not configured
|
||||
("OFFLINE", Color32::GRAY)
|
||||
};
|
||||
|
||||
// Use selectable_label for consistent look, but make it non-interactive
|
||||
// Set 'selected' argument to false as it's just a status display
|
||||
ui.selectable_label(false, egui::RichText::new(text).color(color));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Draws the control buttons in the right column.
|
||||
fn draw_control_buttons(
|
||||
app: &mut ShiftTool,
|
||||
ui: &mut Ui,
|
||||
ctx: &Context,
|
||||
thread_running: bool,
|
||||
) {
|
||||
// Start/Stop Button
|
||||
let (start_stop_text, start_stop_color) = if thread_running {
|
||||
("Stop", DISABLED_COLOR)
|
||||
} else {
|
||||
("Start", Color32::GREEN) // Use Green for Start
|
||||
};
|
||||
if ui
|
||||
.button(
|
||||
egui::RichText::new(start_stop_text)
|
||||
.color(Color32::BLACK) // Text color
|
||||
.background_color(start_stop_color),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
app.handle_start_stop_toggle();
|
||||
}
|
||||
|
||||
// ui.separator();
|
||||
|
||||
// Add/Remove Source Buttons
|
||||
if ui.add_enabled(!thread_running, egui::Button::new("Add Source")).clicked() {
|
||||
app.handle_add_source();
|
||||
}
|
||||
if app.config.data.sources.len() > 1 { // Only show remove if more than 1
|
||||
if ui.add_enabled(!thread_running, egui::Button::new("Remove Source")).clicked() {
|
||||
app.handle_remove_source();
|
||||
}
|
||||
}
|
||||
|
||||
// ui.separator();
|
||||
|
||||
// Add/Remove Receiver Buttons
|
||||
if ui.add_enabled(!thread_running, egui::Button::new("Add Receiver")).clicked() {
|
||||
app.handle_add_receiver();
|
||||
}
|
||||
if !app.config.data.receivers.is_empty() { // Only show remove if > 0
|
||||
if ui.add_enabled(!thread_running, egui::Button::new("Remove Receiver")).clicked() {
|
||||
app.handle_remove_receiver();
|
||||
}
|
||||
}
|
||||
|
||||
// ui.separator();
|
||||
|
||||
// Other Buttons
|
||||
// if ui.add_enabled(!thread_running, egui::Button::new("Save Config")).clicked() {
|
||||
// if let Err(e) = app.config.save() {
|
||||
// log::error!("Failed to manually save config: {}", e);
|
||||
// // Optionally show feedback to user in UI
|
||||
// } else {
|
||||
// log::info!("Configuration saved manually.");
|
||||
// }
|
||||
// }
|
||||
|
||||
if ui.button("About").clicked() {
|
||||
app.state = State::About;
|
||||
}
|
||||
|
||||
if ui.button("Exit").clicked() {
|
||||
// Ask eframe to close the window. `on_exit` will be called.
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
54
src/util.rs
Normal file
54
src/util.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use log::warn; // Use log crate
|
||||
|
||||
/// Reads a specific bit from a u16 value.
|
||||
/// `position` is 0-indexed (0-15).
|
||||
pub(crate) fn read_bit(value: u16, position: u8) -> bool {
|
||||
if position > 15 {
|
||||
warn!("read_bit called with invalid position: {}", position);
|
||||
return false;
|
||||
}
|
||||
(value & (1 << position)) != 0
|
||||
}
|
||||
|
||||
/// Sets or clears a specific bit in a u8 value.
|
||||
/// `bit_position` is 0-indexed (0-7).
|
||||
/// Returns the modified u8 value.
|
||||
pub(crate) fn set_bit(value: u8, bit_position: u8, bit_value: bool) -> u8 {
|
||||
if bit_position > 7 {
|
||||
warn!("set_bit called with invalid position: {}", bit_position);
|
||||
return value; // Return original value on error
|
||||
}
|
||||
if bit_value {
|
||||
value | (1 << bit_position) // Set the bit to 1
|
||||
} else {
|
||||
value & !(1 << bit_position) // Set the bit to 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines high and low bytes into a u16 value.
|
||||
pub(crate) fn merge_u8_into_u16(high_byte: u8, low_byte: u8) -> u16 {
|
||||
(high_byte as u16) << 8 | (low_byte as u16)
|
||||
}
|
||||
|
||||
/// Checks if a device firmware string is supported.
|
||||
/// 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; }
|
||||
|
||||
// Example fixed list check:
|
||||
// let supported_firmware = [
|
||||
// "VIRPIL Controls 20220720",
|
||||
// "VIRPIL Controls 20230328",
|
||||
// "VIRPIL Controls 20240323",
|
||||
// ];
|
||||
// supported_firmware.contains(&firmware_string.as_str())
|
||||
|
||||
if firmware_string.is_empty() || firmware_string == "Unknown Firmware" {
|
||||
warn!("Device has missing or unknown firmware string.");
|
||||
// Decide if these should be allowed or not. Allowing for now.
|
||||
}
|
||||
true
|
||||
}
|
||||
Reference in New Issue
Block a user