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, mouse, and consumer-control 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
Start every physical validation session with node-discovery. It identifies
the active keyboard, mouse, and consumer HID nodes with the smallest practical
host-side signal before running broader scenarios.
For regular full-path validation after node discovery, run combo. It
exercises the keyboard, mouse, and consumer-control paths in one pass. Use
keyboard, mouse, or consumer only when you need to isolate a specific
domain. The keyboard scenario includes the text burst plus extended function,
navigation, system, and application keys. The consumer scenario includes volume,
media transport, menu/application, and browser/navigation controls.
Warning
Loopback scenarios inject real host-visible HID reports. Even
node-discovery can leak keyboard, mouse, or consumer-control input to the
host UI. Run these scenarios only in a test session where unexpected key,
pointer, scroll, media, navigation, or application-launch effects are
acceptable.
The node-discovery scenario is intentionally tiny: it emits one F13
press/release pair, two mouse relative events (REL_X=1, REL_X=-1), and the
consumer volume up/down press/release pair. It is useful when duplicate gadget
instances are visible and you need to find which keyboard, mouse, and consumer
nodes are live before running combo.
- the Pi is connected to the host through the OTG-capable data path
bluetooth_2_usb.serviceis active on the PiB2U_AUTO=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 ./venv/bin/bluetooth_2_usb udev install --repo-root "$PWD"Recommended baseline checks on the Pi:
sudo bluetooth_2_usb smoketest --verbose
sudo bluetooth_2_usb debug --duration 10| Argument | Meaning |
|---|---|
--scenario SCENARIO |
Deterministic scenario to inject. Default: combo. |
--pre-delay-ms MS |
Delay after virtual device creation. Default: 1000. |
--event-gap-ms MS |
Delay between emitted events. Default: scenario-specific. |
--post-delay-ms MS |
Delay after injection before closing virtual devices. Default: scenario-specific. |
--keyboard-name NAME |
Virtual keyboard device name. |
--mouse-name NAME |
Virtual mouse device name. |
--consumer-name NAME |
Virtual consumer-control device name. |
--output {text,json} |
Choose the output format. Default: text. |
| Argument | Meaning |
|---|---|
--scenario SCENARIO |
Expected scenario to observe. Default: combo. |
--timeout-sec SECONDS |
Timeout waiting for relay events. Default: scenario-specific. |
--devices DEVICES |
Comma-separated host-side gadget device filters. Required. |
--output {text,json} |
Choose the output format. Default: text. |
On Linux:
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario node-discovery --output jsonExperimental: macOS
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario node-discovery --output jsonNote
Experimental - unvalidated on real macOS hosts. The macOS variant uses the same capture command, but it has not yet been validated on real macOS hardware.
On Windows:
$env:PYTHONPATH = "$PWD\src"
python -m bluetooth_2_usb loopback capture --devices '<device filter>' --scenario node-discovery --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. Use a Python environment where
python -c "import hid" succeeds.
Note
Loopback JSON is intentionally summarized. Normal injector output reports the scenario name, timing, device names, and expected event counts instead of the full event sequence. Capture timeouts report the candidates, any nodes that completed before the timeout, per-role progress counters, and the next expected event. They do not dump the full remaining sequence.
With the repository virtual environment on Windows:
.\venv\Scripts\python.exe -m bluetooth_2_usb loopback capture --devices '<device filter>' --scenario node-discovery --output jsonFrom the repository checkout on the host:
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario comboCapture behavior:
- requires
--devicesto select the gadget HID device by path,uniq,phys, Bluetooth MAC-shapeduniq, or case-insensitive product-name fragment - waits up to the scenario-specific timeout for the complete sequence (
20seconds by default;node-discoveryuses10seconds andcombouses60seconds) - 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 loopback lock file; do not run multiple inject/capture sessions in parallel against the same host/Pi pair
When multiple Bluetooth-2-USB gadgets are attached, select the target by its
host-visible USB serial exposed as uniq:
venv/bin/bluetooth_2_usb loopback capture \
--scenario combo \
--devices '<usb serial>'If discovery is ambiguous, pass multiple comma-separated filters:
venv/bin/bluetooth_2_usb loopback capture \
--scenario combo \
--devices '<keyboard path>,<mouse path>,<consumer path>'Keep this command running while you trigger the Pi-side injection.
When duplicate gadget instances are visible, first identify the active nodes with the minimal discovery scenario:
venv/bin/bluetooth_2_usb loopback capture \
--scenario node-discovery \
--devices '<keyboard path>,<mouse path>,<consumer path>'Then, on the Pi:
sudo bluetooth_2_usb loopback inject --scenario node-discoveryUse the node set that succeeds here when pinning the full combo capture.
Important
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 bluetooth_2_usb loopback inject --scenario comboThe injector uses a 25 ms default event gap for every scenario except
mouse, which stays at 0 ms to keep its fast-motion stress behavior. Override
the default with --event-gap-ms only when intentionally stress-testing relay
throughput.
Note
When bluetooth_2_usb.service is active, the injector waits up to the default
service-settle window before emitting events. This avoids racing a freshly
re-enumerated USB HID gadget before the host has started draining reports. Set
B2U_LOOPBACK_SERVICE_SETTLE_SEC=0 to disable that loopback-only wait.
Invalid values are ignored and the default settle window is used.
The injector creates temporary virtual devices named:
B2U Test KeyboardB2U Test MouseB2U Test Consumer
and emits this deterministic sequence:
- keyboard: an alternating-case burst with modifier transitions
- mouse: large relative X/Y movement, vertical wheel deltas, horizontal pan deltas, then all configured mouse button bits press/release
- node-discovery: F13 press/release,
REL_X=1,REL_X=-1, and consumer volume up/down press/release - consumer: volume, media transport, menu/application, and browser/navigation controls
For mouse wheel and horizontal wheel steps, the injector emits paired low-res
and high-res evdev events in the same SYN_REPORT frame. The host capture
expects the relay to emit one equivalent USB HID wheel or pan step.
The mouse gadget report uses one button byte, signed 16-bit relative X/Y, and signed 8-bit vertical wheel and horizontal pan.
On Windows, the current Raw Input capture backend only maps mouse button bits
through BTN_EXTRA. Windows can still run every public scenario, but mouse
button validation is partial for mouse and combo; skipped buttons are
reported as windows_skipped_mouse_buttons.
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.
Injector JSON includes a compact expectation summary:
{
"expected": {
"name": "combo",
"keyboard_steps": 250,
"mouse_rel_steps": 8,
"mouse_button_steps": 16,
"consumer_steps": 26,
"total_steps": 300
}
}Timeout JSON focuses on the useful debugging state:
{
"message": "Timed out waiting for combo reports after 30.0s",
"details": {
"summary": {
"keyboard": "250/250 complete",
"mouse": "2/8 rel, 0/16 buttons",
"consumer": "0/26"
},
"progress": {
"mouse": [
{
"node": "/dev/hidraw3",
"rel_steps_seen": 2,
"rel_steps_expected": 8,
"button_steps_seen": 0,
"button_steps_expected": 16,
"next_expected_rel": "REL_X=-210000"
}
]
}
}
}Windows Raw Input capture also includes aggregate message counts and a small sample of observed events for device-matching issues; it intentionally avoids a complete Raw Input event stream.
Keyboard-only:
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario keyboard
sudo bluetooth_2_usb loopback inject --scenario keyboardMouse-only:
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario mouse
sudo bluetooth_2_usb loopback inject --scenario mouseConsumer-control only:
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario consumer
sudo bluetooth_2_usb loopback inject --scenario consumerMinimal node discovery:
venv/bin/bluetooth_2_usb loopback capture --devices '<device filter>' --scenario node-discovery
sudo bluetooth_2_usb loopback inject --scenario node-discovery- 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 ./venv/bin/bluetooth_2_usb udev install --repo-root "$PWD"- the relay service on the Pi may not be active
- auto relay 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 bluetooth_2_usb --list --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 is intentionally forceful enough to validate chunked mouse motion, scrolling, modifier transitions, and all configured mouse button bits, so the capture should be treated as a dedicated verification session rather than as a transparent observer.
The loopback validator 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_loopback.lock - host Linux/macOS:
/tmp/bluetooth_2_usb_loopback.lock - Pi:
/tmp/bluetooth_2_usb_loopback.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