use std::collections::HashMap;
use std::path::PathBuf;
use web_time::{Duration, Instant};
use repose_core::input::SensorSample;
use super::{SensorBackend, SensorReading};
#[derive(Clone, Copy, Debug)]
pub struct MotionScale {
pub gyroscope: f32,
pub accelerometer: f32,
}
impl Default for MotionScale {
fn default() -> Self {
Self {
gyroscope: 0.01,
accelerometer: 0.001,
}
}
}
const ACCEL_AXES: [evdev::AbsoluteAxisCode; 3] = [
evdev::AbsoluteAxisCode::ABS_X,
evdev::AbsoluteAxisCode::ABS_Y,
evdev::AbsoluteAxisCode::ABS_Z,
];
const GYRO_AXES: [evdev::AbsoluteAxisCode; 3] = [
evdev::AbsoluteAxisCode::ABS_RX,
evdev::AbsoluteAxisCode::ABS_RY,
evdev::AbsoluteAxisCode::ABS_RZ,
];
const GAMEPAD_BUTTONS: [evdev::KeyCode; 5] = [
evdev::KeyCode::BTN_SOUTH,
evdev::KeyCode::BTN_EAST,
evdev::KeyCode::BTN_NORTH,
evdev::KeyCode::BTN_WEST,
evdev::KeyCode::BTN_TRIGGER,
];
const MOTION_SUFFIXES: [&str; 2] = [" Motion Sensors", " (IMU)"];
const NINTENDO_VENDOR: u16 = 0x057e;
#[derive(Clone, Copy, Debug)]
struct AxisSet {
codes: [u16; 3],
scale: [f32; 3],
}
impl AxisSet {
fn index(&self, code: u16) -> Option<usize> {
self.codes.iter().position(|candidate| *candidate == code)
}
fn scale_raw(&self, raw: [i32; 3]) -> [f32; 3] {
[
raw[0] as f32 * self.scale[0],
raw[1] as f32 * self.scale[1],
raw[2] as f32 * self.scale[2],
]
}
}
#[derive(Debug)]
struct MotionDevice {
name: String,
frame: fn([f32; 3]) -> [f32; 3],
path: PathBuf,
device: evdev::Device,
gyroscope: Option<AxisSet>,
accelerometer: Option<AxisSet>,
gyro_raw: [i32; 3],
accel_raw: [i32; 3],
gyro_dirty: bool,
accel_dirty: bool,
gone: bool,
}
impl MotionDevice {
fn readings(&mut self) -> Vec<SensorReading> {
let mut out = Vec::with_capacity(2);
if self.gyro_dirty {
let data = self
.gyroscope
.as_ref()
.map(|axes| axes.scale_raw(self.gyro_raw));
if let Some(data) = data {
let data = (self.frame)(data);
out.push(SensorReading {
device: self.name.clone(),
sample: SensorSample::gyroscope(data[0], data[1], data[2]),
});
}
self.gyro_dirty = false;
}
if self.accel_dirty {
let data = self
.accelerometer
.as_ref()
.map(|axes| axes.scale_raw(self.accel_raw));
if let Some(data) = data {
let data = (self.frame)(data);
out.push(SensorReading {
device: self.name.clone(),
sample: SensorSample::accelerometer(data[0], data[1], data[2]),
});
}
self.accel_dirty = false;
}
out
}
fn drain(&mut self) {
loop {
let events = match self.read_events() {
Ok(events) => events,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => return,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {
self.resync();
return;
}
Err(error) => {
log::debug!("sensor: {}: {error}", self.path.display());
self.gone = true;
return;
}
};
if events.is_empty() {
if self.device.get_absinfo().is_err() {
log::debug!("sensor: {}: node closed", self.path.display());
self.gone = true;
}
return;
}
for event in events {
let evdev::EventSummary::AbsoluteAxis(_, code, value) = event.destructure() else {
continue;
};
let accel = self.accelerometer.and_then(|axes| axes.index(code.0));
let gyro = self.gyroscope.and_then(|axes| axes.index(code.0));
if let Some(axis) = accel {
self.accel_raw[axis] = value;
self.accel_dirty = true;
}
if let Some(axis) = gyro {
self.gyro_raw[axis] = value;
self.gyro_dirty = true;
}
}
}
}
fn read_events(&mut self) -> std::io::Result<Vec<evdev::InputEvent>> {
Ok(self.device.fetch_events()?.collect())
}
fn resync(&mut self) {
let (_, values) = abs_state(&self.device);
if values.is_empty() {
return;
}
self.gyro_raw = current_raw(&values, &GYRO_AXES);
self.accel_raw = current_raw(&values, &ACCEL_AXES);
self.gyro_dirty = self.gyroscope.is_some();
self.accel_dirty = self.accelerometer.is_some();
}
fn stop_readings(&self) -> Vec<SensorReading> {
if self.gyroscope.is_none() {
return Vec::new();
}
vec![SensorReading {
device: self.name.clone(),
sample: SensorSample::gyroscope(0.0, 0.0, 0.0),
}]
}
}
pub struct EvdevSensorBackend {
devices: Vec<MotionDevice>,
known_paths: Vec<PathBuf>,
scale: MotionScale,
last_scan: Instant,
rescan_after: Duration,
stale: bool,
}
impl EvdevSensorBackend {
pub fn new() -> Self {
Self::with_scale(MotionScale::default())
}
pub fn with_scale(scale: MotionScale) -> Self {
let known_paths = event_paths();
Self {
devices: discover(&known_paths, HashMap::new(), scale),
known_paths,
scale,
last_scan: Instant::now(),
rescan_after: Duration::from_secs(2),
stale: false,
}
}
pub fn device_names(&self) -> Vec<&str> {
self.devices
.iter()
.map(|device| device.name.as_str())
.collect()
}
fn rescan(&mut self) -> Vec<SensorReading> {
self.last_scan = Instant::now();
let known_paths = event_paths();
if known_paths.is_empty() {
return Vec::new();
}
if known_paths == self.known_paths && !self.stale {
return Vec::new();
}
let mut keep: HashMap<PathBuf, MotionDevice> = std::mem::take(&mut self.devices)
.into_iter()
.map(|device| (device.path.clone(), device))
.collect();
let vanished: Vec<PathBuf> = keep
.keys()
.filter(|path| !known_paths.contains(path))
.cloned()
.collect();
let mut dropped = Vec::new();
for path in vanished {
if let Some(device) = keep.remove(&path) {
dropped.push(device);
}
}
self.devices = discover(&known_paths, keep, self.scale);
self.known_paths = known_paths;
self.stale = false;
dropped
.iter()
.flat_map(MotionDevice::stop_readings)
.collect()
}
}
impl Default for EvdevSensorBackend {
fn default() -> Self {
Self::new()
}
}
impl SensorBackend for EvdevSensorBackend {
fn poll_sensors(&mut self) -> Vec<SensorReading> {
let mut out = Vec::new();
if self.last_scan.elapsed() >= self.rescan_after {
out = self.rescan();
}
let mut failed = false;
self.devices.retain_mut(|device| {
device.drain();
if device.gone {
out.extend(device.stop_readings());
failed = true;
false
} else {
out.extend(device.readings());
true
}
});
if failed {
self.stale = true;
}
out
}
}
fn discover(
paths: &[PathBuf],
mut keep: HashMap<PathBuf, MotionDevice>,
scale: MotionScale,
) -> Vec<MotionDevice> {
let mut devices: Vec<MotionDevice> = Vec::new();
let mut fresh: Vec<(PathBuf, evdev::Device)> = Vec::new();
for path in paths {
if let Some(existing) = keep.remove(path) {
devices.push(existing);
continue;
}
match evdev::Device::open(path) {
Ok(device) => match device.set_nonblocking(true) {
Ok(()) => fresh.push((path.clone(), device)),
Err(error) => log::warn!("sensor: {}: {error}", path.display()),
},
Err(error) => log::debug!("sensor: {}: {error}", path.display()),
}
}
let pads = pad_names(&fresh);
for (path, device) in fresh {
if !device.properties().contains(evdev::PropType::ACCELEROMETER) {
continue;
}
let resolutions = abs_state(&device).0;
let name = resolve_name(&device, &pads);
let accelerometer = axis_set(
&device,
ACCEL_AXES,
scale.accelerometer,
"accelerometer",
&resolutions,
);
let gyroscope = axis_set(
&device,
GYRO_AXES,
scale.gyroscope,
"gyroscope",
&resolutions,
);
if accelerometer.is_none() && gyroscope.is_none() {
continue;
}
log::info!(
"sensor: {name} ({}) gyro={} accel={}",
path.display(),
gyroscope.is_some(),
accelerometer.is_some()
);
devices.push(MotionDevice {
name,
frame: frame_for(device.input_id().vendor()),
path,
device,
gyroscope,
accelerometer,
gyro_raw: [0; 3],
accel_raw: [0; 3],
gyro_dirty: false,
accel_dirty: false,
gone: false,
});
devices
.last_mut()
.expect("motion device just pushed")
.resync();
}
devices
}
fn pad_names(fresh: &[(PathBuf, evdev::Device)]) -> HashMap<String, Vec<String>> {
let mut pads: HashMap<String, Vec<String>> = HashMap::new();
for (_, device) in fresh {
if !is_pad(device) {
continue;
}
let Some(name) = device.name().filter(|name| !name.is_empty()) else {
continue;
};
for key in correlate_keys(device) {
pads.entry(key).or_default().push(name.to_string());
}
}
pads
}
fn correlate_keys(device: &evdev::Device) -> Vec<String> {
let mut keys = Vec::new();
if let Some(phys) = device.physical_path().filter(|phys| !phys.is_empty()) {
keys.push(format!("phys:{phys}"));
}
if let Some(uniq) = device.unique_name().filter(|uniq| !uniq.is_empty()) {
keys.push(format!("uniq:{uniq}"));
}
let id = device.input_id();
keys.push(format!("id:{:04x}:{:04x}", id.vendor(), id.product()));
keys
}
fn resolve_name(device: &evdev::Device, pads: &HashMap<String, Vec<String>>) -> String {
for key in correlate_keys(device) {
if let Some([only]) = pads.get(&key).map(Vec::as_slice) {
return only.clone();
}
}
strip_motion_suffix(device.name().unwrap_or_default()).to_owned()
}
fn strip_motion_suffix(name: &str) -> &str {
MOTION_SUFFIXES
.iter()
.find_map(|suffix| name.strip_suffix(suffix))
.unwrap_or(name)
}
fn nintendo_frame(data: [f32; 3]) -> [f32; 3] {
[-data[1], data[2], -data[0]]
}
fn frame_for(vendor: u16) -> fn([f32; 3]) -> [f32; 3] {
if vendor == NINTENDO_VENDOR {
nintendo_frame
} else {
std::convert::identity
}
}
fn is_pad(device: &evdev::Device) -> bool {
device
.supported_keys()
.is_some_and(|keys| GAMEPAD_BUTTONS.iter().any(|code| keys.contains(*code)))
}
fn axis_set(
device: &evdev::Device,
codes: [evdev::AbsoluteAxisCode; 3],
fallback: f32,
kind: &str,
resolutions: &HashMap<u16, f32>,
) -> Option<AxisSet> {
let supported = device.supported_absolute_axes()?;
if !codes.iter().all(|code| supported.contains(*code)) {
return None;
}
let mut scale = [fallback; 3];
for (axis, code) in codes.iter().enumerate() {
match resolutions.get(&code.0) {
Some(resolution) => scale[axis] = 1.0 / resolution,
None => log::warn!(
"sensor: {}: {kind} axis {} has no resolution, assuming {fallback} per count",
device.name().unwrap_or("?"),
code.0
),
}
}
Some(AxisSet {
codes: [codes[0].0, codes[1].0, codes[2].0],
scale,
})
}
fn abs_state(device: &evdev::Device) -> (HashMap<u16, f32>, HashMap<u16, i32>) {
let mut resolutions = HashMap::new();
let mut values = HashMap::new();
match device.get_absinfo() {
Ok(absinfo) => {
for (code, info) in absinfo {
if info.resolution() > 0 {
resolutions.insert(code.0, info.resolution() as f32);
}
values.insert(code.0, info.value());
}
}
Err(error) => log::warn!(
"sensor: {}: cannot read axis state ({error}), assuming the configured scale",
device.name().unwrap_or("?")
),
}
(resolutions, values)
}
fn current_raw(values: &HashMap<u16, i32>, codes: &[evdev::AbsoluteAxisCode; 3]) -> [i32; 3] {
[
values.get(&codes[0].0).copied().unwrap_or(0),
values.get(&codes[1].0).copied().unwrap_or(0),
values.get(&codes[2].0).copied().unwrap_or(0),
]
}
fn event_paths() -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir("/dev/input") else {
return Vec::new();
};
let mut paths: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("event"))
})
.collect();
paths.sort();
paths
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{ACCEL_AXES, current_raw, frame_for, strip_motion_suffix};
#[test]
fn current_raw_zero_fills_axes_the_node_does_not_report() {
let mut values = HashMap::new();
values.insert(evdev::AbsoluteAxisCode::ABS_X.0, 11);
values.insert(evdev::AbsoluteAxisCode::ABS_Z.0, 33);
assert_eq!(current_raw(&values, &ACCEL_AXES), [11, 0, 33]);
assert_eq!(current_raw(&HashMap::new(), &ACCEL_AXES), [0, 0, 0]);
}
#[test]
fn motion_node_names_resolve_to_their_pad_name() {
assert_eq!(
strip_motion_suffix("Steam Deck Motion Sensors"),
"Steam Deck"
);
assert_eq!(
strip_motion_suffix(
"Sony Interactive Entertainment Wireless Controller Motion Sensors"
),
"Sony Interactive Entertainment Wireless Controller"
);
assert_eq!(
strip_motion_suffix("Nintendo Switch Pro Controller (IMU)"),
"Nintendo Switch Pro Controller"
);
assert_eq!(strip_motion_suffix("Steam Deck"), "Steam Deck");
}
#[test]
fn only_nintendo_axes_are_rotated_into_the_shared_frame() {
assert_eq!(frame_for(0x057e)([1.0, 2.0, 3.0]), [-2.0, 3.0, -1.0]);
assert_eq!(frame_for(0x054c)([1.0, 2.0, 3.0]), [1.0, 2.0, 3.0]);
assert_eq!(frame_for(0x028e)([1.0, 2.0, 3.0]), [1.0, 2.0, 3.0]);
}
}