Skip to content

Instantly share code, notes, and snippets.

@ukBaz

ukBaz/README.md Secret

Last active Apr 20, 2021
Embed
What would you like to do?
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.btkkbservice.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>
@Octabond

This comment has been minimized.

Copy link

@Octabond Octabond commented Nov 11, 2018

Thanks for sharing thus ukBaz!

When the device disconnects either due to a link failure or it being out of range, I can't connect again. I need to manually restart the emulator and remove and re-add and re-pair the device.

Any solution to this?

@minoskt

This comment has been minimized.

Copy link

@minoskt minoskt commented Jul 27, 2019

Thanks for sharing thus ukBaz!

When the device disconnects either due to a link failure or it being out of range, I can't connect again. I need to manually restart the emulator and remove and re-add and re-pair the device.

Any solution to this?

Just add self.listen() to def on_disconnect(self) method.

@4dTM

This comment has been minimized.

Copy link

@4dTM 4dTM commented Aug 12, 2019

First, thank you so much for making creating this code and detailed instructions! I have to admit that I am a total noob to the wonderful world of the raspberry and maybe you could give me a little hint. (I've just enroled for a bachelor, so developing my skills might take a bit ;)

Some Background: I am currently building a turntable that automatically triggers a camera in predefined positions. I've already managed to create a nice-ish UI with node-red and get all the mechanics running. The last missing piece is the camera trigger for smartphones (I've already implemented the raspi cam but smartphones would greatly improve the usability). After a lot of reasearch I've found out, that I just most smartphones' cameras can be triggered via the "volume-up" button and that cheap bluetooth remotes work by mimicing a HID-device.

Getting to the point: ;)
I've managed to connect my iOS device to the raspberry pi and I would need some kind of script to send the "Volume-Up" command to it. As far as I can understand, your code does much more, but I do not know how to modify it. Maybe you could give me a hint?

Thank you so much in advance!

Best, Thomas

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 13, 2019

Hi Thomas,

I don't know the answer to your question. If I was to attempt it, I would break the problem down so that I could workout the answer.

  1. I would use the current code with a keyboard attached to the Raspberry Pi(RPi). With the RPi connected over Bluetooth to the phone I would press the volume-up button on the keyboard. If that took a picture then I would move on to the next stage.
  2. Modify the send_keys method in the kb_client.py to print out the state (key pressed value) when I took the picture.
    https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#file-kb_client-py-L77-L78
    def send_keys(self):
        print(self.state)
        self.btk_service.send_keys(self.state)
  1. At this point you have proved that pressing the volume-up button works and you know the value that is sent to the phone when you press that button.
    At this point. you don't need the kb_client.py file any more and can replace it with much simpler code.
    Below is an outline of what it might look like:
HID_DBUS = 'org.yaptb.btkbservice'
HID_SRVC = '/org/yaptb/btkbservice'

class CameraRemote:
    """
    Send the HID messages to the keyboard D-Bus server for the volume-up button
    """
    def __init__(self):
        self.bus = dbus.SystemBus()
        self.btkobject = self.bus.get_object(HID_DBUS,
                                             HID_SRVC)
        self.btk_service = dbus.Interface(self.btkobject,
                                          HID_DBUS)

    def take_photo(self):
        the_value_from_step_2 = ????????????
        self.btk_service.send_keys(the_value_from_step_2)

if __name__ == '__main__':
    cr = CameraRemote()
    cr.take_photo()

Remember, I haven't tested any of this. It would just be my plan for how I would attempt to do this.

I hope that helps.

@4dTM

This comment has been minimized.

Copy link

@4dTM 4dTM commented Aug 13, 2019

I hope that helps.

That helped a lot!! Thank you very much for the fast and great answer.
I've spend the last few hours tinkering with the code, So far I can confirm that the pairing works with android and iOS and it is possible to input most values through Keyboard --(USB)--> RPI --(Bluetooth)--> Android/iOS, which I confirmed with the notepad on the phones.
After a little bit of trial and error I found the code to trigger the android's camera (when the camera menu is open):

     self.btk_service.send_keys([161, 1, 0, 0, 40, 0, 0, 0, 0, 0])
     self.btk_service.send_keys([161, 1, 0, 0, 0, 0, 0, 0, 0, 0])

With iOS I have been less successful, so far I can write text, make screenshots, open the search ... but not trigger the camera. But I will give it another try later.

Do you know how I can find out the meaning of each value in the above shown arrays?
For instance
self.btk_service.send_keys([161, 1, 138, 0, 33, 0, 0, 0, 0, 0])
self.btk_service.send_keys([161, 1, 170, 0, 32, 0, 0, 0, 0, 0])
both (and maybe 10 others) trigger the screenshot on the iphone. I've already tried all combinations which are possible when changing the 3rd and 5th value of the array. These are the values that typically change, when looking at the keyboard input... But I do not know what I could be missing. Brute-forcing all other value-combinations would be not feasible, as some combinations lock the phone or close the camera menu or open the search...

BTW, would this be a possible way to remotely control the phone?? It feals weird to be able to do so much on the phone after pairing without needing any further actions...

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 13, 2019

I've already tried all combinations which are possible when changing the 3rd and 5th value of the array.

The 3rd place (or state[2]) in the array is for the modifier keys as defined by modkeys in keymap.py. The numbers refer to their location in the binary 8 bit number. If you hold the left shift key I would expect that to go to 2. left shift and left ctrl would report 3.
The 5th place and the rest of the array (or state[4:]) values are defined by keytable in keymap.py. The number represents which key(s) are pressed

Do you know how I can find out the meaning of each value in the above shown arrays?

     self.btk_service.send_keys([161, 1, 0, 0, 40, 0, 0, 0, 0, 0])
     self.btk_service.send_keys([161, 1, 0, 0, 0, 0, 0, 0, 0, 0])

Looking at the tables in keymap.py I would say that this is the enter key being pressed and released.

Based on your experiments I would assume that you want to send the volume-up (237) key. I would add some delay between the down and up. Maybe something like:

def take_photo(self):
    volume_up = [161, 1, 0, 0, 237, 0, 0, 0, 0, 0]
    all_keys_up = [161, 1, 0, 0, 0, 0, 0, 0, 0, 0]
    self.btk_service.send_keys(volume_up)
    time.sleep(0.001)
    self.btk_service.send_keys(all_keys_up)

To do a combination of power and volume-down which I believe is a screenshot on Android, my expectation is that the following would do it:

def take_screenshot(self):
    pwr_and_vol_down= [161, 1, 0, 0, 102, 238, 0, 0, 0, 0]
    all_keys_up = [161, 1, 0, 0, 0, 0, 0, 0, 0, 0]
    self.btk_service.send_keys(pwr_and_vol_down)
    time.sleep(0.001)
    self.btk_service.send_keys(all_keys_up)

BTW, would this be a possible way to remotely control the phone??

I think you have more experience of this than I do :-)
I searched for "external keyboard shortcuts with your Android device" and it gave me some articles of people using their phones with external keyboards. Once you know the keys you can look them up in keymap.py and modify the commands above as required.

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Sep 19, 2019

Hello,

I have a question about the btk_server. I often will get the error:
self.scontrol.bind((self.address, self.P_CTRL)) OSError: [Errno 98] Address already in use

Until I reboot my pi. To my understanding, this is because the client side is now closing the socket. My client in this case is Windows 10. Is there a way to forcibly bind without rebooting my pi?

Any tips for modifying so I can automatically reconnect this BT Keyboard to the Windows 10 PC after it has already been paired?

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Sep 19, 2019

Hello,

I have a question about the btk_server. I often will get the error:
self.scontrol.bind((self.address, self.P_CTRL)) OSError: [Errno 98] Address already in use

Until I reboot my pi. To my understanding, this is because the client side is now closing the socket. My client in this case is Windows 10. Is there a way to forcibly bind without rebooting my pi?

Any tips for modifying so I can automatically reconnect this BT Keyboard to the Windows 10 PC after it has already been paired?

Actually, this was because I was using SIGINT to close my program instead of SIGQUIT. My mistake. Hope this helps someone else.

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Sep 26, 2019

Thanks for sharing thus ukBaz!
When the device disconnects either due to a link failure or it being out of range, I can't connect again. I need to manually restart the emulator and remove and re-add and re-pair the device.
Any solution to this?

Just add self.listen() to def on_disconnect(self) method.

What about if I want to reconnect if the server needs to restart? I.E. Pi restarts and I have bootup script to run btk_server.

Currently adding listen() to on disconnect doesn't always work. If I toggle my windows Bluetooth adapter from on to off and back to on, then the server will reconnect.

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Sep 26, 2019

What about if I want to reconnect if the server needs to restart? I.E. Pi restarts and I have bootup script to run btk_server.

Currently adding listen() to on disconnect doesn't always work. If I toggle my windows Bluetooth adapter from on to off and back to on, then the server will reconnect.

The first time the RPi starts up it will not be the on_disconnect listen that will be waiting. It will be the one in BTKbService.
I don't know the answer to this problem, but I would have thought that if the RPi is accepting a connection after toggling the Bluetooth adapter on the Windows machine, there can't be too much wrong with what the RPi is doing.
My only thought would be if the boot-up script is running too soon in the boot sequence. How are you starting the btk_server script?

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Sep 27, 2019

What about if I want to reconnect if the server needs to restart? I.E. Pi restarts and I have bootup script to run btk_server.
Currently adding listen() to on disconnect doesn't always work. If I toggle my windows Bluetooth adapter from on to off and back to on, then the server will reconnect.

The first time the RPi starts up it will not be the on_disconnect listen that will be waiting. It will be the one in BTKbService.
I don't know the answer to this problem, but I would have thought that if the RPi is accepting a connection after toggling the Bluetooth adapter on the Windows machine, there can't be too much wrong with what the RPi is doing.
My only thought would be if the boot-up script is running too soon in the boot sequence. How are you starting the btk_server script?

Sorry, let me explain better. After already connecting to the server via Windows 10, I will establish connect to the RPi. If I toggle Windows bluetooth or restart Windows PC, the server will reconnect to the Windows PC automatically, with no problem if I add self.listen() to def on_disconnect(self) method.

My problem is after pairing, that if the RPi needs to restart, and the btk_server is brought back up, it will sit at line 228 print('Waiting for connections') If I toggle bluetooth on Windows, the keyboard server will connect to Windows. I want to automatically try to reconnect to the paired Windows PC if the btk_server restarts. Does that make more sense? I want to be able to recover if either the Windows PC or the RPi restarts after pairing, automatically.

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Sep 28, 2019

My only thought would be if the boot-up script is running too soon in the boot sequence. How are you starting the btk_server script?

My only thought would be if the btk_server script is running too soon in the boot sequence. How are you starting the btk_server script?
If you don't start btk_server automatically, does the RPi connect automatically to the Windows machine when launching the btk_server script manually after the RPi has finished booting?

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Sep 30, 2019

My only thought would be if the boot-up script is running too soon in the boot sequence. How are you starting the btk_server script?

My only thought would be if the btk_server script is running too soon in the boot sequence. How are you starting the btk_server script?
If you don't start btk_server automatically, does the RPi connect automatically to the Windows machine when launching the btk_server script manually after the RPi has finished booting?

This is launching btk_server manually after RPi has finished rbooting, but the windows PC and RPi have already been paired. What is interesting is if I toggle bluetooth on Windows side after the RPi restarts and btk_server is started manually, it will reconnect. I am trying to make it so that the btk_server can connect to an already paired device after the pi is restarted.

The pi will automatically connect to the windows PC if I will kill and restart btk_server, but if I need to restart the RPi, I need to toggle the Windows bluetooth adapter off then on in order for it to reconnect.

Edit:
My problem is that I am blocking via the socket.accept() method: https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#file-btk_server-py-L245

   self.ccontrol, cinfo = self.scontrol.accept()

What changes can I make so that when I am already paired, that socket.accept will pair with my Win 10 PC without having to toggle bluetooth on the Win 10 PC?

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Sep 30, 2019

My only thought would be if the boot-up script is running too soon in the boot sequence. How are you starting the btk_server script?

My only thought would be if the btk_server script is running too soon in the boot sequence. How are you starting the btk_server script?
If you don't start btk_server automatically, does the RPi connect automatically to the Windows machine when launching the btk_server script manually after the RPi has finished booting?

This is launching btk_server manually after RPi has finished rbooting, but the windows PC and RPi have already been paired. What is interesting is if I toggle bluetooth on Windows side after the RPi restarts and btk_server is started manually, it will reconnect. I am trying to make it so that the btk_server can connect to an already paired device after the pi is restarted.

The pi will automatically connect to the windows PC if I will kill and restart btk_server, but if I need to restart the RPi, I need to toggle the Windows bluetooth adapter off then on in order for it to reconnect.

If anyone is interested, I am still trying to work this out, hoping to get answers here: (https://stackoverflow.com/questions/58172692/emulating-bluetooth-hid-configure-bluetooth-service-to-reconnect-to-windows-aft)

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Oct 8, 2019

Anyone here know anything about sdp records? I am thinking this might be the source of my problem. Windows doesnt know when the sdp record is exchanged that this is a device that needs polled to see if its awake to connect to like another bluetooth keyboard/mouse.

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Oct 8, 2019

Anyone here know anything about sdp records?

You might want to take a look at:
https://github.com/GamesCreatorsClub/GCC-Joystick/blob/master/src/python/bt_joystick/sdp_record.py
They appear to have taken the time to investigate the SDP record.

There is also the official Bluetooth Specification:
https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/

@minoskt

This comment has been minimized.

Copy link

@minoskt minoskt commented Oct 18, 2019

Hello all

Is it possible to connect to a specific device (assuming we know its bt address) instead of waiting for other devices to connect to the server?

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Oct 18, 2019

Is it possible to connect to a specific device (assuming we know its bt address) instead of waiting for other devices to connect to the server?

I am not sure I understand the question @minoskt. This is a peripheral so by definition it waits to be connected to.
If you have already established a paired and trusted connection with the peripheral, and have auto-connect enabled, then connection will happen between those trusted devices automatically.
You can disable the peripheral from being discoverable so that only already paired devices can connect.
If that does not answer your question then I'll need some more detail about what you are trying to do and what you have tried already.

@minoskt

This comment has been minimized.

Copy link

@minoskt minoskt commented Oct 18, 2019

Is it possible to connect to a specific device (assuming we know its bt address) instead of waiting for other devices to connect to the server?

I am not sure I understand the question @minoskt. This is a peripheral so by definition it waits to be connected to.
If you have already established a paired and trusted connection with the peripheral, and have auto-connect enabled, then connection will happen between those trusted devices automatically.
You can disable the peripheral from being discoverable so that only already paired devices can connect.
If that does not answer your question then I'll need some more detail about what you are trying to do and what you have tried already.

Thank you for your prompt answer.

I am testing this with an iPhone device (iOS 12.4 on an iPhone 7). If I use an external paired Bluetooth keyboard, at the moment I turn it on it connects to the iPhone (so, the peripheral is the keyboard that connects to the device I guess). That is not what is happening here. I have paired the device with my Pi and have the script running (so I see the "Waiting for connections..." message). I try to reboot the device and expect to re-connect to the btk_server automatically, however that never happens. My question is, is there any way of forcing this connection to happen from the script? So, btk_server to connect to the paired device?

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Oct 19, 2019

@minoskt: I don't have your combination of hardware so can't test. Do you have your iPhone marked at "trusted" on the RPi after you have paired? The easiest way to test if this makes a difference is with bluetoothctl. Some information is available in the blog that this gist is based on:
http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html?showComment=1500650893047#c4070129199662346850

@knlgdgl

This comment has been minimized.

Copy link

@knlgdgl knlgdgl commented Dec 15, 2019

Hey @ukBaz, Thanks for this great guide but I'm unable to pair the Pi with my Windows 10 laptop or my phone. Both keep asking for a pin but I've tried all combinations and it says wrong pin entered. Any ideas? Thanks again!

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Dec 15, 2019

@knlgdgl, there is not enough information in your question for me to make an educated guess as to what might be going wrong.
If you follow the steps in https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#terminal-1, what do you see?
When you try to connect to the RPi, what profile is it trying to connect to?

@knlgdgl

This comment has been minimized.

Copy link

@knlgdgl knlgdgl commented Dec 15, 2019

This is what I get :

pi@raspberrypi:~ $ sudo service bluetooth stop
pi@raspberrypi:~ $ sudo /usr/lib/bluetooth/bluetoothd -P input &
[1] 580
pi@raspberrypi:~ $ 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

And then when I try to pair on my phone or laptop it asks for a PIN. How do I check what profile it's trying to connect to?

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Dec 15, 2019

@knlgdgl, the first clue about the profile would be what icon is used on your phone or windows PC. For example, is there a headset being shown.
The other thing to do would be start bluetoothctl in another window on your RPi, If you type show you should see in the list:
UUID: Human Interface Device... (00001124-0000-1000-8000-00805f9b34fb)
If you leave the bluetoothctl tool running when you try to pair you should see if there are any issues reported.

@fmacrae

This comment has been minimized.

Copy link

@fmacrae fmacrae commented Mar 5, 2020

Man, you are a life saver! I've been trying tons of other repos without luck. I'm an AI / ML geek so that stuff comes easy to me but Bluetooth and d-bus aren't my forte. I'm going to mix this up with some voice recognition I've been working on to hopefully build a voice controlled HID for controlling TVs and things like that for my aunt who's unfortunately paralysed. I'll share my repo with you when I get it going.

@fmacrae

This comment has been minimized.

Copy link

@fmacrae fmacrae commented Mar 6, 2020

Managed to get it connected to and working to phone with this GIST. Fire TV Stick would not pick it up initially as it was seen as a generic device. I've managed to get it paired by adding a line to /etc/bluetooth/main.conf like this to get it recognized as a keyboard -

Class = 0x002540

Got the device code from http://domoticx.com/bluetooth-class-of-device-lijst-cod/

I was ssh into the pi and scratching my head as to why it was not working, then started actually using the pi keyboard. Doh! Don't be stupid like me. Always check the keyboard you type into is the one the evdev library is actually listening to!

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Mar 6, 2020

Thanks @fmacrae for the feedback and further insight. The link to your GIST isn't working, you might want to update if you intended to share that.

@fmacrae

This comment has been minimized.

Copy link

@fmacrae fmacrae commented Mar 11, 2020

@ukBaz Massive thanks. Got a working voice controlled BT input device now that'll help my aunt hopefully by end of week. Repo here https://github.com/fmacrae/BTSquawk (orginal idea was to use Squawk as made up wake word but that was a stupid idea) I will need to refactor it as I just smashed your code into Google's coral TPU demo code and I've been naughty and used magic numbers etc ;) Here's a demo https://youtu.be/F2RCh8H76P0

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Mar 11, 2020

Terrific stuff @fmacrae! I'll even overlook that it would have been better filmed in landscape mode :-)
Thanks for sharing.

@gnattu

This comment has been minimized.

Copy link

@gnattu gnattu commented Jun 4, 2020

Thank you for your work porting this to python3.
However, one thing I found this script handles incorrectly is the key modifiers, more specifically this line:

self.mod_keys = value << mod_key

For the modifiers bit-array, the mod_key index indicates the nth most significant bit. For example, "KEY_LEFTSHIFT": 6, means the 6th most significant bit , or 0b00000010 when value=1,but the bit-wise operation value << mod_key will set the nth least significant bit, or 0b01000000 in this case. This will resulted in wrong modifiers been sent, in the example, the left shift is interpreted as right alt.
The easiest way to fix this is to reverse the order in keymap.py:

modkeys = {
    "KEY_RIGHTMETA": 7,
    "KEY_RIGHTALT": 6,
    "KEY_RIGHTSHIFT": 5,
    "KEY_RIGHTCTRL": 4,
    "KEY_LEFTMETA": 3,
    "KEY_LEFTALT": 2,
    "KEY_LEFTSHIFT": 1,
    "KEY_LEFTCTRL": 0
}

Another problem is the one found by @minoskt. iOS devices never auto-reconnects even though I set trusted to true by bluetoothctl.

@IamBeardo

This comment has been minimized.

Copy link

@IamBeardo IamBeardo commented Jul 9, 2020

The easiest way to fix this is to reverse the order in keymap.py:

or:
self.mod_keys = value << (7-mod_key)

to invert what bit it set

@IamBeardo

This comment has been minimized.

Copy link

@IamBeardo IamBeardo commented Jul 9, 2020

I found that there is other problems with update_mod_keys. As implemented it will only track one key, the last one pressed.
And releasing any mod key previously pressed will clear mod_keys.

on key press: self.mod_keys is set distinct to the integer value 2**mod_key (by bit shifting value (that is 1)). it's not updated to reflect what was already set.
on key release: self.mod_keys is set distinct to 0. bit shifting value (that is 0) is still 0.

the following change to update_mod_keys() should fix this

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

/IamB

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Jul 9, 2020

Thanks @IamBeardo

I've updated the update_mod_keys method with this suggeston.

I wonder if the time has come for someone to take this code and create a proper GitHub repo for it so that pull requests can be handled. Any volunteers?

@IamBeardo

This comment has been minimized.

Copy link

@IamBeardo IamBeardo commented Jul 9, 2020

the screenshot keyboard shortcut for IOS is CMD+SHIFT+3 by the way. That should is now possible to send with the updated update_mod_keys function.

@IamBeardo

This comment has been minimized.

Copy link

@IamBeardo IamBeardo commented Jul 9, 2020

the screenshot keyboard shortcut for IOS is CMD+SHIFT+3 by the way. That should is now possible to send with the updated update_mod_keys function.

i am bit to disorganised to set that up i think. But i plan to experiment more with the code now that i finally found a codebase that worked. I am very grateful you shared this @ukBaz. I tested so many different examples i found around the web. But wasn't able to get any to work until i found your code, that worked right off the bat.

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Jul 18, 2020

@gnattu & @minoskt I've been doing some research on the iPhone and ios devices not automatically reconnecting. I think the issue is because the security is turned off in the above gist. Because there devices haven't been paired, there hasn't been a permanent exchange of keys. This is probably the best summary I've found: https://medium.com/@kbabcockuf/bridging-the-gap-bluetooth-le-security-aab27232a767

I think these are the key lines:
https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#file-btk_server-py-L190-L191

I don't have any iOS hardware to test, so if anyone tests this and get it to work then please leave a note here. Thanks in advance.

@ruundii

This comment has been minimized.

Copy link

@ruundii ruundii commented Jul 24, 2020

NewConnection is not being called because the profile does not listen on 0x11 port. If you specify "PSM":0x11 option (in the opts dictionary) it will and NewConnection will trigger.

BTW, I have got through the pain of creating a draft of a Bluez HID profile which will work as an input device together with the default input host profile. So I can e.g. retransmit Bluetooth input devices to other machines, possibly while doing such things as key remapping. It's not final and not super-clean yet, but started to work. https://github.com/ruundii/bluez

I have also implemented a remapper agent (https://github.com/ruundii/bthidhub). It uses this modified version of bluez. I run it on my raspberry pi, where I connect my keyboard and a mouse. A host PC connects to it as it is a Bluetooth kb+mouse device. The agent remaps keys (essential for apple keyboards to be used on PC) and then transmits the host.

@markuspi

This comment has been minimized.

Copy link

@markuspi markuspi commented Aug 6, 2020

Nice work @ukBaz! Are you planning on creating a repository of this code, so that people can easily collaborate to expand functionality?

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 7, 2020

Hi @markuspi, I am not sure I am best placed to be the BDFL for this project. I am happy for someone to take this code and use it as the base for a new repo. The original repo had an MIT license which needs to be honoured. It may even be that @yaptb is happy to take PR's on that original repo.

@lewisxy

This comment has been minimized.

Copy link

@lewisxy lewisxy commented Aug 13, 2020

Hi, I am using a raspberryPi 3b, and able to reproduce most of this guide (make all the script working, and make phone/computer to pair and connect to it). However, the keyboard seems to work only on my old android phone (android 5.1). I tried my iPhone (ios 12), macbook (OSX 10.14), and windows computer (windows 10 pro), all of them can see the device and connect to it, but the keyboard functionality does not work (i.e. when I pressed a key on the keyboard connected to raspberryPi, nothing happened on computer/phone). After some debugging effort, I am sure the data is being sent, but I am not sure why they did not react to it. Are there special protocols for setting up a bluetooth keyboard?

@ruundii

This comment has been minimized.

Copy link

@ruundii ruundii commented Aug 13, 2020

hi @lewisxy,
I am guessing a little bit here, but I would check if it will work with:

  1. pairing the devices, setting device to ‘trusted’. you can try it by just using bluetoothctl. if indeed this is something what is needed and manual pairing is not a fit for your application, you would need to implement your version of pairing agent (pretty simple)

  2. encryption. bluetooth spec says that keyboard connection must be encrypted. now, this gets a little bit hairy depending on versions of bluetooth on both ends (e.g. see https://github.com/bluez/bluez/blob/fd45d85ad9ff987d5f2d3d7f95ef95f757b0f514/profiles/input/device.c#L1096)
    you can run 'sudo hcitool con' to see if your connections have AUTH and ENCRYPT flags (will appear at the end of each connection description)

copying from set_sec_level btio.c of bluez (https://github.com/bluez/bluez/blob/fd45d85ad9ff987d5f2d3d7f95ef95f757b0f514/btio/btio.c#L455)

setsockopt(sock, SOL_BLUETOOTH, BT_SECURITY, &sec, sizeof(sec))
and then also

static int l2cap_set_lm(int sock, int level)
{
	int lm_map[] = {
		0,
		L2CAP_LM_AUTH,
		L2CAP_LM_AUTH | L2CAP_LM_ENCRYPT,
		L2CAP_LM_AUTH | L2CAP_LM_ENCRYPT | L2CAP_LM_SECURE,
	}, opt = lm_map[level];

	if (setsockopt(sock, SOL_L2CAP, L2CAP_LM, &opt, sizeof(opt)) < 0)
		return -errno;

	return 0;
}

bluez sets medium level of security for keyboards, which corresponds to L2CAP_LM_AUTH | L2CAP_LM_ENCRYPT flags here.

  1. also, as it was mentioned previously, check that you set a keyboard related class in /etc/bluetooth/main.conf. 0x002540 would do, as things like apple keyboard use that class, I use 0x0005C0 for keyboard+mouse, so 0x000540 should also work for just keyboard.
@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 13, 2020

@lewisxy, try changing the following settings to True
https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#file-btk_server-py-L190-L191

and then after starting the server, pair your computer. You might want to have bluetoothctl running on the RPi when you do this to give you some visibility that things are working.

This has been discussed a few times above so there might be some further clues there

@lewisxy

This comment has been minimized.

Copy link

@lewisxy lewisxy commented Aug 13, 2020

@ruundii I tried the first approach you mentioned to trust the devices after pairing with trust <MAC_ADDR> in bluetoothctl, and now it works on all my devices. Thanks very much for the tip!
The only remaining questions is how to do this programmatically? Because if I were to develop an actual production software, it's not convenient, and may not even be possible to access bluetoothctl to trust it manually.

@ruundii

This comment has been minimized.

Copy link

@ruundii ruundii commented Aug 13, 2020

@lewisxy, Great! glad to hear it worked.

You need to create and register your own Bluetooth pairing agent (org.bluez.Agent1 on dbus). Have a look here and here.

In these examples I use an alternative library for dbus - dasbus, but you can relatively easily find an implementation of org.bluez.Agent1 based on dbus-python which is used in this gist. Bluez samples have one

Ah, and I think you need to change RequireAuthentication/RequireAuthorization options as well, so the pairing is actually triggered.

@PulgaFeroz

This comment has been minimized.

Copy link

@PulgaFeroz PulgaFeroz commented Aug 30, 2020

Ok, took me an evening but I found the issue some of us were struggling: HID device initiating connection (or reconnection) to HID host (pi reconnects to say window box), at least @JimPatrizi @minoskt and me were looking for it

1st: the sdp record indicates that PI wont initiate a connection / reconnection

	<attribute id="0x0205"> --> HIDReconnectInitiate
		<boolean value="false" /> --> Who reconnects? false means HID Device wont and would rely that the other end will
	</attribute>

So first thing is to change that value to "true"

Then, the code does not have a 'reconnect' method, I quickly stitched one in BTKbDevice as:

def reconnect(self):
   print("Trying reconnect...")                                                                                                                                                                                  
   while True:
       try:
           hidHost = 'XX:XX:XX:XX:XX:XX'
           self.ccontrol = BluetoothSocket(L2CAP)
           self.cinterrupt = BluetoothSocket(L2CAP)
           self.ccontrol.connect((hidHost, self.P_CTRL))
           self.cinterrupt.connect((hidHost, self.P_INTR))
           print("Connected!")
           break
       except Exception, ex:
           print("didnt connect, will retry..." + str(ex))
           time.sleep(1)

And for testing it, I modified BTKbService init method with:

        #start listening for connections
        if len(sys.argv) > 1:
            self.device.reconnect()
        else:
            self.device.listen()

To verify, you can turn on windows bluetooth, stop and reboot the pi, then launch it like:

sudo python btk_server.py 1

or similar, just the presence of a parameter will invoke 'reconnect' instead of 'listen', you obviously need to supply the address to which you want to reconnect (hidHost in that draft), ideally you want to do it to the last device it was connected

pffff that took me quite some investigation, some curious things i noticed in windows: In the "Bluetooth and other devices", turning it off and on fast (means less than 10 seconds off) doesn't really work as you may expect, but if instead you use the action center bt icon it does, that is turning off / on within a second works better

image

Another thing to notice is that, at least in windows, if you stop the service in the PI, windows will notice and show that the bt device is not connected, if you restart the pi service within the next 10 to 20 seconds, windows is the one that initiates a reconnection attempt! to test this properly when you stop the PI service you need to wait a minute or so before starting it up to be sure who is initiating the connection / reconnection

As they say: if there's no pic it didn't happen:

image

Hope I save somebody else those painful hours

Cheers

-Mat

@JimPatrizi

This comment has been minimized.

Copy link

@JimPatrizi JimPatrizi commented Aug 31, 2020

Very nice Mat! I understand all of your pain and frustration for this. I am interested in circling back to this, as it fell of my radar. Thank you for sharing this info with the members here!

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 31, 2020

Thanks @PulgaFeroz for taking a look at this.

I have a couple of questions about what you have done.

You have used the function BluetoothSocket which doesn't seem to be available. Have you defined this elsewhere? I have seen a function of this name when people have imported pyBluez. This should be an unnecessary dependency and definitely not one I would keen to use.

Well done for picking your way through the manual SDP record. However, I would ideally like to move away from having the very obfuscated manual SDP entry. The Profile API allows for SDP to be specified with the Version and Profile enters

I would prefer to move towards how it was done in the test-hfp example given in the BlueZ repository. The edited highlights are:

HF_3WAY			= 0x0002
HF_CLI			= 0x0004
HF_VOICE_RECOGNITION	= 0x0008
HF_REMOTE_VOL		= 0x0010
HF_ENHANCED_STATUS	= 0x0020
HF_ENHANCED_CONTROL	= 0x0040
HF_CODEC_NEGOTIATION	= 0x0080


HF_FEATURES = (HF_3WAY | HF_CLI | HF_VOICE_RECOGNITION |
			HF_REMOTE_VOL | HF_ENHANCED_STATUS |
			HF_ENHANCED_CONTROL | HF_CODEC_NEGOTIATION)

	opts = {
			"Version" : dbus.UInt16(0x0106),
			"Features" : dbus.UInt16(HF_FEATURES),
		}

manager.RegisterProfile(options.path, "hfp-hf", opts)
@PulgaFeroz

This comment has been minimized.

Copy link

@PulgaFeroz PulgaFeroz commented Aug 31, 2020

Oh damn, you're right, I started working with the original code, then moved to your improved version and while crafting a solution I mixed up old and new code, so indeed BluetoothSocket came from pybluez

I'm not familiar with most of the involved libraries (neither bluetooth for that matter :D) so no idea at the moment what can replace the BluetoothSocket, I'll search around

BTW, I found also a linux command line tool for doing exactly this: https://linux.die.net/man/8/btkbdd and there seem to be others around, some extra info may lurk in that code.

@PulgaFeroz

This comment has been minimized.

Copy link

@PulgaFeroz PulgaFeroz commented Aug 31, 2020

seems like a normal socket will do... looking at your code:

self.scontrol = socket.socket(socket.AF_BLUETOOTH,
                                      socket.SOCK_SEQPACKET,
                                      socket.BTPROTO_L2CAP)

The server socket is created with a standard socket, the client socket would not need to be different, didn't tried yet but replacing BluetoothSocket for socket.socket should work

(those were his last famous words) :)

@PulgaFeroz

This comment has been minimized.

Copy link

@PulgaFeroz PulgaFeroz commented Aug 31, 2020

yup, I confirmed it:

    def reconnect(self, hidHost):
        print("Trying reconnect...")
        while True:
        try:
            # hidHost = 'XX:XX:XX:XX:XX:XX'    
            self.ccontrol = socket.socket(socket.AF_BLUETOOTH,
                                          socket.SOCK_SEQPACKET,
                                          socket.BTPROTO_L2CAP)
            self.cinterrupt = socket.socket(socket.AF_BLUETOOTH,
                                            socket.SOCK_SEQPACKET,
                                            socket.BTPROTO_L2CAP)
            self.ccontrol.connect((hidHost, self.P_CTRL))
            self.cinterrupt.connect((hidHost, self.P_INTR))
            print("Connected!")
        except Exception as ex:
            print("didnt connect, will retry..." + str(ex))
            time.sleep(1)

Works, sorry for the mixup :D

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 31, 2020

Well done @PulgaFeroz on testing that the standard Python sockets library can be used.

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Aug 31, 2020

I've been investigating how to switch from using the manual SDP record, to using the other method mentioned in the Profile API.

I've looked at the specification for Bluetooth Human Interface Device Profile (HID) which is at:
https://www.bluetooth.com/specifications/profiles-overview/
There is section 5.3.4 Bluetooth HID SDP Attributes which has the attribute names, attribute IDs. Not sure how to set values without doing the full manual SDP description.

Section 5.4.2 Connectability also looks of interest.

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Sep 1, 2020

From the Human Interface Device (HID) Profile , section 5.4.2 Connectability
image

Attribute name  attribute id Current setting @PulgaFeroz proposal
HIDVirtualCable 0x0204 FALSE FALSE
HIDReconnectInitiate 0x0205 FALSE TRUE
HIDNormallyConnectable 0x020D TRUE TRUE

My reading of the table is that ideally you would want all three values to be True.

From section 4.5.1 Virtual Cable Establishment:

If the HIDVirtualCable SDP attribute is set to TRUE, then a Virtual Cable is considered to be established after both the HID Control and HID Interrupt L2CAP channels have been opened.

With the HIDVirtualCable set to true, then I think the change made previously of adding the on_disconnect method should be enough as the listen method opens the sockets. This would remove the need for adding the reconnect method.

Is anyone able to test this with Apple hardware?

@PulgaFeroz

This comment has been minimized.

Copy link

@PulgaFeroz PulgaFeroz commented Sep 1, 2020

From what I've read the 'virtual cable' has some special considerations, didn't go thru the full spec of it, I think that as far as the service wants to talk 1-1 was ok.

Of course different use cases needs different tweaks, in my case I built a password manager in a pi zero, I want to initiate the connection from the device (not the host, at least for now), so in my case I don't use the 'listen' method or start listening when a connection is closed

There's something weird about the need of using the computer physical mouse or keyboard in order to be able to connect the wireless keyboard isn't it :)

@PulgaFeroz

This comment has been minimized.

Copy link

@PulgaFeroz PulgaFeroz commented Sep 1, 2020

Oh forgot to mention, there are a few rough corners to iron out, something I noticed is that if the (python) service is running and I open 'sudo bluetoothctl' then the service throws errors.

ERROR:dbus.connection:Exception in handler for D-Bus signal:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/dbus/connection.py", line 230, in maybe_handle_message
    self._handler(*args, **kwargs)
TypeError: interfaces_added() takes 1 positional argument but 3 were given

I also observed a few times (I think right after pairing) the host connects and gets disconnected immediately, it gets into a funky state, stopping and restarting the bt service + python service and things go back to normal, I saw this happening at least twice

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Sep 2, 2020

The error you are seeing will be because this:

    def interfaces_added(self):
        pass

Needs to be changed to this:

    def interfaces_added(self, path, device_info):
        pass

This method gets called if a new device is discovered

@neo774

This comment has been minimized.

Copy link

@neo774 neo774 commented Feb 21, 2021

Hey ukBaz,
thank you for the work you put into it! I am looking for such programm and tried to run it under a Linuxmint setup. I get to the point where the programm is waiting for a connection but when I try to pair my phone with the emulated keyboard I get the message that the keyboard rejected pairing. Do you have any Idea how to solve this?

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Feb 21, 2021

It is difficult to know what this issue is from the information you have given. It is usually helpful to have a second terminal window open with bluetoothctl running and the default agent selected. Hopefully that will give you some more information.

@neo774

This comment has been minimized.

Copy link

@neo774 neo774 commented Feb 22, 2021

Hey, thank you for your quick repsonse. I tried what you suggest and get a bit more insights.
I got to the point where on both devices code numbers where displayed. When I accepted the control socket and interrupt channel were connected but directly afterwards a dbus error ocurred: interface_added() takes 1 positional argument but 3 were given. On the same time in the bluetoothctl terminal I was asked to proceed with service authorization. Could the problem occur due to this further authorization requests in sense paring process is not yet completed, but the python script already proceeds?

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Feb 23, 2021

Have tried the solution in the comment above your original comment?
https://gist.github.com/ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0#gistcomment-3439321

@neo774

This comment has been minimized.

Copy link

@neo774 neo774 commented Feb 23, 2021

No, but know I tried and I was able to establish a connection - thx a lot for your help! (I didn't check the lines above agein...) Now, I got some problems with the keyboardclient. Analysed it and somehow the evdev module is not working probably in my setup - the read_loop function is not recording keyboard presses (I checked with evtest and set the eventfile number properly). Could this be due to kernel interferences? I checked with fuser that there root processes running on the corresponding event file.

@neo774

This comment has been minimized.

Copy link

@neo774 neo774 commented Feb 24, 2021

Okay, I found out that there is more then one eventfile corresponding to the controller. When I adjusted it the client proceeded till the send_keys() line after pushing a button. But unfortunately I got no no reaction on the connected smartphone and shortly after I got the following error:

dbus.proxies:Introspect error on :1.2589:/org/yaptb/btkbservice: dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NoReply: 
Did not receive a reply. Possible causes include: 
    the remote application did not send a reply, 
    the message bus security policy blocked the reply,
    the reply timeout expired, or the network connection was broken.
@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Feb 25, 2021

@neo774, you are not really giving me enough information to help much with this. Are you running something like sudo busctl monitor org.yaptb.btkkbservice to see what is happening on the D-Bus?
If I was to speculate, of the three items the error suggests as possible causes, the security policy not being set is the most likely as that is one of the set-up set-up described above. Did you do the sudo cp org.yaptb.btkkbservice.conf /etc/dbus-1/system.d step?

@neo774

This comment has been minimized.

Copy link

@neo774 neo774 commented Feb 25, 2021

Hey, thank you for your patience in trying to help me!
Now it, works though I didn't change anything. (I did the setup you asked for as a first step before even trying the server script the first time)
I just wanted to run the scripts with busctl monitor on as you suggested and it worked straight away without any errors occurring ...
I can't explain why, but anyway - seems solved.

One little thing: when connecting in the bluetoothctl monitor I always get asked to authorize services (from the connected device I guess) and have to type in yes two times manually - can I somehow automate that confirmation?

[M1;39m[agent] Authorize service 00001108-0000-1000-8000-00805f9b34fb (yes/no): yes
Authorize service
[M1;39m[agent] Authorize service 0000110d-0000-1000-8000-00805f9b34fb (yes/no): yes
@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Feb 25, 2021

@neo774, you haven't said what OS your phone is running. There are various discussions in the comments above about this topic. I would have thought adding your device as a trusted device would resolve the need to authorize the service each time.

There is always a degree of bit rot over time with any unmaintained software. This gist was a response to the bit rot of the original article. Maybe this gist has outlived its usefulness. It is not something I continue to use so sits here unmaintained. There are some people that have brought great knowledge to this topic and as a result, I am reluctant to delete the record this gist has created as it might offer some insight to someone.

@neo774

This comment has been minimized.

Copy link

@neo774 neo774 commented Feb 25, 2021

@ukBaz, sure I understand. I didn't want to bother you (just didn't know who else to ask). In fact this project is totally what I was looking for and certainly it is useful for others as well - there is actually not so much software out there addressing this functionality. Therefore thx a lot!
(The OS is Android 11; I will check the comments above to see how to add it as a trusted device)

@ukBaz

This comment has been minimized.

Copy link
Owner Author

@ukBaz ukBaz commented Apr 3, 2021

@HeadHodge has created an example that uses the newer HID over GATT Profile (HOGP) and thought it might be a useful reference for people:

https://github.com/HeadHodge/Bluez-HID-over-GATT-Keyboard-Emulator-Example

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