使用Python和pyusb库获取HID设备(USB无线鼠标)的数据(充电信息)
我正在尝试通过pyusb与无线鼠标Ninjutso Sora V2进行通信,以获取充电信息。之前我为Razer鼠标做过类似的事情,链接在这里:Razer tray。现在我想为Sora鼠标做一个类似的脚本。
这个鼠标使用基于网页的软件和webhid API来更改设置和获取状态信息。这个软件叫做NinjaForce。我不太懂JavaScript,但我在代码中找到了一个函数,想用pyusb实现其中的一部分:
async function _(e) {
const t = new Uint8Array(31);
t[0] = 13,
t[3] = 1,
t[6] = 22,
await e.sendFeatureReport(5, t),
await a(90);
let r = await e.receiveFeatureReport(5);
K.profile = r.getUint8(9)
}
const l = async e=>{
let t = new Uint8Array(31);
t[0] = 9,
t[3] = 1,
await e.sendFeatureReport(5, t),
await a(90);
let r = await e.receiveFeatureReport(5);
K.version = [...Array(4)].map(((e,t)=>r.getUint8(12 - t).toString(16).padStart(2, "0"))).join("")
}
, d = async e=>{
const t = new Uint8Array(31);
t[0] = 21,
t[3] = 1,
await a(90),
await e.sendFeatureReport(5, t),
await a(90);
let r = await e.receiveFeatureReport(5);
K.battery = r.getUint8(9),
K.charging = r.getUint8(10),
K.fullCharge = r.getUint8(11),
K.online = r.getUint8(12)
}
, p = async e=>{
let t = e.productId;
if (44572 === t || 44684 === t) {
const r = new Uint8Array(31);
r[0] = 34,
r[3] = 1,
r[6] = 22,
await e.sendFeatureReport(5, r),
await a(90);
let n = await e.receiveFeatureReport(5);
t = n.getUint8(10) << 8 | n.getUint8(9)
}
K.deviceColor = {
44561: "black",
44562: "white",
44563: "pink",
44564: "red",
44565: "#1e22aa",
44566: "transparent"
}[t]
}
我对这部分内容感兴趣:
, d = async e=>{
const t = new Uint8Array(31);
t[0] = 21,
t[3] = 1,
await a(90),
await e.sendFeatureReport(5, t),
await a(90);
let r = await e.receiveFeatureReport(5);
K.battery = r.getUint8(9),
K.charging = r.getUint8(10),
K.fullCharge = r.getUint8(11),
K.online = r.getUint8(12)
我用Wireshark追踪了USB,发现了一些控制传输:帧。我猜测报告的第一个字节0x05是报告编号5。
所以,这是我使用pyusb的实现:
import time
import usb.core
import usb.util
from usb.backend import libusb1
VID = 0x1915
PID = 0xAE1C
def send_feature_report(device, feature_report_data):
device.ctrl_transfer(
bmRequestType=0x21,
bRequest=0x09,
wValue=0x305,
wIndex=1,
data_or_wLength=feature_report_data)
def get_feature_report(device, report_length):
feature_report = device.ctrl_transfer(
bmRequestType=0xA1,
bRequest=0x01,
wValue=0x305,
wIndex=1,
data_or_wLength=report_length)
return feature_report
backend = libusb1.get_backend(find_library=lambda x: R".\libusb-1.0.dll")
dev = usb.core.find(idVendor=VID, idProduct=PID, backend=backend)
print(f"dev: {dev}")
dev.set_configuration()
usb.util.claim_interface(dev, 1)
report = bytearray(32)
report[0] = 5
report[1] = 21
report[4] = 1
print(f"report: {report}")
time.sleep(0.09)
send_feature_report(dev, report)
time.sleep(0.09)
result = get_feature_report(dev, 32)
usb.util.dispose_resources(dev)
usb.util.release_interface(dev, 1)
print(f"result: {result}")
输出:
dev: DEVICE ID 1915:ae1c on Bus 001 Address 010 =================
bLength : 0x12 (18 bytes)
bDescriptorType : 0x1 Device
bcdUSB : 0x200 USB 2.0
bDeviceClass : 0x0 Specified at interface
bDeviceSubClass : 0x0
bDeviceProtocol : 0x0
bMaxPacketSize0 : 0x40 (64 bytes)
idVendor : 0x1915
idProduct : 0xae1c
bcdDevice : 0x200 Device 2.0
iManufacturer : 0x1 Ninjutso
iProduct : 0x2 Ninjutso Sora V2
iSerialNumber : 0x3 000000000000
bNumConfigurations : 0x1
CONFIGURATION 1: 500 mA ==================================
bLength : 0x9 (9 bytes)
bDescriptorType : 0x2 Configuration
wTotalLength : 0x42 (66 bytes)
bNumInterfaces : 0x2
bConfigurationValue : 0x1
iConfiguration : 0x4 Default configuration
bmAttributes : 0xe0 Self Powered, Remote Wakeup
bMaxPower : 0xfa (500 mA)
INTERFACE 0: Human Interface Device ====================
bLength : 0x9 (9 bytes)
bDescriptorType : 0x4 Interface
bInterfaceNumber : 0x0
bAlternateSetting : 0x0
bNumEndpoints : 0x1
bInterfaceClass : 0x3 Human Interface Device
bInterfaceSubClass : 0x1
bInterfaceProtocol : 0x2
iInterface : 0x0
ENDPOINT 0x81: Interrupt IN ==========================
bLength : 0x7 (7 bytes)
bDescriptorType : 0x5 Endpoint
bEndpointAddress : 0x81 IN
bmAttributes : 0x3 Interrupt
wMaxPacketSize : 0x40 (64 bytes)
bInterval : 0x1
INTERFACE 1: Human Interface Device ====================
bLength : 0x9 (9 bytes)
bDescriptorType : 0x4 Interface
bInterfaceNumber : 0x1
bAlternateSetting : 0x0
bNumEndpoints : 0x2
bInterfaceClass : 0x3 Human Interface Device
bInterfaceSubClass : 0x1
bInterfaceProtocol : 0x0
iInterface : 0x0
ENDPOINT 0x82: Interrupt IN ==========================
bLength : 0x7 (7 bytes)
bDescriptorType : 0x5 Endpoint
bEndpointAddress : 0x82 IN
bmAttributes : 0x3 Interrupt
wMaxPacketSize : 0x40 (64 bytes)
bInterval : 0x1
ENDPOINT 0x2: Interrupt OUT ==========================
bLength : 0x7 (7 bytes)
bDescriptorType : 0x5 Endpoint
bEndpointAddress : 0x2 OUT
bmAttributes : 0x3 Interrupt
wMaxPacketSize : 0x40 (64 bytes)
bInterval : 0x1
report: bytearray(b'\x05\x15\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
Traceback (most recent call last):
File "c:\Users\xxxx\Documents\Python\sorav2_tray\usb1.py", line 40, in <module>
send_feature_report(dev, report)
File "c:\Users\xxxx\Documents\Python\sorav2_tray\usb1.py", line 10, in send_feature_report
device.ctrl_transfer(
File "C:\Users\xxxx\Documents\Python\sorav2_tray\.venv\Lib\site-packages\usb\core.py", line 1082, in ctrl_transfer
ret = self._ctx.backend.ctrl_transfer(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\xxxx\Documents\Python\sorav2_tray\.venv\Lib\site-packages\usb\backend\libusb1.py", line 893, in ctrl_transfer
ret = _check(self.lib.libusb_control_transfer(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\xxxx\Documents\Python\sorav2_tray\.venv\Lib\site-packages\usb\backend\libusb1.py", line 604, in _check
raise USBError(_strerror(ret), ret, _libusb_errno[ret])
usb.core.USBError: [Errno 5] Input/Output Error
我无论怎么尝试都得到[Errno 5] 输入/输出错误
。也许我需要将主函数的所有请求按顺序实现?我对webhid函数sendFeatureReport
和receiveFeatureReport
的实现是否正确?我需要一些建议。
更新 1:尝试不在报告数据中包含报告ID(5),如下面所建议的:
import time
import usb.core
import usb.util
from usb.backend import libusb1
VID = 0x1915
PID = 0xAE1C
def send_feature_report(device, feature_report_data):
device.ctrl_transfer(
bmRequestType=0x21,
bRequest=0x09,
wValue=0x0305,
wIndex=1,
data_or_wLength=feature_report_data)
def get_feature_report(device, report_length):
feature_report = device.ctrl_transfer(
bmRequestType=0xA1,
bRequest=0x01,
wValue=0x0305,
wIndex=1,
data_or_wLength=report_length)
return feature_report
backend = libusb1.get_backend(find_library=lambda x: R".\libusb-1.0.dll")
dev = usb.core.find(idVendor=VID, idProduct=PID, backend=backend)
dev.set_configuration()
usb.util.claim_interface(dev, 1)
report = bytearray(31)
report[0] = 21
report[3] = 1
print(f"report: {report}")
send_feature_report(dev, report)
time.sleep(0.09)
result = get_feature_report(dev, )
usb.util.dispose_resources(dev)
usb.util.release_interface(dev, 1)
print(f"result: {result}")
没有成功——仍然得到usb.core.USBError: [Errno 5] 输入/输出错误
更新 2:尝试使用hidapitester发送特征报告。看起来也不行。
(.venv) PS C:\Users\xxxx\Documents\Python\sorav2_tray> .\hidapitester.exe --vidpid 1915:AE1C --list-detail
1915/AE1C: Ninjutso - Ninjutso Sora V2
vendorId: 0x1915
productId: 0xAE1C
usagePage: 0x0001
usage: 0x0006
serial_number: 000000000000
interface: 1
path: \\?\HID#VID_1915&PID_AE1C&MI_01&Col02#9&24e9f7c2&0&0001#{4d1e55b2-f16f-11cf-88cb-001111000030}\KBD
1915/AE1C: Ninjutso - Ninjutso Sora V2
vendorId: 0x1915
productId: 0xAE1C
usagePage: 0x0001
usage: 0x0002
serial_number: 000000000000
interface: 1
path: \\?\HID#VID_1915&PID_AE1C&MI_01&Col01#9&24e9f7c2&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
1915/AE1C: Ninjutso - Ninjutso Sora V2
vendorId: 0x1915
productId: 0xAE1C
usagePage: 0x000C
usage: 0x0001
serial_number: 000000000000
interface: 1
path: \\?\HID#VID_1915&PID_AE1C&MI_01&Col03#9&24e9f7c2&0&0002#{4d1e55b2-f16f-11cf-88cb-001111000030}
1915/AE1C: Ninjutso - Ninjutso Sora V2
vendorId: 0x1915
productId: 0xAE1C
usagePage: 0xFFA0
usage: 0x0001
serial_number: 000000000000
interface: 1
path: \\?\HID#VID_1915&PID_AE1C&MI_01&Col04#9&24e9f7c2&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}
1915/AE1C: Ninjutso - Ninjutso Sora V2
vendorId: 0x1915
productId: 0xAE1C
usagePage: 0x0001
usage: 0x0002
serial_number: 000000000000
interface: 0
path: \\?\HID#VID_1915&PID_AE1C&MI_00#9&112ba00&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
(.venv) PS C:\Users\xxxx\Documents\Python\sorav2_tray> .\hidapitester.exe -l 32 --vidpid 1915:AE1C --open --send-feature 5,21,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 --timeout 90 --read-feature 5
Opening device, vid/pid: 0x1915/0xAE1C
Writing 32-byte feature report...wrote -1 bytes:
05 15 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Reading 32-byte feature report, report_id 5...read -1 bytes:
05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Closing device
更新 3:成功了!我通过使用hidapitester,向特定的usepage发送报告,得到了响应——usagePage: 0xFFA0
。
(.venv) PS C:\Users\xxxx\Documents\Python\sorav2_tray> .\hidapitester.exe -l 32 --vidpid 1915:AE1C --usagePage 0xFFA0 --open --send-feature 5,21,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 --timeout 90 --read-feature 5
Opening device, vid/pid:0x1915/0xAE1C, usagePage/usage: FFA0/0
Device opened
Writing 32-byte feature report...wrote 32 bytes:
05 15 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Reading 32-byte feature report, report_id 5...read 32 bytes:
05 15 00 00 01 00 00 00 00 5C 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78
Closing device
更新 4:最终实现使用HIDAPI
而不是pyusb
:
import time
import hid
VID = 0x1915
PID = 0xAE1C
USAGE_PAGE = 0xFFA0
def get_path(device_list, usage_page):
for device in device_list:
if device['usage_page'] == usage_page:
return device['path']
def get_battery():
device_list = hid.enumerate(VID, PID)
path = get_path(device_list, USAGE_PAGE)
print(f"Device path: {path}")
device = hid.device()
device.open_path(path)
report = [0] * 32
report[0] = 5
report[1] = 21
report[4] = 1
print(f"Sending report:\t {report}")
device.send_feature_report(report)
time.sleep(0.09)
res = device.get_feature_report(5, 32)
print(f"Recieved report:\t {res}")
device.close()
return res[9]
if __name__ == "__main__":
battery = get_battery()
print(f'Charge: {battery}%')
输出:
Device path: b'\\\\?\\HID#VID_1915&PID_AE1C&MI_01&Col04#9&24e9f7c2&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}'
Sending report: [5, 21, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Recieved report: [5, 21, 0, 0, 1, 0, 0, 0, 0, 91, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119]
Charge: 91%
1 个回答
这个错误可能是因为发送的报告大小不符合预期。
USB HID控制传输会把报告ID放在wValue
里。这意味着你不应该把它作为报告缓冲区的第一个字节。报告的大小是指报告的字节长度,如果设备使用报告ID的话,报告ID字节是不算在内的。
从你的代码来看,wValue
总是0x305
(特征报告5)。所以,你应该把低字节设置为报告ID,并且在传递给control_transfer
的报告数据中不要包含这个字节。
详细信息请查看7.2 类特定请求部分,内容在HID设备类定义中。