0

I am building a Raspberry Pi project involving a touchscreen user interface, and reading temperature sensors via Bluetooth Low Energy. I'm using Python, the BLEAK Bluetooth library, and the graphics.py simple graphics package.

Graphics.py kind of wants to own the event loop. BLEAK is deeply involved with asyncio and wants to live in its own asyncio event loop. So I forked a thread to take care of the Bluetooth stuff, which simply deposits its readings in global variables. All well and good until the user presses a Quit button or some error terminates the program. If I don't do a try-finally and clean up the Bluetooth stuff properly, it won't even find the device the next time around; something deeper in the OS is jammed when it comes to connecting with that device, until I reboot. But it's in a separate thread, so the flow of control that is going to get that Bluetooth cleaned up is not obvious.

Is there a well-known method for dealing with this sort of thing? A well-known paradigm for mixing asynchronous Python with a graphical user interface?

Joymaker
  • 813
  • 1
  • 9
  • 23
  • There are a number of issues raised on the Bleak repo about similar topics: https://github.com/hbldh/bleak/issues/264 I'm not familiar with `graphics.py` but it looks to be using the Tk() event loop I just couldn't see about D-Bus bindings (used to talk with Pi Bluetooth) for Tk. The GTK library has better bindings to D-Bus https://stackoverflow.com/a/70748086/7721752 – ukBaz Jun 28 '22 at 06:12
  • Oh heavens, those are deep waters! Perhaps I want to fall back to a simpler question: My app has two threads: thread A (main), and thread B. If an error occurs in thread A, how can I get a try...finally type of cleanup action to take place in thread B before the program terminates? Or vice versa? – Joymaker Jun 28 '22 at 18:59

1 Answers1

0

Running an event loop for the GUI and accessing Bluetooth is the way to go.

This can be done using the BlueZ D-Bus API that is documented for the device: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt

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

There is some reasonable documentation around the GTK library: https://pygobject.readthedocs.io/en/latest/getting_started.html

And other tutorials: https://python-gtk-3-tutorial.readthedocs.io/en/latest/install.html

Below is a small example for a device I have that gives notifications over BLE of its temperature:

from time import sleep

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk

ADAPTER_PATH = '/org/bluez/hci0'
TEMP_DATA = 'E95D9250-251D-470A-A062-FA1922DFA9A8'

BLUEZ_BUS_NAME = 'org.bluez'
MNGR_IFACE = 'org.freedesktop.DBus.ObjectManager'
PROP_IFACE = 'org.freedesktop.DBus.Properties'
DEVICE_IFACE = 'org.bluez.Device1'
BLE_CHRC_IFACE = 'org.bluez.GattCharacteristic1'


class MainApp(Gtk.Window):

    def __init__(self, remote_device):
        super().__init__(title="Thermometer")
        self.ble_dev = remote_device
        # Veritical layout box
        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self.add(self.box)

        # horizontal box for buttons
        hbox = Gtk.Box(spacing=6)
        # Connect Button
        button = Gtk.Button.new_with_label("Connect")
        button.connect("clicked", self.connect)
        hbox.pack_start(button, True, True, 0)
        # Disconnect Button
        button = Gtk.Button.new_with_label("Disconnect")
        button.connect("clicked", self.disconnect)
        hbox.pack_start(button, True, True, 0)
        # Add labels to vertical layout
        self.box.pack_start(hbox, True, True, 0)
        self.temp_label = Gtk.Label(label='Temperature')
        self.box.pack_start(self.temp_label, True, True, 0)
        self.temp_value = Gtk.Label(label='Value Here')
        self.box.pack_start(self.temp_value, True, True, 0)

    def update_label(self, proxy, changed_props, invalidated_props):
        """Callback to update with new temperature"""
        props = changed_props.unpack()
        # print(props)
        temperature = props.get('Value')
        if temperature:
            self.temp_value.set_text(str(temperature[0]))

    def connect(self, button):
        """Connect to BLE device and start notifications"""
        self.ble_dev.dev_mthd_proxy.Connect()
        while not self.ble_dev.dev_props_proxy.Get(
                '(ss)', DEVICE_IFACE, 'ServicesResolved'):
            sleep(0.5)
        temp_path = get_characteristic_path(
            self.ble_dev.mngr_proxy,
            self.ble_dev.device_path, TEMP_DATA)
        self.ble_dev.tmp_gatt_proxy = bluez_proxy(temp_path, BLE_CHRC_IFACE)
        self.ble_dev.tmp_gatt_proxy.StartNotify()
        self.ble_dev.tmp_gatt_proxy.connect(
            'g-properties-changed', self.update_label)

    def disconnect(self, button):
        """Stop notifications and disconnect"""
        self.ble_dev.tmp_gatt_proxy.StopNotify()
        self.ble_dev.dev_mthd_proxy.Disconnect()


class TemperateDevice:
    """Collection of proxies for remote device"""
    def __init__(self, address):
        self.device_path = f"{ADAPTER_PATH}/dev_{address.replace(':', '_')}"
        self.mngr_proxy = bluez_proxy('/', MNGR_IFACE)
        self.dev_mthd_proxy = bluez_proxy(self.device_path, DEVICE_IFACE)
        self.dev_props_proxy = bluez_proxy(self.device_path, PROP_IFACE)
        self.tmp_gatt_proxy = None


def get_characteristic_path(mngr, dev_path, uuid):
    """Look up D-Bus path for characteristic UUID"""
    mng_objs = mngr.GetManagedObjects()
    for path in mng_objs:
        chr_uuid = mng_objs[path].get(BLE_CHRC_IFACE, {}).get('UUID')
        if path.startswith(dev_path) and chr_uuid == uuid.casefold():
            return path


def bluez_proxy(object_path, interface):
    """Function to assist with creating BlueZ proxies"""
    return Gio.DBusProxy.new_for_bus_sync(
        bus_type=Gio.BusType.SYSTEM,
        flags=Gio.DBusProxyFlags.NONE,
        info=None,
        name=BLUEZ_BUS_NAME,
        object_path=object_path,
        interface_name=interface,
        cancellable=None)


def main(address):
    print("running application")
    ble_dev = TemperateDevice(address)
    app = MainApp(ble_dev)
    app.show_all()
    Gtk.main()


if __name__ == "__main__":
    main("E1:4B:6C:22:56:F0")

ukBaz
  • 6,985
  • 2
  • 8
  • 31