- 1. Introduction
- 2. Features
- 3. Requirements
- 4. Installation
- 5. Usage
- 6. Troubleshooting
- 7. Bonus points
- 8. Contributing
- 9. License
- 10. Acknowledgments
Convert a Raspberry Pi into a HID proxy that relays Bluetooth keyboard and mouse input to USB. Minimal configuration. Zero hassle.
The issue with Bluetooth devices is that you usually can't use them to wake up sleeping devices, access the BIOS or OS select menu (GRUB). Some devices don't even have a (working) Bluetooth interface.
Sounds familiar? Congratulations! You just found the solution!
- Simple installation and highly automated setup
- Supports multiple input devices (currently keyboard and mouse - more than one of each kind simultaneously)
- Supports 146 multimedia keys (e.g. mute, volume up/down, launch browser, etc.)
- Auto-reconnect feature for input devices (power off, energy saving mode, out of range, etc.)
- Robust error handling and logging
- Installation as a systemd service
- Reliable concurrency using state-of-the-art TaskGroups
- Clean and actively maintained code base
- (Single-board) computer with Bluetooth support, e.g. Raspberry Pi 4B or Raspberry Pi Zero W
- Linux OS with systemd support, e.g. Raspberry Pi OS (recommended)
- Python 3.11 for using TaskGroups
Follow these steps to install and configure the project:
-
Install an OS on your Raspberry Pi (e.g. using Pi Imager)
-
Connect to a network via Ethernet cable or Wi-Fi. Make sure this network has Internet access.
-
(optional) Enable SSH, if you intend to access the Pi remotely.
Note
These settings above may be configured during imaging, on first boot or afterwards.
-
Connect to the Pi and make sure
git
andpython3.11
are installed:sudo apt update && sudo apt upgrade -y && sudo apt install -y git python3.11
-
Pair and trust any Bluetooth devices you wish to relay, either via GUI or via CLI:
bluetoothctl scan on
... wait for your devices to show up and note their MAC addresses (you may also type the first characters and hit
TAB
for auto-completion in the following commands) ...pair A1:B2:C3:D4:E5:F6 trust A1:B2:C3:D4:E5:F6
[!NOTE] Replace
A1:B2:C3:D4:E5:F6
by your input device's Bluetooth MAC address
-
On the Pi, clone the repository:
git clone https://github.com/quaxalber/bluetooth_2_usb.git
-
Navigate to the project folder:
cd bluetooth_2_usb
-
Init the submodules:
git submodule update --init --recursive
-
Check which Linux input devices your Bluetooth devices are mapped to:
python3.11 bluetooth_2_usb.py -l
... and note the device paths of the devices you want to use:
user@raspberrypi:~/bluetooth_2_usb $ python3.11 bluetooth_2_usb.py -l AceRK Mouse 0a:1b:2c:3d:4e:5f /dev/input/event3 <--- AceRK Keyboard 0a:1b:2c:3d:4e:5f /dev/input/event2 <--- vc4-hdmi-1 vc4-hdmi-1/input0 /dev/input/event1 vc4-hdmi-0 vc4-hdmi-0/input0 /dev/input/event0
-
Specify the correct input devices in
bluetooth_2_usb.service
:nano bluetooth_2_usb.service
... and change
event3
andevent2
according to step 9.[!NOTE]
Ctrl + X
>Y
>Enter
to save and exit nano -
(optional) If you wish to test first, without actually sending anything to the target devices, append
-s
to theExecStart=
command to enable sandbox mode. To increase log verbosity add-d
. -
Run the installation script as root:
sudo bash install.sh
-
Restart the Pi (prompt at the end of
install.sh
) -
Verify that the service is running:
service bluetooth_2_usb status
It should look something like this:
user@raspberrypi:~/bluetooth_2_usb $ service bluetooth_2_usb status ● bluetooth_2_usb.service - Bluetooth to USB HID proxy Loaded: loaded (/home/user/bluetooth_2_usb/bluetooth_2_usb.service; enabled; vendor preset: enabled) Active: active (running) since Wed 2023-10-11 18:00:58 BST; 11s ago Main PID: 4256 (python3.11) Tasks: 1 (limit: 8755) CPU: 328ms CGroup: /system.slice/bluetooth_2_usb.service └─4256 python3.11 /usr/bin/bluetooth_2_usb.py -k /dev/input/event2 -m /dev/input/event3 Oct 11 18:00:58 raspberrypi systemd[1]: Started Bluetooth to USB HID proxy. Oct 11 18:00:58 raspberrypi python3.11[4256]: 23-10-11 18:00:58 [INFO] Launching Bluetooth 2 USB v0.4.4 Oct 11 18:01:01 raspberrypi python3.11[4256]: 23-10-11 18:01:01 [INFO] Starting event loop for [device /dev/input/event2, name "AceRK Keyboard", phys "0a:1b:2c:3d:4e:5f"] >> [Keyboard gadget (/dev/hidg1) + Consumer control gadget (/dev/hidg2)] Oct 11 18:01:01 raspberrypi python3.11[4256]: 23-10-11 18:01:01 [INFO] Starting event loop for [device /dev/input/event3, name "AceRK Mouse", phys "0a:1b:2c:3d:4e:5f"] >> [Boot mouse gadget (/dev/hidg0)]
Note
Something seems off? Try yourself in Troubleshooting!
Connect the power USB port of your Pi (Micro-USB or USB-C) via cable with a USB port on your target device. You should hear the USB connection sound (depending on the target device) and be able to access your target device wirelessly using your Bluetooth keyboard or mouse.
Important
It's essential to use the small power port instead of the bigger USB-A ports, since only the power port has the OTG feature required for USB gadgets.
Currently you can provide the following CLI arguments:
user@raspberrypi:~/bluetooth_2_usb $ python3.11 bluetooth_2_usb.py -h
usage: bluetooth_2_usb.py [-h] [--keyboards KEYBOARDS] [--mice MICE] [--sandbox] [--debug] [--log_to_file] [--log_path LOG_PATH] [--version]
Bluetooth to USB HID proxy. Reads incoming mouse and keyboard events (e.g., Bluetooth) and forwards them to USB using Linux's gadget mode.
options:
-h, --help show this help message and exit
--keyboards KEYBOARDS, -k KEYBOARDS
Comma-separated list of input device paths for keyboards to be registered and connected. Default is None. Example: --keyboards /dev/input/event2,/dev/input/event4
--mice MICE, -m MICE Comma-separated list of input device paths for mice to be registered and connected. Default is None. Example: --mice /dev/input/event3,/dev/input/event5
--sandbox, -s Only read input events but do not forward them to the output devices.
--debug, -d Enable debug mode. Increases log verbosity
--log_to_file, -f Add a handler that logs to file additionally to stdout.
--log_path LOG_PATH, -p LOG_PATH
The path of the log file. Default is /var/log/bluetooth_2_usb/bluetooth_2_usb.log.
--version, -v Display the version number of this software.
--list_devices, -l List all available input devices and exit.
The API is designed such that it may be consumed both via CLI and from within external Python code. More details on this coming soon!
This is likely due to the limited power the Pi gets from the host's USB port. Try these steps:
- If available, connect your Pi to a USB 3 port on the host / target device (usually blue).
Important
Do not use the blue (or black) USB-A ports of your Pi to connect. This won't work.
Do use the small USB power port.
-
Try to connect to the Pi via SSH instead of attaching a display directly and remove any unnecessary peripherals.
-
Install a lite version of your OS on the Pi (without GUI)
-
Get a USB-C Data/Power Splitter (or Micro-USB respectively) and draw power from a sufficiently powerful power adaptor. This will ultimately resolve any power-related issues, and your Pi will no longer be dependent on the host's power supply.
Note
The Pi 4B requires 3A/15W for stable operation!
This could be due to a number of reasons. Try these steps:
-
Verify that the service is running:
service bluetooth_2_usb status
-
Verify that you specified the correct input devices in
bluetooth_2_usb.service
and that sandbox mode is off (that is no--sandbox
or-s
flag) -
Verify that your Bluetooth devices are paired, trusted, connected and not blocked:
bluetoothctl info A1:B2:C3:D4:E5:F6
It should look like this:
user@raspberrypi:~/bluetooth_2_usb $ bluetoothctl Agent registered [CHG] Controller 0A:1B:2C:3D:4E:5F Pairable: yes [AceRK]# info A1:B2:C3:D4:E5:F6 Device A1:B2:C3:D4:E5:F6 (random) Name: AceRK Alias: AceRK Paired: yes <--- Trusted: yes <--- Blocked: no <--- Connected: yes <--- WakeAllowed: no LegacyPairing: no UUID: Generic Access Profile (00001800-0000-1000-8000-00805f9b34fb) UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb) UUID: Device Information (0000180a-0000-1000-8000-00805f9b34fb) UUID: Human Interface Device (00001812-0000-1000-8000-00805f9b34fb) UUID: Nordic UART Service (6e400001-b5a3-f393-e0a9-e50e24dcca9e)
Note
Replace A1:B2:C3:D4:E5:F6
by your input device's Bluetooth MAC address
-
Reload and restart service:
sudo systemctl daemon-reload sudo service bluetooth_2_usb restart
-
Reboot Pi
sudo reboot
-
Re-connect the Pi to the host and check that the cable is in good shape
-
Try a different USB port on the host
-
Try connecting to a different host
This is a common issue, especially when the device gets paired with multiple hosts. One simple fix/workaround is to re-pair the device:
bluetoothctl
power off
power on
block A1:B2:C3:D4:E5:F6
remove A1:B2:C3:D4:E5:F6
scan on
pair A1:B2:C3:D4:E5:F6
trust A1:B2:C3:D4:E5:F6
If the issue persists, it's worth trying to delete the cache:
sudo -i
cd '/var/lib/bluetooth/0A:1B:2C:3D:4E:5F/cache'
rm -rf 'A1:B2:C3:D4:E5:F6'
exit
Note
Replace 0A:1B:2C:3D:4E:5F
by your Pi's Bluetooth controller's MAC and A1:B2:C3:D4:E5:F6
by your input device's MAC
Here's a few things you could try:
- Check the log files (default at
/var/log/bluetooth_2_usb/
) for errors
Note
Logging to file requires the -f
flag
-
You may also query the journal to inspect the service logs in real-time:
journalctl -u bluetooth_2_usb.service -n 20 -f
-
Increase log verbosity by appending
-d
to the command in the line starting withExecStart=
inbluetooth_2_usb.service
. -
Reload and restart service:
sudo systemctl daemon-reload sudo service bluetooth_2_usb restart
-
For easier degguging, you may also stop the service
sudo service bluetooth_2_usb stop
and run the script manually, modifying arguments as required, e.g.:
sudo python3.11 bluetooth_2_usb.py -k /dev/input/event2 -m /dev/input/event3 -d
-
When you interact with your Bluetooth devices with
-d
set, you should see debug output in the logs such as:user@raspberrypi:~/bluetooth_2_usb $ sudo python3.11 bluetooth_2_usb.py -k /dev/input/event2 -m /dev/input/event3 -d 23-10-20 09:50:27 [DEBUG] CLI args: Namespace(keyboards=['/dev/input/event2'], mice=['/dev/input/event3'], sandbox=False, debug=True, log_to_file=False, log_path='/var/log/bluetooth_2_usb/bluetooth_2_usb.log', version=False) 23-10-20 09:50:27 [DEBUG] Logging to stdout 23-10-20 09:50:27 [INFO] Launching Bluetooth 2 USB v0.4.4 23-10-20 09:50:27 [DEBUG] Available output devices: [Boot mouse gadget (/dev/hidg0), Keyboard gadget (/dev/hidg1), Consumer control gadget (/dev/hidg2)] 23-10-20 09:50:30 [DEBUG] Sandbox mode disabled. All output devices activated. 23-10-20 09:50:30 [DEBUG] Registered device link: [AceRK Keyboard]>>[/dev/hidg1+/dev/hidg2] 23-10-20 09:50:30 [DEBUG] Registered device link: [AceRK Mouse]>>[/dev/hidg0] 23-10-20 09:50:30 [DEBUG] Connected device link: [AceRK Keyboard]>>[/dev/hidg1+/dev/hidg2] 23-10-20 09:50:30 [DEBUG] Connected device link: [AceRK Mouse]>>[/dev/hidg0] 23-10-20 09:50:30 [DEBUG] Current tasks: {<Task pending name='[AceRK Mouse]>>[/dev/hidg0]' coro=<ComboDeviceHidProxy._async_relay_input_events() running at /home/user/bluetooth_2_usb/bluetooth_2_usb.py:217> cb=[TaskGroup._on_task_done()]>, <Task pending name='[AceRK Keyboard]>>[/dev/hidg1+/dev/hidg2]' coro=<ComboDeviceHidProxy._async_relay_input_events() running at /home/user/bluetooth_2_usb/bluetooth_2_usb.py:217> cb=[TaskGroup._on_task_done()]>, <Task pending name='Task-1' coro=<_main() running at /home/user/bluetooth_2_usb/bluetooth_2_usb.py:375> cb=[_run_until_complete_cb() at /usr/local/lib/python3.11/asyncio/base_events.py:180]>} 23-10-20 09:50:30 [INFO] Starting event loop for [device /dev/input/event2, name "AceRK Keyboard", phys "0a:1b:2c:3d:4e:5f"] >> [Keyboard gadget (/dev/hidg1) + Consumer control gadget (/dev/hidg2)] 23-10-20 09:50:30 [INFO] Starting event loop for [device /dev/input/event3, name "AceRK Mouse", phys "0a:1b:2c:3d:4e:5f"] >> [Boot mouse gadget (/dev/hidg0)] 23-10-20 09:50:42 [DEBUG] Received event: [event at 1697791842.883636, code 04, type 04, val 458756] 23-10-20 09:50:42 [DEBUG] Received event: [key event at 1697791842.883636, 30 (KEY_A), down] 23-10-20 09:50:42 [DEBUG] Converted evdev ecode 0x1E (KEY_A) to HID UsageID 0x04 (A) 23-10-20 09:50:42 [DEBUG] Received event: [synchronization event at 1697791842.883636, SYN_REPORT] 23-10-20 09:51:35 [DEBUG] Received event: [relative axis event at 1697791895.144049, REL_X] 23-10-20 09:51:35 [DEBUG] Moving mouse /dev/hidg0: (x, y, mwheel) = (-125, 0, 0) 23-10-20 09:51:35 [DEBUG] Received event: [synchronization event at 1697791895.144049, SYN_REPORT]
-
Still not resolved? Double-check the installation instructions
-
For more help, open an issue in the GitHub repository
Absolutely! Here's how.
After successfully setting up your Pi as a HID proxy for your Bluetooth devices, you may consider making Raspberry OS read-only. That helps preventing the SD card from wearing out and the file system from getting corrupted when powering off the Raspberry forcefully.
Contributions are welcome! Please read the CONTRIBUTING.md file for guidelines.
This project is licensed under the MIT License - see the LICENSE.md file for details.
"Bluetooth 2 HID" image @PixelGordo is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
- Mike Redrobe for the idea and the basic code logic and HeuristicPerson's bluetooth_2_hid based off this.
- Georgi Valkov for python-evdev making reading input devices a walk in the park.
- The folks at Adafruit for CircuitPython HID and Blinka providing super smooth access to USB gadgets.
- Special thanks to the open-source community for various other libraries and tools.