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%
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
wValueis always0x305(feature report 5). Instead, set the low byte to the report ID and don't include that byte in the report data passed tocontrol_transfer.See 7.2 Class-Specific Requests in the Device Class Definition for HID.