Emulate a Bluetooth keyboard with the Raspberry Pi

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

Emulate a Bluetooth keyboard with the Raspberry Pi

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/PiToothResources.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

To facilitate communication, Bluetooth HID devices employ two sockets: a control socket and an interrupt socket. The control socket primarily handles tasks such as establishing connections, exchanging handshakes, and initiating the communication process. In contrast, the interrupt socket is responsible for transmitting low-latency data, including input reports featuring key presses or output reports illustrating vibration feedback – essential features for gamepads, for instance. Traditionally, these sockets operate on standard ports: 17 for the control socket and 19 for the interrupt socket.

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.

Source: Emulate a Bluetooth keyboard with the Raspberry Pi


About The Author

Ibrar Ayyub

I am an experienced technical writer holding a Master's degree in computer science from BZU Multan, Pakistan University. With a background spanning various industries, particularly in home automation and engineering, I have honed my skills in crafting clear and concise content. Proficient in leveraging infographics and diagrams, I strive to simplify complex concepts for readers. My strength lies in thorough research and presenting information in a structured and logical format.

Follow Us:
LinkedinTwitter

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top