Code rewrite to make the code more modular.
Fixed issue where newer firmware "20241226" wouldn't actually work correctly. Needs testing on older firmware to see if they still work
This commit is contained in:
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.
|
||||
}
|
||||
1124
src/main.rs
1124
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