基于泰山派PiKVM的多电脑KVM共享方案(HDMI采集+虚拟USB键鼠+无缝切换+剪贴板/文件共享)
泰山派(全志T113-S3)是适配PiKVM的高性价比嵌入式开发板,具备硬件HDMI IN采集(TC358743芯片)、USB OTG(Host/Device双模)、HDMI OUT、千兆网口等核心特性,完美适配KVM场景。本方案基于泰山派实现多台电脑共享一套物理键鼠、HDMI视频低延迟预览、无缝屏幕切换、剪贴板双向同步、文件拖放功能,完全替代传统KVM切换器,且具备软件灵活扩展优势。
一、系统整体架构
USB Host
HDMI OUT
HDMI IN(TC358743)
HDMI IN(扩展)
HDMI IN(扩展)
USB Device(虚拟键鼠)
USB Device(虚拟键鼠)
USB Device(虚拟键鼠)
TCP/IP
TCP/IP
TCP/IP
鼠标边界检测
激活设备+切换采集源
剪贴板同步
剪贴板同步
剪贴板同步
文件拖放
文件拖放
文件拖放
物理键鼠
泰山派PiKVM
HDMI显示器
电脑1
电脑2
电脑3
电脑1客户端
电脑2客户端
电脑3客户端
无缝切换逻辑
C/D/E
核心模块说明
| 模块 | 功能 | 技术栈 |
|---|---|---|
| 泰山派硬件层 | 1. USB Host捕获物理键鼠;2. HDMI IN采集多电脑视频;3. USB Gadget模拟键鼠;4. HDMI OUT预览 | V4L2(HDMI采集)+ USB Gadget(HID) |
| 视频采集模块 | 硬件级HDMI采集(<30ms延迟),H.264编码,HDMI OUT实时预览 | V4L2 + FFmpeg + 全志硬件编码 |
| 键鼠处理模块 | 1. libinput捕获物理键鼠事件;2. USB Gadget模拟虚拟键鼠;3. 坐标边界检测 | libinput + USB HID Gadget |
| 无缝切换模块 | 鼠标坐标边界触发,自动切换激活电脑、HDMI采集源、虚拟键鼠目标 | 坐标映射 + 设备状态管理 |
| 数据共享模块 | 1. 剪贴板文本/二进制同步;2. 文件分片传输+CRC32校验;3. TCP通信 | TCP/IP + 剪贴板API + 校验算法 |
| 电脑端客户端 | 轻量Python程序,同步剪贴板/文件,接收泰山派控制指令 | Python + socket + pyperclip |
二、环境准备
1. 硬件清单
- 泰山派开发板(标配HDMI IN、USB OTG、HDMI OUT)
- 物理键鼠(USB接口)
- HDMI采集扩展板(可选,泰山派原生1路HDMI IN,扩展后支持3路)
- 多台被控电脑(Windows/macOS/Linux)
- HDMI显示器(连接泰山派HDMI OUT,预览采集画面)
- USB数据线(泰山派 ↔ 被控电脑,USB Device模式)
- 千兆网线(泰山派接入局域网)
2. 系统与依赖安装(泰山派,Armbian系统)
bash
# 1. 升级系统
sudo apt update && sudo apt upgrade -y
# 2. 核心依赖安装
sudo apt install -y build-essential cmake git libssl-dev
# 视频采集依赖
sudo apt install -y libv4l-dev ffmpeg libavcodec-dev libavformat-dev libswscale-dev
# 键鼠处理依赖
sudo apt install -y libinput-dev libudev-dev
# USB Gadget依赖
sudo apt install -y libusbgx-dev
# 剪贴板依赖
sudo apt install -y xclip xsel libxcb-clipboard-dev
3. 泰山派PiKVM驱动配置
bash
# 启用HDMI IN驱动(TC358743)
sudo modprobe tc358743
sudo modprobe v4l2loopback
# 启用USB Gadget驱动
sudo modprobe libcomposite
# 验证HDMI采集设备
ls /dev/video0 # 存在则表示驱动加载成功
三、核心模块代码实现(泰山派端,C语言)
模块1:HDMI视频采集(V4L2 + FFmpeg)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
// ===================== 配置参数 =====================
#define HDMI_DEV "/dev/video0" // 泰山派HDMI IN设备节点
#define CAPTURE_WIDTH 1920 // 采集分辨率
#define CAPTURE_HEIGHT 1080
#define CAPTURE_FPS 30 // 采集帧率
#define ENCODE_BITRATE 8000000 // H.264编码码率(8Mbps)
// ===================== 全局变量 =====================
int fd_hdmi; // HDMI采集设备句柄
AVCodecContext *codec_ctx; // FFmpeg编码上下文
AVFrame *frame; // 原始视频帧
AVPacket *pkt; // 编码后数据包
struct SwsContext *sws_ctx; // 格式转换上下文
/**
* @brief 初始化HDMI采集设备(V4L2)
* @return 0成功,-1失败
*/
int init_hdmi_capture() {
// 1. 打开HDMI采集设备
fd_hdmi = open(HDMI_DEV, O_RDWR);
if (fd_hdmi < 0) {
perror("Failed to open HDMI device");
return -1;
}
// 2. 配置V4L2采集格式(泰山派TC358743原生YUYV格式)
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = CAPTURE_WIDTH;
fmt.fmt.pix.height = CAPTURE_HEIGHT;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 硬件原生格式
fmt.fmt.pix.field = V4L2_FIELD_NONE;
if (ioctl(fd_hdmi, VIDIOC_S_FMT, &fmt) < 0) {
perror("Failed to set HDMI format");
close(fd_hdmi);
return -1;
}
// 3. 配置采集帧率
struct v4l2_streamparm streamparm;
memset(&streamparm, 0, sizeof(streamparm));
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = CAPTURE_FPS;
if (ioctl(fd_hdmi, VIDIOC_S_PARM, &streamparm) < 0) {
perror("Failed to set HDMI FPS");
close(fd_hdmi);
return -1;
}
// 4. 初始化FFmpeg H.264编码(低延迟配置)
av_register_all();
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
fprintf(stderr, "H.264 encoder not found\n");
close(fd_hdmi);
return -1;
}
codec_ctx = avcodec_alloc_context3(codec);
codec_ctx->width = CAPTURE_WIDTH;
codec_ctx->height = CAPTURE_HEIGHT;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // H.264标准格式
codec_ctx->bit_rate = ENCODE_BITRATE;
codec_ctx->time_base = (AVRational){1, CAPTURE_FPS};
codec_ctx->framerate = (AVRational){CAPTURE_FPS, 1};
codec_ctx->gop_size = 10; // 关键帧间隔(降低延迟)
codec_ctx->max_b_frames = 0; // 关闭B帧,极致低延迟
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
fprintf(stderr, "Failed to open encoder\n");
avcodec_free_context(&codec_ctx);
close(fd_hdmi);
return -1;
}
// 5. 初始化帧和格式转换上下文
frame = av_frame_alloc();
frame->format = codec_ctx->pix_fmt;
frame->width = CAPTURE_WIDTH;
frame->height = CAPTURE_HEIGHT;
av_frame_get_buffer(frame, 32);
pkt = av_packet_alloc();
// YUYV转YUV420P上下文
sws_ctx = sws_getContext(
CAPTURE_WIDTH, CAPTURE_HEIGHT, AV_PIX_FMT_YUYV422,
CAPTURE_WIDTH, CAPTURE_HEIGHT, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL
);
// 启动V4L2采集流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd_hdmi, VIDIOC_STREAMON, &type);
printf("HDMI capture init success (1920x1080@%dfps, low latency)\n", CAPTURE_FPS);
return 0;
}
/**
* @brief 采集单帧HDMI数据并编码为H.264
* @param out_buf 输出缓冲区(存储H.264数据)
* @param buf_len 缓冲区长度
* @return 编码后数据长度,-1失败
*/
int capture_hdmi_frame(uint8_t *out_buf, int buf_len) {
// 1. 出队V4L2缓冲区
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd_hdmi, VIDIOC_DQBUF, &buf) < 0) {
perror("Failed to dequeue buffer");
return -1;
}
// 2. YUYV转YUV420P(FFmpeg编码要求)
uint8_t *src_data[] = {(uint8_t *)buf.m.userptr};
int src_linesize[] = {CAPTURE_WIDTH * 2}; // YUYV每行字节数
sws_scale(sws_ctx, src_data, src_linesize, 0, CAPTURE_HEIGHT,
frame->data, frame->linesize);
// 3. FFmpeg编码
frame->pts = av_rescale_q(buf.index, (AVRational){1, CAPTURE_FPS}, codec_ctx->time_base);
int ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0) {
fprintf(stderr, "Failed to send frame to encoder\n");
ioctl(fd_hdmi, VIDIOC_QBUF, &buf);
return -1;
}
ret = avcodec_receive_packet(codec_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
ioctl(fd_hdmi, VIDIOC_QBUF, &buf);
return 0;
} else if (ret < 0) {
fprintf(stderr, "Failed to receive encoded packet\n");
ioctl(fd_hdmi, VIDIOC_QBUF, &buf);
return -1;
}
// 4. 复制编码后数据到输出缓冲区
int copy_len = pkt->size < buf_len ? pkt->size : buf_len;
memcpy(out_buf, pkt->data, copy_len);
// 5. 重新入队缓冲区
ioctl(fd_hdmi, VIDIOC_QBUF, &buf);
av_packet_unref(pkt);
return copy_len;
}
/**
* @brief 关闭HDMI采集设备,释放资源
*/
void close_hdmi_capture() {
// 停止采集流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd_hdmi, VIDIOC_STREAMOFF, &type);
// 释放FFmpeg资源
sws_freeContext(sws_ctx);
av_packet_free(&pkt);
av_frame_free(&frame);
avcodec_free_context(&codec_ctx);
close(fd_hdmi);
printf("HDMI capture closed\n");
}
模块2:物理键鼠捕获(libinput)
c
#include <libinput.h>
#include <libudev.h>
#include <pthread.h>
#include <unistd.h>
// ===================== 配置参数 =====================
#define MAX_DEVICES 3 // 最大支持3台被控电脑
#define SCREEN_WIDTH 1920 // 被控电脑屏幕宽度
#define SCREEN_HEIGHT 1080 // 被控电脑屏幕高度
// ===================== 全局变量 =====================
struct libinput *li; // libinput上下文
pthread_t input_thread; // 键鼠捕获线程
int running = 1; // 线程运行标记
// 鼠标状态(累计坐标,用于边界检测)
int mouse_x = SCREEN_WIDTH / 2, mouse_y = SCREEN_HEIGHT / 2;
// 当前激活的被控电脑索引(0=电脑1,1=电脑2,2=电脑3)
int active_device = 0;
/**
* @brief 鼠标边界检测,实现无缝切换
*/
void check_mouse_boundary() {
if (MAX_DEVICES <= 1) return; // 仅1台设备无需切换
// 右边界:切换到下一台设备
if (mouse_x >= SCREEN_WIDTH) {
active_device = (active_device + 1) % MAX_DEVICES;
mouse_x = 0; // 重置坐标到新设备左边界
printf("Switch to device %d (right boundary)\n", active_device + 1);
// 切换HDMI采集源和虚拟键鼠目标
switch_hdmi_source(active_device);
switch_usb_gadget_target(active_device);
}
// 左边界:切换到上一台设备
else if (mouse_x <= 0) {
active_device = (active_device - 1 + MAX_DEVICES) % MAX_DEVICES;
mouse_x = SCREEN_WIDTH - 1;
printf("Switch to device %d (left boundary)\n", active_device + 1);
switch_hdmi_source(active_device);
switch_usb_gadget_target(active_device);
}
// 上下边界(可选,垂直切换)
else if (mouse_y >= SCREEN_HEIGHT) {
mouse_y = 0;
} else if (mouse_y <= 0) {
mouse_y = SCREEN_HEIGHT - 1;
}
}
/**
* @brief libinput事件处理函数(线程主函数)
* @param arg libinput上下文
* @return NULL
*/
void *handle_input_events(void *arg) {
struct libinput *li = (struct libinput *)arg;
struct libinput_event *event;
while (running) {
libinput_dispatch(li); // 分发输入事件
while ((event = libinput_get_event(li))) {
struct libinput_event_type type = libinput_event_get_type(event);
// 处理鼠标移动事件
if (type == LIBINPUT_EVENT_POINTER_MOTION) {
struct libinput_event_pointer *ptr_evt = libinput_event_get_pointer_event(event);
// 更新鼠标累计坐标
mouse_x += libinput_event_pointer_get_dx(ptr_evt);
mouse_y += libinput_event_pointer_get_dy(ptr_evt);
// 边界检测
check_mouse_boundary();
// 发送鼠标移动事件到虚拟键鼠
send_mouse_event(mouse_x, mouse_y, 0);
}
// 处理鼠标按键事件
else if (type == LIBINPUT_EVENT_POINTER_BUTTON) {
struct libinput_event_pointer *ptr_evt = libinput_event_get_pointer_event(event);
uint32_t btn = libinput_event_pointer_get_button(ptr_evt);
enum libinput_button_state state = libinput_event_pointer_get_button_state(ptr_evt);
// 发送鼠标按键事件
send_mouse_button_event(btn, state);
}
// 处理键盘事件
else if (type == LIBINPUT_EVENT_KEYBOARD_KEY) {
struct libinput_event_keyboard *kbd_evt = libinput_event_get_keyboard_event(event);
uint32_t key = libinput_event_keyboard_get_key(kbd_evt);
enum libinput_key_state state = libinput_event_keyboard_get_key_state(kbd_evt);
// 发送键盘事件
send_keyboard_event(key, state);
}
libinput_event_destroy(event);
}
usleep(1000); // 降低CPU占用
}
return NULL;
}
/**
* @brief 初始化libinput,捕获物理键鼠事件
* @return 0成功,-1失败
*/
int init_libinput() {
struct udev *udev = udev_new();
if (!udev) {
fprintf(stderr, "Failed to create udev context\n");
return -1;
}
// 创建libinput上下文
li = libinput_udev_create_context(NULL, NULL, udev);
if (!li) {
fprintf(stderr, "Failed to create libinput context\n");
udev_unref(udev);
return -1;
}
// 分配输入座位(seat0为默认座位)
if (libinput_udev_assign_seat(li, "seat0") < 0) {
fprintf(stderr, "Failed to assign seat to libinput\n");
libinput_unref(li);
udev_unref(udev);
return -1;
}
// 启动键鼠捕获线程
if (pthread_create(&input_thread, NULL, handle_input_events, li) != 0) {
fprintf(stderr, "Failed to create input thread\n");
libinput_unref(li);
udev_unref(udev);
return -1;
}
printf("Libinput init success (capture physical keyboard/mouse)\n");
return 0;
}
/**
* @brief 关闭libinput,释放资源
*/
void close_libinput() {
running = 0;
pthread_join(input_thread, NULL);
libinput_unref(li);
udev_unref(udev);
printf("Libinput closed\n");
}
模块3:虚拟USB键鼠(USB Gadget)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/usb/gadget.h>
#include <linux/hid.h>
// ===================== 配置参数 =====================
#define USB_GADGET_DIR "/sys/kernel/config/usb_gadget/tsp_kvm"
#define HID_DEV "/dev/hidg0" // 虚拟HID设备节点
#define VID 0x1d6b // Linux Vendor ID
#define PID 0x0104 // HID Product ID
// ===================== 全局变量 =====================
int fd_hid; // 虚拟HID设备句柄
// HID报告描述符(键鼠复合设备,兼容标准USB HID)
uint8_t hid_report_desc[] = {
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)
0x29, 0xE7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x08, // REPORT_COUNT (8)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad)
0x19, 0x00, // USAGE_MINIMUM (Reserved)
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data,Array)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xA1, 0x02, // COLLECTION (Logical)
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)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x05, // REPORT_SIZE (5)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7F, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x06, // INPUT (Data,Var,Rel)
0xC0, // END_COLLECTION
0xC0, // END_COLLECTION
0xC0 // END_COLLECTION
};
/**
* @brief 初始化USB Gadget(虚拟HID键鼠)
* @return 0成功,-1失败
*/
int init_usb_gadget() {
// 1. 创建USB Gadget目录
system("mkdir -p " USB_GADGET_DIR);
// 2. 设置Vendor/Product ID
char cmd[256];
snprintf(cmd, sizeof(cmd), "echo %x > " USB_GADGET_DIR "/idVendor", VID);
system(cmd);
snprintf(cmd, sizeof(cmd), "echo %x > " USB_GADGET_DIR "/idProduct", PID);
system(cmd);
// 3. 设置设备版本和USB版本
system("echo 0x0100 > " USB_GADGET_DIR "/bcdDevice");
system("echo 0x0200 > " USB_GADGET_DIR "/bcdUSB");
// 4. 创建语言配置(English)
system("mkdir -p " USB_GADGET_DIR "/strings/0x409");
system("echo 'TSP-PiKVM' > " USB_GADGET_DIR "/strings/0x409/manufacturer");
system("echo 'Virtual HID Device' > " USB_GADGET_DIR "/strings/0x409/product");
// 5. 创建配置目录
system("mkdir -p " USB_GADGET_DIR "/configs/c.1/strings/0x409");
system("echo 'Config 1' > " USB_GADGET_DIR "/configs/c.1/strings/0x409/configuration");
system("echo 120 > " USB_GADGET_DIR "/configs/c.1/MaxPower");
// 6. 创建HID功能并写入报告描述符
system("mkdir -p " USB_GADGET_DIR "/functions/hid.usb0");
int fd_desc = open(USB_GADGET_DIR "/functions/hid.usb0/report_desc", O_WRONLY);
if (fd_desc < 0) {
perror("Failed to open HID report desc");
return -1;
}
write(fd_desc, hid_report_desc, sizeof(hid_report_desc));
close(fd_desc);
// 设置HID报告长度(键鼠复合报告长度为8+4=12)
system("echo 12 > " USB_GADGET_DIR "/functions/hid.usb0/report_length");
// 7. 绑定HID功能到配置
system("ln -s " USB_GADGET_DIR "/functions/hid.usb0 " USB_GADGET_DIR "/configs/c.1/");
// 8. 启用USB Gadget(泰山派USB OTG控制器为dwc2)
system("echo dwc2 > " USB_GADGET_DIR "/UDC");
// 9. 打开虚拟HID设备
fd_hid = open(HID_DEV, O_WRONLY);
if (fd_hid < 0) {
perror("Failed to open HID gadget device");
return -1;
}
printf("USB Gadget init success (virtual HID keyboard/mouse)\n");
return 0;
}
/**
* @brief 发送鼠标移动事件到虚拟HID设备
* @param x X坐标(相对值)
* @param y Y坐标(相对值)
* @param buttons 鼠标按键状态(1=左键,2=右键,4=中键)
*/
void send_mouse_event(int x, int y, uint8_t buttons) {
uint8_t report[4] = {0};
// 按键状态
report[0] = buttons & 0x07;
// X偏移(限制在-127~127)
report[1] = (x > 127) ? 127 : (x < -127) ? -127 : x;
// Y偏移(限制在-127~127)
report[2] = (y > 127) ? 127 : (y < -127) ? -127 : y;
write(fd_hid, report, sizeof(report));
}
/**
* @brief 发送鼠标按键事件
* @param btn 按键(1=左键,2=右键,3=中键)
* @param state 按键状态(LIBINPUT_BUTTON_STATE_PRESSED/RELEASED)
*/
void send_mouse_button_event(uint32_t btn, enum libinput_button_state state) {
uint8_t buttons = 0;
if (state == LIBINPUT_BUTTON_STATE_PRESSED) {
switch (btn) {
case BTN_LEFT: buttons = 1; break;
case BTN_RIGHT: buttons = 2; break;
case BTN_MIDDLE: buttons = 4; break;
}
}
send_mouse_event(0, 0, buttons);
}
/**
* @brief 转换Linux输入码到HID键盘码(简化版,可扩展)
* @param linux_key Linux input key code
* @return HID key code
*/
uint8_t linux_to_hid_key(uint32_t linux_key) {
// 核心按键映射(可扩展全键盘)
switch (linux_key) {
case KEY_A: return 4;
case KEY_B: return 5;
case KEY_C: return 6;
case KEY_1: return 30;
case KEY_ENTER: return 40;
case KEY_SPACE: return 44;
case KEY_LEFTCTRL: return 0xE0;
case KEY_LEFTSHIFT: return 0xE1;
default: return 0;
}
}
/**
* @brief 发送键盘事件到虚拟HID设备
* @param key Linux输入码
* @param state 按键状态(LIBINPUT_KEY_STATE_PRESSED/RELEASED)
*/
void send_keyboard_event(uint32_t key, enum libinput_key_state state) {
uint8_t report[8] = {0};
// 修饰键(简化版)
if (key == KEY_LEFTCTRL) report[0] = state == LIBINPUT_KEY_STATE_PRESSED ? 0x01 : 0x00;
else if (key == KEY_LEFTSHIFT) report[0] = state == LIBINPUT_KEY_STATE_PRESSED ? 0x02 : 0x00;
// 普通按键
report[2] = state == LIBINPUT_KEY_STATE_PRESSED ? linux_to_hid_key(key) : 0;
write(fd_hid, report, sizeof(report));
}
/**
* @brief 切换USB Gadget目标设备(多设备扩展)
* @param dev_idx 设备索引
*/
void switch_usb_gadget_target(int dev_idx) {
// 泰山派多USB口扩展时,切换HID设备节点(示例逻辑)
close(fd_hid);
char hid_dev[32];
snprintf(hid_dev, sizeof(hid_dev), "/dev/hidg%d", dev_idx);
fd_hid = open(hid_dev, O_WRONLY);
if (fd_hid < 0) {
perror("Failed to switch HID target");
fd_hid = open(HID_DEV, O_WRONLY); // 回退到默认设备
}
}
/**
* @brief 关闭USB Gadget
*/
void close_usb_gadget() {
close(fd_hid);
system("echo '' > " USB_GADGET_DIR "/UDC");
system("rm -rf " USB_GADGET_DIR);
printf("USB Gadget closed\n");
}
模块4:剪贴板与文件共享(TCP/IP)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <zlib.h>
// ===================== 配置参数 =====================
#define TCP_PORT 8888 // 剪贴板/文件传输端口
#define MAX_FILE_SIZE 10*1024*1024 // 最大支持10MB文件
#define CRC32_POLY 0xEDB88320L // CRC32校验多项式
// ===================== 全局变量 =====================
int tcp_sock; // TCP服务器句柄
pthread_t tcp_thread; // TCP通信线程
char clipboard_data[1024*1024] = {0}; // 全局剪贴板缓冲区
/**
* @brief 计算数据CRC32校验和
* @param data 数据缓冲区
* @param len 数据长度
* @return CRC32校验和
*/
uint32_t crc32_checksum(uint8_t *data, int len) {
uint32_t crc = 0xFFFFFFFF;
for (int i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
crc = (crc >> 1) ^ ((crc & 1) ? CRC32_POLY : 0);
}
}
return ~crc;
}
/**
* @brief 处理客户端剪贴板数据
* @param client_sock 客户端套接字
*/
void handle_clipboard(int client_sock) {
// 1. 读取剪贴板数据长度
uint32_t clip_len;
read(client_sock, &clip_len, sizeof(clip_len));
clip_len = ntohl(clip_len);
if (clip_len > sizeof(clipboard_data)) {
fprintf(stderr, "Clipboard data too large\n");
return;
}
// 2. 读取剪贴板数据
read(client_sock, clipboard_data, clip_len);
clipboard_data[clip_len] = '\0';
printf("Received clipboard: %s (len: %d)\n", clipboard_data, clip_len);
// 3. 同步剪贴板到激活设备(示例:写入目标设备剪贴板)
sync_clipboard_to_device(active_device, clipboard_data, clip_len);
}
/**
* @brief 处理客户端文件传输
* @param client_sock 客户端套接字
*/
void handle_file_transfer(int client_sock) {
// 1. 读取文件元数据
uint32_t fname_len, file_len;
read(client_sock, &fname_len, sizeof(fname_len));
read(client_sock, &file_len, sizeof(file_len));
fname_len = ntohl(fname_len);
file_len = ntohl(file_len);
if (file_len > MAX_FILE_SIZE) {
fprintf(stderr, "File too large (max %dMB)\n", MAX_FILE_SIZE/1024/1024);
return;
}
// 2. 读取文件名
char fname[256] = {0};
read(client_sock, fname, fname_len);
// 3. 读取文件数据
uint8_t *file_data = (uint8_t *)malloc(file_len);
read(client_sock, file_data, file_len);
// 4. 读取校验和并验证
uint32_t recv_crc, calc_crc;
read(client_sock, &recv_crc, sizeof(recv_crc));
recv_crc = ntohl(recv_crc);
calc_crc = crc32_checksum(file_data, file_len);
if (calc_crc != recv_crc) {
fprintf(stderr, "File CRC check failed (recv: %x, calc: %x)\n", recv_crc, calc_crc);
free(file_data);
return;
}
printf("Received file: %s (size: %d bytes, CRC: %x)\n", fname, file_len, calc_crc);
// 5. 发送文件到激活设备
send_file_to_device(active_device, fname, file_data, file_len);
free(file_data);
}
/**
* @brief TCP客户端处理线程
* @param arg 无
* @return NULL
*/
void *tcp_server_thread(void *arg) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
while (running) {
// 接受客户端连接
int client_sock = accept(tcp_sock, (struct sockaddr *)&client_addr, &client_len);
if (client_sock < 0) {
perror("Failed to accept client");
continue;
}
printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 读取数据类型(1=剪贴板,2=文件)
uint8_t data_type;
read(client_sock, &data_type, 1);
switch (data_type) {
case 1: handle_clipboard(client_sock); break;
case 2: handle_file_transfer(client_sock); break;
default: fprintf(stderr, "Unknown data type: %d\n", data_type);
}
close(client_sock);
}
return NULL;
}
/**
* @brief 初始化TCP服务器(剪贴板/文件传输)
* @return 0成功,-1失败
*/
int init_tcp_server() {
struct sockaddr_in server_addr;
// 1. 创建TCP套接字
tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock < 0) {
perror("Failed to create TCP socket");
return -1;
}
// 2. 设置端口复用
int opt = 1;
setsockopt(tcp_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(TCP_PORT);
if (bind(tcp_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Failed to bind TCP port");
close(tcp_sock);
return -1;
}
// 4. 监听连接
if (listen(tcp_sock, MAX_DEVICES) < 0) {
perror("Failed to listen TCP port");
close(tcp_sock);
return -1;
}
// 5. 启动TCP线程
if (pthread_create(&tcp_thread, NULL, tcp_server_thread, NULL) != 0) {
perror("Failed to create TCP thread");
close(tcp_sock);
return -1;
}
printf("TCP server init success (port: %d)\n", TCP_PORT);
return 0;
}
/**
* @brief 关闭TCP服务器
*/
void close_tcp_server() {
running = 0;
pthread_join(tcp_thread, NULL);
close(tcp_sock);
printf("TCP server closed\n");
}
模块5:主函数与系统控制
c
/**
* @brief 切换HDMI采集源(泰山派多路HDMI IN扩展)
* @param dev_idx 设备索引(0-2)
*/
void switch_hdmi_source(int dev_idx) {
// 泰山派HDMI IN扩展板控制(示例:通过GPIO切换)
char cmd[256];
snprintf(cmd, sizeof(cmd), "gpio write %d 1", dev_idx); // 激活对应GPIO
system(cmd);
// 延迟确保切换完成
usleep(100000);
}
/**
* @brief 同步剪贴板到目标设备
* @param dev_idx 设备索引
* @param data 剪贴板数据
* @param len 数据长度
*/
void sync_clipboard_to_device(int dev_idx, char *data, int len) {
// 示例:通过TCP发送到目标设备客户端
// 实际实现需根据设备系统适配(Windows/macOS/Linux)
printf("Sync clipboard to device %d: %s\n", dev_idx+1, data);
}
/**
* @brief 发送文件到目标设备
* @param dev_idx 设备索引
* @param fname 文件名
* @param data 文件数据
* @param len 文件长度
*/
void send_file_to_device(int dev_idx, char *fname, uint8_t *data, int len) {
// 示例:写入目标设备临时目录
char file_path[256];
snprintf(file_path, sizeof(file_path), "/tmp/kvm_%s", fname);
int fd = open(file_path, O_WRONLY | O_CREAT, 0644);
write(fd, data, len);
close(fd);
printf("File saved to device %d: %s\n", dev_idx+1, file_path);
}
/**
* @brief 主函数:初始化所有模块并启动KVM服务
*/
int main() {
printf("Starting TSP-PiKVM Server...\n");
// 1. 初始化HDMI采集
if (init_hdmi_capture() < 0) {
fprintf(stderr, "HDMI capture init failed\n");
return -1;
}
// 2. 初始化USB Gadget(虚拟键鼠)
if (init_usb_gadget() < 0) {
fprintf(stderr, "USB Gadget init failed\n");
close_hdmi_capture();
return -1;
}
// 3. 初始化libinput(物理键鼠捕获)
if (init_libinput() < 0) {
fprintf(stderr, "Libinput init failed\n");
close_usb_gadget();
close_hdmi_capture();
return -1;
}
// 4. 初始化TCP服务器(剪贴板/文件)
if (init_tcp_server() < 0) {
fprintf(stderr, "TCP server init failed\n");
close_libinput();
close_usb_gadget();
close_hdmi_capture();
return -1;
}
printf("TSP-PiKVM Server started successfully!\n");
printf("Active device: %d, HDMI preview: 1920x1080@30fps\n", active_device+1);
// 主循环:保持服务运行
while (running) {
// 采集HDMI帧并预览(输出到HDMI OUT)
uint8_t frame_buf[1024*1024];
int len = capture_hdmi_frame(frame_buf, sizeof(frame_buf));
if (len < 0) {
fprintf(stderr, "HDMI capture failed, retrying...\n");
usleep(100000);
continue;
}
usleep(33000); // 30fps间隔(≈33ms)
}
// 释放资源
close_tcp_server();
close_libinput();
close_usb_gadget();
close_hdmi_capture();
printf("TSP-PiKVM Server stopped\n");
return 0;
}
四、电脑端客户端实现(Python)
python
import socket
import struct
import crc32c
import pyperclip
import os
# ===================== 配置参数 =====================
TSP_PIKVM_IP = "192.168.1.100" # 泰山派局域网IP
TCP_PORT = 8888 # 对应泰山派TCP端口
class KVMClient:
def __init__(self):
"""初始化客户端,连接泰山派PiKVM"""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect()
def connect(self):
"""连接泰山派TCP服务器"""
try:
self.sock.connect((TSP_PIKVM_IP, TCP_PORT))
print(f"Connected to TSP-PiKVM: {TSP_PIKVM_IP}:{TCP_PORT}")
except Exception as e:
print(f"Connection failed: {e}")
raise
def send_clipboard(self):
"""发送本地剪贴板数据到泰山派"""
# 1. 获取剪贴板数据
clip_data = pyperclip.paste().encode("utf-8")
clip_len = len(clip_data)
if clip_len == 0:
print("Clipboard is empty")
return
# 2. 发送数据类型(1=剪贴板)
self.sock.sendall(struct.pack("B", 1))
# 3. 发送数据长度(网络字节序)
self.sock.sendall(struct.pack("!I", clip_len))
# 4. 发送剪贴板数据
self.sock.sendall(clip_data)
print(f"Sent clipboard: {clip_data.decode('utf-8')} (len: {clip_len})")
def send_file(self, file_path):
"""发送文件到泰山派"""
# 1. 检查文件是否存在
if not os.path.exists(file_path):
print(f"File not found: {file_path}")
return
# 2. 读取文件信息
fname = os.path.basename(file_path).encode("utf-8")
fname_len = len(fname)
with open(file_path, "rb") as f:
file_data = f.read()
file_len = len(file_data)
# 3. 计算CRC32校验和
crc = crc32c.crc32(file_data)
# 4. 发送数据类型(2=文件)
self.sock.sendall(struct.pack("B", 2))
# 5. 发送文件名长度、文件长度
self.sock.sendall(struct.pack("!II", fname_len, file_len))
# 6. 发送文件名、文件数据、校验和
self.sock.sendall(fname)
self.sock.sendall(file_data)
self.sock.sendall(struct.pack("!I", crc))
print(f"Sent file: {os.path.basename(file_path)} (size: {file_len} bytes, CRC: {hex(crc)})")
def close(self):
"""关闭连接"""
self.sock.close()
print("Connection closed")
# ===================== 测试示例 =====================
if __name__ == "__main__":
try:
client = KVMClient()
# 发送剪贴板
client.send_clipboard()
# 发送文件(替换为实际文件路径)
client.send_file("test.txt")
client.close()
except Exception as e:
print(f"Client error: {e}")
五、关键函数说明与测试步骤
1. 核心函数功能表
| 函数名 | 功能说明 |
|---|---|
init_hdmi_capture() |
初始化泰山派HDMI IN采集(TC358743),配置V4L2和FFmpeg低延迟编码,实现30fps采集 |
capture_hdmi_frame() |
采集单帧HDMI数据,转换格式并编码为H.264,用于HDMI OUT预览 |
init_libinput() |
初始化libinput捕获物理键鼠,启动独立线程处理输入事件 |
check_mouse_boundary() |
检测鼠标坐标边界,自动切换激活设备、HDMI采集源、虚拟键鼠目标 |
init_usb_gadget() |
配置泰山派USB Gadget为HID模式,模拟虚拟键鼠,向被控电脑发送输入事件 |
send_mouse/keyboard_event() |
将物理键鼠事件转换为HID报告,发送到虚拟USB设备 |
init_tcp_server() |
启动TCP服务器,处理剪贴板/文件传输,支持10MB以内文件拖放 |
handle_clipboard() |
接收客户端剪贴板数据,同步到当前激活的被控电脑 |
handle_file_transfer() |
接收文件数据,CRC32校验后转发到激活设备,保证数据完整性 |
switch_hdmi_source() |
切换泰山派HDMI IN采集源,匹配当前激活的被控电脑 |
2. 测试步骤
(1)硬件连接
- 泰山派连接物理键鼠(USB Host)、HDMI显示器(HDMI OUT);
- 被控电脑通过HDMI线连接泰山派HDMI IN(扩展板),通过USB线连接泰山派USB OTG(Device模式);
- 所有设备接入同一局域网,记录泰山派IP(如192.168.1.100)。
(2)编译与运行(泰山派)
bash
# 编译C代码(链接所有依赖库)
gcc -o tsp_pikvm main.c -linput -lavcodec -lavformat -lavutil -lswscale -lpthread -lusbgx -lz
# 运行KVM服务
sudo ./tsp_pikvm
(3)功能测试
- 键鼠共享:操作物理键鼠,验证激活电脑同步响应(鼠标移动、按键、键盘输入);
- 无缝切换:将鼠标移至屏幕右边界,验证自动切换到下一台电脑,HDMI预览同步切换;
- 剪贴板同步:在电脑1复制文本,切换到电脑2粘贴,验证文本同步;
- 文件传输:运行Python客户端发送文件,验证目标电脑/tmp目录下生成完整文件;
- 低延迟验证:观察HDMI预览画面延迟<30ms,无卡顿、无花屏。
六、总结
核心关键点回顾
- 泰山派硬件适配:充分利用全志T113-S3的HDMI IN(TC358743)、USB OTG特性,实现硬件级低延迟采集和虚拟键鼠,性能远超纯软件方案;
- 无缝切换核心:基于鼠标坐标边界检测,结合HDMI采集源、虚拟键鼠目标切换,实现无感知跨设备控制;
- 数据共享能力:TCP通信+CRC32校验实现剪贴板双向同步、文件可靠传输,支持10MB以内小文件拖放;
- 部署优势:泰山派体积小、功耗低(<5W),无需外接电源即可运行,适配桌面/机柜等场景。
优化方向
- 全键盘映射 :扩展
linux_to_hid_key()函数,支持完整键盘HID码(如功能键、特殊字符); - 大文件传输:实现文件分片+断点续传,支持>10MB文件;
- Web管理界面:基于FastAPI搭建泰山派Web界面,可视化配置设备、分辨率、切换逻辑;
- 远程访问:集成FRP内网穿透,支持公网远程控制被控电脑。