python-bluezero icon indicating copy to clipboard operation
python-bluezero copied to clipboard

Parallel bluetooth low energy advertising and scanning for beacons

Open Severon96 opened this issue 2 years ago • 3 comments

Hello ukBaz!

First many thanks for this great library!

But sadly I have some issues at the moment, when advertising as a ble peripheral and starting a subprocess which searches for iBeacons.

My code looks roughly like that:

from bluezero import peripheral
from bluezero import adapter
from bluetooth.bluetoothdevice import BluetoothDevice
from logsys.logger import Logger
import config
import subprocess


class BluetoothController:

    def __init__(self):
        self.adapter_address = list(adapter.Adapter.available())[0].address
        self.ble_uart = peripheral.Peripheral(adapter_address=self.adapter_address,
                                              local_name=config.BLUETOOTH_APP_NAME)

    def setup_ble(self):
        self.set_service_and_characteristic()
        self.set_listeners()
        self.ble_uart.publish()

    def set_service_and_characteristic(self):
        self.ble_uart.add_service(srv_id=2, uuid=config.BLUETOOTH_SERVICE_UUID, primary=True)
        self.ble_uart.add_characteristic(srv_id=2, chr_id=1, uuid=config.BLUETOOTH_CHARACTERISTIC_UUID,
                                         value=[], notifying=False,
                                         flags=['write', 'write-without-response'],
                                         write_callback=BluetoothDevice.uart_write,
                                         read_callback=None,
                                         notify_callback=None)

    def set_listeners(self):
        self.ble_uart.on_connect = BluetoothDevice.on_connect
        self.ble_uart.on_disconnect = BluetoothDevice.on_disconnect

    def scan_for_beacon(self):
        Logger.debug("start beacon scan")
        print("start beacon scan")
        subprocess.Popen(["python", "parallel-beacon-scan/main.py"], shell=False,
             stdin=None, stdout=None, stderr=None, close_fds=True)

    def beacon_is_barrier_beacon(self, beacon_data):
        uuid = beacon_data[0]
        major = beacon_data[1]
        minor = beacon_data[2]

        return str(uuid) == config.BEACON_UUID and major == config.BEACON_MAJOR and minor == config.BEACON_MINOR

The scan_for_beacon method starts a subprocess, that scans for beacons and looks like that:

from bluezero import observer
import config


class BeaconBluetoothController:

    def scan_for_beacon(self):
        observer.Scanner.start_beacon_scan(on_ibeacon=self.ibeacon_found)

    def ibeacon_found(self, beacon_data):
        if self.beacon_is_barrier_beacon(beacon_data):
            print("is my beacon")

    def beacon_is_barrier_beacon(self, beacon_data):
        uuid = beacon_data[0]
        major = beacon_data[1]
        minor = beacon_data[2]

        is_barrier_beacon = str(uuid) == config.BEACON_UUID and major == config.BEACON_MAJOR and minor == config.BEACON_MINOR

        if is_barrier_beacon:
            print(f"{uuid}.{major}.{minor}")

        return is_barrier_beacon

Now, if the beaconbluetoothcontroller is started as a subprocess I get the following errors:

ERROR:dbus.connection:Exception in handler for D-Bus signal:
Traceback (most recent call last):
  File "/home/pi/.local/lib/python3.9/site-packages/dbus/connection.py", line 218, in maybe_handle_message
    self._handler(*args, **kwargs)
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/adapter.py", line 317, in _interfaces_added
    self.on_device_found(new_dev)
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/observer.py", line 175, in on_device_found
    rssi = bz_device_obj.RSSI
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/device.py", line 219, in RSSI
    return dbus_tools.get(self.remote_device_props,
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/dbus_tools.py", line 407, in get
    raise dbus_exception
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/dbus_tools.py", line 398, in get
    return dbus_prop_obj.Get(dbus_iface, prop_name)
  File "/home/pi/.local/lib/python3.9/site-packages/dbus/proxies.py", line 72, in __call__
    return self._proxy_method(*args, **keywords)
  File "/home/pi/.local/lib/python3.9/site-packages/dbus/proxies.py", line 141, in __call__
    return self._connection.call_blocking(self._named_service,
  File "/home/pi/.local/lib/python3.9/site-packages/dbus/connection.py", line 634, in call_blocking
    reply_message = self.send_message_with_reply_and_block(
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.UnknownObject: Method "Get" with signature "ss" on interface "org.freedesktop.DBus.Properties" doesn't exist

ERROR:dbus.connection:Exception in handler for D-Bus signal:
Traceback (most recent call last):
  File "/home/pi/.local/lib/python3.9/site-packages/dbus/connection.py", line 218, in maybe_handle_message
    self._handler(*args, **kwargs)
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/adapter.py", line 314, in _interfaces_added
    new_dev = device.Device(
  File "/home/pi/.local/lib/python3.9/site-packages/bluezero/device.py", line 53, in __init__
    raise ValueError("Cannot find a device: " + device_addr +
ValueError: Cannot find a device: 21:7D:AE:6C:97:30 using adapter: B8:27:EB:D6:83:A2

If I start the subprocess manually, they aren't thrown. My theory is that there are some issues, when an instance that has already started a BLE process (like advertising itself as Peripheral) is starting a BLE subprocess (like scanning for beacons).

Personally the exceptions aren't that bad for me. The beacon that I'm scanning for is found, nevertheless some devices are causing some issues. But if there is a way to handle them I would be gratefully if you can tell me where my issue is.

Thanks again for your great library.

Severon96 avatar Sep 13 '22 12:09 Severon96

This seems like is similar to https://github.com/ukBaz/python-bluezero/issues/266 .

I suspect the issue is that the scanning is creating new devices in BlueZ which triggers the InterfacesAdded D-Bus signal. The observer code also removes those devices once it has the device information to work around a "feature" in BlueZ that throttles the reporting of discovered devices. Then the self.ble_uart.on_connect is using the same InterfacesAdded D-Bus signal.

So the investigation would need to focus on following section of code to make it more robust to the different scenarios where a InterfacesAdded signal is triggered.

https://github.com/ukBaz/python-bluezero/blob/392a115ace80a6d7257b76708ec24b99ec9e2e5d/bluezero/adapter.py#L304-L322

If you have any feedback or insight into how this could be changed (or tested) then please share.

ukBaz avatar Sep 13 '22 12:09 ukBaz

Thanks for the quick answer and your insight! I have sadly no good idea whats the best way to adjust the code of yours. But if you have access to an ibeacon: Create two python applications. One to advertise as BLE peripheral, one to scan for beacons. When you start the peripheral application, use subprocess.Popen to start the beacon scanner.

This should be enough to reproduce my issues.

Until this issue can be fixed. What would be the best way to handle the thrown exceptions? I tried to wrap different parts of my code in try: except: blocks but nothing helped to catch them.

Severon96 avatar Sep 14 '22 06:09 Severon96

From the start of this project there has always been a philosophy about complexity; that the library would attempt to help people along the learning curve as they learnt about using Bluetooth with Python and that for some they would outgrow the library as they wanted more control.

This meant that there was an acceptance that the abstraction would bring restrictions and that is why the concept of complexity levels was used. An interesting blog around this topic is "When you need to escape an abstraction, how violent is your exit?"

The observer and peripheral modules are at level 10 that looks to simplify event loops and so starts them in the background without the user knowing.

As a result of this the two applications want to create and start an event loop which means they have to run them in different processes to work. You have achieved this by using subprocess for one of them. However, that is not really how the event loop should be used. Both the "applications" should be registered to the same event loop and then the event loop started.

I have hacked together an example that starts just the one event loop: https://github.com/ukBaz/python-bluezero/pull/381/files?diff=split&w=0

When I run the ticket_380 branch I don't see the errors that you are reporting.

I suspect that what this means is that you need to access the lower level API's to make your application(s) work cleanly.

ukBaz avatar Sep 18 '22 10:09 ukBaz

Closing because of inactivity.

ukBaz avatar Aug 06 '23 12:08 ukBaz