0

I'm currently using pexpect in python to use bluetoothctl. I can get a list of all bluetooth devices, but is there a way to narrow down the list to only audio devices (bluetooth speakers, headphones).

Here is my code so far:

import time
import pexpect

#returns list of all availible bluetooth devices
def scan_bluetooth_agents():
    devices = {}

    child = pexpect.spawn('sudo bluetoothctl')
    child.sendline ('scan on')
    time.sleep(10)
    child.sendline ('scan off')

    line = child.readline()
    while b'scan off' not in line:
    if b'Device' in line:
            line = str(line.replace(b"\r\n", b'')).strip("b'").strip("'")
        address, name = line.split('Device ')[1].split(' ', 1)
        devices[name] = address

    line = child.readline()
    child.sendline ('exit')

    return devices

#this is the function that needs to return only audio devices
def valid_devices(devices):
    output = {}
    i = 0
    for device in devices:
        #if device contrains 'JBL' then output[i] = device

if __name__ == "__main__":
    devices = scan_bluetooth_agents()
    print('All Devices:')
    print(devices)
    print('Valid Audio Devices:')
    audioDevices = valid_devices(devices)
1
  • how do you know the difference when you look at the list of devices? ... write the program to make the same distinction
    – jsotola
    Commented Dec 31, 2019 at 1:09

2 Answers 2

0

I don't think bluetoothctl was ever intended to work in this way. While I am sure you could get it to work, the BlueZ developers are free to change the commands in bluetoothctl so your script is likely to break for future versions of bluetoothctl.

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

The pydbus library is very helpful for accessing the BlueZ DBus API: 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 default Bluetooth adapter normally has '/org/bluez/hci0' as its DBus object path.
  3. BlueZ/DBus has an object manager which stores information about devices

To tell if it is an audio device, I would be tempted to look at the services the device is advertising. A list of these service UUIDs is available at: https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/

I did the following script to test the idea:

import pydbus
from gi.repository import GLib

discovery_time = 20

bus = pydbus.SystemBus()
mainloop = GLib.MainLoop()

# Connect to the DBus api for the Bluetooth adapter
adapter = bus.get('org.bluez', '/org/bluez/hci0')

def end_discovery():
    """Handler for end of discovery"""
    mainloop.quit()
    adapter.StopDiscovery()

# Run discovery
adapter.StartDiscovery()
GLib.timeout_add_seconds(discovery_time, end_discovery)
print('Finding nearby devices...')
mainloop.run()

# Iterate around the devices to find audio devices
mngr = bus.get('org.bluez', '/')
mng_objs = mngr.GetManagedObjects()

for path in mng_objs:
    uuids = mng_objs[path].get('org.bluez.Device1', {}).get('UUIDs', [])
    # print(path, uuids)
    for uuid in uuids:
        # Service discovery UUIDs
        # https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/
        # AudioSink - 0x110B - Advanced Audio Distribution Profile (A2DP)
        if uuid.startswith('0000110b'):
            print(mng_objs[path].get('org.bluez.Device1', {}).get('Name'),
                  mng_objs[path].get('org.bluez.Device1', {}).get('Address'))

This found a bluetooth speaker that ws in pairing mode. It will also report any devices that you have already in the list of known devices even if they are not in range.

(venv) pi@raspberrypi:~ $ python audio_devices.py 
Jabra SOLEMATE v1.27.0 50:C9:71:67:41:CD
0

Based on your code, I'm thinking about:

import time
import pexpect

# Returns a list of all available Bluetooth devices
def scan_bluetooth_agents():
    devices = {}

    child = pexpect.spawn('sudo bluetoothctl')
    child.sendline ('scan on')
    time.sleep(10)
    child.sendline ('scan off')

    line = child.readline()
    while b'scan off' not in line:
        if b'Device' in line:
            line = str(line.replace(b"\r\n", b'')).strip("b'").strip("'")
            address, name = line.split('Device ')[1].split(' ', 1)
            devices[name] = address

        line = child.readline()
    child.sendline ('exit')

    return devices

# Returns a dictionary of audio devices only
def valid_devices(devices):
    audio_devices = {}
    i = 0
    for device in devices:
        child = pexpect.spawn(f'sudo bluetoothctl info {devices[device]}')
        child.expect('Connected: yes', timeout=5)
        output = child.before.decode()
        if 'Class: 0x240414' in output: # Check if the device class is audio (0x240414)
            audio_devices[device] = devices[device]
        child.sendline ('exit')

    return audio_devices

if __name__ == "__main__":
    devices = scan_bluetooth_agents()
    print('All Devices:')
    print(devices)
    print('Valid Audio Devices:')
    audio_devices = valid_devices(devices)
    print(audio_devices)

The valid_devices() function takes the dictionary of Bluetooth devices as input and returns a new dictionary that only contains audio devices. To determine whether a device is an audio device, the function invokes bluetoothctl with the info command and checks the device's class. Audio devices typically have a class of 0x240414.

Note that this solution assumes that the bluetoothctl command is available and that the devices are paired and connected. Also, the device class check may not be sufficient to identify all audio devices and may need to be modified based on the specific Bluetooth devices you're using.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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