USB 键盘整蛊专家

这是一个能够让你整蛊别人的设备,将它串联到对方的USB 键盘和主机之间后,你可以用过手机上的 Blinker蓝牙连接到这个设备,然后在 Blinker中输出的信息就会出现在对方的电脑上。

硬件设计如下:

  1. 左上角是预留的调试烧录接口,通过这个接口可以进行烧录,同时  Debug信息也可以通过这个接口发送到 PC端;
  2. 左下角是这个的设计核心,它是一个 ESP32-S3 芯片,通过它实现USB Host 和蓝牙通讯的功能;
  3. ESP32-S3工作电压是 3.3V,这里使用 TLV1117 来实现,这个芯片外围只需要2个 1uf电容
  4. 右下角是 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 下载: