From 4286fb1003fca46249c6e57028e6804088bd5dc9 Mon Sep 17 00:00:00 2001 From: oceans2alaska Date: Thu, 23 Jul 2026 08:32:54 -0700 Subject: [PATCH] Report only real device data and remove unverifiable fields - Drop simulated-controller fallback: simulation now only runs when explicitly enabled in Settings (or outside Tauri), so the app no longer claims a controller is connected when none is plugged in - Derive connection type from the kernel bus type (USB/Bluetooth) instead of an unreliable udev property - Read real battery percentage from /sys/class/power_supply; hide the field entirely when the device reports none - Remove Input mode and Firmware version fields (not obtainable) - Query virtual controller status from the backend instead of a hardcoded per-device flag - Exclude the app's own uinput virtual controller from device discovery - Rescan for controllers indefinitely while disconnected so a controller plugged in at any time is detected without restarting - Strip leftover debug instrumentation from the previous session Co-authored-by: Cursor --- src-tauri/Cargo.lock | 108 +-------- src-tauri/Cargo.toml | 1 - src-tauri/src/commands/mapping.rs | 76 ------ src-tauri/src/device_detection.rs | 101 ++++---- src-tauri/src/evdev_reader.rs | 84 ------- src-tauri/src/lib.rs | 1 - src/components/layout/TopBar.tsx | 4 +- src/pages/Dashboard.tsx | 16 +- src/pages/DeviceInfo.tsx | 20 +- src/pages/Settings.tsx | 3 - src/services/mockControllerBackend.ts | 10 +- src/services/tauriControllerService.ts | 1 + src/stores/controllerStore.ts | 324 +++++++++++++------------ src/stores/profileStore.ts | 9 - src/stores/settingsStore.ts | 7 +- src/types/controller.ts | 8 +- 16 files changed, 242 insertions(+), 531 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fa2b553..97d7b93 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -888,7 +888,6 @@ dependencies = [ "tauri-plugin-os", "tempfile", "thiserror 2.0.19", - "udev", "uuid", ] @@ -1522,12 +1521,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -1807,17 +1800,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2018,16 +2000,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libudev-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" -dependencies = [ - "libc", - "pkg-config", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2676,7 +2648,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.2", + "hermit-abi", "pin-project-lite", "rustix", "windows-sys 0.61.2", @@ -4242,18 +4214,6 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "udev" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" -dependencies = [ - "io-lifetimes", - "libc", - "libudev-sys", - "pkg-config", -] - [[package]] name = "uds_windows" version = "1.2.1" @@ -4791,15 +4751,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -4842,21 +4793,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4914,12 +4850,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4938,12 +4868,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4962,12 +4886,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4998,12 +4916,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5022,12 +4934,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5046,12 +4952,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5070,12 +4970,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e66ae2f..cfeb6ec 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,7 +32,6 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] } uuid = { version = "1", features = ["v4"] } dirs = "6" evdev = "0.13" -udev = "0.9" sysinfo = "0.37" [dev-dependencies] diff --git a/src-tauri/src/commands/mapping.rs b/src-tauri/src/commands/mapping.rs index 01ba106..920e14b 100644 --- a/src-tauri/src/commands/mapping.rs +++ b/src-tauri/src/commands/mapping.rs @@ -1,62 +1,11 @@ -use std::fs::OpenOptions; -use std::io::Write; -use std::time::{SystemTime, UNIX_EPOCH}; - use crate::error::{AppError, AppResult}; use crate::state::AppState; -fn debug_log(hypothesis_id: &str, message: &str, data: serde_json::Value) { - let entry = serde_json::json!({ - "sessionId": "2801e0", - "runId": "initial", - "hypothesisId": hypothesis_id, - "location": "src-tauri/src/commands/mapping.rs", - "message": message, - "data": data, - "timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis(), - }); - match OpenOptions::new() - .create(true) - .append(true) - .open("/home/notdave/Documents/Code Projects/Apps/Linux 8bitdo ultimate interface/.cursor/debug-2801e0.log") - { - Ok(mut file) => { - if let Err(error) = writeln!(file, "{entry}") { - // #region agent log - eprintln!("H13 debug log write failed: {error}"); - // #endregion - } - } - Err(error) => { - // #region agent log - eprintln!("H13 debug log open failed: {error}"); - // #endregion - } - } -} - -pub fn log_remapping_pipeline_startup() { - // #region agent log - debug_log( - "H12", - "Native remapping pipeline initialized", - serde_json::json!({}), - ); - // #endregion -} - /// Creates a managed virtual controller after a deliberate user action. /// Event forwarding remains disabled until the physical device is safely /// grabbed by the pipeline; this avoids accidental duplicate game input. #[tauri::command] pub fn create_virtual_controller(state: tauri::State<'_, AppState>) -> AppResult<()> { - // #region agent log - debug_log( - "H9", - "Virtual controller creation requested", - serde_json::json!({}), - ); - // #endregion let mut virtual_controller = state .virtual_controller .lock() @@ -69,13 +18,6 @@ pub fn create_virtual_controller(state: tauri::State<'_, AppState>) -> AppResult #[tauri::command] pub fn destroy_virtual_controller(state: tauri::State<'_, AppState>) -> AppResult<()> { - // #region agent log - debug_log( - "H9", - "Virtual controller destruction requested", - serde_json::json!({}), - ); - // #endregion let mut virtual_controller = state .virtual_controller .lock() @@ -101,17 +43,6 @@ pub fn apply_remapping_profile( state: tauri::State<'_, AppState>, profile: crate::profile_schema::Profile, ) -> AppResult<()> { - // #region agent log - debug_log( - "H10", - "Remapping profile received by native pipeline", - serde_json::json!({ - "profileId": profile.id, - "leftSensitivity": profile.left_trigger.sensitivity, - "rightSensitivity": profile.right_trigger.sensitivity, - }), - ); - // #endregion let mut active_profile = state .active_remap_profile .lock() @@ -139,12 +70,5 @@ pub fn set_remapping_enabled(state: tauri::State<'_, AppState>, enabled: bool) - .lock() .map_err(|_| AppError::Other("remapping state lock poisoned".to_string()))?; *remapping_enabled = enabled; - // #region agent log - debug_log( - "H10", - "Native remapping state changed", - serde_json::json!({"enabled": enabled}), - ); - // #endregion Ok(()) } diff --git a/src-tauri/src/device_detection.rs b/src-tauri/src/device_detection.rs index ca3a68c..da2677a 100644 --- a/src-tauri/src/device_detection.rs +++ b/src-tauri/src/device_detection.rs @@ -2,18 +2,21 @@ //! //! This module only queries kernel-provided evdev metadata and capability //! bitsets. It never sends vendor commands or accesses firmware protocols. +//! Every reported field is backed by real kernel data; anything the kernel +//! does not expose (e.g. battery on wired controllers) is reported as absent +//! rather than guessed. +use std::fs; use std::path::Path; -use std::{ - fs::OpenOptions, - io::Write, - time::{SystemTime, UNIX_EPOCH}, -}; -use evdev::{AbsoluteAxisCode, Device, KeyCode}; +use evdev::{AbsoluteAxisCode, BusType, Device, KeyCode}; use serde::Serialize; const EIGHTBITDO_VENDOR_ID: u16 = 0x2dc8; +/// Product id of the uinput device this app creates (see `uinput_writer`). +/// It must be excluded from discovery so the app never lists its own output +/// as a physical controller. +const VIRTUAL_CONTROLLER_PRODUCT_ID: u16 = 0x8bcc; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -56,10 +59,7 @@ pub struct ControllerInfo { pub device_path: String, pub vendor_id: u16, pub product_id: u16, - pub mode: String, pub battery_percent: Option, - pub firmware_version: Option, - pub virtual_controller_active: bool, pub capabilities: DeviceCapabilities, pub is_simulated: bool, } @@ -115,25 +115,39 @@ fn is_gamepad(device: &Device) -> bool { has_gamepad_button && has_stick_axis } -fn connection_for(path: &Path) -> String { - let udev_path = udev::Device::from_subsystem_sysname( - "input".to_string(), - path.file_name() - .and_then(|name| name.to_str()) - .unwrap_or_default() - .to_string(), - ); - if let Ok(device) = udev_path { - let mut properties = device.properties(); - if let Some(property) = properties.find(|property| property.name() == "ID_BUS") { - return match property.value().to_string_lossy().as_ref() { - "bluetooth" => "bluetooth".to_string(), - "usb" => "usb".to_string(), - _ => "unknown".to_string(), - }; +/// Maps the kernel-reported bus type to a connection kind. This is +/// authoritative: the kernel always knows which bus a device enumerated on. +/// A 2.4GHz dongle presents as USB because the dongle itself is a USB device. +fn connection_for(bustype: u16) -> String { + match BusType(bustype) { + BusType::BUS_USB => "usb".to_string(), + BusType::BUS_BLUETOOTH => "bluetooth".to_string(), + _ => "unknown".to_string(), + } +} + +/// Reads the battery percentage the kernel reports for this input device, if +/// any. Bluetooth HID controllers expose a `power_supply` entry whose device +/// is an ancestor of the input node; wired controllers usually expose none, +/// in which case `None` is returned and the UI omits the field. +fn battery_percent_for(event_path: &Path) -> Option { + let event_name = event_path.file_name()?.to_str()?; + let input_sysfs = fs::canonicalize(format!("/sys/class/input/{event_name}")).ok()?; + for entry in fs::read_dir("/sys/class/power_supply").ok()?.flatten() { + let supply = entry.path(); + let Ok(supply_device) = fs::canonicalize(supply.join("device")) else { + continue; + }; + if !input_sysfs.starts_with(&supply_device) { + continue; + } + if let Ok(capacity) = fs::read_to_string(supply.join("capacity")) { + if let Ok(percent) = capacity.trim().parse::() { + return Some(percent.min(100)); + } } } - "unknown".to_string() + None } pub fn probe_device(path: &Path, device: &Device) -> Option { @@ -145,6 +159,12 @@ pub fn probe_device(path: &Path, device: &Device) -> Option { let kernel_id = raw_id.as_ref(); let vendor_id = kernel_id.vendor; let product_id = kernel_id.product; + + // Never report this app's own virtual output device as a controller. + if vendor_id == EIGHTBITDO_VENDOR_ID && product_id == VIRTUAL_CONTROLLER_PRODUCT_ID { + return None; + } + let fallback_name = device.name().unwrap_or("Unknown Controller"); let name = known_name(vendor_id, product_id, fallback_name); @@ -184,24 +204,17 @@ pub fn probe_device(path: &Path, device: &Device) -> Option { let has_rumble = device .supported_ff() .is_some_and(|ff| ff.iter().next().is_some()); - let is_eightbitdo = vendor_id == EIGHTBITDO_VENDOR_ID; + let battery_percent = battery_percent_for(path); let path_text = path.to_string_lossy().into_owned(); Some(ControllerInfo { id: path_text.clone(), name, - connection: connection_for(path), + connection: connection_for(kernel_id.bustype), device_path: path_text, vendor_id, product_id, - mode: if is_eightbitdo { - "xinput".to_string() - } else { - "unknown".to_string() - }, - battery_percent: None, - firmware_version: None, - virtual_controller_active: false, + battery_percent, capabilities: DeviceCapabilities { vendor_id, product_id, @@ -209,7 +222,7 @@ pub fn probe_device(path: &Path, device: &Device) -> Option { paddle_count: 0, has_gyro: false, has_rumble, - has_battery: false, + has_battery: battery_percent.is_some(), buttons, axes, }, @@ -222,19 +235,5 @@ pub fn enumerate_controllers() -> Vec { .filter_map(|(path, device)| probe_device(&path, &device)) .collect::>(); controllers.sort_by(|a, b| a.name.cmp(&b.name)); - // #region agent log - let entry = serde_json::json!({ - "sessionId": "2801e0", - "runId": "initial", - "hypothesisId": "H4", - "location": "src-tauri/src/device_detection.rs:enumerate_controllers", - "message": "Controller discovery completed", - "data": {"count": controllers.len(), "controllers": controllers.iter().map(|controller| serde_json::json!({"name": controller.name, "connection": controller.connection, "path": controller.device_path, "vendorId": controller.vendor_id, "productId": controller.product_id})).collect::>()}, - "timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis(), - }); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("/home/notdave/Documents/Code Projects/Apps/Linux 8bitdo ultimate interface/.cursor/debug-2801e0.log") { - let _ = writeln!(file, "{entry}"); - } - // #endregion controllers } diff --git a/src-tauri/src/evdev_reader.rs b/src-tauri/src/evdev_reader.rs index ff48291..a143db1 100644 --- a/src-tauri/src/evdev_reader.rs +++ b/src-tauri/src/evdev_reader.rs @@ -5,10 +5,7 @@ //! behavior. use std::collections::HashMap; -use std::fs::OpenOptions; -use std::io::Write; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; @@ -21,27 +18,6 @@ use crate::state::AppState; pub const INPUT_EVENT_NAME: &str = "controller-input"; pub const READER_STATUS_EVENT_NAME: &str = "controller-reader-status"; -static LEFT_TRIGGER_OUTPUT_LOGGED: AtomicBool = AtomicBool::new(false); -static RIGHT_TRIGGER_OUTPUT_LOGGED: AtomicBool = AtomicBool::new(false); - -fn debug_log(hypothesis_id: &str, message: &str, data: serde_json::Value) { - let entry = serde_json::json!({ - "sessionId": "2801e0", - "runId": "initial", - "hypothesisId": hypothesis_id, - "location": "src-tauri/src/evdev_reader.rs", - "message": message, - "data": data, - "timestamp": timestamp_ms(), - }); - if let Ok(mut file) = OpenOptions::new() - .create(true) - .append(true) - .open("/home/notdave/Documents/Code Projects/Apps/Linux 8bitdo ultimate interface/.cursor/debug-2801e0.log") - { - let _ = writeln!(file, "{entry}"); - } -} #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -146,15 +122,6 @@ fn forward_event( .unwrap_or((0, 255)); let raw = normalized(value, minimum, maximum) as f32; let transformed = crate::input_transform::transform_trigger(raw, &profile.left_trigger); - if !LEFT_TRIGGER_OUTPUT_LOGGED.swap(true, Ordering::Relaxed) { - // #region agent log - debug_log( - "H11", - "Forwarded transformed left trigger", - serde_json::json!({"raw": raw, "output": transformed, "outputValue": (transformed * 255.0).round() as i32}), - ); - // #endregion - } *AbsoluteAxisEvent::new( AbsoluteAxisCode::ABS_Z, (transformed * 255.0).round() as i32, @@ -167,15 +134,6 @@ fn forward_event( .unwrap_or((0, 255)); let raw = normalized(value, minimum, maximum) as f32; let transformed = crate::input_transform::transform_trigger(raw, &profile.right_trigger); - if !RIGHT_TRIGGER_OUTPUT_LOGGED.swap(true, Ordering::Relaxed) { - // #region agent log - debug_log( - "H11", - "Forwarded transformed right trigger", - serde_json::json!({"raw": raw, "output": transformed, "outputValue": (transformed * 255.0).round() as i32}), - ); - // #endregion - } *AbsoluteAxisEvent::new( AbsoluteAxisCode::ABS_RZ, (transformed * 255.0).round() as i32, @@ -215,36 +173,15 @@ pub fn start( .lock() .map_err(|_| AppError::Other("device reader state lock poisoned".to_string()))?; if !readers.insert(device_path.clone()) { - // #region agent log - debug_log( - "H2", - "Reader start skipped because path is already active", - serde_json::json!({"devicePath": device_path}), - ); - // #endregion return Ok(()); } } - // #region agent log - debug_log( - "H2", - "Reader start requested", - serde_json::json!({"devicePath": device_path}), - ); - // #endregion thread::Builder::new() .name(format!("evdev-reader:{device_path}")) .spawn(move || { let result = read_loop(&app, &device_path); if let Err(error) = result { - // #region agent log - debug_log( - "H2", - "Reader loop ended with error", - serde_json::json!({"devicePath": device_path, "error": error.to_string()}), - ); - // #endregion log::warn!("evdev reader for {device_path} stopped: {error}"); if let Ok(mut readers) = app.state::().active_readers.lock() { readers.remove(&device_path); @@ -275,13 +212,6 @@ fn read_loop(app: &AppHandle, device_path: &str) -> AppResult<()> { .map_err(AppError::Io)? .map(|(axis, info)| (axis, (info.as_ref().minimum, info.as_ref().maximum))) .collect::>(); - // #region agent log - debug_log( - "H2", - "Reader opened device", - serde_json::json!({"devicePath": device_path, "axisRangeCount": axis_ranges.len()}), - ); - // #endregion let _ = app.emit( READER_STATUS_EVENT_NAME, @@ -298,22 +228,8 @@ fn read_loop(app: &AppHandle, device_path: &str) -> AppResult<()> { if enabled != grabbed { if enabled { device.grab().map_err(AppError::Io)?; - // #region agent log - debug_log( - "H10", - "Physical controller exclusively grabbed for remapping", - serde_json::json!({"devicePath": device_path}), - ); - // #endregion } else { device.ungrab().map_err(AppError::Io)?; - // #region agent log - debug_log( - "H10", - "Physical controller released from remapping", - serde_json::json!({"devicePath": device_path}), - ); - // #endregion } grabbed = enabled; } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 512cbdd..c000bf1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,7 +26,6 @@ pub fn run() { .build(), ) .setup(|_app| { - crate::commands::mapping::log_remapping_pipeline_startup(); if let Err(err) = profile_manager::ensure_initialized() { log::error!("failed to initialize profile storage: {err}"); } diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 6dd3d52..70acbd7 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -1,4 +1,4 @@ -import { Bluetooth, Cable, Radio, Usb } from "lucide-react"; +import { Bluetooth, Cable, Usb } from "lucide-react"; import { useLocation } from "react-router-dom"; import { Badge } from "@/components/common/Badge"; @@ -11,14 +11,12 @@ import type { ConnectionKind } from "@/types/controller"; const CONNECTION_ICON: Record = { usb: Usb, bluetooth: Bluetooth, - receiver2_4ghz: Radio, unknown: Cable, }; const CONNECTION_LABEL: Record = { usb: "USB", bluetooth: "Bluetooth", - receiver2_4ghz: "2.4GHz Receiver", unknown: "Unknown", }; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 19cec54..b138721 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -12,6 +12,13 @@ import { useControllerStore } from "@/stores/controllerStore"; import { useProfileStore } from "@/stores/profileStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { processStick, processTrigger } from "@/utils/deadzoneMath"; +import type { ConnectionKind } from "@/types/controller"; + +const CONNECTION_LABEL: Record = { + usb: "USB", + bluetooth: "Bluetooth", + unknown: "Unknown connection", +}; export function Dashboard() { const controllers = useControllerStore((s) => s.controllers); @@ -20,6 +27,7 @@ export function Dashboard() { const pollRateHz = useControllerStore((s) => s.pollRateHz); const connectionStatus = useControllerStore((s) => s.connectionStatus); const connectionMessage = useControllerStore((s) => s.connectionMessage); + const virtualControllerActive = useControllerStore((s) => s.virtualControllerActive); const draftProfile = useProfileStore((s) => s.draftProfile); const summaries = useProfileStore((s) => s.summaries); @@ -60,16 +68,16 @@ export function Dashboard() { Connected - {activeController.mode.toUpperCase()} + {CONNECTION_LABEL[activeController.connection]} {activeController.batteryPercent !== null && ( {activeController.batteryPercent}% )} - + - Virtual controller {activeController.virtualControllerActive ? "active" : "inactive"} + Virtual controller {virtualControllerActive ? "active" : "inactive"} {activeController.isSimulated && Simulated device} @@ -79,7 +87,7 @@ export function Dashboard() {

{connectionStatus === "reconnecting" ? connectionMessage ?? "Reconnecting to controller…" - : "No controller detected. Connect an 8BitDo Ultimate controller to get started."} + : connectionMessage ?? "No controller detected. Connect an 8BitDo Ultimate controller to get started."}

)} diff --git a/src/pages/DeviceInfo.tsx b/src/pages/DeviceInfo.tsx index 2954093..89a29e4 100644 --- a/src/pages/DeviceInfo.tsx +++ b/src/pages/DeviceInfo.tsx @@ -2,14 +2,12 @@ import type { ReactNode } from "react"; import { Badge } from "@/components/common/Badge"; import { Panel } from "@/components/common/Panel"; -import { StatusDot } from "@/components/common/StatusDot"; import { PageShell } from "@/components/layout/PageShell"; import { useControllerStore } from "@/stores/controllerStore"; const CONNECTION_LABEL: Record = { usb: "USB", bluetooth: "Bluetooth", - receiver2_4ghz: "2.4GHz Receiver", unknown: "Unknown", }; @@ -66,21 +64,9 @@ export function DeviceInfo() { - - - - - - {controller.virtualControllerActive ? "Active" : "Inactive"} - - } - /> + {controller.batteryPercent !== null && ( + + )}
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index d3c3317..e67f7ff 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -33,9 +33,6 @@ export function Settings() { const [diagnostics, setDiagnostics] = useState(null); async function handleRemappingChange(enabled: boolean) { - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H14',location:'src/pages/Settings.tsx:handleRemappingChange',message:'Settings remapping toggle changed',data:{enabled},timestamp:Date.now()})}).catch(()=>{}); - // #endregion setRemappingEnabled(enabled); try { await setControllerRemapping(enabled); diff --git a/src/services/mockControllerBackend.ts b/src/services/mockControllerBackend.ts index 6f37b90..230dc2e 100644 --- a/src/services/mockControllerBackend.ts +++ b/src/services/mockControllerBackend.ts @@ -33,10 +33,7 @@ export class MockControllerBackend implements ControllerBackend { devicePath: "/dev/input/event33 (simulated)", vendorId: DEMO_DEVICE_CAPABILITIES.vendorId, productId: DEMO_DEVICE_CAPABILITIES.productId, - mode: "xinput", - batteryPercent: 82, - firmwareVersion: null, - virtualControllerActive: true, + batteryPercent: null, capabilities: DEMO_DEVICE_CAPABILITIES, isSimulated: true, }; @@ -74,9 +71,8 @@ export class MockControllerBackend implements ControllerBackend { return this.rawEventEmitter.on(callback); } - async setRemappingEnabled(enabled: boolean): Promise { - this.info = { ...this.info, virtualControllerActive: enabled }; - this.controllersEmitter.emit(this.snapshotControllers()); + async setRemappingEnabled(_enabled: boolean): Promise { + // The simulated backend has no real uinput device to toggle. } setSimulatedConnected(connected: boolean): void { diff --git a/src/services/tauriControllerService.ts b/src/services/tauriControllerService.ts index 9223ad5..3d9c4b8 100644 --- a/src/services/tauriControllerService.ts +++ b/src/services/tauriControllerService.ts @@ -36,6 +36,7 @@ export const tauriControllerService = { startReader: (devicePath: string) => invoke("start_device_reader", { devicePath }), applyRemappingProfile: (profile: Profile) => invoke("apply_remapping_profile", { profile }), setRemappingEnabled: (enabled: boolean) => invoke("set_remapping_enabled", { enabled }), + virtualControllerActive: () => invoke("virtual_controller_active"), onInput: async (callback: (event: RawInputEvent) => void): Promise<() => void> => listen(INPUT_EVENT, (event) => callback(toRawInputEvent(event.payload))), onReaderStatus: async (callback: (status: ReaderStatus) => void): Promise<() => void> => diff --git a/src/stores/controllerStore.ts b/src/stores/controllerStore.ts index 73bf31b..0a465c0 100644 --- a/src/stores/controllerStore.ts +++ b/src/stores/controllerStore.ts @@ -7,21 +7,10 @@ import type { ControllerInfo, ControllerLiveState, RawInputEvent } from "@/types const MAX_EVENT_LOG = 200; const MAX_POLL_SAMPLES = 30; -const RECONNECT_INTERVAL_MS = 1_500; -const MAX_RECONNECT_ATTEMPTS = 20; +const RESCAN_INTERVAL_MS = 2_000; export type ControllerConnectionStatus = "connected" | "reconnecting" | "disconnected" | "simulation"; -function samePhysicalController(a: ControllerInfo, b: ControllerInfo): boolean { - return ( - a.vendorId === b.vendorId && - a.productId === b.productId && - a.name === b.name && - a.connection === b.connection && - a.mode === b.mode - ); -} - function estimatePollRateHz(timestamps: number[]): number | null { if (timestamps.length < 2) return null; const span = timestamps[timestamps.length - 1] - timestamps[0]; @@ -29,6 +18,19 @@ function estimatePollRateHz(timestamps: number[]): number | null { return Math.round(((timestamps.length - 1) / span) * 1000); } +/** + * Matches a controller across reconnects, where the kernel usually assigns a + * new /dev/input/event* node (and therefore a new id). + */ +function samePhysicalController(a: ControllerInfo, b: ControllerInfo): boolean { + return ( + a.vendorId === b.vendorId && + a.productId === b.productId && + a.name === b.name && + a.connection === b.connection + ); +} + interface ControllerState { controllers: ControllerInfo[]; activeControllerId: string | null; @@ -38,6 +40,7 @@ interface ControllerState { latencyEstimateMs: number | null; connectionStatus: ControllerConnectionStatus; connectionMessage: string | null; + virtualControllerActive: boolean; initialized: boolean; usingSimulation: boolean; init: () => Promise; @@ -48,17 +51,14 @@ interface ControllerState { } let pollTimestamps: number[] = []; -let hasLoggedFirstPhysicalEvent = false; -let reconnectTimer: ReturnType | null = null; -let reconnectAttempts = 0; +let rescanTimer: ReturnType | null = null; let refreshInFlight = false; -function clearReconnectTimer(): void { - if (reconnectTimer !== null) { - clearTimeout(reconnectTimer); - reconnectTimer = null; +function clearRescanTimer(): void { + if (rescanTimer !== null) { + clearTimeout(rescanTimer); + rescanTimer = null; } - reconnectAttempts = 0; } export const useControllerStore = create((set, get) => ({ @@ -70,6 +70,7 @@ export const useControllerStore = create((set, get) => ({ latencyEstimateMs: null, connectionStatus: "disconnected", connectionMessage: null, + virtualControllerActive: false, initialized: false, usingSimulation: false, @@ -77,31 +78,6 @@ export const useControllerStore = create((set, get) => ({ if (get().initialized) return; set({ initialized: true }); - const simulationRequested = useSettingsStore.getState().simulationMode; - let realControllers: ControllerInfo[] = []; - try { - realControllers = simulationRequested ? [] : await tauriControllerService.list(); - } catch { - // Browser-only development and missing permissions both retain the safe - // simulated fallback rather than leaving the UI unusable. - } - - const useSimulation = simulationRequested || realControllers.length === 0; - set({ - controllers: useSimulation ? [] : realControllers, - activeControllerId: useSimulation ? null : realControllers[0]?.id ?? null, - usingSimulation: useSimulation, - connectionStatus: useSimulation ? "simulation" : "connected", - connectionMessage: useSimulation - ? simulationRequested - ? "Simulation mode is enabled." - : "No physical controller detected; using simulated input." - : null, - }); - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H3',location:'src/stores/controllerStore.ts:init',message:'Controller backend selected',data:{simulationRequested,realControllerCount:realControllers.length,useSimulation,devices:realControllers.map((controller)=>({id:controller.id,name:controller.name,connection:controller.connection,devicePath:controller.devicePath}))},timestamp:Date.now()})}).catch(()=>{}); - // #endregion - const receiveLiveState = (liveState: ControllerLiveState) => { pollTimestamps.push(liveState.timestamp); if (pollTimestamps.length > MAX_POLL_SAMPLES) pollTimestamps.shift(); @@ -111,12 +87,6 @@ export const useControllerStore = create((set, get) => ({ }; const receiveRawEvent = (event: RawInputEvent) => { - if (!get().usingSimulation && !hasLoggedFirstPhysicalEvent) { - hasLoggedFirstPhysicalEvent = true; - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H2',location:'src/stores/controllerStore.ts:receiveRawEvent',message:'First physical input event received',data:{type:event.type,code:event.code,rawValue:event.rawValue,processedValue:event.processedValue},timestamp:Date.now()})}).catch(()=>{}); - // #endregion - } set((state) => ({ eventLog: [event, ...state.eventLog].slice(0, MAX_EVENT_LOG) })); set((state) => { const liveState = state.liveState ?? { @@ -135,113 +105,153 @@ export const useControllerStore = create((set, get) => ({ }); }; - if (!useSimulation) { - const active = realControllers[0]; - await tauriControllerService.onInput(receiveRawEvent); - const refreshNativeDevices = async (): Promise => { - if (refreshInFlight) return false; - refreshInFlight = true; - try { - const previousActive = get().activeController(); - const devices = await tauriControllerService.list(); - const nextActive = - devices.find((controller) => controller.id === previousActive?.id) ?? - (previousActive ? devices.find((controller) => samePhysicalController(controller, previousActive)) : undefined) ?? - devices[0]; - - if (!nextActive) { - set({ - controllers: [], - activeControllerId: null, - liveState: null, - pollRateHz: null, - latencyEstimateMs: null, - connectionStatus: "reconnecting", - connectionMessage: "Waiting for the controller to reconnect…", - }); - return false; - } - - set({ - controllers: devices, - activeControllerId: nextActive.id, - liveState: null, - pollRateHz: null, - latencyEstimateMs: null, - connectionStatus: "connected", - connectionMessage: null, - }); - await tauriControllerService.startReader(nextActive.devicePath); - clearReconnectTimer(); - return true; - } catch { - set({ - connectionStatus: "reconnecting", - connectionMessage: "Unable to access the controller; retrying…", - }); - return false; - } finally { - refreshInFlight = false; - } - }; - - const scheduleReconnect = (): void => { - if (reconnectTimer !== null) return; - const attemptReconnect = async (): Promise => { - reconnectTimer = null; - if (await refreshNativeDevices()) return; - reconnectAttempts += 1; - if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { - set({ - connectionStatus: "disconnected", - connectionMessage: "Controller did not reconnect. Reconnect it or restart input detection.", - }); - return; - } - reconnectTimer = setTimeout(() => void attemptReconnect(), RECONNECT_INTERVAL_MS); - }; - void attemptReconnect(); - }; - - await tauriControllerService.onReaderStatus((status) => { - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H6',location:'src/stores/controllerStore.ts:onReaderStatus',message:'Physical reader status changed',data:status,timestamp:Date.now()})}).catch(()=>{}); - // #endregion - const activeController = get().activeController(); - if (status.connected && activeController?.devicePath === status.devicePath) { - clearReconnectTimer(); - set({ connectionStatus: "connected", connectionMessage: null }); - return; - } - if (!status.connected && activeController?.devicePath === status.devicePath) { - set({ - liveState: null, - pollRateHz: null, - latencyEstimateMs: null, - connectionStatus: "reconnecting", - connectionMessage: status.message ?? "Controller disconnected; searching for it again…", - }); - scheduleReconnect(); - } + const startSimulation = async (message: string) => { + set({ + controllers: [], + activeControllerId: null, + usingSimulation: true, + connectionStatus: "simulation", + connectionMessage: message, }); - if (active) { - await tauriControllerService.startReader(active.devicePath); - } + await mockControllerBackend.init(); + mockControllerBackend.onControllersChanged((controllers) => { + set((state) => ({ + controllers, + activeControllerId: controllers.some((c) => c.id === state.activeControllerId) + ? state.activeControllerId + : controllers[0]?.id ?? null, + })); + }); + mockControllerBackend.onLiveState(receiveLiveState); + mockControllerBackend.onRawEvent(receiveRawEvent); + }; + + if (useSettingsStore.getState().simulationMode) { + await startSimulation("Simulation mode is enabled in Settings."); return; } - await mockControllerBackend.init(); - mockControllerBackend.onControllersChanged((controllers) => { - set((state) => ({ - controllers, - activeControllerId: controllers.some((c) => c.id === state.activeControllerId) - ? state.activeControllerId - : controllers[0]?.id ?? null, - })); - }); + let realControllers: ControllerInfo[]; + try { + realControllers = await tauriControllerService.list(); + } catch { + // The backend is unreachable only when running in a plain browser + // (no Tauri). Fall back to the clearly-labeled simulated controller so + // frontend development stays possible. + await startSimulation("Native backend unavailable; using simulated input."); + return; + } - mockControllerBackend.onLiveState(receiveLiveState); - mockControllerBackend.onRawEvent(receiveRawEvent); + const refreshVirtualControllerActive = async () => { + try { + set({ virtualControllerActive: await tauriControllerService.virtualControllerActive() }); + } catch { + set({ virtualControllerActive: false }); + } + }; + + const refreshNativeDevices = async (): Promise => { + if (refreshInFlight) return false; + refreshInFlight = true; + try { + const previousActive = get().activeController(); + const devices = await tauriControllerService.list(); + const nextActive = + devices.find((controller) => controller.id === previousActive?.id) ?? + (previousActive + ? devices.find((controller) => samePhysicalController(controller, previousActive)) + : undefined) ?? + devices[0]; + + if (!nextActive) { + set((state) => ({ + controllers: [], + activeControllerId: null, + liveState: null, + pollRateHz: null, + latencyEstimateMs: null, + connectionStatus: state.connectionStatus === "reconnecting" ? "reconnecting" : "disconnected", + connectionMessage: + state.connectionStatus === "reconnecting" + ? "Waiting for the controller to reconnect…" + : "No controller detected.", + })); + return false; + } + + set({ + controllers: devices, + activeControllerId: nextActive.id, + connectionStatus: "connected", + connectionMessage: null, + }); + await tauriControllerService.startReader(nextActive.devicePath); + return true; + } catch { + set({ + connectionStatus: "reconnecting", + connectionMessage: "Unable to access the controller; retrying…", + }); + return false; + } finally { + refreshInFlight = false; + } + }; + + /** + * Polls until a controller is present. Runs indefinitely so a controller + * plugged in at any time is picked up without restarting the app. + */ + const scheduleRescan = (): void => { + if (rescanTimer !== null) return; + const attempt = async (): Promise => { + rescanTimer = null; + if (await refreshNativeDevices()) return; + rescanTimer = setTimeout(() => void attempt(), RESCAN_INTERVAL_MS); + }; + rescanTimer = setTimeout(() => void attempt(), RESCAN_INTERVAL_MS); + }; + + await tauriControllerService.onInput(receiveRawEvent); + await tauriControllerService.onReaderStatus((status) => { + const activeController = get().activeController(); + if (activeController?.devicePath !== status.devicePath) return; + if (status.connected) { + clearRescanTimer(); + set({ connectionStatus: "connected", connectionMessage: null }); + return; + } + set({ + liveState: null, + pollRateHz: null, + latencyEstimateMs: null, + connectionStatus: "reconnecting", + connectionMessage: status.message ?? "Controller disconnected; searching for it again…", + }); + scheduleRescan(); + }); + await refreshVirtualControllerActive(); + + const active = realControllers[0]; + if (active) { + set({ + controllers: realControllers, + activeControllerId: active.id, + usingSimulation: false, + connectionStatus: "connected", + connectionMessage: null, + }); + await tauriControllerService.startReader(active.devicePath); + } else { + set({ + controllers: [], + activeControllerId: null, + usingSimulation: false, + connectionStatus: "disconnected", + connectionMessage: "No controller detected.", + }); + scheduleRescan(); + } }, activeController: () => { @@ -263,14 +273,16 @@ export const useControllerStore = create((set, get) => ({ }, setRemappingEnabled: async (enabled) => { - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H8',location:'src/stores/controllerStore.ts:setRemappingEnabled',message:'Remapping toggle requested',data:{enabled,usingSimulation:get().usingSimulation,activeControllerId:get().activeControllerId},timestamp:Date.now()})}).catch(()=>{}); - // #endregion if (get().usingSimulation) { await mockControllerBackend.setRemappingEnabled(enabled); return; } await tauriControllerService.setRemappingEnabled(enabled); + try { + set({ virtualControllerActive: await tauriControllerService.virtualControllerActive() }); + } catch { + set({ virtualControllerActive: false }); + } }, clearEventLog: () => set({ eventLog: [] }), diff --git a/src/stores/profileStore.ts b/src/stores/profileStore.ts index e4d9f38..195d459 100644 --- a/src/stores/profileStore.ts +++ b/src/stores/profileStore.ts @@ -75,9 +75,6 @@ export const useProfileStore = create((set, get) => ({ const nextDraft = updater(deepClone(draft)); const isDirty = !isEqual(nextDraft, get().activeProfile); set({ draftProfile: nextDraft, isDirty }); - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H1',location:'src/stores/profileStore.ts:updateDraft',message:'Profile draft updated',data:{profileId:nextDraft.id,isDirty,leftTrigger:nextDraft.leftTrigger,rightTrigger:nextDraft.rightTrigger},timestamp:Date.now()})}).catch(()=>{}); - // #endregion }, applyDraft: async () => { @@ -90,9 +87,6 @@ export const useProfileStore = create((set, get) => ({ useUIStore.getState().pushToast(`Could not apply remapping: ${err}`, "danger"); return; } - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'post-fix',hypothesisId:'H7',location:'src/stores/profileStore.ts:applyDraft',message:'Profile successfully applied to native remapping pipeline',data:{profileId:draft.id},timestamp:Date.now()})}).catch(()=>{}); - // #endregion }, revertDraft: () => { @@ -111,9 +105,6 @@ export const useProfileStore = create((set, get) => ({ set({ activeProfile: saved, draftProfile: deepClone(saved), isDirty: false, loading: false }); await get().refreshList(); useUIStore.getState().pushToast(`Saved profile "${saved.name}".`, "success"); - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H7',location:'src/stores/profileStore.ts:saveDraft',message:'Profile draft saved',data:{profileId:saved.id},timestamp:Date.now()})}).catch(()=>{}); - // #endregion } catch (err) { set({ loading: false, error: String(err) }); useUIStore.getState().pushToast(`Failed to save profile: ${err}`, "danger"); diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index fae167f..06c33c6 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -22,12 +22,7 @@ export const useSettingsStore = create()( remappingEnabled: true, setRemappingEnabled: (remappingEnabled) => set({ remappingEnabled }), simulationMode: false, - setSimulationMode: (simulationMode) => { - // #region agent log - fetch('http://127.0.0.1:7773/ingest/f58e9523-b2dd-4860-abd0-167312081aae',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'2801e0'},body:JSON.stringify({sessionId:'2801e0',runId:'initial',hypothesisId:'H5',location:'src/stores/settingsStore.ts:setSimulationMode',message:'Simulation mode changed',data:{simulationMode},timestamp:Date.now()})}).catch(()=>{}); - // #endregion - set({ simulationMode }); - }, + setSimulationMode: (simulationMode) => set({ simulationMode }), autoSwitchProfiles: false, setAutoSwitchProfiles: (autoSwitchProfiles) => set({ autoSwitchProfiles }), }), diff --git a/src/types/controller.ts b/src/types/controller.ts index e09ce1f..b1a8915 100644 --- a/src/types/controller.ts +++ b/src/types/controller.ts @@ -7,9 +7,7 @@ * `mockControllerBackend` provides a realistic example. */ -export type ConnectionKind = "usb" | "bluetooth" | "receiver2_4ghz" | "unknown"; - -export type ControllerMode = "xinput" | "directinput" | "switch" | "unknown"; +export type ConnectionKind = "usb" | "bluetooth" | "unknown"; export interface ButtonCapability { id: string; @@ -45,10 +43,8 @@ export interface ControllerInfo { devicePath: string; vendorId: number; productId: number; - mode: ControllerMode; + /** Kernel-reported battery percentage; null when the device reports none. */ batteryPercent: number | null; - firmwareVersion: string | null; - virtualControllerActive: boolean; capabilities: DeviceCapabilities; isSimulated: boolean; }