Skip to content

Commit

Permalink
fix(LegionGo): Add accel_gyro_3d implementation for IIO IMU driver.
Browse files Browse the repository at this point in the history
- Adds accel_gyro_3d implementation of IIO IMU driver. This implementation doesn't support setting scale or sample frequency and has a different scaling factor for the gyro and accelerometer than BMI devices.
- Renames IIO IMU to BMI IMU for disabiguation and improves debug statements.
- Adds mount matric for LegionGo display IMU.
  • Loading branch information
pastaq committed Jun 27, 2024
1 parent 29a7f6a commit 235c641
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 27 deletions.
8 changes: 8 additions & 0 deletions rootfs/usr/share/inputplumber/devices/50-legion_go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,17 @@ source_devices:
- group: imu
iio:
name: accel_3d
mount_matrix:
x: [0, 1, 0]
y: [0, 0, -1]
z: [-1, 0, 0]
- group: imu
iio:
name: gyro_3d
mount_matrix:
x: [0, 1, 0]
y: [0, 0, -1]
z: [-1, 0, 0]

# The target input device(s) that the virtual device profile can use
target_devices:
Expand Down
79 changes: 58 additions & 21 deletions src/input/source/iio.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
pub mod iio_imu;
pub mod accel_gyro_3d;
pub mod bmi_imu;

use std::error::Error;

use glob_match::glob_match;
use tokio::sync::{broadcast, mpsc};
use tokio::sync::mpsc;
use zbus::{fdo, Connection};
use zbus_macros::dbus_interface;

Expand All @@ -19,6 +20,12 @@ use super::SourceCommand;
/// Size of the [SourceCommand] buffer for receiving output events
const BUFFER_SIZE: usize = 2048;

/// List of available drivers
enum DriverType {
Unknown,
BmiImu,
AccelGryo3D,
}
/// DBusInterface exposing information about a IIO device
pub struct DBusInterface {
info: Device,
Expand Down Expand Up @@ -119,25 +126,55 @@ impl IIODevice {
return Err("Unable to determine IIO driver because no name was found".into());
};

// IIO IMU Driver
if glob_match(
"{i2c-10EC5280*,i2c-BMI*,accel-display,bmi*-imu,gyro_3d,accel_3d}",
name.as_str(),
) {
log::info!("Detected IMU: {name}");
let tx = self.composite_tx.clone();
let rx = self.rx.take().unwrap();
let mut driver = iio_imu::IMU::new(
self.info.clone(),
self.config.clone(),
tx,
rx,
self.get_id(),
);
driver.run().await?;
} else {
return Err(format!("Unsupported IIO device: {name}").into());
match self.get_driver_type(&name) {
DriverType::Unknown => Err(format!("No driver for IMU found. {}", name).into()),
DriverType::BmiImu => {
log::info!("Detected BMI_IMU");
let tx = self.composite_tx.clone();
let rx = self.rx.take().unwrap();
let mut driver = bmi_imu::IMU::new(
self.info.clone(),
self.config.clone(),
tx,
rx,
self.get_id(),
);
driver.run().await?;
Ok(())
}

DriverType::AccelGryo3D => {
log::info!("Detected IMU: {name}");
let tx = self.composite_tx.clone();
let rx = self.rx.take().unwrap();
let mut driver = accel_gyro_3d::IMU::new(
self.info.clone(),
self.config.clone(),
tx,
rx,
self.get_id(),
);
driver.run().await?;
Ok(())
}
}
Ok(())
}

fn get_driver_type(&self, name: &str) -> DriverType {
log::debug!("Finding driver for interface: {:?}", self.info);
// BMI_IMU
if glob_match("{i2c-10EC5280*,i2c-BMI*,bmi*-imu}", name) {
log::info!("Detected Steam Deck");
return DriverType::BmiImu;
}

// AccelGryo3D
if glob_match("{gyro_3d,accel_3d}", name) {
log::info!("Detected Legion Go");
return DriverType::AccelGryo3D;
}

// Unknown
DriverType::Unknown
}
}
197 changes: 197 additions & 0 deletions src/input/source/iio/accel_gyro_3d.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use std::{error::Error, f64::consts::PI, thread};

use tokio::sync::mpsc::{self, error::TryRecvError};

use crate::{
config,
drivers::iio_imu::{self, driver::Driver, info::MountMatrix},
iio::device::Device,
input::{
capability::{Capability, Gamepad},
composite_device::Command,
event::{native::NativeEvent, value::InputValue, Event},
source::SourceCommand,
},
};

/// AccelGyro3D implementation of IIO interface used by the Legion Go and MSI Claw
#[derive(Debug)]
#[allow(clippy::upper_case_acronyms)]
pub struct IMU {
info: Device,
config: Option<config::IIO>,
composite_tx: mpsc::Sender<Command>,
rx: Option<mpsc::Receiver<SourceCommand>>,
device_id: String,
}

impl IMU {
pub fn new(
info: Device,
config: Option<config::IIO>,
composite_tx: mpsc::Sender<Command>,
rx: mpsc::Receiver<SourceCommand>,
device_id: String,
) -> Self {
Self {
info,
config,
composite_tx,
rx: Some(rx),
device_id,
}
}

pub async fn run(&mut self) -> Result<(), Box<dyn Error>> {
log::debug!("Starting Accel Gyro IMU driver");

// Get the device id and name for the driver
let id = self.info.id.clone().unwrap_or_default();
let name = self.info.name.clone().unwrap_or_default();
let device_id = self.device_id.clone();
let tx = self.composite_tx.clone();
let mut rx = self.rx.take().unwrap();

// Override the mount matrix if one is defined in the config
let mount_matrix = if let Some(config) = self.config.as_ref() {
if let Some(matrix_config) = config.mount_matrix.as_ref() {
let matrix = MountMatrix {
x: (matrix_config.x[0], matrix_config.x[1], matrix_config.x[2]),
y: (matrix_config.y[0], matrix_config.y[1], matrix_config.y[2]),
z: (matrix_config.z[0], matrix_config.z[1], matrix_config.z[2]),
};
Some(matrix)
} else {
None
}
} else {
None
};

// Spawn a blocking task with the given poll rate to poll the IMU for
// data.
let task =
tokio::task::spawn_blocking(move || -> Result<(), Box<dyn Error + Send + Sync>> {
let mut driver = Driver::new(id, name, mount_matrix)?;
loop {
receive_commands(&mut rx, &mut driver)?;
let events = driver.poll()?;
let native_events = translate_events(events);
for event in native_events {
log::trace!("Sending IMU event to CompositeDevice: {:?}", event);
// Don't send un-implemented events
if matches!(event.as_capability(), Capability::NotImplemented) {
continue;
}
tx.blocking_send(Command::ProcessEvent(
device_id.clone(),
Event::Native(event),
))?;
}
// Sleep between each poll iteration
thread::sleep(driver.sample_delay);
}
});

// Wait for the task to finish
if let Err(e) = task.await? {
log::error!("Error running Accel Gyro IMU Driver: {:?}", e);
return Err(e.to_string().into());
}

log::debug!("Accel Gyro IMU driver stopped");

Ok(())
}
}

/// Translate the given driver events into native events
fn translate_events(events: Vec<iio_imu::event::Event>) -> Vec<NativeEvent> {
events.into_iter().map(translate_event).collect()
}

/// Translate the given driver event into a native event
fn translate_event(event: iio_imu::event::Event) -> NativeEvent {
match event {
iio_imu::event::Event::Accelerometer(data) => {
let cap = Capability::Gamepad(Gamepad::Accelerometer);
let value = InputValue::Vector3 {
x: Some(data.x * 10.0),
y: Some(data.y * 10.0),
z: Some(data.z * 10.0),
};
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::Gamepad(Gamepad::Gyro);
let value = InputValue::Vector3 {
x: Some(data.x * (180.0 / PI) * 500.0),
y: Some(data.y * (180.0 / PI) * 500.0),
z: Some(data.z * (180.0 / PI) * 500.0),
};
NativeEvent::new(cap, value)
}
}
}

/// Read commands sent to this device from the channel until it is
/// empty.
fn receive_commands(
rx: &mut mpsc::Receiver<SourceCommand>,
driver: &mut Driver,
) -> Result<(), Box<dyn Error + Send + Sync>> {
const MAX_COMMANDS: u8 = 64;
let mut commands_processed = 0;
loop {
match rx.try_recv() {
Ok(cmd) => match cmd {
SourceCommand::EraseEffect(_, _) => (),
SourceCommand::GetSampleRatesAvail(_, _) => (),
SourceCommand::GetScalesAvail(_, _) => (),
SourceCommand::SetSampleRate(_, _, _) => (),
SourceCommand::SetScale(_, _, _) => (),
SourceCommand::UpdateEffect(_, _) => (),
SourceCommand::UploadEffect(_, _) => (),
SourceCommand::WriteEvent(_) => (),
SourceCommand::GetSampleRate(kind, tx) => {
let result = driver.get_sample_rate(kind.as_str());
let send_result = match result {
Ok(rate) => tx.send(Ok(rate)),
Err(e) => tx.send(Err(e.to_string().into())),
};
if let Err(e) = send_result {
log::error!("Failed to send GetSampleRate, {e:?}");
}
}
SourceCommand::GetScale(kind, tx) => {
let result = driver.get_scale(kind.as_str());
let send_result = match result {
Ok(rate) => tx.send(Ok(rate)),
Err(e) => tx.send(Err(e.to_string().into())),
};
if let Err(e) = send_result {
log::error!("Failed to send GetScale, {e:?}");
}
}
SourceCommand::Stop => return Err("Device stopped".into()),
},
Err(e) => match e {
TryRecvError::Empty => return Ok(()),
TryRecvError::Disconnected => {
log::debug!("Receive channel disconnected");
return Err("Receive channel disconnected".into());
}
},
};

commands_processed += 1;
if commands_processed >= MAX_COMMANDS {
return Ok(());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use core::time;
use std::{any::Any, error::Error, f64::consts::PI, thread};
use std::{error::Error, f64::consts::PI, thread};

use nix::libc::ETIMEDOUT;
use tokio::sync::mpsc::{self, error::TryRecvError};

use crate::{
Expand Down Expand Up @@ -45,7 +43,7 @@ impl IMU {
}

pub async fn run(&mut self) -> Result<(), Box<dyn Error>> {
log::debug!("Starting IIO IMU driver");
log::debug!("Starting BMI IMU driver");

// Get the device id and name for the driver
let id = self.info.id.clone().unwrap_or_default();
Expand Down Expand Up @@ -97,11 +95,11 @@ impl IMU {

// Wait for the task to finish
if let Err(e) = task.await? {
log::error!("Error running IIO Driver: {:?}", e);
log::error!("Error running BMI IMU Driver: {:?}", e);
return Err(e.to_string().into());
}

log::debug!("IIO IMU driver stopped");
log::debug!("BMI IMU driver stopped");

Ok(())
}
Expand Down

0 comments on commit 235c641

Please sign in to comment.