Bluetooth game controller with bthidd(8)

The ‘What’ and the ‘Why’​

Introduction​

I decided to write up my notes from my journey of getting a wireless controller to work as a Bluetooth human input device under FreeBSD 14.1-RELEASE, namely by patching bthidd(8), in the hope that it is useful or applicable for someone else.

Effectively the same topic was already addressed, albeit inconclusively, here: Thread playstation-5-dualsense-controller-pairing.80786. Much of the knowledge gathered there was applicable to my case.

This is presented as two posts, the first containing relevant background information, and the second an actual guide/how-to.

Background​

I had never owned or used a controller until two weeks ago; this was not least an exercise in demystification. I was looking in the first instance for an analogue input device for emulation (primarily PCSX2) via SDL. For lack of other knowledge, I settled on the DualShock 4 because a) it is readily available and replaceable, and b) already had robust support as a USB input device under FreeBSD >= 13.0 with wulf's ps4dshock(4) kernel module. While USB support is itself already quite satisfactory, sometimes it's more comfortable not to have an extra cable dangling around, and the experience offered by the controller seemed incomplete without at least rudimentary Bluetooth connectivity.

Requirements​

The sole objective was to capture analogue stick and button input events. Other features supported by the ps4dshock(4) driver, but not part of this project/how-to, are:
  • LED (‘light bar’) color changes
  • Synaptics touchpad support
  • Accelerometer/gyroscope events
  • Haptic feedback (‘rumble’)
Some of these might be trivial to implement, others presumably much less so. I would point anyone keen to figure them out to the source code of the kernel module (/usr/src/sys/dev/hid/ps4dshock.c).

Preliminary Investigation​

When plugged in via USB, the device worked OOTB with all expected features. However, when it was connected via Bluetooth (cf. Steps 1–2 below), even after the bthidd service was started, no input device showed up, nor was it detected in any form by SDL. The key, as it turns out, is to add support for gamepad devices to the user-space daemon bthidd(8) – this is actually quite easy to do.

Helpful Debugging Tools and Tips​

The following tools were very helpful in the process of figuring this all out:
  • comms/hcidump – like tcpdump(1) for Bluetooth/L2CAP, useful for inspecting reports from connected Bluetooth devices (run with -x flag)
  • cat /var/run/devd.pipe – useful for seeing when devices are attached/detached
  • x11/evtest – for debugging evdev events generated by from uinput devices
  • x11/controllermap – for generating SDL-compatible controller button maps
Examples of their use will be given in the relevant sections below.

The ‘How’​

Step 1: Enabling Bluetooth​

Unless you have a laptop or something else with a builtin Bluetooth chip, you will likely need a USB adapter.

I used the USB-BT500 from ASUS (apparently with a Realtek chip inside), which works OOTB despite FreeBSD's nominally shaky support for Bluetooth 4.0/5.0. Their USB-BT400 model apparently also works well, as do adapters with Broadcom chips. No sponsorship/promotion here. YMMV.

The adapter was detected at boot and created a ubt device:
Code:
# dmesg | grep ubt
ubt0 on uhub1
ubt0: <Realtek ASUS USB-BT500, class 224/1, rev 1.10/2.00, addr 3> on usbus0
The relevant NetGraph modules ought to be autoloaded at boot, but you may want to make sure they are:
Code:
# kldstat | awk '/ng/ {print $5}'
ng_ubt.ko
ng_hci.ko
ng_bluetooth.ko
ng_l2cap.ko
ng_btsocket.ko
ng_socket.ko
You may also need to (re)start the bluetooth service.
Code:
service bluetooth start ubt0
It had already been started by devd (cf. /etc/devd/bluetooth.conf) in my case.

Finally, start the hcsecd service. This is needed for manging the device link keys.
Code:
service hcsecd start

Step 2: Pairing the Controller​

First, scan for Bluetooth devices:
Code:
hccontrol -n ubt0hci Inquiry
The output should contain blocks like the following, where (and hereinafter) 00:11:22:33:aa:bb is the address of your device and ubt0hci is the NetGraph HCI node corresponding to your Bluetooth adapter (replace as relevant).
Code:
Inquiry result #0
    BD_ADDR: 00:11:22:33:aa:bb
    Page Scan Rep. Mode: 0x1
    Page Scan Period Mode: 0x2
    Page Scan Mode: 00
    Class: 00:25:08
    Clock offset: 0x1c60
Then create a connection to the address indentified as your controller in the output of the command above:
Code:
hccontrol -n ubt0hci Create_Connection 00:11:22:33:aa:bb
Some posts I read suggest enabling write authentication in the event of pairing problems:
Code:
hccontrol -n ubt0hci Write_Authentication_Enable 1
I couldn't determine whether this was necessary, but enabled it for safety's sake. At the very least, it didn't seem to do any harm.

Finally, you need to query and store the HID descriptor in /etc/bluetooth/bthidd.conf (this will become important later):
Code:
bthidcontrol -a 00:11:22:33:aa:bb query >> /etc/bluetooth/bthidd.conf
Note that the DualShock 4 actually lies (!) about its HID descriptor, so instead of the above, you should copy the following. This is the first 'collection' that covers the button/stick input reports, taken from the ps4dshock(4) source code; if you want to try adding gyro or other features, you may need to include additional sections.

Code:
device {
    bdaddr            00:11:22:33:aa:bb;
    name            "Wireless Controller";
    vendor_id        0x054c;
    product_id        0x09cc;
    version            0x0100;
    control_psm        0x11;
    interrupt_psm        0x13;
    reconnect_initiate    true;
    battery_power        true;
    normally_connectable    false;
    hid_descriptor        {
        0x05 0x01 0x09 0x05 0xa1 0x01 0x85 0x01
        0x09 0x30 0x09 0x31 0x09 0x33 0x09 0x34
        0x15 0x00 0x26 0xff 0x00 0x75 0x08 0x95
        0x04 0x81 0x02 0x09 0x39 0x15 0x00 0x25
        0x07 0x35 0x00 0x46 0x3b 0x01 0x65 0x14
        0x75 0x04 0x95 0x01 0x81 0x42 0x65 0x00
        0x45 0x00 0x05 0x09 0x19 0x01 0x29 0x0e
        0x15 0x00 0x25 0x01 0x75 0x01 0x95 0x0e
        0x81 0x02 0x06 0x00 0xff 0x09 0x20 0x75
        0x06 0x95 0x01 0x15 0x00 0x25 0x3f 0x81
        0x02 0x05 0x01 0x09 0x32 0x09 0x35 0x15
        0x00 0x26 0xff 0x00 0x75 0x08 0x95 0x02
        0x81 0x02 0xc0
    };
}

This is the point where normally we'd start the bthidd service, as per most instructions about Bluetooth mice/keyboards, but it will need to be patched in order to get the controller to work.

Step 3: Patching bthidd(8)

This is where things get a bit trickier.

As established above, even though the DualShock 4 paired successfully, input was not being registered after starting bthidd(8).

At this point, it was tempting to give up – were it not for the tantalizing fact that, after starting bthidd(8), the output of hcidump(1) showed reports whose values, moreover, changed predictably with button presses or stick movements, e.g.:
Code:
# hcidump -x
HCIDump - HCI packet analyzer ver 1.5
device: any snap_len: 65535 filter: 0xffffffffffffffff
> ACL data: handle 0x0002 flags 0x02 dlen 15
    L2CAP(d): cid 0x43 len 11 [psm 0]
      A1 01 80 80 7A 81 08 00 00 00 00
> ACL data: handle 0x0002 flags 0x02 dlen 15
    L2CAP(d): cid 0x43 len 11 [psm 0]
      A1 01 00 8B 7B 81 08 00 00 00 00
In other words, the device was obviously being polled by bthidd(8). It was not being registered as an input device, but something was happening.

An investigation of the bthidd(8) source code revealed that the daemon is only written to pick up input from devices classified as mice and keyboards. The device class is determined based on the HID device descriptor from the HID map saved in /etc/bluetooth/bthidd.conf.

As this is a user-space tool, it is relatively easy to patch and test in situ as you don't even need to recompile your kernel, etc. Moreover, wulf's work on uinput/evdev makes it relatively trivial to set up new types of input devices.

A patch is provided as an attachment (with a *.txt extension – why doesn't XenForo allow uploading *.diff or *.patch files?!). Download it and cd to the directory where it is saved, and run the following commands. (Make sure bthidd is not running.)
Code:
cp -rv /usr/src/usr.sbin/bluetooth/bthidd $PWD
cd bthidd/
# change file name as relevant
patch < ../bthidd.patch.txt
make
mv -v /usr/sbin/bthidd /usr/sbin/bthidd.old
cp -v bthidd /usr/sbin/bthidd
Disclaimer: I am a bad self-taught C programmer with no knowledge of FreeBSD's HID or Bluetooth subsystems, or of HID report maps, prior undertaking this project. So caveat emptor, and suggestions for improvement are welcome. That said, the patch works perfectly for me and did not break anything in my system.

Now that this is done, we can finally start bthidd in one of two ways (functionally equivalent):
Code:
/usr/sbin/bthidd -u -c /etc/bluetooth/bthidd.conf
or
Code:
service bthidd start
This should trigger the creation of an /dev/input/event* device:
Code:
# cat /var/run/devd.pipe
!system=DEVFS subsystem=CDEV type=CREATE cdev=input/event9
Note that for the device to be accessible to non-root users, you'll have to add a Devfs rule to /etc/devfs.rules, e.g.:
Code:
add path 'input/*' mode 0660 group operator
We can then read the device properties and input events with evtest(1):

Code:
# evtest /dev/input/event9
Input driver version is 1.0.1
Input device ID: bus 0x5 vendor 0x54c product 0x9cc version 0x100
Input device name: "Wireless Controller, bdaddr 00:11:22:33:aa:bb"
Supported events:
  Event type 1 (EV_KEY)
    Event code 304 (BTN_SOUTH)
    Event code 305 (BTN_EAST)
    Event code 307 (BTN_NORTH)
    Event code 308 (BTN_WEST)
    Event code 309 (BTN_Z)
    Event code 310 (BTN_TL)
    Event code 311 (BTN_TR)
    Event code 312 (BTN_TL2)
    Event code 313 (BTN_TR2)
    Event code 314 (BTN_SELECT)
    Event code 315 (BTN_START)
    Event code 316 (BTN_MODE)
    Event code 317 (BTN_THUMBL)
    Event code 318 (BTN_THUMBR)
  Event type 3 (EV_ABS)
    Event code 0 (ABS_X)
      Value    124
      Min        0
      Max      255
      Flat      15
    Event code 1 (ABS_Y)
      Value    128
      Min        0
      Max      255
      Flat      15
    Event code 2 (ABS_Z)
      Value      0
      Min        0
      Max      255
    Event code 3 (ABS_RX)
      Value    122
      Min        0
      Max      255
      Flat      15
    Event code 4 (ABS_RY)
      Value    129
      Min        0
      Max      255
      Flat      15
    Event code 5 (ABS_RZ)
      Value      0
      Min        0
      Max      255
    Event code 16 (ABS_HAT0X)
      Value      0
      Min       -1
      Max        1
    Event code 17 (ABS_HAT0Y)
      Value      0
      Min       -1
      Max        1
Properties:
  Property type 1 (INPUT_PROP_DIRECT)
Testing ... (interrupt to exit)
Event: time 1739922581.396298, type 3 (EV_ABS), code 3 (ABS_RX), value 123
Event: time 1739922581.396298, -------------- SYN_REPORT ------------
Event: time 1739922581.396300, type 3 (EV_ABS), code 4 (ABS_RY), value 128
Event: time 1739922581.396300, -------------- SYN_REPORT ------------
Event: time 1739922581.397844, type 3 (EV_ABS), code 3 (ABS_RX), value 122
Event: time 1739922581.397844, -------------- SYN_REPORT ------------

Step 4: Automating the Process​

I created a small script that automates the pairing and connection events, assuming a patched bthidd(8) from Step 3. It also automatically handles the steps of disconnecting an already connected device.

Save the contents of the code block (click 'Spoiler' to open below), e.g. as setup_connection.sh, and execute it as root while passing BT_ADDR as an environment variable, e.g. env BD_ADDR='00:11:22:33:aa:bb' sh setup_connection.sh.

Code:
#!/bin/sh

if [ "$(id -u)" -ne 0 ]
then
  printf '%s\n' 'Error: Please run this script as root.'
  exit 1
fi

# this may not be necessary
hccontrol -n ubt0hci Write_Authentication_Enable 1

# either pass BD_ADDR as environent variable or set it here
#BD_ADDR='00:11:22:33:aa:bb'

if [ -z "$BD_ADDR" ]
then
  printf '%s\n' 'Error: Please specify a BD_ADDR.'
  exit 1
fi

CONN=$(hccontrol -n ubt0hci Read_Connection_List \
       | grep $BD_ADDR | awk '{print $2}')

if [ ! -z "$CONN" ]
then
  hccontrol -n ubt0hci Disconnect $CONN 1>/dev/null
fi

false
while [ $? -ne 0 ]
do hccontrol -n ubt0hci Inquiry \
  && hccontrol -n ubt0hci Create_Connection $BD_ADDR
done

bthidcontrol -a $BD_ADDR Forget

pgrep -q bthidd && pkill bthidd

/usr/sbin/bthidd -u -c /etc/bluetooth/bthidd.conf

Step 5: Using the Device With Applications​

Note that SDL relies on a gamecontrollerdb.txt (alternatively: game_controller_db.txt, or similar), generally supplied with the resource files of each respective application, to determine the button mappings. A specific entry for the controller in question is required.

The entries can be generated with x11/controllermap and added to the file. The following ought to work for the DualShock 4 (again, replace the value for bdaddr with your controller's address):
Code:
# FreeBSD Bluetooth
050000004c050000cc09000000010000,Wireless Controller bdaddr 00:11:22:33:aa:bb,platform:FreeBSD,crc:f21b,a:b0,b:b1,x:b3,y:b2,back:b9,guide:b11,start:b10,leftstick:b12,rightstick:b13,leftshoulder:b5,rightshoulder:b6,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,misc1:b4,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,

Results and Desirables​

With the patch and the connection process documented above, I am able to establish a stable connection to the DualShock 4. The controller is picked up by all SDL applications I've tested; there is no noticeable latency in processing the input events; and the connection is maintained as long as bthidd is running, terminating as soon as the process is killed.

For now, this fix is specific to the DualShock 4. The patch is designed to be flexible/extensible, but it's definitely not production-grade. However, if the DualSense or other controllers are equally well-behaved, it should in theory carry over easily to them. I could try to add support for the DualSense to the patch if someone would provide an accurate HID report descriptor for it. In general, I'd be happy to help anyone in the community who feels sufficiently confident about the above steps, to get a Bluetooth controller working on FreeBSD.

Of course, there are other features (LEDs, gyro, rumble, touchpad) that fall outside of the scope of this post. Since haptic feedback and setting the LEDs would require sending events TO the peripheral, implementing these functions could be less trivial. They may have to wait for a fuller integration of the Bluetooth stack with iichid and the new HID stack built up around it. This is not out of the question, but I am not aware of any definite timeline for it.
 

Attachments

Last edited:
Back
Top