using System; using System.Collections.Generic; ////TODO: array support ////TODO: delimiter support ////TODO: designator support #pragma warning disable CS0649 namespace UnityEngine.InputSystem.HID { /// /// Turns binary HID descriptors into instances. /// /// /// For information about the format, see the /// Device Class Definition for Human Interface Devices section 6.2.2. /// internal static class HIDParser { /// /// Parse a HID report descriptor as defined by section 6.2.2 of the /// HID /// specification and add the elements and collections from the /// descriptor to the given . /// /// Buffer containing raw HID report descriptor. /// HID device descriptor to complete with the information /// from the report descriptor. Elements and collections will get added to this descriptor. /// True if the report descriptor was successfully parsed. /// /// Will also set , /// , and /// . /// public static unsafe bool ParseReportDescriptor(byte[] buffer, ref HID.HIDDeviceDescriptor deviceDescriptor) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); fixed(byte* bufferPtr = buffer) { return ParseReportDescriptor(bufferPtr, buffer.Length, ref deviceDescriptor); } } public unsafe static bool ParseReportDescriptor(byte* bufferPtr, int bufferLength, ref HID.HIDDeviceDescriptor deviceDescriptor) { // Item state. var localItemState = new HIDItemStateLocal(); var globalItemState = new HIDItemStateGlobal(); // Lists where we accumulate the data from the HID items. var reports = new List(); var elements = new List(); var collections = new List(); var currentCollection = -1; // Parse the linear list of items. var endPtr = bufferPtr + bufferLength; var currentPtr = bufferPtr; while (currentPtr < endPtr) { var firstByte = *currentPtr; ////TODO if (firstByte == 0xFE) throw new NotImplementedException("long item support"); // Read item header. var itemSize = (byte)(firstByte & 0x3); var itemTypeAndTag = (byte)(firstByte & 0xFC); ++currentPtr; // Process item. switch (itemTypeAndTag) { // ------------ Global Items -------------- // These set item state permanently until it is reset. // Usage Page case (int)HIDItemTypeAndTag.UsagePage: globalItemState.usagePage = ReadData(itemSize, currentPtr, endPtr); break; // Report Count case (int)HIDItemTypeAndTag.ReportCount: globalItemState.reportCount = ReadData(itemSize, currentPtr, endPtr); break; // Report Size case (int)HIDItemTypeAndTag.ReportSize: globalItemState.reportSize = ReadData(itemSize, currentPtr, endPtr); break; // Report ID case (int)HIDItemTypeAndTag.ReportID: globalItemState.reportId = ReadData(itemSize, currentPtr, endPtr); break; // Logical Minimum case (int)HIDItemTypeAndTag.LogicalMinimum: globalItemState.logicalMinimum = ReadData(itemSize, currentPtr, endPtr); break; // Logical Maximum case (int)HIDItemTypeAndTag.LogicalMaximum: globalItemState.logicalMaximum = ReadData(itemSize, currentPtr, endPtr); break; // Physical Minimum case (int)HIDItemTypeAndTag.PhysicalMinimum: globalItemState.physicalMinimum = ReadData(itemSize, currentPtr, endPtr); break; // Physical Maximum case (int)HIDItemTypeAndTag.PhysicalMaximum: globalItemState.physicalMaximum = ReadData(itemSize, currentPtr, endPtr); break; // Unit Exponent case (int)HIDItemTypeAndTag.UnitExponent: globalItemState.unitExponent = ReadData(itemSize, currentPtr, endPtr); break; // Unit case (int)HIDItemTypeAndTag.Unit: globalItemState.unit = ReadData(itemSize, currentPtr, endPtr); break; // ------------ Local Items -------------- // These set the state for the very next elements to be generated. // Usage case (int)HIDItemTypeAndTag.Usage: localItemState.SetUsage(ReadData(itemSize, currentPtr, endPtr)); break; // Usage Minimum case (int)HIDItemTypeAndTag.UsageMinimum: localItemState.usageMinimum = ReadData(itemSize, currentPtr, endPtr); break; // Usage Maximum case (int)HIDItemTypeAndTag.UsageMaximum: localItemState.usageMaximum = ReadData(itemSize, currentPtr, endPtr); break; // ------------ Main Items -------------- // These emit things into the descriptor based on the local and global item state. // Collection case (int)HIDItemTypeAndTag.Collection: // Start new collection. var parentCollection = currentCollection; currentCollection = collections.Count; collections.Add(new HID.HIDCollectionDescriptor { type = (HID.HIDCollectionType)ReadData(itemSize, currentPtr, endPtr), parent = parentCollection, usagePage = globalItemState.GetUsagePage(0, ref localItemState), usage = localItemState.GetUsage(0), firstChild = elements.Count }); HIDItemStateLocal.Reset(ref localItemState); break; // EndCollection case (int)HIDItemTypeAndTag.EndCollection: if (currentCollection == -1) return false; // Close collection. var collection = collections[currentCollection]; collection.childCount = elements.Count - collection.firstChild; collections[currentCollection] = collection; // Switch back to parent collection (if any). currentCollection = collection.parent; HIDItemStateLocal.Reset(ref localItemState); break; // Input/Output/Feature case (int)HIDItemTypeAndTag.Input: case (int)HIDItemTypeAndTag.Output: case (int)HIDItemTypeAndTag.Feature: // Determine report type. var reportType = itemTypeAndTag == (int)HIDItemTypeAndTag.Input ? HID.HIDReportType.Input : itemTypeAndTag == (int)HIDItemTypeAndTag.Output ? HID.HIDReportType.Output : HID.HIDReportType.Feature; // Find report. var reportIndex = HIDReportData.FindOrAddReport(globalItemState.reportId, reportType, reports); var report = reports[reportIndex]; // If we have a report ID, then reports start with an 8 byte report ID. // Shift our offsets accordingly. if (report.currentBitOffset == 0 && globalItemState.reportId.HasValue) report.currentBitOffset = 8; // Add elements to report. var reportCount = globalItemState.reportCount.GetValueOrDefault(1); var flags = ReadData(itemSize, currentPtr, endPtr); for (var i = 0; i < reportCount; ++i) { var element = new HID.HIDElementDescriptor { usage = localItemState.GetUsage(i) & 0xFFFF, // Mask off usage page, if set. usagePage = globalItemState.GetUsagePage(i, ref localItemState), reportType = reportType, reportSizeInBits = globalItemState.reportSize.GetValueOrDefault(8), reportOffsetInBits = report.currentBitOffset, reportId = globalItemState.reportId.GetValueOrDefault(1), flags = (HID.HIDElementFlags)flags, logicalMin = globalItemState.logicalMinimum.GetValueOrDefault(0), logicalMax = globalItemState.logicalMaximum.GetValueOrDefault(0), physicalMin = globalItemState.GetPhysicalMin(), physicalMax = globalItemState.GetPhysicalMax(), unitExponent = globalItemState.unitExponent.GetValueOrDefault(0), unit = globalItemState.unit.GetValueOrDefault(0), }; report.currentBitOffset += element.reportSizeInBits; elements.Add(element); } reports[reportIndex] = report; HIDItemStateLocal.Reset(ref localItemState); break; } if (itemSize == 3) currentPtr += 4; else currentPtr += itemSize; } deviceDescriptor.elements = elements.ToArray(); deviceDescriptor.collections = collections.ToArray(); // Set usage and usage page on device descriptor to what's // on the toplevel application collection. foreach (var collection in collections) { if (collection.parent == -1 && collection.type == HID.HIDCollectionType.Application) { deviceDescriptor.usage = collection.usage; deviceDescriptor.usagePage = collection.usagePage; break; } } return true; } private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr) { if (itemSize == 0) return 0; // Read byte. if (itemSize == 1) { if (currentPtr >= endPtr) return 0; return *currentPtr; } // Read short. if (itemSize == 2) { if (currentPtr + 2 >= endPtr) return 0; var data1 = *currentPtr; var data2 = *(currentPtr + 1); return (data2 << 8) | data1; } // Read int. if (itemSize == 3) // Item size 3 means 4 bytes! { if (currentPtr + 4 >= endPtr) return 0; var data1 = *currentPtr; var data2 = *(currentPtr + 1); var data3 = *(currentPtr + 2); var data4 = *(currentPtr + 3); return (data4 << 24) | (data3 << 24) | (data2 << 8) | data1; } Debug.Assert(false, "Should not reach here"); return 0; } private struct HIDReportData { public int reportId; public HID.HIDReportType reportType; public int currentBitOffset; public static int FindOrAddReport(int? reportId, HID.HIDReportType reportType, List reports) { var id = 1; if (reportId.HasValue) id = reportId.Value; for (var i = 0; i < reports.Count; ++i) { if (reports[i].reportId == id && reports[i].reportType == reportType) return i; } reports.Add(new HIDReportData { reportId = id, reportType = reportType }); return reports.Count - 1; } } // All types and tags with size bits (low order two bits) masked out (i.e. being 0). private enum HIDItemTypeAndTag { Input = 0x80, Output = 0x90, Feature = 0xB0, Collection = 0xA0, EndCollection = 0xC0, UsagePage = 0x04, LogicalMinimum = 0x14, LogicalMaximum = 0x24, PhysicalMinimum = 0x34, PhysicalMaximum = 0x44, UnitExponent = 0x54, Unit = 0x64, ReportSize = 0x74, ReportID = 0x84, ReportCount = 0x94, Push = 0xA4, Pop = 0xB4, Usage = 0x08, UsageMinimum = 0x18, UsageMaximum = 0x28, DesignatorIndex = 0x38, DesignatorMinimum = 0x48, DesignatorMaximum = 0x58, StringIndex = 0x78, StringMinimum = 0x88, StringMaximum = 0x98, Delimiter = 0xA8, } // State that needs to be defined for each main item separately. // See section 6.2.2.8 private struct HIDItemStateLocal { public int? usage; public int? usageMinimum; public int? usageMaximum; public int? designatorIndex; public int? designatorMinimum; public int? designatorMaximum; public int? stringIndex; public int? stringMinimum; public int? stringMaximum; public List usageList; // Wipe state but preserve usageList allocation. public static void Reset(ref HIDItemStateLocal state) { var usageList = state.usageList; state = new HIDItemStateLocal(); if (usageList != null) { usageList.Clear(); state.usageList = usageList; } } // Usage can be set repeatedly to provide an enumeration of usages. public void SetUsage(int value) { if (usage.HasValue) { if (usageList == null) usageList = new List(); usageList.Add(usage.Value); } usage = value; } // Get usage for Nth element in [0-reportCount] list. public int GetUsage(int index) { // If we have minimum and maximum usage, interpolate between that. if (usageMinimum.HasValue && usageMaximum.HasValue) { var min = usageMinimum.Value; var max = usageMaximum.Value; var range = max - min; if (range < 0) return 0; if (index >= range) return max; return min + index; } // If we have a list of usages, index into that. if (usageList != null && usageList.Count > 0) { var usageCount = usageList.Count; if (index >= usageCount) return usage.Value; return usageList[index]; } if (usage.HasValue) return usage.Value; ////TODO: min/max return 0; } } // State that is carried over from main item to main item. // See section 6.2.2.7 private struct HIDItemStateGlobal { public int? usagePage; public int? logicalMinimum; public int? logicalMaximum; public int? physicalMinimum; public int? physicalMaximum; public int? unitExponent; public int? unit; public int? reportSize; public int? reportCount; public int? reportId; public HID.UsagePage GetUsagePage(int index, ref HIDItemStateLocal localItemState) { if (!usagePage.HasValue) { var usage = localItemState.GetUsage(index); return (HID.UsagePage)(usage >> 16); } return (HID.UsagePage)usagePage.Value; } public int GetPhysicalMin() { if (physicalMinimum == null || physicalMaximum == null || (physicalMinimum.Value == 0 && physicalMaximum.Value == 0)) return logicalMinimum.GetValueOrDefault(0); return physicalMinimum.Value; } public int GetPhysicalMax() { if (physicalMinimum == null || physicalMaximum == null || (physicalMinimum.Value == 0 && physicalMaximum.Value == 0)) return logicalMaximum.GetValueOrDefault(0); return physicalMaximum.Value; } } } }