【ESP32】两种模拟 USB 鼠标的方法

上一篇水文中,老周给大伙伴们扯了关于 idf 中添加自定义 Arduino 组件的方案。这次咱们做一下 USB 鼠标玩玩。

很遗憾的是,老周无能,在 Arduino-esp32 组件依赖 TinyUSB 组件时无法进行编译,不管怎么配置都会找不到 tusb.h 文件;就算把 tinyUSB 内置到 arduino-esp32 的源码中也报错;调整代码中的 extern C 语句,又会导致找不到 C++ 类......反正,就是搞不下来。不过,在 idf 中使用 esp_tinyusb 组件是可以正常编译的。据说官方的 arduino-lib-builder 项目 clone 下来是可以正常编译(当然,官方只是说在 Ubuntu 和 树莓派 上测试通过,并没说在 Windows 下可以编译。有人说在 WSL 中可以编译,不过老周未测试,不敢下结论)。

思考其原因,大概有三:1、C 和 C++ 代码混合编译经常会这样;2、可能需要定义特殊的宏;3、官方的 builder 项目中是要对代码"打补丁"后再编译的,可能要改什么。

其实,自己编译一般是有计划修改源代码或订制自己的库。如果没这个需求,咱们直接用官方编译好的库,可以少一些折腾。

根据老周实战的结果,给大伙伴推荐两种 esp32 模拟 USB 鼠标的方案(为了让大伙学得没有压力,USB 键盘暂时不弄)。这两种方案老周都是验证过的,能运行,并且电脑能识别出鼠标。接下来,开工!

方案A:使用 Arduino 库(这种是最简单的)

因为用到 Arduino IDE,老周简单说一下安装事项。咱们作为一名合格的、有技术含量的、迷倒千万妹子的码农,绝对不能在安装开发工具这个关卡给夹脑袋,否则,说句好唱不好听的,真的太低能了。Arduino 2 是重新开发过的,有那么点 VS Code 的味了。老周建议下载 .zip 版本,这个是最好的,解压出来,想放哪就放哪,不依赖系统目录。

打开Arduino IDE,执行菜音【文件】-【首选项】。在设置窗口中滚动到下方,有个"其他开发板管理地址",点击输入框右边的按钮。

在弹出的对话框中填入以下URL,并点"确定"。

复制代码
https://github.com/espressif/arduino-esp32/releases/download/3.2.0/package_esp32_index.json

设置这个URL后才能获取到乐鑫官方最新的库。

接下来最折腾的是安装 esp32 库,因为不可描述的原因,有时会连不上 github,导致很多压缩包下载不了。

在IDE的开发板管理器窗格中,搜索"esp32",就能找到乐鑫官方维护的库,现在最新是 3.2.0。

不过,相信各位都知道有文件加速这种网站,你网上搜搜就有了。我们可以从 JSON 文件中获取到各个压缩包的下载链接的,方法如下:

1、找到你的用户目录下的 AppData/local,里面有个 Arduino15 目录;

2、进去 Arduino15 目录,你会看到几个 JSON 文件;

3、如果你只使用发布版本,不使用预览版,那直接找到 package_esp32_index.json 文件;

4、打开上面提到的 JSON 文件(用 VS Code 最好),从 platforms 下的 toolsDependencies 节点可以知道依赖的工具。

复制代码
{
  "name": "esp32",
  "architecture": "esp32",
  "version": "3.2.0",
  "category": "ESP32",
  "url": "https://github.com/espressif/arduino-esp32/releases/download/3.2.0/esp32-3.2.0.zip",
  "archiveFileName": "esp32-3.2.0.zip",
  "checksum": "SHA-256:d38b16fef6e519fc0d19bc5af0b39cdbed7dfc2ce69214c1971ded0e61ecd911",
  "size": "25447136",
  "help": {
    "online": ""
  },
  "boards": [
    {
      "name": "ESP32 Dev Board"
    },
    {
      "name": "ESP32-C3 Dev Board"
    },
    {
      "name": "ESP32-C6 Dev Board"
    },
    {
      "name": "ESP32-H2 Dev Board"
    },
    {
      "name": "ESP32-P4 Dev Board"
    },
    {
      "name": "ESP32-S2 Dev Board"
    },
    {
      "name": "ESP32-S3 Dev Board"
    },
    {
      "name": "Arduino Nano ESP32"
    }
  ],
  "toolsDependencies": [
    {
      "packager": "esp32",
      "name": "esp32-arduino-libs",
      "version": "idf-release_v5.4-2f7dcd86-v1"
    },
    {
      "packager": "esp32",
      "name": "esp-x32",
      "version": "2411"
    },
    {
      "packager": "esp32",
      "name": "xtensa-esp-elf-gdb",
      "version": "14.2_20240403"
    },
    {
      "packager": "esp32",
      "name": "esp-rv32",
      "version": "2411"
    },
    {
      "packager": "esp32",
      "name": "riscv32-esp-elf-gdb",
      "version": "14.2_20240403"
    },
    {
      "packager": "esp32",
      "name": "openocd-esp32",
      "version": "v0.12.0-esp32-20241016"
    },
    {
      "packager": "esp32",
      "name": "esptool_py",
      "version": "4.9.dev3"
    },
    {
      "packager": "esp32",
      "name": "mkspiffs",
      "version": "0.2.3"
    },
    {
      "packager": "esp32",
      "name": "mklittlefs",
      "version": "3.0.0-gnu12-dc7f933"
    },
    {
      "packager": "arduino",
      "name": "dfu-util",
      "version": "0.11.0-arduino5"
    }
  ]
},

然后在 Arduino IDE 中看看哪个文件下载挂了。

错误信息中已经告诉咱们下载链接了,直接复制到加速工具下载。下载后扔到 Arduino15/stagging/packages 目录下,然后重新打开 Arduino IDE ,再安装一次。直到所有包都正确下载。如果错误信息中没看到URL,可以根据工具名称和版本,在上面提到的 package_esp32_index.json 文件中查找下载地址。

复制代码
          "name": "esp-rv32",
          "version": "2411",
          "systems": [
            {
              "host": "x86_64-pc-linux-gnu",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-x86_64-linux-gnu.tar.gz",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-x86_64-linux-gnu.tar.gz",
              "checksum": "SHA-256:a16942465d33c7f0334c16e83bc6feb62e06eeb79cf19099293480bb8d48c0cd",
              "size": "593721156"
            },
            {
              "host": "aarch64-linux-gnu",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-aarch64-linux-gnu.tar.gz",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-aarch64-linux-gnu.tar.gz",
              "checksum": "SHA-256:22486233d0e0fd58a54ae453b701f195f1432fc6f2e17085b9d6c8d5d9acefb7",
              "size": "587879927"
            },
            {
              "host": "arm-linux-gnueabihf",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-arm-linux-gnueabi.tar.gz",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-arm-linux-gnueabi.tar.gz",
              "checksum": "SHA-256:27a72d5d96cdb56dae2a1da5dfde1717c18a8c1f9a1454c8e34a8bd34abe662d",
              "size": "586531522"
            },
            {
              "host": "i686-pc-linux-gnu",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-i586-linux-gnu.tar.gz",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-i586-linux-gnu.tar.gz",
              "checksum": "SHA-256:b7bd6e4cd53a4c55831d48e96a3d500bfffb091bec84a30bc8c3ad687e3eb3a2",
              "size": "597070471"
            },
            {
              "host": "x86_64-apple-darwin",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-x86_64-apple-darwin_signed.tar.gz",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-x86_64-apple-darwin_signed.tar.gz",
              "checksum": "SHA-256:5f8b571e1aedbe9f856f3bdeca6600cd5510ccff1ca102c4f001421eda560585",
              "size": "602343061"
            },
            {
              "host": "arm64-apple-darwin",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-aarch64-apple-darwin_signed.tar.gz",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-aarch64-apple-darwin_signed.tar.gz",
              "checksum": "SHA-256:a7276042a7eb2d33c2dff7167539e445c32c07d43a2c6827e86d035642503e0b",
              "size": "578521565"
            },
            {
              "host": "i686-mingw32",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-i686-w64-mingw32.zip",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-i686-w64-mingw32.zip",
              "checksum": "SHA-256:54193a97bd75205678ead8d11f00b351cfa3c2a6e5ab5d966341358b9f9422d7",
              "size": "672055172"
            },
            {
              "host": "x86_64-mingw32",
              "url": "https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-x86_64-w64-mingw32.zip",
              "archiveFileName": "riscv32-esp-elf-14.2.0_20241119-x86_64-w64-mingw32.zip",
              "checksum": "SHA-256:24c8407fa467448d394e0639436a5ede31caf1838e35e8435e19df58ebed438c",
              "size": "677812937"
            }
          ]
        },

根据不同的系统平台选好目标,其中,url 字段就是下载地址了。

接下来可以干活了。用封装好的 arduino 库模拟 USB 鼠标是很简单的,只用一个 USBHIDMouse 类就搞定。

1、实例化;

2、调用 begin 方法初始化;

3、移动鼠标时调用 move 方法。该方法的声明如下:

复制代码
void move(int16_t x, int16_t y, int8_t wheel = 0, int8_t pan = 0);

x、y 就是水平和垂直方向上移动的量,相对坐标,比如,x = 5,就是鼠标向右移动5个单位(像素)。后面两个参数默认给了0,调用时如不需要可以不传值。wheel 是滚轮的滚动量,pan 表示水平滚动的量(要用到水平滚动时)。

咱们写一段代码,让鼠标在屏幕上画正方形,即向右 -> 向下 -> 向左 -> 向上回到原来的位置。

复制代码
#include "USB.h"
#include "USBHIDMouse.h"

USBHIDMouse mouse;             // 实例化
const int8_t move_d = 3;       // 单次鼠标移动量
const int total_count = 200;   // 一个方向移动总次数
int count;    // 记录发了多少次坐标
int step;     // 后面用于做比较,0表示向左,1表示向下......

void setup() {
  count = 0;
  step = 0;
  USB.begin();      // 注意,不要忘了这一行
  mouse.begin();    // 初始化
}

void loop() {
  switch (step) 
  {
  case 0:        // 向右移动
    if(count < total_count)
    {
      mouse.move(move_d, 0);
      count++;
    }
    else
    {
      // 换下一个移动方向
      step = 1;
      count = 0;
    }
    break;
  case 1:           // 向下移动
    if(count < total_count)
    {
      mouse.move(0, move_d);
      count ++;
    }
    else
    {
      // 下一个方向
      step = 2;
      count = 0;
    }
    break;
  case 2:         // 向左移动
    if(count < total_count)
    {
      mouse.move(-move_d, 0);
      count++;
    }
    else
    {
      step = 3;
      count=0;
    }
    break;
  case 3:         // 向上移动
    if(count < total_count)
    {
      mouse.move(0, -move_d);
      count++;
    }
    else
    {
      step = 0;
      count = 0;
    }
    break;
  default:
    break;
  }
  delay(10);    // 延时(毫秒级)
}

相信大伙伴们能看懂代码的。首先,包含 USB.h 和 USBHIDMouse.h;然后直接可以创建 USBHIDMouse 实例。在初始化时,一定要先初始化 USB,再初始化鼠标,即 USB.begin 方法一定要先调用。在 loop 函数中,用 move 方法移动鼠标就是了,简单吧。

写好程序后,需要配置一下 USB Mode 参数。在 Arduino IDE 窗口中,点击【工具】菜单。在子菜单中执行【USB Mode: XXX】->【USB OTG(TinyUSB)】。

如果不修改 USB Mode,烧录之后电脑可能识别不到,或者要重置几次才能识别。这个就是关闭默认的串口输出,所以你不能通过 USB 口来查看日志了。

编译,上传到 esp32 开发板上,有的板子是手动进入烧录模式的,可能要手动重启一下板子。如果没问题,你会看到鼠标动了。

方案B:idf 搭配 esp_tinyusb 组件

每次看到有人鼓吹图形化开发什么的,心里就想嘲笑一番。为啥呢?其实那个是给小朋友玩的,不是咱们成年人用的。能不能快速成形跟用不用图形工具无关,也与用不用低代码无关,而是跟有没有被严重封装好的组件。比如,上面咱们用的 USBHIDMouse 类,人家就是高度封装好的,基本上一行代码初始化就可以读写了,这样写代码甚至比你用鼠标拖控件还快(除非你 C++ 语法学得极烂)。

现在很多工具,真的,营销成分多一些。就算给小朋友玩,好玩是好玩了,但的确培养不了什么编程习惯。以前给小朋友学用的是 BASIC 语言,最起码还是真枪实弹地写代码。代码不见得要写复杂,几行,几段都行。主要是养成好的思维和习惯,才有身临其境的氛围。老周上初中的时候,也是用 QBASIC 入门的,还是 DOS 窗口的,写一些数学算法的东西,还有从奥赛书上抄的算法。也没觉得有多难,还更有乐趣。

扯远了,下面介绍第二种方案。虽然严重封装好的组件好用,但也有很显著的缺点的。如果在初始化时候需要配置详细的参数,比如,电脑识别到鼠标后,显示我自定义的厂商名称,产品ID等。一种方法是把 Arduino 的库的代码自己修改再用;另一种更好的方法是用 idf 实现,控制起来更灵活,尽管要多写点代码。实际开发中经常会这样的,不是你想偷懒就能偷的。

先介绍一下库,这里实际上会用到两个库。到 Esp Component Registry 上搜索"tinyusb",会搜到两个结果。

第三个已经"过时",不必管它。tinyusb 就是乐鑫移植的 tinnyUSB 库,而 esp_tinyusb 是做进一步封装,让你用起来更带劲。所以,esp_tinyusb 依赖 tinyusb。

在 idf 中直接使用 tinyusb 库,你有以下方案可选:

1、执行 idf.py add-dependency 命令,让 idf 工具自动替你下载;

2、手动下载库,放到项目目录下的 components 子目录中,无需在 CMake 中设置 EXTRA_COMPONENT_DIRS 变量。idf 工具会自动查找 components 目录下的组件;

3、手动下载,放到项目以外的目录下,需要通过 EXTRA_COMPONENT_DIRS 变量设置组件所在目录。

下面老周将采用第2种方法。手动下载 esp_tinyusb 和 tinyusb 两个库, 然后在项目的根目录下新建一个 components

目录,并把两个库解压到此目录下。

其结构如下:

这次老周用另一台电脑写代码,配置比较高,编译起来快。这台机器装的是 Mint Linux,操作和 Windows 下一样。用乐鑫官方的 VS Code 扩展工具新建一个空项目(和前面介绍自定义 Arduino 组件一样)。

新建项目后,第一时间做好配置。

1、选好开发板型号。

2、打开 main 目录下的 CMakeLists.txt 文件,在注册 main 组件时依赖 esp_tinyusb 库。

复制代码
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "."
                    REQUIRES esp_tinyusb)

可以点 VS Code 底部状态栏上的"打开 IDF 终端"按钮,打开命令窗口,执行 idf.py reconfigure 命令,如果没报错,就说明没有语法错误了。

3、点击 VS Code 底部状态栏上的"SDK 编辑器"按钮,打开配置页面。

4、找到 Tiny USB Stack 节点下的"Human Interface Device Class(HID)" 条目,把"TinyUSB HID interface count"设置为 1。这个值默认是0,不改的话相关代码不会编译。

如果你好奇为什么的话,可以打开 esp_tinyusb 组件下的 include/tusb_config.h 文件,然后看到这两个地方。

复制代码
#ifndef CONFIG_TINYUSB_HID_COUNT
#   define CONFIG_TINYUSB_HID_COUNT 0
#endif

// 此处省略 711 个字

// Enabled device class driver
#define CFG_TUD_CDC                 CONFIG_TINYUSB_CDC_COUNT
#define CFG_TUD_MSC                 CONFIG_TINYUSB_MSC_ENABLED
#define CFG_TUD_HID CONFIG_TINYUSB_HID_COUNT
#define CFG_TUD_MIDI                CONFIG_TINYUSB_MIDI_COUNT
#define CFG_TUD_VENDOR              CONFIG_TINYUSB_VENDOR_COUNT
#define CFG_TUD_ECM_RNDIS           CONFIG_TINYUSB_NET_MODE_ECM_RNDIS
#define CFG_TUD_NCM                 CONFIG_TINYUSB_NET_MODE_NCM
#define CFG_TUD_DFU                 CONFIG_TINYUSB_DFU_MODE_DFU
#define CFG_TUD_DFU_RUNTIME         CONFIG_TINYUSB_DFU_MODE_DFU_RUNTIME
#define CFG_TUD_BTH                 CONFIG_TINYUSB_BTH_ENABLED

记住 CFG_TUD_HID 宏的值是来自 TINYUSB_HID_COUNT。

然后在 tinyusb 库中找到 src/class/hid/hid_device.c 文件。可以看到,如果 CFG_TUD_HID 宏的值不是大于 0 的话,那么代码就不会编译。

复制代码
#include "tusb_option.h"

#if (CFG_TUD_ENABLED && **CFG_TUD_HID**)

//--------------------------------------------------------------------+
// INCLUDE
//--------------------------------------------------------------------+
#include "device/usbd.h"
#include "device/usbd_pvt.h"

#include "hid_device.h"

............

#endif

现在你懂了吧,为什么要把那个配置项改为1。


现在打开 main.c 文件,开始写代码。

USB 描述符是很复杂的东西,有兴趣的话可以去看看 USB 协议定义说明,没兴趣的话,直接从示例代码抄过来就行。这里没啥技巧可言,都是标准化的东东。

复制代码
#include <stdio.h>
#include "tinyusb.h"
#include "class/hid/hid_device.h"

/************* TinyUSB 描述符 ****************/

#define TUSB_DESC_TOTAL_LEN      (TUD_CONFIG_DESC_LEN + CFG_TUD_HID * TUD_HID_DESC_LEN)

/**
 * @brief HID report descriptor(上报描述符)
 *
 * In this example we implement Keyboard + Mouse HID device,
 * so we must define both report descriptors
 */
const uint8_t hid_report_descriptor[] = {
    // 如果你要模拟键盘,请取消下面的注释
    // TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_ITF_PROTOCOL_KEYBOARD)),
    TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(HID_ITF_PROTOCOL_MOUSE))
};

/**
 * @brief String descriptor(字符描述符)
 */
const char* hid_string_descriptor[5] = {
    // array of pointer to string descriptors
    (char[]){0x09, 0x04},     // 0: is supported language is English (0x0409)
    "GuangDong-Fish",         // 1: 生产商
    "Big-Mouse",              // 2: 产品
    "8848",                   // 3: 序列号
    "8848 HID Interface",     // 4: HID 接口名称
};

/**
 * @brief Configuration descriptor
 *
 * This is a simple configuration descriptor that defines 1 configuration and 1 HID interface
 */
static const uint8_t hid_configuration_descriptor[] = {
    // Configuration number, interface count, string index, total length, attribute, power in mA
    TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),

    // Interface number, string index, boot protocol, report descriptor len, EP In address, size & polling interval
    TUD_HID_DESCRIPTOR(0, 4, false, sizeof(hid_report_descriptor), 0x81, 16, 10),
};

/********* TinyUSB HID 回调函数 ***************/

// Invoked when received GET HID REPORT DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)
{
    // We use only one interface and one HID report descriptor, so we can ignore parameter 'instance'
    return hid_report_descriptor;
}

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
{
    (void) instance;
    (void) report_id;
    (void) report_type;
    (void) buffer;
    (void) reqlen;

    return 0;
}

// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize)
{
}

字符描述符那里,可以根据实际情况改一下产商、产品、序列号等信息,其他代码不用改。注意,这几个回调函数一定要留着,就算你用不上,也要留个空函数在那里:
tud_hid_descriptor_report_cb
tud_hid_get_report_cb
tud_hid_set_report_cb

好了,接下来 app_main 函数中的代码就要咱们自己写了。

先用 tinyusb_config_t 结构体进行配置。

复制代码
    const tinyusb_config_t tucfg =
        {
            .device_descriptor = NULL, // 不需要
            .external_phy = false,
            // 下面配置字符描述符
            .string_descriptor = hid_string_descriptor,
            .string_descriptor_count = 5, // 数组中元素个数
#if (TUD_OPT_HIGH_SPEED)
            .fs_configuration_descriptor = hid_configuration_descriptor, // HID configuration descriptor for full-speed and high-speed are the same
            .hs_configuration_descriptor = hid_configuration_descriptor,
            .qualifier_descriptor = NULL,
#else
            .configuration_descriptor = hid_configuration_descriptor,
#endif // TUD_OPT_HIGH_SPEED
        };

然后调用 tinyusb_driver_install 函数,完成初始化。

复制代码
    esp_err_t result = ESP_OK;
    result = tinyusb_driver_install(&tucfg);
    // 检查一下是否初始化成功
    if (result != ESP_OK)
    {
        // ESP_LOGE("tusb", "tusb 初始化失败,主任务退出");
        // return;
        esp_restart();     // 重启
    }

初始化已经完成,现在可以向主机发送鼠标操作了。发送鼠标信号用的是以下函数:

复制代码
bool tud_hid_mouse_report(uint8_t report_id, uint8_t buttons, int8_t x, int8_t y, int8_t vertical, int8_t horizontal)

各参数含义如下:

**report_id:**报数据的ID,这个ID由前面的 retport 描述符指定,请回看上面的代码,即 hid_report_descriptor 变量。

复制代码
const uint8_t hid_report_descriptor[] = {
    // 如果你要模拟键盘,请取消下面的注释
    // TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_ITF_PROTOCOL_KEYBOARD)),
    TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(HID_ITF_PROTOCOL_MOUSE))};

这里已经指定了鼠标的 report ID 是 HID_ITF_PROTOCOL_MOUSE,键盘的 report ID 是 HID_ITF_PROTOCOL_KEYBOARD。因此,在调用 tud_hid_mouse_report 函数时,report_id 参数的值就是 HID_ITF_PROTOCOL_MOUSE。

**buttons:**鼠标是否按下特定的键,参数值来自以下枚举类型:

复制代码
typedef enum
{
  MOUSE_BUTTON_LEFT     = TU_BIT(0), ///< Left button
  MOUSE_BUTTON_RIGHT    = TU_BIT(1), ///< Right button
  MOUSE_BUTTON_MIDDLE   = TU_BIT(2), ///< Middle button
  MOUSE_BUTTON_BACKWARD = TU_BIT(3), ///< Backward button,
  MOUSE_BUTTON_FORWARD  = TU_BIT(4), ///< Forward button,
}hid_mouse_button_bm_t;

**x、y:**鼠标移动的坐标量(相对),正值表示向右/向下移动,负值表示鼠标向左/上移动。
**vertical:**垂直滚动量,一般就是鼠标滚轮的滚动量。
**horizontal:**水平滚动的量(有时候会用到)。

为了让示例简单好懂,咱们在一个循环中先让鼠标向右下角移动,随后向左上角移动相同的次数。

复制代码
    int8_t move_dis = 5;        // 每次移动的量
    const uint16_t steps = 300; // 移动多少步
    const int step_delay = 20;  // 每一次移动后的延时
    uint16_t n;
    while (true)
    {
        if (*tud_mounted()*)
        {
            // 正向移动
            for (n = 0; n < steps; n++)
            {
                tud_hid_mouse_report(
                    HID_ITF_PROTOCOL_MOUSE, // 报告ID
                    0,                      // 无任何键按下
                    move_dis,               // X坐标上的移动量
                    move_dis,               // Y坐标上的移动量
                    0,                      // 无垂直滚动
                    0                       // 无水平滚动
                );
                vTaskDelay(pdMS_TO_TICKS(step_delay));
            }
            vTaskDelay(pdMS_TO_TICKS(800));
            // 反向移动
            for (n = 0; n < steps; n++)
            {
                tud_hid_mouse_report(
                    HID_ITF_PROTOCOL_MOUSE,
                    0,
                    -move_dis,
                    -move_dis,
                    0,
                    0);
                vTaskDelay(pdMS_TO_TICKS(step_delay));
            }
        }
        // 等待一段时间
        vTaskDelay(pdMS_TO_TICKS(2500));
    }

有一点很重要:每轮循环在移动鼠标前,一定要访问一下 tud_mounted 函数,确保它返回 true 才能发送 report 数据,否则会导致电脑识别不到鼠标。

使用 Linux 的话,在烧录时会有个 50 米大天坑。不填这个坑你是无法用 UART 或 USB JTag 来烧录的。就算你把当前用户添加到 dialout 分组也解决不了。系统因缺少 openOCD 的 udev rule 文件,openOCD 将无法连接。

解决:先找到你随 esp idf 一同安装的 openOCD 目录(在你指定的 IDF_TOOLS_PATH 下面),找到 tools/openocd-esp32/v0.12.0-esp32-<版本号>/openocd-esp32/share/openocd/contrib 目录,里面有个 60-openocd.rules 文件。把它复制到 /etc/udev/rules.d 目录下。

复制代码
sudo cp <60-openocd.rules文件路径> /etc/udev/rules.d/

重启系统后,就能烧录了。

使用 USB 模拟键鼠后,你的 ESP32 板子就不能再使用 USB 口来查看日志了,而且这个玩法似乎用处不大,毕竟你不太可能真拿它来当鼠标用。不过,如果你的开发板带陀螺仪的话,那倒可以做成姿态鼠标,通过在空中旋转来移动鼠标。对,就是所谓的"空中飞鼠"。可能,也许,或者用蓝牙来模拟键鼠会好一些,不占用 USB 口,电池供电时不需要数据线。