[Python] Emulate a keyboard with BlueZ 5

Discussão em 'Python' iniciado por Stack, Maio 21, 2025.

  1. Stack

    Stack Membro Participativo

    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 == hex_key and event.value == 0:
    # Code is 0 so we need to depress it
    self.state = 0x00
    elif self.state == 0x00 and event.value == 1:
    # If the current space is empty and the key is being pressed
    self.state = 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 == hex_key and event.value == 0:
    # Code is 0 so we need to depress it
    self.state = 0x00
    elif self.state == 0x00 and event.value == 1:
    # If the current space is empty and the key is being pressed
    self.state = 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!

    Continue reading...

Compartilhe esta Página