In todayâs lesson, we will explore how to utilize a Raspberry Pi as a Bluetooth keyboard and develop our program to transmit input to the target device. This hands-on experience will grant us a deeper understanding of Bluetooth protocol functionality, socket operations, and data transmission through simple protocols. Additionally, we will delve into the realm of binary data, examining its various manifestations.
Weâll be implementing a Raspberry Pi-based Bluetooth keyboard that utilizes the latest Raspbian image as its operating system. For flashing an image to an SD card, refer to our primary tutorial. Youâll only need to follow the steps up to write the image to the SD card. Youâll need to make slight modifications to the instructions for using the latest Raspbian image, as opposed to the Debian one. The specific image we utilized was â2012-12-16-wheezy-raspbian.zipâ.
There are various methods to complete this tutorial, depending on your level of comfort working in a console-based environment. Our subject matter expert, for instance, accomplished this tutorial using an SSH connection. Nevertheless, if this approach seems uninteresting to you, it is possible to launch it through the Raspberry Pi console as well as through the LXDE interface. However, youâll require a USB hub, as youâll need to connect three USB peripherals: Such ancillary items as a keyboard, a mouse, and a Bluetooth dongle.
Resources
A Raspberry Pi with all necessary peripherals
A USB Bluetooth dongle
A USB hub â if you want to use a keyboard and mouse at the same time as the Bluetooth dongle
A device to connect the virtual keyboard to â Liam tested it on an Android
smartphone and his Linux computer
Step by step
Step 01
Setting up the Pi
To begin, connect a power cable, network cable, Bluetooth dongle, and USB keyboard to your Raspberry Pi. Youâll also need a screen for the initial setup and configuration. Once youâve completed the initial setup, you can use SSH or a console-based environment to manage your device. If you prefer to use the LXDE environment, youâll need a screen and mouse. During the boot process, Raspbian will display a configuration menu. Select the option to expand the root file system, as we require installing additional packages. Once youâve done that, choose Finish and then confirm the reboot by selecting Yes.
Step 02
Logging into the Pi
A message will be displayed with the current IP address of the Pi during the boot process of Raspbian. This will be useful if you are using SSH, which is enabled by default on the latest Raspbian image. Open up a terminal on your Linux computer and type:
ssh pi@[your Piâs IP address].
Type yes when asked if you want to connect and then enter the password, raspberry. Alternatively, simply type the username pi, and password raspberry at the login screen.
Step 03
Installing the required packages
This may take a while to process, especially the second command.
The first command downloads and updates the package list. The second command installs essential Bluetooth packages, accompanied by Python bindings for them. The final two packages are necessary for installing the evdev Python module, which captures keyboard input, since it is not currently a part of the Raspbian package repositories. Instead, we must ensure that the Python development package is present and employ pip, a Python package manager, to compile the evdev module from its source and then install it.
Step 04
Disabling the default Bluetooth plug-ins
As weâve progressed, weâve successfully installed the Linux Bluetooth stack, BlueZ. Initially, it comes with a vast array of services enabled, which we donât require and could further complicate debugging if issues arise.
The configuration file we need to edit is /etc/Bluetooth/main.conf. You can open that file in the nano editor using the command sudo nano /etc/Bluetooth/main.conf. You need to change the following line:
#DisablePlugins = network,input
toâŠ
DisablePlugins = network,input,audio,pnat,sap,serial
Then save the changes using Ctrl+O, followed by pressing Enter and pressing Ctrl+X. You need to restart the Bluetooth daemon for the changes to take effect. You can do this using:
sudo /etc/init.d/bluetooth restart
You can use
sudo sdptool browse local
to verify that there are no services remaining.
Step 05
Starting the project
Letâs finally begin coding, shall we? To kick-start our project, create a new folder for it. Run the command cd/home/pi to navigate to the desired location. Next, create a folder called PiTooth using themkdir command. Once the folder is created, switch to it and generate a blank project file using the command touch PiTooth.py. To make the file executable, use the commandchmod +x PiTooth.py.
#!/usr/bin/python2.7
This tells the shell that it needs to run using the Python 2.7 interpreter.
You can also add a short comment about what the program does:
#!/usr/bin/python2.7 # # PiTooth allows the Raspberry Pi to act as a Bluetooth keyboard, and relays # keypresses from a USB keyboard to a Bluetooth client. Written by Liam Fraser # for a Linux User & Developer tutorial. #
Step 06
Downloading necessary files
This project necessitates a couple of files. You could create them independently, but it would require reading numerous protocol specifications, similar to what our expert did when developing this tutorial. This falls outside the scope of this tutorial. It will become apparent what these files are used for in due course.
Save the changes to the file that youâve made so far and exit nano. Download the required files using the command wget http:// liamfraser.co.uk/lud/PiToothâ Resources.zip.
Unzip the files from the zip archive using the command unzip PiTooth-Resources.zip. Use ls to verify that the new files have appeared. Once you have done this, you can remove the zip file using the command:
rm PiTooth-Resources.zip pi@raspberrypi ~/PiTooth $ ls keymap.py PiTooth.py sdp_record. xml
Step 07
The imports
Itâs time we return to nano! Our first task is to import the necessary modules, which are plentiful. Weâve provided comments next to the less apparent ones to explain their purposes. SDP denotes Service Discovery Protocol. The utilizes of SDP and D-Bus will become apparent in the near future.
import os # Used to call external commands import sys # Used to exit the script import bluetooth from bluetooth import * import dbus # Used to set up the SDP record import time # Used for pausing the process import evdev # Used to get input from the keyboard from evdev import * import keymap # Used to map evdev input to hid keycodes
Step 08
The Bluetooth class
Our Bluetooth class begins with an initialization function, a familiar starting point. To ensure proper formatting, weâll adhere to the convention of using four spaces for indentation instead of tabs, as spaces are universally consistent across editors. Before proceeding, weâll configure the Bluetooth dongle to function as a keyboard. Weâll accomplish this using the hciconfig command, which serves a purpose similar to ifconfig for Bluetooth devices.
To begin, we will set the device class, a hexadecimal code representative of a keyboard. Next, we will assign the device name as âRaspberry Piâ by replacing the space with a backslash. Finally, we will configure the device to be discoverable, allowing other devices to establish a connection with it.
class Bluetooth: def __init__(self): # Set the device class to a keyboard and set the name os.system(âhciconfig hci0 class 0x002540â) os.system(âhciconfig hci0 name Raspberry\ Piâ) # Make device discoverable os.system(âhciconfig hci0 piscanâ)
Step 09
Bluetooth sockets
Before we create the sockets, weâre going to declare a couple of constants to hold the port numbers. The constants can be put outside of the initialiser function as they will be the same for every instance of the class. The beginning of the Bluetooth class should now look like this:
class Bluetooth: # Define the ports we'll use P_CTRL = 17 P_INTR = 1
def change_state(self, event): evdev_code = ecodes.KEY[event.code] modkey_element = keymap.modkey(evdev_code) if modkey_element > 0: # Need to set one of the modifier bits if self.state[2][modkey_ element] == 0: self.state[2][modkey_element] = 1 else: self.state[2][modkey_element] = 0 else: # Get the hex keycode of the key hex_key = keymap.convert(ecodes.KEY[event.code]) # Loop through elements 4 to 9 of the input report structure for i in range (4, 10): if self.state[i] == hex_key and event.value == 0: # Code is 0 so we need to depress it self.state[i] = 0x00 elif self.state[i] == 0x00 and event.value == 1: # If the current space is empty and the key is being pressed self.state[i] = hex_key break
Step 10
Create the certificates
Now, back in our initialization function, weâre focusing on creating a couple of sockets that will allow us to receive connections from clients. Additionally, we need to associate these sockets with the corresponding ports. In Bluetooth, L2CAP is a type of socket. When binding the sockets to a port, we use a tuple to specify the MAC address of the Bluetooth dongle, which we can omit since weâre working with a single dongle, and the port to bind the socket to. Note that we access the port constants by referencing the class name, rather than using self since the constants are class attributes rather than instance attributes.
# Define our two server sockets for communication self.scontrol = BluetoothSocket(L2CAP) self.sinterrupt = BluetoothSocket(L2CAP) # Bind these sockets to a port self.scontrol.bind((ââ, Bluetooth.P_CTRL)) self.sinterrupt.bind((ââ, Bluetooth.P_INTR))
Step 11
Using D-Bus
On Linux systems, D-Bus is a widely utilized framework that enables the exchange of data and remote procedure calls (invoking functions within different programs) between different processes. Additionally, the BlueZ Bluetooth stack offers a D-Bus interface, providing access to specific capabilities not available through the Python module.
Weâre going to utilize D-Bus to broadcast a Bluetooth SDP record. The SDP acronym represents Service Discovery Protocol. This record promotes the virtual keyboard and provides details about it, such as the layout of input reports (which are sent when a key is pressed) and other features like the language of the keyboard. Youâve already obtained an SDP record crafted by our expert earlier, as these files are largely beyond the scope of this tutorial.
We are wrapping our D-Bus code within a try-except block because it may encounter errors, making it helpful to include error messages. We begin by acquiring an org.bluez.Manager interface and the path to the default adapter. With this, we can obtain the service interface for that adapter, which is utilized for registering SDP records.
# Set up dbus for advertising the service record self.bus = dbus.SystemBus() try: self.manager = dbus.Interface(self.bus.get_object(âorg.bluezâ, â/â), âorg.bluez.Managerâ) adapter_path = self.manager.DefaultAdapter() self.service = dbus.Interface(self.bus.get_object(âorg.bluezâ, adapter_path), âorg.bluez.Serviceâ) except: sys.exit(âCould not configure bluetooth. Is bluetoothd started?â)
STEP 12
Reading the SDP record
Now that weâve set up D-Bus, we need to read the contents of the XML file describing the SDP record into a variable. Again, weâre doing this inside a try-except block in case the file is missing. The sys.path[0] variable contains the path to the directory containing the script:
# Read the service record from file try: fh = open(sys.path[0] + â/sdp_record.xmlâ, ârâ) except: sys.exit(âCould not open the sdp record. Exiting...â) self.service_record = fh.read() fh.close()
STEP 13
Listening for a connection
Weâll now define a listen function, which publishes the SDP record to the SDP server and thereafter hangs in anticipation of a connection from a client device. This device initially connects to the control socket and then subsequently to the interrupt socket. When a connection is accepted, it yields a socket for that connection and a tuple consisting of the clientâs MAC address and the port they utilized to connect. Our expert introduced a couple of constants to facilitate code readability when retrieving the values within the tuple.
class Bluetooth: HOST = 0 # BT Mac address PORT = 1 # Bluetooth Port Number... def listen(self): # Advertise our service record self.service_handle = self. service.AddRecord(self.service_record) print âService record addedâ # Start listening on the server sockets self.scontrol.listen(1) # Limit of 1 connection self.sinterrupt.listen(1) print âWaiting for a connectionâ self.ccontrol, self.cinfo = self.scontrol.accept() print âGot a connection on the control channel from â + self.cinfo[Bluetooth.HOST] self.cinterrupt, self.cinfo = self.sinterrupt.accept() print âGot a connection on the interrupt channel from â + self.cinfo[Bluetooth.HOST]
STEP 14
Adding the main function
We now have fundamental code in place which enables the acceptance of connections from devices. It will momentarily disregard those connections, but this doesnât concern us yet. Our plan is to develop a primary function at the bottom of the file to instantiate the Bluetooth class and then invoke the listen function to await a connection. Inside the primary function, we will ascertain whether the user initiating the code possesses root privileges. If not, itâs unnecessary to proceed as the script lacks functionality in this situation.
if __name__ == â__main__â: # We can only run as root if not os.geteuid() == 0: sys.exit(âOnly root can run this scriptâ) bt = Bluetooth() bt.listen()
STEP 15
Pairing with the clientâs device
Before accepting connections from client devices, we must initially pair with them. For this process, we will adhere to an Android device. Since itâs beneficial to broadcast the virtual keyboardâs SDP record concurrently, weâre now prompted to open an additional shell on the Raspberry Pi. You can achieve this by opening another SSH session or, if youâre utilizing the Pi with a keyboard and screen connected, pressing Ctrl+Alt+F2 to navigate to the next console (pressing Ctrl+Alt+F1 will return you to the first console).
To proceed, initialize PiTooth in the primary shell by utilizing the sudo./PiTooth command. This will confirm that the service record has been added and that it is expecting a connection from a client device. At this juncture, head to the Bluetooth settings on your client device and configure it to be discoverable. Next, navigate to the secondary shell. Upon entering this shell, execute the hcitool scan command, which will divulge the MAC address of the device you intend to pair with. Once youâve located this address, use the bluez-simple-agent hci0 [client MAC address] command, followed by entering a PIN and subsequently verifying that PIN on your client device. At this stage, you should now be paired with the Raspberry Pi.
Note that if you ever have to unpair your client device, you can use the command bluez-test-device remove [client MAC address].
STEP 16
Testing the code so far
We might as well test the code we have so far while itâs waiting for a connection. Switch back to your other shell and then scroll down to the Raspberry Pi entry on your client device. Select the Connect option. Note that our expert sometimes had to reset the Bluetooth on his clientâs device by turning it off and on again because it got upset when the code simply dropped the connections as soon as it connected. Our output looked like this:
pi@raspberrypi ~/PiTooth $ sudo ./ PiTooth.py Service record added Waiting for a connection Got a connection on the control channel from [Client MAC] Got a connection on the interrupt channel from [Client MAC]
STEP 17
Input reports
It will be useful to explore input reports before we begin coding the Keyboard class. At the top of this page, there is a table describing an input report from the Bluetooth HID specification. An input report has a length of 9 bytes and is prefixed with a byte that signifies the subsequent bytes as an input report. The initial byte containing Report ID consistently carries a value of 0Ă01, which is the standard value for a keyboard input report.
Understanding the intricacies of binary data will be advantageous before we proceed. A byte is comprised of eight distinct bits which can assume a value of either 1 or 0. A byte is capable of representing a numerical value (in decimal) of up to 255. Any sequence in the form of 0Ă is a hexadecimal value. For instance, 0Ă01 is equivalent to 00000001 in binary and has a decimal value of 1. The size of a character, for instance, the letter A, is also equivalent to a byte. This information will prove useful shortly when we need to convert between various data forms.
The subsequent byte specifies the status of modifier keys, which comprises Ctrl, Alt, Shift, and Windows key. Each bit corresponding to that key is set to 1 if the key is being pressed, and 0 if it is not being depressed. The third byte remains reserved, and the subsequent bytes are used to accommodate up to six key events.
STEP 18
The Keyboard class
Weâre going to start by defining the structure of an input report, with all of the keys turned off by default. Then we loop until we can get a keyboard device.
class Keyboard(): def __init__(self): # The structure for an bt keyboard input report (size is 10 bytes) self.state = [ 0xA1, # This is an input report 0x01, # Usage report = Keyboard # Bit array for Modifier keys [0, # Right GUI - (usually the Windows key) 0, # Right ALT 0, # Right Shift 0, # Right Control 0, # Left GUI - (again, usually the Windows key) 0, # Left ALT 0, # Left Shift 0], # Left Control 0x00, # Vendor reserved 0x00, # Rest is space for 6 keys 0x00, 0x00, 0x00, 0x00, 0x00 ] # Keep trying to get a keyboard have_dev = False while have_dev == False: try: # Try and get a keyboard - should always be event0 as weâre only # plugging one thing in self.dev = InputDevice(â/dev/input/event0â) have_dev = True except OSError: print âKeyboard not found, waiting 3 seconds and retrying" time.sleep(3) print "Found a keyboard"
STEP 19
Changing the keyboardâs state
Before proceeding, you will find it advantageous to briefly examine the keymap.py document supplied by our expert. This file consists of a dictionary mapping evdev keys to Bluetooth keys along with mapping evdev modifier keys to the bit that needs to be set in Input Report. The Bluetooth values are represented in decimal rather than hexadecimal, but this does not pose an issue since Python can effortlessly convert hexadecimal to decimal.
This function processes an evdev event and updates the keyboardâs state. Initially, we determine if the key is a modifier key and return a negative value if itâs not a modifier. If it is a modifier, we toggle the corresponding bit in the Input Reportâs bit array. If not, we retrieve the Bluetooth code for the key and iterate through the key elements. When the key is released (indicated by an event value of 0), we locate the key and set its state to depressed by resetting its value to zero. If the key is being pressed, we assign its value to the first available position and exit the loop to maintain efficiency.
def change_state(self, event): evdev_code = ecodes.KEY[event.code] modkey_element = keymap.modkey(evdev_code) if modkey_element > 0: # Need to set one of the modifier bits if self.state[2][modkey_ element] == 0: self.state[2][modkey_element] = 1 else: self.state[2][modkey_element] = 0 else: # Get the hex keycode of the key hex_key = keymap.convert(ecodes.KEY[event.code]) # Loop through elements 4 to 9 of the input report structure for i in range (4, 10): if self.state[i] == hex_key and event.value == 0: # Code is 0 so we need to depress it self.state[i] = 0x00 elif self.state[i] == 0x00 and event.value == 1: # If the current space is empty and the key is being pressed self.state[i] = hex_key break
STEP 20
The event loop
The event loopâs operation is strikingly straightforward; it accepts an instance of the Bluetooth class as input and continually captures keyboard input. Notably, evdev keys exhibit three states: press, depress, and hold. We focus exclusively on press and depress states, since the Bluetooth protocol does not necessitate handling hold states. It is implicitly assumed that a key is being held if its value does not return to zero. Consequently, we overlook hold events using the condition âevent. value < 2â.
Having cleared the initial stages, we now seamlessly forward the event to the function responsible for updating the keyboardâs state. Subsequently, we invoke the send_input method of the Bluetooth class, which we will shortly develop.
def event_loop(self, bt): for event in self.dev.read_loop(): # Only bother if we hit a key and itâs an up or down event if event.type == ecodes. EV_KEY and event.value < 2: self.change_state(event) bt.send_input(self.state)
STEP 21
Sending input
Reread the Bluetooth class and craft the send_input function. While it might seem intricate, this function essentially translates the Input Report from the Keyboard class into a sendable string compatible with the Python Bluetooth module.
So, we take an Input Report as input and initialize an empty string to build and send. We then iterate through each element in the report, checking if itâs an array. If it is, it represents the modifier byte, which we convert to a string of eight binary characters by iterating over each element and appending it to the `bin_str`. We then convert this binary data to an integer in base 2, and then to a single-byte character that can be added to the `hex_str`. If itâs not an array, we directly convert the element to a character and append it to the `hex_str`. Once the `hex_str` is complete, we send it through the interrupt channel.
def send_input(self, ir): # Convert the hex array to a string hex_str = ââ for element in ir: if type(element) is list: # This is our bit array - convrt it to a single byte represented # as a char bin_str = ââ for bit in element: bin_str += str(bit) hex_str += chr(int(bin_str, 2)) else: # This is a hex value - we can convert it straight to a char hex_str += chr(element) # Send an input report self.cinterrupt.send(hex_str)
STEP 22
The final piece
We need to finish off our code by initialising the Keyboard class and calling the event loop function, passing through the instance of the Bluetooth class.
kb = Keyboard() kb.event_loop(bt)
STEP 23
Testing the code
Thatâs the final piece! While this implementation is quite basic, the idea has been successfully demonstrated. The performance could be optimized further, but for now, the concept is established. Weâre ignoring the control channel and any Output Reports that could potentially control LEDs on the keyboard. To use this code, simply run it and start typing on the keyboard to send input to the client. Be prepared to see some nonsensical text in the console if youâre using the Pi with a screen.