1# Copyright 2021-2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# -----------------------------------------------------------------------------
16# Imports
17# -----------------------------------------------------------------------------
18import asyncio
19import sys
20import os
21import logging
22from bumble.colors import color
23
24from bumble.core import ProtocolError
25from bumble.device import Device, Peer
26from bumble.gatt import show_services
27from bumble.transport import open_transport_or_link
28from bumble.utils import AsyncRunner
29
30
31# -----------------------------------------------------------------------------
32class Listener(Device.Listener):
33    def __init__(self, device):
34        self.device = device
35
36    @AsyncRunner.run_in_task()
37    # pylint: disable=invalid-overridden-method
38    async def on_connection(self, connection):
39        print(f'=== Connected to {connection}')
40
41        # Discover all services
42        print('=== Discovering services')
43        peer = Peer(connection)
44        await peer.discover_services()
45        for service in peer.services:
46            await service.discover_characteristics()
47            for characteristic in service.characteristics:
48                await characteristic.discover_descriptors()
49
50        print('=== Services discovered')
51        show_services(peer.services)
52
53        # Discover all attributes
54        print('=== Discovering attributes')
55        attributes = await peer.discover_attributes()
56        for attribute in attributes:
57            print(attribute)
58        print('=== Attributes discovered')
59
60        # Read all attributes
61        for attribute in attributes:
62            try:
63                value = await peer.read_value(attribute)
64                print(color(f'0x{attribute.handle:04X} = {value.hex()}', 'green'))
65            except ProtocolError as error:
66                print(color(f'cannot read {attribute.handle:04X}:', 'red'), error)
67            except TimeoutError:
68                print(color('read timeout'))
69
70
71# -----------------------------------------------------------------------------
72async def main() -> None:
73    if len(sys.argv) < 3:
74        print(
75            'Usage: run_gatt_client.py <device-config> <transport-spec> '
76            '[<bluetooth-address>]'
77        )
78        print('example: run_gatt_client.py device1.json usb:0 E1:CA:72:48:C4:E8')
79        return
80
81    print('<<< connecting to HCI...')
82    async with await open_transport_or_link(sys.argv[2]) as hci_transport:
83        print('<<< connected')
84
85        # Create a device to manage the host, with a custom listener
86        device = Device.from_config_file_with_hci(
87            sys.argv[1], hci_transport.source, hci_transport.sink
88        )
89        device.listener = Listener(device)
90        await device.power_on()
91
92        # Connect to a peer
93        if len(sys.argv) > 3:
94            target_address = sys.argv[3]
95            print(f'=== Connecting to {target_address}...')
96            await device.connect(target_address)
97        else:
98            await device.start_advertising()
99
100        await asyncio.get_running_loop().create_future()
101
102
103# -----------------------------------------------------------------------------
104logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
105asyncio.run(main())
106