├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── boot.sh ├── dbus └── org.thanhle.btkbservice.conf ├── keyboard ├── kb_client.py ├── keymap.py └── send_string.py ├── mouse ├── mouse_client.py └── mouse_emulate.py ├── server ├── btk_server.py └── sdp_record.xml ├── setup.sh ├── uninstall.sh ├── updateMac.sh └── updateName.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: quangthanh010290 4 | patreon: relaxtech 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | bluetooth.service.bk 103 | 104 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 quangthanh010290 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/quangthanh010290/keyboard_mouse_emulate_on_raspberry.svg?branch=master)](https://travis-ci.com/quangthanh010290/keyboard_mouse_emulate_on_raspberry) 2 | 3 | # Make things work first 4 | 5 | ## Step 1: Setup 6 | 7 | ``` 8 | sudo ./setup.sh 9 | ``` 10 | 11 | 12 | ## Step 2.1: Add your host mac 13 | 14 | Go to `./server/btk_server.py`, fill your host's mac address to `TARGET_ADDRESS` variable at line 26 15 | 16 | > Lazy to write at scipt, so you need to change it manually 17 | 18 | 19 | ## Step 2.2: Run the Server 20 | 21 | ``` 22 | sudo ./boot.sh 23 | ``` 24 | 25 | 26 | ## Step 3.1: Run Keyboard Client (using physical keyboard) 27 | 28 | - Need a physical keyboard connected to raspberry PI board 29 | 30 | ``` 31 | ./keyboard/kb_client.py 32 | ``` 33 | 34 | ## Step 3.2: Run Keyboard Client (no need physical keyboard, send string through dbus) 35 | 36 | - Dont need a physical keyboard connected to raspberry PI board 37 | 38 | ``` 39 | ./keyboard/send_string.py "hello client, I'm a keyboard" 40 | ``` 41 | 42 | ## Step 3.3: Run mouse client (using physical mouse) 43 | 44 | - Need a physical mouse connected to raspberry PI board 45 | ``` 46 | ./mouse/mouse_client.py 47 | ``` 48 | 49 | ## Step 3.4: Run Mouse client (no need physical mouse, string mouse data through dbus) 50 | 51 | - Dont need a physical mouse connected to raspberry PI board 52 | ``` 53 | ./mouse/mouse_emulate.py 0 10 0 0 54 | ``` 55 | 56 | # To understand what I'm doing in the background 57 | [Make Raspberry Pi3 as an emulator bluetooth keyboard](https://thanhle.me/make-raspberry-pi3-as-an-emulator-bluetooth-keyboard/) 58 | 59 | ## Keyboard setup demo (old version) 60 | 61 | [![ScreenShot](https://i0.wp.com/thanhle.me/wp-content/uploads/2020/02/bluetooth_mouse_emulate_on_ra%CC%81pberry.jpg)](https://www.youtube.com/watch?v=fFpIvjS4AXs) 62 | 63 | ## Mouse setup demo (ongoing) 64 | [Emulate Bluetooth mouse with Raspberry Pi](https://thanhle.me/emulate-bluetooth-mouse-with-raspberry-pi/) 65 | [![ScreenShot](https://i0.wp.com/thanhle.me/wp-content/uploads/2020/08/bluetooth_mouse_emulation_on_raspberry.jpg)](https://www.youtube.com/watch?v=fFpIvjS4AXs) 66 | -------------------------------------------------------------------------------- /boot.sh: -------------------------------------------------------------------------------- 1 | #Stop the background process 2 | sudo hciconfig hci0 down 3 | sudo systemctl daemon-reload 4 | sudo /etc/init.d/bluetooth start 5 | # Update mac address 6 | ./updateMac.sh 7 | #Update Name 8 | ./updateName.sh ThanhLe_Keyboard_Mouse 9 | #Get current Path 10 | export C_PATH=$(pwd) 11 | 12 | tmux kill-window -t thanhle:app >/dev/null 2>&1 13 | 14 | [ ! -z "$(tmux has-session -t thanhle 2>&1)" ] && tmux new-session -s thanhle -n app -d 15 | [ ! -z "$(tmux has-session -t thanhle:app 2>&1)" ] && { 16 | tmux new-window -t thanhle -n app 17 | } 18 | [ ! -z "$(tmux has-session -t thanhle:app.1 2>&1)" ] && tmux split-window -t thanhle:app -h 19 | [ ! -z "$(tmux has-session -t thanhle:app.2 2>&1)" ] && tmux split-window -t thanhle:app.1 -v 20 | tmux send-keys -t thanhle:app.0 'cd $C_PATH/server && sudo ./btk_server.py' C-m 21 | tmux send-keys -t thanhle:app.1 'cd $C_PATH/mouse && reset' C-m 22 | tmux send-keys -t thanhle:app.2 'cd $C_PATH/keyboard && reset' C-m -------------------------------------------------------------------------------- /dbus/org.thanhle.btkbservice.conf: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /keyboard/kb_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Thanhle Bluetooth keyboard emulation service 4 | # keyboard copy client. 5 | # Reads local key events and forwards them to the btk_server DBUS service 6 | # 7 | import os # used to all external commands 8 | import sys # used to exit the script 9 | import dbus 10 | import dbus.service 11 | import dbus.mainloop.glib 12 | import time 13 | import evdev # used to get input from the keyboard 14 | from evdev import * 15 | import keymap # used to map evdev input to hid keodes 16 | 17 | 18 | # Define a client to listen to local key events 19 | class Keyboard(): 20 | 21 | def __init__(self): 22 | # the structure for a bt keyboard input report (size is 10 bytes) 23 | 24 | self.state = [ 25 | 0xA1, # this is an input report 26 | 0x01, # Usage report = Keyboard 27 | # Bit array for Modifier keys 28 | [0, # Right GUI - Windows Key 29 | 0, # Right ALT 30 | 0, # Right Shift 31 | 0, # Right Control 32 | 0, # Left GUI 33 | 0, # Left ALT 34 | 0, # Left Shift 35 | 0], # Left Control 36 | 0x00, # Vendor reserved 37 | 0x00, # rest is space for 6 keys 38 | 0x00, 39 | 0x00, 40 | 0x00, 41 | 0x00, 42 | 0x00] 43 | 44 | print("setting up DBus Client") 45 | 46 | self.bus = dbus.SystemBus() 47 | self.btkservice = self.bus.get_object( 48 | 'org.thanhle.btkbservice', '/org/thanhle/btkbservice') 49 | self.iface = dbus.Interface(self.btkservice, 'org.thanhle.btkbservice') 50 | print("waiting for keyboard") 51 | # keep trying to key a keyboard 52 | have_dev = False 53 | while have_dev == False: 54 | try: 55 | # try and get a keyboard - should always be event0 as 56 | # we're only plugging one thing in 57 | self.dev = InputDevice("/dev/input/event0") 58 | have_dev = True 59 | except OSError: 60 | print("Keyboard not found, waiting 3 seconds and retrying") 61 | time.sleep(3) 62 | print("found a keyboard") 63 | 64 | def change_state(self, event): 65 | evdev_code = ecodes.KEY[event.code] 66 | modkey_element = keymap.modkey(evdev_code) 67 | 68 | if modkey_element > 0: 69 | if self.state[2][modkey_element] == 0: 70 | self.state[2][modkey_element] = 1 71 | else: 72 | self.state[2][modkey_element] = 0 73 | else: 74 | # Get the keycode of the key 75 | hex_key = keymap.convert(ecodes.KEY[event.code]) 76 | # Loop through elements 4 to 9 of the inport report structure 77 | for i in range(4, 10): 78 | if self.state[i] == hex_key and event.value == 0: 79 | # Code 0 so we need to depress it 80 | self.state[i] = 0x00 81 | elif self.state[i] == 0x00 and event.value == 1: 82 | # if the current space if empty and the key is being pressed 83 | self.state[i] = hex_key 84 | break 85 | 86 | # poll for keyboard events 87 | def event_loop(self): 88 | for event in self.dev.read_loop(): 89 | # only bother if we hit a key and its an up or down event 90 | if event.type == ecodes.EV_KEY and event.value < 2: 91 | self.change_state(event) 92 | self.send_input() 93 | 94 | # forward keyboard events to the dbus service 95 | def send_input(self): 96 | bin_str = "" 97 | element = self.state[2] 98 | for bit in element: 99 | bin_str += str(bit) 100 | a = self.state 101 | print(*a) 102 | self.iface.send_keys(int(bin_str, 2), self.state[4:10]) 103 | 104 | 105 | if __name__ == "__main__": 106 | 107 | print("Setting up keyboard") 108 | 109 | kb = Keyboard() 110 | 111 | print("starting event loop") 112 | kb.event_loop() 113 | -------------------------------------------------------------------------------- /keyboard/keymap.py: -------------------------------------------------------------------------------- 1 | # 2 | # https://thanhle.me/make-raspberry-pi3-as-an-emulator-bluetooth-keyboard/ 3 | # 4 | # 5 | # 6 | # Convert value returned from Linux event device ("evdev") to a HID code. This 7 | # is reverse of what's actually hardcoded in the kernel. 8 | # 9 | # Thanh Le 10 | # License: GPL 11 | # 12 | # Ported to a Python module by Thanh Le 13 | # 14 | 15 | keytable = { 16 | "KEY_RESERVED" : 0, 17 | "KEY_ESC" : 41, 18 | "KEY_1" : 30, 19 | "KEY_2" : 31, 20 | "KEY_3" : 32, 21 | "KEY_4" : 33, 22 | "KEY_5" : 34, 23 | "KEY_6" : 35, 24 | "KEY_7" : 36, 25 | "KEY_8" : 37, 26 | "KEY_9" : 38, 27 | "KEY_0" : 39, 28 | "KEY_MINUS" : 45, 29 | "KEY_EQUAL" : 46, 30 | "KEY_BACKSPACE" : 42, 31 | "KEY_TAB" : 43, 32 | "KEY_Q" : 20, 33 | "KEY_W" : 26, 34 | "KEY_E" : 8, 35 | "KEY_R" : 21, 36 | "KEY_T" : 23, 37 | "KEY_Y" : 28, 38 | "KEY_U" : 24, 39 | "KEY_I" : 12, 40 | "KEY_O" : 18, 41 | "KEY_P" : 19, 42 | "KEY_LEFTBRACE" : 47, 43 | "KEY_RIGHTBRACE" : 48, 44 | "KEY_ENTER" : 40, 45 | "KEY_LEFTCTRL" : 224, 46 | "KEY_A" : 4, 47 | "KEY_S" : 22, 48 | "KEY_D" : 7, 49 | "KEY_F" : 9, 50 | "KEY_G" : 10, 51 | "KEY_H" : 11, 52 | "KEY_J" : 13, 53 | "KEY_K" : 14, 54 | "KEY_L" : 15, 55 | "KEY_SEMICOLON" : 51, 56 | "KEY_APOSTROPHE" : 52, 57 | "KEY_GRAVE" : 53, 58 | "KEY_LEFTSHIFT" : 225, 59 | "KEY_BACKSLASH" : 50, 60 | "KEY_Z" : 29, 61 | "KEY_X" : 27, 62 | "KEY_C" : 6, 63 | "KEY_V" : 25, 64 | "KEY_B" : 5, 65 | "KEY_N" : 17, 66 | "KEY_M" : 16, 67 | "KEY_COMMA" : 54, 68 | "KEY_DOT" : 55, 69 | "KEY_SLASH" : 56, 70 | "KEY_RIGHTSHIFT" : 229, 71 | "KEY_KPASTERISK" : 85, 72 | "KEY_LEFTALT" : 226, 73 | "KEY_SPACE" : 44, 74 | "KEY_CAPSLOCK" : 57, 75 | "KEY_F1" : 58, 76 | "KEY_F2" : 59, 77 | "KEY_F3" : 60, 78 | "KEY_F4" : 61, 79 | "KEY_F5" : 62, 80 | "KEY_F6" : 63, 81 | "KEY_F7" : 64, 82 | "KEY_F8" : 65, 83 | "KEY_F9" : 66, 84 | "KEY_F10" : 67, 85 | "KEY_NUMLOCK" : 83, 86 | "KEY_SCROLLLOCK" : 71, 87 | "KEY_KP7" : 95, 88 | "KEY_KP8" : 96, 89 | "KEY_KP9" : 97, 90 | "KEY_KPMINUS" : 86, 91 | "KEY_KP4" : 92, 92 | "KEY_KP5" : 93, 93 | "KEY_KP6" : 94, 94 | "KEY_KPPLUS" : 87, 95 | "KEY_KP1" : 89, 96 | "KEY_KP2" : 90, 97 | "KEY_KP3" : 91, 98 | "KEY_KP0" : 98, 99 | "KEY_KPDOT" : 99, 100 | "KEY_ZENKAKUHANKAKU" : 148, 101 | "KEY_102ND" : 100, 102 | "KEY_F11" : 68, 103 | "KEY_F12" : 69, 104 | "KEY_RO" : 135, 105 | "KEY_KATAKANA" : 146, 106 | "KEY_HIRAGANA" : 147, 107 | "KEY_HENKAN" : 138, 108 | "KEY_KATAKANAHIRAGANA" : 136, 109 | "KEY_MUHENKAN" : 139, 110 | "KEY_KPJPCOMMA" : 140, 111 | "KEY_KPENTER" : 88, 112 | "KEY_RIGHTCTRL" : 228, 113 | "KEY_KPSLASH" : 84, 114 | "KEY_SYSRQ" : 70, 115 | "KEY_RIGHTALT" : 230, 116 | "KEY_HOME" : 74, 117 | "KEY_UP" : 82, 118 | "KEY_PAGEUP" : 75, 119 | "KEY_LEFT" : 80, 120 | "KEY_RIGHT" : 79, 121 | "KEY_END" : 77, 122 | "KEY_DOWN" : 81, 123 | "KEY_PAGEDOWN" : 78, 124 | "KEY_INSERT" : 73, 125 | "KEY_DELETE" : 76, 126 | "KEY_MUTE" : 239, 127 | "KEY_VOLUMEDOWN" : 238, 128 | "KEY_VOLUMEUP" : 237, 129 | "KEY_POWER" : 102, 130 | "KEY_KPEQUAL" : 103, 131 | "KEY_PAUSE" : 72, 132 | "KEY_KPCOMMA" : 133, 133 | "KEY_HANGEUL" : 144, 134 | "KEY_HANJA" : 145, 135 | "KEY_YEN" : 137, 136 | "KEY_LEFTMETA" : 227, 137 | "KEY_RIGHTMETA" : 231, 138 | "KEY_COMPOSE" : 101, 139 | "KEY_STOP" : 243, 140 | "KEY_AGAIN" : 121, 141 | "KEY_PROPS" : 118, 142 | "KEY_UNDO" : 122, 143 | "KEY_FRONT" : 119, 144 | "KEY_COPY" : 124, 145 | "KEY_OPEN" : 116, 146 | "KEY_PASTE" : 125, 147 | "KEY_FIND" : 244, 148 | "KEY_CUT" : 123, 149 | "KEY_HELP" : 117, 150 | "KEY_CALC" : 251, 151 | "KEY_SLEEP" : 248, 152 | "KEY_WWW" : 240, 153 | "KEY_COFFEE" : 249, 154 | "KEY_BACK" : 241, 155 | "KEY_FORWARD" : 242, 156 | "KEY_EJECTCD" : 236, 157 | "KEY_NEXTSONG" : 235, 158 | "KEY_PLAYPAUSE" : 232, 159 | "KEY_PREVIOUSSONG" : 234, 160 | "KEY_STOPCD" : 233, 161 | "KEY_REFRESH" : 250, 162 | "KEY_EDIT" : 247, 163 | "KEY_SCROLLUP" : 245, 164 | "KEY_SCROLLDOWN" : 246, 165 | "KEY_F13" : 104, 166 | "KEY_F14" : 105, 167 | "KEY_F15" : 106, 168 | "KEY_F16" : 107, 169 | "KEY_F17" : 108, 170 | "KEY_F18" : 109, 171 | "KEY_F19" : 110, 172 | "KEY_F20" : 111, 173 | "KEY_F21" : 112, 174 | "KEY_F22" : 113, 175 | "KEY_F23" : 114, 176 | "KEY_F24" : 115 177 | } 178 | 179 | # Map modifier keys to array element in the bit array 180 | modkeys = { 181 | "KEY_RIGHTMETA" : 0, 182 | "KEY_RIGHTALT" : 1, 183 | "KEY_RIGHTSHIFT" : 2, 184 | "KEY_RIGHTCTRL" : 3, 185 | "KEY_LEFTMETA" : 4, 186 | "KEY_LEFTALT": 5, 187 | "KEY_LEFTSHIFT": 6, 188 | "KEY_LEFTCTRL": 7 189 | } 190 | 191 | def convert(evdev_keycode): 192 | return keytable[evdev_keycode] 193 | 194 | def modkey(evdev_keycode): 195 | if evdev_keycode in modkeys: 196 | return modkeys[evdev_keycode] 197 | else: 198 | return -1 # Return an invalid array element 199 | -------------------------------------------------------------------------------- /keyboard/send_string.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os # used to all external commands 3 | import sys # used to exit the script 4 | import dbus 5 | import dbus.service 6 | import dbus.mainloop.glib 7 | import time 8 | # import thread 9 | import keymap 10 | 11 | 12 | class BtkStringClient(): 13 | # constants 14 | KEY_DOWN_TIME = 0.01 15 | KEY_DELAY = 0.01 16 | 17 | def __init__(self): 18 | # the structure for a bt keyboard input report (size is 10 bytes) 19 | self.state = [ 20 | 0xA1, # this is an input report 21 | 0x01, # Usage report = Keyboard 22 | # Bit array for Modifier keys 23 | [0, # Right GUI - Windows Key 24 | 0, # Right ALT 25 | 0, # Right Shift 26 | 0, # Right Control 27 | 0, # Left GUI 28 | 0, # Left ALT 29 | 0, # Left Shift 30 | 0], # Left Control 31 | 0x00, # Vendor reserved 32 | 0x00, # rest is space for 6 keys 33 | 0x00, 34 | 0x00, 35 | 0x00, 36 | 0x00, 37 | 0x00] 38 | self.scancodes = { 39 | "-": "KEY_MINUS", 40 | "=": "KEY_EQUAL", 41 | ";": "KEY_SEMICOLON", 42 | "'": "KEY_APOSTROPHE", 43 | "`": "KEY_GRAVE", 44 | "\\": "KEY_BACKSLASH", 45 | ",": "KEY_COMMA", 46 | ".": "KEY_DOT", 47 | "/": "KEY_SLASH", 48 | "_": "key_minus", 49 | "+": "key_equal", 50 | ":": "key_semicolon", 51 | "\"": "key_apostrophe", 52 | "~": "key_grave", 53 | "|": "key_backslash", 54 | "<": "key_comma", 55 | ">": "key_dot", 56 | "?": "key_slash", 57 | " ": "KEY_SPACE", 58 | } 59 | 60 | # connect with the Bluetooth keyboard server 61 | print("setting up DBus Client") 62 | self.bus = dbus.SystemBus() 63 | self.btkservice = self.bus.get_object( 64 | 'org.thanhle.btkbservice', '/org/thanhle/btkbservice') 65 | self.iface = dbus.Interface(self.btkservice, 'org.thanhle.btkbservice') 66 | 67 | def send_key_state(self): 68 | """sends a single frame of the current key state to the emulator server""" 69 | bin_str = "" 70 | element = self.state[2] 71 | for bit in element: 72 | bin_str += str(bit) 73 | self.iface.send_keys(int(bin_str, 2), self.state[4:10]) 74 | 75 | def send_key_down(self, scancode, modifiers): 76 | """sends a key down event to the server""" 77 | self.state[2] = modifiers 78 | self.state[4] = scancode 79 | self.send_key_state() 80 | 81 | def send_key_up(self): 82 | """sends a key up event to the server""" 83 | self.state[4] = 0 84 | self.send_key_state() 85 | 86 | def send_string(self, string_to_send): 87 | for c in string_to_send: 88 | cu = c.upper() 89 | modifiers = [ 0, 0, 0, 0, 0, 0, 0, 0 ] 90 | if cu in self.scancodes: 91 | scantablekey = self.scancodes[cu] 92 | if scantablekey.islower(): 93 | modifiers = [ 0, 0, 0, 0, 0, 0, 1, 0 ] 94 | scantablekey = scantablekey.upper() 95 | else: 96 | if c.isupper(): 97 | modifiers = [ 0, 0, 0, 0, 0, 0, 1, 0 ] 98 | scantablekey = "KEY_" + cu 99 | 100 | try: 101 | scancode = keymap.keytable[scantablekey] 102 | except KeyError: 103 | print("character not found in keytable:", c) 104 | else: 105 | self.send_key_down(scancode, modifiers) 106 | time.sleep(BtkStringClient.KEY_DOWN_TIME) 107 | self.send_key_up() 108 | time.sleep(BtkStringClient.KEY_DELAY) 109 | 110 | 111 | if __name__ == "__main__": 112 | if(len(sys.argv) < 2): 113 | print("Usage: send_string ") 114 | exit() 115 | dc = BtkStringClient() 116 | string_to_send = sys.argv[1] 117 | print("Sending " + string_to_send) 118 | dc.send_string(string_to_send) 119 | print("Done.") 120 | -------------------------------------------------------------------------------- /mouse/mouse_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import dbus 4 | import dbus.service 5 | import dbus.mainloop.glib 6 | import time 7 | import evdev 8 | from evdev import * 9 | import logging 10 | from logging import debug, info, warning, error 11 | import os 12 | import sys 13 | from select import select 14 | import pyudev 15 | import re 16 | 17 | logging.basicConfig(level=logging.DEBUG) 18 | 19 | 20 | class InputDevice(): 21 | inputs = [] 22 | 23 | @staticmethod 24 | def init(): 25 | context = pyudev.Context() 26 | devs = context.list_devices(subsystem="input") 27 | InputDevice.monitor = pyudev.Monitor.from_netlink(context) 28 | InputDevice.monitor.filter_by(subsystem='input') 29 | InputDevice.monitor.start() 30 | for d in [*devs]: 31 | InputDevice.add_device(d) 32 | 33 | @staticmethod 34 | def add_device(dev): 35 | if dev.device_node == None or not re.match(".*/event\\d+", dev.device_node): 36 | return 37 | try: 38 | if "ID_INPUT_MOUSE" in dev.properties: 39 | print("detected mouse: " + dev.device_node) 40 | InputDevice.inputs.append(MouseInput(dev.device_node)) 41 | except OSError: 42 | error("Failed to connect to %s", dev.device_node) 43 | 44 | @staticmethod 45 | def remove_device(dev): 46 | if dev.device_node == None or not re.match(".*/event\\d+", dev.device_node): 47 | return 48 | InputDevice.inputs = list( 49 | filter(lambda i: i.device_node != dev.device_node, InputDevice.inputs)) 50 | print("Disconnected %s", dev) 51 | 52 | @staticmethod 53 | def set_leds_all(ledvalue): 54 | for dev in InputDevice.inputs: 55 | dev.set_leds(ledvalue) 56 | 57 | @staticmethod 58 | def grab(on): 59 | if on: 60 | for dev in InputDevice.inputs: 61 | dev.device.grab() 62 | else: 63 | for dev in InputDevice.inputs: 64 | dev.device.ungrab() 65 | 66 | def __init__(self, device_node): 67 | self.device_node = device_node 68 | self.device = evdev.InputDevice(device_node) 69 | self.device.grab() 70 | info("Connected %s", self) 71 | 72 | def fileno(self): 73 | return self.device.fd 74 | 75 | def __str__(self): 76 | return "%s@%s (%s)" % (self.__class__.__name__, self.device_node, self.device.name) 77 | 78 | 79 | class MouseInput(InputDevice): 80 | def __init__(self, device_node): 81 | super().__init__(device_node) 82 | self.state = [0, 0, 0, 0] 83 | self.x = 0 84 | self.y = 0 85 | self.z = 0 86 | self.change = False 87 | self.last = 0 88 | self.bus = dbus.SystemBus() 89 | self.btkservice = self.bus.get_object( 90 | 'org.thanhle.btkbservice', '/org/thanhle/btkbservice') 91 | self.iface = dbus.Interface(self.btkservice, 'org.thanhle.btkbservice') 92 | self.mouse_delay = 20 / 1000 93 | self.mouse_speed = 1 94 | 95 | def send_current(self, ir): 96 | try: 97 | self.iface.send_mouse(0, bytes(ir)) 98 | except OSError as err: 99 | error(err) 100 | 101 | def change_state(self, event): 102 | if event.type == ecodes.EV_SYN: 103 | current = time.monotonic() 104 | diff = 20/1000 105 | if current - self.last < diff and not self.change: 106 | return 107 | self.last = current 108 | speed = 1 109 | self.state[1] = min(127, max(-127, int(self.x * speed))) & 255 110 | self.state[2] = min(127, max(-127, int(self.y * speed))) & 255 111 | self.state[3] = min(127, max(-127, self.z)) & 255 112 | self.x = 0 113 | self.y = 0 114 | self.z = 0 115 | self.change = False 116 | self.send_current(self.state) 117 | if event.type == ecodes.EV_KEY: 118 | debug("Key event %s %d", ecodes.BTN[event.code], event.value) 119 | self.change = True 120 | if event.code >= 272 and event.code <= 276 and event.value < 2: 121 | button_no = event.code - 272 122 | if event.value == 1: 123 | self.state[0] |= 1 << button_no 124 | else: 125 | self.state[0] &= ~(1 << button_no) 126 | if event.type == ecodes.EV_REL: 127 | if event.code == 0: 128 | self.x += event.value 129 | if event.code == 1: 130 | self.y += event.value 131 | if event.code == 8: 132 | self.z += event.value 133 | 134 | def get_info(self): 135 | print("hello") 136 | 137 | def set_leds(self, ledvalue): 138 | pass 139 | 140 | 141 | if __name__ == "__main__": 142 | InputDevice.init() 143 | while True: 144 | desctiptors = [*InputDevice.inputs, InputDevice.monitor] 145 | r = select(desctiptors, [], []) 146 | for i in InputDevice.inputs: 147 | try: 148 | for event in i.device.read(): 149 | i.change_state(event) 150 | except OSError as err: 151 | warning(err) 152 | -------------------------------------------------------------------------------- /mouse/mouse_emulate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | import dbus 6 | import dbus.service 7 | import dbus.mainloop.glib 8 | 9 | 10 | class MouseClient(): 11 | def __init__(self): 12 | super().__init__() 13 | self.state = [0, 0, 0, 0] 14 | self.bus = dbus.SystemBus() 15 | self.btkservice = self.bus.get_object( 16 | 'org.thanhle.btkbservice', '/org/thanhle/btkbservice') 17 | self.iface = dbus.Interface(self.btkservice, 'org.thanhle.btkbservice') 18 | def send_current(self): 19 | try: 20 | self.iface.send_mouse(0, bytes(self.state)) 21 | except OSError as err: 22 | error(err) 23 | 24 | if __name__ == "__main__": 25 | 26 | if (len(sys.argv) < 5): 27 | print("Usage: mouse_emulate [button_num dx dy dz]") 28 | exit() 29 | client = MouseClient() 30 | client.state[0] = int(sys.argv[1]) 31 | client.state[1] = int(sys.argv[2]) 32 | client.state[2] = int(sys.argv[3]) 33 | client.state[3] = int(sys.argv[4]) 34 | print("state:", client.state) 35 | client.send_current() 36 | 37 | -------------------------------------------------------------------------------- /server/btk_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Bluetooth keyboard/Mouse emulator DBUS Service 4 | # 5 | 6 | from __future__ import absolute_import, print_function 7 | from optparse import OptionParser, make_option 8 | import os 9 | import sys 10 | import uuid 11 | import dbus 12 | import dbus.service 13 | import dbus.mainloop.glib 14 | import time 15 | import socket 16 | from gi.repository import GLib 17 | from dbus.mainloop.glib import DBusGMainLoop 18 | import logging 19 | from logging import debug, info, warning, error 20 | import bluetooth 21 | from bluetooth import * 22 | 23 | logging.basicConfig(level=logging.DEBUG) 24 | 25 | # @todo fill your host mac here manually 26 | TARGET_ADDRESS = "" 27 | 28 | class BTKbDevice(): 29 | # change these constants 30 | MY_ADDRESS = "B8:27:EB:C5:B3:27" 31 | MY_DEV_NAME = "ThanhLe_Keyboard_Mouse" 32 | 33 | # define some constants 34 | P_CTRL = 17 # Service port - must match port configured in SDP record 35 | P_INTR = 19 # Interrupt port - must match port configured in SDP record 36 | # dbus path of the bluez profile we will create 37 | # file path of the sdp record to load 38 | SDP_RECORD_PATH = sys.path[0] + "/sdp_record.xml" 39 | UUID = "00001124-0000-1000-8000-00805f9b34fb" 40 | 41 | def __init__(self): 42 | print("2. Setting up BT device") 43 | self.init_bt_device() 44 | self.init_bluez_profile() 45 | 46 | # configure the bluetooth hardware device 47 | def init_bt_device(self): 48 | print("3. Configuring Device name " + BTKbDevice.MY_DEV_NAME) 49 | # set the device class to a keybord and set the name 50 | os.system("hciconfig hci0 up") 51 | os.system("hciconfig hci0 name " + BTKbDevice.MY_DEV_NAME) 52 | # make the device discoverable 53 | os.system("hciconfig hci0 piscan") 54 | 55 | # set up a bluez profile to advertise device capabilities from a loaded service record 56 | def init_bluez_profile(self): 57 | print("4. Configuring Bluez Profile") 58 | # setup profile options 59 | service_record = self.read_sdp_service_record() 60 | opts = { 61 | "AutoConnect": True, 62 | "ServiceRecord": service_record 63 | } 64 | # retrieve a proxy for the bluez profile interface 65 | bus = dbus.SystemBus() 66 | manager = dbus.Interface(bus.get_object( 67 | "org.bluez", "/org/bluez"), "org.bluez.ProfileManager1") 68 | manager.RegisterProfile("/org/bluez/hci0", BTKbDevice.UUID, opts) 69 | print("6. Profile registered ") 70 | os.system("hciconfig hci0 class 0x002540") 71 | 72 | # read and return an sdp record from a file 73 | def read_sdp_service_record(self): 74 | print("5. Reading service record") 75 | try: 76 | fh = open(BTKbDevice.SDP_RECORD_PATH, "r") 77 | except: 78 | sys.exit("Could not open the sdp record. Exiting...") 79 | return fh.read() 80 | 81 | def setup_socket(self): 82 | self.scontrol = socket.socket( 83 | socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) # BluetoothSocket(L2CAP) 84 | self.sinterrupt = socket.socket( 85 | socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) # BluetoothSocket(L2CAP) 86 | self.scontrol.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 87 | self.sinterrupt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 88 | # bind these sockets to a port - port zero to select next available 89 | self.scontrol.bind((socket.BDADDR_ANY, self.P_CTRL)) 90 | self.sinterrupt.bind((socket.BDADDR_ANY, self.P_INTR)) 91 | 92 | # listen for incoming client connections 93 | def listen(self): 94 | print("\033[0;33m7. Waiting for connections\033[0m") 95 | 96 | # key point: use connect to get the host request for the accept() below 97 | # it work, I just dont care for having been into it for 2days 98 | self.setup_socket() 99 | try: 100 | # must be ahead of listen or 'File descriptor in bad state' 101 | self.scontrol.connect((TARGET_ADDRESS, self.P_CTRL)) 102 | except socket.error as err: 103 | # it was expect to failed 104 | print("Connect failed: "+str(err)) 105 | 106 | # this may not work 107 | # os.system("bluetoothctl connect " + TARGET_ADDRESS) 108 | 109 | self.setup_socket() 110 | 111 | # Start listening on the server sockets 112 | self.scontrol.listen(5) 113 | self.sinterrupt.listen(5) 114 | 115 | self.ccontrol, cinfo = self.scontrol.accept() 116 | print ( 117 | "\033[0;32mGot a connection on the control channel from %s \033[0m" % cinfo[0]) 118 | 119 | self.cinterrupt, cinfo = self.sinterrupt.accept() 120 | print ( 121 | "\033[0;32mGot a connection on the interrupt channel from %s \033[0m" % cinfo[0]) 122 | 123 | # send a string to the bluetooth host machine 124 | def send_string(self, message): 125 | try: 126 | self.cinterrupt.send(bytes(message)) 127 | except OSError as err: 128 | error(err) 129 | self.listen() 130 | 131 | 132 | class BTKbService(dbus.service.Object): 133 | 134 | def __init__(self): 135 | print("1. Setting up service") 136 | # set up as a dbus service 137 | bus_name = dbus.service.BusName( 138 | "org.thanhle.btkbservice", bus=dbus.SystemBus()) 139 | dbus.service.Object.__init__( 140 | self, bus_name, "/org/thanhle/btkbservice") 141 | # create and setup our device 142 | self.device = BTKbDevice() 143 | # start listening for connections 144 | self.device.listen() 145 | 146 | @dbus.service.method('org.thanhle.btkbservice', in_signature='yay') 147 | def send_keys(self, modifier_byte, keys): 148 | print("Get send_keys request through dbus") 149 | print("key msg: ", keys) 150 | state = [ 0xA1, 1, 0, 0, 0, 0, 0, 0, 0, 0 ] 151 | state[2] = int(modifier_byte) 152 | count = 4 153 | for key_code in keys: 154 | if(count < 10): 155 | state[count] = int(key_code) 156 | count += 1 157 | self.device.send_string(state) 158 | 159 | @dbus.service.method('org.thanhle.btkbservice', in_signature='yay') 160 | def send_mouse(self, modifier_byte, keys): 161 | state = [0xA1, 2, 0, 0, 0, 0] 162 | count = 2 163 | for key_code in keys: 164 | if(count < 6): 165 | state[count] = int(key_code) 166 | count += 1 167 | self.device.send_string(state) 168 | 169 | 170 | # main routine 171 | if __name__ == "__main__": 172 | # we an only run as root 173 | try: 174 | if not os.geteuid() == 0: 175 | sys.exit("Only root can run this script") 176 | 177 | if TARGET_ADDRESS == "": 178 | sys.exit("Please fill your host mac address in line 26") 179 | 180 | DBusGMainLoop(set_as_default=True) 181 | myservice = BTKbService() 182 | loop = GLib.MainLoop() 183 | loop.run() 184 | except KeyboardInterrupt: 185 | sys.exit() 186 | -------------------------------------------------------------------------------- /server/sdp_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo apt-get update -y 3 | sudo apt-get install -y --ignore-missing git tmux bluez bluez-tools bluez-firmware 4 | sudo apt-get install -y --ignore-missing python3 python3-dev python3-pip python3-dbus python3-pyudev python3-evdev python3-gi 5 | 6 | sudo apt-get install -y libbluetooth-dev 7 | sudo PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install git+https://github.com/pybluez/pybluez.git#egg=pybluez 8 | 9 | sudo cp dbus/org.thanhle.btkbservice.conf /etc/dbus-1/system.d 10 | sudo systemctl restart dbus.service 11 | 12 | sudo sed -i '/^ExecStart=/ s/$/ --noplugin=input/' /lib/systemd/system/bluetooth.service 13 | sudo systemctl daemon-reload 14 | sudo systemctl restart bluetooth.service 15 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f "bluetooth.service.bk" ] ; then 4 | sudo cp bluetooth.service.bk /lib/systemd/system/bluetooth.service 5 | sudo systemctl daemon-reload 6 | sudo /etc/init.d/bluetooth start 7 | fi -------------------------------------------------------------------------------- /updateMac.sh: -------------------------------------------------------------------------------- 1 | mac=$(hciconfig hci0 | awk '/BD Address: /{print $3}') 2 | if [ "$mac" != NULL ] ; then 3 | echo $mac 4 | a="MY_ADDRESS" 5 | b="\"$mac\"" 6 | sed -i -e "s/\($a = \).*/\1$b/" server/btk_server.py 7 | fi 8 | #echo $mac 9 | -------------------------------------------------------------------------------- /updateName.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | a="\"$1\"" 3 | sed -i -e "s/\(MY_DEV_NAME = \).*/\1$a/" server/btk_server.py 4 | --------------------------------------------------------------------------------