Skip to content

Instantly share code, notes, and snippets.

@scientificRat
Last active September 18, 2024 00:16
Show Gist options
  • Save scientificRat/be2bbac0769bfa04820bc73edc009bdf to your computer and use it in GitHub Desktop.
Save scientificRat/be2bbac0769bfa04820bc73edc009bdf to your computer and use it in GitHub Desktop.
Use raspberry pi as Bluetooth HID mouse/keyboard emulator

Dependency:

python>=3.5

sudo apt-get install python-gobject pi-bluetooth bluez bluez-tools bluez-firmware 
sudo pip3 install evdev
sudo pip3 install gattlib
sudo pip3 install pybluez
sudo pip3 install pybluez\[ble\]

Specification: the hid communication protocol is determined by the string in sdp.xml: 05010906a101850175019508050719e029e715002501810295017508810395057501050819012905910295017503910395067508150026ff000507190029ff8100c005010902A10185020901A1000509190129031500250175019503810275059501810105010930093109381581257F750895038106C0C0 which means:

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0xE0,        //   Usage Minimum (0xE0)
0x29, 0xE7,        //   Usage Maximum (0xE7)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01,        //   Report Count (1)
0x75, 0x08,        //   Report Size (8)
0x81, 0x03,        //   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x05,        //   Report Count (5)
0x75, 0x01,        //   Report Size (1)
0x05, 0x08,        //   Usage Page (LEDs)
0x19, 0x01,        //   Usage Minimum (Num Lock)
0x29, 0x05,        //   Usage Maximum (Kana)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x95, 0x01,        //   Report Count (1)
0x75, 0x03,        //   Report Size (3)
0x91, 0x03,        //   Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x95, 0x06,        //   Report Count (6)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0x00,        //   Usage Minimum (0x00)
0x29, 0xFF,        //   Usage Maximum (0xFF)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x02,        // Usage (Mouse)
0xA1, 0x01,        // Collection (Application)
0x85, 0x02,        //   Report ID (2)
0x09, 0x01,        //   Usage (Pointer)
0xA1, 0x00,        //   Collection (Physical)
0x05, 0x09,        //     Usage Page (Button)
0x19, 0x01,        //     Usage Minimum (0x01)
0x29, 0x03,        //     Usage Maximum (0x03)
0x15, 0x00,        //     Logical Minimum (0)
0x25, 0x01,        //     Logical Maximum (1)
0x75, 0x01,        //     Report Size (1)
0x95, 0x03,        //     Report Count (3)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x05,        //     Report Size (5)
0x95, 0x01,        //     Report Count (1)
0x81, 0x01,        //     Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x09, 0x38,        //     Usage (Wheel)
0x15, 0x81,        //     Logical Minimum (-127)
0x25, 0x7F,        //     Logical Maximum (127)
0x75, 0x08,        //     Report Size (8)
0x95, 0x03,        //     Report Count (3)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              //   End Collection
0xC0,              // End Collection

// 120 bytes
#
# Taken from https://www.gadgetdaily.xyz/create-a-cool-sliding-and-scrollable-mobile-menu/
#
# Convert value returned from Linux event device ("evdev") to a HID code. This
# is reverse of what's actually hardcoded in the kernel.
#
# Lubomir Rintel <lkundrak@v3.sk>
# License: GPL
#
# Ported to a Python module by Liam Fraser.
#
keytable = {
"KEY_RESERVED": 0,
"KEY_ESC": 41,
"KEY_1": 30,
"KEY_2": 31,
"KEY_3": 32,
"KEY_4": 33,
"KEY_5": 34,
"KEY_6": 35,
"KEY_7": 36,
"KEY_8": 37,
"KEY_9": 38,
"KEY_0": 39,
"KEY_MINUS": 45,
"KEY_EQUAL": 46,
"KEY_BACKSPACE": 42,
"KEY_TAB": 43,
"KEY_Q": 20,
"KEY_W": 26,
"KEY_E": 8,
"KEY_R": 21,
"KEY_T": 23,
"KEY_Y": 28,
"KEY_U": 24,
"KEY_I": 12,
"KEY_O": 18,
"KEY_P": 19,
"KEY_LEFTBRACE": 47,
"KEY_RIGHTBRACE": 48,
"KEY_ENTER": 40,
"KEY_LEFTCTRL": 224,
"KEY_A": 4,
"KEY_S": 22,
"KEY_D": 7,
"KEY_F": 9,
"KEY_G": 10,
"KEY_H": 11,
"KEY_J": 13,
"KEY_K": 14,
"KEY_L": 15,
"KEY_SEMICOLON": 51,
"KEY_APOSTROPHE": 52,
"KEY_GRAVE": 53,
"KEY_LEFTSHIFT": 225,
"KEY_BACKSLASH": 50,
"KEY_Z": 29,
"KEY_X": 27,
"KEY_C": 6,
"KEY_V": 25,
"KEY_B": 5,
"KEY_N": 17,
"KEY_M": 16,
"KEY_COMMA": 54,
"KEY_DOT": 55,
"KEY_SLASH": 56,
"KEY_RIGHTSHIFT": 229,
"KEY_KPASTERISK": 85,
"KEY_LEFTALT": 226,
"KEY_SPACE": 44,
"KEY_CAPSLOCK": 57,
"KEY_F1": 58,
"KEY_F2": 59,
"KEY_F3": 60,
"KEY_F4": 61,
"KEY_F5": 62,
"KEY_F6": 63,
"KEY_F7": 64,
"KEY_F8": 65,
"KEY_F9": 66,
"KEY_F10": 67,
"KEY_NUMLOCK": 83,
"KEY_SCROLLLOCK": 71,
"KEY_KP7": 95,
"KEY_KP8": 96,
"KEY_KP9": 97,
"KEY_KPMINUS": 86,
"KEY_KP4": 92,
"KEY_KP5": 93,
"KEY_KP6": 94,
"KEY_KPPLUS": 87,
"KEY_KP1": 89,
"KEY_KP2": 90,
"KEY_KP3": 91,
"KEY_KP0": 98,
"KEY_KPDOT": 99,
"KEY_ZENKAKUHANKAKU": 148,
"KEY_102ND": 100,
"KEY_F11": 68,
"KEY_F12": 69,
"KEY_RO": 135,
"KEY_KATAKANA": 146,
"KEY_HIRAGANA": 147,
"KEY_HENKAN": 138,
"KEY_KATAKANAHIRAGANA": 136,
"KEY_MUHENKAN": 139,
"KEY_KPJPCOMMA": 140,
"KEY_KPENTER": 88,
"KEY_RIGHTCTRL": 228,
"KEY_KPSLASH": 84,
"KEY_SYSRQ": 70,
"KEY_RIGHTALT": 230,
"KEY_HOME": 74,
"KEY_UP": 82,
"KEY_PAGEUP": 75,
"KEY_LEFT": 80,
"KEY_RIGHT": 79,
"KEY_END": 77,
"KEY_DOWN": 81,
"KEY_PAGEDOWN": 78,
"KEY_INSERT": 73,
"KEY_DELETE": 76,
"KEY_MUTE": 239,
"KEY_VOLUMEDOWN": 238,
"KEY_VOLUMEUP": 237,
"KEY_POWER": 102,
"KEY_KPEQUAL": 103,
"KEY_PAUSE": 72,
"KEY_KPCOMMA": 133,
"KEY_HANGEUL": 144,
"KEY_HANJA": 145,
"KEY_YEN": 137,
"KEY_LEFTMETA": 227,
"KEY_RIGHTMETA": 231,
"KEY_COMPOSE": 101,
"KEY_STOP": 243,
"KEY_AGAIN": 121,
"KEY_PROPS": 118,
"KEY_UNDO": 122,
"KEY_FRONT": 119,
"KEY_COPY": 124,
"KEY_OPEN": 116,
"KEY_PASTE": 125,
"KEY_FIND": 244,
"KEY_CUT": 123,
"KEY_HELP": 117,
"KEY_CALC": 251,
"KEY_SLEEP": 248,
"KEY_WWW": 240,
"KEY_COFFEE": 249,
"KEY_BACK": 241,
"KEY_FORWARD": 242,
"KEY_EJECTCD": 236,
"KEY_NEXTSONG": 235,
"KEY_PLAYPAUSE": 232,
"KEY_PREVIOUSSONG": 234,
"KEY_STOPCD": 233,
"KEY_REFRESH": 250,
"KEY_EDIT": 247,
"KEY_SCROLLUP": 245,
"KEY_SCROLLDOWN": 246,
"KEY_F13": 104,
"KEY_F14": 105,
"KEY_F15": 106,
"KEY_F16": 107,
"KEY_F17": 108,
"KEY_F18": 109,
"KEY_F19": 110,
"KEY_F20": 111,
"KEY_F21": 112,
"KEY_F22": 113,
"KEY_F23": 114,
"KEY_F24": 115
}
# Map modifier keys to array element in the bit array
modkeys = {
"KEY_RIGHTMETA": 0,
"KEY_RIGHTALT": 1,
"KEY_RIGHTSHIFT": 2,
"KEY_RIGHTCTRL": 3,
"KEY_LEFTMETA": 4,
"KEY_LEFTALT": 5,
"KEY_LEFTSHIFT": 6,
"KEY_LEFTCTRL": 7
}
def convert(evdev_keycode):
return keytable[evdev_keycode]
def modkey(evdev_keycode):
if evdev_keycode in modkeys:
return modkeys[evdev_keycode]
else:
return -1 # Return an invalid array element
import sys
import os
import dbus
import time
import traceback
import keymap
import bluetooth
import dbus.service
import dbus.mainloop.glib
from dbus.mainloop.glib import DBusGMainLoop
class BluetoothBluezProfile(dbus.service.Object):
fd = -1
@dbus.service.method("org.bluez.Profile1", in_signature="", out_signature="")
def Release(self):
print("Release")
exit(-1)
@dbus.service.method("org.bluez.Profile1",
in_signature="", out_signature="")
def Cancel(self):
print("Cancel")
@dbus.service.method("org.bluez.Profile1", in_signature="oha{sv}", out_signature="")
def NewConnection(self, path, fd, properties):
self.fd = fd.take()
print("NewConnection(%s, %d)" % (path, self.fd))
for key in properties.keys():
if key == "Version" or key == "Features":
print(" %s = 0x%04x" % (key, properties[key]))
else:
print(" %s = %s" % (key, properties[key]))
@dbus.service.method("org.bluez.Profile1", in_signature="o", out_signature="")
def RequestDisconnection(self, path):
print("RequestDisconnection(%s)" % (path))
if (self.fd > 0):
os.close(self.fd)
self.fd = -1
def __init__(self, bus, path):
dbus.service.Object.__init__(self, bus, path)
# create a bluetooth device to emulate a HID keyboard/mouse,
# advertize a SDP record using our bluez profile class
#
class BTDevice:
BT_ADDRESS = "DC:A6:32:60:EE:13" # use hciconfig to check
BT_DEV_NAME = "Real_Keyboard"
# define some constants
P_CTRL = 17 # Service port - must match port configured in SDP record
P_INTR = 19 # Service port - must match port configured in SDP record #Interrrupt port
PROFILE_DBUS_PATH = "/bluez/hzy/hidbluetooth_profile" # dbus path of the bluez profile we will create
SDP_RECORD_PATH = "sdp_record.xml" # file path of the sdp record to load
UUID = "00001124-0000-1000-8000-00805f9b34fb"
def __init__(self):
print("Setting up Bluetooth device")
self.init_bt_device()
self.init_bluez_profile()
# configure the bluetooth hardware device
def init_bt_device(self):
print("Configuring for name " + BTDevice.BT_DEV_NAME)
os.system("hciconfig hci0 up")
os.system("sudo hciconfig hci0 class 0x05C0") # General Discoverable Mode
os.system("sudo hciconfig hci0 name " + BTDevice.BT_DEV_NAME)
# make the device discoverable
os.system("sudo hciconfig hci0 piscan")
# set up a bluez profile to advertise device capabilities from a loaded service record
def init_bluez_profile(self):
print("Configuring Bluez Profile")
# setup profile options
service_record = self.read_sdp_service_record()
opts = {
"ServiceRecord": service_record,
"Role": "server",
"RequireAuthentication": False,
"RequireAuthorization": False
}
# retrieve a proxy for the bluez profile interface
bus = dbus.SystemBus()
manager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez"), "org.bluez.ProfileManager1")
profile = BluetoothBluezProfile(bus, self.PROFILE_DBUS_PATH)
manager.RegisterProfile(self.PROFILE_DBUS_PATH, self.UUID, opts)
print("Profile registered ")
# read and return an sdp record from a file
def read_sdp_service_record(self):
print("Reading service record")
try:
fh = open(self.SDP_RECORD_PATH, "r")
except Exception as e:
traceback.print_exc()
print(e)
sys.exit("Could not open the sdp record. Exiting...")
return fh.read()
# listen for incoming client connections
# ideally this would be handled by the Bluez 5 profile
# but that didn't seem to work
def listen(self):
print("Waiting for connections")
self.scontrol = bluetooth.BluetoothSocket(bluetooth.L2CAP)
self.sinterrupt = bluetooth.BluetoothSocket(bluetooth.L2CAP)
print("bind...")
# bind these sockets to a port - port zero to select next available
self.scontrol.bind((self.BT_ADDRESS, self.P_CTRL))
self.sinterrupt.bind((self.BT_ADDRESS, self.P_INTR))
print("listen...")
# Start listening on the server sockets
self.scontrol.listen(1) # Limit of 1 connection
self.sinterrupt.listen(1)
print("ready to accept...")
self.ccontrol, cinfo = self.scontrol.accept()
print("Got a connection on the control channel from " + cinfo[0])
self.cinterrupt, cinfo = self.sinterrupt.accept()
print("Got a connection on the interrupt channel from " + cinfo[0])
# send a string to the bluetooth host machine
def send_string(self, message):
self.cinterrupt.send(message)
def close(self):
self.scontrol.close()
self.sinterrupt.close()
def send_keys(self, modifier_byte, keys):
cmd_bytes = bytearray()
cmd_bytes.append(0xA1)
cmd_bytes.append(0x01) # report id
cmd_bytes.append(modifier_byte)
cmd_bytes.append(0x00)
assert len(keys) == 6
for key_code in keys:
cmd_bytes.append(key_code)
self.send_string(bytes(cmd_bytes))
def send_mouse(self, buttons, rel_move):
cmd_bytes = bytearray()
cmd_bytes.append(0xA1)
cmd_bytes.append(0x02) # report id
cmd_bytes.append(buttons)
cmd_bytes.append(rel_move[0])
cmd_bytes.append(rel_move[1])
cmd_bytes.append(rel_move[2])
self.send_string(bytes(cmd_bytes))
def send_string(device, string, key_down_time=0.01, key_delay=0.05):
for c in string:
key = 'KEY_' + c.upper()
if key in keymap.keytable:
code = keymap.keytable[key]
device.send_keys(0, [code, 0, 0, 0, 0, 0])
time.sleep(key_down_time)
device.send_keys(0, [0, 0, 0, 0, 0, 0])
time.sleep(key_delay)
def main():
if not os.geteuid() == 0:
sys.exit("Only root can run this script")
print("restart bluetooth")
os.system("sudo service bluetoothd stop")
os.system("sudo service dbus restart")
os.system("sudo /usr/sbin/bluetoothd -p time&")
os.system("sudo hciconfig hci0 down")
os.system("sudo hciconfig hci0 up")
DBusGMainLoop(set_as_default=True)
device = BTDevice()
device.listen()
print("init success")
while True:
v = input("input str>>>")
if v == 'q':
break
elif v == 'm':
print("send mouse")
device.send_mouse(0, [10, 30, 1])
else:
print('send:', v)
send_string(device, v)
if __name__ == '__main__':
main()
<?xml version="1.0" encoding="UTF-8" ?>
<!--
A description of these fields can be found in the following links:
http://www.bluecove.org/bluecove/apidocs/javax/bluetooth/ServiceRecord.html
https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
-->
<record>
<attribute id="0x0001"> <!-- Service Class ID List -->
<sequence>
<uuid value="0x1124" /> <!-- Human Interface Device -->
</sequence>
</attribute>
<attribute id="0x0004"> <!-- Protocol Descriptor List -->
<sequence>
<sequence>
<uuid value="0x0100" /> <!-- L2CAP -->
<uint16 value="0x0011" /> <!-- HIDP -->
</sequence>
<sequence>
<uuid value="0x0011" /> <!-- HIDP -->
</sequence>
</sequence>
</attribute>
<attribute id="0x0005"> <!-- Browse Group List -->
<sequence>
<uuid value="0x1002" />
</sequence>
</attribute>
<attribute id="0x0006"> <!-- Language Based Attribute ID List -->
<sequence>
<uint16 value="0x656e" /> <!-- code_ISO639 -->
<uint16 value="0x006a" /> <!-- encoding -->
<uint16 value="0x0100" /> <!-- base_offset -->
</sequence>
</attribute>
<attribute id="0x0009"> <!-- Bluetooth Profile Descriptor List -->
<sequence>
<sequence>
<uuid value="0x1124" /> <!-- Human Interface Device -->
<uint16 value="0x0100" /> <!-- L2CAP -->
</sequence>
</sequence>
</attribute>
<attribute id="0x000d"> <!-- Additional Protocol Descriptor Lists -->
<sequence>
<sequence>
<sequence>
<uuid value="0x0100" /> <!-- L2CAP -->
<uint16 value="0x0013" />
</sequence>
<sequence>
<uuid value="0x0011" /> <!-- HIDP -->
</sequence>
</sequence>
</sequence>
</attribute>
<attribute id="0x0100">
<text value="Bluetooth_Keyboard/Mouse" />
</attribute>
<attribute id="0x0101">
<text value="USB > BT Keyboard/Mouse" />
</attribute>
<attribute id="0x0102">
<text value="Raspberry Pi 3" />
</attribute>
<attribute id="0x0200">
<uint16 value="0x0100" />
</attribute>
<attribute id="0x0201">
<uint16 value="0x0111" />
</attribute>
<attribute id="0x0202">
<uint8 value="0x40" />
</attribute>
<attribute id="0x0203">
<uint8 value="0x00" />
</attribute>
<attribute id="0x0204">
<boolean value="false" />
</attribute>
<attribute id="0x0205">
<boolean value="false" />
</attribute>
<attribute id="0x0206">
<sequence>
<sequence>
<uint8 value="0x22" />
<text encoding="hex" value="05010906a101850175019508050719e029e715002501810295017508810395057501050819012905910295017503910395067508150026ff000507190029ff8100c005010902A10185020901A1000509190129031500250175019503810275059501810105010930093109381581257F750895038106C0C0"/>
</sequence>
</sequence>
</attribute>
<attribute id="0x0207">
<sequence>
<sequence>
<uint16 value="0x0409" />
<uint16 value="0x0100" />
</sequence>
</sequence>
</attribute>
<attribute id="0x020b">
<uint16 value="0x0100" />
</attribute>
<attribute id="0x020c">
<uint16 value="0x0c80" />
</attribute>
<attribute id="0x020d">
<boolean value="true" />
</attribute>
<attribute id="0x020e">
<boolean value="false" />
</attribute>
<attribute id="0x020f">
<uint16 value="0x0640" />
</attribute>
<attribute id="0x0210">
<uint16 value="0x0320" />
</attribute>
</record>
@yesimxev
Copy link

Thanks for your great work! I'm trying to run this on Android (NetHunter). Can you give me a brief explanation what hardware did you use and which OS on the raspberry? The device I try to use says wrong PIN. Although I can see the auth is disabled

@Guation
Copy link

Guation commented Sep 13, 2024

Thanks for your great work! I'm trying to run this on Android (NetHunter). Can you give me a brief explanation what hardware did you use and which OS on the raspberry? The device I try to use says wrong PIN. Although I can see the auth is disabled

Before you can connect to the Raspberry Pi from your phone, you should first pair it using the bluetoothctl pair command.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment