Get data (charge information) from HID-device (USB wireless mouse) with Python and pyusb library

73 Views Asked by At

I'm trying to communicate with wireless mouse Ninjutso Sora V2 to get charge information using pyusb. I've made something similar for Razer mouse before - Razer tray. I would like to make similar script for Sora mouse.

This mouse uses web-based software and webhid api to change settings and get status information. It's called NinjaForce. I don't know JS but I've found this function in code and would like to implement part of it with 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]
}

I'm interested in this part:

  , 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)

I've traced USB with Wireshark and have found this control transfers: frame. I'm guessing that first byte of report 0x05 is report number 5.

So, this is my implementation using 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}")

Output:

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

I'm getting [Errno 5] Input/Output Error no matter what. Maybe, i need to implement all requests of main function in chain? Is my implementation of webhid functions sendFeatureReport and receiveFeatureReport correct? I need an advice.

Update 1: Tried to not include report id (5) in report data as suggested below:

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}")

No luck - still getting usb.core.USBError: [Errno 5] Input/Output Error

Update 2 Tried to send feature report with hidapitester. Looks like it doesn't work either.

(.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

Update 3 Success! I managed to get response using hidapitester by sending report to specific 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

Update 4 Final implementation using HIDAPI instead of 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}%')

Output:

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

There are 1 best solutions below

1
Matt Reynolds On

This error is probably caused by sending a report with an unexpected size.

USB HID control transfers put the report ID in wValue. This means you should not include it as the first byte of the report buffer. The report size is the length of the report in bytes, excluding the report ID byte if the device uses report IDs.

From your code it looks like wValue is always 0x305 (feature report 5). Instead, set the low byte to the report ID and don't include that byte in the report data passed to control_transfer.

See 7.2 Class-Specific Requests in the Device Class Definition for HID.