I'm working on a setup where I want to connect a RaspberryPi-3 to another machine. Most of the time it will be a Windows machine. On the Pi I want to forward all Keyboard strokes via Bluetooth to the other (Windows) machine.
Therefore I did some research, someone called Liam wrote a Python script a few years ago, which apparently worked fine with BlueZ 4. So I decided to give it a shot on BlueZ 5. No luck so far.
Well, I thought, ok, lets downgrade the version. So I compiled it from source, version 4.10. No luck. The raspberry didn't even know it had a bluetooth adapter installed, possibly there was something wrong with a few drivers.
So then I started to get into BlueZ 5, at first it was easy. But actually I am struggling now, to get the sdp_Record.xml registered. As well as a stable connection.
So first of all the reference file with BlueZ 4 implementation:
BlueZ4.py:
#!/usr/bin/python2.7
#
# PiTooth allows the Raspberry Pi to act as a Bluetooth keyboard, and relays
# keypresses from a USB keyboard to a Bluetooth client. Written by Liam Fraser
# for a Linux User & Developer tutorial.
#
import os # Used to call external commands
import sys # Used to exit the script
import bluetooth
from bluetooth import *
import dbus # Used to set up the SDP record
import time # Used for pausing the process
import evdev # Used to get input from the keyboard
from evdev import *
import keymap # Used to map evdev input to hid keycodes
class Bluetooth:
HOST = 0 # BT Mac address
PORT = 1 # Bluetooth Port Number
# Define the ports we'll use
P_CTRL = 17
P_INTR = 19
def __init__(self):
# Set the device class to a keyboard and set the name
os.system("hciconfig hci0 class 0x002540")
os.system("hciconfig hci0 name Raspberry\ Pi")
# Make device discoverable
os.system("hciconfig hci0 piscan")
# Define our two server sockets for communication
self.scontrol = BluetoothSocket(L2CAP)
self.sinterrupt = BluetoothSocket(L2CAP)
# Bind these sockets to a port
self.scontrol.bind(("", Bluetooth.P_CTRL))
self.sinterrupt.bind(("", Bluetooth.P_INTR))
# Set up dbus for advertising the service record
self.bus = dbus.SystemBus()
try:
self.manager = dbus.Interface(self.bus.get_object("org.bluez", "/"),
"org.bluez.Manager")
adapter_path = self.manager.DefaultAdapter()
self.service = dbus.Interface(self.bus.get_object("org.bluez", adapter_path),
"org.bluez.Service")
except:
sys.exit("[Bluetooth - L.50] Could not configure bluetooth. Is bluetoothd started?")
# Read the service record from file
try:
fh = open(sys.path[0] + "/sdp_record.xml", "r")
except:
sys.exit("[Bluetooth - L.56] Could not open the sdp record. Exiting...")
self.service_record = fh.read()
fh.close()
def listen(self):
# Advertise our service record
self.service_handle = self.service.AddRecord(self.service_record)
print "[Bluetooth - L.63] Service record added"
# Start listening on the server sockets
self.scontrol.listen(1) # Limit of 1 connection
self.sinterrupt.listen(1)
print "[Bluetooth - L.68] Waiting for a connection"
self.ccontrol, self.cinfo = self.scontrol.accept()
print "[Bluetooth - L.70] Got a connection on the control channel from " + self.cinfo[Bluetooth.HOST]
self.cinterrupt, self.cinfo = self.sinterrupt.accept()
print "[Bluetooth - L.72] Got a connection on the interrupt channel from " + self.cinfo[Bluetooth.HOST]
def send_input(self, ir):
# Convert the hex array to a string
hex_str = ""
for element in ir:
if type(element) is list:
# This is our bit array - convert it to a single byte represented
# as a char
bin_str = ""
for bit in element:
bin_str += str(bit)
hex_str += chr(int(bin_str, 2))
else:
# This is a hex value - we can convert it straight to a char
hex_str += chr(element)
# Send an input report
self.cinterrupt.send(hex_str)
class Keyboard():
def __init__(self):
# The structure for an bt keyboard input report (size is 10 bytes)
self.state = [
0xA1, # This is an input report
0x01, # Usage report = Keyboard
# Bit array for Modifier keys (D7 being the first element, D0 being last)
[0, # Right GUI - (usually the Windows key)
0, # Right ALT
0, # Right Shift
0, # Right Control
0, # Left GUI - (again, usually the Windows key)
0, # Left ALT
0, # Left Shift
0], # Left Control
0x00, # Vendor reserved
0x00, # Rest is space for 6 keys
0x00,
0x00,
0x00,
0x00,
0x00 ]
# Keep trying to get a keyboard
have_dev = False
while have_dev == False:
try:
# Try and get a keyboard - should always be event0 as we're only
# plugging one thing in
self.dev = InputDevice("/dev/input/event0")
have_dev = True
except OSError:
print "[Keyboard - L.124] - Keyboard not found, waiting 3 seconds and retrying"
time.sleep(3)
print "[Keyboard - L.127]Found a keyboard"
def change_state(self, event):
evdev_code = ecodes.KEY[event.code]
modkey_element = keymap.modkey(evdev_code)
if modkey_element > 0:
# Need to set one of the modifier bits
if self.state[2][modkey_element] == 0:
self.state[2][modkey_element] = 1
else:
self.state[2][modkey_element] = 0
else:
# Get the hex keycode of the key
hex_key = keymap.convert(ecodes.KEY[event.code])
# Loop through elements 4 to 9 of the input report structure
for i in range (4, 10):
if self.state[i] == hex_key and event.value == 0:
# Code is 0 so we need to depress it
self.state[i] = 0x00
elif self.state[i] == 0x00 and event.value == 1:
# If the current space is empty and the key is being pressed
self.state[i] = hex_key
break
def event_loop(self, bt):
for event in self.dev.read_loop():
# Only bother if we a key and it's an up or down event
if event.type == ecodes.EV_KEY and event.value < 2:
self.change_state(event)
bt.send_input(self.state)
if __name__ == "__main__":
# We can only run as root
if not os.geteuid() == 0:
sys.exit("[FATAL] - Only root can run this script (sudo?)")
bt = Bluetooth()
bt.listen()
kb = Keyboard()
kb.event_loop(bt)
What I did so far was trying to migrate this old BlueZ 4 code to version 5.
My implementation so far:
#!/usr/bin/python2.7
import os
import sys
import bluetooth
from bluetooth import *
import dbus
import time
import evdev
from evdev import *
import keymap
class Bluetooth:
HOST = "<REMOTEMACHINEMAC>" #<PIMAC>
#HOST = 0
PORT = 1
# Define the ports we'll use
P_CTRL = 17
P_INTR = 19
def __init__(self):
os.system("hciconfig hci0 class 0x002540")
os.system("hciconfig hci0 name Raspberry\ Pi")
# Define our two server sockets for communication
self.scontrol = BluetoothSocket(L2CAP)
self.sinterrupt = BluetoothSocket(L2CAP)
# Bind these sockets to a port
self.scontrol.bind(("", Bluetooth.P_CTRL))
self.sinterrupt.bind(("", Bluetooth.P_INTR))
# Set up dbus for advertising the service record
self.bus = dbus.SystemBus()
# Set up dbus for advertising the service record
try:
self.objManager = dbus.Interface(self.bus.get_object("org.bluez", "/"),
"org.freedesktop.DBus.ObjectManager")
#print self.manager.GetManagedObjects()["/org/bluez/hci0"]
self.manager = dbus.Interface(self.bus.get_object("org.bluez", "/org/bluez"),
"org.bluez.ProfileManager1")
self.hci_props = dbus.Interface(self.bus.get_object("org.bluez", "/org/bluez/hci0"),
"org.freedesktop.DBus.Properties")
except:
print sys.exc_info()
sys.exit("[FATAL] Could not set up Bluez5")
# Read the service record from file
try:
fh = open(sys.path[0] + "/sdp_record.xml", "r")
except:
sys.exit("[Bluetooth - L.56] Could not open the sdp record. Exiting...")
self.service_record = fh.read()
fh.close()
try:
opts = { "AutoConnect": 1, "ServiceRecord": self.service_record }
uuidarray = self.hci_props.Get("org.bluez.Adapter1", "UUIDs")
for uuids in uuidarray:
try:
self.manager.RegisterProfile("/org/bluez/hci0", uuids, opts)
except:
print uuids
print "Service Record saved!"
except:
print "Service Records saved. Probably already exists"
#print sys.exc_info()
#sys.exit("Error updating service record")
print "Update class again"
#os.system("hciconfig hci0 class 0x002540")
#os.system("hciconfig hci0 name Raspberry\ Pi")
def listen(self):
# Advertise our service record
#self.service_handle = self.service.AddRecord(self.service_record)
#print "[Bluetooth - L.63] Service record added"
# Start listening on the server sockets
self.scontrol.listen(1) # Limit of 1 connection
self.sinterrupt.listen(1)
print "[Bluetooth - L.68] Waiting for a connection"
self.ccontrol, self.cinfo = self.scontrol.accept()
print "[Bluetooth - L.70] Got a connection on the control channel from " + self.cinfo[Bluetooth.HOST]
self.cinterrupt, self.cinfo = self.sinterrupt.accept()
print "[Bluetooth - L.72] Got a connection on the interrupt channel from " + self.cinfo[Bluetooth.HOST]
def python_to_data(self, data):
if isinstance(data, str):
data = dbus.String(data)
elif isinstance(data, bool):
data = dbus.Boolean(data)
elif isinstance(data, int):
data = dbus.Int64(data)
elif isinstance(data, float):
data = dbus.Double(data)
elif isinstance(data, list):
data = dbus.Array([self.python_to_data(value) for value in data], signature='v')
elif isinstance(data, dict):
data = dbus.Dictionary(data, signature='sv')
for key in data.keys():
data[key] = self.python_to_data(data[key])
return data
class Keyboard():
def __init__(self):
# The structure for an bt keyboard input report (size is 10 bytes)
self.state = [
0xA1, # This is an input report
0x01, # Usage report = Keyboard
# Bit array for Modifier keys (D7 being the first element, D0 being last)
[0, # Right GUI - (usually the Windows key)
0, # Right ALT
0, # Right Shift
0, # Right Control
0, # Left GUI - (again, usually the Windows key)
0, # Left ALT
0, # Left Shift
0], # Left Control
0x00, # Vendor reserved
0x00, # Rest is space for 6 keys
0x00,
0x00,
0x00,
0x00,
0x00 ]
# Keep trying to get a keyboard
have_dev = False
while have_dev == False:
try:
# Try and get a keyboard - should always be event0 as we're only
# plugging one thing in
self.dev = InputDevice("/dev/input/event0")
have_dev = True
except OSError:
print "[Keyboard - L.124] - Keyboard not found, waiting 3 seconds and retrying"
time.sleep(3)
print "[Keyboard - L.127]Found a keyboard"
def change_state(self, event):
evdev_code = ecodes.KEY[event.code]
modkey_element = keymap.modkey(evdev_code)
if modkey_element > 0:
# Need to set one of the modifier bits
if self.state[2][modkey_element] == 0:
self.state[2][modkey_element] = 1
else:
self.state[2][modkey_element] = 0
else:
# Get the hex keycode of the key
hex_key = keymap.convert(ecodes.KEY[event.code])
# Loop through elements 4 to 9 of the input report structure
for i in range (4, 10):
if self.state[i] == hex_key and event.value == 0:
# Code is 0 so we need to depress it
self.state[i] = 0x00
elif self.state[i] == 0x00 and event.value == 1:
# If the current space is empty and the key is being pressed
self.state[i] = hex_key
break
def event_loop(self, bt):
for event in self.dev.read_loop():
# Only bother if we a key and it's an up or down event
if event.type == ecodes.EV_KEY and event.value < 2:
self.change_state(event)
bt.send_input(self.state)
if __name__ == "__main__":
# We can only run as root
if not os.geteuid() == 0:
sys.exit("[FATAL] - Only root can run this script (sudo?)")
bt = Bluetooth()
bt.listen()
kb = Keyboard()
kb.event_loop(bt)
The thing is, I'm wondering about now are:
HOST
variable correct?PORT
correct?Because, my print out of the Python script is:
[Bluetooth - L.68] Waiting for a connection
as well as, the bluetooth connection drops as soons as it "connects" with the remote machine.
What I also noticed is, that I think the SPD_Record.xml won't get set correctly.
SDP_Record.xml for reference:
<?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="true" />
</attribute>
<attribute id="0x0205">
<boolean value="true" />
</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="false" />
</attribute>
<attribute id="0x020e">
<boolean value="true" />
</attribute>
<attribute id="0x020f">
<uint16 value="0x0640" />
</attribute>
<attribute id="0x0210">
<uint16 value="0x0320" />
</attribute>
</record>
I'd be more than glad, if some can help or point me in the right direction, to get this working again.
Thanks in advance for your help!
After searching the worldwide web further and further, I stumbled upon an interessting repository on GitHub:
https://github.com/quangthanh010290/BL_keyboard_RPI
and also the very interessting website:
http://www.mlabviet.com/2017/09/make-raspberry-pi3-as-emulator.html
After looking through the code and some tweaks, I was able to get this thing completely working. Also my questions has been answered. What I've learned from it:
Binding an empty address: self.scontrol.bind(("", Bluetooth.P_CTRL))
Didn't work because the bluetoothd
service, kept me from binding to it. This I noticed, because using @quangthanh010290 script it told me, that my bluetooth MAC was already in use.
After killing it: sudo killall bluetoothd
I could properly bind to the given MAC address and everything worked fine.