2

I'm connecting to a custom BLE enabled board. Running bluetoothctl, I can pair and connect no problem from the command line.

I've been attempting to script it using Python and pexpect. Pexpect spawns a bluetoothctl instance, and then sends commands to and reads info from the bluetoothctl process.

I can interact with this process no problem, until I issue the command to connect to BLE peripheral. As soon as it connect, I can no longer interact with the process. Any attempts to read info from it time out, sending it commands has no effect.

Pexpect usage is based on this guys script.

Here's what my code looks like:

class Bluetoothctl:
    """A wrapper for bluetoothctl utility."""
    def __init__(self):
        subprocess.check_output("rfkill unblock bluetooth", shell=True)
        self.process = pexpect.spawnu("bluetoothctl", echo=False)

    def send(self, command, pause=0):
        self.process.send(f"{command}\n")
        time.sleep(pause)
        if self.process.expect(["bluetooth", pexpect.EOF]):
            raise Exception(f"failed after {command}")

    def get_output(self, *args, **kwargs):
        """Run a command in bluetoothctl prompt, return output as a list of lines."""
        self.send(*args, **kwargs)
        return self.process.before.split("\r\n")

    def get_device_info(self, mac_address):
        """Get device info by mac address."""
        try:
            out = self.get_output(f"info {mac_address}")
        except Exception as e:
            logger.error(e)
            return False
        else:
            return out

    def connect(self, mac_address):
        """Try to connect to a device by mac address."""
        try:
            self.send(f"connect {mac_address}", 2)
        except Exception as e:
            logger.error(e)
            return False
        else:
            res = self.process.expect(
                ["Failed to connect", "Connection successful", pexpect.EOF]
            )
            return res == 1

bl = Bluetoothctl()
print( bl.get_device_info( [mac] ) ) // THIS RETURNS INFO SUCCESSFULLY
bl.connect( [mac] )                  // CONNECTS SUCESSFULLY
print( bl.get_device_info( [mac] ) ) // This fails by timing out

So before I connect, I get the device info, then I connect successfully, then I try the same command to get device info, and it fails (returns no response after 30 seconds)

What's going wrong? How can I find out more info about this? Thanks.

3

I don't think bluetoothctl was ever intended to work in this way. While I am sure you could get it to work, BlueZ are often changing the commands in bluetoothctl so your script is likely to break when newer versions on bluetoothctl are released.

A more programmatic way of doing this is using the documented DBus API.

https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt

https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt

https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt

It looks like you want to do the Central Role and so there is the pydbus library which will help you greatly: https://pypi.org/project/pydbus/

Some useful things to know to get you started:

  1. The Dbus service for bluez is called 'org.bluez'
  2. The Bluetooth adapter on a Raspberry Pi is normally '/org/bluez/hci0' DBus object path.
  3. The DBus Object path to a device is the adapter path plus the mac address prepended by 'dev_' and the semi-colons replaced with underscores. i.e. 'DE:82:35:E7:43:BE' would be found at '/org/bluez/hci0/dev_DE_82_35_E7_43_BE'
import pydbus
bus = pydbus.SystemBus()
adapter = bus.get('org.bluez', '/org/bluez/hci0')
dev = bus.get('org.bluez', '/org/bluez/hci0/dev_DE_82_35_E7_43_BE')
dev.Connect()
# Get commands and properties available
print(dir(adapter))
print(dir(dev))

To read and write you need to find the path to the characteristic which is a little more work. Typically you know the UUID of the characteristic you are interested in so to get the value from button A on a BBC micro:bit it would be:

mngr = bus.get('org.bluez', '/')

def get_characteristic_path(device_path, uuid):
    mng_objs = mngr.GetManagedObjects()
    for path in mng_objs:
        chr_uuid = mng_objs[path].get('org.bluez.GattCharacteristic1', {}).get('UUID')
        if path.startswith(device_path) and chr_uuid == uuid:
           return path

char_path = get_characteristic_path(dev._path, 'e95dda90-251d-470a-a062-fa1922dfa9a8')
btn = bus.get('org.bluez', char_path)
btn.ReadValue({})
# [0]

Writing to a characteristic is similar. Here is an example of reading and writing to the Temperature Period on a BBC micro:bit

tmp_period_path = get_characteristic_path(dev._path, 'e95d1b25-251d-470a-a062-fa1922dfa9a8')
tmp_period = bus.get('org.bluez', tmp_period_path)
tmp_period.ReadValue({})
# Result is:
# [232, 3]
# To get it as an integer:
int.from_bytes(tmp_period.ReadValue({}), byteorder='little')
# 1000

# To write and new value of 1500
new_value = 1500
tmp_period.WriteValue(new_value.to_bytes(2, byteorder='little'), {})
tmp_period.ReadValue({})
# [220, 5]
dev.Disconnect()

Hope that is helpful.

9

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.