Skip to Main Content

Testing Bluetooth Low Energy Peripherals using a Python Script

When developing a Bluetooth Low Energy peripheral, it’s often useful to have an app that can connect to and test the peripheral to ensure it’s working as expected. If, however, you don’t already have a companion app for the device, you’d either have to write one from scratch, or use an existing app, such as our flagship mobile app LightBlue® (available on Android and iOS).  

LightBlue® allows you to connect to Bluetooth Low Energy devices, read and write to characteristics, subscribe to notifications, and a lot more. Though it’s a powerful tool, you’ll probably need a way to automate more complex tests for long-term development. This is a situation where we can leverage the rapid prototyping capabilities of Python and write a script that emulates a BLE client.

As luck would have it, we recently worked on a device that had a flaky BLE connection with its companion mobile app. We’ll break down how a Python script and Adafruit’s BluetoothLE library was used to test the robustness of the BLE connections, and figure out whether the source of the issue was the mobile app or the device itself. 

The nice thing about Adafruit’s BluefruitLE library is that it works on both Linux and macOS. Under the hood, it uses BlueZ on Linux and CoreBluetooth on macOS, but it abstracts away all platform-specific BLE code behind the API. We should also note that this library does not support Windows. For Windows development, an alternative could be to use PyGatt with a BlueGiga dongle.

Getting started

Follow the instructions on Adafruit’s website to install the library. The library uses Apple’s PyObjC Python library to interact with CoreBluetooth. Apple includes this library with the version of Python installed with macOS. If you’re on a Mac and using a non-Apple version of Python (like the one installed with Homebrew), or running Python in a virtual environment, you may need to manually install PyObjC.

Writing the testing tool

For demonstration purposes, instead of testing with an actual Bluetooth Low Energy peripheral, We’ll show you how to use LightBlue® to create a “virtual” peripheral. It’s a neat little feature — though currently available only on iOS. 

  • Launch the LightBlue® app and go to the “Virtual Devices” tab. Tap “+” on the top right to see the list of available GATT profiles that the app can emulate. Then, select the “Blank” profile and tap Save:
New Virtual Peripheral Screen Shot
  • Open the “Blank” peripheral we just created. Tap on the characteristic 0x2222, and make the characteristic writeable by tapping Read under the Property section and selecting Write from the list (both Read and Write should be checked). 
  • Return to the characteristic details screen and confirm both Read and Write appear under the list of properties.
Characteristic Properties Screen Shot

Now that we have our virtual peripheral running, let’s write our BLE client to test it!

Testing the Script

Adafruit’s BluefruitLE library contains sample scripts, which are a good starting point to understand how to use the library. We expanded their low_level.py script to build the test client. It was pretty straightforward to use, other than an issue we had when trying to write to characteristics. Apparently the write_value function expects a byte array (rather than a String).

The script below will scan to discover the virtual peripheral we just created, connect to it, write a random value to the characteristic, and then attempt to read it. If the write operation works, the read value should match the value written. The script keeps track of operations that failed and shows the results at the end. The main loop repeats these operations a number of times.

import time
import uuid
import random
import Adafruit_BluefruitLE
 
DEVICE_NAME = "Blank"
 
# Define service and characteristic UUIDs used by the peripheral.
SERVICE_UUID = uuid.UUID('00001111-0000-1000-8000-00805F9B34FB')
TX_CHAR_UUID = uuid.UUID('00002222-0000-1000-8000-00805F9B34FB')
RX_CHAR_UUID = uuid.UUID('00002222-0000-1000-8000-00805F9B34FB')
 
# Get the BLE provider for the current platform.
ble = Adafruit_BluefruitLE.get_provider()
 
 
def scan_for_peripheral(adapter):
    """Scan for BLE peripheral and return device if found"""
    print('  Searching for device...')
    try:
        adapter.start_scan()
        # Scan for the peripheral (will time out after 60 seconds
        # but you can specify an optional timeout_sec parameter to change it).
        device = ble.find_device(name=DEVICE_NAME)
        if device is None:
            raise RuntimeError('Failed to find device!')
        return device
    finally:
        # Make sure scanning is stopped before exiting.
        adapter.stop_scan()
 
 
def sleep_random(min_ms=1, max_ms=1000):
    """Add a random sleep interval between 1ms to 1000ms"""
    duration_sec = random.randrange(min_ms, max_ms)/1000
    print('   Sleeping for ' + str(duration_sec) + 'sec')
    time.sleep(duration_sec)
 
 
def main():
    """Main loop to process BLE events"""
    test_iteration = 0
    echo_mismatch_count = 0
    misc_error_count = 0
 
    # Clear any cached data because both BlueZ and CoreBluetooth have issues with
    # caching data and it going stale.
    ble.clear_cached_data()
 
    # Get the first available BLE network adapter and make sure it's powered on.
    adapter = ble.get_default_adapter()
    try:
        adapter.power_on()
        print('Using adapter: {0}'.format(adapter.name))
 
        # This loop contains the main logic for testing the BLE peripheral.
        # We scan and connect to the peripheral, discover services,
        # read/write to characteristics, and keep track of errors.
        # This test repeats 10 times.
        while test_iteration < 10:
            connected_to_peripheral = False
 
            while not connected_to_peripheral:
                try:
                    peripheral = scan_for_peripheral(adapter)
                    peripheral.connect(timeout_sec=10)
                    connected_to_peripheral = True
                    test_iteration += 1
                    print('-- Test iteration #{} --'.format(test_iteration))
                except BaseException as e:
                    print("Connection failed: " + str(e))
                    time.sleep(1)
                    print("Retrying...")
 
            try:
                print('  Discovering services and characteristics...')
                peripheral.discover([SERVICE_UUID], [TX_CHAR_UUID, RX_CHAR_UUID])
 
                # Find the service and its characteristics
                service = peripheral.find_service(SERVICE_UUID)
                tx = service.find_characteristic(TX_CHAR_UUID)
                rx = service.find_characteristic(RX_CHAR_UUID)
 
                # Randomize the intervals between different operations
                # to simulate user-triggered BLE actions.
                sleep_random(1, 1000)
 
                # Write random value to characteristic.
                write_val = bytearray([random.randint(1, 255)])
                print('  Writing ' + str(write_val) + ' to the write char')
                tx.write_value(write_val)
 
                sleep_random(1, 1000)
 
                # Read characteristic and make sure it matches the value written.
                read_val = rx.read_value()
                print('  Read ' + str(read_val) + ' from the read char')
                if write_val != read_val:
                    echo_mismatch_count = echo_mismatch_count + 1
                    print('  Read value does not match value written')
 
                peripheral.disconnect()
                sleep_random(1, 1000)
            except BaseException as e:
                misc_error_count = misc_error_count + 1
                print('Unexpected error: ' + str(e))
                print('Current error count: ' + str(misc_error_count))
                time.sleep(1)
                print('Retrying...')
   
 finally:
        # Disconnect device on exit.
        peripheral.disconnect
        print('\nConnection count: ' + str(test_iteration))
        print('Echo mismatch count: ' + str(echo_mismatch_count))
        print('Misc error count: ' + str(misc_error_count))
 
 
# Initialize the BLE system.  MUST be called before other BLE calls!
ble.initialize()
 
# Start the mainloop to process BLE events, and run the provided function in
# a background thread.  When the provided main function stops running, returns
# an integer status code, or throws an error the program will exit.
ble.run_mainloop_with(main)

The above script demonstrates a simple echo test for a peripheral that can easily be expanded. You should now have a good idea of how to get started using Python and Adafruit’s BluefruitLE library to test Bluetooth Low Energy peripherals more quickly.

Happy Pythoning!

Interested in Learning More?

Learn more about the makers of LightBlue®–what we do, why we do it, and learn about our culture and values.