1042 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1042 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
| # SPDX-License-Identifier: MIT
 | |
| 
 | |
| from __future__ import annotations  # noqa:F407
 | |
| 
 | |
| import functools
 | |
| import struct
 | |
| import sys
 | |
| import textwrap
 | |
| import typing
 | |
| import warnings
 | |
| 
 | |
| from typing import Any
 | |
| from typing import Dict
 | |
| from typing import Iterable
 | |
| from typing import Iterator
 | |
| from typing import List
 | |
| from typing import Optional
 | |
| from typing import Sequence
 | |
| from typing import TextIO
 | |
| from typing import Tuple
 | |
| from typing import Union
 | |
| 
 | |
| if sys.version_info >= (3, 8):
 | |
|     from typing import Literal
 | |
| else:  # pragma: no cover
 | |
|     from typing_extensions import Literal
 | |
| 
 | |
| import hid_parser.data
 | |
| 
 | |
| __version__ = "0.0.3"
 | |
| 
 | |
| 
 | |
| class HIDWarning(Warning):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class HIDComplianceWarning(HIDWarning):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class HIDReportWarning(HIDWarning):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class HIDUnsupportedWarning(HIDWarning):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class Type:
 | |
|     MAIN = 0
 | |
|     GLOBAL = 1
 | |
|     LOCAL = 2
 | |
| 
 | |
| 
 | |
| class TagMain:
 | |
|     INPUT = 0b1000
 | |
|     OUTPUT = 0b1001
 | |
|     FEATURE = 0b1011
 | |
|     COLLECTION = 0b1010
 | |
|     END_COLLECTION = 0b1100
 | |
| 
 | |
| 
 | |
| class TagGlobal:
 | |
|     USAGE_PAGE = 0b0000
 | |
|     LOGICAL_MINIMUM = 0b0001
 | |
|     LOGICAL_MAXIMUM = 0b0010
 | |
|     PHYSICAL_MINIMUM = 0b0011
 | |
|     PHYSICAL_MAXIMUM = 0b0100
 | |
|     UNIT_EXPONENT = 0b0101
 | |
|     UNIT = 0b0110
 | |
|     REPORT_SIZE = 0b0111
 | |
|     REPORT_ID = 0b1000
 | |
|     REPORT_COUNT = 0b1001
 | |
|     PUSH = 0b1010
 | |
|     POP = 0b1011
 | |
| 
 | |
| 
 | |
| class TagLocal:
 | |
|     USAGE = 0b0000
 | |
|     USAGE_MINIMUM = 0b0001
 | |
|     USAGE_MAXIMUM = 0b0010
 | |
|     DESIGNATOR_INDEX = 0b0011
 | |
|     DESIGNATOR_MINIMUM = 0b0100
 | |
|     DESIGNATOR_MAXIMUM = 0b0101
 | |
|     STRING_INDEX = 0b0111
 | |
|     STRING_MINIMUM = 0b1000
 | |
|     STRING_MAXIMUM = 0b1001
 | |
|     DELIMITER = 0b1010
 | |
| 
 | |
| 
 | |
| def _data_bit_shift(data: Sequence[int], offset: int, length: int) -> Sequence[int]:
 | |
|     if not length > 0:
 | |
|         raise ValueError(f"Invalid specified length: {length}")
 | |
| 
 | |
|     left_extra = offset % 8
 | |
|     right_extra = 8 - (offset + length) % 8
 | |
|     start_offset = offset // 8
 | |
|     end_offset = (offset + length - 1) // 8
 | |
|     byte_length = (length - 1) // 8 + 1
 | |
| 
 | |
|     if not end_offset < len(data):
 | |
|         raise ValueError(f"Invalid data length: {len(data)} (expecting {end_offset + 1})")
 | |
| 
 | |
|     shifted = [0] * byte_length
 | |
| 
 | |
|     if right_extra == 8:
 | |
|         right_extra = 0
 | |
| 
 | |
|     i = end_offset
 | |
|     shifted_offset = byte_length - 1
 | |
|     while shifted_offset >= 0:
 | |
|         shifted[shifted_offset] = data[i] >> right_extra
 | |
| 
 | |
|         if i - start_offset >= 0:
 | |
|             shifted[shifted_offset] |= (data[i - 1] & (0xFF >> (8 - right_extra))) << (8 - right_extra)
 | |
| 
 | |
|         shifted_offset -= 1
 | |
|         i -= 1
 | |
| 
 | |
|     shifted[0] &= 0xFF >> ((left_extra + right_extra) % 8)
 | |
| 
 | |
|     if not len(shifted) == byte_length:
 | |
|         raise ValueError("Invalid data")
 | |
| 
 | |
|     return shifted
 | |
| 
 | |
| 
 | |
| class BitNumber(int):
 | |
|     def __init__(self, value: int):
 | |
|         self._value = value
 | |
| 
 | |
|     def __int__(self) -> int:
 | |
|         return self._value
 | |
| 
 | |
|     def __eq__(self, other: Any) -> bool:
 | |
|         try:
 | |
|             return self._value == int(other)
 | |
|         except:  # noqa: E722
 | |
|             return False
 | |
| 
 | |
|     @property
 | |
|     def byte(self) -> int:
 | |
|         """
 | |
|         Number of bytes
 | |
|         """
 | |
|         return self._value // 8
 | |
| 
 | |
|     @property
 | |
|     def bit(self) -> int:
 | |
|         """
 | |
|         Number of unaligned bits
 | |
| 
 | |
|         n.byte * 8 + n.bits = n
 | |
|         """
 | |
|         if self.byte == 0:
 | |
|             return self._value
 | |
| 
 | |
|         return self._value % (self.byte * 8)
 | |
| 
 | |
|     @staticmethod
 | |
|     def _param_repr(value: int, unit: str) -> str:
 | |
|         if value != 1:
 | |
|             unit += "s"
 | |
|         return f"{value}{unit}"
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         byte_str = self._param_repr(self.byte, "byte")
 | |
|         bit_str = self._param_repr(self.bit, "bit")
 | |
| 
 | |
|         if self.byte == 0 and self.bit == 0:
 | |
|             return bit_str
 | |
| 
 | |
|         parts = []
 | |
|         if self.byte != 0:
 | |
|             parts.append(byte_str)
 | |
|         if self.bit != 0:
 | |
|             parts.append(bit_str)
 | |
| 
 | |
|         return " ".join(parts)
 | |
| 
 | |
| 
 | |
| class Usage:
 | |
|     def __init__(
 | |
|         self, page: Optional[int] = None, usage: Optional[int] = None, *, extended_usage: Optional[int] = None
 | |
|     ) -> None:
 | |
|         if extended_usage and page and usage:
 | |
|             raise ValueError("You need to specify either the usage page and usage or the extended usage")
 | |
|         if extended_usage is not None:
 | |
|             self.page = extended_usage >> (2 * 8)
 | |
|             self.usage = extended_usage & 0xFFFF
 | |
|         elif page is not None and usage is not None:
 | |
|             self.page = page
 | |
|             self.usage = usage
 | |
|         else:
 | |
|             raise ValueError("No usage specified")
 | |
| 
 | |
|     def __int__(self) -> int:
 | |
|         return self.page << (2 * 8) | self.usage
 | |
| 
 | |
|     def __eq__(self, other: Any) -> bool:
 | |
|         if not isinstance(other, self.__class__):
 | |
|             return False
 | |
|         return self.page == other.page and self.usage == other.usage
 | |
| 
 | |
|     def __hash__(self) -> int:
 | |
|         return self.usage << (2 * 8) + self.page
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         try:
 | |
|             page_str = hid_parser.data.UsagePages.get_description(self.page)
 | |
|         except KeyError:
 | |
|             page_str = f"0x{self.page:04x}"
 | |
|             usage_str = f"0x{self.usage:04x}"
 | |
|         else:
 | |
|             try:
 | |
|                 page = hid_parser.data.UsagePages.get_subdata(self.page)
 | |
|                 usage_str = page.get_description(self.usage)
 | |
|             except (KeyError, ValueError):
 | |
|                 usage_str = f"0x{self.usage:04x}"
 | |
|         return f"Usage(page={page_str}, usage={usage_str})"
 | |
| 
 | |
|     @property
 | |
|     def usage_types(self) -> Tuple[hid_parser.data.UsageTypes]:
 | |
|         subdata = hid_parser.data.UsagePages.get_subdata(self.page).get_subdata(self.usage)
 | |
| 
 | |
|         if isinstance(subdata, tuple):
 | |
|             types = subdata
 | |
|         else:
 | |
|             types = (subdata,)
 | |
| 
 | |
|         for typ in types:
 | |
|             if not isinstance(typ, hid_parser.data.UsageTypes):
 | |
|                 raise ValueError(f"Expecting usage type but got '{type(typ)}'")
 | |
| 
 | |
|         return typing.cast(Tuple[hid_parser.data.UsageTypes], types)
 | |
| 
 | |
| 
 | |
| class UsageValue:
 | |
|     def __init__(self, item: MainItem, value: int):
 | |
|         self._item = item
 | |
|         self._value = value
 | |
| 
 | |
|     def __int__(self) -> int:
 | |
|         return self.value
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         return repr(self.value)
 | |
| 
 | |
|     @property
 | |
|     def value(self) -> Union[int, bool]:
 | |
|         return self._value
 | |
| 
 | |
|     @property
 | |
|     def constant(self) -> bool:
 | |
|         return self._item.constant
 | |
| 
 | |
|     @property
 | |
|     def data(self) -> bool:
 | |
|         return self._item.data
 | |
| 
 | |
|     @property
 | |
|     def relative(self) -> bool:
 | |
|         return self._item.relative
 | |
| 
 | |
|     @property
 | |
|     def absolute(self) -> bool:
 | |
|         return self._item.absolute
 | |
| 
 | |
| 
 | |
| class VendorUsageValue(UsageValue):
 | |
|     def __init__(
 | |
|         self,
 | |
|         item: MainItem,
 | |
|         *,
 | |
|         value: Optional[int] = None,
 | |
|         value_list: Optional[List[int]] = None,
 | |
|     ):
 | |
|         self._item = item
 | |
|         if value:
 | |
|             self._list = [value]
 | |
|         elif value_list:
 | |
|             self._list = value_list
 | |
|         else:
 | |
|             self._list = []
 | |
| 
 | |
|     def __int__(self) -> int:
 | |
|         return self.value
 | |
| 
 | |
|     def __iter__(self) -> Iterator[int]:
 | |
|         return iter(self.list)
 | |
| 
 | |
|     @property
 | |
|     def value(self) -> Union[int, bool]:
 | |
|         return int.from_bytes(self._list, byteorder="little")
 | |
| 
 | |
|     @property
 | |
|     def list(self) -> List[int]:
 | |
|         return self._list
 | |
| 
 | |
| 
 | |
| class BaseItem:
 | |
|     def __init__(self, offset: int, size: int):
 | |
|         self._offset = BitNumber(offset)
 | |
|         self._size = BitNumber(size)
 | |
| 
 | |
|     @property
 | |
|     def offset(self) -> BitNumber:
 | |
|         return self._offset
 | |
| 
 | |
|     @property
 | |
|     def size(self) -> BitNumber:
 | |
|         return self._size
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         return f"{self.__class__.__name__}(offset={self.offset}, size={self.size})"
 | |
| 
 | |
| 
 | |
| class PaddingItem(BaseItem):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class MainItem(BaseItem):
 | |
|     def __init__(
 | |
|         self,
 | |
|         offset: int,
 | |
|         size: int,
 | |
|         flags: int,
 | |
|         logical_min: int,
 | |
|         logical_max: int,
 | |
|         physical_min: Optional[int] = None,
 | |
|         physical_max: Optional[int] = None,
 | |
|     ):
 | |
|         super().__init__(offset, size)
 | |
|         self._flags = flags
 | |
|         self._logical_min = logical_min
 | |
|         self._logical_max = logical_max
 | |
|         self._physical_min = physical_min
 | |
|         self._physical_max = physical_max
 | |
|         # TODO: unit
 | |
| 
 | |
|     @property
 | |
|     def offset(self) -> BitNumber:
 | |
|         return self._offset
 | |
| 
 | |
|     @property
 | |
|     def size(self) -> BitNumber:
 | |
|         return self._size
 | |
| 
 | |
|     @property
 | |
|     def logical_min(self) -> int:
 | |
|         return self._logical_min
 | |
| 
 | |
|     @property
 | |
|     def logical_max(self) -> int:
 | |
|         return self._logical_max
 | |
| 
 | |
|     @property
 | |
|     def physical_min(self) -> Optional[int]:
 | |
|         return self._physical_min
 | |
| 
 | |
|     @property
 | |
|     def physical_max(self) -> Optional[int]:
 | |
|         return self._physical_max
 | |
| 
 | |
|     # flags
 | |
| 
 | |
|     @property
 | |
|     def constant(self) -> bool:
 | |
|         return self._flags & (1 << 0) != 0
 | |
| 
 | |
|     @property
 | |
|     def data(self) -> bool:
 | |
|         return self._flags & (1 << 0) == 0
 | |
| 
 | |
|     @property
 | |
|     def relative(self) -> bool:
 | |
|         return self._flags & (1 << 2) != 0
 | |
| 
 | |
|     @property
 | |
|     def absolute(self) -> bool:
 | |
|         return self._flags & (1 << 2) == 0
 | |
| 
 | |
| 
 | |
| class VariableItem(MainItem):
 | |
|     _INCOMPATIBLE_TYPES = (
 | |
|         # array types
 | |
|         hid_parser.data.UsageTypes.SELECTOR,
 | |
|         # collection types
 | |
|         hid_parser.data.UsageTypes.NAMED_ARRAY,
 | |
|         hid_parser.data.UsageTypes.COLLECTION_APPLICATION,
 | |
|         hid_parser.data.UsageTypes.COLLECTION_LOGICAL,
 | |
|         hid_parser.data.UsageTypes.COLLECTION_PHYSICAL,
 | |
|         hid_parser.data.UsageTypes.USAGE_SWITCH,
 | |
|         hid_parser.data.UsageTypes.USAGE_MODIFIER,
 | |
|     )
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         offset: int,
 | |
|         size: int,
 | |
|         flags: int,
 | |
|         usage: Usage,
 | |
|         logical_min: int,
 | |
|         logical_max: int,
 | |
|         physical_min: Optional[int] = None,
 | |
|         physical_max: Optional[int] = None,
 | |
|     ):
 | |
|         super().__init__(offset, size, flags, logical_min, logical_max, physical_min, physical_max)
 | |
|         self._usage = usage
 | |
| 
 | |
|         try:
 | |
|             if all(usage_type in self._INCOMPATIBLE_TYPES for usage_type in usage.usage_types):
 | |
|                 warnings.warn(HIDComplianceWarning(f"{usage} has no compatible usage types with a variable item"))  # noqa
 | |
|         except (KeyError, ValueError):
 | |
|             pass
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         return f"VariableItem(offset={self.offset}, size={self.size}, usage={self.usage})"
 | |
| 
 | |
|     def parse(self, data: Sequence[int]) -> UsageValue:
 | |
|         data = _data_bit_shift(data, self.offset, self.size)
 | |
| 
 | |
|         if hid_parser.data.UsageTypes.LINEAR_CONTROL in self.usage.usage_types or any(
 | |
|             usage_type in hid_parser.data.UsageTypesData and usage_type != hid_parser.data.UsageTypes.SELECTOR
 | |
|             for usage_type in self.usage.usage_types
 | |
|         ):  # int
 | |
|             value = int.from_bytes(data, byteorder="little")
 | |
|         elif (
 | |
|             hid_parser.data.UsageTypes.ON_OFF_CONTROL in self.usage.usage_types
 | |
|             and not self.preferred_state
 | |
|             and self.logical_min == -1
 | |
|             and self.logical_max == 1
 | |
|         ):  # bool - -1 is false
 | |
|             value = int.from_bytes(data, byteorder="little") == 1
 | |
|         else:  # bool
 | |
|             value = bool.from_bytes(data, byteorder="little")
 | |
| 
 | |
|         return UsageValue(self, value)
 | |
| 
 | |
|     @property
 | |
|     def usage(self) -> Usage:
 | |
|         return self._usage
 | |
| 
 | |
|     # flags (variable only, see HID spec 1.11 page 32)
 | |
| 
 | |
|     @property
 | |
|     def wrap(self) -> bool:
 | |
|         return self._flags & (1 << 3) != 0
 | |
| 
 | |
|     @property
 | |
|     def linear(self) -> bool:
 | |
|         return self._flags & (1 << 4) != 0
 | |
| 
 | |
|     @property
 | |
|     def preferred_state(self) -> bool:
 | |
|         return self._flags & (1 << 5) != 0
 | |
| 
 | |
|     @property
 | |
|     def null_state(self) -> bool:
 | |
|         return self._flags & (1 << 6) != 0
 | |
| 
 | |
|     @property
 | |
|     def buffered_bytes(self) -> bool:
 | |
|         return self._flags & (1 << 7) != 0
 | |
| 
 | |
|     @property
 | |
|     def bitfield(self) -> bool:
 | |
|         return self._flags & (1 << 7) == 0
 | |
| 
 | |
| 
 | |
| class ArrayItem(MainItem):
 | |
|     _INCOMPATIBLE_TYPES = (
 | |
|         # variable types
 | |
|         hid_parser.data.UsageTypes.LINEAR_CONTROL,
 | |
|         hid_parser.data.UsageTypes.ON_OFF_CONTROL,
 | |
|         hid_parser.data.UsageTypes.MOMENTARY_CONTROL,
 | |
|         hid_parser.data.UsageTypes.ONE_SHOT_CONTROL,
 | |
|         hid_parser.data.UsageTypes.RE_TRIGGER_CONTROL,
 | |
|         hid_parser.data.UsageTypes.STATIC_VALUE,
 | |
|         hid_parser.data.UsageTypes.STATIC_FLAG,
 | |
|         hid_parser.data.UsageTypes.DYNAMIC_VALUE,
 | |
|         hid_parser.data.UsageTypes.DYNAMIC_FLAG,
 | |
|         # collection types
 | |
|         hid_parser.data.UsageTypes.NAMED_ARRAY,
 | |
|         hid_parser.data.UsageTypes.COLLECTION_APPLICATION,
 | |
|         hid_parser.data.UsageTypes.COLLECTION_LOGICAL,
 | |
|         hid_parser.data.UsageTypes.COLLECTION_PHYSICAL,
 | |
|         hid_parser.data.UsageTypes.USAGE_SWITCH,
 | |
|         hid_parser.data.UsageTypes.USAGE_MODIFIER,
 | |
|     )
 | |
|     _IGNORE_USAGE_VALUES = ((hid_parser.data.UsagePages.KEYBOARD_KEYPAD_PAGE, hid_parser.data.KeyboardKeypad.NO_EVENT),)
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         offset: int,
 | |
|         size: int,
 | |
|         count: int,
 | |
|         flags: int,
 | |
|         usages: List[Usage],
 | |
|         logical_min: int,
 | |
|         logical_max: int,
 | |
|         physical_min: Optional[int] = None,
 | |
|         physical_max: Optional[int] = None,
 | |
|     ):
 | |
|         super().__init__(offset, size, flags, logical_min, logical_max, physical_min, physical_max)
 | |
|         self._count = count
 | |
|         self._usages = usages
 | |
|         self._page = self._usages[0].page if usages else None
 | |
| 
 | |
|         for usage in self._usages:
 | |
|             if usage.page != self._page:
 | |
|                 raise ValueError(f"Mismatching usage page in usage: {usage} (expecting {self._usages[0]})")
 | |
|             try:
 | |
|                 if all(usage_type in self._INCOMPATIBLE_TYPES for usage_type in usage.usage_types):
 | |
|                     warnings.warn(HIDComplianceWarning(f"{usage} has no compatible usage types with an array item"))  # noqa
 | |
|             except (KeyError, ValueError):
 | |
|                 pass
 | |
| 
 | |
|         self._ignore_usages: List[Usage] = []
 | |
|         for page, usage_id in self._IGNORE_USAGE_VALUES:
 | |
|             assert isinstance(page, int) and isinstance(usage_id, int)
 | |
|             self._ignore_usages.append(Usage(page, usage_id))
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         return (
 | |
|             textwrap.dedent(
 | |
|                 """
 | |
|             ArrayItem(
 | |
|                 offset={}, size={}, count={},
 | |
|                 usages=[
 | |
|                     {},
 | |
|                 ],
 | |
|             )
 | |
|         """
 | |
|             )
 | |
|             .strip()
 | |
|             .format(
 | |
|                 self.offset,
 | |
|                 self.size,
 | |
|                 self.count,
 | |
|                 ",\n        ".join(repr(usage) for usage in self.usages),
 | |
|             )
 | |
|         )
 | |
| 
 | |
|     def parse(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
 | |
|         usage_values: Dict[Usage, UsageValue] = {}
 | |
| 
 | |
|         for i in range(self.count):
 | |
|             aligned_data = _data_bit_shift(data, self.offset + i * 8, self.size)
 | |
|             usage = Usage(self._page, int.from_bytes(aligned_data, byteorder="little"))
 | |
| 
 | |
|             if usage in self._ignore_usages:
 | |
|                 continue
 | |
| 
 | |
|             # vendor usages don't have usage any standard type - just save the raw data
 | |
|             if usage.page in hid_parser.data.UsagePages.VENDOR_PAGE:
 | |
|                 if usage not in usage_values:
 | |
|                     usage_values[usage] = VendorUsageValue(
 | |
|                         self,
 | |
|                         value=int.from_bytes(aligned_data, byteorder="little"),
 | |
|                     )
 | |
|                 typing.cast(VendorUsageValue, usage_values[usage]).list.append(
 | |
|                     int.from_bytes(aligned_data, byteorder="little")
 | |
|                 )
 | |
|                 continue
 | |
| 
 | |
|             if usage in self._usages and all(usage_type not in self._INCOMPATIBLE_TYPES for usage_type in usage.usage_types):
 | |
|                 usage_values[usage] = UsageValue(self, True)
 | |
| 
 | |
|         return usage_values
 | |
| 
 | |
|     @property
 | |
|     def count(self) -> int:
 | |
|         return self._count
 | |
| 
 | |
|     @property
 | |
|     def usages(self) -> List[Usage]:
 | |
|         return self._usages
 | |
| 
 | |
| 
 | |
| class InvalidReportDescriptor(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| # report ID (None for no report ID), item list
 | |
| _ITEM_POOL = Dict[Optional[int], List[BaseItem]]
 | |
| 
 | |
| 
 | |
| class ReportDescriptor:
 | |
|     def __init__(self, data: Sequence[int]) -> None:
 | |
|         self._data = data
 | |
| 
 | |
|         for byte in data:
 | |
|             if byte < 0 or byte > 255:
 | |
|                 raise InvalidReportDescriptor(
 | |
|                     f"A report descriptor should be represented by a list of bytes: found value {byte}"
 | |
|                 )
 | |
| 
 | |
|         self._input: _ITEM_POOL = {}
 | |
|         self._output: _ITEM_POOL = {}
 | |
|         self._feature: _ITEM_POOL = {}
 | |
| 
 | |
|         self._parse()
 | |
| 
 | |
|     @property
 | |
|     def data(self) -> Sequence[int]:
 | |
|         return self._data
 | |
| 
 | |
|     @property
 | |
|     def input_report_ids(self) -> List[Optional[int]]:
 | |
|         return list(self._input.keys())
 | |
| 
 | |
|     @property
 | |
|     def output_report_ids(self) -> List[Optional[int]]:
 | |
|         return list(self._output.keys())
 | |
| 
 | |
|     @property
 | |
|     def feature_report_ids(self) -> List[Optional[int]]:
 | |
|         return list(self._feature.keys())
 | |
| 
 | |
|     def _get_report_size(self, items: List[BaseItem]) -> BitNumber:
 | |
|         size = 0
 | |
|         for item in items:
 | |
|             if isinstance(item, ArrayItem):
 | |
|                 size += item.size * item.count
 | |
|             else:
 | |
|                 size += item.size
 | |
|         return BitNumber(size)
 | |
| 
 | |
|     def get_input_items(self, report_id: Optional[int] = None) -> List[BaseItem]:
 | |
|         return self._input[report_id]
 | |
| 
 | |
|     @functools.lru_cache(maxsize=16)  # noqa
 | |
|     def get_input_report_size(self, report_id: Optional[int] = None) -> BitNumber:
 | |
|         return self._get_report_size(self.get_input_items(report_id))
 | |
| 
 | |
|     def get_output_items(self, report_id: Optional[int] = None) -> List[BaseItem]:
 | |
|         return self._output[report_id]
 | |
| 
 | |
|     @functools.lru_cache(maxsize=16)  # noqa
 | |
|     def get_output_report_size(self, report_id: Optional[int] = None) -> BitNumber:
 | |
|         return self._get_report_size(self.get_output_items(report_id))
 | |
| 
 | |
|     def get_feature_items(self, report_id: Optional[int] = None) -> List[BaseItem]:
 | |
|         return self._feature[report_id]
 | |
| 
 | |
|     @functools.lru_cache(maxsize=16)  # noqa
 | |
|     def get_feature_report_size(self, report_id: Optional[int] = None) -> BitNumber:
 | |
|         return self._get_report_size(self.get_feature_items(report_id))
 | |
| 
 | |
|     def _parse_report_items(self, items: List[BaseItem], data: Sequence[int]) -> Dict[Usage, UsageValue]:
 | |
|         parsed: Dict[Usage, UsageValue] = {}
 | |
|         for item in items:
 | |
|             if isinstance(item, VariableItem):
 | |
|                 parsed[item.usage] = item.parse(data)
 | |
|             elif isinstance(item, ArrayItem):
 | |
|                 usage_values = item.parse(data)
 | |
|                 for usage in usage_values:
 | |
|                     if usage in parsed:
 | |
|                         warnings.warn(HIDReportWarning(f"Overriding usage: {usage}"))  # noqa
 | |
|                 parsed.update(usage_values)
 | |
|             elif isinstance(item, PaddingItem):
 | |
|                 pass
 | |
|             else:
 | |
|                 raise TypeError(f"Unknown item: {item}")
 | |
|         return parsed
 | |
| 
 | |
|     def _parse_report(self, item_poll: _ITEM_POOL, data: Sequence[int]) -> Dict[Usage, UsageValue]:
 | |
|         if None in item_poll:  # unnumbered reports
 | |
|             return self._parse_report_items(item_poll[None], data)
 | |
|         else:  # numbered reports
 | |
|             return self._parse_report_items(item_poll[data[0]], data[1:])
 | |
| 
 | |
|     def parse_input_report(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
 | |
|         return self._parse_report(self._input, data)
 | |
| 
 | |
|     def parse_output_report(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
 | |
|         return self._parse_report(self._output, data)
 | |
| 
 | |
|     def parse_feature_report(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
 | |
|         return self._parse_report(self._feature, data)
 | |
| 
 | |
|     def _iterate_raw(self) -> Iterable[Tuple[int, int, Optional[int]]]:
 | |
|         i = 0
 | |
|         while i < len(self.data):
 | |
|             prefix = self.data[i]
 | |
|             tag = (prefix & 0b11110000) >> 4
 | |
|             typ = (prefix & 0b00001100) >> 2
 | |
|             size = prefix & 0b00000011
 | |
| 
 | |
|             if size == 3:  # 6.2.2.2
 | |
|                 size = 4
 | |
| 
 | |
|             if size == 0:
 | |
|                 data = None
 | |
|             elif size == 1:
 | |
|                 if i + 1 >= len(self.data):
 | |
|                     raise InvalidReportDescriptor(f"Invalid size: expecting >={i + 1}, got {len(self.data)}")
 | |
|                 data = self.data[i + 1]
 | |
|             else:
 | |
|                 if i + 1 + size >= len(self.data):
 | |
|                     raise InvalidReportDescriptor(f"Invalid size: expecting >={i + 1 + size}, got {len(self.data)}")
 | |
|                 if size == 2:
 | |
|                     pack_type = "H"
 | |
|                 elif size == 4:
 | |
|                     pack_type = "L"
 | |
|                 else:
 | |
|                     raise ValueError(f"Invalid item size: {size}")
 | |
|                 data = struct.unpack(f"<{pack_type}", bytes(self.data[i + 1 : i + 1 + size]))[0]
 | |
| 
 | |
|             yield typ, tag, data
 | |
| 
 | |
|             i += size + 1
 | |
| 
 | |
|     def _append_item(
 | |
|         self,
 | |
|         offset_list: Dict[Optional[int], int],
 | |
|         pool: _ITEM_POOL,
 | |
|         report_id: Optional[int],
 | |
|         item: BaseItem,
 | |
|     ) -> None:
 | |
|         offset_list[report_id] += item.size
 | |
|         if report_id in pool:
 | |
|             pool[report_id].append(item)
 | |
|         else:
 | |
|             pool[report_id] = [item]
 | |
| 
 | |
|     def _append_items(
 | |
|         self,
 | |
|         offset_list: Dict[Optional[int], int],
 | |
|         pool: _ITEM_POOL,
 | |
|         report_id: Optional[int],
 | |
|         report_count: int,
 | |
|         report_size: int,
 | |
|         usages: List[Usage],
 | |
|         flags: int,
 | |
|         data: Dict[str, Any],
 | |
|     ) -> None:
 | |
|         item: BaseItem
 | |
|         is_array = flags & (1 << 1) == 0  # otherwise variable
 | |
| 
 | |
|         """
 | |
|         HID 1.11, 6.2.2.9 says reports can be byte aligned by declaring a
 | |
|         main item without usage. A main item can have multiple usages, as I
 | |
|         interpret it, items are only considered padding when they have NO
 | |
|         usages.
 | |
|         """
 | |
|         if len(usages) == 0 or not usages:
 | |
|             for _ in range(report_count):
 | |
|                 item = PaddingItem(offset_list[report_id], report_size)
 | |
|                 self._append_item(offset_list, pool, report_id, item)
 | |
|             return
 | |
| 
 | |
|         if is_array:
 | |
|             item = ArrayItem(
 | |
|                 offset=offset_list[report_id],
 | |
|                 size=report_size,
 | |
|                 usages=usages,
 | |
|                 count=report_count,
 | |
|                 flags=flags,
 | |
|                 **data,
 | |
|             )
 | |
|             self._append_item(offset_list, pool, report_id, item)
 | |
|         else:
 | |
|             if len(usages) != report_count:
 | |
|                 error_str = f"Expecting {report_count} usages but got {len(usages)}"
 | |
|                 if len(usages) == 1:
 | |
|                     warnings.warn(HIDComplianceWarning(error_str))  # noqa
 | |
|                     usages *= report_count
 | |
|                 else:
 | |
|                     raise InvalidReportDescriptor(error_str)
 | |
| 
 | |
|             for usage in usages:
 | |
|                 item = VariableItem(
 | |
|                     offset=offset_list[report_id],
 | |
|                     size=report_size,
 | |
|                     usage=usage,
 | |
|                     flags=flags,
 | |
|                     **data,
 | |
|                 )
 | |
|                 self._append_item(offset_list, pool, report_id, item)
 | |
| 
 | |
|     def _parse(self, level: int = 0, file: TextIO = sys.stdout) -> None:  # noqa: C901
 | |
|         offset_input: Dict[Optional[int], int] = {
 | |
|             None: 0,
 | |
|         }
 | |
|         offset_output: Dict[Optional[int], int] = {
 | |
|             None: 0,
 | |
|         }
 | |
|         offset_feature: Dict[Optional[int], int] = {
 | |
|             None: 0,
 | |
|         }
 | |
|         report_id: Optional[int] = None
 | |
|         report_count: Optional[int] = None
 | |
|         report_size: Optional[int] = None
 | |
|         usage_page: Optional[int] = None
 | |
|         usages: List[Usage] = []
 | |
|         usage_min: Optional[int] = None
 | |
|         glob: Dict[str, Any] = {}
 | |
|         local: Dict[str, Any] = {}
 | |
| 
 | |
|         for typ, tag, data in self._iterate_raw():
 | |
|             if typ == Type.MAIN:
 | |
|                 if tag in (TagMain.COLLECTION, TagMain.END_COLLECTION):
 | |
|                     usages = []
 | |
| 
 | |
|                 # we only care about input, output and features for now
 | |
|                 if tag not in (TagMain.INPUT, TagMain.OUTPUT, TagMain.FEATURE):
 | |
|                     continue
 | |
| 
 | |
|                 if report_count is None:
 | |
|                     raise InvalidReportDescriptor("Trying to append an item but no report count given")
 | |
|                 if report_size is None:
 | |
|                     raise InvalidReportDescriptor("Trying to append an item but no report size given")
 | |
| 
 | |
|                 if tag == TagMain.INPUT:
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid input item")
 | |
|                     self._append_items(
 | |
|                         offset_input, self._input, report_id, report_count, report_size, usages, data, {**glob, **local}
 | |
|                     )
 | |
| 
 | |
|                 elif tag == TagMain.OUTPUT:
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid output item")
 | |
|                     self._append_items(
 | |
|                         offset_output, self._output, report_id, report_count, report_size, usages, data, {**glob, **local}
 | |
|                     )
 | |
| 
 | |
|                 elif tag == TagMain.FEATURE:
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid feature item")
 | |
|                     self._append_items(
 | |
|                         offset_feature, self._feature, report_id, report_count, report_size, usages, data, {**glob, **local}
 | |
|                     )
 | |
| 
 | |
|                 # clear local
 | |
|                 usages = []
 | |
|                 usage_min = None
 | |
|                 local = {}
 | |
| 
 | |
|                 # we don't care about collections for now, maybe in the future...
 | |
| 
 | |
|             elif typ == Type.GLOBAL:
 | |
|                 if tag == TagGlobal.USAGE_PAGE:
 | |
|                     usage_page = data
 | |
| 
 | |
|                 elif tag == TagGlobal.LOGICAL_MINIMUM:
 | |
|                     glob["logical_min"] = data
 | |
| 
 | |
|                 elif tag == TagGlobal.LOGICAL_MAXIMUM:
 | |
|                     glob["logical_max"] = data
 | |
| 
 | |
|                 elif tag == TagGlobal.PHYSICAL_MINIMUM:
 | |
|                     glob["physical_min"] = data
 | |
| 
 | |
|                 elif tag == TagGlobal.PHYSICAL_MAXIMUM:
 | |
|                     glob["physical_max"] = data
 | |
| 
 | |
|                 elif tag == TagGlobal.REPORT_SIZE:
 | |
|                     report_size = data
 | |
| 
 | |
|                 elif tag == TagGlobal.REPORT_ID:
 | |
|                     if not report_id and (self._input or self._output or self._feature):
 | |
|                         raise InvalidReportDescriptor("Tried to set a report ID in a report that does not use them")
 | |
|                     report_id = data
 | |
|                     # initialize the item offset for this report ID
 | |
|                     for offset_list in (offset_input, offset_output, offset_feature):
 | |
|                         if report_id not in offset_list:
 | |
|                             offset_list[report_id] = 0
 | |
| 
 | |
|                 elif tag in (TagGlobal.UNIT, TagGlobal.UNIT_EXPONENT):
 | |
|                     warnings.warn(  # noqa
 | |
|                         HIDUnsupportedWarning("Data specifies a unit or unit exponent, but we don't support those yet")
 | |
|                     )
 | |
| 
 | |
|                 elif tag in (TagGlobal.PUSH, TagGlobal.POP):
 | |
|                     warnings.warn(HIDUnsupportedWarning("Push and pop are not supported yet"))  # noqa
 | |
| 
 | |
|                 elif tag == TagGlobal.REPORT_COUNT:
 | |
|                     report_count = data
 | |
| 
 | |
|                 else:
 | |
|                     raise NotImplementedError(f"Unsupported global tag: {bin(tag)}")
 | |
| 
 | |
|             elif typ == Type.LOCAL:
 | |
|                 if tag == TagLocal.USAGE:
 | |
|                     if usage_page is None:
 | |
|                         raise InvalidReportDescriptor("Usage field found but no usage page")
 | |
|                     usages.append(Usage(usage_page, data))
 | |
| 
 | |
|                 elif tag == TagLocal.USAGE_MINIMUM:
 | |
|                     usage_min = data
 | |
| 
 | |
|                 elif tag == TagLocal.USAGE_MAXIMUM:
 | |
|                     if usage_min is None:
 | |
|                         raise InvalidReportDescriptor("Usage maximum set but no usage minimum")
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid usage maximum value")
 | |
|                     for i in range(usage_min, data + 1):
 | |
|                         usages.append(Usage(usage_page, i))
 | |
|                     usage_min = None
 | |
| 
 | |
|                 elif tag in (TagLocal.STRING_INDEX, TagLocal.STRING_MINIMUM, TagLocal.STRING_MAXIMUM):
 | |
|                     pass  # we don't care about this information to parse the reports
 | |
| 
 | |
|                 else:
 | |
|                     raise NotImplementedError(f"Unsupported local tag: {bin(tag)}")
 | |
| 
 | |
|     @staticmethod
 | |
|     def _get_main_item_desc(value: int) -> str:
 | |
|         fields = [
 | |
|             "Constant" if value & (1 << 0) else "Data",
 | |
|             "Variable" if value & (1 << 1) else "Array",
 | |
|             "Relative" if value & (1 << 2) else "Absolute",
 | |
|         ]
 | |
|         if value & (1 << 1):
 | |
|             # variable only
 | |
|             fields += [
 | |
|                 "Wrap" if value & (1 << 3) else "No Wrap",
 | |
|                 "Non Linear" if value & (1 << 4) else "Linear",
 | |
|                 "No Preferred State" if value & (1 << 5) else "Preferred State",
 | |
|                 "Null State" if value & (1 << 6) else "No Null position",
 | |
|                 "Buffered Bytes" if value & (1 << 8) else "Bit Field",
 | |
|             ]
 | |
|         return ", ".join(fields)
 | |
| 
 | |
|     def print(self, level: int = 0, file: TextIO = sys.stdout) -> None:  # noqa: C901
 | |
|         def printl(string: str) -> None:
 | |
|             print(" " * level + string, file=file)
 | |
| 
 | |
|         usage_data: Union[Literal[False], Optional[hid_parser.data._Data]] = False
 | |
| 
 | |
|         for typ, tag, data in self._iterate_raw():
 | |
|             if typ == Type.MAIN:
 | |
|                 if tag == TagMain.INPUT:
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid input item")
 | |
|                     printl(f"Input ({self._get_main_item_desc(data)})")
 | |
| 
 | |
|                 elif tag == TagMain.OUTPUT:
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid output item")
 | |
|                     printl(f"Output ({self._get_main_item_desc(data)})")
 | |
| 
 | |
|                 elif tag == TagMain.FEATURE:
 | |
|                     if data is None:
 | |
|                         raise InvalidReportDescriptor("Invalid feature item")
 | |
|                     printl(f"Feature ({self._get_main_item_desc(data)})")
 | |
| 
 | |
|                 elif tag == TagMain.COLLECTION:
 | |
|                     printl(f"Collection ({hid_parser.data.Collections.get_description(data)})")
 | |
|                     level += 1
 | |
| 
 | |
|                 elif tag == TagMain.END_COLLECTION:
 | |
|                     level -= 1
 | |
|                     printl("End Collection")
 | |
| 
 | |
|             elif typ == Type.GLOBAL:
 | |
|                 if tag == TagGlobal.USAGE_PAGE:
 | |
|                     try:
 | |
|                         printl(f"Usage Page ({hid_parser.data.UsagePages.get_description(data)})")
 | |
|                         try:
 | |
|                             usage_data = hid_parser.data.UsagePages.get_subdata(data)
 | |
|                         except ValueError:
 | |
|                             usage_data = None
 | |
|                     except KeyError:
 | |
|                         printl(f"Usage Page (Unknown 0x{data:04x})")
 | |
| 
 | |
|                 elif tag == TagGlobal.LOGICAL_MINIMUM:
 | |
|                     printl(f"Logical Minimum ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.LOGICAL_MAXIMUM:
 | |
|                     printl(f"Logical Maximum ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.PHYSICAL_MINIMUM:
 | |
|                     printl(f"Physical Minimum ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.PHYSICAL_MAXIMUM:
 | |
|                     printl(f"Physical Maximum ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.UNIT_EXPONENT:
 | |
|                     printl(f"Unit Exponent (0x{data:04x})")
 | |
| 
 | |
|                 elif tag == TagGlobal.UNIT:
 | |
|                     printl(f"Unit (0x{data:04x})")
 | |
| 
 | |
|                 elif tag == TagGlobal.REPORT_SIZE:
 | |
|                     printl(f"Report Size ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.REPORT_ID:
 | |
|                     printl(f"Report ID (0x{data:02x})")
 | |
| 
 | |
|                 elif tag == TagGlobal.REPORT_COUNT:
 | |
|                     printl(f"Report Count ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.PUSH:
 | |
|                     printl(f"Push ({data})")
 | |
| 
 | |
|                 elif tag == TagGlobal.POP:
 | |
|                     printl(f"Pop ({data})")
 | |
| 
 | |
|             elif typ == Type.LOCAL:
 | |
|                 if tag == TagLocal.USAGE:
 | |
|                     if usage_data is False:
 | |
|                         raise InvalidReportDescriptor("Usage field found but no usage page")
 | |
| 
 | |
|                     if usage_data:
 | |
|                         try:
 | |
|                             printl(f"Usage ({usage_data.get_description(data)})")
 | |
|                         except KeyError:
 | |
|                             printl(f"Usage (Unknown, 0x{data:04x})")
 | |
|                     else:
 | |
|                         printl(f"Usage (0x{data:04x})")
 | |
| 
 | |
|                 elif tag == TagLocal.USAGE_MINIMUM:
 | |
|                     printl(f"Usage Minimum ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.USAGE_MAXIMUM:
 | |
|                     printl(f"Usage Maximum ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.DESIGNATOR_INDEX:
 | |
|                     printl(f"Designator Index ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.DESIGNATOR_MINIMUM:
 | |
|                     printl(f"Designator Minimum ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.DESIGNATOR_MAXIMUM:
 | |
|                     printl(f"Designator Maximum ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.STRING_INDEX:
 | |
|                     printl(f"String Index ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.STRING_MINIMUM:
 | |
|                     printl(f"String Minimum ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.STRING_MAXIMUM:
 | |
|                     printl(f"String Maximum ({data})")
 | |
| 
 | |
|                 elif tag == TagLocal.DELIMITER:
 | |
|                     printl(f"Delemiter ({data})")
 |