Search code examples
pythonbluetoothraspberry-pihidbluez

Preventing RPi 4 from connecting through a2dp protocol


A little bit of background, I want to use my Pi to emulate a bluetooth keyboard for my phone. I have actually managed to get it to work with my phone but I'm having a hard time with automatic connection.

I want the pi to scan nearby devices and initiate connection with already paired devices but for some reason it doesn't work.

If my pi is connected to a screen (my screen has a built in speaker) than for some reason automatic connection only connects to audio profiles and I have to manually connect to the HID profile, if my pi is not connected to a screen it just can't connect at all and I'm getting

org.bluez.Error.Failed: Protocol not available

when I'm trying to view the bluetooth status using sudo systemctl status bluetooth I get a hint about what protocol is missing.. this is the output:

● bluetooth.service - Bluetooth service
   Loaded: loaded (/lib/systemd/system/bluetooth.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2021-08-11 11:43:46 IDT; 52min ago
     Docs: man:bluetoothd(8)
 Main PID: 627 (bluetoothd)
   Status: "Running"
    Tasks: 1 (limit: 4915)
   CGroup: /system.slice/bluetooth.service
           └─627 /usr/lib/bluetooth/bluetoothd -P input

Aug 11 11:57:37 raspberrypi bluetoothd[627]: a2dp-source profile connect failed for {DEVICE MAC}: Protocol not available
Aug 11 12:00:53 raspberrypi bluetoothd[627]: Endpoint registered: sender=:1.96 path=/MediaEndpoint/A2DPSource
Aug 11 12:00:53 raspberrypi bluetoothd[627]: Endpoint registered: sender=:1.96 path=/MediaEndpoint/A2DPSink
Aug 11 12:01:10 raspberrypi bluetoothd[627]: Endpoint unregistered: sender=:1.96 path=/MediaEndpoint/A2DPSource
Aug 11 12:01:10 raspberrypi bluetoothd[627]: Endpoint unregistered: sender=:1.96 path=/MediaEndpoint/A2DPSink
Aug 11 12:04:43 raspberrypi bluetoothd[627]: a2dp-source profile connect failed for {DEVICE MAC}: Protocol not available
Aug 11 12:05:02 raspberrypi bluetoothd[627]: a2dp-source profile connect failed for {DEVICE MAC}: Protocol not available
Aug 11 12:28:12 raspberrypi bluetoothd[627]: a2dp-source profile connect failed for {DEVICE MAC}: Protocol not available
Aug 11 12:28:27 raspberrypi bluetoothd[627]: a2dp-source profile connect failed for {DEVICE MAC}: Protocol not available
Aug 11 12:28:40 raspberrypi bluetoothd[627]: a2dp-source profile connect failed for {DEVICE MAC}: Protocol not available

I feel like I've tried everything at this point, the lack of online documentation/information is really frustrating.

I have even purged pulseaudio completely from my device in hopes that it would stop trying to connect to audio profiles but it didn't work.

Here is some of the code that I think is relevant:

The device class

class BTKbDevice:

    """This class is used to define the bluetooth controller properties and capabilities"""

def __init__(self):
            
        # Set up device
        system_helper.init_device()

        # log periodical scan results
        ScanLogHelper().run()

        # Declare class fields
        self.server_control_port = None
        self.server_interrupt_port = None
        self.client_control_port = None
        self.client_interrupt_port = None

        # define some constants
        self.__CONTROL_PORT = 17  # Service port - must match port configured in SDP record
        self.__INTERRUPTION_PORT = 19  # Interrupt port - must match port configured in SDP record
        self.__CURRENTLY_CONNECTED_DEVICE = ""  # Used for logging connection/disconnection events

        print("device started")

    # listen for incoming client connections
    def listen(self):

        # We are not using BluetoothSocket constructor to have access to setsockopt method later
        # instead we use the native socket equivalent
        self.server_control_port = socket.socket(
            socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)  # BluetoothSocket(L2CAP)
        self.server_interrupt_port = socket.socket(
            socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)  # BluetoothSocket(L2CAP)

        # This allows the system to reuse the same port for different connections
        # this is useful for situations where for some reason the port wasn't closed properly
        # i.e. crashes, keyboard interrupts etc.
        self.server_control_port.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_interrupt_port.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # bind these sockets to a port
        # use BDADDR_ANY because we are only really interested in defining a constant port
        self.server_control_port.bind((socket.BDADDR_ANY, self.__CONTROL_PORT))
        self.server_interrupt_port.bind((socket.BDADDR_ANY, self.__INTERRUPTION_PORT))

        # Start listening on the server sockets
        self.server_control_port.listen(1)
        self.server_interrupt_port.listen(1)

        # Wait for connections
        # the accept() method will block code execution until a connection was established
        self.client_control_port, client_information = self.server_control_port.accept()
        self.client_interrupt_port, client_information = self.server_interrupt_port.accept()

        # We need to remember the connected device for disconnection logging
        # client_information[0] is device's mac address
        self.__CURRENTLY_CONNECTED_DEVICE = client_information[0]

    def device_disconnected(self):
        self.__CURRENTLY_CONNECTED_DEVICE = ""

    def is_currently_connected_exists(self):
        return self.__CURRENTLY_CONNECTED_DEVICE != ""

    def get_currently_connected_device(self):
        return self.__CURRENTLY_CONNECTED_DEVICE

    # Cleanup
    def close_connections(self):
        self.server_control_port.close()
        self.server_interrupt_port.close()
        self.client_control_port.close()
        self.client_interrupt_port.close()

The service class

class BTKbService(dbus.service.Object):

    def __init__(self):

        # set up as a dbus service
        bus_name = dbus.service.BusName(
            "org.thanhle.btkbservice", bus=dbus.SystemBus())
        dbus.service.Object.__init__(
            self, bus_name, "/org/thanhle/btkbservice")

        print("service started. starting device")

        # create and setup our device
        self.device = BTKbDevice()

        # start listening for connections
        self.device.listen()

system_helper.py

"""A utility for handling system related operations and events"""

UUID = "00001124-0000-1000-8000-00805f9b34fb"


# Get available bluetooth devices list from system
def get_controllers_info():
    return subprocess.getoutput("hcitool dev")


# Check if our device is available
def is_controller_available():
    device_data = get_controllers_info()
    return const.MY_ADDRESS in device_data.split()


# Handle device initialization
def init_device():
    __init_hardware()
    __init_bluez_profile()


# Configure the bluetooth hardware device
def __init_hardware():

    # Reset everything to make sure there are no problems
    os.system("hciconfig hci0 down")
    os.system("systemctl daemon-reload")
    os.system("/etc/init.d/bluetooth start")

    # Activate device and set device name
    os.system("hciconfig hci0 up")
    os.system("hciconfig hci0 name " + const.MY_DEV_NAME)

    # make the device discoverable
    os.system("hciconfig hci0 piscan")


# set up a bluez profile to advertise device capabilities from a loaded service record
def __init_bluez_profile():
    # read and return an sdp record from a file
    service_record = __read_sdp_service_record()

    # setup profile options
    opts = {
        "AutoConnect": True,
        "RequireAuthorization": False,
        "ServiceRecord": service_record
    }

    # retrieve a proxy for the bluez profile interface
    bus = dbus.SystemBus()
    manager = dbus.Interface(bus.get_object(
        "org.bluez", "/org/bluez"), "org.bluez.ProfileManager1")
    manager.RegisterProfile("/org/bluez/hci0", UUID, opts)

    # Set device class
    os.system("hciconfig hci0 class 0x0025C0")

def __read_sdp_service_record():
    try:
        fh = open(const.SDP_RECORD_PATH, "r")
    except OSError:
        sys.exit("Could not open the sdp record. Exiting...")
    return fh.read()


def get_connected_devices_data():
    return subprocess.getoutput("hcitool con")


def get_paired_devices_data():
    return subprocess.getoutput("bluetoothctl paired-devices")

connection_helper.py The method initiate_connection_with_devices_in_range get's called every 10 seconds or so by another file that isn't relevant to this problem.


""" Responsible for finding paired devices in range and attempting connection with them """


def initiate_connection_with_devices_in_range(nearby_devices):
    
    print("init connection started")
    
    # Get paired devices from system
    paired_devices = system_helper.get_paired_devices_data()
    
    print("Paired device:\n" + paired_devices)

    # Check nearby devices for a match
    # no need to request data from bus if no paired device is available
    for device in nearby_devices:
        mac, name = device
        print("checking for device " + name + " " + mac)
        if mac in paired_devices.split():
            print(name + " is paired, let's attempt connection")
            # Paired device found, try to connect
            __attempt_connection()


def __attempt_connection():
    
    print("attempting connection")
    
    # Get reference for the bus object, and for the objects it manages
    bus = dbus.SystemBus()
    manager = dbus.Interface(bus.get_object("org.bluez", "/"),
                             "org.freedesktop.DBus.ObjectManager")
    objects = manager.GetManagedObjects()

    # Extract device objects from bus
    all_devices = (str(path) for path, interfaces in objects.items() if
                   "org.bluez.Device1" in interfaces.keys())

    # Extract only devices managed by our adapter
    device_list = None
    for path, interfaces in objects.items():
        if "org.bluez.Adapter1" not in interfaces.keys():
            continue
        device_list = [d for d in all_devices if d.startswith(path + "/")]

    if device_list is not None:
        
        print(device_list)
        # Devices found, attempt connection
        for dev_path in device_list:
            print("trying to connect keyboard profile with " + dev_path)
            dev_obj = bus.get_object('org.bluez', dev_path)
            methods = dbus.Interface(dev_obj, 'org.bluez.Device1')
            props = dbus.Interface(dev_obj, dbus.PROPERTIES_IFACE)
            try:
                methods.Connect()
                
            except Exception as e:
                print("Exception caught in connect method! {}".format(e))
                # this actually print Exception caught in connect method! org.bluez.Error.Failed: Protocol not available

If I manually connect from my phone it works just fine, only automatic connection is problematic at the moment.

ANY help would be appreciated, most of what I did so far is the result of trial and error so it's possible that I made a mistake somewhere

I think that at the moment the pi "wants" to connect as an audio device but lacks ability to do so as it is not connected to any hardware that will allow it.. So somehow I need to make it "forget" about the audio profiles.

I would gladly provide more information if needed.


Solution

  • This does seem a little back to front as you have the peripheral trying to connect to the phone. Have you looked at using ConnectProfile rather than Connect?

    If you want to have the HID device initiating connection (or reconnection) to HID host (RPi reconnects to your phone), then that is a change in the SDP record. More information at the following gist