├── .gitignore ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── rustfmt.toml ├── .gitattributes ├── notif-demo.gif ├── install ├── 10-uinput.rules ├── 10-surface-dial.rules └── surface-dial.service ├── src ├── controller │ ├── controls │ │ ├── mod.rs │ │ ├── null.rs │ │ ├── scroll.rs │ │ ├── zoom.rs │ │ ├── media.rs │ │ ├── volume.rs │ │ ├── scroll_mt.rs │ │ ├── media_with_volume.rs │ │ └── paddle.rs │ └── mod.rs ├── common.rs ├── error.rs ├── config.rs ├── main.rs ├── dial_device │ ├── haptics.rs │ ├── events.rs │ └── mod.rs └── fake_input.rs ├── Cargo.toml ├── notes ├── HID_Report_Descriptor.txt └── descriptor.c ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: prilik 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | wrap_comments = true 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | notes/* linguist-documentation 2 | -------------------------------------------------------------------------------- /notif-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniel5151/surface-dial-linux/HEAD/notif-demo.gif -------------------------------------------------------------------------------- /install/10-uinput.rules: -------------------------------------------------------------------------------- 1 | KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess", TAG+="udev-acl" 2 | -------------------------------------------------------------------------------- /install/10-surface-dial.rules: -------------------------------------------------------------------------------- 1 | KERNEL=="hidraw*", SUBSYSTEM=="hidraw", SUBSYSTEMS=="hid", DRIVERS=="microsoft", TAG+="uaccess", TAG+="udev-acl" 2 | -------------------------------------------------------------------------------- /install/surface-dial.service: -------------------------------------------------------------------------------- 1 | # IMPORTANT: modify the "ExecStart" field to reflect your surface-dial-daemon install dir 2 | 3 | [Unit] 4 | Description=Surface Dial Daemon 5 | 6 | [Service] 7 | Type=simple 8 | StandardOutput=journal 9 | ExecStart=%h/.cargo/bin/surface-dial-daemon 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /src/controller/controls/mod.rs: -------------------------------------------------------------------------------- 1 | mod media; 2 | mod media_with_volume; 3 | mod null; 4 | mod paddle; 5 | mod scroll; 6 | mod scroll_mt; 7 | mod volume; 8 | mod zoom; 9 | 10 | pub use self::media::*; 11 | pub use self::media_with_volume::*; 12 | pub use self::null::*; 13 | pub use self::paddle::*; 14 | pub use self::scroll::*; 15 | pub use self::scroll_mt::*; 16 | pub use self::volume::*; 17 | pub use self::zoom::*; 18 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use notify_rust::error::Result as NotifyResult; 2 | use notify_rust::{Hint, Notification, NotificationHandle, Timeout}; 3 | 4 | pub fn action_notification(msg: &str, icon: &str) -> NotifyResult { 5 | eprintln!("sending notification: {}", msg); 6 | Notification::new() 7 | .hint(Hint::Transient(true)) 8 | .hint(Hint::Category("device".into())) 9 | .timeout(Timeout::Milliseconds(100)) 10 | .summary("Surface Dial") 11 | .body(msg) 12 | .icon(icon) 13 | .show() 14 | } 15 | -------------------------------------------------------------------------------- /src/controller/controls/null.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{ControlMode, ControlModeMeta}; 2 | use crate::dial_device::DialHaptics; 3 | use crate::error::Result; 4 | 5 | impl ControlMode for () { 6 | fn meta(&self) -> ControlModeMeta { 7 | ControlModeMeta { 8 | name: "null", 9 | icon: "", 10 | haptics: false, 11 | steps: 3600, 12 | } 13 | } 14 | 15 | fn on_btn_press(&mut self, _haptics: &DialHaptics) -> Result<()> { 16 | Ok(()) 17 | } 18 | 19 | fn on_btn_release(&mut self, _haptics: &DialHaptics) -> Result<()> { 20 | Ok(()) 21 | } 22 | 23 | fn on_dial(&mut self, _haptics: &DialHaptics, _delta: i32) -> Result<()> { 24 | Ok(()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: The daemon isn't working properly :'( 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | _A clear and concise description of what the bug is._ 13 | 14 | **Desktop (please complete the following information):** 15 | - Distro: (e.g: Ubuntu 20.04) 16 | - Desktop Environment: (e.g: GNOME) 17 | - output of `uname -a`: 18 | 19 | **Service Logs** 20 | 21 | _Please copy the output of `journalctl -u surface-dial.service` / the daemon's `stdout/stderr`._ 22 | 23 | When possible, please reproduce the bug with the `RUST_BACKTRACE=1` environment variable set. Having a detailed backtrace will make it easier to diagnose (and hopefully fix) the bug. 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surface-dial-daemon" 3 | version = "0.1.0" 4 | authors = ["Daniel Prilik "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | directories = "3.0" 9 | # master includes a PR that implements `Send` for `Device` and `UInputDevice` 10 | evdev-rs = { git = "https://github.com/ndesh26/evdev-rs.git", rev = "8e995b8bf" } 11 | hidapi = { version = "1.2.3", default-features = false, features = ["linux-shared-hidraw"] } 12 | lazy_static = "1.4" 13 | nix = "0.19.0" 14 | notify-rust = "4" 15 | parking_lot = "0.11.0" 16 | signal-hook = "0.1.16" 17 | udev = "0.5" 18 | 19 | # HACK: Using >1 virtual uinput devices will segfault in release builds. 20 | # 21 | # While spooky, this isn't a show-stopper for us Pragmatic Programmers™, as we 22 | # can simply disable optimizations for `evdev-rs` and have things work Okay™. 23 | # 24 | # That said, I would like to find some time to narrow down why this is happening 25 | # and fix it. Maybe later... 26 | [profile.release.package.evdev-rs] 27 | opt-level = 0 28 | -------------------------------------------------------------------------------- /src/controller/controls/scroll.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{ControlMode, ControlModeMeta}; 2 | use crate::dial_device::DialHaptics; 3 | use crate::error::{Error, Result}; 4 | use crate::fake_input::{self, ScrollStep}; 5 | 6 | pub struct Scroll {} 7 | 8 | impl Scroll { 9 | pub fn new() -> Scroll { 10 | Scroll {} 11 | } 12 | } 13 | 14 | impl ControlMode for Scroll { 15 | fn meta(&self) -> ControlModeMeta { 16 | ControlModeMeta { 17 | name: "Scroll", 18 | icon: "input-mouse", 19 | haptics: false, 20 | steps: 90, 21 | } 22 | } 23 | 24 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 25 | Ok(()) 26 | } 27 | 28 | fn on_btn_release(&mut self, _haptics: &DialHaptics) -> Result<()> { 29 | Ok(()) 30 | } 31 | 32 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 33 | if delta > 0 { 34 | eprintln!("scroll down"); 35 | fake_input::scroll_step(ScrollStep::Down).map_err(Error::Evdev)?; 36 | } else { 37 | eprintln!("scroll up"); 38 | fake_input::scroll_step(ScrollStep::Up).map_err(Error::Evdev)?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/controller/controls/zoom.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{ControlMode, ControlModeMeta}; 2 | use crate::dial_device::DialHaptics; 3 | use crate::error::{Error, Result}; 4 | use crate::fake_input; 5 | 6 | use evdev_rs::enums::EV_KEY; 7 | 8 | pub struct Zoom {} 9 | 10 | impl Zoom { 11 | pub fn new() -> Zoom { 12 | Zoom {} 13 | } 14 | } 15 | 16 | impl ControlMode for Zoom { 17 | fn meta(&self) -> ControlModeMeta { 18 | ControlModeMeta { 19 | name: "Zoom", 20 | icon: "zoom-in", 21 | haptics: true, 22 | steps: 36, 23 | } 24 | } 25 | 26 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 27 | Ok(()) 28 | } 29 | 30 | fn on_btn_release(&mut self, _haptics: &DialHaptics) -> Result<()> { 31 | Ok(()) 32 | } 33 | 34 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 35 | if delta > 0 { 36 | eprintln!("zoom in"); 37 | fake_input::key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_EQUAL]) 38 | .map_err(Error::Evdev)?; 39 | } else { 40 | eprintln!("zoom out"); 41 | fake_input::key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_MINUS]) 42 | .map_err(Error::Evdev)?; 43 | } 44 | 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/controller/controls/media.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{ControlMode, ControlModeMeta}; 2 | use crate::dial_device::DialHaptics; 3 | use crate::error::{Error, Result}; 4 | use crate::fake_input; 5 | 6 | use evdev_rs::enums::EV_KEY; 7 | 8 | pub struct Media {} 9 | 10 | impl Media { 11 | pub fn new() -> Media { 12 | Media {} 13 | } 14 | } 15 | 16 | impl ControlMode for Media { 17 | fn meta(&self) -> ControlModeMeta { 18 | ControlModeMeta { 19 | name: "Media", 20 | icon: "applications-multimedia", 21 | haptics: true, 22 | steps: 36, 23 | } 24 | } 25 | 26 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 27 | Ok(()) 28 | } 29 | 30 | fn on_btn_release(&mut self, _: &DialHaptics) -> Result<()> { 31 | fake_input::key_click(&[EV_KEY::KEY_PLAYPAUSE]).map_err(Error::Evdev)?; 32 | Ok(()) 33 | } 34 | 35 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 36 | if delta > 0 { 37 | eprintln!("next song"); 38 | fake_input::key_click(&[EV_KEY::KEY_NEXTSONG]).map_err(Error::Evdev)?; 39 | } else { 40 | eprintln!("last song"); 41 | fake_input::key_click(&[EV_KEY::KEY_PREVIOUSSONG]).map_err(Error::Evdev)?; 42 | } 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/controller/controls/volume.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{ControlMode, ControlModeMeta}; 2 | use crate::dial_device::DialHaptics; 3 | use crate::error::{Error, Result}; 4 | use crate::fake_input; 5 | 6 | use evdev_rs::enums::EV_KEY; 7 | 8 | pub struct Volume {} 9 | 10 | impl Volume { 11 | pub fn new() -> Volume { 12 | Volume {} 13 | } 14 | } 15 | 16 | impl ControlMode for Volume { 17 | fn meta(&self) -> ControlModeMeta { 18 | ControlModeMeta { 19 | name: "Volume", 20 | icon: "audio-volume-high", 21 | haptics: true, 22 | steps: 36 * 2, 23 | } 24 | } 25 | 26 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 27 | Ok(()) 28 | } 29 | 30 | fn on_btn_release(&mut self, _: &DialHaptics) -> Result<()> { 31 | eprintln!("mute"); 32 | fake_input::key_click(&[EV_KEY::KEY_MUTE]).map_err(Error::Evdev)?; 33 | Ok(()) 34 | } 35 | 36 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 37 | if delta > 0 { 38 | eprintln!("volume up"); 39 | fake_input::key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEUP]) 40 | .map_err(Error::Evdev)?; 41 | } else { 42 | eprintln!("volume down"); 43 | fake_input::key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEDOWN]) 44 | .map_err(Error::Evdev)?; 45 | } 46 | 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::io; 3 | 4 | use evdev_rs::InputEvent; 5 | 6 | pub type Result = std::result::Result; 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | ConfigFile(String), 11 | OpenDevInputDir(io::Error), 12 | OpenEventFile(std::path::PathBuf, io::Error), 13 | HidError(hidapi::HidError), 14 | MissingDial, 15 | MultipleDials, 16 | UnexpectedEvt(InputEvent), 17 | Evdev(io::Error), 18 | Notif(notify_rust::error::Error), 19 | TermSig, 20 | } 21 | 22 | impl fmt::Display for Error { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Error::ConfigFile(e) => write!(f, "Could not open config file: {}", e), 26 | Error::OpenDevInputDir(e) => write!(f, "Could not open /dev/input directory: {}", e), 27 | Error::OpenEventFile(path, e) => write!(f, "Could not open {:?}: {}", path, e), 28 | Error::HidError(e) => write!(f, "HID API Error: {}", e), 29 | Error::MissingDial => write!(f, "Could not find the Surface Dial"), 30 | Error::MultipleDials => write!(f, "Found multiple dials"), 31 | Error::UnexpectedEvt(evt) => write!(f, "Unexpected event: {:?}", evt), 32 | Error::Evdev(e) => write!(f, "Evdev error: {}", e), 33 | Error::Notif(e) => write!(f, "Notification error: {}", e), 34 | Error::TermSig => write!(f, "Received termination signal (either SIGTERM or SIGINT)"), 35 | } 36 | } 37 | } 38 | 39 | impl std::error::Error for Error {} 40 | -------------------------------------------------------------------------------- /src/controller/controls/scroll_mt.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{ControlMode, ControlModeMeta}; 2 | use crate::dial_device::DialHaptics; 3 | use crate::error::{Error, Result}; 4 | use crate::fake_input; 5 | 6 | pub struct ScrollMT { 7 | acc_delta: i32, 8 | } 9 | 10 | impl ScrollMT { 11 | pub fn new() -> ScrollMT { 12 | ScrollMT { acc_delta: 0 } 13 | } 14 | } 15 | 16 | impl ControlMode for ScrollMT { 17 | fn meta(&self) -> ControlModeMeta { 18 | ControlModeMeta { 19 | name: "Scroll (Fake Multitouch - EXPERIMENTAL)", 20 | icon: "input-mouse", 21 | haptics: false, 22 | steps: 3600, 23 | } 24 | } 25 | 26 | fn on_start(&mut self, _haptics: &DialHaptics) -> Result<()> { 27 | self.acc_delta = 0; 28 | 29 | // HACK: for some reason, if scroll mode is the startup mode, then just calling 30 | // `scroll_mt_start` doesn't work as expected. 31 | std::thread::spawn(move || { 32 | fake_input::scroll_mt_end().unwrap(); 33 | std::thread::sleep(std::time::Duration::from_millis(200)); 34 | fake_input::scroll_mt_start().unwrap(); 35 | }); 36 | 37 | Ok(()) 38 | } 39 | 40 | fn on_end(&mut self, _haptics: &DialHaptics) -> Result<()> { 41 | fake_input::scroll_mt_end().map_err(Error::Evdev)?; 42 | Ok(()) 43 | } 44 | 45 | // HACK: the button will reset the scroll event, which sometimes helps 46 | 47 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 48 | fake_input::scroll_mt_end().map_err(Error::Evdev)?; 49 | Ok(()) 50 | } 51 | 52 | fn on_btn_release(&mut self, _haptics: &DialHaptics) -> Result<()> { 53 | fake_input::scroll_mt_start().map_err(Error::Evdev)?; 54 | Ok(()) 55 | } 56 | 57 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 58 | self.acc_delta += delta; 59 | fake_input::scroll_mt_step(self.acc_delta).map_err(Error::Evdev)?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::prelude::*; 3 | 4 | use crate::error::{Error, Result}; 5 | 6 | // This current implementation is incredibly barebones and brittle. 7 | // It literally just reads/writes the last selected mode from the file as a raw 8 | // index into the modes array. That's it. 9 | // 10 | // No TOML, No JSON, just raw text. 11 | // 12 | // While it wouldn't be too hard to get a proper serde-based implementation up 13 | // and running, it'll just bump compile times up for no good reason, so I'll 14 | // only set it up once I need the added complexity. 15 | 16 | // TODO: stop using strings for errors lol 17 | 18 | pub struct Config { 19 | pub last_mode: usize, 20 | } 21 | 22 | fn get_cfg_file() -> Result { 23 | let proj_dirs = directories::ProjectDirs::from("com", "prilik", "surface-dial-daemon") 24 | .ok_or_else(|| Error::ConfigFile("could not open config directory".into()))?; 25 | let cfg_folder = proj_dirs.config_dir(); 26 | let cfg_file_path = proj_dirs.config_dir().join("config.txt"); 27 | 28 | fs::create_dir_all(cfg_folder) 29 | .map_err(|e| Error::ConfigFile(format!("could not create config dir: {}", e)))?; 30 | 31 | if !cfg_file_path.exists() { 32 | fs::write(&cfg_file_path, "0") 33 | .map_err(|e| Error::ConfigFile(format!("could not write to config file: {}", e)))?; 34 | } 35 | 36 | let cfg_file = fs::OpenOptions::new() 37 | .write(true) 38 | .read(true) 39 | .open(cfg_file_path) 40 | .map_err(|e| Error::ConfigFile(format!("could not open config file: {}", e)))?; 41 | 42 | Ok(cfg_file) 43 | } 44 | 45 | impl Config { 46 | pub fn from_disk() -> Result { 47 | let mut cfg_file = get_cfg_file()?; 48 | 49 | let mut content = String::new(); 50 | cfg_file 51 | .read_to_string(&mut content) 52 | .map_err(|e| Error::ConfigFile(format!("could not read the config file: {}", e)))?; 53 | 54 | let last_mode = content 55 | .parse() 56 | .map_err(|e| Error::ConfigFile(format!("could not parse the config file: {}", e)))?; 57 | 58 | Ok(Config { last_mode }) 59 | } 60 | 61 | pub fn to_disk(&self) -> Result<()> { 62 | let mut cfg_file = get_cfg_file()?; 63 | cfg_file 64 | .write_all(format!("{}", self.last_mode).as_bytes()) 65 | .map_err(|e| Error::ConfigFile(format!("could not write to the config file: {}", e)))?; 66 | 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code)] 2 | #![allow(clippy::collapsible_if, clippy::new_without_default)] 3 | 4 | pub mod common; 5 | mod config; 6 | pub mod controller; 7 | mod dial_device; 8 | mod error; 9 | mod fake_input; 10 | 11 | use std::sync::mpsc; 12 | 13 | use crate::controller::DialController; 14 | use crate::dial_device::DialDevice; 15 | use crate::error::{Error, Result}; 16 | 17 | use notify_rust::{Hint, Notification, Timeout}; 18 | use signal_hook::{iterator::Signals, SIGINT, SIGTERM}; 19 | 20 | fn main() { 21 | let (terminate_tx, terminate_rx) = mpsc::channel::>(); 22 | 23 | std::thread::spawn({ 24 | let terminate_tx = terminate_tx.clone(); 25 | move || { 26 | let signals = Signals::new(&[SIGTERM, SIGINT]).unwrap(); 27 | for sig in signals.forever() { 28 | eprintln!("received signal {:?}", sig); 29 | let _ = terminate_tx.send(Err(Error::TermSig)); 30 | } 31 | } 32 | }); 33 | 34 | std::thread::spawn({ 35 | let terminate_tx = terminate_tx; 36 | move || { 37 | let _ = terminate_tx.send(controller_main()); 38 | } 39 | }); 40 | 41 | let (silent, msg, icon) = match terminate_rx.recv() { 42 | Ok(Ok(())) => (true, "".into(), ""), 43 | Ok(Err(e)) => { 44 | println!("Error: {}", e); 45 | match e { 46 | Error::TermSig => (false, "Terminated!".into(), "dialog-warning"), 47 | // HACK: silently exit if the dial disconnects 48 | Error::Evdev(e) if e.raw_os_error() == Some(19) => (true, "".into(), ""), 49 | other => (false, format!("Error: {}", other), "dialog-error"), 50 | } 51 | } 52 | Err(_) => { 53 | println!("Error: Unexpected Error"); 54 | (false, "Unexpected Error".into(), "dialog-error") 55 | } 56 | }; 57 | 58 | if !silent { 59 | Notification::new() 60 | .hint(Hint::Transient(true)) 61 | .hint(Hint::Category("device".into())) 62 | .timeout(Timeout::Milliseconds(100)) 63 | .summary("Surface Dial") 64 | .body(&msg) 65 | .icon(icon) 66 | .show() 67 | .unwrap(); 68 | } 69 | 70 | // cleaning up threads is hard... 71 | std::process::exit(1); 72 | } 73 | 74 | fn controller_main() -> Result<()> { 75 | println!("Started"); 76 | 77 | let cfg = config::Config::from_disk()?; 78 | 79 | let dial = DialDevice::new(std::time::Duration::from_millis(750))?; 80 | 81 | let mut controller = DialController::new( 82 | dial, 83 | cfg.last_mode, 84 | vec![ 85 | Box::new(controller::controls::Scroll::new()), 86 | Box::new(controller::controls::ScrollMT::new()), 87 | Box::new(controller::controls::Zoom::new()), 88 | Box::new(controller::controls::Volume::new()), 89 | Box::new(controller::controls::Media::new()), 90 | Box::new(controller::controls::MediaWithVolume::new()), 91 | Box::new(controller::controls::Paddle::new()), 92 | ], 93 | ); 94 | 95 | controller.run() 96 | } 97 | -------------------------------------------------------------------------------- /src/controller/controls/media_with_volume.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | use std::time::Duration; 3 | 4 | use crate::controller::{ControlMode, ControlModeMeta}; 5 | use crate::dial_device::DialHaptics; 6 | use crate::error::{Error, Result}; 7 | use crate::fake_input; 8 | 9 | use evdev_rs::enums::EV_KEY; 10 | 11 | fn double_click_worker(click: mpsc::Receiver<()>, release: mpsc::Receiver<()>) -> Result<()> { 12 | loop { 13 | // drain any spurious clicks/releases 14 | for _ in click.try_iter() {} 15 | for _ in release.try_iter() {} 16 | 17 | click.recv().unwrap(); 18 | // recv with timeout, in case this is a long-press 19 | match release.recv_timeout(Duration::from_secs(1)) { 20 | Ok(()) => {} 21 | Err(mpsc::RecvTimeoutError::Timeout) => continue, 22 | Err(mpsc::RecvTimeoutError::Disconnected) => panic!(), 23 | } 24 | 25 | match click.recv_timeout(Duration::from_millis(250)) { 26 | Ok(()) => { 27 | // double click 28 | release.recv().unwrap(); // should only fire after button is released 29 | eprintln!("next track"); 30 | fake_input::key_click(&[EV_KEY::KEY_NEXTSONG]).map_err(Error::Evdev)?; 31 | } 32 | Err(mpsc::RecvTimeoutError::Timeout) => { 33 | // single click 34 | eprintln!("play/pause"); 35 | fake_input::key_click(&[EV_KEY::KEY_PLAYPAUSE]).map_err(Error::Evdev)?; 36 | } 37 | Err(mpsc::RecvTimeoutError::Disconnected) => panic!(), 38 | } 39 | } 40 | } 41 | 42 | pub struct MediaWithVolume { 43 | click_tx: mpsc::Sender<()>, 44 | release_tx: mpsc::Sender<()>, 45 | worker_handle: Option>>, 46 | } 47 | 48 | impl MediaWithVolume { 49 | pub fn new() -> MediaWithVolume { 50 | let (click_tx, click_rx) = mpsc::channel(); 51 | let (release_tx, release_rx) = mpsc::channel(); 52 | 53 | let worker_handle = std::thread::spawn(move || double_click_worker(click_rx, release_rx)); 54 | 55 | MediaWithVolume { 56 | click_tx, 57 | release_tx, 58 | worker_handle: Some(worker_handle), 59 | } 60 | } 61 | } 62 | 63 | impl ControlMode for MediaWithVolume { 64 | fn meta(&self) -> ControlModeMeta { 65 | ControlModeMeta { 66 | name: "Media + Volume", 67 | icon: "applications-multimedia", 68 | haptics: true, 69 | steps: 36 * 2, 70 | } 71 | } 72 | 73 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 74 | if self.click_tx.send(()).is_err() { 75 | self.worker_handle 76 | .take() 77 | .unwrap() 78 | .join() 79 | .expect("panic on thread join")?; 80 | } 81 | Ok(()) 82 | } 83 | 84 | fn on_btn_release(&mut self, _: &DialHaptics) -> Result<()> { 85 | if self.release_tx.send(()).is_err() { 86 | self.worker_handle 87 | .take() 88 | .unwrap() 89 | .join() 90 | .expect("panic on thread join")?; 91 | } 92 | Ok(()) 93 | } 94 | 95 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 96 | if delta > 0 { 97 | eprintln!("volume up"); 98 | fake_input::key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEUP]) 99 | .map_err(Error::Evdev)?; 100 | } else { 101 | eprintln!("volume down"); 102 | fake_input::key_click(&[EV_KEY::KEY_LEFTSHIFT, EV_KEY::KEY_VOLUMEDOWN]) 103 | .map_err(Error::Evdev)?; 104 | } 105 | 106 | Ok(()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/dial_device/haptics.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | 3 | use hidapi::{HidApi, HidDevice}; 4 | 5 | use crate::error::{Error, Result}; 6 | 7 | /// Proxy object - forwards requests to the DialHapticsWorker task 8 | pub struct DialHaptics { 9 | msg: mpsc::Sender, 10 | } 11 | 12 | impl DialHaptics { 13 | pub(super) fn new(msg: mpsc::Sender) -> Result { 14 | Ok(DialHaptics { msg }) 15 | } 16 | 17 | /// `steps` should be a value between 0 and 3600, which corresponds to the 18 | /// number of subdivisions the dial should use. 19 | pub fn set_mode(&self, haptics: bool, steps: u16) -> Result<()> { 20 | let _ = (self.msg).send(DialHapticsWorkerMsg::SetMode { haptics, steps }); 21 | Ok(()) 22 | } 23 | 24 | pub fn buzz(&self, repeat: u8) -> Result<()> { 25 | let _ = (self.msg).send(DialHapticsWorkerMsg::Manual { repeat }); 26 | Ok(()) 27 | } 28 | } 29 | 30 | #[derive(Debug)] 31 | pub(super) enum DialHapticsWorkerMsg { 32 | DialConnected, 33 | DialDisconnected, 34 | SetMode { haptics: bool, steps: u16 }, 35 | Manual { repeat: u8 }, 36 | } 37 | 38 | pub(super) struct DialHapticsWorker { 39 | msg: mpsc::Receiver, 40 | } 41 | 42 | impl DialHapticsWorker { 43 | pub(super) fn new(msg: mpsc::Receiver) -> Result { 44 | Ok(DialHapticsWorker { msg }) 45 | } 46 | 47 | pub(super) fn run(&mut self) -> Result<()> { 48 | loop { 49 | eprintln!("haptics worker is waiting..."); 50 | 51 | loop { 52 | match self.msg.recv().unwrap() { 53 | DialHapticsWorkerMsg::DialConnected => break, 54 | other => eprintln!("haptics worker dropped an event: {:?}", other), 55 | } 56 | } 57 | 58 | eprintln!("haptics worker is ready"); 59 | 60 | let api = HidApi::new().map_err(Error::HidError)?; 61 | let hid_device = api.open(0x045e, 0x091b).map_err(Error::HidError)?; 62 | let wrapper = DialHidWrapper { hid_device }; 63 | 64 | loop { 65 | match self.msg.recv().unwrap() { 66 | DialHapticsWorkerMsg::DialConnected => { 67 | eprintln!("Unexpected haptics worker ready event."); 68 | // should be fine though? 69 | } 70 | DialHapticsWorkerMsg::DialDisconnected => break, 71 | DialHapticsWorkerMsg::SetMode { haptics, steps } => { 72 | wrapper.set_mode(haptics, steps)? 73 | } 74 | DialHapticsWorkerMsg::Manual { repeat } => wrapper.buzz(repeat)?, 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | struct DialHidWrapper { 82 | hid_device: HidDevice, 83 | } 84 | 85 | impl DialHidWrapper { 86 | /// `steps` should be a value between 0 and 3600, which corresponds to the 87 | /// number of subdivisions the dial should use. If left unspecified, this 88 | /// defaults to 36 (an arbitrary choice that "feels good" most of the time) 89 | fn set_mode(&self, haptics: bool, steps: u16) -> Result<()> { 90 | assert!(steps <= 3600); 91 | 92 | let steps_lo = steps & 0xff; 93 | let steps_hi = (steps >> 8) & 0xff; 94 | 95 | let mut buf = [0; 8]; 96 | buf[0] = 1; 97 | buf[1] = steps_lo as u8; // steps 98 | buf[2] = steps_hi as u8; // steps 99 | buf[3] = 0x00; // Repeat Count 100 | buf[4] = if haptics { 0x03 } else { 0x02 }; // auto trigger 101 | buf[5] = 0x00; // Waveform Cutoff Time 102 | buf[6] = 0x00; // retrigger period 103 | buf[7] = 0x00; // retrigger period 104 | self.hid_device 105 | .send_feature_report(&buf[..8]) 106 | .map_err(Error::HidError)?; 107 | 108 | Ok(()) 109 | } 110 | 111 | fn buzz(&self, repeat: u8) -> Result<()> { 112 | let mut buf = [0; 5]; 113 | buf[0] = 0x01; // Report ID 114 | buf[1] = repeat; // RepeatCount 115 | buf[2] = 0x03; // ManualTrigger 116 | buf[3] = 0x00; // RetriggerPeriod (lo) 117 | buf[4] = 0x00; // RetriggerPeriod (hi) 118 | self.hid_device.write(&buf).map_err(Error::HidError)?; 119 | Ok(()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/dial_device/events.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::sync::mpsc; 3 | use std::time::Duration; 4 | 5 | use evdev_rs::{InputEvent, ReadStatus}; 6 | use std::os::unix::io::AsRawFd; 7 | 8 | use super::DialHapticsWorkerMsg; 9 | 10 | pub enum RawInputEvent { 11 | Event(ReadStatus, InputEvent), 12 | Connect, 13 | Disconnect, 14 | } 15 | 16 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 17 | pub enum DialInputKind { 18 | Control, 19 | MultiAxis, 20 | } 21 | 22 | pub struct EventsWorker { 23 | events: mpsc::Sender, 24 | haptics_msg: mpsc::Sender, 25 | input_kind: DialInputKind, 26 | } 27 | 28 | impl EventsWorker { 29 | pub(super) fn new( 30 | input_kind: DialInputKind, 31 | events: mpsc::Sender, 32 | haptics_msg: mpsc::Sender, 33 | ) -> EventsWorker { 34 | EventsWorker { 35 | input_kind, 36 | events, 37 | haptics_msg, 38 | } 39 | } 40 | 41 | fn udev_to_evdev(&self, device: &udev::Device) -> std::io::Result> { 42 | let devnode = match device.devnode() { 43 | Some(path) => path, 44 | None => return Ok(None), 45 | }; 46 | 47 | // we care about the `/dev/input/eventXX` device, which is a child of the 48 | // actual input device (that has a nice name we can match against) 49 | match device.parent() { 50 | None => return Ok(None), 51 | Some(parent) => { 52 | let name = parent 53 | .property_value("NAME") 54 | .unwrap_or_else(|| std::ffi::OsStr::new("")) 55 | .to_string_lossy(); 56 | 57 | match (self.input_kind, name.as_ref()) { 58 | (DialInputKind::Control, r#""Surface Dial System Control""#) => {} 59 | (DialInputKind::MultiAxis, r#""Surface Dial System Multi Axis""#) => {} 60 | _ => return Ok(None), 61 | } 62 | } 63 | } 64 | 65 | let file = fs::File::open(devnode)?; 66 | evdev_rs::Device::new_from_fd(file).map(Some) 67 | } 68 | 69 | fn event_loop(&mut self, device: evdev_rs::Device) -> std::io::Result<()> { 70 | // HACK: don't want to double-send these events 71 | if self.input_kind != DialInputKind::Control { 72 | self.haptics_msg 73 | .send(DialHapticsWorkerMsg::DialConnected) 74 | .unwrap(); 75 | self.events.send(RawInputEvent::Connect).unwrap(); 76 | } 77 | 78 | loop { 79 | let _ = self 80 | .events 81 | .send(match device.next_event(evdev_rs::ReadFlag::BLOCKING) { 82 | Ok((read_status, event)) => RawInputEvent::Event(read_status, event), 83 | // this error corresponds to the device disconnecting, which is fine 84 | Err(e) if e.raw_os_error() == Some(19) => break, 85 | Err(e) => return Err(e), 86 | }); 87 | } 88 | 89 | // HACK: don't want to double-send these events 90 | if self.input_kind != DialInputKind::Control { 91 | self.haptics_msg 92 | .send(DialHapticsWorkerMsg::DialDisconnected) 93 | .unwrap(); 94 | self.events.send(RawInputEvent::Disconnect).unwrap(); 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | pub fn run(&mut self) -> std::io::Result<()> { 101 | // eagerly check if the device already exists 102 | 103 | let mut enumerator = { 104 | let mut e = udev::Enumerator::new()?; 105 | e.match_subsystem("input")?; 106 | e 107 | }; 108 | for device in enumerator.scan_devices()? { 109 | let dev = match self.udev_to_evdev(&device)? { 110 | None => continue, 111 | Some(dev) => dev, 112 | }; 113 | 114 | self.event_loop(dev)?; 115 | } 116 | 117 | // enter udev event loop to gracefully handle disconnect/reconnect 118 | 119 | let mut socket = udev::MonitorBuilder::new()? 120 | .match_subsystem("input")? 121 | .listen()?; 122 | 123 | loop { 124 | nix::poll::ppoll( 125 | &mut [nix::poll::PollFd::new( 126 | socket.as_raw_fd(), 127 | nix::poll::PollFlags::POLLIN, 128 | )], 129 | None, 130 | nix::sys::signal::SigSet::empty(), 131 | ) 132 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 133 | 134 | let event = match socket.next() { 135 | Some(evt) => evt, 136 | None => { 137 | std::thread::sleep(Duration::from_millis(10)); 138 | continue; 139 | } 140 | }; 141 | 142 | if !matches!(event.event_type(), udev::EventType::Add) { 143 | continue; 144 | } 145 | 146 | let dev = match self.udev_to_evdev(&event.device())? { 147 | None => continue, 148 | Some(dev) => dev, 149 | }; 150 | 151 | self.event_loop(dev)?; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/controller/controls/paddle.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::sync::mpsc; 3 | use std::thread::JoinHandle; 4 | use std::time::Duration; 5 | 6 | use crate::controller::{ControlMode, ControlModeMeta}; 7 | use crate::dial_device::DialHaptics; 8 | use crate::error::Result; 9 | use crate::fake_input; 10 | 11 | use evdev_rs::enums::EV_KEY; 12 | 13 | // everything is done in a worker, as we need use `recv_timeout` as a (very) 14 | // poor man's `select!`. 15 | 16 | enum Msg { 17 | Kill, 18 | ButtonDown, 19 | ButtonUp, 20 | Delta(i32), 21 | Enabled(bool), 22 | } 23 | 24 | struct Worker { 25 | msg: mpsc::Receiver, 26 | 27 | timeout: u64, 28 | falloff: i32, 29 | cap: i32, 30 | deadzone: i32, 31 | 32 | enabled: bool, 33 | last_delta: i32, 34 | velocity: i32, 35 | } 36 | 37 | impl Worker { 38 | pub fn new(msg: mpsc::Receiver) -> Worker { 39 | Worker { 40 | msg, 41 | 42 | // tweak these for "feel" 43 | timeout: 5, 44 | falloff: 10, 45 | cap: 250, 46 | deadzone: 10, 47 | 48 | enabled: false, 49 | last_delta: 0, 50 | velocity: 0, 51 | } 52 | } 53 | 54 | pub fn run(&mut self) { 55 | loop { 56 | let falloff = self.velocity.abs() / self.falloff + 1; 57 | 58 | let msg = if self.enabled { 59 | self.msg.recv_timeout(Duration::from_millis(self.timeout)) 60 | } else { 61 | self.msg 62 | .recv() 63 | .map_err(|_| mpsc::RecvTimeoutError::Disconnected) 64 | }; 65 | 66 | match msg { 67 | Ok(Msg::Kill) => return, 68 | Ok(Msg::Enabled(enabled)) => { 69 | self.enabled = enabled; 70 | if !enabled { 71 | fake_input::key_release(&[ 72 | EV_KEY::KEY_SPACE, 73 | EV_KEY::KEY_LEFT, 74 | EV_KEY::KEY_RIGHT, 75 | ]) 76 | .unwrap() 77 | } 78 | } 79 | Ok(Msg::ButtonDown) => fake_input::key_press(&[EV_KEY::KEY_SPACE]).unwrap(), 80 | Ok(Msg::ButtonUp) => fake_input::key_release(&[EV_KEY::KEY_SPACE]).unwrap(), 81 | Ok(Msg::Delta(delta)) => { 82 | // abrupt direction change! 83 | if (delta < 0) != (self.last_delta < 0) { 84 | self.velocity = 0 85 | } 86 | self.last_delta = delta; 87 | 88 | self.velocity += delta 89 | } 90 | Err(mpsc::RecvTimeoutError::Timeout) => match self.velocity.cmp(&0) { 91 | Ordering::Equal => {} 92 | Ordering::Less => self.velocity += falloff, 93 | Ordering::Greater => self.velocity -= falloff, 94 | }, 95 | Err(other) => panic!("{}", other), 96 | } 97 | 98 | // clamp velocity within the cap bounds 99 | if self.velocity > self.cap { 100 | self.velocity = self.cap; 101 | } else if self.velocity < -self.cap { 102 | self.velocity = -self.cap; 103 | } 104 | 105 | if self.velocity.abs() < self.deadzone { 106 | fake_input::key_release(&[EV_KEY::KEY_LEFT, EV_KEY::KEY_RIGHT]).unwrap(); 107 | continue; 108 | } 109 | 110 | match self.velocity.cmp(&0) { 111 | Ordering::Equal => {} 112 | Ordering::Less => fake_input::key_press(&[EV_KEY::KEY_LEFT]).unwrap(), 113 | Ordering::Greater => fake_input::key_press(&[EV_KEY::KEY_RIGHT]).unwrap(), 114 | } 115 | 116 | // eprintln!("{:?}", self.velocity); 117 | } 118 | } 119 | } 120 | 121 | /// A bit of a misnomer, since it's only left-right. 122 | pub struct Paddle { 123 | _worker: JoinHandle<()>, 124 | msg: mpsc::Sender, 125 | } 126 | 127 | impl Drop for Paddle { 128 | fn drop(&mut self) { 129 | let _ = self.msg.send(Msg::Kill); 130 | } 131 | } 132 | 133 | impl Paddle { 134 | pub fn new() -> Paddle { 135 | let (msg_tx, msg_rx) = mpsc::channel(); 136 | 137 | let worker = std::thread::spawn(move || Worker::new(msg_rx).run()); 138 | 139 | Paddle { 140 | _worker: worker, 141 | msg: msg_tx, 142 | } 143 | } 144 | } 145 | 146 | impl ControlMode for Paddle { 147 | fn meta(&self) -> ControlModeMeta { 148 | ControlModeMeta { 149 | name: "Paddle", 150 | icon: "input-gaming", 151 | haptics: false, 152 | steps: 3600, 153 | } 154 | } 155 | 156 | fn on_start(&mut self, _haptics: &DialHaptics) -> Result<()> { 157 | let _ = self.msg.send(Msg::Enabled(true)); 158 | Ok(()) 159 | } 160 | 161 | fn on_end(&mut self, _haptics: &DialHaptics) -> Result<()> { 162 | let _ = self.msg.send(Msg::Enabled(false)); 163 | Ok(()) 164 | } 165 | 166 | fn on_btn_press(&mut self, _: &DialHaptics) -> Result<()> { 167 | let _ = self.msg.send(Msg::ButtonDown); 168 | Ok(()) 169 | } 170 | 171 | fn on_btn_release(&mut self, _: &DialHaptics) -> Result<()> { 172 | let _ = self.msg.send(Msg::ButtonUp); 173 | Ok(()) 174 | } 175 | 176 | fn on_dial(&mut self, _: &DialHaptics, delta: i32) -> Result<()> { 177 | let _ = self.msg.send(Msg::Delta(delta)); 178 | Ok(()) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/dial_device/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | use std::time::Duration; 3 | 4 | use crate::error::{Error, Result}; 5 | 6 | mod events; 7 | mod haptics; 8 | 9 | use haptics::{DialHapticsWorker, DialHapticsWorkerMsg}; 10 | 11 | pub use haptics::DialHaptics; 12 | 13 | /// Encapsulates all the the nitty-gritty (and pretty gnarly) device handling 14 | /// code, exposing a simple interface to wait for incoming [`DialEvent`]s. 15 | pub struct DialDevice { 16 | // configurable constants 17 | long_press_timeout: Duration, 18 | 19 | // handles 20 | haptics: DialHaptics, 21 | events: mpsc::Receiver, 22 | 23 | // mutable state 24 | possible_long_press: bool, 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct DialEvent { 29 | pub time: Duration, 30 | pub kind: DialEventKind, 31 | } 32 | 33 | #[derive(Debug)] 34 | pub enum DialEventKind { 35 | Connect, 36 | Disconnect, 37 | 38 | Ignored, 39 | ButtonPress, 40 | ButtonRelease, 41 | Dial(i32), 42 | 43 | /// NOTE: this is a synthetic event, and is _not_ directly provided by the 44 | /// dial itself. 45 | ButtonLongPress, 46 | } 47 | 48 | impl DialDevice { 49 | pub fn new(long_press_timeout: Duration) -> Result { 50 | let (events_tx, events_rx) = mpsc::channel(); 51 | let (haptics_msg_tx, haptics_msg_rx) = mpsc::channel(); 52 | 53 | // TODO: interleave control events with regular events 54 | // (once we figure out what control events actually do...) 55 | 56 | std::thread::spawn({ 57 | let haptics_msg_tx = haptics_msg_tx.clone(); 58 | let mut worker = events::EventsWorker::new( 59 | events::DialInputKind::MultiAxis, 60 | events_tx, 61 | haptics_msg_tx, 62 | ); 63 | move || { 64 | worker.run().unwrap(); 65 | eprintln!("the events worker died!"); 66 | } 67 | }); 68 | 69 | std::thread::spawn({ 70 | let mut worker = DialHapticsWorker::new(haptics_msg_rx)?; 71 | move || { 72 | if let Err(err) = worker.run() { 73 | eprintln!("Unexpected haptics worker error! {}", err); 74 | } 75 | eprintln!("the haptics worker died!"); 76 | // there's no coming back from this. 77 | std::process::exit(0); 78 | } 79 | }); 80 | 81 | Ok(DialDevice { 82 | long_press_timeout, 83 | events: events_rx, 84 | haptics: DialHaptics::new(haptics_msg_tx)?, 85 | 86 | possible_long_press: false, 87 | }) 88 | } 89 | 90 | /// Blocks until a new dial event comes occurs. 91 | // TODO?: rewrite code using async/await? 92 | // TODO?: "cheat" by exposing an async interface to the current next_event impl 93 | pub fn next_event(&mut self) -> Result { 94 | let evt = if self.possible_long_press { 95 | self.events.recv_timeout(self.long_press_timeout) 96 | } else { 97 | self.events 98 | .recv() 99 | .map_err(|_| mpsc::RecvTimeoutError::Disconnected) 100 | }; 101 | 102 | let event = match evt { 103 | Ok(events::RawInputEvent::Event(_event_status, event)) => { 104 | // assert!(matches!(axis_status, ReadStatus::Success)); 105 | let event = 106 | DialEvent::from_raw_evt(event.clone()).ok_or(Error::UnexpectedEvt(event))?; 107 | 108 | match event.kind { 109 | DialEventKind::ButtonPress => self.possible_long_press = true, 110 | DialEventKind::ButtonRelease => self.possible_long_press = false, 111 | _ => {} 112 | } 113 | 114 | event 115 | } 116 | Ok(events::RawInputEvent::Connect) => { 117 | DialEvent { 118 | time: Duration::from_secs(0), // this could be improved... 119 | kind: DialEventKind::Connect, 120 | } 121 | } 122 | Ok(events::RawInputEvent::Disconnect) => { 123 | DialEvent { 124 | time: Duration::from_secs(0), // this could be improved... 125 | kind: DialEventKind::Disconnect, 126 | } 127 | } 128 | Err(mpsc::RecvTimeoutError::Timeout) => { 129 | self.possible_long_press = false; 130 | DialEvent { 131 | time: Duration::from_secs(0), // this could be improved... 132 | kind: DialEventKind::ButtonLongPress, 133 | } 134 | } 135 | Err(_e) => panic!("Could not recv event"), 136 | }; 137 | 138 | Ok(event) 139 | } 140 | 141 | pub fn haptics(&self) -> &DialHaptics { 142 | &self.haptics 143 | } 144 | } 145 | 146 | impl DialEvent { 147 | fn from_raw_evt(evt: evdev_rs::InputEvent) -> Option { 148 | use evdev_rs::enums::*; 149 | 150 | let evt_kind = match evt.event_type { 151 | EventType::EV_SYN | EventType::EV_MSC => DialEventKind::Ignored, 152 | EventType::EV_KEY => match evt.event_code { 153 | EventCode::EV_KEY(EV_KEY::BTN_0) => match evt.value { 154 | 0 => DialEventKind::ButtonRelease, 155 | 1 => DialEventKind::ButtonPress, 156 | _ => return None, 157 | }, 158 | _ => return None, 159 | }, 160 | EventType::EV_REL => match evt.event_code { 161 | EventCode::EV_REL(EV_REL::REL_DIAL) => DialEventKind::Dial(evt.value), 162 | _ => return None, 163 | }, 164 | _ => return None, 165 | }; 166 | 167 | let evt = DialEvent { 168 | time: Duration::new(evt.time.tv_sec as u64, (evt.time.tv_usec * 1000) as u32), 169 | kind: evt_kind, 170 | }; 171 | 172 | Some(evt) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/controller/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use crate::dial_device::{DialDevice, DialEventKind, DialHaptics}; 4 | use crate::error::{Error, Result}; 5 | 6 | pub mod controls; 7 | 8 | pub struct ControlModeMeta { 9 | /// Mode Name (as displayed in the Meta selection menu) 10 | name: &'static str, 11 | /// Mode Icon (as displayed in the Meta selection menu) 12 | /// 13 | /// This can be a file:// url, or a standard FreeDesktop icon name. 14 | icon: &'static str, 15 | /// Enable automatic haptic feedback when rotating the dial. 16 | haptics: bool, 17 | /// How many sections the dial should be divided into (from 0 to 3600). 18 | steps: u16, 19 | } 20 | 21 | pub trait ControlMode { 22 | fn meta(&self) -> ControlModeMeta; 23 | 24 | fn on_start(&mut self, _haptics: &DialHaptics) -> Result<()> { 25 | Ok(()) 26 | } 27 | 28 | fn on_end(&mut self, _haptics: &DialHaptics) -> Result<()> { 29 | Ok(()) 30 | } 31 | 32 | fn on_btn_press(&mut self, haptics: &DialHaptics) -> Result<()>; 33 | fn on_btn_release(&mut self, haptics: &DialHaptics) -> Result<()>; 34 | fn on_dial(&mut self, haptics: &DialHaptics, delta: i32) -> Result<()>; 35 | } 36 | 37 | enum ActiveMode { 38 | Normal(usize), 39 | Meta, 40 | } 41 | 42 | pub struct DialController { 43 | device: DialDevice, 44 | 45 | modes: Vec>, 46 | active_mode: ActiveMode, 47 | 48 | new_mode: Arc>>, 49 | meta_mode: Box, // concrete type is always `MetaMode` 50 | } 51 | 52 | impl DialController { 53 | pub fn new( 54 | device: DialDevice, 55 | initial_mode: usize, 56 | modes: Vec>, 57 | ) -> DialController { 58 | let metas = modes.iter().map(|m| m.meta()).collect(); 59 | 60 | let new_mode = Arc::new(Mutex::new(None)); 61 | 62 | DialController { 63 | device, 64 | 65 | modes, 66 | active_mode: ActiveMode::Normal(initial_mode), 67 | 68 | new_mode: new_mode.clone(), 69 | meta_mode: Box::new(MetaMode::new(new_mode, initial_mode, metas)), 70 | } 71 | } 72 | 73 | pub fn run(&mut self) -> Result<()> { 74 | loop { 75 | let evt = self.device.next_event()?; 76 | let haptics = self.device.haptics(); 77 | 78 | if let Some(new_mode) = self.new_mode.lock().unwrap().take() { 79 | self.active_mode = ActiveMode::Normal(new_mode); 80 | let mode = &mut self.modes[new_mode]; 81 | 82 | haptics.set_mode(mode.meta().haptics, mode.meta().steps)?; 83 | mode.on_start(haptics)?; 84 | } 85 | 86 | let mode = match self.active_mode { 87 | ActiveMode::Normal(idx) => &mut self.modes[idx], 88 | ActiveMode::Meta => &mut self.meta_mode, 89 | }; 90 | 91 | match evt.kind { 92 | DialEventKind::Ignored => {} 93 | 94 | DialEventKind::Connect => { 95 | eprintln!("Dial Connected"); 96 | haptics.set_mode(mode.meta().haptics, mode.meta().steps)?; 97 | mode.on_start(haptics)? 98 | } 99 | DialEventKind::Disconnect => { 100 | eprintln!("Dial Disconnected"); 101 | mode.on_end(haptics)? 102 | } 103 | 104 | DialEventKind::ButtonPress => mode.on_btn_press(haptics)?, 105 | DialEventKind::ButtonRelease => mode.on_btn_release(haptics)?, 106 | DialEventKind::Dial(delta) => mode.on_dial(haptics, delta)?, 107 | 108 | DialEventKind::ButtonLongPress => { 109 | eprintln!("long press!"); 110 | if !matches!(self.active_mode, ActiveMode::Meta) { 111 | mode.on_end(haptics)?; 112 | self.active_mode = ActiveMode::Meta; 113 | // meta_mode sets haptic feedback manually 114 | self.meta_mode.on_start(haptics)?; 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | /// A mode for switching between modes. 123 | struct MetaMode { 124 | // constant 125 | metas: Vec, 126 | 127 | // stateful (across invocations) 128 | current_mode: usize, 129 | new_mode: Arc>>, 130 | 131 | // reset in on_start 132 | first_release: bool, 133 | notif: Option, 134 | } 135 | 136 | impl MetaMode { 137 | fn new( 138 | new_mode: Arc>>, 139 | current_mode: usize, 140 | metas: Vec, 141 | ) -> MetaMode { 142 | MetaMode { 143 | metas, 144 | 145 | current_mode, 146 | new_mode, 147 | 148 | first_release: true, 149 | notif: None, 150 | } 151 | } 152 | } 153 | 154 | impl ControlMode for MetaMode { 155 | fn meta(&self) -> ControlModeMeta { 156 | unreachable!() // meta mode never queries itself 157 | } 158 | 159 | fn on_start(&mut self, haptics: &DialHaptics) -> Result<()> { 160 | use notify_rust::*; 161 | self.notif = Some( 162 | Notification::new() 163 | .hint(Hint::Resident(true)) 164 | .hint(Hint::Category("device".into())) 165 | .timeout(Timeout::Never) 166 | .summary("Surface Dial") 167 | .body(&format!( 168 | "Entered Meta Mode (From Mode: {})", 169 | self.metas[self.current_mode].name 170 | )) 171 | .icon("emblem-system") 172 | .show() 173 | .map_err(Error::Notif)?, 174 | ); 175 | 176 | haptics.set_mode(true, 36)?; 177 | haptics.buzz(1)?; 178 | 179 | self.first_release = true; 180 | 181 | Ok(()) 182 | } 183 | 184 | fn on_btn_press(&mut self, _haptics: &DialHaptics) -> Result<()> { 185 | Ok(()) 186 | } 187 | 188 | fn on_btn_release(&mut self, haptics: &DialHaptics) -> Result<()> { 189 | if self.first_release { 190 | self.first_release = false; 191 | } else { 192 | *self.new_mode.lock().unwrap() = Some(self.current_mode); 193 | 194 | crate::config::Config { 195 | last_mode: self.current_mode, 196 | } 197 | .to_disk()?; 198 | 199 | self.notif.take().unwrap().close(); 200 | haptics.buzz(1)?; 201 | } 202 | Ok(()) 203 | } 204 | 205 | fn on_dial(&mut self, _haptics: &DialHaptics, delta: i32) -> Result<()> { 206 | if delta > 0 { 207 | self.current_mode += 1; 208 | } else { 209 | if self.current_mode == 0 { 210 | self.current_mode = self.metas.len() - 1; 211 | } else { 212 | self.current_mode -= 1; 213 | } 214 | }; 215 | 216 | self.current_mode %= self.metas.len(); 217 | 218 | let mode_meta = &self.metas[self.current_mode]; 219 | if let Some(ref mut notification) = self.notif { 220 | notification 221 | .body(&format!("New Mode: {}", mode_meta.name)) 222 | .icon(mode_meta.icon); 223 | notification.update(); 224 | } 225 | 226 | Ok(()) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /notes/HID_Report_Descriptor.txt: -------------------------------------------------------------------------------- 1 | # extracted using cat /sys/bus/hid/devices/*:045E:091B.*/report_descriptor | hidrd-convert -o spec 2 | 3 | Usage Page (Desktop), ; Generic desktop controls (01h) 4 | Usage (0Eh), 5 | Collection (Application), 6 | Report ID (1), 7 | Usage Page (Digitizer), ; Digitizer (0Dh) 8 | Usage (Puck), ; Puck (21h, logical collection) 9 | Collection (Logical), 10 | Logical Minimum (0), 11 | Logical Maximum (1), 12 | Report Size (1), 13 | Report Count (1), 14 | Collection (Physical), 15 | Usage Page (Button), ; Button (09h) 16 | Usage (01h), 17 | Input (Variable), 18 | Usage Page (Digitizer), ; Digitizer (0Dh) 19 | Usage (Touch), ; Touch (33h, momentary control) 20 | Input (Variable), 21 | Report Count (6), 22 | Input (Constant, Variable), 23 | Collection (Logical), 24 | Usage Page (Desktop), ; Generic desktop controls (01h) 25 | Usage (Dial), ; Dial (37h, dynamic value) 26 | Logical Minimum (-32767), 27 | Logical Maximum (32767), 28 | Report Size (16), 29 | Report Count (1), 30 | Input (Variable, Relative), 31 | Physical Minimum (0), 32 | Physical Maximum (3600), 33 | Logical Minimum (0), 34 | Logical Maximum (3600), 35 | Usage (Resolution Multiplier), ; Resolution multiplier (48h, dynamic value) 36 | Feature (Variable), 37 | Physical Maximum (0), 38 | End Collection, 39 | Unit Exponent (14), 40 | Unit (Centimeter), 41 | Physical Maximum (0), 42 | Logical Maximum (0), 43 | Usage (X), ; X (30h, dynamic value) 44 | Input (Variable, Null State), 45 | Usage (Y), ; Y (31h, dynamic value) 46 | Physical Maximum (0), 47 | Logical Maximum (0), 48 | Input (Variable, Null State), 49 | Usage Page (Digitizer), ; Digitizer (0Dh) 50 | Usage (Width), ; Width (48h, dynamic value) 51 | Logical Minimum (58), 52 | Logical Maximum (58), 53 | Report Size (8), 54 | Unit Exponent (15), 55 | Physical Minimum (58), 56 | Physical Maximum (58), 57 | Input (Constant, Variable), 58 | Unit Exponent (0), 59 | Unit, 60 | Physical Minimum (0), 61 | Physical Maximum (0), 62 | Usage Page (0Eh), ; 0Eh, reserved 63 | Usage (01h), 64 | Collection (Logical), 65 | Logical Minimum (0), 66 | Logical Maximum (255), 67 | Usage (24h), 68 | Feature (Variable, Null State), 69 | Usage (24h), 70 | Output (Variable, Null State), 71 | Logical Minimum (1), 72 | Logical Maximum (7), 73 | Usage (20h), 74 | Feature (Variable, Null State), 75 | Usage (21h), 76 | Output (Variable, Null State), 77 | Logical Maximum (10), 78 | Usage (28h), 79 | Feature (Variable, Null State), 80 | Report Size (16), 81 | Logical Maximum (2000), 82 | Usage (25h), 83 | Feature (Variable, Null State), 84 | Usage (25h), 85 | Output (Variable, Null State), 86 | Report ID (2), 87 | Report Size (32), 88 | Logical Minimum (65591), 89 | Logical Maximum (65591), 90 | Usage (22h), 91 | Feature (Variable), 92 | Usage (11h), 93 | Collection (Logical), 94 | Usage Page (Ordinal), ; Ordinal (0Ah) 95 | Report Count (3), 96 | Usage (03h), 97 | Usage (04h), 98 | Usage (05h), 99 | Report Size (8), 100 | Logical Minimum (0), 101 | Logical Maximum (-1), 102 | Feature (Variable), 103 | End Collection, 104 | Usage Page (0Eh), ; 0Eh, reserved 105 | Usage (10h), 106 | Collection (Logical), 107 | Usage Page (Ordinal), ; Ordinal (0Ah) 108 | Report Count (1), 109 | Logical Minimum (3), 110 | Logical Maximum (3), 111 | Physical Minimum (4099), 112 | Physical Maximum (4099), 113 | Usage (03h), 114 | Feature (Variable), 115 | Logical Minimum (4), 116 | Logical Maximum (4), 117 | Physical Minimum (4100), 118 | Physical Maximum (4100), 119 | Usage (04h), 120 | Feature (Variable), 121 | Logical Minimum (5), 122 | Logical Maximum (5), 123 | Physical Minimum (4100), 124 | Physical Maximum (4100), 125 | Usage (05h), 126 | Feature (Variable), 127 | Physical Minimum (0), 128 | Physical Maximum (0), 129 | End Collection, 130 | End Collection, 131 | End Collection, 132 | End Collection, 133 | End Collection, 134 | Usage Page (FF07h), ; FF07h, vendor-defined 135 | Usage (70h), 136 | Collection (Application), 137 | Report ID (48), 138 | Logical Minimum (0), 139 | Logical Maximum (-1), 140 | Report Count (1), 141 | Report Size (8), 142 | Usage (00h), 143 | Output (Variable), 144 | End Collection, 145 | Usage (71h), 146 | Collection (Application), 147 | Logical Minimum (0), 148 | Logical Maximum (-1), 149 | Report Size (8), 150 | Report Count (72), 151 | Report ID (42), 152 | Usage (C6h), 153 | Input (Variable, Buffered Bytes), 154 | Usage (C7h), 155 | Output (Variable, Buffered Bytes), 156 | Report Count (52), 157 | Usage (C8h), 158 | Feature (Constant, Variable, Buffered Bytes), 159 | Report ID (43), 160 | Usage (C9h), 161 | Input (Variable, Buffered Bytes), 162 | Usage (CAh), 163 | Output (Variable, Buffered Bytes), 164 | Usage (CBh), 165 | Feature (Variable, Buffered Bytes), 166 | Logical Minimum (-2147483648), 167 | Logical Maximum (2147483647), 168 | Report Size (32), 169 | Report Count (4), 170 | Report ID (44), 171 | Usage Minimum (CCh), 172 | Usage Maximum (CFh), 173 | Input (Variable), 174 | Report Count (4), 175 | Report ID (45), 176 | Usage Minimum (D8h), 177 | Usage Maximum (DBh), 178 | Input (Variable), 179 | Report Count (4), 180 | Usage Minimum (DCh), 181 | Usage Maximum (DFh), 182 | Output (Variable), 183 | Usage Minimum (E0h), 184 | Usage Maximum (E3h), 185 | Feature (Variable), 186 | Report ID (46), 187 | Usage Minimum (E4h), 188 | Usage Maximum (E7h), 189 | Input (Variable), 190 | Usage Minimum (E8h), 191 | Usage Maximum (EBh), 192 | Output (Variable), 193 | Report Count (11), 194 | Usage Minimum (ECh), 195 | Usage Maximum (EFh), 196 | Feature (Variable), 197 | Report Count (4), 198 | Report ID (47), 199 | Usage Minimum (F0h), 200 | Usage Maximum (F3h), 201 | Input (Variable), 202 | Usage Minimum (F4h), 203 | Usage Maximum (F7h), 204 | Output (Variable), 205 | Usage Minimum (F8h), 206 | Usage Maximum (FBh), 207 | Feature (Variable), 208 | End Collection, 209 | Usage Page (Desktop), ; Generic desktop controls (01h) 210 | Usage (Sys Control), ; System control (80h, application collection) 211 | Collection (Application), 212 | Report ID (50), 213 | Usage (Sys Sleep), ; System sleep (82h, one-shot control) 214 | Usage (Sys Wake Up), ; System wake up (83h, one-shot control) 215 | Logical Minimum (0), 216 | Logical Maximum (1), 217 | Report Count (2), 218 | Report Size (1), 219 | Input (Variable), 220 | Report Count (6), 221 | Input (Constant, Variable), 222 | End Collection, 223 | Usage (72h), 224 | Collection (Application), 225 | Report ID (49), 226 | Report Count (10), 227 | Report Size (8), 228 | Logical Minimum (0), 229 | Logical Maximum (-1), 230 | Usage (C6h), 231 | Input (Variable), 232 | Usage (C7h), 233 | Output (Variable), 234 | End Collection 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # surface-dial-linux 2 | 3 | A Linux userspace controller for the [Microsoft Surface Dial](https://www.microsoft.com/en-us/p/surface-dial/925r551sktgn). Requires Linux Kernel 4.19 or higher. 4 | 5 | As you might have guessed by the lack of recent commits, this project has entered **maintenence mode**. While I will not be implementing any new features myself, I am more than glad to take a look at any bug reports and/or review any PRs sent my way! 6 | 7 | ## Overview 8 | 9 | `surface-dial-daemon` receives raw events from the surface dial and translates them to more conventional input events. 10 | 11 | The daemon uses FreeDesktop notifications to provide visual feedback when switching between actions. 12 | 13 | ![](notif-demo.gif) 14 | 15 | 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. 16 | 17 | ### Operating Modes 18 | 19 | Holding down the button for \~750ms will open up a meta-menu (using FreeDesktop notifications) which allows you to switch between operating modes on-the-fly. The daemon persists the last selected mode on-disk, so if you only care about one mode (e.g: scrolling), you can just set it and forget it. 20 | 21 | Modes in **bold** should be considered **EXPERIMENTAL**. They seem to work alright (some of the time), but they would really benefit from some more polish / bug fixes. 22 | 23 | | Mode | Click | Rotate | Notes | 24 | | ---------------------------- | ----------------- | -------------------- | -------------------------------------------------------------------------------------- | 25 | | Scroll | - | Scroll | Fakes chunky mouse-wheel scrolling 1 | 26 | | **Scroll (Fake Multitouch)** | Reset Touch Event | Scroll | Fakes smooth two-finger scrolling | 27 | | Zoom | - | Zoom | | 28 | | Volume | Mute | Volume | | 29 | | Media | Play/Pause | Next/Prev Track | | 30 | | Media + Volume | Play/Pause | Volume | Double-click = Next Track | 31 | | **Paddle Controller** | Space | Left/Right Arrow Key | Play [arkanoid](https://www.google.com/search?q=arkanoid+paddle) as the devs intended! | 32 | 33 | 1 At the time of writing, almost all Linux userspace programs don't take advantage of the newer high-resolution scroll wheel events, and only support the older, chunkier scroll wheel events. Check out [this blog post](https://who-t.blogspot.com/2020/04/high-resolution-wheel-scrolling-in.html) for more details. 34 | 35 | ### Custom Modes 36 | 37 | At the moment, all modes (along with their meta-menu ordering) are hard-coded into the daemon itself. 38 | 39 | If you don't mind hacking together a bit of [very simple] Rust code, adding new modes should be fairly straightforward - just add a new `ControlMode` implementation under `src/controller/controls` and instantiate it in `main.rs`. 40 | 41 | If you ended up implementing new mode you think others would find useful, please consider upstreaming it! 42 | 43 | ## Building 44 | 45 | Building `surface-dial-daemon` requires the following: 46 | 47 | - Linux Kernel 4.19 or higher 48 | - A fairly recent version of the Rust compiler 49 | - `libudev` 50 | - `libevdev` 51 | - `hidapi` 52 | 53 | You can install Rust through [`rustup`](https://rustup.rs/). 54 | 55 | Unless you're a cool hackerman, the easiest way to get `libudev`, `libevdev`, and `hidapi` is via your distro's package manager. 56 | 57 | ```bash 58 | # e.g: on ubuntu 59 | sudo apt install libevdev-dev libhidapi-dev libudev-dev 60 | ``` 61 | 62 | On certain Ubuntu distros, you may also need to install the `librust-libdbus-sys-dev` package: 63 | 64 | ```bash 65 | sudo apt install librust-libdbus-sys-dev 66 | ``` 67 | 68 | Once those are installed, `surface-dial-daemon` can be built using the standard `cargo build` flow. 69 | 70 | ```bash 71 | cargo build -p surface-dial-daemon --release 72 | ``` 73 | 74 | The resulting binary is output to `target/release/surface-dial-daemon` 75 | 76 | ## Running 77 | 78 | The daemon is able to handle the dial disconnecting/reconnecting, so as long as it's running in the background, things should Just Work:tm:. 79 | 80 | Note that the daemon must run as a _user process_ (**not** as root), as it needs access to the user's D-Bus to send notifications. 81 | 82 | Having to run as a user process complicates things a bit, as the daemon must be able to access several restricted-by-default devices under `/dev/`. Notably, the `/dev/uinput` device and the Surface Dial's `/dev/hidrawX` device will need to have their permissions changed for things to work correctly. The proper way to do this is using the included [udev rules](https://wiki.debian.org/udev), though if you just want to get something up and running, `sudo chmod 666 ` should work fine (though it will revert back once you reboot!). 83 | 84 | See the Installation section below for how to set up the permissions / udev rules. 85 | 86 | During development, the easiest way to run `surface-dial-linux` is using `cargo`: 87 | 88 | ```bash 89 | cargo run -p surface-dial-daemon 90 | ``` 91 | 92 | Alternatively, you can run the daemon directly using the executable at `target/release/surface-dial-daemon`. 93 | 94 | ## Installation 95 | 96 | I encourage you to tweak the following setup procedure for your particular Linux configuration. 97 | 98 | The following steps have been tested working on Ubuntu 20.04/20.10. 99 | 100 | ```bash 101 | # Install the `surface-dial-daemon` (i.e: build it, and place it under ~/.cargo/bin/surface-dial-daemon) 102 | # You could also just copy the executable from /target/release/surface-dial-daemon to wherever you like. 103 | cargo install --path . 104 | 105 | # add self to the existing /dev/input group (either `input` or `plugdev`, depending on your distro) 106 | sudo gpasswd -a $(whoami) $(stat -c "%G" /dev/input/event0) 107 | 108 | # install the systemd user service 109 | mkdir -p ~/.config/systemd/user/ 110 | cp ./install/surface-dial.service ~/.config/systemd/user/surface-dial.service 111 | 112 | # install the udev rules 113 | sudo cp ./install/10-uinput.rules /etc/udev/rules.d/10-uinput.rules 114 | sudo cp ./install/10-surface-dial.rules /etc/udev/rules.d/10-surface-dial.rules 115 | 116 | # reload systemd + udev 117 | systemctl --user daemon-reload 118 | sudo udevadm control --reload 119 | 120 | # enable and start the user service 121 | systemctl --user enable surface-dial.service 122 | systemctl --user start surface-dial.service 123 | ``` 124 | 125 | To see if the service is running correctly, run `systemctl --user status surface-dial.service`. 126 | 127 | You may need to reboot to have the various groups / udev rules propagate. You may also need to change DisableSecurity to DisableSecurity=true in /etc/bluetooth/network.conf to successfully pair the Surface Dial. 128 | 129 | If things aren't working, feel free to file a bug report! 130 | 131 | _Call for Contributors:_ It would be awesome to have a proper packaging pipeline set up as well (e.g for deb/rpm). 132 | 133 | ## Implementation Notes 134 | 135 | Core functionality is provided by the following libraries. 136 | 137 | - `libudev` to monitor when the dial connects/disconnects. 138 | - `libevdev` to read events from the surface dial through `/dev/input/eventXX`, and to fake input through `/dev/uinput`. 139 | - `hidapi` to configure dial sensitivity + haptics. 140 | - `notify-rust` to send desktop notifications over D-Bus. 141 | 142 | The code makes heavy use of threads + channels to do non-blocking event handling. While async/await would have been a great fit for an application like this, I optimized for getting something up-and-running rather than maximum performance. Plus, the Rust wrappers around `libudev`, `libevdev`, and `hidapi` don't have native async/await support, so it would have been a lot more work for not too much gain. 143 | 144 | The codebase is reasonably well organized, aside from the `dial_device` implementation, which is admittedly a bit gnarly. There's a bit of of thread/channel spaghetti going on to ensure that the lifetime of the haptics object lines up with the lifetime of the `libevdev` objects (as reported by `libudev`). All things considered, it's not _too_ messy, but it could certainly use some cleanup. Fortunately, if you're only interested in implementing new operating modes, you won't have to worry about any of that, as all the nitty-gritty device interaction is neatly encapsulated behind the `ControlMode` trait. 145 | 146 | ## Feature Roadmap 147 | 148 | This is a rough outline of features I'd like to see implemented in this daemon. There's a non-zero chance that at some point the daemon will be "good enough" for me, and some features will be left unimplemented. 149 | 150 | Contributions are more than welcome! 151 | 152 | - [x] Interpreting raw Surface Dial events 153 | - [x] Haptic Feedback 154 | - https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/radial-controller-protocol-implementation 155 | - https://www.usb.org/sites/default/files/hutrr63b_-_haptics_page_redline_0.pdf 156 | - https://www.usb.org/sites/default/files/hut1_21.pdf 157 | - _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!_ 158 | - [x] Set up a framework to easily implement various operating modes 159 | - [x] In-code abstraction over Surface Dial Events / Haptics API 160 | - [ ] Config file(s) to create simple custom modes 161 | - [x] Dynamically switching between operating modes 162 | - [x] Using a long-press activated "meta-mode" 163 | - [ ] Context-sensitive (based on the currently open application) 164 | - [ ] Config-file support 165 | - [ ] Adjusting timings (e.g: long press timeout, double-click window, etc...) 166 | - [ ] Custom operating mode ordering in the meta-menu 167 | - [x] Visual Feedback 168 | - [x] FreeDesktop Notifications 169 | - [ ] \(longshot\) Windows-like Wheel menu 170 | - [x] Last selected mode persistence (between daemon invocations) 171 | - [x] Gracefully handling disconnect/reconnect 172 | - [ ] Packaging pipeline (e.g: deb/rpm) 173 | -------------------------------------------------------------------------------- /src/fake_input.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use evdev_rs::enums::*; 4 | use evdev_rs::{Device, InputEvent, TimeVal, UInputDevice}; 5 | use parking_lot::ReentrantMutex; 6 | 7 | // this should be a fairly high number, as the axis is from 0..(MT_BASELINE*2) 8 | const MT_BASELINE: i32 = std::i32::MAX / 8; 9 | // higher = slower scrolling 10 | const MT_SENSITIVITY: i32 = 48; 11 | 12 | pub struct FakeInputs { 13 | keyboard: ReentrantMutex, 14 | touchpad: ReentrantMutex, 15 | } 16 | 17 | lazy_static::lazy_static! { 18 | pub static ref FAKE_INPUTS: FakeInputs = { 19 | let keyboard = (|| -> io::Result<_> { 20 | let device = Device::new().unwrap(); 21 | device.set_name("Surface Dial Virtual Keyboard/Mouse"); 22 | 23 | device.enable(&EventType::EV_SYN)?; 24 | device.enable(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))?; 25 | 26 | device.enable(&EventType::EV_MSC)?; 27 | device.enable(&EventCode::EV_MSC(EV_MSC::MSC_SCAN))?; 28 | 29 | device.enable(&EventType::EV_KEY)?; 30 | { 31 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFTSHIFT))?; 32 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFTCTRL))?; 33 | 34 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_MUTE))?; 35 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEDOWN))?; 36 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEUP))?; 37 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_NEXTSONG))?; 38 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PLAYPAUSE))?; 39 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PREVIOUSSONG))?; 40 | 41 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFT))?; 42 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_RIGHT))?; 43 | 44 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_SPACE))?; 45 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_EQUAL))?; 46 | device.enable(&EventCode::EV_KEY(EV_KEY::KEY_MINUS))?; 47 | } 48 | 49 | device.enable(&EventType::EV_REL)?; 50 | { 51 | device.enable(&EventCode::EV_REL(EV_REL::REL_WHEEL))?; 52 | device.enable(&EventCode::EV_REL(EV_REL::REL_WHEEL_HI_RES))?; 53 | } 54 | 55 | Ok(ReentrantMutex::new(UInputDevice::create_from_device(&device)?)) 56 | })().expect("failed to install virtual mouse/keyboard device"); 57 | 58 | let touchpad = (|| -> io::Result<_> { 59 | let device = Device::new().unwrap(); 60 | device.set_name("Surface Dial Virtual Touchpad"); 61 | 62 | device.enable(&InputProp::INPUT_PROP_BUTTONPAD)?; 63 | device.enable(&InputProp::INPUT_PROP_POINTER)?; 64 | 65 | device.enable(&EventType::EV_SYN)?; 66 | device.enable(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))?; 67 | 68 | device.enable(&EventType::EV_KEY)?; 69 | { 70 | device.enable(&EventCode::EV_KEY(EV_KEY::BTN_LEFT))?; 71 | device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_FINGER))?; 72 | device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOUCH))?; 73 | device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_DOUBLETAP))?; 74 | device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_TRIPLETAP))?; 75 | device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_QUADTAP))?; 76 | } 77 | 78 | // roughly copied from my laptop's trackpad (Aero 15x) 79 | device.enable(&EventType::EV_ABS)?; 80 | { 81 | let mut abs_info = evdev_rs::AbsInfo { 82 | value: 0, 83 | minimum: 0, 84 | maximum: 0, 85 | fuzz: 0, 86 | flat: 0, 87 | resolution: 0, 88 | }; 89 | 90 | abs_info.minimum = 0; 91 | abs_info.maximum = 4; 92 | device.enable_event_code(&EventCode::EV_ABS(EV_ABS::ABS_MT_SLOT), Some(&abs_info))?; 93 | 94 | abs_info.minimum = 0; 95 | abs_info.maximum = 65535; 96 | device.enable_event_code( 97 | &EventCode::EV_ABS(EV_ABS::ABS_MT_TRACKING_ID), 98 | Some(&abs_info), 99 | )?; 100 | 101 | abs_info.resolution = MT_SENSITIVITY; 102 | abs_info.minimum = 0; 103 | abs_info.maximum = MT_BASELINE * 2; 104 | abs_info.value = MT_BASELINE; 105 | device.enable_event_code( 106 | &EventCode::EV_ABS(EV_ABS::ABS_MT_POSITION_X), 107 | Some(&abs_info), 108 | )?; 109 | 110 | abs_info.value = MT_BASELINE; 111 | abs_info.minimum = 0; 112 | abs_info.maximum = MT_BASELINE * 2; 113 | abs_info.resolution = MT_SENSITIVITY; 114 | device.enable_event_code( 115 | &EventCode::EV_ABS(EV_ABS::ABS_MT_POSITION_Y), 116 | Some(&abs_info), 117 | )?; 118 | } 119 | 120 | Ok(ReentrantMutex::new(UInputDevice::create_from_device(&device)?)) 121 | })().expect("failed to install virtual touchpad device"); 122 | 123 | // HACK: give the kernel a chance to register the new devices. If this 124 | // line is omitted, the first fake input is likely to be dropped. 125 | std::thread::sleep(std::time::Duration::from_millis(500)); 126 | 127 | FakeInputs { 128 | keyboard, 129 | touchpad 130 | } 131 | }; 132 | } 133 | 134 | macro_rules! input_event { 135 | ($type:ident, $code:ident, $value:expr) => { 136 | InputEvent { 137 | time: TimeVal::new(0, 0), 138 | event_code: EventCode::$type($type::$code), 139 | event_type: EventType::$type, 140 | value: $value, 141 | } 142 | }; 143 | } 144 | 145 | fn kbd_syn_report() -> io::Result<()> { 146 | (FAKE_INPUTS.keyboard.lock()).write_event(&input_event!(EV_SYN, SYN_REPORT, 0)) 147 | } 148 | 149 | pub fn key_click(keys: &[EV_KEY]) -> io::Result<()> { 150 | key_press(keys)?; 151 | key_release(keys)?; 152 | Ok(()) 153 | } 154 | 155 | pub fn key_press(keys: &[EV_KEY]) -> io::Result<()> { 156 | let keyboard = FAKE_INPUTS.keyboard.lock(); 157 | 158 | for key in keys { 159 | keyboard.write_event(&InputEvent { 160 | time: TimeVal::new(0, 0), 161 | event_code: EventCode::EV_KEY(*key), 162 | event_type: EventType::EV_KEY, 163 | value: 1, 164 | })?; 165 | } 166 | kbd_syn_report()?; 167 | Ok(()) 168 | } 169 | 170 | pub fn key_release(keys: &[EV_KEY]) -> io::Result<()> { 171 | let keyboard = FAKE_INPUTS.keyboard.lock(); 172 | 173 | for key in keys.iter().clone() { 174 | keyboard.write_event(&InputEvent { 175 | time: TimeVal::new(0, 0), 176 | event_code: EventCode::EV_KEY(*key), 177 | event_type: EventType::EV_KEY, 178 | value: 0, 179 | })?; 180 | } 181 | kbd_syn_report()?; 182 | Ok(()) 183 | } 184 | 185 | pub fn scroll_step(dir: ScrollStep) -> io::Result<()> { 186 | let keyboard = FAKE_INPUTS.keyboard.lock(); 187 | 188 | // copied from my razer blackwidow chroma mouse 189 | keyboard.write_event(&InputEvent { 190 | time: TimeVal::new(0, 0), 191 | event_code: EventCode::EV_REL(EV_REL::REL_WHEEL), 192 | event_type: EventType::EV_REL, 193 | value: match dir { 194 | ScrollStep::Down => -1, 195 | ScrollStep::Up => 1, 196 | }, 197 | })?; 198 | keyboard.write_event(&InputEvent { 199 | time: TimeVal::new(0, 0), 200 | event_code: EventCode::EV_REL(EV_REL::REL_WHEEL_HI_RES), 201 | event_type: EventType::EV_REL, 202 | value: match dir { 203 | ScrollStep::Down => -120, 204 | ScrollStep::Up => 120, 205 | }, 206 | })?; 207 | kbd_syn_report()?; 208 | Ok(()) 209 | } 210 | 211 | fn touch_syn_report() -> io::Result<()> { 212 | (FAKE_INPUTS.touchpad.lock()).write_event(&input_event!(EV_SYN, SYN_REPORT, 0)) 213 | } 214 | 215 | pub fn scroll_mt_start() -> io::Result<()> { 216 | let touchpad = FAKE_INPUTS.touchpad.lock(); 217 | 218 | { 219 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_SLOT, 0))?; 220 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_TRACKING_ID, 1))?; 221 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_POSITION_X, MT_BASELINE))?; 222 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_POSITION_Y, MT_BASELINE))?; 223 | 224 | touchpad.write_event(&input_event!(EV_KEY, BTN_TOUCH, 1))?; 225 | touchpad.write_event(&input_event!(EV_KEY, BTN_TOOL_FINGER, 1))?; 226 | 227 | touchpad.write_event(&input_event!(EV_ABS, ABS_X, MT_BASELINE))?; 228 | touchpad.write_event(&input_event!(EV_ABS, ABS_Y, MT_BASELINE))?; 229 | } 230 | 231 | touch_syn_report()?; 232 | 233 | { 234 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_SLOT, 1))?; 235 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_TRACKING_ID, 2))?; 236 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_POSITION_X, MT_BASELINE / 2))?; 237 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_POSITION_Y, MT_BASELINE))?; 238 | 239 | touchpad.write_event(&input_event!(EV_KEY, BTN_TOOL_FINGER, 0))?; 240 | touchpad.write_event(&input_event!(EV_KEY, BTN_TOOL_DOUBLETAP, 1))?; 241 | } 242 | 243 | touch_syn_report()?; 244 | 245 | Ok(()) 246 | } 247 | 248 | pub fn scroll_mt_step(delta: i32) -> io::Result<()> { 249 | let touchpad = FAKE_INPUTS.touchpad.lock(); 250 | 251 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_SLOT, 0))?; 252 | touchpad.write_event(&input_event!( 253 | EV_ABS, 254 | ABS_MT_POSITION_Y, 255 | MT_BASELINE + delta 256 | ))?; 257 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_SLOT, 1))?; 258 | touchpad.write_event(&input_event!( 259 | EV_ABS, 260 | ABS_MT_POSITION_Y, 261 | MT_BASELINE + delta 262 | ))?; 263 | 264 | touchpad.write_event(&input_event!(EV_ABS, ABS_Y, MT_BASELINE + delta))?; 265 | 266 | touch_syn_report()?; 267 | 268 | Ok(()) 269 | } 270 | 271 | pub fn scroll_mt_end() -> io::Result<()> { 272 | let touchpad = FAKE_INPUTS.touchpad.lock(); 273 | 274 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_SLOT, 0))?; 275 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_TRACKING_ID, -1))?; 276 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_SLOT, 1))?; 277 | touchpad.write_event(&input_event!(EV_ABS, ABS_MT_TRACKING_ID, -1))?; 278 | 279 | touchpad.write_event(&input_event!(EV_KEY, BTN_TOUCH, 0))?; 280 | touchpad.write_event(&input_event!(EV_KEY, BTN_TOOL_DOUBLETAP, 0))?; 281 | 282 | touch_syn_report()?; 283 | 284 | Ok(()) 285 | } 286 | 287 | pub enum ScrollStep { 288 | Up, 289 | Down, 290 | } 291 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "arc-swap" 5 | version = "0.4.7" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" 8 | 9 | [[package]] 10 | name = "arrayref" 11 | version = "0.3.6" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 14 | 15 | [[package]] 16 | name = "arrayvec" 17 | version = "0.5.2" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 20 | 21 | [[package]] 22 | name = "autocfg" 23 | version = "1.0.1" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 26 | 27 | [[package]] 28 | name = "base64" 29 | version = "0.12.3" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 32 | 33 | [[package]] 34 | name = "bitflags" 35 | version = "0.9.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" 38 | 39 | [[package]] 40 | name = "bitflags" 41 | version = "1.2.1" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 44 | 45 | [[package]] 46 | name = "blake2b_simd" 47 | version = "0.5.10" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" 50 | dependencies = [ 51 | "arrayref", 52 | "arrayvec", 53 | "constant_time_eq", 54 | ] 55 | 56 | [[package]] 57 | name = "block" 58 | version = "0.1.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 61 | 62 | [[package]] 63 | name = "cc" 64 | version = "1.0.61" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "0.1.10" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 73 | 74 | [[package]] 75 | name = "cfg-if" 76 | version = "1.0.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 79 | 80 | [[package]] 81 | name = "chrono" 82 | version = "0.4.19" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 85 | dependencies = [ 86 | "libc", 87 | "num-integer", 88 | "num-traits", 89 | "time", 90 | "winapi", 91 | ] 92 | 93 | [[package]] 94 | name = "cloudabi" 95 | version = "0.1.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" 98 | dependencies = [ 99 | "bitflags 1.2.1", 100 | ] 101 | 102 | [[package]] 103 | name = "constant_time_eq" 104 | version = "0.1.5" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 107 | 108 | [[package]] 109 | name = "crossbeam-utils" 110 | version = "0.7.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 113 | dependencies = [ 114 | "autocfg", 115 | "cfg-if 0.1.10", 116 | "lazy_static", 117 | ] 118 | 119 | [[package]] 120 | name = "dbus" 121 | version = "0.8.4" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "5cd9e78c210146a1860f897db03412fd5091fd73100778e43ee255cca252cf32" 124 | dependencies = [ 125 | "libc", 126 | "libdbus-sys", 127 | ] 128 | 129 | [[package]] 130 | name = "directories" 131 | version = "3.0.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "f8fed639d60b58d0f53498ab13d26f621fd77569cc6edb031f4cc36a2ad9da0f" 134 | dependencies = [ 135 | "dirs-sys", 136 | ] 137 | 138 | [[package]] 139 | name = "dirs" 140 | version = "1.0.5" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" 143 | dependencies = [ 144 | "libc", 145 | "redox_users", 146 | "winapi", 147 | ] 148 | 149 | [[package]] 150 | name = "dirs-sys" 151 | version = "0.3.5" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 154 | dependencies = [ 155 | "libc", 156 | "redox_users", 157 | "winapi", 158 | ] 159 | 160 | [[package]] 161 | name = "evdev-rs" 162 | version = "0.4.0" 163 | source = "git+https://github.com/ndesh26/evdev-rs.git?rev=8e995b8bf#8e995b8bfc1f8ed844ae47edaa4286c99e8c88b5" 164 | dependencies = [ 165 | "bitflags 1.2.1", 166 | "evdev-sys", 167 | "libc", 168 | "log", 169 | ] 170 | 171 | [[package]] 172 | name = "evdev-sys" 173 | version = "0.2.1" 174 | source = "git+https://github.com/ndesh26/evdev-rs.git?rev=8e995b8bf#8e995b8bfc1f8ed844ae47edaa4286c99e8c88b5" 175 | dependencies = [ 176 | "cc", 177 | "libc", 178 | "pkg-config", 179 | ] 180 | 181 | [[package]] 182 | name = "getrandom" 183 | version = "0.1.15" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" 186 | dependencies = [ 187 | "cfg-if 0.1.10", 188 | "libc", 189 | "wasi 0.9.0+wasi-snapshot-preview1", 190 | ] 191 | 192 | [[package]] 193 | name = "hidapi" 194 | version = "1.2.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "5c6ffb97f2ec5835ec73bcea5256fc2cd57a13c5958230778ef97f11900ba661" 197 | dependencies = [ 198 | "cc", 199 | "libc", 200 | "pkg-config", 201 | ] 202 | 203 | [[package]] 204 | name = "instant" 205 | version = "0.1.8" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613" 208 | dependencies = [ 209 | "cfg-if 1.0.0", 210 | ] 211 | 212 | [[package]] 213 | name = "lazy_static" 214 | version = "1.4.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 217 | 218 | [[package]] 219 | name = "libc" 220 | version = "0.2.80" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" 223 | 224 | [[package]] 225 | name = "libdbus-sys" 226 | version = "0.2.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0" 229 | dependencies = [ 230 | "pkg-config", 231 | ] 232 | 233 | [[package]] 234 | name = "libudev-sys" 235 | version = "0.1.4" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" 238 | dependencies = [ 239 | "libc", 240 | "pkg-config", 241 | ] 242 | 243 | [[package]] 244 | name = "lock_api" 245 | version = "0.4.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" 248 | dependencies = [ 249 | "scopeguard", 250 | ] 251 | 252 | [[package]] 253 | name = "log" 254 | version = "0.4.11" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 257 | dependencies = [ 258 | "cfg-if 0.1.10", 259 | ] 260 | 261 | [[package]] 262 | name = "mac-notification-sys" 263 | version = "0.3.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76" 266 | dependencies = [ 267 | "cc", 268 | "chrono", 269 | "dirs", 270 | "objc-foundation", 271 | ] 272 | 273 | [[package]] 274 | name = "malloc_buf" 275 | version = "0.0.6" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 278 | dependencies = [ 279 | "libc", 280 | ] 281 | 282 | [[package]] 283 | name = "nix" 284 | version = "0.19.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "85db2feff6bf70ebc3a4793191517d5f0331100a2f10f9bf93b5e5214f32b7b7" 287 | dependencies = [ 288 | "bitflags 1.2.1", 289 | "cc", 290 | "cfg-if 0.1.10", 291 | "libc", 292 | ] 293 | 294 | [[package]] 295 | name = "notify-rust" 296 | version = "4.0.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "144acee6a0543dc74893e4b8a33936b5b0a94cc2d4ab024afd0c6daff7afc3c0" 299 | dependencies = [ 300 | "dbus", 301 | "mac-notification-sys", 302 | "winrt-notification", 303 | ] 304 | 305 | [[package]] 306 | name = "num-integer" 307 | version = "0.1.44" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 310 | dependencies = [ 311 | "autocfg", 312 | "num-traits", 313 | ] 314 | 315 | [[package]] 316 | name = "num-traits" 317 | version = "0.2.14" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 320 | dependencies = [ 321 | "autocfg", 322 | ] 323 | 324 | [[package]] 325 | name = "objc" 326 | version = "0.2.7" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 329 | dependencies = [ 330 | "malloc_buf", 331 | ] 332 | 333 | [[package]] 334 | name = "objc-foundation" 335 | version = "0.1.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 338 | dependencies = [ 339 | "block", 340 | "objc", 341 | "objc_id", 342 | ] 343 | 344 | [[package]] 345 | name = "objc_id" 346 | version = "0.1.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 349 | dependencies = [ 350 | "objc", 351 | ] 352 | 353 | [[package]] 354 | name = "parking_lot" 355 | version = "0.11.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" 358 | dependencies = [ 359 | "instant", 360 | "lock_api", 361 | "parking_lot_core", 362 | ] 363 | 364 | [[package]] 365 | name = "parking_lot_core" 366 | version = "0.8.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" 369 | dependencies = [ 370 | "cfg-if 0.1.10", 371 | "cloudabi", 372 | "instant", 373 | "libc", 374 | "redox_syscall", 375 | "smallvec", 376 | "winapi", 377 | ] 378 | 379 | [[package]] 380 | name = "pkg-config" 381 | version = "0.3.19" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 384 | 385 | [[package]] 386 | name = "quote" 387 | version = "0.3.15" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 390 | 391 | [[package]] 392 | name = "redox_syscall" 393 | version = "0.1.57" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 396 | 397 | [[package]] 398 | name = "redox_users" 399 | version = "0.3.5" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 402 | dependencies = [ 403 | "getrandom", 404 | "redox_syscall", 405 | "rust-argon2", 406 | ] 407 | 408 | [[package]] 409 | name = "rust-argon2" 410 | version = "0.8.2" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" 413 | dependencies = [ 414 | "base64", 415 | "blake2b_simd", 416 | "constant_time_eq", 417 | "crossbeam-utils", 418 | ] 419 | 420 | [[package]] 421 | name = "scopeguard" 422 | version = "1.1.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 425 | 426 | [[package]] 427 | name = "signal-hook" 428 | version = "0.1.16" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" 431 | dependencies = [ 432 | "libc", 433 | "signal-hook-registry", 434 | ] 435 | 436 | [[package]] 437 | name = "signal-hook-registry" 438 | version = "1.2.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" 441 | dependencies = [ 442 | "arc-swap", 443 | "libc", 444 | ] 445 | 446 | [[package]] 447 | name = "smallvec" 448 | version = "1.4.2" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" 451 | 452 | [[package]] 453 | name = "strum" 454 | version = "0.8.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da" 457 | 458 | [[package]] 459 | name = "strum_macros" 460 | version = "0.8.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8" 463 | dependencies = [ 464 | "quote", 465 | "syn", 466 | ] 467 | 468 | [[package]] 469 | name = "surface-dial-daemon" 470 | version = "0.1.0" 471 | dependencies = [ 472 | "directories", 473 | "evdev-rs", 474 | "hidapi", 475 | "lazy_static", 476 | "nix", 477 | "notify-rust", 478 | "parking_lot", 479 | "signal-hook", 480 | "udev", 481 | ] 482 | 483 | [[package]] 484 | name = "syn" 485 | version = "0.11.11" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 488 | dependencies = [ 489 | "quote", 490 | "synom", 491 | "unicode-xid", 492 | ] 493 | 494 | [[package]] 495 | name = "synom" 496 | version = "0.11.3" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 499 | dependencies = [ 500 | "unicode-xid", 501 | ] 502 | 503 | [[package]] 504 | name = "time" 505 | version = "0.1.44" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 508 | dependencies = [ 509 | "libc", 510 | "wasi 0.10.0+wasi-snapshot-preview1", 511 | "winapi", 512 | ] 513 | 514 | [[package]] 515 | name = "udev" 516 | version = "0.5.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "048df778e99eea028c08cca7853b9b521df6948b59bb29ab8bb737c057f58e6d" 519 | dependencies = [ 520 | "libc", 521 | "libudev-sys", 522 | ] 523 | 524 | [[package]] 525 | name = "unicode-xid" 526 | version = "0.0.4" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 529 | 530 | [[package]] 531 | name = "wasi" 532 | version = "0.9.0+wasi-snapshot-preview1" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 535 | 536 | [[package]] 537 | name = "wasi" 538 | version = "0.10.0+wasi-snapshot-preview1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 541 | 542 | [[package]] 543 | name = "winapi" 544 | version = "0.3.9" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 547 | dependencies = [ 548 | "winapi-i686-pc-windows-gnu", 549 | "winapi-x86_64-pc-windows-gnu", 550 | ] 551 | 552 | [[package]] 553 | name = "winapi-i686-pc-windows-gnu" 554 | version = "0.4.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 557 | 558 | [[package]] 559 | name = "winapi-x86_64-pc-windows-gnu" 560 | version = "0.4.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 563 | 564 | [[package]] 565 | name = "winrt" 566 | version = "0.4.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d" 569 | dependencies = [ 570 | "winapi", 571 | ] 572 | 573 | [[package]] 574 | name = "winrt-notification" 575 | version = "0.2.2" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "6c31a65da50d792c6f9bd2e3216249566c4fb1d2d34f9b7d2d66d2e93f62a242" 578 | dependencies = [ 579 | "strum", 580 | "strum_macros", 581 | "winapi", 582 | "winrt", 583 | "xml-rs", 584 | ] 585 | 586 | [[package]] 587 | name = "xml-rs" 588 | version = "0.6.1" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621" 591 | dependencies = [ 592 | "bitflags 0.9.1", 593 | ] 594 | -------------------------------------------------------------------------------- /notes/descriptor.c: -------------------------------------------------------------------------------- 1 | // generated by https://github.com/abend0c1/hidrdd 2 | 3 | //-------------------------------------------------------------------------------- 4 | // Decoded Application Collection 5 | //-------------------------------------------------------------------------------- 6 | 7 | /* 8 | 05 01 (GLOBAL) USAGE_PAGE 0x0001 Generic Desktop Page 9 | 09 0E (LOCAL) USAGE 0x0001000E System Multi-axis Controller (Application Collection) 10 | A1 01 (MAIN) COLLECTION 0x01 Application (Usage=0x0001000E: Page=Generic Desktop Page, Usage=System Multi-axis Controller, Type=Application Collection) 11 | 85 01 (GLOBAL) REPORT_ID 0x01 (1) 12 | 05 0D (GLOBAL) USAGE_PAGE 0x000D Digitizer Device Page 13 | 09 21 (LOCAL) USAGE 0x000D0021 Puck (Logical Collection) 14 | A1 02 (MAIN) COLLECTION 0x02 Logical (Usage=0x000D0021: Page=Digitizer Device Page, Usage=Puck, Type=Logical Collection) 15 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 15 00 with 14 16 | 25 01 (GLOBAL) LOGICAL_MAXIMUM 0x01 (1) 17 | 75 01 (GLOBAL) REPORT_SIZE 0x01 (1) Number of bits per field 18 | 95 01 (GLOBAL) REPORT_COUNT 0x01 (1) Number of fields 19 | A1 00 (MAIN) COLLECTION 0x00 Physical (Usage=0x0: Page=, Usage=, Type=) <-- Error: COLLECTION must be preceded by a known USAGE <-- Warning: USAGE type should be CP (Physical Collection) 20 | 05 09 (GLOBAL) USAGE_PAGE 0x0009 Button Page 21 | 09 01 (LOCAL) USAGE 0x00090001 Button 1 Primary/trigger (Selector, On/Off Control, Momentary Control, or One Shot Control) 22 | 81 02 (MAIN) INPUT 0x00000002 (1 field x 1 bit) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 23 | 05 0D (GLOBAL) USAGE_PAGE 0x000D Digitizer Device Page 24 | 09 33 (LOCAL) USAGE 0x000D0033 Touch (Momentary Control) 25 | 81 02 (MAIN) INPUT 0x00000002 (1 field x 1 bit) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 26 | 95 06 (GLOBAL) REPORT_COUNT 0x06 (6) Number of fields 27 | 81 03 (MAIN) INPUT 0x00000003 (6 fields x 1 bit) 1=Constant 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 28 | A1 02 (MAIN) COLLECTION 0x02 Logical (Usage=0x0: Page=, Usage=, Type=) <-- Error: COLLECTION must be preceded by a known USAGE <-- Warning: USAGE type should be CL (Logical Collection) 29 | 05 01 (GLOBAL) USAGE_PAGE 0x0001 Generic Desktop Page 30 | 09 37 (LOCAL) USAGE 0x00010037 Dial (Dynamic Value) 31 | 16 0180 (GLOBAL) LOGICAL_MINIMUM 0x8001 (-32767) 32 | 26 FF7F (GLOBAL) LOGICAL_MAXIMUM 0x7FFF (32767) 33 | 75 10 (GLOBAL) REPORT_SIZE 0x10 (16) Number of bits per field 34 | 95 01 (GLOBAL) REPORT_COUNT 0x01 (1) Number of fields 35 | 81 06 (MAIN) INPUT 0x00000006 (1 field x 16 bits) 0=Data 1=Variable 1=Relative 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 36 | 35 00 (GLOBAL) PHYSICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 35 00 with 34 37 | 46 100E (GLOBAL) PHYSICAL_MAXIMUM 0x0E10 (3600) 38 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 15 00 with 14 39 | 26 100E (GLOBAL) LOGICAL_MAXIMUM 0x0E10 (3600) 40 | 09 48 (LOCAL) USAGE 0x00010048 Resolution Multiplier (Dynamic Value) 41 | B1 02 (MAIN) FEATURE 0x00000002 (1 field x 16 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 42 | 45 00 (GLOBAL) PHYSICAL_MAXIMUM 0x00 (0) <-- Info: Consider replacing 45 00 with 44 43 | C0 (MAIN) END_COLLECTION Logical 44 | 55 0E (GLOBAL) UNIT_EXPONENT 0x0E (Unit Value x 10⁻²) 45 | 65 11 (GLOBAL) UNIT 0x11 Distance in metres [1 cm units] (1=System=SI Linear, 1=Length=Centimetre) 46 | 46 0000 (GLOBAL) PHYSICAL_MAXIMUM 0x0000 (0) <-- Redundant: PHYSICAL_MAXIMUM is already 0 <-- Info: Consider replacing 46 0000 with 44 47 | 26 0000 (GLOBAL) LOGICAL_MAXIMUM 0x0000 (0) <-- Info: Consider replacing 26 0000 with 24 48 | 09 30 (LOCAL) USAGE 0x00010030 X (Dynamic Value) 49 | 81 42 (MAIN) INPUT 0x00000042 (1 field x 16 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 50 | 09 31 (LOCAL) USAGE 0x00010031 Y (Dynamic Value) 51 | 46 0000 (GLOBAL) PHYSICAL_MAXIMUM 0x0000 (0) <-- Redundant: PHYSICAL_MAXIMUM is already 0 <-- Info: Consider replacing 46 0000 with 44 52 | 26 0000 (GLOBAL) LOGICAL_MAXIMUM 0x0000 (0) <-- Redundant: LOGICAL_MAXIMUM is already 0 <-- Info: Consider replacing 26 0000 with 24 53 | 81 42 (MAIN) INPUT 0x00000042 (1 field x 16 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 54 | 05 0D (GLOBAL) USAGE_PAGE 0x000D Digitizer Device Page 55 | 09 48 (LOCAL) USAGE 0x000D0048 Width (Dynamic Value) 56 | 15 3A (GLOBAL) LOGICAL_MINIMUM 0x3A (58) 57 | 25 3A (GLOBAL) LOGICAL_MAXIMUM 0x3A (58) 58 | 75 08 (GLOBAL) REPORT_SIZE 0x08 (8) Number of bits per field 59 | 55 0F (GLOBAL) UNIT_EXPONENT 0x0F (Unit Value x 10⁻¹) 60 | 35 3A (GLOBAL) PHYSICAL_MINIMUM 0x3A (58) 61 | 45 3A (GLOBAL) PHYSICAL_MAXIMUM 0x3A (58) 62 | 81 03 (MAIN) INPUT 0x00000003 (1 field x 8 bits) 1=Constant 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 63 | 55 00 (GLOBAL) UNIT_EXPONENT 0x00 (Unit Value x 10⁰) <-- Info: Consider replacing 55 00 with 54 64 | 65 00 (GLOBAL) UNIT 0x00 No unit (0=None) <-- Info: Consider replacing 65 00 with 64 65 | 35 00 (GLOBAL) PHYSICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 35 00 with 34 66 | 45 00 (GLOBAL) PHYSICAL_MAXIMUM 0x00 (0) <-- Info: Consider replacing 45 00 with 44 67 | 05 0E (GLOBAL) USAGE_PAGE 0x000E Haptics Page 68 | 09 01 (LOCAL) USAGE 0x000E0001 Simple Haptic Controller (Application or Logical Collection) 69 | A1 02 (MAIN) COLLECTION 0x02 Logical (Usage=0x000E0001: Page=Haptics Page, Usage=Simple Haptic Controller, Type=Application or Logical Collection) 70 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 15 00 with 14 71 | 26 FF00 (GLOBAL) LOGICAL_MAXIMUM 0x00FF (255) 72 | 09 24 (LOCAL) USAGE 0x000E0024 Repeat Count (Dynamic Value) 73 | B1 42 (MAIN) FEATURE 0x00000042 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 74 | 09 24 (LOCAL) USAGE 0x000E0024 Repeat Count (Dynamic Value) 75 | 91 42 (MAIN) OUTPUT 0x00000042 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 76 | 15 01 (GLOBAL) LOGICAL_MINIMUM 0x01 (1) 77 | 25 07 (GLOBAL) LOGICAL_MAXIMUM 0x07 (7) 78 | 09 20 (LOCAL) USAGE 0x000E0020 Auto Trigger (Dynamic Value) 79 | B1 42 (MAIN) FEATURE 0x00000042 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 80 | 09 21 (LOCAL) USAGE 0x000E0021 Manual Trigger (Dynamic Value) 81 | 91 42 (MAIN) OUTPUT 0x00000042 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 82 | 25 0A (GLOBAL) LOGICAL_MAXIMUM 0x0A (10) 83 | 09 28 (LOCAL) USAGE 0x000E0028 Waveform Cutoff Time (Static Value) 84 | B1 42 (MAIN) FEATURE 0x00000042 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 85 | 75 10 (GLOBAL) REPORT_SIZE 0x10 (16) Number of bits per field 86 | 26 D007 (GLOBAL) LOGICAL_MAXIMUM 0x07D0 (2000) 87 | 09 25 (LOCAL) USAGE 0x000E0025 Retrigger Period (Dynamic Value) 88 | B1 42 (MAIN) FEATURE 0x00000042 (1 field x 16 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 89 | 09 25 (LOCAL) USAGE 0x000E0025 Retrigger Period (Dynamic Value) 90 | 91 42 (MAIN) OUTPUT 0x00000042 (1 field x 16 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 1=Null 0=NonVolatile 0=Bitmap 91 | 85 02 (GLOBAL) REPORT_ID 0x02 (2) 92 | 75 20 (GLOBAL) REPORT_SIZE 0x20 (32) Number of bits per field 93 | 17 37000100 (GLOBAL) LOGICAL_MINIMUM 0x00010037 (65591) 94 | 27 37000100 (GLOBAL) LOGICAL_MAXIMUM 0x00010037 (65591) 95 | 09 22 (LOCAL) USAGE 0x000E0022 Auto Trigger Associated Control (Static Value) 96 | B1 02 (MAIN) FEATURE 0x00000002 (1 field x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 97 | 09 11 (LOCAL) USAGE 0x000E0011 Duration List (Named Array Collection) 98 | A1 02 (MAIN) COLLECTION 0x02 Logical (Usage=0x000E0011: Page=Haptics Page, Usage=Duration List, Type=Named Array Collection) <-- Warning: USAGE type should be CL (Logical Collection) 99 | 05 0A (GLOBAL) USAGE_PAGE 0x000A Ordinal Page 100 | 95 03 (GLOBAL) REPORT_COUNT 0x03 (3) Number of fields 101 | 09 03 (LOCAL) USAGE 0x000A0003 Instance 3 (Usage Modifier Collection) 102 | 09 04 (LOCAL) USAGE 0x000A0004 Instance 4 (Usage Modifier Collection) 103 | 09 05 (LOCAL) USAGE 0x000A0005 Instance 5 (Usage Modifier Collection) 104 | 75 08 (GLOBAL) REPORT_SIZE 0x08 (8) Number of bits per field 105 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 15 00 with 14 106 | 25 FF (GLOBAL) LOGICAL_MAXIMUM 0xFF (-1) 107 | B1 02 (MAIN) FEATURE 0x00000002 (3 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 108 | C0 (MAIN) END_COLLECTION Logical 109 | 05 0E (GLOBAL) USAGE_PAGE 0x000E Haptics Page 110 | 09 10 (LOCAL) USAGE 0x000E0010 Waveform List (Named Array Collection) 111 | A1 02 (MAIN) COLLECTION 0x02 Logical (Usage=0x000E0010: Page=Haptics Page, Usage=Waveform List, Type=Named Array Collection) <-- Warning: USAGE type should be CL (Logical Collection) 112 | 05 0A (GLOBAL) USAGE_PAGE 0x000A Ordinal Page 113 | 95 01 (GLOBAL) REPORT_COUNT 0x01 (1) Number of fields 114 | 15 03 (GLOBAL) LOGICAL_MINIMUM 0x03 (3) 115 | 25 03 (GLOBAL) LOGICAL_MAXIMUM 0x03 (3) 116 | 36 0310 (GLOBAL) PHYSICAL_MINIMUM 0x1003 (4099) 117 | 46 0310 (GLOBAL) PHYSICAL_MAXIMUM 0x1003 (4099) 118 | 09 03 (LOCAL) USAGE 0x000A0003 Instance 3 (Usage Modifier Collection) 119 | B1 02 (MAIN) FEATURE 0x00000002 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 120 | 15 04 (GLOBAL) LOGICAL_MINIMUM 0x04 (4) 121 | 25 04 (GLOBAL) LOGICAL_MAXIMUM 0x04 (4) 122 | 36 0410 (GLOBAL) PHYSICAL_MINIMUM 0x1004 (4100) 123 | 46 0410 (GLOBAL) PHYSICAL_MAXIMUM 0x1004 (4100) 124 | 09 04 (LOCAL) USAGE 0x000A0004 Instance 4 (Usage Modifier Collection) 125 | B1 02 (MAIN) FEATURE 0x00000002 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 126 | 15 05 (GLOBAL) LOGICAL_MINIMUM 0x05 (5) 127 | 25 05 (GLOBAL) LOGICAL_MAXIMUM 0x05 (5) 128 | 36 0410 (GLOBAL) PHYSICAL_MINIMUM 0x1004 (4100) <-- Redundant: PHYSICAL_MINIMUM is already 4100 129 | 46 0410 (GLOBAL) PHYSICAL_MAXIMUM 0x1004 (4100) <-- Redundant: PHYSICAL_MAXIMUM is already 4100 130 | 09 05 (LOCAL) USAGE 0x000A0005 Instance 5 (Usage Modifier Collection) 131 | B1 02 (MAIN) FEATURE 0x00000002 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 132 | 35 00 (GLOBAL) PHYSICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 35 00 with 34 133 | 45 00 (GLOBAL) PHYSICAL_MAXIMUM 0x00 (0) <-- Info: Consider replacing 45 00 with 44 134 | C0 (MAIN) END_COLLECTION Logical 135 | C0 (MAIN) END_COLLECTION Logical 136 | C0 (MAIN) END_COLLECTION Physical 137 | C0 (MAIN) END_COLLECTION Logical 138 | C0 (MAIN) END_COLLECTION Application 139 | */ 140 | 141 | //-------------------------------------------------------------------------------- 142 | // Generic Desktop Page featureReport 01 (Device <-> Host) 143 | //-------------------------------------------------------------------------------- 144 | 145 | typedef struct 146 | { 147 | uint8_t reportId; // Report ID = 0x01 (1) 148 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL: 149 | uint16_t GD_SystemMultiaxisControllerPuckResolutionMultiplier; // Usage 0x00010048: Resolution Multiplier, Value = 0 to 3600, Physical = Value 150 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL:SimpleHapticController 151 | uint8_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerRepeatCount; // Usage 0x000E0024: Repeat Count, Value = 0 to 255 152 | uint8_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerAutoTrigger; // Usage 0x000E0020: Auto Trigger, Value = 1 to 7 153 | uint8_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerWaveformCutoffTime; // Usage 0x000E0028: Waveform Cutoff Time, Value = 1 to 10 154 | uint16_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerRetriggerPeriod; // Usage 0x000E0025: Retrigger Period, Value = 1 to 2000 155 | } featureReport01_t; 156 | 157 | 158 | //-------------------------------------------------------------------------------- 159 | // Haptics Page featureReport 02 (Device <-> Host) 160 | //-------------------------------------------------------------------------------- 161 | 162 | typedef struct 163 | { 164 | uint8_t reportId; // Report ID = 0x02 (2) 165 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL:SimpleHapticController 166 | uint32_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerAutoTriggerAssociatedControl; // Usage 0x000E0022: Auto Trigger Associated Control, Value = 65591 to 65591 167 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL:SimpleHapticController CL:DurationList 168 | uint8_t ORD_SystemMultiaxisControllerPuckSimpleHapticControllerDurationListInstance3; // Usage 0x000A0003: Instance 3, Value = 0 to -1 169 | uint8_t ORD_SystemMultiaxisControllerPuckSimpleHapticControllerDurationListInstance4; // Usage 0x000A0004: Instance 4, Value = 0 to -1 170 | uint8_t ORD_SystemMultiaxisControllerPuckSimpleHapticControllerDurationListInstance5; // Usage 0x000A0005: Instance 5, Value = 0 to -1 171 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL:SimpleHapticController CL:WaveformList 172 | uint8_t ORD_SystemMultiaxisControllerPuckSimpleHapticControllerWaveformListInstance3; // Usage 0x000A0003: Instance 3, Value = 3 to 3, Physical = ((Value - 3) x 0 / 0 + 4099) 173 | uint8_t ORD_SystemMultiaxisControllerPuckSimpleHapticControllerWaveformListInstance4; // Usage 0x000A0004: Instance 4, Value = 4 to 4, Physical = ((Value - 4) x 0 / 0 + 4100) 174 | uint8_t ORD_SystemMultiaxisControllerPuckSimpleHapticControllerWaveformListInstance5; // Usage 0x000A0005: Instance 5, Value = 5 to 5, Physical = ((Value - 5) x 0 / 0 + 4100) 175 | } featureReport02_t; 176 | 177 | 178 | //-------------------------------------------------------------------------------- 179 | // Button Page inputReport 01 (Device --> Host) 180 | //-------------------------------------------------------------------------------- 181 | 182 | typedef struct 183 | { 184 | uint8_t reportId; // Report ID = 0x01 (1) 185 | // Collection: CA:SystemMulti-axisController CL:Puck CP: 186 | uint8_t BTN_SystemMultiaxisControllerPuckButton1 : 1; // Usage 0x00090001: Button 1 Primary/trigger, Value = 0 to 1 187 | uint8_t DIG_SystemMultiaxisControllerPuckTouch : 1; // Usage 0x000D0033: Touch, Value = 0 to 1 188 | uint8_t : 1; // Pad 189 | uint8_t : 1; // Pad 190 | uint8_t : 1; // Pad 191 | uint8_t : 1; // Pad 192 | uint8_t : 1; // Pad 193 | uint8_t : 1; // Pad 194 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL: 195 | int16_t GD_SystemMultiaxisControllerPuckDial; // Usage 0x00010037: Dial, Value = -32767 to 32767 196 | // Collection: CA:SystemMulti-axisController CL:Puck CP: 197 | uint16_t GD_SystemMultiaxisControllerPuckX; // Usage 0x00010030: X, Value = 0 to 0, Physical = Value x 0 / 0 in 10⁻⁴ m units 198 | uint16_t GD_SystemMultiaxisControllerPuckY; // Usage 0x00010031: Y, Value = 0 to 0, Physical = Value x 0 / 0 in 10⁻⁴ m units 199 | uint8_t DIG_SystemMultiaxisControllerPuckWidth; // Usage 0x000D0048: Width, Value = 58 to 58, Physical = ((Value - 58) x 0 / 0 + 58) in 10⁻³ m units 200 | } inputReport01_t; 201 | 202 | 203 | //-------------------------------------------------------------------------------- 204 | // Haptics Page outputReport 01 (Device <-- Host) 205 | //-------------------------------------------------------------------------------- 206 | 207 | typedef struct 208 | { 209 | uint8_t reportId; // Report ID = 0x01 (1) 210 | // Collection: CA:SystemMulti-axisController CL:Puck CP: CL:SimpleHapticController 211 | uint8_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerRepeatCount; // Usage 0x000E0024: Repeat Count, Value = 0 to 255 212 | uint8_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerManualTrigger; // Usage 0x000E0021: Manual Trigger, Value = 1 to 7 213 | uint16_t HAP_SystemMultiaxisControllerPuckSimpleHapticControllerRetriggerPeriod; // Usage 0x000E0025: Retrigger Period, Value = 1 to 2000 214 | } outputReport01_t; 215 | 216 | 217 | //-------------------------------------------------------------------------------- 218 | // Decoded Application Collection 219 | //-------------------------------------------------------------------------------- 220 | 221 | /* 222 | 06 07FF (GLOBAL) USAGE_PAGE 0xFF07 Vendor-defined 223 | 09 70 (LOCAL) USAGE 0xFF070070 <-- Warning: Undocumented usage (document it by inserting 0070 into file FF07.conf) <-- Error: Usage Modifier COLLECTION item (A1 06) expected for USAGE 0x000A0005 Instance 5 (Usage Modifier Collection) 224 | A1 01 (MAIN) COLLECTION 0x01 Application (Usage=0xFF070070: Page=Vendor-defined, Usage=, Type=) <-- Error: COLLECTION must be preceded by a known USAGE 225 | 85 30 (GLOBAL) REPORT_ID 0x30 (48) '0' 226 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 15 00 with 14 227 | 25 FF (GLOBAL) LOGICAL_MAXIMUM 0xFF (-1) 228 | 95 01 (GLOBAL) REPORT_COUNT 0x01 (1) Number of fields <-- Redundant: REPORT_COUNT is already 1 229 | 75 08 (GLOBAL) REPORT_SIZE 0x08 (8) Number of bits per field <-- Redundant: REPORT_SIZE is already 8 230 | 09 00 (LOCAL) USAGE 0xFF070000 <-- Info: Consider replacing 09 00 with 08 231 | 91 02 (MAIN) OUTPUT 0x00000002 (1 field x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 232 | C0 (MAIN) END_COLLECTION Application 233 | */ 234 | 235 | //-------------------------------------------------------------------------------- 236 | // Vendor-defined outputReport 30 (Device <-- Host) 237 | //-------------------------------------------------------------------------------- 238 | 239 | typedef struct 240 | { 241 | uint8_t reportId; // Report ID = 0x30 (48) '0' 242 | // Collection: CA: 243 | uint8_t VEN_0000; // Usage 0xFF070000: , Value = 0 to -1 244 | } outputReport30_t; 245 | 246 | 247 | //-------------------------------------------------------------------------------- 248 | // Decoded Application Collection 249 | //-------------------------------------------------------------------------------- 250 | 251 | /* 252 | 09 71 (LOCAL) USAGE 0xFF070071 <-- Warning: Undocumented usage (document it by inserting 0071 into file FF07.conf) 253 | A1 01 (MAIN) COLLECTION 0x01 Application (Usage=0xFF070071: Page=Vendor-defined, Usage=, Type=) <-- Error: COLLECTION must be preceded by a known USAGE 254 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Redundant: LOGICAL_MINIMUM is already 0 <-- Info: Consider replacing 15 00 with 14 255 | 25 FF (GLOBAL) LOGICAL_MAXIMUM 0xFF (-1) <-- Redundant: LOGICAL_MAXIMUM is already -1 256 | 75 08 (GLOBAL) REPORT_SIZE 0x08 (8) Number of bits per field <-- Redundant: REPORT_SIZE is already 8 257 | 95 48 (GLOBAL) REPORT_COUNT 0x48 (72) Number of fields 258 | 85 2A (GLOBAL) REPORT_ID 0x2A (42) 259 | 09 C6 (LOCAL) USAGE 0xFF0700C6 <-- Warning: Undocumented usage (document it by inserting 00C6 into file FF07.conf) 260 | 82 0201 (MAIN) INPUT 0x00000102 (72 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 1=Buffer <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 261 | 09 C7 (LOCAL) USAGE 0xFF0700C7 <-- Warning: Undocumented usage (document it by inserting 00C7 into file FF07.conf) 262 | 92 0201 (MAIN) OUTPUT 0x00000102 (72 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 1=Buffer <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 263 | 95 34 (GLOBAL) REPORT_COUNT 0x34 (52) Number of fields 264 | 09 C8 (LOCAL) USAGE 0xFF0700C8 <-- Warning: Undocumented usage (document it by inserting 00C8 into file FF07.conf) 265 | B2 0301 (MAIN) FEATURE 0x00000103 (52 fields x 8 bits) 1=Constant 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 1=Buffer 266 | 85 2B (GLOBAL) REPORT_ID 0x2B (43) 267 | 09 C9 (LOCAL) USAGE 0xFF0700C9 <-- Warning: Undocumented usage (document it by inserting 00C9 into file FF07.conf) 268 | 82 0201 (MAIN) INPUT 0x00000102 (52 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 1=Buffer <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 269 | 09 CA (LOCAL) USAGE 0xFF0700CA <-- Warning: Undocumented usage (document it by inserting 00CA into file FF07.conf) 270 | 92 0201 (MAIN) OUTPUT 0x00000102 (52 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 1=Buffer <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 271 | 09 CB (LOCAL) USAGE 0xFF0700CB <-- Warning: Undocumented usage (document it by inserting 00CB into file FF07.conf) 272 | B2 0201 (MAIN) FEATURE 0x00000102 (52 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 1=Buffer <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 273 | 17 00000080 (GLOBAL) LOGICAL_MINIMUM 0x80000000 (-2147483648) 274 | 27 FFFFFF7F (GLOBAL) LOGICAL_MAXIMUM 0x7FFFFFFF (2147483647) 275 | 75 20 (GLOBAL) REPORT_SIZE 0x20 (32) Number of bits per field 276 | 95 04 (GLOBAL) REPORT_COUNT 0x04 (4) Number of fields 277 | 85 2C (GLOBAL) REPORT_ID 0x2C (44) 278 | 19 CC (LOCAL) USAGE_MINIMUM 0xFF0700CC <-- Warning: Undocumented usage (document it by inserting 00CC into file FF07.conf) 279 | 29 CF (LOCAL) USAGE_MAXIMUM 0xFF0700CF <-- Warning: Undocumented usage (document it by inserting 00CF into file FF07.conf) 280 | 81 02 (MAIN) INPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 281 | 95 04 (GLOBAL) REPORT_COUNT 0x04 (4) Number of fields <-- Redundant: REPORT_COUNT is already 4 282 | 85 2D (GLOBAL) REPORT_ID 0x2D (45) 283 | 19 D8 (LOCAL) USAGE_MINIMUM 0xFF0700D8 <-- Warning: Undocumented usage (document it by inserting 00D8 into file FF07.conf) 284 | 29 DB (LOCAL) USAGE_MAXIMUM 0xFF0700DB <-- Warning: Undocumented usage (document it by inserting 00DB into file FF07.conf) 285 | 81 02 (MAIN) INPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 286 | 95 04 (GLOBAL) REPORT_COUNT 0x04 (4) Number of fields <-- Redundant: REPORT_COUNT is already 4 287 | 19 DC (LOCAL) USAGE_MINIMUM 0xFF0700DC <-- Warning: Undocumented usage (document it by inserting 00DC into file FF07.conf) 288 | 29 DF (LOCAL) USAGE_MAXIMUM 0xFF0700DF <-- Warning: Undocumented usage (document it by inserting 00DF into file FF07.conf) 289 | 91 02 (MAIN) OUTPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 290 | 19 E0 (LOCAL) USAGE_MINIMUM 0xFF0700E0 <-- Warning: Undocumented usage (document it by inserting 00E0 into file FF07.conf) 291 | 29 E3 (LOCAL) USAGE_MAXIMUM 0xFF0700E3 <-- Warning: Undocumented usage (document it by inserting 00E3 into file FF07.conf) 292 | B1 02 (MAIN) FEATURE 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 293 | 85 2E (GLOBAL) REPORT_ID 0x2E (46) 294 | 19 E4 (LOCAL) USAGE_MINIMUM 0xFF0700E4 <-- Warning: Undocumented usage (document it by inserting 00E4 into file FF07.conf) 295 | 29 E7 (LOCAL) USAGE_MAXIMUM 0xFF0700E7 <-- Warning: Undocumented usage (document it by inserting 00E7 into file FF07.conf) 296 | 81 02 (MAIN) INPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 297 | 19 E8 (LOCAL) USAGE_MINIMUM 0xFF0700E8 <-- Warning: Undocumented usage (document it by inserting 00E8 into file FF07.conf) 298 | 29 EB (LOCAL) USAGE_MAXIMUM 0xFF0700EB <-- Warning: Undocumented usage (document it by inserting 00EB into file FF07.conf) 299 | 91 02 (MAIN) OUTPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 300 | 95 0B (GLOBAL) REPORT_COUNT 0x0B (11) Number of fields 301 | 19 EC (LOCAL) USAGE_MINIMUM 0xFF0700EC <-- Warning: Undocumented usage (document it by inserting 00EC into file FF07.conf) 302 | 29 EF (LOCAL) USAGE_MAXIMUM 0xFF0700EF <-- Warning: Undocumented usage (document it by inserting 00EF into file FF07.conf) 303 | B1 02 (MAIN) FEATURE 0x00000002 (11 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 304 | 95 04 (GLOBAL) REPORT_COUNT 0x04 (4) Number of fields 305 | 85 2F (GLOBAL) REPORT_ID 0x2F (47) 306 | 19 F0 (LOCAL) USAGE_MINIMUM 0xFF0700F0 <-- Warning: Undocumented usage (document it by inserting 00F0 into file FF07.conf) 307 | 29 F3 (LOCAL) USAGE_MAXIMUM 0xFF0700F3 <-- Warning: Undocumented usage (document it by inserting 00F3 into file FF07.conf) 308 | 81 02 (MAIN) INPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 309 | 19 F4 (LOCAL) USAGE_MINIMUM 0xFF0700F4 <-- Warning: Undocumented usage (document it by inserting 00F4 into file FF07.conf) 310 | 29 F7 (LOCAL) USAGE_MAXIMUM 0xFF0700F7 <-- Warning: Undocumented usage (document it by inserting 00F7 into file FF07.conf) 311 | 91 02 (MAIN) OUTPUT 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 312 | 19 F8 (LOCAL) USAGE_MINIMUM 0xFF0700F8 <-- Warning: Undocumented usage (document it by inserting 00F8 into file FF07.conf) 313 | 29 FB (LOCAL) USAGE_MAXIMUM 0xFF0700FB <-- Warning: Undocumented usage (document it by inserting 00FB into file FF07.conf) 314 | B1 02 (MAIN) FEATURE 0x00000002 (4 fields x 32 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 315 | C0 (MAIN) END_COLLECTION Application 316 | */ 317 | 318 | //-------------------------------------------------------------------------------- 319 | // Vendor-defined featureReport 2A (Device <-> Host) 320 | //-------------------------------------------------------------------------------- 321 | 322 | typedef struct 323 | { 324 | uint8_t reportId; // Report ID = 0x2A (42) 325 | // Collection: CA: 326 | uint8_t VEN_00C8[52]; // Usage 0xFF0700C8: , Value = 0 to -1 327 | } featureReport2A_t; 328 | 329 | 330 | //-------------------------------------------------------------------------------- 331 | // Vendor-defined featureReport 2B (Device <-> Host) 332 | //-------------------------------------------------------------------------------- 333 | 334 | typedef struct 335 | { 336 | uint8_t reportId; // Report ID = 0x2B (43) 337 | // Collection: CA: 338 | uint8_t VEN_00CB[52]; // Usage 0xFF0700CB: , Value = 0 to -1 339 | } featureReport2B_t; 340 | 341 | 342 | //-------------------------------------------------------------------------------- 343 | // Vendor-defined featureReport 2D (Device <-> Host) 344 | //-------------------------------------------------------------------------------- 345 | 346 | typedef struct 347 | { 348 | uint8_t reportId; // Report ID = 0x2D (45) 349 | // Collection: CA: 350 | int32_t VEN_00E0; // Usage 0xFF0700E0: , Value = -2147483648 to 2147483647 351 | int32_t VEN_00E1; // Usage 0xFF0700E1: , Value = -2147483648 to 2147483647 352 | int32_t VEN_00E2; // Usage 0xFF0700E2: , Value = -2147483648 to 2147483647 353 | int32_t VEN_00E3; // Usage 0xFF0700E3: , Value = -2147483648 to 2147483647 354 | } featureReport2D_t; 355 | 356 | 357 | //-------------------------------------------------------------------------------- 358 | // Vendor-defined featureReport 2E (Device <-> Host) 359 | //-------------------------------------------------------------------------------- 360 | 361 | typedef struct 362 | { 363 | uint8_t reportId; // Report ID = 0x2E (46) 364 | // Collection: CA: 365 | int32_t VEN_00EC; // Usage 0xFF0700EC: , Value = -2147483648 to 2147483647 366 | int32_t VEN_00ED; // Usage 0xFF0700ED: , Value = -2147483648 to 2147483647 367 | int32_t VEN_00EE; // Usage 0xFF0700EE: , Value = -2147483648 to 2147483647 368 | int32_t VEN_00EF[8]; // Usage 0xFF0700EF: , Value = -2147483648 to 2147483647 369 | } featureReport2E_t; 370 | 371 | 372 | //-------------------------------------------------------------------------------- 373 | // Vendor-defined featureReport 2F (Device <-> Host) 374 | //-------------------------------------------------------------------------------- 375 | 376 | typedef struct 377 | { 378 | uint8_t reportId; // Report ID = 0x2F (47) 379 | // Collection: CA: 380 | int32_t VEN_00F8; // Usage 0xFF0700F8: , Value = -2147483648 to 2147483647 381 | int32_t VEN_00F9; // Usage 0xFF0700F9: , Value = -2147483648 to 2147483647 382 | int32_t VEN_00FA; // Usage 0xFF0700FA: , Value = -2147483648 to 2147483647 383 | int32_t VEN_00FB; // Usage 0xFF0700FB: , Value = -2147483648 to 2147483647 384 | } featureReport2F_t; 385 | 386 | 387 | //-------------------------------------------------------------------------------- 388 | // Vendor-defined inputReport 2A (Device --> Host) 389 | //-------------------------------------------------------------------------------- 390 | 391 | typedef struct 392 | { 393 | uint8_t reportId; // Report ID = 0x2A (42) 394 | // Collection: CA: 395 | uint8_t VEN_00C6[72]; // Usage 0xFF0700C6: , Value = 0 to -1 396 | } inputReport2A_t; 397 | 398 | 399 | //-------------------------------------------------------------------------------- 400 | // Vendor-defined inputReport 2B (Device --> Host) 401 | //-------------------------------------------------------------------------------- 402 | 403 | typedef struct 404 | { 405 | uint8_t reportId; // Report ID = 0x2B (43) 406 | // Collection: CA: 407 | uint8_t VEN_00C9[52]; // Usage 0xFF0700C9: , Value = 0 to -1 408 | } inputReport2B_t; 409 | 410 | 411 | //-------------------------------------------------------------------------------- 412 | // Vendor-defined inputReport 2C (Device --> Host) 413 | //-------------------------------------------------------------------------------- 414 | 415 | typedef struct 416 | { 417 | uint8_t reportId; // Report ID = 0x2C (44) 418 | // Collection: CA: 419 | int32_t VEN_00CC; // Usage 0xFF0700CC: , Value = -2147483648 to 2147483647 420 | int32_t VEN_00CD; // Usage 0xFF0700CD: , Value = -2147483648 to 2147483647 421 | int32_t VEN_00CE; // Usage 0xFF0700CE: , Value = -2147483648 to 2147483647 422 | int32_t VEN_00CF; // Usage 0xFF0700CF: , Value = -2147483648 to 2147483647 423 | } inputReport2C_t; 424 | 425 | 426 | //-------------------------------------------------------------------------------- 427 | // Vendor-defined inputReport 2D (Device --> Host) 428 | //-------------------------------------------------------------------------------- 429 | 430 | typedef struct 431 | { 432 | uint8_t reportId; // Report ID = 0x2D (45) 433 | // Collection: CA: 434 | int32_t VEN_00D8; // Usage 0xFF0700D8: , Value = -2147483648 to 2147483647 435 | int32_t VEN_00D9; // Usage 0xFF0700D9: , Value = -2147483648 to 2147483647 436 | int32_t VEN_00DA; // Usage 0xFF0700DA: , Value = -2147483648 to 2147483647 437 | int32_t VEN_00DB; // Usage 0xFF0700DB: , Value = -2147483648 to 2147483647 438 | } inputReport2D_t; 439 | 440 | 441 | //-------------------------------------------------------------------------------- 442 | // Vendor-defined inputReport 2E (Device --> Host) 443 | //-------------------------------------------------------------------------------- 444 | 445 | typedef struct 446 | { 447 | uint8_t reportId; // Report ID = 0x2E (46) 448 | // Collection: CA: 449 | int32_t VEN_00E4; // Usage 0xFF0700E4: , Value = -2147483648 to 2147483647 450 | int32_t VEN_00E5; // Usage 0xFF0700E5: , Value = -2147483648 to 2147483647 451 | int32_t VEN_00E6; // Usage 0xFF0700E6: , Value = -2147483648 to 2147483647 452 | int32_t VEN_00E7; // Usage 0xFF0700E7: , Value = -2147483648 to 2147483647 453 | } inputReport2E_t; 454 | 455 | 456 | //-------------------------------------------------------------------------------- 457 | // Vendor-defined inputReport 2F (Device --> Host) 458 | //-------------------------------------------------------------------------------- 459 | 460 | typedef struct 461 | { 462 | uint8_t reportId; // Report ID = 0x2F (47) 463 | // Collection: CA: 464 | int32_t VEN_00F0; // Usage 0xFF0700F0: , Value = -2147483648 to 2147483647 465 | int32_t VEN_00F1; // Usage 0xFF0700F1: , Value = -2147483648 to 2147483647 466 | int32_t VEN_00F2; // Usage 0xFF0700F2: , Value = -2147483648 to 2147483647 467 | int32_t VEN_00F3; // Usage 0xFF0700F3: , Value = -2147483648 to 2147483647 468 | } inputReport2F_t; 469 | 470 | 471 | //-------------------------------------------------------------------------------- 472 | // Vendor-defined outputReport 2A (Device <-- Host) 473 | //-------------------------------------------------------------------------------- 474 | 475 | typedef struct 476 | { 477 | uint8_t reportId; // Report ID = 0x2A (42) 478 | // Collection: CA: 479 | uint8_t VEN_00C7[72]; // Usage 0xFF0700C7: , Value = 0 to -1 480 | } outputReport2A_t; 481 | 482 | 483 | //-------------------------------------------------------------------------------- 484 | // Vendor-defined outputReport 2B (Device <-- Host) 485 | //-------------------------------------------------------------------------------- 486 | 487 | typedef struct 488 | { 489 | uint8_t reportId; // Report ID = 0x2B (43) 490 | // Collection: CA: 491 | uint8_t VEN_00CA[52]; // Usage 0xFF0700CA: , Value = 0 to -1 492 | } outputReport2B_t; 493 | 494 | 495 | //-------------------------------------------------------------------------------- 496 | // Vendor-defined outputReport 2D (Device <-- Host) 497 | //-------------------------------------------------------------------------------- 498 | 499 | typedef struct 500 | { 501 | uint8_t reportId; // Report ID = 0x2D (45) 502 | // Collection: CA: 503 | int32_t VEN_00DC; // Usage 0xFF0700DC: , Value = -2147483648 to 2147483647 504 | int32_t VEN_00DD; // Usage 0xFF0700DD: , Value = -2147483648 to 2147483647 505 | int32_t VEN_00DE; // Usage 0xFF0700DE: , Value = -2147483648 to 2147483647 506 | int32_t VEN_00DF; // Usage 0xFF0700DF: , Value = -2147483648 to 2147483647 507 | } outputReport2D_t; 508 | 509 | 510 | //-------------------------------------------------------------------------------- 511 | // Vendor-defined outputReport 2E (Device <-- Host) 512 | //-------------------------------------------------------------------------------- 513 | 514 | typedef struct 515 | { 516 | uint8_t reportId; // Report ID = 0x2E (46) 517 | // Collection: CA: 518 | int32_t VEN_00E8; // Usage 0xFF0700E8: , Value = -2147483648 to 2147483647 519 | int32_t VEN_00E9; // Usage 0xFF0700E9: , Value = -2147483648 to 2147483647 520 | int32_t VEN_00EA; // Usage 0xFF0700EA: , Value = -2147483648 to 2147483647 521 | int32_t VEN_00EB; // Usage 0xFF0700EB: , Value = -2147483648 to 2147483647 522 | } outputReport2E_t; 523 | 524 | 525 | //-------------------------------------------------------------------------------- 526 | // Vendor-defined outputReport 2F (Device <-- Host) 527 | //-------------------------------------------------------------------------------- 528 | 529 | typedef struct 530 | { 531 | uint8_t reportId; // Report ID = 0x2F (47) 532 | // Collection: CA: 533 | int32_t VEN_00F4; // Usage 0xFF0700F4: , Value = -2147483648 to 2147483647 534 | int32_t VEN_00F5; // Usage 0xFF0700F5: , Value = -2147483648 to 2147483647 535 | int32_t VEN_00F6; // Usage 0xFF0700F6: , Value = -2147483648 to 2147483647 536 | int32_t VEN_00F7; // Usage 0xFF0700F7: , Value = -2147483648 to 2147483647 537 | } outputReport2F_t; 538 | 539 | 540 | //-------------------------------------------------------------------------------- 541 | // Decoded Application Collection 542 | //-------------------------------------------------------------------------------- 543 | 544 | /* 545 | 05 01 (GLOBAL) USAGE_PAGE 0x0001 Generic Desktop Page 546 | 09 80 (LOCAL) USAGE 0x00010080 System Control (Application Collection) 547 | A1 01 (MAIN) COLLECTION 0x01 Application (Usage=0x00010080: Page=Generic Desktop Page, Usage=System Control, Type=Application Collection) 548 | 85 32 (GLOBAL) REPORT_ID 0x32 (50) '2' 549 | 09 82 (LOCAL) USAGE 0x00010082 System Sleep (One Shot Control) 550 | 09 83 (LOCAL) USAGE 0x00010083 System Wake Up (One Shot Control) 551 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Info: Consider replacing 15 00 with 14 552 | 25 01 (GLOBAL) LOGICAL_MAXIMUM 0x01 (1) 553 | 95 02 (GLOBAL) REPORT_COUNT 0x02 (2) Number of fields 554 | 75 01 (GLOBAL) REPORT_SIZE 0x01 (1) Number of bits per field 555 | 81 02 (MAIN) INPUT 0x00000002 (2 fields x 1 bit) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 556 | 95 06 (GLOBAL) REPORT_COUNT 0x06 (6) Number of fields 557 | 81 03 (MAIN) INPUT 0x00000003 (6 fields x 1 bit) 1=Constant 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap 558 | C0 (MAIN) END_COLLECTION Application 559 | */ 560 | 561 | //-------------------------------------------------------------------------------- 562 | // Generic Desktop Page inputReport 32 (Device --> Host) 563 | //-------------------------------------------------------------------------------- 564 | 565 | typedef struct 566 | { 567 | uint8_t reportId; // Report ID = 0x32 (50) '2' 568 | // Collection: CA:SystemControl 569 | uint8_t GD_SystemControlSystemSleep : 1; // Usage 0x00010082: System Sleep, Value = 0 to 1 570 | uint8_t GD_SystemControlSystemWakeUp : 1; // Usage 0x00010083: System Wake Up, Value = 0 to 1 571 | uint8_t : 1; // Pad 572 | uint8_t : 1; // Pad 573 | uint8_t : 1; // Pad 574 | uint8_t : 1; // Pad 575 | uint8_t : 1; // Pad 576 | uint8_t : 1; // Pad 577 | } inputReport32_t; 578 | 579 | 580 | //-------------------------------------------------------------------------------- 581 | // Decoded Application Collection 582 | //-------------------------------------------------------------------------------- 583 | 584 | /* 585 | 09 72 (LOCAL) USAGE 0x00010072 <-- Warning: Undocumented usage (document it by inserting 0072 into file 0001.conf) 586 | A1 01 (MAIN) COLLECTION 0x01 Application (Usage=0x00010072: Page=Generic Desktop Page, Usage=, Type=) <-- Error: COLLECTION must be preceded by a known USAGE <-- Warning: USAGE type should be CA (Application Collection) 587 | 85 31 (GLOBAL) REPORT_ID 0x31 (49) '1' 588 | 95 0A (GLOBAL) REPORT_COUNT 0x0A (10) Number of fields 589 | 75 08 (GLOBAL) REPORT_SIZE 0x08 (8) Number of bits per field 590 | 15 00 (GLOBAL) LOGICAL_MINIMUM 0x00 (0) <-- Redundant: LOGICAL_MINIMUM is already 0 <-- Info: Consider replacing 15 00 with 14 591 | 25 FF (GLOBAL) LOGICAL_MAXIMUM 0xFF (-1) 592 | 09 C6 (LOCAL) USAGE 0x000100C6 Wireless Radio Button (On/Off Control) 593 | 81 02 (MAIN) INPUT 0x00000002 (10 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 594 | 09 C7 (LOCAL) USAGE 0x000100C7 Wireless Radio LED (On/Off Control) 595 | 91 02 (MAIN) OUTPUT 0x00000002 (10 fields x 8 bits) 0=Data 1=Variable 0=Absolute 0=NoWrap 0=Linear 0=PrefState 0=NoNull 0=NonVolatile 0=Bitmap <-- Error: LOGICAL_MAXIMUM (-1) is less than LOGICAL_MINIMUM (0) 596 | C0 (MAIN) END_COLLECTION Application 597 | */ 598 | 599 | //-------------------------------------------------------------------------------- 600 | // Generic Desktop Page inputReport 31 (Device --> Host) 601 | //-------------------------------------------------------------------------------- 602 | 603 | typedef struct 604 | { 605 | uint8_t reportId; // Report ID = 0x31 (49) '1' 606 | // Collection: CA: 607 | uint8_t GD_WirelessRadioButton[10]; // Usage 0x000100C6: Wireless Radio Button, Value = 0 to -1 608 | } inputReport31_t; 609 | 610 | 611 | //-------------------------------------------------------------------------------- 612 | // Generic Desktop Page outputReport 31 (Device <-- Host) 613 | //-------------------------------------------------------------------------------- 614 | 615 | typedef struct 616 | { 617 | uint8_t reportId; // Report ID = 0x31 (49) '1' 618 | // Collection: CA: 619 | uint8_t GD_WirelessRadioLed[10]; // Usage 0x000100C7: Wireless Radio LED, Value = 0 to -1 620 | } outputReport31_t; 621 | 622 | --------------------------------------------------------------------------------