initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
66
Cargo.lock
generated
Normal file
66
Cargo.lock
generated
Normal 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
8
Cargo.toml
Normal 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
109
README.md
Normal 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
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
wrap_comments = true
|
||||
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()
|
||||
}
|
||||
1
surface-dial-udev.rules
Normal file
1
surface-dial-udev.rules
Normal 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
4
surface-dial.service
Normal 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
|
||||
Reference in New Issue
Block a user