├── .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 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------