Skip to content

Instantly share code, notes, and snippets.

@jlobos
Forked from scientificRat/keymap.py
Created June 28, 2024 16:40
Show Gist options
  • Save jlobos/0123247e46b598b11e35769891b4ad07 to your computer and use it in GitHub Desktop.
Save jlobos/0123247e46b598b11e35769891b4ad07 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 <[email protected]>
# 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment