add long-press + notif based mode switching
oooooh boy, this is looking pretty slick. I'm kind-of impressed I was able to throw this together in ~2 days of post-work hacking (though I guess they were some pretty late-nights...) There's really only one feature left that I _need_ to implement, which is the on-disk persistence for selected mode. That shouldn't be too tricky though...
This commit is contained in:
24
README.md
24
README.md
@@ -18,12 +18,13 @@ You've been warned :eyes:
|
||||
|
||||
## Overview
|
||||
|
||||
Consists of two components:
|
||||
`surface-dial-daemon` is a background daemon which recieves raw events and translates them to various actions.
|
||||
|
||||
- `surface-dial-daemon` - A background daemon which recieves raw events and translates them to various actions.
|
||||
- `surface-dial-cli` - Controller to configure daemon functionality (e.g: change operating modes)
|
||||
Aside from haptic feedback, the daemon also uses FreeDesktop notifications to provide visual feedback when performing various actions.
|
||||
|
||||
It would be cool to create some sort of GUI overlay (similar to the Windows one), though that's a bit out of scope at the moment.
|
||||

|
||||
|
||||
It would be cool to create some sort of GUI overlay (similar to the Windows one), though that's out of scope at the moment.
|
||||
|
||||
## Functionality
|
||||
|
||||
@@ -33,12 +34,12 @@ It would be cool to create some sort of GUI overlay (similar to the Windows one)
|
||||
- [x] Media Controls
|
||||
- [x] D-Pad (emulated left, right, and space key)
|
||||
- [x] Scrolling / Zooming
|
||||
- [ ] \(meta\) Specify modes via config file(s)
|
||||
- [ ] Dynamically switch between operating modes
|
||||
- _currently requires re-compiling the daemon_
|
||||
- [ ] Using some-sort of on-device mechanism (e.g: long-press)
|
||||
- [ ] Using `surface-dial-cli` application
|
||||
- [ ] \(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)
|
||||
- [ ] Context-sensitive (based on currently open application)
|
||||
- [ ] Mode Persistence
|
||||
- _At the moment, whenever the dial disconnects, the daemon is re-launched, which resets the active mode to the default one. It would be good to have some on-disk persistence to remember the last selected mode._
|
||||
- [x] Haptic Feedback
|
||||
- https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-protocol-implementation
|
||||
- https://www.usb.org/sites/default/files/hutrr63b_-_haptics_page_redline_0.pdf
|
||||
@@ -46,6 +47,7 @@ It would be cool to create some sort of GUI overlay (similar to the Windows one)
|
||||
- _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)
|
||||
|
||||
Feel free to contribute new features!
|
||||
@@ -88,10 +90,6 @@ cargo build -p surface-dial-daemon && sudo target/debug/surface-dial-daemon
|
||||
|
||||
Note the use of `sudo`, as `surface-dial-daemon` requires permission to access files under `/dev/input/` and `/dev/uinput`.
|
||||
|
||||
## Using `surface-dial-cli`
|
||||
|
||||
TODO (the controller cli doesn't exist yet lol)
|
||||
|
||||
## Installation
|
||||
|
||||
As you might have noticed, the daemon dies whenever the Surface Dial disconnects (which happens after a brief period of inactivity).
|
||||
|
||||
BIN
notif-demo.gif
Normal file
BIN
notif-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@@ -2,6 +2,7 @@ use notify_rust::error::Result as NotifyResult;
|
||||
use notify_rust::{Hint, Notification, NotificationHandle, Timeout};
|
||||
|
||||
pub fn action_notification(msg: &str, icon: &str) -> NotifyResult<NotificationHandle> {
|
||||
eprintln!("sending notification: {}", msg);
|
||||
Notification::new()
|
||||
.hint(Hint::Transient(true))
|
||||
.hint(Hint::Category("device".into()))
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::mpsc;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::controller::ControlMode;
|
||||
use crate::controller::{ControlMode, ControlModeMeta};
|
||||
use crate::dial_device::DialHaptics;
|
||||
use crate::fake_input::FakeInput;
|
||||
use crate::DynResult;
|
||||
@@ -123,6 +123,13 @@ impl DPad {
|
||||
}
|
||||
|
||||
impl ControlMode for DPad {
|
||||
fn meta(&self) -> ControlModeMeta {
|
||||
ControlModeMeta {
|
||||
name: "Paddle",
|
||||
icon: "input-gaming",
|
||||
}
|
||||
}
|
||||
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
haptics.set_mode(false, Some(3600))?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::controller::ControlMode;
|
||||
use crate::controller::{ControlMode, ControlModeMeta};
|
||||
use crate::dial_device::DialHaptics;
|
||||
use crate::fake_input::FakeInput;
|
||||
use crate::DynResult;
|
||||
@@ -18,6 +18,13 @@ impl Media {
|
||||
}
|
||||
|
||||
impl ControlMode for Media {
|
||||
fn meta(&self) -> ControlModeMeta {
|
||||
ControlModeMeta {
|
||||
name: "Media",
|
||||
icon: "applications-multimedia",
|
||||
}
|
||||
}
|
||||
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
haptics.set_mode(false, Some(36))?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use crate::controller::ControlMode;
|
||||
use crate::controller::{ControlMode, ControlModeMeta};
|
||||
use crate::dial_device::DialHaptics;
|
||||
use crate::DynResult;
|
||||
|
||||
pub struct Null {}
|
||||
|
||||
impl Null {
|
||||
pub fn new() -> Null {
|
||||
Null {}
|
||||
impl ControlMode for () {
|
||||
fn meta(&self) -> ControlModeMeta {
|
||||
ControlModeMeta {
|
||||
name: "null",
|
||||
icon: "",
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlMode for Null {
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
haptics.set_mode(false, Some(0))?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::common::action_notification;
|
||||
use crate::controller::ControlMode;
|
||||
use crate::controller::{ControlMode, ControlModeMeta};
|
||||
use crate::dial_device::DialHaptics;
|
||||
use crate::fake_input::{FakeInput, ScrollStep};
|
||||
use crate::DynResult;
|
||||
@@ -26,6 +26,13 @@ const ZOOM_SENSITIVITY: u16 = 36;
|
||||
const SCROLL_SENSITIVITY: u16 = 90;
|
||||
|
||||
impl ControlMode for ScrollZoom {
|
||||
fn meta(&self) -> ControlModeMeta {
|
||||
ControlModeMeta {
|
||||
name: "Scroll/Zoom",
|
||||
icon: "input-mouse",
|
||||
}
|
||||
}
|
||||
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
|
||||
Ok(())
|
||||
@@ -43,7 +50,7 @@ impl ControlMode for ScrollZoom {
|
||||
action_notification("Zoom Mode", "zoom-in")?;
|
||||
haptics.set_mode(false, Some(ZOOM_SENSITIVITY))?;
|
||||
} else {
|
||||
action_notification("ScrollZoom Mode", "input-mouse")?;
|
||||
action_notification("Scroll Mode", "input-mouse")?;
|
||||
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::controller::ControlMode;
|
||||
use crate::controller::{ControlMode, ControlModeMeta};
|
||||
use crate::dial_device::DialHaptics;
|
||||
use crate::fake_input::FakeInput;
|
||||
use crate::DynResult;
|
||||
@@ -18,8 +18,15 @@ impl Volume {
|
||||
}
|
||||
|
||||
impl ControlMode for Volume {
|
||||
fn meta(&self) -> ControlModeMeta {
|
||||
ControlModeMeta {
|
||||
name: "Volume",
|
||||
icon: "audio-volume-high",
|
||||
}
|
||||
}
|
||||
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
haptics.set_mode(false, Some(36 * 2))?;
|
||||
haptics.set_mode(true, Some(36 * 2))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,191 @@
|
||||
use crate::DynResult;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::dial_device::{DialDevice, DialEventKind, DialHaptics};
|
||||
use crate::DynResult;
|
||||
|
||||
pub mod controls;
|
||||
|
||||
pub struct ControlModeMeta {
|
||||
name: &'static str,
|
||||
icon: &'static str,
|
||||
}
|
||||
|
||||
pub trait ControlMode {
|
||||
fn meta(&self) -> ControlModeMeta;
|
||||
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
||||
fn on_end(&mut self, _haptics: &DialHaptics) -> DynResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_btn_press(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
||||
fn on_btn_release(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
||||
fn on_dial(&mut self, haptics: &DialHaptics, delta: i32) -> DynResult<()>;
|
||||
}
|
||||
|
||||
enum ActiveMode {
|
||||
Normal(usize),
|
||||
Meta,
|
||||
}
|
||||
|
||||
pub struct DialController {
|
||||
device: DialDevice,
|
||||
|
||||
mode: Box<dyn ControlMode>,
|
||||
modes: Vec<Box<dyn ControlMode>>,
|
||||
active_mode: ActiveMode,
|
||||
|
||||
new_mode: Arc<Mutex<Option<usize>>>,
|
||||
meta_mode: Box<dyn ControlMode>, // always MetaMode
|
||||
}
|
||||
|
||||
impl DialController {
|
||||
pub fn new(device: DialDevice, default_mode: Box<dyn ControlMode>) -> DialController {
|
||||
DialController {
|
||||
mode: default_mode,
|
||||
pub fn new(device: DialDevice, modes: Vec<Box<dyn ControlMode>>) -> DialController {
|
||||
let metas = modes.iter().map(|m| m.meta()).collect();
|
||||
|
||||
let new_mode = Arc::new(Mutex::new(None));
|
||||
|
||||
DialController {
|
||||
device,
|
||||
|
||||
modes,
|
||||
active_mode: ActiveMode::Normal(0),
|
||||
|
||||
new_mode: new_mode.clone(),
|
||||
meta_mode: Box::new(MetaMode::new(new_mode, 0, metas)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> DynResult<()> {
|
||||
let haptics = self.device.haptics();
|
||||
|
||||
self.mode.on_start(haptics)?;
|
||||
self.modes[0].on_start(self.device.haptics())?;
|
||||
|
||||
loop {
|
||||
let evt = self.device.next_event()?;
|
||||
let haptics = self.device.haptics();
|
||||
|
||||
if let Some(new_mode) = self.new_mode.lock().unwrap().take() {
|
||||
self.active_mode = ActiveMode::Normal(new_mode);
|
||||
self.modes[new_mode].on_start(haptics)?;
|
||||
}
|
||||
|
||||
let mode = match self.active_mode {
|
||||
ActiveMode::Normal(idx) => &mut self.modes[idx],
|
||||
ActiveMode::Meta => &mut self.meta_mode,
|
||||
};
|
||||
|
||||
// TODO: press and hold (+ rotate?) to switch between modes
|
||||
|
||||
match evt.kind {
|
||||
DialEventKind::Ignored => {}
|
||||
DialEventKind::ButtonPress => self.mode.on_btn_press(haptics)?,
|
||||
DialEventKind::ButtonRelease => self.mode.on_btn_release(haptics)?,
|
||||
DialEventKind::Dial(delta) => self.mode.on_dial(haptics, delta)?,
|
||||
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) {
|
||||
mode.on_end(haptics)?;
|
||||
self.active_mode = ActiveMode::Meta;
|
||||
self.meta_mode.on_start(haptics)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mode for switching between modes.
|
||||
struct MetaMode {
|
||||
// constant
|
||||
metas: Vec<ControlModeMeta>,
|
||||
|
||||
// stateful (across invocations)
|
||||
current_mode: usize,
|
||||
new_mode: Arc<Mutex<Option<usize>>>,
|
||||
|
||||
// reset in on_start
|
||||
first_release: bool,
|
||||
notif: Option<notify_rust::NotificationHandle>,
|
||||
}
|
||||
|
||||
impl MetaMode {
|
||||
fn new(
|
||||
new_mode: Arc<Mutex<Option<usize>>>,
|
||||
current_mode: usize,
|
||||
metas: Vec<ControlModeMeta>,
|
||||
) -> MetaMode {
|
||||
MetaMode {
|
||||
metas,
|
||||
|
||||
current_mode,
|
||||
new_mode,
|
||||
|
||||
first_release: true,
|
||||
notif: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlMode for MetaMode {
|
||||
fn meta(&self) -> ControlModeMeta {
|
||||
unreachable!() // meta mode never queries itself
|
||||
}
|
||||
|
||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
use notify_rust::*;
|
||||
self.notif = Some(
|
||||
Notification::new()
|
||||
.hint(Hint::Resident(true))
|
||||
.hint(Hint::Category("device".into()))
|
||||
.timeout(Timeout::Never)
|
||||
.summary("Surface Dial")
|
||||
.body(&format!(
|
||||
"Entered Meta Mode (From Mode: {})",
|
||||
self.metas[self.current_mode].name
|
||||
))
|
||||
.icon("emblem-system")
|
||||
.show()?,
|
||||
);
|
||||
|
||||
haptics.buzz(1)?;
|
||||
|
||||
self.first_release = true;
|
||||
|
||||
haptics.set_mode(true, Some(36))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_btn_press(&mut self, _haptics: &DialHaptics) -> DynResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_btn_release(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||
if self.first_release {
|
||||
self.first_release = false;
|
||||
} else {
|
||||
*self.new_mode.lock().unwrap() = Some(self.current_mode);
|
||||
haptics.buzz(1)?;
|
||||
|
||||
self.notif.take().unwrap().close();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_dial(&mut self, _haptics: &DialHaptics, delta: i32) -> DynResult<()> {
|
||||
if delta > 0 {
|
||||
self.current_mode += 1;
|
||||
} else {
|
||||
self.current_mode -= 1;
|
||||
}
|
||||
|
||||
self.current_mode %= self.metas.len();
|
||||
|
||||
let mode_meta = &self.metas[self.current_mode];
|
||||
if let Some(ref mut notification) = self.notif {
|
||||
notification
|
||||
.body(&format!("New Mode: {}", mode_meta.name))
|
||||
.icon(mode_meta.icon);
|
||||
notification.update();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::fs;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use evdev_rs::{Device, InputEvent};
|
||||
@@ -7,10 +8,11 @@ use hidapi::{HidApi, HidDevice};
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct DialDevice {
|
||||
// TODO: explore what the control channel can be used for...
|
||||
_control: Device,
|
||||
axis: Device,
|
||||
long_press_timeout: Duration,
|
||||
haptics: DialHaptics,
|
||||
events: mpsc::Receiver<DialEvent>,
|
||||
|
||||
possible_long_press: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -25,10 +27,11 @@ pub enum DialEventKind {
|
||||
ButtonPress,
|
||||
ButtonRelease,
|
||||
Dial(i32),
|
||||
ButtonLongPress,
|
||||
}
|
||||
|
||||
impl DialDevice {
|
||||
pub fn new() -> Result<DialDevice, crate::Error> {
|
||||
pub fn new(long_press_timeout: Duration) -> Result<DialDevice, crate::Error> {
|
||||
let mut control = None;
|
||||
let mut axis = None;
|
||||
|
||||
@@ -62,24 +65,66 @@ impl DialDevice {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
std::thread::spawn({
|
||||
let events = events_tx;
|
||||
move || {
|
||||
loop {
|
||||
let (_axis_status, axis_evt) = axis
|
||||
.next_event(evdev_rs::ReadFlag::NORMAL)
|
||||
.expect("Error::Evdev");
|
||||
// assert!(matches!(axis_status, ReadStatus::Success));
|
||||
let event = DialEvent::from_raw_evt(axis_evt.clone())
|
||||
.expect("Error::UnexpectedEvt(axis_evt)");
|
||||
|
||||
events.send(event).expect("failed to send axis event");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DialDevice {
|
||||
_control: control.ok_or(Error::MissingDial)?,
|
||||
axis: axis.ok_or(Error::MissingDial)?,
|
||||
long_press_timeout,
|
||||
events: events_rx,
|
||||
haptics: DialHaptics::new()?,
|
||||
|
||||
possible_long_press: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_event(&self) -> Result<DialEvent, Error> {
|
||||
// TODO: figure out how to interleave control events into the same event stream.
|
||||
pub fn next_event(&mut self) -> Result<DialEvent, Error> {
|
||||
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 (_axis_status, axis_evt) = self
|
||||
.axis
|
||||
.next_event(evdev_rs::ReadFlag::NORMAL)
|
||||
.map_err(Error::Evdev)?;
|
||||
// assert!(matches!(axis_status, ReadStatus::Success));
|
||||
|
||||
let event =
|
||||
DialEvent::from_raw_evt(axis_evt.clone()).ok_or(Error::UnexpectedEvt(axis_evt))?;
|
||||
let event = match evt {
|
||||
Ok(event) => {
|
||||
match event.kind {
|
||||
DialEventKind::ButtonPress => self.possible_long_press = true,
|
||||
DialEventKind::ButtonRelease => self.possible_long_press = false,
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
20
src/main.rs
20
src/main.rs
@@ -24,7 +24,7 @@ fn main() {
|
||||
fn true_main() -> DynResult<()> {
|
||||
println!("Started.");
|
||||
|
||||
let dial = DialDevice::new()?;
|
||||
let dial = DialDevice::new(std::time::Duration::from_millis(750))?;
|
||||
println!("Found the dial.");
|
||||
|
||||
std::thread::spawn(move || {
|
||||
@@ -34,7 +34,7 @@ fn true_main() -> DynResult<()> {
|
||||
.timeout(Timeout::Never)
|
||||
.summary("Surface Dial")
|
||||
.body("Active!")
|
||||
.icon("input-mouse")
|
||||
.icon("media-optical") // it should be vaguely circular :P
|
||||
.show()
|
||||
.expect("failed to send notification");
|
||||
|
||||
@@ -46,13 +46,15 @@ fn true_main() -> DynResult<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// let default_mode = Box::new(controller::controls::Null::new());
|
||||
let default_mode = Box::new(controller::controls::ScrollZoom::new());
|
||||
// let default_mode = Box::new(controller::controls::Volume::new());
|
||||
// let default_mode = Box::new(controller::controls::Media::new());
|
||||
// let default_mode = Box::new(controller::controls::DPad::new());
|
||||
|
||||
let mut controller = DialController::new(dial, default_mode);
|
||||
let mut controller = DialController::new(
|
||||
dial,
|
||||
vec![
|
||||
Box::new(controller::controls::ScrollZoom::new()),
|
||||
Box::new(controller::controls::Volume::new()),
|
||||
Box::new(controller::controls::Media::new()),
|
||||
Box::new(controller::controls::DPad::new()),
|
||||
],
|
||||
);
|
||||
|
||||
controller.run()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user