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
|
## 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.
|
Aside from haptic feedback, the daemon also uses FreeDesktop notifications to provide visual feedback when performing various actions.
|
||||||
- `surface-dial-cli` - Controller to configure daemon functionality (e.g: change operating modes)
|
|
||||||
|
|
||||||
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
|
## 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] Media Controls
|
||||||
- [x] D-Pad (emulated left, right, and space key)
|
- [x] D-Pad (emulated left, right, and space key)
|
||||||
- [x] Scrolling / Zooming
|
- [x] Scrolling / Zooming
|
||||||
- [ ] \(meta\) Specify modes via config file(s)
|
- [ ] \(meta\) custom modes specified via config file(s)
|
||||||
- [ ] Dynamically switch between operating modes
|
- [x] Dynamically switch between operating modes
|
||||||
- _currently requires re-compiling the daemon_
|
- [x] Using some-sort of on-device mechanism (e.g: long-press)
|
||||||
- [ ] Using some-sort of on-device mechanism (e.g: long-press)
|
|
||||||
- [ ] Using `surface-dial-cli` application
|
|
||||||
- [ ] Context-sensitive (based on currently open application)
|
- [ ] 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
|
- [x] Haptic Feedback
|
||||||
- https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-protocol-implementation
|
- 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
|
- 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!_
|
- _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] Desktop Notifications
|
||||||
- [x] On Launch
|
- [x] On Launch
|
||||||
|
- [x] When switching between modes
|
||||||
- [x] When switching between sub-modes (e.g: scroll/zoom)
|
- [x] When switching between sub-modes (e.g: scroll/zoom)
|
||||||
|
|
||||||
Feel free to contribute new features!
|
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`.
|
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
|
## Installation
|
||||||
|
|
||||||
As you might have noticed, the daemon dies whenever the Surface Dial disconnects (which happens after a brief period of inactivity).
|
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};
|
use notify_rust::{Hint, Notification, NotificationHandle, Timeout};
|
||||||
|
|
||||||
pub fn action_notification(msg: &str, icon: &str) -> NotifyResult<NotificationHandle> {
|
pub fn action_notification(msg: &str, icon: &str) -> NotifyResult<NotificationHandle> {
|
||||||
|
eprintln!("sending notification: {}", msg);
|
||||||
Notification::new()
|
Notification::new()
|
||||||
.hint(Hint::Transient(true))
|
.hint(Hint::Transient(true))
|
||||||
.hint(Hint::Category("device".into()))
|
.hint(Hint::Category("device".into()))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::mpsc;
|
|||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::controller::ControlMode;
|
use crate::controller::{ControlMode, ControlModeMeta};
|
||||||
use crate::dial_device::DialHaptics;
|
use crate::dial_device::DialHaptics;
|
||||||
use crate::fake_input::FakeInput;
|
use crate::fake_input::FakeInput;
|
||||||
use crate::DynResult;
|
use crate::DynResult;
|
||||||
@@ -123,6 +123,13 @@ impl DPad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ControlMode for DPad {
|
impl ControlMode for DPad {
|
||||||
|
fn meta(&self) -> ControlModeMeta {
|
||||||
|
ControlModeMeta {
|
||||||
|
name: "Paddle",
|
||||||
|
icon: "input-gaming",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||||
haptics.set_mode(false, Some(3600))?;
|
haptics.set_mode(false, Some(3600))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::controller::ControlMode;
|
use crate::controller::{ControlMode, ControlModeMeta};
|
||||||
use crate::dial_device::DialHaptics;
|
use crate::dial_device::DialHaptics;
|
||||||
use crate::fake_input::FakeInput;
|
use crate::fake_input::FakeInput;
|
||||||
use crate::DynResult;
|
use crate::DynResult;
|
||||||
@@ -18,6 +18,13 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ControlMode for Media {
|
impl ControlMode for Media {
|
||||||
|
fn meta(&self) -> ControlModeMeta {
|
||||||
|
ControlModeMeta {
|
||||||
|
name: "Media",
|
||||||
|
icon: "applications-multimedia",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||||
haptics.set_mode(false, Some(36))?;
|
haptics.set_mode(false, Some(36))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use crate::controller::ControlMode;
|
use crate::controller::{ControlMode, ControlModeMeta};
|
||||||
use crate::dial_device::DialHaptics;
|
use crate::dial_device::DialHaptics;
|
||||||
use crate::DynResult;
|
use crate::DynResult;
|
||||||
|
|
||||||
pub struct Null {}
|
impl ControlMode for () {
|
||||||
|
fn meta(&self) -> ControlModeMeta {
|
||||||
impl Null {
|
ControlModeMeta {
|
||||||
pub fn new() -> Null {
|
name: "null",
|
||||||
Null {}
|
icon: "",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl ControlMode for Null {
|
|
||||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||||
haptics.set_mode(false, Some(0))?;
|
haptics.set_mode(false, Some(0))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::common::action_notification;
|
use crate::common::action_notification;
|
||||||
use crate::controller::ControlMode;
|
use crate::controller::{ControlMode, ControlModeMeta};
|
||||||
use crate::dial_device::DialHaptics;
|
use crate::dial_device::DialHaptics;
|
||||||
use crate::fake_input::{FakeInput, ScrollStep};
|
use crate::fake_input::{FakeInput, ScrollStep};
|
||||||
use crate::DynResult;
|
use crate::DynResult;
|
||||||
@@ -26,6 +26,13 @@ const ZOOM_SENSITIVITY: u16 = 36;
|
|||||||
const SCROLL_SENSITIVITY: u16 = 90;
|
const SCROLL_SENSITIVITY: u16 = 90;
|
||||||
|
|
||||||
impl ControlMode for ScrollZoom {
|
impl ControlMode for ScrollZoom {
|
||||||
|
fn meta(&self) -> ControlModeMeta {
|
||||||
|
ControlModeMeta {
|
||||||
|
name: "Scroll/Zoom",
|
||||||
|
icon: "input-mouse",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||||
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
|
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -43,7 +50,7 @@ impl ControlMode for ScrollZoom {
|
|||||||
action_notification("Zoom Mode", "zoom-in")?;
|
action_notification("Zoom Mode", "zoom-in")?;
|
||||||
haptics.set_mode(false, Some(ZOOM_SENSITIVITY))?;
|
haptics.set_mode(false, Some(ZOOM_SENSITIVITY))?;
|
||||||
} else {
|
} else {
|
||||||
action_notification("ScrollZoom Mode", "input-mouse")?;
|
action_notification("Scroll Mode", "input-mouse")?;
|
||||||
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
|
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::dial_device::DialHaptics;
|
||||||
use crate::fake_input::FakeInput;
|
use crate::fake_input::FakeInput;
|
||||||
use crate::DynResult;
|
use crate::DynResult;
|
||||||
@@ -18,8 +18,15 @@ impl Volume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ControlMode for Volume {
|
impl ControlMode for Volume {
|
||||||
|
fn meta(&self) -> ControlModeMeta {
|
||||||
|
ControlModeMeta {
|
||||||
|
name: "Volume",
|
||||||
|
icon: "audio-volume-high",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
|
||||||
haptics.set_mode(false, Some(36 * 2))?;
|
haptics.set_mode(true, Some(36 * 2))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,191 @@
|
|||||||
use crate::DynResult;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crate::dial_device::{DialDevice, DialEventKind, DialHaptics};
|
use crate::dial_device::{DialDevice, DialEventKind, DialHaptics};
|
||||||
|
use crate::DynResult;
|
||||||
|
|
||||||
pub mod controls;
|
pub mod controls;
|
||||||
|
|
||||||
|
pub struct ControlModeMeta {
|
||||||
|
name: &'static str,
|
||||||
|
icon: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ControlMode {
|
pub trait ControlMode {
|
||||||
|
fn meta(&self) -> ControlModeMeta;
|
||||||
|
|
||||||
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
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_press(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
||||||
fn on_btn_release(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
fn on_btn_release(&mut self, haptics: &DialHaptics) -> DynResult<()>;
|
||||||
fn on_dial(&mut self, haptics: &DialHaptics, delta: i32) -> DynResult<()>;
|
fn on_dial(&mut self, haptics: &DialHaptics, delta: i32) -> DynResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ActiveMode {
|
||||||
|
Normal(usize),
|
||||||
|
Meta,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DialController {
|
pub struct DialController {
|
||||||
device: DialDevice,
|
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 {
|
impl DialController {
|
||||||
pub fn new(device: DialDevice, default_mode: Box<dyn ControlMode>) -> DialController {
|
pub fn new(device: DialDevice, modes: Vec<Box<dyn ControlMode>>) -> DialController {
|
||||||
DialController {
|
let metas = modes.iter().map(|m| m.meta()).collect();
|
||||||
mode: default_mode,
|
|
||||||
|
|
||||||
|
let new_mode = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
DialController {
|
||||||
device,
|
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<()> {
|
pub fn run(&mut self) -> DynResult<()> {
|
||||||
let haptics = self.device.haptics();
|
self.modes[0].on_start(self.device.haptics())?;
|
||||||
|
|
||||||
self.mode.on_start(haptics)?;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let evt = self.device.next_event()?;
|
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
|
// TODO: press and hold (+ rotate?) to switch between modes
|
||||||
|
|
||||||
match evt.kind {
|
match evt.kind {
|
||||||
DialEventKind::Ignored => {}
|
DialEventKind::Ignored => {}
|
||||||
DialEventKind::ButtonPress => self.mode.on_btn_press(haptics)?,
|
DialEventKind::ButtonPress => mode.on_btn_press(haptics)?,
|
||||||
DialEventKind::ButtonRelease => self.mode.on_btn_release(haptics)?,
|
DialEventKind::ButtonRelease => mode.on_btn_release(haptics)?,
|
||||||
DialEventKind::Dial(delta) => self.mode.on_dial(haptics, delta)?,
|
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::fs;
|
||||||
|
use std::sync::mpsc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use evdev_rs::{Device, InputEvent};
|
use evdev_rs::{Device, InputEvent};
|
||||||
@@ -7,10 +8,11 @@ use hidapi::{HidApi, HidDevice};
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
pub struct DialDevice {
|
pub struct DialDevice {
|
||||||
// TODO: explore what the control channel can be used for...
|
long_press_timeout: Duration,
|
||||||
_control: Device,
|
|
||||||
axis: Device,
|
|
||||||
haptics: DialHaptics,
|
haptics: DialHaptics,
|
||||||
|
events: mpsc::Receiver<DialEvent>,
|
||||||
|
|
||||||
|
possible_long_press: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -25,10 +27,11 @@ pub enum DialEventKind {
|
|||||||
ButtonPress,
|
ButtonPress,
|
||||||
ButtonRelease,
|
ButtonRelease,
|
||||||
Dial(i32),
|
Dial(i32),
|
||||||
|
ButtonLongPress,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DialDevice {
|
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 control = None;
|
||||||
let mut axis = 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 {
|
Ok(DialDevice {
|
||||||
_control: control.ok_or(Error::MissingDial)?,
|
long_press_timeout,
|
||||||
axis: axis.ok_or(Error::MissingDial)?,
|
events: events_rx,
|
||||||
haptics: DialHaptics::new()?,
|
haptics: DialHaptics::new()?,
|
||||||
|
|
||||||
|
possible_long_press: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_event(&self) -> Result<DialEvent, Error> {
|
pub fn next_event(&mut self) -> Result<DialEvent, Error> {
|
||||||
// TODO: figure out how to interleave control events into the same event stream.
|
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
|
let event = match evt {
|
||||||
.axis
|
Ok(event) => {
|
||||||
.next_event(evdev_rs::ReadFlag::NORMAL)
|
match event.kind {
|
||||||
.map_err(Error::Evdev)?;
|
DialEventKind::ButtonPress => self.possible_long_press = true,
|
||||||
// assert!(matches!(axis_status, ReadStatus::Success));
|
DialEventKind::ButtonRelease => self.possible_long_press = false,
|
||||||
|
_ => {}
|
||||||
let event =
|
}
|
||||||
DialEvent::from_raw_evt(axis_evt.clone()).ok_or(Error::UnexpectedEvt(axis_evt))?;
|
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)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@@ -24,7 +24,7 @@ fn main() {
|
|||||||
fn true_main() -> DynResult<()> {
|
fn true_main() -> DynResult<()> {
|
||||||
println!("Started.");
|
println!("Started.");
|
||||||
|
|
||||||
let dial = DialDevice::new()?;
|
let dial = DialDevice::new(std::time::Duration::from_millis(750))?;
|
||||||
println!("Found the dial.");
|
println!("Found the dial.");
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -34,7 +34,7 @@ fn true_main() -> DynResult<()> {
|
|||||||
.timeout(Timeout::Never)
|
.timeout(Timeout::Never)
|
||||||
.summary("Surface Dial")
|
.summary("Surface Dial")
|
||||||
.body("Active!")
|
.body("Active!")
|
||||||
.icon("input-mouse")
|
.icon("media-optical") // it should be vaguely circular :P
|
||||||
.show()
|
.show()
|
||||||
.expect("failed to send notification");
|
.expect("failed to send notification");
|
||||||
|
|
||||||
@@ -46,13 +46,15 @@ fn true_main() -> DynResult<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// let default_mode = Box::new(controller::controls::Null::new());
|
let mut controller = DialController::new(
|
||||||
let default_mode = Box::new(controller::controls::ScrollZoom::new());
|
dial,
|
||||||
// let default_mode = Box::new(controller::controls::Volume::new());
|
vec![
|
||||||
// let default_mode = Box::new(controller::controls::Media::new());
|
Box::new(controller::controls::ScrollZoom::new()),
|
||||||
// let default_mode = Box::new(controller::controls::DPad::new());
|
Box::new(controller::controls::Volume::new()),
|
||||||
|
Box::new(controller::controls::Media::new()),
|
||||||
let mut controller = DialController::new(dial, default_mode);
|
Box::new(controller::controls::DPad::new()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
controller.run()
|
controller.run()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user