Initial commit

This commit is contained in:
2026-06-26 15:35:13 +02:00
commit b56dd2a34e
23 changed files with 2407 additions and 0 deletions
+13
View File
@@ -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.
+86
View File
@@ -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.
+85
View File
@@ -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.
+100
View File
@@ -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.
+87
View File
@@ -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.
+82
View File
@@ -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`.