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

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,