基于ESP32-S3+Barrier实现多电脑KVM共享方案(无缝切换+剪贴板/文件共享)
一、方案核心架构
Barrier(原Synergy)是开源跨平台KVM工具,核心基于TCP/IP实现键鼠事件、剪贴板、文件的跨设备同步。本方案将ESP32-S3作为轻量化Barrier Server,替代传统PC端Server,实现多台电脑(Windows/macOS/Linux)共享一套物理键鼠,支持无缝屏幕切换、剪贴板双向同步、文件拖放,且ESP32-S3具备低功耗、小型化、易部署的优势。
USB HID
TCP/24800
TCP/24800
TCP/24800
剪贴板同步
剪贴板同步
剪贴板同步
文件拖放
文件拖放
文件拖放
屏幕边界检测
激活对应Client
物理键鼠
ESP32-S3 Barrier Server
电脑1(Barrier Client)
电脑2(Barrier Client)
电脑3(Barrier Client)
无缝切换逻辑
C/D/E
核心模块说明
| 模块 |
功能 |
技术栈 |
| ESP32-S3 Server |
1. 捕获物理键鼠HID事件;2. 实现Barrier协议解析/封装;3. 多Client连接管理;4. 切换/剪贴板/文件共享逻辑 |
ESP-IDF/Arduino + USB HID + TCP/IP + Barrier协议 |
| 电脑端Client |
1. 安装官方Barrier Client;2. 连接ESP32-S3 Server;3. 接收键鼠事件并模拟输入;4. 同步剪贴板/文件 |
官方Barrier客户端(v2.4.0+) |
| 无缝切换 |
检测鼠标移动至屏幕边缘,自动切换ESP32-S3的目标Client,实现无感知切换 |
屏幕分辨率配置 + 鼠标坐标边界检测 |
| 剪贴板共享 |
监听Client剪贴板变化,同步至ESP32-S3并转发至所有在线Client |
文本/二进制剪贴板数据封装 + TCP传输 |
| 文件拖放 |
接收Client文件拖放请求,转发文件数据至目标Client(支持小文件直传) |
文件分片传输 + 校验机制 |
二、环境准备
1. 硬件清单
- ESP32-S3开发板(需支持USB Host模式,推荐带USB OTG接口的型号,如ESP32-S3-DevKitC-1)
- 物理键鼠(USB接口)
- USB OTG转接头(ESP32-S3 ↔ 物理键鼠)
- 多台电脑(作为Client,需接入同一局域网)
- 网线/WiFi(ESP32-S3接入局域网,确保与Client互通)
2. 软件依赖
三、Barrier核心协议基础(ESP32-S3实现关键)
Barrier基于二进制消息协议通信,默认TCP端口24800,核心消息类型:
| 消息类型 |
功能 |
核心格式(简化) |
| 键鼠事件(DATA) |
发送鼠标移动/按键、键盘按键 |
头部(4字节)+ 事件类型(1字节)+ 数据(N字节) |
| 剪贴板(CLIPBOARD) |
同步剪贴板文本/数据 |
头部 + 数据类型(文本/图片)+ 数据长度 + 数据 |
| 屏幕切换(SCREEN) |
通知Client激活/失活 |
头部 + 目标Client名称 + 激活状态 |
| 文件拖放(FILE) |
传输文件路径/二进制数据 |
头部 + 文件名称 + 文件大小 + 分片数据 |
四、ESP32-S3完整代码实现(Arduino框架)
1. 核心配置与全局变量
cpp
复制代码
#include <WiFi.h>
#include <USBHID.h>
#include <HIDReports.h>
#include <AsyncTCP.h>
#include <vector>
// ===================== 核心配置 =====================
// WiFi配置(接入局域网)
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PWD "你的WiFi密码"
// Barrier配置
#define BARRIER_PORT 24800 // Barrier默认端口
#define MAX_CLIENTS 3 // 最大支持3台Client
#define SCREEN_WIDTH 1920 // Client屏幕宽度(用于边界检测)
#define SCREEN_HEIGHT 1080 // Client屏幕高度
// 键鼠HID配置
USBHID usb_hid; // USB HID主机,捕获物理键鼠
HIDReportParser parser; // HID报告解析器
// ===================== 全局变量 =====================
// WiFi/TCP相关
AsyncServer server(BARRIER_PORT); // 异步TCP服务器
std::vector<AsyncClient*> clients; // 已连接的Client列表
AsyncClient* active_client = nullptr; // 当前激活的Client
// 键鼠状态
struct MouseState {
int x = 0, y = 0; // 鼠标坐标(累计值)
uint8_t buttons = 0; // 鼠标按键(1=左键,2=右键,4=中键)
} mouse_state;
struct KeyboardState {
uint8_t modifier = 0; // 修饰键(Ctrl/Shift/Alt/Win)
uint8_t keys[6] = {0}; // 按键码(最多6个同时按下)
} keyboard_state;
// 剪贴板状态
String clipboard_data = ""; // 全局剪贴板数据
bool clipboard_updated = false; // 剪贴板更新标记
// 屏幕切换状态
int current_screen_idx = 0; // 当前激活的屏幕索引(0=电脑1,1=电脑2,2=电脑3)
2. 初始化函数(WiFi+TCP+USB HID)
cpp
复制代码
/**
* @brief WiFi连接函数
* @details 连接指定WiFi,循环重试直到成功,打印局域网IP
*/
void connectWiFi() {
Serial.printf("Connecting to WiFi: %s...\n", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PWD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi Connected!");
Serial.print("ESP32-S3 LAN IP: ");
Serial.println(WiFi.localIP());
}
/**
* @brief USB HID初始化函数
* @details 初始化USB Host模式,捕获物理键鼠的HID报告
*/
void initUSBHID() {
usb_hid.begin();
usb_hid.setReportParser(0, &parser); // 绑定HID报告解析器
while (!usb_hid.ready()) {
delay(10);
}
Serial.println("USB HID Host Ready (键鼠已连接)");
}
/**
* @brief TCP服务器初始化函数
* @details 启动Barrier TCP服务器,监听Client连接/断开/数据接收事件
*/
void initTCPServer() {
// Client连接事件
server.onClient([](void* arg, AsyncClient* client) {
Serial.printf("New Client Connected: %s:%d\n", client->remoteIP().toString().c_str(), client->remotePort());
// 限制最大Client数
if (clients.size() >= MAX_CLIENTS) {
client->close(true);
Serial.println("Max Clients Reached - Disconnect New Client");
return;
}
// 添加到Client列表
clients.push_back(client);
// 初始激活第一个Client
if (active_client == nullptr) {
active_client = client;
Serial.printf("Active Client: %s:%d\n", active_client->remoteIP().toString().c_str(), active_client->remotePort());
}
// Client数据接收事件(处理剪贴板/文件拖放)
client->onData([](void* arg, AsyncClient* client, void* data, size_t len) {
uint8_t* buf = (uint8_t*)data;
handleClientData(client, buf, len); // 解析Client发送的数据(剪贴板/文件)
}, nullptr);
// Client断开事件
client->onDisconnect([](void* arg, AsyncClient* client) {
Serial.printf("Client Disconnected: %s:%d\n", client->remoteIP().toString().c_str(), client->remotePort());
// 从列表移除
for (auto it = clients.begin(); it != clients.end(); ++it) {
if (*it == client) {
clients.erase(it);
break;
}
}
// 若断开的是激活Client,切换到下一个
if (client == active_client) {
active_client = clients.empty() ? nullptr : clients[0];
if (active_client != nullptr) {
Serial.printf("Switch to Client: %s:%d\n", active_client->remoteIP().toString().c_str(), active_client->remotePort());
}
}
client->free();
}, nullptr);
}, nullptr);
// 启动TCP服务器
server.begin();
Serial.printf("Barrier TCP Server Started on Port: %d\n", BARRIER_PORT);
}
/**
* @brief 系统初始化入口
*/
void setup() {
Serial.begin(115200);
delay(100);
// 初始化WiFi
connectWiFi();
// 初始化USB HID(捕获物理键鼠)
initUSBHID();
// 初始化TCP服务器(Barrier Server)
initTCPServer();
Serial.println("ESP32-S3 Barrier Server Init Complete!");
}
3. 键鼠事件捕获与Barrier协议封装
cpp
复制代码
/**
* @brief 解析HID鼠标报告,更新鼠标状态
* @param report HID鼠标报告数据
* @param len 报告长度
*/
void parseMouseReport(uint8_t* report, size_t len) {
if (len < 4) return; // 最小鼠标报告长度:按键(1)+X(1)+Y(1)+滚轮(1)
// 更新鼠标按键
mouse_state.buttons = report[0];
// 更新鼠标坐标(累计值,用于边界检测)
mouse_state.x += (int8_t)report[1];
mouse_state.y += (int8_t)report[2];
// 屏幕边界检测(无缝切换)
checkScreenBoundary();
// 封装Barrier鼠标事件并发送到激活Client
sendMouseEventToClient();
}
/**
* @brief 解析HID键盘报告,更新键盘状态
* @param report HID键盘报告数据
* @param len 报告长度
*/
void parseKeyboardReport(uint8_t* report, size_t len) {
if (len < 8) return; // 最小键盘报告长度:修饰键(1)+保留(1)+按键(6)
// 更新键盘状态
keyboard_state.modifier = report[0];
memcpy(keyboard_state.keys, &report[2], 6);
// 封装Barrier键盘事件并发送到激活Client
sendKeyboardEventToClient();
}
/**
* @brief 屏幕边界检测,实现无缝切换
* @details 当鼠标坐标超出当前屏幕范围,切换到下一个Client
*/
void checkScreenBoundary() {
if (clients.size() <= 1) return; // 仅1台Client无需切换
// 右边界:切换到下一个Client
if (mouse_state.x >= SCREEN_WIDTH) {
current_screen_idx = (current_screen_idx + 1) % clients.size();
active_client = clients[current_screen_idx];
mouse_state.x = 0; // 重置鼠标坐标到新屏幕左边界
Serial.printf("Switch to Screen %d (Client: %s:%d)\n", current_screen_idx+1,
active_client->remoteIP().toString().c_str(), active_client->remotePort());
}
// 左边界:切换到上一个Client
else if (mouse_state.x <= 0) {
current_screen_idx = (current_screen_idx - 1 + clients.size()) % clients.size();
active_client = clients[current_screen_idx];
mouse_state.x = SCREEN_WIDTH - 1; // 重置鼠标坐标到新屏幕右边界
Serial.printf("Switch to Screen %d (Client: %s:%d)\n", current_screen_idx+1,
active_client->remoteIP().toString().c_str(), active_client->remotePort());
}
}
/**
* @brief 封装Barrier鼠标事件并发送到激活Client
*/
void sendMouseEventToClient() {
if (active_client == nullptr || !active_client->connected()) return;
// Barrier鼠标事件包格式(简化版,兼容官方Client)
uint8_t packet[32];
memset(packet, 0, sizeof(packet));
// 1. 协议头部(Barrier固定魔数)
packet[0] = 0x00; packet[1] = 0x00; packet[2] = 0x00; packet[3] = 0x01;
// 2. 事件类型:鼠标事件(0x02)
packet[4] = 0x02;
// 3. 鼠标按键
packet[5] = mouse_state.buttons;
// 4. 鼠标X/Y坐标(小端序)
memcpy(&packet[6], &mouse_state.x, 2);
memcpy(&packet[8], &mouse_state.y, 2);
// 发送到激活Client
active_client->write(packet, 10);
}
/**
* @brief 封装Barrier键盘事件并发送到激活Client
*/
void sendKeyboardEventToClient() {
if (active_client == nullptr || !active_client->connected()) return;
// Barrier键盘事件包格式(简化版)
uint8_t packet[32];
memset(packet, 0, sizeof(packet));
// 1. 协议头部
packet[0] = 0x00; packet[1] = 0x00; packet[2] = 0x00; packet[3] = 0x01;
// 2. 事件类型:键盘事件(0x01)
packet[4] = 0x01;
// 3. 修饰键
packet[5] = keyboard_state.modifier;
// 4. 按键码(前3个按键)
memcpy(&packet[6], keyboard_state.keys, 3);
// 发送到激活Client
active_client->write(packet, 9);
}
/**
* @brief 主循环:监听HID键鼠报告
*/
void loop() {
// 读取USB HID报告(键鼠事件)
uint8_t hid_report[64];
int len = usb_hid.read(hid_report, sizeof(hid_report));
if (len > 0) {
// 判断报告类型:鼠标(首字节为按键)/键盘(首字节为修饰键)
if (len == 4) { // 鼠标报告长度
parseMouseReport(hid_report, len);
} else if (len == 8) { // 键盘报告长度
parseKeyboardReport(hid_report, len);
}
}
// 同步剪贴板(若有更新)
if (clipboard_updated) {
syncClipboardToAllClients();
clipboard_updated = false;
}
delay(1); // 降低CPU占用
}
4. 剪贴板同步与文件拖放处理
cpp
复制代码
/**
* @brief 处理Client发送的数据(剪贴板/文件拖放)
* @param client 发送数据的Client
* @param data 数据缓冲区
* @param len 数据长度
*/
void handleClientData(AsyncClient* client, uint8_t* data, size_t len) {
// 解析数据类型:剪贴板(0x03)/文件拖放(0x04)
if (data[4] == 0x03) { // 剪贴板数据
// 提取剪贴板内容(偏移5:数据长度,偏移7:实际数据)
size_t clip_len = (data[5] << 8) | data[6];
char clip_buf[clip_len + 1];
memcpy(clip_buf, &data[7], clip_len);
clip_buf[clip_len] = '\0';
// 更新全局剪贴板
clipboard_data = String(clip_buf);
clipboard_updated = true;
Serial.printf("Received Clipboard from Client %s:%d: %s\n",
client->remoteIP().toString().c_str(), client->remotePort(), clipboard_data.c_str());
}
else if (data[4] == 0x04) { // 文件拖放数据
// 提取文件信息(偏移5:文件名长度,偏移7:文件名,后续为文件数据)
size_t fname_len = (data[5] << 8) | data[6];
char fname[fname_len + 1];
memcpy(fname, &data[7], fname_len);
fname[fname_len] = '\0';
// 提取文件数据
size_t file_len = len - 7 - fname_len;
uint8_t* file_data = &data[7 + fname_len];
Serial.printf("Received File from Client %s:%d: %s (Size: %d bytes)\n",
client->remoteIP().toString().c_str(), client->remotePort(), fname, file_len);
// 转发文件到激活Client(非发送方)
if (client != active_client && active_client != nullptr) {
sendFileToClient(active_client, fname, fname_len, file_data, file_len);
}
}
}
/**
* @brief 同步剪贴板数据到所有在线Client
*/
void syncClipboardToAllClients() {
if (clipboard_data.isEmpty() || clients.empty()) return;
// 封装Barrier剪贴板数据包
uint8_t packet[1024];
memset(packet, 0, sizeof(packet));
// 1. 协议头部
packet[0] = 0x00; packet[1] = 0x00; packet[2] = 0x00; packet[3] = 0x01;
// 2. 数据类型:剪贴板(0x03)
packet[4] = 0x03;
// 3. 剪贴板数据长度(小端序)
size_t clip_len = clipboard_data.length();
packet[5] = (clip_len >> 8) & 0xFF;
packet[6] = clip_len & 0xFF;
// 4. 剪贴板数据
memcpy(&packet[7], clipboard_data.c_str(), clip_len);
// 发送到所有Client
for (auto client : clients) {
if (client->connected()) {
client->write(packet, 7 + clip_len);
}
}
Serial.printf("Sync Clipboard to All Clients: %s\n", clipboard_data.c_str());
}
/**
* @brief 发送文件数据到目标Client
* @param client 目标Client
* @param fname 文件名
* @param fname_len 文件名长度
* @param file_data 文件数据
* @param file_len 文件长度
*/
void sendFileToClient(AsyncClient* client, char* fname, size_t fname_len, uint8_t* file_data, size_t file_len) {
if (!client->connected()) return;
// 封装Barrier文件数据包
uint8_t packet[2048];
memset(packet, 0, sizeof(packet));
// 1. 协议头部
packet[0] = 0x00; packet[1] = 0x00; packet[2] = 0x00; packet[3] = 0x01;
// 2. 数据类型:文件拖放(0x04)
packet[4] = 0x04;
// 3. 文件名长度(小端序)
packet[5] = (fname_len >> 8) & 0xFF;
packet[6] = fname_len & 0xFF;
// 4. 文件名
memcpy(&packet[7], fname, fname_len);
// 5. 文件数据
memcpy(&packet[7 + fname_len], file_data, file_len);
// 发送文件数据包
client->write(packet, 7 + fname_len + file_len);
Serial.printf("Send File to Client %s:%d: %s\n",
client->remoteIP().toString().c_str(), client->remotePort(), fname);
}
五、电脑端Barrier Client配置
1. 安装与基础配置
- 下载并安装官方Barrier Client(v2.4.0+);
- 打开Client,在「Server IP」栏输入ESP32-S3的局域网IP(如192.168.1.100);
- 在「Screen Name」栏设置唯一名称(如Screen1/Screen2/Screen3),与ESP32-S3的
current_screen_idx对应;
- 点击「Connect」,Client将自动连接ESP32-S3的24800端口。
2. 无缝切换配置
- 在Client的「Settings」→「Screen」中,设置屏幕分辨率(与ESP32-S3的
SCREEN_WIDTH/SCREEN_HEIGHT一致);
- 勾选「Allow switching to this screen」,启用屏幕切换;
- 配置屏幕布局(如Screen1在左,Screen2在右),与ESP32-S3的边界检测逻辑匹配。
3. 剪贴板/文件共享配置
- 在Client的「Settings」→「Advanced」中,勾选「Enable clipboard sharing」和「Enable file transfer」;
- 确认文件传输路径(默认桌面),完成后保存配置。
六、关键函数说明与测试步骤
1. 核心函数功能表
| 函数名 |
功能说明 |
connectWiFi() |
连接局域网WiFi,获取ESP32-S3的IP地址,为TCP通信提供网络基础 |
initUSBHID() |
初始化USB Host模式,捕获物理键鼠的HID报告 |
initTCPServer() |
启动异步TCP服务器,监听24800端口,管理Client的连接/断开/数据接收 |
parseMouseReport() |
解析HID鼠标报告,更新鼠标坐标/按键状态,触发边界检测和事件发送 |
parseKeyboardReport() |
解析HID键盘报告,更新修饰键/按键状态,封装并发送键盘事件 |
checkScreenBoundary() |
检测鼠标坐标是否超出屏幕范围,自动切换激活的Client,实现无缝切换 |
handleClientData() |
解析Client发送的剪贴板/文件数据,更新全局剪贴板或转发文件 |
syncClipboardToAllClients() |
将全局剪贴板数据同步到所有在线Client,实现剪贴板共享 |
sendFileToClient() |
将接收的文件数据转发到目标Client,实现跨设备文件拖放 |
2. 测试步骤
(1)基础连通性测试
- 烧录代码到ESP32-S3,连接物理键鼠,上电后查看串口打印的IP地址;
- 多台电脑安装Barrier Client,输入ESP32-S3的IP并连接;
- 查看ESP32-S3串口,确认Client连接日志(
New Client Connected),表示连通成功。
(2)键鼠共享测试
- 操作物理键鼠,验证当前激活的Client是否同步响应(鼠标移动、按键、键盘输入);
- 将鼠标移动到屏幕右边界,验证是否自动切换到下一个Client,且键鼠控制权同步转移。
(3)剪贴板共享测试
- 在Screen1的Client中复制文本(如"Test Clipboard");
- 在Screen2的Client中粘贴,验证文本是否同步;
- 反向操作(Screen2复制→Screen1粘贴),确认双向同步。
(4)文件拖放测试
- 在Screen1的Client中,将一个小文件(<1MB)拖放到Screen2的Client窗口;
- 查看Screen2的桌面,确认文件是否成功传输;
- 验证ESP32-S3串口的文件传输日志(
Received File/Send File)。
七、总结
核心关键点回顾
- ESP32-S3核心角色:作为轻量化Barrier Server,通过USB Host捕获物理键鼠HID事件,封装为Barrier协议包后发送到激活的Client,替代传统PC端Server;
- 无缝切换实现:基于鼠标坐标边界检测,自动切换激活的Client,配合Client端屏幕布局配置,实现无感知跨屏切换;
- 数据共享核心:通过TCP传输封装的剪贴板/文件数据包,实现多Client间的剪贴板双向同步、小文件拖放;
- 优势特性:ESP32-S3低功耗、小型化,无需额外PC作为Server,部署成本低,适配多平台Client(Windows/macOS/Linux)。
优化方向
- 协议兼容:完善Barrier全协议支持(如滚轮事件、自定义快捷键),提升与官方Client的兼容性;
- 大文件传输:实现文件分片传输+校验机制,支持大于2MB的文件拖放;
- 延迟优化:采用UDP协议传输键鼠事件,降低输入延迟(<10ms);
- GUI配置:增加ESP32-S3的Web配置页面,可视化设置屏幕分辨率、Client数量、切换逻辑。