项目介绍
本项目使用了 Silicon Labs 单片机制作了一款可编程的鼠标,并提供了其对应的 Python 控制代码。设备从电脑上接收控制信号,并将控制信号转化为HID报文并通过蓝牙发送到任意设备上。通过设置不同的报文信息,理论上可以实现任意的鼠标操作,甚至包含人类不可做到的毫秒级点击、移动。
该项目的背景是目前一部分应用程序对鼠标自动操作进行了拦截,导致无法使用Python进行自动化控制。但是可编程鼠标作为一个外界的物理硬件不会被应用程序所拦截,可以正常的使用。
开发平台
EFR32xG24 Explorer套件是一个基于EFR32MG24片上系统的超低成本、小尺寸开发和评估平台,该套件专注于物联网应用的快速原型化和概念创建2.4 GHz无线协议的IoT应用程序,包括蓝牙LE、蓝牙Mesh、Zigbee、Thread 和 Matter。它是围绕EFR32MG24 SoC设计的,而EFR32MG24 SoC是开发能源友好型联网物联网应用的理想器件系列。
设计思路
在这个项目中,我在硬件方面实现了串口信息的接收、转化和提取,同时还将上位机部分的鼠标控制代码抽象为了一个 Python 类,可以插入到任意现有的 Python 程序中,以替代 PyautoGUI 等框架。目前已经实现了单击、移动等常见的鼠标操作。与此同时,单片机部分还额外实现了键盘的控制代码,可以自行进行补充键盘部分的控制功能。
硬件部分使用了 Silicon Labs 官方的 IDE 进行开发。在现有的 Bluetooth - HID Keyboard 模板上,增加了关于HID鼠标相关的报文。同时额外增加了一个状态变量,使用BTN0按钮中断改变收发状态。总体的流程图如下图所示:
当单片机启动之后会进入一个输入字符的循环,接收到结束符后会将接收的信息设置为 HID report,并通过蓝牙发送到已连接的设备上。一个按键中断。板上的 BTN0 按钮按下就会切换系统的控制状态。当控制状态为 false 的时候,将不会响应任何来自电脑的操作请求。
上位机部分使用 Python 代码实现。功能设计思路如下:
- 要打开串口
- 将用户的抽象操作转化为HID报文
- 通过串口将报文发送到单片机上
设计要点
在MCU端,由于目前使用的程序修改自官方的 Bluetooth - HID Keyboard 示例上,其本身不具备鼠标的功能。我参照博客实现了键鼠复合设备的HID reportmap。
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xa1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0xe0, // Usage Minimum (Keyboard LeftControl)
0x29, 0xe7, // Usage Maximum (Keyboard Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute) Modifier byte
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant) Reserved byte
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (Reserved (no event indicated))
0x29, 0x65, // Usage Maximum (Keyboard Application)
0x81, 0x00, // Input (Data, Array) Key arrays (6 bytes)
0xc0 // End Collection
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x02, // REPORT_ID (2)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x03, // Usage Maximum (3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input(Data, Variable, Absolute); 3 button bits
0x95, 0x01, // Report Count(1)
0x75, 0x05, // Report Size(5)
0x81, 0x03, // Input(Constant); 5 bit padding
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x03, // Report Count (3)
0x81, 0x06, // Input(Data, Variable, Relative); 3 position bytes (X,Y,Wheel)
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
在实践之后,我发现官方提供的蓝牙发送代码中不包含对 Report ID 进行设置的功能。探索未果后,在群友的指导下完成了 ReportID 的设置。在图形化设置界面中新增一个 Report 并修改 Report ID,即可在自动生成的代码中找到其对应的索引值。
串口接收部分通过不断的读取字符实现用户输入字符的存储功能,当检测到结束字符时会发送对应的结果。这一过程中存在的问题是,当用户没有输入时,getchar
返回的值为 0xff,这导致的直接问题是,在发送鼠标和键盘的移动距离时无法发送 0xff。因此在上位机部分对这部分功能进行了一些调整,当输入数值的范围超过上限时,会自动进行调整。
上位机代码部分中,有两个点需要注意。第一点是默认的鼠标 HID ReportMap 中,X轴和Y轴的移动距离是使用八位有符号数进行表示的。在上位机发送对应的数值时,由于Python的编码的原因,需要将有符号输入其对应的补码转换成无符号数的形式,以保证发送的正确性。
第二点是 windows 本身允许调整鼠标移动速度带来的移动数值误差。在编写移动到绝对坐标功能时,我本来的实现思路是使用 PyautoGUI 库获取鼠标的当前位置,再与目标位置计算得到X轴和Y轴的相对移动距离,之后进行移动。但实际上,由于 Windows 平台允许用户调整鼠标的移动速度,设置的数值并不能对应实际移动的像素值。为此我在移动之后会检查当前位置是否与目标位置仍有偏差,如果偏差大于某个值,就继续重复移动过程。由于每次移动之后等待的间隔只有0.1秒,所以上述过程对于用户来说其实是不可感知的。
def move(self, x, y):
"""Move the mouse to the specified position."""
while True:
cur_x, cur_y = pyautogui.position()
x_move = x - cur_x
y_move = y - cur_y
# calculate the real move distance based on the screen resolution and the system mouse speed setting.
x_move = int(x_move / self.windows_mouse_speed)
y_move = int(y_move / self.windows_mouse_speed)
print(f" cur pos {cur_x} {cur_y}, diff {x_move} {y_move}")
if abs(x_move) < self.move_threshold and abs(y_move) < self.move_threshold:
print("break")
break
# The max distance of a move event is 126 units in any direction. To guarantee the uart transmission.
# We need to split the move event into several smaller events.
while abs(x_move) > 0 or abs(y_move) > 0:
print(f"move {x_move} {y_move}")
if abs(x_move) > 126:
self.set_x(126 if x_move > 0 else -126)
x_move -= 126 if x_move > 0 else -126
else:
self.set_x(x_move)
x_move = 0
if abs(y_move) > 126:
self.set_y(126 if y_move > 0 else -126)
y_move -= 126 if y_move > 0 else -126
else:
self.set_y(y_move)
y_move = 0
print(self.x, self.y)
self.send()
time.sleep(0.1)
最后为了测试这部分代码的普适性,我下载了一个来自 GitHub 的开源 PVZ-OpenCV 项目来测试鼠标的移动效果。具体来说,我把原来项目当中使用 PyautoGUI 进行移动和点击的代码替换成了自己的鼠标类。实际效果如视频所示,鼠标有着较快的反应速度。
总结
关于这个项目的总结是,目前 SiliconLabs 官方的一些教程并不是很完整。在尝试修改 HID report map 的时候,大部分教程都是根据 STM32 进行迁移的,实现起来有些麻烦。同时 SiliconLabs 的蓝牙的配置是完全图形化的,但是我没有找到相关的文档。就导致更改 HID report ID 的时候出现了很多问题,不过好在有群友的帮助。
自己实现一个蓝牙键盘鼠标是自接触硬件以来一直的一个小目标,有机会能够借着这次项目进行实现,整体还是比较开心的。
参考:
- 官方的BLE键盘教程:https://docs.silabs.com/bluetooth/2.13/code-examples/applications/ble-hid-keyboard
- 【BLE】HID设备的实现(蓝牙自拍杆、蓝牙键盘、蓝牙鼠标、HID复合设备):https://blog.csdn.net/qq_34254642/article/details/126672201