├── README.md ├── LICENSE.md ├── fake_keyboard_record.xml └── keyboard.py /README.md: -------------------------------------------------------------------------------- 1 | chipturner/bluetooth 2 | ========= 3 | 4 | My Linux bluetooth playground. So far, just a python script that can 5 | make your linux machine act as a bluetooth keyboard for other 6 | devices. More docs to come. 7 | 8 | Useful links: 9 | 10 | I started here, but it was missing details: 11 | http://www.linuxuser.co.uk/tutorials/emulate-a-bluetooth-keyboard-with-the-raspberry-pi 12 | 13 | So then I went here, and the associated book: 14 | http://people.csail.mit.edu/albert/bluez-intro/ 15 | http://www.amazon.com/Bluetooth-Essentials-Programmers-Albert-Huang/dp/0521703751/ 16 | 17 | The USB HID spec was helpful in figuring out how to actually talk 18 | things like handshakes (which are necessary, and left out of the first 19 | link above). 20 | https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=246761 21 | 22 | I stumbled across this, which was also helpful: 23 | https://code.google.com/p/androhid/source/checkout 24 | 25 | Finally, the bluez source code was solid as well: 26 | http://www.bluez.org/ 27 | http://www.bluez.org/development/git/ 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Chip Turner 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of bluetooth nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /fake_keyboard_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 | -------------------------------------------------------------------------------- /keyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import bluetooth 5 | import dbus 6 | import itertools 7 | import logging 8 | import os 9 | import subprocess 10 | import sys 11 | import time 12 | 13 | HCI_KEYBOARD_CONTROL_PORT = 17 14 | HCI_KEYBOARD_INTERRUPT_PORT = 19 15 | 16 | def read_relative_file(name): 17 | full_path = os.path.join(os.path.dirname(sys.argv[0]), name) 18 | with open(full_path) as fh: 19 | return fh.read() 20 | 21 | def init_adapter(interface, device_name): 22 | # TODO: switch to ctypes; bluetooth._bluetooth doesn't support the 23 | # APIs we need, sadly 24 | logging.info("Running hciconfig for " + interface) 25 | subprocess.call(["hciconfig", interface, "class", "0x002540"]) 26 | subprocess.call(["hciconfig", interface, "name", device_name]) 27 | subprocess.call(["hciconfig", interface, "piscan"]) 28 | 29 | # Become discoverable by asking bluez to advertise our SDP for us 30 | logging.info("Advertising via sdb") 31 | bus = dbus.SystemBus() 32 | manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.bluez.Manager") 33 | adapter_path = manager.FindAdapter(interface) 34 | service = dbus.Interface(bus.get_object("org.bluez", adapter_path), "org.bluez.Service") 35 | service.AddRecord(read_relative_file("fake_keyboard_record.xml")) 36 | 37 | def main(): 38 | logging.basicConfig(format="%(asctime)s %(levelname)-8s %(message)s", level=logging.INFO) 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument("--interface", help="interface, defaults to hci0", default="hci0") 41 | parser.add_argument("--reconnect", help="reconnect to this device (mac addr), otherwise become browsable") 42 | arguments = parser.parse_args() 43 | 44 | # HID mapping is weird. this is just a subset. 45 | hid_byte_map = dict() 46 | for c in range(ord('a'), ord('z')): 47 | hid_byte_map[chr(c)] = c - ord('a') + 4 48 | hid_byte_map[' '] = 0x2c 49 | 50 | init_adapter(arguments.interface, "Linux Keyboard") 51 | 52 | # Either become browsable and answer, or reconnect. Pairing seems 53 | # to be optional? 54 | target_addr = arguments.reconnect 55 | if not target_addr: 56 | logging.info("Listening for connection") 57 | control_listen_socket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 58 | control_listen_socket.bind(("", HCI_KEYBOARD_CONTROL_PORT)) 59 | control_listen_socket.listen(1) 60 | interrupt_listen_socket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 61 | interrupt_listen_socket.bind(("", HCI_KEYBOARD_INTERRUPT_PORT)) 62 | interrupt_listen_socket.listen(1) 63 | 64 | control_socket, control_info = control_listen_socket.accept() 65 | interrupt_socket, interrupt_info = interrupt_listen_socket.accept() 66 | 67 | logging.info("Connection established to %s (%s)" % (control_info, interrupt_info)) 68 | connected = False 69 | while not connected: 70 | header = ord(control_socket.recv(1)) 71 | msg_type, msg_data = (header >> 4), (header & 0b1111) 72 | logging.info("data is %02x %02x" % (msg_type, msg_data)) 73 | if msg_type == 0x09: # SET_IDLE 74 | logging.info("looks like windows") 75 | connected = True 76 | elif msg_type == 0x07: # SET_PROTOCOL 77 | logging.info("looks like iphone") 78 | connected = True 79 | else: 80 | logging.fatal("aiee who knows") 81 | 82 | else: 83 | # just connect to the specified destination 84 | logging.info("Reconnecting to %s" % target_addr) 85 | control_socket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 86 | control_socket.connect((target_addr, HCI_KEYBOARD_CONTROL_PORT)) 87 | interrupt_socket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 88 | interrupt_socket.connect((target_addr, HCI_KEYBOARD_INTERRUPT_PORT)) 89 | 90 | # We have a connection, spam away! 91 | logging.info("Socket connected, sending handshake") 92 | control_socket.send(chr(0)) 93 | logging.info("spamming chip!") 94 | 95 | for byte in itertools.cycle("chip "): 96 | hid_byte = hid_byte_map[byte] 97 | msg = "".join(chr(c) for c in (0xa1, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, hid_byte)) 98 | interrupt_socket.send(msg) 99 | msg = "".join(chr(c) for c in (0xa1, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)) 100 | interrupt_socket.send(msg) 101 | time.sleep(0.05) 102 | 103 | 104 | if __name__ == '__main__': 105 | sys.exit(main()) 106 | --------------------------------------------------------------------------------