# 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, Dict, Iterable, Iterator, List, Optional, Sequence, TextIO, Tuple, 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})")