这是一个能够让你整蛊别人的设备,将它串联到对方的USB 键盘和主机之间后,你可以用过手机上的 Blinker蓝牙连接到这个设备,然后在 Blinker中输出的信息就会出现在对方的电脑上。
硬件设计如下:
- 左上角是预留的调试烧录接口,通过这个接口可以进行烧录,同时 Debug信息也可以通过这个接口发送到 PC端;
- 左下角是这个的设计核心,它是一个 ESP32-S3 芯片,通过它实现USB Host 和蓝牙通讯的功能;
- ESP32-S3工作电压是 3.3V,这里使用 TLV1117 来实现,这个芯片外围只需要2个 1uf电容
- 右下角是 Ch9329 芯片,它是一个 HID 转串口芯片,在这个设计中用于实现USB键盘的功能。
CH9326是一款HID转串口免驱芯片。CH9326支持双向数据传输,用于接收串口数据,并按照HID类设备规范,将数据打包通过USB口上传给计算机,或者从计算机接收符合HID类设备的USB数据包,并从串口进行发送。通过提供的上位机软件,用户也可自行配置芯片的VID、PID,以及各种字符串描述符。芯片是 SOP16 封装,容易焊接。
设计的基本思路是:ESP32-S3 负责解析USB键盘数据,用这种方法来获得按键信息。之后,将获得的信息通过串口发送给CH9326, 然后 Ch9326会实现PC端的模拟按键。可以看到,这个设备对于PC端来说是透明的。之后,可以使用 Blinker 的蓝牙功能连接手机和这个设备,之后就可以从手机端发送字符给PC。
PCB 设计如下:
成品如下(彩色丝印,镀金工艺,背面是设计的一个二维码):
编写 Arduino 代码如下:
#include <elapsedMillis.h>
#include <usb/usb_host.h>
#include "show_desc.hpp"
#include "usbhhelp.hpp"
#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};
bool isKeyboard = false;
bool isKeyboardReady = false;
uint8_t KeyboardInterval;
bool isKeyboardPolling = false;
elapsedMillis KeyboardTimer;
const size_t KEYBOARD_IN_BUFFER_SIZE = 8;
usb_transfer_t *KeyboardIn = NULL;
// 将 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);
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 keyboard_transfer_cb(usb_transfer_t *transfer)
{
if (Device_Handle == transfer->device_handle) {
isKeyboardPolling = false;
if (transfer->status == 0) {
if (transfer->actual_num_bytes == 8) {
uint8_t *const p = transfer->data_buffer;
ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
// USB Host 解析得到的数据,传输给PC
//
memcpy(&keypress[5],p,transfer->actual_num_bytes);
SendData((byte*)keypress, sizeof(keypress));
}
else {
ESP_LOGI("", "Keyboard boot hid transfer too short or long");
}
}
else {
ESP_LOGI("", "transfer->status %d", transfer->status);
}
}
}
void check_interface_desc_boot_keyboard(const void *p)
{
const usb_intf_desc_t *intf = (const usb_intf_desc_t *)p;
if ((intf->bInterfaceClass == USB_CLASS_HID) &&
(intf->bInterfaceSubClass == 1) &&
(intf->bInterfaceProtocol == 1)) {
isKeyboard = true;
ESP_LOGI("", "Claiming a boot keyboard!");
esp_err_t err = usb_host_interface_claim(Client_Handle, Device_Handle,
intf->bInterfaceNumber, intf->bAlternateSetting);
if (err != ESP_OK) ESP_LOGI("", "usb_host_interface_claim failed: %x", err);
}
}
void prepare_endpoint(const void *p)
{
const usb_ep_desc_t *endpoint = (const usb_ep_desc_t *)p;
esp_err_t err;
// must be interrupt for HID
if ((endpoint->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) != USB_BM_ATTRIBUTES_XFER_INT) {
ESP_LOGI("", "Not interrupt endpoint: 0x%02x", endpoint->bmAttributes);
return;
}
if (endpoint->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) {
err = usb_host_transfer_alloc(KEYBOARD_IN_BUFFER_SIZE, 0, &KeyboardIn);
if (err != ESP_OK) {
KeyboardIn = NULL;
ESP_LOGI("", "usb_host_transfer_alloc In fail: %x", err);
return;
}
KeyboardIn->device_handle = Device_Handle;
KeyboardIn->bEndpointAddress = endpoint->bEndpointAddress;
KeyboardIn->callback = keyboard_transfer_cb;
KeyboardIn->context = NULL;
isKeyboardReady = true;
KeyboardInterval = endpoint->bInterval;
ESP_LOGI("", "USB boot keyboard ready");
}
else {
ESP_LOGI("", "Ignoring interrupt Out endpoint");
}
}
void show_config_desc_full(const usb_config_desc_t *config_desc)
{
// Full decode of config desc.
const uint8_t *p = &config_desc->val[0];
static uint8_t USB_Class = 0;
uint8_t bLength;
for (int i = 0; i < config_desc->wTotalLength; i += bLength, p += bLength) {
bLength = *p;
if ((i + bLength) <= config_desc->wTotalLength) {
const uint8_t bDescriptorType = *(p + 1);
switch (bDescriptorType) {
case USB_B_DESCRIPTOR_TYPE_DEVICE:
ESP_LOGI("", "USB Device Descriptor should not appear in config");
break;
case USB_B_DESCRIPTOR_TYPE_CONFIGURATION:
show_config_desc(p);
break;
case USB_B_DESCRIPTOR_TYPE_STRING:
ESP_LOGI("", "USB string desc TBD");
break;
case USB_B_DESCRIPTOR_TYPE_INTERFACE:
USB_Class = show_interface_desc(p);
check_interface_desc_boot_keyboard(p);
break;
case USB_B_DESCRIPTOR_TYPE_ENDPOINT:
show_endpoint_desc(p);
if (isKeyboard && KeyboardIn == NULL) prepare_endpoint(p);
break;
case USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER:
// Should not be config config?
ESP_LOGI("", "USB device qual desc TBD");
break;
case USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION:
// Should not be config config?
ESP_LOGI("", "USB Other Speed TBD");
break;
case USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER:
// Should not be config config?
ESP_LOGI("", "USB Interface Power TBD");
break;
case 0x21:
if (USB_Class == USB_CLASS_HID) {
show_hid_desc(p);
}
break;
default:
ESP_LOGI("", "Unknown USB Descriptor Type: 0x%x", bDescriptorType);
break;
}
}
else {
ESP_LOGI("", "USB Descriptor invalid");
return;
}
}
}
void setup()
{
// 初始化调试串口
Serial.begin(115200);
// 初始 CH9329 串口
Serial2.begin(9600, SERIAL_8N1, 14, 13, false, 1000, 112);
//Serial2.begin(9600);
#if defined(BLINKER_PRINT)
BLINKER_DEBUG.stream(BLINKER_PRINT);
#endif
// 初始化blinker
Blinker.begin();
Blinker.attachData(dataRead);
usbh_setup(show_config_desc_full);
}
void loop()
{
usbh_task();
Blinker.run();
if (isKeyboardReady && !isKeyboardPolling && (KeyboardTimer > KeyboardInterval)) {
KeyboardIn->num_bytes = 8;
esp_err_t err = usb_host_transfer_submit(KeyboardIn);
if (err != ESP_OK) {
ESP_LOGI("", "usb_host_transfer_submit In fail: %x", err);
}
isKeyboardPolling = true;
KeyboardTimer = 0;
}
while (Serial.available()) {
char c = Serial.read();
if (c == 'q') {
boolean shift = false;
// 填写要发送的 ScanCode
keypress[5] = 0x08;
SendData((byte*)keypress, sizeof(keypress));
delay(20);
keypress[5] = 0;
SendData((byte*)keypress, sizeof(keypress));
}
Serial.print(c);
}
}
将板卡装入外壳后的照片:
完整的代码:
电路图和PCB 下载: