Skip to content

Instantly share code, notes, and snippets.

@ukBaz

ukBaz/README.md Secret

Last active February 26, 2024 11:31
Show Gist options
  • Star 36 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0 to your computer and use it in GitHub Desktop.
Save ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0 to your computer and use it in GitHub Desktop.
Create a Bluetooth HID server

This was an experiment to turn a Raspberry Pi into a Human Interface Device (HID). A keyboard to be more precise.

I followed the instructions at the following location to get me started: http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html

I wanted to move to Python3 and tidy things up on the Bluetooth side to bring it in to line with current ways things are done in BlueZ.

Configure Raspberry Pi.

These instructions assuming you have BlueZ 5.43 installed. You can check this with:

$ bluetoothctl -v
5.43

Ensure Raspberry Pi is at the latest version:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Check that the packages required for this are installed

sudo apt-get install python3-dbus
sudo pip install evdev

Here is an outline of things I changed:

Moved to Python3

I wanted to do this because not only is it a good thing to do but it also allowed some of the dependancies to be removed. After Python 3.3 Bluetooth sockets are supported in the native Python installs. The downside to this is that there are clear distinctions between str and bytes in the code. For me, this broke the keyboard client. This is what required the biggest re-write to get Python3 working.

Reconfigure the Bluetooth Daemon

The instructions worked that were provided but things have moved on a little bit. To stop the Bluetooth daemon running then the following command is preferred:

sudo service bluetooth stop

The input Bluetooth plugin needs to be removed so that it does not grab the sockets we require access to. As the original author says the way this was documented could be improved. If you want to restart the daemon (without the input plugin) from the command line then the following would seem the preferred:

sudo /usr/lib/bluetooth/bluetoothd -P input

If you want to make this the default for this Raspberry Pi then modify the /lib/systemd/system/bluetooth.service file. You will need to change the Service line from:

ExecStart=/usr/lib/bluetooth/bluetoothd

to

ExecStart=/usr/lib/bluetooth/bluetoothd -P input

Configure D-Bus

When a new service is created on the D-Bus, this service needs to be configured.

sudo cp org.yaptb.btkbservice.conf /etc/dbus-1/system.d

Event loop

The original article used Gtk for the event loop. I changed it to the library that I normally use and this removed the warning the original author was getting.

hciconfig

This command has been deprecated in the BlueZ project. https://wiki.archlinux.org/index.php/bluetooth#Deprecated_BlueZ_tools

In the setup of the original article the hciconfig command used to get the BD address. I have modified this so that the code queries the adapter and gets the address.

There were also os.system calls to hciconfig from within the Python. With the new BlueZ D-Bus interface these are unnecessary and have been replaced with D-Bus calls.

Sockets

Moving to a new version (> 3.3?) of Python will not require the import bluetooth line that was there previously. More information on the Python socket support of Bluetooth is available at: https://docs.python.org/3.4/library/socket.html#socket-families

Registering of Profile

As the original author noted, the registering of the HID profile does not seem to work as documented at: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/profile-api.txt The NewConnection method did not seem to get called on a new connection being made. Requests to the BlueZ mailing list did not seem to yield any insight as to why this is.

Pairing

With the settings used in this setup the pairing steps described in the original tutorial should not be required. While this is probably not a sensible choice for a real situation, for this experiment I chose convenience over security.

Below is a transcript from the two terminal I had open for this experiment.

Terminal 1

pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/server $ sudo service bluetooth stop
pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/server $ sudo /usr/lib/bluetooth/bluetoothd -P input &
pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/server $ sudo python3 btk_server.py
Setting up service
Setting up BT device
Configuring for name BT_HID_Keyboard
Configuring Bluez Profile
Reading service record
Profile registered
Waiting for connections

Scan for the keyboard Pi and connect from main computer

8C:2D:AA:44:0E:3A connected on the control socket
8C:2D:AA:44:0E:3A connected on the interrupt channel

Terminal 2

pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/keyboard $ python3 kb_client.py
Setting up keyboard
found a keyboard
starting event loop
Listening...
#!/usr/bin/python3
"""
Bluetooth HID keyboard emulator DBUS Service
Original idea taken from:
http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html
Moved to Python 3 and tested with BlueZ 5.43
"""
import os
import sys
import dbus
import dbus.service
import socket
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
class HumanInterfaceDeviceProfile(dbus.service.Object):
"""
BlueZ D-Bus Profile for HID
"""
fd = -1
@dbus.service.method('org.bluez.Profile1',
in_signature='', out_signature='')
def Release(self):
print('Release')
mainloop.quit()
@dbus.service.method('org.bluez.Profile1',
in_signature='oha{sv}', out_signature='')
def NewConnection(self, path, fd, properties):
self.fd = fd.take()
print('NewConnection({}, {})'.format(path, self.fd))
for key in properties.keys():
if key == 'Version' or key == 'Features':
print(' {} = 0x{:04x}'.format(key,
properties[key]))
else:
print(' {} = {}'.format(key, properties[key]))
@dbus.service.method('org.bluez.Profile1',
in_signature='o', out_signature='')
def RequestDisconnection(self, path):
print('RequestDisconnection {}'.format(path))
if self.fd > 0:
os.close(self.fd)
self.fd = -1
class BTKbDevice:
"""
create a bluetooth device to emulate a HID keyboard
"""
MY_DEV_NAME = 'BT_HID_Keyboard'
# Service port - must match port configured in SDP record
P_CTRL = 17
# Service port - must match port configured in SDP record#Interrrupt port
P_INTR = 19
# BlueZ dbus
PROFILE_DBUS_PATH = '/bluez/yaptb/btkb_profile'
ADAPTER_IFACE = 'org.bluez.Adapter1'
DEVICE_INTERFACE = 'org.bluez.Device1'
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
# file path of the sdp record to laod
install_dir = os.path.dirname(os.path.realpath(__file__))
SDP_RECORD_PATH = os.path.join(install_dir,
'sdp_record.xml')
# UUID for HID service (1124)
# https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
UUID = '00001124-0000-1000-8000-00805f9b34fb'
def __init__(self, hci=0):
self.scontrol = None
self.ccontrol = None # Socket object for control
self.sinterrupt = None
self.cinterrupt = None # Socket object for interrupt
self.dev_path = '/org/bluez/hci{}'.format(hci)
print('Setting up BT device')
self.bus = dbus.SystemBus()
self.adapter_methods = dbus.Interface(
self.bus.get_object('org.bluez',
self.dev_path),
self.ADAPTER_IFACE)
self.adapter_property = dbus.Interface(
self.bus.get_object('org.bluez',
self.dev_path),
self.DBUS_PROP_IFACE)
self.bus.add_signal_receiver(self.interfaces_added,
dbus_interface=self.DBUS_OM_IFACE,
signal_name='InterfacesAdded')
self.bus.add_signal_receiver(self._properties_changed,
dbus_interface=self.DBUS_PROP_IFACE,
signal_name='PropertiesChanged',
arg0=self.DEVICE_INTERFACE,
path_keyword='path')
print('Configuring for name {}'.format(BTKbDevice.MY_DEV_NAME))
self.config_hid_profile()
# set the Bluetooth device configuration
self.alias = BTKbDevice.MY_DEV_NAME
self.discoverabletimeout = 0
self.discoverable = True
def interfaces_added(self):
pass
def _properties_changed(self, interface, changed, invalidated, path):
if self.on_disconnect is not None:
if 'Connected' in changed:
if not changed['Connected']:
self.on_disconnect()
def on_disconnect(self):
print('The client has been disconnect')
self.listen()
@property
def address(self):
"""Return the adapter MAC address."""
return self.adapter_property.Get(self.ADAPTER_IFACE,
'Address')
@property
def powered(self):
"""
power state of the Adapter.
"""
return self.adapter_property.Get(self.ADAPTER_IFACE, 'Powered')
@powered.setter
def powered(self, new_state):
self.adapter_property.Set(self.ADAPTER_IFACE, 'Powered', new_state)
@property
def alias(self):
return self.adapter_property.Get(self.ADAPTER_IFACE,
'Alias')
@alias.setter
def alias(self, new_alias):
self.adapter_property.Set(self.ADAPTER_IFACE,
'Alias',
new_alias)
@property
def discoverabletimeout(self):
"""Discoverable timeout of the Adapter."""
return self.adapter_props.Get(self.ADAPTER_IFACE,
'DiscoverableTimeout')
@discoverabletimeout.setter
def discoverabletimeout(self, new_timeout):
self.adapter_property.Set(self.ADAPTER_IFACE,
'DiscoverableTimeout',
dbus.UInt32(new_timeout))
@property
def discoverable(self):
"""Discoverable state of the Adapter."""
return self.adapter_props.Get(
self.ADAPTER_INTERFACE, 'Discoverable')
@discoverable.setter
def discoverable(self, new_state):
self.adapter_property.Set(self.ADAPTER_IFACE,
'Discoverable',
new_state)
def config_hid_profile(self):
"""
Setup and register HID Profile
"""
print('Configuring Bluez Profile')
service_record = self.read_sdp_service_record()
opts = {
'Role': 'server',
'RequireAuthentication': False,
'RequireAuthorization': False,
'AutoConnect': True,
'ServiceRecord': service_record,
}
manager = dbus.Interface(self.bus.get_object('org.bluez',
'/org/bluez'),
'org.bluez.ProfileManager1')
HumanInterfaceDeviceProfile(self.bus,
BTKbDevice.PROFILE_DBUS_PATH)
manager.RegisterProfile(BTKbDevice.PROFILE_DBUS_PATH,
BTKbDevice.UUID,
opts)
print('Profile registered ')
@staticmethod
def read_sdp_service_record():
"""
Read and return SDP record from a file
:return: (string) SDP record
"""
print('Reading service record')
try:
fh = open(BTKbDevice.SDP_RECORD_PATH, 'r')
except OSError:
sys.exit('Could not open the sdp record. Exiting...')
return fh.read()
def listen(self):
"""
Listen for connections coming from HID client
"""
print('Waiting for connections')
self.scontrol = socket.socket(socket.AF_BLUETOOTH,
socket.SOCK_SEQPACKET,
socket.BTPROTO_L2CAP)
self.scontrol.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sinterrupt = socket.socket(socket.AF_BLUETOOTH,
socket.SOCK_SEQPACKET,
socket.BTPROTO_L2CAP)
self.sinterrupt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.scontrol.bind((self.address, self.P_CTRL))
self.sinterrupt.bind((self.address, self.P_INTR))
# Start listening on the server sockets
self.scontrol.listen(1) # Limit of 1 connection
self.sinterrupt.listen(1)
self.ccontrol, cinfo = self.scontrol.accept()
print('{} connected on the control socket'.format(cinfo[0]))
self.cinterrupt, cinfo = self.sinterrupt.accept()
print('{} connected on the interrupt channel'.format(cinfo[0]))
def send(self, msg):
"""
Send HID message
:param msg: (bytes) HID packet to send
"""
self.cinterrupt.send(bytes(bytearray(msg)))
class BTKbService(dbus.service.Object):
"""
Setup of a D-Bus service to recieve HID messages from other
processes.
Send the recieved HID messages to the Bluetooth HID server to send
"""
def __init__(self):
print('Setting up service')
bus_name = dbus.service.BusName('org.yaptb.btkbservice',
bus=dbus.SystemBus())
dbus.service.Object.__init__(self, bus_name, '/org/yaptb/btkbservice')
# create and setup our device
self.device = BTKbDevice()
# start listening for socket connections
self.device.listen()
@dbus.service.method('org.yaptb.btkbservice',
in_signature='ay')
def send_keys(self, cmd):
self.device.send(cmd)
if __name__ == '__main__':
# The sockets require root permission
if not os.geteuid() == 0:
sys.exit('Only root can run this script')
DBusGMainLoop(set_as_default=True)
myservice = BTKbService()
mainloop = GLib.MainLoop()
mainloop.run()
import dbus
import evdev
import keymap
from time import sleep
HID_DBUS = 'org.yaptb.btkbservice'
HID_SRVC = '/org/yaptb/btkbservice'
class Kbrd:
"""
Take the events from a physically attached keyboard and send the
HID messages to the keyboard D-Bus server.
"""
def __init__(self):
self.target_length = 6
self.mod_keys = 0b00000000
self.pressed_keys = []
self.have_kb = False
self.dev = None
self.bus = dbus.SystemBus()
self.btkobject = self.bus.get_object(HID_DBUS,
HID_SRVC)
self.btk_service = dbus.Interface(self.btkobject,
HID_DBUS)
self.wait_for_keyboard()
def wait_for_keyboard(self, event_id=0):
"""
Connect to the input event file for the keyboard.
Can take a parameter of an integer that gets appended to the end of
/dev/input/event
:param event_id: Optional parameter if the keyboard is not event0
"""
while not self.have_kb:
try:
# try and get a keyboard - should always be event0 as
# we're only plugging one thing in
self.dev = evdev.InputDevice('/dev/input/event{}'.format(
event_id))
self.have_kb = True
except OSError:
print('Keyboard not found, waiting 3 seconds and retrying')
sleep(3)
print('found a keyboard')
def update_mod_keys(self, mod_key, value):
"""
Which modifier keys are active is stored in an 8 bit number.
Each bit represents a different key. This method takes which bit
and its new value as input
:param mod_key: The value of the bit to be updated with new value
:param value: Binary 1 or 0 depending if pressed or released
"""
bit_mask = 1 << (7-mod_key)
if value: # set bit
self.mod_keys |= bit_mask
else: # clear bit
self.mod_keys &= ~bit_mask
def update_keys(self, norm_key, value):
if value < 1:
self.pressed_keys.remove(norm_key)
elif norm_key not in self.pressed_keys:
self.pressed_keys.insert(0, norm_key)
len_delta = self.target_length - len(self.pressed_keys)
if len_delta < 0:
self.pressed_keys = self.pressed_keys[:len_delta]
elif len_delta > 0:
self.pressed_keys.extend([0] * len_delta)
@property
def state(self):
"""
property with the HID message to send for the current keys pressed
on the keyboards
:return: bytes of HID message
"""
return [0xA1, 0x01, self.mod_keys, 0, *self.pressed_keys]
def send_keys(self):
self.btk_service.send_keys(self.state)
def event_loop(self):
"""
Loop to check for keyboard events and send HID message
over D-Bus keyboard service when they happen
"""
print('Listening...')
for event in self.dev.read_loop():
# only bother if we hit a key and its an up or down event
if event.type == evdev.ecodes.EV_KEY and event.value < 2:
key_str = evdev.ecodes.KEY[event.code]
mod_key = keymap.modkey(key_str)
if mod_key > -1:
self.update_mod_keys(mod_key, event.value)
else:
self.update_keys(keymap.convert(key_str), event.value)
self.send_keys()
if __name__ == '__main__':
print('Setting up keyboard')
kb = Kbrd()
print('starting event loop')
kb.event_loop()
#
# www.linuxuser.co.uk/tutorials/emulate-a-bluetooth-keyboard-with-the-raspberry-pi
#
#
#
# Convert value returned from Linux event device ("evdev") to a HID code.
# This is reverse of what's actually hardcoded in the kernel.
#
# Lubomir Rintel <lkundrak@v3.sk>
# License: GPL
#
# Ported to a Python module by Liam Fraser.
#
keytable = {
"KEY_RESERVED": 0,
"KEY_ESC": 41,
"KEY_1": 30,
"KEY_2": 31,
"KEY_3": 32,
"KEY_4": 33,
"KEY_5": 34,
"KEY_6": 35,
"KEY_7": 36,
"KEY_8": 37,
"KEY_9": 38,
"KEY_0": 39,
"KEY_MINUS": 45,
"KEY_EQUAL": 46,
"KEY_BACKSPACE": 42,
"KEY_TAB": 43,
"KEY_Q": 20,
"KEY_W": 26,
"KEY_E": 8,
"KEY_R": 21,
"KEY_T": 23,
"KEY_Y": 28,
"KEY_U": 24,
"KEY_I": 12,
"KEY_O": 18,
"KEY_P": 19,
"KEY_LEFTBRACE": 47,
"KEY_RIGHTBRACE": 48,
"KEY_ENTER": 40,
"KEY_LEFTCTRL": 224,
"KEY_A": 4,
"KEY_S": 22,
"KEY_D": 7,
"KEY_F": 9,
"KEY_G": 10,
"KEY_H": 11,
"KEY_J": 13,
"KEY_K": 14,
"KEY_L": 15,
"KEY_SEMICOLON": 51,
"KEY_APOSTROPHE": 52,
"KEY_GRAVE": 53,
"KEY_LEFTSHIFT": 225,
"KEY_BACKSLASH": 50,
"KEY_Z": 29,
"KEY_X": 27,
"KEY_C": 6,
"KEY_V": 25,
"KEY_B": 5,
"KEY_N": 17,
"KEY_M": 16,
"KEY_COMMA": 54,
"KEY_DOT": 55,
"KEY_SLASH": 56,
"KEY_RIGHTSHIFT": 229,
"KEY_KPASTERISK": 85,
"KEY_LEFTALT": 226,
"KEY_SPACE": 44,
"KEY_CAPSLOCK": 57,
"KEY_F1": 58,
"KEY_F2": 59,
"KEY_F3": 60,
"KEY_F4": 61,
"KEY_F5": 62,
"KEY_F6": 63,
"KEY_F7": 64,
"KEY_F8": 65,
"KEY_F9": 66,
"KEY_F10": 67,
"KEY_NUMLOCK": 83,
"KEY_SCROLLLOCK": 71,
"KEY_KP7": 95,
"KEY_KP8": 96,
"KEY_KP9": 97,
"KEY_KPMINUS": 86,
"KEY_KP4": 92,
"KEY_KP5": 93,
"KEY_KP6": 94,
"KEY_KPPLUS": 87,
"KEY_KP1": 89,
"KEY_KP2": 90,
"KEY_KP3": 91,
"KEY_KP0": 98,
"KEY_KPDOT": 99,
"KEY_ZENKAKUHANKAKU": 148,
"KEY_102ND": 100,
"KEY_F11": 68,
"KEY_F12": 69,
"KEY_RO": 135,
"KEY_KATAKANA": 146,
"KEY_HIRAGANA": 147,
"KEY_HENKAN": 138,
"KEY_KATAKANAHIRAGANA": 136,
"KEY_MUHENKAN": 139,
"KEY_KPJPCOMMA": 140,
"KEY_KPENTER": 88,
"KEY_RIGHTCTRL": 228,
"KEY_KPSLASH": 84,
"KEY_SYSRQ": 70,
"KEY_RIGHTALT": 230,
"KEY_HOME": 74,
"KEY_UP": 82,
"KEY_PAGEUP": 75,
"KEY_LEFT": 80,
"KEY_RIGHT": 79,
"KEY_END": 77,
"KEY_DOWN": 81,
"KEY_PAGEDOWN": 78,
"KEY_INSERT": 73,
"KEY_DELETE": 76,
"KEY_MUTE": 239,
"KEY_VOLUMEDOWN": 238,
"KEY_VOLUMEUP": 237,
"KEY_POWER": 102,
"KEY_KPEQUAL": 103,
"KEY_PAUSE": 72,
"KEY_KPCOMMA": 133,
"KEY_HANGEUL": 144,
"KEY_HANJA": 145,
"KEY_YEN": 137,
"KEY_LEFTMETA": 227,
"KEY_RIGHTMETA": 231,
"KEY_COMPOSE": 101,
"KEY_STOP": 243,
"KEY_AGAIN": 121,
"KEY_PROPS": 118,
"KEY_UNDO": 122,
"KEY_FRONT": 119,
"KEY_COPY": 124,
"KEY_OPEN": 116,
"KEY_PASTE": 125,
"KEY_FIND": 244,
"KEY_CUT": 123,
"KEY_HELP": 117,
"KEY_CALC": 251,
"KEY_SLEEP": 248,
"KEY_WWW": 240,
"KEY_COFFEE": 249,
"KEY_BACK": 241,
"KEY_FORWARD": 242,
"KEY_EJECTCD": 236,
"KEY_NEXTSONG": 235,
"KEY_PLAYPAUSE": 232,
"KEY_PREVIOUSSONG": 234,
"KEY_STOPCD": 233,
"KEY_REFRESH": 250,
"KEY_EDIT": 247,
"KEY_SCROLLUP": 245,
"KEY_SCROLLDOWN": 246,
"KEY_F13": 104,
"KEY_F14": 105,
"KEY_F15": 106,
"KEY_F16": 107,
"KEY_F17": 108,
"KEY_F18": 109,
"KEY_F19": 110,
"KEY_F20": 111,
"KEY_F21": 112,
"KEY_F22": 113,
"KEY_F23": 114,
"KEY_F24": 115
}
# Map modifier keys to array element in the bit array
modkeys = {
"KEY_RIGHTMETA": 0,
"KEY_RIGHTALT": 1,
"KEY_RIGHTSHIFT": 2,
"KEY_RIGHTCTRL": 3,
"KEY_LEFTMETA": 4,
"KEY_LEFTALT": 5,
"KEY_LEFTSHIFT": 6,
"KEY_LEFTCTRL": 7
}
def convert(evdev_keycode):
return keytable[evdev_keycode]
def modkey(evdev_keycode):
if evdev_keycode in modkeys:
return modkeys[evdev_keycode]
else:
return -1 # Return an invalid array element
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy context="default">
<allow own="org.yaptb.btkbservice"/>
<allow send_destination="org.yaptb.btkbservice"/>
</policy>
</busconfig>
<?xml version="1.0" encoding="UTF-8" ?>
<record>
<attribute id="0x0001">
<sequence>
<uuid value="0x1124" />
</sequence>
</attribute>
<attribute id="0x0004">
<sequence>
<sequence>
<uuid value="0x0100" />
<uint16 value="0x0011" />
</sequence>
<sequence>
<uuid value="0x0011" />
</sequence>
</sequence>
</attribute>
<attribute id="0x0005">
<sequence>
<uuid value="0x1002" />
</sequence>
</attribute>
<attribute id="0x0006">
<sequence>
<uint16 value="0x656e" />
<uint16 value="0x006a" />
<uint16 value="0x0100" />
</sequence>
</attribute>
<attribute id="0x0009">
<sequence>
<sequence>
<uuid value="0x1124" />
<uint16 value="0x0100" />
</sequence>
</sequence>
</attribute>
<attribute id="0x000d">
<sequence>
<sequence>
<sequence>
<uuid value="0x0100" />
<uint16 value="0x0013" />
</sequence>
<sequence>
<uuid value="0x0011" />
</sequence>
</sequence>
</sequence>
</attribute>
<attribute id="0x0100">
<text value="Raspberry Pi Virtual Keyboard" />
</attribute>
<attribute id="0x0101">
<text value="USB > BT Keyboard" />
</attribute>
<attribute id="0x0102">
<text value="Raspberry Pi" />
</attribute>
<attribute id="0x0200">
<uint16 value="0x0100" />
</attribute>
<attribute id="0x0201">
<uint16 value="0x0111" />
</attribute>
<attribute id="0x0202">
<uint8 value="0x40" />
</attribute>
<attribute id="0x0203">
<uint8 value="0x00" />
</attribute>
<attribute id="0x0204">
<boolean value="false" />
</attribute>
<attribute id="0x0205">
<boolean value="false" />
</attribute>
<attribute id="0x0206">
<sequence>
<sequence>
<uint8 value="0x22" />
<text encoding="hex" value="05010906a101850175019508050719e029e715002501810295017508810395057501050819012905910295017503910395067508150026ff000507190029ff8100c0050c0901a1018503150025017501950b0a23020a21020ab10109b809b609cd09b509e209ea09e9093081029501750d8103c0" />
</sequence>
</sequence>
</attribute>
<attribute id="0x0207">
<sequence>
<sequence>
<uint16 value="0x0409" />
<uint16 value="0x0100" />
</sequence>
</sequence>
</attribute>
<attribute id="0x020b">
<uint16 value="0x0100" />
</attribute>
<attribute id="0x020c">
<uint16 value="0x0c80" />
</attribute>
<attribute id="0x020d">
<boolean value="true" />
</attribute>
<attribute id="0x020e">
<boolean value="false" />
</attribute>
<attribute id="0x020f">
<uint16 value="0x0640" />
</attribute>
<attribute id="0x0210">
<uint16 value="0x0320" />
</attribute>
</record>
@ackerleytng
Copy link

ackerleytng commented Sep 28, 2021

I'm trying to follow this guide to build a bluetooth "keyboard" that is able to send a few keystrokes, such as the equivalent of alt-tab, to my Android phone.

So far, I've been able to

  • Register a profile through org.bluez.ProfileManager1.RegisterProfile with an sdp_record.xml that I'm not sure is completely correct
  • Register a service through org.bluez.Profile1 that exports a few methods

I would start the program on my laptop (linux), which does the above 2 registrations, and then use bluetoothctl to manually do discoverable on, and then get my Android phone to pair with the laptop. Pairing is successful, but the service, which listens on ports 0x11 and 0x13, never seem to receive any connections. accept() is never called.

Is my overall understanding right? The laptop, acting as the bluetooth keyboard, will serve a profile and a service (SDP), that the phone will read. When it tries to pair with the laptop, it will read the ports listed in the profile and attempt to connect to those ports?

Any ideas on where I can start debugging?

@ukBaz
Copy link
Author

ukBaz commented Sep 28, 2021

The general advice I give to get Bluetooth debug information on Linux is the following.

When running your script have separate terminals open with the following running to get more debug information:

  • bluetootctl
  • journalctl -f -u bluetooth
  • sudo busctl monitor org.bluez
  • sudo btmon

Your general understanding sounds correct but the trouble is always in the detail. You don't mention about re-configuring the bluetooth daemon which is a very important step. Does your laptop show up as a keyboard after you have paired the two devices?

@ackerleytng
Copy link

Thank you soooo much! You hit the nail on the head!

I'm trying to reproduce this in golang.

When binding, I initially didn't specify the address to bind to. Binding succeeded, but I'm also not sure which interface it was binding to, and there was no EADDRINUSE error. If I read and use the org.bluez.Adapter1 Address read from /org/bluez/hci0, then I start getting the EADDRINUSE error.

I definitely needed to reconfigure the bluetooth daemon to remove the default input plugin by stopping bluetoothd and then restarting it as follows

sudo /usr/lib/bluetooth/bluetoothd --noplugin input

(--noplugin is the same as -P documented above)

Thanks once again!

@TehseenHasan
Copy link

TehseenHasan commented Oct 5, 2021

I followed the procedure and Now My PC is connected to the Bluetooth of the Raspberry Pi Zero W. Terminal 1 is showing this :

>>> %cd /home/pi/Desktop/BluetoothHID
>>> %Run btk_server.py
Setting up service
Setting up BT device
Configuring for name BT_HID_Keyboard
Configuring Bluez Profile
Reading service record
Profile registered 
Waiting for connections
D8:0F:99:73:BC:14 connected on the control socket
D8:0F:99:73:BC:14 connected on the interrupt channel

But when I run the kb_client.py it is giving this error:

sudo python3 kb_client.py
Setting up keyboard
Keyboard not found, waiting 3 seconds and retrying
found a keyboard
Keyboard not found, waiting 3 seconds and retrying
found a keyboard
Keyboard not found, waiting 3 seconds and retrying
found a keyboard
Keyboard not found, waiting 3 seconds and retrying

and So on......

I am using my Raspberry Pi through VNC Server and there are no Physical Keyboard or Mouse is attached to the Rasberry Pi USB port.

I want to send the text data from my Custom python Code to the PC over Bluetooth HID. It seems that in your kb_client.py code you are trying to detect a physical keyboard attached to the RPi and then sending its keystrokes over the Bluetooth.

But I just need to send a custom string from my python code, not from the keyboard. Please guide me on how can I do this. Thanks a lot!

UPDATE!

for testing, I just connected a USB keyboard to my Raspberry Pi Zero W and then run sudo python3 kb.client.py and now It is working perfectly and I am receiving the keystrokes from the Raspberry Pi's keyboard to my PC over Bluetooth HID.

Now my only question is that how can I send the custom string data through my python code instead of using a physical keyboard.?
Thanks!

@ukBaz
Copy link
Author

ukBaz commented Oct 5, 2021

@TehseenHasan, this kind of question has been asked further up in this gist. I think it starts about here https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#gistcomment-2996833

@joyobooto
Copy link

Is there a plan to get mouse support working? I've found a similar project https://github.com/quangthanh010290/keyboard_mouse_emulate_on_raspberry/blob/master/mouse/mouse_client.py which I'm trying to migrate over but my knowledge over the libraries used is limiting me a little...

@HeadHodge
Copy link

you can use your existing code by modifying your sdp file to add a second usb hid report descriptor for a mouse.

it takes effort, but this tool helps: https://eleccelerator.com/usbdescreqparser/

once the report descriptor is modified, you send reports for either the keyboard or mouse by specifying the correct report id in your reports.

Here is an example of a report descriptor i made, that works well, that has 3 reports for a keyboard, mouse and consumer device.

//Report Reference Id = 1 (128 key Keyboard)
//Modifier Byte: bit 0 is L CTRL, bit 1 is L SHIFT, bit 2 is L ALT, bit 3 is L GUI, bit 4 is R CTRL, bit 5 is R SHIFT, bit 6 is R ALT, and bit 7 is R GUI
0x05, 0x01, 	  // Usage Page (Generic Desktop Ctrls)
0x09, 0x06, 	  // Usage (Keyboard)
0xa1, 0x01, 	  // Collection (Application)
0x85, 0x01, 	  //   Report ID (1)
0x05, 0x07, 	  //   Usage Page (Kbrd/Keypad)
0x19, 0xe0, 	  //   Usage Minimum (0xE0)
0x29, 0xe7, 	  //   Usage Maximum (0xE7)
0x15, 0x00, 	  //   Logical Minimum (0)
0x25, 0x01, 	  //   Logical Maximum (1)
0x75, 0x01, 	  //   Report Size (1)
0x95, 0x08, 	  //   Report Count (8)
0x81, 0x02, 	  //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01, 	  //   Report Count (1)
0x75, 0x08, 	  //   Report Size (8)
0x15, 0x00, 	  //   Logical Minimum (0)
0x25, 0x65, 	  //   Logical Maximum (101)
0x05, 0x07, 	  //   Usage Page (Kbrd/Keypad)
0x19, 0x00, 	  //   Usage Minimum (0x00)
0x29, 0x65, 	  //   Usage Maximum (0x65)
0x81, 0x00, 	  //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xc0,			  // End Collection

//Report Reference Id = 2 (Consumer Entertainment Control)
0x05, 0x0C, 	  // Usage Page (Consumer)
0x09, 0x01, 	  // Usage (Consumer Control)
0xA1, 0x01, 	  // Collection (Application)
0x85, 0x02, 	  //   Report ID (2)
0x75, 0x10, 	  //   Report Size (16)
0x95, 0x01, 	  //   Report Count (1)
0x15, 0x01,		  //   Logical Minimum (1)
0x26, 0xff, 0x07, //   Logical Maximum (2047)
0x19, 0x01, 	  //   Usage Minimum (Consumer Control)
0x2A, 0xff, 0x07, //   Usage Maximum (0x07FF)
0x81, 0x00, 	  //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, 	  	  	  // End Collection

//Report Reference Id = 3 (Generic Mouse)
0x05, 0x01,       // Usage Page (Generic Desktop Ctrls)
0x09, 0x02,       // Usage (Mouse)
0xA1, 0x01,       // Collection (Application)
0x85, 0x03, 	  //   Report ID (3)
0x09, 0x01,       //   Usage (Pointer)
0xA1, 0x00,       //   Collection (Physical)
0x05, 0x09,       //     Usage Page (Button)
0x19, 0x01,       //     Usage Minimum (0x01)
0x29, 0x03,       //     Usage Maximum (0x03)
0x15, 0x00,       //     Logical Minimum (0)
0x25, 0x01,       //     Logical Maximum (1)
0x95, 0x03,       //     Report Count (3)
0x75, 0x01,       //     Report Size (1)
0x81, 0x02,       //     Input (Data,Var,Abs,No Wrap,Linear,...)
0x95, 0x01,       //     Report Count (1)
0x75, 0x05,       //     Report Size (5)
0x81, 0x03,       //     Input (Const,Var,Abs,No Wrap,Linear,...)
0x05, 0x01,       //     Usage Page (Generic Desktop Ctrls)
0x09, 0x30,       //     Usage (X)
0x09, 0x31,       //     Usage (Y)
0x15, 0x81,       //     Logical Minimum (-127)
0x25, 0x7F,       //     Logical Maximum (127)
0x75, 0x08,       //     Report Size (8)
0x95, 0x02,       //     Report Count (2)
0x81, 0x06,       //     Input (Data,Var,Rel,No Wrap,Linear,...)
0xC0,             //   End Collection
0xC0,             // End Collection

@joyobooto
Copy link

Thanks, I've made a little progress (just getting mouse buttons working). Wondering if you have a code sample on how you sent using your report descriptor? My modifications for the mouse client detect buttons correctly but when I send button info, it triggers media keys for reasons I do not know yet...

@HeadHodge
Copy link

hi,

i don't use bt 3.0 classic any longer, just bt 4.2 or bt 5.0, so don't have any good code snips for you.

looking at report 3 (mouse) above, the report code boils down to a report of 3 bytes.

byte 1: uses 1st 3 bits of the byte to determine button presses. 0-no buttons, 1-left button, 2-right button, 3-center button. the remaining bits of the byte are ignored.

byte 2: X-axis cursor relative offset movement from current cursor location.

byte 3: Y-axis cursor relative offset movement from current cursor location.

this is from memory but example use something like:

    elif(reportNum == 3):    
        #Send Report #3
        print('SEND Report 3')
        #Send Report #3
        await loop.sock_sendall(options['connection'], bytes([ 0xA1, reportNum, buttons, x-axis, y-axis ]))
        await asyncio.sleep(hold)  
        await loop.sock_sendall(options['connection'], bytes([ 0xA1, reportNum, 0, 0, 0 ]))
        return        

@ams1
Copy link

ams1 commented Apr 26, 2023

This page just made my day! :) THANK YOU! x 10

Could you please share a few thoughts (if it doesn't take a lot of time) on making both the btk_server and the kb_client start automatically? I tried a few things but failed.

BTW it worked on a raspberry pi zero w.

Again THANK YOU for taking the time to make this clean.

@ukBaz
Copy link
Author

ukBaz commented Apr 26, 2023

Hi @ams1,

Thanks for the kind words.

I always point people at the following very useful blog for starting scripts automatically at start-up:
https://blog.usedbytes.com/2019/11/run-at-startup-without-rc.local/

@ams1
Copy link

ams1 commented Apr 27, 2023

@ukBaz - great article, you are an awesome person!

Based on that and on "Siri" (aka ChatGPT 😄), using the following prompt:

i want to configure exec of 2 python services: main service and child service.
* the main service requires bluez service to be started
* both services require sudo to run
* both services are run by user root 
* the main service has to start before and is required by the child service
* the main service will be enabled for automatic start
* i want the child service to start automatically when the main service starts
* if the main service crashes or is stopped, i want the child service to be automatically stopped
* i want to always restart after 5 seconds both the main and child service

I've got:

[Unit]
Description=Main Service
Requires=bluetooth.service
After=bluetooth.service

[Service]
Type=exec
User=root
ExecStart=/usr/bin/sudo /usr/bin/python3 /path/to/main_service.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

and

[Unit]
Description=Child Service
Requires=main_service.service
After=main_service.service

[Service]
Type=exec
User=root
ExecStart=/usr/bin/sudo /usr/bin/python3 /path/to/child_service.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

where main service is "btk_server" and child service is "kb_client".

working like a charm!

note: i had to enable BOTH services for automatic start...

P.S.: "Siri" didn't know about the exec type - without reading that article, I would have been back here with a question 😁 :
image

@ukBaz
Copy link
Author

ukBaz commented May 1, 2023

@ams1 Thanks for sharing this update. Very interesting.

@VCBE123
Copy link

VCBE123 commented Aug 9, 2023

Configure D-Bus
When a new service is created on the D-Bus, this service needs to be configured.

sudo cp org.yaptb.btkkbservice.conf /etc/dbus-1/system.d

is this btkkbservice.conf a typro? Btw, I meet a problem, I can connect the BT_HID_keyboard with my host and after run kc_client.py it says

setting up keyboard
found a keyboard
starting event loop
listening...

But when i push some key it output nothing.

I follow the debug metrod and find the journalctl -f -u blutetooth output

Stopping SDP server
Stopped Bluetooth service
starting Bluetooth service
Bluetooth daemon 5.48
D-Bbus setup failed: Name already in use
Unable to get on D-Bus
bluetooth.service: Main process exited,code =exited,status=1/FAILURE
bluetooth.service: Failed with result 'exit-code'
Failed to start Bluetooth service

@ukBaz
Copy link
Author

ukBaz commented Aug 9, 2023

is this btkkbservice.conf a typro?
@VCBE123 It would certainly seem like it.

As for the debug information... There isn't enough context for me to really speculate as to what the issue might be.

@VCBE123
Copy link

VCBE123 commented Aug 10, 2023

is this btkkbservice.conf a typro?
@VCBE123 It would certainly seem like it.

As for the debug information... There isn't enough context for me to really speculate as to what the issue might be.

Thank you for your answer, I solved the problem above. But when My host connect the device, it output

connected on the control socket
connected on the interrupt channel
The client has been disconnect
Waiting for connections

The connection seems not stable, is there any advice ?
BTW I want set my device as a gamepad, so I want to know how to set the message in the state

 @property
    def state(self):
        """
        property with the HID message to send for the current keys pressed
        on the keyboards
        :return: bytes of HID message
        """
        return [0xA1, 0x01, self.mod_keys, 0, *self.pressed_keys]

@decook25
Copy link

decook25 commented Sep 9, 2023

@VCBE123 How did you solve the problem: "is this btkkbservice.conf a typro?" I am stuck in the same place. @ukBaz this is awesome!! struggling hard figuring something out, seems like this is exactly what I was looking for!! just gotta get past that line

@ukBaz
Copy link
Author

ukBaz commented Sep 10, 2023

@VCBE123 and @decook25 ,
The typo has been fixed so it should consistently say btkbservice.conf everywhere and not btkkbservice.conf

The issue of a client connecting and disconnecting is typically caused by the client not finding the service it was expecting on the device it is trying to connect to. It connects, looks for the service it wants, doesn't find it so disconnects. This is speculation on my part. sudo btmon running in a different window will give you more information. Those logs can be taken in to wireshark to make them easier to read.

For the HID meassage @VCBE123 you will need to look at specs (https://www.usb.org/hid) as I'm not familiar with what is required for a game controller.

@yesimxev
Copy link

Such a great work! Is there a way to turn of authentication/pin request? I can't connect from a regular device with no cli

@ukBaz
Copy link
Author

ukBaz commented Sep 23, 2023

@yesimxev , I'm not sure I understand your question as RequireAuthentication and RequireAuthorization are both set to False in the code above. Maybe you can run sudo btmon to see if there is more debug information as to what is requesting the authentication?

@yesimxev
Copy link

yesimxev commented Sep 23, 2023

I can confirm pairing process needs two manual confirmation (yes/no) in bluetoothctl. I'm looking to automate this via results from Google 😄 but it would be great if there's something it could be done in this script to avoid manual interaction.

@ukBaz
Copy link
Author

ukBaz commented Sep 23, 2023

@yesimxev, I'm trying to say that the HID service that the above scripting sets up doesn't require confirmation. Are you sure it isn't another service that is requesting it? What do the btmon snoop logs says?

@yesimxev
Copy link

I'm not sure where to look for another service, please find the btmon log: https://pastebin.com/RdULEUyu

@ukBaz
Copy link
Author

ukBaz commented Sep 23, 2023

The last line of the log is:

= bluetoothd: src/profile.c:ext_io_disconnected() Unable to get io data for...   53.181715

What is it unable to get data for?
Can you set the logging so it isn't truncating the line?
If this is information coming from bluetoothd it might be available with journalctl -r -u bluetooth.

Again I'll draw your attention to the fact that pairing is not required for this service. I suspect the device that is trying to connect to the RPi is trying to connect the audio service and that is the issue.

@yesimxev
Copy link

yesimxev commented Sep 23, 2023

That may be the case. Is there a way to disable audio service? I'll try to just use keyboard bt class. I'll try to get more logs tonight. I understand that pairing us not required. I connect to the bt_keyboard with android using the "normal way". Is that ok?

Btw that's the end if the testing, I disconnected the phone before btmon logging

@yesimxev
Copy link

I tried various workarounds and disabling all the services with sdptool, none helped. Then I found someone solved similar issue by running bluetoothctl --agent NoInputNoOutput in background

@ByteMe666
Copy link

ByteMe666 commented Feb 16, 2024

Hi @ukBaz ,
I've just made efforts recently going in the same direction as you, and as I started out I had found various older projects, which I lost track of actually. The oldest one was 9 years old, and it seems every 3 years someone updated it. Your blog post was helpful and the most recent, so I figured I'd reach out and tell you about my findings.

The main finding I have is I managed to get NewConnection to trigger while pairing using only the BlueZ DBus API methods.

My scenario is that I am trying to send specific bluetooth keyboard strokes to my Windows Laptop from my Raspberry Pi. The reason is I wish to programmatically trigger shortcuts wirelessly on my Windows machine, to pop up or hide menus, for reasons too long and irrelevant to detail. The point is I am not trying to emulate an entire keyboard, just some sequences of key presses.

Similarly to you, I found a working solution that resembles what you've documented here, but, I was frustrated I could not initiate a reconnection from the keyboard emulating device (Pi) after the connection was lost. I also felt using sockets and messing with the bluetooth.service was cheating. So, I tried to get it to work using strictly the BlueZ DBus API.

My code implementation is very straightforward and by the book, and I also commentate on important caveats I've circumvented:

  1. A HumanInterfaceDeviceProfile class identical to yours, with keymap.py and sdp_record.xml files identical.
  2. An Agent class implementing required methods which are described in bluez/doc/org.bluez.Agent.rst in the BlueZ source code.
    The implementation is very basic and handles connection attempts while expecting the user to interact with the CLI to type the PIN or write "yes" to authorize a request.
  3. The AgentManager is used to register the aforementioned manager, and the RequestDefaultAgent() method is called to ensure that my agent is used to handle connection attempts with the CLI methods I defined.
    It is important to note that my agent was ignored and not handling connection attempts when I defined, and registered my agent, and then attempted to pair to a device in different sub-methods of a BluetoothFakeKeyboard class I had created merely to compartimentalize my code. Apparently whatever automatic mechanism behind the scenes that makes BlueZ know what agent to use to handle a connection attempt gets deeply confused if you try to split up your code in methods. I have not cared to figure out what exactly went wrong and just unwrapped any code I had so it executed directly in the top most level of my script.
  4. The last critical step was the Profile Registration, the opts dictionary needed an important extra entry:
    "PSM" : dbus.UInt16(17)
    I identified this issue because btmon logs after successful pairing showed that when my laptop tried to use the advertised keyboard service from my Raspberry Pi, it tried to use PSM 17 but was denied because PSM 17 was not supported.
  5. After adding this entry to the dictionary, I set my adapter to discoverable and my NewConnection method from my Agent triggered when I initiated pairing from my laptop.

And this is as far as I got unfortunately. Now my code fails because my laptop tries to connect to PSM 19 for the interrupt channel, but it also gets denied. Unfortunately, poking around the source code of BlueZ I found the file bluez/src/profile.c which has a parse_ext_op() function which seems to indicate that it is pointless to add "PSM" : dbus.UInt16(19) to the dictionary as that would overwrite the "PSM" : dbus.UInt16(17) entry.

Tomorrow I would try to deal with PSM 19 separately using sockets, effectively loosening my restriction of using exclusively the DBus API. My code structure would attempt to track the paired and or connected status using the BlueZ DBus API, and always ensure separately that a socket is always available to deal with PSM 19 separately. Hopefully PSM 17 would be handled by BlueZ as a control connection for the service. I haven't put a lot of thought into it as I write this, but my initial idea would be to make the PSM 19 socket available in the NewConnection method, and reset it when the connection is lost or dropped.

Hopefully I could still manage to complete the program without resorting to tinkering with bluetooth.service. I would use identical keystroke sending methods to yours to write to the file descriptor NewConnection provides for me. The end result would not be as clean as I'd want it to be, but it would be an improvement.

All the best.

@ByteMe666
Copy link

Update on my previous post: it worked.

I setup PSM 19 with sinterrupt = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET,socket.BTPROTO_L2CAP) in the init function of my BlueZ Profile, and listen() at the end of the init function. When NewConnection is triggered I ran sinterrupt.accept(), and the connection is successful. This was for pairing initiated by the Windows laptop. For whatever reason, pairing has never worked when initiated by the Pi. It may be linked to what follows:

I wanted to reconnect after a disconnection. It makes sense to me the Raspberry Pi would initiate the reconnection. Even though the devices were paired and knew each other, the device.Connect() did not work. Capturing with btmon showed inexplicably that my Raspberry Pi attempted to connect to PSM 25 and was denied. Reading the documentation indicated that the Connect() method makes the Raspberry Pi attempt to connect to a service advertised by my Windows laptop, which is not what I wanted, but may explain why attempts were made to connect to PSM 25, but I did not bother to check what service my Windows laptop advertised there. Out of desperation I just created a PSM 17 scontrol socket just like in your code and tried to scrontrol.connect(): funnily enough I got an error message saying it was rejected, immediately followed by a NewConnection message from my BlueZ Profile and a successful reconnection.

As the code stands:

  • I do not fool around with bluetooth.service in any way (no editing the service file, no stopping the service etc)
  • I need to use sockets to handle the PSM 19 interrupt channel for incoming connections and the reconnexion
  • Basic keyboard connection behavior is achieved:
    a. Allow pairing from the laptop wanting to use the keyboard
    b. When pairing occurs, you may simply type the PIN code in the terminal and hit return
    c. Allow reconnection from the keyboard when it detects the paired laptop is present.

With the current version of the code, if I hide under the rug the "PSM 17 poke" error for reconnection, the code will look and behave like an actual bluetooth keyboard.

@jmkim
Copy link

jmkim commented Feb 20, 2024

Hi, I cloned this gist and followed your README.md, but could not pair the device. The device (BT_HID_Keyboard) appeared on my iPad but not appears on my laptop.

Here is my sudo btmon : https://paste.debian.net/1307899/

Could you please help me for figuring out the issue?

bluetoothctl -v :

bluetoothctl: 5.66

Terminal 1 sudo python3 btk_server.py :

Setting up service
Setting up BT device
Configuring for name BT_HID_Keyboard
Configuring Bluez Profile
Reading service record
Profile registered 
Waiting for connections

Terminal 2 sudo python3 kb_client.py :

Setting up keyboard
found a keyboard
starting event loop
Listening...

@yesimxev
Copy link

Did this not work for you? bluetoothctl --agent NoInputNoOutput in another terminal. It will auto accept connections even if it wanted yes/no

@ByteMe666
Copy link

Update on my previous post: it worked.

I setup PSM 19 with sinterrupt = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET,socket.BTPROTO_L2CAP) in the init function of my BlueZ Profile, and listen() at the end of the init function. When NewConnection is triggered I ran sinterrupt.accept(), and the connection is successful. This was for pairing initiated by the Windows laptop. For whatever reason, pairing has never worked when initiated by the Pi. It may be linked to what follows:

I wanted to reconnect after a disconnection. It makes sense to me the Raspberry Pi would initiate the reconnection. Even though the devices were paired and knew each other, the device.Connect() did not work. Capturing with btmon showed inexplicably that my Raspberry Pi attempted to connect to PSM 25 and was denied. Reading the documentation indicated that the Connect() method makes the Raspberry Pi attempt to connect to a service advertised by my Windows laptop, which is not what I wanted, but may explain why attempts were made to connect to PSM 25, but I did not bother to check what service my Windows laptop advertised there. Out of desperation I just created a PSM 17 scontrol socket just like in your code and tried to scrontrol.connect(): funnily enough I got an error message saying it was rejected, immediately followed by a NewConnection message from my BlueZ Profile and a successful reconnection.

As the code stands:

  • I do not fool around with bluetooth.service in any way (no editing the service file, no stopping the service etc)
  • I need to use sockets to handle the PSM 19 interrupt channel for incoming connections and the reconnexion
  • Basic keyboard connection behavior is achieved:
    a. Allow pairing from the laptop wanting to use the keyboard
    b. When pairing occurs, you may simply type the PIN code in the terminal and hit return
    c. Allow reconnection from the keyboard when it detects the paired laptop is present.

With the current version of the code, if I hide under the rug the "PSM 17 poke" error for reconnection, the code will look and behave like an actual bluetooth keyboard.

I want to give the final word on my attempt.

  1. First I want to roll back one thing I said which isn't true: you need to add -P input to bluetoothd.service if you want the setup to work.
  2. I can be paired by, and I can reconnect to the host if the Raspberry Pi bluetooth keyboard disconnects, however the reverse is not true: if the host turns off their bluetooth and then reappear, the reconnection always fails and despite my best efforts to close the file descriptor and PSM 19 channel, I never successfully get a new connection from the host.

This latter point is a show stopper for me, my intended usecase is that the keyboard is always online and that the host drops out occasionally to power off and reappear at a later point in time. As I cannot reestablish the connection in this scenario, I'm giving up on doing a bluetooth keyboard emulation with BlueZ and the Raspberry Pi. I will try again with an ESP32 and the libraries it offers.

@jmkim
Copy link

jmkim commented Feb 26, 2024

@yesimxev your way worked for pairing. Thanks o/ Can I know the meaning of this command?

@yesimxev
Copy link

This command will act like a bluetooth device without screen or keyboard (like a headset where you can't input pairing code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment