diff --git a/src/config/mod.rs b/src/config/mod.rs index 530e0f3d..91af0b2d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -392,6 +392,9 @@ pub struct IIO { pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + /// Desired sampling rate in Hz. + #[serde(skip_serializing_if = "Option::is_none")] + pub sample_rate: Option, #[deprecated( since = "0.43.0", note = "please use `.config.imu.mount_matrix` instead" diff --git a/src/drivers/iio_imu/driver.rs b/src/drivers/iio_imu/driver.rs index ab7d0e61..19422748 100644 --- a/src/drivers/iio_imu/driver.rs +++ b/src/drivers/iio_imu/driver.rs @@ -17,8 +17,11 @@ use super::{ info::AxisInfo, }; +const DEFAULT_SAMPLE_RATE: f64 = 200.0; + /// Driver for reading IIO IMU data pub struct Driver { + _device: Device, // must outlive Channel raw pointers mount_matrix: MountMatrix, accel: HashMap, accel_info: HashMap, @@ -33,6 +36,7 @@ impl Driver { id: String, name: String, matrix: Option, + sample_rate: Option, ) -> Result> { log::debug!("Creating IIO IMU driver instance for {name}"); @@ -87,9 +91,21 @@ impl Driver { } } - // Calculate the initial sample delay + // Request a higher sampling rate + for (channels, ch_type) in [(&accel, ChannelType::Accel), (&gyro, ChannelType::AnglVel)] { + if channels.is_empty() { + continue; + } + if let Err(err) = + set_sample_rate_or_default(&device, channels, ch_type, sample_rate) + { + log::warn!("Failed to set sample rate: {err}, falling back to max available"); + set_sample_rate_max(&device, channels, ch_type); + } + } Ok(Self { + _device: device, mount_matrix, accel, accel_info, @@ -158,6 +174,10 @@ impl Driver { /// Polls all the channels from the accelerometer fn poll_accel(&self) -> Result, Box> { + if self.accel.is_empty() { + return Ok(None); + } + // Read from each accel channel let mut accel_input = AxisData::default(); for (id, channel) in self.accel.iter() { @@ -186,7 +206,10 @@ impl Driver { /// Polls all the channels from the gyro fn poll_gyro(&self) -> Result, Box> { - // Read from each accel channel + if self.gyro.is_empty() { + return Ok(None); + } + let mut gyro_input = AxisData::default(); for (id, channel) in self.gyro.iter() { // Get the info for the axis and read the data @@ -343,3 +366,131 @@ fn is_driver_loaded(driver_name: &str) -> io::Result { } Ok(false) } + +/// Try to set a specific or default sampling rate. Returns Err if the +/// requested rate is not in the hardware's available list. +fn set_sample_rate_or_default( + device: &Device, + channels: &HashMap, + channel_type: ChannelType, + target_rate: Option, +) -> Result<(), Box> { + let rate = target_rate.unwrap_or(DEFAULT_SAMPLE_RATE); + let avail = read_sample_rates_available(device, channels, &channel_type); + + if !avail.is_empty() && !avail.contains(&rate) { + return Err(format!( + "Requested {rate} Hz not in available rates: {avail:?}" + ) + .into()); + } + + write_sample_rate(device, channels, channel_type, rate) +} + +/// Set sampling rate to the maximum reported by the hardware. +/// Falls back to DEFAULT_SAMPLE_RATE if no available rates are reported. +fn set_sample_rate_max( + device: &Device, + channels: &HashMap, + channel_type: ChannelType, +) { + let avail = read_sample_rates_available(device, channels, &channel_type); + let rate = if avail.is_empty() { + log::warn!( + "No available sample rates reported, using default {DEFAULT_SAMPLE_RATE} Hz" + ); + DEFAULT_SAMPLE_RATE + } else { + let max = avail.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + log::info!("Using max available sample rate: {max} Hz"); + max + }; + + if let Err(err) = write_sample_rate(device, channels, channel_type, rate) { + log::warn!("Failed to set max sample rate: {err}"); + } +} + +/// Write a sampling rate to the device. Tries per-channel first (BMI-style), +/// then falls back to device-level attribute (HID Sensor Hub). +fn write_sample_rate( + device: &Device, + channels: &HashMap, + channel_type: ChannelType, + rate: f64, +) -> Result<(), Box> { + for (id, channel) in channels.iter() { + match channel.attr_write_float("sampling_frequency", rate) { + Ok(_) => { + match channel.attr_read_float("sampling_frequency") { + Ok(actual) => { + log::info!("Set sampling_frequency to {actual} Hz via channel {id}") + } + Err(err) => log::warn!( + "Set sampling_frequency for {id} but read-back failed: {err}, assuming {rate} Hz" + ), + } + return Ok(()); + } + Err(err) => { + log::warn!( + "Per-channel sampling_frequency write failed for {id}: {err}" + ); + } + } + } + + let attr = match channel_type { + ChannelType::Accel => "in_accel_sampling_frequency", + ChannelType::AnglVel => "in_anglvel_sampling_frequency", + _ => return Err("Unknown channel type".into()), + }; + + device.attr_write_float(attr, rate)?; + match device.attr_read_float(attr) { + Ok(actual) => log::info!("Set device-level {attr} to {actual} Hz"), + Err(err) => log::warn!( + "Set {attr} but read-back failed: {err}, assuming {rate} Hz" + ), + } + Ok(()) +} + +/// Read the list of supported sampling rates from the hardware. +/// Tries per-channel attribute first, then device-level global attribute. +fn read_sample_rates_available( + device: &Device, + channels: &HashMap, + channel_type: &ChannelType, +) -> Vec { + for (_, channel) in channels.iter() { + if let Ok(val) = channel.attr_read_str("sampling_frequency_available") { + let rates: Vec = val + .split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect(); + if !rates.is_empty() { + return rates; + } + } + } + + let attr = match channel_type { + ChannelType::Accel => "in_accel_sampling_frequency_available", + ChannelType::AnglVel => "in_anglvel_sampling_frequency_available", + _ => return vec![], + }; + + if let Ok(val) = device.attr_read_str(attr) { + let rates: Vec = val + .split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect(); + if !rates.is_empty() { + return rates; + } + } + + vec![] +} diff --git a/src/input/source/iio.rs b/src/input/source/iio.rs index f85ee01b..8f2a871a 100644 --- a/src/input/source/iio.rs +++ b/src/input/source/iio.rs @@ -121,9 +121,8 @@ impl IioDevice { return DriverType::BmiImu; } - // AccelGryo3D (Windows-style HID sensor proxies) if glob_match("{gyro_3d,accel_3d}", name) { - log::info!("Detected AccelGyro3D: {name}"); + log::info!("Detected HID Sensor Hub IMU: {name}"); return DriverType::AccelGryo3D; } log::debug!("No driver found for IIO Interface: {name}"); diff --git a/src/input/source/iio/accel_gyro_3d.rs b/src/input/source/iio/accel_gyro_3d.rs index 23ce6a68..737dacaa 100644 --- a/src/input/source/iio/accel_gyro_3d.rs +++ b/src/input/source/iio/accel_gyro_3d.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, error::Error, f64::consts::PI, fmt::Debug}; +use std::{collections::HashSet, error::Error, fmt::Debug}; use crate::{ config, @@ -11,6 +11,13 @@ use crate::{ udev::device::UdevDevice, }; +// Scale from IIO SI units to Steam Deck UHID raw LSB. +// IIO channels report m/s² for accel and rad/s for gyro after applying scale: +// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-iio +// UHID LSB constants from src/drivers/steam_deck/driver.rs. +const ACCEL_SCALE_FACTOR: f64 = 1632.6530612244898; // 1 / 0.0006125 (m/s² → UHID LSB) +const GYRO_SCALE_FACTOR: f64 = 916.7324722093172; // (180/π) / 0.0625 (rad/s → °/s → UHID LSB) + pub struct AccelGyro3dImu { driver: Driver, } @@ -39,9 +46,11 @@ impl AccelGyro3dImu { None }; + let sample_rate = config.as_ref().and_then(|c| c.sample_rate); + let id = device_info.sysname(); let name = device_info.name(); - let driver = Driver::new(id, name, mount_matrix)?; + let driver = Driver::new(id, name, mount_matrix, sample_rate)?; Ok(Self { driver }) } @@ -100,23 +109,18 @@ fn translate_event(event: iio_imu::event::Event) -> NativeEvent { iio_imu::event::Event::Accelerometer(data) => { let cap = Capability::Accelerometer(Source::Center); let value = InputValue::Vector3 { - x: Some(data.roll * 10.0), - y: Some(data.pitch * 10.0), - z: Some(data.yaw * 10.0), + x: Some(data.roll * ACCEL_SCALE_FACTOR), + y: Some(data.pitch * ACCEL_SCALE_FACTOR), + z: Some(data.yaw * ACCEL_SCALE_FACTOR), }; NativeEvent::new(cap, value) } iio_imu::event::Event::Gyro(data) => { - // Translate gyro values into the expected units of degrees per sec - // We apply a 500x scale so the motion feels like natural 1:1 motion. - // Adjusting the scale is not possible on the accel_gyro_3d IMU. - // From testing this is the highest scale we can apply before noise - // is amplified to the point the gyro cannot calibrate. let cap = Capability::Gyroscope(Source::Center); let value = InputValue::Vector3 { - x: Some(data.roll * (180.0 / PI) * 1500.0), - y: Some(data.pitch * (180.0 / PI) * 1500.0), - z: Some(data.yaw * (180.0 / PI) * 1500.0), + x: Some(data.roll * GYRO_SCALE_FACTOR), + y: Some(data.pitch * GYRO_SCALE_FACTOR), + z: Some(data.yaw * GYRO_SCALE_FACTOR), }; NativeEvent::new(cap, value) } diff --git a/src/input/source/iio/bmi_imu.rs b/src/input/source/iio/bmi_imu.rs index abeb4c30..329c675f 100644 --- a/src/input/source/iio/bmi_imu.rs +++ b/src/input/source/iio/bmi_imu.rs @@ -39,9 +39,11 @@ impl BmiImu { None }; + let sample_rate = config.as_ref().and_then(|c| c.sample_rate); + let id = device_info.sysname(); let name = device_info.name(); - let driver = Driver::new(id, name, mount_matrix)?; + let driver = Driver::new(id, name, mount_matrix, sample_rate)?; Ok(Self { driver }) }