use libudev to handle device disconnect/reconnect
This change has substantially bumped up the daemon's overall robustness, as the code now ensures that the controller will only start once the /dev/input/eventXX file is set up, which was causing all sorts of issues in the past. Additionally, this change enables the daemon to run as a proper background task that _doesn't_ constantly die / need to be restarted, which removes the need for any janky udev "on add" rules, and instead, a simple systemd user service will suffice.
This commit is contained in:
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -230,6 +230,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.1"
|
||||
@@ -269,6 +279,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85db2feff6bf70ebc3a4793191517d5f0331100a2f10f9bf93b5e5214f32b7b7"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"cc",
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.0.0"
|
||||
@@ -451,9 +473,11 @@ dependencies = [
|
||||
"evdev-rs",
|
||||
"hidapi",
|
||||
"lazy_static",
|
||||
"nix",
|
||||
"notify-rust",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"udev",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -487,6 +511,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "udev"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "048df778e99eea028c08cca7853b9b521df6948b59bb29ab8bb737c057f58e6d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.0.4"
|
||||
|
||||
@@ -10,9 +10,11 @@ directories = "3.0"
|
||||
evdev-rs = { git = "https://github.com/ndesh26/evdev-rs.git", rev = "8e995b8bf" }
|
||||
hidapi = { version = "1.2.3", default-features = false, features = ["linux-shared-hidraw"] }
|
||||
lazy_static = "1.4"
|
||||
nix = "0.19.0"
|
||||
notify-rust = "4"
|
||||
parking_lot = "0.11.0"
|
||||
signal-hook = "0.1.16"
|
||||
udev = "0.5"
|
||||
|
||||
# HACK: Using >1 virtual uinput devices will segfault in release builds.
|
||||
#
|
||||
|
||||
73
README.md
73
README.md
@@ -8,13 +8,13 @@ Things will change.
|
||||
Things will break.
|
||||
Things are probably buggy.
|
||||
|
||||
You've been warned :eyes:
|
||||
Bug reports are appreciated!
|
||||
|
||||
## Overview
|
||||
|
||||
`surface-dial-daemon` is a background daemon which recieves raw events and translates them to various actions.
|
||||
`surface-dial-daemon` is a background daemon which receives raw events from the surface dial, and translates them to various actions.
|
||||
|
||||
Aside from haptic feedback, the daemon also uses FreeDesktop notifications to provide visual feedback when performing various actions.
|
||||
The daemon uses FreeDesktop notifications to provide visual feedback when switching between actions.
|
||||
|
||||

|
||||
|
||||
@@ -22,10 +22,14 @@ It would be cool to create some sort of GUI overlay (similar to the Windows one)
|
||||
|
||||
## Implementation
|
||||
|
||||
- `libevdev` to read events from the surface dial via `/dev/input/eventXX`
|
||||
- `libevdev` to fake input via `/dev/uinput` (for keypresses / media controls)
|
||||
- `hidapi` to configure dial sensitivity + haptics
|
||||
- `notify-rust` to send notifications over D-Bus
|
||||
Core functionality is provided by the following libraries.
|
||||
|
||||
- `libudev` to monitor when the dial connects/disconnects.
|
||||
- `libevdev` to read events from the surface dial through `/dev/input/eventXX`, and to fake input through `/dev/uinput`.
|
||||
- `hidapi` to configure dial sensitivity + haptics.
|
||||
- `notify-rust` to send desktop notifications over D-Bus.
|
||||
|
||||
While the device-handling code itself is somewhat messy at the moment, it should be really easy to add new operating modes. Just add a new mode implementation under `src/controller/controls` (making sure to update `src/controller/controls/mod.rs`), and add it to the list of available modes in `main.rs`!
|
||||
|
||||
## Functionality
|
||||
|
||||
@@ -34,12 +38,12 @@ It would be cool to create some sort of GUI overlay (similar to the Windows one)
|
||||
- [x] Volume Controls
|
||||
- [x] Media Controls
|
||||
- [x] Scrolling - using a virtual mouse-wheel
|
||||
- [x] Scrolling - using a virtual touchpad (for [smoother scrolling](https://who-t.blogspot.com/2020/04/high-resolution-wheel-scrolling-in.html))
|
||||
- [ ] Scrolling - using a virtual touchpad (for [smoother scrolling](https://who-t.blogspot.com/2020/04/high-resolution-wheel-scrolling-in.html)) - **WIP**
|
||||
- [x] Zooming
|
||||
- [x] [Paddle](https://www.google.com/search?q=arkanoid+paddle) (emulated left, right, and space key)
|
||||
- [ ] \(meta\) custom modes specified via config file(s)
|
||||
- [x] Dynamically switch between operating modes
|
||||
- [x] Using some-sort of on-device mechanism (e.g: long-press)
|
||||
- [x] Using a long-press activated "meta" mode
|
||||
- [ ] Context-sensitive (based on currently open application)
|
||||
- [x] Mode Persistence (keep mode when dial disconnects)
|
||||
- [x] Haptic Feedback
|
||||
@@ -47,10 +51,8 @@ It would be cool to create some sort of GUI overlay (similar to the Windows one)
|
||||
- https://www.usb.org/sites/default/files/hutrr63b_-_haptics_page_redline_0.pdf
|
||||
- https://www.usb.org/sites/default/files/hut1_21.pdf
|
||||
- _This was tricky to figure out, but in the end, it was surprisingly straightforward! Big thanks to [Geo](https://www.linkedin.com/in/geo-palakunnel-57718245/) for pointing me in the right direction!_
|
||||
- [x] Desktop Notifications
|
||||
- [x] On Launch
|
||||
- [x] When switching between modes
|
||||
- [x] When switching between sub-modes (e.g: scroll/zoom)
|
||||
- [x] Visual Feedback
|
||||
- [x] FreeDesktop Notifications
|
||||
|
||||
Feel free to contribute new features!
|
||||
|
||||
@@ -60,16 +62,17 @@ Building `surface-dial-daemon` requires the following:
|
||||
|
||||
- Linux Kernel 4.19 or higher
|
||||
- A fairly recent version of the Rust compiler
|
||||
- `libudev`
|
||||
- `libevdev`
|
||||
- `hidapi`
|
||||
|
||||
You can install Rust through [`rustup`](https://rustup.rs/).
|
||||
|
||||
Unless you're a cool hackerman, the easiest way to get `libevdev` and `hidapi` is via your distro's package manager.
|
||||
Unless you're a cool hackerman, the easiest way to get `libudev`, `libevdev`, and `hidapi` is via your distro's package manager.
|
||||
|
||||
```bash
|
||||
# e.g: on ubuntu
|
||||
sudo apt install libevdev-dev libhidapi-dev
|
||||
sudo apt install libevdev-dev libhidapi-dev libudev-dev
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -82,20 +85,37 @@ cargo build -p surface-dial-daemon --release
|
||||
|
||||
The resulting binary is output to `target/release/surface-dial-daemon`
|
||||
|
||||
## Running
|
||||
|
||||
The daemon is able to handle the dial disconnecting/reconnecting, so as long as it's running in the background, things should Just Work:tm:.
|
||||
|
||||
Note that the daemon must run as a _user process_ (**not** as root), as it needs access to the user's D-Bus to send notifications.
|
||||
|
||||
Having to run as a user process complicates things a bit, as the daemon must be able to access several restricted-by-default devices under `/dev/`. Notably, the `/dev/uinput` device will need to have it's permissions changed for things to work correctly. The proper way to do this is using the included [udev rule](https://wiki.debian.org/udev), though if you just want to get something up and running, `sudo chmod 666 /dev/uinput` should work fine (though it will revert back once you reboot!).
|
||||
|
||||
See the Installation instructions below for how to set up the permissions / udev rules.
|
||||
|
||||
During development, the easiest way to run `surface-dial-linux` is using `cargo`:
|
||||
|
||||
```bash
|
||||
cargo run -p surface-dial-daemon
|
||||
```
|
||||
|
||||
Alternatively, you can run the daemon directly using the executable at `target/release/surface-dial-daemon`.
|
||||
|
||||
## Installation
|
||||
|
||||
At the moment, the daemon dies whenever the Surface Dial disconnects (which naturally happens after a brief period of inactivity).
|
||||
I encourage you to tweak the following setup procedure for your particular linux configuration.
|
||||
|
||||
Instead of doing the Right Thing :tm: and having the daemon detect when the dial connects/disconnects (PRs appreciated!), I've come up with a [cunning plan](https://www.youtube.com/watch?v=AsXKS8Nyu8Q) to spawn the daemon whenever the Surface Dial connects.
|
||||
|
||||
This will only work on systems with `systemd`.
|
||||
The following steps have been tested working on Ubuntu 20.04/20.10.
|
||||
|
||||
```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
|
||||
# IMPORTANT: modify the .service file to reflect where you placed the `service-dial-daemon` executable.
|
||||
# if you used `cargo install`, this should be as simple as replacing `danielprilik` with your own user id
|
||||
vi ./install/surface-dial.service
|
||||
|
||||
# create new group for uinput
|
||||
@@ -110,17 +130,22 @@ sudo gpasswd -a $(whoami) $(stat -c "%G" /dev/input/event0)
|
||||
mkdir -p ~/.config/systemd/user/
|
||||
cp ./install/surface-dial.service ~/.config/systemd/user/surface-dial.service
|
||||
|
||||
# install the udev rules
|
||||
# install the udev rule
|
||||
sudo cp ./install/99-uinput.rules /etc/udev/rules.d/99-uinput.rules
|
||||
sudo cp ./install/50-surface-dial.rules /etc/udev/rules.d/50-surface-dial.rules
|
||||
|
||||
# reload systemd + udev
|
||||
systemctl --user daemon-reload
|
||||
sudo udevadm control --reload
|
||||
|
||||
# enable and start the user service
|
||||
systemctl --user enable surface-dial.service
|
||||
systemctl --user start surface-dial.service
|
||||
```
|
||||
|
||||
To see if the service is running correctly, run `systemctl --user status surface-dial.service`.
|
||||
|
||||
You may need to reboot to have the various groups / udev rules propagate.
|
||||
|
||||
## License
|
||||
If things aren't working, feel free to file a bug report!
|
||||
|
||||
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.
|
||||
_Call for Contributors:_ It would be awesome to have a proper rpm/deb package as well.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ACTION=="add", ATTR{name}=="Surface Dial System Multi Axis", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="surface-dial.service"
|
||||
@@ -1,5 +1,12 @@
|
||||
# IMPORTANT: modify the "ExecStart" field to reflect your userid + surface-dial-daemon install dir
|
||||
# IMPORTANT: modify the "ExecStart" field to reflect your surface-dial-daemon install dir
|
||||
|
||||
[Unit]
|
||||
Description=Surface Dial Daemon
|
||||
|
||||
[Service]
|
||||
# HACK: this service needs to run _after_ the /dev/input/eventXX files have been created
|
||||
ExecStart=bash -c 'sleep 1 && /home/danielprilik/.cargo/bin/surface-dial-daemon'
|
||||
Type=simple
|
||||
StandardOutput=journal
|
||||
ExecStart=/home/danielprilik/.cargo/bin/surface-dial-daemon
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct DialController {
|
||||
active_mode: ActiveMode,
|
||||
|
||||
new_mode: Arc<Mutex<Option<usize>>>,
|
||||
meta_mode: Box<dyn ControlMode>, // always MetaMode
|
||||
meta_mode: Box<dyn ControlMode>, // concrete type is always `MetaMode`
|
||||
}
|
||||
|
||||
impl DialController {
|
||||
@@ -60,12 +60,6 @@ impl DialController {
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
let initial_mode = match self.active_mode {
|
||||
ActiveMode::Normal(i) => i,
|
||||
ActiveMode::Meta => 0,
|
||||
};
|
||||
self.modes[initial_mode].on_start(self.device.haptics())?;
|
||||
|
||||
loop {
|
||||
let evt = self.device.next_event()?;
|
||||
let haptics = self.device.haptics();
|
||||
@@ -80,13 +74,22 @@ impl DialController {
|
||||
ActiveMode::Meta => &mut self.meta_mode,
|
||||
};
|
||||
|
||||
// TODO: press and hold (+ rotate?) to switch between modes
|
||||
|
||||
match evt.kind {
|
||||
DialEventKind::Ignored => {}
|
||||
|
||||
DialEventKind::Connect => {
|
||||
eprintln!("Dial Connected");
|
||||
mode.on_start(haptics)?
|
||||
}
|
||||
DialEventKind::Disconnect => {
|
||||
eprintln!("Dial Disconnected");
|
||||
mode.on_end(haptics)?
|
||||
}
|
||||
|
||||
DialEventKind::ButtonPress => mode.on_btn_press(haptics)?,
|
||||
DialEventKind::ButtonRelease => mode.on_btn_release(haptics)?,
|
||||
DialEventKind::Dial(delta) => mode.on_dial(haptics, delta)?,
|
||||
|
||||
DialEventKind::ButtonLongPress => {
|
||||
eprintln!("long press!");
|
||||
if !matches!(self.active_mode, ActiveMode::Meta) {
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
use std::fs;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use evdev_rs::{Device, InputEvent, ReadStatus};
|
||||
use hidapi::{HidApi, HidDevice};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
pub struct DialDevice {
|
||||
long_press_timeout: Duration,
|
||||
haptics: DialHaptics,
|
||||
events: mpsc::Receiver<std::io::Result<(ReadStatus, InputEvent)>>,
|
||||
|
||||
possible_long_press: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DialEvent {
|
||||
pub time: Duration,
|
||||
pub kind: DialEventKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DialEventKind {
|
||||
Ignored,
|
||||
ButtonPress,
|
||||
ButtonRelease,
|
||||
Dial(i32),
|
||||
ButtonLongPress,
|
||||
}
|
||||
|
||||
impl DialDevice {
|
||||
pub fn new(long_press_timeout: Duration) -> Result<DialDevice> {
|
||||
let mut control = None;
|
||||
let mut axis = None;
|
||||
|
||||
// discover the evdev devices
|
||||
for e in fs::read_dir("/dev/input/").map_err(Error::OpenDevInputDir)? {
|
||||
let e = e.map_err(Error::OpenDevInputDir)?;
|
||||
if !e.file_name().to_str().unwrap().starts_with("event") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file =
|
||||
fs::File::open(e.path()).map_err(|err| Error::OpenEventFile(e.path(), err))?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: explore what the control channel can be used for...
|
||||
let _control = control.ok_or(Error::MissingDial)?;
|
||||
let axis = axis.ok_or(Error::MissingDial)?;
|
||||
|
||||
let (events_tx, events_rx) = mpsc::channel();
|
||||
|
||||
// TODO: interleave control events with regular events
|
||||
// (once we figure out what control events actually do...)
|
||||
|
||||
// inb4 "y not async/await"
|
||||
std::thread::spawn({
|
||||
let events = events_tx;
|
||||
move || loop {
|
||||
let _ = events.send(axis.next_event(evdev_rs::ReadFlag::NORMAL));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DialDevice {
|
||||
long_press_timeout,
|
||||
events: events_rx,
|
||||
haptics: DialHaptics::new()?,
|
||||
|
||||
possible_long_press: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_event(&mut self) -> Result<DialEvent> {
|
||||
let evt = if self.possible_long_press {
|
||||
self.events.recv_timeout(self.long_press_timeout)
|
||||
} else {
|
||||
self.events
|
||||
.recv()
|
||||
.map_err(|_| mpsc::RecvTimeoutError::Disconnected)
|
||||
};
|
||||
|
||||
let event = match evt {
|
||||
Ok(Ok((_event_status, event))) => {
|
||||
// assert!(matches!(axis_status, ReadStatus::Success));
|
||||
let event =
|
||||
DialEvent::from_raw_evt(event.clone()).ok_or(Error::UnexpectedEvt(event))?;
|
||||
match event.kind {
|
||||
DialEventKind::ButtonPress => self.possible_long_press = true,
|
||||
DialEventKind::ButtonRelease => self.possible_long_press = false,
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
Ok(Err(e)) => return Err(Error::Evdev(e)),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
self.possible_long_press = false;
|
||||
DialEvent {
|
||||
time: Duration::from_secs(0), // this could be improved...
|
||||
kind: DialEventKind::ButtonLongPress,
|
||||
}
|
||||
}
|
||||
Err(_e) => panic!("Could not recv event"),
|
||||
};
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub fn haptics(&self) -> &DialHaptics {
|
||||
&self.haptics
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DialHaptics {
|
||||
hid_device: HidDevice,
|
||||
}
|
||||
|
||||
impl DialHaptics {
|
||||
fn new() -> Result<DialHaptics> {
|
||||
let api = HidApi::new().map_err(Error::HidError)?;
|
||||
let hid_device = api.open(0x045e, 0x091b).map_err(|_| Error::MissingDial)?;
|
||||
|
||||
// let mut buf = [0; 256];
|
||||
|
||||
// buf[0] = 1;
|
||||
// let len = device
|
||||
// .get_feature_report(&mut buf)
|
||||
// .map_err(Error::HidError)?;
|
||||
// eprintln!("1: {:02x?}", &buf[..len]);
|
||||
|
||||
// buf[0] = 2;
|
||||
// let len = device
|
||||
// .get_feature_report(&mut buf)
|
||||
// .map_err(Error::HidError)?;
|
||||
// eprintln!("2: {:02x?}", &buf[..len]);
|
||||
|
||||
Ok(DialHaptics { hid_device })
|
||||
}
|
||||
|
||||
/// `steps` should be a value between 0 and 3600, which corresponds to the
|
||||
/// number of subdivisions the dial should use. If left unspecified, this
|
||||
/// defaults to 36 (an arbitrary choice that "feels good" most of the time)
|
||||
pub fn set_mode(&self, haptics: bool, steps: Option<u16>) -> Result<()> {
|
||||
let steps = steps.unwrap_or(36);
|
||||
assert!(steps <= 3600);
|
||||
|
||||
let steps_lo = steps & 0xff;
|
||||
let steps_hi = (steps >> 8) & 0xff;
|
||||
|
||||
let mut buf = [0; 8];
|
||||
buf[0] = 1;
|
||||
buf[1] = steps_lo as u8; // steps
|
||||
buf[2] = steps_hi as u8; // steps
|
||||
buf[3] = 0x00; // Repeat Count
|
||||
buf[4] = if haptics { 0x03 } else { 0x02 }; // auto trigger
|
||||
buf[5] = 0x00; // Waveform Cutoff Time
|
||||
buf[6] = 0x00; // retrigger period
|
||||
buf[7] = 0x00; // retrigger period
|
||||
self.hid_device
|
||||
.send_feature_report(&buf[..8])
|
||||
.map_err(Error::HidError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn buzz(&self, repeat: u8) -> Result<()> {
|
||||
let mut buf = [0; 5];
|
||||
buf[0] = 0x01; // Report ID
|
||||
buf[1] = repeat; // RepeatCount
|
||||
buf[2] = 0x03; // ManualTrigger
|
||||
buf[3] = 0x00; // RetriggerPeriod (lo)
|
||||
buf[4] = 0x00; // RetriggerPeriod (hi)
|
||||
self.hid_device.write(&buf).map_err(Error::HidError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
154
src/dial_device/events.rs
Normal file
154
src/dial_device/events.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use std::fs;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use evdev_rs::{InputEvent, ReadStatus};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use super::DialHapticsWorkerMsg;
|
||||
|
||||
pub enum RawInputEvent {
|
||||
Event(ReadStatus, InputEvent),
|
||||
Connect,
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum DialInputKind {
|
||||
Control,
|
||||
MultiAxis,
|
||||
}
|
||||
|
||||
pub struct EventsWorker {
|
||||
events: mpsc::Sender<RawInputEvent>,
|
||||
haptics_msg: mpsc::Sender<DialHapticsWorkerMsg>,
|
||||
input_kind: DialInputKind,
|
||||
}
|
||||
|
||||
impl EventsWorker {
|
||||
pub(super) fn new(
|
||||
input_kind: DialInputKind,
|
||||
events: mpsc::Sender<RawInputEvent>,
|
||||
haptics_msg: mpsc::Sender<DialHapticsWorkerMsg>,
|
||||
) -> EventsWorker {
|
||||
EventsWorker {
|
||||
input_kind,
|
||||
events,
|
||||
haptics_msg,
|
||||
}
|
||||
}
|
||||
|
||||
fn udev_to_evdev(&self, device: &udev::Device) -> std::io::Result<Option<evdev_rs::Device>> {
|
||||
let devnode = match device.devnode() {
|
||||
Some(path) => path,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// we care about the `/dev/input/eventXX` device, which is a child of the
|
||||
// actual input device (that has a nice name we can match against)
|
||||
match device.parent() {
|
||||
None => return Ok(None),
|
||||
Some(parent) => {
|
||||
let name = parent
|
||||
.property_value("NAME")
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_string_lossy();
|
||||
|
||||
match (self.input_kind, name.as_ref()) {
|
||||
(DialInputKind::Control, r#""Surface Dial System Control""#) => {}
|
||||
(DialInputKind::MultiAxis, r#""Surface Dial System Multi Axis""#) => {}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file = fs::File::open(devnode)?;
|
||||
evdev_rs::Device::new_from_fd(file).map(Some)
|
||||
}
|
||||
|
||||
fn event_loop(&mut self, device: evdev_rs::Device) -> std::io::Result<()> {
|
||||
// HACK: don't want to double-send these events
|
||||
if self.input_kind != DialInputKind::Control {
|
||||
self.haptics_msg
|
||||
.send(DialHapticsWorkerMsg::DialConnected)
|
||||
.unwrap();
|
||||
self.events.send(RawInputEvent::Connect).unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
let _ = self
|
||||
.events
|
||||
.send(match device.next_event(evdev_rs::ReadFlag::NORMAL) {
|
||||
Ok((read_status, event)) => RawInputEvent::Event(read_status, event),
|
||||
// this error corresponds to the device disconnecting, which is fine
|
||||
Err(e) if e.raw_os_error() == Some(19) => break,
|
||||
Err(e) => return Err(e),
|
||||
});
|
||||
}
|
||||
|
||||
// HACK: don't want to double-send these events
|
||||
if self.input_kind != DialInputKind::Control {
|
||||
self.haptics_msg
|
||||
.send(DialHapticsWorkerMsg::DialDisconnected)
|
||||
.unwrap();
|
||||
self.events.send(RawInputEvent::Disconnect).unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> std::io::Result<()> {
|
||||
// eagerly check if the device already exists
|
||||
|
||||
let mut enumerator = {
|
||||
let mut e = udev::Enumerator::new()?;
|
||||
e.match_subsystem("input")?;
|
||||
e
|
||||
};
|
||||
for device in enumerator.scan_devices()? {
|
||||
let dev = match self.udev_to_evdev(&device)? {
|
||||
None => continue,
|
||||
Some(dev) => dev,
|
||||
};
|
||||
|
||||
self.event_loop(dev)?;
|
||||
}
|
||||
|
||||
// enter udev event loop to gracefully handle disconnect/reconnect
|
||||
|
||||
let mut socket = udev::MonitorBuilder::new()?
|
||||
.match_subsystem("input")?
|
||||
.listen()?;
|
||||
|
||||
loop {
|
||||
nix::poll::ppoll(
|
||||
&mut [nix::poll::PollFd::new(
|
||||
socket.as_raw_fd(),
|
||||
nix::poll::PollFlags::POLLIN,
|
||||
)],
|
||||
None,
|
||||
nix::sys::signal::SigSet::empty(),
|
||||
)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
|
||||
let event = match socket.next() {
|
||||
Some(evt) => evt,
|
||||
None => {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !matches!(event.event_type(), udev::EventType::Add) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dev = match self.udev_to_evdev(&event.device())? {
|
||||
None => continue,
|
||||
Some(dev) => dev,
|
||||
};
|
||||
|
||||
self.event_loop(dev)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/dial_device/haptics.rs
Normal file
123
src/dial_device/haptics.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::sync::mpsc;
|
||||
|
||||
use hidapi::{HidApi, HidDevice};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Proxy object - forwards requests to the DialHapticsWorker task
|
||||
pub struct DialHaptics {
|
||||
msg: mpsc::Sender<DialHapticsWorkerMsg>,
|
||||
}
|
||||
|
||||
impl DialHaptics {
|
||||
pub(super) fn new(msg: mpsc::Sender<DialHapticsWorkerMsg>) -> Result<DialHaptics> {
|
||||
Ok(DialHaptics { msg })
|
||||
}
|
||||
|
||||
/// `steps` should be a value between 0 and 3600, which corresponds to the
|
||||
/// number of subdivisions the dial should use. If left unspecified, this
|
||||
/// defaults to 36 (an arbitrary choice that "feels good" most of the time)
|
||||
pub fn set_mode(&self, haptics: bool, steps: Option<u16>) -> Result<()> {
|
||||
let _ = (self.msg).send(DialHapticsWorkerMsg::SetMode { haptics, steps });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn buzz(&self, repeat: u8) -> Result<()> {
|
||||
let _ = (self.msg).send(DialHapticsWorkerMsg::Manual { repeat });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum DialHapticsWorkerMsg {
|
||||
DialConnected,
|
||||
DialDisconnected,
|
||||
SetMode { haptics: bool, steps: Option<u16> },
|
||||
Manual { repeat: u8 },
|
||||
}
|
||||
|
||||
pub(super) struct DialHapticsWorker {
|
||||
msg: mpsc::Receiver<DialHapticsWorkerMsg>,
|
||||
}
|
||||
|
||||
impl DialHapticsWorker {
|
||||
pub(super) fn new(msg: mpsc::Receiver<DialHapticsWorkerMsg>) -> Result<DialHapticsWorker> {
|
||||
Ok(DialHapticsWorker { msg })
|
||||
}
|
||||
|
||||
pub(super) fn run(&mut self) -> Result<()> {
|
||||
loop {
|
||||
eprintln!("haptics worker is waiting...");
|
||||
|
||||
loop {
|
||||
match self.msg.recv().unwrap() {
|
||||
DialHapticsWorkerMsg::DialConnected => break,
|
||||
other => eprintln!("haptics worker dropped an event: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("haptics worker is ready");
|
||||
|
||||
let api = HidApi::new().map_err(Error::HidError)?;
|
||||
let hid_device = api.open(0x045e, 0x091b).map_err(|_| Error::MissingDial)?;
|
||||
let wrapper = DialHidWrapper { hid_device };
|
||||
|
||||
loop {
|
||||
match self.msg.recv().unwrap() {
|
||||
DialHapticsWorkerMsg::DialConnected => {
|
||||
eprintln!("Unexpected haptics worker ready event.");
|
||||
// should be fine though?
|
||||
}
|
||||
DialHapticsWorkerMsg::DialDisconnected => break,
|
||||
DialHapticsWorkerMsg::SetMode { haptics, steps } => {
|
||||
wrapper.set_mode(haptics, steps)?
|
||||
}
|
||||
DialHapticsWorkerMsg::Manual { repeat } => wrapper.buzz(repeat)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DialHidWrapper {
|
||||
hid_device: HidDevice,
|
||||
}
|
||||
|
||||
impl DialHidWrapper {
|
||||
/// `steps` should be a value between 0 and 3600, which corresponds to the
|
||||
/// number of subdivisions the dial should use. If left unspecified, this
|
||||
/// defaults to 36 (an arbitrary choice that "feels good" most of the time)
|
||||
fn set_mode(&self, haptics: bool, steps: Option<u16>) -> Result<()> {
|
||||
let steps = steps.unwrap_or(36);
|
||||
assert!(steps <= 3600);
|
||||
|
||||
let steps_lo = steps & 0xff;
|
||||
let steps_hi = (steps >> 8) & 0xff;
|
||||
|
||||
let mut buf = [0; 8];
|
||||
buf[0] = 1;
|
||||
buf[1] = steps_lo as u8; // steps
|
||||
buf[2] = steps_hi as u8; // steps
|
||||
buf[3] = 0x00; // Repeat Count
|
||||
buf[4] = if haptics { 0x03 } else { 0x02 }; // auto trigger
|
||||
buf[5] = 0x00; // Waveform Cutoff Time
|
||||
buf[6] = 0x00; // retrigger period
|
||||
buf[7] = 0x00; // retrigger period
|
||||
self.hid_device
|
||||
.send_feature_report(&buf[..8])
|
||||
.map_err(Error::HidError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn buzz(&self, repeat: u8) -> Result<()> {
|
||||
let mut buf = [0; 5];
|
||||
buf[0] = 0x01; // Report ID
|
||||
buf[1] = repeat; // RepeatCount
|
||||
buf[2] = 0x03; // ManualTrigger
|
||||
buf[3] = 0x00; // RetriggerPeriod (lo)
|
||||
buf[4] = 0x00; // RetriggerPeriod (hi)
|
||||
self.hid_device.write(&buf).map_err(Error::HidError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
170
src/dial_device/mod.rs
Normal file
170
src/dial_device/mod.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
mod events;
|
||||
mod haptics;
|
||||
|
||||
use haptics::{DialHapticsWorker, DialHapticsWorkerMsg};
|
||||
|
||||
pub use haptics::DialHaptics;
|
||||
|
||||
/// Encapsulates all the the nitty-gritty (and pretty gnarly) device handling
|
||||
/// code, exposing a simple interface to wait for incoming [`DialEvent`]s.
|
||||
pub struct DialDevice {
|
||||
// configurable constants
|
||||
long_press_timeout: Duration,
|
||||
|
||||
// handles
|
||||
haptics: DialHaptics,
|
||||
events: mpsc::Receiver<events::RawInputEvent>,
|
||||
|
||||
// mutable state
|
||||
possible_long_press: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DialEvent {
|
||||
pub time: Duration,
|
||||
pub kind: DialEventKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DialEventKind {
|
||||
Connect,
|
||||
Disconnect,
|
||||
|
||||
Ignored,
|
||||
ButtonPress,
|
||||
ButtonRelease,
|
||||
Dial(i32),
|
||||
|
||||
/// NOTE: this is a synthetic event, and is _not_ directly provided by the
|
||||
/// dial itself.
|
||||
ButtonLongPress,
|
||||
}
|
||||
|
||||
impl DialDevice {
|
||||
pub fn new(long_press_timeout: Duration) -> Result<DialDevice> {
|
||||
let (events_tx, events_rx) = mpsc::channel();
|
||||
let (haptics_msg_tx, haptics_msg_rx) = mpsc::channel();
|
||||
|
||||
// TODO: interleave control events with regular events
|
||||
// (once we figure out what control events actually do...)
|
||||
|
||||
std::thread::spawn({
|
||||
let haptics_msg_tx = haptics_msg_tx.clone();
|
||||
let mut worker = events::EventsWorker::new(
|
||||
events::DialInputKind::MultiAxis,
|
||||
events_tx,
|
||||
haptics_msg_tx,
|
||||
);
|
||||
move || {
|
||||
worker.run().unwrap();
|
||||
eprintln!("the events worker died!");
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn({
|
||||
let mut worker = DialHapticsWorker::new(haptics_msg_rx)?;
|
||||
move || {
|
||||
worker.run().unwrap();
|
||||
eprintln!("the haptics worker died!");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DialDevice {
|
||||
long_press_timeout,
|
||||
events: events_rx,
|
||||
haptics: DialHaptics::new(haptics_msg_tx)?,
|
||||
|
||||
possible_long_press: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Blocks until a new dial event comes occurs.
|
||||
// TODO?: rewrite code using async/await?
|
||||
// TODO?: "cheat" by exposing an async interface to the current next_event impl
|
||||
pub fn next_event(&mut self) -> Result<DialEvent> {
|
||||
let evt = if self.possible_long_press {
|
||||
self.events.recv_timeout(self.long_press_timeout)
|
||||
} else {
|
||||
self.events
|
||||
.recv()
|
||||
.map_err(|_| mpsc::RecvTimeoutError::Disconnected)
|
||||
};
|
||||
|
||||
let event = match evt {
|
||||
Ok(events::RawInputEvent::Event(_event_status, event)) => {
|
||||
// assert!(matches!(axis_status, ReadStatus::Success));
|
||||
let event =
|
||||
DialEvent::from_raw_evt(event.clone()).ok_or(Error::UnexpectedEvt(event))?;
|
||||
|
||||
match event.kind {
|
||||
DialEventKind::ButtonPress => self.possible_long_press = true,
|
||||
DialEventKind::ButtonRelease => self.possible_long_press = false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
Ok(events::RawInputEvent::Connect) => {
|
||||
DialEvent {
|
||||
time: Duration::from_secs(0), // this could be improved...
|
||||
kind: DialEventKind::Connect,
|
||||
}
|
||||
}
|
||||
Ok(events::RawInputEvent::Disconnect) => {
|
||||
DialEvent {
|
||||
time: Duration::from_secs(0), // this could be improved...
|
||||
kind: DialEventKind::Disconnect,
|
||||
}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
self.possible_long_press = false;
|
||||
DialEvent {
|
||||
time: Duration::from_secs(0), // this could be improved...
|
||||
kind: DialEventKind::ButtonLongPress,
|
||||
}
|
||||
}
|
||||
Err(_e) => panic!("Could not recv event"),
|
||||
};
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub fn haptics(&self) -> &DialHaptics {
|
||||
&self.haptics
|
||||
}
|
||||
}
|
||||
|
||||
impl DialEvent {
|
||||
fn from_raw_evt(evt: evdev_rs::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)
|
||||
}
|
||||
}
|
||||
14
src/main.rs
14
src/main.rs
@@ -1,3 +1,4 @@
|
||||
#![deny(unsafe_code)]
|
||||
#![allow(clippy::collapsible_if, clippy::new_without_default)]
|
||||
|
||||
pub mod common;
|
||||
@@ -37,16 +38,6 @@ fn main() {
|
||||
}
|
||||
});
|
||||
|
||||
let active_notification = Notification::new()
|
||||
.hint(Hint::Resident(true))
|
||||
.hint(Hint::Category("device".into()))
|
||||
.timeout(Timeout::Never)
|
||||
.summary("Surface Dial")
|
||||
.body("Active!")
|
||||
.icon("media-optical") // it should be vaguely circular :P
|
||||
.show()
|
||||
.expect("Failed to send notification. NOTE: this daemon (probably) can't run as root!");
|
||||
|
||||
let (silent, msg, icon) = match terminate_rx.recv() {
|
||||
Ok(Ok(())) => (true, "".into(), ""),
|
||||
Ok(Err(e)) => {
|
||||
@@ -64,8 +55,6 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
active_notification.close();
|
||||
|
||||
if !silent {
|
||||
Notification::new()
|
||||
.hint(Hint::Transient(true))
|
||||
@@ -88,7 +77,6 @@ fn controller_main() -> Result<()> {
|
||||
let cfg = config::Config::from_disk()?;
|
||||
|
||||
let dial = DialDevice::new(std::time::Duration::from_millis(750))?;
|
||||
println!("Found the dial");
|
||||
|
||||
let mut controller = DialController::new(
|
||||
dial,
|
||||
|
||||
Reference in New Issue
Block a user