简介:这是一份面向嵌入式开发与Python自动化测试初学者的跨平台HID设备控制脚本集,解决Linux(Ubuntu)和Windows环境下Python直接读写USB HID设备的实操难题。资源包含4个文件(3个Python脚本+1份说明文档),总大小仅4KB,轻量易集成:核心脚本分别适配Ubuntu(基于pyusb+sudo权限)与Windows(基于pywinusb),另含屏幕点击模拟示例及详细环境配置指引,覆盖Python 2.7至3.9多版本兼容性验证。已有1525人学习下载,所有脚本均经作者在Ubuntu 20.04和Windows双平台调试完毕,附带典型报错分析(如模块安装错位、权限缺失等排错思路),并明确标注各系统依赖安装方式与运行前提,可直接复用或作为HID通信二次开发的基础模板。
1. 用 Python 在 Ubuntu 和 Windows 上直接读写 HID 设备,不依赖内核驱动或管理员提权
你手头有一块 HID 协议的 USB 外设——可能是定制传感器、工业控制板、加密狗,或是带自定义报告描述符的 HID 键盘/鼠标类设备。你想跳过 C/C++ 编译、绕开 Windows 的 INF 签名限制和 Linux 的 udev 规则配置,用纯 Python 脚本在 Ubuntu 22.04+ 和 Windows 10/11 上完成:打开设备、发送 Feature Report、接收 Input Report、解析二进制数据、实时调试通信时序。这不是调用pyserial串口模拟的方案,而是真正走 USB HID 类协议栈的底层控制。本文覆盖的正是这一场景下最轻量、最稳定、跨平台一致性最高的实现路径:基于hidapi绑定的hidPython 包(非pyusb+ 手动 HID 封包),所有操作均在用户态完成,Ubuntu 下无需sudo,Windows 下无需以管理员身份运行,也无需安装额外驱动程序(系统自带 HID 驱动已足够)。适合嵌入式测试工程师、硬件联调人员及自动化产线脚本开发者。
2. 为什么选hid而不是pyusb或pynput?核心原理与平台差异解析
2.1 HID 协议本质与 Python 绑定层的技术分层
HID(Human Interface Device)并非一种传输方式,而是一套定义在 USB(或 Bluetooth LE)之上的应用层协议规范。它规定了设备如何通过「报告(Report)」结构组织数据:Input Report(设备→主机)、Output Report(主机→设备)、Feature Report(双向配置型数据)。关键点在于:操作系统内核已内置 HID 类驱动,负责将 USB 描述符解析为标准 HID 抽象层;用户态程序只需调用系统提供的 HID API(Linux 下为libhidapi,Windows 下为hid.dll/SetupAPI),即可绕过 USB 底层枚举与控制传输细节。hid包(pip install hid)正是对hidapi的 Python 封装,它屏蔽了平台差异,暴露统一接口;而pyusb是对 USB 协议栈的直接封装,需手动构造 HID 类请求(如SET_REPORT/GET_REPORT控制传输),易出错且跨平台兼容性差;pynput则仅面向标准 HID 输入设备(键盘/鼠标),无法访问自定义报告或 Feature Report。
提示:
hid包底层依赖hidapi库。Ubuntu 下需sudo apt install libhidapi-libusb0(推荐)或libhidapi-hidraw0;Windows 下pip install hid自动附带预编译hidapi.dll,无需额外安装。
2.2 Ubuntu 与 Windows 下设备识别机制的关键差异
| 维度 | Ubuntu(Linux) | Windows |
|---|---|---|
| 设备路径标识 | /dev/hidrawX(hidraw 接口)或/dev/bus/usb/BBB/DDD(USB 接口);hid包默认使用 hidraw 接口,更稳定 | \\\\?\\hid#vid_xxxx&pid_yyyy#...#{...}格式 PnP ID;hid包自动解析,无需手动拼接 |
| 权限模型 | 默认/dev/hidraw*权限为crw------- root:root,普通用户无权访问 → 必须配置 udev 规则或加入plugdev组 | Windows 用户态 HID API 默认允许访问,只要设备被系统识别为 HID 类设备(即描述符中 bInterfaceClass=0x03),无需管理员权限 |
| 枚举稳定性 | hid.enumerate()返回的path字段在设备热插拔后可能变化(如/dev/hidraw0→/dev/hidraw1),但vendor_id/product_id/serial_number恒定 | path字段为持久化 PnP ID,即使设备重插也不会变,更适合生产环境硬编码匹配 |
2.3 安装与验证环境就绪的最小命令集
在 Ubuntu 22.04 上执行以下命令完成环境准备:
# 安装 hidapi 系统库(关键!否则 pip install hid 会编译失败或运行时报错) sudo apt update && sudo apt install -y libhidapi-libusb0 libhidapi-dev # 创建用户组并添加当前用户(避免每次 sudo) sudo groupadd -f plugdev sudo usermod -aG plugdev $USER # 安装 Python 包(注意:必须先装系统库!) pip3 install --upgrade pip pip3 install hid # 验证:列出所有 HID 设备(应看到你的设备 vendor_id/product_id) python3 -c "import hid; print([d for d in hid.enumerate() if d['vendor_id'] == 0x0483])"在 Windows 10/11 上,仅需:
# PowerShell 中执行(管理员权限非必需) pip install hid # 验证:Python 中调用 enumerate 并过滤你的设备(例如 STM32 VID=0x0483) python -c "import hid; [print(d) for d in hid.enumerate() if d['vendor_id']==0x0483]"注意:若 Windows 上
enumerate()返回空列表,请检查设备管理器中该设备是否显示为「人体学输入设备」或「通用串行总线设备」下的 HID 兼容设备;若显示为「未知设备」或带黄色感叹号,说明 USB 描述符不符合 HID 规范,需固件修正。
3. 从零编写跨平台 HID 控制脚本:设备发现、打开、读写全流程
3.1 设备发现与精准匹配策略(避免硬编码路径)
真实项目中,设备序列号(serial_number)或产品 ID(product_id)是唯一可靠标识。以下函数封装了跨平台安全匹配逻辑,支持模糊匹配(如只知 VID/PID)和精确匹配(含序列号):
import hid def find_hid_device(vid, pid, serial=None): """ 跨平台查找 HID 设备,返回 device_info 字典(含 path, vendor_id 等) :param vid: 十六进制 vendor_id (e.g., 0x0483) :param pid: 十六进制 product_id (e.g., 0x5750) :param serial: 可选,设备序列号字符串(Windows/Linux 均支持) :return: dict or None """ devices = hid.enumerate(vid, pid) if not devices: print(f"未找到 VID={hex(vid)} PID={hex(pid)} 的 HID 设备") return None # 优先匹配序列号(最精确) if serial: for d in devices: if d.get('serial_number') == serial: return d print(f"警告:VID={hex(vid)} PID={hex(pid)} 设备存在,但序列号 '{serial}' 不匹配") # 退回到第一个匹配设备(开发阶段常用) return devices[0] # 示例:查找 STM32F4 Discovery 板(常见 VID=0x0483, PID=0x5750) dev_info = find_hid_device(vid=0x0483, pid=0x5750, serial="123456789") if dev_info: print(f"找到设备: {dev_info['product_string']} at {dev_info['path']}")3.2 打开设备并处理平台特有异常
hid.device()构造函数在不同平台下行为一致,但错误码含义需注意:
def open_hid_device(dev_info): """ 安全打开 HID 设备,处理常见平台错误 :param dev_info: find_hid_device() 返回的字典 :return: hid.Device 实例 or None """ try: h = hid.Device(path=dev_info['path']) print(f"✅ 成功打开设备: {h.manufacturer} {h.product}") return h except OSError as e: err_no = e.errno if "Permission denied" in str(e) or err_no == 13: # Linux 权限错误 print("❌ Linux 权限错误:请确认已加入 plugdev 组并重新登录,或临时用 sudo") print(" 解决方案:sudo usermod -aG plugdev $USER && reboot") elif "Access is denied" in str(e) or err_no == 5: # Windows 访问拒绝(极少见) print("❌ Windows 访问被拒:请确认设备管理器中无黄色感叹号,且未被其他程序占用") else: print(f"❌ 未知打开错误: {e}") return None except Exception as e: print(f"❌ 打开设备异常: {e}") return None # 使用示例 h = open_hid_device(dev_info) if not h: exit(1)3.3 发送 Feature Report 与读取 Input Report 的完整交互循环
HID 通信的核心是报告(Report)的收发。Feature Report 常用于设备配置(如设置采样率),Input Report 用于周期性数据上报(如传感器值)。以下代码实现一个健壮的读写循环,包含超时与重试:
import time def send_feature_report(h, report_id, data_bytes): """ 发送 Feature Report(带 Report ID) :param h: hid.Device 实例 :param report_id: 报告 ID(1 字节,0x00 表示无 ID) :param data_bytes: bytes 对象,长度 ≤ 设备最大输出报告长度 :return: True on success """ # 构造带 Report ID 的数据包:[report_id] + data_bytes packet = bytes([report_id]) + data_bytes try: h.send_feature_report(packet) return True except OSError as e: print(f"⚠️ 发送 Feature Report 失败: {e}") return False def read_input_report(h, timeout_ms=1000): """ 读取 Input Report(阻塞,带超时) :param h: hid.Device 实例 :param timeout_ms: 超时毫秒数 :return: bytes or None """ try: # read() 返回 bytes,首字节为 Report ID(若设备有多个报告) data = h.read(64, timeout_ms) # 64 是常见最大报告长度,按需调整 if data: print(f"📥 收到 Input Report (len={len(data)}): {data.hex()}") return data else: print("⚠️ read() 超时,未收到数据") return None except OSError as e: print(f"⚠️ 读取 Input Report 异常: {e}") return None # 主交互循环示例 if h: # 步骤1:发送 Feature Report 配置设备(例如设置模式为 0x01) if send_feature_report(h, report_id=0x01, data_bytes=b'\x01'): print("✅ 已发送配置命令") # 步骤2:连续读取 5 次 Input Report for i in range(5): data = read_input_report(h, timeout_ms=500) if data: # 解析示例:假设前2字节为16位整数温度值(小端) if len(data) >= 3: temp_raw = int.from_bytes(data[1:3], 'little', signed=True) print(f"🌡️ 温度值: {temp_raw} °C") time.sleep(0.2) # 间隔 200ms h.close()重要参数说明:
h.read(size, timeout)中size是缓冲区大小,非单次读取长度;实际返回长度由设备发送的 Input Report 决定。timeout_ms在 Linux 下有效,在 Windows 下部分版本可能忽略,建议设为 100~1000ms 防止永久阻塞。send_feature_report()的packet首字节必须是 Report ID(若设备描述符定义了多个 Feature Report),否则设备可能忽略。
4. 调试实战:抓包分析、常见故障定位与性能优化技巧
4.1 使用usbmon(Linux)与USBlyzer(Windows)进行协议级抓包
当脚本行为异常(如read()总是超时、send_feature_report()无响应),必须验证物理层通信是否正常。此时不能依赖 Python 日志,而要抓取 USB 总线原始数据。
Ubuntu 下启用 usbmon:
# 加载模块 sudo modprobe usbmon # 查看可用 bus(通常为 usbmon0, usbmon1...) ls /sys/kernel/debug/usb/usbmon/ # 抓包(另开终端,Ctrl+C 停止) sudo cat /sys/kernel/debug/usb/usbmon/0u > usbmon.log # 分析:用 Wireshark 打开 usbmon.log,过滤 hid.class,观察 SETUP 包中的 bRequest=0x09 (SET_REPORT) 和 bRequest=0x01 (GET_REPORT)Windows 下使用 USBlyzer(免费版足够):
- 启动 USBlyzer → 选择目标设备 → 开始捕获。
- 过滤
HID Class→ 查看Set_Report和Get_Report请求的数据负载(Data Field)是否与 Python 脚本发送/期望的一致。 - 关键比对点:
Report ID字段位置、wLength(报告长度)、Data Field内容。
提示:若抓包显示
SET_REPORT成功但设备无反应,大概率是固件未正确解析 Report ID 或数据格式;若GET_REPORT无返回,检查设备是否处于主动上报模式(有些 HID 设备需先发命令触发上报)。
4.2 三个必调参数与它们的真实影响
| 参数 | 位置 | 默认值 | 调整建议 | 影响说明 |
|---|---|---|---|---|
read()缓冲区大小 | h.read(size, timeout) | 64 | 设为设备Descriptor中wMaxPacketSize(通常 64)或Input Report最大长度 | 过小导致数据截断;过大无害但浪费内存 |
timeout_ms | h.read()和h.get_feature_report() | 1000 | 传感器类设备设为 500~2000ms;控制类设为 100ms | 过短频繁超时;过长阻塞主线程 |
send_feature_report()数据长度 | 构造packet时 | 由固件决定 | 严格等于固件HID Descriptor中对应 Feature Report 的bSize | 多1字节或少1字节均导致固件拒绝处理 |
4.3 生产环境健壮性增强:自动重连与序列号绑定
在长期运行的产线脚本中,设备热插拔是常态。以下代码实现自动重连,并强制使用序列号确保连接到指定设备(防止插错设备导致误操作):
import time def robust_hid_session(vid, pid, serial, reconnect_delay=2.0): """ 带自动重连的 HID 会话管理器 :param vid, pid, serial: 设备标识 :param reconnect_delay: 重连间隔秒数 :yield: hid.Device 实例(每次 yield 均为新连接) """ while True: dev_info = find_hid_device(vid, pid, serial) if not dev_info: print(f"⏳ 等待设备 {hex(vid)}:{hex(pid)} ({serial}) ...") time.sleep(reconnect_delay) continue h = open_hid_device(dev_info) if h: try: yield h # 提供给业务逻辑使用 finally: h.close() print("🔌 设备已关闭,等待下次重连...") else: print("❌ 设备打开失败,2秒后重试...") time.sleep(reconnect_delay) # 使用示例:每5秒读一次温度,设备断开自动重连 for h in robust_hid_session(vid=0x0483, pid=0x5750, serial="123456789"): for _ in range(5): data = read_input_report(h, timeout_ms=500) if data and len(data) >= 3: temp = int.from_bytes(data[1:3], 'little', signed=True) print(f"📈 实时温度: {temp}°C") time.sleep(1)5. 进阶技巧:解析复杂 HID 描述符与动态生成报告结构
5.1 从hid.enumerate()获取设备能力元数据
hid.enumerate()返回的每个设备字典中,usage_page和usage字段揭示了设备功能类别(如usage_page=0x01表示 Generic Desktop Controls),但更关键的是max_input_report_length、max_output_report_length、max_feature_report_length—— 这些值直接决定了read()和send_feature_report()的安全参数上限:
dev_info = find_hid_device(0x0483, 0x5750) if dev_info: print(f"最大 Input Report 长度: {dev_info['max_input_report_length']} 字节") print(f"最大 Feature Report 长度: {dev_info['max_feature_report_length']} 字节") print(f"Usage Page: {hex(dev_info['usage_page'])}, Usage: {dev_info['usage']}")5.2 使用hidtools解析二进制 HID 描述符(.hid文件)
当需要深度理解设备报告结构(如某字段是 12 位有符号数、某标志位在第 3 字节第 5 位),必须解析其 HID 描述符。hidtools是官方推荐工具:
# Ubuntu 安装 pip install hidtools # 从设备导出描述符(需 root) sudo python3 -c " import hid d = hid.Device(vid=0x0483, pid=0x5750) desc = d.get_descriptor() with open('device.desc', 'wb') as f: f.write(desc) " # 解析为人类可读格式 hid-describe device.desc输出示例片段:
INPUT(1) [Array] Usage Page (Generic Desktop) Usage Minimum (0x00) Usage Maximum (0xFF) Logical Minimum (-128) Logical Maximum (127) Report Size (8) Report Count (64) ...这明确告诉你:Input Report 是一个 64 字节的数组,每个元素是 8 位有符号数(-128~127),可直接用data[0],data[1]访问。
5.3 构建类型安全的报告解析器(Python dataclass)
避免魔法数字,用dataclass封装报告结构,提升可维护性:
from dataclasses import dataclass from typing import List @dataclass class SensorReport: """解析 Input Report 的 dataclass(假设结构:ID(1)+Temp(2)+Humidity(2)+Status(1)+Reserved(58))""" report_id: int temperature: int # int16, little-endian humidity: int # int16, little-endian status: int # uint8 @classmethod def from_bytes(cls, data: bytes): if len(data) < 7: raise ValueError(f"Input Report 长度不足: {len(data)} < 7") return cls( report_id=data[0], temperature=int.from_bytes(data[1:3], 'little', signed=True), humidity=int.from_bytes(data[3:5], 'little', signed=True), status=data[5] ) # 使用 data = read_input_report(h) if data: try: report = SensorReport.from_bytes(data) print(f"✅ 解析成功: T={report.temperature}°C, H={report.humidity}%, Status={report.status}") except ValueError as e: print(f"❌ 解析失败: {e}")将 HID 设备控制从“能跑通”推进到“可维护、可测试、可部署”,关键在于把协议细节(Report ID、字节序、字段偏移)从脚本中解耦出来,固化为可验证的数据结构。这正是hid包配合dataclass和hidtools所提供的工程化路径。
本文还有配套的精品资源,点击获取