Search code examples
pythonlinuxraspberry-pibluezpybluez

Emulate a keyboard with BlueZ 5


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:

  • Is the HOST variable correct?
  • Is even the PORT correct?
  • Did I miss out an important step?

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!


Solution

  • 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.