ESP32-S3多设备KVM远程控制系统-虚拟USB有线键鼠+游戏宏(压枪/自瞄)+云玩一体化实现方案

ESP32-S3多设备KVM远程控制系统

虚拟USB有线键鼠+游戏宏(压枪/自瞄)+云玩一体化实现方案

一、方案整体架构

本方案以ESP32-S3为核心,通过"MQTT物联网通信+USB HID设备模拟+自定义游戏宏"三大核心能力,实现本地主机对远端多台电脑的远程键鼠控制,同时集成压枪、自瞄等游戏宏功能和云玩能力,完整架构如下:
1.发送指令(键鼠/宏/自瞄/KVM)
2.转发指令
3.模拟USB HID键鼠
4.远程桌面回显画面
远端电脑集群
电脑1(识别ESP32为USB键鼠)
电脑2(KVM切换目标)
游戏/办公软件
ESP32-S3核心模块
MQTT订阅/解析模块
USB HID键鼠模拟模块
游戏宏执行模块(压枪/自瞄)
KVM设备切换模块
本地控制端
Python MQTT客户端(指令发送/键鼠捕获)
远程桌面工具(画面回显)
宏配置工具(压枪曲线/自瞄参数)
本地控制端
MQTT服务器
ESP32-S3(远端)
远端电脑集群


二、ESP32-S3端完整实现(C语言+详细注释+函数说明)

1. 开发环境准备

  • Arduino IDE配置
    1. 首选项添加ESP32开发板地址:https://dl.espressif.com/dl/package_esp32_index.json
    2. 安装PubSubClient库(MQTT通信)、USBHID库(默认内置);
    3. 开发板选择:ESP32-S3 Dev Module,配置项:
      • USB ModeUSB-OTG (HID)
      • CPU Frequency80MHz(降功耗)
      • Flash Size4MB

2. 完整代码(带详细注释)

cpp 复制代码
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <USBHID.h>
#include <HIDReports.h>
#include <HIDTypes.h>

/************************ 基础配置区(需根据实际修改) ************************/
// WiFi配置
#define WIFI_SSID       "你的WiFi名称"    // 远端电脑所在WiFi名称
#define WIFI_PWD        "你的WiFi密码"    // 远端电脑所在WiFi密码
// MQTT配置(公共服务器,无需自建;私有服务器可替换为自己的IP)
#define MQTT_SERVER     "broker.emqx.io"  // 公共MQTT服务器地址
#define MQTT_PORT       1883              // MQTT默认端口
#define MQTT_CLIENT_ID  "ESP32-S3-KVM-001"// KVM设备唯一ID(多设备需修改后缀,如002/003)
#define MQTT_TOPIC_CTRL "esp32/kvm/control" // 键鼠/宏/自瞄指令订阅主题
#define MQTT_TOPIC_KVM  "esp32/kvm/switch"  // KVM设备切换订阅主题
// USB HID参数配置
#define MOUSE_MOVE_DELAY 5    // 鼠标移动单步延迟(ms),压枪/自瞄平滑度关键
#define KEY_HOLD_DELAY   50   // 键盘按键保持时间(ms),避免按键识别失败
// KVM状态控制
bool isKVMOnline = true;      // 当前设备是否在线(KVM切换核心标记)

/************************ USB HID 键鼠核心配置 ************************/
USBHID hid; // 初始化USB HID核心对象

// 鼠标HID报告描述符(标准格式,电脑自动识别为USB鼠标)
// 报告结构:3个按键(左/右/中) + X/Y移动(-127~127) + 滚轮(-127~127)
uint8_t mouseReportDesc[] = {
  0x05, 0x01,        // USAGE_PAGE (Generic Desktop) - 通用桌面设备
  0x09, 0x02,        // USAGE (Mouse) - 设备类型为鼠标
  0xA1, 0x01,        // COLLECTION (Application) - 应用级集合
  0x09, 0x01,        //   USAGE (Pointer) - 指针子设备
  0xA1, 0x00,        //   COLLECTION (Physical) - 物理级集合
  0x05, 0x09,        //     USAGE_PAGE (Button) - 按键页
  0x19, 0x01,        //     USAGE_MINIMUM (Button 1) - 最小按键(左键)
  0x29, 0x03,        //     USAGE_MAXIMUM (Button 3) - 最大按键(中键)
  0x15, 0x00,        //     LOGICAL_MINIMUM (0) - 按键逻辑最小值(未按下)
  0x25, 0x01,        //     LOGICAL_MAXIMUM (1) - 按键逻辑最大值(按下)
  0x95, 0x03,        //     REPORT_COUNT (3) - 按键数量(3个)
  0x75, 0x01,        //     REPORT_SIZE (1) - 每个按键占1位
  0x81, 0x02,        //     INPUT (Data,Var,Abs) - 按键输入模式(数据/变量/绝对)
  0x95, 0x01,        //     REPORT_COUNT (1) - 填充位数量
  0x75, 0x05,        //     REPORT_SIZE (5) - 填充位占5位(凑8位字节)
  0x81, 0x01,        //     INPUT (Cnst,Var,Abs) - 填充位输入模式(常量)
  0x05, 0x01,        //     USAGE_PAGE (Generic Desktop) - 回到通用桌面页
  0x09, 0x30,        //     USAGE (X) - X轴移动
  0x09, 0x31,        //     USAGE (Y) - Y轴移动
  0x09, 0x38,        //     USAGE (Wheel) - 滚轮
  0x15, 0x81,        //     LOGICAL_MINIMUM (-127) - 移动最小值
  0x25, 0x7F,        //     LOGICAL_MAXIMUM (127) - 移动最大值
  0x75, 0x08,        //     REPORT_SIZE (8) - 每个移动轴占1字节
  0x95, 0x03,        //     REPORT_COUNT (3) - X/Y/滚轮共3个轴
  0x81, 0x06,        //     INPUT (Data,Var,Rel) - 移动输入模式(数据/变量/相对)
  0xC0,              //   END_COLLECTION - 结束物理级集合
  0xC0               // END_COLLECTION - 结束应用级集合
};

// 键盘HID报告描述符(标准格式,电脑自动识别为USB键盘)
// 报告结构:8个修饰键(Ctrl/Shift/Alt等) + 6个普通按键(支持组合键)
uint8_t keyboardReportDesc[] = {
  0x05, 0x01,        // USAGE_PAGE (Generic Desktop) - 通用桌面设备
  0x09, 0x06,        // USAGE (Keyboard) - 设备类型为键盘
  0xA1, 0x01,        // COLLECTION (Application) - 应用级集合
  0x05, 0x07,        //   USAGE_PAGE (Keyboard/Keypad) - 键盘按键页
  0x19, 0xE0,        //   USAGE_MINIMUM (Keyboard Left Control) - 最小修饰键(左Ctrl)
  0x29, 0xE7,        //   USAGE_MAXIMUM (Keyboard Right GUI) - 最大修饰键(右Win)
  0x15, 0x00,        //   LOGICAL_MINIMUM (0) - 修饰键未按下
  0x25, 0x01,        //   LOGICAL_MAXIMUM (1) - 修饰键按下
  0x95, 0x08,        //   REPORT_COUNT (8) - 修饰键数量(8个)
  0x75, 0x01,        //   REPORT_SIZE (1) - 每个修饰键占1位
  0x81, 0x02,        //   INPUT (Data,Var,Abs) - 修饰键输入模式
  0x95, 0x01,        //   REPORT_COUNT (1) - 填充位数量
  0x75, 0x08,        //   REPORT_SIZE (8) - 填充位占8位
  0x81, 0x03,        //   INPUT (Cnst,Var,Abs) - 填充位输入模式
  0x95, 0x06,        //   REPORT_COUNT (6) - 普通按键数量(支持6键同时按)
  0x75, 0x08,        //   REPORT_SIZE (8) - 每个普通按键占1字节
  0x15, 0x00,        //   LOGICAL_MINIMUM (0) - 按键未按下
  0x25, 0x65,        //   LOGICAL_MAXIMUM (101) - 最大按键码(Application键)
  0x05, 0x07,        //   USAGE_PAGE (Keyboard/Keypad) - 回到键盘按键页
  0x19, 0x00,        //   USAGE_MINIMUM (Reserved) - 保留按键
  0x29, 0x65,        //   USAGE_MAXIMUM (Keyboard Application) - 最大普通按键
  0x81, 0x00,        //   INPUT (Data,Array) - 普通按键输入模式
  0xC0               // END_COLLECTION - 结束应用级集合
};

// 注册HID报告(关联描述符与HID核心对象)
HIDReport mouseReport = {mouseReportDesc, sizeof(mouseReportDesc)};
HIDReport keyboardReport = {keyboardReportDesc, sizeof(keyboardReportDesc)};

/************************ 游戏宏配置区(可自定义) ************************/
// AK47压枪曲线(Y轴偏移量,负数=向上移动,正数=向下移动;数组长度=压枪帧数)
// 逻辑:连续射击时,每帧鼠标向上移动对应像素,抵消后坐力
int8_t ak47Recoil[] = {5, 6, 5, 4, 4, 3, 3, 2, 2, 1, 1, 0};
int ak47Len = sizeof(ak47Recoil)/sizeof(ak47Recoil[0]); // AK47压枪曲线长度

// M416压枪曲线(后坐力更小,偏移量更小)
int8_t m416Recoil[] = {3, 4, 3, 2, 2, 1, 1, 0};
int m416Len = sizeof(m416Recoil)/sizeof(m416Recoil[0]); // M416压枪曲线长度

/************************ 网络通信对象 ************************/
WiFiClient espClient;                // WiFi客户端对象
PubSubClient mqttClient(espClient);  // MQTT客户端对象(基于WiFi)
unsigned long lastReconnectAttempt = 0; // MQTT重连时间标记

/************************ 核心功能函数 - 鼠标控制 ************************/
/**
 * @brief 模拟USB鼠标操作
 * @param x     X轴移动偏移量(-127 ~ 127):正数=右移,负数=左移
 * @param y     Y轴移动偏移量(-127 ~ 127):正数=下移,负数=上移
 * @param btn   鼠标按键状态(0=无按键,1=左键,2=右键,4=中键,可组合如3=左+右键)
 * @param wheel 滚轮偏移量(-127 ~ 127):正数=向上,负数=向下(默认0)
 * @return 无
 * @note 仅当KVM设备在线时执行;报告格式严格匹配HID描述符,确保电脑识别
 */
void mouseControl(int8_t x, int8_t y, uint8_t btn = 0, int8_t wheel = 0) {
  // KVM离线时不执行任何操作
  if(!isKVMOnline) return;
  
  // 构造鼠标HID报告(4字节:按键位 + X + Y + 滚轮)
  uint8_t report[4] = {
    btn,    // 第1字节:按键状态(bit0=左键,bit1=右键,bit2=中键)
    x,      // 第2字节:X轴移动
    y,      // 第3字节:Y轴移动
    wheel   // 第4字节:滚轮移动
  };
  
  // 发送HID报告到电脑,模拟鼠标操作
  hid.sendReport(&mouseReport, report, sizeof(report));
}

/************************ 核心功能函数 - 键盘控制 ************************/
/**
 * @brief 模拟USB键盘操作
 * @param modifier 修饰键状态(8位,每bit对应一个修饰键):
 *        0x01=左Ctrl,0x02=左Shift,0x04=左Alt,0x08=左Win,
 *        0x10=右Ctrl,0x20=右Shift,0x40=右Alt,0x80=右Win
 * @param key      普通按键HID码(参考标准HID键盘码表,如0x04=A,0x05=B,0x1E=0)
 * @return 无
 * @note 1. 按键按下后延迟KEY_HOLD_DELAY再释放,确保电脑识别;
 *       2. 支持组合键(如modifier=0x01+0x02=0x03,key=0x04 → Ctrl+Shift+A)
 */
void keyboardControl(uint8_t modifier, uint8_t key) {
  // KVM离线时不执行任何操作
  if(!isKVMOnline) return;
  
  // 构造键盘HID报告(8字节:修饰键 + 填充位 + 6个普通按键)
  uint8_t report[8] = {
    modifier, // 第1字节:修饰键状态
    0,        // 第2字节:填充位(固定0)
    key,      // 第3字节:普通按键1
    0,        // 第4字节:普通按键2(未使用)
    0,        // 第5字节:普通按键3(未使用)
    0,        // 第6字节:普通按键4(未使用)
    0,        // 第7字节:普通按键5(未使用)
    0         // 第8字节:普通按键6(未使用)
  };
  
  // 发送按键按下报告
  hid.sendReport(&keyboardReport, report, sizeof(report));
  delay(KEY_HOLD_DELAY); // 保持按键按下状态,避免电脑未识别
  
  // 构造按键释放报告(所有按键置0)
  memset(report, 0, sizeof(report));
  // 发送按键释放报告
  hid.sendReport(&keyboardReport, report, sizeof(report));
}

/************************ 核心功能函数 - 压枪宏执行 ************************/
/**
 * @brief 执行指定枪械的压枪宏
 * @param gunType 枪械类型(字符串:"AK47" / "M416",可扩展其他枪械)
 * @return 无
 * @note 1. 压枪逻辑:按住左键的同时,按预定义曲线移动鼠标Y轴,抵消后坐力;
 *       2. 每帧移动延迟MOUSE_MOVE_DELAY,保证压枪平滑;
 *       3. 执行完成后自动释放左键,避免持续射击
 */
void runRecoilMacro(String gunType) {
  // KVM离线时不执行
  if(!isKVMOnline) return;
  
  Serial.print("[压枪宏] 执行枪械:");
  Serial.println(gunType);
  
  // AK47压枪逻辑
  if(gunType == "AK47") {
    for(int i=0; i<ak47Len; i++) {
      // 左键按下 + 按曲线移动Y轴(ak47Recoil[i]为Y轴偏移)
      mouseControl(0, ak47Recoil[i], 1);
      delay(MOUSE_MOVE_DELAY); // 单帧延迟,控制压枪速度
    }
    // 释放左键
    mouseControl(0, 0, 0);
  }
  // M416压枪逻辑
  else if(gunType == "M416") {
    for(int i=0; i<m416Len; i++) {
      mouseControl(0, m416Recoil[i], 1);
      delay(MOUSE_MOVE_DELAY);
    }
    // 释放左键
    mouseControl(0, 0, 0);
  }
  // 可扩展其他枪械(如AWM、喷子等)
  else {
    Serial.println("[压枪宏] 未识别的枪械类型!");
  }
}

/************************ 核心功能函数 - 自瞄功能 ************************/
/**
 * @brief 自瞄功能:将鼠标移动到目标相对坐标
 * @param targetX 目标X轴相对偏移量(正数=右移,负数=左移)
 * @param targetY 目标Y轴相对偏移量(正数=下移,负数=上移)
 * @return 无
 * @note 1. 分步移动:将总偏移量拆分为10步,避免单次移动过大导致电脑卡顿;
 *       2. 最后补全剩余偏移量,保证定位精准;
 *       3. 自瞄坐标需结合远程桌面画面的图像识别结果(Python端计算)
 */
void aimAssist(int16_t targetX, int16_t targetY) {
  // KVM离线时不执行
  if(!isKVMOnline) return;
  
  Serial.print("[自瞄功能] 移动到相对坐标:(");
  Serial.print(targetX);
  Serial.print(", ");
  Serial.println(targetY);
  
  // 分步移动(拆分为10步,平滑移动)
  int16_t stepX = targetX / 10; // X轴单步偏移量
  int16_t stepY = targetY / 10; // Y轴单步偏移量
  
  // 执行10步移动
  for(int i=0; i<10; i++) {
    mouseControl(stepX, stepY, 0);
    delay(MOUSE_MOVE_DELAY);
  }
  
  // 补全剩余偏移量(避免整除后精度丢失)
  int16_t remainX = targetX % 10;
  int16_t remainY = targetY % 10;
  mouseControl(remainX, remainY, 0);
}

/************************ 核心功能函数 - KVM设备切换 ************************/
/**
 * @brief KVM多设备切换:控制当前ESP32设备是否响应指令
 * @param deviceId 目标设备ID(需与MQTT_CLIENT_ID匹配)
 * @return 无
 * @note 1. 仅当deviceId与当前设备ID一致时,设备上线(isKVMOnline=true);
 *       2. 其他设备ID时,当前设备离线(isKVMOnline=false);
 *       3. 切换后向MQTT发送状态反馈,便于本地端确认
 */
void kvmSwitch(String deviceId) {
  // 匹配目标设备ID,当前设备上线
  if(deviceId == MQTT_CLIENT_ID) {
    isKVMOnline = true;
    Serial.println("[KVM切换] 当前设备(" + MQTT_CLIENT_ID + ")上线");
  }
  // 不匹配,当前设备离线
  else {
    isKVMOnline = false;
    Serial.println("[KVM切换] 当前设备(" + MQTT_CLIENT_ID + ")离线");
  }
  
  // 构造KVM状态反馈消息
  String feedbackMsg = "KVM:" + String(MQTT_CLIENT_ID) + (isKVMOnline ? "-ON" : "-OFF");
  // 发布状态到MQTT,本地端可订阅该主题获取状态
  mqttClient.publish(MQTT_TOPIC_KVM, feedbackMsg.c_str());
}

/************************ 核心功能函数 - MQTT指令解析 ************************/
/**
 * @brief 解析MQTT接收的指令,分发到对应功能函数
 * @param cmd MQTT接收的指令字符串(格式见注释)
 * @return 无
 * @note 指令格式定义(竖线分隔,严格匹配):
 *       1. 普通鼠标:M|x|y|btn → 例:M|10|5|1(右移10,下移5,左键按下)
 *       2. 普通键盘:K|mod|key → 例:K|0|4(无修饰键,按A键)、K|1|4(Ctrl+A)
 *       3. 压枪宏:MACRO|gunType → 例:MACRO|AK47(执行AK47压枪)
 *       4. 自瞄:AIM|x|y → 例:AIM|50|30(右移50,下移30)
 *       5. KVM切换:KVM|deviceId → 例:KVM|ESP32-S3-KVM-002(切换到002设备)
 */
void parseMqttCmd(String cmd) {
  // 去除指令首尾空格/换行,避免解析错误
  cmd.trim();
  
  // 1. 解析普通鼠标指令
  if(cmd.startsWith("M|")) {
    // 截取指令体(去掉前缀"M|")
    String cmdBody = cmd.substring(2);
    // 解析X轴偏移
    int x = cmdBody.substring(0, cmdBody.indexOf("|")).toInt();
    cmdBody = cmdBody.substring(cmdBody.indexOf("|")+1);
    // 解析Y轴偏移
    int y = cmdBody.substring(0, cmdBody.indexOf("|")).toInt();
    // 解析按键状态
    int btn = cmdBody.substring(cmdBody.indexOf("|")+1).toInt();
    // 调用鼠标控制函数
    mouseControl((int8_t)x, (int8_t)y, (uint8_t)btn);
  }
  // 2. 解析普通键盘指令
  else if(cmd.startsWith("K|")) {
    String cmdBody = cmd.substring(2);
    // 解析修饰键
    int mod = cmdBody.substring(0, cmdBody.indexOf("|")).toInt();
    // 解析普通按键
    int key = cmdBody.substring(cmdBody.indexOf("|")+1).toInt();
    // 调用键盘控制函数
    keyboardControl((uint8_t)mod, (uint8_t)key);
  }
  // 3. 解析压枪宏指令
  else if(cmd.startsWith("MACRO|")) {
    // 截取枪械类型(去掉前缀"MACRO|")
    String gunType = cmd.substring(6);
    // 调用压枪宏函数
    runRecoilMacro(gunType);
  }
  // 4. 解析自瞄指令
  else if(cmd.startsWith("AIM|")) {
    String cmdBody = cmd.substring(4);
    // 解析X轴目标偏移
    int targetX = cmdBody.substring(0, cmdBody.indexOf("|")).toInt();
    // 解析Y轴目标偏移
    int targetY = cmdBody.substring(cmdBody.indexOf("|")+1).toInt();
    // 调用自瞄函数
    aimAssist((int16_t)targetX, (int16_t)targetY);
  }
  // 5. 解析KVM切换指令
  else if(cmd.startsWith("KVM|")) {
    // 截取设备ID(去掉前缀"KVM|")
    String deviceId = cmd.substring(4);
    // 调用KVM切换函数
    kvmSwitch(deviceId);
  }
  // 未知指令
  else {
    Serial.println("[MQTT解析] 未知指令:" + cmd);
  }
}

/************************ 辅助函数 - WiFi连接 ************************/
/**
 * @brief 连接指定WiFi网络
 * @return 无
 * @note 1. 阻塞式连接,直到WiFi连接成功;
 *       2. 连接成功后打印IP地址,用于调试;
 *       3. 需确保WiFi名称/密码正确,且ESP32与远端电脑在同一局域网
 */
void connectWiFi() {
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  Serial.print("连接WiFi(" + String(WIFI_SSID) + ")...");
  
  // 阻塞等待WiFi连接
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  // 连接成功,打印IP地址
  Serial.println("\nWiFi连接成功!");
  Serial.print("ESP32-S3 IP地址:");
  Serial.println(WiFi.localIP().toString());
}

/************************ 辅助函数 - MQTT重连 ************************/
/**
 * @brief MQTT断连后自动重连
 * @return bool:重连成功返回true,失败返回false
 * @note 1. 重连成功后自动重新订阅指令主题;
 *       2. 避免频繁重连,建议配合主循环的5秒间隔调用
 */
boolean reconnectMQTT() {
  // 连接MQTT服务器(使用唯一客户端ID)
  if (mqttClient.connect(MQTT_CLIENT_ID)) {
    Serial.println("MQTT服务器连接成功!");
    // 重新订阅键鼠/宏指令主题
    mqttClient.subscribe(MQTT_TOPIC_CTRL);
    // 重新订阅KVM切换主题
    mqttClient.subscribe(MQTT_TOPIC_KVM);
    return true;
  } else {
    Serial.print("MQTT连接失败,错误码:");
    Serial.println(mqttClient.state());
    return false;
  }
}

/************************ 回调函数 - MQTT消息接收 ************************/
/**
 * @brief MQTT消息接收回调函数(收到指令时自动触发)
 * @param topic 消息主题
 * @param payload 消息负载(指令内容)
 * @param length 消息长度
 * @return 无
 * @note 1. 将字节数组payload转换为字符串,便于解析;
 *       2. 根据主题分发指令(控制指令/KVM切换指令)
 */
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  // 转换payload为字符串
  String cmd = "";
  for (int i = 0; i < length; i++) {
    cmd += (char)payload[i];
  }
  
  // 打印接收的消息(调试用)
  Serial.print("收到MQTT消息 | 主题:");
  Serial.print(topic);
  Serial.print(" | 指令:");
  Serial.println(cmd);
  
  // 解析控制指令(键鼠/宏/自瞄)
  if(String(topic) == MQTT_TOPIC_CTRL) {
    parseMqttCmd(cmd);
  }
  // 解析KVM切换指令
  else if(String(topic) == MQTT_TOPIC_KVM) {
    kvmSwitch(cmd);
  }
}

/************************ 初始化函数 ************************/
void setup() {
  // 初始化串口(调试用,波特率115200)
  Serial.begin(115200);
  delay(1000); // 等待串口初始化完成

  // ========== 初始化USB HID键鼠 ==========
  // 添加鼠标/键盘HID报告到核心对象
  hid.addReport(&mouseReport);
  hid.addReport(&keyboardReport);
  // 启动HID功能
  hid.begin();
  // 等待HID初始化完成(确保电脑识别)
  while (!hid.ready()) {
    delay(100);
    Serial.print(".");
  }
  Serial.println("\nUSB HID初始化完成!远端电脑应识别为USB有线键鼠。");

  // ========== 连接WiFi ==========
  connectWiFi();

  // ========== 配置MQTT ==========
  // 设置MQTT服务器地址和端口
  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
  // 设置MQTT消息回调函数
  mqttClient.setCallback(mqttCallback);
  // 初始化重连时间标记
  lastReconnectAttempt = millis();
}

/************************ 主循环函数 ************************/
void loop() {
  // ========== MQTT断连重连逻辑 ==========
  if (!mqttClient.connected()) {
    unsigned long now = millis();
    // 每5秒尝试重连一次,避免频繁重试
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      // 重连MQTT服务器
      if (reconnectMQTT()) {
        // 重连成功,重置重连时间标记
        lastReconnectAttempt = 0;
      }
    }
  } 
  // ========== MQTT保持连接 ==========
  else {
    // 处理MQTT消息接收/发送
    mqttClient.loop();
  }

  // 主循环延迟,降低CPU占用
  delay(10);
}

3. ESP32-S3端核心函数详细说明

函数名 核心功能 关键参数说明 调用示例
mouseControl 模拟USB鼠标移动/按键 x:X轴偏移(-127~127);y:Y轴偏移;btn:按键(1=左键) mouseControl(10, 5, 1) → 右移10、下移5、左键按下
keyboardControl 模拟USB键盘按键/组合键 mod:修饰键(0x01=左Ctrl);key:按键码(0x04=A) keyboardControl(0x01, 0x04) → Ctrl+A
runRecoilMacro 执行预定义枪械压枪宏 gunType:枪械类型("AK47"/"M416") runRecoilMacro("AK47") → 执行AK47压枪
aimAssist 自瞄:分步移动鼠标到目标坐标 targetX/targetY:相对偏移量 aimAssist(50, 30) → 右移50、下移30
kvmSwitch KVM多设备切换 deviceId:目标设备ID kvmSwitch("ESP32-S3-KVM-002") → 切换到002设备
parseMqttCmd 解析MQTT指令并分发 cmd:指令字符串(如"M 10

三、本地Python端完整实现(详细注释+函数说明)

1. 开发环境准备

  • Python版本:3.8+

  • 依赖安装

    bash 复制代码
    pip install paho-mqtt pyautogui opencv-python numpy
    • paho-mqtt:MQTT通信核心库;
    • pyautogui:捕获本地键鼠操作(云玩功能);
    • opencv-python/numpy:可选,图像识别计算自瞄坐标。

2. 完整代码(带详细注释)

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ESP32-S3 KVM远程控制Python客户端
功能:
1. MQTT指令发送(键鼠/宏/自瞄/KVM);
2. 云玩功能:捕获本地键鼠操作并转发到ESP32;
3. 自瞄辅助:图像识别计算目标坐标(可选);
4. KVM状态监听:接收ESP32设备上线/离线反馈。
"""

import paho.mqtt.client as mqtt
import time
import pyautogui
import cv2
import numpy as np
from typing import Optional

/************************ 基础配置区(需根据实际修改) ************************/
# MQTT配置(与ESP32端一致)
MQTT_CONFIG = {
    "server": "broker.emqx.io",       # MQTT服务器地址
    "port": 1883,                     # MQTT端口
    "topic_ctrl": "esp32/kvm/control",# 键鼠/宏/自瞄指令发布主题
    "topic_kvm": "esp32/kvm/switch",  # KVM切换/状态反馈主题
    "client_id": "Python-KVM-Client"  # Python客户端唯一ID
}

# 云玩功能配置
CLOUD_PLAY_CONFIG = {
    "fail_safe": False,               # 禁用pyautogui角落退出(避免误触)
    "pause": 0.01,                    # 操作延迟(降低卡顿)
    "mouse_step_delay": 0.005,        # 鼠标转发延迟
    "key_hold_delay": 0.05            # 键盘转发延迟
}

# 自瞄功能配置(图像识别)
AIM_ASSIST_CONFIG = {
    "target_color": (0, 0, 255),      # 目标颜色(红色,BGR格式)
    "color_tolerance": 30,            # 颜色容差
    "screen_res": (1920, 1080)        # 远端电脑屏幕分辨率
}

# 初始化pyautogui配置
pyautogui.FAILSAFE = CLOUD_PLAY_CONFIG["fail_safe"]
pyautogui.PAUSE = CLOUD_PLAY_CONFIG["pause"]

/************************ MQTT客户端核心类 ************************/
class ESP32KVMMQTTClient:
    def __init__(self):
        """初始化MQTT客户端"""
        # 创建MQTT客户端对象
        self.client = mqtt.Client(client_id=MQTT_CONFIG["client_id"])
        # 注册回调函数
        self.client.on_connect = self.on_connect  # 连接成功回调
        self.client.on_message = self.on_message  # 接收消息回调
        self.client.on_disconnect = self.on_disconnect  # 断开连接回调
        
        # 初始化状态
        self.connected = False  # MQTT连接状态
        self.kvm_device_status = {}  # KVM设备状态字典(key=设备ID,value=在线状态)
    
    def on_connect(self, client: mqtt.Client, userdata, flags, rc: int):
        """
        MQTT连接成功回调函数
        :param client: MQTT客户端对象
        :param rc: 连接状态码(0=成功,其他=失败)
        """
        if rc == 0:
            self.connected = True
            print(f"[MQTT] 连接成功!状态码:{rc}")
            # 订阅KVM状态反馈主题
            client.subscribe(MQTT_CONFIG["topic_kvm"])
            print(f"[MQTT] 已订阅KVM状态主题:{MQTT_CONFIG['topic_kvm']}")
        else:
            self.connected = False
            print(f"[MQTT] 连接失败!状态码:{rc}")
    
    def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage):
        """
        MQTT接收消息回调函数
        :param msg: 消息对象(包含topic/payload)
        """
        topic = msg.topic
        payload = msg.payload.decode("utf-8")
        
        # 处理KVM状态反馈
        if topic == MQTT_CONFIG["topic_kvm"]:
            # 解析状态(格式:KVM:ESP32-S3-KVM-001-ON)
            if payload.startswith("KVM:"):
                device_info = payload.split(":")[1]
                device_id = device_info.split("-")[0] + "-" + device_info.split("-")[1] + "-" + device_info.split("-")[2]
                device_status = device_info.split("-")[3] == "ON"
                # 更新设备状态字典
                self.kvm_device_status[device_id] = device_status
                print(f"[KVM状态] 设备 {device_id} → {'在线' if device_status else '离线'}")
        # 其他主题消息(预留)
        else:
            print(f"[MQTT] 收到未知主题消息 | 主题:{topic} | 内容:{payload}")
    
    def on_disconnect(self, client: mqtt.Client, userdata, rc: int):
        """
        MQTT断开连接回调函数
        :param rc: 断开状态码(0=正常断开,1=异常断开)
        """
        self.connected = False
        if rc == 0:
            print(f"[MQTT] 正常断开连接!状态码:{rc}")
        else:
            print(f"[MQTT] 异常断开连接!状态码:{rc},5秒后尝试重连...")
            time.sleep(5)
            self.connect()  # 自动重连
    
    def connect(self):
        """连接MQTT服务器"""
        try:
            self.client.connect(MQTT_CONFIG["server"], MQTT_CONFIG["port"], 60)
            # 启动后台循环(非阻塞)
            self.client.loop_start()
            # 等待连接成功(最多等待5秒)
            timeout = 5
            start_time = time.time()
            while not self.connected and time.time() - start_time < timeout:
                time.sleep(0.1)
            if not self.connected:
                raise TimeoutError("MQTT连接超时")
        except Exception as e:
            print(f"[MQTT] 连接异常:{str(e)}")
            self.connected = False
    
    def disconnect(self):
        """断开MQTT连接"""
        self.client.loop_stop()
        self.client.disconnect()
        self.connected = False
        print("[MQTT] 已断开连接")
    
    /************************ 指令发送函数 - 鼠标 ************************/
    def send_mouse_cmd(self, x: int, y: int, btn: int = 0) -> bool:
        """
        发送鼠标控制指令
        :param x: X轴偏移量(-127~127)
        :param y: Y轴偏移量(-127~127)
        :param btn: 按键状态(0=无,1=左键,2=右键,4=中键)
        :return: 发送成功返回True,失败返回False
        """
        if not self.connected:
            print("[鼠标指令] MQTT未连接,发送失败")
            return False
        
        # 校验参数范围
        x = max(-127, min(127, x))
        y = max(-127, min(127, y))
        btn = max(0, min(7, btn))  # 按键组合最大为7(1+2+4)
        
        # 构造指令(格式:M|x|y|btn)
        cmd = f"M|{x}|{y}|{btn}"
        # 发布指令
        result = self.client.publish(MQTT_CONFIG["topic_ctrl"], cmd)
        # 检查发布结果
        if result.rc == mqtt.MQTT_ERR_SUCCESS:
            print(f"[鼠标指令] 发送成功 → {cmd}")
            return True
        else:
            print(f"[鼠标指令] 发送失败 → {cmd},错误码:{result.rc}")
            return False
    
    /************************ 指令发送函数 - 键盘 ************************/
    def send_keyboard_cmd(self, mod: int = 0, key: int = 0) -> bool:
        """
        发送键盘控制指令
        :param mod: 修饰键(0x01=左Ctrl,0x02=左Shift,0x04=左Alt,0x08=左Win)
        :param key: 普通按键HID码(参考标准HID码表)
        :return: 发送成功返回True,失败返回False
        """
        if not self.connected:
            print("[键盘指令] MQTT未连接,发送失败")
            return False
        
        # 校验参数范围
        mod = max(0, min(0xFF, mod))
        key = max(0, min(0x65, key))
        
        # 构造指令(格式:K|mod|key)
        cmd = f"K|{mod}|{key}"
        # 发布指令
        result = self.client.publish(MQTT_CONFIG["topic_ctrl"], cmd)
        if result.rc == mqtt.MQTT_ERR_SUCCESS:
            print(f"[键盘指令] 发送成功 → {cmd}")
            return True
        else:
            print(f"[键盘指令] 发送失败 → {cmd},错误码:{result.rc}")
            return False
    
    /************************ 指令发送函数 - 压枪宏 ************************/
    def send_recoil_macro_cmd(self, gun_type: str) -> bool:
        """
        发送压枪宏指令
        :param gun_type: 枪械类型("AK47"/"M416")
        :return: 发送成功返回True,失败返回False
        """
        if not self.connected:
            print("[压枪宏] MQTT未连接,发送失败")
            return False
        
        # 校验枪械类型
        if gun_type not in ["AK47", "M416"]:
            print(f"[压枪宏] 不支持的枪械类型:{gun_type}")
            return False
        
        # 构造指令(格式:MACRO|gunType)
        cmd = f"MACRO|{gun_type}"
        # 发布指令
        result = self.client.publish(MQTT_CONFIG["topic_ctrl"], cmd)
        if result.rc == mqtt.MQTT_ERR_SUCCESS:
            print(f"[压枪宏] 发送成功 → {cmd}")
            return True
        else:
            print(f"[压枪宏] 发送失败 → {cmd},错误码:{result.rc}")
            return False
    
    /************************ 指令发送函数 - 自瞄 ************************/
    def send_aim_assist_cmd(self, target_x: int, target_y: int) -> bool:
        """
        发送自瞄指令
        :param target_x: X轴相对偏移量
        :param target_y: Y轴相对偏移量
        :return: 发送成功返回True,失败返回False
        """
        if not self.connected:
            print("[自瞄指令] MQTT未连接,发送失败")
            return False
        
        # 构造指令(格式:AIM|x|y)
        cmd = f"AIM|{target_x}|{target_y}"
        # 发布指令
        result = self.client.publish(MQTT_CONFIG["topic_ctrl"], cmd)
        if result.rc == mqtt.MQTT_ERR_SUCCESS:
            print(f"[自瞄指令] 发送成功 → {cmd}")
            return True
        else:
            print(f"[自瞄指令] 发送失败 → {cmd},错误码:{result.rc}")
            return False
    
    /************************ 指令发送函数 - KVM切换 ************************/
    def send_kvm_switch_cmd(self, device_id: str) -> bool:
        """
        发送KVM切换指令
        :param device_id: 目标设备ID(如"ESP32-S3-KVM-001")
        :return: 发送成功返回True,失败返回False
        """
        if not self.connected:
            print("[KVM切换] MQTT未连接,发送失败")
            return False
        
        # 构造指令(直接发送设备ID)
        cmd = device_id
        # 发布指令
        result = self.client.publish(MQTT_CONFIG["topic_kvm"], cmd)
        if result.rc == mqtt.MQTT_ERR_SUCCESS:
            print(f"[KVM切换] 发送成功 → 切换到设备 {device_id}")
            # 等待状态反馈(最多2秒)
            time.sleep(2)
            # 打印当前设备状态
            print("[KVM状态] 最新设备状态:")
            for dev_id, status in self.kvm_device_status.items():
                print(f"  - {dev_id}: {'在线' if status else '离线'}")
            return True
        else:
            print(f"[KVM切换] 发送失败 → {device_id},错误码:{result.rc}")
            return False

/************************ 云玩功能模块 ************************/
class CloudPlayModule:
    def __init__(self, mqtt_client: ESP32KVMMQTTClient):
        """
        初始化云玩模块
        :param mqtt_client: MQTT客户端对象
        """
        self.mqtt_client = mqtt_client
        self.running = False  # 云玩运行状态
        self.last_mouse_pos = pyautogui.position()  # 上一帧鼠标位置
    
    def capture_mouse(self):
        """捕获本地鼠标操作并转发"""
        # 获取当前鼠标位置
        current_pos = pyautogui.position()
        # 计算偏移量
        dx = current_pos[0] - self.last_mouse_pos[0]
        dy = current_pos[1] - self.last_mouse_pos[1]
        
        # 有偏移时发送鼠标指令
        if dx != 0 or dy != 0:
            self.mqtt_client.send_mouse_cmd(dx, dy)
            # 更新上一帧位置
            self.last_mouse_pos = current_pos
        
        # 检测鼠标左键状态
        if pyautogui.mouseDown(button='left'):
            self.mqtt_client.send_mouse_cmd(0, 0, 1)  # 左键按下
        else:
            self.mqtt_client.send_mouse_cmd(0, 0, 0)  # 左键释放
        
        time.sleep(CLOUD_PLAY_CONFIG["mouse_step_delay"])
    
    def capture_keyboard(self):
        """捕获本地键盘操作并转发(简化版,仅示例)"""
        # 可扩展:通过pyautogui/keyboard库捕获键盘按键
        # 示例:检测A键按下
        if pyautogui.keyDown('a'):
            self.mqtt_client.send_keyboard_cmd(0, 0x04)  # 0x04=A键HID码
        time.sleep(CLOUD_PLAY_CONFIG["key_hold_delay"])
    
    def start(self):
        """启动云玩模式"""
        if not self.mqtt_client.connected:
            print("[云玩] MQTT未连接,无法启动")
            return
        
        self.running = True
        print("[云玩] 启动成功!按Ctrl+C退出")
        
        try:
            while self.running:
                # 捕获鼠标操作
                self.capture_mouse()
                # 捕获键盘操作(可选)
                # self.capture_keyboard()
        except KeyboardInterrupt:
            self.stop()
            print("[云玩] 已退出")
    
    def stop(self):
        """停止云玩模式"""
        self.running = False
        # 释放所有按键
        self.mqtt_client.send_mouse_cmd(0, 0, 0)
        self.mqtt_client.send_keyboard_cmd(0, 0)

/************************ 自瞄辅助模块(图像识别) ************************/
class AimAssistModule:
    @staticmethod
    def find_target(frame: np.ndarray) -> Optional[tuple[int, int]]:
        """
        图像识别找目标(基于颜色)
        :param frame: 远程桌面截图帧(BGR格式)
        :return: 目标中心坐标(x,y),无目标返回None
        """
        # 转换为HSV颜色空间(更易颜色匹配)
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        
        # 定义目标颜色范围
        target_bgr = AIM_ASSIST_CONFIG["target_color"]
        tolerance = AIM_ASSIST_CONFIG["color_tolerance"]
        
        # BGR转HSV
        target_hsv = cv2.cvtColor(np.uint8([[target_bgr]]), cv2.COLOR_BGR2HSV)[0][0]
        # 构造颜色范围
        lower_hsv = np.array([
            max(0, target_hsv[0] - tolerance),
            max(0, target_hsv[1] - tolerance),
            max(0, target_hsv[2] - tolerance)
        ])
        upper_hsv = np.array([
            min(179, target_hsv[0] + tolerance),
            min(255, target_hsv[1] + tolerance),
            min(255, target_hsv[2] + tolerance)
        ])
        
        # 颜色掩码
        mask = cv2.inRange(hsv, lower_hsv, upper_hsv)
        # 形态学操作去噪
        mask = cv2.erode(mask, None, iterations=2)
        mask = cv2.dilate(mask, None, iterations=2)
        
        # 查找轮廓
        contours, _ = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        if len(contours) == 0:
            return None
        
        # 找最大轮廓(目标)
        largest_contour = max(contours, key=cv2.contourArea)
        # 计算轮廓中心
        M = cv2.moments(largest_contour)
        if M["m00"] == 0:
            return None
        
        cX = int(M["m10"] / M["m00"])
        cY = int(M["m01"] / M["m00"])
        
        return (cX, cY)
    
    def calculate_aim_offset(self, target_pos: tuple[int, int]) -> tuple[int, int]:
        """
        计算自瞄偏移量(相对屏幕中心)
        :param target_pos: 目标绝对坐标(x,y)
        :return: 相对偏移量(dx, dy)
        """
        screen_center_x = AIM_ASSIST_CONFIG["screen_res"][0] // 2
        screen_center_y = AIM_ASSIST_CONFIG["screen_res"][1] // 2
        
        dx = target_pos[0] - screen_center_x
        dy = target_pos[1] - screen_center_y
        
        return (dx, dy)

/************************ 主函数(测试/使用示例) ************************/
def main():
    # 1. 初始化MQTT客户端
    mqtt_client = ESP32KVMMQTTClient()
    mqtt_client.connect()
    
    # 等待MQTT连接成功
    time.sleep(1)
    if not mqtt_client.connected:
        print("MQTT连接失败,程序退出")
        return
    
    # 2. 初始化云玩模块
    cloud_play = CloudPlayModule(mqtt_client)
    
    # 3. 初始化自瞄模块
    aim_assist = AimAssistModule()
    
    try:
        # ========== 测试KVM切换 ==========
        mqtt_client.send_kvm_switch_cmd("ESP32-S3-KVM-001")
        time.sleep(1)
        
        # ========== 测试普通鼠标 ==========
        mqtt_client.send_mouse_cmd(10, 5, 0)
        time.sleep(0.5)
        
        # ========== 测试普通键盘(按A键) ==========
        mqtt_client.send_keyboard_cmd(0, 0x04)  # 0x04=A键
        time.sleep(0.5)
        
        # ========== 测试压枪宏(AK47) ==========
        mqtt_client.send_recoil_macro_cmd("AK47")
        time.sleep(1)
        
        # ========== 测试自瞄(相对偏移+50,+30) ==========
        mqtt_client.send_aim_assist_cmd(50, 30)
        time.sleep(1)
        
        # ========== 启动云玩模式(注释掉可关闭) ==========
        # cloud_play.start()
        
    except KeyboardInterrupt:
        print("\n程序被用户中断")
    finally:
        # 清理资源
        mqtt_client.disconnect()
        print("程序正常退出")

if __name__ == "__main__":
    main()

3. Python端核心类/函数详细说明

类/函数名 核心功能 关键参数说明 调用示例
ESP32KVMMQTTClient MQTT客户端核心类,封装所有指令发送逻辑 无(初始化时加载配置) client = ESP32KVMMQTTClient()client.connect()
send_mouse_cmd 发送鼠标控制指令 x/y:偏移量;btn:按键 client.send_mouse_cmd(10,5,1)
send_keyboard_cmd 发送键盘控制指令 mod:修饰键;key:HID码 client.send_keyboard_cmd(0,0x04)(按A键)
send_recoil_macro_cmd 发送压枪宏指令 gun_type:枪械类型 client.send_recoil_macro_cmd("AK47")
send_aim_assist_cmd 发送自瞄指令 target_x/target_y:偏移量 client.send_aim_assist_cmd(50,30)
send_kvm_switch_cmd 发送KVM切换指令 device_id:目标设备ID client.send_kvm_switch_cmd("ESP32-S3-KVM-001")
CloudPlayModule 云玩模块,捕获本地键鼠并转发 mqtt_client:MQTT客户端对象 cloud = CloudPlayModule(client)cloud.start()
AimAssistModule.find_target 图像识别找目标 frame:远程桌面截图帧 AimAssistModule.find_target(frame) → 返回目标坐标

四、功能测试与验证步骤

1. 硬件部署

  1. 将ESP32-S3通过USB Type-C数据线(数据款)连接到远端电脑;
  2. 确保ESP32-S3与远端电脑在同一WiFi网络;
  3. 本地主机通过远程桌面工具(如Windows远程桌面、AnyDesk)连接到远端电脑。

2. 软件部署

  1. 烧录ESP32-S3端代码(修改WiFi/MQTT配置);
  2. 本地运行Python端代码(确保依赖安装完成)。

3. 功能验证

功能 验证方法 预期结果
虚拟USB键鼠 Python发送`M 10
压枪宏 Python发送`MACRO AK47`指令
自瞄 Python发送`AIM 50
KVM切换 Python发送`KVM ESP32-S3-KVM-002`
云玩 启动cloud_play.start() 本地移动鼠标/按键盘,远端电脑同步响应

五、关键避坑指南

  1. USB线选择:必须用"数据款"Type-C线,仅充电线会导致远端电脑无法识别HID设备;
  2. MQTT指令格式 :严格按|分隔,无多余空格(如M|10|5|1,非M | 10 | 5 | 1);
  3. 压枪曲线调整 :根据游戏实际后坐力修改ak47Recoil/m416Recoil数组;
  4. 自瞄坐标校准:远程桌面分辨率缩放会导致坐标偏移,需按实际分辨率调整;
  5. 功耗优化:ESP32-S3端可开启浅睡眠(参考之前的功耗优化方案),降低远端电脑USB供电负载。

六、总结

本方案以"ESP32-S3多设备KVM远程控制系统"为核心主题,实现了:

  1. 基础能力:ESP32-S3模拟USB有线键鼠,通过MQTT接收远程指令;
  2. 核心扩展:KVM多设备切换、游戏压枪宏、自瞄辅助、云玩功能;
  3. 工程化落地:代码模块化设计,每个功能函数都有详细注释和调用示例,新手可快速上手。

方案可适配远程办公、游戏云玩、工业设备远程控制等场景,且支持多设备扩展(仅需增加ESP32-S3设备并修改ID)。

相关推荐
不会kao代码的小王2 小时前
深信服超融合 HCI 核心技术解析:aSV、aSAN 与 aNET 的协同架构
运维·服务器·网络·数据库·github
犀思云2 小时前
电子制造出海全球生产协同:网络韧性构建与运维实战路径
网络·智能仓储·fusionwan·专线·naas
RoboWizard2 小时前
高速游戏电脑畅玩寒假 1月DIY配置攻略
游戏·电脑
Alonse_沃虎电子2 小时前
沃虎电压驱动型PHY vs. 电流驱动型PHY:如何正确连接网络变压器?
网络
yi碗汤园2 小时前
【超详细】TCP编程与UDP编程
网络·网络协议·tcp/ip·unity·udp·visual studio
Godspeed Zhao2 小时前
现代智能汽车中的无线技术33——V2X(5)
网络·汽车
军哥系统集成号2 小时前
PIA协同落地指南:打破六类评估壁垒,实现合规证据复用(合规协同系列第四篇)
网络·安全·web安全
Mar_mxs2 小时前
win11网线连接两台电脑传输文件(解决共享时不知道网络凭据的用户名和密码)
网络