adafruit / Adafruit_CircuitPython_HID

USB Human Interface Device drivers.
MIT License
377 stars 105 forks source link

Gamecontroller setup under mac #126

Open horsto opened 6 months ago

horsto commented 6 months ago

I am using https://learn.adafruit.com/gamepad-qt with https://learn.adafruit.com/esp32-s3-reverse-tft-feather.

My goal is to let the gamepad be recognized as gamecontroller input on my Mac to play a game with it. I have looked at the gamepad example here: https://docs.circuitpython.org/projects/hid/en/latest/examples.html#id4 and added the gamepad boiler plate code shown here: https://learn.adafruit.com/customizing-usb-devices-in-circuitpython/hid-devices#custom-hid-devices-3096614-9 to my boot.py. This doesn't run into any errors, but also does not lead to recognition of a gamecontroller as input device under MacOS.

Has anybody tried this?

horsto commented 6 months ago

code.py

import board
import usb_hid
import board
from micropython import const

from hid_gamepad import Gamepad
gp = Gamepad(usb_hid.devices)

from adafruit_seesaw.seesaw import Seesaw

# Equivalent of Arduino's map() function.
def range_map(x, in_min, in_max, out_min, out_max):
    return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min

BUTTON_X = const(6)
BUTTON_Y = const(2)
BUTTON_A = const(5)
BUTTON_B = const(1)
BUTTON_SELECT = const(0)
BUTTON_START = const(16)
button_mask = const(
    (1 << BUTTON_X)
    | (1 << BUTTON_Y)
    | (1 << BUTTON_A)
    | (1 << BUTTON_B)
    | (1 << BUTTON_SELECT)
    | (1 << BUTTON_START)
)

i2c_bus = board.STEMMA_I2C()  # The built-in STEMMA QT connector on the microcontroller
# i2c_bus = board.I2C()  # Uses board.SCL and board.SDA. Use with breadboard.

gamepad1 = Seesaw(i2c_bus, addr=0x50)
gamepad1.pin_mode_bulk(button_mask, gamepad1.INPUT_PULLUP)

# Equivalent of Arduino's map() function.
def range_map(x, in_min, in_max, out_min, out_max):
    return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min

last_x = 0
last_y = 0

while True:
    x = 1023 - gamepad1.analog_read(14)
    y = 1023 - gamepad1.analog_read(15)

    if (abs(x - last_x) > 3) or (abs(y - last_y) > 3):
        print(x, y)
        last_x = x
        last_y = y

    buttons = gamepad1.digital_read_bulk(button_mask)

    if not buttons & (1 << BUTTON_X):
        print("Button x pressed")
        gp.click_buttons(5)

    # # 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(" release", gamepad_button_num, end="")
    #     else:
    #         gp.press_buttons(gamepad_button_num)
    #         print(" press", gamepad_button_num, end="")

    # Convert range[0, 65535] to -127 to 127
    gp.move_joysticks(
        x=range_map(x, 0, 1023, -127, 127),
        y=range_map(y, 0, 1023, -127, 127),
    )
    # print(" x", ax.value, "y", ay.value)

boot.py

import usb_hid

# This is only one example of a gamepad report 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,
     usb_hid.Device.MOUSE,
     usb_hid.Device.CONSUMER_CONTROL,
     gamepad)
)
dhalbert commented 6 months ago

Check that boot_out.txt does not show any errors. Also try including only the gamepad device in the usb_hid.enable(...). A long time ago, we discovered that the gamepad device needed to go after the mouse: https://github.com/adafruit/circuitpython/pull/4558, which you did, but it macOS could be finicky in other ways.

Try this also on a Windows machine, if you have one, and see if it shows up.

horsto commented 6 months ago

Thanks for the quick reply! boot_out.txt seems clean:

Adafruit CircuitPython 9.0.5 on 2024-05-22; Adafruit Feather ESP32-S3 Reverse TFT with ESP32S3
Board ID:adafruit_feather_esp32s3_reverse_tft
UID:0740D1DCD48C
boot.py output:

I cannot test this on a windows machine. Only leaving gamepad in usb_hid.enable() did not change things for me.

dhalbert commented 6 months ago

I put your boot.py on a CircuitPython board. It works, and presents a gamepad device, which the OS is aware of. This is true on Linux, Windows, and macOS Sonoma 14.5 (I tested on an M1 Mac Mini).

I verified the gamepad's presence with this tool: https://github.com/todbot/hidapitester (there is a .pkg installer in the release). You could try this yourself as well.

% hidapitester --vidpid 239a --list-detail

[other stuff omitted]

239A/8100: Adafruit Industries LLC - NeoKey Trinkey M0
  vendorId:      0x239A
  productId:     0x8100
  usagePage:     0x0001
  usage:         0x0005
  serial_number: D517091D4C51535020312E37301619FF 
  interface:     3 
  path: DevSrvsID:4294974679

However, the question is what macOS is going to do with the gamepad, whether the game can find it, and what your game is expecting. That I don't know, unfortunately.

We found in the past that which gamepads are usable on which OS's varies. It can even depend on the range of the X/Y/Z values. You might ask in our discord, https://adafru.it/discord, whether anyone has succeeded in making a usable gamepad on macOS

dhalbert commented 6 months ago

Here is another "proof of life" for the Gamepad controller. I installed a free app, Controllers Lite, from the App Store. You have to give it permission to monitor input events (when you run it the first time it will be obvious what to do, I think). Here you can see that it is seeing the Gamepad in boot.py:

Screenshot 2024-05-22 at 8 47 23 PM

A hypothesis is that your game doesn't like this particular gamepad definition, for some reason. If you know of a gamepad that works with the game, you could find out its HID report descriptor and emulate that. I know there are also remappers that will transform one kind of gamepad into another, though I know very little about them other than that they exist.

horsto commented 6 months ago

Thanks for the input, this is great. I did try Controller Lite, and yes, it shows up. I also tried emulating XBOX and Playstation controllers (I changed the report descriptor accordingly), but without success.

I am trying to run STEAM games, but those do not recognize my (custom) controller as valid input device, nor does STEAM itself.

I have reached out on Discord, hoping somebody tried something similar.

Also tried https://steamcommunity.com/discussions/forum/2/3117025249776480006/?ctp=6

horsto commented 5 months ago

I have not gotten much further on this. Any input would be appreciated!

ktnyt commented 1 month ago

Hi I know I'm pretty late on this and I haven't quite broken through to getting things to work fully, but I managed to get Final Fantasy XIV to recognize my device as a GamePad by setting a BLE Vendor ID and Product ID. https://github.com/adafruit/Adafruit_CircuitPython_BLE/pull/202

I haven't made my jump to macOS Sequoia just yet so I can't confirm if this will work but maybe using supervisor.set_usb_identification will work for you. And just as a sidenote, I was informed sometime ago that only Bluetooth Gamepads are usable on macOS pre-Sequoia. https://docs.circuitpython.org/en/latest/shared-bindings/supervisor/#supervisor.set_usb_identification

As for my current status, I have been able to get my mac to recognize my gamepad by haven't been able to actually send reports that yield any action in game yet. I've confirmed that the reports are being received on the mac using the https://hardwaretester.com/gamepad website so my current hypothesis is that the game requires a properly crafted HID descriptor to accept input.

FYI here's the descriptor that I'm trying to get to work for Vendor ID 0x045e and Product ID 0x02e0. The following descriptor requires tweaking the adafruit_ble library though and I'm still going through the process of tinkering with the HDIService class so I'll come back for updates if I manage to sort things out.

GAMEPAD_REPORT_DESCRIPTOR = bytes([
    0x05, 0x01,                   # Usage Page (Generic Desktop Ctrls)
    0x09, 0x05,                   # Usage (Game Pad)
    0xA1, 0x01,                   # Collection (Application)
    0x85, 0x01,                   #   Report ID (1)

    0x09, 0x01,                   #   Usage (Pointer)
    0xA1, 0x00,                   #   Collection (Physical)
    0x09, 0x30,                   #     Usage (X)
    0x09, 0x31,                   #     Usage (Y)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x27, 0xFF, 0xFF, 0x00, 0x00, #     Logical Maximum (65534)
    0x95, 0x02,                   #     Report Count (2)
    0x75, 0x10,                   #     Report Size (16)
    0x81, 0x02,                   #     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0xC0,                         #   End Collection

    0x09, 0x01,                   #   Usage (Pointer)
    0xA1, 0x00,                   #   Collection (Physical)
    0x09, 0x33,                   #     Usage (Rx)
    0x09, 0x34,                   #     Usage (Ry)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x27, 0xFF, 0xFF, 0x00, 0x00, #     Logical Maximum (65534)
    0x95, 0x02,                   #     Report Count (2)
    0x75, 0x10,                   #     Report Size (16)
    0x81, 0x02,                   #     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0xC0,                         #   End Collection

    0x05, 0x02,                   #   Usage Page (Generic Desktop Ctrls)
    0x09, 0x32,                   #   Usage (Z)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x26, 0xFF, 0x03,             #   Logical Maximum (1023)
    0x95, 0x01,                   #   Report Count (1)
    0x75, 0x0A,                   #   Report Size (10)
    0x81, 0x02,                   #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x25, 0x00,                   #   Logical Maximum (0)
    0x75, 0x06,                   #   Report Size (6)
    0x95, 0x01,                   #   Report Count (1)
    0x81, 0x03,                   #   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

    0x05, 0x01,                   #   Usage Page (Generic Desktop Ctrls)
    0x09, 0x35,                   #   Usage (Rz)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x26, 0xFF, 0x03,             #   Logical Maximum (1023)
    0x95, 0x01,                   #   Report Count (1)
    0x75, 0x0A,                   #   Report Size (10)
    0x81, 0x02,                   #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x25, 0x00,                   #   Logical Maximum (0)
    0x75, 0x06,                   #   Report Size (6)
    0x95, 0x01,                   #   Report Count (1)
    0x81, 0x03,                   #   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

    0x05, 0x01,                   #   Usage Page (Generic Desktop Ctrls)
    0x09, 0x39,                   #   Usage (Hat switch)
    0x15, 0x01,                   #   Logical Minimum (1)
    0x25, 0x08,                   #   Logical Maximum (8)
    0x35, 0x00,                   #   Physical Minimum (0)
    0x46, 0x3B, 0x01,             #   Physical Maximum (315)
    0x66, 0x14, 0x00,             #   Unit (System: English Rotation, Length: Centimeter)
    0x75, 0x04,                   #   Report Size (4)
    0x95, 0x01,                   #   Report Count (1)
    0x81, 0x42,                   #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
    0x75, 0x04,                   #   Report Size (4)
    0x95, 0x01,                   #   Report Count (1)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x25, 0x00,                   #   Logical Maximum (0)
    0x35, 0x00,                   #   Physical Minimum (0)
    0x45, 0x00,                   #   Physical Maximum (0)
    0x65, 0x00,                   #   Unit (None)
    0x81, 0x03,                   #   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

    0x05, 0x09,                   #   Usage Page (Button)
    0x19, 0x01,                   #   Usage Minimum (0x01)
    0x29, 0x0A,                   #   Usage Maximum (0x0A)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x25, 0x01,                   #   Logical Maximum (1)
    0x75, 0x01,                   #   Report Size (1)
    0x95, 0x0A,                   #   Report Count (10)
    0x81, 0x02,                   #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x25, 0x00,                   #   Logical Maximum (0)
    0x75, 0x06,                   #   Report Size (6)
    0x95, 0x01,                   #   Report Count (1)
    0x81, 0x03,                   #   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

    0x05, 0x01,                   #   Usage Page (Generic Desktop Ctrls)
    0x09, 0x80,                   #   Usage (Sys Control)
    0x85, 0x02,                   #   Report ID (2)
    0xA1, 0x00,                   #   Collection (Physical)
    0x09, 0x85,                   #     Usage (Sys Main Menu)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x25, 0x01,                   #     Logical Maximum (1)
    0x95, 0x01,                   #     Report Count (1)
    0x75, 0x01,                   #     Report Size (1)
    0x81, 0x02,                   #     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x25, 0x00,                   #     Logical Maximum (0)
    0x75, 0x07,                   #     Report Size (7)
    0x95, 0x01,                   #     Report Count (1)
    0x81, 0x03,                   #     Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0xC0,                         #   End Collection
    0x05, 0x0F,                   #   Usage Page (PID Page)
    0x09, 0x21,                   #   Usage (0x21)

    0x85, 0x03,                   #   Report ID (3)
    0xA1, 0x02,                   #   Collection (Logical)
    0x09, 0x97,                   #     Usage (0x97)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x25, 0x01,                   #     Logical Maximum (1)
    0x75, 0x04,                   #     Report Size (4)
    0x95, 0x01,                   #     Report Count (1)
    0x91, 0x02,                   #     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x25, 0x00,                   #     Logical Maximum (0)
    0x75, 0x04,                   #     Report Size (4)
    0x95, 0x01,                   #     Report Count (1)
    0x91, 0x03,                   #     Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x09, 0x70,                   #     Usage (0x70)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x25, 0x64,                   #     Logical Maximum (100)
    0x75, 0x08,                   #     Report Size (8)
    0x95, 0x04,                   #     Report Count (4)
    0x91, 0x02,                   #     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x09, 0x50,                   #     Usage (0x50)
    0x66, 0x01, 0x10,             #     Unit (System: SI Linear, Time: Seconds)
    0x55, 0x0E,                   #     Unit Exponent (-2)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x26, 0xFF, 0x00,             #     Logical Maximum (255)
    0x75, 0x08,                   #     Report Size (8)
    0x95, 0x01,                   #     Report Count (1)
    0x91, 0x02,                   #     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x09, 0xA7,                   #     Usage (0xA7)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x26, 0xFF, 0x00,             #     Logical Maximum (255)
    0x75, 0x08,                   #     Report Size (8)
    0x95, 0x01,                   #     Report Count (1)
    0x91, 0x02,                   #     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0x65, 0x00,                   #     Unit (None)
    0x55, 0x00,                   #     Unit Exponent (0)
    0x09, 0x7C,                   #     Usage (0x7C)
    0x15, 0x00,                   #     Logical Minimum (0)
    0x26, 0xFF, 0x00,             #     Logical Maximum (255)
    0x75, 0x08,                   #     Report Size (8)
    0x95, 0x01,                   #     Report Count (1)
    0x91, 0x02,                   #     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
    0xC0,                         #   End Collection

    0x85, 0x04,                   #   Report ID (4)
    0x05, 0x06,                   #   Usage Page (Generic Dev Ctrls)
    0x09, 0x20,                   #   Usage (Battery Strength)
    0x15, 0x00,                   #   Logical Minimum (0)
    0x26, 0xFF, 0x00,             #   Logical Maximum (255)
    0x75, 0x08,                   #   Report Size (8)
    0x95, 0x01,                   #   Report Count (1)
    0x81, 0x02,                   #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

    0xC0,                         # End Collection
])
ktnyt commented 1 month ago

Hi a very quick update indeed: I got this to work and it was just as I speculated! The following is the code required to reproduce. Note that I'm using Seeed XIAO Sense nrf52840 with an IMU so do alter the reporting code according to your needs! https://gist.github.com/ktnyt/33ad0462a0e7cc49176c78530e320291

tkomde commented 1 day ago

Hi, all

I was curious to find out more about the USB connection. The environment is as follows.

Board : M5Stack AtomS3 Connection: USB Machine : M2 Mac Mini OS: 15.1.1

First of all, the definition of “Gamepad is recognized by Mac” that I adopted is what is shown in System Settings - > Gamepad. hardwaretester.com almost always recognizes if Usage is Gamepad (0x09, 0x05, # Usage (Gamepad)), but I was concerned that in some cases it is not recognized on the Mac OS side. So I tried some tests on Gamepad on Mac OS, and it seems that the following two conditions must be met in order for it to be recognized.

(1) It has all the standard Gamepad Usages

There is a description of the standard mappings of Gamepad (https://learn.microsoft.com/en-us/windows/win32/xinput/directinput-and-xusb-devices). If it has all of these elements, it can be considered a standard gamepad. If you remove the items, it is no longer recognized.

import usb_hid

gamepad = usb_hid.Device(
    report_descriptor=bytes((
        0x05, 0x01,  # Usage Page (Generic Desktop)
        0x09, 0x05,  # Usage (Gamepad)
        0xa1, 0x01,  # Collection (Application)
        0x85, 0x01,  # Report ID
        0xa1, 0x02,  # Collection (logical)
        0x05, 0x01,  # Usage Page (Generic Desktop)
        0x09, 0x30,  # Usage (X)
        0x09, 0x31,  # Usage (Y)
        0x09, 0x32,  # Usage (Z)
        0x09, 0x33,  # Usage (RX)
        0x09, 0x34,  # Usage (RY)
        0x09, 0x39,  # Usage (Hat Switch)
        0x15, 0x80,  # Logical Minimum (-128)
        0x25, 0x7F,  # Logical Maximum (127)
        0x75, 0x08,  # Report Size (8)
        0x95, 0x06,  # Report Count (6)
        0x81, 0x02,  # Input (Data, Variable, Absolute)
        0xc0,  # End Collection
        0xa1, 0x02,  # Collection (logical)
        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, Variable, Absolute)
        0xc0,  # End Collection
        0xc0         # End Collection
    )),
    usage_page=0x01,
    usage=0x05,
    report_ids=(0x01,),
    in_report_lengths=(8, ),
    out_report_lengths=(0, )
)

usb_hid.enable([gamepad])

(2) It has a specific USB vendor/product id.

Before trying with CircuitPython, I first tried Logitech and Elecom USB Gamepads in Direct Input mode. Both are recognized fine on Windows, but the latter was not recognized on the Mac. So I specified the vendor/product id of the Logitech Gamepad on my board(M5StacK AtomS3) and it showed up in the OS settings.

supervisor.set_usb_identification(vid=0x046D, pid=0xC218)

The following is the recognition status of the Mac and the hardwaretester.com when I changed only the pid with the same reportmap.

Recognized

Screenshot 2024-11-23 at 20 09 24

Not recognized(only pid changed)

Screenshot 2024-11-23 at 21 02 23

(2) may be controlled by Apple in some way. Try at your own risk.