ukBaz / python-bluezero

A simple Python interface to Bluez
MIT License
387 stars 112 forks source link

Reducing complexity of creating a Central device #314

Closed ukBaz closed 3 years ago

ukBaz commented 3 years ago

The most common thing people want to do with BLE is connect to a peripheral so they can read and write some characteristics. There is a lot of code inside of Bluezero to achieve the Central role. But could it be achieve with less?

Well actually, yes. Below is an experiment to see what that might look like.

from time import sleep
from pydbus import SystemBus

BLUEZ_SERVICE = 'org.bluez'
BLUEZ_DEV_IFACE = 'org.bluez.Device1'
BLUEZ_CHR_IFACE = 'org.bluez.GattCharacteristic1'

class Central:

    def __init__(self, address):
        self.bus = SystemBus()
        self.mngr = self.bus.get(BLUEZ_SERVICE, '/')
        self.dev_path = self._from_device_address(address)
        self.device = self.bus.get(BLUEZ_SERVICE, self.dev_path)
        self.chars = {}

    def _from_device_address(self, addr):
        """Look up D-Bus object path from device address"""
        mng_objs = self.mngr.GetManagedObjects()
        for path in mng_objs:
            dev_addr = mng_objs[path].get(BLUEZ_DEV_IFACE, {}).get('Address', '')
            if addr.casefold() == dev_addr.casefold():
                return path

    def _get_device_chars(self):
        mng_objs = self.mngr.GetManagedObjects()
        for path in mng_objs:
            chr_uuid = mng_objs[path].get(BLUEZ_CHR_IFACE, {}).get('UUID')
            if path.startswith(self.dev_path) and chr_uuid:
                self.chars[chr_uuid] = self.bus.get(BLUEZ_SERVICE, path)

    def connect(self):
        """
        Connect to device.
        Wait for GATT services to be resolved before returning
        """
        self.device.Connect()
        while not self.device.ServicesResolved:
            sleep(0.5)
        self._get_device_chars()

    def disconnect(self):
        """Disconnect from device"""
        self.device.Disconnect()

    def char_write(self, uuid, value):
        """Write value to given GATT characteristic UUID"""
        if uuid.casefold() in self.chars:
            self.chars[uuid.casefold()].WriteValue(value, {})
        else:
            raise KeyError(f'UUID {uuid} not found')

    def char_read(self, uuid):
        """Read value of given GATT characteristic UUID"""
        if uuid.casefold() in self.chars:
            return self.chars[uuid.casefold()].ReadValue({})
        else:
            raise KeyError(f'UUID {uuid} not found')

This means that a user would be able to connect to a device and read and write with the following:

ubit = 'E5:10:5E:37:11:2d'
led_text = 'e95d93ee-251d-470a-a062-fa1922dfa9A8'
led_matrix_state = 'e95d7b77-251d-470a-a062-fa1922dfa9a8'

dev = Central(ubit)
dev.connect()
dev.char_write(led_text, b'test')
dev.char_write(led_matrix_state, [1, 2, 4, 8, 16])
print(dev.char_read(led_matrix_state))
dev.disconnect()

I don't think this is what Bluezero should become. Or should it? The goal of the library was to provide an easy on-ramp to using Bluetooth.

WayneKeenan commented 3 years ago

That looks good to me. Both... You had/have the concept of API layers a while back/still?( 1, 10, 100 ?)
This just seems to be the lowest detail one (I'd sure use) that the higher level ones, that new to BLE users use, are built on.

ukBaz commented 3 years ago

Thanks for the feedback @WayneKeenan . For now I've split this idea into a separate library that is available at: https://github.com/ukBaz/BLE_GATT to keep things clean.

Talking of API levels... I'm continuing to prototype ideas using the BlueZ Bluetooth Management API over at: https://gist.github.com/ukBaz/3336ad4f662399c1246cdf2e4d75df10#file-btmgmt_socket-py

Not sure where that is going to end up...

I'll close this ticket as this idea can be tracked over at BLE_GATT