From cf1b0a4eb493ed9d2c080cd1dc38ec087b70ca90 Mon Sep 17 00:00:00 2001 From: Daniel Prilik Date: Thu, 29 Oct 2020 19:31:32 -0400 Subject: [PATCH] add scroll + notifications oh my god it took far too long to get notifications working... --- Cargo.lock | 362 +++++++++++++++++- Cargo.toml | 4 +- README.md | 46 ++- .../50-surface-dial.rules | 2 +- install/99-uinput.rules | 1 + install/surface-dial.service | 5 + notes/HID_Report_Descriptor.txt | 234 +++++++++++ rustfmt.toml | 2 +- src/common.rs | 14 + src/controller/controls/dpad.rs | 2 +- src/controller/controls/media.rs | 2 +- src/controller/controls/mod.rs | 2 + src/controller/controls/scroll_zoom.rs | 68 ++++ src/controller/controls/volume.rs | 7 +- src/dial_device.rs | 7 +- src/error.rs | 13 +- src/fake_input.rs | 69 +++- src/main.rs | 19 +- surface-dial.service | 4 - 19 files changed, 810 insertions(+), 53 deletions(-) rename surface-dial-udev.rules => install/50-surface-dial.rules (51%) create mode 100644 install/99-uinput.rules create mode 100644 install/surface-dial.service create mode 100644 notes/HID_Report_Descriptor.txt create mode 100644 src/controller/controls/scroll_zoom.rs delete mode 100644 surface-dial.service diff --git a/Cargo.lock b/Cargo.lock index 6cfa7a2..4975b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,11 +1,58 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "cc" version = "1.0.61" @@ -18,11 +65,63 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "dbus" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd9e78c210146a1860f897db03412fd5091fd73100778e43ee255cca252cf32" +dependencies = [ + "libc", + "libdbus-sys", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "evdev-rs" version = "0.4.0" +source = "git+https://github.com/daniel5151/evdev-rs.git#0750443a1eb8edfa34cdfcf91e8c5c6cf6a12c3b" dependencies = [ - "bitflags", + "bitflags 1.2.1", "evdev-sys", "libc", "log", @@ -31,18 +130,45 @@ dependencies = [ [[package]] name = "evdev-sys" version = "0.2.1" +source = "git+https://github.com/daniel5151/evdev-rs.git#0750443a1eb8edfa34cdfcf91e8c5c6cf6a12c3b" dependencies = [ "cc", "libc", "pkg-config", ] +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "libdbus-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0" +dependencies = [ + "pkg-config", +] + [[package]] name = "log" version = "0.4.11" @@ -52,15 +178,249 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mac-notification-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76" +dependencies = [ + "cc", + "chrono", + "dirs", + "objc-foundation", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "notify-rust" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144acee6a0543dc74893e4b8a33936b5b0a94cc2d4ab024afd0c6daff7afc3c0" +dependencies = [ + "dbus", + "mac-notification-sys", + "winrt-notification", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "pkg-config" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "rust-argon2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "strum" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da" + +[[package]] +name = "strum_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "surface-dial-daemon" version = "0.1.0" dependencies = [ "evdev-rs", + "notify-rust", +] + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote", + "synom", + "unicode-xid", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winrt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winrt-notification" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c31a65da50d792c6f9bd2e3216249566c4fb1d2d34f9b7d2d66d2e93f62a242" +dependencies = [ + "strum", + "strum_macros", + "winapi", + "winrt", + "xml-rs", +] + +[[package]] +name = "xml-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621" +dependencies = [ + "bitflags 0.9.1", ] diff --git a/Cargo.toml b/Cargo.toml index 036eb4f..b588cb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,6 @@ authors = ["Daniel Prilik "] edition = "2018" [dependencies] -evdev-rs = { path = "../evdev-rs/" } +# custom fork which implements `Send` on UInputDevice +evdev-rs = { git = "https://github.com/daniel5151/evdev-rs.git", sha = "cc9060d" } +notify-rust = "4" diff --git a/README.md b/README.md index 7a42de8..8922b70 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,24 @@ It would be cool to create some sort of GUI overlay (similar to the Windows one) ## Functionality - [x] Interpret raw Surface Dial event -- [ ] Dynamically switch between operating modes - - [ ] Context-sensitive (based on currently open application) - - [ ] Using `surface-dial-cli` application - - [ ] Using some-sort of on-device mechanism (e.g: long-press) -- Various Operating Modes +- Operating Modes - [x] Volume Controls - [x] Media Controls - [x] D-Pad (emulated left, right, and space key) - - [ ] Scrolling / Zooming + - [x] Scrolling / Zooming + - [ ] \(meta\) Specify modes via config file(s) +- [ ] Dynamically switch between operating modes + - _currently required re-compiling the daemon_ + - [ ] Context-sensitive (based on currently open application) + - [ ] Using `surface-dial-cli` application + - [ ] Using some-sort of on-device mechanism (e.g: long-press) +- [ ] Haptic Feedback + - This is tough one, as it doesn't seem like the kernel driver exposes any haptic feedback interface... +- [x] Desktop Notifications + - [x] On Launch + - [x] When switching between sub-modes (e.g: scroll/zoom) -Feel free to suggest / contribute new features! +Feel free to contribute new features! ## Building @@ -90,19 +97,30 @@ If your distro doesn't use `systemd`, you'll have to come up with something your cargo install --path . # IMPORTANT: modify the .service file to reflect where you placed the `service-dial-daemon` executable -vi surface-dial.service +vi ./install/surface-dial.service -# install the systemd service -sudo cp surface-dial.service /etc/systemd/system/surface-dial.service -# install the service-dial udev rule -sudo cp surface-dial-udev.rules /etc/udev/rules.d/50-surface-dial.rules +# create new group for uinput +# (the 99-uinput.rules file changes the group of /dev/uinput to this new group) +sudo groupdadd -f uinput + +# add self to the new uinput group and the existing /dev/input group +sudo gpasswd -a $(whoami) uinput +sudo gpasswd -a $(whoami) $(stat -c "%G" /dev/input/event0) + +# install the systemd user service +mkdir -p ~/.config/systemd/user/ +cp ./install/surface-dial.service ~/.config/systemd/user/surface-dial.service + +# install the udev rules +sudo cp ./install/99-uinput.rules /etc/udev/rules.d/99-uinput.rules +sudo cp ./install/50-surface-dial.rules /etc/udev/rules.d/50-surface-dial.rules # reload systemd + udev -sudo systemctl daemon-reload +systemctl --user daemon-reload sudo udevadm control --reload ``` -You many need to disconnect + reconnect the Surface Dial for the `udev` rule to trigger. +You may need to reboot to have the various groups / udev rules propagate. ## License diff --git a/surface-dial-udev.rules b/install/50-surface-dial.rules similarity index 51% rename from surface-dial-udev.rules rename to install/50-surface-dial.rules index 36d86e7..07e737e 100644 --- a/surface-dial-udev.rules +++ b/install/50-surface-dial.rules @@ -1 +1 @@ -ACTION=="add", ATTR{name}=="Surface Dial System Multi Axis", TAG+="systemd", ENV{SYSTEMD_WANTS}="surface-dial.service" +ACTION=="add", ATTR{name}=="Surface Dial System Multi Axis", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="surface-dial.service" diff --git a/install/99-uinput.rules b/install/99-uinput.rules new file mode 100644 index 0000000..e6bad61 --- /dev/null +++ b/install/99-uinput.rules @@ -0,0 +1 @@ +KERNEL=="uinput", GROUP="uinput", MODE:”0660" diff --git a/install/surface-dial.service b/install/surface-dial.service new file mode 100644 index 0000000..b614743 --- /dev/null +++ b/install/surface-dial.service @@ -0,0 +1,5 @@ +# IMPORTANT: modify the "ExecStart" field to reflect your userid + surface-dial-daemon install dir + +[Service] +# HACK: this service needs to run _after_ the /dev/input/eventXX files have been created +ExecStart=bash -c 'sleep 1 && /home/danielprilik/.cargo/bin/surface-dial-daemon' diff --git a/notes/HID_Report_Descriptor.txt b/notes/HID_Report_Descriptor.txt new file mode 100644 index 0000000..544e171 --- /dev/null +++ b/notes/HID_Report_Descriptor.txt @@ -0,0 +1,234 @@ +# extracted using hid-example.c + https://eleccelerator.com/usbdescreqparser/ + +0x05, 0x01, // Usage Page (Generic Desktop Ctrls) +0x09, 0x0E, // Usage (0x0E) +0xA1, 0x01, // Collection (Application) +0x85, 0x01, // Report ID (1) +0x05, 0x0D, // Usage Page (Digitizer) +0x09, 0x21, // Usage (Puck) +0xA1, 0x02, // Collection (Logical) +0x15, 0x00, // Logical Minimum (0) +0x25, 0x01, // Logical Maximum (1) +0x75, 0x01, // Report Size (1) +0x95, 0x01, // Report Count (1) +0xA1, 0x00, // Collection (Physical) +0x05, 0x09, // Usage Page (Button) +0x09, 0x01, // Usage (0x01) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x05, 0x0D, // Usage Page (Digitizer) +0x09, 0x33, // Usage (Touch) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x95, 0x06, // Report Count (6) +0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0xA1, 0x02, // Collection (Logical) +0x05, 0x01, // Usage Page (Generic Desktop Ctrls) +0x09, 0x37, // Usage (Dial) +0x16, 0x01, 0x80, // Logical Minimum (-32767) +0x26, 0xFF, 0x7F, // Logical Maximum (32767) +0x75, 0x10, // Report Size (16) +0x95, 0x01, // Report Count (1) +0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) +0x35, 0x00, // Physical Minimum (0) +0x46, 0x10, 0x0E, // Physical Maximum (3600) +0x15, 0x00, // Logical Minimum (0) +0x26, 0x10, 0x0E, // Logical Maximum (3600) +0x09, 0x48, // Usage (0x48) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x45, 0x00, // Physical Maximum (0) +0xC0, // End Collection +0x55, 0x0E, // Unit Exponent (-2) +0x65, 0x11, // Unit (System: SI Linear, Length: Centimeter) +0x46, 0x00, 0x00, // Physical Maximum (0) +0x26, 0x00, 0x00, // Logical Maximum (0) +0x09, 0x30, // Usage (X) +0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State) +0x09, 0x31, // Usage (Y) +0x46, 0x00, 0x00, // Physical Maximum (0) +0x26, 0x00, 0x00, // Logical Maximum (0) +0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State) +0x05, 0x0D, // Usage Page (Digitizer) +0x09, 0x48, // Usage (0x48) +0x15, 0x3A, // Logical Minimum (58) +0x25, 0x3A, // Logical Maximum (58) +0x75, 0x08, // Report Size (8) +0x55, 0x0F, // Unit Exponent (-1) +0x35, 0x3A, // Physical Minimum (58) +0x45, 0x3A, // Physical Maximum (58) +0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x55, 0x00, // Unit Exponent (0) +0x65, 0x00, // Unit (None) +0x35, 0x00, // Physical Minimum (0) +0x45, 0x00, // Physical Maximum (0) +0x05, 0x0E, // Usage Page (Reserved 0x0E) +0x09, 0x01, // Usage (0x01) +0xA1, 0x02, // Collection (Logical) +0x15, 0x00, // Logical Minimum (0) +0x26, 0xFF, 0x00, // Logical Maximum (255) +0x09, 0x24, // Usage (0x24) +0xB1, 0x42, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x09, 0x24, // Usage (0x24) +0x91, 0x42, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x15, 0x01, // Logical Minimum (1) +0x25, 0x07, // Logical Maximum (7) +0x09, 0x20, // Usage (0x20) +0xB1, 0x42, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x09, 0x21, // Usage (0x21) +0x91, 0x42, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x25, 0x0A, // Logical Maximum (10) +0x09, 0x28, // Usage (0x28) +0xB1, 0x42, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x75, 0x10, // Report Size (16) +0x26, 0xD0, 0x07, // Logical Maximum (2000) +0x09, 0x25, // Usage (0x25) +0xB1, 0x42, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x09, 0x25, // Usage (0x25) +0x91, 0x42, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State,Non-volatile) +0x85, 0x02, // Report ID (2) +0x75, 0x20, // Report Size (32) +0x17, 0x37, 0x00, 0x01, 0x00, // Logical Minimum (65590) +0x27, 0x37, 0x00, 0x01, 0x00, // Logical Maximum (65590) +0x09, 0x22, // Usage (0x22) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x09, 0x11, // Usage (0x11) +0xA1, 0x02, // Collection (Logical) +0x05, 0x0A, // Usage Page (Ordinal) +0x95, 0x03, // Report Count (3) +0x09, 0x03, // Usage (0x03) +0x09, 0x04, // Usage (0x04) +0x09, 0x05, // Usage (0x05) +0x75, 0x08, // Report Size (8) +0x15, 0x00, // Logical Minimum (0) +0x25, 0xFF, // Logical Maximum (-1) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0xC0, // End Collection +0x05, 0x0E, // Usage Page (Reserved 0x0E) +0x09, 0x10, // Usage (0x10) +0xA1, 0x02, // Collection (Logical) +0x05, 0x0A, // Usage Page (Ordinal) +0x95, 0x01, // Report Count (1) +0x15, 0x03, // Logical Minimum (3) +0x25, 0x03, // Logical Maximum (3) +0x36, 0x03, 0x10, // Physical Minimum (4099) +0x46, 0x03, 0x10, // Physical Maximum (4099) +0x09, 0x03, // Usage (0x03) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x15, 0x04, // Logical Minimum (4) +0x25, 0x04, // Logical Maximum (4) +0x36, 0x04, 0x10, // Physical Minimum (4100) +0x46, 0x04, 0x10, // Physical Maximum (4100) +0x09, 0x04, // Usage (0x04) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x15, 0x05, // Logical Minimum (5) +0x25, 0x05, // Logical Maximum (5) +0x36, 0x04, 0x10, // Physical Minimum (4100) +0x46, 0x04, 0x10, // Physical Maximum (4100) +0x09, 0x05, // Usage (0x05) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x35, 0x00, // Physical Minimum (0) +0x45, 0x00, // Physical Maximum (0) +0xC0, // End Collection +0xC0, // End Collection +0xC0, // End Collection +0xC0, // End Collection +0xC0, // End Collection +0x06, 0x07, 0xFF, // Usage Page (Vendor Defined 0xFF07) +0x09, 0x70, // Usage (0x70) +0xA1, 0x01, // Collection (Application) +0x85, 0x30, // Report ID (48) +0x15, 0x00, // Logical Minimum (0) +0x25, 0xFF, // Logical Maximum (-1) +0x95, 0x01, // Report Count (1) +0x75, 0x08, // Report Size (8) +0x09, 0x00, // Usage (0x00) +0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0xC0, // End Collection +0x09, 0x71, // Usage (0x71) +0xA1, 0x01, // Collection (Application) +0x15, 0x00, // Logical Minimum (0) +0x25, 0xFF, // Logical Maximum (-1) +0x75, 0x08, // Report Size (8) +0x95, 0x48, // Report Count (72) +0x85, 0x2A, // Report ID (42) +0x09, 0xC6, // Usage (0xC6) +0x82, 0x02, 0x01, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Buffered Bytes) +0x09, 0xC7, // Usage (0xC7) +0x92, 0x02, 0x01, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile,Buffered Bytes) +0x95, 0x34, // Report Count (52) +0x09, 0xC8, // Usage (0xC8) +0xB2, 0x03, 0x01, // Feature (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile,Buffered Bytes) +0x85, 0x2B, // Report ID (43) +0x09, 0xC9, // Usage (0xC9) +0x82, 0x02, 0x01, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Buffered Bytes) +0x09, 0xCA, // Usage (0xCA) +0x92, 0x02, 0x01, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile,Buffered Bytes) +0x09, 0xCB, // Usage (0xCB) +0xB2, 0x02, 0x01, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile,Buffered Bytes) +0x17, 0x00, 0x00, 0x00, 0x80, // Logical Minimum (-2147483649) +0x27, 0xFF, 0xFF, 0xFF, 0x7F, // Logical Maximum (2147483646) +0x75, 0x20, // Report Size (32) +0x95, 0x04, // Report Count (4) +0x85, 0x2C, // Report ID (44) +0x19, 0xCC, // Usage Minimum (0xCC) +0x29, 0xCF, // Usage Maximum (0xCF) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x95, 0x04, // Report Count (4) +0x85, 0x2D, // Report ID (45) +0x19, 0xD8, // Usage Minimum (0xD8) +0x29, 0xDB, // Usage Maximum (0xDB) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x95, 0x04, // Report Count (4) +0x19, 0xDC, // Usage Minimum (0xDC) +0x29, 0xDF, // Usage Maximum (0xDF) +0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x19, 0xE0, // Usage Minimum (0xE0) +0x29, 0xE3, // Usage Maximum (0xE3) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x2E, // Report ID (46) +0x19, 0xE4, // Usage Minimum (0xE4) +0x29, 0xE7, // Usage Maximum (0xE7) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x19, 0xE8, // Usage Minimum (0xE8) +0x29, 0xEB, // Usage Maximum (0xEB) +0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x95, 0x0B, // Report Count (11) +0x19, 0xEC, // Usage Minimum (0xEC) +0x29, 0xEF, // Usage Maximum (0xEF) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x95, 0x04, // Report Count (4) +0x85, 0x2F, // Report ID (47) +0x19, 0xF0, // Usage Minimum (0xF0) +0x29, 0xF3, // Usage Maximum (0xF3) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x19, 0xF4, // Usage Minimum (0xF4) +0x29, 0xF7, // Usage Maximum (0xF7) +0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x19, 0xF8, // Usage Minimum (0xF8) +0x29, 0xFB, // Usage Maximum (0xFB) +0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0xC0, // End Collection +0x05, 0x01, // Usage Page (Generic Desktop Ctrls) +0x09, 0x80, // Usage (Sys Control) +0xA1, 0x01, // Collection (Application) +0x85, 0x32, // Report ID (50) +0x09, 0x82, // Usage (Sys Sleep) +0x09, 0x83, // Usage (Sys Wake Up) +0x15, 0x00, // Logical Minimum (0) +0x25, 0x01, // Logical Maximum (1) +0x95, 0x02, // Report Count (2) +0x75, 0x01, // Report Size (1) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x95, 0x06, // Report Count (6) +0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0xC0, // End Collection +0x09, 0x72, // Usage (0x72) +0xA1, 0x01, // Collection (Application) +0x85, 0x31, // Report ID (49) +0x95, 0x0A, // Report Count (10) +0x75, 0x08, // Report Size (8) +0x15, 0x00, // Logical Minimum (0) +0x25, 0xFF, // Logical Maximum (-1) +0x09, 0xC6, // Usage (0xC6) +0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x09, 0xC7, // Usage (0xC7) +0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0xC0, // End Collection diff --git a/rustfmt.toml b/rustfmt.toml index 606e292..b2715b2 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -wrap_comments = true \ No newline at end of file +wrap_comments = true diff --git a/src/common.rs b/src/common.rs index 6aee25b..8c55803 100644 --- a/src/common.rs +++ b/src/common.rs @@ -32,3 +32,17 @@ impl ThresholdHelper { None } } + +use notify_rust::error::Result as NotifyResult; +use notify_rust::{Hint, Notification, NotificationHandle, Timeout}; + +pub fn action_notification(msg: &str, icon: &str) -> NotifyResult { + Notification::new() + .hint(Hint::Transient(true)) + .hint(Hint::Category("device".into())) + .timeout(Timeout::Milliseconds(100)) + .summary("Surface Dial") + .body(msg) + .icon(icon) + .show() +} diff --git a/src/controller/controls/dpad.rs b/src/controller/controls/dpad.rs index aa82773..694bfe1 100644 --- a/src/controller/controls/dpad.rs +++ b/src/controller/controls/dpad.rs @@ -87,7 +87,7 @@ impl Worker { Ordering::Greater => self.fake_input.key_press(&[EV_KEY::KEY_RIGHT]).unwrap(), } - eprintln!("{:?}", self.velocity); + // eprintln!("{:?}", self.velocity); } } } diff --git a/src/controller/controls/media.rs b/src/controller/controls/media.rs index 185a0aa..b50ca3d 100644 --- a/src/controller/controls/media.rs +++ b/src/controller/controls/media.rs @@ -23,11 +23,11 @@ impl Media { impl ControlMode for Media { fn on_btn_press(&mut self) -> DynResult<()> { - self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?; Ok(()) } fn on_btn_release(&mut self) -> DynResult<()> { + self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?; Ok(()) } diff --git a/src/controller/controls/mod.rs b/src/controller/controls/mod.rs index a3247fe..c06cb95 100644 --- a/src/controller/controls/mod.rs +++ b/src/controller/controls/mod.rs @@ -1,7 +1,9 @@ mod dpad; mod media; +mod scroll_zoom; mod volume; pub use self::dpad::*; pub use self::media::*; +pub use self::scroll_zoom::*; pub use self::volume::*; diff --git a/src/controller/controls/scroll_zoom.rs b/src/controller/controls/scroll_zoom.rs new file mode 100644 index 0000000..815b0a5 --- /dev/null +++ b/src/controller/controls/scroll_zoom.rs @@ -0,0 +1,68 @@ +use crate::common::{action_notification, DialDir, ThresholdHelper}; +use crate::controller::ControlMode; +use crate::fake_input::{FakeInput, ScrollStep}; +use crate::DynResult; + +use evdev_rs::enums::EV_KEY; + +pub struct ScrollZoom { + thresh: ThresholdHelper, + zoom: bool, + + fake_input: FakeInput, +} + +impl ScrollZoom { + pub fn new(sensitivity: i32) -> ScrollZoom { + ScrollZoom { + thresh: ThresholdHelper::new(sensitivity), + zoom: false, + + fake_input: FakeInput::new(), + } + } +} + +impl ControlMode for ScrollZoom { + fn on_btn_press(&mut self) -> DynResult<()> { + Ok(()) + } + + fn on_btn_release(&mut self) -> DynResult<()> { + self.zoom = !self.zoom; + if self.zoom { + action_notification("Zoom Mode", "zoom-in")?; + } else { + action_notification("ScrollZoom Mode", "input-mouse")?; + } + + Ok(()) + } + + fn on_dial(&mut self, delta: i32) -> DynResult<()> { + match self.thresh.update(delta) { + None => {} + Some(DialDir::Left) => { + if self.zoom { + eprintln!("zoom out"); + self.fake_input + .key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_MINUS])?; + } else { + eprintln!("scroll up"); + self.fake_input.scroll_step(ScrollStep::Up)?; + } + } + Some(DialDir::Right) => { + if self.zoom { + eprintln!("zoom in"); + self.fake_input + .key_click(&[EV_KEY::KEY_LEFTCTRL, EV_KEY::KEY_EQUAL])?; + } else { + eprintln!("scroll down"); + self.fake_input.scroll_step(ScrollStep::Down)?; + } + } + } + Ok(()) + } +} diff --git a/src/controller/controls/volume.rs b/src/controller/controls/volume.rs index 0675af2..af4081d 100644 --- a/src/controller/controls/volume.rs +++ b/src/controller/controls/volume.rs @@ -24,14 +24,13 @@ impl Volume { impl ControlMode for Volume { fn on_btn_press(&mut self) -> DynResult<()> { // TODO: support double-click to mute - - eprintln!("play/pause"); - // self.fake_input.mute()? - self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?; Ok(()) } fn on_btn_release(&mut self) -> DynResult<()> { + eprintln!("play/pause"); + // self.fake_input.mute()? + self.fake_input.key_click(&[EV_KEY::KEY_PLAYPAUSE])?; Ok(()) } diff --git a/src/dial_device.rs b/src/dial_device.rs index 18d71c0..e0731ba 100644 --- a/src/dial_device.rs +++ b/src/dial_device.rs @@ -30,13 +30,14 @@ impl DialDevice { let mut control = None; let mut axis = None; - for e in fs::read_dir("/dev/input/").map_err(Error::Io)? { - let e = e.map_err(Error::Io)?; + for e in fs::read_dir("/dev/input/").map_err(Error::OpenDevInputDir)? { + let e = e.map_err(Error::OpenDevInputDir)?; if !e.file_name().to_str().unwrap().starts_with("event") { continue; } - let file = fs::File::open(e.path()).map_err(Error::Io)?; + let file = + fs::File::open(e.path()).map_err(|err| Error::OpenEventFile(e.path(), err))?; let dev = Device::new_from_fd(file).map_err(Error::Evdev)?; match dev.name() { diff --git a/src/error.rs b/src/error.rs index b667bf2..8fd86e7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,24 +1,29 @@ use std::fmt; +use std::io; use evdev_rs::InputEvent; #[derive(Debug)] pub enum Error { + OpenDevInputDir(io::Error), + OpenEventFile(std::path::PathBuf, io::Error), MissingDial, MultipleDials, UnexpectedEvt(InputEvent), - Evdev(std::io::Error), - Io(std::io::Error), + Evdev(io::Error), + Io(io::Error), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::OpenDevInputDir(e) => write!(f, "Could not open /dev/input directory: {}", e), + Error::OpenEventFile(path, e) => write!(f, "Could not open {:?}: {}", path, e), Error::MissingDial => write!(f, "Could not find the Surface Dial"), Error::MultipleDials => write!(f, "Found multiple dials"), Error::UnexpectedEvt(evt) => write!(f, "Unexpected event: {:?}", evt), - Error::Evdev(e) => write!(f, "Evdev error: {:?}", e), - Error::Io(e) => write!(f, "Io error: {:?}", e), + Error::Evdev(e) => write!(f, "Evdev error: {}", e), + Error::Io(e) => write!(f, "Io error: {}", e), } } } diff --git a/src/fake_input.rs b/src/fake_input.rs index 335ad11..7dccca6 100644 --- a/src/fake_input.rs +++ b/src/fake_input.rs @@ -12,23 +12,35 @@ fn get_fake_input() -> io::Result<&'static UInputDevice> { device.enable(&EventType::EV_SYN)?; device.enable(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))?; - device.enable(&EventType::EV_KEY)?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFTSHIFT))?; - - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_MUTE))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEDOWN))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEUP))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_NEXTSONG))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PLAYPAUSE))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PREVIOUSSONG))?; - - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFT))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_RIGHT))?; - device.enable(&EventCode::EV_KEY(EV_KEY::KEY_SPACE))?; - device.enable(&EventType::EV_MSC)?; device.enable(&EventCode::EV_MSC(EV_MSC::MSC_SCAN))?; + device.enable(&EventType::EV_KEY)?; + { + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFTSHIFT))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFTCTRL))?; + + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_MUTE))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEDOWN))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_VOLUMEUP))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_NEXTSONG))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PLAYPAUSE))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_PREVIOUSSONG))?; + + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_LEFT))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_RIGHT))?; + + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_SPACE))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_EQUAL))?; + device.enable(&EventCode::EV_KEY(EV_KEY::KEY_MINUS))?; + } + + device.enable(&EventType::EV_REL)?; + { + device.enable(&EventCode::EV_REL(EV_REL::REL_WHEEL))?; + device.enable(&EventCode::EV_REL(EV_REL::REL_WHEEL_HI_RES))?; + } + unsafe { FAKE_INPUT = Some(UInputDevice::create_from_device(&device)?) } } unsafe { Ok(FAKE_INPUT.as_ref().unwrap()) } @@ -99,4 +111,33 @@ impl FakeInput { self.syn_report()?; Ok(()) } + + pub fn scroll_step(&self, dir: ScrollStep) -> io::Result<()> { + // copied from my razer blackwidow chroma mouse + self.uinput.write_event(&InputEvent { + time: TimeVal::new(0, 0), + event_code: EventCode::EV_REL(EV_REL::REL_WHEEL), + event_type: EventType::EV_REL, + value: match dir { + ScrollStep::Down => -1, + ScrollStep::Up => 1, + }, + })?; + self.uinput.write_event(&InputEvent { + time: TimeVal::new(0, 0), + event_code: EventCode::EV_REL(EV_REL::REL_WHEEL_HI_RES), + event_type: EventType::EV_REL, + value: match dir { + ScrollStep::Down => -120, + ScrollStep::Up => 120, + }, + })?; + self.syn_report()?; + Ok(()) + } +} + +pub enum ScrollStep { + Up, + Down, } diff --git a/src/main.rs b/src/main.rs index 9c527b3..a1852e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,24 @@ use crate::controller::DialController; use crate::dial_device::DialDevice; use crate::error::Error; -fn main() -> DynResult<()> { +fn main() { + if let Err(e) = true_main() { + println!("{}", e); + } +} + +fn true_main() -> DynResult<()> { + println!("Started."); + let dial = DialDevice::new()?; println!("Found the dial."); - let default_mode = Box::new(controller::controls::Volume::new(30)); - // let default_mode = Box::new(controls::Media::new(50)); - // let default_mode = Box::new(controls::DPad::new()); + common::action_notification("Active!", "input-mouse")?; + + let default_mode = Box::new(controller::controls::ScrollZoom::new(30)); + // let default_mode = Box::new(controller::controls::Volume::new(30)); + // let default_mode = Box::new(controller::controls::Media::new(50)); + // let default_mode = Box::new(controller::controls::DPad::new()); let mut controller = DialController::new(dial, default_mode); diff --git a/surface-dial.service b/surface-dial.service deleted file mode 100644 index e92ff87..0000000 --- a/surface-dial.service +++ /dev/null @@ -1,4 +0,0 @@ -# modify ExecStart to wherever you put surface-dial-daemon -[Service] -Type=oneshot -ExecStart=/home/danielprilik/.cargo/bin/surface-dial-daemon