• 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# -----------------------------------------------------------------------------
26import asyncio
27import logging
28import struct
29from colors import color
30
31from .core import ProtocolError, TimeoutError
32from .hci import *
33from .att import *
34from .gatt import (
35    GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
36    GATT_REQUEST_TIMEOUT,
37    GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
38    GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
39    GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
40    Characteristic
41)
42
43# -----------------------------------------------------------------------------
44# Logging
45# -----------------------------------------------------------------------------
46logger = logging.getLogger(__name__)
47
48
49# -----------------------------------------------------------------------------
50# Proxies
51# -----------------------------------------------------------------------------
52class AttributeProxy(EventEmitter):
53    def __init__(self, client, handle, end_group_handle, attribute_type):
54        EventEmitter.__init__(self)
55        self.client           = client
56        self.handle           = handle
57        self.end_group_handle = end_group_handle
58        self.type             = attribute_type
59
60    async def read_value(self, no_long_read=False):
61        return await self.client.read_value(self.handle, no_long_read)
62
63    async def write_value(self, value, with_response=False):
64        return await self.client.write_value(self.handle, value, with_response)
65
66    def __str__(self):
67        return f'Attribute(handle=0x{self.handle:04X}, type={self.uuid})'
68
69
70class ServiceProxy(AttributeProxy):
71    @staticmethod
72    def from_client(cls, client, service_uuid):
73        # The service and its characteristics are considered to have already been discovered
74        services = client.get_services_by_uuid(service_uuid)
75        service = services[0] if services else None
76        return cls(service) if service else None
77
78    def __init__(self, client, handle, end_group_handle, uuid, primary=True):
79        attribute_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE if primary else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE
80        super().__init__(client, handle, end_group_handle, attribute_type)
81        self.uuid            = uuid
82        self.characteristics = []
83
84    async def discover_characteristics(self, uuids=[]):
85        return await self.client.discover_characteristics(uuids, self)
86
87    def get_characteristics_by_uuid(self, uuid):
88        return self.client.get_characteristics_by_uuid(uuid, self)
89
90    def __str__(self):
91        return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
92
93
94class CharacteristicProxy(AttributeProxy):
95    def __init__(self, client, handle, end_group_handle, uuid, properties):
96        super().__init__(client, handle, end_group_handle, uuid)
97        self.uuid                   = uuid
98        self.properties             = properties
99        self.descriptors            = []
100        self.descriptors_discovered = False
101
102    def get_descriptor(self, descriptor_type):
103        for descriptor in self.descriptors:
104            if descriptor.type == descriptor_type:
105                return descriptor
106
107    async def discover_descriptors(self):
108        return await self.client.discover_descriptors(self)
109
110    async def subscribe(self, subscriber=None):
111        return await self.client.subscribe(self, subscriber)
112
113    def __str__(self):
114        return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
115
116
117class DescriptorProxy(AttributeProxy):
118    def __init__(self, client, handle, descriptor_type):
119        super().__init__(client, handle, 0, descriptor_type)
120
121    def __str__(self):
122        return f'Descriptor(handle=0x{self.handle:04X}, type={self.type})'
123
124
125class ProfileServiceProxy:
126    '''
127    Base class for profile-specific service proxies
128    '''
129    @classmethod
130    def from_client(cls, client):
131        return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
132
133
134# -----------------------------------------------------------------------------
135# GATT Client
136# -----------------------------------------------------------------------------
137class Client:
138    def __init__(self, connection):
139        self.connection               = connection
140        self.mtu                      = ATT_DEFAULT_MTU
141        self.mtu_exchange_done        = False
142        self.request_semaphore        = asyncio.Semaphore(1)
143        self.pending_request          = None
144        self.pending_response         = None
145        self.notification_subscribers = {}  # Notification subscribers, by attribute handle
146        self.indication_subscribers   = {}  # Indication subscribers, by attribute handle
147        self.services                 = []
148
149    def send_gatt_pdu(self, pdu):
150        self.connection.send_l2cap_pdu(ATT_CID, pdu)
151
152    async def send_command(self, command):
153        logger.debug(f'GATT Command from client: [0x{self.connection.handle:04X}] {command}')
154        self.send_gatt_pdu(command.to_bytes())
155
156    async def send_request(self, request):
157        logger.debug(f'GATT Request from client: [0x{self.connection.handle:04X}] {request}')
158
159        # Wait until we can send (only one pending command at a time for the connection)
160        response = None
161        async with self.request_semaphore:
162            assert(self.pending_request is None)
163            assert(self.pending_response is None)
164
165            # Create a future value to hold the eventual response
166            self.pending_response = asyncio.get_running_loop().create_future()
167            self.pending_request  = request
168
169            try:
170                self.send_gatt_pdu(request.to_bytes())
171                response = await asyncio.wait_for(self.pending_response, GATT_REQUEST_TIMEOUT)
172            except asyncio.TimeoutError:
173                logger.warning(color('!!! GATT Request timeout', 'red'))
174                raise TimeoutError(f'GATT timeout for {request.name}')
175            finally:
176                self.pending_request  = None
177                self.pending_response = None
178
179        return response
180
181    def send_confirmation(self, confirmation):
182        logger.debug(f'GATT Confirmation from client: [0x{self.connection.handle:04X}] {confirmation}')
183        self.send_gatt_pdu(confirmation.to_bytes())
184
185    async def request_mtu(self, mtu):
186        # Check the range
187        if mtu < ATT_DEFAULT_MTU:
188            raise ValueError(f'MTU must be >= {ATT_DEFAULT_MTU}')
189        if mtu > 0xFFFF:
190            raise ValueError('MTU must be <= 0xFFFF')
191
192        # We can only send one request per connection
193        if self.mtu_exchange_done:
194            return
195
196        # Send the request
197        self.mtu_exchange_done = True
198        response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu = mtu))
199        if response.op_code == ATT_ERROR_RESPONSE:
200            raise ProtocolError(
201                response.error_code,
202                'att',
203                ATT_PDU.error_name(response.error_code),
204                response
205            )
206
207        self.mtu = max(ATT_DEFAULT_MTU, response.server_rx_mtu)
208        return self.mtu
209
210    def get_services_by_uuid(self, uuid):
211        return [service for service in self.services if service.uuid == uuid]
212
213    def get_characteristics_by_uuid(self, uuid, service = None):
214        services = [service] if service else self.services
215        return [c for c in [c for s in services for c in s.characteristics] if c.uuid == uuid]
216
217    def on_service_discovered(self, service):
218        ''' Add a service to the service list if it wasn't already there '''
219        already_known = False
220        for existing_service in self.services:
221            if existing_service.handle == service.handle:
222                already_known = True
223                break
224        if not already_known:
225            self.services.append(service)
226
227    async def discover_services(self, uuids = None):
228        '''
229        See Vol 3, Part G - 4.4.1 Discover All Primary Services
230        '''
231        starting_handle = 0x0001
232        services = []
233        while starting_handle < 0xFFFF:
234            response = await self.send_request(
235                ATT_Read_By_Group_Type_Request(
236                    starting_handle      = starting_handle,
237                    ending_handle        = 0xFFFF,
238                    attribute_group_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
239                )
240            )
241            if response is None:
242                # TODO raise appropriate exception
243                return []
244
245            # Check if we reached the end of the iteration
246            if response.op_code == ATT_ERROR_RESPONSE:
247                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
248                    # Unexpected end
249                    logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}')
250                    # TODO raise appropriate exception
251                    return
252                break
253
254            for attribute_handle, end_group_handle, attribute_value in response.attributes:
255                if attribute_handle < starting_handle or end_group_handle < attribute_handle:
256                    # Something's not right
257                    logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
258                    return
259
260                # Create a service proxy for this service
261                service = ServiceProxy(
262                    self,
263                    attribute_handle,
264                    end_group_handle,
265                    UUID.from_bytes(attribute_value),
266                    True
267                )
268
269                # Filter out returned services based on the given uuids list
270                if (not uuids) or (service.uuid in uuids):
271                    services.append(service)
272
273                # Add the service to the peer's service list
274                self.on_service_discovered(service)
275
276            # Stop if for some reason the list was empty
277            if not response.attributes:
278                break
279
280            # Move on to the next chunk
281            starting_handle = response.attributes[-1][1] + 1
282
283        return services
284
285    async def discover_service(self, uuid):
286        '''
287        See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
288        '''
289
290        # Force uuid to be a UUID object
291        if type(uuid) is str:
292            uuid = UUID(uuid)
293
294        starting_handle = 0x0001
295        services = []
296        while starting_handle < 0xFFFF:
297            response = await self.send_request(
298                ATT_Find_By_Type_Value_Request(
299                    starting_handle = starting_handle,
300                    ending_handle   = 0xFFFF,
301                    attribute_type  = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
302                    attribute_value = uuid.to_pdu_bytes()
303                )
304            )
305            if response is None:
306                # TODO raise appropriate exception
307                return []
308
309            # Check if we reached the end of the iteration
310            if response.op_code == ATT_ERROR_RESPONSE:
311                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
312                    # Unexpected end
313                    logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}')
314                    # TODO raise appropriate exception
315                    return
316                break
317
318            for attribute_handle, end_group_handle in response.handles_information:
319                if attribute_handle < starting_handle or end_group_handle < attribute_handle:
320                    # Something's not right
321                    logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
322                    return
323
324                # Create a service proxy for this service
325                service = ServiceProxy(self, attribute_handle, end_group_handle, uuid, True)
326
327                # Add the service to the peer's service list
328                services.append(service)
329                self.on_service_discovered(service)
330
331                # Check if we've reached the end already
332                if end_group_handle == 0xFFFF:
333                    break
334
335            # Stop if for some reason the list was empty
336            if not response.handles_information:
337                break
338
339            # Move on to the next chunk
340            starting_handle = response.handles_information[-1][1] + 1
341
342        return services
343
344    async def discover_included_services(self, service):
345        '''
346        See Vol 3, Part G - 4.5.1 Find Included Services
347        '''
348        # TODO
349        return []
350
351    async def discover_characteristics(self, uuids, service):
352        '''
353        See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2 Discover Characteristics by UUID
354        '''
355
356        # Cast the UUIDs type from string to object if needed
357        uuids = [UUID(uuid) if type(uuid) is str else uuid for uuid in uuids]
358
359        # Decide which services to discover for
360        services = [service] if service else self.services
361
362        # Perform characteristic discovery for each service
363        discovered_characteristics = []
364        for service in services:
365            starting_handle = service.handle
366            ending_handle   = service.end_group_handle
367
368            characteristics = []
369            while starting_handle <= ending_handle:
370                response = await self.send_request(
371                    ATT_Read_By_Type_Request(
372                        starting_handle = starting_handle,
373                        ending_handle   = ending_handle,
374                        attribute_type  = GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
375                    )
376                )
377                if response is None:
378                    # TODO raise appropriate exception
379                    return []
380
381                # Check if we reached the end of the iteration
382                if response.op_code == ATT_ERROR_RESPONSE:
383                    if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
384                        # Unexpected end
385                        logger.warning(f'!!! unexpected error while discovering characteristics: {HCI_Constant.error_name(response.error_code)}')
386                        # TODO raise appropriate exception
387                        return
388                    break
389
390                # Stop if for some reason the list was empty
391                if not response.attributes:
392                    break
393
394                # Process all characteristics returned in this iteration
395                for attribute_handle, attribute_value in response.attributes:
396                    if attribute_handle < starting_handle:
397                        # Something's not right
398                        logger.warning(f'bogus handle value: {attribute_handle}')
399                        return []
400
401                    properties, handle = struct.unpack_from('<BH', attribute_value)
402                    characteristic_uuid = UUID.from_bytes(attribute_value[3:])
403                    characteristic = CharacteristicProxy(self, handle, 0, characteristic_uuid, properties)
404
405                    # Set the previous characteristic's end handle
406                    if characteristics:
407                        characteristics[-1].end_group_handle = attribute_handle - 1
408
409                    characteristics.append(characteristic)
410
411                # Move on to the next characteristics
412                starting_handle = response.attributes[-1][0] + 1
413
414            # Set the end handle for the last characteristic
415            if characteristics:
416                characteristics[-1].end_group_handle = service.end_group_handle
417
418            # Set the service's characteristics
419            characteristics = [c for c in characteristics if not uuids or c.uuid in uuids]
420            service.characteristics = characteristics
421            discovered_characteristics.extend(characteristics)
422
423        return discovered_characteristics
424
425    async def discover_descriptors(self, characteristic = None, start_handle = None, end_handle = None):
426        '''
427        See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
428        '''
429        if characteristic:
430            starting_handle = characteristic.handle + 1
431            ending_handle   = characteristic.end_group_handle
432        elif start_handle and end_handle:
433            starting_handle = start_handle
434            ending_handle   = end_handle
435        else:
436            return []
437
438        descriptors = []
439        while starting_handle <= ending_handle:
440            response = await self.send_request(
441                ATT_Find_Information_Request(
442                    starting_handle = starting_handle,
443                    ending_handle   = ending_handle
444                )
445            )
446            if response is None:
447                # TODO raise appropriate exception
448                return []
449
450            # Check if we reached the end of the iteration
451            if response.op_code == ATT_ERROR_RESPONSE:
452                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
453                    # Unexpected end
454                    logger.warning(f'!!! unexpected error while discovering descriptors: {HCI_Constant.error_name(response.error_code)}')
455                    # TODO raise appropriate exception
456                    return []
457                break
458
459            # Stop if for some reason the list was empty
460            if not response.information:
461                break
462
463            # Process all descriptors returned in this iteration
464            for attribute_handle, attribute_uuid in response.information:
465                if attribute_handle < starting_handle:
466                    # Something's not right
467                    logger.warning(f'bogus handle value: {attribute_handle}')
468                    return []
469
470                descriptor = DescriptorProxy(self, attribute_handle, UUID.from_bytes(attribute_uuid))
471                descriptors.append(descriptor)
472                # TODO: read descriptor value
473
474            # Move on to the next descriptor
475            starting_handle = response.information[-1][0] + 1
476
477        # Set the characteristic's descriptors
478        if characteristic:
479            characteristic.descriptors = descriptors
480
481        return descriptors
482
483    async def discover_attributes(self):
484        '''
485        Discover all attributes, regardless of type
486        '''
487        starting_handle = 0x0001
488        ending_handle   = 0xFFFF
489        attributes = []
490        while True:
491            response = await self.send_request(
492                ATT_Find_Information_Request(
493                    starting_handle = starting_handle,
494                    ending_handle   = ending_handle
495                )
496            )
497            if response is None:
498                return []
499
500            # Check if we reached the end of the iteration
501            if response.op_code == ATT_ERROR_RESPONSE:
502                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
503                    # Unexpected end
504                    logger.warning(f'!!! unexpected error while discovering attributes: {HCI_Constant.error_name(response.error_code)}')
505                    return []
506                break
507
508            for attribute_handle, attribute_uuid in response.information:
509                if attribute_handle < starting_handle:
510                    # Something's not right
511                    logger.warning(f'bogus handle value: {attribute_handle}')
512                    return []
513
514                attribute = AttributeProxy(self, attribute_handle, 0, UUID.from_bytes(attribute_uuid))
515                attributes.append(attribute)
516
517            # Move on to the next attributes
518            starting_handle = attributes[-1].handle + 1
519
520        return attributes
521
522    async def subscribe(self, characteristic, subscriber=None):
523        # If we haven't already discovered the descriptors for this characteristic, do it now
524        if not characteristic.descriptors_discovered:
525            await self.discover_descriptors(characteristic)
526
527        # Look for the CCCD descriptor
528        cccd = characteristic.get_descriptor(GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)
529        if not cccd:
530            logger.warning('subscribing to characteristic with no CCCD descriptor')
531            return
532
533        # Set the subscription bits and select the subscriber set
534        bits = 0
535        subscriber_sets = []
536        if characteristic.properties & Characteristic.NOTIFY:
537            bits |= 0x0001
538            subscriber_sets.append(self.notification_subscribers.setdefault(characteristic.handle, set()))
539        if characteristic.properties & Characteristic.INDICATE:
540            bits |= 0x0002
541            subscriber_sets.append(self.indication_subscribers.setdefault(characteristic.handle, set()))
542
543        # Add subscribers to the sets
544        for subscriber_set in subscriber_sets:
545            if subscriber is not None:
546                subscriber_set.add(subscriber)
547            subscriber_set.add(lambda value: characteristic.emit('update', self.connection, value))
548
549        await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
550
551    async def read_value(self, attribute, no_long_read=False):
552        '''
553        See Vol 3, Part G - 4.8.1 Read Characteristic Value
554
555        `attribute` can be an Attribute object, or a handle value
556        '''
557
558        # Send a request to read
559        attribute_handle = attribute if type(attribute) is int else attribute.handle
560        response = await self.send_request(ATT_Read_Request(attribute_handle = attribute_handle))
561        if response is None:
562            raise TimeoutError('read timeout')
563        if response.op_code == ATT_ERROR_RESPONSE:
564            raise ProtocolError(
565                response.error_code,
566                'att',
567                ATT_PDU.error_name(response.error_code),
568                response
569            )
570
571        # If the value is the max size for the MTU, try to read more unless the caller
572        # specifically asked not to do that
573        attribute_value = response.attribute_value
574        if not no_long_read and len(attribute_value) == self.mtu - 1:
575            logger.debug('using READ BLOB to get the rest of the value')
576            offset = len(attribute_value)
577            while True:
578                response = await self.send_request(
579                    ATT_Read_Blob_Request(attribute_handle = attribute_handle, value_offset = offset)
580                )
581                if response is None:
582                    raise TimeoutError('read timeout')
583                if response.op_code == ATT_ERROR_RESPONSE:
584                    if response.error_code == ATT_ATTRIBUTE_NOT_LONG_ERROR or response.error_code == ATT_INVALID_OFFSET_ERROR:
585                        break
586                    raise ProtocolError(
587                        response.error_code,
588                        'att',
589                        ATT_PDU.error_name(response.error_code),
590                        response
591                    )
592
593                part = response.part_attribute_value
594                attribute_value += part
595
596                if len(part) < self.mtu - 1:
597                    break
598
599                offset += len(part)
600
601        # Return the value as bytes
602        return attribute_value
603
604    async def read_characteristics_by_uuid(self, uuid, service):
605        '''
606        See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
607        '''
608
609        if service is None:
610            starting_handle = 0x0001
611            ending_handle   = 0xFFFF
612        else:
613            starting_handle = service.handle
614            ending_handle   = service.end_group_handle
615
616        characteristics_values = []
617        while starting_handle <= ending_handle:
618            response = await self.send_request(
619                ATT_Read_By_Type_Request(
620                    starting_handle = starting_handle,
621                    ending_handle   = ending_handle,
622                    attribute_type  = uuid
623                )
624            )
625            if response is None:
626                # TODO raise appropriate exception
627                return []
628
629            # Check if we reached the end of the iteration
630            if response.op_code == ATT_ERROR_RESPONSE:
631                if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
632                    # Unexpected end
633                    logger.warning(f'!!! unexpected error while reading characteristics: {HCI_Constant.error_name(response.error_code)}')
634                    # TODO raise appropriate exception
635                    return []
636                break
637
638            # Stop if for some reason the list was empty
639            if not response.attributes:
640                break
641
642            # Process all characteristics returned in this iteration
643            for attribute_handle, attribute_value in response.attributes:
644                if attribute_handle < starting_handle:
645                    # Something's not right
646                    logger.warning(f'bogus handle value: {attribute_handle}')
647                    return []
648
649                characteristics_values.append(attribute_value)
650
651            # Move on to the next characteristics
652            starting_handle = response.attributes[-1][0] + 1
653
654        return characteristics_values
655
656    async def write_value(self, attribute, value, with_response=False):
657        '''
658        See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic Value
659
660        `attribute` can be an Attribute object, or a handle value
661        '''
662
663        # Send a request or command to write
664        attribute_handle = attribute if type(attribute) is int else attribute.handle
665        if with_response:
666            response = await self.send_request(
667                ATT_Write_Request(
668                    attribute_handle = attribute_handle,
669                    attribute_value  = value
670                )
671            )
672            if response.op_code == ATT_ERROR_RESPONSE:
673                raise ProtocolError(
674                    response.error_code,
675                    'att',
676                    ATT_PDU.error_name(response.error_code), response
677                )
678        else:
679            await self.send_command(
680                ATT_Write_Command(
681                    attribute_handle = attribute_handle,
682                    attribute_value  = value
683                )
684            )
685
686    def on_gatt_pdu(self, att_pdu):
687        logger.debug(f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}')
688        if att_pdu.op_code in ATT_RESPONSES:
689            if self.pending_request is None:
690                # Not expected!
691                logger.warning('!!! unexpected response, there is no pending request')
692                return
693
694            # Sanity check: the response should match the pending request unless it is an error response
695            if att_pdu.op_code != ATT_ERROR_RESPONSE:
696                expected_response_name = self.pending_request.name.replace('_REQUEST', '_RESPONSE')
697                if att_pdu.name != expected_response_name:
698                    logger.warning(f'!!! mismatched response: expected {expected_response_name}')
699                    return
700
701            # Return the response to the coroutine that is waiting for it
702            self.pending_response.set_result(att_pdu)
703        else:
704            handler_name = f'on_{att_pdu.name.lower()}'
705            handler = getattr(self, handler_name, None)
706            if handler is not None:
707                handler(att_pdu)
708            else:
709                logger.warning(f'{color(f"--- Ignoring GATT Response from [0x{self.connection.handle:04X}]:", "red")} {att_pdu}')
710
711    def on_att_handle_value_notification(self, notification):
712        # Call all subscribers
713        subscribers = self.notification_subscribers.get(notification.attribute_handle, [])
714        if not subscribers:
715            logger.warning('!!! received notification with no subscriber')
716        for subscriber in subscribers:
717            subscriber(notification.attribute_value)
718
719    def on_att_handle_value_indication(self, indication):
720        # Call all subscribers
721        subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
722        if not subscribers:
723            logger.warning('!!! received indication with no subscriber')
724        for subscriber in subscribers:
725            subscriber(indication.attribute_value)
726
727        # Confirm that we received the indication
728        self.send_confirmation(ATT_Handle_Value_Confirmation())
729