hbldh / bleak

A cross platform Bluetooth Low Energy Client for Python using asyncio
MIT License
1.77k stars 294 forks source link

[Enhancement] - More useful example get_services.py #262

Closed GilShoshan94 closed 4 years ago

GilShoshan94 commented 4 years ago

Description

@hbldh and the other contributors. Hi, Thank you so much for this amazing library !!! Very impressive that you made a platform agnostic GATT client.

I am still messing around and was trying the examples. I found the 'get_services.py' not useful enough as it just prints:

Services: <bleak.backends.service.BleakGATTServiceCollection object at 0x00000183F9F2D4C0>

I had to explore this object in debug mode as well as to dig a bit in the library to understand what this object has to offer. I wrote a script that explores each services, characteristics and descriptors and extracts the available information. I think it can be a good example for newcomers and wanted to contribute back.

Here's my script:

"""
Services
----------------
An example showing how to fetch all services and print them.
Updated on 2019-03-25 by hbldh <henrik.blidh@nedomkull.com>
Updated on 2020-08-05 by GilShoshan94
"""

import asyncio
import platform

from bleak import BleakClient

async def print_services(mac_addr: str):
    async with BleakClient(mac_addr, timeout=5.0) as client:
        svcs = await client.get_services()
        # print("Obj: ", type(svcs))  # <class 'bleak.backends.service.BleakGATTServiceCollection'>
        # print("Services: ", type(svcs.services))  # <class 'dict'>
        # print("Characteristic: ", type(svcs.characteristics))  # <class 'dict'>
        # print("Descriptors: ", type(svcs.descriptors))  # <class 'dict'>
        print(get_infos(svcs))

def get_infos(svcs):
    sp = "   "  # space indent
    spm = "- "  # space indent with mark
    separator = f"{'_'*70}\n"  # separator between services
    res = separator
    for serv in svcs.services.values():
        res += (
            f"{spm}Service UUID: {serv.uuid} :\n"
            f"{sp*1}Description: {serv.description}\n"
            f"{sp*1}Characteristics: ({len(serv.characteristics)})\n"
        )
        for char in serv.characteristics:
            res += (
                f"\n{sp*1}{spm}Characteristic UUID: {char.uuid} :\n"
                f"{sp*2}Description: {char.description}\n"
                f"{sp*2}Handle: {char.handle}\n"
                f"{sp*2}Properties: {char.properties}\n"
                f"{sp*2}Descriptiors: ({len(char.descriptors)})\n"
            )
            for desc in char.descriptors:
                res += (
                    f"\n{sp*2}{spm}Descriptor UUID: {desc.uuid}\n"
                    f"{sp*3}Description: {desc.description}\n"
                    f"{sp*3}Handle: {desc.handle}\n"
                )

        res += separator
    return res

if __name__ == "__main__":
    # Change to your device's address here if you are using Windows or Linux or Mac
    address = "7F:2E:03:F1:2C:1E" if platform.system() != "Darwin" else "B9EA5233-37EF-4DD6-87A8-2A875E821C46"
    loop = asyncio.get_event_loop()
    loop.run_until_complete(print_services(address))

And when I tried it on my BLE headset (I had to higher the timeout to 5.0) this is the result:

______________________________________________________________________
- Service UUID: 0000febe-0000-1000-8000-00805f9b34fb :
   Description: Bose Corporation
   Characteristics: (4)

   - Characteristic UUID: 9ec813b4-256b-4090-93a8-a4f0e9107733 :
      Description:
      Handle: 2
      Properties: ['read', 'notify']
      Descriptiors: (1)

      - Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb
         Description: Client Characteristic Configuration
         Handle: 4

   - Characteristic UUID: d417c028-9818-4354-99d1-2ac09d074591 :
      Description:
      Handle: 5
      Properties: ['read', 'write-without-response', 'write', 'notify']
      Descriptiors: (1)

      - Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb
         Description: Client Characteristic Configuration
         Handle: 7

   - Characteristic UUID: c65b8f2f-aee2-4c89-b758-bc4892d6f2d8 :
      Description:
      Handle: 8
      Properties: ['read', 'write-without-response', 'write', 'notify']
      Descriptiors: (1)

      - Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb
         Description: Client Characteristic Configuration
         Handle: 10

   - Characteristic UUID: 234bfbd5-e3b3-4536-a3fe-723620d4b78d :
      Description:
      Handle: 11
      Properties: ['write']
      Descriptiors: (0)
______________________________________________________________________
- Service UUID: 00001801-0000-1000-8000-00805f9b34fb :
   Description: Generic Attribute Profile
   Characteristics: (1)

   - Characteristic UUID: 00002a05-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 14
      Properties: ['indicate']
      Descriptiors: (1)

      - Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb
         Description: Client Characteristic Configuration
         Handle: 16
______________________________________________________________________
- Service UUID: 00001800-0000-1000-8000-00805f9b34fb :
   Description: Generic Access Profile
   Characteristics: (2)

   - Characteristic UUID: 00002a00-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 18
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a01-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 20
      Properties: ['read']
      Descriptiors: (0)
______________________________________________________________________
- Service UUID: 0000180a-0000-1000-8000-00805f9b34fb :
   Description: Device Information
   Characteristics: (8)

   - Characteristic UUID: 00002a29-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 23
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a24-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 25
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a25-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 27
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a27-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 29
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a26-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 31
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a28-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 33
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a23-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 35
      Properties: ['read']
      Descriptiors: (0)

   - Characteristic UUID: 00002a50-0000-1000-8000-00805f9b34fb :
      Description:
      Handle: 37
      Properties: ['read']
      Descriptiors: (0)
______________________________________________________________________

Which I think is more useful. This example also shows some of the properties available.

Again thank you so much for this library. I am continuing to explore it, if it is welcomed, I can share more useful example as I am writing some utilities for myself. Let me know.

GilShoshan94 commented 4 years ago

Never mind, just saw to service_explorer.py example.... I mist it. I close this.

hbldh commented 4 years ago

I appreciate the effort. If you find a better way to textually represent a BleakGATTServiceCollection, either make a PR adding a better method to the __str__ method or as a separate "verbose" method!

GilShoshan94 commented 4 years ago

Hi @hbldh , I ended up building from your example service_explorer.py. I just used f-string format the get a more consistent output.

Here is my script, look at the _get_infos(client: BleakClient). It could be use in the BleakGATTServiceCollection object in the __str__ method.

If you find it good I can make the PR (I admit, I never did a PR in an open source project and don't know what it is custom to do).

def print_all_services_info(mac_addr: str, timeout: float = 2.0):
    """
    Explores all the services, charateristics and descriptors of the GATT server and prints a report.

    Parameters
    ----------
    mac_addr : str
        MAC Address of the BLE device.
    timeout : float, optional
        Timeout for required ``discover`` call, by default 2.0.
    """

    async def _get_infos(client: BleakClient) -> str:
        """
        Extracts the all the infos for a report.
        """
        serv_collection = await client.get_services()
        sp = "    "  # space indent
        separator = f"{'_'*200}\n"  # separator between services
        res = separator
        # `for serv in serv_collection:` is equivalent to `for serv in serv_collection.services.values():`
        for serv in serv_collection:
            res += f"[Service] {serv.uuid} : {serv.description}\n"
            for char in serv.characteristics:
                if "read" in char.properties:
                    try:
                        value = bytes(await client.read_gatt_char(char.handle))
                    except Exception as e:
                        value = str(str(e).encode())
                else:
                    value = str(b"")
                prop = f"({','.join(char.properties)})"
                res += (
                    f"{sp}[Characteristic] {char.uuid} : (Handle: {char.handle:<3}) "
                    f"{prop:<42} | Name: {char.description:<40} | Value: {value}\n"
                )
                for desc in char.descriptors:
                    value = bytes(await client.read_gatt_descriptor(desc.handle))
                    res += (
                        f"{sp*2}[Descriptor] {desc.uuid} : (Handle: {desc.handle:<3}) "
                        f"{'':<42} | Name: {desc.description:<40} | Value: {value}\n"
                    )

            res += separator
        return res

    async def print_infos(mac_addr: str, timeout: float):
        async with BleakClient(mac_addr, timeout=timeout) as client:
            res = await _get_infos(client)
            print(res)

    loop = asyncio.get_event_loop()
    loop.run_until_complete(print_infos(mac_addr, timeout))

And here is the result when I tried it on my headphone:

________________________________________________________________________________________________________________________________________________________________________________________________________
[Service] 0000febe-0000-1000-8000-00805f9b34fb : Bose Corporation
    [Characteristic] 9ec813b4-256b-4090-93a8-a4f0e9107733 : (Handle: 2  ) (read,notify)                              | Name:                                          | Value: b'\x00\x00\x00\x00\x00\x00'
        [Descriptor] 00002902-0000-1000-8000-00805f9b34fb : (Handle: 4  )                                            | Name: Client Characteristic Configuration      | Value: b'\x00\x00'
    [Characteristic] d417c028-9818-4354-99d1-2ac09d074591 : (Handle: 5  ) (read,write-without-response,write,notify) | Name:                                          | Value: b''
        [Descriptor] 00002902-0000-1000-8000-00805f9b34fb : (Handle: 7  )                                            | Name: Client Characteristic Configuration      | Value: b'\x00\x00'
    [Characteristic] c65b8f2f-aee2-4c89-b758-bc4892d6f2d8 : (Handle: 8  ) (read,write-without-response,write,notify) | Name:                                          | Value: b'Could not get GATT characteristics for c65b8f2f-aee2-4c89-b758-bc4892d6f2d8: ProtocolError (Error: 0x05)'
        [Descriptor] 00002902-0000-1000-8000-00805f9b34fb : (Handle: 10 )                                            | Name: Client Characteristic Configuration      | Value: b'\x00\x00'
    [Characteristic] 234bfbd5-e3b3-4536-a3fe-723620d4b78d : (Handle: 11 ) (write)                                    | Name:                                          | Value: b''
________________________________________________________________________________________________________________________________________________________________________________________________________
[Service] 00001801-0000-1000-8000-00805f9b34fb : Generic Attribute Profile
    [Characteristic] 00002a05-0000-1000-8000-00805f9b34fb : (Handle: 14 ) (indicate)                                 | Name:                                          | Value: b''
        [Descriptor] 00002902-0000-1000-8000-00805f9b34fb : (Handle: 16 )                                            | Name: Client Characteristic Configuration      | Value: b'\x02\x00'
________________________________________________________________________________________________________________________________________________________________________________________________________
[Service] 00001800-0000-1000-8000-00805f9b34fb : Generic Access Profile
    [Characteristic] 00002a00-0000-1000-8000-00805f9b34fb : (Handle: 18 ) (read)                                     | Name:                                          | Value: b'LE-GS Bose QuietComfort 35'
    [Characteristic] 00002a01-0000-1000-8000-00805f9b34fb : (Handle: 20 ) (read)                                     | Name:                                          | Value: b'\x00\x00'
________________________________________________________________________________________________________________________________________________________________________________________________________
[Service] 0000180a-0000-1000-8000-00805f9b34fb : Device Information
    [Characteristic] 00002a29-0000-1000-8000-00805f9b34fb : (Handle: 23 ) (read)                                     | Name:                                          | Value: b'Bose Corporation'
    [Characteristic] 00002a24-0000-1000-8000-00805f9b34fb : (Handle: 25 ) (read)                                     | Name:                                          | Value: b'759944-0010'
    [Characteristic] 00002a25-0000-1000-8000-00805f9b34fb : (Handle: 27 ) (read)                                     | Name:                                          | Value: b'072546Z62660675AE'
    [Characteristic] 00002a27-0000-1000-8000-00805f9b34fb : (Handle: 29 ) (read)                                     | Name:                                          | Value: b'1.0.0'
    [Characteristic] 00002a26-0000-1000-8000-00805f9b34fb : (Handle: 31 ) (read)                                     | Name:                                          | Value: b'3.0.3'
    [Characteristic] 00002a28-0000-1000-8000-00805f9b34fb : (Handle: 33 ) (read)                                     | Name:                                          | Value: b'3.0.3'
    [Characteristic] 00002a23-0000-1000-8000-00805f9b34fb : (Handle: 35 ) (read)                                     | Name:                                          | Value: b'\x00\x00\x00\x00\x00\x1f\xdf\x08'
    [Characteristic] 00002a50-0000-1000-8000-00805f9b34fb : (Handle: 37 ) (read)                                     | Name:                                          | Value: b'\x01\x9e\x00\x0c@\x03\x03'
________________________________________________________________________________________________________________________________________________________________________________________________________
hbldh commented 4 years ago

I still want to support Python 3.5 so I cannot use f-strings I am afraid. Apart from that, a __str__ method cannot use await keywords, so this would in that case be a formatter/data-fetcher method and might by added to e.g. bleak/utils.py or a new bleak/formatting.py file.

GilShoshan94 commented 4 years ago

@hbldh I forgot the f-string was introduced in 3.6. sorry about that... I tried it here with .format() . I am very new to async (I started learning it because your library use it).

We can still do a version without await for the __str__, we simply don't request the value (But we lose it...). Like that (in bleak\backends\service.py in the class BleakGATTService):

def __str__(self) -> str:
    """
    Extracts the all the infos for a report.
    """
    sp = "    "  # space indent
    separator = "{0}\n".format('_'*200)  # separator between services
    result = separator
    for serv in self.services.values():
        result += "[Service] {0} : {1}\n".format(serv.uuid, serv.description)
        for char in serv.characteristics:
            prop = "({0})".format(','.join(char.properties))
            result += (
                "{0}[Characteristic] {1} : (Handle: {2:<3}) ".format(sp, char.uuid, char.handle)
                "{0:<42} | Name: {1:<40}\n".format(prop, char.description)
            )
            for desc in char.descriptors:
                result += (
                    "{0}[Descriptor] {1} : (Handle: {2:<3}) ".format(sp*2, desc.uuid, desc.handle)
                    "{0:<42} | Name: {1:<40}\n".format('', desc.description)
                )
        result += separator
    return result 

And the result should look like that:

...
________________________________________________________________________________________________________________________________________________________________________________________________________
[Service] 00001801-0000-1000-8000-00805f9b34fb : Generic Attribute Profile
    [Characteristic] 00002a05-0000-1000-8000-00805f9b34fb : (Handle: 14 ) (indicate)                                 | Name:                        
        [Descriptor] 00002902-0000-1000-8000-00805f9b34fb : (Handle: 16 )                                            | Name: Client Characteristic Configuration
________________________________________________________________________________________________________________________________________________________________________________________________________
[Service] 00001800-0000-1000-8000-00805f9b34fb : Generic Access Profile
    [Characteristic] 00002a00-0000-1000-8000-00805f9b34fb : (Handle: 18 ) (read)                                     | Name:                
    [Characteristic] 00002a01-0000-1000-8000-00805f9b34fb : (Handle: 20 ) (read)                                     | Name:                 
________________________________________________________________________________________________________________________________________________________________________________________________________
...

I tried to convert the f-string to str.format() I did not test this code.