initial commit
This commit is contained in:
34
src/common.rs
Normal file
34
src/common.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
pub enum DialDir {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub struct ThresholdHelper {
|
||||
sensitivity: i32,
|
||||
pos: i32,
|
||||
}
|
||||
|
||||
impl ThresholdHelper {
|
||||
pub fn new(sensitivity: i32) -> ThresholdHelper {
|
||||
ThresholdHelper {
|
||||
sensitivity,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, delta: i32) -> Option<DialDir> {
|
||||
self.pos += delta;
|
||||
|
||||
if self.pos > self.sensitivity {
|
||||
self.pos -= self.sensitivity;
|
||||
return Some(DialDir::Right);
|
||||
}
|
||||
|
||||
if self.pos < -self.sensitivity {
|
||||
self.pos += self.sensitivity;
|
||||
return Some(DialDir::Left);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
145
src/controller/controls/dpad.rs
Normal file
145
src/controller/controls/dpad.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::controller::ControlMode;
|
||||
use crate::fake_input::FakeInput;
|
||||
use crate::DynResult;
|
||||
|
||||
use evdev_rs::enums::EV_KEY;
|
||||
|
||||
enum Msg {
|
||||
Kill,
|
||||
Delta(i32),
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
msg: mpsc::Receiver<Msg>,
|
||||
fake_input: FakeInput,
|
||||
|
||||
timeout: u64,
|
||||
falloff: i32,
|
||||
cap: i32,
|
||||
deadzone: i32,
|
||||
|
||||
last_delta: i32,
|
||||
velocity: i32,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn new(msg: mpsc::Receiver<Msg>) -> Worker {
|
||||
Worker {
|
||||
msg,
|
||||
fake_input: FakeInput::new(),
|
||||
|
||||
// tweak these for "feel"
|
||||
timeout: 5,
|
||||
falloff: 10,
|
||||
cap: 250,
|
||||
deadzone: 10,
|
||||
|
||||
last_delta: 0,
|
||||
velocity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
loop {
|
||||
let falloff = self.velocity.abs() / self.falloff + 1;
|
||||
|
||||
match self.msg.recv_timeout(Duration::from_millis(self.timeout)) {
|
||||
Ok(Msg::Kill) => return,
|
||||
Ok(Msg::Delta(delta)) => {
|
||||
// abrupt direction change!
|
||||
if (delta < 0) != (self.last_delta < 0) {
|
||||
self.velocity = 0
|
||||
}
|
||||
self.last_delta = delta;
|
||||
|
||||
self.velocity += delta
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => match self.velocity.cmp(&0) {
|
||||
Ordering::Equal => {}
|
||||
Ordering::Less => self.velocity += falloff,
|
||||
Ordering::Greater => self.velocity -= falloff,
|
||||
},
|
||||
Err(other) => panic!("{}", other),
|
||||
}
|
||||
|
||||
// clamp velocity within the cap bounds
|
||||
if self.velocity > self.cap {
|
||||
self.velocity = self.cap;
|
||||
} else if self.velocity < -self.cap {
|
||||
self.velocity = -self.cap;
|
||||
}
|
||||
|
||||
if self.velocity.abs() < self.deadzone {
|
||||
self.fake_input
|
||||
.key_release(&[EV_KEY::KEY_LEFT, EV_KEY::KEY_RIGHT])
|
||||
.unwrap();
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.velocity.cmp(&0) {
|
||||
Ordering::Equal => {}
|
||||
Ordering::Less => self.fake_input.key_press(&[EV_KEY::KEY_LEFT]).unwrap(),
|
||||
Ordering::Greater => self.fake_input.key_press(&[EV_KEY::KEY_RIGHT]).unwrap(),
|
||||
}
|
||||
|
||||
eprintln!("{:?}", self.velocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bit of a misnomer, since it's only left-right.
|
||||
pub struct DPad {
|
||||
_worker: JoinHandle<()>,
|
||||
msg: mpsc::Sender<Msg>,
|
||||
|
||||
fake_input: FakeInput,
|
||||
}
|
||||
|
||||
impl Drop for DPad {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.msg.send(Msg::Kill);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DPad {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DPad {
|
||||
pub fn new() -> DPad {
|
||||
let (msg_tx, msg_rx) = mpsc::channel();
|
||||
|
||||
let worker = std::thread::spawn(move || Worker::new(msg_rx).run());
|
||||
|
||||
DPad {
|
||||
_worker: worker,
|
||||
msg: msg_tx,
|
||||
|
||||
fake_input: FakeInput::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlMode for DPad {
|
||||
fn on_btn_press(&mut self) -> DynResult<()> {
|
||||
eprintln!("space");
|
||||
self.fake_input.key_click(&[EV_KEY::KEY_SPACE])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_btn_release(&mut self) -> DynResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
|
||||
self.msg.send(Msg::Delta(delta))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
48
src/controller/controls/media.rs
Normal file
48
src/controller/controls/media.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::common::{DialDir, ThresholdHelper};
|
||||
use crate::controller::ControlMode;
|
||||
use crate::fake_input::FakeInput;
|
||||
use crate::DynResult;
|
||||
|
||||
use evdev_rs::enums::EV_KEY;
|
||||
|
||||
pub struct Media {
|
||||
thresh: ThresholdHelper,
|
||||
|
||||
fake_input: FakeInput,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
pub fn new(sensitivity: i32) -> Media {
|
||||
Media {
|
||||
thresh: ThresholdHelper::new(sensitivity),
|
||||
|
||||
fake_input: FakeInput::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlMode for Media {
|
||||
fn on_btn_press(&mut self) -> DynResult<()> {
|
||||
self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_btn_release(&mut self) -> DynResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
|
||||
match self.thresh.update(delta) {
|
||||
Some(DialDir::Left) => {
|
||||
eprintln!("next song");
|
||||
self.fake_input.key_click(&[EV_KEY::KEY_NEXTSONG])?;
|
||||
}
|
||||
Some(DialDir::Right) => {
|
||||
eprintln!("last song");
|
||||
self.fake_input.key_click(&[EV_KEY::KEY_PREVIOUSSONG])?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
7
src/controller/controls/mod.rs
Normal file
7
src/controller/controls/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod dpad;
|
||||
mod media;
|
||||
mod volume;
|
||||
|
||||
pub use self::dpad::*;
|
||||
pub use self::media::*;
|
||||
pub use self::volume::*;
|
||||
54
src/controller/controls/volume.rs
Normal file
54
src/controller/controls/volume.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::common::{DialDir, ThresholdHelper};
|
||||
use crate::controller::ControlMode;
|
||||
use crate::fake_input::FakeInput;
|
||||
use crate::DynResult;
|
||||
|
||||
use evdev_rs::enums::EV_KEY;
|
||||
|
||||
pub struct Volume {
|
||||
thresh: ThresholdHelper,
|
||||
|
||||
fake_input: FakeInput,
|
||||
}
|
||||
|
||||
impl Volume {
|
||||
pub fn new(sensitivity: i32) -> Volume {
|
||||
Volume {
|
||||
thresh: ThresholdHelper::new(sensitivity),
|
||||
|
||||
fake_input: FakeInput::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlMode for Volume {
|
||||
fn on_btn_press(&mut self) -> DynResult<()> {
|
||||
// TODO: support double-click to mute
|
||||
|
||||
eprintln!("play/pause");
|
||||
// self.fake_input.mute()?
|
||||
self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_btn_release(&mut self) -> DynResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
|
||||
match self.thresh.update(delta) {
|
||||
Some(DialDir::Left) => {
|
||||
eprintln!("volume down");
|
||||
self.fake_input
|
||||
.key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEDOWN])?
|
||||
}
|
||||
Some(DialDir::Right) => {
|
||||
eprintln!("volume up");
|
||||
self.fake_input
|
||||
.key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEUP])?
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
42
src/controller/mod.rs
Normal file
42
src/controller/mod.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::DynResult;
|
||||
|
||||
use crate::dial_device::{DialDevice, DialEventKind};
|
||||
|
||||
pub mod controls;
|
||||
|
||||
pub trait ControlMode {
|
||||
fn on_btn_press(&mut self) -> DynResult<()>;
|
||||
fn on_btn_release(&mut self) -> DynResult<()>;
|
||||
fn on_dial(&mut self, delta: i32) -> DynResult<()>;
|
||||
}
|
||||
|
||||
pub struct DialController {
|
||||
device: DialDevice,
|
||||
|
||||
mode: Box<dyn ControlMode>,
|
||||
}
|
||||
|
||||
impl DialController {
|
||||
pub fn new(device: DialDevice, default_mode: Box<dyn ControlMode>) -> DialController {
|
||||
DialController {
|
||||
mode: default_mode,
|
||||
|
||||
device,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> DynResult<()> {
|
||||
loop {
|
||||
let evt = self.device.next_event()?;
|
||||
|
||||
// TODO: press and hold + rotate to switch between modes
|
||||
|
||||
match evt.kind {
|
||||
DialEventKind::Ignored => {}
|
||||
DialEventKind::ButtonPress => self.mode.on_btn_press()?,
|
||||
DialEventKind::ButtonRelease => self.mode.on_btn_release()?,
|
||||
DialEventKind::Dial(delta) => self.mode.on_dial(delta)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/dial_device.rs
Normal file
111
src/dial_device.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use evdev_rs::{Device, InputEvent};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct DialDevice {
|
||||
// TODO: explore what the control channel can be used for...
|
||||
_control: Device,
|
||||
axis: Device,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DialEvent {
|
||||
pub time: Duration,
|
||||
pub kind: DialEventKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DialEventKind {
|
||||
Ignored,
|
||||
ButtonPress,
|
||||
ButtonRelease,
|
||||
Dial(i32),
|
||||
}
|
||||
|
||||
impl DialDevice {
|
||||
pub fn new() -> Result<DialDevice, crate::Error> {
|
||||
let mut control = None;
|
||||
let mut axis = None;
|
||||
|
||||
for e in fs::read_dir("/dev/input/").map_err(Error::Io)? {
|
||||
let e = e.map_err(Error::Io)?;
|
||||
if !e.file_name().to_str().unwrap().starts_with("event") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = fs::File::open(e.path()).map_err(Error::Io)?;
|
||||
let dev = Device::new_from_fd(file).map_err(Error::Evdev)?;
|
||||
|
||||
match dev.name() {
|
||||
Some("Surface Dial System Control") => match control {
|
||||
None => control = Some(dev),
|
||||
Some(_) => return Err(Error::MultipleDials),
|
||||
},
|
||||
Some("Surface Dial System Multi Axis") => match axis {
|
||||
None => axis = Some(dev),
|
||||
Some(_) => return Err(Error::MultipleDials),
|
||||
},
|
||||
// Some(other) => println!("{:?}", other),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// early return once both were found
|
||||
if control.is_some() && axis.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DialDevice {
|
||||
_control: control.ok_or(Error::MissingDial)?,
|
||||
axis: axis.ok_or(Error::MissingDial)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_event(&self) -> Result<DialEvent, Error> {
|
||||
// TODO: figure out how to interleave control events into the same event stream.
|
||||
|
||||
let (_axis_status, axis_evt) = self
|
||||
.axis
|
||||
.next_event(evdev_rs::ReadFlag::NORMAL)
|
||||
.map_err(Error::Evdev)?;
|
||||
// assert!(matches!(axis_status, ReadStatus::Success));
|
||||
|
||||
let event =
|
||||
DialEvent::from_raw_evt(axis_evt.clone()).ok_or(Error::UnexpectedEvt(axis_evt))?;
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl DialEvent {
|
||||
fn from_raw_evt(evt: InputEvent) -> Option<DialEvent> {
|
||||
use evdev_rs::enums::*;
|
||||
|
||||
let evt_kind = match evt.event_type {
|
||||
EventType::EV_SYN | EventType::EV_MSC => DialEventKind::Ignored,
|
||||
EventType::EV_KEY => match evt.event_code {
|
||||
EventCode::EV_KEY(EV_KEY::BTN_0) => match evt.value {
|
||||
0 => DialEventKind::ButtonRelease,
|
||||
1 => DialEventKind::ButtonPress,
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
},
|
||||
EventType::EV_REL => match evt.event_code {
|
||||
EventCode::EV_REL(EV_REL::REL_DIAL) => DialEventKind::Dial(evt.value),
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let evt = DialEvent {
|
||||
time: Duration::new(evt.time.tv_sec as u64, (evt.time.tv_usec * 1000) as u32),
|
||||
kind: evt_kind,
|
||||
};
|
||||
|
||||
Some(evt)
|
||||
}
|
||||
}
|
||||
26
src/error.rs
Normal file
26
src/error.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::fmt;
|
||||
|
||||
use evdev_rs::InputEvent;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
MissingDial,
|
||||
MultipleDials,
|
||||
UnexpectedEvt(InputEvent),
|
||||
Evdev(std::io::Error),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::MissingDial => write!(f, "Could not find the Surface Dial"),
|
||||
Error::MultipleDials => write!(f, "Found multiple dials"),
|
||||
Error::UnexpectedEvt(evt) => write!(f, "Unexpected event: {:?}", evt),
|
||||
Error::Evdev(e) => write!(f, "Evdev error: {:?}", e),
|
||||
Error::Io(e) => write!(f, "Io error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
102
src/fake_input.rs
Normal file
102
src/fake_input.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::io;
|
||||
|
||||
use evdev_rs::enums::*;
|
||||
use evdev_rs::{Device, InputEvent, TimeVal, UInputDevice};
|
||||
|
||||
static mut FAKE_INPUT: Option<UInputDevice> = None;
|
||||
fn get_fake_input() -> io::Result<&'static UInputDevice> {
|
||||
if unsafe { FAKE_INPUT.is_none() } {
|
||||
let device = Device::new().unwrap();
|
||||
device.set_name("Surface Dial Virtual Input");
|
||||
|
||||
device.enable(&EventType::EV_SYN)?;
|
||||
device.enable(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))?;
|
||||
|
||||
device.enable(&EventType::EV_KEY)?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFTSHIFT))?;
|
||||
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_MUTE))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEDOWN))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEUP))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_NEXTSONG))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PLAYPAUSE))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PREVIOUSSONG))?;
|
||||
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFT))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_RIGHT))?;
|
||||
device.enable(&EventCode::EV_KEY(EV_KEY::KEY_SPACE))?;
|
||||
|
||||
device.enable(&EventType::EV_MSC)?;
|
||||
device.enable(&EventCode::EV_MSC(EV_MSC::MSC_SCAN))?;
|
||||
|
||||
unsafe { FAKE_INPUT = Some(UInputDevice::create_from_device(&device)?) }
|
||||
}
|
||||
unsafe { Ok(FAKE_INPUT.as_ref().unwrap()) }
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct FakeInput {
|
||||
uinput: &'static UInputDevice,
|
||||
}
|
||||
|
||||
macro_rules! input_event {
|
||||
($type:ident, $code:ident, $value:expr) => {
|
||||
InputEvent {
|
||||
time: TimeVal::new(0, 0),
|
||||
event_code: EventCode::$type($type::$code),
|
||||
event_type: EventType::$type,
|
||||
value: $value,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for FakeInput {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeInput {
|
||||
pub fn new() -> FakeInput {
|
||||
FakeInput {
|
||||
uinput: get_fake_input().expect("could not install fake input device"),
|
||||
}
|
||||
}
|
||||
|
||||
fn syn_report(&self) -> io::Result<()> {
|
||||
self.uinput
|
||||
.write_event(&input_event!(EV_SYN, SYN_REPORT, 0))
|
||||
}
|
||||
|
||||
pub fn key_click(&self, keys: &[EV_KEY]) -> io::Result<()> {
|
||||
self.key_press(keys)?;
|
||||
self.key_release(keys)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn key_press(&self, keys: &[EV_KEY]) -> io::Result<()> {
|
||||
for key in keys {
|
||||
self.uinput.write_event(&InputEvent {
|
||||
time: TimeVal::new(0, 0),
|
||||
event_code: EventCode::EV_KEY(*key),
|
||||
event_type: EventType::EV_KEY,
|
||||
value: 1,
|
||||
})?;
|
||||
}
|
||||
self.syn_report()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn key_release(&self, keys: &[EV_KEY]) -> io::Result<()> {
|
||||
for key in keys.iter().clone() {
|
||||
self.uinput.write_event(&InputEvent {
|
||||
time: TimeVal::new(0, 0),
|
||||
event_code: EventCode::EV_KEY(*key),
|
||||
event_type: EventType::EV_KEY,
|
||||
value: 0,
|
||||
})?;
|
||||
}
|
||||
self.syn_report()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
24
src/main.rs
Normal file
24
src/main.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
mod common;
|
||||
pub mod controller;
|
||||
mod dial_device;
|
||||
mod error;
|
||||
mod fake_input;
|
||||
|
||||
pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
use crate::controller::DialController;
|
||||
use crate::dial_device::DialDevice;
|
||||
use crate::error::Error;
|
||||
|
||||
fn main() -> DynResult<()> {
|
||||
let dial = DialDevice::new()?;
|
||||
println!("Found the dial.");
|
||||
|
||||
let default_mode = Box::new(controller::controls::Volume::new(30));
|
||||
// let default_mode = Box::new(controls::Media::new(50));
|
||||
// let default_mode = Box::new(controls::DPad::new());
|
||||
|
||||
let mut controller = DialController::new(dial, default_mode);
|
||||
|
||||
controller.run()
|
||||
}
|
||||
Reference in New Issue
Block a user