Skip to content

feat: add extended mouse forwarding#169

Merged
quaxalber merged 34 commits intostagingfrom
feat/mouse-pan-forwarding
Apr 27, 2026
Merged

feat: add extended mouse forwarding#169
quaxalber merged 34 commits intostagingfrom
feat/mouse-pan-forwarding

Conversation

@quaxalber
Copy link
Copy Markdown
Owner

@quaxalber quaxalber commented Apr 26, 2026

Summary

Pi/URB findings

  • Validated on a Pi 4B with the loopback harness: mouse-only and keyboard+mouse scenarios passed.
  • usbmon confirmed that a 7-byte configfs report_length caused every 7-byte mouse interrupt-IN completion to be followed by an extra 0-byte completion.
  • Setting the mouse configfs request size to 8 makes the 7-byte HID report a short packet and eliminated the extra 0-byte completions in the capture.
  • Tradeoff: the descriptor and userspace report remain 7 bytes, while configfs advertises an 8-byte request size. This is narrowly scoped to the mouse gadget and should be low risk, but it is still a Pi/dwc2 workaround rather than a semantic HID descriptor change.

Validation

  • venv/bin/black --check src tests
  • venv/bin/ruff check src tests
  • venv/bin/python -m unittest discover -s tests -v
  • venv/bin/python -m compileall src tests
  • python -m build
  • Pi 4B: smoketest.sh --verbose, debug.sh --duration 10, bluetoothctl show, btmgmt info
  • Pi 4B loopback harness plus usbmon before/after capture for the mouse interrupt endpoint
  • Pi 4B loopback with local eac464c8d2c020b87f693c2093a3021e4593ba98 capture harness: combo passed with keyboard_steps_seen=6, mouse_rel_steps_seen=11, mouse_button_steps_seen=4; mouse_fast passed with mouse_rel_steps_seen=8

Closes #11.
Closes #167.
Closes #168.

Summary by CodeRabbit

  • New Features

    • Extended mouse support: hi-res vertical wheel, horizontal pan, larger XY range, extra side/task buttons, chunked/accumulated movement reports; gadget reports use an extended mouse layout.
    • New scenarios: "mouse_fast" and "mouse_buttons_intrusive"; default mouse now uses safe button steps and a coalesced tail.
  • Documentation

    • Expanded test scenario docs with detailed mouse movement, wheel/pan mapping, examples, and Windows-specific limitations.
  • Refactor

    • Event relay buffers/coalesces relative motion and preserves fractional wheel/pan across reports.
  • Tests

    • Broadened coverage for extended mouse format, mapping, coalescing, chunking, and Windows backend validation.

s117 and others added 6 commits April 26, 2026 12:50
(cherry picked from commit 409e2e6)
The extended report uses 16-bit value for X/Y movements, which encodes
fast-moving pointer events better.

(cherry picked from commit eea2332)
Co-authored-by: John L <admin@0x10c.pw>
Co-authored-by: John L <admin@0x10c.pw>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Walkthrough

Adds an extended HID mouse (1-byte buttons, signed 16-bit X/Y, signed 8-bit wheel/pan), maps more BTN_* codes, buffers/coalesces relative events at SYN_REPORT, chunks/clamps movements into HID-sized reports with fractional accumulation, updates relay retry logic, and extends Linux/Windows capture and test harnesses for new scenarios.

Changes

Cohort / File(s) Summary
Extended Mouse HID Implementation
src/bluetooth_2_usb/extended_mouse.py, src/bluetooth_2_usb/hid_layout.py, src/bluetooth_2_usb/evdev.py
New ExtendedMouse class implementing 7‑byte reports (buttons, signed i16 X/Y, signed i8 wheel/pan) with fractional remainder tracking and chunked sends. HID descriptors added/converted to itemized form; evdev mapping expanded to include hi‑res REL_WHEEL/REL_HWHEEL scaled into wheel/pan and additional BTN_* → ExtendedMouse bits.
Relay and Gadget Infrastructure
src/bluetooth_2_usb/relay.py, src/bluetooth_2_usb/gadget_config.py
Relay now buffers relative deltas until SYN_REPORT, coalesces frames, preserves fractional wheel/pan across SYNs, chunks large XY/wheel/pan into multiple HID reports with clamping, replaces Mouse → ExtendedMouse, and centralizes HID action retry logic. Gadget rebuild respects optional configfs_report_length.
Test Harness (Linux & Windows)
src/bluetooth_2_usb/test_harness_common.py, src/bluetooth_2_usb/test_harness_capture.py, src/bluetooth_2_usb/test_harness_inject.py, src/bluetooth_2_usb/test_harness_capture_windows.py
Adds new scenarios (mouse_fast, mouse_buttons_intrusive), button/REL constants, supports coalesced tail counts, injection emits hi‑res REL events and coalesced tails, capture matchers accept 7‑byte reports with wheel/pan and cumulative button state; Windows backend tracks button state, emits signed 16‑bit XY, and fails fast on unsupported intrusive buttons.
Tests
tests/test_relay.py, tests/test_test_harness.py
Extensive tests added/updated for ExtendedMouse encoding, XY chunking, fractional wheel/pan accumulation, SYN coalescing, HID write retry behavior, expanded scenario validation, and Windows prerequisite failure for intrusive buttons. Gadget descriptor/report-length assertions updated.
Documentation
docs/host-relay-loopback.md
Documents new deterministic mouse injection sequence (including vertical/horizontal wheel hi‑res pairs, coalesced tail, side/extra button bits), mouse_fast behavior, mouse report layout, Windows limitations/prereq rejection for intrusive buttons, and warns about mouse_buttons_intrusive host UI interaction.

Sequence Diagram(s)

sequenceDiagram
  participant Input as Input Device / evdev
  participant Parser as evdev.py (parse → x,y,mwheel,pan)
  participant Relay as DeviceRelay (buffer & SYN coalesce)
  participant HID as ExtendedMouse (chunking, fractional accum)
  participant Gadget as USB Gadget / hidraw

  Input->>Parser: raw REL_/BTN_ events
  Parser->>Relay: parsed deltas (x,y,mwheel,pan) + button changes
  Relay->>Relay: accumulate until SYN_REPORT
  Relay->>HID: send chunked move/press actions (via retry wrapper)
  HID->>Gadget: write 7‑byte HID reports (may be multiple)
  Gadget-->>HID: write result / BlockingIOError (retry)
  HID-->>Relay: success / failure (relay may abort)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🖱️ A seven‑byte mouse now sings in bytes,
Wheels both wide and sideways spin through nights,
Frames coalesce, fractions patiently kept,
Buttons mapped where extra bits had slept,
HID reports march out — small, steady flights ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All coding requirements from linked issues #11, #167, and #168 are met: horizontal pan support [#11, #167], signed 16-bit X/Y movement [#167], extended mouse buttons [#167], frame coalescing [#167], and comprehensive validation [#167].
Out of Scope Changes check ✅ Passed The PR maintains focused scope on extended mouse forwarding. Documentation, HID gadget configuration, relay logic, test harness, and validation changes are all directly tied to the mouse extension objectives without introducing unrelated features.
Title check ✅ Passed The title 'feat: add extended mouse forwarding' directly reflects the main objective of the PR, which is to add support for extended mouse forwarding including extra buttons, horizontal pan, and signed 16-bit relative movement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mouse-pan-forwarding

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/bluetooth_2_usb/relay.py (2)

231-238: Docstring return type is stale.

The docstring at line 235-236 mentions "Mouse object" but the method now returns ExtendedMouse | None.

📝 Proposed doc fix
     def get_mouse(self) -> ExtendedMouse | None:
         """
         Get the Mouse gadget.

-        :return: A Mouse object, or None if not initialized
-        :rtype: Mouse | None
+        :return: An ExtendedMouse object, or None if not initialized
+        :rtype: ExtendedMouse | None
         """
         return self._gadgets["mouse"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/relay.py` around lines 231 - 238, The docstring for
get_mouse() is out of date: it says "A Mouse object" and uses :rtype: Mouse |
None, but the method returns ExtendedMouse | None; update the docstring in
get_mouse (and the :rtype/:return description) to reference ExtendedMouse
instead of Mouse so the docstring matches the actual return type.

915-950: Consider extracting shared retry/error logic with _process_event_with_retry.

Both methods share identical retry loop structure, delay, and exception handling for BlockingIOError and BrokenPipeError. A shared helper could reduce duplication, but given this is a Raspberry Pi HID gadget with specific failure modes, keeping them separate for clarity is also reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/relay.py` around lines 915 - 950, The retry and exception
handling in _process_mouse_delta_with_retry is identical to other handlers —
extract the shared loop into a helper _process_event_with_retry(action:
Callable[..., Any], action_name: str, *args, **kwargs) that encapsulates the
for-loop, BlockingIOError handling (incrementing self._hid_write_retries and
sleeping), BrokenPipeError handling (incrementing self._hid_write_failures,
logging the detailed message, clearing self._relaying_active if set, and
returning), and generic Exception handling (incrementing
self._hid_write_failures and logging); then replace the body of
_process_mouse_delta_with_retry to call _process_event_with_retry(lambda:
self._gadget_manager.get_mouse().move(x,y,wheel,pan), "mouse.move", ) while
ensuring you still raise/handle the "mouse gadget not initialized" case inside
the action or check before calling the helper so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bluetooth_2_usb/extended_mouse.py`:
- Around line 47-49: The move method currently casts pan to int which drops
fractional hi-res pan values coming from get_mouse_movement; change the logic in
move (the pan variable in def move) to preserve fractional parts across
reporting cycles: keep pan as a float, compute the integer amount to send each
iteration (using sign-aware truncation, e.g.,
int(math.copysign(math.floor(abs(pan)), pan)) or extracting the integer part
while preserving sign), subtract that sent integer from pan to retain the
fractional remainder, and use the integer amount when emitting REL_HWHEEL_HI_RES
reports so slow/high-resolution horizontal scroll accumulates correctly across
calls.

In `@src/bluetooth_2_usb/test_harness_capture_windows.py`:
- Around line 493-502: The code currently returns early when RI_MOUSE_WHEEL or
RI_MOUSE_HORIZONTAL_WHEEL are set, dropping lLastX/lLastY and preventing
coalesced REL_* steps; instead, detect wheel flags from raw_mouse.ulButtons
(button_flags), extract the wheel value (use ctypes.c_short((raw_mouse.ulButtons
>> 16) & 0xFFFF).value for both vertical and horizontal as needed), append the
appropriate wheel report bytes to reports (like you already do for pan) but do
NOT return immediately—remove the early returns in the RI_MOUSE_WHEEL and
RI_MOUSE_HORIZONTAL_WHEEL branches so the function continues to process
lLastX/lLastY and other flags before returning the combined reports. Ensure you
still only append wheel reports when the extracted wheel value is nonzero.

---

Nitpick comments:
In `@src/bluetooth_2_usb/relay.py`:
- Around line 231-238: The docstring for get_mouse() is out of date: it says "A
Mouse object" and uses :rtype: Mouse | None, but the method returns
ExtendedMouse | None; update the docstring in get_mouse (and the :rtype/:return
description) to reference ExtendedMouse instead of Mouse so the docstring
matches the actual return type.
- Around line 915-950: The retry and exception handling in
_process_mouse_delta_with_retry is identical to other handlers — extract the
shared loop into a helper _process_event_with_retry(action: Callable[..., Any],
action_name: str, *args, **kwargs) that encapsulates the for-loop,
BlockingIOError handling (incrementing self._hid_write_retries and sleeping),
BrokenPipeError handling (incrementing self._hid_write_failures, logging the
detailed message, clearing self._relaying_active if set, and returning), and
generic Exception handling (incrementing self._hid_write_failures and logging);
then replace the body of _process_mouse_delta_with_retry to call
_process_event_with_retry(lambda:
self._gadget_manager.get_mouse().move(x,y,wheel,pan), "mouse.move", ) while
ensuring you still raise/handle the "mouse gadget not initialized" case inside
the action or check before calling the helper so behavior remains identical.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bf5c8be2-ca87-4a6d-a33b-d0dba38bd0b8

📥 Commits

Reviewing files that changed from the base of the PR and between 98aea26 and dc927f5.

📒 Files selected for processing (12)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/gadget_config.py
  • src/bluetooth_2_usb/hid_layout.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • src/bluetooth_2_usb/test_harness_inject.py
  • tests/test_relay.py
  • tests/test_test_harness.py

Comment thread src/bluetooth_2_usb/extended_mouse.py Outdated
Comment thread src/bluetooth_2_usb/test_harness_capture_windows.py Outdated
@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/bluetooth_2_usb/relay.py (1)

1052-1064: Note: move_mouse now appears unused in the coalescing flow.

With RelEvent events handled via _accumulate_mouse_movement + _flush_pending_mouse_movement, this function isn't called from DeviceRelay. It's still reachable through relay_event() for external callers. If this is intentional API preservation, consider adding a brief docstring note; otherwise it could be removed in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/relay.py` around lines 1052 - 1064, The move_mouse
function appears unused by the internal coalescing flow
(_accumulate_mouse_movement and _flush_pending_mouse_movement) but is still
reachable via relay_event and may be intended as a public compatibility API;
either add a brief docstring note on move_mouse clarifying it's preserved for
external callers/legacy API (mention DeviceRelay and relay_event as callers) or
remove the function in a follow-up cleanup if truly unused — update the
docstring on move_mouse to state its preserved/legacy purpose and intended
callers (relay_event/DeviceRelay) so future maintainers know why it remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/bluetooth_2_usb/relay.py`:
- Around line 1052-1064: The move_mouse function appears unused by the internal
coalescing flow (_accumulate_mouse_movement and _flush_pending_mouse_movement)
but is still reachable via relay_event and may be intended as a public
compatibility API; either add a brief docstring note on move_mouse clarifying
it's preserved for external callers/legacy API (mention DeviceRelay and
relay_event as callers) or remove the function in a follow-up cleanup if truly
unused — update the docstring on move_mouse to state its preserved/legacy
purpose and intended callers (relay_event/DeviceRelay) so future maintainers
know why it remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef1e26a1-a65a-4bbc-828e-5256177c5fca

📥 Commits

Reviewing files that changed from the base of the PR and between 98aea26 and ccc095a.

📒 Files selected for processing (12)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/gadget_config.py
  • src/bluetooth_2_usb/hid_layout.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • src/bluetooth_2_usb/test_harness_inject.py
  • tests/test_relay.py
  • tests/test_test_harness.py

@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/bluetooth_2_usb/hid_layout.py (1)

80-88: AC Pan HID descriptor bytes are functionally correct.

The web search confirmed 0x0238 is the correct usage code for AC Pan (Consumer page 0x0C). The bytes parse correctly as a valid HID descriptor. However, the extended 2-byte usage item (0x0A 0x38 0x02) crosses the declared two-byte row boundary, violating the stated formatting goal on lines 9–10 and making the item boundaries harder to audit at a glance. Consider restructuring lines 81–82 to keep HID items within two-byte rows for consistency with the rest of the descriptor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/hid_layout.py` around lines 80 - 88, The AC Pan extended
usage item (bytes 0x0A, 0x38, 0x02) currently crosses the two-byte row boundary;
update the HID descriptor in hid_layout.py so that the entire extended usage
item is kept on a single row (i.e., group 0x0A, 0x38, 0x02 together) and then
resume the existing two-byte row grouping for subsequent items (like 0x15, 0x81,
0x7F, etc.) so item boundaries are auditable and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/bluetooth_2_usb/hid_layout.py`:
- Around line 80-88: The AC Pan extended usage item (bytes 0x0A, 0x38, 0x02)
currently crosses the two-byte row boundary; update the HID descriptor in
hid_layout.py so that the entire extended usage item is kept on a single row
(i.e., group 0x0A, 0x38, 0x02 together) and then resume the existing two-byte
row grouping for subsequent items (like 0x15, 0x81, 0x7F, etc.) so item
boundaries are auditable and consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 40a2f1fd-8e8f-474d-8ba3-fb9b94f4a68c

📥 Commits

Reviewing files that changed from the base of the PR and between 98aea26 and 1a34b36.

📒 Files selected for processing (13)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/args.py
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/gadget_config.py
  • src/bluetooth_2_usb/hid_layout.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • src/bluetooth_2_usb/test_harness_inject.py
  • tests/test_relay.py
  • tests/test_test_harness.py

@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_test_harness.py (1)

587-624: Add Windows button Raw Input coverage here.

These new _mouse_event_to_reports() tests cover wheel/pan and 16-bit motion, but they still never assert any RI_MOUSE_*_BUTTON_(DOWN|UP) conversion. Since MOUSE_BUTTON_STEPS is now part of mouse/combo, a button down/up case here would catch Windows-only regressions before they turn into harness timeouts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_test_harness.py` around lines 587 - 624, Add a test case
exercising Windows raw input button down/up conversion to
_mouse_event_to_reports: create a RAWMOUSE with ulButtons set to an
RI_MOUSE_*_BUTTON_DOWN (and another for _UP) plus a 16-bit button mask (e.g.,
0x0001 << 16 or 0xFFFF << 16) and assert the produced reports include the
expected button press/release report bytes alongside any wheel/pan/motion
reports; reference the _mouse_event_to_reports function, RAWMOUSE struct,
RI_MOUSE_*_BUTTON_DOWN/UP constants and MOUSE_BUTTON_STEPS to locate where to
add the new assertions so Windows-only regressions are caught.
src/bluetooth_2_usb/hid_layout.py (1)

149-177: Preserve configfs_report_length when cloning from an existing gadget device.

If base_device is already a GadgetHidDevice, omitting the override currently drops its configfs_report_length. Keeping inheritance behavior consistent with other fields avoids accidental fallback to descriptor-length semantics in future clones.

Suggested patch
-            configfs_report_length=configfs_report_length,
+            configfs_report_length=(
+                getattr(base_device, "configfs_report_length", None)
+                if configfs_report_length is None
+                else configfs_report_length
+            ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/hid_layout.py` around lines 149 - 177, When cloning a
GadgetHidDevice, the configfs_report_length field is being set directly to the
passed configfs_report_length argument which drops the base device's value when
the argument is None; update the factory/clone call (the classmethod that
returns cls(...), which constructs a GadgetHidDevice from base_device) to
preserve inheritance by using base_device.configfs_report_length when
configfs_report_length is None (i.e., set configfs_report_length =
base_device.configfs_report_length if the param is None else the provided
param).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bluetooth_2_usb/test_harness_capture_windows.py`:
- Around line 493-511: The _mouse_event_to_reports function currently only emits
wheel/X/Y reports and ignores RI_MOUSE_*_BUTTON_DOWN/UP flags, causing missing
button-state reports; update _mouse_event_to_reports to maintain a persistent
mouse button state (e.g., module-level or self.mouse_button_state) and map
RI_MOUSE_LEFT_BUTTON_DOWN/UP, RI_MOUSE_RIGHT_BUTTON_DOWN/UP,
RI_MOUSE_MIDDLE_BUTTON_DOWN/UP and any extra RI_MOUSE_* button flags into the
first byte of each emitted report (bitmask per button), updating that stored
state when a DOWN sets the bit and UP clears it; include the current accumulated
button-state byte as the first byte when constructing every report (including
wheel, horizontal wheel, X and Y reports) so button press/release events produce
the expected reports for the matcher.

---

Nitpick comments:
In `@src/bluetooth_2_usb/hid_layout.py`:
- Around line 149-177: When cloning a GadgetHidDevice, the
configfs_report_length field is being set directly to the passed
configfs_report_length argument which drops the base device's value when the
argument is None; update the factory/clone call (the classmethod that returns
cls(...), which constructs a GadgetHidDevice from base_device) to preserve
inheritance by using base_device.configfs_report_length when
configfs_report_length is None (i.e., set configfs_report_length =
base_device.configfs_report_length if the param is None else the provided
param).

In `@tests/test_test_harness.py`:
- Around line 587-624: Add a test case exercising Windows raw input button
down/up conversion to _mouse_event_to_reports: create a RAWMOUSE with ulButtons
set to an RI_MOUSE_*_BUTTON_DOWN (and another for _UP) plus a 16-bit button mask
(e.g., 0x0001 << 16 or 0xFFFF << 16) and assert the produced reports include the
expected button press/release report bytes alongside any wheel/pan/motion
reports; reference the _mouse_event_to_reports function, RAWMOUSE struct,
RI_MOUSE_*_BUTTON_DOWN/UP constants and MOUSE_BUTTON_STEPS to locate where to
add the new assertions so Windows-only regressions are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8269d14f-5df3-4844-8b43-efb923cc3830

📥 Commits

Reviewing files that changed from the base of the PR and between 98aea26 and 0f3e2e6.

📒 Files selected for processing (13)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/args.py
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/gadget_config.py
  • src/bluetooth_2_usb/hid_layout.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • src/bluetooth_2_usb/test_harness_inject.py
  • tests/test_relay.py
  • tests/test_test_harness.py

Comment thread src/bluetooth_2_usb/test_harness_capture_windows.py
@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bluetooth_2_usb/evdev.py`:
- Around line 1291-1294: The horizontal wheel handling currently adds both
REL_HWHEEL and REL_HWHEEL_HI_RES into _pending_rel_pan causing doubled pan;
modify the handler in evdev event processing to prefer the hi-res event: when
REL_HWHEEL_HI_RES is received set pan to input_event.value/120 and mark a flag
(e.g., _seen_rel_hwheel_hi_res) for the current SYN frame, and when REL_HWHEEL
is received ignore it if that flag is set for this frame; reset the flag at SYN
(where relay.py accumulates and flushes _pending_rel_pan) so low-res fallback is
only used when no hi-res event arrived in that SYN frame.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 656a7755-3b11-4d44-8378-f2d928373678

📥 Commits

Reviewing files that changed from the base of the PR and between 98aea26 and b40a8d6.

📒 Files selected for processing (13)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/args.py
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/gadget_config.py
  • src/bluetooth_2_usb/hid_layout.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • src/bluetooth_2_usb/test_harness_inject.py
  • tests/test_relay.py
  • tests/test_test_harness.py

Comment thread src/bluetooth_2_usb/evdev.py Outdated
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bluetooth_2_usb/test_harness_capture_windows.py`:
- Around line 559-610: The _mouse_event_to_reports function currently emits
wheel/pan reports before coalesced X/Y, causing the shared mouse sequence to see
pan -> x -> y; change the emission order so any lLastX and lLastY reports are
appended before RI_MOUSE_WHEEL or RI_MOUSE_HORIZONTAL_WHEEL reports (or combine
into a single report containing button state, X bytes, Y bytes, and wheel/pan
bytes) to preserve the coalesced tail; reference symbols:
_mouse_event_to_reports, lLastX, lLastY, RI_MOUSE_WHEEL,
RI_MOUSE_HORIZONTAL_WHEEL, and _mouse_button_state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 05bfbb5c-0c37-4213-bb1f-f271daf8cc4f

📥 Commits

Reviewing files that changed from the base of the PR and between fdb0275 and f578110.

📒 Files selected for processing (7)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • tests/test_relay.py
  • tests/test_test_harness.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/bluetooth_2_usb/extended_mouse.py
  • docs/host-relay-loopback.md
  • tests/test_relay.py
  • tests/test_test_harness.py

Comment thread src/bluetooth_2_usb/test_harness_capture_windows.py
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bluetooth_2_usb/test_harness_capture.py (1)

233-254: ⚠️ Potential issue | 🟠 Major

Validate button state on motion packets too.

Lines 233-250 only compare buttons when the packet has no motion. That means a button press piggybacked onto a movement chunk is accepted even if the scenario expects that button transition later, so misordered or sticky button bits can slip through capture. Motion packets should still be checked against the current _button_state; only the step advancement needs to wait for the zero-motion button report.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/test_harness_capture.py` around lines 233 - 254, The code
only validates button bits when rel_x, rel_y, wheel and pan are all zero; add a
check for button state on packets that contain motion by comparing the incoming
buttons value against the current self._button_state and raising
CaptureMismatchError if they differ. Locate the motion branch (where rel_x,
rel_y, wheel, pan are not all zero) and insert a validation using
self._button_state (and use self._apply_button_step / self.expected_button_steps
and self.button_index only for determining expected future transitions),
ensuring only the zero-motion path advances self.button_index while motion
packets still assert buttons == self._button_state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/host-relay-loopback.md`:
- Around line 158-163: Update the docs/host-relay-loopback.md section that
instructs running the mouse_buttons_intrusive scenario to add a Windows-specific
caveat: note that src/bluetooth_2_usb/test_harness_capture_windows.py rejects
scenarios containing BTN_FORWARD, BTN_BACK and BTN_TASK with EXIT_PREREQUISITE
so the mouse_buttons_intrusive path will immediately fail on Windows backends
that do not surface all eight mouse-button bits; recommend running the scenario
only on non-Windows/backends that support all eight bits or provide an
alternative scenario for Windows.

In `@src/bluetooth_2_usb/test_harness_capture.py`:
- Around line 195-201: The current __post_init__ groups expected_rel_steps into
_pending_rel_remaining_by_code, losing cross-axis ordering; change it to
preserve sequence (e.g., store an ordered list like _pending_rel_remaining:
list[tuple[int,int]] populated from expected_rel_steps in __post_init__) and
update the REL-event matcher logic to consume from that ordered list
front-to-back, only allowing relaxed matching across axes within the same
multi-axis report (i.e., when a single input report contains multiple REL_*
events, permit matching those out-of-order among themselves but never allow a
REL from a later expected position in the global sequence to match before an
earlier one). Ensure all places that referenced _pending_rel_remaining_by_code
are updated to use the ordered structure and that the same change is applied to
the analogous logic noted in the second block (the matcher that builds/consumes
pending relative steps).

---

Outside diff comments:
In `@src/bluetooth_2_usb/test_harness_capture.py`:
- Around line 233-254: The code only validates button bits when rel_x, rel_y,
wheel and pan are all zero; add a check for button state on packets that contain
motion by comparing the incoming buttons value against the current
self._button_state and raising CaptureMismatchError if they differ. Locate the
motion branch (where rel_x, rel_y, wheel, pan are not all zero) and insert a
validation using self._button_state (and use self._apply_button_step /
self.expected_button_steps and self.button_index only for determining expected
future transitions), ensuring only the zero-motion path advances
self.button_index while motion packets still assert buttons ==
self._button_state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d0951eb1-6c23-4de5-9f41-c994b0d1bf7a

📥 Commits

Reviewing files that changed from the base of the PR and between 98aea26 and f23258c.

📒 Files selected for processing (13)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/args.py
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/gadget_config.py
  • src/bluetooth_2_usb/hid_layout.py
  • src/bluetooth_2_usb/relay.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • src/bluetooth_2_usb/test_harness_common.py
  • src/bluetooth_2_usb/test_harness_inject.py
  • tests/test_relay.py
  • tests/test_test_harness.py

Comment thread docs/host-relay-loopback.md
Comment thread src/bluetooth_2_usb/evdev.py Outdated
Comment thread src/bluetooth_2_usb/test_harness_capture.py Outdated
@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bluetooth_2_usb/test_harness_capture.py (1)

398-413: ⚠️ Potential issue | 🟡 Minor

Accept the unnumbered 5-byte pan layout too.

The new pan-aware branch only recognizes the compact format when it arrives as 0x02 + payload. A valid unnumbered buttons,x,y,wheel,pan report still falls through to None, so the capture harness will reject that layout as an unexpected mouse report.

🩹 Proposed fix
     if len(report) == 8 and report[0] == 0x02:
         payload = report[1:]
     elif len(report) == 7:
         payload = report
     elif len(report) == 6 and report[0] == 0x02:
         payload = report[1:]
     elif len(report) == 5 and report[0] == 0x02:
         payload = report[1:]
+    elif len(report) == 5:
+        payload = report
     elif len(report) == 4 and report[0] == 0x02:
         payload = report[1:]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/test_harness_capture.py` around lines 398 - 413, The code
path that extracts payload from incoming 'report' only accepts the 5-byte pan
layout when it is prefixed with 0x02; update the branch that sets 'payload' (the
if/elif chain handling len(report) == 8/7/6/5/4/3) to also accept an unnumbered
5-byte report by adding an elif len(report) == 5: payload = report case (or
merge it with the existing 5-byte check) so the unnumbered buttons,x,y,wheel,pan
layout is treated as valid and not returned as None.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/bluetooth_2_usb/test_harness_capture.py`:
- Around line 398-413: The code path that extracts payload from incoming
'report' only accepts the 5-byte pan layout when it is prefixed with 0x02;
update the branch that sets 'payload' (the if/elif chain handling len(report) ==
8/7/6/5/4/3) to also accept an unnumbered 5-byte report by adding an elif
len(report) == 5: payload = report case (or merge it with the existing 5-byte
check) so the unnumbered buttons,x,y,wheel,pan layout is treated as valid and
not returned as None.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c69b0dce-80a3-46dd-900e-9eb9e02a0b97

📥 Commits

Reviewing files that changed from the base of the PR and between f23258c and 97c8ff8.

📒 Files selected for processing (6)
  • docs/host-relay-loopback.md
  • src/bluetooth_2_usb/evdev.py
  • src/bluetooth_2_usb/extended_mouse.py
  • src/bluetooth_2_usb/test_harness_capture.py
  • tests/test_relay.py
  • tests/test_test_harness.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/host-relay-loopback.md

@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bluetooth_2_usb/test_harness_capture.py (1)

217-256: ⚠️ Potential issue | 🟠 Major

Validate mouse button state on motion reports too.

The new zero-motion gate means buttons is only checked for button-only packets. Any movement packet with a stale or missing button byte now slips through, even though the HID report always carries current button state and the Windows helper already emits that state on motion packets. This can hide relay bugs where a press/release is coalesced with movement or the button bit stays latched after release.

🔧 Suggested direction
         if rel_events:
             self._apply_rel_report(rel_events)

-        if rel_x == 0 and rel_y == 0 and wheel == 0 and pan == 0:
-            if self.button_index >= len(self.expected_button_steps):
-                if buttons == 0:
-                    return
-                raise CaptureMismatchError(
-                    f"Unexpected extra mouse button report {report.hex(sep=' ')}"
-                )
-
-            if not self.rel_complete:
-                if buttons != 0:
-                    raise CaptureMismatchError(
-                        "Mouse button report arrived before movement"
-                    )
-                return
-
-            expected = self.expected_button_steps[self.button_index]
-            expected_buttons = self._apply_button_step(expected)
-            if buttons != expected_buttons:
-                raise CaptureMismatchError(
-                    f"Unexpected mouse button report {report.hex(sep=' ')}; expected {expected.describe()}"
-                )
-            self.button_index += 1
+        if buttons != self._button_state:
+            if self.button_index >= len(self.expected_button_steps):
+                raise CaptureMismatchError(
+                    f"Unexpected mouse button bits in report {report.hex(sep=' ')}"
+                )
+            if not self.rel_complete:
+                raise CaptureMismatchError("Mouse button report arrived before movement")
+
+            expected = self.expected_button_steps[self.button_index]
+            expected_buttons = self._apply_button_step(expected)
+            if buttons != expected_buttons:
+                raise CaptureMismatchError(
+                    f"Unexpected mouse button report {report.hex(sep=' ')}; expected {expected.describe()}"
+                )
+            self.button_index += 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bluetooth_2_usb/test_harness_capture.py` around lines 217 - 256, Validate
the mouse button bits for every parsed report (buttons, rel_x, rel_y, wheel,
pan), not only for zero-motion reports: after parsing the report but before
returning/applying rel events, check if self.button_index >=
len(self.expected_button_steps) and ensure buttons == 0 (raise the existing
"Unexpected extra mouse button report" error if not); otherwise if not
self.rel_complete enforce that buttons == 0 (raise "Mouse button report arrived
before movement" if violated); when a button step is expected compute
expected_buttons via expected = self.expected_button_steps[self.button_index]
and expected_buttons = self._apply_button_step(expected) and compare buttons to
expected_buttons (raise the existing "Unexpected mouse button report ...;
expected ..." error if they differ) — preserving the existing error messages and
increments of self.button_index when matched, but perform these checks for both
motion and zero-motion packets (i.e., move the button-validation logic out of
the zero-motion-only branch so it runs before/alongside the
rel_events/_apply_rel_report handling).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/bluetooth_2_usb/test_harness_capture.py`:
- Around line 217-256: Validate the mouse button bits for every parsed report
(buttons, rel_x, rel_y, wheel, pan), not only for zero-motion reports: after
parsing the report but before returning/applying rel events, check if
self.button_index >= len(self.expected_button_steps) and ensure buttons == 0
(raise the existing "Unexpected extra mouse button report" error if not);
otherwise if not self.rel_complete enforce that buttons == 0 (raise "Mouse
button report arrived before movement" if violated); when a button step is
expected compute expected_buttons via expected =
self.expected_button_steps[self.button_index] and expected_buttons =
self._apply_button_step(expected) and compare buttons to expected_buttons (raise
the existing "Unexpected mouse button report ...; expected ..." error if they
differ) — preserving the existing error messages and increments of
self.button_index when matched, but perform these checks for both motion and
zero-motion packets (i.e., move the button-validation logic out of the
zero-motion-only branch so it runs before/alongside the
rel_events/_apply_rel_report handling).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2458531a-7cbe-42a6-bc36-a2494c24836b

📥 Commits

Reviewing files that changed from the base of the PR and between 97c8ff8 and f404b35.

📒 Files selected for processing (3)
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • tests/test_test_harness.py
✅ Files skipped from review due to trivial changes (1)
  • src/bluetooth_2_usb/test_harness_capture_windows.py

@quaxalber
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bluetooth_2_usb/test_harness_capture_windows.py`:
- Around line 524-526: The helper _mouse_i16_bytes clamps input incorrectly at
-32767, losing the valid int16 lower bound; update the clamping to allow the
full signed 16-bit range by using -32768 as the minimum (e.g., clamp to between
-32768 and 32767) and then call to_bytes(2, "little", signed=True) as before so
-32768 is preserved in the little-endian int16 encoding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3411cc61-1c37-428c-8ff8-a526d3958439

📥 Commits

Reviewing files that changed from the base of the PR and between 97c8ff8 and eac464c.

📒 Files selected for processing (3)
  • src/bluetooth_2_usb/test_harness_capture.py
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • tests/test_test_harness.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/bluetooth_2_usb/test_harness_capture.py
  • tests/test_test_harness.py

Comment thread src/bluetooth_2_usb/test_harness_capture_windows.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/test_test_harness.py (2)

127-132: Make the unknown-scenario assertion less order-sensitive.

This hard-coded list will fail on the next scenario addition or a harmless registration reorder, even if get_scenario() still reports the right error. Build the expected names from SCENARIOS or only assert the stable prefix / missing scenario name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_test_harness.py` around lines 127 - 132, The test
test_invalid_scenario_name_is_reported_cleanly currently asserts a hard-coded
list of scenario names making it brittle; change it to build the expected
portion of the error dynamically from the registered SCENARIOS (or assert only
the stable parts). Specifically, update the assertion for get_scenario("nope")
to either (a) construct the "Expected one of: ..." substring by joining the
keys/names from SCENARIOS (e.g., sorted(SCENARIOS.keys())) so the message
reflects current registrations, or (b) only assert the stable prefix "Unknown
scenario 'nope'." and that the error mentions "Expected one of" without
enumerating names; locate this in the
test_invalid_scenario_name_is_reported_cleanly test and adjust the
assertRaisesRegex accordingly.

20-36: Use module-qualified calls for functions when the module reloads during tests.

After importlib.reload(test_harness_capture_windows) in the test's finally block, the imported aliases _mouse_event_to_reports and _reset_mouse_button_state remain bound to the pre-reload function objects. Subsequent test methods will exercise stale code, potentially masking bugs if behavior changes on reload. Replace direct calls with module-qualified calls like test_harness_capture_windows._mouse_event_to_reports().

Also applies to: 856-867

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_test_harness.py` around lines 20 - 36, The tests import private
functions like _mouse_event_to_reports and _reset_mouse_button_state by name
which become stale after importlib.reload(test_harness_capture_windows); change
the tests to call the functions via the module object (e.g.
test_harness_capture_windows._mouse_event_to_reports(...) and
test_harness_capture_windows._reset_mouse_button_state(...)) instead of the
imported aliases, and apply the same module-qualified replacement for other
private symbols used (e.g. _stable_device_identity, _extract_device_identities,
_device_matches_candidate, _keyboard_event_to_report) including the other
occurrences noted (lines ~856-867).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_test_harness.py`:
- Around line 127-132: The test test_invalid_scenario_name_is_reported_cleanly
currently asserts a hard-coded list of scenario names making it brittle; change
it to build the expected portion of the error dynamically from the registered
SCENARIOS (or assert only the stable parts). Specifically, update the assertion
for get_scenario("nope") to either (a) construct the "Expected one of: ..."
substring by joining the keys/names from SCENARIOS (e.g.,
sorted(SCENARIOS.keys())) so the message reflects current registrations, or (b)
only assert the stable prefix "Unknown scenario 'nope'." and that the error
mentions "Expected one of" without enumerating names; locate this in the
test_invalid_scenario_name_is_reported_cleanly test and adjust the
assertRaisesRegex accordingly.
- Around line 20-36: The tests import private functions like
_mouse_event_to_reports and _reset_mouse_button_state by name which become stale
after importlib.reload(test_harness_capture_windows); change the tests to call
the functions via the module object (e.g.
test_harness_capture_windows._mouse_event_to_reports(...) and
test_harness_capture_windows._reset_mouse_button_state(...)) instead of the
imported aliases, and apply the same module-qualified replacement for other
private symbols used (e.g. _stable_device_identity, _extract_device_identities,
_device_matches_candidate, _keyboard_event_to_report) including the other
occurrences noted (lines ~856-867).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dd20ffc3-9594-4bee-85bb-57392bc4d4ef

📥 Commits

Reviewing files that changed from the base of the PR and between eac464c and 93a670d.

📒 Files selected for processing (2)
  • src/bluetooth_2_usb/test_harness_capture_windows.py
  • tests/test_test_harness.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/bluetooth_2_usb/test_harness_capture_windows.py

@quaxalber quaxalber marked this pull request as ready for review April 26, 2026 18:52
@quaxalber quaxalber changed the title Add extended mouse forwarding feat: add extended mouse forwarding Apr 26, 2026
@quaxalber quaxalber merged commit c4525d4 into staging Apr 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants