Qt与Hid设备通信

news2025/5/19 12:40:17

什么是HID?

HID(Human Interface Device)是‌直接与人交互的电子设备‌,通过标准化协议实现用户与计算机或其他设备的通信,典型代表包括键盘、鼠标、游戏手柄等。‌

为什么HID要与qt进行通信?

我这里的应用场景是数位板与我自己写的上位机进行通信,用户可以在上位机软件中手动设置数位板上按键代表的快捷键。

如何通信?

1. 引入 hidapi 库

hidapi库是一个第三方库需要下载。下载完编译之后把 hidapi.h,hidapi.dll,hidapi.lib放到项目根目录下。

hid设备初始化有以下几个步骤:

2. 初始化 hidapi

hid_init()

3. 枚举和选择设备

void enumerateDevices() {
    struct hid_device_info *devs = hid_enumerate(0x0, 0x0); // 参数为VID和PID,0x0表示匹配所有
    struct hid_device_info *cur_dev = devs;

    while (cur_dev) {
        printf("Device Found\n  type: %04hx %04hx\n  path: %s\n  serial_number: %ls",
               cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number);
        printf("\n");
        printf("  Manufacturer: %ls\n", cur_dev->manufacturer_string);
        printf("  Product: %ls\n", cur_dev->product_string);
        printf("  Release: %hx\n", cur_dev->release_number);
        printf("  Interface Number: %d\n\n", cur_dev->interface_number);

        cur_dev = cur_dev->next;
    }

    hid_free_enumeration(devs);
}

4. 打开设备

hid_device *handle;
handle = hid_open(0x1234, 0x5678, NULL); // 替换为设备的VID和PID

5. 设置非阻塞模式(可选)

int res = hid_set_nonblocking(handle, 1); // 参数为1表示非阻塞模式
if (res < 0) {
    // 处理设置失败的情况
}

 6. 读取和写入数据

// 读取数据
unsigned char buf[256];
int res = hid_read(handle, buf, sizeof(buf));
if (res < 0) {
    // 处理读取错误
} else {
    // 处理读取到的数据
}

// 写入数据
unsigned char data[] = {0x00, 0x01, 0x02}; // 示例数据
res = hid_write(handle, data, sizeof(data));
if (res < 0) {
    // 处理写入错误
}

 7. 关闭设备和释放资源

hid_close(handle);
hid_exit();

示例代码整合

void MainWindow::HidInit()
{
    // 1. 初始化HIDAPI
    if (hid_init() != 0) {
        qDebug() << "[错误] HIDAPI初始化失败";
        return;
    }
    else{
        qDebug() << "[正确] HIDAPI初始化成功";
    }

    // 2. 枚举设备
    qDebug() << "[调试] 开始枚举HID设备...";
    hid_device_info *devs = hid_enumerate(0x0, 0x0);

    if (!devs) {
        qDebug() << "[错误] 无法枚举HID设备,可能没有HID设备连接";
        hid_exit();
        return;
    }

    hid_device_info *cur_dev = devs;
    char* devicePath = nullptr;
    bool deviceFound = false;
    int deviceCount = 0; // 用于统计发现的HID设备数量

    qDebug() << "[调试] 开始遍历HID设备列表...";

    while (cur_dev) {
        deviceCount++;

        // 打印所有HID设备信息,用于调试
        qDebug().nospace() << "[调试] 设备 #" << deviceCount
                           << ": VID=0x" << QString::number(cur_dev->vendor_id, 16).toUpper()
                           << ", PID=0x" << QString::number(cur_dev->product_id, 16).toUpper();
                           // << ", 路径=" << QString::fromWCharArray(cur_dev->path);

        if (cur_dev->vendor_id == TARGET_VID && cur_dev->product_id == TARGET_PID) {
            devicePath = _strdup(cur_dev->path);
            deviceFound = true;

            qDebug() << "\n[信息] 找到目标设备:";
            qDebug() << "路径:" << devicePath;
            qDebug() << "制造商:" << (cur_dev->manufacturer_string ? QString::fromWCharArray(cur_dev->manufacturer_string) : "N/A");
            qDebug() << "产品名:" << (cur_dev->product_string ? QString::fromWCharArray(cur_dev->product_string) : "N/A");
            qDebug() << "接口号:" << cur_dev->interface_number;
            break;
        }

        cur_dev = cur_dev->next;
    }

    qDebug() << "[调试] 遍历完成,共发现" << deviceCount << "个HID设备";

    hid_free_enumeration(devs);

    if (!deviceFound) {
        qDebug() << "[错误] 未找到目标设备 (VID: 0x" << QString::number(TARGET_VID, 16).toUpper()
                 << ", PID: 0x" << QString::number(TARGET_PID, 16).toUpper() << ")";
        hid_exit();
        return;
    }

    // 3. 打开设备
    qDebug() << "[调试] 尝试打开目标设备...";
    hid_device* handle = hid_open_path(devicePath);
    if (!handle) {
        qDebug() << "[错误] 无法打开设备:" << QString::fromWCharArray(hid_error(nullptr));
        free(devicePath);
        hid_exit();
        return;
    }

    // 3. 打开设备
    handle = hid_open_path(devicePath);
    if (!handle) {
        qDebug() << "[错误] 无法打开设备:" << QString::fromWCharArray(hid_error(nullptr));
        free(devicePath);
        hid_exit();
        return;
    }

    // 设置非阻塞模式
    hid_set_nonblocking(handle, 1);
    qDebug() << "\n[信息] 设备已成功打开";

    // 4. 尝试通信
    const int REPORT_SIZE = 65; // 64字节数据 + 1字节报告ID
    unsigned char buf[REPORT_SIZE] = {0};

    // 尝试不同报告ID (0x00-0xFF)
    for (int report_id = 0x00; report_id <= 0xFF; report_id++) {
        // 4.1 尝试特性报告
        buf[0] = report_id;
        buf[1] = 0x01; // 示例命令

        qDebug() << "\n[调试] 尝试报告ID: 0x" << QString::number(report_id, 16).toUpper();

        int res = hid_send_feature_report(handle, buf, REPORT_SIZE);
        if (res > 0) {
            qDebug() << "[成功] 特性报告发送成功 (ID: 0x"
                     << QString::number(report_id, 16).toUpper() << ")";
            break;
        } else if (report_id == 0xFF) {
            qDebug() << "[警告] 所有特性报告尝试失败";
        }

        // 4.2 尝试输出报告
        res = hid_write(handle, buf, REPORT_SIZE);
        if (res > 0) {
            qDebug() << "[成功] 输出报告发送成功 (ID: 0x"
                     << QString::number(report_id, 16).toUpper() << ")";
            break;
        } else if (report_id == 0xFF) {
            qDebug() << "[警告] 所有输出报告尝试失败";
        }
    }

    // 5. 读取响应 (5秒超时)
    qDebug() << "\n[信息] 等待设备响应...";
    int timeout_ms = 5000;
    QElapsedTimer timer;
    timer.start();

    while (timer.elapsed() < timeout_ms) {
        int res = hid_read(handle, buf, REPORT_SIZE);
        if (res > 0) {
            qDebug() << "[成功] 收到" << res << "字节数据:";

            // 打印接收到的数据 (十六进制格式)
            QString hexData;
            for (int i = 0; i < res; i++) {
                hexData += "0x" + QString::number(buf[i], 16).toUpper().rightJustified(2, '0') + " ";
                if ((i+1) % 8 == 0) hexData += "\n";
            }
            qDebug() << hexData;
            break;
        } else if (res == 0) {
            QThread::msleep(100); // 避免CPU占用过高
        } else {
            qDebug() << "[错误] 读取失败:" << QString::fromWCharArray(hid_error(handle));
            break;
        }
    }

    if (timer.elapsed() >= timeout_ms) {
        qDebug() << "[警告] 读取超时,未收到响应";
    }

    // 6. 清理资源
    hid_close(handle);
    free(devicePath);
    hid_exit();
    qDebug() << "\n[信息] HID通信结束";
}

 运行效果演示(我接入的是wacom数位板):

 全是0xFF为未激活状态(初始状态)。

总结操作流程

  1. 确认设备功能与协议:明确设备是输入型(主动上报)还是命令型(需指令触发)。
  2. 发送测试指令:若无文档,通过简单指令试探设备响应模式。
  3. 解析数据结构:根据响应数据的变化规律,逆向推导字节含义(如坐标、状态、校验等)。
  4. 编写业务逻辑:基于解析结果,实现数据处理或控制功能(如鼠标模拟、设备配置等)。

解析报告数据

如何解析

用以下结构来存储报告:

struct TabletData {
    quint8 reportId;        // 报告ID
    quint16 x;              // X坐标(0-最大值)
    quint16 y;              // Y坐标(0-最大值)
    quint16 pressure;       // 压力值(0-最大值)
    QList<int> buttons;     // 按下的按钮列表(按钮编号从1开始)
};

 创建一个TabletData 类型的函数:

该函数对报告进行解析,第0字节是报告ID,第1字节是按钮位置........

TabletData HidManager::parseTabletData(const QByteArray& data) {
    TabletData result;
    if (data.isEmpty()) return result;

    result.reportId = data[0];

    switch (result.reportId) {
    case 0x11: // 按钮报告(假设按钮在字节1-2)
        for (int byteIdx = 1; byteIdx < 3; byteIdx++) {
            if (byteIdx >= data.size()) break;
            unsigned char byte = data[byteIdx];
            for (int bitIdx = 0; bitIdx < 8; bitIdx++) {
                if ((byte & (1 << bitIdx)) != 0) { // 1表示按下
                    result.buttons.append((byteIdx - 1) * 8 + bitIdx + 1);
                }
            }
        }
        break;

    case 0x10: // **关键修改**:坐标/压力报告ID改为0x10
        // 解析坐标和压力(假设坐标在字节1-4,压力在字节5-6)
        if (data.size() >= 5) {
            // 小端序解析:低字节在前,高字节在后
            result.x = static_cast<quint16>(data[1]) | (static_cast<quint16>(data[2]) << 8);
            result.y = static_cast<quint16>(data[3]) | (static_cast<quint16>(data[4]) << 8);
        }
        if (data.size() >= 7) {
            result.pressure = static_cast<quint16>(data[5]) | (static_cast<quint16>(data[6]) << 8);
        }
        result.buttons.clear(); // 坐标报告不含按钮,清空列表
        break;

    default:
        qWarning() << "未知报告ID:" << QString::number(result.reportId, 16);
        break;
    }

    return result;
}

 写一个打印输出函数

void HidManager::handleHidData(const QByteArray& data)
{
    // 数据为空或与上次完全相同则直接返回
    static QByteArray lastDataFrame;
    if (data.isEmpty() || data == lastDataFrame) return;
    lastDataFrame = data;

    // 解析数据到结构体
    TabletData currentData = parseTabletData(data);

    // 打印原始数据和解析结果(调试用)
    if (debugMode) { // 可添加调试开关
        QString hexData;
        for (int i = 0; i < data.size(); i++) {
            hexData += "0x" + QString::number((unsigned char)data[i], 16).toUpper().rightJustified(2, '0') + " ";
            if ((i+1) % 8 == 0) hexData += "\n";
        }
        qDebug() << "收到新数据:" << hexData;

        qDebug() << "解析后数据:"
                 << "报告ID:" << QString::number(currentData.reportId, 16)
                 << "坐标: (" << currentData.x << ", " << currentData.y << ")"
                 << "压力:" << currentData.pressure
                 << "按钮:" << currentData.buttons;
    }

    // 静态变量存储上次数据,用于检测变化
    static TabletData lastData;

    // 检查关键数据是否变化(按钮、坐标、压力)
    bool isButtonChanged = (currentData.buttons != lastData.buttons);
    bool isPositionChanged = (currentData.x != lastData.x || currentData.y != lastData.y);
    bool isPressureChanged = (currentData.pressure != lastData.pressure);

    // 根据变化类型发送不同信号
    if (isButtonChanged) {
        emit buttonStateChanged(currentData.buttons);
    }

    if (isPositionChanged || isPressureChanged) {
        emit tabletMoved(currentData.x, currentData.y, currentData.pressure);
    }

    // 更新上次数据缓存
    lastData = currentData;
}

打印输出:

拿wacom数位板举例。以下是连接wacom数位板之后,数位笔滑动之后wacom数位板发送过来的报告:

解析内容

报告第一个字节为报告ID用来区分用户进行的是什么操作

当报告ID为0X10时代表坐标移动

当报告ID为0X11时代表按键按下

例:

按下第一个按键,此时报告ID为0x11,表示按键事件发生。此时第2个字节发生了变化,也就是第一个字节被按下了:

当用数位笔在数位板上滑动之后收到如下报告:

报告ID为0x10,表示坐标发生变化。坐标在字节1-4,压力在字节5-6:

HID设备按键与Qt界面UI按键的快捷键绑定实现

流程图

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2378473.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024 山东省ccpc省赛

目录 I&#xff08;签到&#xff09; 题目简述&#xff1a; 思路&#xff1a; 代码&#xff1a; A&#xff08;二分答案&#xff09; 题目简述&#xff1a; 思路&#xff1a; 代码&#xff1a; K&#xff08;构造&#xff09; 题目&#xff1a; 思路&#xff1a; 代…

SAP HCM 0008数据存储逻辑

0008信息类型&#xff1a;0008信息类型是存储员工基本薪酬的地方&#xff0c;因为很多企业都会都薪酬带宽&#xff0c;都会按岗定薪&#xff0c;所以在上线前为体现工资体系的标准化&#xff0c;都会在配置对应的薪酬关系&#xff0c;HCM叫间接评估&#xff0c;今天我们就分析下…

如何使用通义灵码辅助学习C++编程 - AI编程助手提升效率

一、引言 C 是一门功能强大且灵活的编程语言&#xff0c;在软件开发、系统编程、游戏开发等领域广泛应用。然而&#xff0c;其复杂的语法和丰富的特性使得学习曲线较为陡峭。对于初学者而言&#xff0c;在学习过程中难免会遇到各种问题&#xff0c;如语法理解困难、代码调试耗…

【Docker】CentOS 8.2 安装Docker教程

目录 1.卸载 2.安装依赖 3.设置yum源 4.安装Docker 5.启动Docker 6.设置Docker开机自启 7.验证Docker是否安装成功 8.配置多个国内镜像地址 9.重启Docker 10.Docker指令大全 10.1.启动与关闭Docker 10.2.Docker镜像操作 10.3.Docker容器操作 10.4.Docker Compose操作…

K230 ISP:一种新的白平衡标定方法

第一次遇见需要利用光谱响应曲线进行白平衡标定的方法。很好奇是如何利用光谱响应曲线进行白平衡标定的。 参考资料参考&#xff1a;K230 ISP图像调优指南 K230 介绍 嘉楠科技 Kendryte 系列 AIoT 芯片中的最新一代 AIoT SoC K230 芯片采用全新的多核异构单元加速计算架构&a…

桃芯ingchips——windows HID键盘例程无法同时连接两个,但是安卓手机可以的问题

目录 环境 现象 原理及解决办法 环境 PC&#xff1a;windows11 安卓&#xff1a;Android14 例程使用的是HID Keyboard&#xff0c;板子使用的是91870CQ的开发板&#xff0c;DB870CC1A 现象 连接安卓手机时并不会出现该现象&#xff0c;两个开发板都可以当做键盘给手机发按…

[Linux] vim及gcc工具

目录 一、vim 1.vim的模式 2.vim的命令集 (1):命令模式 (2):底行模式 3.vim配置 二、gcc 1.gcc格式及选项 2.工作布置 三、自动化构建工具makefile 1.基本使用方法 2.配置文件解析 3.拓展 在linux操作系统的常用工具中&#xff0c;常用vim来进行程序的编写&#xff1b…

MySQL只操作同一条记录也会死锁吗?

大家好&#xff0c;我是锋哥。今天分享关于【MySQL只操作同一条记录也会死锁吗?】面试题。希望对大家有帮助&#xff1b; MySQL里where条件的顺序影响索引使用吗&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在MySQL中&#xff0c;死锁通常发生在多…

数据结构与算法——双向链表

双向链表 定义链表分类双向链表&#xff1a;带头双向循环链表 初始化打印尾插头插尾删头删查找在pos(指定位置)之后插入结点在pos(指定位置)之前插入结点删除pos(指定位置)的结点销毁顺序表与链表的分析 定义 链表分类 单向和双向 带头和不带头 带头是指存在一个头结点&…

MODBUS RTU调试助手使用方法详解

一、软件简介 485调试助手是一款常用的串口通信调试工具&#xff0c;专门用于RS-485总线设备的测试、调试和通信监控。它支持多种串口参数设置&#xff0c;提供数据收发功能&#xff0c;是工业现场调试的必备工具之一。 二、软件安装与启动 1. 系统要求 Windows 7/10/11操作…

自由学习记录(60)

Lecture 16 Ray Tracing 4_哔哩哔哩_bilibili 老师说的“高频采样”问题是什么&#xff1f; 现在考虑一个特殊情况&#xff1a; ❗ 一个像素内&#xff0c;图像信号变化很剧烈&#xff08;高频&#xff09;&#xff1a; 比如&#xff1a; 细网格纹理 马赛克背景 很高频的…

现代计算机图形学Games101入门笔记(三)

三维变换 具体形式缩放&#xff0c;平移 特殊点旋转。这里涉及到坐标系&#xff0c;先统一定义右手坐标系&#xff0c;根据叉乘和右手螺旋判定方向。这里还能法线Ry Sina 正负与其他两个旋转不一样。这里可以用右手螺旋&#xff0c;x叉乘z&#xff0c;发现大拇指朝下&#xff0…

WeakAuras Lua Script <BiaoGe>

WeakAuras Lua Script <BiaoGe> 表格拍卖插件WA字符串 表格字符串代码&#xff1a; !WA:2!S3xA3XXXrcoE2VH9l7ZFy)C969PvDpSrRgaeuhljFlUiiSWbxaqXDx(4RDd0vtulB0fMUQMhwMZJsAO5HenLnf1LPSUT4iBrjRzSepL(pS)e2bDdWp5)cBEvzLhrMvvnAkj7zWJeO7mJ8kYiJmYiImYF0b(XR)JR9JRD…

chrome 浏览器插件 myTools, 日常小工具。

1. 起因&#xff0c; 目的: 比如&#xff0c;chatgpt, google&#xff0c; 打开网页&#xff0c;就能直接输入文字&#xff0c;然后 grok 就不行&#xff0c;必须用鼠标点一下&#xff0c;才能输入文字。 对我而言&#xff0c;是个痛点&#xff01;写个插件&#xff0c;自动点…

智慧校园(含实验室)智能化专项汇报方案

该方案聚焦智慧校园(含实验室)智能化建设,针对传统实验室在运营监管、环境监测、安全管控、排课考勤等方面的问题,依据《智慧校园总体框架》等标准,设计数字孪生平台、实验室综合管理平台、消安电一体化平台三大核心平台,涵盖通信、安防、建筑设备管理等设施,涉及 395 个…

第三十四节:特征检测与描述-SIFT/SURF 特征 (专利算法)

一、特征检测:计算机视觉的基石 在计算机视觉领域中,特征检测与描述是实现图像理解的核心技术。就像人类通过识别物体边缘、角点等特征来认知世界,算法通过检测图像中的关键特征点来实现: 图像匹配与拼接 物体识别与跟踪 三维重建 运动分析 其中,SIFT(Scale-Invariant F…

【前端优化】vue2 webpack4项目升级webpack5,大大提升运行速度

记录一下过程 手里有个老项目&#xff0c;vue2webpack4 项目很大&#xff0c;每次运行、运行都要将近10分钟 现在又要往里面写很多东西&#xff0c;再不优化&#xff0c;开发着会更难受&#xff0c;所以决定先将它升级至webpack5 最初失败的尝试 直接在项目里安装了webpack5 但…

Nginx应用场景详解与配置指南

1. 什么是Nginx&#xff1f; Nginx&#xff08;发音为"engine-x"&#xff09;是一个高性能的HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP代理服务器。它以高性能、稳定性、丰富的功能集、简单的配置和低资源消耗而闻名。 2. Nginx的主要应用场景 2.1 …

vue2 切换主题色以及单页面好使方法

今天要新增一个页面要根据不同公司切换不同页面主题色&#xff0c;一点一点来&#xff0c;怎么快速更改 el-pagination 分页组件主题色。 <el-pagination :page-size"pageSize" :pager-count"pageCount"layout"sizes, prev, pager, next, jumper,…

JavaScript【6】事件

1.概述&#xff1a; 在 JavaScript 中&#xff0c;事件&#xff08;Event&#xff09;是浏览器或 DOM&#xff08;文档对象模型&#xff09;与 JavaScript 代码之间交互的一种机制。它代表了在浏览器环境中发生的特定行为或者动作&#xff0c;比如用户点击鼠标、敲击键盘、页面…