• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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# GATT - Generic Attribute Profile
17# Client
18#
19# See Bluetooth spec @ Vol 3, Part G
20#
21# -----------------------------------------------------------------------------
22
23# -----------------------------------------------------------------------------
24# Imports
25# -----------------------------------------------------------------------------
26from __future__ import annotations
27import asyncio
28import logging
29import struct
30from typing import List, Optional
31
32from pyee import EventEmitter
33
34from .colors import color
35from .hci import HCI_Constant
36from .att import (
37    ATT_ATTRIBUTE_NOT_FOUND_ERROR,
38    ATT_ATTRIBUTE_NOT_LONG_ERROR,
39    ATT_CID,
40    ATT_DEFAULT_MTU,
41    ATT_ERROR_RESPONSE,
42    ATT_INVALID_OFFSET_ERROR,
43    ATT_PDU,
44    ATT_RESPONSES,
45    ATT_Exchange_MTU_Request,
46    ATT_Find_By_Type_Value_Request,
47    ATT_Find_Information_Request,
48    ATT_Handle_Value_Confirmation,
49    ATT_Read_Blob_Request,
50    ATT_Read_By_Group_Type_Request,
51    ATT_Read_By_Type_Request,
52    ATT_Read_Request,
53    ATT_Write_Command,
54    ATT_Write_Request,
55    ATT_Error,
56)
57from . import core
58from .core import UUID, InvalidStateError, ProtocolError
59from .gatt import (
60    GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
61    GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
62    GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
63    GATT_REQUEST_TIMEOUT,
64    GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
65    Service,
66    Characteristic,
67    ClientCharacteristicConfigurationBits,
68)
69
70# -----------------------------------------------------------------------------
71# Logging
72# -----------------------------------------------------------------------------
73logger = logging.getLogger(__name__)
74
75
76# -----------------------------------------------------------------------------
77# Proxies
78# -----------------------------------------------------------------------------
79class AttributeProxy(EventEmitter):
80    client: Client
81
82    def __init__(self, client, handle, end_group_handle, attribute_type):
83        EventEmitter.__init__(self)
84        self.client = client
85        self.handle = handle
86        self.end_group_handle = end_group_handle
87        self.type = attribute_type
88
89    async def read_value(self, no_long_read=False):
90        return self.decode_value(
91            await self.client.read_value(self.handle, no_long_read)
92        )
93
94    async def write_value(self, value, with_response=False):
95        return await self.client.write_value(
96            self.handle, self.encode_value(value), with_response
97        )
98
99    def encode_value(self, value):
100        return value
101
102    def decode_value(self, value_bytes):
103        return value_bytes
104
105    def __str__(self):
106        return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
107
108
109class ServiceProxy(AttributeProxy):
110    uuid: UUID
111    characteristics: List[CharacteristicProxy]
112
113    @staticmethod
114    def from_client(service_class, client, service_uuid):
115        # The service and its characteristics are considered to have already been
116        # discovered
117        services = client.get_services_by_uuid(service_uuid)
118        service = services[0] if services else None
119        return service_class(service) if service else None
120
121    def __init__(self, client, handle, end_group_handle, uuid, primary=True):
122        attribute_type = (
123            GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
124            if primary
125            else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE
126        )
127        super().__init__(client, handle, end_group_handle, attribute_type)
128        self.uuid = uuid
129        self.characteristics = []
130
131    async def discover_characteristics(self, uuids=()):
132        return await self.client.discover_characteristics(uuids, self)
133
134    def get_characteristics_by_uuid(self, uuid):
135        return self.client.get_characteristics_by_uuid(uuid, self)
136
137    def __str__(self):
138        return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
139
140
141class CharacteristicProxy(AttributeProxy):
142    descriptors: List[DescriptorProxy]
143
144    def __init__(self, client, handle, end_group_handle, uuid, properties):
145        super().__init__(client, handle, end_group_handle, uuid)
146        self.uuid = uuid
147        self.properties = properties
148        self.descriptors = []
149        self.descriptors_discovered = False
150        self.subscribers = {}  # Map from subscriber to proxy subscriber
151
152    def get_descriptor(self, descriptor_type):
153        for descriptor in self.descriptors:
154            if descriptor.type == descriptor_type:
155                return descriptor
156
157        return None
158
159    async def discover_descriptors(self):
160        return await self.client.discover_descriptors(self)
161
162    async def subscribe(self, subscriber=None, prefer_notify=True):
163        if subscriber is not None:
164            if subscriber in self.subscribers:
165                # We already have a proxy subscriber
166                subscriber = self.subscribers[subscriber]
167            else:
168                # Create and register a proxy that will decode the value
169                original_subscriber = subscriber
170
171                def on_change(value):
172                    original_subscriber(self.decode_value(value))
173
174                self.subscribers[subscriber] = on_change
175                subscriber = on_change
176
177        return await self.client.subscribe(self, subscriber, prefer_notify)
178
179    async def unsubscribe(self, subscriber=None):
180        if subscriber in self.subscribers:
181            subscriber = self.subscribers.pop(subscriber)
182
183        return await self.client.unsubscribe(self, subscriber)
184
185    def __str__(self):
186        return (
187            f'Characteristic(handle=0x{self.handle:04X}, '
188            f'uuid={self.uuid}, '
189            f'properties={Characteristic.properties_as_string(self.properties)})'
190        )
191
192
193class DescriptorProxy(AttributeProxy):
194    def __init__(self, client, handle, descriptor_type):
195        super().__init__(client, handle, 0, descriptor_type)
196
197    def __str__(self):
198        return f'Descriptor(handle=0x{self.handle:04X}, type={self.type})'
199
200
201class ProfileServiceProxy:
202    '''
203    Base class for profile-specific service proxies
204    '''
205
206    @classmethod
207    def from_client(cls, client):
208        return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
209
210
211# -----------------------------------------------------------------------------
212# GATT Client
213# -----------------------------------------------------------------------------
214class Client:
215    services: List[ServiceProxy]
216
217    def __init__(self, connection):
218        self.connection = connection
219        self.mtu_exchange_done = False
220        self.request_semaphore = asyncio.Semaphore(1)
221        self.pending_request = None
222        self.pending_response = None
223        self.notification_subscribers = (
224            {}
225        )  # Notification subscribers, by attribute handle
226        self.indication_subscribers = {}  # Indication subscribers, by attribute handle
227        self.services = []
228
229    def send_gatt_pdu(self, pdu):
230        self.connection.send_l2cap_pdu(ATT_CID, pdu)
231
232    async def send_command(self, command):
233        logger.debug(
234            f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
235        )
236        self.send_gatt_pdu(command.to_bytes())
237
238    async def send_request(self, request):
239        logger.debug(
240            f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
241        )
242
243        # Wait until we can send (only one pending command at a time for the connection)
244        response = None
245        async with self.request_semaphore:
246            assert self.pending_request is None
247            assert self.pending_response is None
248
249            # Create a future value to hold the eventual response
250            self.pending_response = asyncio.get_running_loop().create_future()
251            self.pending_request = request
252
253            try:
254                self.send_gatt_pdu(request.to_bytes())
255                response = await asyncio.wait_for(
256                    self.pending_response, GATT_REQUEST_TIMEOUT
257                )
258            except asyncio.TimeoutError as error:
259                logger.warning(color('!!! GATT Request timeout', 'red'))
260                raise core.TimeoutError(f'GATT timeout for {request.name}') from error
261            finally:
262                self.pending_request = None
263                self.pending_response = None
264
265        return response
266
267    def send_confirmation(self, confirmation):
268        logger.debug(
269            f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
270            f'{confirmation}'
271        )
272        self.send_gatt_pdu(confirmation.to_bytes())
273
274    async def request_mtu(self, mtu):
275        # Check the range
276        if mtu < ATT_DEFAULT_MTU:
277            raise ValueError(f'MTU must be >= {ATT_DEFAULT_MTU}')
278        if mtu > 0xFFFF:
279            raise ValueError('MTU must be <= 0xFFFF')
280
281        # We can only send one request per connection
282        if self.mtu_exchange_done:
283            return self.connection.att_mtu
284
285        # Send the request
286        self.mtu_exchange_done = True
287        response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu))
288        if response.op_code == ATT_ERROR_RESPONSE:
289            raise ProtocolError(
290                response.error_code,
291                'att',
292                ATT_PDU.error_name(response.error_code),
293                response,
294            )
295
296        # Compute the final MTU
297        self.connection.att_mtu = min(mtu, response.server_rx_mtu)
298
299        return self.connection.att_mtu
300
301    def get_services_by_uuid(self, uuid):
302        return [service for service in self.services if service.uuid == uuid]
303
304    def get_characteristics_by_uuid(self, uuid, service=None):
305        services = [service] if service else self.services
306        return [
307            c
308            for c in [c for s in services for c in s.characteristics]
309            if c.uuid == uuid
310        ]
311
312    def on_service_discovered(self, service):
313        '''Add a service to the service list if it wasn't already there'''
314        already_known = False
315        for existing_service in self.services:
316            if existing_service.handle == service.handle:
317                already_known = True
318                break
319        if not already_known:
320            self.services.append(service)
321
322    async def discover_services(self, uuids=None) -> List[ServiceProxy]:
323        '''
324        See Vol 3, Part G - 4.4.1 Discover All Primary Services
325        '''
326        starting_handle = 0x0001
327        services = []
328        while starting_handle < 0xFFFF:
329            response = await self.send_request(
330                ATT_Read_By_Group_Type_Request(
331                    starting_handle=starting_handle,
332                    ending_handle=0xFFFF,
333                    attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
334                )
335            )
336            if response is None:
337                # TODO raise appropriate exception
338                return []
339
340            # Check if we reached the end of the iteration
341            if response.op_code == ATT_ERROR_RESPONSE:
342                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
343                    # Unexpected end
344                    logger.warning(
345                        '!!! unexpected error while discovering services: '
346                        f'{HCI_Constant.error_name(response.error_code)}'
347                    )
348                    raise ATT_Error(
349                        error_code=response.error_code,
350                        message='Unexpected error while discovering services',
351                    )
352                break
353
354            for (
355                attribute_handle,
356                end_group_handle,
357                attribute_value,
358            ) in response.attributes:
359                if (
360                    attribute_handle < starting_handle
361                    or end_group_handle < attribute_handle
362                ):
363                    # Something's not right
364                    logger.warning(
365                        f'bogus handle values: {attribute_handle} {end_group_handle}'
366                    )
367                    return []
368
369                # Create a service proxy for this service
370                service = ServiceProxy(
371                    self,
372                    attribute_handle,
373                    end_group_handle,
374                    UUID.from_bytes(attribute_value),
375                    True,
376                )
377
378                # Filter out returned services based on the given uuids list
379                if (not uuids) or (service.uuid in uuids):
380                    services.append(service)
381
382                # Add the service to the peer's service list
383                self.on_service_discovered(service)
384
385            # Stop if for some reason the list was empty
386            if not response.attributes:
387                break
388
389            # Move on to the next chunk
390            starting_handle = response.attributes[-1][1] + 1
391
392        return services
393
394    async def discover_service(self, uuid):
395        '''
396        See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
397        '''
398
399        # Force uuid to be a UUID object
400        if isinstance(uuid, str):
401            uuid = UUID(uuid)
402
403        starting_handle = 0x0001
404        services = []
405        while starting_handle < 0xFFFF:
406            response = await self.send_request(
407                ATT_Find_By_Type_Value_Request(
408                    starting_handle=starting_handle,
409                    ending_handle=0xFFFF,
410                    attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
411                    attribute_value=uuid.to_pdu_bytes(),
412                )
413            )
414            if response is None:
415                # TODO raise appropriate exception
416                return []
417
418            # Check if we reached the end of the iteration
419            if response.op_code == ATT_ERROR_RESPONSE:
420                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
421                    # Unexpected end
422                    logger.warning(
423                        '!!! unexpected error while discovering services: '
424                        f'{HCI_Constant.error_name(response.error_code)}'
425                    )
426                    # TODO raise appropriate exception
427                    return
428                break
429
430            for attribute_handle, end_group_handle in response.handles_information:
431                if (
432                    attribute_handle < starting_handle
433                    or end_group_handle < attribute_handle
434                ):
435                    # Something's not right
436                    logger.warning(
437                        f'bogus handle values: {attribute_handle} {end_group_handle}'
438                    )
439                    return
440
441                # Create a service proxy for this service
442                service = ServiceProxy(
443                    self, attribute_handle, end_group_handle, uuid, True
444                )
445
446                # Add the service to the peer's service list
447                services.append(service)
448                self.on_service_discovered(service)
449
450                # Check if we've reached the end already
451                if end_group_handle == 0xFFFF:
452                    break
453
454            # Stop if for some reason the list was empty
455            if not response.handles_information:
456                break
457
458            # Move on to the next chunk
459            starting_handle = response.handles_information[-1][1] + 1
460
461        return services
462
463    async def discover_included_services(self, _service):
464        '''
465        See Vol 3, Part G - 4.5.1 Find Included Services
466        '''
467        # TODO
468        return []
469
470    async def discover_characteristics(
471        self, uuids, service: Optional[ServiceProxy]
472    ) -> List[CharacteristicProxy]:
473        '''
474        See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
475        Discover Characteristics by UUID
476        '''
477
478        # Cast the UUIDs type from string to object if needed
479        uuids = [UUID(uuid) if isinstance(uuid, str) else uuid for uuid in uuids]
480
481        # Decide which services to discover for
482        services = [service] if service else self.services
483
484        # Perform characteristic discovery for each service
485        discovered_characteristics: List[CharacteristicProxy] = []
486        for service in services:
487            starting_handle = service.handle
488            ending_handle = service.end_group_handle
489
490            characteristics: List[CharacteristicProxy] = []
491            while starting_handle <= ending_handle:
492                response = await self.send_request(
493                    ATT_Read_By_Type_Request(
494                        starting_handle=starting_handle,
495                        ending_handle=ending_handle,
496                        attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
497                    )
498                )
499                if response is None:
500                    # TODO raise appropriate exception
501                    return []
502
503                # Check if we reached the end of the iteration
504                if response.op_code == ATT_ERROR_RESPONSE:
505                    if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
506                        # Unexpected end
507                        logger.warning(
508                            '!!! unexpected error while discovering characteristics: '
509                            f'{HCI_Constant.error_name(response.error_code)}'
510                        )
511                        raise ATT_Error(
512                            error_code=response.error_code,
513                            message='Unexpected error while discovering characteristics',
514                        )
515                    break
516
517                # Stop if for some reason the list was empty
518                if not response.attributes:
519                    break
520
521                # Process all characteristics returned in this iteration
522                for attribute_handle, attribute_value in response.attributes:
523                    if attribute_handle < starting_handle:
524                        # Something's not right
525                        logger.warning(f'bogus handle value: {attribute_handle}')
526                        return []
527
528                    properties, handle = struct.unpack_from('<BH', attribute_value)
529                    characteristic_uuid = UUID.from_bytes(attribute_value[3:])
530                    characteristic = CharacteristicProxy(
531                        self, handle, 0, characteristic_uuid, properties
532                    )
533
534                    # Set the previous characteristic's end handle
535                    if characteristics:
536                        characteristics[-1].end_group_handle = attribute_handle - 1
537
538                    characteristics.append(characteristic)
539
540                # Move on to the next characteristics
541                starting_handle = response.attributes[-1][0] + 1
542
543            # Set the end handle for the last characteristic
544            if characteristics:
545                characteristics[-1].end_group_handle = service.end_group_handle
546
547            # Set the service's characteristics
548            characteristics = [
549                c for c in characteristics if not uuids or c.uuid in uuids
550            ]
551            service.characteristics = characteristics
552            discovered_characteristics.extend(characteristics)
553
554        return discovered_characteristics
555
556    async def discover_descriptors(
557        self,
558        characteristic: Optional[CharacteristicProxy] = None,
559        start_handle=None,
560        end_handle=None,
561    ) -> List[DescriptorProxy]:
562        '''
563        See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
564        '''
565        if characteristic:
566            starting_handle = characteristic.handle + 1
567            ending_handle = characteristic.end_group_handle
568        elif start_handle and end_handle:
569            starting_handle = start_handle
570            ending_handle = end_handle
571        else:
572            return []
573
574        descriptors: List[DescriptorProxy] = []
575        while starting_handle <= ending_handle:
576            response = await self.send_request(
577                ATT_Find_Information_Request(
578                    starting_handle=starting_handle, ending_handle=ending_handle
579                )
580            )
581            if response is None:
582                # TODO raise appropriate exception
583                return []
584
585            # Check if we reached the end of the iteration
586            if response.op_code == ATT_ERROR_RESPONSE:
587                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
588                    # Unexpected end
589                    logger.warning(
590                        '!!! unexpected error while discovering descriptors: '
591                        f'{HCI_Constant.error_name(response.error_code)}'
592                    )
593                    # TODO raise appropriate exception
594                    return []
595                break
596
597            # Stop if for some reason the list was empty
598            if not response.information:
599                break
600
601            # Process all descriptors returned in this iteration
602            for attribute_handle, attribute_uuid in response.information:
603                if attribute_handle < starting_handle:
604                    # Something's not right
605                    logger.warning(f'bogus handle value: {attribute_handle}')
606                    return []
607
608                descriptor = DescriptorProxy(
609                    self, attribute_handle, UUID.from_bytes(attribute_uuid)
610                )
611                descriptors.append(descriptor)
612                # TODO: read descriptor value
613
614            # Move on to the next descriptor
615            starting_handle = response.information[-1][0] + 1
616
617        # Set the characteristic's descriptors
618        if characteristic:
619            characteristic.descriptors = descriptors
620
621        return descriptors
622
623    async def discover_attributes(self):
624        '''
625        Discover all attributes, regardless of type
626        '''
627        starting_handle = 0x0001
628        ending_handle = 0xFFFF
629        attributes = []
630        while True:
631            response = await self.send_request(
632                ATT_Find_Information_Request(
633                    starting_handle=starting_handle, ending_handle=ending_handle
634                )
635            )
636            if response is None:
637                return []
638
639            # Check if we reached the end of the iteration
640            if response.op_code == ATT_ERROR_RESPONSE:
641                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
642                    # Unexpected end
643                    logger.warning(
644                        '!!! unexpected error while discovering attributes: '
645                        f'{HCI_Constant.error_name(response.error_code)}'
646                    )
647                    return []
648                break
649
650            for attribute_handle, attribute_uuid in response.information:
651                if attribute_handle < starting_handle:
652                    # Something's not right
653                    logger.warning(f'bogus handle value: {attribute_handle}')
654                    return []
655
656                attribute = AttributeProxy(
657                    self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
658                )
659                attributes.append(attribute)
660
661            # Move on to the next attributes
662            starting_handle = attributes[-1].handle + 1
663
664        return attributes
665
666    async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
667        # If we haven't already discovered the descriptors for this characteristic,
668        # do it now
669        if not characteristic.descriptors_discovered:
670            await self.discover_descriptors(characteristic)
671
672        # Look for the CCCD descriptor
673        cccd = characteristic.get_descriptor(
674            GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
675        )
676        if not cccd:
677            logger.warning('subscribing to characteristic with no CCCD descriptor')
678            return
679
680        if (
681            characteristic.properties & Characteristic.NOTIFY
682            and characteristic.properties & Characteristic.INDICATE
683        ):
684            if prefer_notify:
685                bits = ClientCharacteristicConfigurationBits.NOTIFICATION
686                subscribers = self.notification_subscribers
687            else:
688                bits = ClientCharacteristicConfigurationBits.INDICATION
689                subscribers = self.indication_subscribers
690        elif characteristic.properties & Characteristic.NOTIFY:
691            bits = ClientCharacteristicConfigurationBits.NOTIFICATION
692            subscribers = self.notification_subscribers
693        elif characteristic.properties & Characteristic.INDICATE:
694            bits = ClientCharacteristicConfigurationBits.INDICATION
695            subscribers = self.indication_subscribers
696        else:
697            raise InvalidStateError("characteristic is not notify or indicate")
698
699        # Add subscribers to the sets
700        subscriber_set = subscribers.setdefault(characteristic.handle, set())
701        if subscriber is not None:
702            subscriber_set.add(subscriber)
703        # Add the characteristic as a subscriber, which will result in the
704        # characteristic emitting an 'update' event when a notification or indication
705        # is received
706        subscriber_set.add(characteristic)
707
708        await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
709
710    async def unsubscribe(self, characteristic, subscriber=None):
711        # If we haven't already discovered the descriptors for this characteristic,
712        # do it now
713        if not characteristic.descriptors_discovered:
714            await self.discover_descriptors(characteristic)
715
716        # Look for the CCCD descriptor
717        cccd = characteristic.get_descriptor(
718            GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
719        )
720        if not cccd:
721            logger.warning('unsubscribing from characteristic with no CCCD descriptor')
722            return
723
724        if subscriber is not None:
725            # Remove matching subscriber from subscriber sets
726            for subscriber_set in (
727                self.notification_subscribers,
728                self.indication_subscribers,
729            ):
730                subscribers = subscriber_set.get(characteristic.handle, [])
731                if subscriber in subscribers:
732                    subscribers.remove(subscriber)
733
734                    # Cleanup if we removed the last one
735                    if not subscribers:
736                        del subscriber_set[characteristic.handle]
737        else:
738            # Remove all subscribers for this attribute from the sets!
739            self.notification_subscribers.pop(characteristic.handle, None)
740            self.indication_subscribers.pop(characteristic.handle, None)
741
742        if not self.notification_subscribers and not self.indication_subscribers:
743            # No more subscribers left
744            await self.write_value(cccd, b'\x00\x00', with_response=True)
745
746    async def read_value(self, attribute, no_long_read=False):
747        '''
748        See Vol 3, Part G - 4.8.1 Read Characteristic Value
749
750        `attribute` can be an Attribute object, or a handle value
751        '''
752
753        # Send a request to read
754        attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
755        response = await self.send_request(
756            ATT_Read_Request(attribute_handle=attribute_handle)
757        )
758        if response is None:
759            raise TimeoutError('read timeout')
760        if response.op_code == ATT_ERROR_RESPONSE:
761            raise ProtocolError(
762                response.error_code,
763                'att',
764                ATT_PDU.error_name(response.error_code),
765                response,
766            )
767
768        # If the value is the max size for the MTU, try to read more unless the caller
769        # specifically asked not to do that
770        attribute_value = response.attribute_value
771        if not no_long_read and len(attribute_value) == self.connection.att_mtu - 1:
772            logger.debug('using READ BLOB to get the rest of the value')
773            offset = len(attribute_value)
774            while True:
775                response = await self.send_request(
776                    ATT_Read_Blob_Request(
777                        attribute_handle=attribute_handle, value_offset=offset
778                    )
779                )
780                if response is None:
781                    raise TimeoutError('read timeout')
782                if response.op_code == ATT_ERROR_RESPONSE:
783                    if response.error_code in (
784                        ATT_ATTRIBUTE_NOT_LONG_ERROR,
785                        ATT_INVALID_OFFSET_ERROR,
786                    ):
787                        break
788                    raise ProtocolError(
789                        response.error_code,
790                        'att',
791                        ATT_PDU.error_name(response.error_code),
792                        response,
793                    )
794
795                part = response.part_attribute_value
796                attribute_value += part
797
798                if len(part) < self.connection.att_mtu - 1:
799                    break
800
801                offset += len(part)
802
803        # Return the value as bytes
804        return attribute_value
805
806    async def read_characteristics_by_uuid(self, uuid, service):
807        '''
808        See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
809        '''
810
811        if service is None:
812            starting_handle = 0x0001
813            ending_handle = 0xFFFF
814        else:
815            starting_handle = service.handle
816            ending_handle = service.end_group_handle
817
818        characteristics_values = []
819        while starting_handle <= ending_handle:
820            response = await self.send_request(
821                ATT_Read_By_Type_Request(
822                    starting_handle=starting_handle,
823                    ending_handle=ending_handle,
824                    attribute_type=uuid,
825                )
826            )
827            if response is None:
828                # TODO raise appropriate exception
829                return []
830
831            # Check if we reached the end of the iteration
832            if response.op_code == ATT_ERROR_RESPONSE:
833                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
834                    # Unexpected end
835                    logger.warning(
836                        '!!! unexpected error while reading characteristics: '
837                        f'{HCI_Constant.error_name(response.error_code)}'
838                    )
839                    # TODO raise appropriate exception
840                    return []
841                break
842
843            # Stop if for some reason the list was empty
844            if not response.attributes:
845                break
846
847            # Process all characteristics returned in this iteration
848            for attribute_handle, attribute_value in response.attributes:
849                if attribute_handle < starting_handle:
850                    # Something's not right
851                    logger.warning(f'bogus handle value: {attribute_handle}')
852                    return []
853
854                characteristics_values.append(attribute_value)
855
856            # Move on to the next characteristics
857            starting_handle = response.attributes[-1][0] + 1
858
859        return characteristics_values
860
861    async def write_value(self, attribute, value, with_response=False):
862        '''
863        See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic
864        Value
865
866        `attribute` can be an Attribute object, or a handle value
867        '''
868
869        # Send a request or command to write
870        attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
871        if with_response:
872            response = await self.send_request(
873                ATT_Write_Request(
874                    attribute_handle=attribute_handle, attribute_value=value
875                )
876            )
877            if response.op_code == ATT_ERROR_RESPONSE:
878                raise ProtocolError(
879                    response.error_code,
880                    'att',
881                    ATT_PDU.error_name(response.error_code),
882                    response,
883                )
884        else:
885            await self.send_command(
886                ATT_Write_Command(
887                    attribute_handle=attribute_handle, attribute_value=value
888                )
889            )
890
891    def on_gatt_pdu(self, att_pdu):
892        logger.debug(
893            f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
894        )
895        if att_pdu.op_code in ATT_RESPONSES:
896            if self.pending_request is None:
897                # Not expected!
898                logger.warning('!!! unexpected response, there is no pending request')
899                return
900
901            # Sanity check: the response should match the pending request unless it is
902            # an error response
903            if att_pdu.op_code != ATT_ERROR_RESPONSE:
904                expected_response_name = self.pending_request.name.replace(
905                    '_REQUEST', '_RESPONSE'
906                )
907                if att_pdu.name != expected_response_name:
908                    logger.warning(
909                        f'!!! mismatched response: expected {expected_response_name}'
910                    )
911                    return
912
913            # Return the response to the coroutine that is waiting for it
914            self.pending_response.set_result(att_pdu)
915        else:
916            handler_name = f'on_{att_pdu.name.lower()}'
917            handler = getattr(self, handler_name, None)
918            if handler is not None:
919                handler(att_pdu)
920            else:
921                logger.warning(
922                    color(
923                        '--- Ignoring GATT Response from '
924                        f'[0x{self.connection.handle:04X}]: ',
925                        'red',
926                    )
927                    + str(att_pdu)
928                )
929
930    def on_att_handle_value_notification(self, notification):
931        # Call all subscribers
932        subscribers = self.notification_subscribers.get(
933            notification.attribute_handle, []
934        )
935        if not subscribers:
936            logger.warning('!!! received notification with no subscriber')
937        for subscriber in subscribers:
938            if callable(subscriber):
939                subscriber(notification.attribute_value)
940            else:
941                subscriber.emit('update', notification.attribute_value)
942
943    def on_att_handle_value_indication(self, indication):
944        # Call all subscribers
945        subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
946        if not subscribers:
947            logger.warning('!!! received indication with no subscriber')
948        for subscriber in subscribers:
949            if callable(subscriber):
950                subscriber(indication.attribute_value)
951            else:
952                subscriber.emit('update', indication.attribute_value)
953
954        # Confirm that we received the indication
955        self.send_confirmation(ATT_Handle_Value_Confirmation())
956