| 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 |
|
|
| |
| #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 |
|
|
| |
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| #pragma warning disable CS0649, CS0219 |
| namespace UnityEngine.InputSystem.HID |
| { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")] |
| public class HID : InputDevice |
| { |
| internal const string kHIDInterface = "HID"; |
| internal const string kHIDNamespace = "HID"; |
|
|
| |
| |
| |
| |
| public static FourCC QueryHIDReportDescriptorDeviceCommandType { get { return new FourCC('H', 'I', 'D', 'D'); } } |
|
|
| |
| |
| |
| |
| public static FourCC QueryHIDReportDescriptorSizeDeviceCommandType { get { return new FourCC('H', 'I', 'D', 'S'); } } |
|
|
| public static FourCC QueryHIDParsedReportDescriptorDeviceCommandType { get { return new FourCC('H', 'I', 'D', 'P'); } } |
|
|
| |
| |
| |
| 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; |
|
|
| |
| |
| |
| |
| internal static string OnFindLayoutForDevice(ref InputDeviceDescription description, string matchedLayout, |
| InputDeviceExecuteCommandDelegate executeDeviceCommand) |
| { |
| |
| if (!string.IsNullOrEmpty(matchedLayout)) |
| return null; |
|
|
| |
| if (description.interfaceName != kHIDInterface) |
| return null; |
|
|
| |
| var hidDeviceDescriptor = ReadHIDDeviceDescriptor(ref description, executeDeviceCommand); |
|
|
| if (!HIDSupport.supportedHIDUsages.Contains(new HIDSupport.HIDPageUsage(hidDeviceDescriptor.usagePage, hidDeviceDescriptor.usage))) |
| return null; |
|
|
| |
| var hasUsableElements = false; |
| if (hidDeviceDescriptor.elements != null) |
| { |
| foreach (var element in hidDeviceDescriptor.elements) |
| { |
| if (element.IsUsableElement()) |
| { |
| hasUsableElements = true; |
| break; |
| } |
| } |
| } |
|
|
| |
| if (!hasUsableElements) |
| return null; |
|
|
| |
| |
| 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); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| var usageName = ""; |
| if (baseLayout != "Joystick") |
| { |
| usageName = hidDeviceDescriptor.usagePage == UsagePage.GenericDesktop |
| ? $" {(GenericDesktop) hidDeviceDescriptor.usage}" |
| : $" {hidDeviceDescriptor.usagePage}-{hidDeviceDescriptor.usage}"; |
| } |
|
|
| |
| |
| |
| |
| |
| 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 |
| { |
| |
| 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); |
| } |
|
|
| |
| deviceMatcher = deviceMatcher |
| .WithCapability("usage", hidDeviceDescriptor.usage) |
| .WithCapability("usagePage", hidDeviceDescriptor.usagePage); |
|
|
| |
| |
| 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"); |
|
|
| |
| |
| |
| var needToRequestDescriptor = true; |
| var hidDeviceDescriptor = new HIDDeviceDescriptor(); |
| if (!string.IsNullOrEmpty(deviceDescription.capabilities)) |
| { |
| try |
| { |
| hidDeviceDescriptor = HIDDeviceDescriptor.FromJson(deviceDescription.capabilities); |
|
|
| |
| |
| 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); |
| } |
| } |
|
|
| |
| |
| |
|
|
| |
| if (needToRequestDescriptor) |
| { |
| |
| var sizeOfDescriptorCommand = new InputDeviceCommand(QueryHIDReportDescriptorSizeDeviceCommandType); |
| var sizeOfDescriptorInBytes = executeCommandDelegate(ref sizeOfDescriptorCommand); |
| if (sizeOfDescriptorInBytes > 0) |
| { |
| |
| using (var buffer = |
| InputDeviceCommand.AllocateNative(QueryHIDReportDescriptorDeviceCommandType, (int)sizeOfDescriptorInBytes)) |
| { |
| var commandPtr = (InputDeviceCommand*)buffer.GetUnsafePtr(); |
| if (executeCommandDelegate(ref *commandPtr) != sizeOfDescriptorInBytes) |
| return new HIDDeviceDescriptor(); |
|
|
| |
| if (!HIDParser.ParseReportDescriptor((byte*)commandPtr->payloadPtr, (int)sizeOfDescriptorInBytes, ref hidDeviceDescriptor)) |
| return new HIDDeviceDescriptor(); |
| } |
|
|
| |
| deviceDescription.capabilities = hidDeviceDescriptor.ToJson(); |
| } |
| else |
| { |
| |
| |
| |
| |
| |
| |
|
|
| const int kMaxDescriptorBufferSize = 2 * 1024 * 1024; |
| using (var buffer = |
| InputDeviceCommand.AllocateNative(QueryHIDParsedReportDescriptorDeviceCommandType, kMaxDescriptorBufferSize)) |
| { |
| var commandPtr = (InputDeviceCommand*)buffer.GetUnsafePtr(); |
| var utf8Length = executeCommandDelegate(ref *commandPtr); |
| if (utf8Length < 0) |
| return new HIDDeviceDescriptor(); |
|
|
| |
| |
| 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 |
| { |
| hidDeviceDescriptor = HIDDeviceDescriptor.FromJson(descriptorJson); |
| } |
| catch (Exception exception) |
| { |
| Debug.LogError($"Could not parse HID descriptor of device '{deviceDescription}'"); |
| Debug.LogException(exception); |
| return new HIDDeviceDescriptor(); |
| } |
|
|
| |
| 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); |
|
|
| |
| |
| |
| |
| 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()); |
|
|
| |
| 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")); |
| } |
|
|
| |
| 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; |
|
|
| |
| if (haveStick && (element.Is(UsagePage.GenericDesktop, (int)GenericDesktop.X) || |
| element.Is(UsagePage.GenericDesktop, (int)GenericDesktop.Y))) |
| continue; |
|
|
| var layout = element.DetermineLayout(); |
| if (layout != null) |
| { |
| |
| var name = element.DetermineName(); |
| Debug.Assert(!string.IsNullOrEmpty(name)); |
| name = StringHelpers.MakeUniqueName(name, builder.controls, x => x.name); |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| |
| [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; |
|
|
| |
| 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() |
| { |
| |
| |
| |
|
|
| 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(); |
| |
| text = char.ToLowerInvariant(text[0]) + text.Substring(1); |
| return text; |
| } |
|
|
| |
| 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; |
|
|
| |
|
|
| 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: |
| |
| 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: |
| |
| 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 }; |
| |
| 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(); |
|
|
| |
| |
| |
| 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 (logicalMin == 0 && logicalMax == 0) |
| return "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5"; |
| var min = minFloatValue; |
| var max = maxFloatValue; |
| |
| 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: |
| |
| if (hasNullState) |
| { |
| |
| |
| |
|
|
| |
| if (logicalMin >= 1) |
| return new PrimitiveValue(logicalMin - 1); |
|
|
| |
| 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: |
| |
| |
| 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) |
| { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| var nullValue = DetermineDefaultState(); |
| if (nullValue.isEmpty) |
| return; |
|
|
| |
|
|
| 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); |
| } |
| } |
| } |
|
|
| |
| |
| |
| [Serializable] |
| public struct HIDCollectionDescriptor |
| { |
| public HIDCollectionType type; |
| public int usage; |
| public UsagePage usagePage; |
| public int parent; |
| public int childCount; |
| public int firstChild; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| [Serializable] |
| public struct HIDDeviceDescriptor |
| { |
| |
| |
| |
| |
| |
| |
| |
| public int vendorId; |
|
|
| |
| |
| |
| public int productId; |
| public int usage; |
| public UsagePage usagePage; |
|
|
| |
| |
| |
| public int inputReportSize; |
|
|
| |
| |
| |
| public int outputReportSize; |
|
|
| |
| |
| |
| 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 |
| { |
| |
| |
| |
| |
| 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, ','); |
|
|
| |
| 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, ','); |
|
|
| |
| 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, ','); |
|
|
| |
| 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 |
| } |
| } |
|
|
| |
| |
| |
| 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; |
| return this; |
| } |
|
|
| public HIDDeviceDescriptorBuilder AddElement(UsagePage usagePage, int usage, int sizeInBits) |
| { |
| if (m_Elements == null) |
| { |
| m_Elements = new List<HIDElementDescriptor>(); |
| } |
| else |
| { |
| |
| foreach (var element in m_Elements) |
| { |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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, |
| Power = 0x84, |
| BarCodeScanner = 0x8C, |
| MagneticStripeReader = 0x8E, |
| Camera = 0x90, |
| Arcade = 0x91, |
| VendorDefined = 0xFF00, |
| } |
|
|
| |
| |
| |
| |
| 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 |
| } |
| } |
| } |
|
|