1
0
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-protocol-implementation

With a little bit of trial and error (and a crash-course in how the heck
HID even works), I figured out how to get the dial to provide haptic
feedback!

Along the way, I also learned that you can take advantage of the
(incorrectly named) Resolution Multiplier field to customize how many
"steps" the dial should have, offloading the work to the device itself!

Very cool!!
This commit is contained in:
Daniel Prilik
2020-10-30 19:59:23 -04:00
parent 858209484f
commit e6fa6845fe
16 changed files with 1139 additions and 365 deletions

View File

@@ -1,38 +1,3 @@
pub enum DialDir {
Left,
Right,
}
pub struct ThresholdHelper {
sensitivity: i32,
pos: i32,
}
impl ThresholdHelper {
pub fn new(sensitivity: i32) -> ThresholdHelper {
ThresholdHelper {
sensitivity,
pos: 0,
}
}
pub fn update(&mut self, delta: i32) -> Option<DialDir> {
self.pos += delta;
if self.pos > self.sensitivity {
self.pos -= self.sensitivity;
return Some(DialDir::Right);
}
if self.pos < -self.sensitivity {
self.pos += self.sensitivity;
return Some(DialDir::Left);
}
None
}
}
use notify_rust::error::Result as NotifyResult;
use notify_rust::{Hint, Notification, NotificationHandle, Timeout};

View File

@@ -4,6 +4,7 @@ use std::thread::JoinHandle;
use std::time::Duration;
use crate::controller::ControlMode;
use crate::dial_device::DialHaptics;
use crate::fake_input::FakeInput;
use crate::DynResult;
@@ -106,12 +107,6 @@ impl Drop for DPad {
}
}
impl Default for DPad {
fn default() -> Self {
Self::new()
}
}
impl DPad {
pub fn new() -> DPad {
let (msg_tx, msg_rx) = mpsc::channel();
@@ -128,17 +123,22 @@ impl DPad {
}
impl ControlMode for DPad {
fn on_btn_press(&mut self) -> DynResult<()> {
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
haptics.set_mode(false, Some(3600))?;
Ok(())
}
fn on_btn_press(&mut self, _: &DialHaptics) -> DynResult<()> {
eprintln!("space");
self.fake_input.key_click(&[EV_KEY::KEY_SPACE])?;
Ok(())
}
fn on_btn_release(&mut self) -> DynResult<()> {
fn on_btn_release(&mut self, _: &DialHaptics) -> DynResult<()> {
Ok(())
}
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> DynResult<()> {
self.msg.send(Msg::Delta(delta))?;
Ok(())
}

View File

@@ -1,47 +1,44 @@
use crate::common::{DialDir, ThresholdHelper};
use crate::controller::ControlMode;
use crate::dial_device::DialHaptics;
use crate::fake_input::FakeInput;
use crate::DynResult;
use evdev_rs::enums::EV_KEY;
pub struct Media {
thresh: ThresholdHelper,
fake_input: FakeInput,
}
impl Media {
pub fn new(sensitivity: i32) -> Media {
pub fn new() -> Media {
Media {
thresh: ThresholdHelper::new(sensitivity),
fake_input: FakeInput::new(),
}
}
}
impl ControlMode for Media {
fn on_btn_press(&mut self) -> DynResult<()> {
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
haptics.set_mode(false, Some(36))?;
Ok(())
}
fn on_btn_release(&mut self) -> DynResult<()> {
fn on_btn_press(&mut self, _: &DialHaptics) -> DynResult<()> {
Ok(())
}
fn on_btn_release(&mut self, _: &DialHaptics) -> DynResult<()> {
self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?;
Ok(())
}
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
match self.thresh.update(delta) {
Some(DialDir::Left) => {
eprintln!("next song");
self.fake_input.key_click(&[EV_KEY::KEY_NEXTSONG])?;
}
Some(DialDir::Right) => {
eprintln!("last song");
self.fake_input.key_click(&[EV_KEY::KEY_PREVIOUSSONG])?;
}
None => {}
fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> DynResult<()> {
if delta > 0 {
eprintln!("last song");
self.fake_input.key_click(&[EV_KEY::KEY_PREVIOUSSONG])?;
} else {
eprintln!("next song");
self.fake_input.key_click(&[EV_KEY::KEY_NEXTSONG])?;
}
Ok(())
}

View File

@@ -1,9 +1,11 @@
mod dpad;
mod media;
mod null;
mod scroll_zoom;
mod volume;
pub use self::dpad::*;
pub use self::media::*;
pub use self::null::*;
pub use self::scroll_zoom::*;
pub use self::volume::*;

View File

@@ -0,0 +1,30 @@
use crate::controller::ControlMode;
use crate::dial_device::DialHaptics;
use crate::DynResult;
pub struct Null {}
impl Null {
pub fn new() -> Null {
Null {}
}
}
impl ControlMode for Null {
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
haptics.set_mode(false, Some(0))?;
Ok(())
}
fn on_btn_press(&mut self, _haptics: &DialHaptics) -> DynResult<()> {
Ok(())
}
fn on_btn_release(&mut self, _haptics: &DialHaptics) -> DynResult<()> {
Ok(())
}
fn on_dial(&mut self, _haptics: &DialHaptics, _delta: i32) -> DynResult<()> {
Ok(())
}
}

View File

@@ -1,21 +1,20 @@
use crate::common::{action_notification, DialDir, ThresholdHelper};
use crate::common::action_notification;
use crate::controller::ControlMode;
use crate::dial_device::DialHaptics;
use crate::fake_input::{FakeInput, ScrollStep};
use crate::DynResult;
use evdev_rs::enums::EV_KEY;
pub struct ScrollZoom {
thresh: ThresholdHelper,
zoom: bool,
fake_input: FakeInput,
}
impl ScrollZoom {
pub fn new(sensitivity: i32) -> ScrollZoom {
pub fn new() -> ScrollZoom {
ScrollZoom {
thresh: ThresholdHelper::new(sensitivity),
zoom: false,
fake_input: FakeInput::new(),
@@ -23,46 +22,55 @@ impl ScrollZoom {
}
}
const ZOOM_SENSITIVITY: u16 = 36;
const SCROLL_SENSITIVITY: u16 = 90;
impl ControlMode for ScrollZoom {
fn on_btn_press(&mut self) -> DynResult<()> {
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
Ok(())
}
fn on_btn_release(&mut self) -> DynResult<()> {
fn on_btn_press(&mut self, _: &DialHaptics) -> DynResult<()> {
Ok(())
}
fn on_btn_release(&mut self, haptics: &DialHaptics) -> DynResult<()> {
self.zoom = !self.zoom;
haptics.buzz(1)?;
if self.zoom {
action_notification("Zoom Mode", "zoom-in")?;
haptics.set_mode(false, Some(ZOOM_SENSITIVITY))?;
} else {
action_notification("ScrollZoom Mode", "input-mouse")?;
haptics.set_mode(false, Some(SCROLL_SENSITIVITY))?;
}
Ok(())
}
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
match self.thresh.update(delta) {
None => {}
Some(DialDir::Left) => {
if self.zoom {
eprintln!("zoom out");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_MINUS])?;
} else {
eprintln!("scroll up");
self.fake_input.scroll_step(ScrollStep::Up)?;
}
fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> DynResult<()> {
if delta > 0 {
if self.zoom {
eprintln!("zoom in");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_EQUAL])?;
} else {
eprintln!("scroll down");
self.fake_input.scroll_step(ScrollStep::Down)?;
}
Some(DialDir::Right) => {
if self.zoom {
eprintln!("zoom in");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_EQUAL])?;
} else {
eprintln!("scroll down");
self.fake_input.scroll_step(ScrollStep::Down)?;
}
} else {
if self.zoom {
eprintln!("zoom out");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_MINUS])?;
} else {
eprintln!("scroll up");
self.fake_input.scroll_step(ScrollStep::Up)?;
}
}
Ok(())
}
}

View File

@@ -1,53 +1,51 @@
use crate::common::{DialDir, ThresholdHelper};
use crate::controller::ControlMode;
use crate::dial_device::DialHaptics;
use crate::fake_input::FakeInput;
use crate::DynResult;
use evdev_rs::enums::EV_KEY;
pub struct Volume {
thresh: ThresholdHelper,
fake_input: FakeInput,
}
impl Volume {
pub fn new(sensitivity: i32) -> Volume {
pub fn new() -> Volume {
Volume {
thresh: ThresholdHelper::new(sensitivity),
fake_input: FakeInput::new(),
}
}
}
impl ControlMode for Volume {
fn on_btn_press(&mut self) -> DynResult<()> {
fn on_start(&mut self, haptics: &DialHaptics) -> DynResult<()> {
haptics.set_mode(false, Some(36 * 2))?;
Ok(())
}
fn on_btn_press(&mut self, _: &DialHaptics) -> DynResult<()> {
// TODO: support double-click to mute
Ok(())
}
fn on_btn_release(&mut self) -> DynResult<()> {
fn on_btn_release(&mut self, _: &DialHaptics) -> DynResult<()> {
eprintln!("play/pause");
// self.fake_input.mute()?
self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?;
Ok(())
}
fn on_dial(&mut self, delta: i32) -> DynResult<()> {
match self.thresh.update(delta) {
Some(DialDir::Left) => {
eprintln!("volume down");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEDOWN])?
}
Some(DialDir::Right) => {
eprintln!("volume up");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEUP])?
}
None => {}
fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> DynResult<()> {
if delta > 0 {
eprintln!("volume up");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEUP])?;
} else {
eprintln!("volume down");
self.fake_input
.key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEDOWN])?;
}
Ok(())
}
}

View File

@@ -1,13 +1,14 @@
use crate::DynResult;
use crate::dial_device::{DialDevice, DialEventKind};
use crate::dial_device::{DialDevice, DialEventKind, DialHaptics};
pub mod controls;
pub trait ControlMode {
fn on_btn_press(&mut self) -> DynResult<()>;
fn on_btn_release(&mut self) -> DynResult<()>;
fn on_dial(&mut self, delta: i32) -> DynResult<()>;
fn on_start(&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_dial(&mut self, haptics: &DialHaptics, delta: i32) -> DynResult<()>;
}
pub struct DialController {
@@ -26,16 +27,20 @@ impl DialController {
}
pub fn run(&mut self) -> DynResult<()> {
let haptics = self.device.haptics();
self.mode.on_start(haptics)?;
loop {
let evt = self.device.next_event()?;
// TODO: press and hold + rotate to switch between modes
// TODO: press and hold (+ rotate?) to switch between modes
match evt.kind {
DialEventKind::Ignored => {}
DialEventKind::ButtonPress => self.mode.on_btn_press()?,
DialEventKind::ButtonRelease => self.mode.on_btn_release()?,
DialEventKind::Dial(delta) => self.mode.on_dial(delta)?,
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)?,
}
}
}

View File

@@ -2,6 +2,7 @@ use std::fs;
use std::time::Duration;
use evdev_rs::{Device, InputEvent};
use hidapi::{HidApi, HidDevice};
use crate::error::Error;
@@ -9,6 +10,7 @@ pub struct DialDevice {
// TODO: explore what the control channel can be used for...
_control: Device,
axis: Device,
haptics: DialHaptics,
}
#[derive(Debug)]
@@ -30,6 +32,7 @@ impl 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") {
@@ -62,6 +65,7 @@ impl DialDevice {
Ok(DialDevice {
_control: control.ok_or(Error::MissingDial)?,
axis: axis.ok_or(Error::MissingDial)?,
haptics: DialHaptics::new()?,
})
}
@@ -79,6 +83,10 @@ impl DialDevice {
Ok(event)
}
pub fn haptics(&self) -> &DialHaptics {
&self.haptics
}
}
impl DialEvent {
@@ -110,3 +118,67 @@ impl DialEvent {
Some(evt)
}
}
pub struct DialHaptics {
hid_device: HidDevice,
}
impl DialHaptics {
fn new() -> Result<DialHaptics, Error> {
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<(), Error> {
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<(), Error> {
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(())
}
}

View File

@@ -7,6 +7,7 @@ use evdev_rs::InputEvent;
pub enum Error {
OpenDevInputDir(io::Error),
OpenEventFile(std::path::PathBuf, io::Error),
HidError(hidapi::HidError),
MissingDial,
MultipleDials,
UnexpectedEvt(InputEvent),
@@ -19,6 +20,7 @@ impl fmt::Display for Error {
match self {
Error::OpenDevInputDir(e) => write!(f, "Could not open /dev/input directory: {}", e),
Error::OpenEventFile(path, e) => write!(f, "Could not open {:?}: {}", path, e),
Error::HidError(e) => write!(f, "HID API Error: {}", e),
Error::MissingDial => write!(f, "Could not find the Surface Dial"),
Error::MultipleDials => write!(f, "Found multiple dials"),
Error::UnexpectedEvt(evt) => write!(f, "Unexpected event: {:?}", evt),

View File

@@ -1,3 +1,5 @@
#![allow(clippy::collapsible_if, clippy::new_without_default)]
mod common;
pub mod controller;
mod dial_device;
@@ -10,6 +12,9 @@ use crate::controller::DialController;
use crate::dial_device::DialDevice;
use crate::error::Error;
use notify_rust::{Hint, Notification, Timeout};
use signal_hook::{iterator::Signals, SIGINT, SIGTERM};
fn main() {
if let Err(e) = true_main() {
println!("{}", e);
@@ -22,11 +27,29 @@ fn true_main() -> DynResult<()> {
let dial = DialDevice::new()?;
println!("Found the dial.");
common::action_notification("Active!", "input-mouse")?;
std::thread::spawn(move || {
let active_notification = Notification::new()
.hint(Hint::Resident(true))
.hint(Hint::Category("device".into()))
.timeout(Timeout::Never)
.summary("Surface Dial")
.body("Active!")
.icon("input-mouse")
.show()
.expect("failed to send notification");
let default_mode = Box::new(controller::controls::ScrollZoom::new(30));
// let default_mode = Box::new(controller::controls::Volume::new(30));
// let default_mode = Box::new(controller::controls::Media::new(50));
let signals = Signals::new(&[SIGTERM, SIGINT]).unwrap();
for sig in signals.forever() {
eprintln!("received signal {:?}", sig);
active_notification.close();
std::process::exit(1);
}
});
// 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);