基于ESP32-S3+Barrier实现多电脑KVM共享方案(无缝切换+剪贴板/文件共享)

基于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. 软件依赖

设备/模块 依赖安装/配置步骤
ESP32开发环境 Arduino IDE安装ESP32板库(≥2.0.14),安装以下库: ① USBHID(内置) ② WiFi(内置) ③ AsyncTCP(异步TCP,提升并发)
电脑端Client 下载官方Barrier客户端: Windows:https://github.com/debauchee/barrier/releases Linux:sudo apt install barrier
协议参考 Barrier开源协议文档:https://github.com/debauchee/barrier/wiki/Protocol

三、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. 安装与基础配置

  1. 下载并安装官方Barrier Client(v2.4.0+);
  2. 打开Client,在「Server IP」栏输入ESP32-S3的局域网IP(如192.168.1.100);
  3. 在「Screen Name」栏设置唯一名称(如Screen1/Screen2/Screen3),与ESP32-S3的current_screen_idx对应;
  4. 点击「Connect」,Client将自动连接ESP32-S3的24800端口。

2. 无缝切换配置

  1. 在Client的「Settings」→「Screen」中,设置屏幕分辨率(与ESP32-S3的SCREEN_WIDTH/SCREEN_HEIGHT一致);
  2. 勾选「Allow switching to this screen」,启用屏幕切换;
  3. 配置屏幕布局(如Screen1在左,Screen2在右),与ESP32-S3的边界检测逻辑匹配。

3. 剪贴板/文件共享配置

  1. 在Client的「Settings」→「Advanced」中,勾选「Enable clipboard sharing」和「Enable file transfer」;
  2. 确认文件传输路径(默认桌面),完成后保存配置。

六、关键函数说明与测试步骤

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)基础连通性测试
  1. 烧录代码到ESP32-S3,连接物理键鼠,上电后查看串口打印的IP地址;
  2. 多台电脑安装Barrier Client,输入ESP32-S3的IP并连接;
  3. 查看ESP32-S3串口,确认Client连接日志(New Client Connected),表示连通成功。
(2)键鼠共享测试
  1. 操作物理键鼠,验证当前激活的Client是否同步响应(鼠标移动、按键、键盘输入);
  2. 将鼠标移动到屏幕右边界,验证是否自动切换到下一个Client,且键鼠控制权同步转移。
(3)剪贴板共享测试
  1. 在Screen1的Client中复制文本(如"Test Clipboard");
  2. 在Screen2的Client中粘贴,验证文本是否同步;
  3. 反向操作(Screen2复制→Screen1粘贴),确认双向同步。
(4)文件拖放测试
  1. 在Screen1的Client中,将一个小文件(<1MB)拖放到Screen2的Client窗口;
  2. 查看Screen2的桌面,确认文件是否成功传输;
  3. 验证ESP32-S3串口的文件传输日志(Received File/Send File)。

七、总结

核心关键点回顾

  1. ESP32-S3核心角色:作为轻量化Barrier Server,通过USB Host捕获物理键鼠HID事件,封装为Barrier协议包后发送到激活的Client,替代传统PC端Server;
  2. 无缝切换实现:基于鼠标坐标边界检测,自动切换激活的Client,配合Client端屏幕布局配置,实现无感知跨屏切换;
  3. 数据共享核心:通过TCP传输封装的剪贴板/文件数据包,实现多Client间的剪贴板双向同步、小文件拖放;
  4. 优势特性:ESP32-S3低功耗、小型化,无需额外PC作为Server,部署成本低,适配多平台Client(Windows/macOS/Linux)。

优化方向

  1. 协议兼容:完善Barrier全协议支持(如滚轮事件、自定义快捷键),提升与官方Client的兼容性;
  2. 大文件传输:实现文件分片传输+校验机制,支持大于2MB的文件拖放;
  3. 延迟优化:采用UDP协议传输键鼠事件,降低输入延迟(<10ms);
  4. GUI配置:增加ESP32-S3的Web配置页面,可视化设置屏幕分辨率、Client数量、切换逻辑。
相关推荐
SmartRadio3 小时前
基于RK3568实现多电脑KVM共享方案(HDMI采集+虚拟USB键鼠+无缝切换+剪贴板/文件共享)
运维·服务器·网络·电脑·kvm·rk3568
TheNextByte14 小时前
如何将音乐从Android手机传输到电脑 [4 种方法]
android·智能手机·电脑
天机玄正5 小时前
纯享-windows笔记本/电脑安装rocky-linux9
windows·电脑
xcvadfun3945 小时前
笔记本电脑闪屏,笔记本电脑闪来闪去
电脑
Name_NaN_None5 小时前
电脑鼠标失灵/没有鼠标怎么办?——「应急方法」
电脑·远程工作
触想工业平板电脑一体机6 小时前
【触想智能】工业电脑一体机死机的原因以及解决办法分析
网络·电脑
SmartRadio6 小时前
基于泰山派PiKVM的多电脑KVM共享方案(HDMI采集+虚拟USB键鼠+无缝切换+剪贴板/文件共享)
网络·lora·计算机外设·电脑
开开心心就好6 小时前
视频伪装软件,.vsec格式批量伪装播放专用
java·linux·开发语言·网络·python·电脑·php
Name_NaN_None6 小时前
电脑没有键盘或完全失灵,怎么输入控制电脑?-「应急方案」
电脑