Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gamecontroller setup under mac #126

Open
horsto opened this issue May 22, 2024 · 9 comments
Open

Gamecontroller setup under mac #126

horsto opened this issue May 22, 2024 · 9 comments

Comments

@horsto
Copy link

horsto commented May 22, 2024

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
Copy link
Author

horsto commented May 22, 2024

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
Copy link
Collaborator

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: adafruit/circuitpython#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
Copy link
Author

horsto commented May 22, 2024

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
�[2K�[0Gboot.py output:

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

@dhalbert
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Author

horsto commented May 26, 2024

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
Copy link
Author

horsto commented Jun 6, 2024

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

@ktnyt
Copy link

ktnyt commented Oct 19, 2024

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.
adafruit/Adafruit_CircuitPython_BLE#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
Copy link

ktnyt commented Oct 19, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants