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配置 :
- 首选项添加ESP32开发板地址:
https://dl.espressif.com/dl/package_esp32_index.json; - 安装
PubSubClient库(MQTT通信)、USBHID库(默认内置); - 开发板选择:
ESP32-S3 Dev Module,配置项:USB Mode:USB-OTG (HID)CPU Frequency:80MHz(降功耗)Flash Size:4MB
- 首选项添加ESP32开发板地址:
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+
-
依赖安装 :
bashpip install paho-mqtt pyautogui opencv-python numpypaho-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. 硬件部署
- 将ESP32-S3通过USB Type-C数据线(数据款)连接到远端电脑;
- 确保ESP32-S3与远端电脑在同一WiFi网络;
- 本地主机通过远程桌面工具(如Windows远程桌面、AnyDesk)连接到远端电脑。
2. 软件部署
- 烧录ESP32-S3端代码(修改WiFi/MQTT配置);
- 本地运行Python端代码(确保依赖安装完成)。
3. 功能验证
| 功能 | 验证方法 | 预期结果 |
|---|---|---|
| 虚拟USB键鼠 | Python发送`M | 10 |
| 压枪宏 | Python发送`MACRO | AK47`指令 |
| 自瞄 | Python发送`AIM | 50 |
| KVM切换 | Python发送`KVM | ESP32-S3-KVM-002` |
| 云玩 | 启动cloud_play.start() |
本地移动鼠标/按键盘,远端电脑同步响应 |
五、关键避坑指南
- USB线选择:必须用"数据款"Type-C线,仅充电线会导致远端电脑无法识别HID设备;
- MQTT指令格式 :严格按
|分隔,无多余空格(如M|10|5|1,非M | 10 | 5 | 1); - 压枪曲线调整 :根据游戏实际后坐力修改
ak47Recoil/m416Recoil数组; - 自瞄坐标校准:远程桌面分辨率缩放会导致坐标偏移,需按实际分辨率调整;
- 功耗优化:ESP32-S3端可开启浅睡眠(参考之前的功耗优化方案),降低远端电脑USB供电负载。
六、总结
本方案以"ESP32-S3多设备KVM远程控制系统"为核心主题,实现了:
- 基础能力:ESP32-S3模拟USB有线键鼠,通过MQTT接收远程指令;
- 核心扩展:KVM多设备切换、游戏压枪宏、自瞄辅助、云玩功能;
- 工程化落地:代码模块化设计,每个功能函数都有详细注释和调用示例,新手可快速上手。
方案可适配远程办公、游戏云玩、工业设备远程控制等场景,且支持多设备扩展(仅需增加ESP32-S3设备并修改ID)。