基于RK3568实现多电脑KVM共享方案(HDMI采集+虚拟USB键鼠+无缝切换+剪贴板/文件共享)

基于RK3568实现多电脑KVM共享方案(HDMI采集+虚拟USB键鼠+无缝切换+剪贴板/文件共享)

RK3568是高性能四核ARM Cortex-A55嵌入式处理器,具备硬件HDMI IN采集、多USB接口(支持Host/Device模式)、千兆网口等特性,非常适合作为KVM核心节点。本方案以RK3568为核心,实现多台电脑共享一套物理键鼠HDMI视频采集预览无缝屏幕切换剪贴板双向同步文件拖放等功能,替代传统KVM切换器,兼具软件灵活性与硬件低延迟优势。

一、系统整体架构

USB Host
HDMI OUT
HDMI IN
HDMI IN
HDMI IN
USB Device(虚拟键鼠)
USB Device(虚拟键鼠)
USB Device(虚拟键鼠)
TCP/IP
TCP/IP
TCP/IP
边界检测
激活对应设备
剪贴板同步
剪贴板同步
剪贴板同步
文件拖放
文件拖放
文件拖放
物理键鼠
RK3568
HDMI显示器
电脑1
电脑2
电脑3
电脑1客户端
电脑2客户端
电脑3客户端
无缝切换逻辑
C/D/E

核心模块说明

模块 功能 技术栈
RK3568硬件层 1. USB Host捕获物理键鼠;2. HDMI IN采集多电脑视频;3. USB Device模拟键鼠;4. HDMI OUT输出预览 RK3568驱动(v4l2/USB Gadget)
视频采集模块 硬件级HDMI采集,编码为H.264流,低延迟预览(<50ms) V4L2 + FFmpeg
键鼠处理模块 1. libinput捕获物理键鼠事件;2. USB Gadget模拟虚拟键鼠;3. 坐标边界检测 libinput + USB Gadget(HID)
数据共享模块 1. 剪贴板文本/二进制同步;2. 文件分片传输+校验;3. TCP通信 Clipboard + TCP/IP + 校验算法
无缝切换模块 基于鼠标坐标边界检测,自动切换激活电脑,同步视频采集与键鼠控制权 坐标映射 + 设备状态管理
电脑端客户端 轻量程序,同步剪贴板/文件,接收RK3568的控制指令 C++/Python(跨平台)

二、环境准备

1. 硬件清单

  • RK3568开发板(需带HDMI IN/OUT、至少3路USB接口,推荐Firefly RK3568开发板)
  • 物理键鼠(USB接口)
  • HDMI分配器/采集卡(若RK3568 HDMI IN路数不足,扩展至3路)
  • 多台电脑(作为被控端,需支持USB Device连接)
  • HDMI显示器(连接RK3568 HDMI OUT,预览采集画面)
  • USB数据线(RK3568 ↔ 被控电脑)

2. 软件依赖(RK3568端,基于Buildroot/Linux)

bash 复制代码
# 1. 基础依赖
sudo apt install build-essential cmake git libssl-dev
# 2. 视频采集依赖
sudo apt install libv4l-dev ffmpeg libavcodec-dev libavformat-dev libswscale-dev
# 3. 键鼠处理依赖
sudo apt install libinput-dev libudev-dev
# 4. USB Gadget依赖
sudo apt install libusbgx-dev
# 5. 剪贴板依赖
sudo apt install libxcb-clipboard-dev libxcb-xclipboard-dev

3. 系统镜像配置(Buildroot)

  1. 下载RK3568 Buildroot源码:git clone https://github.com/friendlyarm/buildroot.git -b nanopi-r5s
  2. 配置内核选项,启用:
    • CONFIG_USB_GADGET(USB Device模式)
    • CONFIG_USB_HID_GADGET(虚拟HID键鼠)
    • CONFIG_VIDEO_V4L2(HDMI采集)
    • CONFIG_NET_TCP(TCP通信)
  3. 编译镜像并烧录至RK3568:make -j8

三、核心模块代码实现(RK3568端,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>

// HDMI采集设备路径(RK3568 HDMI IN对应节点)
#define HDMI_DEV "/dev/video0"
// 采集分辨率(与被控电脑一致)
#define WIDTH 1920
#define HEIGHT 1080
#define FPS 30

// 全局变量
int fd_hdmi;                  // HDMI采集设备句柄
AVCodecContext *codec_ctx;    // 编码上下文
AVFrame *frame;               // 原始帧
AVPacket *pkt;                // 编码包

/**
 * @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. 设置采集格式(YUYV,硬件原生格式)
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = 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 = 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 = WIDTH;
    codec_ctx->height = HEIGHT;
    codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    codec_ctx->bit_rate = 8000000; // 8Mbps,保证画质
    codec_ctx->time_base = (AVRational){1, FPS};
    codec_ctx->framerate = (AVRational){FPS, 1};
    codec_ctx->gop_size = 10; // 关键帧间隔
    codec_ctx->max_b_frames = 0; // 关闭B帧,降低延迟

    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;
    }

    // 初始化帧和数据包
    frame = av_frame_alloc();
    frame->format = codec_ctx->pix_fmt;
    frame->width = WIDTH;
    frame->height = HEIGHT;
    av_frame_get_buffer(frame, 32);

    pkt = av_packet_alloc();

    printf("HDMI capture init success (1920x1080@%dfps)\n", FPS);
    return 0;
}

/**
 * @brief 采集一帧HDMI数据并编码
 * @return 编码后的H.264数据长度,-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要求)
    struct SwsContext *sws_ctx = sws_getContext(
        WIDTH, HEIGHT, AV_PIX_FMT_YUYV422,
        WIDTH, HEIGHT, AV_PIX_FMT_YUV420P,
        SWS_BILINEAR, NULL, NULL, NULL
    );

    uint8_t *src_data[] = {(uint8_t *)buf.m.userptr};
    int src_linesize[] = {WIDTH * 2}; // YUYV每行字节数
    sws_scale(sws_ctx, src_data, src_linesize, 0, HEIGHT,
              frame->data, frame->linesize);
    sws_freeContext(sws_ctx);

    // 3. 编码为H.264
    frame->pts = av_rescale_q(buf.index, (AVRational){1, FPS}, codec_ctx->time_base);
    int ret = avcodec_send_frame(codec_ctx, frame);
    if (ret < 0) {
        fprintf(stderr, "Failed to send frame\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 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);

    // 释放资源
    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>

// 全局变量
struct libinput *li;          // libinput上下文
pthread_t input_thread;       // 键鼠捕获线程
int running = 1;              // 线程运行标记
// 鼠标坐标(累计值,用于边界检测)
int mouse_x = WIDTH / 2, mouse_y = HEIGHT / 2;
// 当前激活的被控电脑索引(0=电脑1,1=电脑2,2=电脑3)
int active_device = 0;
#define MAX_DEVICES 3

/**
 * @brief libinput事件处理回调
 * @param li libinput上下文
 * @return 0成功
 */
int handle_input_event(struct libinput *li) {
    struct libinput_event *event;
    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_event = libinput_event_get_pointer_event(event);
            // 更新鼠标坐标
            mouse_x += libinput_event_pointer_get_dx(ptr_event);
            mouse_y += libinput_event_pointer_get_dy(ptr_event);
            // 边界检测,触发无缝切换
            check_mouse_boundary();
            // 发送鼠标事件到激活的设备
            send_mouse_event(mouse_x, mouse_y, 
                             libinput_event_pointer_get_button_state(ptr_event));
        }
        // 处理鼠标按键事件
        else if (type == LIBINPUT_EVENT_POINTER_BUTTON) {
            struct libinput_event_pointer *ptr_event = libinput_event_get_pointer_event(event);
            send_mouse_button_event(libinput_event_pointer_get_button(ptr_event),
                                    libinput_event_pointer_get_button_state(ptr_event));
        }
        // 处理键盘事件
        else if (type == LIBINPUT_EVENT_KEYBOARD_KEY) {
            struct libinput_event_keyboard *kbd_event = libinput_event_get_keyboard_event(event);
            send_keyboard_event(libinput_event_keyboard_get_key(kbd_event),
                                libinput_event_keyboard_get_key_state(kbd_event));
        }

        libinput_event_destroy(event);
    }

    return 0;
}

/**
 * @brief 鼠标边界检测,实现无缝切换
 */
void check_mouse_boundary() {
    // 右边界:切换到下一个设备
    if (mouse_x >= WIDTH) {
        active_device = (active_device + 1) % MAX_DEVICES;
        mouse_x = 0; // 重置坐标到左边界
        printf("Switch to device %d\n", active_device + 1);
        // 切换HDMI采集源到新设备
        switch_hdmi_source(active_device);
    }
    // 左边界:切换到上一个设备
    else if (mouse_x <= 0) {
        active_device = (active_device - 1 + MAX_DEVICES) % MAX_DEVICES;
        mouse_x = WIDTH - 1;
        printf("Switch to device %d\n", active_device + 1);
        switch_hdmi_source(active_device);
    }
    // 上下边界(可选,支持垂直切换)
    else if (mouse_y >= HEIGHT) {
        mouse_y = 0;
    } else if (mouse_y <= 0) {
        mouse_y = HEIGHT - 1;
    }
}

/**
 * @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;
    }

    li = libinput_udev_create_context(NULL, NULL, udev);
    if (!li) {
        fprintf(stderr, "Failed to create libinput context\n");
        udev_unref(udev);
        return -1;
    }

    // 添加所有输入设备
    if (libinput_udev_assign_seat(li, "seat0") < 0) {
        fprintf(stderr, "Failed to assign seat\n");
        libinput_unref(li);
        udev_unref(udev);
        return -1;
    }

    // 启动键鼠捕获线程
    if (pthread_create(&input_thread, NULL, (void *)handle_input_event, li) != 0) {
        fprintf(stderr, "Failed to create input thread\n");
        libinput_unref(li);
        udev_unref(udev);
        return -1;
    }

    printf("Libinput init success (capture 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>

// USB Gadget设备路径
#define USB_GADGET_DEV "/dev/hidg0"
int fd_hid; // 虚拟HID设备句柄

/**
 * @brief 初始化USB Gadget(虚拟HID键鼠)
 * @return 0成功,-1失败
 */
int init_usb_gadget() {
    // 1. 配置USB Gadget为HID模式(RK3568需提前加载gadget驱动)
    system("modprobe libcomposite");
    system("mkdir -p /sys/kernel/config/usb_gadget/rk3568_kvm");
    system("echo 0x1d6b > /sys/kernel/config/usb_gadget/rk3568_kvm/idVendor"); // Linux VID
    system("echo 0x0104 > /sys/kernel/config/usb_gadget/rk3568_kvm/idProduct"); // HID PID
    system("echo 0x0100 > /sys/kernel/config/usb_gadget/rk3568_kvm/bcdDevice");
    system("echo 0x0200 > /sys/kernel/config/usb_gadget/rk3568_kvm/bcdUSB");

    // 2. 配置HID报告描述符(键鼠复合设备)
    uint8_t hid_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
    };

    // 写入HID描述符
    int fd_desc = open("/sys/kernel/config/usb_gadget/rk3568_kvm/functions/hid.usb0/report_desc", O_WRONLY);
    write(fd_desc, hid_desc, sizeof(hid_desc));
    close(fd_desc);

    // 启用USB Gadget
    system("ln -s /sys/kernel/config/usb_gadget/rk3568_kvm/functions/hid.usb0 /sys/kernel/config/usb_gadget/rk3568_kvm/configs/c.1/");
    system("echo 1 > /sys/kernel/config/usb_gadget/rk3568_kvm/UDC");

    // 打开虚拟HID设备
    fd_hid = open(USB_GADGET_DEV, O_WRONLY);
    if (fd_hid < 0) {
        perror("Failed to open HID gadget");
        return -1;
    }

    printf("USB gadget init success (virtual keyboard/mouse)\n");
    return 0;
}

/**
 * @brief 发送鼠标移动事件到虚拟HID设备
 * @param x X坐标
 * @param y Y坐标
 * @param buttons 鼠标按键状态
 */
void send_mouse_event(int x, int y, uint32_t buttons) {
    uint8_t report[4] = {0};
    // 按键状态(左键/右键/中键)
    report[0] = buttons & 0x07;
    // X偏移(相对值)
    report[1] = (x > 127) ? 127 : (x < -127) ? -127 : x;
    // Y偏移(相对值)
    report[2] = (y > 127) ? 127 : (y < -127) ? -127 : y;

    write(fd_hid, report, sizeof(report));
}

/**
 * @brief 发送键盘事件到虚拟HID设备
 * @param key 按键码(Linux input码)
 * @param state 按键状态(按下/释放)
 */
void send_keyboard_event(uint32_t key, enum libinput_key_state state) {
    uint8_t report[8] = {0};
    // 转换Linux input码到HID码(简化版,可扩展全键盘)
    uint8_t hid_key = convert_linux_to_hid(key);
    // 修饰键(Ctrl/Shift/Alt等)
    report[0] = get_modifier_key(key);
    // 按键码(最多6个)
    report[2] = (state == LIBINPUT_KEY_STATE_PRESSED) ? hid_key : 0;

    write(fd_hid, report, sizeof(report));
}

/**
 * @brief 关闭USB Gadget
 */
void close_usb_gadget() {
    close(fd_hid);
    system("echo '' > /sys/kernel/config/usb_gadget/rk3568_kvm/UDC");
    printf("USB gadget closed\n");
}

模块4:剪贴板共享与文件拖放

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>

#define TCP_PORT 8888       // 剪贴板/文件传输端口
#define MAX_FILE_SIZE 1024*1024*10 // 最大支持10MB文件
#define CHECKSUM_LEN 4      // 校验和长度

// 全局变量
int tcp_sock;               // TCP服务器句柄
pthread_t tcp_thread;       // TCP通信线程

/**
 * @brief 初始化TCP服务器,用于剪贴板/文件传输
 * @return 0成功,-1失败
 */
int init_tcp_server() {
    struct sockaddr_in server_addr;
    tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_sock < 0) {
        perror("Failed to create TCP socket");
        return -1;
    }

    // 绑定端口
    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;
    }

    // 监听连接
    if (listen(tcp_sock, MAX_DEVICES) < 0) {
        perror("Failed to listen TCP port");
        close(tcp_sock);
        return -1;
    }

    // 启动TCP线程
    if (pthread_create(&tcp_thread, NULL, handle_tcp_client, 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客户端连接(剪贴板/文件)
 * @param arg 线程参数
 * @return NULL
 */
void *handle_tcp_client(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);

        if (data_type == 1) { // 剪贴板数据
            handle_clipboard_data(client_sock);
        } else if (data_type == 2) { // 文件数据
            handle_file_data(client_sock);
        }

        close(client_sock);
    }

    return NULL;
}

/**
 * @brief 处理剪贴板数据同步
 * @param client_sock 客户端套接字
 */
void handle_clipboard_data(int client_sock) {
    // 读取剪贴板数据长度
    uint32_t clip_len;
    read(client_sock, &clip_len, sizeof(clip_len));
    clip_len = ntohl(clip_len);

    // 读取剪贴板数据
    char *clip_data = (char *)malloc(clip_len + 1);
    read(client_sock, clip_data, clip_len);
    clip_data[clip_len] = '\0';

    printf("Received clipboard: %s\n", clip_data);

    // 同步到所有激活设备
    sync_clipboard_to_device(active_device, clip_data, clip_len);

    free(clip_data);
}

/**
 * @brief 处理文件拖放数据
 * @param client_sock 客户端套接字
 */
void handle_file_data(int client_sock) {
    // 读取文件信息
    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);

    // 读取文件名
    char *fname = (char *)malloc(fname_len + 1);
    read(client_sock, fname, fname_len);
    fname[fname_len] = '\0';

    // 读取文件数据
    uint8_t *file_data = (uint8_t *)malloc(file_len);
    read(client_sock, file_data, file_len);

    // 校验和验证
    uint32_t checksum, recv_checksum;
    read(client_sock, &recv_checksum, sizeof(recv_checksum));
    checksum = calculate_checksum(file_data, file_len);
    if (checksum != ntohl(recv_checksum)) {
        fprintf(stderr, "File checksum error\n");
        free(fname);
        free(file_data);
        return;
    }

    printf("Received file: %s (size: %d bytes)\n", fname, file_len);

    // 发送文件到激活设备
    send_file_to_device(active_device, fname, fname_len, file_data, file_len);

    free(fname);
    free(file_data);
}

/**
 * @brief 计算数据校验和(CRC32)
 * @param data 数据缓冲区
 * @param len 数据长度
 * @return 校验和
 */
uint32_t calculate_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) ? 0xEDB88320 : 0);
        }
    }
    return ~crc;
}

/**
 * @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采集源(对应不同被控电脑)
 * @param dev_idx 设备索引(0-2)
 */
void switch_hdmi_source(int dev_idx) {
    // RK3568 HDMI IN多路切换(根据硬件实现,示例为sysfs控制)
    char cmd[64];
    snprintf(cmd, sizeof(cmd), "echo %d > /sys/class/video/hdmi_in/source", dev_idx);
    system(cmd);
    printf("HDMI source switched to device %d\n", dev_idx + 1);
}

/**
 * @brief 系统初始化入口
 * @return 0成功
 */
int main() {
    // 1. 初始化HDMI采集
    if (init_hdmi_capture() < 0) {
        return -1;
    }

    // 2. 初始化USB Gadget(虚拟键鼠)
    if (init_usb_gadget() < 0) {
        close_hdmi_capture();
        return -1;
    }

    // 3. 初始化libinput(物理键鼠捕获)
    if (init_libinput() < 0) {
        close_usb_gadget();
        close_hdmi_capture();
        return -1;
    }

    // 4. 初始化TCP服务器(剪贴板/文件)
    if (init_tcp_server() < 0) {
        close_libinput();
        close_usb_gadget();
        close_hdmi_capture();
        return -1;
    }

    // 主循环
    printf("RK3568 KVM server started\n");
    while (running) {
        // 采集HDMI帧并预览(输出到HDMI OUT)
        uint8_t frame_buf[1024*1024];
        int len = capture_hdmi_frame(frame_buf, sizeof(frame_buf));
        if (len > 0) {
            // 输出到HDMI OUT(省略,根据RK3568显示驱动实现)
        }
        usleep(1000); // 1ms延迟,降低CPU占用
    }

    // 释放资源
    close_tcp_server();
    close_libinput();
    close_usb_gadget();
    close_hdmi_capture();

    return 0;
}

四、电脑端客户端实现(Python轻量版)

python 复制代码
import socket
import struct
import crc32c
import pyperclip
import os

# RK3568的IP地址和端口
RK3568_IP = "192.168.1.100"
TCP_PORT = 8888

class KVMClient:
    def __init__(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect()

    def connect(self):
        """连接RK3568 TCP服务器"""
        try:
            self.sock.connect((RK3568_IP, TCP_PORT))
            print("Connected to RK3568 KVM server")
        except Exception as e:
            print(f"Connect failed: {e}")

    def send_clipboard(self):
        """发送剪贴板数据到RK3568"""
        clip_data = pyperclip.paste().encode("utf-8")
        clip_len = len(clip_data)
        
        # 发送数据类型(1=剪贴板)
        self.sock.sendall(struct.pack("B", 1))
        # 发送长度
        self.sock.sendall(struct.pack("!I", clip_len))
        # 发送数据
        self.sock.sendall(clip_data)
        print(f"Sent clipboard: {clip_data.decode('utf-8')}")

    def send_file(self, file_path):
        """发送文件到RK3568"""
        if not os.path.exists(file_path):
            print("File not found")
            return
        
        # 读取文件信息
        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)
        
        # 计算校验和
        checksum = crc32c.crc32(file_data)
        
        # 发送数据类型(2=文件)
        self.sock.sendall(struct.pack("B", 2))
        # 发送文件名长度、文件长度
        self.sock.sendall(struct.pack("!II", fname_len, file_len))
        # 发送文件名、文件数据、校验和
        self.sock.sendall(fname)
        self.sock.sendall(file_data)
        self.sock.sendall(struct.pack("!I", checksum))
        print(f"Sent file: {os.path.basename(file_path)} (size: {file_len} bytes)")

    def close(self):
        """关闭连接"""
        self.sock.close()

# 示例:发送剪贴板和文件
if __name__ == "__main__":
    client = KVMClient()
    # 发送剪贴板
    client.send_clipboard()
    # 发送文件(替换为实际路径)
    client.send_file("test.txt")
    client.close()

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

1. 核心函数功能表

函数名 功能说明
init_hdmi_capture() 初始化RK3568 HDMI IN采集,配置V4L2格式和FFmpeg编码,实现低延迟视频采集
capture_hdmi_frame() 采集单帧HDMI数据,转换为YUV420P并编码为H.264,用于预览输出
init_libinput() 初始化libinput,捕获物理键鼠事件,启动独立线程处理输入
check_mouse_boundary() 检测鼠标坐标边界,自动切换激活设备和HDMI采集源,实现无缝切换
init_usb_gadget() 配置USB Gadget为HID模式,模拟虚拟键鼠,向被控电脑发送输入事件
init_tcp_server() 启动TCP服务器,处理电脑端的剪贴板和文件数据,实现跨设备同步
handle_clipboard_data() 接收剪贴板数据,同步到当前激活的被控电脑
handle_file_data() 接收文件数据,校验后转发到激活设备,支持10MB以内文件拖放
switch_hdmi_source() 切换RK3568的HDMI IN采集源,匹配当前激活的被控电脑

2. 测试步骤

(1)硬件连接
  1. RK3568连接物理键鼠(USB Host)、HDMI显示器(HDMI OUT);
  2. 被控电脑通过HDMI线连接RK3568 HDMI IN,通过USB线连接RK3568 USB Device;
  3. 所有设备接入同一局域网,记录RK3568的IP地址。
(2)软件部署
  1. 编译RK3568端C代码:gcc -o rk3568_kvm main.c -linput -lavcodec -lavformat -lavutil -lswscale -lpthread -lusbgx
  2. 运行KVM服务:./rk3568_kvm
  3. 被控电脑运行Python客户端,连接RK3568的TCP端口。
(3)功能测试
  1. 键鼠共享:操作物理键鼠,验证激活电脑是否同步响应;
  2. 无缝切换:将鼠标移动到屏幕右边界,验证是否自动切换到下一台电脑,HDMI预览同步切换;
  3. 剪贴板同步:在一台电脑复制文本,在激活电脑粘贴,验证同步成功;
  4. 文件拖放:发送1MB以内的测试文件,验证目标电脑接收完整且校验通过;
  5. 视频预览:确认HDMI显示器的采集画面延迟<50ms,无卡顿。

六、总结

核心关键点回顾

  1. RK3568硬件优势:利用HDMI IN/OUT硬件采集、USB Host/Device双模特性,实现低延迟(<50ms)的视频采集和键鼠模拟,性能远超纯软件方案;
  2. 无缝切换核心:基于鼠标坐标边界检测,结合HDMI采集源切换,实现无感知的设备切换,替代传统KVM切换器;
  3. 数据共享能力:通过TCP实现剪贴板双向同步、文件分片传输+校验,兼顾易用性和可靠性;
  4. 灵活性扩展:支持最多3台被控电脑,可通过扩展HDMI采集卡增加路数,适配更多场景。

优化方向

  1. 延迟优化:启用RK3568硬件编码加速,将视频延迟降至20ms以内;
  2. 全键盘映射 :扩展convert_linux_to_hid()函数,支持完整的键盘HID码映射;
  3. 大文件传输:实现文件断点续传,支持超过10MB的大文件拖放;
  4. Web管理:增加RK3568的Web界面,可视化配置设备、分辨率、切换逻辑。
相关推荐
SmartRadio2 小时前
基于ESP32-S3+Barrier实现多电脑KVM共享方案(无缝切换+剪贴板/文件共享)
电脑·esp32·kvm·远程·虚拟键盘·虚拟鼠标
zhangrelay2 小时前
如何让手机电脑流畅飞起低碳节能性能拉满-软件安装篇-ESR-Extended Support Release-延长支持版-LTS
linux·运维·笔记·学习
147API2 小时前
Prompt Injection 怎么防:攻击面与工程防线(含安全 Checklist)
网络·安全·prompt
未来之窗软件服务2 小时前
服务器运维(二十五)终端安全证书管控与Nginx HTTPS 部署—东方仙盟练气期
运维·服务器·安全·仙盟创梦ide·东方仙盟
Anastasiozzzz2 小时前
Docker介绍与常见指令
运维·docker·容器
雨季6662 小时前
构建 OpenHarmony 智能场景自动化配置面板:Flutter 实现可视化规则编排
运维·flutter·自动化
信创天地2 小时前
从 “替代” 到 “超越”:信创系统架构师如何筑牢自主可控技术底座
运维·安全·系统架构·开源·dubbo·risc-v
乾元2 小时前
社交工程 2.0:生成式 AI 驱动的高拟真钓鱼与认知对抗
网络·人工智能·安全·机器学习·架构
摆摊的豆丁2 小时前
FreeRTOS-Plus-TCP 协议支持与网络编程指南
网络·网络协议·tcp/ip·freertos