使用Python和pyusb库获取HID设备(USB无线鼠标)的数据(充电信息)

-1 投票
1 回答
71 浏览
提问于 2025-04-12 23:44

我正在尝试通过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函数sendFeatureReportreceiveFeatureReport的实现是否正确?我需要一些建议。

更新 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 个回答

0

这个错误可能是因为发送的报告大小不符合预期。

USB HID控制传输会把报告ID放在wValue里。这意味着你不应该把它作为报告缓冲区的第一个字节。报告的大小是指报告的字节长度,如果设备使用报告ID的话,报告ID字节是不算在内的。

从你的代码来看,wValue总是0x305(特征报告5)。所以,你应该把低字节设置为报告ID,并且在传递给control_transfer的报告数据中不要包含这个字节。

详细信息请查看7.2 类特定请求部分,内容在HID设备类定义中。

撰写回答