Exposing Steam Controller as HID in new Input System

Hello,

I’ve beem trying to add support for my SteamController, since I couldn’t hook it up out of the box.
I’ve done some research and found this:
https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/HID.html

Then I’ve found some references for Hid Inputs for Steam Controller here:

I’ve followed along creating InputDevice/Gamepad and HID Input Report as follows:

SteamControllerHID.cs

#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;

namespace CFG.Runtime.Input
{
    // Using InputControlLayoutAttribute, we tell the system about the state
    // struct we created, which includes where to find all the InputControl
    // attributes that we placed on there. This is how the Input System knows
    // what controls to create and how to configure them.
    [InputControlLayout(stateType = typeof(SteamControllerHIDInputReport2))]
#if UNITY_EDITOR
    [InitializeOnLoad] // Make sure static constructor is called during startup.
#endif
    public class SteamControllerHID :
        //Gamepad
        InputDevice
    {
        static SteamControllerHID()
        {
            // This is one way to match the Device.
            //InputSystem.RegisterLayout<SteamControllerHID>(name:null,
            //    new InputDeviceMatcher()
            //        .WithInterface("HID")
            //        .WithManufacturer("Valve Software")
            //        .WithProduct("Steam Controller"));//Wireless Steam Controller
            //InputSystem.RegisterLayout<SteamControllerHID>(name: null,
            //    new InputDeviceMatcher()
            //        .WithInterface("HID")
            //        .WithManufacturer("Valve Software")
            //        .WithProduct("Valve"));//Wired Controller

            // Alternatively, you can also match by PID and VID, which is generally
            // more reliable for HIDs.
            InputSystem.RegisterLayout<SteamControllerHID>(name: "Wireless Steam Controller",
                matches: new InputDeviceMatcher()
                    .WithInterface("HID")
                    .WithCapability("vendorId", 0x28de) // Valve Software
                    .WithCapability("productId", 0x1142)); // Wireless Steam Controller
            InputSystem.RegisterLayout<SteamControllerHID>(name: "Wired Steam Controller",
                matches: new InputDeviceMatcher()
                    .WithInterface("HID")
                    .WithCapability("vendorId", 0x28de) // Valve Software
                    .WithCapability("productId", 0x1102)); // Wired Controller
        }

        // In the Player, to trigger the calling of the static constructor,
        // create an empty method annotated with RuntimeInitializeOnLoadMethod.
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void Init() { }
    }
}

SteamControllerHIDInputReport2.cs

using System.Runtime.InteropServices;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;

namespace CFG.Runtime.Input
{
    // We receive data as raw HID input reports. This struct
    // describes the raw binary format of such a report.
    [StructLayout(LayoutKind.Explicit, Size = 60)]
    public struct SteamControllerHIDInputReport2 : IInputStateTypeInfo
    {
        // Because all HID input reports are tagged with the 'HID ' FourCC,
        // this is the format we need to use for this state struct.
        public FourCC format => new FourCC('H', 'I', 'D');

        //FieldOffset 5-8 sequence number
        [InputControl(name = "rightTriggerButton", layout = "Button", bit = 0)]
        [InputControl(name = "leftTriggerButton", layout = "Button", bit = 1)]
        [InputControl(name = "rightShoulder", layout = "Button", bit = 2)]
        [InputControl(name = "leftShoulder", layout = "Button", bit = 3)]
        [InputControl(name = "buttonNorth", displayName = "Y", layout = "Button", bit = 4)]
        [InputControl(name = "buttonEast", displayName = "B", layout = "Button", bit = 5)]
        [InputControl(name = "buttonWest", displayName = "X", layout = "Button", bit = 6)]
        [InputControl(name = "buttonSouth", displayName = "A", layout = "Button", bit = 7)]
        [FieldOffset(9)] public byte buttonsSet1;
        [InputControl(name = "leftPad", layout = "Stick", format = "BYTE")]
        [InputControl(name = "leftPad/Up", layout = "Button", bit = 0)]
        [InputControl(name = "leftPad/Right", layout = "Button", bit = 1)]
        [InputControl(name = "leftPad/Left", layout = "Button", bit = 2)]
        [InputControl(name = "leftPad/Down", layout = "Button", bit = 3)]
        [InputControl(name = "select", displayName = "Back", layout = "Button", bit = 4)]
        [InputControl(name = "systemButton", displayName = "Steam Home Button", layout = "Button", bit = 5)]
        [InputControl(name = "start", displayName = "Start", layout = "Button", bit = 6)]
        [InputControl(name = "leftGrip", layout = "Button", bit = 7)]
        [FieldOffset(10)] public byte buttonsSet2;
        [InputControl(name = "righPad", layout = "Stick", format = "BYTE")]
        [InputControl(name = "rightGrip", layout = "Button", bit = 0)]
        [InputControl(name = "leftPad/Clicked", layout = "Button", bit = 1)]
        [InputControl(name = "righPad/Clicked", layout = "Button", bit = 2)]
        [InputControl(name = "leftDpadTouched", displayName = "Left Dpad Touched", layout = "Button", bit = 3)]
        [InputControl(name = "rightDpadTouched", displayName = "Right Dpad Touched", layout = "Button", bit = 4)]
        //offset 5 unknown
        [InputControl(name = "joystick_clicked", layout = "Button", bit = 6)]
        [InputControl(name = "lpad_and_joy", layout = "Button", bit = 7)]
        [FieldOffset(11)] public byte buttonsSet3;


        [InputControl(name = "leftTrigger", layout = "Button", format = "BYTE")]
        [FieldOffset(12)] public byte leftTrigger;
        [InputControl(name = "rightTrigger", layout = "Button", format = "BYTE")]
        [FieldOffset(13)] public byte q1;
        //FieldOffset 14-16 always 0
        [InputControl(name = "leftStickX0", layout = "Button", format = "BYTE")]
        [FieldOffset(17)] public byte leftStickX0;
        [InputControl(name = "leftStickX1", layout = "Button", format = "BYTE")]
        [FieldOffset(18)] public byte leftStickX1;
        [InputControl(name = "leftStickY0", layout = "Button", format = "BYTE")]
        [FieldOffset(19)] public byte leftStickY0;
        [InputControl(name = "leftStickY1", layout = "Button", format = "BYTE")]
        [FieldOffset(20)] public byte leftStickY1;
    }
}

I was able to get most of the inputs right but the stick and touch pads are more tricky.
I wanted at least to add joystick support for basic movement but I have a problem with getting correct values. Because it looks like X and Y axis data are returned in signed16 type.
So I need to merge/aggregate two bytes into one layout entry but I tried different things with InputControlAttribute without any success.

Here are values that I’ve managed to retrieve:

Pressing down on the joytick x=0, y=-1 :

Pressing left on the joystick x=-1, y = 0 :


Pressing up on the joystick x=0, y=1 :

Pressing right on the joystick x=1, y=0 :

When joystick is in default position X0, X1, Y0, Y1 values are set to 0

So i see that values respond to joystick but I think I need to somehow aggregate two bytes for X and two bytes for Y, and then normalize it in attribute with parameters = "normalize,normalizeMin…

Maybe someone knows how to aggregate those values?

2 Likes

Ok , after some sleep I was able to fix this

First version that worked:

[InputControl(name = "leftStickX", layout = "Button", format = "SHRT",
            parameters = "normalize,normalizeMin=-0.9999695,normalizeMax=0.9999695,normalizeZero=0")]
        [FieldOffset(17)] public short leftStickX;
        [InputControl(name = "leftStickY", layout = "Button", format = "SHRT",
            parameters = "normalize,normalizeMin=-0.9999695,normalizeMax=0.9999695,normalizeZero=0")]
        [FieldOffset(19)] public short leftStickY;

Second version for Gamepad layout:

 [InputControl(name = "leftStick", layout = "Stick", format = "VC2S")]
        [InputControl(name = "leftStick/x", offset = 0, layout = "Axis", format = "SHRT",
        parameters = "normalize,normalizeMin=-0.9999695,normalizeMax=0.9999695,normalizeZero=0")]
        [InputControl(name = "leftStick/left", offset = 0,
        parameters = "normalize,normalizeMin=-1,normalizeMax=1,normalizeZero=-0.5,clamp,clampMin=-1,clampMax=0,invert")]
        [InputControl(name = "leftStick/right", offset = 0,
        parameters = "normalize,normalizeMin=-1,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0,clampMax=1")]
        [InputControl(name = "leftStick/y", offset = 2, layout = "Axis", format = "SHRT",
        parameters = "normalize,normalizeMin=-0.9999695,normalizeMax=0.9999695,normalizeZero=0")]
        [InputControl(name = "leftStick/up", offset = 2,
        parameters = "normalize,normalizeMin=-1,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0,clampMax=1,invert=false")]
        [InputControl(name = "leftStick/down", offset = 2,
        parameters = "normalize,normalizeMin=-1,normalizeMax=1,normalizeZero=-0.5,clamp,clampMin=-1,clampMax=0,invert")]
        [FieldOffset(17)] public short leftStickX;
        [FieldOffset(19)] public short leftStickY;

Useful documentation:

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.Layouts.InputControlAttribute.html
https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.LowLevel.InputStateBlock.html
https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.Utilities.FourCC.html
https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.Controls.AxisControl.html

4 Likes