1
0

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:
Daniel Prilik
2020-10-30 23:39:12 -04:00
parent e6fa6845fe
commit d14a92dfe5
11 changed files with 280 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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))?;
}

View File

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

View File

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

View File

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

View File

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