Closed mcuee closed 7 months ago
You can use "raw HID" and write your own report descriptor, but it's not part of the library. Unusual uses of HID could be done in another library. We try to keep this particular library small so it fits on all possible boards.
https://learn.adafruit.com/custom-hid-devices-in-circuitpython https://learn.adafruit.com/customizing-usb-devices-in-circuitpython/hid-devices#custom-hid-devices-3096614
Thanks. I will close this issue then.
For now I use Adafruit_TinyUSB_Arduino instead. https://github.com/adafruit/Adafruit_TinyUSB_Arduino/tree/master/examples/HID/hid_generic_inout
The following boot.py code seems to work, at least for USB HID emulation.
import usb_hid
# This is only one example of a gamepad descriptor, and may not suit your needs.
CUSTOM_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xff, # Usage page (Vendor Defined Page1)
0x09, 0x01, # Usage (Vendor Page 1)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # Report ID (1)
0x09, 0x00, # Usage (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits or 1 byte)
0x95, 0x40, # Report Count (64)
0x82, 0x02, 0x01, # Input (Data,Var,Abs,Buf)
0x85, 0x02, # Report ID (2)
0x09, 0x00, # Usage (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits or 1 byte)
0x95, 0x40, # Report Count (64)
0x92, 0x02, 0x01, # Output (Data,Var,Abs,Buf)
0xC0, # End Collection
))
custom_device = usb_hid.Device(
report_descriptor=CUSTOM_REPORT_DESCRIPTOR,
usage_page=0xff00, # Vendor defined
usage=0x01, # Vendor page 1
report_ids=(1, 2),
in_report_lengths=(64,0),
out_report_lengths=(0, 64),
)
usb_hid.enable(
(usb_hid.Device.CONSUMER_CONTROL, # strange that I need to keep one of the three default ones to get this to work
custom_device)
)
However, sending input report does not work. Maybe there is a simple fix to the following code.py
.
import time
import usb_hid
import adafruit_hid
custom_device = adafruit_hid.find_device(usb_hid.devices, usage_page=0xff00, usage=0x01)
print("custom_device:", custom_device)
while True:
report = bytearray(64) # must be same size as specified in HID Report Descriptor in boot.py
report = custom_device.get_last_received_report(2)
print(["%02x" % x for x in report])
time.sleep(1)
report[0] = 1
custom_device.send_report(report, 1)
hidapitester output
(py310x64venv) PS C:\work\hid\hidapitester> .\hidapitester --vidpid 2e8a:102e --usagePage=0xff00 --usage 1 --open --list-detail
Opening device, vid/pid:0x2E8A/0x102E, usagePage/usage: FF00/1
Device opened
2E8A/102E: VCC-GND Studio - CircuitPython HID
vendorId: 0x2E8A
productId: 0x102E
usagePage: 0xFF00
usage: 0x0001
serial_number: DE6185100F4D6522
interface: 3
path: \\?\HID#VID_2E8A&PID_102E&MI_03&Col02#8&15f7af61&0&0001#{4d1e55b2-f16f-11cf-88cb-001111000030}
Closing device
(py310x64venv) PS C:\work\hid\hidapitester> .\hidapitester --vidpid 2e8a:102e --usagePage=0xff00 --usage 1 --open --send-output 2,3,4,5 --read-input 1
Opening device, vid/pid:0x2E8A/0x102E, usagePage/usage: FF00/1
Device opened
Writing output report of 64-bytes...wrote 65 bytes:
02 03 04 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 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
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Closing device
usb_hid.enable(
(usb_hid.Device.CONSUMER_CONTROL, # strange that I need to keep one of the three default ones to get this to work
custom_device)
Do you mean that usb_hid.enable((custom_device,))
does not work? How does it not work?
Note that the argument must be a tuple (so it must have a comma).
However, sending input report does not work.
Do you mean sending from the CircuitPython device? .send_report()
will prefix the report with the specified report id. You don't need to do that youself. (Is that what you are doing with report[0] = 1
?)
Do you mean that
usb_hid.enable((custom_device,))
does not work? How does it not work? Note that the argument must be a tuple (so it must have a comma).
Thanks for the help, this works now.
usb_hid.enable(
(custom_device,)
)
Adafruit CircuitPython 8.1.0 on 2023-05-22; VCC-GND Studio YD RP2040 with rp2040
Board ID:vcc_gnd_yd_rp2040
UID:DE6185100F4D6522
boot.py output:
Do you mean sending from the CircuitPython device? .send_report() will prefix the report with the specified report id. You don't need to do that youself. (Is that what you are doing with report[0] = 1 ?)
I see. Thanks. But it still does not work.
I have changed the report size to be smaller (4 bytes) and the following code still does not work for the input report (from CircuitPython to host).
boot.py
import usb_hid
# This is only one example of a gamepad descriptor, and may not suit your needs.
CUSTOM_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xff, # Usage page (Vendor Defined Page1)
0x09, 0x01, # Usage (Vendor Page 1)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # Report ID (1)
0x09, 0x00, # Usage (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits)
0x95, 0x04, # Report Count (4 fields)
0x82, 0x02, 0x01, # Input (Data,Var,Abs,Buf)
0x85, 0x02, # Report ID (2)
0x09, 0x00, # Usage (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits)
0x95, 0x04, # Report Count (4 fields)
0x92, 0x02, 0x01, # Output (Data,Var,Abs,Buf)
0xC0, # End Collection
))
custom_device = usb_hid.Device(
report_descriptor=CUSTOM_REPORT_DESCRIPTOR,
usage_page=0xff00, # Vendor defined
usage=0x01, # Vendor page 1
report_ids=(1, 2),
in_report_lengths=(4,0),
out_report_lengths=(0,4),
)
usb_hid.enable(
(custom_device,)
)
code.py
import time
import usb_hid
import adafruit_hid
custom_device = adafruit_hid.find_device(usb_hid.devices, usage_page=0xff00, usage=0x01)
print("custom_device:", custom_device)
while True:
report = bytearray(4) # must be same size as specified in HID Report Descriptor in boot.py
# report[0] = 1
report[1] = 21
report[2] = 22
report[3] = 23
# report[4] = 24
custom_device.send_report(report, 1)
time.sleep(1)
report = custom_device.get_last_received_report(2)
print(["%02x" % x for x in report])
hidapitester output
PS C:\work\hid\hidapitester> .\hidapitester --vidpid 2e8a:102e --usagePage=0xff00 --usage 1 --length 4 --open --send-output 2,3,4,5,6
Opening device, vid/pid:0x2E8A/0x102E, usagePage/usage: FF00/1
Device opened
Writing output report of 4-bytes...wrote 5 bytes:
02 03 04 05
Closing device
PS C:\work\hid\hidapitester> .\hidapitester --vidpid 2e8a:102e --usagePage=0xff00 --usage 1 --length 4 --open --read-input
Opening device, vid/pid:0x2E8A/0x102E, usagePage/usage: FF00/1
Device opened
Reading 5-byte input report 0, 250 msec timeout...read 0 bytes:
Closing device
Another thing, the above boot.py
does not work on Sam D21.
Adafruit CircuitPython 8.1.0 on 2023-05-22; SparkFun SAMD21 Mini Breakout with samd21g18
Board ID:sparkfun_samd21_mini
UID:7CA3AF314A34555020312E3436260FFF
boot.py output:
Traceback (most recent call last):
File "boot.py", line 35, in <module>
ValueError: report_ids length must be <= 1
BTW, this is part of my efforts to create test devices for HIDAPI project.
This PR fixed "Raw HID" support, and was tested with no report ID's. See the test program in the PR posts. You might find it helpful: https://github.com/adafruit/circuitpython/pull/7806
Another thing, the above
boot.py
does not work on Sam D21.
This is strange -- I don't see any code in the CircuitPython implementation that would generate a <=
error. Could you double-check that boot.py
is exactly the same on that board?
This PR fixed "Raw HID" support, and was tested with no report ID's. See the test program in the PR posts. You might find it helpful: adafruit/circuitpython#7806
Yes that example works very well. I have added an INPUT report (from device to host) and it works as well on the Raspberry Pi Pico. Tested under Windows 11 x64 and macOS 13.4 (Mac Mini M1, ARM64).
No change to boot.py
import usb_hid
RAWHID_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xFF, # Usage Page (Vendor Defined 0xFF00)
0x09, 0x01, # Usage (0x01)
0xA1, 0x01, # Collection (Application)
0x09, 0x02, # Usage (0x02)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x40, # Report Count (64)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x03, # Usage (0x03)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x40, # Report Count (64)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, # End Collection
))
raw_hid = usb_hid.Device(
report_descriptor=RAWHID_REPORT_DESCRIPTOR,
usage_page=0xFF00,
usage=0x01,
report_ids=(0,),
in_report_lengths=(64,),
out_report_lengths=(64,),
)
usb_hid.enable((raw_hid,))
#usb_hid.enable((usb_hid.Device.KEYBOARD,))
Minor changes to code.py to enable INPUT report (from device to host).
import usb_hid
import time
d = usb_hid.devices[0]
while True:
report = bytearray(64) # must be same size as specified in HID Report Descriptor in boot.py
report[0] = 1
report[1] = 2
report[2] = 3
report[3] = 4
report[63] = 64
d.send_report(report)
time.sleep(1)
print(d.get_last_received_report())
hidapitester output under Windows 11 1) OUTPUT report is okay
PS C:\work\hid\hidapitester> .\hidapitester --vidpid 2e8a:102e --open --length 64 --send-output 1,2,3,4,5,6,7,8
Opening device, vid/pid: 0x2E8A/0x102E
Writing output report of 64-bytes...wrote 65 bytes:
01 02 03 04 05 06 07 08 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 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
Serial monitor output.
None
b'\x02\x03\x04\x05\x06\x07\x08\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\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\x00\x00\x00'
None
None
2) INPUT report is also working.
PS C:\work\hid\hidapitester> .\hidapitester --vidpid 2e8a:102e --open --length 64 --read-input-forever
Opening device, vid/pid: 0x2E8A/0x102E
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 64 bytes:
01 02 03 04 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 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 40
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 64 bytes:
01 02 03 04 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 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 40
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 64 bytes:
01 02 03 04 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 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 40
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...
And the code works fine with SAMD21 as well.
Adafruit CircuitPython 8.1.0 on 2023-05-22; SparkFun SAMD21 Mini Breakout with samd21g18
Board ID:sparkfun_samd21_mini
UID:7CA3AF314A34555020312E3436260FFF
boot.py output:
hidapitester output
PS C:\work\hid\hidapitester> .\hidapitester --vidpid 1b4f:8d22 --open --length 64 --send-output 1,2,3,4,5,6,7,8
Opening device, vid/pid: 0x1B4F/0x8D22
Writing output report of 64-bytes...wrote 65 bytes:
01 02 03 04 05 06 07 08 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 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
PS C:\work\hid\hidapitester> .\hidapitester --vidpid 1b4f:8d22 --open --length 64 --read-input-forever
Opening device, vid/pid: 0x1B4F/0x8D22
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 64 bytes:
01 02 03 04 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 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 40
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 64 bytes:
01 02 03 04 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 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 40
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 64 bytes:
01 02 03 04 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 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 40
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Reading 64-byte input report 0, 250 msec timeout...read 0 bytes:
Another thing, the above
boot.py
does not work on Sam D21.This is strange -- I don't see any code in the CircuitPython implementation that would generate a
<=
error. Could you double-check thatboot.py
is exactly the same on that board?
Indeed this is strange. The code is exactly the same.
import usb_hid
# This is only one example of a gamepad descriptor, and may not suit your needs.
CUSTOM_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xff, # Usage page (Vendor Defined Page1)
0x09, 0x01, # Usage (Vendor Page 1)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # Report ID (1)
0x09, 0x00, # Usage (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits)
0x95, 0x04, # Report Count (4 fields)
0x82, 0x02, 0x01, # Input (Data,Var,Abs,Buf)
0x85, 0x02, # Report ID (2)
0x09, 0x00, # Usage (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits)
0x95, 0x04, # Report Count (4 fields)
0x92, 0x02, 0x01, # Output (Data,Var,Abs,Buf)
0xC0, # End Collection
))
custom_device = usb_hid.Device(
report_descriptor=CUSTOM_REPORT_DESCRIPTOR,
usage_page=0xff00, # Vendor defined
usage=0x01, # Vendor page 1
report_ids=(1, 2),
in_report_lengths=(4,0),
out_report_lengths=(0,4),
)
usb_hid.enable(
(custom_device,)
)
Adafruit CircuitPython 8.1.0 on 2023-05-22; SparkFun SAMD21 Mini Breakout with samd21g18
Board ID:sparkfun_samd21_mini
UID:7CA3AF314A34555020312E3436260FFF
boot.py output:
Traceback (most recent call last):
File "boot.py", line 35, in <module>
ValueError: report_ids length must be <= 1
@dhalbert
So I use your boot.py and just adding the report ID, SAMD21 will fail.
import usb_hid
RAWHID_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xFF, # Usage Page (Vendor Defined 0xFF00)
0x09, 0x01, # Usage (0x01)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # Report ID (1)
0x09, 0x02, # Usage (0x02)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x40, # Report Count (64)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0x02, # Report ID (2)
0x09, 0x03, # Usage (0x03)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8)
0x95, 0x40, # Report Count (64)
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, # End Collection
))
raw_hid = usb_hid.Device(
report_descriptor=RAWHID_REPORT_DESCRIPTOR,
usage_page=0xFF00,
usage=0x01,
report_ids=(1,2),
in_report_lengths=(64,0),
out_report_lengths=(0,64),
)
usb_hid.enable((raw_hid,))
#usb_hid.enable((usb_hid.Device.KEYBOARD,))
Adafruit CircuitPython 8.1.0 on 2023-05-22; SparkFun SAMD21 Mini Breakout with samd21g18
Board ID:sparkfun_samd21_mini
UID:7CA3AF314A34555020312E3436260FFF
boot.py output:
Traceback (most recent call last):
File "boot.py", line 31, in <module>
ValueError: report_ids length must be <= 1
I can verify that "rawhid" code that works on QTPY RP2040 does not work on QTPY M0 SAMD21 with the "boot_out.txt" message of ValueError: report_ids length must be <= 1
.
It seems the SAMD21 usb_hid
can only support one report, while the RP2040 one can support more than one. That is, changing the above code to be a single report works on SAMD21
# boot.py
import usb_hid
REPORT_COUNT = 63 # size of report in bytes
CUSTOM_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xff, # Usage page (Vendor Defined Page1)
0x09, 0x01, # Usage (Vendor Page 1)
0xA1, 0x01, # Collection (Application)
0x85, 0x02, # Report ID (2)
0x09, 0x00, # Usage (Undefined)
0x09, 0x00, # Usage Page (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits)
0x95, REPORT_COUNT, # Report Count (64 fields)
0x82, 0x02, 0x01, # Input (Data,Var,Abs,Buf)
0x92, 0x02, 0x01, # Output (Data,Var,Abs,Buf)
0xC0, # End Collection
))
raw_hid = usb_hid.Device(
report_descriptor=CUSTOM_REPORT_DESCRIPTOR,
usage_page=0xff00, # Vendor defined
usage=0x01, # Vendor page 1
report_ids=(2,),
in_report_lengths=(REPORT_COUNT,),
out_report_lengths=(REPORT_COUNT,),
)
usb_hid.enable( (raw_hid,) )
# code.py
import time
import usb_hid
import adafruit_hid
raw_hid = adafruit_hid.find_device(usb_hid.devices, usage_page=0xff00, usage=0x01)
print("raw_hid: %04x %04x" % (raw_hid.usage_page, raw_hid.usage) )
while True:
out_report = raw_hid.get_last_received_report(2) # out from computer
if out_report:
print("len:",len(out_report),["%02x" % x for x in out_report])
time.sleep(0.5)
print("sending copy on reportid 2")
in_report = bytearray(out_report) # copy in case we want to modify
raw_hid.send_report(in_report, 2); # in to computer
OK, yes, this is because of circuitpython/ports/atmel-samd/mpconfigport.h
:
// Only support simpler HID descriptors on SAMD21.
#define CIRCUITPY_USB_HID_MAX_REPORT_IDS_PER_DESCRIPTOR (1)
The default is 6
.
This was to save flash space on the SAMD21 builds: https://github.com/adafruit/circuitpython/pull/5272. This should be documented in a Limitations section.
@dhalbert
Thanks for the help.
Another strange thing, @todbot and I are both puzzled why changing REPORT_COUNT = 64
will lead to problem for reading back the Input Report. It seems the library might have an issue with devices with Report IDs.
With REPORT_COUNT = 63
the code by @todbot works perfectly.
mcuee@mcuees-Mac-mini hidapitester % ./hidapitester --vidpid=2e8a:102e --open -l 64 --send-output 2,3,4,5 --timeout 1000 --read-input
Opening device, vid/pid: 0x2E8A/0x102E
Writing output report of 64-bytes...wrote 64 bytes:
02 03 04 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 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
Reading 64-byte input report 0, 1000 msec timeout...read 64 bytes:
02 03 04 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 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
With REPORT_COUNT = 64
the code by @todbot does not work for the HID Input Report.
mcuee@mcuees-Mac-mini hidapitester % ./hidapitester --vidpid=2e8a:102e --open -l 65 --send-output 2,3,4,5 --timeout 1000 --read-input
Opening device, vid/pid: 0x2E8A/0x102E
Writing output report of 65-bytes...wrote 65 bytes:
02 03 04 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 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
Reading 65-byte input report 0, 1000 msec timeout...read 0 bytes:
Closing device
mcuee@mcuees-Mac-mini hidapitester % ./hidapitester --vidpid=2e8a:102e --open -l 64 --send-output 2,3,4,5 --timeout 1000 --read-input
Opening device, vid/pid: 0x2E8A/0x102E
Writing output report of 64-bytes...wrote 64 bytes:
02 03 04 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 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
Reading 64-byte input report 0, 1000 msec timeout...read 0 bytes:
Closing device
Once removing the report ID, @todbot's code works fine with 64 bytes REPORTS_COUNT.
# boot.py
import usb_hid
REPORT_COUNT = 64 # size of report in bytes
CUSTOM_REPORT_DESCRIPTOR = bytes((
0x06, 0x00, 0xff, # Usage page (Vendor Defined Page1)
0x09, 0x01, # Usage (Vendor Page 1)
0xA1, 0x01, # Collection (Application)
# 0x85, 0x02, # Report ID (2)
0x09, 0x00, # Usage (Undefined)
0x09, 0x00, # Usage Page (Undefined)
0x15, 0x00, # Logical Minimum (0)
0x26, 0xFF, 0x00, # Logical Maximum (255)
0x75, 0x08, # Report Size (8 bits)
0x95, REPORT_COUNT, # Report Count (64 fields)
0x82, 0x02, 0x01, # Input (Data,Var,Abs,Buf)
0x92, 0x02, 0x01, # Output (Data,Var,Abs,Buf)
0xC0, # End Collection
))
raw_hid = usb_hid.Device(
report_descriptor=CUSTOM_REPORT_DESCRIPTOR,
usage_page=0xff00, # Vendor defined
usage=0x01, # Vendor page 1
report_ids=(0,),
in_report_lengths=(REPORT_COUNT,),
out_report_lengths=(REPORT_COUNT,),
)
usb_hid.enable( (raw_hid,) )
# code.py
import time
import usb_hid
import adafruit_hid
raw_hid = adafruit_hid.find_device(usb_hid.devices, usage_page=0xff00, usage=0x01)
print("raw_hid: %04x %04x" % (raw_hid.usage_page, raw_hid.usage) )
while True:
out_report = raw_hid.get_last_received_report() # out from computer
if out_report:
print("len:",len(out_report),["%02x" % x for x in out_report])
time.sleep(0.5)
print("sending copy on reportid 0")
in_report = bytearray(out_report) # copy in case we want to modify
raw_hid.send_report(in_report); # in to computer
mcuee@mcuees-Mac-mini hidapitester % ./hidapitester --vidpid=2e8a:102e --open -l 64 --send-output 2,3,4,5 --timeout 2000 --read-input
Opening device, vid/pid: 0x2E8A/0x102E
Writing output report of 64-bytes...wrote 64 bytes:
02 03 04 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 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
Reading 64-byte input report 0, 2000 msec timeout...read 64 bytes:
02 03 04 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 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
@dhalbert
Another thing, I do not see how feature report is supported.
The PR does not show how to specify feature_report_length.
For Feature report examples, you can refer to the following Arduino example by @todbot. https://github.com/todbot/hidapitester/tree/master/test_hardware/hidtest_tinyusb
@mcuee @todbot
The PR does not show how to specify feature_report_length.
For a feature report, use the same length in in_report_length
and out_report_length
. Internally, these specify buffers to allocate.
Another strange thing, @todbot and I are both puzzled why changing
REPORT_COUNT = 64
will lead to problem for reading back the Input Report. It seems the library might have an issue with devices with Report IDs
The wMaxPacketSize for the compose HID device is set to 64 bytes here. I think this may be limiting the report size? https://github.com/adafruit/circuitpython/blob/main/shared-module/usb_hid/__init__.c#L39
...
#define HID_IN_ENDPOINT_INDEX (20)
0x03, // 21 bmAttributes (Interrupt)
0x40, 0x00, // 22,23 wMaxPacketSize 64
0x08, // 24 bInterval 8 (unit depends on device speed)
0x07, // 25 bLength
0x05, // 26 bDescriptorType (Endpoint)
0xFF, // 27 bEndpointAddress (OUT/H2D) [SET AT RUNTIME]
#define HID_OUT_ENDPOINT_INDEX (27)
0x03, // 28 bmAttributes (Interrupt)
0x40, 0x00, // 29,30 wMaxPacketSize 64
0x08, // 31 bInterval 8 (unit depends on device speed)
**** Someone just found a potential issue with TinyUSB and feature ports, and proposed a PR fix, which has not yet been reviewed: https://github.com/hathach/tinyusb/pull/2119. This may have something to do with any feature report issues you are seeing.
There is currently a heuristic about the report_id used for OUT reports https://github.com/adafruit/circuitpython/blob/main/shared-module/usb_hid/Device.c#L300. I think this should work for you anyway, due to the way the heuristic works. The underlying problem is something that TinyUSB could fix and is an open issue: https://github.com/hathach/tinyusb/issues/1990
if (report_id == 0 && report_type == HID_REPORT_TYPE_INVALID) {
// This could be a report with a non-zero report ID in the first byte, or
// it could be for report ID 0.
// Heuristic: see if there's a device with report ID 0, and if its report length matches
// the size of the incoming buffer. In that case, assume the first byte is not the report ID,
// but is data. Otherwise use the first byte as the report id.
if (usb_hid_get_device_with_report_id(0, &hid_device, &id_idx) &&
hid_device &&
hid_device->out_report_buffers[id_idx] &&
hid_device->out_report_lengths[id_idx] == bufsize) {
// Use as is, with report_id 0.
} else {
// No matching report ID 0, so use the first byte as the report ID.
report_id = buffer[0];
buffer++;
bufsize--;
}
} else if (report_type != HID_REPORT_TYPE_OUTPUT && report_type != HID_REPORT_TYPE_FEATURE) {
return;
}
Hi there :)
I struggle with getting a USB HID gamepad up and running for macOS.
What I did:
boot.py
import usb_hid
# This is only one example of a gamepad descriptor, and may not suit your needs.
GAMEPAD_REPORT_DESCRIPTOR = bytes((
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x05, # Usage (Game Pad)
0xA1, 0x01, # Collection (Application)
0x85, 0x04, # Report ID (4)
0x05, 0x09, # Usage Page (Button)
0x19, 0x01, # Usage Minimum (Button 1)
0x29, 0x10, # Usage Maximum (Button 16)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x75, 0x01, # Report Size (1)
0x95, 0x10, # Report Count (16)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x15, 0x81, # Logical Minimum (-127)
0x25, 0x7F, # Logical Maximum (127)
0x09, 0x30, # Usage (X)
0x09, 0x31, # Usage (Y)
0x09, 0x32, # Usage (Z)
0x09, 0x35, # Usage (Rz)
0x75, 0x08, # Report Size (8)
0x95, 0x04, # Report Count (4)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection
))
gamepad = usb_hid.Device(
report_descriptor=GAMEPAD_REPORT_DESCRIPTOR,
usage_page=0x01, # Generic Desktop Control
usage=0x05, # Gamepad
report_ids=(4,), # Descriptor uses report ID 4.
in_report_lengths=(6,), # This gamepad sends 6 bytes in its report.
out_report_lengths=(0,), # It does not receive any reports.
)
usb_hid.enable(
(usb_hid.Device.KEYBOARD,
gamepad)
)
code.py
import time
import board
import digitalio
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode
from hid_gamepad import Gamepad
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
######## Keyboard, Mouse, GamePad
keyb = Keyboard(usb_hid.devices)
keyb_layout = KeyboardLayoutUS(keyb)
gp = Gamepad(usb_hid.devices)
# Create some buttons. The physical buttons are connected
# to ground on one side and these and these pins on the other.
button_pins = (board.GP0,)
gamepad_buttons = (1,)
buttons = [digitalio.DigitalInOut(pin) for pin in button_pins]
for button in buttons:
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
while True:
# Buttons are grounded when pressed (.value = False).
for i, button in enumerate(buttons):
gamepad_button_num = gamepad_buttons[i]
if button.value:
gp.release_buttons(gamepad_button_num)
print("", gamepad_button_num, end=" ON")
time.sleep(0.25)
else:
gp.press_buttons(gamepad_button_num)
print("", gamepad_button_num, end=" OFF")
time.sleep(0.25)
Its is working fine so far (ignore the keyboard stuff, I do not need it at that time, just for testing.) BUT: What I would expect is, that the pico now shows up as a USB HID device with one button/switch somewhere in the macOS Operating System Preferences (Gamepads?).
But it is not showing up anywhere. So has anyone some Experience with macOS 13 or 14 how "announce" the Pico as a Gamepad for usage?
(Background: I want to build a board of switches in order to build a cockpit interface for X-Plane)
Any hint appreciated :)
Ok, update on that... I installed an App called "Controllers Lite" from the Mac App Store and the Pico is showing up and the button input is recognized.
So basically the HID interface is working fine... Now I have to find an answer to the question, what has to be done, that the controller shows up in the macOS Preferences and also in x-plane...
Maybe a hint could be, that the above mentioned app recognizes the switch toggle as input from button 272 (ID: 280). Which is strange, as I declared for the HID Device to only have 16 Buttons... ?
The example gamepad descriptor may not work on macOS. If I remember right, I could not produce a gamepad report descriptor that worked across Windows, Linux, and macOS (or maybe iOS?) all at once. That is one reason we dropped GamePad from the library.
The example gamepad descriptor may not work on macOS. If I remember right, I could not produce a gamepad report descriptor that worked across Windows, Linux, and macOS (or maybe iOS?) all at once. That is one reason we dropped GamePad from the library.
okay, but that means that it might be possible to get it running if one would know, which bytes are to be set in the descriptor, in order to make it work correctly with macOS? But as its not documented anywhere its more a try and error approach... :/
I don't know of a working descriptor for macOS. In the past, we added gamepad here: https://github.com/adafruit/circuitpython/pull/776, and then later removed it. The comments in that PR indicate that macOS could see the gamepad via (new URL) https://hardwaretester.com/gamepad.
It sounds like applications can see the gamepad, but there may be no associated Preferences. It might be System Information, and you can list USB devices with ioreg -p IOUSB
.
As for the button numbers, that is some mapping we don't have control over, and I think you will just need to try them to see the mapping. I don't know where that would be documented.
Thanks @dhalbert I tried a few things but gave up. I then gave the pico board a try, by using Arduino IDE and installed the boards-package for the RP2040.
I then used the Adafruit USB_HID library and the pico was immediately recognized by X-Plane as a joystick input device. I managed to get everything to work. I have no clue what they do differently... I wanted to look into their code for the protocol, but was not able to find the C-libs for that part. Adafruit itself has only docs for python?
To be honest, I'd rather would use python top write my code. My Arduino C-skills are insufficient.
Closing this as it does not appear to be an issue specific to this library There are some TinyUSB improvements that would help in the long run.
It will be good to provide the support for generic HID device support.
Example: Jan Axelson provides example generic HID device (looping back Output Report to Input Report, looping back Feature OUT report to Feature IN report). http://janaxelson.com/hidpage.htm