Firstborn/Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Plugins/HID/HID.cs
Schaken-Mods b486678290 Library -Artifacts
Library -Artifacts
2023-03-28 12:24:16 -05:00

1443 lines
63 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.Scripting;
#if UNITY_2021_2_OR_NEWER
using UnityEngine.Pool;
#endif
// HID support is currently broken in 32-bit Windows standalone players. Consider 32bit Windows players unsupported for now.
#if UNITY_STANDALONE_WIN && !UNITY_64
#warning The 32-bit Windows player is not currently supported by the Input System. HID input will not work in the player. Please use x86_64, if possible.
#endif
////REVIEW: there will probably be lots of cases where the HID device creation process just needs a little tweaking; we should
//// have better mechanism to do that without requiring to replace the entire process wholesale
////TODO: expose the layout builder so that other layout builders can use it for their own purposes
////REVIEW: how are we dealing with multiple different input reports on the same device?
////REVIEW: move the enums and structs out of here and into UnityEngine.InputSystem.HID? Or remove the "HID" name prefixes from them?
////TODO: add blacklist for devices we really don't want to use (like apple's internal trackpad)
////TODO: add a way to mark certain layouts (such as HID layouts) as fallbacks; ideally, affect the layout matching score
////TODO: enable this to handle devices that split their input into multiple reports
#pragma warning disable CS0649, CS0219
namespace UnityEngine.InputSystem.HID
{
/// <summary>
/// A generic HID input device.
/// </summary>
/// <remarks>
/// This class represents a best effort to mirror the control setup of a HID
/// discovered in the system. It is used only as a fallback where we cannot
/// match the device to a specific product we know of. Wherever possible we
/// construct more specific device representations such as Gamepad.
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
public class HID : InputDevice
{
internal const string kHIDInterface = "HID";
internal const string kHIDNamespace = "HID";
/// <summary>
/// Command code for querying the HID report descriptor from a device.
/// </summary>
/// <seealso cref="InputDevice.ExecuteCommand{TCommand}"/>
public static FourCC QueryHIDReportDescriptorDeviceCommandType { get { return new FourCC('H', 'I', 'D', 'D'); } }
/// <summary>
/// Command code for querying the HID report descriptor size in bytes from a device.
/// </summary>
/// <seealso cref="InputDevice.ExecuteCommand{TCommand}"/>
public static FourCC QueryHIDReportDescriptorSizeDeviceCommandType { get { return new FourCC('H', 'I', 'D', 'S'); } }
public static FourCC QueryHIDParsedReportDescriptorDeviceCommandType { get { return new FourCC('H', 'I', 'D', 'P'); } }
/// <summary>
/// The HID device descriptor as received from the system.
/// </summary>
public HIDDeviceDescriptor hidDescriptor
{
get
{
if (!m_HaveParsedHIDDescriptor)
{
if (!string.IsNullOrEmpty(description.capabilities))
m_HIDDescriptor = JsonUtility.FromJson<HIDDeviceDescriptor>(description.capabilities);
m_HaveParsedHIDDescriptor = true;
}
return m_HIDDescriptor;
}
}
private bool m_HaveParsedHIDDescriptor;
private HIDDeviceDescriptor m_HIDDescriptor;
// This is the workhorse for figuring out fallback options for HIDs attached to the system.
// If the system cannot find a more specific layout for a given HID, this method will try
// to produce a layout builder on the fly based on the HID descriptor received from
// the device.
internal static string OnFindLayoutForDevice(ref InputDeviceDescription description, string matchedLayout,
InputDeviceExecuteCommandDelegate executeDeviceCommand)
{
// If the system found a matching layout, there's nothing for us to do.
if (!string.IsNullOrEmpty(matchedLayout))
return null;
// If the device isn't a HID, we're not interested.
if (description.interfaceName != kHIDInterface)
return null;
// Read HID descriptor.
var hidDeviceDescriptor = ReadHIDDeviceDescriptor(ref description, executeDeviceCommand);
if (!HIDSupport.supportedHIDUsages.Contains(new HIDSupport.HIDPageUsage(hidDeviceDescriptor.usagePage, hidDeviceDescriptor.usage)))
return null;
// Determine if there's any usable elements on the device.
var hasUsableElements = false;
if (hidDeviceDescriptor.elements != null)
{
foreach (var element in hidDeviceDescriptor.elements)
{
if (element.IsUsableElement())
{
hasUsableElements = true;
break;
}
}
}
// If not, there's nothing we can do with the device.
if (!hasUsableElements)
return null;
////TODO: we should be able to differentiate a HID joystick from other joysticks in bindings alone
// Determine base layout.
var baseType = typeof(HID);
var baseLayout = "HID";
if (hidDeviceDescriptor.usagePage == UsagePage.GenericDesktop)
{
if (hidDeviceDescriptor.usage == (int)GenericDesktop.Joystick || hidDeviceDescriptor.usage == (int)GenericDesktop.Gamepad)
{
baseLayout = "Joystick";
baseType = typeof(Joystick);
}
}
// A HID may implement the HID interface arbitrary many times, each time with a different
// usage page + usage combination. In a OS, this will typically come out as multiple separate
// devices. Thus, to make layout names unique, we have to take usages into account. What we do
// is we tag the usage name onto the layout name *except* if it's a joystick or gamepad. This
// gives us nicer names for joysticks while still disambiguating other devices correctly.
var usageName = "";
if (baseLayout != "Joystick")
{
usageName = hidDeviceDescriptor.usagePage == UsagePage.GenericDesktop
? $" {(GenericDesktop) hidDeviceDescriptor.usage}"
: $" {hidDeviceDescriptor.usagePage}-{hidDeviceDescriptor.usage}";
}
////REVIEW: these layout names are impossible to bind to; come up with a better way
////TODO: match HID layouts by vendor and product ID
////REVIEW: this probably works fine for most products out there but I'm not sure it works reliably for all cases
// Come up with a unique template name. HIDs are required to have product and vendor IDs.
// We go with the string versions if we have them and with the numeric versions if we don't.
string layoutName;
var deviceMatcher = InputDeviceMatcher.FromDeviceDescription(description);
if (!string.IsNullOrEmpty(description.product) && !string.IsNullOrEmpty(description.manufacturer))
{
layoutName = $"{kHIDNamespace}::{description.manufacturer} {description.product}{usageName}";
}
else if (!string.IsNullOrEmpty(description.product))
{
layoutName = $"{kHIDNamespace}::{description.product}{usageName}";
}
else
{
// Sanity check to make sure we really have the data we expect.
if (hidDeviceDescriptor.vendorId == 0)
return null;
layoutName =
$"{kHIDNamespace}::{hidDeviceDescriptor.vendorId:X}-{hidDeviceDescriptor.productId:X}{usageName}";
deviceMatcher = deviceMatcher
.WithCapability("productId", hidDeviceDescriptor.productId)
.WithCapability("vendorId", hidDeviceDescriptor.vendorId);
}
// Also match by usage. See comment above about multiple HID interfaces on the same device.
deviceMatcher = deviceMatcher
.WithCapability("usage", hidDeviceDescriptor.usage)
.WithCapability("usagePage", hidDeviceDescriptor.usagePage);
// Register layout builder that will turn the HID descriptor into an
// InputControlLayout instance.
var layout = new HIDLayoutBuilder
{
displayName = description.product,
hidDescriptor = hidDeviceDescriptor,
parentLayout = baseLayout,
deviceType = baseType ?? typeof(HID)
};
InputSystem.RegisterLayoutBuilder(() => layout.Build(),
layoutName, baseLayout, deviceMatcher);
return layoutName;
}
internal static unsafe HIDDeviceDescriptor ReadHIDDeviceDescriptor(ref InputDeviceDescription deviceDescription,
InputDeviceExecuteCommandDelegate executeCommandDelegate)
{
if (deviceDescription.interfaceName != kHIDInterface)
throw new ArgumentException(
$"Device '{deviceDescription}' is not a HID");
// See if we have to request a HID descriptor from the device.
// We support having the descriptor directly as a JSON string in the `capabilities`
// field of the device description.
var needToRequestDescriptor = true;
var hidDeviceDescriptor = new HIDDeviceDescriptor();
if (!string.IsNullOrEmpty(deviceDescription.capabilities))
{
try
{
hidDeviceDescriptor = HIDDeviceDescriptor.FromJson(deviceDescription.capabilities);
// If there's elements in the descriptor, we're good with the descriptor. If there aren't,
// we go and ask the device for a full descriptor.
if (hidDeviceDescriptor.elements != null && hidDeviceDescriptor.elements.Length > 0)
needToRequestDescriptor = false;
}
catch (Exception exception)
{
Debug.LogError($"Could not parse HID descriptor of device '{deviceDescription}'");
Debug.LogException(exception);
}
}
////REVIEW: we *could* switch to a single path here that supports *only* parsed descriptors but it'd
//// mean having to switch *every* platform supporting HID to the hack we currently have to do
//// on Windows
// Request descriptor, if necessary.
if (needToRequestDescriptor)
{
// Try to get the size of the HID descriptor from the device.
var sizeOfDescriptorCommand = new InputDeviceCommand(QueryHIDReportDescriptorSizeDeviceCommandType);
var sizeOfDescriptorInBytes = executeCommandDelegate(ref sizeOfDescriptorCommand);
if (sizeOfDescriptorInBytes > 0)
{
// Now try to fetch the HID descriptor.
using (var buffer =
InputDeviceCommand.AllocateNative(QueryHIDReportDescriptorDeviceCommandType, (int)sizeOfDescriptorInBytes))
{
var commandPtr = (InputDeviceCommand*)buffer.GetUnsafePtr();
if (executeCommandDelegate(ref *commandPtr) != sizeOfDescriptorInBytes)
return new HIDDeviceDescriptor();
// Try to parse the HID report descriptor.
if (!HIDParser.ParseReportDescriptor((byte*)commandPtr->payloadPtr, (int)sizeOfDescriptorInBytes, ref hidDeviceDescriptor))
return new HIDDeviceDescriptor();
}
// Update the descriptor on the device with the information we got.
deviceDescription.capabilities = hidDeviceDescriptor.ToJson();
}
else
{
// The device may not support binary descriptors but may support parsed descriptors so
// try the IOCTL for parsed descriptors next.
//
// This path exists pretty much only for the sake of Windows where it is not possible to get
// unparsed/binary descriptors from the device (and where getting element offsets is only possible
// with some dirty hacks we're performing in the native runtime).
const int kMaxDescriptorBufferSize = 2 * 1024 * 1024; ////TODO: switch to larger buffer based on return code if request fails
using (var buffer =
InputDeviceCommand.AllocateNative(QueryHIDParsedReportDescriptorDeviceCommandType, kMaxDescriptorBufferSize))
{
var commandPtr = (InputDeviceCommand*)buffer.GetUnsafePtr();
var utf8Length = executeCommandDelegate(ref *commandPtr);
if (utf8Length < 0)
return new HIDDeviceDescriptor();
// Turn UTF-8 buffer into string.
////TODO: is there a way to not have to copy here?
var utf8 = new byte[utf8Length];
fixed(byte* utf8Ptr = utf8)
{
UnsafeUtility.MemCpy(utf8Ptr, commandPtr->payloadPtr, utf8Length);
}
var descriptorJson = Encoding.UTF8.GetString(utf8, 0, (int)utf8Length);
// Try to parse the HID report descriptor.
try
{
hidDeviceDescriptor = HIDDeviceDescriptor.FromJson(descriptorJson);
}
catch (Exception exception)
{
Debug.LogError($"Could not parse HID descriptor of device '{deviceDescription}'");
Debug.LogException(exception);
return new HIDDeviceDescriptor();
}
// Update the descriptor on the device with the information we got.
deviceDescription.capabilities = descriptorJson;
}
}
}
return hidDeviceDescriptor;
}
public static string UsagePageToString(UsagePage usagePage)
{
return (int)usagePage >= 0xFF00 ? "Vendor-Defined" : usagePage.ToString();
}
public static string UsageToString(UsagePage usagePage, int usage)
{
switch (usagePage)
{
case UsagePage.GenericDesktop:
return ((GenericDesktop)usage).ToString();
case UsagePage.Simulation:
return ((Simulation)usage).ToString();
default:
return null;
}
}
[Serializable]
private class HIDLayoutBuilder
{
public string displayName;
public HIDDeviceDescriptor hidDescriptor;
public string parentLayout;
public Type deviceType;
public InputControlLayout Build()
{
var builder = new InputControlLayout.Builder
{
displayName = displayName,
type = deviceType,
extendsLayout = parentLayout,
stateFormat = new FourCC('H', 'I', 'D')
};
var xElement = Array.Find(hidDescriptor.elements,
element => element.usagePage == UsagePage.GenericDesktop &&
element.usage == (int)GenericDesktop.X);
var yElement = Array.Find(hidDescriptor.elements,
element => element.usagePage == UsagePage.GenericDesktop &&
element.usage == (int)GenericDesktop.Y);
////REVIEW: in case the X and Y control are non-contiguous, should we even turn them into a stick
////REVIEW: there *has* to be an X and a Y for us to be able to successfully create a joystick
// If GenericDesktop.X and GenericDesktop.Y are both present, turn the controls
// into a stick.
var haveStick = xElement.usage == (int)GenericDesktop.X && yElement.usage == (int)GenericDesktop.Y;
if (haveStick)
{
int bitOffset, byteOffset, sizeInBits;
if (xElement.reportOffsetInBits <= yElement.reportOffsetInBits)
{
bitOffset = xElement.reportOffsetInBits % 8;
byteOffset = xElement.reportOffsetInBits / 8;
sizeInBits = (yElement.reportOffsetInBits + yElement.reportSizeInBits) -
xElement.reportOffsetInBits;
}
else
{
bitOffset = yElement.reportOffsetInBits % 8;
byteOffset = yElement.reportOffsetInBits / 8;
sizeInBits = (xElement.reportOffsetInBits + xElement.reportSizeInBits) -
yElement.reportSizeInBits;
}
const string stickName = "stick";
builder.AddControl(stickName)
.WithDisplayName("Stick")
.WithLayout("Stick")
.WithBitOffset((uint)bitOffset)
.WithByteOffset((uint)byteOffset)
.WithSizeInBits((uint)sizeInBits)
.WithUsages(CommonUsages.Primary2DMotion);
var xElementParameters = xElement.DetermineParameters();
var yElementParameters = yElement.DetermineParameters();
builder.AddControl(stickName + "/x")
.WithFormat(xElement.isSigned ? InputStateBlock.FormatSBit : InputStateBlock.FormatBit)
.WithByteOffset((uint)(xElement.reportOffsetInBits / 8 - byteOffset))
.WithBitOffset((uint)(xElement.reportOffsetInBits % 8))
.WithSizeInBits((uint)xElement.reportSizeInBits)
.WithParameters(xElementParameters)
.WithDefaultState(xElement.DetermineDefaultState())
.WithProcessors(xElement.DetermineProcessors());
builder.AddControl(stickName + "/y")
.WithFormat(yElement.isSigned ? InputStateBlock.FormatSBit : InputStateBlock.FormatBit)
.WithByteOffset((uint)(yElement.reportOffsetInBits / 8 - byteOffset))
.WithBitOffset((uint)(yElement.reportOffsetInBits % 8))
.WithSizeInBits((uint)yElement.reportSizeInBits)
.WithParameters(yElementParameters)
.WithDefaultState(yElement.DetermineDefaultState())
.WithProcessors(yElement.DetermineProcessors());
// Propagate parameters needed on x and y to the four button controls.
builder.AddControl(stickName + "/up")
.WithParameters(
StringHelpers.Join(",", yElementParameters, "clamp=2,clampMin=-1,clampMax=0,invert=true"));
builder.AddControl(stickName + "/down")
.WithParameters(
StringHelpers.Join(",", yElementParameters, "clamp=2,clampMin=0,clampMax=1,invert=false"));
builder.AddControl(stickName + "/left")
.WithParameters(
StringHelpers.Join(",", xElementParameters, "clamp=2,clampMin=-1,clampMax=0,invert"));
builder.AddControl(stickName + "/right")
.WithParameters(
StringHelpers.Join(",", xElementParameters, "clamp=2,clampMin=0,clampMax=1"));
}
// Process HID descriptor.
var elements = hidDescriptor.elements;
var elementCount = elements.Length;
for (var i = 0; i < elementCount; ++i)
{
ref var element = ref elements[i];
if (element.reportType != HIDReportType.Input)
continue;
// Skip X and Y if we already turned them into a stick.
if (haveStick && (element.Is(UsagePage.GenericDesktop, (int)GenericDesktop.X) ||
element.Is(UsagePage.GenericDesktop, (int)GenericDesktop.Y)))
continue;
var layout = element.DetermineLayout();
if (layout != null)
{
// Assign unique name.
var name = element.DetermineName();
Debug.Assert(!string.IsNullOrEmpty(name));
name = StringHelpers.MakeUniqueName(name, builder.controls, x => x.name);
// Add control.
var control =
builder.AddControl(name)
.WithDisplayName(element.DetermineDisplayName())
.WithLayout(layout)
.WithByteOffset((uint)element.reportOffsetInBits / 8)
.WithBitOffset((uint)element.reportOffsetInBits % 8)
.WithSizeInBits((uint)element.reportSizeInBits)
.WithFormat(element.DetermineFormat())
.WithDefaultState(element.DetermineDefaultState())
.WithProcessors(element.DetermineProcessors());
var parameters = element.DetermineParameters();
if (!string.IsNullOrEmpty(parameters))
control.WithParameters(parameters);
var usages = element.DetermineUsages();
if (usages != null)
control.WithUsages(usages);
element.AddChildControls(ref element, name, ref builder);
}
}
return builder.Build();
}
}
public enum HIDReportType
{
Unknown,
Input,
Output,
Feature
}
public enum HIDCollectionType
{
Physical = 0x00,
Application = 0x01,
Logical = 0x02,
Report = 0x03,
NamedArray = 0x04,
UsageSwitch = 0x05,
UsageModifier = 0x06
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Flags", Justification = "No better term for underlying data.")]
[Flags]
public enum HIDElementFlags
{
Constant = 1 << 0,
Variable = 1 << 1,
Relative = 1 << 2,
Wrap = 1 << 3,
NonLinear = 1 << 4,
NoPreferred = 1 << 5,
NullState = 1 << 6,
Volatile = 1 << 7,
BufferedBytes = 1 << 8
}
/// <summary>
/// Descriptor for a single report element.
/// </summary>
[Serializable]
public struct HIDElementDescriptor
{
public int usage;
public UsagePage usagePage;
public int unit;
public int unitExponent;
public int logicalMin;
public int logicalMax;
public int physicalMin;
public int physicalMax;
public HIDReportType reportType;
public int collectionIndex;
public int reportId;
public int reportSizeInBits;
public int reportOffsetInBits;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "flags", Justification = "No better term for underlying data.")]
public HIDElementFlags flags;
// Fields only relevant to arrays.
public int? usageMin;
public int? usageMax;
public bool hasNullState => (flags & HIDElementFlags.NullState) == HIDElementFlags.NullState;
public bool hasPreferredState => (flags & HIDElementFlags.NoPreferred) != HIDElementFlags.NoPreferred;
public bool isArray => (flags & HIDElementFlags.Variable) != HIDElementFlags.Variable;
public bool isNonLinear => (flags & HIDElementFlags.NonLinear) == HIDElementFlags.NonLinear;
public bool isRelative => (flags & HIDElementFlags.Relative) == HIDElementFlags.Relative;
public bool isConstant => (flags & HIDElementFlags.Constant) == HIDElementFlags.Constant;
public bool isWrapping => (flags & HIDElementFlags.Wrap) == HIDElementFlags.Wrap;
internal bool isSigned => logicalMin < 0;
internal float minFloatValue
{
get
{
if (isSigned)
{
var minValue = (int)-(long)(1UL << (reportSizeInBits - 1));
var maxValue = (int)((1UL << (reportSizeInBits - 1)) - 1);
return NumberHelpers.IntToNormalizedFloat(logicalMin, minValue, maxValue) * 2.0f - 1.0f;
}
else
{
Debug.Assert(logicalMin >= 0, $"Expected logicalMin to be unsigned");
var maxValue = (uint)((1UL << reportSizeInBits) - 1);
return NumberHelpers.UIntToNormalizedFloat((uint)logicalMin, 0, maxValue);
}
}
}
internal float maxFloatValue
{
get
{
if (isSigned)
{
var minValue = (int)-(long)(1UL << (reportSizeInBits - 1));
var maxValue = (int)((1UL << (reportSizeInBits - 1)) - 1);
return NumberHelpers.IntToNormalizedFloat(logicalMax, minValue, maxValue) * 2.0f - 1.0f;
}
else
{
Debug.Assert(logicalMax >= 0, $"Expected logicalMax to be unsigned");
var maxValue = (uint)((1UL << reportSizeInBits) - 1);
return NumberHelpers.UIntToNormalizedFloat((uint)logicalMax, 0, maxValue);
}
}
}
public bool Is(UsagePage usagePage, int usage)
{
return usagePage == this.usagePage && usage == this.usage;
}
internal string DetermineName()
{
// It's rare for HIDs to declare string names for items and HID drivers may report weird strings
// plus there's no guarantee that these names are unique per item. So, we don't bother here with
// device/driver-supplied names at all but rather do our own naming.
switch (usagePage)
{
case UsagePage.Button:
if (usage == 1)
return "trigger";
return $"button{usage}";
case UsagePage.GenericDesktop:
if (usage == (int)GenericDesktop.HatSwitch)
return "hat";
var text = ((GenericDesktop)usage).ToString();
// Lower-case first letter.
text = char.ToLowerInvariant(text[0]) + text.Substring(1);
return text;
}
// Fallback that generates a somewhat useless but at least very informative name.
return $"UsagePage({usagePage:X}) Usage({usage:X})";
}
internal string DetermineDisplayName()
{
switch (usagePage)
{
case UsagePage.Button:
if (usage == 1)
return "Trigger";
return $"Button {usage}";
case UsagePage.GenericDesktop:
return ((GenericDesktop)usage).ToString();
}
return null;
}
internal bool IsUsableElement()
{
switch (usage)
{
case (int)GenericDesktop.X:
case (int)GenericDesktop.Y:
return usagePage == UsagePage.GenericDesktop;
default:
return DetermineLayout() != null;
}
}
internal string DetermineLayout()
{
if (reportType != HIDReportType.Input)
return null;
////TODO: deal with arrays
switch (usagePage)
{
case UsagePage.Button:
return "Button";
case UsagePage.GenericDesktop:
switch (usage)
{
case (int)GenericDesktop.X:
case (int)GenericDesktop.Y:
case (int)GenericDesktop.Z:
case (int)GenericDesktop.Rx:
case (int)GenericDesktop.Ry:
case (int)GenericDesktop.Rz:
case (int)GenericDesktop.Vx:
case (int)GenericDesktop.Vy:
case (int)GenericDesktop.Vz:
case (int)GenericDesktop.Vbrx:
case (int)GenericDesktop.Vbry:
case (int)GenericDesktop.Vbrz:
case (int)GenericDesktop.Slider:
case (int)GenericDesktop.Dial:
case (int)GenericDesktop.Wheel:
return "Axis";
case (int)GenericDesktop.Select:
case (int)GenericDesktop.Start:
case (int)GenericDesktop.DpadUp:
case (int)GenericDesktop.DpadDown:
case (int)GenericDesktop.DpadLeft:
case (int)GenericDesktop.DpadRight:
return "Button";
case (int)GenericDesktop.HatSwitch:
// Only support hat switches with 8 directions.
if (logicalMax - logicalMin + 1 == 8)
return "Dpad";
break;
}
break;
}
return null;
}
internal FourCC DetermineFormat()
{
switch (reportSizeInBits)
{
case 8:
return isSigned ? InputStateBlock.FormatSByte : InputStateBlock.FormatByte;
case 16:
return isSigned ? InputStateBlock.FormatShort : InputStateBlock.FormatUShort;
case 32:
return isSigned ? InputStateBlock.FormatInt : InputStateBlock.FormatUInt;
default:
// Generic bitfield value.
return InputStateBlock.FormatBit;
}
}
internal InternedString[] DetermineUsages()
{
if (usagePage == UsagePage.Button && usage == 1)
return new[] {CommonUsages.PrimaryTrigger, CommonUsages.PrimaryAction};
if (usagePage == UsagePage.Button && usage == 2)
return new[] {CommonUsages.SecondaryTrigger, CommonUsages.SecondaryAction};
if (usagePage == UsagePage.GenericDesktop && usage == (int)GenericDesktop.Rz)
return new[] { CommonUsages.Twist };
////TODO: assign hatswitch usage to first and only to first hatswitch element
return null;
}
internal string DetermineParameters()
{
if (usagePage == UsagePage.GenericDesktop)
{
switch (usage)
{
case (int)GenericDesktop.X:
case (int)GenericDesktop.Z:
case (int)GenericDesktop.Rx:
case (int)GenericDesktop.Rz:
case (int)GenericDesktop.Vx:
case (int)GenericDesktop.Vz:
case (int)GenericDesktop.Vbrx:
case (int)GenericDesktop.Vbrz:
case (int)GenericDesktop.Slider:
case (int)GenericDesktop.Dial:
case (int)GenericDesktop.Wheel:
return DetermineAxisNormalizationParameters();
// Our Ys tend to be the opposite of what most HIDs do. We can't be sure and may well
// end up inverting a value here when we shouldn't but as always with the HID fallback,
// let's try to do what *seems* to work with the majority of devices.
case (int)GenericDesktop.Y:
case (int)GenericDesktop.Ry:
case (int)GenericDesktop.Vy:
case (int)GenericDesktop.Vbry:
return StringHelpers.Join(",", "invert", DetermineAxisNormalizationParameters());
}
}
return null;
}
private string DetermineAxisNormalizationParameters()
{
// If we have min/max bounds on the axis values, set up normalization on the axis.
// NOTE: We put the center in the middle between min/max as we can't know where the
// resting point of the axis is (may be on min if it's a trigger, for example).
if (logicalMin == 0 && logicalMax == 0)
return "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5";
var min = minFloatValue;
var max = maxFloatValue;
// Do nothing if result of floating-point conversion is already normalized.
if (Mathf.Approximately(0f, min) && Mathf.Approximately(0f, max))
return null;
var zero = min + (max - min) / 2.0f;
return string.Format(CultureInfo.InvariantCulture, "normalize,normalizeMin={0},normalizeMax={1},normalizeZero={2}", min, max, zero);
}
internal string DetermineProcessors()
{
switch (usagePage)
{
case UsagePage.GenericDesktop:
switch (usage)
{
case (int)GenericDesktop.X:
case (int)GenericDesktop.Y:
case (int)GenericDesktop.Z:
case (int)GenericDesktop.Rx:
case (int)GenericDesktop.Ry:
case (int)GenericDesktop.Rz:
case (int)GenericDesktop.Vx:
case (int)GenericDesktop.Vy:
case (int)GenericDesktop.Vz:
case (int)GenericDesktop.Vbrx:
case (int)GenericDesktop.Vbry:
case (int)GenericDesktop.Vbrz:
case (int)GenericDesktop.Slider:
case (int)GenericDesktop.Dial:
case (int)GenericDesktop.Wheel:
return "axisDeadzone";
}
break;
}
return null;
}
internal PrimitiveValue DetermineDefaultState()
{
switch (usagePage)
{
case UsagePage.GenericDesktop:
switch (usage)
{
case (int)GenericDesktop.HatSwitch:
// Figure out null state for hat switches.
if (hasNullState)
{
// We're looking for a value that is out-of-range with respect to the
// logical min and max but in range with respect to what we can store
// in the bits we have.
// Test lower bound, we can store >= 0.
if (logicalMin >= 1)
return new PrimitiveValue(logicalMin - 1);
// Test upper bound, we can store <= maxValue.
var maxValue = (1UL << reportSizeInBits) - 1;
if ((ulong)logicalMax < maxValue)
return new PrimitiveValue(logicalMax + 1);
}
break;
case (int)GenericDesktop.X:
case (int)GenericDesktop.Y:
case (int)GenericDesktop.Z:
case (int)GenericDesktop.Rx:
case (int)GenericDesktop.Ry:
case (int)GenericDesktop.Rz:
case (int)GenericDesktop.Vx:
case (int)GenericDesktop.Vy:
case (int)GenericDesktop.Vz:
case (int)GenericDesktop.Vbrx:
case (int)GenericDesktop.Vbry:
case (int)GenericDesktop.Vbrz:
case (int)GenericDesktop.Slider:
case (int)GenericDesktop.Dial:
case (int)GenericDesktop.Wheel:
// For axes that are *NOT* stored as signed values (which we assume are
// centered on 0), put the default state in the middle between the min and max.
if (!isSigned)
{
var defaultValue = logicalMin + (logicalMax - logicalMin) / 2;
if (defaultValue != 0)
return new PrimitiveValue(defaultValue);
}
break;
}
break;
}
return new PrimitiveValue();
}
internal void AddChildControls(ref HIDElementDescriptor element, string controlName, ref InputControlLayout.Builder builder)
{
if (usagePage == UsagePage.GenericDesktop && usage == (int)GenericDesktop.HatSwitch)
{
// There doesn't seem to be enough specificity in the HID spec to reliably figure this case out.
// Albeit detail is scarce, we could probably make some inferences based on the unit setting
// of the hat switch but even then it seems there's much left to the whims of a hardware manufacturer.
// Even if we know values go clockwise (HID spec doesn't really say; probably can be inferred from unit),
// which direction do we start with? Is 0 degrees up or right?
//
// What we do here is simply make the assumption that we're dealing with degrees here, that we go clockwise,
// and that 0 degrees is up (which is actually the opposite of the coordinate system suggested in 5.9 of
// of the HID spec but seems to be what manufacturers are actually using in practice). Of course, if the
// device we're looking at actually sets things up differently, then we end up with either an incorrectly
// oriented or (worse) a non-functional hat switch.
var nullValue = DetermineDefaultState();
if (nullValue.isEmpty)
return;
////REVIEW: this probably only works with hatswitches that have their null value at logicalMax+1
builder.AddControl(controlName + "/up")
.WithFormat(InputStateBlock.FormatBit)
.WithLayout("DiscreteButton")
.WithParameters(string.Format(CultureInfo.InvariantCulture,
"minValue={0},maxValue={1},nullValue={2},wrapAtValue={3}",
logicalMax, logicalMin + 1, nullValue.ToString(), logicalMax))
.WithBitOffset((uint)element.reportOffsetInBits % 8)
.WithSizeInBits((uint)reportSizeInBits);
builder.AddControl(controlName + "/right")
.WithFormat(InputStateBlock.FormatBit)
.WithLayout("DiscreteButton")
.WithParameters(string.Format(CultureInfo.InvariantCulture,
"minValue={0},maxValue={1}",
logicalMin + 1, logicalMin + 3))
.WithBitOffset((uint)element.reportOffsetInBits % 8)
.WithSizeInBits((uint)reportSizeInBits);
builder.AddControl(controlName + "/down")
.WithFormat(InputStateBlock.FormatBit)
.WithLayout("DiscreteButton")
.WithParameters(string.Format(CultureInfo.InvariantCulture,
"minValue={0},maxValue={1}",
logicalMin + 3, logicalMin + 5))
.WithBitOffset((uint)element.reportOffsetInBits % 8)
.WithSizeInBits((uint)reportSizeInBits);
builder.AddControl(controlName + "/left")
.WithFormat(InputStateBlock.FormatBit)
.WithLayout("DiscreteButton")
.WithParameters(string.Format(CultureInfo.InvariantCulture,
"minValue={0},maxValue={1}",
logicalMin + 5, logicalMin + 7))
.WithBitOffset((uint)element.reportOffsetInBits % 8)
.WithSizeInBits((uint)reportSizeInBits);
}
}
}
/// <summary>
/// Descriptor for a collection of HID elements.
/// </summary>
[Serializable]
public struct HIDCollectionDescriptor
{
public HIDCollectionType type;
public int usage;
public UsagePage usagePage;
public int parent; // -1 if no parent.
public int childCount;
public int firstChild;
}
/// <summary>
/// HID descriptor for a HID class device.
/// </summary>
/// <remarks>
/// This is a processed view of the combined descriptors provided by a HID as defined
/// in the HID specification, i.e. it's a combination of information from the USB device
/// descriptor, HID class descriptor, and HID report descriptor.
/// </remarks>
[Serializable]
public struct HIDDeviceDescriptor
{
/// <summary>
/// USB vendor ID.
/// </summary>
/// <remarks>
/// To get the string version of the vendor ID, see <see cref="InputDeviceDescription.manufacturer"/>
/// on <see cref="InputDevice.description"/>.
/// </remarks>
public int vendorId;
/// <summary>
/// USB product ID.
/// </summary>
public int productId;
public int usage;
public UsagePage usagePage;
/// <summary>
/// Maximum size of individual input reports sent by the device.
/// </summary>
public int inputReportSize;
/// <summary>
/// Maximum size of individual output reports sent to the device.
/// </summary>
public int outputReportSize;
/// <summary>
/// Maximum size of individual feature reports exchanged with the device.
/// </summary>
public int featureReportSize;
public HIDElementDescriptor[] elements;
public HIDCollectionDescriptor[] collections;
public string ToJson()
{
return JsonUtility.ToJson(this, true);
}
public static HIDDeviceDescriptor FromJson(string json)
{
#if UNITY_2021_2_OR_NEWER
try
{
// HID descriptors, when formatted correctly, are always json strings with no whitespace and a
// predictable order of elements, so we can try and use this simple predictive parser to extract
// the data. If for any reason the data is not formatted correctly, we'll automatically fall back
// to Unity's default json parser.
var descriptor = new HIDDeviceDescriptor();
var jsonSpan = json.AsSpan();
var parser = new PredictiveParser();
parser.ExpectSingleChar(jsonSpan, '{');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.vendorId = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.productId = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.usage = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.usagePage = (UsagePage)parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.inputReportSize = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.outputReportSize = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.AcceptString(jsonSpan, out _);
parser.ExpectSingleChar(jsonSpan, ':');
descriptor.featureReportSize = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
// elements
parser.AcceptString(jsonSpan, out var key);
if (key.ToString() != "elements") return descriptor;
parser.ExpectSingleChar(jsonSpan, ':');
parser.ExpectSingleChar(jsonSpan, '[');
using var pool = ListPool<HIDElementDescriptor>.Get(out var elements);
while (!parser.AcceptSingleChar(jsonSpan, ']'))
{
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectSingleChar(jsonSpan, '{');
HIDElementDescriptor elementDesc = default;
parser.AcceptSingleChar(jsonSpan, '}');
parser.AcceptSingleChar(jsonSpan, ',');
// usage
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.usage = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.usagePage = (UsagePage)parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.unit = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.unitExponent = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.logicalMin = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.logicalMax = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.physicalMin = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.physicalMax = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.collectionIndex = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.reportType = (HIDReportType)parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.reportId = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
// reportCount. We don't store this one
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
parser.AcceptInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.reportSizeInBits = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.reportOffsetInBits = parser.ExpectInt(jsonSpan);
parser.AcceptSingleChar(jsonSpan, ',');
parser.ExpectString(jsonSpan);
parser.ExpectSingleChar(jsonSpan, ':');
elementDesc.flags = (HIDElementFlags)parser.ExpectInt(jsonSpan);
parser.ExpectSingleChar(jsonSpan, '}');
elements.Add(elementDesc);
}
descriptor.elements = elements.ToArray();
return descriptor;
}
catch (Exception)
{
Debug.LogWarning($"Couldn't parse HID descriptor with fast parser. Using fallback");
return JsonUtility.FromJson<HIDDeviceDescriptor>(json);
}
#else
return JsonUtility.FromJson<HIDDeviceDescriptor>(json);
#endif
}
}
/// <summary>
/// Helper to quickly build descriptors for arbitrary HIDs.
/// </summary>
public struct HIDDeviceDescriptorBuilder
{
public UsagePage usagePage;
public int usage;
public HIDDeviceDescriptorBuilder(UsagePage usagePage, int usage)
: this()
{
this.usagePage = usagePage;
this.usage = usage;
}
public HIDDeviceDescriptorBuilder(GenericDesktop usage)
: this(UsagePage.GenericDesktop, (int)usage)
{
}
public HIDDeviceDescriptorBuilder StartReport(HIDReportType reportType, int reportId = 1)
{
m_CurrentReportId = reportId;
m_CurrentReportType = reportType;
m_CurrentReportOffsetInBits = 8; // Report ID.
return this;
}
public HIDDeviceDescriptorBuilder AddElement(UsagePage usagePage, int usage, int sizeInBits)
{
if (m_Elements == null)
{
m_Elements = new List<HIDElementDescriptor>();
}
else
{
// Make sure the usage and usagePage combination is unique.
foreach (var element in m_Elements)
{
// Skip elements that aren't in the same report.
if (element.reportId != m_CurrentReportId || element.reportType != m_CurrentReportType)
continue;
if (element.usagePage == usagePage && element.usage == usage)
throw new InvalidOperationException(
$"Cannot add two elements with the same usage page '{usagePage}' and usage '0x{usage:X} the to same device");
}
}
m_Elements.Add(new HIDElementDescriptor
{
usage = usage,
usagePage = usagePage,
reportOffsetInBits = m_CurrentReportOffsetInBits,
reportSizeInBits = sizeInBits,
reportType = m_CurrentReportType,
reportId = m_CurrentReportId
});
m_CurrentReportOffsetInBits += sizeInBits;
return this;
}
public HIDDeviceDescriptorBuilder AddElement(GenericDesktop usage, int sizeInBits)
{
return AddElement(UsagePage.GenericDesktop, (int)usage, sizeInBits);
}
public HIDDeviceDescriptorBuilder WithPhysicalMinMax(int min, int max)
{
var index = m_Elements.Count - 1;
if (index < 0)
throw new InvalidOperationException("No element has been added to the descriptor yet");
var element = m_Elements[index];
element.physicalMin = min;
element.physicalMax = max;
m_Elements[index] = element;
return this;
}
public HIDDeviceDescriptorBuilder WithLogicalMinMax(int min, int max)
{
var index = m_Elements.Count - 1;
if (index < 0)
throw new InvalidOperationException("No element has been added to the descriptor yet");
var element = m_Elements[index];
element.logicalMin = min;
element.logicalMax = max;
m_Elements[index] = element;
return this;
}
public HIDDeviceDescriptor Finish()
{
var descriptor = new HIDDeviceDescriptor
{
usage = usage,
usagePage = usagePage,
elements = m_Elements?.ToArray(),
collections = m_Collections?.ToArray(),
};
return descriptor;
}
private int m_CurrentReportId;
private HIDReportType m_CurrentReportType;
private int m_CurrentReportOffsetInBits;
private List<HIDElementDescriptor> m_Elements;
private List<HIDCollectionDescriptor> m_Collections;
private int m_InputReportSize;
private int m_OutputReportSize;
private int m_FeatureReportSize;
}
/// <summary>
/// Enumeration of HID usage pages.
/// </summary>00
/// <remarks>
/// Note that some of the values are actually ranges.
/// </remarks>
/// <seealso href="http://www.usb.org/developers/hidpage/Hut1_12v2.pdf"/>
public enum UsagePage
{
Undefined = 0x00,
GenericDesktop = 0x01,
Simulation = 0x02,
VRControls = 0x03,
SportControls = 0x04,
GameControls = 0x05,
GenericDeviceControls = 0x06,
Keyboard = 0x07,
LEDs = 0x08,
Button = 0x09,
Ordinal = 0x0A,
Telephony = 0x0B,
Consumer = 0x0C,
Digitizer = 0x0D,
PID = 0x0F,
Unicode = 0x10,
AlphanumericDisplay = 0x14,
MedicalInstruments = 0x40,
Monitor = 0x80, // Starts here and goes up to 0x83.
Power = 0x84, // Starts here and goes up to 0x87.
BarCodeScanner = 0x8C,
MagneticStripeReader = 0x8E,
Camera = 0x90,
Arcade = 0x91,
VendorDefined = 0xFF00, // Starts here and goes up to 0xFFFF.
}
/// <summary>
/// Usages in the GenericDesktop HID usage page.
/// </summary>
/// <seealso href="http://www.usb.org/developers/hidpage/Hut1_12v2.pdf"/>
public enum GenericDesktop
{
Undefined = 0x00,
Pointer = 0x01,
Mouse = 0x02,
Joystick = 0x04,
Gamepad = 0x05,
Keyboard = 0x06,
Keypad = 0x07,
MultiAxisController = 0x08,
TabletPCControls = 0x09,
AssistiveControl = 0x0A,
X = 0x30,
Y = 0x31,
Z = 0x32,
Rx = 0x33,
Ry = 0x34,
Rz = 0x35,
Slider = 0x36,
Dial = 0x37,
Wheel = 0x38,
HatSwitch = 0x39,
CountedBuffer = 0x3A,
ByteCount = 0x3B,
MotionWakeup = 0x3C,
Start = 0x3D,
Select = 0x3E,
Vx = 0x40,
Vy = 0x41,
Vz = 0x42,
Vbrx = 0x43,
Vbry = 0x44,
Vbrz = 0x45,
Vno = 0x46,
FeatureNotification = 0x47,
ResolutionMultiplier = 0x48,
SystemControl = 0x80,
SystemPowerDown = 0x81,
SystemSleep = 0x82,
SystemWakeUp = 0x83,
SystemContextMenu = 0x84,
SystemMainMenu = 0x85,
SystemAppMenu = 0x86,
SystemMenuHelp = 0x87,
SystemMenuExit = 0x88,
SystemMenuSelect = 0x89,
SystemMenuRight = 0x8A,
SystemMenuLeft = 0x8B,
SystemMenuUp = 0x8C,
SystemMenuDown = 0x8D,
SystemColdRestart = 0x8E,
SystemWarmRestart = 0x8F,
DpadUp = 0x90,
DpadDown = 0x91,
DpadRight = 0x92,
DpadLeft = 0x93,
SystemDock = 0xA0,
SystemUndock = 0xA1,
SystemSetup = 0xA2,
SystemBreak = 0xA3,
SystemDebuggerBreak = 0xA4,
ApplicationBreak = 0xA5,
ApplicationDebuggerBreak = 0xA6,
SystemSpeakerMute = 0xA7,
SystemHibernate = 0xA8,
SystemDisplayInvert = 0xB0,
SystemDisplayInternal = 0xB1,
SystemDisplayExternal = 0xB2,
SystemDisplayBoth = 0xB3,
SystemDisplayDual = 0xB4,
SystemDisplayToggleIntExt = 0xB5,
SystemDisplaySwapPrimarySecondary = 0xB6,
SystemDisplayLCDAutoScale = 0xB7
}
public enum Simulation
{
Undefined = 0x00,
FlightSimulationDevice = 0x01,
AutomobileSimulationDevice = 0x02,
TankSimulationDevice = 0x03,
SpaceshipSimulationDevice = 0x04,
SubmarineSimulationDevice = 0x05,
SailingSimulationDevice = 0x06,
MotorcycleSimulationDevice = 0x07,
SportsSimulationDevice = 0x08,
AirplaneSimulationDevice = 0x09,
HelicopterSimulationDevice = 0x0A,
MagicCarpetSimulationDevice = 0x0B,
BicylcleSimulationDevice = 0x0C,
FlightControlStick = 0x20,
FlightStick = 0x21,
CyclicControl = 0x22,
CyclicTrim = 0x23,
FlightYoke = 0x24,
TrackControl = 0x25,
Aileron = 0xB0,
AileronTrim = 0xB1,
AntiTorqueControl = 0xB2,
AutopilotEnable = 0xB3,
ChaffRelease = 0xB4,
CollectiveControl = 0xB5,
DiveBreak = 0xB6,
ElectronicCountermeasures = 0xB7,
Elevator = 0xB8,
ElevatorTrim = 0xB9,
Rudder = 0xBA,
Throttle = 0xBB,
FlightCommunications = 0xBC,
FlareRelease = 0xBD,
LandingGear = 0xBE,
ToeBreak = 0xBF,
Trigger = 0xC0,
WeaponsArm = 0xC1,
WeaponsSelect = 0xC2,
WingFlaps = 0xC3,
Accelerator = 0xC4,
Brake = 0xC5,
Clutch = 0xC6,
Shifter = 0xC7,
Steering = 0xC8,
TurretDirection = 0xC9,
BarrelElevation = 0xCA,
DivePlane = 0xCB,
Ballast = 0xCC,
BicycleCrank = 0xCD,
HandleBars = 0xCE,
FrontBrake = 0xCF,
RearBrake = 0xD0
}
public enum Button
{
Undefined = 0,
Primary,
Secondary,
Tertiary
}
}
}