Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,9 @@ pub struct IIO {
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Desired sampling rate in Hz.
#[serde(skip_serializing_if = "Option::is_none")]
pub sample_rate: Option<f64>,
#[deprecated(
since = "0.43.0",
note = "please use `<SourceDevice>.config.imu.mount_matrix` instead"
Expand Down
155 changes: 153 additions & 2 deletions src/drivers/iio_imu/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Channel>,
accel_info: HashMap<String, AxisInfo>,
Expand All @@ -33,6 +36,7 @@ impl Driver {
id: String,
name: String,
matrix: Option<MountMatrix>,
sample_rate: Option<f64>,
) -> Result<Self, Box<dyn Error + Send + Sync>> {
log::debug!("Creating IIO IMU driver instance for {name}");

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -158,6 +174,10 @@ impl Driver {

/// Polls all the channels from the accelerometer
fn poll_accel(&self) -> Result<Option<Event>, Box<dyn Error + Send + Sync>> {
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() {
Expand Down Expand Up @@ -186,7 +206,10 @@ impl Driver {

/// Polls all the channels from the gyro
fn poll_gyro(&self) -> Result<Option<Event>, Box<dyn Error + Send + Sync>> {
// 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
Expand Down Expand Up @@ -343,3 +366,131 @@ fn is_driver_loaded(driver_name: &str) -> io::Result<bool> {
}
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<String, Channel>,
channel_type: ChannelType,
target_rate: Option<f64>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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<String, Channel>,
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<String, Channel>,
channel_type: ChannelType,
rate: f64,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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<String, Channel>,
channel_type: &ChannelType,
) -> Vec<f64> {
for (_, channel) in channels.iter() {
if let Ok(val) = channel.attr_read_str("sampling_frequency_available") {
let rates: Vec<f64> = 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<f64> = val
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if !rates.is_empty() {
return rates;
}
}

vec![]
}
3 changes: 1 addition & 2 deletions src/input/source/iio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
30 changes: 17 additions & 13 deletions src/input/source/iio/accel_gyro_3d.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
}
Expand Down Expand Up @@ -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 })
}
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion src/input/source/iio/bmi_imu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
Loading