adafruit / Adafruit_CircuitPython_HID

USB Human Interface Device drivers.
MIT License
364 stars 106 forks source link

Generic HID device support #115

Closed mcuee closed 7 months ago

mcuee commented 1 year ago

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

dhalbert commented 1 year 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

mcuee commented 1 year ago

Thanks. I will close this issue then.

mcuee commented 1 year ago

For now I use Adafruit_TinyUSB_Arduino instead. https://github.com/adafruit/Adafruit_TinyUSB_Arduino/tree/master/examples/HID/hid_generic_inout

mcuee commented 1 year ago

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)
)
mcuee commented 1 year ago

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
dhalbert commented 1 year ago
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 ?)

mcuee commented 1 year ago

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:
mcuee commented 1 year ago

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
mcuee commented 1 year ago

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
mcuee commented 1 year ago

BTW, this is part of my efforts to create test devices for HIDAPI project.

dhalbert commented 1 year ago

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

dhalbert commented 1 year ago

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?

mcuee commented 1 year ago

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...
mcuee commented 1 year ago

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:
mcuee commented 1 year ago

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?

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
mcuee commented 1 year ago

@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
todbot commented 1 year ago

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
dhalbert commented 1 year ago

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.

mcuee commented 1 year ago

@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
mcuee commented 1 year ago

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
mcuee commented 1 year ago

@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

dhalbert commented 1 year ago

@mcuee @todbot

Feature report lengths

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.

Maximum report lengths

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)

Feature reports possible bug.

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

OUT Report report_id heuristic

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;
    }
marschro commented 9 months ago

Hi there :)

I struggle with getting a USB HID gamepad up and running for macOS.

What I did:

  1. I have a Raspberry Pico
  2. I set up some On/Off Switches on the Pico
  3. I installed CircuitPython 8.x on the Pico
  4. I added the adafruit_hid library to the Pico and also the hid_gamepad.py from the examples
  5. I created a boot.py file on the Pico
  6. I added a bit of code for testing - currently just one single switch

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

marschro commented 9 months ago

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

dhalbert commented 9 months ago

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.

marschro commented 9 months ago

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... :/

dhalbert commented 9 months ago

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.

marschro commented 9 months ago

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.

dhalbert commented 7 months ago

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.