diff --git a/hid_parser/__init__.py b/hid_parser/__init__.py index 448d46f..bacb3a8 100644 --- a/hid_parser/__init__.py +++ b/hid_parser/__init__.py @@ -819,6 +819,7 @@ def _parse(self, level: int = 0, file: TextIO = sys.stdout) -> None: # noqa: C9 usage_min: int | None = None glob: dict[str, Any] = {} local: dict[str, Any] = {} + state_stack: list[tuple[Any, ...]] = [] for typ, tag, data in self._iterate_raw(): if typ == Type.MAIN: @@ -926,6 +927,27 @@ def _parse(self, level: int = 0, file: TextIO = sys.stdout) -> None: # noqa: C9 elif tag == TagGlobal.REPORT_COUNT: report_count = data + elif tag == TagGlobal.PUSH: + state_stack.append( + ( + usage_page, + report_id, + report_count, + report_size, + dict(glob), + ) + ) + + elif tag == TagGlobal.POP: + if not state_stack: + msg = 'Pop without a matching Push' + raise InvalidReportDescriptor(msg) + usage_page, report_id, report_count, report_size, glob = state_stack.pop() + # a pushed/recovered report id may have no offset slot yet + for offset_list in (offset_input, offset_output, offset_feature): + if report_id not in offset_list: + offset_list[report_id] = 0 + else: msg = f'Unsupported global tag: {bin(tag)}' raise NotImplementedError(msg) diff --git a/tests/test_parse.py b/tests/test_parse.py index ae44ef6..23be027 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -295,3 +295,72 @@ def test_hypothesis(rdesc): hid_parser.ReportDescriptor(rdesc) except (hid_parser.InvalidReportDescriptor, NotImplementedError): pass + + +# fmt: off + +push_pop_rdesc = [ + 0x05, 0x01, # Usage Page (Generic Desktop) 0 + 0x09, 0x02, # Usage (Mouse) 2 + 0xa1, 0x01, # Collection (Application) 4 + 0x09, 0x01, # Usage (Pointer) 6 + 0xa1, 0x00, # Collection (Physical) 8 + 0x15, 0x00, # Logical Minimum (0) 10 + 0x25, 0x7f, # Logical Maximum (127) 12 + 0x75, 0x08, # Report Size (8) 14 + 0x95, 0x01, # Report Count (1) 16 + 0xa4, # Push 18 + 0x15, 0x81, # Logical Minimum (-127) 19 + 0x25, 0x7f, # Logical Maximum (127) 21 + 0x75, 0x08, # Report Size (8) 23 + 0x95, 0x02, # Report Count (2) 25 + 0x09, 0x30, # Usage (X) 27 + 0x09, 0x31, # Usage (Y) 29 + 0x81, 0x02, # Input (Data,Var,Abs) 31 + 0xb4, # Pop 33 + 0x09, 0x38, # Usage (Wheel) 34 + 0x81, 0x02, # Input (Data,Var,Abs) 36 + 0xc0, # End Collection 38 + 0xc0, # End Collection 39 +] + +# fmt: on + + +def test_parse_push_pop(): + rdesc = hid_parser.ReportDescriptor(push_pop_rdesc) + + items = rdesc.get_input_items() + + assert len(items) == 3 + + x, y, wheel = items + + assert isinstance(x, hid_parser.VariableItem) + assert x.usage == hid_parser.Usage(hid_parser.data.UsagePages.GENERIC_DESKTOP_CONTROLS_PAGE, 0x30) + assert int(x.offset) == 0 + assert int(x.size) == 8 + assert x.logical_min == -127 + assert x.logical_max == 127 + + assert isinstance(y, hid_parser.VariableItem) + assert y.usage == hid_parser.Usage(hid_parser.data.UsagePages.GENERIC_DESKTOP_CONTROLS_PAGE, 0x31) + assert int(y.offset) == 8 + assert int(y.size) == 8 + assert y.logical_min == -127 + assert y.logical_max == 127 + + # after Pop the pushed logical min/max should have been restored + assert isinstance(wheel, hid_parser.VariableItem) + assert wheel.usage == hid_parser.Usage(hid_parser.data.UsagePages.GENERIC_DESKTOP_CONTROLS_PAGE, 0x38) + assert int(wheel.offset) == 8 * 2 + assert int(wheel.size) == 8 + assert wheel.logical_min == 0 + assert wheel.logical_max == 127 + + +def test_parse_pop_without_push_raises(): + rdesc = [0xb4] # Pop without a matching Push + + with pytest.raises(hid_parser.InvalidReportDescriptor): + hid_parser.ReportDescriptor(rdesc)