Use this guide when you need to prove that the relay path works end to end without depending on a paired Bluetooth keyboard or mouse.
The flow is:
- prepare a host Python environment with
hidapi - start a host-side capture against the gadget HID device
- inject deterministic virtual keyboard and mouse events on the Pi
- verify that the capture observes the expected relayed sequence
This validates the path:
Pi virtual input device -> bluetooth_2_usb relay -> USB HID gadget -> host HID device
- the Pi is connected to the host through the OTG-capable data path
bluetooth_2_usb.serviceis active on the PiB2U_AUTO_DISCOVER=trueis enabled in/etc/default/bluetooth_2_usb/dev/uinputexists on the Pi- the host Python environment has
hidapiinstalled for gadget discovery
Additional Linux preconditions:
- install the host-side USB udev rule
- the host user running the capture is in the
inputgroup
Prepare the host Python environment once:
python3 -m pip install -r requirements-host-capture.txtOn Linux, install the udev rule once:
sudo ./scripts/install-hid-udev-rule.shRecommended baseline checks on the Pi:
sudo /opt/bluetooth_2_usb/scripts/smoketest.sh --verbose
sudo /opt/bluetooth_2_usb/scripts/debug.sh --duration 5On Linux:
./scripts/loopback-capture.sh --scenario keyboard --timeout-sec 1 --output jsonExperimental: macOS
./scripts/loopback-capture.sh --scenario keyboard --timeout-sec 1 --output jsonNote
Experimental - unvalidated on real macOS hosts. The macOS variant uses the same shell wrapper, but it has not yet been validated on real macOS hardware.
On Windows:
powershell -ExecutionPolicy Bypass -File .\scripts\loopback-capture.ps1 --scenario keyboard --timeout-sec 1 --output jsonIf the Pi gadget is visible, the output will include candidate keyboard, mouse,
or consumer HID device paths even if the short timeout expires. On Windows,
strict capture of the actual relay sequence uses Raw Input; hidapi remains a
discovery step, not the primary event backend.
From the repository checkout on the host:
./scripts/loopback-capture.sh --scenario comboDefault behavior:
- detects the gadget HID device by product name and HID usage
- waits up to
5seconds for the complete sequence - may temporarily claim the gadget HID interfaces while the capture runs, so do not assume the local desktop will process the same inputs during that window
- uses a single harness lock file; do not run multiple inject/capture sessions in parallel against the same host/Pi pair
If automatic detection is ambiguous, pin the nodes explicitly:
./scripts/loopback-capture.sh \
--scenario combo \
--keyboard-node '<candidate keyboard path>' \
--mouse-node '<candidate mouse path>'Keep this command running while you trigger the Pi-side injection.
Before each fresh Windows validation run after changing the gadget descriptor layout or USB identity:
- set the Pi to the intended software revision
- reboot the Pi
- perform a Windows PnP admin reset
- only then start the host capture
On the Pi:
sudo /opt/bluetooth_2_usb/scripts/loopback-inject.sh --scenario comboThe injector creates temporary virtual devices named:
B2U Test KeyboardB2U Test Mouse
and emits this deterministic sequence:
- keyboard:
KEY_F13,KEY_F14,KEY_F15down/up - mouse:
REL_X +1,REL_X -1,REL_Y +1,REL_Y -1,REL_WHEEL +1,REL_WHEEL -1,REL_HWHEEL +1,REL_HWHEEL -1, one coalescedREL_X +2/REL_Y -3/REL_HWHEEL +1frame, then all eight mouse button bits press/release
The mouse gadget report uses one button byte, signed 16-bit relative X/Y, and signed 8-bit vertical wheel and horizontal pan.
The host capture exits 0 and reports that it observed the expected relay
reports on the host gadget HID device.
The Pi-side injector exits 0 and reports that it injected the expected test
sequence through /dev/uinput.
Keyboard-only:
./scripts/loopback-capture.sh --scenario keyboard
sudo /opt/bluetooth_2_usb/scripts/loopback-inject.sh --scenario keyboardMouse-only:
./scripts/loopback-capture.sh --scenario mouse
sudo /opt/bluetooth_2_usb/scripts/loopback-inject.sh --scenario mouseConsumer-control only:
./scripts/loopback-capture.sh --scenario consumer
sudo /opt/bluetooth_2_usb/scripts/loopback-inject.sh --scenario consumer- the Pi gadget may not be enumerated on the host
- the OTG cable or port may be wrong
- the host may not expose the gadget HID device yet
- the host Python may not have
hidapiinstalled for discovery
On Linux, also confirm that the udev rule was installed and the Pi was reconnected afterwards.
On Linux this usually means hidapi can enumerate the USB gadget but lacks the
required write access to the underlying USB device node.
Check:
id
ls -l /dev/bus/usb/*/*If needed:
sudo ./scripts/install-hid-udev-rule.sh- the relay service on the Pi may not be active
- auto-discovery may be off
- the Pi may not have picked up the temporary virtual devices
- the host gadget HID device may be present but not currently carrying reports
- on Windows, candidate enumeration may be fine while Raw Input still sees the wrong device instance after a stale PnP state; re-run the PnP admin reset
Check on the Pi:
systemctl is-active bluetooth_2_usb.service
sudo /opt/bluetooth_2_usb/venv/bin/python -m bluetooth_2_usb --list_devices --output json
sudo journalctl -u bluetooth_2_usb.service -n 100 --no-pagerThe kernel/device access prerequisite for virtual test devices is missing.
Check:
ls -l /dev/uinputThat can happen. Opening the gadget HID interfaces for capture may temporarily claim them while the test is running, which can reduce or suppress normal local handling of the same keyboard, mouse, or consumer inputs.
The loopback sequence still uses non-text keyboard keys and tiny mouse-rel movements so the test remains low-impact if the local desktop does process the events, but the capture should be treated as a dedicated verification session rather than as a transparent observer.
The harness uses a single lock file and will reject parallel runs. If no other run is active, clear the stale lock file and retry.
Lock paths:
- host Windows:
%TEMP%\bluetooth_2_usb_test_harness.lock - host Linux/macOS:
/tmp/bluetooth_2_usb_test_harness.lock - Pi:
/tmp/bluetooth_2_usb_test_harness.lock
This exact loopback test is hardware-only and is not expected to run inside GitHub Actions.
CI should instead cover:
- scenario definitions
- node autodetection and deduplication
- event matching logic
- CLI argument parsing
- exit-code behavior