Linux virtual USB-to-Bluetooth bridge for DualSense and DualSense Edge Wireless Controllers, focused on USB-only features such as 4-channel audio-based haptic feedback. Detailed DualSense output and haptics packet handling is based on DS5Dongle and protocol capture research.
Warning
This project is still in development. Performance has not been tuned, and race conditions may still exist. Many components currently run in userspace, so continuous context switching can add significant overhead.
Application
-> Userspace audio server (e.g. PipeWire)
-(ALSA PCM)-> Linux ALSA stack [snd-usb-audio.ko]
-(USB isochronous OUT URBs)-> **Linux USB stack [vds_hcd.ko]**
**-(/dev/vdsX)-> vdsd (implements raw Bluetooth HID handling)**
**-(AF_BLUETOOTH L2CAP socket)->** Linux Bluetooth stack
-(Bluetooth HID Control/Interrupt)-> DualSense (Edge) controller
This project includes a custom virtual USB HCD primarily because Linux's
dummy_hcd module does not support isochronous transfers, which are required
for the USB audio path used by DualSense haptics. The custom HCD currently also
hosts the /dev/vdsX bridge and virtual port lifecycle, but that architecture
may be revisited if dummy_hcd gains suitable isochronous transfer support.
The current architecture should therefore be treated as temporary. Future work
will move more of the bridge from userspace into the kernel to reduce
context-switching overhead.
Currently, the virtual USB HID endpoints are fixed to a 1000 Hz polling rate. This affects host-side HID report scheduling, input/output latency, USB/HCD wakeups, and CPU/context-switch load.
Important
Current limitation: run bluetoothd with the input plugin disabled
(--noplugin=input). vDS needs direct ownership of the Bluetooth HID Control
and Interrupt L2CAP channels. If the normal BlueZ input plugin is active, it
can claim the controller first and expose it as a regular Bluetooth gamepad
instead of letting vDS bridge it as a virtual USB controller.
Pair each physical controller once before registering it with vdsctl. Start
bluetoothctl, put the controller in Bluetooth pairing mode (hold
Create and PS until the light bar blinks rapidly), then use the MAC address
printed by
scan on:
agent NoInputNoOutput
default-agent
pairable on
scan on
pair XX:XX:XX:XX:XX:XX
trust XX:XX:XX:XX:XX:XX
scan off
quit
When re-pairing a controller that BlueZ already knows, run
remove XX:XX:XX:XX:XX:XX before scan on.
Build the kernel module locally:
make -C moduleInstall or remove it through DKMS:
sudo make -C module install
sudo make -C module uninstallLoad the module:
sudo modprobe vds_hcdTo expose more than one virtual controller:
sudo modprobe vds_hcd max_port=2Build the daemon and CLI:
cmake . -B build
make -C buildInstall or remove the userspace tools:
sudo make -C build install
sudo make -C build uninstallThe installed tools are:
vdsd
vdsctlLoad two virtual controller ports:
sudo modprobe vds_hcd max_port=2Start the daemon:
sudo vdsdBind two paired/trusted Bluetooth DualSense or DualSense Edge controllers:
sudo vdsctl attach aa:bb:cc:dd:ee:01 --identity ds5 --limit-dev /dev/vds0
sudo vdsctl attach aa:bb:cc:dd:ee:02 --identity dse --limit-dev /dev/vds1Important
Configure the virtual controller audio device as 48 kHz 4-channel S16_LE PCM. The exact setup differs by audio stack, such as PipeWire, PulseAudio, ALSA, or JACK.
List persistent bindings:
sudo vdsctl listToggle packet-level daemon tracing:
sudo vdsctl trace on
sudo vdsctl trace offDetach each binding:
sudo vdsctl detach aa:bb:cc:dd:ee:XXCheck that the virtual USB controller enumerated:
lsusb -d 054c:Check input devices and force-feedback support with standard tools:
evtest
fftest /dev/input/eventXCheck the virtual USB audio endpoint with standard ALSA tools:
aplay -l
speaker-test -D hw:<card-number>,<device-number> -c 4 -r 48000 -F S16_LE -t sineInspect daemon logs:
sudo tail -f /var/log/vdsd.log