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",
|
"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]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -269,6 +279,18 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "notify-rust"
|
name = "notify-rust"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
@@ -451,9 +473,11 @@ dependencies = [
|
|||||||
"evdev-rs",
|
"evdev-rs",
|
||||||
"hidapi",
|
"hidapi",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"nix",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
|
"udev",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -487,6 +511,16 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ directories = "3.0"
|
|||||||
evdev-rs = { git = "https://github.com/ndesh26/evdev-rs.git", rev = "8e995b8bf" }
|
evdev-rs = { git = "https://github.com/ndesh26/evdev-rs.git", rev = "8e995b8bf" }
|
||||||
hidapi = { version = "1.2.3", default-features = false, features = ["linux-shared-hidraw"] }
|
hidapi = { version = "1.2.3", default-features = false, features = ["linux-shared-hidraw"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
nix = "0.19.0"
|
||||||
notify-rust = "4"
|
notify-rust = "4"
|
||||||
parking_lot = "0.11.0"
|
parking_lot = "0.11.0"
|
||||||
signal-hook = "0.1.16"
|
signal-hook = "0.1.16"
|
||||||
|
udev = "0.5"
|
||||||
|
|
||||||
# HACK: Using >1 virtual uinput devices will segfault in release builds.
|
# 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 will break.
|
||||||
Things are probably buggy.
|
Things are probably buggy.
|
||||||
|
|
||||||
You've been warned :eyes:
|
Bug reports are appreciated!
|
||||||
|
|
||||||
## Overview
|
## 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
|
## Implementation
|
||||||
|
|
||||||
- `libevdev` to read events from the surface dial via `/dev/input/eventXX`
|
Core functionality is provided by the following libraries.
|
||||||
- `libevdev` to fake input via `/dev/uinput` (for keypresses / media controls)
|
|
||||||
- `hidapi` to configure dial sensitivity + haptics
|
- `libudev` to monitor when the dial connects/disconnects.
|
||||||
- `notify-rust` to send notifications over D-Bus
|
- `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
|
## 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] Volume Controls
|
||||||
- [x] Media Controls
|
- [x] Media Controls
|
||||||
- [x] Scrolling - using a virtual mouse-wheel
|
- [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] Zooming
|
||||||
- [x] [Paddle](https://www.google.com/search?q=arkanoid+paddle) (emulated left, right, and space key)
|
- [x] [Paddle](https://www.google.com/search?q=arkanoid+paddle) (emulated left, right, and space key)
|
||||||
- [ ] \(meta\) custom modes specified via config file(s)
|
- [ ] \(meta\) custom modes specified via config file(s)
|
||||||
- [x] Dynamically switch between operating modes
|
- [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)
|
- [ ] Context-sensitive (based on currently open application)
|
||||||
- [x] Mode Persistence (keep mode when dial disconnects)
|
- [x] Mode Persistence (keep mode when dial disconnects)
|
||||||
- [x] Haptic Feedback
|
- [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/hutrr63b_-_haptics_page_redline_0.pdf
|
||||||
- https://www.usb.org/sites/default/files/hut1_21.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!_
|
- _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] Visual Feedback
|
||||||
- [x] On Launch
|
- [x] FreeDesktop Notifications
|
||||||
- [x] When switching between modes
|
|
||||||
- [x] When switching between sub-modes (e.g: scroll/zoom)
|
|
||||||
|
|
||||||
Feel free to contribute new features!
|
Feel free to contribute new features!
|
||||||
|
|
||||||
@@ -60,16 +62,17 @@ Building `surface-dial-daemon` requires the following:
|
|||||||
|
|
||||||
- Linux Kernel 4.19 or higher
|
- Linux Kernel 4.19 or higher
|
||||||
- A fairly recent version of the Rust compiler
|
- A fairly recent version of the Rust compiler
|
||||||
|
- `libudev`
|
||||||
- `libevdev`
|
- `libevdev`
|
||||||
- `hidapi`
|
- `hidapi`
|
||||||
|
|
||||||
You can install Rust through [`rustup`](https://rustup.rs/).
|
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
|
```bash
|
||||||
# e.g: on ubuntu
|
# e.g: on ubuntu
|
||||||
sudo apt install libevdev-dev libhidapi-dev
|
sudo apt install libevdev-dev libhidapi-dev libudev-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -82,20 +85,37 @@ cargo build -p surface-dial-daemon --release
|
|||||||
|
|
||||||
The resulting binary is output to `target/release/surface-dial-daemon`
|
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
|
## 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.
|
The following steps have been tested working on Ubuntu 20.04/20.10.
|
||||||
|
|
||||||
This will only work on systems with `systemd`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install the `surface-dial-daemon` (i.e: build it, and place it under ~/.cargo/bin/surface-dial-daemon)
|
# 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.
|
# You could also just copy the executable from /target/release/surface-dial-daemon to wherever you like.
|
||||||
cargo install --path .
|
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
|
vi ./install/surface-dial.service
|
||||||
|
|
||||||
# create new group for uinput
|
# create new group for uinput
|
||||||
@@ -110,17 +130,22 @@ sudo gpasswd -a $(whoami) $(stat -c "%G" /dev/input/event0)
|
|||||||
mkdir -p ~/.config/systemd/user/
|
mkdir -p ~/.config/systemd/user/
|
||||||
cp ./install/surface-dial.service ~/.config/systemd/user/surface-dial.service
|
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/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
|
# reload systemd + udev
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
sudo udevadm control --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.
|
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]
|
[Service]
|
||||||
# HACK: this service needs to run _after_ the /dev/input/eventXX files have been created
|
Type=simple
|
||||||
ExecStart=bash -c 'sleep 1 && /home/danielprilik/.cargo/bin/surface-dial-daemon'
|
StandardOutput=journal
|
||||||
|
ExecStart=/home/danielprilik/.cargo/bin/surface-dial-daemon
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub struct DialController {
|
|||||||
active_mode: ActiveMode,
|
active_mode: ActiveMode,
|
||||||
|
|
||||||
new_mode: Arc<Mutex<Option<usize>>>,
|
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 {
|
impl DialController {
|
||||||
@@ -60,12 +60,6 @@ impl DialController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&mut self) -> Result<()> {
|
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 {
|
loop {
|
||||||
let evt = self.device.next_event()?;
|
let evt = self.device.next_event()?;
|
||||||
let haptics = self.device.haptics();
|
let haptics = self.device.haptics();
|
||||||
@@ -80,13 +74,22 @@ impl DialController {
|
|||||||
ActiveMode::Meta => &mut self.meta_mode,
|
ActiveMode::Meta => &mut self.meta_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: press and hold (+ rotate?) to switch between modes
|
|
||||||
|
|
||||||
match evt.kind {
|
match evt.kind {
|
||||||
DialEventKind::Ignored => {}
|
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::ButtonPress => mode.on_btn_press(haptics)?,
|
||||||
DialEventKind::ButtonRelease => mode.on_btn_release(haptics)?,
|
DialEventKind::ButtonRelease => mode.on_btn_release(haptics)?,
|
||||||
DialEventKind::Dial(delta) => mode.on_dial(haptics, delta)?,
|
DialEventKind::Dial(delta) => mode.on_dial(haptics, delta)?,
|
||||||
|
|
||||||
DialEventKind::ButtonLongPress => {
|
DialEventKind::ButtonLongPress => {
|
||||||
eprintln!("long press!");
|
eprintln!("long press!");
|
||||||
if !matches!(self.active_mode, ActiveMode::Meta) {
|
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)]
|
#![allow(clippy::collapsible_if, clippy::new_without_default)]
|
||||||
|
|
||||||
pub mod common;
|
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() {
|
let (silent, msg, icon) = match terminate_rx.recv() {
|
||||||
Ok(Ok(())) => (true, "".into(), ""),
|
Ok(Ok(())) => (true, "".into(), ""),
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -64,8 +55,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
active_notification.close();
|
|
||||||
|
|
||||||
if !silent {
|
if !silent {
|
||||||
Notification::new()
|
Notification::new()
|
||||||
.hint(Hint::Transient(true))
|
.hint(Hint::Transient(true))
|
||||||
@@ -88,7 +77,6 @@ fn controller_main() -> Result<()> {
|
|||||||
let cfg = config::Config::from_disk()?;
|
let cfg = config::Config::from_disk()?;
|
||||||
|
|
||||||
let dial = DialDevice::new(std::time::Duration::from_millis(750))?;
|
let dial = DialDevice::new(std::time::Duration::from_millis(750))?;
|
||||||
println!("Found the dial");
|
|
||||||
|
|
||||||
let mut controller = DialController::new(
|
let mut controller = DialController::new(
|
||||||
dial,
|
dial,
|
||||||
|
|||||||
Reference in New Issue
Block a user