adafruit / Adafruit_CircuitPython_HID

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

Gamecontroller setup under mac #126

Open horsto opened 1 month ago

horsto commented 1 month 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 1 month 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 1 month 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 1 month 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 1 month 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 1 month 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 1 month 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 4 weeks ago

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