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 <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 08:32:54 -07:00
parent 6507f5f8b3
commit 4286fb1003
16 changed files with 242 additions and 531 deletions

108
src-tauri/Cargo.lock generated
View File

@@ -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"

View File

@@ -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]

View File

@@ -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(())
}

View File

@@ -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<u8>,
pub firmware_version: Option<String>,
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<u8> {
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::<u8>() {
return Some(percent.min(100));
}
}
}
"unknown".to_string()
None
}
pub fn probe_device(path: &Path, device: &Device) -> Option<ControllerInfo> {
@@ -145,6 +159,12 @@ pub fn probe_device(path: &Path, device: &Device) -> Option<ControllerInfo> {
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<ControllerInfo> {
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<ControllerInfo> {
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<ControllerInfo> {
.filter_map(|(path, device)| probe_device(&path, &device))
.collect::<Vec<_>>();
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::<Vec<_>>()},
"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
}

View File

@@ -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::<AppState>().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::<HashMap<_, _>>();
// #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;
}

View File

@@ -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}");
}