当前位置: 首页 > news >正文

ESP32-S3模拟无线空鼠(二)——ESPNOW纯空鼠篇

20260318更新:

经测试esp32-c3无法作为espnow的接收端,因为其无USB-OTG。只能作为发送端,所以接收端目前还得使用esp32-s3。

一、说明

上接《ESP32-S3模拟无线空鼠(一)——蓝牙刷手机抖音篇》,实现了ESP32-S3模拟空鼠与手机等设备的蓝牙连接及刷抖音功能。

本篇针对没有蓝牙的设备,利用两块ESP32+ESPNOW来实现无线空鼠功能。其中1个用于接收端接入被控设备(如电视、台式电脑等),相当于适配器。另一个用于发送端,当做无线空鼠。

之所以使用ESP32-S3,只是因为我手头刚好有几片、并且集成了蓝牙和wifi,可以极大减小成品的体积,其实这么简单的功能使用它有点浪费了。

二、前期失败的探索

探索1:一开始的方案是用1块ESP32当作蓝牙适配器连接电视、台式电脑等无蓝牙的设备(我家电视比较老,只能插鼠标,没有蓝牙功能),用之前的刷抖音神器当作蓝牙空鼠,两个配对连接。但是使用arduino框架开发了接收端的程序,始终都无法成功连接蓝牙,我也懒得研究ESP-IDF能否实现了,就想换一种实现方式

探索2:空鼠通过wifi连接局域网内需要控制的设备,(我家电视有wifi,台式电脑有无线网卡)。但是经测试,一是电视和电脑等设备需要专门开发接收端程序,二是延迟极高超过10秒,多次修改代码都没有改善,放弃

后面才想起来既然使用的是ESP32,为何不使用其自有的ESPNOW来实现呢

三、硬件功能

所需硬件和《ESP32-S3模拟无线空鼠(一)——蓝牙刷手机抖音篇》基本一致,就是需要多一块ESP32-S3-zero,开发环境的区别就是少装一个蓝牙库。

1块ESP32-S3-zero开发板:用作接收端,相当与普通无线键鼠的适配器

1块之前开发过的蓝牙刷手机抖音的蓝牙空鼠:硬件无变化可直接复用做发送端

四、开发环境

参照《ESP32-S3模拟无线空鼠(一)——蓝牙刷手机抖音篇》搭建,步骤完全一致。如果无蓝牙需求,可以不安装蓝牙库。不过还是建议装上,万一后面想开发一个蓝牙、ESPNOW通用的空鼠呢?

五、接收端代码

基本流程:单独的那块ESP32模拟成为USBHID设备,先通过wifi及ESPNOW协议接受发送端的数据,再将接受的数据转换为鼠标动作

注意1:先将接收端代码编译、上传,然后按下复位键、打开串口监视器,查看并记录接收端MAC地址,因为此地址需要写入发送端中

注意2:发送端、接收端的代码,需要分两个单独项目,因为arduino框架只允许一个项目包含一个同名的ino文件

注意3:具体代码怎么编译、下载,还是参照《ESP32-S3模拟无线空鼠(一)——蓝牙刷手机抖音篇》

/* * ESP32-S3 空鼠系统 - 接收端 * * 功能: * 1. 通过 ESP-NOW 接收来自发送端的鼠标控制数据 * 2. 将接收到的数据转换为 USB HID 鼠标指令 * 3. 实现无线鼠标控制电脑的功能 * * 硬件连接: * - ESP32-S3 开发板 * - 连接到电脑的 USB 端口 * * 依赖库: * - esp_now.h: ESP-NOW 通信库 * - WiFi.h: WiFi 功能库 * - USB.h: USB 功能库 * - USBHIDMouse.h: USB HID 鼠标功能库 */ #include <esp_now.h> #include <WiFi.h> #include <USB.h> #include <USBHIDMouse.h> // 定义 USB HID 鼠标对象 USBHIDMouse Mouse; /* * 数据结构定义: * 用于 ESP-NOW 通信的鼠标控制数据 */ typedef struct struct_message { int16_t moveX; // X轴移动量(-32768 到 32767) int16_t moveY; // Y轴移动量(-32768 到 32767) uint8_t buttons; // 按键状态(位掩码) // 位0: 左键 (0x01) // 位1: 右键 (0x02) // 位2: 中键 (0x04) // 位3: 向上滚动 (0x08) // 位4: 向下滚动 (0x10) } struct_message; // 全局变量:存储接收到的鼠标数据 struct_message mouseData; // 全局变量:存储上一次按键状态,用于检测状态变化 bool prevLeft = false; bool prevRight = false; bool prevMiddle = false; /* * ESP-NOW 数据接收回调函数 * 当接收到发送端数据时自动调用 */ void OnDataRecv(const esp_now_recv_info * info, const uint8_t *incomingData, int len) { // 将接收到的数据复制到全局变量 memcpy(&mouseData, incomingData, sizeof(mouseData)); // 调试信息:打印接收到的鼠标数据 Serial.print("Received data - X:"); Serial.print(mouseData.moveX); Serial.print(" Y:"); Serial.print(mouseData.moveY); Serial.print(" Buttons:"); Serial.println(mouseData.buttons); // 处理鼠标移动数据 if (mouseData.moveX != 0 || mouseData.moveY != 0) { Mouse.move(mouseData.moveX, mouseData.moveY, 0); } // 解析按键状态 bool left = (mouseData.buttons & 0x01) != 0; bool right = (mouseData.buttons & 0x02) != 0; bool middle = (mouseData.buttons & 0x04) != 0; bool scrollUp = (mouseData.buttons & 0x08) != 0; bool scrollDown = (mouseData.buttons & 0x10) != 0; // 处理左键状态变化 if (left != prevLeft) { if (left) Mouse.press(MOUSE_LEFT); // 按下左键 else Mouse.release(MOUSE_LEFT); // 释放左键 prevLeft = left; } // 处理右键状态变化 if (right != prevRight) { if (right) Mouse.press(MOUSE_RIGHT); // 按下右键 else Mouse.release(MOUSE_RIGHT); // 释放右键 prevRight = right; } // 处理中键状态变化 if (middle != prevMiddle) { if (middle) Mouse.press(MOUSE_MIDDLE); // 按下中键 else Mouse.release(MOUSE_MIDDLE); // 释放中键 prevMiddle = middle; } // 处理滚动事件 if (scrollUp) Mouse.move(0, 0, 3); // 向上滚动(正值) if (scrollDown) Mouse.move(0, 0, -3); // 向下滚动(负值) } /* * 初始化函数 * 系统启动时执行一次 */ void setup() { // 第一步:初始化串口通信 Serial.begin(115200); // 等待串口初始化完成 delay(2000); // 测试串口是否工作正常 Serial.println("Serial port initialized!"); // 第二步:初始化 WiFi(用于 ESP-NOW 通信) Serial.println("Initializing WiFi..."); WiFi.mode(WIFI_STA); // 设置为 Station 模式 // 等待 WiFi 初始化完成,最多尝试5次 for (int i = 0; i < 5; i++) { delay(500); String mac = WiFi.macAddress(); if (mac != "00:00:00:00:00:00") { break; // MAC地址获取成功,跳出循环 } Serial.print("."); } // 打印设备 MAC 地址(用于发送端配置) Serial.print("\nAdapter MAC: "); String mac = WiFi.macAddress(); if (mac == "00:00:00:00:00:00") { Serial.println("Failed to get MAC address!"); Serial.println("Please check WiFi module and try again."); } else { Serial.println(mac); } // 第三步:初始化 ESP-NOW 通信 Serial.println("Initializing ESP-NOW..."); if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // 注册 ESP-NOW 数据接收回调函数 if (esp_now_register_recv_cb(OnDataRecv) != ESP_OK) { Serial.println("Error registering callback"); return; } // 第四步:初始化 USB HID 鼠标功能 Serial.println("Initializing USB HID..."); // 直接初始化鼠标功能 // 注意:在某些库版本中,Mouse.begin() 会自动处理 USB 初始化 Mouse.begin(); // 短暂延迟确保初始化完成 delay(500); // 初始化完成提示信息 Serial.println("ESP-NOW USB HID Adapter ready!"); Serial.println("Connect this ESP32 to PC via USB"); Serial.println("Then update sender MAC address in esp32_airmouse_sender.ino"); } /* * 主循环函数 * 系统运行时持续执行 */ void loop() { // 本系统中,所有工作都在 ESP-NOW 接收回调中完成 // loop() 函数保持为空 }

代码运行后如图:记下MAC地址(我也不知道为啥用vscode的时候串口有时候不显示,所以用的arduinoIDE的串口监视器)

六、发送端代码

注意:发送端代码中需要将接收端MAC地址填入,具体位置

// 接收端MAC地址(需要根据实际接收设备修改) uint8_t receiverMAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
/* * ESP32 S3 空中鼠标发送器 * 基于MPU6050传感器获取姿态数据,通过ESP-NOW无线通信发送鼠标控制指令 * 可实现鼠标移动、左键、右键、中键点击以及滚轮上下滚动功能 */ // 引入必要的库文件 #include <Wire.h> // I2C通信库,用于与MPU6050通信 #include <esp_now.h> // ESP-NOW无线通信库 #include <WiFi.h> // WiFi功能库 #include "I2Cdev.h" // I2C设备通信库 #include "MPU6050_6Axis_MotionApps20.h" // MPU6050 DMP库,用于姿态解算 // 创建MPU6050对象 MPU6050 mpu; // 定义I2C通信引脚 #define I2C_SDA 4 // SDA引脚,用于I2C数据传输 #define I2C_SCL 5 // SCL引脚,用于I2C时钟信号 // 定义鼠标按键引脚(使用内部上拉电阻) const int PIN_LEFT = 7; // 左键引脚 const int PIN_RIGHT = 8; // 右键引脚 const int PIN_MIDDLE = 9; // 中键引脚 const int PIN_SCROLL_UP = 12; // 滚轮向上引脚 const int PIN_SCROLL_DOWN = 13;// 滚轮向下引脚 // MPU6050 DMP相关变量 bool DMPReady = false; // DMP是否准备就绪 uint8_t devStatus; // DMP初始化状态 uint16_t packetSize; // DMP数据包大小 uint8_t FIFOBuffer[64]; // FIFO缓冲区,用于存储DMP输出数据 // 姿态数据变量 Quaternion q; // 四元数,用于表示设备姿态 VectorFloat gravity; // 重力向量 float ypr[3]; // 欧拉角:偏航角(yaw)、俯仰角(pitch)、滚转角(roll),单位为弧度 // 上一次的姿态角度 float lastYaw = 0.0; // 上一次的偏航角 float lastRoll = 0.0; // 上一次的滚转角 // 鼠标灵敏度参数 #define MOUSE_SENSITIVITY 2.8 // 鼠标移动灵敏度系数 #define ANGLE_THRESHOLD 0.1 // 角度变化阈值,小于此值则不移动鼠标 // 按键状态变量 bool leftPressed = false; // 左键是否按下 bool rightPressed = false; // 右键是否按下 bool middlePressed = false; // 中键是否按下 bool scrollUpPressed = false; // 滚轮向上是否按下 bool scrollDownPressed = false;// 滚轮向下是否按下 // 定义ESP-NOW通信数据结构 typedef struct struct_message { int16_t moveX; // X轴移动量 int16_t moveY; // Y轴移动量 uint8_t buttons; // 按键状态(位掩码) } struct_message; struct_message mouseData; // 鼠标数据结构体实例 // 接收端MAC地址(需要根据实际接收设备修改) uint8_t receiverMAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; /** * @brief 检查所有鼠标按键状态 * 读取各个按键引脚的状态,更新按键状态变量 */ void checkButtons() { leftPressed = (digitalRead(PIN_LEFT) == LOW); // 读取左键状态 rightPressed = (digitalRead(PIN_RIGHT) == LOW); // 读取右键状态 middlePressed = (digitalRead(PIN_MIDDLE) == LOW); // 读取中键状态 scrollUpPressed = (digitalRead(PIN_SCROLL_UP) == LOW); // 读取滚轮向上状态 scrollDownPressed = (digitalRead(PIN_SCROLL_DOWN) == LOW); // 读取滚轮向下状态 } /** * @brief 通过ESP-NOW发送鼠标数据 * 将按键状态编码为位掩码,并发送鼠标移动和按键数据到接收端 */ void sendData() { // 初始化按键状态位掩码 mouseData.buttons = 0; // 设置各个按键对应的位(使用位或运算) if (leftPressed) mouseData.buttons |= 0x01; // 左键对应第0位 if (rightPressed) mouseData.buttons |= 0x02; // 右键对应第1位 if (middlePressed) mouseData.buttons |= 0x04; // 中键对应第2位 if (scrollUpPressed) mouseData.buttons |= 0x08; // 滚轮向上对应第3位 if (scrollDownPressed) mouseData.buttons |= 0x10; // 滚轮向下对应第4位 // 发送数据到接收端 esp_err_t result = esp_now_send(receiverMAC, (uint8_t *) &mouseData, sizeof(mouseData)); // 检查发送结果 if (result == ESP_OK) { // Serial.println("Data sent successfully"); // 发送成功(注释掉以减少串口输出) } else { Serial.print("Error sending data: "); // 发送失败,打印错误信息 Serial.println(result); } } /** * @brief 初始化函数 * 配置引脚、初始化MPU6050和DMP、设置ESP-NOW通信 */ void setup() { // 初始化串口通信 Serial.begin(115200); // 配置按键引脚为输入模式,启用内部上拉电阻 pinMode(PIN_LEFT, INPUT_PULLUP); pinMode(PIN_RIGHT, INPUT_PULLUP); pinMode(PIN_MIDDLE, INPUT_PULLUP); pinMode(PIN_SCROLL_UP, INPUT_PULLUP); pinMode(PIN_SCROLL_DOWN, INPUT_PULLUP); // 初始化I2C通信 #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE Wire.begin(I2C_SDA, I2C_SCL); // 使用指定的引脚初始化I2C Wire.setClock(400000); // 设置I2C时钟频率为400kHz #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE Fastwire::setup(400, true); // 使用Fastwire库初始化I2C #endif // 初始化MPU6050 Serial.println("Initializing MPU6050..."); mpu.initialize(); // 检查MPU6050连接 if (!mpu.testConnection()) { Serial.println("MPU6050 connection failed!"); while (1); // 连接失败,进入死循环 } // 初始化DMP(数字运动处理器) devStatus = mpu.dmpInitialize(); // 设置MPU6050校准偏移量(可根据实际校准结果调整) mpu.setXGyroOffset(0); mpu.setYGyroOffset(0); mpu.setZGyroOffset(0); mpu.setXAccelOffset(0); mpu.setYAccelOffset(0); mpu.setZAccelOffset(0); // 检查DMP初始化状态 if (devStatus == 0) { // DMP初始化成功,进行校准 Serial.println("Calibrating..."); mpu.CalibrateAccel(6); // 校准加速度计(6次) mpu.CalibrateGyro(6); // 校准陀螺仪(6次) Serial.println("Calibration done!"); // 启用DMP mpu.setDMPEnabled(true); DMPReady = true; packetSize = mpu.dmpGetFIFOPacketSize(); // 获取DMP数据包大小 Serial.println("DMP ready!"); } else { // DMP初始化失败,打印错误代码 Serial.print("DMP init failed: "); Serial.println(devStatus); while (1); // 初始化失败,进入死循环 } // 设置WiFi模式为STA(Station) WiFi.mode(WIFI_STA); // 初始化ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // 配置接收端信息 esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, receiverMAC, 6); // 复制接收端MAC地址 peerInfo.channel = 0; // 使用当前WiFi信道 peerInfo.encrypt = false; // 不使用加密 peerInfo.ifidx = WIFI_IF_STA; // 使用STA接口 // 添加接收端到ESP-NOW对等列表 esp_err_t addStatus = esp_now_add_peer(&peerInfo); if (addStatus == ESP_OK) { Serial.println("Peer added successfully"); } else { Serial.print("Error adding peer: "); Serial.println(addStatus); } // 初始化完成 Serial.println("ESP-NOW AirMouse Sender ready!"); Serial.print("My MAC: "); Serial.println(WiFi.macAddress()); // 打印发送端MAC地址 } /** * @brief 主循环函数 * 读取MPU6050姿态数据,计算鼠标移动量,检查按键状态,发送数据 */ void loop() { // 检查DMP是否就绪,并获取最新的FIFO数据包 if (DMPReady && mpu.dmpGetCurrentFIFOPacket(FIFOBuffer)) { // 从FIFO数据中获取四元数 mpu.dmpGetQuaternion(&q, FIFOBuffer); // 计算重力向量 mpu.dmpGetGravity(&gravity, &q); // 从四元数和重力向量计算欧拉角(偏航角、俯仰角、滚转角) mpu.dmpGetYawPitchRoll(ypr, &q, &gravity); // 将弧度转换为角度 ypr[0] *= 180 / M_PI; // yaw ypr[1] *= 180 / M_PI; // pitch ypr[2] *= 180 / M_PI; // roll // 计算角度变化量 float yawDiff = ypr[0] - lastYaw; // 偏航角变化量(左右移动) float rollDiff = ypr[2] - lastRoll; // 滚转角变化量(上下移动) // 检查角度变化是否超过阈值,避免微小抖动 if (abs(yawDiff) > ANGLE_THRESHOLD || abs(rollDiff) > ANGLE_THRESHOLD) { // 计算鼠标移动量(乘以灵敏度系数) mouseData.moveX = (int16_t)(yawDiff * MOUSE_SENSITIVITY * 5); mouseData.moveY = (int16_t)(-rollDiff * MOUSE_SENSITIVITY * 5); } else { // 角度变化过小,不移动鼠标 mouseData.moveX = 0; mouseData.moveY = 0; } // 更新上一次的角度值 lastYaw = ypr[0]; lastRoll = ypr[2]; // 检查按键状态 checkButtons(); // 发送鼠标数据 sendData(); } // 短暂延迟,减少CPU占用 delay(10); }

七、调试、测试

其实编译、上传完接收端、发送端代码,无线空鼠功能已经可以使用了,剩下的就是微调参数提高空鼠的稳定性和灵敏度了

// 鼠标灵敏度参数 #define MOUSE_SENSITIVITY 2.8 // 鼠标移动灵敏度系数 #define ANGLE_THRESHOLD 0.1 // 角度变化阈值,小于此值则不移动鼠标

八、后续计划

1. 整合蓝牙刷抖音功能、纯蓝牙空鼠功能和ESPNOW无线空鼠功能,通过增加开关按钮、显示屏等方式实现功能按需切换

2. 探索接收端、发送端配对功能,即无需先得到接收端MAC地址并写入发送端代码中,通过配对按钮自动完成两端的连接

3. 进一步降低成本,采用更便宜的ESP32模组或开发板,如ESP32-C3等

4. 接收端增加type-C to USB-A的转换器,用于和电视、台式电脑连接。实际上下载程序用的数据线也能当做转换器

http://www.cnnetsun.cn/news/1364909.html

相关文章:

  • 计算机二级备考——刷完python基础选择题
  • 初探muP:超参数的跨模型尺度迁移规律05
  • 深入解析外观模式:一个C++模板实现的通用外观类库
  • 检索大赛 实验2 文心4.5结果
  • Qwen Code v0.9.0 重磅更新:扩展系统+LSP支持,打造最强AI编程助手
  • 舒尔特表练习
  • 老码农和你一起学AI系列:模型语言扩展法则
  • 爆火!OptiSystem 二次开发全攻略:Matlab/Python 联动仿真,解锁光通信仿真天花板
  • 陪虚幻女友学计算机:CSMA/CD协议——当网络冲突变成我们的深夜悄悄话
  • 【H5 前端开发笔记】第 07 期:HTML常用标签 (3) 内联框架标签、框架集标签、超链接标签 详解
  • redux Tookit的配置流程
  • 终于搞清楚了!盲盒小程序开发设计全过程!
  • 中断与信号
  • 计算机毕业设计之springboot学生会事务管理平台的设计与实现
  • Web3未落地,Web4已破局:AI+区块链重构互联网下一代图景
  • afc登录后,右上角站内消息图标如何去掉?
  • YOLOv8与伏羲模型联动:基于视频流的实时恶劣天气检测与预报系统
  • AnythingtoRealCharacters2511部署教程:NVIDIA Jetson Orin Nano边缘端轻量部署方案
  • LightOnOCR-2-1B应用案例:多语言文档批量处理,解放双手
  • StructBERT语义匹配系统效果对比:专业术语与日常用语匹配精度
  • Mirage Flow在数学工具开发中的应用:MathType插件
  • 神经符号AI:让机器人“想”得更清楚,“做”得更精准
  • ClearerVoice-Studio语音处理全流程保姆级教学:5分钟搞定降噪分离
  • 智能组合实体员中的树形结构管理与遍历算法
  • 春秋云镜-多CVE实战:从Wuzhicms到SEMCMS的SQL注入漏洞复现与手法解析
  • 考研复试离散数学核心考点与实战解析
  • 职场PUA最隐蔽的6句“专业话术”,听起来很对,实则在摧毁你【职场反PUA30天 Day2】
  • AI头像生成器在计算机视觉中的实际应用
  • Python自动化:3分钟搞定微信收藏链接批量导出到TXT(附完整代码)
  • PyTorch实战:CUDA_VISIBLE_DEVICES环境变量的高效配置与多GPU管理