1
0

initial commit

This commit is contained in:
Daniel Prilik
2020-10-29 00:30:06 -04:00
commit c4039edc55
17 changed files with 783 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

66
Cargo.lock generated Normal file
View File

@@ -0,0 +1,66 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "cc"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "evdev-rs"
version = "0.4.0"
dependencies = [
"bitflags",
"evdev-sys",
"libc",
"log",
]
[[package]]
name = "evdev-sys"
version = "0.2.1"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "libc"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
[[package]]
name = "log"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if",
]
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "surface-dial-daemon"
version = "0.1.0"
dependencies = [
"evdev-rs",
]

8
Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "surface-dial-daemon"
version = "0.1.0"
authors = ["Daniel Prilik <danielprilik@gmail.com>"]
edition = "2018"
[dependencies]
evdev-rs = { path = "../evdev-rs/" }

109
README.md Normal file
View File

@@ -0,0 +1,109 @@
# surface-dial-linux
A Linux userspace controller for the [Microsoft Surface Dial](https://www.microsoft.com/en-us/p/surface-dial/925r551sktgn). Requires Linux Kernel 4.19 or higher.
- Uses the [`evdev`](https://en.wikipedia.org/wiki/Evdev) API + `libevdev` to read events from the surface dial.
- Uses `libevdev` to fake input via `/dev/uinput` (for keypresses / media controls)
**DISCLAIMER: This is WIP software!**
Things will change.
Things will break.
Things are probably buggy.
There's also a non-zero chance that I'll just stop working on it at some point once I decide that it's Good Enough:tm: for me.
You've been warned :eyes:
## Overview
Consists of two components:
- `surface-dial-daemon` - A background daemon which recieves raw events and translates them to various actions.
- `surface-dial-cli` - Controller to configure daemon functionality (e.g: change operating modes)
It would be cool to create some sort of GUI overlay (similar to the Windows one), though that's a bit out of scope at the moment.
## Functionality
- [x] Interpret raw Surface Dial event
- [ ] Dynamically switch between operating modes
- [ ] Context-sensitive (based on currently open application)
- [ ] Using `surface-dial-cli` application
- [ ] Using some-sort of on-device mechanism (e.g: long-press)
- Various Operating Modes
- [x] Volume Controls
- [x] Media Controls
- [x] D-Pad (emulated left, right, and space key)
- [ ] Scrolling / Zooming
Feel free to suggest / contribute new features!
## Building
Building `surface-dial-daemon` requires the following:
- A fairly recent version of the Rust compiler
- `libevdev`
If `libevdev` is not installed, the `evdev_rs` Rust library will try to build it from source, which may require other bits of build tooling. As such, it's recommended to install `libevdev` if it's available through your distribution.
```bash
# e.g: on ubuntu
sudo apt install libevdev-dev
```
Otherwise, `surface-dial-daemon` uses the bog-standard `cargo build` flow.
```bash
cargo build -p surface-dial-daemon --release
```
The resulting binary is output to `target/release/surface-dial-daemon`
## Running `surface-dial-daemon`
For testing changes locally, you'll typically want to run the following:
```bash
cargo build -p surface-dial-daemon && sudo target/debug/surface-dial-daemon
```
Note the use of `sudo`, as `surface-dial-daemon` requires permission to access files under `/dev/input/` and `/dev/uinput`.
## Using `surface-dial-cli`
TODO (the controller cli doesn't exist yet lol)
## Installation
As you might have noticed, the daemon dies whenever the Surface Dial disconnects (which happens after a brief period of inactivity).
I personally haven't figured out a good way to have the daemon gracefully handle the dial connecting/disconnecting (PRs appreciated!), so instead, I've come up with a [cunning plan](https://www.youtube.com/watch?v=AsXKS8Nyu8Q) to spawn the daemon whenever the Surface Dial connects :wink:
This will only work on systems with `systemd`.
If your distro doesn't use `systemd`, you'll have to come up with something yourself I'm afraid...
```bash
# Install the `surface-dial-daemon` (i.e: build it, and place it under ~/.cargo/bin/surface-dial-daemon)
# You could also just copy the executable from /target/release/surface-dial-daemon to wherever you like.
cargo install --path .
# IMPORTANT: modify the .service file to reflect where you placed the `service-dial-daemon` executable
vi surface-dial.service
# install the systemd service
sudo cp surface-dial.service /etc/systemd/system/surface-dial.service
# install the service-dial udev rule
sudo cp surface-dial-udev.rules /etc/udev/rules.d/50-surface-dial.rules
# reload systemd + udev
sudo systemctl daemon-reload
sudo udevadm control --reload
```
You many need to disconnect + reconnect the Surface Dial for the `udev` rule to trigger.
## License
At the moment, this software is deliberately unlicensed. I'm not opposed to adding a license at some point, it's moreso that I don't think the project is at the stage where it needs a license.

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
wrap_comments = true

34
src/common.rs Normal file
View 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
}
}

View 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(())
}
}

View 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(())
}
}

View File

@@ -0,0 +1,7 @@
mod dpad;
mod media;
mod volume;
pub use self::dpad::*;
pub use self::media::*;
pub use self::volume::*;

View 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
View 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
View 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
View 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
View 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
View 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()
}

1
surface-dial-udev.rules Normal file
View File

@@ -0,0 +1 @@
ACTION=="add", ATTR{name}=="Surface Dial System Multi Axis", TAG+="systemd", ENV{SYSTEMD_WANTS}="surface-dial.service"

4
surface-dial.service Normal file
View File

@@ -0,0 +1,4 @@
# modify ExecStart to wherever you put surface-dial-daemon
[Service]
Type=oneshot
ExecStart=/home/danielprilik/.cargo/bin/surface-dial-daemon