基于泰山派PiKVM的多电脑KVM共享方案(HDMI采集+虚拟USB键鼠+无缝切换+剪贴板/文件共享)

基于泰山派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)硬件连接
  1. 泰山派连接物理键鼠(USB Host)、HDMI显示器(HDMI OUT);
  2. 被控电脑通过HDMI线连接泰山派HDMI IN(扩展板),通过USB线连接泰山派USB OTG(Device模式);
  3. 所有设备接入同一局域网,记录泰山派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)功能测试
  1. 键鼠共享:操作物理键鼠,验证激活电脑同步响应(鼠标移动、按键、键盘输入);
  2. 无缝切换:将鼠标移至屏幕右边界,验证自动切换到下一台电脑,HDMI预览同步切换;
  3. 剪贴板同步:在电脑1复制文本,切换到电脑2粘贴,验证文本同步;
  4. 文件传输:运行Python客户端发送文件,验证目标电脑/tmp目录下生成完整文件;
  5. 低延迟验证:观察HDMI预览画面延迟<30ms,无卡顿、无花屏。

六、总结

核心关键点回顾

  1. 泰山派硬件适配:充分利用全志T113-S3的HDMI IN(TC358743)、USB OTG特性,实现硬件级低延迟采集和虚拟键鼠,性能远超纯软件方案;
  2. 无缝切换核心:基于鼠标坐标边界检测,结合HDMI采集源、虚拟键鼠目标切换,实现无感知跨设备控制;
  3. 数据共享能力:TCP通信+CRC32校验实现剪贴板双向同步、文件可靠传输,支持10MB以内小文件拖放;
  4. 部署优势:泰山派体积小、功耗低(<5W),无需外接电源即可运行,适配桌面/机柜等场景。

优化方向

  1. 全键盘映射 :扩展linux_to_hid_key()函数,支持完整键盘HID码(如功能键、特殊字符);
  2. 大文件传输:实现文件分片+断点续传,支持>10MB文件;
  3. Web管理界面:基于FastAPI搭建泰山派Web界面,可视化配置设备、分辨率、切换逻辑;
  4. 远程访问:集成FRP内网穿透,支持公网远程控制被控电脑。
相关推荐
linux kernel2 小时前
第四部分:传输层
服务器·网络
m0_737302582 小时前
火山引擎安全增强型云服务器,筑牢AI时代数据屏障
网络·人工智能
开开心心就好2 小时前
视频伪装软件,.vsec格式批量伪装播放专用
java·linux·开发语言·网络·python·电脑·php
极安代理3 小时前
HTTP代理,什么是HTTP代理,HTTP代理的用途有哪些?
网络·网络协议·http
努力的小帅3 小时前
Linux_网络基础(1)
linux·网络·网络协议
Name_NaN_None3 小时前
电脑没有键盘或完全失灵,怎么输入控制电脑?-「应急方案」
电脑
带鱼吃猫3 小时前
网络通信:udp套接字实现echoserver和翻译功能
网络·网络协议·udp
EverydayJoy^v^3 小时前
RH134学习进程——九.访问网络附加存储
linux·网络·学习
卓应米老师3 小时前
【eNSP实验配置】点到点链路上的OSPF
网络·智能路由器·华为认证·hcia认证实验指南