使用 BlueZ 5 模拟键盘

Emulate a keyboard with BlueZ 5

我正在进行一个设置,我想将 RaspberryPi-3 连接到另一台机器。大多数时候它将是 Windows 机器。在 Pi 上,我想通过蓝牙将所有键盘敲击转发到另一台 (Windows) 机器。

因此我做了一些研究,一个叫 Liam 的人在几年前写了一个 Python 脚本,显然它在 BlueZ 4 上运行良好。所以我决定在 BlueZ 5 上试一试。运气不好所以远.

嗯,我想,好吧,让我们降级版本。所以我从源代码编译它,版本 4.10。没有运气。树莓派甚至不知道它安装了蓝牙适配器,可能是一些驱动有问题。

然后我开始接触 BlueZ 5,一开始很容易。但实际上我现在正在努力让 sdp_Record.xml 注册。以及稳定的连接。

所以首先是 BlueZ 4 实现的参考文件:

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)

到目前为止我所做的是尝试将这个旧的 BlueZ 4 代码迁移到版本 5。

到目前为止我的实施:

#!/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)

问题是,我现在想知道的是:

因为,我从 Python 脚本中打印出来的是:

[Bluetooth - L.68] Waiting for a connection

此外,蓝牙连接一旦 "connects" 与远程机器断开。

我还注意到,我认为 SPD_Record.xml 设置不正确。

SDP_Record.xml供参考:

<?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>

如果有人能帮助我或为我指明正确的方向,我会非常高兴,让我重新开始工作。

在此先感谢您的帮助!

在万维网上搜索得越来越远后,我在 GitHub 上偶然发现了一个有趣的存储库:

https://github.com/quangthanh010290/BL_keyboard_RPI

还有非常有趣的网站:

http://www.mlabviet.com/2017/09/make-raspberry-pi3-as-emulator.html

在查看代码并进行一些调整后,我能够让这个东西完全正常工作。我的问题也得到了回答。我从中学到了什么:

绑定空地址:self.scontrol.bind(("", Bluetooth.P_CTRL))

没有用,因为 bluetoothd 服务阻止我绑定到它。我注意到了这一点,因为使用 @quangthanh010290 脚本它告诉我,我的蓝牙 MAC 已经在使用中。

杀死它后:sudo killall bluetoothd 我可以正确绑定到给定的 MAC 地址并且一切正常。

适合我。我唯一需要做的是在整个过程之前 运行 命令 tmux start-server ,否则会在找不到进程时出错。