完整代码
https://github.com/schaepher/keyboard_mouse_emulate_on_raspberry
大致流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| Bluetooth Keyboard
+------------------------+
| |
| +-----------+ |
| | USB | |
| | keyboard | |
| +-----+-----+ |
| | HID protocol |
| v |
| +-----+-----+ |
| |Linux input| |
| +-----+-----+ |
| | event |
| v |
| +-----+-----+ |
| | evdev | |
| +-----+-----+ |
| | |
| v |
| +-----+-----+ |
| |event code | |
| | to | |
| | HID code | |
| +-----+-----+ |
| | dbus |
| v |
| +-----+-----+ |
| | Bluetooth | |
| | keyboard | | +-----------+
| | service | | | Windows |
| +-----+-----+ | +-----+-----+
| | send | ^ HID protocol
| v | |
| +-----+-----+ | +-----+-----+
| | Bluetooth | | | Bluetooth |
| | service | | +-------> | service |
| +-----------+ | L2CAP +-----------+
| |
+------------------------+
|
主要做三件事:
- 获取 Linux 的输入事件
- 将输入事件转化为 HID 协议的数据
- 将转换后的 HID 协议的数据发送到蓝牙服务
下面会先测试获取输入事件和发送数据到蓝牙,即输入输出。然后再处理数据转换。
安装依赖
1
2
| sudo apt-get install libbluetooth-dev python-gobject bluez bluez-tools bluez-firmware python-bluez python-dev python-pip
pip install pybluez evdev pydbus future
|
获取 Linux 的输入事件
当我们直接用 USB 键盘插入到使用 Linux 系统的树莓派上的之后,键盘的输入将会通过 Linux 的输入子系统,以事件的形式发送出去。
我们可以使用 evdev 这个库来监听这些事件。
evdev 官方文档见:
https://python-evdev.readthedocs.io/en/latest/usage.html
输入事件获取测试
分两步:
- 查看有哪些输入设备,选择其中一个
- 获取选择设备的事件,并把事件打印出来
查看设备列表:
1
2
3
4
| import evdev
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
for device in devices:
print(device.path, device.name, device.phys)
|
会有类似以下的输出:
1
2
| /dev/input/event1 USB Keyboard usb-0000:00:12.1-2/input0
/dev/input/event0 USB Optical Mouse usb-0000:00:12.0-2/input0
|
选择键盘输入:
1
2
3
4
5
6
| import evdev
device = evdev.InputDevice('/dev/input/event1')
print(device)
for event in device.read_loop():
print(evdev.categorize(event))
|
如果有多个 Keyboard,则需要一个个尝试。
此时按键盘,屏幕将会有输出。例如:
1
2
3
4
5
6
7
8
9
10
| event at 1612004615.311680, code 04, type 04, val 458756
key event at 1612004615.311680, 30 (KEY_A), down
synchronization event at 1612004615.311680, SYN_REPORT
key event at 1612004615.576959, 30 (KEY_A), hold
synchronization event at 1612004615.576959, SYN_REPORT
key event at 1612004615.616967, 30 (KEY_A), hold
synchronization event at 1612004615.616967, SYN_REPORT
event at 1612004615.856933, code 04, type 04, val 458756
key event at 1612004615.856933, 30 (KEY_A), up
synchronization event at 1612004615.856933, SYN_REPORT
|
发送数据到蓝牙服务
启动蓝牙并设置为键盘服务
hciconfig
命令用于控制蓝牙设备。
/etc/init.d/bluetooth
用于控制蓝牙服务。
首先确保蓝牙设备已被关闭,然后启动蓝牙服务。
1
2
| sudo hciconfig hci0 down
sudo /etc/init.d/bluetooth start
|
现在再次启动蓝牙设备:
1
2
3
4
5
6
7
| sudo hciconfig hci0 up
# 将蓝牙类型设置为键鼠设备
sudo hciconfig hci0 class 0x0025C0
# 设置蓝牙名称
sudo hciconfig hci0 name my-keyboard
# 让其他蓝牙设备可以发现这个蓝牙设备
sudo hciconfig hci0 piscan
|
配置蓝牙服务
Linux 的官方蓝牙协议栈是 BlueZ,我们需要对 BlueZ 做一些配置。一共有两个必要的配置:UUID 和 ServiceRecord。
如果不配置 ServiceRecord,那么连接后键盘的输入会混乱。比如在 Windows 上按 d 这个按钮,会变成 Win + d。
ServiceRecord 是一个 xml 格式的数据。具体内容可以先不理解,直接用就行。内容如下:
sdp_record.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
| <?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="0xC0" />
</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="05010906a101850175019508050719e029e715002501810295017508810395057501050819012905910295017503910395067508150026ff000507190029ff8100c005010902a10185020901a100950575010509190129051500250181029501750381017508950305010930093109381581257f8106c0c0" />
</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="false" />
</attribute>
<attribute id="0x020f">
<uint16 value="0x0640" />
</attribute>
<attribute id="0x0210">
<uint16 value="0x0320" />
</attribute>
</record>
|
配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
| import dbus
bus = dbus.SystemBus()
bluez = bus.get_object("org.bluez", "/org/bluez")
manager = dbus.Interface(bluez, "org.bluez.ProfileManager1")
f_sdp = open("./sdp_record.xml", "r")
opts = {
"AutoConnect": True,
"ServiceRecord": f_sdp.read()
}
uuid = "05262649-d40f-483d-b445-73b000d19028"
manager.RegisterProfile("/org/bluez/hci0", uuid, opts)
|
创建服务器接收请求
- 创建蓝牙 Socket
- 等待连接
蓝牙需要创建两个 Socket,分别是控制管道 (control) 和中断管道 (interrupt)。其中 interrupt 用于发送数据。
https://blog.csdn.net/zhoutaopower/article/details/82469665
控制管道主要用于下面3个方面:
- 接收/响应USB主机的控制请求以及相关的类数据
- 在USB主机查询时传输数据(如响应Get_Report请求等)
- 接收USB主机的数据
中断管道主要用于下面两个方面: - USB主机接收USB设备的异步传输数据
- USB主机发送有实时性要求的数据给USB设备
以下端口要与 sdp_record 里面配置的一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import socket
PORT_CTRL = 17
s_control = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
s_control.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s_control.bind((socket.BDADDR_ANY, PORT_CTRL))
s_control.listen(5)
conn_control, addr_info = s_control.accept()
PORT_INTR = 19
s_interrupt = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
s_interrupt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s_interrupt.bind((socket.BDADDR_ANY, PORT_INTR))
s_interrupt.listen(5)
conn_interrupt, addr_info = s_interrupt.accept()
# 13 是按键 j
message = [ 0xA1, 1, 0, 0, 13, 0, 0, 0, 0, 0 ]
conn_interrupt.send(bytes(message))
message = [ 0xA1, 1, 0, 0, 0, 0, 0, 0, 0, 0 ]
conn_interrupt.send(bytes(message))
|
发送给蓝牙的数据是 HID 协议格式的数据。
第一个数字表示这是一个输入报告。二进制表示为 10100001,前面的 101000 表示 Collection ,后面的 01 表示 Application (mouse, keyboard)。
第二个数字当 1 时表示键盘,2 时表示鼠标。
第三个数字表示 Modifier Keys。二进制的 8 个 bit 分别代表:左 ctrl, 左 shift, 左 alt, 左 Win, 右 ctrl, 右 shift, 右 alt, 右 Win
第四个数字是保留字符。
最后六个数字分别代表一个普通按键。例如数字键和字母键。这就表示一次按下最多有效的普通按键是 6 个。
https://www.usb.org/document-library/device-class-definition-hid-111
可下载 pdf 文档。
第一个数字的解释来自于: 6.2.2.4 Main Items
第二个数字的解释来自于: 4.3 Protocols
第三个开始的数字的解释来自于:Appendix B: Boot Interface Descriptors