Skip to content

biste5/surface-dial-alsa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Surface Dial Volume Knob for Linux

A Raspberry Pi makes a capable headless audio player for multi-room setups (Snapcast, Music Assistant, etc.). But headless means no physical controls.

This project turns a Microsoft Surface Dial into one: rotate to change volume, press to mute. You end up with a systemd service that connects to the Dial, auto-starts on boot, and survives reboots.

How it works

The Dial pairs as a standard Bluetooth HID device. It exposes rotation (REL_DIAL) and button (BTN_0) events on a single input node.

dial-volume.py reads those events with evdev. A single twist of the Dial fires a burst of rotation events, so the script batches them before acting. It then drives the ALSA mixer via amixer to change or mute the volume.

Requirements

  • Microsoft Surface Dial (Bluetooth)
  • Raspberry Pi or other Linux with a Bluetooth adapter
  • A working ALSA output with an amixer control (this setup uses PCM)
  • python3-evdev (installed in Setup step 2)

Setup

Note: This guide was written and tested on DietPi (Debian-based). Steps should work as-is on any Debian-based distro. Commands, package names and some paths may differ with other distros.

1. Bluetooth + pairing

Enable Bluetooth and pair the Dial (hold its button until the LED pulses):

sudo systemctl enable --now bluetooth
sudo bluetoothctl
# inside the shell:
power on
agent on
default-agent
scan on
# wait for: [NEW] Device <MAC> Surface Dial
pair <MAC>
trust <MAC>
connect <MAC>
scan off
quit

Enable auto-reconnect by adding to /etc/bluetooth/main.conf:

[Policy]
AutoEnable=true
sudo systemctl restart bluetooth

2. Identify the input device + mixer

sudo apt install python3-evdev -y
cat /proc/bus/input/devices | grep -A4 "Surface Dial System Multi Axis"   # note Handlers=eventX
amixer scontrols                                                          # note the control name

Update DEVICE_PATH and MIXER_CONTROL at the top of dial-volume.py to match. Note: the event number can change across reboots — if the Dial stops working, re-check and update it.

3. Install the script and service

sudo cp scripts/dial-volume.py /usr/local/bin/dial-volume.py
sudo chmod +x /usr/local/bin/dial-volume.py
sudo cp scripts/dial-volume.service /etc/systemd/system/dial-volume.service
sudo systemctl daemon-reload
sudo systemctl enable --now dial-volume
systemctl status dial-volume

Controls

Action Result
Rotate clockwise Volume up
Rotate counter-clockwise Volume down
Press Toggle mute

Tuning

Edit the constants at the top of dial-volume.py:

Variable Meaning
VOLUME_STEP % change per accepted rotation batch
MIN_THRESHOLD Minimum net rotation per batch before volume changes — higher = less sensitive (5 is a good default)

Then apply the changes by restarting the service:

sudo nano /usr/local/bin/dial-volume.py   # edit the constants
sudo systemctl restart dial-volume        # apply

To reverse direction, swap the sign in current_vol += VOLUME_STEP if net > 0 else -VOLUME_STEP.

Troubleshooting

sudo journalctl -u dial-volume -f                                          # live logs
sudo bluetoothctl connect <MAC>                                            # manual reconnect
cat /proc/bus/input/devices | grep -A4 "Surface Dial System Multi"         # re-check event number
sudo python3 scripts/dial-debug.py                                         # inspect raw events / amixer output
  • Volume jumps wildly: raise MIN_THRESHOLD. Don't use fractional VOLUME_STEPamixer only handles integers reliably.
  • "not found": the event path likely changed after a reboot; update DEVICE_PATH.
  • Permissions: the service runs as root so this isn't an issue. To run manually without sudo, add your user to the input and audio groups.

Reference

The Dial emits these input events (codes):

Event type Code Meaning
EV_REL REL_DIAL Rotation (positive = clockwise)
EV_KEY BTN_0 (256) Button press
EV_MSC MSC_SCAN Scancode metadata — ignored
EV_SYN Event separator — ignored

Credits

  • python-evdev by Georgi Valkov — Python bindings for the Linux input subsystem, used here to read raw events from the Dial.

License

MIT — see LICENSE.

About

Linux Systemd service to have a Surface Dial knob controlling alsamixer via bluetooth

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors

Languages