Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Renewed Driver Documentation
|
||||
|
||||
This folder documents the current concept driver implementation.
|
||||
|
||||
- [Architecture](architecture.md): process layout, module responsibilities, and data flow.
|
||||
- [Behavior](behavior.md): user-visible controls, scrolling model, sensitivity, haptics, and OSD.
|
||||
- [Input and Output Protocol](input-output.md): Surface Dial event decoding and virtual uinput devices.
|
||||
- [Installation and Service](installation-service.md): install files, udev rules, systemd user service, and logs.
|
||||
- [Troubleshooting](troubleshooting.md): raw event inspection and common failure modes.
|
||||
|
||||
The driver intentionally stays small. The main design choice is to keep the
|
||||
event loop explicit and avoid the configurable mode framework from the older
|
||||
daemon until there is a concrete need for it.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Architecture
|
||||
|
||||
`surface-dial-renewed` is a userspace Linux driver. It reads the Microsoft
|
||||
Surface Dial through evdev, emits virtual input devices through uinput, uses HID
|
||||
for haptic feedback, and uses KDE's OSD service for sensitivity feedback.
|
||||
|
||||
## Runtime Threads
|
||||
|
||||
The process has one main event loop and one input worker thread.
|
||||
|
||||
The input worker is created by `DialReader::spawn` in `src/dial.rs`. It finds
|
||||
the `Surface Dial System Multi Axis` event node through udev, blocks on evdev
|
||||
events, and sends simplified raw events through a channel.
|
||||
|
||||
The main thread owns all output state:
|
||||
|
||||
- `DriverState` in `src/main.rs`
|
||||
- `VirtualInput` in `src/virtual_input.rs`
|
||||
- `Haptics` in `src/haptics.rs`
|
||||
- `SensitivityNotifier` in `src/notify.rs`
|
||||
|
||||
Keeping output ownership on one thread avoids locking around uinput devices and
|
||||
keeps gesture lifecycle decisions local to the main loop.
|
||||
|
||||
## Module Responsibilities
|
||||
|
||||
`src/main.rs`
|
||||
|
||||
The state machine. It decides whether a Dial rotation means scroll or
|
||||
sensitivity adjustment. It starts and ends multitouch gestures, updates
|
||||
sensitivity, and calls haptics/OSD hooks.
|
||||
|
||||
`src/dial.rs`
|
||||
|
||||
Device discovery and event decoding. It converts evdev events into
|
||||
`DialEventKind` values: connect, disconnect, button press/release, long press,
|
||||
rotation, and ignored noise.
|
||||
|
||||
`src/virtual_input.rs`
|
||||
|
||||
Creates two uinput devices:
|
||||
|
||||
- `Surface Dial Renewed Touchpad` for multitouch scrolling.
|
||||
- `Surface Dial Renewed Wheel` for regular wheel mode.
|
||||
|
||||
The touchpad device is intentionally not advertised as a buttonpad and does not
|
||||
expose `BTN_LEFT`, reducing the chance that very short gestures become tap or
|
||||
right-click behavior.
|
||||
|
||||
`src/sensitivity.rs`
|
||||
|
||||
Defines the 1-32 sensitivity scale and maps each level to scroll scaling.
|
||||
|
||||
`src/haptics.rs`
|
||||
|
||||
Opens the Dial's HID device and sends manual haptic buzz reports. Haptics are
|
||||
best-effort; failure disables haptics but does not stop scrolling.
|
||||
|
||||
`src/notify.rs`
|
||||
|
||||
Shows the sensitivity value. KDE uses `qdbus6` with
|
||||
`org.kde.osdService.showText`. If that fails, the code falls back to
|
||||
FreeDesktop notifications.
|
||||
|
||||
`src/error.rs`
|
||||
|
||||
Small shared error enum for the concept driver.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. `DialReader` finds `Surface Dial System Multi Axis`.
|
||||
2. Raw evdev events are sent to the main loop.
|
||||
3. `main.rs` decodes intent:
|
||||
- button held + rotate: sensitivity adjustment
|
||||
- rotate alone: scrolling
|
||||
- connect/disconnect: haptics and gesture cleanup
|
||||
4. `VirtualInput` emits either multitouch or wheel events.
|
||||
5. `Haptics` buzzes on selector entry and sensitivity level changes.
|
||||
6. `SensitivityNotifier` displays `Surface Dial sensitivity N/32`.
|
||||
|
||||
## Reconnect Model
|
||||
|
||||
The input worker scans for an existing Dial at startup. If the device is removed
|
||||
or an input read fails, it sends a disconnect event, sleeps briefly, and retries
|
||||
discovery/monitoring. The main loop ends any active multitouch gesture and drops
|
||||
the HID haptics handle on disconnect. On reconnect it reopens haptics.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Driver Behavior
|
||||
|
||||
## Default Mode
|
||||
|
||||
The default mode is multitouch scrolling. Run explicitly with:
|
||||
|
||||
```bash
|
||||
surface-dial-renewed --mode multitouch
|
||||
```
|
||||
|
||||
Regular wheel mode exists for fallback/testing:
|
||||
|
||||
```bash
|
||||
surface-dial-renewed --mode wheel
|
||||
```
|
||||
|
||||
## Normal Rotation
|
||||
|
||||
Normal Dial rotation emits a synthetic two-finger touchpad scroll through
|
||||
`Surface Dial Renewed Touchpad`.
|
||||
|
||||
The driver starts a virtual two-finger contact on the first rotation event,
|
||||
moves both virtual fingers vertically, and ends the touch gesture after no
|
||||
rotation has arrived for 2 seconds.
|
||||
|
||||
The 2 second hold is intentional. Very slow physical Dial movement can emit
|
||||
sparse `REL_DIAL` events. If the virtual fingers are lifted too quickly, libinput
|
||||
can treat those sparse movements as many tiny gestures and ignore them. Holding
|
||||
the contact makes slow and precise movement accumulate into one gesture.
|
||||
|
||||
## Sensitivity Selector
|
||||
|
||||
Hold the Dial down and rotate to change sensitivity.
|
||||
|
||||
- Range: `1/32` through `32/32`
|
||||
- Default: `7/32`
|
||||
- Step threshold: `48` raw Dial units per sensitivity level
|
||||
- Display: KDE OSD text, falling back to notification
|
||||
- Haptics: one buzz when entering the selector, one buzz per accepted level
|
||||
|
||||
While the button is held, rotation never scrolls. If a multitouch scroll gesture
|
||||
is active when the button is pressed, it is ended before sensitivity adjustment.
|
||||
|
||||
## Sensitivity Scaling
|
||||
|
||||
The sensitivity level maps to milli-pixels per raw Dial unit:
|
||||
|
||||
```text
|
||||
500 + (level - 1) * 250
|
||||
```
|
||||
|
||||
This means level 1 is `0.5` virtual coordinate units per raw Dial unit, level 7
|
||||
is `2.0`, and level 32 is `8.25`.
|
||||
|
||||
The driver stores fractional remainders while a gesture is active. A nonzero raw
|
||||
Dial event is rounded away from zero if it would otherwise produce no integer
|
||||
coordinate movement.
|
||||
|
||||
## Haptics
|
||||
|
||||
Haptics are deliberately limited:
|
||||
|
||||
- Enter sensitivity selector: buzz once.
|
||||
- Change sensitivity level: buzz once.
|
||||
|
||||
Normal scrolling does not buzz. It was tested and removed because it was
|
||||
distracting and hard to align with physical rotation consistently.
|
||||
|
||||
## OSD
|
||||
|
||||
On KDE, the driver calls:
|
||||
|
||||
```text
|
||||
org.kde.plasmashell /org/kde/osdService org.kde.osdService.showText
|
||||
```
|
||||
|
||||
The displayed text is:
|
||||
|
||||
```text
|
||||
Surface Dial sensitivity N/32
|
||||
```
|
||||
|
||||
KDE exposes polished volume/brightness OSD methods, but those are semantically
|
||||
volume/brightness-specific. The generic custom progress API is not callable as a
|
||||
normal method on this system, so the driver uses text OSD instead.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Input and Output Protocol
|
||||
|
||||
## Physical Dial Input
|
||||
|
||||
The driver reads the `Surface Dial System Multi Axis` evdev device.
|
||||
|
||||
Find it with:
|
||||
|
||||
```bash
|
||||
rg -n -C 8 "Surface Dial System Multi Axis" /proc/bus/input/devices
|
||||
```
|
||||
|
||||
Then inspect raw data:
|
||||
|
||||
```bash
|
||||
sudo evtest /dev/input/eventXX
|
||||
```
|
||||
|
||||
Expected raw events:
|
||||
|
||||
```text
|
||||
EV_REL / REL_DIAL: rotation
|
||||
EV_KEY / BTN_0: button press and release
|
||||
EV_SYN and EV_MSC: ignored framing/scancode events
|
||||
```
|
||||
|
||||
The `Surface Dial System Control` node is not used for scrolling. It exposes
|
||||
sleep/wakeup-style keys, not rotation.
|
||||
|
||||
## Virtual Touchpad
|
||||
|
||||
The multitouch output device is named:
|
||||
|
||||
```text
|
||||
Surface Dial Renewed Touchpad
|
||||
```
|
||||
|
||||
It exposes a type-B multitouch touchpad with slots. On gesture start it creates
|
||||
two active tracking IDs. On movement it moves both slots along the Y axis and
|
||||
updates the single-touch compatibility `ABS_Y` value.
|
||||
|
||||
Important implementation details:
|
||||
|
||||
- Two fingers are used because libinput interprets that as scroll.
|
||||
- `INPUT_PROP_POINTER` is enabled.
|
||||
- `INPUT_PROP_BUTTONPAD` is intentionally not enabled.
|
||||
- `BTN_LEFT` is intentionally not exposed.
|
||||
- Pressure/touch-major/tool-width axes are exposed to look more like a real
|
||||
touchpad.
|
||||
|
||||
The lack of buttonpad/left-button capability reduces accidental tap/click
|
||||
interpretation when a scroll gesture is short.
|
||||
|
||||
## Gesture Lifecycle
|
||||
|
||||
`multitouch_start`
|
||||
|
||||
Creates slot 0 and slot 1 with tracking IDs, positions, pressure, touch major,
|
||||
and two-finger tool state.
|
||||
|
||||
`multitouch_move`
|
||||
|
||||
Moves both slots to the same new Y position. The driver keeps an accumulated
|
||||
`mt_position` relative to `MT_BASELINE`.
|
||||
|
||||
`multitouch_settle`
|
||||
|
||||
Emits one final stationary frame before ending. This reduces the chance that the
|
||||
final movement is interpreted as a flick.
|
||||
|
||||
`multitouch_end`
|
||||
|
||||
Sets both tracking IDs to `-1`, releases `BTN_TOUCH` and tool keys, and reports
|
||||
zero pressure.
|
||||
|
||||
## Coordinate Model
|
||||
|
||||
`MT_BASELINE` is `10_000`, with coordinates from `0` to `20_000`.
|
||||
|
||||
The driver recenters the synthetic gesture if `mt_position` exceeds
|
||||
`MT_RECENTER_THRESHOLD`. This avoids running into coordinate limits during long
|
||||
scrolls.
|
||||
|
||||
## Virtual Wheel
|
||||
|
||||
The fallback wheel device is named:
|
||||
|
||||
```text
|
||||
Surface Dial Renewed Wheel
|
||||
```
|
||||
|
||||
It emits both:
|
||||
|
||||
```text
|
||||
REL_WHEEL
|
||||
REL_WHEEL_HI_RES
|
||||
```
|
||||
|
||||
Wheel mode is implemented but has not received the same tuning as multitouch
|
||||
mode.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Installation and Systemd Service
|
||||
|
||||
## Files
|
||||
|
||||
Install helpers:
|
||||
|
||||
```text
|
||||
renewed_driver/install.sh
|
||||
renewed_driver/uninstall.sh
|
||||
```
|
||||
|
||||
Systemd user service:
|
||||
|
||||
```text
|
||||
renewed_driver/install/surface-dial-renewed.service
|
||||
```
|
||||
|
||||
udev rules:
|
||||
|
||||
```text
|
||||
renewed_driver/install/70-surface-dial-renewed-uinput.rules
|
||||
renewed_driver/install/70-surface-dial-renewed-input.rules
|
||||
renewed_driver/install/70-surface-dial-renewed-hidraw.rules
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./renewed_driver/install.sh
|
||||
```
|
||||
|
||||
This performs:
|
||||
|
||||
1. `cargo install --path renewed_driver`
|
||||
2. udev rule installation under `/etc/udev/rules.d`
|
||||
3. udev reload and trigger
|
||||
4. systemd user service installation
|
||||
5. `systemctl --user enable --now surface-dial-renewed.service`
|
||||
|
||||
The service runs as the desktop user, not root. That is required for access to
|
||||
the user session D-Bus used by KDE OSD.
|
||||
|
||||
## Service
|
||||
|
||||
The service runs:
|
||||
|
||||
```text
|
||||
%h/.cargo/bin/surface-dial-renewed --mode multitouch
|
||||
```
|
||||
|
||||
It restarts on failure with a 2 second delay.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
systemctl --user status surface-dial-renewed.service
|
||||
systemctl --user restart surface-dial-renewed.service
|
||||
journalctl --user -u surface-dial-renewed.service -f
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
The udev rules grant user-session access to:
|
||||
|
||||
- `/dev/uinput`
|
||||
- Surface Dial input event nodes
|
||||
- Surface Dial HID raw node for haptics
|
||||
|
||||
If input permissions still fail, add the user to the group that owns
|
||||
`/dev/input/event*`, then log out and back in:
|
||||
|
||||
```bash
|
||||
sudo gpasswd -a "$(whoami)" "$(stat -c '%G' /dev/input/event0)"
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./renewed_driver/uninstall.sh
|
||||
```
|
||||
|
||||
This disables the user service, removes service/rule files, reloads udev, and
|
||||
uninstalls the Cargo binary.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Check the Service
|
||||
|
||||
```bash
|
||||
systemctl --user status surface-dial-renewed.service
|
||||
journalctl --user -u surface-dial-renewed.service -n 100 --no-pager
|
||||
```
|
||||
|
||||
Expected startup lines:
|
||||
|
||||
```text
|
||||
surface-dial-renewed: running in Multitouch mode
|
||||
surface-dial-renewed: dial connected
|
||||
```
|
||||
|
||||
## Check Raw Dial Rotation
|
||||
|
||||
Find the current event node:
|
||||
|
||||
```bash
|
||||
rg -n -C 8 "Surface Dial System Multi Axis" /proc/bus/input/devices
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
sudo evtest /dev/input/eventXX
|
||||
```
|
||||
|
||||
Rotation should produce:
|
||||
|
||||
```text
|
||||
type 2 (EV_REL), code 7 (REL_DIAL), value ...
|
||||
```
|
||||
|
||||
If `REL_DIAL` does not appear, the driver cannot scroll. Reconnect the Dial over
|
||||
Bluetooth and check again.
|
||||
|
||||
## Wrong Event Node
|
||||
|
||||
The Control node is not the rotation node. It looks like:
|
||||
|
||||
```text
|
||||
Surface Dial System Control
|
||||
```
|
||||
|
||||
and exposes `KEY_SLEEP`/`KEY_WAKEUP`. Use `Surface Dial System Multi Axis` for
|
||||
rotation.
|
||||
|
||||
## Haptics Work But Scroll Does Not
|
||||
|
||||
Haptics use HID raw access; scrolling uses evdev input plus uinput output.
|
||||
Haptics working only proves the HID path works.
|
||||
|
||||
Check:
|
||||
|
||||
1. Raw `REL_DIAL` events with `evtest`.
|
||||
2. Service logs for `dial connected`.
|
||||
3. `/dev/uinput` permissions.
|
||||
4. Whether `Surface Dial Renewed Touchpad` appears in `/proc/bus/input/devices`.
|
||||
|
||||
## Sensitivity OSD Warnings
|
||||
|
||||
The service may log messages from `qdbus6` about locale fallback, for example
|
||||
`Detected locale "C"`. The OSD still works. This is caused by the systemd user
|
||||
service environment lacking a UTF-8 locale variable.
|
||||
|
||||
If it becomes noisy, set locale environment for the user service or replace the
|
||||
shell-out OSD backend with a direct D-Bus implementation.
|
||||
|
||||
## Reconnect Oddities
|
||||
|
||||
After Bluetooth reconnects, event numbers can change. Always rediscover with:
|
||||
|
||||
```bash
|
||||
rg -n -C 8 "Surface Dial" /proc/bus/input/devices
|
||||
```
|
||||
|
||||
If button events appear but rotation does not, disconnect and reconnect the Dial
|
||||
again. The driver cannot synthesize rotation if the kernel is not emitting
|
||||
`REL_DIAL`.
|
||||
Reference in New Issue
Block a user