这次做的项目能够帮你把手上的手机变成能够控制电脑的键盘和鼠标。
基本原理上是:用户通过手机应用程序经由BLE蓝牙和FireBeetle 进行通讯,FireBeetle收到之后再通过USB接口将数据发送到电脑上。从原理上看整体分作三部分:硬件的选择和设计,Arduino 代码的编写和手机端程序设计。
首先介绍硬件的选择和设计。FireBeetle是DFRobot 出品的基于 ESP32 的开发板,它能够支持蓝牙和 WIFI 的通讯,它带有USB转串口芯片但是无法将自身模拟为USB键盘鼠标设备,为了实现这个功能,还需要设计一个USB键盘鼠标Shield。经过研究,最终选择了 WCH 出品的CH9329芯片进行实现。CH9329是一款串口转标准 USB HID 设备(键盘、鼠标、自定义 HID)芯片,根据不同的工作模式,在电脑上可被识别为标准的 USB 键盘设备、 USB 鼠标设备或自定义 HID 类设备。该芯片接收客户端发送过来的串口数据,并按照 HID 类设备规范,将数据先进行打包再通过 USB 口上传给计算机。这款芯片基本特性如下:
●支持 12Mbps 全速 USB 传输,兼容 USB V2.0,内置晶振。
● 默认串口通信波特率为 9600bps,支持各种常见波特率。
● 支持 5V 电源电压和 3.3V 电源电压。
● 多种芯片工作模式, 适应不同应用需求。
● 多种串口通信模式,灵活切换。
● 支持普通键盘和多媒体键盘功能,支持全键盘功能。
● 支持相对鼠标和绝对鼠标功能。
● 支持自定义 HID 类设备功能,可用于单纯数据传输。
● 支持 ASCII 码字符输入和区位码汉字输入。
● 支持远程唤醒电脑功能。
● 支持串口或 USB 口配置芯片参数。
● 可自行配置芯片的 VID、 PID,以及芯片各种字符串描述符。
● 可自行配置芯片的默认波特率。
● 可自行配置芯片通信地址,实现同一个串口下挂载多个芯片。
● 可自行配置回车字符。
● 可自行配置过滤字符串,以便进行无效字符过滤。
● 符合 USB 相关规范,符合 HID 类设备相关规范。
● 采用小体积的 SOP-16 无铅封装,兼容 RoHS。
对于这次的设计来说,通过串口就能实现USB键盘鼠标,非常方便。确定了芯片之后,接下来即可着手Shield设计了。电路设计如下:
左侧和中间是 FireBeetle 的接口,右侧是USB 公头,右下是CH9329芯片。
下面是CH9329 的最小系统电路,芯片内置了晶振,外部只需要一个0.1uf(C1)的电容即可正常工作。
图中的 Pin1 是用来标志芯片配置完成的引脚(#ACT),Pin2、3、4、5是用来配置芯片功能的引脚,通过组合可以在上电的时候实现芯片的功能选择。
工作模式 | MODE1电平 | MODE0 电平 | 功能说明 |
模式0 | 1 | 1 | 模拟标准USB键盘+USB鼠标设备+USB自定义HID类设备(默认) 该模式下CH9329芯片在电脑上识别为USB键盘、USB鼠标和自定义HID类设备的多功能复合设备,USB键盘包含普通键和多媒体键, USB鼠标包含相对鼠标和绝对鼠标。 该模式功能最全,可以实现USB键盘和USB鼠标的全部功能。 MODE0引脚和MODE1引脚内置了上拉电阻,当这两个引脚悬空时,芯片处于本模式。 |
模式1 | 1 | 0 | 模拟标准USB键盘设备 该模式下CH9329芯片在电脑上识别为单一USB键盘设备, USB键盘只包含普通键,不包含多媒体键,支持全键盘模式,适用于部分不支持复合设备的系统。 |
模式2 | 0 | 1 | 模拟标准USB键盘+USB鼠标设备 该模式下CH9329芯片在电脑上识别为USB键盘和USB鼠标的多功能复合设备, USB键盘包含普通键和多媒体键, USB鼠标包含相对鼠标和绝对鼠标。 注: Linux/Android/苹果等操作系统下, 出于兼容性考虑,建议使用该模式。 |
模式3 | 0 | 0 | 模拟标准USB自定义HID类设备 该模式下CH9329芯片在电脑上识别为单一USB自定义HID类设备,具有上传和下传2个通道,可以实现串口和HID数据透传功能。CH9329芯片如果接收到串口数据,则打包通过USB上传,如果接收到USB下传数据,则通过串口进行发送。 这个模式可以方便用户实现串口转HID。 |
串口通信模式 | CFG1电平 | CFG0电平 | 功能 |
模式0 | 1 | 1 | 协议传输模式(默认) 该模式一般适用于既需要使用USB键盘功能,又 需要使用USB鼠标功能的应用。如果需要使用全 键盘功能,也建议采用该模式。 CFG0引脚和CFG1引脚内置了上拉电阻,当这两个引脚悬空时,芯片处于本模式。 |
模式1 | 1 | 0 | ASCII模式 该模式下客户串口设备向CH9329芯片发送串口 数据时,可以发送ASCII码字符数据,也可以发 送区位码汉字数据。 该模式适用于只需要使用USB键盘中可见ASCII 字符的应用。 |
模式2 | 0 | 1 | 透传模式 该模式下客户串口设备向CH9329芯片发送串口 数据时,可以是任意16进制数据。 该模式适用于CH9329芯片处于芯片工作模式3的 应用。 |
PCB 设计如下:
3D预览:
接下来开始手机端程序的设计。经过考察,选择了点灯科技出品的 Blinker,这是一套专业且易用物联网解决方案,提供了服务器、应用、设备端SDK支持。简单便捷的应用配合多设备支持的SDK,可以让开发者在3分钟内实现设备的接入。 点灯服务有三个版本,社区版开源且免费,让大家可以体验到点灯方案的特点和优势;云服务版提供更多增值服务与功能,且有效降低客户的项目实施成本,让客户更快的进行物联网升级;商业版可进行独立部署,可以满足客户更多样的需求。这次我们使用它提供的ESP32 支持通过蓝牙连接FireBeetle 开发板。首先,安装 Arduino 的库,在https://diandeng.tech/dev 页面下载 Arduino 库。之后解压放到 Arduino 的 Library目录下。
之后,烧写示例文件:
\blinker-library\examples\Blinker_Widgets\Blinker_Button\Button_BLE\Button_BLE.ino
打开手机上的“点灯 Blinker”程序之后开始创建控制设备的应用:
1.创建一个新设备:
2.添加一个独立设备
3.选择蓝牙接入
4.这时手机会执行一个搜索蓝牙设备的动作,这也是为什么要提前刷上一个示例代码的原因
5.在界面上放置一个输入框(当作键盘用于输入字符),一个摇杆组件(用于控制鼠标)和六个按钮(分别用于实现鼠标左键单击,左键双击,中键单击,右键单击,以及输入键盘回车键)
6.每个组件可以进行属性的调整,包括显示的文字和名称。
设置好了之后,在界面上操作数据可以在Arduino 的串口监视器中看到当有事件信息,其中有摇杆的动作、按钮事件和文本框的输入内容。
接下来就可以进行 Arduino 代码的编写了, 关键代码有:
- 代码首部加入#define BLINKER_BLE这个定义后, Blinker 库能够帮助用户完成大部分的蓝牙操作,使用者只需要专注于“收到数据如何处理”而不必关心“如何收到数据”。
- Setup函数中通过Button1.attach(button1_callback); 绑定按键和处理函数,当按键发生后会自动调用 button1_callback() 函数来处理;
- Setup函数中通过Blinker.attachData(dataRead);绑定数据处理函数, dataRead() 函数能够收到输入框和摇杆的数据;
- 收到的输入框数据是 ASCII 码,通过Asc2Scancode() 函数转化为HID Scancode 再发送给 CH9329 芯片;
#define BLINKER_PRINT Serial
#define BLINKER_BLE
#include <Blinker.h>
//键盘数据
char keypress[] = {0x57, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10};
//鼠标数据
char mousemove[] = {0x57, 0xAB, 0x00, 0x05, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
// 左键单击
BlinkerButton Button1("btn-l1");
// 左键双击
BlinkerButton Button2("btn-l2");
// 右键单击
BlinkerButton Button3("btn-r1");
// 中键单击
BlinkerButton Button4("btn-m1");
// 回车
BlinkerButton Button5("btn-rtn");
// 左键单击
void button1_callback(const String & state) {
BLINKER_LOG("Left Click ", state);
// 触发鼠标左键
mousemove[6] = 0x01;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
// 鼠标左键抬起
mousemove[6] = 0x00;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
}
// 左键双击
void button2_callback(const String & state) {
BLINKER_LOG("Left Double Click ", state);
// 触发鼠标左键
mousemove[6] = 0x01;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
// 鼠标左键抬起
mousemove[6] = 0x00;
SendData((byte*)mousemove, sizeof(mousemove));
delay(20);
// 再来一次
mousemove[6] = 0x01;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
mousemove[6] = 0x00;
SendData((byte*)mousemove, sizeof(mousemove));
delay(20);
}
// 右键单击
void button3_callback(const String & state) {
BLINKER_LOG("Right Click ", state);
// 触发鼠标右键
mousemove[6] = 0x02;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
mousemove[6] = 0x00;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
}
// 中键双击
void button4_callback(const String & state) {
BLINKER_LOG("Middle Click ", state);
// 触发鼠标中键
mousemove[6] = 0x04;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
mousemove[6] = 0x00;
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
}
// 回车
void button5_callback(const String & state) {
BLINKER_LOG("Enter ", state);
// 键盘回车
keypress[7] = 0x28;
SendData((byte*)keypress, sizeof(keypress));
delay(10);
keypress[7] = 0;
SendData((byte*)keypress, sizeof(keypress));
delay(10);
}
// 将 Buffer 指向的内容,size 长度,计算 checksum 之后发送到Serial2
void SendData(byte *Buffer, byte size) {
byte sum = 0;
for (int i = 0; i < size - 1; i++) {
Serial2.write(*Buffer);
sum = sum + *Buffer;
Buffer++;
}
*Buffer = sum;
Serial2.write(sum);
}
// 将ASCII 字符转化为 HID Scancode值
byte Asc2Scancode(byte Asc, boolean *shift) {
if ((Asc >= 'a') && (Asc <= 'z')) {
*shift = false;
return (Asc - 'a' + 0x04);
}
if ((Asc >= 'A') && (Asc <= 'Z')) {
*shift = true;
return (Asc - 'A' + 0x04);
}
if ((Asc >= '1') && (Asc <= '0')) {
*shift = false;
return (Asc - '0' + 0x1E);
}
if (Asc == '>') {
*shift = true;
return (0x37);
}
if (Asc == '.') {
*shift = false;
return (0x37);
}
if (Asc == '_') {
*shift = true;
return (0x2D);
}
if (Asc == '-') {
*shift = false;
return (0x2D);
}
return 0;
}
// 如果未绑定的组件被触发,则会执行其中内容
// 这里的游戏摇杆和输入框都会在这里处理
void dataRead(const String & data)
{
BLINKER_LOG("Blinker readString: ", data);
// 判断是否为游戏摇杆
if (data.indexOf("joy") != -1) {
BLINKER_LOG("Joy Move");
String StrX, StrY;
// 将摇杆坐标从输入中分离出来
StrX = data.substring(data.indexOf("[") + 1, data.indexOf(","));
StrY = data.substring(data.indexOf(",") + 1, data.indexOf("]"));
BLINKER_LOG("", StrX); BLINKER_LOG("", StrY);
// 摇杆数据按照鼠标发送出去
mousemove[7] = map(StrX.toInt(), 0, 255, -127, 127);
mousemove[8] = map(StrY.toInt(), 0, 255, -127, 127);
SendData((byte*)mousemove, sizeof(mousemove));
delay(10);
mousemove[7] = 0;
mousemove[8] = 0;
} else {
boolean shift;
byte scanCode;
for (int i = 0; i < data.length(); i++) {
BLINKER_LOG("Key In", data.charAt(1));
// 将收到的 ASCII 转为 ScanCode
scanCode = Asc2Scancode(data.charAt(i), &shift);
// 一些按键当有 Shift 按下时会发生转义
if (scanCode != 0) {
if (shift == true) {
keypress[5] = 0x02;
}
BLINKER_LOG("Scancode", scanCode);
// 填写要发送的 ScanCode
keypress[7] = scanCode;
SendData((byte*)keypress, sizeof(keypress));
delay(10);
keypress[5] = 0x00; keypress[7] = 0;
SendData((byte*)keypress, sizeof(keypress));
delay(10);
}
}
}
}
void setup() {
// 初始化调试串口
Serial.begin(115200);
// 初始 CH9329 串口
Serial2.begin(9600);
#if defined(BLINKER_PRINT)
BLINKER_DEBUG.stream(BLINKER_PRINT);
#endif
// 初始化blinker
Blinker.begin();
Blinker.attachData(dataRead);
Button1.attach(button1_callback);
Button2.attach(button2_callback);
Button3.attach(button3_callback);
Button4.attach(button4_callback);
Button5.attach(button5_callback);
}
void loop() {
Blinker.run();
}
工作的视频