1
0

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:
Daniel Prilik
2020-11-06 00:22:56 -05:00
parent 559a28c2d7
commit f7a261cb8a
11 changed files with 555 additions and 276 deletions

34
Cargo.lock generated
View File

@@ -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"

View File

@@ -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.
#

View File

@@ -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.
![](notif-demo.gif)
@@ -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.

View File

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

View File

@@ -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

View File

@@ -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) {

View File

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

View File

@@ -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,